lilblueyes commited on
Commit
ca77800
·
1 Parent(s): 94d6275

Add llama.cpp intent generation and custom UI

Browse files
Files changed (2) hide show
  1. app.py +286 -51
  2. requirements.txt +2 -0
app.py CHANGED
@@ -1,38 +1,222 @@
1
  import os
 
2
  import time
3
  import tempfile
4
 
5
  import gradio as gr
6
  import soundfile as sf
7
  import torch
 
8
  from qwen_tts import Qwen3TTSModel
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
 
 
 
 
 
 
10
 
11
- MODEL_ID = os.getenv("MODEL_ID", "Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice")
 
 
 
12
 
13
- model = None
 
 
 
 
 
14
 
 
 
 
 
 
 
 
 
 
15
 
16
- def get_model():
17
- global model
 
18
 
19
- if model is not None:
20
- return model
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
 
22
  if torch.cuda.is_available():
23
- model = Qwen3TTSModel.from_pretrained(
24
- MODEL_ID,
25
  device_map="cuda:0",
26
  dtype=torch.bfloat16,
27
  )
28
  else:
29
- model = Qwen3TTSModel.from_pretrained(
30
- MODEL_ID,
31
  device_map="cpu",
32
  dtype=torch.float32,
33
  )
34
 
35
- return model
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
 
38
  def generate_tts(text, language, speaker, instruction):
@@ -40,9 +224,9 @@ def generate_tts(text, language, speaker, instruction):
40
  instruction = (instruction or "").strip()
41
 
42
  if not text:
43
- raise gr.Error("Écris une phrase à synthétiser.")
44
 
45
- tts = get_model()
46
 
