Sheikh Mohammad Rakib commited on
Commit
3c29af3
Β·
1 Parent(s): be4375c

feat: implement Gradio-based Memory Keeper interface with multi-modal AI integration

Browse files
Files changed (2) hide show
  1. app.py +295 -0
  2. requirements.txt +55 -0
app.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import spaces
3
+ import requests
4
+ import uuid
5
+ import base64
6
+ import json
7
+ from pathlib import Path
8
+
9
+ # ── CONFIG ────────────────────────────────────────────────────────────────────
10
+ BUILD_PERSONA_URL = "https://sheikhmdrakib-career--build-persona.modal.run"
11
+ CHAT_URL = "https://sheikhmdrakib-career--chat.modal.run"
12
+ TRANSCRIBE_URL = "https://sheikhmdrakib-career--transcribe.modal.run"
13
+ VISION_URL = "https://sheikhmdrakib-career--describe-photo.modal.run"
14
+ OCR_URL = "https://sheikhmdrakib-career--ocr-document.modal.run"
15
+ TTS_URL = "https://sheikhmdrakib-career--text-to-speech.modal.run"
16
+ LIST_PERSONAS_URL = "https://sheikhmdrakib-career--list-personas.modal.run"
17
+ # ─────────────────────────────────────────────────────────────────────────────
18
+
19
+
20
+ def encode_file(path):
21
+ with open(path, "rb") as f:
22
+ return base64.b64encode(f.read()).decode()
23
+
24
+
25
+ @spaces.GPU
26
+ def build_persona(name, relationship, text_input, photo_captions, voice_file, photo_files, scanned_files):
27
+ if not name.strip():
28
+ return "❌ Please enter the person's name.", None, gr.update()
29
+
30
+ texts = [t.strip() for t in text_input.strip().split("---") if t.strip()] if text_input.strip() else []
31
+ captions = [c.strip() for c in photo_captions.strip().split("\n") if c.strip()] if photo_captions.strip() else []
32
+ voice_transcripts = []
33
+
34
+ if not texts and not captions and voice_file is None and not photo_files and not scanned_files:
35
+ return "❌ Please provide at least one input.", None, gr.update()
36
+
37
+ # 1. Transcribe voice note (Cohere ASR)
38
+ if voice_file is not None:
39
+ try:
40
+ r = requests.post(TRANSCRIBE_URL, json={
41
+ "audio_b64": encode_file(voice_file),
42
+ "filename": Path(voice_file).name,
43
+ }, timeout=180)
44
+ transcript = r.json().get("transcript", "")
45
+ if transcript:
46
+ voice_transcripts.append(transcript)
47
+ except Exception as e:
48
+ return f"❌ Voice transcription failed: {e}", None, gr.update()
49
+
50
+ # 2. Describe uploaded photos (MiniCPM-V)
51
+ if photo_files:
52
+ for photo in photo_files:
53
+ try:
54
+ r = requests.post(VISION_URL, json={"image_b64": encode_file(photo)}, timeout=180)
55
+ desc = r.json().get("description", "")
56
+ if desc:
57
+ captions.append(desc)
58
+ except Exception as e:
59
+ captions.append(f"[Photo description failed: {e}]")
60
+
61
+ # 3. OCR scanned letters (Nemotron Parse)
62
+ if scanned_files:
63
+ for scan in scanned_files:
64
+ try:
65
+ r = requests.post(OCR_URL, json={"image_b64": encode_file(scan)}, timeout=180)
66
+ ocr_text = r.json().get("text", "")
67
+ if ocr_text:
68
+ texts.append(ocr_text)
69
+ except Exception as e:
70
+ texts.append(f"[OCR failed: {e}]")
71
+
72
+ # 4. Build persona (Qwen 32B)
73
+ persona_id = str(uuid.uuid4())[:8]
74
+ try:
75
+ r = requests.post(BUILD_PERSONA_URL, json={
76
+ "persona_id": persona_id, "name": name.strip(),
77
+ "relationship": relationship.strip(),
78
+ "texts": texts, "photo_captions": captions,
79
+ "voice_transcripts": voice_transcripts,
80
+ }, timeout=1200)
81
+ result = r.json()
82
+ if result.get("success"):
83
+ persona = result["persona"]
84
+ summary = f"""βœ… **{name}'s memory has been preserved.**
85
+
86
+ **Persona ID:** `{persona_id}`
87
+ **Personality:** {', '.join(persona.get('personality_traits', [])[:3])}
88
+ **Language:** {persona.get('language', 'Auto')}
89
+ **Memories captured:** {len(persona.get('key_memories', []))}
90
+ **Voice style:** {persona.get('voice_description', 'N/A')}
91
+
92
+ Go to the **πŸ’¬ Talk** tab and enter the Persona ID."""
93
+ return summary, persona_id, gr.update(value=persona_id)
94
+ else:
95
+ return f"❌ Error: {result}", None, gr.update()
96
+ except Exception as e:
97
+ return f"❌ Failed: {e}", None, gr.update()
98
+
99
+
100
+ @spaces.GPU
101
+ def chat_with_persona(persona_id, message, history, language, enable_voice):
102
+ history = history or []
103
+
104
+ if not persona_id.strip():
105
+ history = history + [{"role": "assistant", "content": "⚠️ Please enter a Persona ID first."}]
106
+ return "", history, None
107
+
108
+ if not message.strip():
109
+ return "", history, None
110
+
111
+ try:
112
+ r = requests.post(CHAT_URL, json={
113
+ "persona_id": persona_id.strip(),
114
+ "history": [{"role": m["role"], "content": m["content"]} for m in history],
115
+ "message": message.strip(),
116
+ "language": language,
117
+ }, timeout=180)
118
+ result = r.json()
119
+ response_text = result.get("text", result.get("response", "..."))
120
+ voice_desc = result.get("voice_description", "warm elderly voice")
121
+ except Exception as e:
122
+ response_text = f"⚠️ Error: {e}"
123
+ voice_desc = "warm elderly voice"
124
+
125
+ history = history + [
126
+ {"role": "user", "content": message},
127
+ {"role": "assistant", "content": response_text},
128
+ ]
129
+
130
+ # Generate voice response (VoxCPM2)
131
+ audio_path = None
132
+ if enable_voice:
133
+ try:
134
+ r = requests.post(TTS_URL, json={
135
+ "text": response_text,
136
+ "voice_description": voice_desc,
137
+ }, timeout=180)
138
+ if r.status_code == 200:
139
+ import tempfile
140
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
141
+ f.write(r.content)
142
+ audio_path = f.name
143
+ except Exception:
144
+ pass
145
+
146
+ return "", history, audio_path
147
+
148
+
149
+ def load_personas():
150
+ for attempt in range(2):
151
+ try:
152
+ r = requests.get(LIST_PERSONAS_URL, timeout=90)
153
+ personas = r.json().get("personas", [])
154
+ if not personas:
155
+ return "No personas saved yet."
156
+ lines = [f"**{p['name']}** ({p['relationship']}) β€” ID: `{p['id']}`" for p in personas]
157
+ return "\n\n".join(lines)
158
+ except Exception as e:
159
+ if attempt == 0:
160
+ continue
161
+ return f"⚠️ Modal is waking up, please try again in 30 seconds."
162
+
163
+
164
+ # ── UI ────────────────────────────────────────────────────────────────────────
165
+
166
+ css = """
167
+ @import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=Source+Sans+3:wght@300;400;600&display=swap');
168
+ * { box-sizing: border-box; }
169
+ body, .gradio-container { background: #0e0b08 !important; font-family: 'Source Sans 3', sans-serif !important; color: #e8dcc8 !important; }
170
+ .gradio-container { max-width: 900px !important; margin: 0 auto !important; }
171
+ h1, h2, h3 { font-family: 'Lora', serif !important; color: #d4a96a !important; }
172
+ .header-title { text-align: center; font-family: 'Lora', serif; font-size: 2.4em; color: #d4a96a; margin: 24px 0 4px 0; }
173
+ .header-sub { text-align: center; color: #8a7560; font-size: 1em; margin-bottom: 28px; font-style: italic; }
174
+ .divider { border: none; border-top: 1px solid #2a2015; margin: 20px 0; }
175
+ label { color: #8a7560 !important; font-size: 0.85em !important; letter-spacing: 0.08em !important; text-transform: uppercase !important; }
176
+ textarea, input[type="text"] { background: #1a1510 !important; border: 1px solid #3a2e1e !important; color: #e8dcc8 !important; border-radius: 6px !important; }
177
+ .model-badge { display: inline-block; background: #1f1710; border: 1px solid #3a2e1e; border-radius: 4px; padding: 2px 8px; font-size: 0.75em; color: #8a7560; margin: 2px; }
178
+ """
179
+
180
+ with gr.Blocks(title="Memory Keeper") as demo:
181
+
182
+ gr.HTML("""
183
+ <div class="header-title">πŸ•―οΈ Memory Keeper</div>
184
+ <div class="header-sub">Preserve the voice of someone you love. Talk to them again.</div>
185
+ <hr class="divider">
186
+ <div style="text-align:center; margin-bottom:16px;">
187
+ <span class="model-badge">🧠 MiniCPM4.1-8B (8B)</span>
188
+ <span class="model-badge">πŸ‘οΈ MiniCPM-V 4.6 (8B)</span>
189
+ <span class="model-badge">🎀 Cohere Transcribe (2B)</span>
190
+ <span class="model-badge">πŸ“„ Nemotron Parse v1.2 (&lt;1B)</span>
191
+ <span class="model-badge">🌍 Tiny Aya Fire (3.35B)</span>
192
+ <span class="model-badge">🌊 Tiny Aya Water (3.35B)</span>
193
+ <span class="model-badge">πŸ”Š VoxCPM2 (~1B)</span>
194
+ <span class="model-badge">Total: ~26.7B params</span>
195
+ </div>
196
+ """)
197
+
198
+ with gr.Tabs():
199
+
200
+ # ── TAB 1: PRESERVE ──
201
+ with gr.Tab("πŸ“œ Preserve a Memory"):
202
+ gr.HTML("<p style='color:#8a7560; font-style:italic; margin-bottom:16px;'>Upload letters, photos, voice notes, or scanned documents. Each is processed by a specialized AI model.</p>")
203
+
204
+ with gr.Row():
205
+ name_input = gr.Textbox(label="Their Name", value="Dadu")
206
+ relationship_input = gr.Textbox(label="Your Relationship", value="Grandfather")
207
+
208
+ text_input = gr.Textbox(
209
+ label="πŸ“ Letters / Diary Entries / Writings",
210
+ value="Dear family,\n\nWork hard in your studies. Knowledge is the one thing no one can take from you. When I was young, I walked four miles to school every day β€” no shoes, no complaints. We were poor but we were honest.\n\nAlways be kind to your mother. She sacrifices more than you will ever know.\n\nSave your money. A good name lasts longer than any wealth.\n\nI am proud of all of you. Your Dadu misses your faces.\n---\nBeta, I heard you got good marks. This makes my heart so happy. Keep going.",
211
+ lines=6,
212
+ )
213
+
214
+ with gr.Row():
215
+ photo_files = gr.File(
216
+ label="πŸ–ΌοΈ Photos (MiniCPM-V 4.6 will describe them)",
217
+ file_count="multiple", file_types=["image"],
218
+ )
219
+ scanned_files = gr.File(
220
+ label="πŸ“„ Scanned Letters/Docs (Nemotron Parse OCR)",
221
+ file_count="multiple", file_types=["image"],
222
+ )
223
+
224
+ photo_captions = gr.Textbox(
225
+ label="πŸ–ΌοΈ Manual Photo Captions (optional, one per line)",
226
+ value="Old black and white photo, Dadu sitting on a wooden chair, wearing white panjabi, smiling warmly\nEid gathering, Dadu surrounded by grandchildren, laughing out loud\nDadu in the garden early morning, watering plants carefully",
227
+ lines=3,
228
+ )
229
+
230
+ voice_input = gr.Audio(
231
+ label="🎀 Voice Note (Cohere Transcribe ASR)",
232
+ type="filepath", sources=["upload", "microphone"],
233
+ )
234
+
235
+ build_btn = gr.Button("✨ Preserve Their Memory", variant="primary")
236
+ build_output = gr.Markdown()
237
+ persona_id_state = gr.State()
238
+ persona_id_hidden = gr.Textbox(visible=False)
239
+
240
+ build_btn.click(
241
+ fn=build_persona,
242
+ inputs=[name_input, relationship_input, text_input, photo_captions,
243
+ voice_input, photo_files, scanned_files],
244
+ outputs=[build_output, persona_id_state, persona_id_hidden],
245
+ show_progress="full",
246
+ )
247
+
248
+ # ── TAB 2: TALK ──
249
+ with gr.Tab("πŸ’¬ Talk to Them"):
250
+ gr.HTML("<p style='color:#8a7560; font-style:italic; margin-bottom:16px;'>Enter the Persona ID and start a conversation. Enable voice to hear them speak.</p>")
251
+
252
+ with gr.Row():
253
+ persona_id_input = gr.Textbox(label="Persona ID", placeholder="e.g. a3f9c2b1")
254
+ language_select = gr.Dropdown(
255
+ label="Language", choices=["auto", "English", "Bengali", "Hindi", "Chinese", "Japanese", "Korean", "Thai"], value="auto",
256
+ )
257
+ enable_voice = gr.Checkbox(label="πŸ”Š Voice Response (VoxCPM2)", value=False)
258
+
259
+ chatbot = gr.Chatbot(label="", height=420, placeholder="*Their words will appear here...*")
260
+
261
+ with gr.Row():
262
+ msg_input = gr.Textbox(label="Your message", value="What is the most important lesson in life?", lines=2, scale=4)
263
+ send_btn = gr.Button("Send β†’", variant="primary", scale=1)
264
+
265
+ voice_output = gr.Audio(label="πŸ”Š Voice Response", visible=True, autoplay=True)
266
+ clear_btn = gr.Button("Clear conversation", variant="secondary", size="sm")
267
+ chat_history = gr.State([])
268
+
269
+ send_btn.click(
270
+ fn=chat_with_persona,
271
+ inputs=[persona_id_input, msg_input, chat_history, language_select, enable_voice],
272
+ outputs=[msg_input, chatbot, voice_output],
273
+ )
274
+ msg_input.submit(
275
+ fn=chat_with_persona,
276
+ inputs=[persona_id_input, msg_input, chat_history, language_select, enable_voice],
277
+ outputs=[msg_input, chatbot, voice_output],
278
+ )
279
+ clear_btn.click(lambda: ([], []), outputs=[chat_history, chatbot])
280
+
281
+ # ── TAB 3: SAVED ──
282
+ with gr.Tab("πŸ“ Saved Memories"):
283
+ refresh_btn = gr.Button("πŸ”„ Load Saved Memories", variant="secondary")
284
+ personas_output = gr.Markdown()
285
+ refresh_btn.click(fn=load_personas, outputs=personas_output)
286
+
287
+ gr.HTML("""
288
+ <hr class="divider">
289
+ <p style='text-align:center; color:#3a2e1e; font-size:0.8em; font-style:italic;'>
290
+ Built for Build Small Hackathon Β· 6 AI Models Β· Hosted on Modal + Hugging Face
291
+ </p>
292
+ """)
293
+
294
+ if __name__ == "__main__":
295
+ demo.launch(css=css)
requirements.txt ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-doc==0.0.4
2
+ annotated-types==0.7.0
3
+ anyio==4.13.0
4
+ audioop-lts==0.2.2
5
+ brotli==1.2.0
6
+ certifi==2026.5.20
7
+ charset-normalizer==3.4.7
8
+ click==8.4.1
9
+ colorama==0.4.6
10
+ fastapi==0.136.3
11
+ filelock==3.29.1
12
+ fsspec==2026.4.0
13
+ gradio==6.16.0
14
+ gradio_client==2.5.0
15
+ groovy==0.1.2
16
+ h11==0.16.0
17
+ hf-gradio==0.4.1
18
+ hf-xet==1.5.0
19
+ httpcore==1.0.9
20
+ httpx==0.28.1
21
+ huggingface_hub==1.18.0
22
+ idna==3.18
23
+ Jinja2==3.1.6
24
+ markdown-it-py==4.2.0
25
+ MarkupSafe==3.0.3
26
+ mdurl==0.1.2
27
+ numpy==2.4.6
28
+ orjson==3.11.9
29
+ packaging==26.2
30
+ pandas==3.0.3
31
+ pillow==12.2.0
32
+ pydantic==2.13.4
33
+ pydantic_core==2.46.4
34
+ pydub==0.25.1
35
+ Pygments==2.20.0
36
+ python-dateutil==2.9.0.post0
37
+ python-multipart==0.0.32
38
+ pytz==2026.2
39
+ PyYAML==6.0.3
40
+ requests==2.34.2
41
+ rich==15.0.0
42
+ safehttpx==0.1.7
43
+ semantic-version==2.10.0
44
+ shellingham==1.5.4
45
+ six==1.17.0
46
+ starlette==1.2.1
47
+ tomlkit==0.14.0
48
+ tqdm==4.68.1
49
+ typer==0.25.1
50
+ typing-inspection==0.4.2
51
+ typing_extensions==4.15.0
52
+ tzdata==2026.2
53
+ urllib3==2.7.0
54
+ uvicorn==0.49.0
55
+ spaces