Files changed (4) hide show
  1. AGENTS.md +8 -0
  2. app.py +810 -141
  3. puppet_theater/models.py +2 -0
  4. puppet_theater/session.py +1 -0
AGENTS.md CHANGED
@@ -84,6 +84,14 @@ Run the app locally:
84
  uv run python app.py
85
  ```
86
 
 
 
 
 
 
 
 
 
87
  Check syntax:
88
 
89
  ```bash
 
84
  uv run python app.py
85
  ```
86
 
87
+ For **live reload** while you edit `app.py` (including the `CUSTOM_CSS` string), use the Gradio CLI instead of `python` directly. It watches the project directory and restarts the server when the file changes:
88
+
89
+ ```bash
90
+ uv run gradio app.py
91
+ ```
92
+
93
+ The Blocks instance is named `app`; Gradio discovers it automatically. If you ever need to pin the name explicitly, use `uv run gradio app.py --demo-name app`.
94
+
95
  Check syntax:
96
 
97
  ```bash
app.py CHANGED
@@ -21,8 +21,16 @@ EMPTY_STAGE = """
21
  <div class="puppet-stage stage-empty">
22
  <div class="stage-valance"></div>
23
  <div class="stage-backdrop">
24
- <div class="stage-marquee">AI Puppet Theater</div>
25
- <div class="empty-stage-copy">Enter a premise and raise the curtain.</div>
 
 
 
 
 
 
 
 
26
  </div>
27
  <div class="stage-floorboards"></div>
28
  </div>
@@ -56,6 +64,26 @@ PROP_EMOJI = {
56
  "mirror": "🪞",
57
  }
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  CUSTOM_CSS = """
60
  body,
61
  .gradio-container {
@@ -65,10 +93,63 @@ body,
65
  color: #f8efe4 !important;
66
  }
67
  .gradio-container {
68
- max-width: 1180px !important;
69
- padding-top: 1rem !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
71
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  .gradio-container .prose,
73
  .gradio-container label,
74
  .gradio-container span,
@@ -104,12 +185,20 @@ body,
104
  .gradio-container .block-info,
105
  .gradio-container .label-wrap span,
106
  .gradio-container label > span {
107
- background: rgba(34, 17, 31, 0.88) !important;
108
- border: 1px solid rgba(246, 196, 83, 0.28) !important;
109
  border-radius: 6px !important;
110
  color: #ffd166 !important;
111
  font-weight: 700 !important;
112
  }
 
 
 
 
 
 
 
 
 
 
113
  .gradio-container .wrap,
114
  .gradio-container .styler,
115
  .gradio-container .form,
@@ -125,17 +214,20 @@ body,
125
  color: #f8efe4 !important;
126
  }
127
  .app-title h1 {
128
- color: #f8efe4;
129
- font-family: Georgia, "Times New Roman", serif;
130
  font-size: 2.15rem;
131
- letter-spacing: 0;
 
132
  margin-bottom: 0;
133
  text-align: center;
 
134
  }
135
  .app-title p {
136
- color: #cbb7a1;
137
  font-size: 0.95rem;
138
- margin: 0.15rem 0 0.8rem;
 
139
  text-align: center;
140
  }
141
  .gradio-container h3,
@@ -145,40 +237,246 @@ body,
145
  color: #f8efe4 !important;
146
  }
147
  .premise-panel {
148
- background: rgba(42, 20, 38, 0.72);
149
- border-color: rgba(246, 196, 83, 0.3);
150
- box-shadow: 0 16px 32px rgba(0, 0, 0, 0.2);
151
- padding: 0.55rem 0.65rem 0.65rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  }
153
- .premise-panel .block,
154
  .premise-panel .wrap,
155
  .premise-panel .styler,
156
  .premise-panel .form,
157
  .premise-panel .block > div {
158
- background: rgba(42, 20, 38, 0.78) !important;
159
  }
160
  .control-panel {
161
- background: rgba(34, 17, 31, 0.76);
162
- border: 1px solid rgba(246, 196, 83, 0.22);
163
- border-radius: 8px;
164
- box-shadow: 0 14px 34px rgba(0, 0, 0, 0.22);
165
- padding: 0.55rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  }
167
  .control-panel .block,
168
  .control-panel .wrap,
169
  .control-panel .styler,
170
  .control-panel .form,
171
  .control-panel .block > div {
172
- background: rgba(34, 17, 31, 0.78) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  }
174
  .control-panel .row,
175
- .premise-panel .row {
 
176
  background: transparent !important;
177
  }
178
  .control-panel h3 {
179
- color: #f8efe4;
180
- margin: 0 0 0.35rem;
181
- font-size: 1rem;
 
 
 
 
 
 
182
  }
183
  .control-panel .prose,
184
  .control-panel .prose h3,
@@ -186,17 +484,20 @@ body,
186
  color: #f8efe4 !important;
187
  }