47
  wavs, sr = tts.generate_custom_voice(
48
  text=text,
@@ -58,45 +242,80 @@ def generate_tts(text, language, speaker, instruction):
58
 
59
  sf.write(output_path, wavs[0], sr)
60
 
61
- intent_json = {
62
- "detected_glosses": [],
63
- "detected_facial_expression": "not_connected_yet",
64
- "subtitle": text,
65
- "voice_instruction": instruction,
66
- "language": language,
67
- "speaker": speaker,
68
- "pipeline_stage": "tts_only_mvp",
69
- }
70
 
71
- return output_path, text, intent_json
72
 
 
 
 
 
 
 
 
 
 
 
73
 
74
- with gr.Blocks(title="ASL to TTS MVP") as demo:
75
- gr.Markdown(
76
- """
77
- # ASL to TTS MVP
78
-
79
- Première version: on teste seulement la brique TTS.
80
 
81
- Ensuite, on branchera:
82
- video ASL -> glosses -> emotion -> intent JSON -> subtitle -> voice instruction -> TTS.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  """
84
  )
85
 
86
  with gr.Row():
87
- with gr.Column():
88
- text_input = gr.Textbox(
89
- label="Subtitle temporaire",
90
- value="Hello, I am happy to see you today.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  lines=3,
92
  )
93
 
94
- instruction_input = gr.Textbox(
95
  label="Voice instruction",
96
- value="Speak with a warm, happy, expressive voice.",
97
- lines=2,
98
  )
99
 
 
 
 
 
 
 
 
 
100
  language_input = gr.Dropdown(
101
  label="Language",
102
  choices=[
@@ -131,26 +350,42 @@ with gr.Blocks(title="ASL to TTS MVP") as demo:
131
  value="Ryan",
132
  )
133
 
134
- button = gr.Button("Generate speech")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
- with gr.Column():
137
- audio_output = gr.Audio(label="Generated audio", type="filepath")
138
- subtitle_output = gr.Textbox(label="Subtitle")
139
- json_output = gr.JSON(label="Intent JSON")
 
140
 
141
- button.click(
142
  fn=generate_tts,
143
  inputs=[
144
- text_input,
145
  language_input,
146
  speaker_input,
147
- instruction_input,
148
- ],
149
- outputs=[
150
- audio_output,
151
- subtitle_output,
152
- json_output,
153
  ],
 
154
  )
155
 
156
 
 
1
  import os
2
+ import json
3
  import time
4
  import tempfile
5
 
6
  import gradio as gr
7
  import soundfile as sf
8
  import torch
9
+
10
  from qwen_tts import Qwen3TTSModel
11
+ from llama_cpp import Llama
12
+
13
+
14
+ TTS_MODEL_ID = os.getenv("TTS_MODEL_ID", "Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice")
15
+
16
+ LLM_REPO_ID = os.getenv("LLM_REPO_ID", "Qwen/Qwen2.5-1.5B-Instruct-GGUF")
17
+ LLM_FILENAME = os.getenv("LLM_FILENAME", "qwen2.5-1.5b-instruct-q4_k_m.gguf")
18
+
19
+ tts_model = None
20
+ llm_model = None
21
+
22
+
23
+ CUSTOM_CSS = """
24
+ :root {
25
+ --bg: #050816;
26
+ --panel: rgba(255, 255, 255, 0.075);
27
+ --panel-border: rgba(255, 255, 255, 0.16);
28
+ --text: #f8fafc;
29
+ --muted: #94a3b8;
30
+ --accent: #8b5cf6;
31
+ --accent-2: #06b6d4;
32
+ }
33
+
34
+ .gradio-container {
35
+ background:
36
+ radial-gradient(circle at 20% 20%, rgba(139, 92, 246, 0.30), transparent 28%),
37
+ radial-gradient(circle at 80% 0%, rgba(6, 182, 212, 0.24), transparent 28%),
38
+ linear-gradient(135deg, #050816 0%, #0f172a 55%, #111827 100%) !important;
39
+ color: var(--text) !important;
40
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif !important;
41
+ }
42
+
43
+ #hero {
44
+ padding: 28px;
45
+ border: 1px solid var(--panel-border);
46
+ border-radius: 28px;
47
+ background: linear-gradient(135deg, rgba(255,255,255,0.10), rgba(255,255,255,0.04));
48
+ box-shadow: 0 24px 80px rgba(0,0,0,0.35);
49
+ backdrop-filter: blur(18px);
50
+ }
51
 
52
+ #hero h1 {
53
+ font-size: 42px;
54
+ line-height: 1.05;
55
+ margin-bottom: 8px;
56
+ letter-spacing: -0.04em;
57
+ }
58
 
59
+ #hero p {
60
+ color: var(--muted);
61
+ font-size: 16px;
62
+ }
63
 
64
+ .badge-row {
65
+ display: flex;
66
+ flex-wrap: wrap;
67
+ gap: 10px;
68
+ margin-top: 16px;
69
+ }
70
 
71
+ .badge {
72
+ padding: 8px 12px;
73
+ border-radius: 999px;
74
+ background: rgba(139, 92, 246, 0.16);
75
+ border: 1px solid rgba(139, 92, 246, 0.34);
76
+ color: #ddd6fe;
77
+ font-weight: 700;
78
+ font-size: 13px;
79
+ }
80
 
81
+ .block, .form, .panel {
82
+ border-radius: 22px !important;
83
+ }
84
 
85
+ textarea, input, select {
86
+ background: rgba(15, 23, 42, 0.72) !important;
87
+ color: var(--text) !important;
88
+ border-color: rgba(255,255,255,0.14) !important;
89
+ }
90
+
91
+ button.primary, button {
92
+ border-radius: 999px !important;
93
+ font-weight: 800 !important;
94
+ }
95
+
96
+ #run_llm {
97
+ background: linear-gradient(135deg, var(--accent), var(--accent-2)) !important;
98
+ color: white !important;
99
+ border: none !important;
100
+ }
101
+
102
+ #run_tts {
103
+ background: linear-gradient(135deg, #f97316, #ec4899) !important;
104
+ color: white !important;
105
+ border: none !important;
106
+ }
107
+
108
+ .footer-note {
109
+ color: var(--muted);
110
+ font-size: 13px;
111
+ text-align: center;
112
+ }
113
+ """
114
+
115
+
116
+ def get_tts_model():
117
+ global tts_model
118
+
119
+ if tts_model is not None:
120
+ return tts_model
121
 
122
  if torch.cuda.is_available():
123
+ tts_model = Qwen3TTSModel.from_pretrained(
124
+ TTS_MODEL_ID,
125
  device_map="cuda:0",
126
  dtype=torch.bfloat16,
127
  )
128
  else:
129
+ tts_model = Qwen3TTSModel.from_pretrained(
130
+ TTS_MODEL_ID,
131
  device_map="cpu",
132
  dtype=torch.float32,
133
  )
134
 
135
+ return tts_model
136
+
137
+
138
+ def get_llm_model():
139
+ global llm_model
140
+
141
+ if llm_model is not None:
142
+ return llm_model
143
+
144
+ # llama-cpp-python downloads the GGUF from Hugging Face.
145
+ # Q4_K_M is a good first compromise for CPU Spaces.
146
+ llm_model = Llama.from_pretrained(
147
+ repo_id=LLM_REPO_ID,
148
+ filename=LLM_FILENAME,
149
+ n_ctx=2048,
150
+ n_threads=max(2, os.cpu_count() or 2),
151
+ n_gpu_layers=-1 if torch.cuda.is_available() else 0,
152
+ verbose=True,
153
+ )
154
+
155
+ return llm_model
156
+
157
+
158
+ def safe_json_loads(text):
159
+ try:
160
+ return json.loads(text)
161
+ except Exception:
162
+ return {
163
+ "raw_input": text,
164
+ "warning": "Input was not valid JSON, treated as raw text.",
165
+ }
166
+
167
+
168
+ def generate_subtitle_and_instruction(intent_json_text):
169
+ intent = safe_json_loads(intent_json_text)
170
+
171
+ system_prompt = (
172
+ "You are an assistant inside an ASL-to-speech accessibility app. "
173
+ "Your job is to convert detected ASL glosses, pose/facial emotion, "
174
+ "and intent metadata into a short natural subtitle and a precise "
175
+ "voice instruction for a TTS model. "
176
+ "Return only valid JSON with exactly two keys: subtitle and voice_instruction."
177
+ )
178
+
179
+ user_prompt = f"""
180
+ Input intent data:
181
+ {json.dumps(intent, ensure_ascii=False, indent=2)}
182
+
183
+ Rules:
184
+ - Do not invent details that are not supported by the input.
185
+ - Keep the subtitle short and natural.
186
+ - The voice_instruction should describe tone, emotion, pace, and intensity.
187
+ - Return only JSON.
188
+ """
189
+
190
+ llm = get_llm_model()
191
+
192
+ result = llm.create_chat_completion(
193
+ messages=[
194
+ {"role": "system", "content": system_prompt},
195
+ {"role": "user", "content": user_prompt},
196
+ ],
197
+ temperature=0.2,
198
+ max_tokens=256,
199
+ )
200
+
201
+ content = result["choices"][0]["message"]["content"].strip()
202
+
203
+ try:
204
+ parsed = json.loads(content)
205
+ except Exception:
206
+ parsed = {
207
+ "subtitle": content,
208
+ "voice_instruction": "Speak clearly and naturally.",
209
+ }
210
+
211
+ subtitle = parsed.get("subtitle", "").strip()
212
+ voice_instruction = parsed.get("voice_instruction", "").strip()
213
+
214
+ if not subtitle:
215
+ subtitle = "I want to say something."
216
+ if not voice_instruction:
217
+ voice_instruction = "Speak clearly and naturally."
218
+
219
+ return subtitle, voice_instruction, parsed
220
 
221
 
222
  def generate_tts(text, language, speaker, instruction):
 
224
  instruction = (instruction or "").strip()
225
 
226
  if not text:
227
+ raise gr.Error("Aucun subtitle à synthétiser.")
228
 
229
+ tts = get_tts_model()
230
 
231
  wavs, sr = tts.generate_custom_voice(
232
  text=text,
 
242
 
243
  sf.write(output_path, wavs[0], sr)
244
 
245
+ return output_path
 
 
 
 
 
 
 
 
246
 
 
247
 
248
+ DEFAULT_INTENT = {
249
+ "detected_glosses": ["I", "HAPPY", "SEE", "YOU"],
250
+ "detected_facial_expression": "happy",
251
+ "emotion_profile": {
252
+ "dominant": "joy",
253
+ "confidence": 0.83,
254
+ },
255
+ "communication_intent": "friendly_greeting",
256
+ "pipeline_stage": "mock_asl_intent_for_llama_cpp_test",
257
+ }
258
 
 
 
 
 
 
 
259
 
260
+ with gr.Blocks(
261
+ title="SignSpeak Local",
262
+ css=CUSTOM_CSS,
263
+ theme=gr.themes.Base(),
264
+ ) as demo:
265
+ gr.HTML(
266
+ """
267
+ <section id="hero">
268
+ <h1>SignSpeak Local</h1>
269
+ <p>
270
+ ASL video to expressive speech, built as a local-first accessibility pipeline.
271
+ Current milestone: llama.cpp intent generation + Qwen3-TTS voice synthesis.
272
+ </p>
273
+ <div class="badge-row">
274
+ <span class="badge">🦙 llama.cpp</span>
275
+ <span class="badge">🔌 local-first</span>
276
+ <span class="badge">🎨 custom Gradio UI</span>
277
+ <span class="badge">🗣️ expressive TTS</span>
278
+ </div>
279
+ </section>
280
  """
281
  )
282
 
283
  with gr.Row():
284
+ with gr.Column(scale=1):
285
+ gr.Markdown("## 1. Intent input")
286
+
287
+ intent_input = gr.Textbox(
288
+ label="Mock intent JSON",
289
+ value=json.dumps(DEFAULT_INTENT, ensure_ascii=False, indent=2),
290
+ lines=13,
291
+ )
292
+
293
+ run_llm_button = gr.Button(
294
+ "Generate subtitle with llama.cpp",
295
+ elem_id="run_llm",
296
+ )
297
+
298
+ with gr.Column(scale=1):
299
+ gr.Markdown("## 2. llama.cpp output")
300
+
301
+ subtitle_output = gr.Textbox(
302
+ label="Subtitle",
303
  lines=3,
304
  )
305
 
306
+ instruction_output = gr.Textbox(
307
  label="Voice instruction",
308
+ lines=3,
 
309
  )
310
 
311
+ llm_json_output = gr.JSON(
312
+ label="LLM structured output",
313
+ )
314
+
315
+ with gr.Row():
316
+ with gr.Column(scale=1):
317
+ gr.Markdown("## 3. Voice synthesis")
318
+
319
  language_input = gr.Dropdown(
320
  label="Language",
321
  choices=[
 
350
  value="Ryan",
351
  )
352
 
353
+ run_tts_button = gr.Button(
354
+ "Generate expressive speech",
355
+ elem_id="run_tts",
356
+ )
357
+
358
+ with gr.Column(scale=1):
359
+ gr.Markdown("## 4. Result")
360
+
361
+ audio_output = gr.Audio(
362
+ label="Generated audio",
363
+ type="filepath",
364
+ )
365
+
366
+ gr.HTML(
367
+ """
368
+ <p class="footer-note">
369
+ Hackathon target badges: Llama Champion, Off-Brand, Off the Grid.
370
+ </p>
371
+ """
372
+ )
373
 
374
+ run_llm_button.click(
375
+ fn=generate_subtitle_and_instruction,
376
+ inputs=[intent_input],
377
+ outputs=[subtitle_output, instruction_output, llm_json_output],
378
+ )
379
 
380
+ run_tts_button.click(
381
  fn=generate_tts,
382
  inputs=[
383
+ subtitle_output,
384
  language_input,
385
  speaker_input,
386
+ instruction_output,
 
 
 
 
 
387
  ],
388
+ outputs=[audio_output],
389
  )
390
 
391
 
requirements.txt CHANGED
@@ -2,3 +2,5 @@ gradio
2
  qwen-tts
3
  soundfile
4
  torch
 
 
 
2
  qwen-tts
3
  soundfile
4
  torch
5
+ llama-cpp-python
6
+ huggingface-hub