lilblueyes commited on
Commit
5d91112
·
1 Parent(s): bb615f1

Fix llama.cpp JSON parsing before TTS

Browse files
Files changed (1) hide show
  1. app.py +89 -24
app.py CHANGED
@@ -165,26 +165,92 @@ def safe_json_loads(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()
@@ -194,29 +260,28 @@ Rules:
194
  {"role": "system", "content": system_prompt},
195
  {"role": "user", "content": user_prompt},
196
  ],
197
- temperature=0.2,
198
  max_tokens=96,
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):
 
165
  }
166
 
167
 
168
+ def extract_json_object(text):
169
+ """
170
+ Extract the first valid JSON object from a model response.
171
+
172
+ Handles:
173
+ - pure JSON
174
+ - ```json ... ```
175
+ - text before/after JSON
176
+ """
177
+ if not text:
178
+ raise ValueError("Empty model response")
179
+
180
+ cleaned = text.strip()
181
+
182
+ if cleaned.startswith("```"):
183
+ cleaned = cleaned.replace("```json", "", 1)
184
+ cleaned = cleaned.replace("```JSON", "", 1)
185
+ cleaned = cleaned.replace("```", "")
186
+ cleaned = cleaned.strip()
187
+
188
+ try:
189
+ return json.loads(cleaned)
190
+ except Exception:
191
+ pass
192
+
193
+ start = cleaned.find("{")
194
+ end = cleaned.rfind("}")
195
+
196
+ if start == -1 or end == -1 or end <= start:
197
+ raise ValueError(f"No JSON object found in model response: {text}")
198
+
199
+ candidate = cleaned[start:end + 1]
200
+ return json.loads(candidate)
201
+
202
+
203
+ def normalize_llm_output(parsed):
204
+ subtitle = str(parsed.get("subtitle", "")).strip()
205
+ voice_instruction = str(parsed.get("voice_instruction", "")).strip()
206
+
207
+ if not subtitle:
208
+ subtitle = "I want to say something."
209
+
210
+ if not voice_instruction:
211
+ voice_instruction = "Speak clearly and naturally."
212
+
213
+ forbidden_fragments = ["```", '"subtitle"', '"voice_instruction"', "{", "}"]
214
+ if any(fragment in subtitle for fragment in forbidden_fragments):
215
+ subtitle = "I am happy to see you."
216
+
217
+ return {
218
+ "subtitle": subtitle,
219
+ "voice_instruction": voice_instruction,
220
+ }
221
+
222
+
223
  def generate_subtitle_and_instruction(intent_json_text):
224
  intent = safe_json_loads(intent_json_text)
225
 
226
  system_prompt = (
227
  "You are an assistant inside an ASL-to-speech accessibility app. "
228
+ "Convert detected ASL glosses and emotion metadata into speech output. "
229
+ "You must return raw JSON only. "
230
+ "Do not use markdown. "
231
+ "Do not wrap the response in ```json fences. "
232
+ "Return exactly this schema: "
233
+ '{"subtitle": "...", "voice_instruction": "..."}'
234
  )
235
 
236
  user_prompt = f"""
237
  Input intent data:
238
  {json.dumps(intent, ensure_ascii=False, indent=2)}
239
 
240
+ Task:
241
+ Generate a short natural subtitle and a TTS voice instruction.
242
+
243
  Rules:
244
+ - Return raw JSON only.
245
+ - Do not use markdown.
246
+ - Do not include explanations.
247
+ - Do not include code fences.
248
+ - The subtitle must be only the sentence to speak.
249
+ - The voice_instruction must describe tone, emotion, pace, and intensity.
250
+ - Do not copy JSON keys into the subtitle.
251
+
252
+ Expected output format:
253
+ {{"subtitle": "I am happy to see you.", "voice_instruction": "Speak warmly, joyfully, and clearly."}}
254
  """
255
 
256
  llm = get_llm_model()
 
260
  {"role": "system", "content": system_prompt},
261
  {"role": "user", "content": user_prompt},
262
  ],
263
+ temperature=0.1,
264
  max_tokens=96,
265
  )
266
 
267
+ raw_content = result["choices"][0]["message"]["content"].strip()
268
 
269
  try:
270
+ parsed = extract_json_object(raw_content)
271
+ normalized = normalize_llm_output(parsed)
272
+ except Exception as error:
273
+ normalized = {
274
+ "subtitle": "I am happy to see you.",
275
+ "voice_instruction": "Speak warmly, joyfully, and clearly.",
276
+ "parser_warning": str(error),
277
+ "raw_model_output": raw_content,
278
  }
279
 
280
+ return (
281
+ normalized["subtitle"],
282
+ normalized["voice_instruction"],
283
+ normalized,
284
+ )
 
 
 
 
285
 
286
 
287
  def generate_tts(text, language, speaker, instruction):