PlotweaverModel commited on
Commit
a5a0552
·
verified ·
1 Parent(s): 3536736

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +138 -77
app.py CHANGED
@@ -173,101 +173,162 @@ SOURCE_LANGUAGES = ["English", "Chinese", "Japanese", "Korean", "German",
173
  "Hindi", "Swahili", "Auto-detect"]
174
 
175
  def translate_text(client, text, source_lang, target_lang):
176
- """Translate text between languages using the LLM."""
177
  if not client:
178
- raise gr.Error("DASHSCOPE_API_KEY needed for translation. Add it in Settings > Secrets.")
179
  if source_lang == target_lang:
180
  return text
181
-
182
- source_hint = f" (source language: {source_lang})" if source_lang != "Auto-detect" else ""
183
- response = client.chat.completions.create(
184
- model=OMNI_MODEL, modalities=["text"],
185
- messages=[{
186
- "role": "system",
187
- "content": f"Translate the following text into {target_lang}. Output ONLY the translation, nothing else.",
188
- }, {
189
- "role": "user",
190
- "content": f"Translate this{source_hint}:\n\n{text}",
191
- }],
192
- )
193
- translated = response.choices[0].message.content.strip()
194
- print(f"[Translate] {source_lang} -> {target_lang}: {len(text)} -> {len(translated)} chars")
195
- return translated
 
 
 
 
 
 
 
196
 
197
 
198
  # ==========================================
199
  # EMOTION ANALYSIS
200
  # ==========================================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  def analyze_emotions(client, text):
202
- """Split text into segments with emotion instructions."""
203
  if not client:
204
  return [{"text": text, "emotion": ""}]
205
- try:
206
- response = client.chat.completions.create(
207
- model=OMNI_MODEL, modalities=["text"],
208
- messages=[{
209
- "role": "system",
210
- "content": (
211
- "You are an audiobook director. Split text into segments where the emotional "
212
- "tone changes. For each segment, provide a specific emotion/delivery instruction.\n\n"
213
- "Output ONLY valid JSON:\n"
214
- '{"segments": [\n'
215
- ' {"text": "The lighthouse stood tall.", "emotion": "Atmospheric, steady narration"},\n'
216
- ' {"text": "She ran!", "emotion": "Urgent, breathless, rising tension"}\n'
217
- "]}\n\n"
218
- "Rules: Include ALL text. 1-4 sentences per segment. Be specific with emotions. "
219
- "No markdown. ONLY JSON."
220
- ),
221
- }, {"role": "user", "content": f"Direct this:\n\n{text[:6000]}"}],
222
- )
223
- raw = response.choices[0].message.content.strip()
224
- raw = re.sub(r'^```json\s*', '', raw)
225
- raw = re.sub(r'\s*```$', '', raw)
226
- data = json.loads(raw)
227
- segs = data.get("segments", [])
228
- if segs:
229
- print(f"[Emotions] {len(segs)} segments")
230
- return segs
231
- except Exception as e:
232
- print(f"[Emotions] Failed: {e}")
233
- return [{"text": text, "emotion": ""}]
 
 
 
 
 
 
 
 
 
 
234
 
235
 
236
  def detect_characters_and_emotions(client, text):
