ACloudCenter commited on
Commit
081b956
·
1 Parent(s): 34142eb

Add script title generation, show in header and audio player, CFG default 2.0

Browse files
Files changed (1) hide show
  1. app.py +131 -53
app.py CHANGED
@@ -134,14 +134,17 @@ STYLE:
134
  - Include personality — people joke, digress slightly, use analogies, get passionate about topics
135
 
136
  FORMAT RULES:
137
- - Use EXACTLY this format: "Speaker N: dialogue text" where N starts at 1
 
 
138
  - Each turn is separated by a blank line
139
  - Choose the right number of speakers for the scenario (1 to 4 max)
140
  - Keep the total script under {max_words} words
141
- - Output ONLY the script — no stage directions, no commentary, no preamble"""
142
 
143
 
144
- def generate_script_from_prompt(prompt: str) -> tuple[list[dict], int]:
 
145
  system = SCRIPT_SYSTEM_PROMPT.format(max_words=SCRIPT_MAX_WORDS)
146
  response = llm_client.chat_completion(
147
  messages=[
@@ -152,6 +155,14 @@ def generate_script_from_prompt(prompt: str) -> tuple[list[dict], int]:
152
  temperature=0.7,
153
  )
154
  raw = response.choices[0].message.content
 
 
 
 
 
 
 
 
155
  turns = parse_script_to_turns(raw)
156
  turns = turns[:MAX_TURNS]
157
  total_words = sum(len(t["text"].split()) for t in turns)
@@ -160,7 +171,7 @@ def generate_script_from_prompt(prompt: str) -> tuple[list[dict], int]:
160
  total_words = sum(len(t["text"].split()) for t in turns)
161
  speaker_ids = {t["speaker"] for t in turns}
162
  num_speakers = max(min(len(speaker_ids), 4), 1) if speaker_ids else 1
163
- return turns, num_speakers
164
 
165
 
166
  # --- Modal Connection ---
@@ -246,6 +257,25 @@ CUSTOM_CSS = """
246
  padding: 48px 20px !important;
247
  opacity: 0.6;
248
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  """
250
 
251
 
@@ -274,15 +304,31 @@ AUDIO_STAGE_LABELS = {
274
  }
275
 
276
 
277
- def build_primary_status(stage: str, status_line: str) -> str:
 
278
  title, default_desc = PRIMARY_STAGE_MESSAGES.get(stage, ("Working", "Processing..."))
279
- desc_parts = []
280
- if default_desc:
281
- desc_parts.append(default_desc)
282
- if status_line and status_line not in desc_parts:
283
- desc_parts.append(status_line)
284
- desc = "\n\n".join(desc_parts) if desc_parts else status_line
285
- return f"### {title}\n{desc}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
286
 
287
 
288
  # ========================================================
@@ -298,6 +344,7 @@ def create_demo_interface():
298
 
299
  # --- State ---
300
  turns_state = gr.State([])
 
301
 
302
  # ---- BANNER ----
303
  gr.HTML("""
@@ -346,7 +393,7 @@ def create_demo_interface():
346
 
347
  # ---- STEP 2: SCRIPT EDITOR ----
348
  with gr.Row():
349
- gr.HTML("<h3 style='margin:0'>Script</h3>")
350
  duration_display = gr.HTML(value="")
351
 
352
  with gr.Column(elem_classes="conversation-scroll"):
@@ -441,7 +488,7 @@ def create_demo_interface():
441
  speaker_selections.append(s)
442
  with gr.Row():
443
  cfg_scale = gr.Slider(
444
- minimum=1.0, maximum=2.0, value=1.3, step=0.05,
445
  label="CFG Scale",
446
  )
447
 
@@ -450,9 +497,9 @@ def create_demo_interface():
450
  "Generate Conference Audio", size="lg", variant="primary",
451
  elem_classes="cta-btn",
452
  )
 
453
 
454
  # ---- OUTPUT ----
