ShubhamSetia commited on
Commit
c7437ae
·
1 Parent(s): 8855e7f

feat: add browser tts and conversation pacing

Browse files
Files changed (1) hide show
  1. app.py +645 -21
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
- PLAYBACK_DELAY_SECONDS = 0.75
 
 
 
 
 
 
 
 
 
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
- transcript_lines = [
1649
- "Transcript:",
1650
- "No puppet lines yet. The first beat will be added in the next milestone.",
1651
- ]
1652
- if session.transcript:
1653
- transcript_lines = ["Transcript:"]
1654
- for index, beat in enumerate(session.transcript, start=1):
1655
- transcript_lines.append(f"{index}. {beat.speaker}: {beat.line}")
 
 
 
 
 
 
 
 
 
 
 
1656
 
1657
- return "\n".join(transcript_lines)
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(PLAYBACK_DELAY_SECONDS)
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.Textbox(
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
+ "&": "&amp;",
1713
+ "<": "&lt;",
1714
+ ">": "&gt;",
1715
+ '"': "&quot;",
1716
+ "'": "&#39;",
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
+ "&": "&amp;",
1897
+ "<": "&lt;",
1898
+ ">": "&gt;",
1899
+ '"': "&quot;",
1900
+ "'": "&#39;",
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__":