188
  .puppet-stage {
189
- min-height: 430px;
190
  border: 5px solid #3b0a16;
191
  border-radius: 14px;
192
  background:
193
  linear-gradient(90deg, rgba(59, 10, 22, 0.98) 0 10%, transparent 10% 90%, rgba(59, 10, 22, 0.98) 90% 100%),
194
  linear-gradient(180deg, rgba(42, 20, 38, 0.96), rgba(13, 6, 14, 0.98));
 
195
  color: #f8efe4;
196
  display: flex;
197
  flex-direction: column;
198
  align-items: stretch;
199
  justify-content: stretch;
 
 
 
200
  position: relative;
201
  overflow: hidden;
202
  box-shadow:
@@ -233,12 +534,76 @@ body,
233
  position: relative;
234
  z-index: 3;
235
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  .stage-backdrop {
237
  background:
238
  radial-gradient(circle at 50% 8%, rgba(255, 224, 150, 0.28), transparent 19rem),
239
  radial-gradient(circle at 24% 58%, rgba(255, 224, 150, 0.12), transparent 14rem),
240
  linear-gradient(180deg, #2a1426 0%, #22111f 62%, #130911 100%);
241
- flex: 1;
 
 
 
 
242
  padding: 0.72rem 7.2rem 0.8rem;
243
  position: relative;
244
  z-index: 1;
@@ -277,13 +642,63 @@ body,
277
  .stage-copy strong {
278
  color: #f8efe4;
279
  }
280
- .empty-stage-copy {
281
- color: #cbb7a1;
282
- font-size: 1rem;
283
- margin-top: 5.8rem;
 
 
 
 
 
 
 
284
  text-align: center;
285
- position: relative;
286
- z-index: 2;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  }
288
  .stage-floorboards {
289
  height: 58px;
@@ -296,17 +711,24 @@ body,
296
  }
297
  .speech-bubble {
298
  animation: bubble-in 0.24s ease-out;
299
- background: rgba(18, 10, 18, 0.82);
300
  border: 1px solid rgba(246, 196, 83, 0.5);
301
  border-radius: 16px;
302
  box-shadow: 0 18px 30px rgba(0, 0, 0, 0.34);
 
303
  color: #f8efe4;
304
- margin: 0.55rem auto 0;
305
- max-width: 46rem;
 
 
 
306
  padding: 0.72rem 0.95rem;
307
- position: relative;
 
 
308
  text-align: center;
309
- z-index: 4;
 
310
  }
311
  .speech-bubble::after {
312
  border-left: 10px solid transparent;
@@ -330,15 +752,28 @@ body,
330
  color: #f8efe4;
331
  font-size: 0.96rem;
332
  line-height: 1.35;
 
333
  }
334
  .actor-row {
335
- display: grid;
336
- grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
 
 
337
  gap: 0.55rem;
338
- margin-top: 0.72rem;
 
339
  position: relative;
340
  z-index: 3;
341
  }
 
 
 
 
 
 
 
 
 
342
  .actor-card {
343
  background: rgba(70, 38, 36, 0.72);
344
  border: 1px solid rgba(246, 196, 83, 0.45);
@@ -349,6 +784,7 @@ body,
349
  position: relative;
350
  transform-origin: bottom center;
351
  text-align: center;
 
352
  }
353
  .actor-card::after {
354
  background: #7c3f17;
@@ -461,31 +897,157 @@ body,
461
  margin: 0.2rem;
462
  padding: 0.22rem 0.55rem;
463
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  .gradio-container button.primary,
465
  .gradio-container button.primary-action,
466
  .gradio-container button.run-one-action {
467
- background: #f97316 !important;
468
- border-color: #f97316 !important;
469
- box-shadow: 0 10px 24px rgba(249, 115, 22, 0.25) !important;
470
- color: #fff7ed !important;
 
 
 
 
 
 
 
 
 
 
 
471
  }
472
  .gradio-container button.secondary,
473
- .gradio-container button.secondary-action,
474
  .gradio-container button.audience-action-button {
475
- background: #3f3148 !important;
476
- border-color: rgba(246, 196, 83, 0.22) !important;
477
- color: #f8efe4 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
478
  }
479
  .gradio-container button.reset-action {
480
- background: #3b0a16 !important;
481
- border-color: rgba(246, 196, 83, 0.24) !important;
482
- color: #f8efe4 !important;
 
 
 
 
 
 
 
 
 
483
  }
484
- .transcript-box,
485
  .gradio-container .accordion {
486
- background: rgba(13, 6, 14, 0.58) !important;
487
- border-color: rgba(246, 196, 83, 0.18) !important;
 
488
  color: #f8efe4 !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
489
  }
490
  @keyframes puppet-bounce {
491
  from { transform: translateY(0) rotate(-0.4deg); }
@@ -501,7 +1063,9 @@ body,
501
  }
502
  @media (max-width: 760px) {
503
  .puppet-stage {
504
- min-height: 560px;
 
 
505
  }
506
  .puppet-stage::before,
507
  .puppet-stage::after {
@@ -514,7 +1078,11 @@ body,
514
  font-size: 1.2rem;
515
  }
516
  .actor-row {
517
- grid-template-columns: repeat(2, minmax(0, 1fr));
 
 
 
 
518
  }
519
  .actor-card {
520
  min-height: 126px;
@@ -522,15 +1090,22 @@ body,
522
  }
523
 
524
  /* Final Gradio chrome overrides: keep the whole app in the theater palette. */
525
- .gradio-container {
526
- width: min(1200px, calc(100vw - 2rem)) !important;
527
- }
528
- .gradio-container .gr-group {
529
  background: rgba(34, 17, 31, 0.84) !important;
530
  border: 1px solid rgba(246, 196, 83, 0.2) !important;
531
  border-radius: 8px !important;
532
  color: #f8efe4 !important;
533
  }
 
 
 
 
 
 
 
 
 
534
  .gradio-container .gr-group .form,
535
  .gradio-container .gr-group .block,
536
  .gradio-container .gr-group .wrap,
@@ -538,45 +1113,45 @@ body,
538
  .gradio-container .gr-group .secondary-wrap,
539
  .gradio-container .gr-group .input-container,
540
  .gradio-container .gr-group label {
541
- background: transparent !important;
542
  color: #f8efe4 !important;
543
  }
544
  .gradio-container input,
545
  .gradio-container textarea,
546
- .gradio-container select,
547
- .gradio-container .dropdown-container,
548
- .gradio-container .wrap-inner {
549
  background: rgba(10, 12, 23, 0.9) !important;
550
  color: #f8efe4 !important;
551
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  .gradio-container .control-panel input,
553
  .gradio-container .control-panel textarea,
554
- .gradio-container .control-panel .wrap-inner,
555
  .gradio-container .premise-panel textarea {
556
- border: 1px solid rgba(246, 196, 83, 0.24) !important;
557
- }
558
- .gradio-container button {
559
- background: #3f3148 !important;
560
- border: 1px solid rgba(246, 196, 83, 0.24) !important;
561
- color: #f8efe4 !important;
562
- }
563
- .gradio-container button.primary,
564
- .gradio-container button.primary-action,
565
- .gradio-container button.run-one-action {
566
- background: #f97316 !important;
567
- border-color: #f97316 !important;
568
- color: #fff7ed !important;
569
- }
570
- .gradio-container button.reset-action {
571
- background: #3b0a16 !important;
572
- border-color: rgba(246, 196, 83, 0.32) !important;
573
  }
574
  .gradio-container .html-container,
575
  .gradio-container .gradio-style {
576
  width: 100% !important;
577
  }
578
  .puppet-stage {
579
- min-height: 500px;
 
 
580
  width: 100%;
581
  }
582
  .puppet-stage::before,
@@ -591,16 +1166,21 @@ body,
591
  white-space: normal;
592
  }
593
  .speech-bubble {
594
- margin-top: 0.48rem;
595
- max-width: 44rem;
596
  padding: 0.58rem 0.82rem;
597
  }
598
  .actor-row {
599
- align-items: end;
600
- grid-template-columns: repeat(auto-fit, minmax(116px, 1fr));
 
 
601
  gap: 0.62rem;
 
602
  margin-top: 0.82rem;
603
  }
 
 
 
604
  .actor-card {
605
  align-content: start;
606
  background: radial-gradient(circle at 50% 18%, rgba(246, 196, 83, 0.13), rgba(70, 38, 36, 0.72) 58%);
@@ -668,7 +1248,6 @@ body,
668
  padding: 0.75rem 2.45rem;
669
  }
670
  .actor-row {
671
- grid-template-columns: repeat(2, minmax(0, 1fr));
672
  gap: 0.45rem;
673
  }
674
  .actor-card {
@@ -688,16 +1267,29 @@ body,
688
  .app-title p {
689
  margin-bottom: 0.55rem;
690
  }
691
- .premise-panel {
692
- padding: 0.42rem 0.55rem 0.52rem;
693
- }
694
  .stage-output,
695
  .stage-output .html-container,
696
  .stage-output .gradio-style {
697
  margin-bottom: 0 !important;
698
  }
699
  .puppet-stage {
700
- min-height: 390px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
701
  }
702
  .stage-valance {
703
  height: 34px;
@@ -716,8 +1308,7 @@ body,
716
  }
717
  .speech-bubble {
718
  border-radius: 12px;
719
- margin-top: 0.34rem;
720
- max-width: 40rem;
721
  padding: 0.42rem 0.7rem;
722
  }
723
  .speech-speaker {
@@ -725,11 +1316,19 @@ body,
725
  }
726
  .speech-line {
727
  font-size: 0.86rem;
 
728
  }
729
  .actor-row {
730
- grid-template-columns: repeat(auto-fit, minmax(104px, 1fr));
 
 
 
731
  gap: 0.5rem;
732
- margin-top: 0.55rem;
 
 
 
 
733
  }
734
  .actor-card {
735
  border-radius: 14px;
@@ -788,18 +1387,18 @@ body,
788
  }
789
  .control-panel {
790
  margin-top: 0 !important;
791
- padding: 0.42rem;
792
  }
793
  .control-panel h3 {
794
  margin-bottom: 0.2rem;
795
  }
796
  .gradio-container .row {
797
  gap: 0.55rem !important;
 
798
  }
799
- .stage-output + .row,
800
- .stage-output + div,
801
- .control-panel + .control-panel {
802
- margin-top: 0.45rem !important;
803
  }
804
  .transcript-section,
805
  .gradio-container .accordion {
@@ -807,26 +1406,74 @@ body,
807
  }
808
  @media (max-width: 760px) {
809
  .puppet-stage {
810
- min-height: 430px;
 
 
811
  }
812
  .stage-backdrop {
813
  padding: 0.52rem 2.15rem;
814
  }
815
  .actor-row {
816
- grid-template-columns: repeat(2, minmax(0, 1fr));
817
  }
818
  .speech-line {
819
  font-size: 0.8rem;
820
  }
821
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
822
  """
823
 
824
 
 
 
 
 
 
 
 
 
 
825
  def render_stage(session: TheaterSession | None) -> str:
826
  if session is None:
827
  return EMPTY_STAGE
828
 
829
- actor_cards = []
 
 
 
 
830
  latest_beat = session.transcript[-1] if session.transcript else None
831
  latest_speaker = latest_beat.speaker if latest_beat else None
832
  for actor in session.actors:
@@ -835,25 +1482,28 @@ def render_stage(session: TheaterSession | None) -> str:
835
  role_line = actor.goal.split(".", maxsplit=1)[0]
836
  held_prop = actor.held_prop or "nothing"
837
  held_emoji = PROP_EMOJI.get(held_prop.lower(), "🎁") if actor.held_prop else ""
838
- actor_cards.append(
 
 
 
 
 
 
 
 
839
  f"""
840
- <div class="actor-card{active_class}">
841
- <div class="actor-avatar">{escape(actor.avatar)}</div>
842
- <div class="actor-name">{escape(actor.name)}</div>
843
- {active_label}
844
- <div class="actor-detail">{escape(role_line)}</div>
845
- <div class="held-prop"><span>Holding: {escape((held_emoji + " ") if held_emoji else "")}{escape(held_prop)}</span></div>
 
 
 
846
  </div>
847
  """
848
  )
849
- latest_line = ""
850
- if latest_beat is not None:
851
- latest_line = f"""
852
- <div class="speech-bubble">
853
- <div class="speech-speaker">{escape(latest_beat.speaker)}</div>
854
- <div class="speech-line">{escape(latest_beat.line)}</div>
855
- </div>
856
- """
857
  audience_action = ""
858
  if session.latest_audience_action is not None:
859
  audience_action = f"""
@@ -873,18 +1523,27 @@ def render_stage(session: TheaterSession | None) -> str:
873
  </div>
874
  """
875
 
 
 
 
 
 
 
 
 
 
876
  return f"""
877
  <div class="puppet-stage stage-live">
878
  <div class="stage-valance"></div>
879
  <div class="stage-backdrop">
 
880
  <div class="stage-marquee">{escape(session.show_title)}</div>
881
  <div class="stage-copy">
882
  <strong>Setting:</strong> {escape(session.setting)}<br />
883
  <strong>Premise:</strong> {escape(session.premise)}
884
  </div>
885
- {latest_line}
886
  <div class="actor-row">
887
- {''.join(actor_cards)}
888
  </div>
889
  <div class="stage-events">
890
  {audience_action}
@@ -1223,35 +1882,39 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1223
  )
1224
 
1225
  with gr.Group(elem_classes=["control-panel", "premise-panel"]):
1226
- premise_input = gr.Textbox(
1227
- label="Premise",
1228
- placeholder="A moon detective interrogates a suspicious toaster...",
1229
- lines=1,
1230
- )
1231
- with gr.Row():
1232
- create_button = gr.Button("Create Show", variant="primary", elem_classes=["primary-action"])
1233
- reset_button = gr.Button("Reset", elem_classes=["reset-action"])
 
1234
 
1235
  stage_output = gr.HTML(value=EMPTY_STAGE, label="Stage", elem_classes=["stage-output"])
1236
 
1237
- with gr.Row():
1238
  with gr.Group(elem_classes=["control-panel"]):
1239
- gr.Markdown("### Show Controls")
1240
  with gr.Row():
1241
  run_one_button = gr.Button(
1242
  "Run One Beat",
1243
  variant="primary",
1244
  elem_classes=["run-one-action"],
1245
  )
1246
- run_full_button = gr.Button("Run Full Act", elem_classes=["secondary-action"])
1247
 
1248
  with gr.Group(elem_classes=["control-panel"]):
1249
- gr.Markdown("### Audience")
1250
  prop_input = gr.Dropdown(
1251
- choices=["rubber duck", "egg", "flowers", "tomato", "tiny crown", "scroll"],
1252
  value="rubber duck",
1253
  allow_custom_value=True,
1254
- label="Prop",
 
 
 
1255
  )
1256
  with gr.Row():
1257
  throw_prop_button = gr.Button(
@@ -1274,7 +1937,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1274
  label="Transcript",
1275
  lines=7,
1276
  interactive=False,
1277
- elem_classes=["transcript-box"],
1278
  )
1279
  with gr.Accordion("Behind the Curtain", open=False):
1280
  director_output = gr.Textbox(
@@ -1282,6 +1945,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1282
  label="Director Log",
1283
  lines=6,
1284
  interactive=False,
 
1285
  )
1286
  with gr.Accordion("Trace / Debug", open=False):
1287
  trace_output = gr.Textbox(
@@ -1289,6 +1953,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1289
  label="Trace Events",
1290
  lines=6,
1291
  interactive=False,
 
1292
  )
1293
  with gr.Accordion("Backend", open=False):
1294
  backend_select = gr.Dropdown(
@@ -1319,12 +1984,13 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1319
  label="Use deterministic actor lines for OpenBMB full-act playback",
1320
  interactive=True,
1321
  )
1322
- warm_up_button = gr.Button("Warm up OpenBMB", elem_classes=["secondary-action"])
1323
  backend_output = gr.Textbox(
1324
  value=EMPTY_BACKEND,
1325
  label="Model Settings",
1326
  lines=8,
1327
  interactive=False,
 
1328
  )
1329
 
1330
  create_button.click(
@@ -1389,4 +2055,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
1389
 
1390
 
1391
  if __name__ == "__main__":
1392
- app.launch(css=CUSTOM_CSS, theme=gr.themes.Soft())
 
 
 
 
21
  <div class="puppet-stage stage-empty">
22
  <div class="stage-valance"></div>
23
  <div class="stage-backdrop">
24
+ <div class="stage-curtains stage-curtains-closed" aria-hidden="true">
25
+ <div class="stage-curtain stage-curtain-left"></div>
26
+ <div class="stage-curtain stage-curtain-right"></div>
27
+ </div>
28
+ <div class="stage-empty-playbill">
29
+ <p class="playbill-kicker">Tonight</p>
30
+ <h2 class="playbill-title">AI Puppet Theater</h2>
31
+ <p class="playbill-ornament" aria-hidden="true">— ✦ —</p>
32
+ <p class="playbill-tagline">Enter a premise and raise the curtain.</p>
33
+ </div>
34
  </div>
35
  <div class="stage-floorboards"></div>
36
  </div>
 
64
  "mirror": "🪞",
65
  }
66
 
67
+ DEFAULT_STAGE_PROPS: list[str] = [
68
+ "rubber duck",
69
+ "egg",
70
+ "flowers",
71
+ "tomato",
72
+ "tiny crown",
73
+ "scroll",
74
+ ]
75
+
76
+
77
+ def _prop_dropdown_label(canonical: str) -> str:
78
+ emoji = PROP_EMOJI.get(canonical.lower(), "🎁")
79
+ title = " ".join(part.capitalize() for part in canonical.split())
80
+ return f"{emoji} {title}"
81
+
82
+
83
+ PROP_DROPDOWN_CHOICES: list[tuple[str, str]] = [
84
+ (_prop_dropdown_label(name), name) for name in DEFAULT_STAGE_PROPS
85
+ ]
86
+
87
  CUSTOM_CSS = """
88
  body,
89
  .gradio-container {
 
93
  color: #f8efe4 !important;
94
  }
95
  .gradio-container {
96
+ --theater-burgundy: #3b0a16;
97
+ --theater-plum: #2a1426;
98
+ --theater-plum-light: #3d1a35;
99
+ --theater-gold: #f6c453;
100
+ --theater-gold-soft: rgba(246, 196, 83, 0.35);
101
+ --theater-cream: #f8efe4;
102
+ --theater-amber-top: #fcd34d;
103
+ --theater-amber-mid: #d97706;
104
+ --theater-amber-deep: #9a3412;
105
+ --checkbox-label-padding: 0 !important;
106
+ box-sizing: border-box !important;
107
+ width: min(1200px, calc(100vw - 2rem)) !important;
108
+ max-width: min(1200px, calc(100vw - 2rem)) !important;
109
+ margin-left: auto !important;
110
+ margin-right: auto !important;
111
+ padding: 1rem 1rem 1.5rem !important;
112
  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
113
  }
114
+ .backstage-stack {
115
+ align-items: stretch !important;
116
+ gap: 0.85rem !important;
117
+ margin-left: auto !important;
118
+ margin-right: auto !important;
119
+ margin-top: 0.5rem !important;
120
+ max-width: 52rem;
121
+ width: 100% !important;
122
+ }
123
+ .backstage-stack > div {
124
+ min-width: 0 !important;
125
+ width: 100% !important;
126
+ }
127
+ .stage-output.block {
128
+ border: 1px solid var(--theater-gold-soft) !important;
129
+ border-radius: 14px !important;
130
+ box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35) !important;
131
+ overflow: hidden !important;
132
+ padding: 0 !important;
133
+ }
134
+ .stage-output .label-wrap {
135
+ background: linear-gradient(180deg, #5c1628 0%, #3b0a16 100%) !important;
136
+ border-bottom: 2px solid var(--theater-gold) !important;
137
+ border-radius: 12px 12px 0 0 !important;
138
+ margin: 0 !important;
139
+ padding: 0.35rem 0.75rem !important;
140
+ }
141
+ .stage-output .label-wrap span,
142
+ .stage-output label > span {
143
+ color: var(--theater-gold) !important;
144
+ font-family: Georgia, "Times New Roman", ui-serif, serif !important;
145
+ font-size: 0.82rem !important;
146
+ font-weight: 700 !important;
147
+ letter-spacing: 0.12em !important;
148
+ text-transform: uppercase !important;
149
+ }
150
+ .stage-output .html-container {
151
+ border-radius: 0 0 12px 12px !important;
152
+ }
153
  .gradio-container .prose,
154
  .gradio-container label,
155
  .gradio-container span,
 
185
  .gradio-container .block-info,
186
  .gradio-container .label-wrap span,
187
  .gradio-container label > span {
 
 
188
  border-radius: 6px !important;
189
  color: #ffd166 !important;
190
  font-weight: 700 !important;
191
  }
192
+ .gradio-container .accordion .block .label-wrap span {
193
+ background: transparent !important;
194
+ border: none !important;
195
+ border-radius: 0 !important;
196
+ box-shadow: none !important;
197
+ color: #cbb7a1 !important;
198
+ font-weight: 600 !important;
199
+ letter-spacing: 0.03em !important;
200
+ text-transform: none !important;
201
+ }
202
  .gradio-container .wrap,
203
  .gradio-container .styler,
204
  .gradio-container .form,
 
214
  color: #f8efe4 !important;
215
  }
216
  .app-title h1 {
217
+ color: #fff7ed;
218
+ font-family: Georgia, "Times New Roman", ui-serif, serif;
219
  font-size: 2.15rem;
220
+ font-weight: 700;
221
+ letter-spacing: 0.02em;
222
  margin-bottom: 0;
223
  text-align: center;
224
+ text-shadow: 0 2px 24px rgba(0, 0, 0, 0.45);
225
  }
226
  .app-title p {
227
+ color: #d4c4b4;
228
  font-size: 0.95rem;
229
+ letter-spacing: 0.04em;
230
+ margin: 0.2rem 0 0.85rem;
231
  text-align: center;
232
  }
233
  .gradio-container h3,
 
237
  color: #f8efe4 !important;
238
  }
239
  .premise-panel {
240
+ background:
241
+ linear-gradient(145deg, rgba(246, 196, 83, 0.08) 0%, transparent 42%),
242
+ linear-gradient(180deg, rgba(52, 24, 46, 0.92) 0%, rgba(34, 17, 31, 0.94) 100%);
243
+ border: 1px solid rgba(246, 196, 83, 0.32) !important;
244
+ border-radius: 12px !important;
245
+ box-shadow:
246
+ 0 0 0 1px rgba(59, 10, 22, 0.55),
247
+ 0 18px 36px rgba(0, 0, 0, 0.28);
248
+ box-sizing: border-box !important;
249
+ overflow: hidden !important;
250
+ padding: 1rem 1.15rem 1.15rem !important;
251
+ }
252
+ .premise-panel .premise-stack,
253
+ .gradio-container .gr-group.premise-panel .premise-stack {
254
+ background: transparent !important;
255
+ border: none !important;
256
+ box-shadow: none !important;
257
+ gap: 0.65rem !important;
258
+ margin: 0 !important;
259
+ padding: 0 !important;
260
+ width: 100% !important;
261
+ }
262
+ .premise-panel .premise-actions,
263
+ .gradio-container .gr-group.premise-panel .premise-actions {
264
+ box-sizing: border-box !important;
265
+ margin: 0 !important;
266
+ max-width: 100% !important;
267
+ padding: 0 !important;
268
+ width: 100% !important;
269
+ }
270
+ .premise-panel .form {
271
+ gap: 0.55rem !important;
272
+ padding: 0 !important;
273
  }
 
274
  .premise-panel .wrap,
275
  .premise-panel .styler,
276
  .premise-panel .form,
277
  .premise-panel .block > div {
278
+ background: transparent !important;
279
  }
280
  .control-panel {
281
+ background:
282
+ linear-gradient(160deg, rgba(246, 196, 83, 0.06) 0%, transparent 50%),
283
+ linear-gradient(180deg, rgba(45, 22, 42, 0.95) 0%, rgba(26, 12, 24, 0.97) 100%);
284
+ border: 1px solid rgba(246, 196, 83, 0.28) !important;
285
+ border-radius: 12px !important;
286
+ box-shadow:
287
+ 0 0 0 1px rgba(59, 10, 22, 0.45),
288
+ 0 14px 32px rgba(0, 0, 0, 0.3);
289
+ padding: 1.15rem 1.2rem 1.25rem !important;
290
+ }
291
+ .show-controls-title {
292
+ padding-left: 8px;
293
+ padding-right: 8px;
294
+ padding-top: 6px;
295
+ }
296
+ .audience-title {
297
+ padding-left: 8px;
298
+ padding-right: 8px;
299
+ padding-top: 6px;
300
+ }
301
+ .control-panel .form {
302
+ gap: 0.75rem !important;
303
+ padding: 0 !important;
304
  }
305
  .control-panel .block,
306
  .control-panel .wrap,
307
  .control-panel .styler,
308
  .control-panel .form,
309
  .control-panel .block > div {
310
+ background: transparent !important;
311
+ }
312
+ .control-panel .row {
313
+ gap: 0.65rem !important;
314
+ }
315
+ .control-panel .prose {
316
+ margin-bottom: 0.65rem !important;
317
+ }
318
+ .control-panel .prose h3 {
319
+ margin-top: 0 !important;
320
+ }
321
+ .control-panel .label-wrap span,
322
+ .control-panel label > span,
323
+ .premise-panel .label-wrap span,
324
+ .premise-panel label > span {
325
+ background: transparent !important;
326
+ border: none !important;
327
+ border-radius: 0 !important;
328
+ box-shadow: none !important;
329
+ color: #e8ddd4 !important;
330
+ display: block !important;
331
+ font-size: 0.78rem !important;
332
+ font-weight: 600 !important;
333
+ letter-spacing: 0.04em !important;
334
+ padding: 0 0 0.28rem 0 !important;
335
+ text-transform: none !important;
336
+ }
337
+ .control-panel .dropdown-container,
338
+ .premise-panel .dropdown-container {
339
+ background: transparent !important;
340
+ border: none !important;
341
+ box-shadow: none !important;
342
+ margin-top: 0 !important;
343
+ padding: 0 !important;
344
+ }
345
+ .control-panel .dropdown-container .wrap-inner,
346
+ .control-panel .dropdown-container .secondary-wrap,
347
+ .control-panel .single-select,
348
+ .premise-panel .dropdown-container .wrap-inner,
349
+ .premise-panel .dropdown-container .secondary-wrap {
350
+ background: rgba(8, 10, 18, 0.92) !important;
351
+ border: 1px solid rgba(246, 196, 83, 0.38) !important;
352
+ border-radius: 10px !important;
353
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04) !important;
354
+ }
355
+ .control-panel .dropdown-container button,
356
+ .control-panel .dropdown-container .icon,
357
+ .premise-panel .dropdown-container button {
358
+ background: transparent !important;
359
+ border: none !important;
360
+ box-shadow: none !important;
361
+ }
362
+ /* Audience prop picker: clearer label, large click target, gold border, solid option list */
363
+ .gradio-container .control-panel .prop-picker .label-wrap span,
364
+ .gradio-container .control-panel .prop-picker label > span {
365
+ color: #fcd34d !important;
366
+ font-family: Georgia, "Times New Roman", ui-serif, serif !important;
367
+ font-size: 0.82rem !important;
368
+ font-weight: 700 !important;
369
+ letter-spacing: 0.07em !important;
370
+ padding-bottom: 0.35rem !important;
371
+ text-transform: uppercase !important;
372
+ }
373
+ .gradio-container .control-panel .prop-picker .block-info,
374
+ .gradio-container .control-panel .prop-picker span[data-testid="block-info"] {
375
+ color: rgba(232, 221, 212, 0.88) !important;
376
+ font-size: 0.8rem !important;
377
+ font-weight: 500 !important;
378
+ line-height: 1.4 !important;
379
+ margin-top: 0.15rem !important;
380
+ max-width: 40rem;
381
+ }
382
+ .gradio-container .control-panel .prop-picker .single-select input,
383
+ .gradio-container .control-panel .prop-picker .single-select span {
384
+ font-size: 1rem !important;
385
+ letter-spacing: 0.02em !important;
386
+ }
387
+ /* Outer rim + padding so the whole control reads as one big control */
388
+ .gradio-container .control-panel .prop-picker .dropdown-container {
389
+ background: rgba(14, 10, 22, 0.94) !important;
390
+ border: 2px solid rgba(246, 196, 83, 0.62) !important;
391
+ border-radius: 14px !important;
392
+ box-shadow:
393
+ 0 0 0 1px rgba(59, 10, 22, 0.45),
394
+ 0 8px 26px rgba(0, 0, 0, 0.42) !important;
395
+ box-sizing: border-box !important;
396
+ margin-top: 0.1rem !important;
397
+ padding: 0.35rem !important;
398
+ }
399
+ .gradio-container .control-panel .prop-picker .dropdown-container .wrap-inner,
400
+ .gradio-container .control-panel .prop-picker .dropdown-container .secondary-wrap,
401
+ .gradio-container .control-panel .prop-picker .single-select {
402
+ border-radius: 11px !important;
403
+ min-height: 3.5rem !important;
404
+ transition:
405
+ border-color 0.15s ease,
406
+ box-shadow 0.15s ease !important;
407
+ }
408
+ .gradio-container .control-panel .prop-picker .dropdown-container .wrap-inner,
409
+ .gradio-container .control-panel .prop-picker .dropdown-container .secondary-wrap {
410
+ align-items: center !important;
411
+ background: rgba(8, 10, 18, 0.98) !important;
412
+ border: 2px solid rgba(246, 196, 83, 0.48) !important;
413
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06) !important;
414
+ padding: 0.85rem 1.05rem !important;
415
+ }
416
+ .gradio-container .control-panel .prop-picker .single-select {
417
+ min-height: 3.5rem !important;
418
+ }
419
+ .gradio-container .control-panel .prop-picker .dropdown-container button {
420
+ min-height: 2.85rem !important;
421
+ min-width: 2.85rem !important;
422
+ }
423
+ .gradio-container .control-panel .prop-picker .dropdown-container:focus-within {
424
+ border-color: #f6c453 !important;
425
+ box-shadow:
426
+ 0 0 0 2px rgba(246, 196, 83, 0.35),
427
+ 0 10px 28px rgba(0, 0, 0, 0.48) !important;
428
+ }
429
+ .gradio-container .control-panel .prop-picker .dropdown-container:focus-within .wrap-inner,
430
+ .gradio-container .control-panel .prop-picker .dropdown-container:focus-within .secondary-wrap {
431
+ border-color: #f6c453 !important;
432
+ box-shadow:
433
+ 0 0 0 1px rgba(246, 196, 83, 0.35),
434
+ inset 0 1px 0 rgba(255, 255, 255, 0.07) !important;
435
+ }
436
+ /* Closed-state dropdown cue: chevron always visible on the trigger (not the open list) */
437
+ .gradio-container .control-panel .prop-picker .dropdown-container,
438
+ .gradio-container .control-panel .prop-picker .dropdown-container .wrap-inner,
439
+ .gradio-container .control-panel .prop-picker .dropdown-container .secondary-wrap {
440
+ overflow: visible !important;
441
+ }
442
+ .gradio-container .control-panel .prop-picker .dropdown-container .wrap-inner {
443
+ position: relative !important;
444
+ }
445
+ .gradio-container .control-panel .prop-picker .dropdown-container .wrap-inner::after {
446
+ border-color: var(--theater-gold) transparent transparent transparent;
447
+ border-style: solid;
448
+ border-width: 8px 6px 0 6px;
449
+ content: "";
450
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
451
+ opacity: 1;
452
+ pointer-events: none;
453
+ position: absolute;
454
+ right: 0.72rem;
455
+ top: 50%;
456
+ transform: translateY(-40%);
457
+ z-index: 50;
458
+ }
459
+ .gradio-container .control-panel .prop-picker .dropdown-container button svg {
460
+ height: 0 !important;
461
+ opacity: 0 !important;
462
+ overflow: hidden !important;
463
+ width: 0 !important;
464
  }
465
  .control-panel .row,
466
+ .premise-panel .row,
467
+ .premise-panel .premise-actions {
468
  background: transparent !important;
469
  }
470
  .control-panel h3 {
471
+ border-bottom: 1px solid rgba(246, 196, 83, 0.2);
472
+ color: #fff7ed;
473
+ font-family: Georgia, "Times New Roman", ui-serif, serif;
474
+ font-size: 0.92rem;
475
+ font-weight: 700;
476
+ letter-spacing: 0.06em;
477
+ margin: 0 0 0.5rem;
478
+ padding-bottom: 0.35rem;
479
+ text-transform: uppercase;
480
  }
481
  .control-panel .prose,
482
  .control-panel .prose h3,
 
484
  color: #f8efe4 !important;
485
  }
486
  .puppet-stage {
 
487
  border: 5px solid #3b0a16;
488
  border-radius: 14px;
489
  background:
490
  linear-gradient(90deg, rgba(59, 10, 22, 0.98) 0 10%, transparent 10% 90%, rgba(59, 10, 22, 0.98) 90% 100%),
491
  linear-gradient(180deg, rgba(42, 20, 38, 0.96), rgba(13, 6, 14, 0.98));
492
+ box-sizing: border-box;
493
  color: #f8efe4;
494
  display: flex;
495
  flex-direction: column;
496
  align-items: stretch;
497
  justify-content: stretch;
498
+ height: 600px;
499
+ max-height: 600px;
500
+ min-height: 600px;
501
  position: relative;
502
  overflow: hidden;
503
  box-shadow:
 
534
  position: relative;
535
  z-index: 3;
536
  }
537
+ .stage-curtains {
538
+ display: flex;
539
+ flex-direction: row;
540
+ inset: 0;
541
+ overflow: hidden;
542
+ pointer-events: none;
543
+ position: absolute;
544
+ z-index: 15;
545
+ }
546
+ .stage-curtain {
547
+ background:
548
+ repeating-linear-gradient(90deg, rgba(0, 0, 0, 0.1) 0 2px, transparent 2px 24px),
549
+ linear-gradient(180deg, #7f1533 0%, #4a0a18 55%, #2d050e 100%);
550
+ box-shadow:
551
+ inset 0 0 38px rgba(0, 0, 0, 0.5),
552
+ inset -5px 0 0 rgba(246, 196, 83, 0.32);
553
+ flex: 1 1 50%;
554
+ height: 100%;
555
+ min-width: 50%;
556
+ }
557
+ .stage-curtains-animate .stage-curtain-left {
558
+ animation: stage-curtain-left-open 1.45s cubic-bezier(0.22, 1, 0.36, 1) 0.1s forwards;
559
+ transform-origin: left center;
560
+ }
561
+ .stage-curtains-animate .stage-curtain-right {
562
+ animation: stage-curtain-right-open 1.45s cubic-bezier(0.22, 1, 0.36, 1) 0.1s forwards;
563
+ box-shadow:
564
+ inset 0 0 38px rgba(0, 0, 0, 0.5),
565
+ inset 5px 0 0 rgba(246, 196, 83, 0.32);
566
+ transform-origin: right center;
567
+ }
568
+ .stage-curtains-closed .stage-curtain-left,
569
+ .stage-curtains-closed .stage-curtain-right {
570
+ animation: none !important;
571
+ }
572
+ .stage-curtains-closed .stage-curtain-left {
573
+ transform: translateX(1.5%);
574
+ }
575
+ .stage-curtains-closed .stage-curtain-right {
576
+ box-shadow:
577
+ inset 0 0 38px rgba(0, 0, 0, 0.5),
578
+ inset 5px 0 0 rgba(246, 196, 83, 0.32);
579
+ transform: translateX(-1.5%);
580
+ }
581
+ @keyframes stage-curtain-left-open {
582
+ from { transform: translateX(0); }
583
+ to { transform: translateX(-108%); }
584
+ }
585
+ @keyframes stage-curtain-right-open {
586
+ from { transform: translateX(0); }
587
+ to { transform: translateX(108%); }
588
+ }
589
+ @media (prefers-reduced-motion: reduce) {
590
+ .stage-curtains-animate .stage-curtain-left,
591
+ .stage-curtains-animate .stage-curtain-right {
592
+ animation: none !important;
593
+ opacity: 0;
594
+ transform: none !important;
595
+ }
596
+ }
597
  .stage-backdrop {
598
  background:
599
  radial-gradient(circle at 50% 8%, rgba(255, 224, 150, 0.28), transparent 19rem),
600
  radial-gradient(circle at 24% 58%, rgba(255, 224, 150, 0.12), transparent 14rem),
601
  linear-gradient(180deg, #2a1426 0%, #22111f 62%, #130911 100%);
602
+ display: flex;
603
+ flex: 1 1 0;
604
+ flex-direction: column;
605
+ min-height: 0;
606
+ overflow: hidden;
607
  padding: 0.72rem 7.2rem 0.8rem;
608
  position: relative;
609
  z-index: 1;
 
642
  .stage-copy strong {
643
  color: #f8efe4;
644
  }
645
+ .stage-empty-playbill {
646
+ border: 2px solid rgba(246, 196, 83, 0.55);
647
+ border-radius: 4px;
648
+ box-shadow:
649
+ 0 0 0 1px rgba(59, 10, 22, 0.9),
650
+ 0 0 48px rgba(255, 209, 102, 0.22),
651
+ inset 0 0 36px rgba(0, 0, 0, 0.35);
652
+ left: 50%;
653
+ max-width: min(22rem, 88%);
654
+ padding: 1.35rem 1.5rem 1.45rem;
655
+ position: absolute;
656
  text-align: center;
657
+ top: 50%;
658
+ transform: translate(-50%, -50%);
659
+ z-index: 22;
660
+ background:
661
+ radial-gradient(ellipse 120% 80% at 50% 20%, rgba(255, 224, 160, 0.14), transparent 55%),
662
+ linear-gradient(180deg, rgba(42, 20, 38, 0.92) 0%, rgba(19, 9, 17, 0.94) 100%);
663
+ }
664
+ .playbill-kicker {
665
+ color: #ffd166;
666
+ font-family: Georgia, "Times New Roman", serif;
667
+ font-size: 0.72rem;
668
+ font-weight: 700;
669
+ letter-spacing: 0.38em;
670
+ margin: 0 0 0.35rem;
671
+ text-transform: uppercase;
672
+ }
673
+ .playbill-title {
674
+ color: #fff7ed;
675
+ font-family: Georgia, "Times New Roman", serif;
676
+ font-size: clamp(1.35rem, 3.2vw, 1.85rem);
677
+ font-weight: 700;
678
+ letter-spacing: 0.04em;
679
+ line-height: 1.15;
680
+ margin: 0;
681
+ text-shadow: 0 3px 22px rgba(0, 0, 0, 0.75);
682
+ }
683
+ .playbill-ornament {
684
+ color: rgba(246, 196, 83, 0.75);
685
+ font-size: 0.82rem;
686
+ letter-spacing: 0.12em;
687
+ margin: 0.45rem 0 0.35rem;
688
+ }
689
+ .playbill-tagline {
690
+ color: #e8d5c4;
691
+ font-size: 0.95rem;
692
+ line-height: 1.45;
693
+ margin: 0;
694
+ }
695
+ .puppet-stage.stage-empty .stage-backdrop::before {
696
+ background: radial-gradient(ellipse 70% 55% at 50% 42%, rgba(255, 240, 200, 0.12), transparent 62%);
697
+ content: "";
698
+ inset: 0;
699
+ pointer-events: none;
700
+ position: absolute;
701
+ z-index: 1;
702
  }
703
  .stage-floorboards {
704
  height: 58px;
 
711
  }
712
  .speech-bubble {
713
  animation: bubble-in 0.24s ease-out;
714
+ background: rgba(18, 10, 18, 0.92);
715
  border: 1px solid rgba(246, 196, 83, 0.5);
716
  border-radius: 16px;
717
  box-shadow: 0 18px 30px rgba(0, 0, 0, 0.34);
718
+ box-sizing: border-box;
719
  color: #f8efe4;
720
+ left: 0;
721
+ margin: 0 0 0.35rem 0;
722
+ max-height: 7.25rem;
723
+ overflow-x: hidden;
724
+ overflow-y: auto;
725
  padding: 0.72rem 0.95rem;
726
+ position: absolute;
727
+ right: 0;
728
+ bottom: 100%;
729
  text-align: center;
730
+ width: auto;
731
+ z-index: 6;
732
  }
733
  .speech-bubble::after {
734
  border-left: 10px solid transparent;
 
752
  color: #f8efe4;
753
  font-size: 0.96rem;
754
  line-height: 1.35;
755
+ overflow-wrap: anywhere;
756
  }
757
  .actor-row {
758
+ align-items: flex-end;
759
+ display: flex;
760
+ flex-shrink: 0;
761
+ flex-wrap: wrap;
762
  gap: 0.55rem;
763
+ justify-content: center;
764
+ margin-top: 0.91rem;
765
  position: relative;
766
  z-index: 3;
767
  }
768
+ .actor-column {
769
+ align-items: center;
770
+ display: flex;
771
+ flex: 0 0 auto;
772
+ flex-direction: column;
773
+ justify-content: flex-end;
774
+ min-width: 0;
775
+ position: relative;
776
+ }
777
  .actor-card {
778
  background: rgba(70, 38, 36, 0.72);
779
  border: 1px solid rgba(246, 196, 83, 0.45);
 
784
  position: relative;
785
  transform-origin: bottom center;
786
  text-align: center;
787
+ z-index: 1;
788
  }
789
  .actor-card::after {
790
  background: #7c3f17;
 
897
  margin: 0.2rem;
898
  padding: 0.22rem 0.55rem;
899
  }
900
+ .gradio-container button {
901
+ background: linear-gradient(180deg, #4f2f45 0%, #2f1828 100%) !important;
902
+ border: 1px solid rgba(246, 196, 83, 0.26) !important;
903
+ border-radius: 10px !important;
904
+ color: #f8efe4 !important;
905
+ font-weight: 600 !important;
906
+ letter-spacing: 0.03em !important;
907
+ min-height: 2.45rem !important;
908
+ padding: 0.5rem 1rem !important;
909
+ transition:
910
+ background 0.15s ease,
911
+ border-color 0.15s ease,
912
+ box-shadow 0.15s ease,
913
+ transform 0.12s ease !important;
914
+ }
915
+ .gradio-container button:hover:not(:disabled) {
916
+ transform: translateY(-1px);
917
+ }
918
+ .gradio-container button:active:not(:disabled) {
919
+ transform: translateY(0);
920
+ }
921
  .gradio-container button.primary,
922
  .gradio-container button.primary-action,
923
  .gradio-container button.run-one-action {
924
+ background: linear-gradient(180deg, #fde68a 0%, #d97706 42%, #b45309 100%) !important;
925
+ border: 1px solid rgba(252, 211, 77, 0.65) !important;
926
+ box-shadow:
927
+ 0 2px 0 rgba(124, 45, 18, 0.65),
928
+ 0 10px 26px rgba(217, 119, 6, 0.35) !important;
929
+ color: #1a0a06 !important;
930
+ text-shadow: 0 1px 0 rgba(255, 247, 237, 0.35);
931
+ }
932
+ .gradio-container button.primary:hover:not(:disabled),
933
+ .gradio-container button.primary-action:hover:not(:disabled),
934
+ .gradio-container button.run-one-action:hover:not(:disabled) {
935
+ background: linear-gradient(180deg, #fef3c7 0%, #ea580c 45%, #c2410c 100%) !important;
936
+ box-shadow:
937
+ 0 2px 0 rgba(124, 45, 18, 0.55),
938
+ 0 12px 28px rgba(234, 88, 12, 0.38) !important;
939
  }
940
  .gradio-container button.secondary,
 
941
  .gradio-container button.audience-action-button {
942
+ background: linear-gradient(180deg, #6b2a3a 0%, #4a1522 48%, #2d0c14 100%) !important;
943
+ border: 1px solid rgba(252, 165, 165, 0.35) !important;
944
+ box-shadow:
945
+ inset 0 1px 0 rgba(255, 255, 255, 0.07),
946
+ 0 6px 18px rgba(0, 0, 0, 0.32) !important;
947
+ color: #fff5f5 !important;
948
+ }
949
+ .gradio-container button.secondary:hover:not(:disabled),
950
+ .gradio-container button.audience-action-button:hover:not(:disabled) {
951
+ border-color: rgba(254, 202, 202, 0.55) !important;
952
+ box-shadow:
953
+ inset 0 1px 0 rgba(255, 255, 255, 0.12),
954
+ 0 8px 22px rgba(0, 0, 0, 0.36) !important;
955
+ }
956
+ .gradio-container button.cue-action {
957
+ background: linear-gradient(180deg, #5c4a2f 0%, #3d3020 50%, #261c12 100%) !important;
958
+ border: 1px solid rgba(246, 196, 83, 0.45) !important;
959
+ box-shadow:
960
+ inset 0 1px 0 rgba(255, 255, 255, 0.08),
961
+ 0 6px 18px rgba(0, 0, 0, 0.3) !important;
962
+ color: #fff8eb !important;
963
+ }
964
+ .gradio-container button.cue-action:hover:not(:disabled) {
965
+ background: linear-gradient(180deg, #6e5a38 0%, #4a3b26 50%, #322818 100%) !important;
966
+ border-color: rgba(253, 224, 71, 0.55) !important;
967
+ box-shadow:
968
+ inset 0 1px 0 rgba(255, 255, 255, 0.12),
969
+ 0 8px 22px rgba(0, 0, 0, 0.34) !important;
970
  }
971
  .gradio-container button.reset-action {
972
+ background: linear-gradient(180deg, #5c1218 0%, #3b0a16 55%, #2a0508 100%) !important;
973
+ border: 1px solid rgba(246, 196, 83, 0.28) !important;
974
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05) !important;
975
+ color: #f5e6dc !important;
976
+ }
977
+ .gradio-container button.reset-action:hover:not(:disabled) {
978
+ border-color: rgba(255, 209, 102, 0.45) !important;
979
+ }
980
+ .transcript-box {
981
+ background: transparent !important;
982
+ border: none !important;
983
+ box-shadow: none !important;
984
  }
 
985
  .gradio-container .accordion {
986
+ background: rgba(13, 6, 14, 0.72) !important;
987
+ border: 1px solid rgba(246, 196, 83, 0.22) !important;
988
+ border-radius: 12px !important;
989
  color: #f8efe4 !important;
990
+ overflow: hidden;
991
+ }
992
+ .gradio-container .accordion .block,
993
+ .gradio-container .accordion .form,
994
+ .gradio-container .accordion .wrap:not(.label-wrap) {
995
+ background: transparent !important;
996
+ border: none !important;
997
+ box-shadow: none !important;
998
+ }
999
+ .gradio-container .accordion textarea,
1000
+ .gradio-container .accordion .wrap-inner {
1001
+ background: rgba(10, 12, 23, 0.88) !important;
1002
+ border: 1px solid rgba(246, 196, 83, 0.22) !important;
1003
+ border-radius: 8px !important;
1004
+ }
1005
+ .gradio-container .accordion .no-field-label .label-wrap {
1006
+ display: none !important;
1007
+ }
1008
+ .gradio-container .accordion .no-field-label .wrap {
1009
+ margin-top: 0 !important;
1010
+ }
1011
+ .gradio-container .accordion > .label-wrap {
1012
+ background: linear-gradient(180deg, rgba(72, 28, 52, 0.98) 0%, rgba(42, 20, 38, 0.99) 100%) !important;
1013
+ border-bottom: 1px solid rgba(246, 196, 83, 0.22) !important;
1014
+ }
1015
+ .gradio-container .accordion > .label-wrap span {
1016
+ background: transparent !important;
1017
+ border: none !important;
1018
+ border-radius: 0 !important;
1019
+ box-shadow: none !important;
1020
+ color: #ffd166 !important;
1021
+ font-family: Georgia, "Times New Roman", ui-serif, serif !important;
1022
+ font-size: 0.78rem !important;
1023
+ letter-spacing: 0.08em !important;
1024
+ text-transform: uppercase !important;
1025
+ }
1026
+ .gradio-container .accordion > .label-wrap button {
1027
+ background: transparent !important;
1028
+ border: none !important;
1029
+ box-shadow: none !important;
1030
+ color: #ffd166 !important;
1031
+ }
1032
+ .gradio-container .accordion > .label-wrap button:hover {
1033
+ background: rgba(255, 209, 102, 0.12) !important;
1034
+ }
1035
+ .gradio-container .accordion .dropdown-container {
1036
+ background: transparent !important;
1037
+ border: none !important;
1038
+ box-shadow: none !important;
1039
+ }
1040
+ .gradio-container .accordion .dropdown-container .wrap-inner,
1041
+ .gradio-container .accordion .dropdown-container .secondary-wrap {
1042
+ background: rgba(8, 10, 18, 0.92) !important;
1043
+ border: 1px solid rgba(246, 196, 83, 0.32) !important;
1044
+ border-radius: 10px !important;
1045
+ }
1046
+ .gradio-container input[type="range"] {
1047
+ accent-color: #f6c453;
1048
+ }
1049
+ .gradio-container .slider input {
1050
+ --slider-color: #f6c453;
1051
  }
1052
  @keyframes puppet-bounce {
1053
  from { transform: translateY(0) rotate(-0.4deg); }
 
1063
  }
1064
  @media (max-width: 760px) {
1065
  .puppet-stage {
1066
+ height: 600px;
1067
+ max-height: 600px;
1068
+ min-height: 600px;
1069
  }
1070
  .puppet-stage::before,
1071
  .puppet-stage::after {
 
1078
  font-size: 1.2rem;
1079
  }
1080
  .actor-row {
1081
+ gap: 0.45rem;
1082
+ }
1083
+ .backstage-stack {
1084
+ gap: 0.65rem !important;
1085
+ max-width: 100% !important;
1086
  }
1087
  .actor-card {
1088
  min-height: 126px;
 
1090
  }
1091
 
1092
  /* Final Gradio chrome overrides: keep the whole app in the theater palette. */
1093
+ /* Generic groups only — theater panels use .control-panel / .premise-panel below. */
1094
+ .gradio-container .gr-group:not(.control-panel):not(.premise-panel) {
 
 
1095
  background: rgba(34, 17, 31, 0.84) !important;
1096
  border: 1px solid rgba(246, 196, 83, 0.2) !important;
1097
  border-radius: 8px !important;
1098
  color: #f8efe4 !important;
1099
  }
1100
+ .gradio-container .gr-group.control-panel {
1101
+ border-radius: 12px !important;
1102
+ box-sizing: border-box !important;
1103
+ }
1104
+ .gradio-container .gr-group.premise-panel {
1105
+ border-radius: 12px !important;
1106
+ box-sizing: border-box !important;
1107
+ overflow: hidden !important;
1108
+ }
1109
  .gradio-container .gr-group .form,
1110
  .gradio-container .gr-group .block,
1111
  .gradio-container .gr-group .wrap,
 
1113
  .gradio-container .gr-group .secondary-wrap,
1114
  .gradio-container .gr-group .input-container,
1115
  .gradio-container .gr-group label {
 
1116
  color: #f8efe4 !important;
1117
  }
1118
  .gradio-container input,
1119
  .gradio-container textarea,
1120
+ .gradio-container select {
 
 
1121
  background: rgba(10, 12, 23, 0.9) !important;
1122
  color: #f8efe4 !important;
1123
  }
1124
+ .gradio-container .control-panel .dropdown-container,
1125
+ .gradio-container .premise-panel .dropdown-container {
1126
+ background: transparent !important;
1127
+ border: none !important;
1128
+ box-shadow: none !important;
1129
+ }
1130
+ .gradio-container .control-panel .dropdown-container .wrap-inner,
1131
+ .gradio-container .control-panel .dropdown-container .secondary-wrap,
1132
+ .gradio-container .control-panel .single-select,
1133
+ .gradio-container .premise-panel .dropdown-container .wrap-inner,
1134
+ .gradio-container .premise-panel .dropdown-container .secondary-wrap {
1135
+ background: rgba(8, 10, 18, 0.92) !important;
1136
+ border: 1px solid rgba(246, 196, 83, 0.38) !important;
1137
+ border-radius: 10px !important;
1138
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04) !important;
1139
+ }
1140
  .gradio-container .control-panel input,
1141
  .gradio-container .control-panel textarea,
 
1142
  .gradio-container .premise-panel textarea {
1143
+ background: rgba(8, 10, 18, 0.92) !important;
1144
+ border: 1px solid rgba(246, 196, 83, 0.3) !important;
1145
+ border-radius: 10px !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1146
  }
1147
  .gradio-container .html-container,
1148
  .gradio-container .gradio-style {
1149
  width: 100% !important;
1150
  }
1151
  .puppet-stage {
1152
+ height: 600px;
1153
+ max-height: 600px;
1154
+ min-height: 600px;
1155
  width: 100%;
1156
  }
1157
  .puppet-stage::before,
 
1166
  white-space: normal;
1167
  }
1168
  .speech-bubble {
1169
+ max-height: 6.75rem;
 
1170
  padding: 0.58rem 0.82rem;
1171
  }
1172
  .actor-row {
1173
+ align-items: flex-end;
1174
+ display: flex;
1175
+ flex-shrink: 0;
1176
+ flex-wrap: wrap;
1177
  gap: 0.62rem;
1178
+ justify-content: center;
1179
  margin-top: 0.82rem;
1180
  }
1181
+ .actor-column {
1182
+ position: relative;
1183
+ }
1184
  .actor-card {
1185
  align-content: start;
1186
  background: radial-gradient(circle at 50% 18%, rgba(246, 196, 83, 0.13), rgba(70, 38, 36, 0.72) 58%);
 
1248
  padding: 0.75rem 2.45rem;
1249
  }
1250
  .actor-row {
 
1251
  gap: 0.45rem;
1252
  }
1253
  .actor-card {
 
1267
  .app-title p {
1268
  margin-bottom: 0.55rem;
1269
  }
 
 
 
1270
  .stage-output,
1271
  .stage-output .html-container,
1272
  .stage-output .gradio-style {
1273
  margin-bottom: 0 !important;
1274
  }
1275
  .puppet-stage {
1276
+ height: 600px;
1277
+ max-height: 600px;
1278
+ min-height: 600px;
1279
+ }
1280
+ .stage-empty-playbill {
1281
+ max-width: min(19rem, 92%);
1282
+ padding: 1rem 1.1rem 1.12rem;
1283
+ }
1284
+ .playbill-kicker {
1285
+ font-size: 0.62rem;
1286
+ letter-spacing: 0.28em;
1287
+ }
1288
+ .playbill-title {
1289
+ font-size: clamp(1.12rem, 4vw, 1.42rem);
1290
+ }
1291
+ .playbill-tagline {
1292
+ font-size: 0.82rem;
1293
  }
1294
  .stage-valance {
1295
  height: 34px;
 
1308
  }
1309
  .speech-bubble {
1310
  border-radius: 12px;
1311
+ max-height: 5.75rem;
 
1312
  padding: 0.42rem 0.7rem;
1313
  }
1314
  .speech-speaker {
 
1316
  }
1317
  .speech-line {
1318
  font-size: 0.86rem;
1319
+ overflow-wrap: anywhere;
1320
  }
1321
  .actor-row {
1322
+ align-items: flex-end;
1323
+ display: flex;
1324
+ flex-shrink: 0;
1325
+ flex-wrap: wrap;
1326
  gap: 0.5rem;
1327
+ justify-content: center;
1328
+ margin-top: 5.50rem;
1329
+ }
1330
+ .actor-column {
1331
+ position: relative;
1332
  }
1333
  .actor-card {
1334
  border-radius: 14px;
 
1387
  }
1388
  .control-panel {
1389
  margin-top: 0 !important;
1390
+ padding: 1rem 1.1rem 1.15rem !important;
1391
  }
1392
  .control-panel h3 {
1393
  margin-bottom: 0.2rem;
1394
  }
1395
  .gradio-container .row {
1396
  gap: 0.55rem !important;
1397
+ padding: 12px !important;
1398
  }
1399
+ /* Column gap separates Show Controls from Audience; avoid doubling with sibling margin. */
1400
+ .backstage-stack .control-panel + .control-panel {
1401
+ margin-top: 0 !important;
 
1402
  }
1403
  .transcript-section,
1404
  .gradio-container .accordion {
 
1406
  }
1407
  @media (max-width: 760px) {
1408
  .puppet-stage {
1409
+ height: 600px;
1410
+ max-height: 600px;
1411
+ min-height: 600px;
1412
  }
1413
  .stage-backdrop {
1414
  padding: 0.52rem 2.15rem;
1415
  }
1416
  .actor-row {
1417
+ gap: 0.42rem;
1418
  }
1419
  .speech-line {
1420
  font-size: 0.8rem;
1421
  }
1422
  }
1423
+ /* Gradio dropdown option panels are often portaled; keep them opaque and above the stage */
1424
+ [role="listbox"] {
1425
+ z-index: 10050 !important;
1426
+ background-color: rgba(12, 10, 20, 0.98) !important;
1427
+ border: 2px solid rgba(246, 196, 83, 0.58) !important;
1428
+ border-radius: 12px !important;
1429
+ box-shadow: 0 22px 56px rgba(0, 0, 0, 0.72) !important;
1430
+ backdrop-filter: blur(12px);
1431
+ -webkit-backdrop-filter: blur(12px);
1432
+ max-height: min(70vh, 22rem) !important;
1433
+ overflow-y: auto !important;
1434
+ overflow-x: hidden !important;
1435
+ }
1436
+ [role="option"] {
1437
+ padding: 0.55rem 0.85rem !important;
1438
+ font-size: 0.95rem !important;
1439
+ }
1440
+ [role="option"][aria-selected="true"],
1441
+ [role="option"]:hover,
1442
+ [role="option"][data-highlighted] {
1443
+ background-color: rgba(246, 196, 83, 0.18) !important;
1444
+ }
1445
+ /* Gradio 6.5.x: listbox inside .contain — min 40px panel floor + 40px option rows */
1446
+ .gradio-container.gradio-container-6-5-1 .contain [role="listbox"] {
1447
+ min-height: 40px !important;
1448
+ padding-left: 8px !important;
1449
+ }
1450
+ .gradio-container.gradio-container-6-5-1 .contain [role="listbox"] [role="option"] {
1451
+ align-items: center !important;
1452
+ box-sizing: border-box !important;
1453
+ display: flex !important;
1454
+ min-height: 40px !important;
1455
+ }
1456
  """
1457
 
1458
 
1459
+ THEATER_THEME = gr.themes.Soft(
1460
+ primary_hue=gr.themes.colors.amber,
1461
+ secondary_hue=gr.themes.colors.rose,
1462
+ neutral_hue=gr.themes.colors.stone,
1463
+ font=(gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"),
1464
+ font_mono=(gr.themes.GoogleFont("IBM Plex Mono"), "ui-monospace", "Consolas", "monospace"),
1465
+ )
1466
+
1467
+
1468
  def render_stage(session: TheaterSession | None) -> str:
1469
  if session is None:
1470
  return EMPTY_STAGE
1471
 
1472
+ show_opening_curtain = session.play_opening_curtain
1473
+ if session.play_opening_curtain:
1474
+ session.play_opening_curtain = False
1475
+
1476
+ actor_columns: list[str] = []
1477
  latest_beat = session.transcript[-1] if session.transcript else None
1478
  latest_speaker = latest_beat.speaker if latest_beat else None
1479
  for actor in session.actors:
 
1482
  role_line = actor.goal.split(".", maxsplit=1)[0]
1483
  held_prop = actor.held_prop or "nothing"
1484
  held_emoji = PROP_EMOJI.get(held_prop.lower(), "🎁") if actor.held_prop else ""
1485
+ bubble_html = ""
1486
+ if latest_beat is not None and actor.name == latest_speaker:
1487
+ bubble_html = f"""
1488
+ <div class="speech-bubble">
1489
+ <div class="speech-speaker">{escape(latest_beat.speaker)}</div>
1490
+ <div class="speech-line">{escape(latest_beat.line)}</div>
1491
+ </div>
1492
+ """
1493
+ actor_columns.append(
1494
  f"""
1495
+ <div class="actor-column">
1496
+ {bubble_html}
1497
+ <div class="actor-card{active_class}">
1498
+ <div class="actor-avatar">{escape(actor.avatar)}</div>
1499
+ <div class="actor-name">{escape(actor.name)}</div>
1500
+ {active_label}
1501
+ <div class="actor-detail">{escape(role_line)}</div>
1502
+ <div class="held-prop"><span>Holding: {escape((held_emoji + " ") if held_emoji else "")}{escape(held_prop)}</span></div>
1503
+ </div>
1504
  </div>
1505
  """
1506
  )
 
 
 
 
 
 
 
 
1507
  audience_action = ""
1508
  if session.latest_audience_action is not None:
1509
  audience_action = f"""
 
1523
  </div>
1524
  """
1525
 
1526
+ curtain_html = ""
1527
+ if show_opening_curtain:
1528
+ curtain_html = """
1529
+ <div class="stage-curtains stage-curtains-animate" aria-hidden="true">
1530
+ <div class="stage-curtain stage-curtain-left"></div>
1531
+ <div class="stage-curtain stage-curtain-right"></div>
1532
+ </div>
1533
+ """
1534
+
1535
  return f"""
1536
  <div class="puppet-stage stage-live">
1537
  <div class="stage-valance"></div>
1538
  <div class="stage-backdrop">
1539
+ {curtain_html}
1540
  <div class="stage-marquee">{escape(session.show_title)}</div>
1541
  <div class="stage-copy">
1542
  <strong>Setting:</strong> {escape(session.setting)}<br />
1543
  <strong>Premise:</strong> {escape(session.premise)}
1544
  </div>
 
1545
  <div class="actor-row">
1546
+ {''.join(actor_columns)}
1547
  </div>
1548
  <div class="stage-events">
1549
  {audience_action}
 
1882
  )
1883
 
1884
  with gr.Group(elem_classes=["control-panel", "premise-panel"]):
1885
+ with gr.Column(elem_classes=["premise-stack"]):
1886
+ premise_input = gr.Textbox(
1887
+ label="Premise",
1888
+ placeholder="A moon detective interrogates a suspicious toaster...",
1889
+ lines=1,
1890
+ )
1891
+ with gr.Row(elem_classes=["premise-actions"]):
1892
+ create_button = gr.Button("Create Show", variant="primary", elem_classes=["primary-action"])
1893
+ reset_button = gr.Button("Reset", elem_classes=["reset-action"])
1894
 
1895
  stage_output = gr.HTML(value=EMPTY_STAGE, label="Stage", elem_classes=["stage-output"])
1896
 
1897
+ with gr.Column(elem_classes=["backstage-stack"]):
1898
  with gr.Group(elem_classes=["control-panel"]):
1899
+ gr.Markdown("### Show Controls", elem_classes=["show-controls-title", "panel-heading"])
1900
  with gr.Row():
1901
  run_one_button = gr.Button(
1902
  "Run One Beat",
1903
  variant="primary",
1904
  elem_classes=["run-one-action"],
1905
  )
1906
+ run_full_button = gr.Button("Run Full Act", elem_classes=["cue-action"])
1907
 
1908
  with gr.Group(elem_classes=["control-panel"]):
1909
+ gr.Markdown("### Audience", elem_classes=["audience-title", "panel-heading"])
1910
  prop_input = gr.Dropdown(
1911
+ choices=PROP_DROPDOWN_CHOICES,
1912
  value="rubber duck",
1913
  allow_custom_value=True,
1914
+ filterable=True,
1915
+ label="Choose a prop",
1916
+ info="Pick from the chest or type your own, then press **Throw Prop**.",
1917
+ elem_classes=["prop-picker"],
1918
  )
1919
  with gr.Row():
1920
  throw_prop_button = gr.Button(
 
1937
  label="Transcript",
1938
  lines=7,
1939
  interactive=False,
1940
+ elem_classes=["transcript-box", "no-field-label"],
1941
  )
1942
  with gr.Accordion("Behind the Curtain", open=False):
1943
  director_output = gr.Textbox(
 
1945
  label="Director Log",
1946
  lines=6,
1947
  interactive=False,
1948
+ elem_classes=["no-field-label"],
1949
  )
1950
  with gr.Accordion("Trace / Debug", open=False):
1951
  trace_output = gr.Textbox(
 
1953
  label="Trace Events",
1954
  lines=6,
1955
  interactive=False,
1956
+ elem_classes=["no-field-label"],
1957
  )
1958
  with gr.Accordion("Backend", open=False):
1959
  backend_select = gr.Dropdown(
 
1984
  label="Use deterministic actor lines for OpenBMB full-act playback",
1985
  interactive=True,
1986
  )
1987
+ warm_up_button = gr.Button("Warm up OpenBMB", elem_classes=["cue-action"])
1988
  backend_output = gr.Textbox(
1989
  value=EMPTY_BACKEND,
1990
  label="Model Settings",
1991
  lines=8,
1992
  interactive=False,
1993
+ elem_classes=["no-field-label"],
1994
  )
1995
 
1996
  create_button.click(
 
2055
 
2056
 
2057
  if __name__ == "__main__":
2058
+ # For live reload on save (CSS in CUSTOM_CSS, layout, handlers), run:
2059
+ # uv run gradio app.py
2060
+ # Spaces and plain `uv run python app.py` stay non-reloading.
2061
+ app.launch(css=CUSTOM_CSS, theme=THEATER_THEME)
puppet_theater/models.py CHANGED
@@ -67,3 +67,5 @@ class TheaterSession:
67
  backend_model_id: str | None = None
68
  backend_max_new_tokens: int = 80
69
  backend_temperature: float = 0.8
 
 
 
67
  backend_model_id: str | None = None
68
  backend_max_new_tokens: int = 80
69
  backend_temperature: float = 0.8
70
+ # One-shot: show opening-curtain animation on the first stage render after create.
71
+ play_opening_curtain: bool = False
puppet_theater/session.py CHANGED
@@ -98,4 +98,5 @@ def create_show_from_premise(
98
  backend_model_id=backend_model_id,
99
  backend_max_new_tokens=backend_max_new_tokens,
100
  backend_temperature=backend_temperature,
 
101
  )
 
98
  backend_model_id=backend_model_id,
99
  backend_max_new_tokens=backend_max_new_tokens,
100
  backend_temperature=backend_temperature,
101
+ play_opening_curtain=True,
102
  )