455
- primary_status = gr.Markdown(value="", elem_id="primary-status")
456
  complete_audio_output = gr.Audio(
457
  label=AUDIO_LABEL_DEFAULT,
458
  type="numpy",
@@ -502,10 +549,17 @@ def create_demo_interface():
502
  )
503
 
504
  # --- AI Script Generation ---
 
505
  def _no_change(status_html):
506
  return (gr.update(), gr.update(), status_html,
 
507
  gr.update(), *[gr.update()] * 4)
508
 
 
 
 
 
 
509
  def on_generate_script(prompt):
510
  if not prompt or not prompt.strip():
511
  gr.Warning("Please enter a prompt.")
@@ -515,7 +569,7 @@ def create_demo_interface():
515
  yield _no_change("<em>Writing script...</em>")
516
 
517
  try:
518
- turns, detected = generate_script_from_prompt(prompt.strip())
519
  if not turns:
520
  yield _no_change("<em>Empty result — try a more descriptive prompt.</em>")
521
  return
@@ -524,7 +578,10 @@ def create_demo_interface():
524
  while len(voices) < 4:
525
  voices.append(None)
526
 
 
527
  yield (turns, estimate_duration(turns), "",
 
 
528
  detected, *voices[:4])
529
  except Exception as e:
530
  print(f"Script generation error: {e}")
@@ -539,14 +596,16 @@ def create_demo_interface():
539
  fn=on_generate_script,
540
  inputs=[script_prompt],
541
  outputs=[turns_state, duration_display, script_gen_status,
 
542
  num_speakers] + speaker_selections,
543
  )
544
 
545
  # --- Load examples ---
546
  def load_example(idx):
547
  if idx >= len(EXAMPLE_SCRIPTS):
548
- return [], 2, "", *[None] * 4
549
 
 
550
  script = EXAMPLE_SCRIPTS_NATURAL[idx]
551
  num = SCRIPT_SPEAKER_COUNTS[idx] if idx < len(SCRIPT_SPEAKER_COUNTS) else 1
552
  turns = parse_script_to_turns(script)
@@ -555,50 +614,68 @@ def create_demo_interface():
555
  while len(voices) < 4:
556
  voices.append(None)
557
 
558
- return turns, num, estimate_duration(turns), *voices[:4]
 
 
 
559
 
560
  for idx, btn in enumerate(example_buttons):
561
  btn.click(
562
  fn=lambda i=idx: load_example(i),
563
  inputs=[],
564
- outputs=[turns_state, num_speakers, duration_display] + speaker_selections,
 
565
  queue=False,
566
  )
567
 
568
  # --- Generate audio ---
 
 
 
 
 
 
 
 
 
569
  def generate_podcast_wrapper(
570
  model_choice, num_speakers_val, turns, *speakers_and_params
571
  ):
 
 
 
572
  if remote_generate_function is None:
573
- yield (
574
- build_primary_status("error", "Modal backend is offline."),
575
- gr.update(label=AUDIO_STAGE_LABELS.get("error", AUDIO_LABEL_DEFAULT)),
576
- "ERROR: Modal function not deployed.",
577
  )
578
  return
579
 
580
  script = turns_to_script(turns)
581
  if not script.strip():
582
- yield (
583
- build_primary_status("error", "No script to generate."),
584
- gr.update(label=AUDIO_STAGE_LABELS.get("error", AUDIO_LABEL_DEFAULT)),
585
- "Add dialogue before generating.",
586
  )
587
  return
588
 
589
  word_count = len(script.split())
590
  if word_count > MAX_SCRIPT_WORDS:
591
- yield (
592
- build_primary_status("error",
593
  f"Script too long: {word_count} words (max {MAX_SCRIPT_WORDS}). "
594
- "Shorten some turns to keep generation costs reasonable."),
595
- gr.update(label=AUDIO_STAGE_LABELS.get("error", AUDIO_LABEL_DEFAULT)),
596
- f"Script has {word_count} words, max is {MAX_SCRIPT_WORDS}.",
597
  )
