Commit ·
081b956
1
Parent(s): 34142eb
Add script title generation, show in header and audio player, CFG default 2.0
Browse files
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 |
-
-
|
|
|
|
|
|
|
| 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
|
|
|
|
| 278 |
title, default_desc = PRIMARY_STAGE_MESSAGES.get(stage, ("Working", "Processing..."))
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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),
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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 |
-
|
| 575 |
-
|
| 576 |
-
"ERROR: Modal function not deployed.",
|
| 577 |
)
|
| 578 |
return
|
| 579 |
|
| 580 |
script = turns_to_script(turns)
|
| 581 |
if not script.strip():
|
| 582 |
-
yield (
|
| 583 |
-
|
| 584 |
-
|
| 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 |
-
|
| 593 |
f"Script too long: {word_count} words (max {MAX_SCRIPT_WORDS}). "
|
| 594 |
-
"Shorten some turns
|
| 595 |
-
|
| 596 |
-
f"Script has {word_count} words, max is {MAX_SCRIPT_WORDS}.",
|
| 597 |
)
|
| 598 |
return
|
| 599 |
|
| 600 |
-
|
| 601 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 634 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 658 |
gr.update(value=audio_payload, label=AUDIO_LABEL_DEFAULT),
|
| 659 |
current_log,
|
| 660 |
)
|
| 661 |
else:
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
current_log,
|
| 667 |
)
|
| 668 |
except Exception as e:
|
| 669 |
tb = traceback.format_exc()
|
| 670 |
print(f"Error calling Modal: {e}")
|
| 671 |
-
yield (
|
| 672 |
-
|
| 673 |
-
|
| 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 = "✓"
|
| 314 |
+
color = "#22c55e"
|
| 315 |
+
cls = "gen-status"
|
| 316 |
+
elif stage == "error":
|
| 317 |
+
icon = "✗"
|
| 318 |
+
color = "#ef4444"
|
| 319 |
+
cls = "gen-status"
|
| 320 |
+
else:
|
| 321 |
+
icon = "●"
|
| 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 ====================
|