Commit ·
c7437ae
1
Parent(s): 8855e7f
feat: add browser tts and conversation pacing
Browse files
app.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
from html import escape
|
|
|
|
| 2 |
import os
|
| 3 |
from time import sleep
|
| 4 |
|
|
@@ -43,7 +44,7 @@ EMPTY_STAGE = """
|
|
| 43 |
</div>
|
| 44 |
"""
|
| 45 |
|
| 46 |
-
EMPTY_TRANSCRIPT = "No show yet. The transcript will appear here.
|
| 47 |
EMPTY_AGENT_STATE = "<div class=\"agent-state-empty\">No agents on stage yet.</div>"
|
| 48 |
EMPTY_DIRECTOR_LOG = "No director notes yet."
|
| 49 |
EMPTY_TRACE = "No trace events yet."
|
|
@@ -80,7 +81,16 @@ OPENBMB_MODEL_ID = os.getenv("OPENBMB_MODEL_ID", DEFAULT_OPENBMB_MODEL_ID)
|
|
| 80 |
HF_API_MODEL_ID = os.getenv("HF_API_MODEL_ID", DEFAULT_HF_API_MODEL_ID)
|
| 81 |
DEFAULT_MAX_NEW_TOKENS = 120
|
| 82 |
DEFAULT_TEMPERATURE = 0.75
|
| 83 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
SHOW_LENGTH_CHOICES = [
|
| 85 |
("Short", "short"),
|
| 86 |
("Standard", "standard"),
|
|
@@ -110,6 +120,51 @@ DEFAULT_STAGE_PROPS: list[str] = [
|
|
| 110 |
"scroll",
|
| 111 |
]
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
def _prop_dropdown_label(canonical: str) -> str:
|
| 115 |
emoji = PROP_EMOJI.get(canonical.lower(), "🎁")
|
|
@@ -121,6 +176,49 @@ PROP_DROPDOWN_CHOICES: list[tuple[str, str]] = [
|
|
| 121 |
(_prop_dropdown_label(name), name) for name in DEFAULT_STAGE_PROPS
|
| 122 |
]
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
CUSTOM_CSS = """
|
| 125 |
body,
|
| 126 |
.gradio-container {
|
|
@@ -1021,6 +1119,63 @@ body,
|
|
| 1021 |
border: none !important;
|
| 1022 |
box-shadow: none !important;
|
| 1023 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1024 |
.agent-state-grid {
|
| 1025 |
display: grid;
|
| 1026 |
gap: 0.55rem;
|
|
@@ -1538,6 +1693,268 @@ THEATER_THEME = gr.themes.Soft(
|
|
| 1538 |
)
|
| 1539 |
|
| 1540 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1541 |
def render_stage(session: TheaterSession | None) -> str:
|
| 1542 |
if session is None:
|
| 1543 |
return EMPTY_STAGE
|
|
@@ -1645,16 +2062,27 @@ def render_transcript(session: TheaterSession | None) -> str:
|
|
| 1645 |
if session is None:
|
| 1646 |
return EMPTY_TRANSCRIPT
|
| 1647 |
|
| 1648 |
-
|
| 1649 |
-
"
|
| 1650 |
-
|
| 1651 |
-
]
|
| 1652 |
-
|
| 1653 |
-
|
| 1654 |
-
|
| 1655 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1656 |
|
| 1657 |
-
return "
|
| 1658 |
|
| 1659 |
|
| 1660 |
def render_agent_state(session: TheaterSession | None) -> str:
|
|
@@ -1807,6 +2235,7 @@ def render_outputs(session: TheaterSession | None):
|
|
| 1807 |
return (
|
| 1808 |
render_stage(session),
|
| 1809 |
render_transcript(session),
|
|
|
|
| 1810 |
render_agent_state(session),
|
| 1811 |
render_director_log(session),
|
| 1812 |
render_trace(session),
|
|
@@ -1835,6 +2264,7 @@ def create_show(
|
|
| 1835 |
None,
|
| 1836 |
EMPTY_STAGE,
|
| 1837 |
"No premise yet. Add a premise to raise the curtain.",
|
|
|
|
| 1838 |
EMPTY_AGENT_STATE,
|
| 1839 |
EMPTY_DIRECTOR_LOG,
|
| 1840 |
EMPTY_TRACE,
|
|
@@ -1874,6 +2304,7 @@ def reset_show():
|
|
| 1874 |
"",
|
| 1875 |
EMPTY_STAGE,
|
| 1876 |
EMPTY_TRANSCRIPT,
|
|
|
|
| 1877 |
EMPTY_AGENT_STATE,
|
| 1878 |
EMPTY_DIRECTOR_LOG,
|
| 1879 |
EMPTY_TRACE,
|
|
@@ -1884,6 +2315,9 @@ def reset_show():
|
|
| 1884 |
DEFAULT_MAX_NEW_TOKENS,
|
| 1885 |
DEFAULT_TEMPERATURE,
|
| 1886 |
True,
|
|
|
|
|
|
|
|
|
|
| 1887 |
EMPTY_BACKEND,
|
| 1888 |
)
|
| 1889 |
|
|
@@ -1900,6 +2334,7 @@ def advance_one_beat(
|
|
| 1900 |
None,
|
| 1901 |
EMPTY_STAGE,
|
| 1902 |
"Create a show before running a beat.",
|
|
|
|
| 1903 |
EMPTY_AGENT_STATE,
|
| 1904 |
EMPTY_DIRECTOR_LOG,
|
| 1905 |
EMPTY_TRACE,
|
|
@@ -1925,6 +2360,7 @@ def advance_full_act(
|
|
| 1925 |
None,
|
| 1926 |
EMPTY_STAGE,
|
| 1927 |
"Create a show before running the full act.",
|
|
|
|
| 1928 |
EMPTY_AGENT_STATE,
|
| 1929 |
EMPTY_DIRECTOR_LOG,
|
| 1930 |
EMPTY_TRACE,
|
|
@@ -1974,7 +2410,7 @@ def advance_full_act(
|
|
| 1974 |
session.director_mode = selected_director_mode
|
| 1975 |
yield session, *render_outputs(session)
|
| 1976 |
if session.beat_index < session.max_beats:
|
| 1977 |
-
sleep(
|
| 1978 |
|
| 1979 |
|
| 1980 |
def throw_audience_prop(
|
|
@@ -1990,6 +2426,7 @@ def throw_audience_prop(
|
|
| 1990 |
None,
|
| 1991 |
EMPTY_STAGE,
|
| 1992 |
"Create a show before throwing a prop.",
|
|
|
|
| 1993 |
EMPTY_AGENT_STATE,
|
| 1994 |
EMPTY_DIRECTOR_LOG,
|
| 1995 |
EMPTY_TRACE,
|
|
@@ -2015,6 +2452,7 @@ def summon_audience_actor(
|
|
| 2015 |
None,
|
| 2016 |
EMPTY_STAGE,
|
| 2017 |
"Create a show before summoning an actor.",
|
|
|
|
| 2018 |
EMPTY_AGENT_STATE,
|
| 2019 |
EMPTY_DIRECTOR_LOG,
|
| 2020 |
EMPTY_TRACE,
|
|
@@ -2039,6 +2477,7 @@ def request_audience_finale(
|
|
| 2039 |
None,
|
| 2040 |
EMPTY_STAGE,
|
| 2041 |
"Create a show before requesting a finale.",
|
|
|
|
| 2042 |
EMPTY_AGENT_STATE,
|
| 2043 |
EMPTY_DIRECTOR_LOG,
|
| 2044 |
EMPTY_TRACE,
|
|
@@ -2133,6 +2572,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2133 |
reset_button = gr.Button("Reset", elem_classes=["reset-action"])
|
| 2134 |
|
| 2135 |
stage_output = gr.HTML(value=EMPTY_STAGE, label="Stage", elem_classes=["stage-output"])
|
|
|
|
| 2136 |
|
| 2137 |
with gr.Column(elem_classes=["backstage-stack"]):
|
| 2138 |
with gr.Group(elem_classes=["control-panel"]):
|
|
@@ -2145,6 +2585,31 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2145 |
)
|
| 2146 |
run_full_button = gr.Button("Run Full Act", elem_classes=["cue-action"])
|
| 2147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2148 |
with gr.Group(elem_classes=["control-panel"]):
|
| 2149 |
gr.Markdown("### Audience", elem_classes=["audience-title", "panel-heading"])
|
| 2150 |
prop_input = gr.Dropdown(
|
|
@@ -2172,11 +2637,9 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2172 |
)
|
| 2173 |
|
| 2174 |
with gr.Accordion("Transcript", open=False, elem_classes=["transcript-section"]):
|
| 2175 |
-
transcript_output = gr.
|
| 2176 |
value=EMPTY_TRANSCRIPT,
|
| 2177 |
label="Transcript",
|
| 2178 |
-
lines=7,
|
| 2179 |
-
interactive=False,
|
| 2180 |
elem_classes=["transcript-box", "no-field-label"],
|
| 2181 |
)
|
| 2182 |
with gr.Accordion("Agent State", open=False):
|
|
@@ -2250,7 +2713,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2250 |
elem_classes=["no-field-label"],
|
| 2251 |
)
|
| 2252 |
|
| 2253 |
-
create_button.click(
|
| 2254 |
create_show,
|
| 2255 |
inputs=[
|
| 2256 |
premise_input,
|
|
@@ -2265,6 +2728,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2265 |
session_state,
|
| 2266 |
stage_output,
|
| 2267 |
transcript_output,
|
|
|
|
| 2268 |
agent_state_output,
|
| 2269 |
director_output,
|
| 2270 |
trace_output,
|
|
@@ -2272,7 +2736,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2272 |
backend_output,
|
| 2273 |
],
|
| 2274 |
)
|
| 2275 |
-
run_one_button.click(
|
| 2276 |
advance_one_beat,
|
| 2277 |
inputs=[
|
| 2278 |
session_state,
|
|
@@ -2285,6 +2749,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2285 |
session_state,
|
| 2286 |
stage_output,
|
| 2287 |
transcript_output,
|
|
|
|
| 2288 |
agent_state_output,
|
| 2289 |
director_output,
|
| 2290 |
trace_output,
|
|
@@ -2292,7 +2757,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2292 |
backend_output,
|
| 2293 |
],
|
| 2294 |
)
|
| 2295 |
-
run_full_button.click(
|
| 2296 |
advance_full_act,
|
| 2297 |
inputs=[
|
| 2298 |
session_state,
|
|
@@ -2306,6 +2771,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2306 |
session_state,
|
| 2307 |
stage_output,
|
| 2308 |
transcript_output,
|
|
|
|
| 2309 |
agent_state_output,
|
| 2310 |
director_output,
|
| 2311 |
trace_output,
|
|
@@ -2313,7 +2779,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2313 |
backend_output,
|
| 2314 |
],
|
| 2315 |
)
|
| 2316 |
-
throw_prop_button.click(
|
| 2317 |
throw_audience_prop,
|
| 2318 |
inputs=[
|
| 2319 |
session_state,
|
|
@@ -2327,6 +2793,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2327 |
session_state,
|
| 2328 |
stage_output,
|
| 2329 |
transcript_output,
|
|
|
|
| 2330 |
agent_state_output,
|
| 2331 |
director_output,
|
| 2332 |
trace_output,
|
|
@@ -2334,7 +2801,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2334 |
backend_output,
|
| 2335 |
],
|
| 2336 |
)
|
| 2337 |
-
summon_actor_button.click(
|
| 2338 |
summon_audience_actor,
|
| 2339 |
inputs=[
|
| 2340 |
session_state,
|
|
@@ -2348,6 +2815,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2348 |
session_state,
|
| 2349 |
stage_output,
|
| 2350 |
transcript_output,
|
|
|
|
| 2351 |
agent_state_output,
|
| 2352 |
director_output,
|
| 2353 |
trace_output,
|
|
@@ -2355,7 +2823,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2355 |
backend_output,
|
| 2356 |
],
|
| 2357 |
)
|
| 2358 |
-
request_finale_button.click(
|
| 2359 |
request_audience_finale,
|
| 2360 |
inputs=[
|
| 2361 |
session_state,
|
|
@@ -2368,6 +2836,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2368 |
session_state,
|
| 2369 |
stage_output,
|
| 2370 |
transcript_output,
|
|
|
|
| 2371 |
agent_state_output,
|
| 2372 |
director_output,
|
| 2373 |
trace_output,
|
|
@@ -2389,6 +2858,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2389 |
actor_input,
|
| 2390 |
stage_output,
|
| 2391 |
transcript_output,
|
|
|
|
| 2392 |
agent_state_output,
|
| 2393 |
director_output,
|
| 2394 |
trace_output,
|
|
@@ -2399,9 +2869,163 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 2399 |
max_new_tokens_input,
|
| 2400 |
temperature_input,
|
| 2401 |
deterministic_full_act_input,
|
|
|
|
|
|
|
|
|
|
| 2402 |
backend_output,
|
| 2403 |
],
|
| 2404 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2405 |
|
| 2406 |
|
| 2407 |
if __name__ == "__main__":
|
|
|
|
| 1 |
from html import escape
|
| 2 |
+
import json
|
| 3 |
import os
|
| 4 |
from time import sleep
|
| 5 |
|
|
|
|
| 44 |
</div>
|
| 45 |
"""
|
| 46 |
|
| 47 |
+
EMPTY_TRANSCRIPT = '<div class="conversation-empty">No show yet. The transcript will appear here.</div>'
|
| 48 |
EMPTY_AGENT_STATE = "<div class=\"agent-state-empty\">No agents on stage yet.</div>"
|
| 49 |
EMPTY_DIRECTOR_LOG = "No director notes yet."
|
| 50 |
EMPTY_TRACE = "No trace events yet."
|
|
|
|
| 81 |
HF_API_MODEL_ID = os.getenv("HF_API_MODEL_ID", DEFAULT_HF_API_MODEL_ID)
|
| 82 |
DEFAULT_MAX_NEW_TOKENS = 120
|
| 83 |
DEFAULT_TEMPERATURE = 0.75
|
| 84 |
+
MIN_PLAYBACK_DELAY_SECONDS = 0.55
|
| 85 |
+
MAX_PLAYBACK_DELAY_SECONDS = 1.85
|
| 86 |
+
VOICE_MODE_CHOICES = [
|
| 87 |
+
("Off", "off"),
|
| 88 |
+
("Browser TTS: Character Voices", "character"),
|
| 89 |
+
("Browser TTS: Narrator Only", "narrator"),
|
| 90 |
+
]
|
| 91 |
+
DEFAULT_VOICE_MODE = "off"
|
| 92 |
+
EMPTY_TTS_PAYLOAD = "{}"
|
| 93 |
+
EMPTY_TTS_STATUS = "Voice mode is off."
|
| 94 |
SHOW_LENGTH_CHOICES = [
|
| 95 |
("Short", "short"),
|
| 96 |
("Standard", "standard"),
|
|
|
|
| 120 |
"scroll",
|
| 121 |
]
|
| 122 |
|
| 123 |
+
VOICE_STYLE_PRESETS = [
|
| 124 |
+
{
|
| 125 |
+
"label": "Warm Puppet",
|
| 126 |
+
"pitch": 1.04,
|
| 127 |
+
"rate": 0.96,
|
| 128 |
+
"volume": 0.9,
|
| 129 |
+
"voiceNameHints": ["Google US English", "Samantha", "Jenny", "Aria"],
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"label": "Tiny Mischief",
|
| 133 |
+
"pitch": 1.14,
|
| 134 |
+
"rate": 0.98,
|
| 135 |
+
"volume": 0.88,
|
| 136 |
+
"voiceNameHints": ["Google UK English Female", "Samantha", "Jenny", "Aria"],
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"label": "Grumpy Elder",
|
| 140 |
+
"pitch": 0.92,
|
| 141 |
+
"rate": 0.9,
|
| 142 |
+
"volume": 0.9,
|
| 143 |
+
"voiceNameHints": ["Daniel", "Alex", "Google UK English Male", "Microsoft Guy"],
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"label": "Bright Hero",
|
| 147 |
+
"pitch": 1.02,
|
| 148 |
+
"rate": 1.02,
|
| 149 |
+
"volume": 0.9,
|
| 150 |
+
"voiceNameHints": ["Google US English", "Microsoft David", "Microsoft Mark", "Alex"],
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"label": "Soft Oracle",
|
| 154 |
+
"pitch": 0.98,
|
| 155 |
+
"rate": 0.86,
|
| 156 |
+
"volume": 0.82,
|
| 157 |
+
"voiceNameHints": ["Google UK English Female", "Microsoft Zira", "Samantha", "Aria"],
|
| 158 |
+
},
|
| 159 |
+
]
|
| 160 |
+
NARRATOR_VOICE_STYLE = {
|
| 161 |
+
"label": "Narrator",
|
| 162 |
+
"pitch": 0.96,
|
| 163 |
+
"rate": 0.88,
|
| 164 |
+
"volume": 0.82,
|
| 165 |
+
"voiceNameHints": ["Daniel", "Alex", "Google UK English Male", "Microsoft Guy", "Google US English"],
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
|
| 169 |
def _prop_dropdown_label(canonical: str) -> str:
|
| 170 |
emoji = PROP_EMOJI.get(canonical.lower(), "🎁")
|
|
|
|
| 176 |
(_prop_dropdown_label(name), name) for name in DEFAULT_STAGE_PROPS
|
| 177 |
]
|
| 178 |
|
| 179 |
+
|
| 180 |
+
def voice_style_for_actor(session: TheaterSession, speaker_name: str) -> dict[str, object]:
|
| 181 |
+
for index, actor in enumerate(session.actors):
|
| 182 |
+
if actor.name == speaker_name:
|
| 183 |
+
return dict(VOICE_STYLE_PRESETS[index % len(VOICE_STYLE_PRESETS)])
|
| 184 |
+
return dict(NARRATOR_VOICE_STYLE)
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
def latest_tts_payload(session: TheaterSession | None) -> str:
|
| 188 |
+
if session is None or not session.transcript:
|
| 189 |
+
return EMPTY_TTS_PAYLOAD
|
| 190 |
+
|
| 191 |
+
latest_beat = session.transcript[-1]
|
| 192 |
+
latest_tool = session.latest_tool_result
|
| 193 |
+
if latest_tool is not None and latest_beat.tool_request is not None and latest_tool.actor_name == latest_beat.speaker:
|
| 194 |
+
payload = {
|
| 195 |
+
"id": f"{session.session_id}:{len(session.transcript)}:tool:{latest_tool.tool_name}",
|
| 196 |
+
"kind": "tool",
|
| 197 |
+
"speaker": "Stage Oracle",
|
| 198 |
+
"text": latest_tool.result,
|
| 199 |
+
"style": NARRATOR_VOICE_STYLE,
|
| 200 |
+
"narratorStyle": NARRATOR_VOICE_STYLE,
|
| 201 |
+
}
|
| 202 |
+
else:
|
| 203 |
+
payload = {
|
| 204 |
+
"id": f"{session.session_id}:{len(session.transcript)}:actor:{latest_beat.speaker}",
|
| 205 |
+
"kind": "actor",
|
| 206 |
+
"speaker": latest_beat.speaker,
|
| 207 |
+
"text": latest_beat.line,
|
| 208 |
+
"style": voice_style_for_actor(session, latest_beat.speaker),
|
| 209 |
+
"narratorStyle": NARRATOR_VOICE_STYLE,
|
| 210 |
+
}
|
| 211 |
+
return json.dumps(payload, ensure_ascii=True)
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def playback_delay_for_latest_line(session: TheaterSession) -> float:
|
| 215 |
+
if not session.transcript:
|
| 216 |
+
return MIN_PLAYBACK_DELAY_SECONDS
|
| 217 |
+
latest = session.transcript[-1].line
|
| 218 |
+
word_count = len(latest.split())
|
| 219 |
+
delay = 0.65 + (word_count * 0.28)
|
| 220 |
+
return max(MIN_PLAYBACK_DELAY_SECONDS, min(4.25, delay))
|
| 221 |
+
|
| 222 |
CUSTOM_CSS = """
|
| 223 |
body,
|
| 224 |
.gradio-container {
|
|
|
|
| 1119 |
border: none !important;
|
| 1120 |
box-shadow: none !important;
|
| 1121 |
}
|
| 1122 |
+
.conversation-empty {
|
| 1123 |
+
color: #cbb7a1;
|
| 1124 |
+
font-size: 0.88rem;
|
| 1125 |
+
padding: 0.45rem 0.2rem;
|
| 1126 |
+
}
|
| 1127 |
+
.conversation-transcript {
|
| 1128 |
+
display: grid;
|
| 1129 |
+
gap: 0.38rem;
|
| 1130 |
+
padding: 0.15rem 0;
|
| 1131 |
+
}
|
| 1132 |
+
.conversation-turn {
|
| 1133 |
+
max-width: min(42rem, 84%);
|
| 1134 |
+
}
|
| 1135 |
+
.conversation-turn.speaker-changed {
|
| 1136 |
+
margin-top: 0.42rem;
|
| 1137 |
+
}
|
| 1138 |
+
.conversation-turn.same-speaker {
|
| 1139 |
+
margin-top: 0.04rem;
|
| 1140 |
+
}
|
| 1141 |
+
.conversation-turn:nth-child(even) {
|
| 1142 |
+
justify-self: end;
|
| 1143 |
+
}
|
| 1144 |
+
.conversation-speaker {
|
| 1145 |
+
color: #ffd166;
|
| 1146 |
+
font-size: 0.68rem;
|
| 1147 |
+
font-weight: 800;
|
| 1148 |
+
letter-spacing: 0.07em;
|
| 1149 |
+
margin: 0 0 0.12rem 0.18rem;
|
| 1150 |
+
text-transform: uppercase;
|
| 1151 |
+
}
|
| 1152 |
+
.conversation-bubble {
|
| 1153 |
+
background: rgba(10, 12, 23, 0.88);
|
| 1154 |
+
border: 1px solid rgba(246, 196, 83, 0.24);
|
| 1155 |
+
border-radius: 11px;
|
| 1156 |
+
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.24);
|
| 1157 |
+
color: #f8efe4;
|
| 1158 |
+
font-size: 0.9rem;
|
| 1159 |
+
line-height: 1.38;
|
| 1160 |
+
padding: 0.54rem 0.68rem;
|
| 1161 |
+
overflow-wrap: anywhere;
|
| 1162 |
+
}
|
| 1163 |
+
.conversation-turn.latest .conversation-bubble {
|
| 1164 |
+
background: rgba(246, 196, 83, 0.13);
|
| 1165 |
+
border-color: rgba(246, 196, 83, 0.52);
|
| 1166 |
+
box-shadow:
|
| 1167 |
+
0 0 0 1px rgba(246, 196, 83, 0.14),
|
| 1168 |
+
0 10px 24px rgba(0, 0, 0, 0.28);
|
| 1169 |
+
}
|
| 1170 |
+
.voice-status {
|
| 1171 |
+
color: #cbb7a1;
|
| 1172 |
+
font-size: 0.8rem;
|
| 1173 |
+
min-height: 1.2rem;
|
| 1174 |
+
padding: 0.15rem 0.05rem 0;
|
| 1175 |
+
}
|
| 1176 |
+
.voice-status strong {
|
| 1177 |
+
color: #ffd166;
|
| 1178 |
+
}
|
| 1179 |
.agent-state-grid {
|
| 1180 |
display: grid;
|
| 1181 |
gap: 0.55rem;
|
|
|
|
| 1693 |
)
|
| 1694 |
|
| 1695 |
|
| 1696 |
+
BROWSER_TTS_JS = r"""
|
| 1697 |
+
async (mode, payloadJson, previewKind = "", options = {}) => {
|
| 1698 |
+
const root = globalThis;
|
| 1699 |
+
const speech = root.speechSynthesis;
|
| 1700 |
+
root.aiPuppetSpeech = root.aiPuppetSpeech || {
|
| 1701 |
+
lastAutoId: null,
|
| 1702 |
+
voices: [],
|
| 1703 |
+
voicesReady: false,
|
| 1704 |
+
queue: [],
|
| 1705 |
+
speaking: false,
|
| 1706 |
+
};
|
| 1707 |
+
root.aiPuppetSpeech.voices = Array.isArray(root.aiPuppetSpeech.voices) ? root.aiPuppetSpeech.voices : [];
|
| 1708 |
+
root.aiPuppetSpeech.queue = Array.isArray(root.aiPuppetSpeech.queue) ? root.aiPuppetSpeech.queue : [];
|
| 1709 |
+
root.aiPuppetSpeech.voicesReady = Boolean(root.aiPuppetSpeech.voicesReady);
|
| 1710 |
+
root.aiPuppetSpeech.speaking = Boolean(root.aiPuppetSpeech.speaking);
|
| 1711 |
+
const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (char) => ({
|
| 1712 |
+
"&": "&",
|
| 1713 |
+
"<": "<",
|
| 1714 |
+
">": ">",
|
| 1715 |
+
'"': """,
|
| 1716 |
+
"'": "'",
|
| 1717 |
+
})[char]);
|
| 1718 |
+
if (mode === "off") {
|
| 1719 |
+
if (speech) {
|
| 1720 |
+
root.aiPuppetSpeech.queue = [];
|
| 1721 |
+
root.aiPuppetSpeech.speaking = false;
|
| 1722 |
+
speech.cancel();
|
| 1723 |
+
}
|
| 1724 |
+
return '<div class="voice-status">Voice mode is off.</div>';
|
| 1725 |
+
}
|
| 1726 |
+
if (!("speechSynthesis" in root) || typeof SpeechSynthesisUtterance === "undefined") {
|
| 1727 |
+
return '<div class="voice-status"><strong>Browser TTS unavailable.</strong> The show still works without voice.</div>';
|
| 1728 |
+
}
|
| 1729 |
+
|
| 1730 |
+
const cleanSpokenText = (value) => {
|
| 1731 |
+
return String(value || "")
|
| 1732 |
+
.replace(/\[[^\]]{1,140}\]/g, " ")
|
| 1733 |
+
.replace(/\*[^*]{1,140}\*/g, " ")
|
| 1734 |
+
.replace(/\([^)]{1,90}\)/g, " ")
|
| 1735 |
+
.replace(/[\u{1F000}-\u{1FAFF}\u{2600}-\u{27BF}]/gu, " ")
|
| 1736 |
+
.replace(/\s+/g, " ")
|
| 1737 |
+
.trim();
|
| 1738 |
+
};
|
| 1739 |
+
const loadVoices = () => new Promise((resolve) => {
|
| 1740 |
+
const current = speech.getVoices ? speech.getVoices() : [];
|
| 1741 |
+
if (current.length > 0) {
|
| 1742 |
+
root.aiPuppetSpeech.voices = current;
|
| 1743 |
+
root.aiPuppetSpeech.voicesReady = true;
|
| 1744 |
+
resolve(current);
|
| 1745 |
+
return;
|
| 1746 |
+
}
|
| 1747 |
+
let settled = false;
|
| 1748 |
+
const finish = () => {
|
| 1749 |
+
if (settled) return;
|
| 1750 |
+
settled = true;
|
| 1751 |
+
const voices = speech.getVoices ? speech.getVoices() : [];
|
| 1752 |
+
root.aiPuppetSpeech.voices = voices;
|
| 1753 |
+
root.aiPuppetSpeech.voicesReady = voices.length > 0;
|
| 1754 |
+
resolve(voices);
|
| 1755 |
+
};
|
| 1756 |
+
speech.onvoiceschanged = () => finish();
|
| 1757 |
+
setTimeout(finish, 700);
|
| 1758 |
+
});
|
| 1759 |
+
const selectVoice = (voices, style) => {
|
| 1760 |
+
if (!voices || voices.length === 0) return null;
|
| 1761 |
+
const hints = (style.voiceNameHints || []).map((hint) => String(hint).toLowerCase());
|
| 1762 |
+
const byHint = voices.find((candidate) => {
|
| 1763 |
+
const name = String(candidate.name || "").toLowerCase();
|
| 1764 |
+
return hints.some((hint) => name.includes(hint) || hint.includes(name));
|
| 1765 |
+
});
|
| 1766 |
+
if (byHint) return byHint;
|
| 1767 |
+
const english = voices.find((candidate) => {
|
| 1768 |
+
const lang = String(candidate.lang || "").toLowerCase();
|
| 1769 |
+
const name = String(candidate.name || "").toLowerCase();
|
| 1770 |
+
return lang.startsWith("en") || name.includes("english");
|
| 1771 |
+
});
|
| 1772 |
+
if (english) return english;
|
| 1773 |
+
const anyVoice = voices[0] || null;
|
| 1774 |
+
if (anyVoice) return anyVoice;
|
| 1775 |
+
if (Number.isInteger(style.voiceIndex) && voices.length > 0) {
|
| 1776 |
+
return voices[Math.abs(style.voiceIndex) % voices.length] || null;
|
| 1777 |
+
}
|
| 1778 |
+
return null;
|
| 1779 |
+
};
|
| 1780 |
+
const payloadFromJson = () => {
|
| 1781 |
+
try {
|
| 1782 |
+
return JSON.parse(payloadJson || "{}");
|
| 1783 |
+
} catch (_error) {
|
| 1784 |
+
return null;
|
| 1785 |
+
}
|
| 1786 |
+
};
|
| 1787 |
+
const buildTask = (payload) => {
|
| 1788 |
+
const narratorStyle = payload.narratorStyle || payload.style || {};
|
| 1789 |
+
const activeStyle = mode === "narrator" || previewKind === "narrator"
|
| 1790 |
+
? narratorStyle
|
| 1791 |
+
: (payload.style || narratorStyle);
|
| 1792 |
+
let text = payload.text;
|
| 1793 |
+
let speaker = payload.speaker ? String(payload.speaker) : "Latest line";
|
| 1794 |
+
if (previewKind === "actor") {
|
| 1795 |
+
text = `Previewing ${activeStyle.label || "character voice"}. The puppet voice is softer now.`;
|
| 1796 |
+
speaker = activeStyle.label || "Actor voice";
|
| 1797 |
+
} else if (previewKind === "narrator") {
|
| 1798 |
+
text = "Previewing narrator voice. The curtain rises, and the scene begins.";
|
| 1799 |
+
speaker = activeStyle.label || "Narrator";
|
| 1800 |
+
} else if (mode === "narrator") {
|
| 1801 |
+
speaker = "Narrator";
|
| 1802 |
+
}
|
| 1803 |
+
text = cleanSpokenText(text);
|
| 1804 |
+
return { id: payload.id || `${Date.now()}:${speaker}`, text, speaker, style: activeStyle };
|
| 1805 |
+
};
|
| 1806 |
+
const updateStatus = (html) => {
|
| 1807 |
+
const status = document.querySelector(".voice-status");
|
| 1808 |
+
if (status) status.outerHTML = html;
|
| 1809 |
+
};
|
| 1810 |
+
const drainQueue = async () => {
|
| 1811 |
+
if (root.aiPuppetSpeech.speaking || root.aiPuppetSpeech.queue.length === 0) return;
|
| 1812 |
+
const task = root.aiPuppetSpeech.queue.shift();
|
| 1813 |
+
try {
|
| 1814 |
+
const voices = await loadVoices();
|
| 1815 |
+
const utterance = new SpeechSynthesisUtterance(task.text);
|
| 1816 |
+
utterance.pitch = Math.max(0.1, Math.min(2, Number(task.style.pitch ?? 1)));
|
| 1817 |
+
utterance.rate = Math.max(0.1, Math.min(2, Number(task.style.rate ?? 1)));
|
| 1818 |
+
utterance.volume = Math.max(0, Math.min(1, Number(task.style.volume ?? 1)));
|
| 1819 |
+
const voice = selectVoice(voices, task.style);
|
| 1820 |
+
if (voice) utterance.voice = voice;
|
| 1821 |
+
const label = task.style.label ? ` (${escapeHtml(task.style.label)})` : "";
|
| 1822 |
+
const statusHtml = `<div class="voice-status">Speaking: <strong>${escapeHtml(task.speaker)}</strong>${label}</div>`;
|
| 1823 |
+
root.aiPuppetSpeech.speaking = true;
|
| 1824 |
+
updateStatus(statusHtml);
|
| 1825 |
+
utterance.onend = () => {
|
| 1826 |
+
root.aiPuppetSpeech.speaking = false;
|
| 1827 |
+
drainQueue();
|
| 1828 |
+
};
|
| 1829 |
+
utterance.onerror = () => {
|
| 1830 |
+
root.aiPuppetSpeech.speaking = false;
|
| 1831 |
+
drainQueue();
|
| 1832 |
+
};
|
| 1833 |
+
speech.speak(utterance);
|
| 1834 |
+
} catch (error) {
|
| 1835 |
+
root.aiPuppetSpeech.speaking = false;
|
| 1836 |
+
updateStatus('<div class="voice-status"><strong>Browser TTS could not start.</strong> Try Speak Latest Line once, then run the show again.</div>');
|
| 1837 |
+
}
|
| 1838 |
+
};
|
| 1839 |
+
|
| 1840 |
+
const payload = payloadFromJson();
|
| 1841 |
+
if (!payload) {
|
| 1842 |
+
return '<div class="voice-status">No speakable line is ready yet.</div>';
|
| 1843 |
+
}
|
| 1844 |
+
const task = buildTask(payload);
|
| 1845 |
+
if (!task.text) {
|
| 1846 |
+
return '<div class="voice-status">No speakable line is ready yet.</div>';
|
| 1847 |
+
}
|
| 1848 |
+
|
| 1849 |
+
if (!options.queue) {
|
| 1850 |
+
root.aiPuppetSpeech.queue = [];
|
| 1851 |
+
root.aiPuppetSpeech.speaking = false;
|
| 1852 |
+
speech.cancel();
|
| 1853 |
+
}
|
| 1854 |
+
root.aiPuppetSpeech.queue.push(task);
|
| 1855 |
+
drainQueue();
|
| 1856 |
+
const label = task.style.label ? ` (${escapeHtml(task.style.label)})` : "";
|
| 1857 |
+
const prefix = options.queue && root.aiPuppetSpeech.speaking ? "Queued" : "Speaking";
|
| 1858 |
+
return `<div class="voice-status">${prefix}: <strong>${escapeHtml(task.speaker)}</strong>${label}</div>`;
|
| 1859 |
+
}
|
| 1860 |
+
"""
|
| 1861 |
+
|
| 1862 |
+
|
| 1863 |
+
AUTO_BROWSER_TTS_JS = r"""
|
| 1864 |
+
async (mode, autoSpeak, payloadJson) => {
|
| 1865 |
+
const root = globalThis;
|
| 1866 |
+
root.aiPuppetSpeech = root.aiPuppetSpeech || {};
|
| 1867 |
+
root.aiPuppetSpeech.lastAutoId = root.aiPuppetSpeech.lastAutoId || null;
|
| 1868 |
+
root.aiPuppetSpeech.voices = Array.isArray(root.aiPuppetSpeech.voices) ? root.aiPuppetSpeech.voices : [];
|
| 1869 |
+
root.aiPuppetSpeech.queue = Array.isArray(root.aiPuppetSpeech.queue) ? root.aiPuppetSpeech.queue : [];
|
| 1870 |
+
root.aiPuppetSpeech.voicesReady = Boolean(root.aiPuppetSpeech.voicesReady);
|
| 1871 |
+
root.aiPuppetSpeech.speaking = Boolean(root.aiPuppetSpeech.speaking);
|
| 1872 |
+
const speech = root.speechSynthesis;
|
| 1873 |
+
try {
|
| 1874 |
+
if (mode === "off") {
|
| 1875 |
+
if (speech) speech.cancel();
|
| 1876 |
+
return '<div class="voice-status">Voice mode is off.</div>';
|
| 1877 |
+
}
|
| 1878 |
+
if (!autoSpeak) {
|
| 1879 |
+
return '<div class="voice-status">Auto-speak is off. Use Speak Latest Line to replay.</div>';
|
| 1880 |
+
}
|
| 1881 |
+
let payload = {};
|
| 1882 |
+
try {
|
| 1883 |
+
payload = JSON.parse(payloadJson || "{}");
|
| 1884 |
+
} catch (_error) {
|
| 1885 |
+
return '<div class="voice-status">No speakable line is ready yet.</div>';
|
| 1886 |
+
}
|
| 1887 |
+
if (!payload.id || root.aiPuppetSpeech.lastAutoId === payload.id) {
|
| 1888 |
+
return '<div class="voice-status">Browser TTS is ready.</div>';
|
| 1889 |
+
}
|
| 1890 |
+
root.aiPuppetSpeech.lastAutoId = payload.id;
|
| 1891 |
+
const speakLatest = %s;
|
| 1892 |
+
return await speakLatest(mode, payloadJson, "", { queue: true });
|
| 1893 |
+
} catch (error) {
|
| 1894 |
+
const detail = error && error.message ? String(error.message) : String(error || "unknown error");
|
| 1895 |
+
const safeDetail = detail.replace(/[&<>"']/g, (char) => ({
|
| 1896 |
+
"&": "&",
|
| 1897 |
+
"<": "<",
|
| 1898 |
+
">": ">",
|
| 1899 |
+
'"': """,
|
| 1900 |
+
"'": "'",
|
| 1901 |
+
})[char]);
|
| 1902 |
+
return `<div class="voice-status"><strong>Auto-speak could not start.</strong> ${safeDetail}</div>`;
|
| 1903 |
+
}
|
| 1904 |
+
}
|
| 1905 |
+
""" % BROWSER_TTS_JS
|
| 1906 |
+
|
| 1907 |
+
|
| 1908 |
+
REGISTER_BROWSER_TTS_JS = r"""
|
| 1909 |
+
() => {
|
| 1910 |
+
globalThis.aiPuppetSpeakLatest = %s;
|
| 1911 |
+
if ("speechSynthesis" in globalThis && globalThis.speechSynthesis.getVoices) {
|
| 1912 |
+
globalThis.aiPuppetSpeech = globalThis.aiPuppetSpeech || {};
|
| 1913 |
+
globalThis.aiPuppetSpeech.lastAutoId = globalThis.aiPuppetSpeech.lastAutoId || null;
|
| 1914 |
+
globalThis.aiPuppetSpeech.voices = Array.isArray(globalThis.aiPuppetSpeech.voices) ? globalThis.aiPuppetSpeech.voices : [];
|
| 1915 |
+
globalThis.aiPuppetSpeech.queue = Array.isArray(globalThis.aiPuppetSpeech.queue) ? globalThis.aiPuppetSpeech.queue : [];
|
| 1916 |
+
globalThis.aiPuppetSpeech.voicesReady = Boolean(globalThis.aiPuppetSpeech.voicesReady);
|
| 1917 |
+
globalThis.aiPuppetSpeech.speaking = Boolean(globalThis.aiPuppetSpeech.speaking);
|
| 1918 |
+
const load = () => {
|
| 1919 |
+
const voices = globalThis.speechSynthesis.getVoices();
|
| 1920 |
+
globalThis.aiPuppetSpeech.voices = voices;
|
| 1921 |
+
globalThis.aiPuppetSpeech.voicesReady = voices.length > 0;
|
| 1922 |
+
};
|
| 1923 |
+
globalThis.speechSynthesis.onvoiceschanged = load;
|
| 1924 |
+
load();
|
| 1925 |
+
}
|
| 1926 |
+
globalThis.aiPuppetStopSpeaking = () => {
|
| 1927 |
+
if ("speechSynthesis" in globalThis) {
|
| 1928 |
+
globalThis.aiPuppetSpeech = globalThis.aiPuppetSpeech || {};
|
| 1929 |
+
globalThis.aiPuppetSpeech.queue = [];
|
| 1930 |
+
globalThis.aiPuppetSpeech.speaking = false;
|
| 1931 |
+
globalThis.speechSynthesis.cancel();
|
| 1932 |
+
return '<div class="voice-status">Speech stopped.</div>';
|
| 1933 |
+
}
|
| 1934 |
+
return '<div class="voice-status"><strong>Browser TTS unavailable.</strong> The show still works without voice.</div>';
|
| 1935 |
+
};
|
| 1936 |
+
return '<div class="voice-status">Voice mode is off.</div>';
|
| 1937 |
+
}
|
| 1938 |
+
""" % BROWSER_TTS_JS
|
| 1939 |
+
|
| 1940 |
+
|
| 1941 |
+
STOP_SPEAKING_JS = r"""
|
| 1942 |
+
() => {
|
| 1943 |
+
if (globalThis.aiPuppetStopSpeaking) {
|
| 1944 |
+
return globalThis.aiPuppetStopSpeaking();
|
| 1945 |
+
}
|
| 1946 |
+
if ("speechSynthesis" in globalThis) {
|
| 1947 |
+
globalThis.aiPuppetSpeech = globalThis.aiPuppetSpeech || {};
|
| 1948 |
+
globalThis.aiPuppetSpeech.queue = [];
|
| 1949 |
+
globalThis.aiPuppetSpeech.speaking = false;
|
| 1950 |
+
globalThis.speechSynthesis.cancel();
|
| 1951 |
+
return '<div class="voice-status">Speech stopped.</div>';
|
| 1952 |
+
}
|
| 1953 |
+
return '<div class="voice-status"><strong>Browser TTS unavailable.</strong> The show still works without voice.</div>';
|
| 1954 |
+
}
|
| 1955 |
+
"""
|
| 1956 |
+
|
| 1957 |
+
|
| 1958 |
def render_stage(session: TheaterSession | None) -> str:
|
| 1959 |
if session is None:
|
| 1960 |
return EMPTY_STAGE
|
|
|
|
| 2062 |
if session is None:
|
| 2063 |
return EMPTY_TRANSCRIPT
|
| 2064 |
|
| 2065 |
+
if not session.transcript:
|
| 2066 |
+
return '<div class="conversation-empty">No puppet lines yet. Run one beat to start the scene.</div>'
|
| 2067 |
+
|
| 2068 |
+
turns: list[str] = []
|
| 2069 |
+
previous_speaker: str | None = None
|
| 2070 |
+
latest_index = len(session.transcript)
|
| 2071 |
+
for index, beat in enumerate(session.transcript, start=1):
|
| 2072 |
+
speaker_changed = previous_speaker is not None and previous_speaker != beat.speaker
|
| 2073 |
+
gap_class = "speaker-changed" if speaker_changed else "same-speaker"
|
| 2074 |
+
latest_class = " latest" if index == latest_index else ""
|
| 2075 |
+
turns.append(
|
| 2076 |
+
f"""
|
| 2077 |
+
<div class="conversation-turn {gap_class}{latest_class}">
|
| 2078 |
+
<div class="conversation-speaker">{escape(beat.speaker)}</div>
|
| 2079 |
+
<div class="conversation-bubble">{escape(beat.line)}</div>
|
| 2080 |
+
</div>
|
| 2081 |
+
"""
|
| 2082 |
+
)
|
| 2083 |
+
previous_speaker = beat.speaker
|
| 2084 |
|
| 2085 |
+
return f'<div class="conversation-transcript">{"".join(turns)}</div>'
|
| 2086 |
|
| 2087 |
|
| 2088 |
def render_agent_state(session: TheaterSession | None) -> str:
|
|
|
|
| 2235 |
return (
|
| 2236 |
render_stage(session),
|
| 2237 |
render_transcript(session),
|
| 2238 |
+
latest_tts_payload(session),
|
| 2239 |
render_agent_state(session),
|
| 2240 |
render_director_log(session),
|
| 2241 |
render_trace(session),
|
|
|
|
| 2264 |
None,
|
| 2265 |
EMPTY_STAGE,
|
| 2266 |
"No premise yet. Add a premise to raise the curtain.",
|
| 2267 |
+
EMPTY_TTS_PAYLOAD,
|
| 2268 |
EMPTY_AGENT_STATE,
|
| 2269 |
EMPTY_DIRECTOR_LOG,
|
| 2270 |
EMPTY_TRACE,
|
|
|
|
| 2304 |
"",
|
| 2305 |
EMPTY_STAGE,
|
| 2306 |
EMPTY_TRANSCRIPT,
|
| 2307 |
+
EMPTY_TTS_PAYLOAD,
|
| 2308 |
EMPTY_AGENT_STATE,
|
| 2309 |
EMPTY_DIRECTOR_LOG,
|
| 2310 |
EMPTY_TRACE,
|
|
|
|
| 2315 |
DEFAULT_MAX_NEW_TOKENS,
|
| 2316 |
DEFAULT_TEMPERATURE,
|
| 2317 |
True,
|
| 2318 |
+
DEFAULT_VOICE_MODE,
|
| 2319 |
+
True,
|
| 2320 |
+
EMPTY_TTS_STATUS,
|
| 2321 |
EMPTY_BACKEND,
|
| 2322 |
)
|
| 2323 |
|
|
|
|
| 2334 |
None,
|
| 2335 |
EMPTY_STAGE,
|
| 2336 |
"Create a show before running a beat.",
|
| 2337 |
+
EMPTY_TTS_PAYLOAD,
|
| 2338 |
EMPTY_AGENT_STATE,
|
| 2339 |
EMPTY_DIRECTOR_LOG,
|
| 2340 |
EMPTY_TRACE,
|
|
|
|
| 2360 |
None,
|
| 2361 |
EMPTY_STAGE,
|
| 2362 |
"Create a show before running the full act.",
|
| 2363 |
+
EMPTY_TTS_PAYLOAD,
|
| 2364 |
EMPTY_AGENT_STATE,
|
| 2365 |
EMPTY_DIRECTOR_LOG,
|
| 2366 |
EMPTY_TRACE,
|
|
|
|
| 2410 |
session.director_mode = selected_director_mode
|
| 2411 |
yield session, *render_outputs(session)
|
| 2412 |
if session.beat_index < session.max_beats:
|
| 2413 |
+
sleep(playback_delay_for_latest_line(session))
|
| 2414 |
|
| 2415 |
|
| 2416 |
def throw_audience_prop(
|
|
|
|
| 2426 |
None,
|
| 2427 |
EMPTY_STAGE,
|
| 2428 |
"Create a show before throwing a prop.",
|
| 2429 |
+
EMPTY_TTS_PAYLOAD,
|
| 2430 |
EMPTY_AGENT_STATE,
|
| 2431 |
EMPTY_DIRECTOR_LOG,
|
| 2432 |
EMPTY_TRACE,
|
|
|
|
| 2452 |
None,
|
| 2453 |
EMPTY_STAGE,
|
| 2454 |
"Create a show before summoning an actor.",
|
| 2455 |
+
EMPTY_TTS_PAYLOAD,
|
| 2456 |
EMPTY_AGENT_STATE,
|
| 2457 |
EMPTY_DIRECTOR_LOG,
|
| 2458 |
EMPTY_TRACE,
|
|
|
|
| 2477 |
None,
|
| 2478 |
EMPTY_STAGE,
|
| 2479 |
"Create a show before requesting a finale.",
|
| 2480 |
+
EMPTY_TTS_PAYLOAD,
|
| 2481 |
EMPTY_AGENT_STATE,
|
| 2482 |
EMPTY_DIRECTOR_LOG,
|
| 2483 |
EMPTY_TRACE,
|
|
|
|
| 2572 |
reset_button = gr.Button("Reset", elem_classes=["reset-action"])
|
| 2573 |
|
| 2574 |
stage_output = gr.HTML(value=EMPTY_STAGE, label="Stage", elem_classes=["stage-output"])
|
| 2575 |
+
latest_tts_payload_output = gr.Textbox(value=EMPTY_TTS_PAYLOAD, visible=False)
|
| 2576 |
|
| 2577 |
with gr.Column(elem_classes=["backstage-stack"]):
|
| 2578 |
with gr.Group(elem_classes=["control-panel"]):
|
|
|
|
| 2585 |
)
|
| 2586 |
run_full_button = gr.Button("Run Full Act", elem_classes=["cue-action"])
|
| 2587 |
|
| 2588 |
+
with gr.Group(elem_classes=["control-panel"]):
|
| 2589 |
+
gr.Markdown("### Voice", elem_classes=["show-controls-title", "panel-heading"])
|
| 2590 |
+
voice_mode_select = gr.Radio(
|
| 2591 |
+
choices=VOICE_MODE_CHOICES,
|
| 2592 |
+
value=DEFAULT_VOICE_MODE,
|
| 2593 |
+
label="Voice Mode",
|
| 2594 |
+
interactive=True,
|
| 2595 |
+
)
|
| 2596 |
+
auto_speak_checkbox = gr.Checkbox(
|
| 2597 |
+
value=True,
|
| 2598 |
+
label="Auto-speak latest line",
|
| 2599 |
+
interactive=True,
|
| 2600 |
+
)
|
| 2601 |
+
with gr.Row():
|
| 2602 |
+
speak_latest_button = gr.Button("Speak Latest Line", elem_classes=["cue-action"])
|
| 2603 |
+
stop_speaking_button = gr.Button("Stop Speaking", elem_classes=["reset-action"])
|
| 2604 |
+
with gr.Row():
|
| 2605 |
+
preview_actor_voice_button = gr.Button("Preview Actor Voice", elem_classes=["cue-action"])
|
| 2606 |
+
preview_narrator_voice_button = gr.Button("Preview Narrator Voice", elem_classes=["cue-action"])
|
| 2607 |
+
tts_status_output = gr.HTML(
|
| 2608 |
+
value=f'<div class="voice-status">{EMPTY_TTS_STATUS}</div>',
|
| 2609 |
+
label="Voice Status",
|
| 2610 |
+
elem_classes=["no-field-label"],
|
| 2611 |
+
)
|
| 2612 |
+
|
| 2613 |
with gr.Group(elem_classes=["control-panel"]):
|
| 2614 |
gr.Markdown("### Audience", elem_classes=["audience-title", "panel-heading"])
|
| 2615 |
prop_input = gr.Dropdown(
|
|
|
|
| 2637 |
)
|
| 2638 |
|
| 2639 |
with gr.Accordion("Transcript", open=False, elem_classes=["transcript-section"]):
|
| 2640 |
+
transcript_output = gr.HTML(
|
| 2641 |
value=EMPTY_TRANSCRIPT,
|
| 2642 |
label="Transcript",
|
|
|
|
|
|
|
| 2643 |
elem_classes=["transcript-box", "no-field-label"],
|
| 2644 |
)
|
| 2645 |
with gr.Accordion("Agent State", open=False):
|
|
|
|
| 2713 |
elem_classes=["no-field-label"],
|
| 2714 |
)
|
| 2715 |
|
| 2716 |
+
create_event = create_button.click(
|
| 2717 |
create_show,
|
| 2718 |
inputs=[
|
| 2719 |
premise_input,
|
|
|
|
| 2728 |
session_state,
|
| 2729 |
stage_output,
|
| 2730 |
transcript_output,
|
| 2731 |
+
latest_tts_payload_output,
|
| 2732 |
agent_state_output,
|
| 2733 |
director_output,
|
| 2734 |
trace_output,
|
|
|
|
| 2736 |
backend_output,
|
| 2737 |
],
|
| 2738 |
)
|
| 2739 |
+
run_one_event = run_one_button.click(
|
| 2740 |
advance_one_beat,
|
| 2741 |
inputs=[
|
| 2742 |
session_state,
|
|
|
|
| 2749 |
session_state,
|
| 2750 |
stage_output,
|
| 2751 |
transcript_output,
|
| 2752 |
+
latest_tts_payload_output,
|
| 2753 |
agent_state_output,
|
| 2754 |
director_output,
|
| 2755 |
trace_output,
|
|
|
|
| 2757 |
backend_output,
|
| 2758 |
],
|
| 2759 |
)
|
| 2760 |
+
run_full_event = run_full_button.click(
|
| 2761 |
advance_full_act,
|
| 2762 |
inputs=[
|
| 2763 |
session_state,
|
|
|
|
| 2771 |
session_state,
|
| 2772 |
stage_output,
|
| 2773 |
transcript_output,
|
| 2774 |
+
latest_tts_payload_output,
|
| 2775 |
agent_state_output,
|
| 2776 |
director_output,
|
| 2777 |
trace_output,
|
|
|
|
| 2779 |
backend_output,
|
| 2780 |
],
|
| 2781 |
)
|
| 2782 |
+
throw_prop_event = throw_prop_button.click(
|
| 2783 |
throw_audience_prop,
|
| 2784 |
inputs=[
|
| 2785 |
session_state,
|
|
|
|
| 2793 |
session_state,
|
| 2794 |
stage_output,
|
| 2795 |
transcript_output,
|
| 2796 |
+
latest_tts_payload_output,
|
| 2797 |
agent_state_output,
|
| 2798 |
director_output,
|
| 2799 |
trace_output,
|
|
|
|
| 2801 |
backend_output,
|
| 2802 |
],
|
| 2803 |
)
|
| 2804 |
+
summon_actor_event = summon_actor_button.click(
|
| 2805 |
summon_audience_actor,
|
| 2806 |
inputs=[
|
| 2807 |
session_state,
|
|
|
|
| 2815 |
session_state,
|
| 2816 |
stage_output,
|
| 2817 |
transcript_output,
|
| 2818 |
+
latest_tts_payload_output,
|
| 2819 |
agent_state_output,
|
| 2820 |
director_output,
|
| 2821 |
trace_output,
|
|
|
|
| 2823 |
backend_output,
|
| 2824 |
],
|
| 2825 |
)
|
| 2826 |
+
request_finale_event = request_finale_button.click(
|
| 2827 |
request_audience_finale,
|
| 2828 |
inputs=[
|
| 2829 |
session_state,
|
|
|
|
| 2836 |
session_state,
|
| 2837 |
stage_output,
|
| 2838 |
transcript_output,
|
| 2839 |
+
latest_tts_payload_output,
|
| 2840 |
agent_state_output,
|
| 2841 |
director_output,
|
| 2842 |
trace_output,
|
|
|
|
| 2858 |
actor_input,
|
| 2859 |
stage_output,
|
| 2860 |
transcript_output,
|
| 2861 |
+
latest_tts_payload_output,
|
| 2862 |
agent_state_output,
|
| 2863 |
director_output,
|
| 2864 |
trace_output,
|
|
|
|
| 2869 |
max_new_tokens_input,
|
| 2870 |
temperature_input,
|
| 2871 |
deterministic_full_act_input,
|
| 2872 |
+
voice_mode_select,
|
| 2873 |
+
auto_speak_checkbox,
|
| 2874 |
+
tts_status_output,
|
| 2875 |
backend_output,
|
| 2876 |
],
|
| 2877 |
)
|
| 2878 |
+
app.load(
|
| 2879 |
+
None,
|
| 2880 |
+
outputs=[tts_status_output],
|
| 2881 |
+
js=REGISTER_BROWSER_TTS_JS,
|
| 2882 |
+
)
|
| 2883 |
+
voice_mode_select.change(
|
| 2884 |
+
None,
|
| 2885 |
+
inputs=[voice_mode_select],
|
| 2886 |
+
outputs=[tts_status_output],
|
| 2887 |
+
js=r"""
|
| 2888 |
+
(mode) => {
|
| 2889 |
+
if (mode !== "off") {
|
| 2890 |
+
if (!("speechSynthesis" in globalThis) || typeof SpeechSynthesisUtterance === "undefined") {
|
| 2891 |
+
return '<div class="voice-status"><strong>Browser TTS unavailable.</strong> The show still works without voice.</div>';
|
| 2892 |
+
}
|
| 2893 |
+
if (globalThis.speechSynthesis.getVoices) globalThis.speechSynthesis.getVoices();
|
| 2894 |
+
try {
|
| 2895 |
+
const unlock = new SpeechSynthesisUtterance(" ");
|
| 2896 |
+
unlock.volume = 0;
|
| 2897 |
+
unlock.rate = 1;
|
| 2898 |
+
globalThis.speechSynthesis.cancel();
|
| 2899 |
+
globalThis.speechSynthesis.speak(unlock);
|
| 2900 |
+
} catch (_error) {
|
| 2901 |
+
// Some browsers ignore silent unlock utterances; normal speaking still works.
|
| 2902 |
+
}
|
| 2903 |
+
const status = mode === "narrator"
|
| 2904 |
+
? '<div class="voice-status">Browser TTS narrator-only mode is ready.</div>'
|
| 2905 |
+
: '<div class="voice-status">Browser TTS character voices are ready.</div>';
|
| 2906 |
+
return status;
|
| 2907 |
+
}
|
| 2908 |
+
if ("speechSynthesis" in globalThis) globalThis.speechSynthesis.cancel();
|
| 2909 |
+
return '<div class="voice-status">Voice mode is off.</div>';
|
| 2910 |
+
}
|
| 2911 |
+
""",
|
| 2912 |
+
)
|
| 2913 |
+
auto_speak_checkbox.change(
|
| 2914 |
+
None,
|
| 2915 |
+
inputs=[voice_mode_select, auto_speak_checkbox],
|
| 2916 |
+
outputs=[tts_status_output],
|
| 2917 |
+
js=r"""
|
| 2918 |
+
(mode, autoSpeak) => {
|
| 2919 |
+
if (mode === "off") {
|
| 2920 |
+
return autoSpeak
|
| 2921 |
+
? '<div class="voice-status">Auto-speak is ready, but voice mode is off.</div>'
|
| 2922 |
+
: '<div class="voice-status">Auto-speak is off.</div>';
|
| 2923 |
+
}
|
| 2924 |
+
return autoSpeak
|
| 2925 |
+
? '<div class="voice-status">Auto-speak will read each new line.</div>'
|
| 2926 |
+
: '<div class="voice-status">Auto-speak is off. Use Speak Latest Line manually.</div>';
|
| 2927 |
+
}
|
| 2928 |
+
""",
|
| 2929 |
+
)
|
| 2930 |
+
speak_latest_button.click(
|
| 2931 |
+
None,
|
| 2932 |
+
inputs=[voice_mode_select, latest_tts_payload_output],
|
| 2933 |
+
outputs=[tts_status_output],
|
| 2934 |
+
js=BROWSER_TTS_JS,
|
| 2935 |
+
)
|
| 2936 |
+
preview_actor_voice_button.click(
|
| 2937 |
+
None,
|
| 2938 |
+
inputs=[voice_mode_select, latest_tts_payload_output],
|
| 2939 |
+
outputs=[tts_status_output],
|
| 2940 |
+
js=r"""
|
| 2941 |
+
(mode, payloadJson) => {
|
| 2942 |
+
const activeMode = mode === "off" ? "character" : mode;
|
| 2943 |
+
const payload = payloadJson && payloadJson !== "{}"
|
| 2944 |
+
? payloadJson
|
| 2945 |
+
: JSON.stringify({
|
| 2946 |
+
speaker: "Warm Puppet",
|
| 2947 |
+
text: "Previewing actor voice.",
|
| 2948 |
+
style: {
|
| 2949 |
+
label: "Warm Puppet",
|
| 2950 |
+
pitch: 1.04,
|
| 2951 |
+
rate: 0.96,
|
| 2952 |
+
volume: 0.9,
|
| 2953 |
+
voiceNameHints: ["Google US English", "Samantha", "Jenny", "Aria"],
|
| 2954 |
+
},
|
| 2955 |
+
narratorStyle: {
|
| 2956 |
+
label: "Narrator",
|
| 2957 |
+
pitch: 0.96,
|
| 2958 |
+
rate: 0.88,
|
| 2959 |
+
volume: 0.82,
|
| 2960 |
+
voiceNameHints: ["Daniel", "Alex", "Google UK English Male", "Microsoft Guy", "Google US English"],
|
| 2961 |
+
},
|
| 2962 |
+
});
|
| 2963 |
+
const speakLatest = %s;
|
| 2964 |
+
return speakLatest(activeMode, payload, "actor");
|
| 2965 |
+
}
|
| 2966 |
+
""" % BROWSER_TTS_JS,
|
| 2967 |
+
)
|
| 2968 |
+
preview_narrator_voice_button.click(
|
| 2969 |
+
None,
|
| 2970 |
+
inputs=[voice_mode_select, latest_tts_payload_output],
|
| 2971 |
+
outputs=[tts_status_output],
|
| 2972 |
+
js=r"""
|
| 2973 |
+
(_mode, payloadJson) => {
|
| 2974 |
+
const payload = payloadJson && payloadJson !== "{}"
|
| 2975 |
+
? payloadJson
|
| 2976 |
+
: JSON.stringify({
|
| 2977 |
+
speaker: "Narrator",
|
| 2978 |
+
text: "Previewing narrator voice.",
|
| 2979 |
+
style: {
|
| 2980 |
+
label: "Warm Puppet",
|
| 2981 |
+
pitch: 1.04,
|
| 2982 |
+
rate: 0.96,
|
| 2983 |
+
volume: 0.9,
|
| 2984 |
+
voiceNameHints: ["Google US English", "Samantha", "Jenny", "Aria"],
|
| 2985 |
+
},
|
| 2986 |
+
narratorStyle: {
|
| 2987 |
+
label: "Narrator",
|
| 2988 |
+
pitch: 0.96,
|
| 2989 |
+
rate: 0.88,
|
| 2990 |
+
volume: 0.82,
|
| 2991 |
+
voiceNameHints: ["Daniel", "Alex", "Google UK English Male", "Microsoft Guy", "Google US English"],
|
| 2992 |
+
},
|
| 2993 |
+
});
|
| 2994 |
+
const speakLatest = %s;
|
| 2995 |
+
return speakLatest("narrator", payload, "narrator");
|
| 2996 |
+
}
|
| 2997 |
+
""" % BROWSER_TTS_JS,
|
| 2998 |
+
)
|
| 2999 |
+
stop_speaking_button.click(
|
| 3000 |
+
None,
|
| 3001 |
+
outputs=[tts_status_output],
|
| 3002 |
+
js=STOP_SPEAKING_JS,
|
| 3003 |
+
)
|
| 3004 |
+
reset_button.click(
|
| 3005 |
+
None,
|
| 3006 |
+
outputs=[tts_status_output],
|
| 3007 |
+
js=STOP_SPEAKING_JS,
|
| 3008 |
+
)
|
| 3009 |
+
latest_tts_payload_output.change(
|
| 3010 |
+
None,
|
| 3011 |
+
inputs=[voice_mode_select, auto_speak_checkbox, latest_tts_payload_output],
|
| 3012 |
+
outputs=[tts_status_output],
|
| 3013 |
+
js=AUTO_BROWSER_TTS_JS,
|
| 3014 |
+
)
|
| 3015 |
+
for event in [
|
| 3016 |
+
create_event,
|
| 3017 |
+
run_one_event,
|
| 3018 |
+
run_full_event,
|
| 3019 |
+
throw_prop_event,
|
| 3020 |
+
summon_actor_event,
|
| 3021 |
+
request_finale_event,
|
| 3022 |
+
]:
|
| 3023 |
+
event.then(
|
| 3024 |
+
None,
|
| 3025 |
+
inputs=[voice_mode_select, auto_speak_checkbox, latest_tts_payload_output],
|
| 3026 |
+
outputs=[tts_status_output],
|
| 3027 |
+
js=AUTO_BROWSER_TTS_JS,
|
| 3028 |
+
)
|
| 3029 |
|
| 3030 |
|
| 3031 |
if __name__ == "__main__":
|