598
  return
599
 
600
- yield (
601
- build_primary_status("connecting", "Provisioning GPU..."),
 
 
602
  gr.update(label=AUDIO_STAGE_LABELS.get("connecting", AUDIO_LABEL_DEFAULT)),
603
  "Requesting GPU on Modal.com...",
604
  )
@@ -607,7 +684,6 @@ def create_demo_interface():
607
  speakers = speakers_and_params[:4]
608
  cfg_scale_val = speakers_and_params[4]
609
  current_log = ""
610
- last_audio_label = AUDIO_STAGE_LABELS.get("connecting", AUDIO_LABEL_DEFAULT)
611
  last_stage = "connecting"
612
 
613
  for update in remote_generate_function.remote_gen(
@@ -629,9 +705,9 @@ def create_demo_interface():
629
  status_line = update.get("status") or "Processing..."
630
  current_log = update.get("log", current_log)
631
 
632
- audio_label = AUDIO_STAGE_LABELS.get(stage_key)
633
- if not audio_label:
634
- audio_label = f"Audio ({stage_key.replace('_',' ')})"
635
  if stage_key == "complete":
636
  audio_label = AUDIO_LABEL_DEFAULT
637
 
@@ -639,12 +715,13 @@ def create_demo_interface():
639
  if audio_payload is not None:
640
  audio_update = gr.update(value=audio_payload, label=AUDIO_LABEL_DEFAULT)
641
 
642
- yield (
643
- build_primary_status(stage_key, status_line),
 
 
644
  audio_update,
645
  current_log,
646
  )
647
- last_audio_label = audio_label
648
  last_stage = stage_key
649
  else:
650
  audio_payload, log_text = (
@@ -653,31 +730,32 @@ def create_demo_interface():
653
  if log_text:
654
  current_log = log_text
655
  if audio_payload is not None:
656
- yield (
657
- build_primary_status("complete", "Ready."),
 
658
  gr.update(value=audio_payload, label=AUDIO_LABEL_DEFAULT),
659
  current_log,
660
  )
661
  else:
662
- yield (
663
- build_primary_status("generating_audio",
664
- current_log.splitlines()[-1] if current_log else "Processing..."),
665
- gr.update(label=AUDIO_STAGE_LABELS.get("generating_audio", last_audio_label)),
666
- current_log,
667
  )
668
  except Exception as e:
669
  tb = traceback.format_exc()
670
  print(f"Error calling Modal: {e}")
671
- yield (
672
- build_primary_status("error", "Inference failed."),
673
- gr.update(label=AUDIO_STAGE_LABELS.get("error", AUDIO_LABEL_DEFAULT)),
674
- f"Error: {e}\n\n{tb}",
675
  )
676
 
677
  generate_btn.click(
678
  fn=generate_podcast_wrapper,
679
  inputs=[model_dropdown, num_speakers, turns_state] + speaker_selections + [cfg_scale],
680
- outputs=[primary_status, complete_audio_output, log_output],
681
  )
682
 
683
  # ==================== ARCHITECTURE TAB ====================
 
134
  - Include personality — people joke, digress slightly, use analogies, get passionate about topics
135
 
136
  FORMAT RULES:
137
+ - Start with a title on the FIRST LINE in this format: "Title: Your Script Title Here"
138
+ - Then a blank line, then the dialogue
139
+ - Use EXACTLY this format for dialogue: "Speaker N: dialogue text" where N starts at 1
140
  - Each turn is separated by a blank line
141
  - Choose the right number of speakers for the scenario (1 to 4 max)
142
  - Keep the total script under {max_words} words
143
+ - Output ONLY the title and script — no stage directions, no commentary, no preamble"""
144
 
145
 
146
+ def generate_script_from_prompt(prompt: str) -> tuple[list[dict], int, str]:
147
+ """Returns (turns, num_speakers, title)."""
148
  system = SCRIPT_SYSTEM_PROMPT.format(max_words=SCRIPT_MAX_WORDS)
149
  response = llm_client.chat_completion(
150
  messages=[
 
155
  temperature=0.7,
156
  )
157
  raw = response.choices[0].message.content
158
+
159
+ # Extract title from first line if present
160
+ title = ""
161
+ lines = raw.strip().split("\n")
162
+ if lines and lines[0].lower().startswith("title:"):
163
+ title = lines[0].split(":", 1)[1].strip()
164
+ raw = "\n".join(lines[1:])
165
+
166
  turns = parse_script_to_turns(raw)
167
  turns = turns[:MAX_TURNS]
168
  total_words = sum(len(t["text"].split()) for t in turns)
 
171
  total_words = sum(len(t["text"].split()) for t in turns)
172
  speaker_ids = {t["speaker"] for t in turns}
173
  num_speakers = max(min(len(speaker_ids), 4), 1) if speaker_ids else 1
174
+ return turns, num_speakers, title
175
 
176
 
177
  # --- Modal Connection ---
 
257
  padding: 48px 20px !important;
258
  opacity: 0.6;
259
  }
260
+
261
+ /* ---- Generation status banner ---- */
262
+ .gen-status {
263
+ border-radius: 10px;
264
+ padding: 16px 20px;
265
+ margin-top: 8px;
266
+ text-align: center;
267
+ font-size: 1.05em;
268
+ min-height: 0;
269
+ }
270
+ .gen-status-active {
271
+ background: var(--background-fill-secondary);
272
+ border: 1px solid var(--border-color-primary);
273
+ animation: pulse-border 2s ease-in-out infinite;
274
+ }
275
+ @keyframes pulse-border {
276
+ 0%, 100% { border-color: var(--border-color-primary); }
277
+ 50% { border-color: #6366f1; }
278
+ }
279
  """
280
 
281
 
 
304
  }
305
 
306
 
307
+ def build_status_html(stage: str, status_line: str) -> str:
308
+ """Build an HTML status banner for the generation progress."""
309
  title, default_desc = PRIMARY_STAGE_MESSAGES.get(stage, ("Working", "Processing..."))
310
+ desc = status_line or default_desc or ""
311
+
312
+ if stage == "complete":
313
+ icon = "&#10003;"
314
+ color = "#22c55e"
315
+ cls = "gen-status"
316
+ elif stage == "error":
317
+ icon = "&#10007;"
318
+ color = "#ef4444"
319
+ cls = "gen-status"
320
+ else:
321
+ icon = "&#9679;"
322
+ color = "#6366f1"
323
+ cls = "gen-status gen-status-active"
324
+
325
+ return (
326
+ f'<div class="{cls}">'
327
+ f'<span style="color:{color}; font-size:1.3em; vertical-align:middle;">{icon}</span> '
328
+ f'<strong>{title}</strong>'
329
+ f'<br><span style="opacity:0.75; font-size:0.9em;">{desc}</span>'
330
+ f'</div>'
331
+ )
332
 
333
 
334
  # ========================================================
 
344
 
345
  # --- State ---
346
  turns_state = gr.State([])
347
+ script_title_state = gr.State("")
348
 
349
  # ---- BANNER ----
350
  gr.HTML("""
 
393
 
394
  # ---- STEP 2: SCRIPT EDITOR ----
395
  with gr.Row():
396
+ script_title_display = gr.HTML(value="<h3 style='margin:0'>Script</h3>")
397
  duration_display = gr.HTML(value="")
398
 
399
  with gr.Column(elem_classes="conversation-scroll"):
 
488
  speaker_selections.append(s)
489
  with gr.Row():
490
  cfg_scale = gr.Slider(
491
+ minimum=1.0, maximum=2.0, value=2.0, step=0.05,
492
  label="CFG Scale",
493
  )
494
 
 
497
  "Generate Conference Audio", size="lg", variant="primary",
498
  elem_classes="cta-btn",
499
  )
500
+ primary_status = gr.HTML(value="", elem_classes="gen-status")
501
 
502
  # ---- OUTPUT ----
 
503
  complete_audio_output = gr.Audio(
504
  label=AUDIO_LABEL_DEFAULT,
505
  type="numpy",
 
549
  )
550
 
551
  # --- AI Script Generation ---
552
+ # outputs: turns, duration, status, title_display, audio_label, num_speakers, *4 voices
553
  def _no_change(status_html):
554
  return (gr.update(), gr.update(), status_html,
555
+ gr.update(), gr.update(),
556
  gr.update(), *[gr.update()] * 4)
557
 
558
+ def _make_title_html(title):
559
+ if title:
560
+ return f"<h3 style='margin:0'>{title}</h3>"
561
+ return "<h3 style='margin:0'>Script</h3>"
562
+
563
  def on_generate_script(prompt):
564
  if not prompt or not prompt.strip():
565
  gr.Warning("Please enter a prompt.")
 
569
  yield _no_change("<em>Writing script...</em>")
570
 
571
  try:
572
+ turns, detected, title = generate_script_from_prompt(prompt.strip())
573
  if not turns:
574
  yield _no_change("<em>Empty result — try a more descriptive prompt.</em>")
575
  return
 
578
  while len(voices) < 4:
579
  voices.append(None)
580
 
581
+ audio_label = title if title else AUDIO_LABEL_DEFAULT
582
  yield (turns, estimate_duration(turns), "",
583
+ _make_title_html(title),
584
+ gr.update(label=audio_label),
585
  detected, *voices[:4])
586
  except Exception as e:
587
  print(f"Script generation error: {e}")
 
596
  fn=on_generate_script,
597
  inputs=[script_prompt],
598
  outputs=[turns_state, duration_display, script_gen_status,
599
+ script_title_display, complete_audio_output,
600
  num_speakers] + speaker_selections,
601
  )
602
 
603
  # --- Load examples ---
604
  def load_example(idx):
605
  if idx >= len(EXAMPLE_SCRIPTS):
606
+ return [], 2, "", "<h3 style='margin:0'>Script</h3>", gr.update(), *[None] * 4
607
 
608
+ title = example_names[idx]
609
  script = EXAMPLE_SCRIPTS_NATURAL[idx]
610
  num = SCRIPT_SPEAKER_COUNTS[idx] if idx < len(SCRIPT_SPEAKER_COUNTS) else 1
611
  turns = parse_script_to_turns(script)
 
614
  while len(voices) < 4:
615
  voices.append(None)
616
 
617
+ return (turns, num, estimate_duration(turns),
618
+ f"<h3 style='margin:0'>{title}</h3>",
619
+ gr.update(label=title),
620
+ *voices[:4])
621
 
622
  for idx, btn in enumerate(example_buttons):
623
  btn.click(
624
  fn=lambda i=idx: load_example(i),
625
  inputs=[],
626
+ outputs=[turns_state, num_speakers, duration_display,
627
+ script_title_display, complete_audio_output] + speaker_selections,
628
  queue=False,
629
  )
630
 
631
  # --- Generate audio ---
632
+ def _gen_yield(status_html, btn_label, btn_interactive, audio_update, log_text):
633
+ """Helper to yield consistent 5-tuple outputs."""
634
+ return (
635
+ status_html,
636
+ gr.update(value=btn_label, interactive=btn_interactive),
637
+ audio_update,
638
+ log_text,
639
+ )
640
+
641
  def generate_podcast_wrapper(
642
  model_choice, num_speakers_val, turns, *speakers_and_params
643
  ):
644
+ BTN_BUSY = "Generating..."
645
+ BTN_READY = "Generate Conference Audio"
646
+
647
  if remote_generate_function is None:
648
+ yield _gen_yield(
649
+ build_status_html("error", "Modal backend is offline."),
650
+ BTN_READY, True,
651
+ gr.update(), "ERROR: Modal function not deployed.",
652
  )
653
  return
654
 
655
  script = turns_to_script(turns)
656
  if not script.strip():
657
+ yield _gen_yield(
658
+ build_status_html("error", "No script to generate."),
659
+ BTN_READY, True,
660
+ gr.update(), "Add dialogue before generating.",
661
  )
662
  return
663
 
664
  word_count = len(script.split())
665
  if word_count > MAX_SCRIPT_WORDS:
666
+ yield _gen_yield(
667
+ build_status_html("error",
668
  f"Script too long: {word_count} words (max {MAX_SCRIPT_WORDS}). "
669
+ "Shorten some turns."),
670
+ BTN_READY, True,
671
+ gr.update(), f"Script has {word_count} words, max is {MAX_SCRIPT_WORDS}.",
672
  )
673
  return
674
 
675
+ # Disable button, show connecting status
676
+ yield _gen_yield(
677
+ build_status_html("connecting", "Provisioning GPU resources..."),
678
+ BTN_BUSY, False,
679
  gr.update(label=AUDIO_STAGE_LABELS.get("connecting", AUDIO_LABEL_DEFAULT)),
680
  "Requesting GPU on Modal.com...",
681
  )
 
684
  speakers = speakers_and_params[:4]
685
  cfg_scale_val = speakers_and_params[4]
686
  current_log = ""
 
687
  last_stage = "connecting"
688
 
689
  for update in remote_generate_function.remote_gen(
 
705
  status_line = update.get("status") or "Processing..."
706
  current_log = update.get("log", current_log)
707
 
708
+ audio_label = AUDIO_STAGE_LABELS.get(stage_key,
709
+ f"Audio ({stage_key.replace('_',' ')})")
710
+ is_done = stage_key in ("complete", "error")
711
  if stage_key == "complete":
712
  audio_label = AUDIO_LABEL_DEFAULT
713
 
 
715
  if audio_payload is not None:
716
  audio_update = gr.update(value=audio_payload, label=AUDIO_LABEL_DEFAULT)
717
 
718
+ yield _gen_yield(
719
+ build_status_html(stage_key, status_line),
720
+ BTN_READY if is_done else BTN_BUSY,
721
+ is_done,
722
  audio_update,
723
  current_log,
724
  )
 
725
  last_stage = stage_key
726
  else:
727
  audio_payload, log_text = (
 
730
  if log_text:
731
  current_log = log_text
732
  if audio_payload is not None:
733
+ yield _gen_yield(
734
+ build_status_html("complete", "Ready."),
735
+ BTN_READY, True,
736
  gr.update(value=audio_payload, label=AUDIO_LABEL_DEFAULT),
737
  current_log,
738
  )
739
  else:
740
+ status_line = current_log.splitlines()[-1] if current_log else "Processing..."
741
+ yield _gen_yield(
742
+ build_status_html("generating_audio", status_line),
743
+ BTN_BUSY, False,
744
+ gr.update(), current_log,
745
  )
746
  except Exception as e:
747
  tb = traceback.format_exc()
748
  print(f"Error calling Modal: {e}")
749
+ yield _gen_yield(
750
+ build_status_html("error", "Inference failed."),
751
+ BTN_READY, True,
752
+ gr.update(), f"Error: {e}\n\n{tb}",
753
  )
754
 
755
  generate_btn.click(
756
  fn=generate_podcast_wrapper,
757
  inputs=[model_dropdown, num_speakers, turns_state] + speaker_selections + [cfg_scale],
758
+ outputs=[primary_status, generate_btn, complete_audio_output, log_output],
759
  )
760
 
761
  # ==================== ARCHITECTURE TAB ====================