237
- """Detect characters + emotions for multi-speaker mode."""
238
- response = client.chat.completions.create(
239
- model=OMNI_MODEL, modalities=["text"],
240
- messages=[{
241
- "role": "system",
242
- "content": (
243
- "You are an audiobook director. Analyze this story:\n"
244
- "1. Identify all characters with genders\n"
245
- "2. Split into segments by speaker\n"
246
- "3. Add emotion instructions per segment\n\n"
247
- "Output ONLY valid JSON:\n"
248
- '{"characters": [{"name": "Narrator", "gender": "neutral"}, '
249
- '{"name": "Elena", "gender": "female"}],\n'
250
- '"segments": [{"speaker": "Narrator", "text": "...", "emotion": "Calm narration"}, '
251
- '{"speaker": "Elena", "text": "...", "emotion": "Wistful, dreamy"}]}\n\n'
252
- "Rules: Narrator handles non-dialogue. Include ALL text. "
253
- "Be specific with emotions. No markdown. ONLY JSON."
254
- ),
255
- }, {"role": "user", "content": f"Direct this story:\n\n{text[:8000]}"}],
256
- )
257
- raw = response.choices[0].message.content.strip()
258
- raw = re.sub(r'^```json\s*', '', raw)
259
- raw = re.sub(r'\s*```$', '', raw)
260
- try:
261
- data = json.loads(raw)
262
- return data.get("characters", []), data.get("segments", [])
263
- except Exception:
264
- return [{"name": "Narrator", "gender": "neutral"}], [{"speaker": "Narrator", "text": text, "emotion": ""}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
265
 
266
 
267
  # ==========================================
268
  # SINGLE SPEAKER: Generate with emotions
269
  # ==========================================
270
- @spaces.GPU
271
  def generate_single_speaker(text_input, file_input, source_lang, target_lang, speaker_label,
272
  use_clone, clone_audio, clone_transcript,
273
  progress=gr.Progress()):
@@ -385,7 +446,7 @@ def generate_single_speaker(text_input, file_input, source_lang, target_lang, sp
385
  # ==========================================
386
  # MULTI-SPEAKER: Generate with character voices
387
  # ==========================================
388
- @spaces.GPU
389
  def generate_multi_speaker(text_input, file_input, source_lang, target_lang, progress=gr.Progress()):
390
  resolved = resolve_text(text_input, file_input)
391
  if len(resolved) < 30:
 
173
  "Hindi", "Swahili", "Auto-detect"]
174
 
175
  def translate_text(client, text, source_lang, target_lang):
176
+ """Translate text between languages. Handles long texts by chunking."""
177
  if not client:
178
+ raise gr.Error("DASHSCOPE_API_KEY needed for translation.")
179
  if source_lang == target_lang:
180
  return text
181
+
182
+ chunks = split_for_llm(text, max_chars=4000)
183
+ translated_parts = []
184
+
185
+ for ci, chunk in enumerate(chunks):
186
+ source_hint = f" (source language: {source_lang})" if source_lang != "Auto-detect" else ""
187
+ response = client.chat.completions.create(
188
+ model=OMNI_MODEL, modalities=["text"],
189
+ messages=[{
190
+ "role": "system",
191
+ "content": f"Translate the following text into {target_lang}. Output ONLY the translation.",
192
+ }, {
193
+ "role": "user",
194
+ "content": f"Translate this{source_hint}:\n\n{chunk}",
195
+ }],
196
+ )
197
+ translated_parts.append(response.choices[0].message.content.strip())
198
+ print(f"[Translate] Chunk {ci+1}/{len(chunks)} done")
199
+
200
+ result = "\n\n".join(translated_parts)
201
+ print(f"[Translate] {source_lang} -> {target_lang}: {len(text)} -> {len(result)} chars")
202
+ return result
203
 
204
 
205
  # ==========================================
206
  # EMOTION ANALYSIS
207
  # ==========================================
208
+ def split_for_llm(text, max_chars=4000):
209
+ """Split long text into chunks at paragraph boundaries for LLM processing."""
210
+ if len(text) <= max_chars:
211
+ return [text]
212
+ chunks, paragraphs, current = [], re.split(r'\n\s*\n', text), ""
213
+ for para in paragraphs:
214
+ para = para.strip()
215
+ if not para:
216
+ continue
217
+ if len(current) + len(para) + 2 > max_chars and current:
218
+ chunks.append(current.strip())
219
+ current = para
220
+ else:
221
+ current = (current + "\n\n" + para).strip()
222
+ if current.strip():
223
+ chunks.append(current.strip())
224
+ return chunks if chunks else [text]
225
+
226
+
227
  def analyze_emotions(client, text):
228
+ """Split text into segments with emotion instructions. Handles long texts."""
229
  if not client:
230
  return [{"text": text, "emotion": ""}]
231
+
232
+ chunks = split_for_llm(text, max_chars=4000)
233
+ all_segments = []
234
+
235
+ for ci, chunk in enumerate(chunks):
236
+ try:
237
+ response = client.chat.completions.create(
238
+ model=OMNI_MODEL, modalities=["text"],
239
+ messages=[{
240
+ "role": "system",
241
+ "content": (
242
+ "You are an audiobook director. Split text into segments where the emotional "
243
+ "tone changes. For each segment, provide a specific emotion/delivery instruction.\n\n"
244
+ "Output ONLY valid JSON:\n"
245
+ '{"segments": [\n'
246
+ ' {"text": "The lighthouse stood tall.", "emotion": "Atmospheric, steady narration"},\n'
247
+ ' {"text": "She ran!", "emotion": "Urgent, breathless, rising tension"}\n'
248
+ "]}\n\n"
249
+ "Rules: Include ALL text. 1-4 sentences per segment. Be specific with emotions. "
250
+ "No markdown. ONLY JSON."
251
+ ),
252
+ }, {"role": "user", "content": f"Direct this:\n\n{chunk}"}],
253
+ )
254
+ raw = response.choices[0].message.content.strip()
255
+ raw = re.sub(r'^```json\s*', '', raw)
256
+ raw = re.sub(r'\s*```$', '', raw)
257
+ data = json.loads(raw)
258
+ segs = data.get("segments", [])
259
+ if segs:
260
+ all_segments.extend(segs)
261
+ else:
262
+ all_segments.append({"text": chunk, "emotion": ""})
263
+ print(f"[Emotions] Chunk {ci+1}/{len(chunks)}: {len(segs)} segments")
264
+ except Exception as e:
265
+ print(f"[Emotions] Chunk {ci+1} failed: {e}")
266
+ all_segments.append({"text": chunk, "emotion": ""})
267
+
268
+ print(f"[Emotions] Total: {len(all_segments)} segments from {len(chunks)} chunks")
269
+ return all_segments if all_segments else [{"text": text, "emotion": ""}]
270
 
271
 
272
  def detect_characters_and_emotions(client, text):
273
+ """Detect characters + emotions for multi-speaker mode. Handles long texts."""
274
+ chunks = split_for_llm(text, max_chars=5000)
275
+ all_characters = {}
276
+ all_segments = []
277
+
278
+ for ci, chunk in enumerate(chunks):
279
+ try:
280
+ response = client.chat.completions.create(
281
+ model=OMNI_MODEL, modalities=["text"],
282
+ messages=[{
283
+ "role": "system",
284
+ "content": (
285
+ "You are an audiobook director. Analyze this story:\n"
286
+ "1. Identify all characters with genders\n"
287
+ "2. Split into segments by speaker\n"
288
+ "3. Add emotion instructions per segment\n\n"
289
+ "Output ONLY valid JSON:\n"
290
+ '{"characters": [{"name": "Narrator", "gender": "neutral"}, '
291
+ '{"name": "Elena", "gender": "female"}],\n'
292
+ '"segments": [{"speaker": "Narrator", "text": "...", "emotion": "Calm narration"}, '
293
+ '{"speaker": "Elena", "text": "...", "emotion": "Wistful, dreamy"}]}\n\n'
294
+ "Rules: Narrator handles non-dialogue. Include ALL text. "
295
+ "Be specific with emotions. No markdown. ONLY JSON."
296
+ ),
297
+ }, {"role": "user", "content": f"Direct this story:\n\n{chunk}"}],
298
+ )
299
+ raw = response.choices[0].message.content.strip()
300
+ raw = re.sub(r'^```json\s*', '', raw)
301
+ raw = re.sub(r'\s*```$', '', raw)
302
+ data = json.loads(raw)
303
+
304
+ # Merge characters (keep unique by name)
305
+ for c in data.get("characters", []):
306
+ name = c.get("name", "Narrator")
307
+ if name not in all_characters:
308
+ all_characters[name] = c
309
+
310
+ all_segments.extend(data.get("segments", []))
311
+ print(f"[MultiSpeaker] Chunk {ci+1}/{len(chunks)}: {len(data.get('segments', []))} segments")
312
+ except Exception as e:
313
+ print(f"[MultiSpeaker] Chunk {ci+1} failed: {e}")
314
+ all_segments.append({"speaker": "Narrator", "text": chunk, "emotion": ""})
315
+
316
+ # Ensure Narrator exists
317
+ if "Narrator" not in all_characters:
318
+ all_characters["Narrator"] = {"name": "Narrator", "gender": "neutral"}
319
+
320
+ characters = list(all_characters.values())
321
+ # Put Narrator first
322
+ characters.sort(key=lambda c: 0 if c["name"] == "Narrator" else 1)
323
+
324
+ print(f"[MultiSpeaker] Total: {len(characters)} characters, {len(all_segments)} segments")
325
+ return characters, all_segments
326
 
327
 
328
  # ==========================================
329
  # SINGLE SPEAKER: Generate with emotions
330
  # ==========================================
331
+ @spaces.GPU(duration=600)
332
  def generate_single_speaker(text_input, file_input, source_lang, target_lang, speaker_label,
333
  use_clone, clone_audio, clone_transcript,
334
  progress=gr.Progress()):
 
446
  # ==========================================
447
  # MULTI-SPEAKER: Generate with character voices
448
  # ==========================================
449
+ @spaces.GPU(duration=600)
450
  def generate_multi_speaker(text_input, file_input, source_lang, target_lang, progress=gr.Progress()):
451
  resolved = resolve_text(text_input, file_input)
452
  if len(resolved) < 30: