Commit ·
9543536
1
Parent(s): e44d065
refactor: add model backend foundation
Browse files- app.py +33 -11
- puppet_theater/__init__.py +7 -1
- puppet_theater/backends.py +180 -0
- puppet_theater/director.py +19 -61
- puppet_theater/models.py +27 -0
- puppet_theater/prompts.py +57 -0
- puppet_theater/session.py +3 -0
app.py
CHANGED
|
@@ -27,6 +27,7 @@ EMPTY_STAGE = """
|
|
| 27 |
EMPTY_TRANSCRIPT = "No show yet. The transcript will appear here."
|
| 28 |
EMPTY_DIRECTOR_LOG = "No director notes yet."
|
| 29 |
EMPTY_TRACE = "No trace events yet."
|
|
|
|
| 30 |
PLAYBACK_DELAY_SECONDS = 0.75
|
| 31 |
PROP_EMOJI = {
|
| 32 |
"rubber duck": "🐤",
|
|
@@ -911,12 +912,23 @@ def render_trace(session: TheaterSession | None) -> str:
|
|
| 911 |
return "\n".join(f"- {entry}" for entry in session.trace_events)
|
| 912 |
|
| 913 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
def render_outputs(session: TheaterSession | None):
|
| 915 |
return (
|
| 916 |
render_stage(session),
|
| 917 |
render_transcript(session),
|
| 918 |
render_director_log(session),
|
| 919 |
render_trace(session),
|
|
|
|
| 920 |
)
|
| 921 |
|
| 922 |
|
|
@@ -929,6 +941,7 @@ def create_show(premise: str, session: TheaterSession | None):
|
|
| 929 |
"No premise yet. Add a premise to raise the curtain.",
|
| 930 |
EMPTY_DIRECTOR_LOG,
|
| 931 |
EMPTY_TRACE,
|
|
|
|
| 932 |
)
|
| 933 |
|
| 934 |
session = create_show_from_premise(premise)
|
|
@@ -945,12 +958,13 @@ def reset_show():
|
|
| 945 |
EMPTY_TRANSCRIPT,
|
| 946 |
EMPTY_DIRECTOR_LOG,
|
| 947 |
EMPTY_TRACE,
|
|
|
|
| 948 |
)
|
| 949 |
|
| 950 |
|
| 951 |
def advance_one_beat(session: TheaterSession | None):
|
| 952 |
if session is None:
|
| 953 |
-
return None, EMPTY_STAGE, "Create a show before running a beat.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE
|
| 954 |
|
| 955 |
session = run_one_beat(session)
|
| 956 |
return session, *render_outputs(session)
|
|
@@ -958,7 +972,7 @@ def advance_one_beat(session: TheaterSession | None):
|
|
| 958 |
|
| 959 |
def advance_full_act(session: TheaterSession | None):
|
| 960 |
if session is None:
|
| 961 |
-
yield None, EMPTY_STAGE, "Create a show before running the full act.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE
|
| 962 |
return
|
| 963 |
|
| 964 |
if session.beat_index >= session.max_beats:
|
|
@@ -975,7 +989,7 @@ def advance_full_act(session: TheaterSession | None):
|
|
| 975 |
|
| 976 |
def throw_audience_prop(session: TheaterSession | None, prop_name: str):
|
| 977 |
if session is None:
|
| 978 |
-
return None, EMPTY_STAGE, "Create a show before throwing a prop.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE
|
| 979 |
|
| 980 |
session = throw_prop(session, prop_name)
|
| 981 |
return session, *render_outputs(session)
|
|
@@ -983,7 +997,7 @@ def throw_audience_prop(session: TheaterSession | None, prop_name: str):
|
|
| 983 |
|
| 984 |
def summon_audience_actor(session: TheaterSession | None, actor_name: str):
|
| 985 |
if session is None:
|
| 986 |
-
return None, EMPTY_STAGE, "Create a show before summoning an actor.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE
|
| 987 |
|
| 988 |
session = summon_actor(session, actor_name)
|
| 989 |
return session, *render_outputs(session)
|
|
@@ -991,7 +1005,7 @@ def summon_audience_actor(session: TheaterSession | None, actor_name: str):
|
|
| 991 |
|
| 992 |
def request_audience_finale(session: TheaterSession | None):
|
| 993 |
if session is None:
|
| 994 |
-
return None, EMPTY_STAGE, "Create a show before requesting a finale.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE
|
| 995 |
|
| 996 |
session = request_finale(session)
|
| 997 |
return session, *render_outputs(session)
|
|
@@ -1076,36 +1090,43 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 1076 |
lines=6,
|
| 1077 |
interactive=False,
|
| 1078 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1079 |
|
| 1080 |
create_button.click(
|
| 1081 |
create_show,
|
| 1082 |
inputs=[premise_input, session_state],
|
| 1083 |
-
outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
|
| 1084 |
)
|
| 1085 |
run_one_button.click(
|
| 1086 |
advance_one_beat,
|
| 1087 |
inputs=[session_state],
|
| 1088 |
-
outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
|
| 1089 |
)
|
| 1090 |
run_full_button.click(
|
| 1091 |
advance_full_act,
|
| 1092 |
inputs=[session_state],
|
| 1093 |
-
outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
|
| 1094 |
)
|
| 1095 |
throw_prop_button.click(
|
| 1096 |
throw_audience_prop,
|
| 1097 |
inputs=[session_state, prop_input],
|
| 1098 |
-
outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
|
| 1099 |
)
|
| 1100 |
summon_actor_button.click(
|
| 1101 |
summon_audience_actor,
|
| 1102 |
inputs=[session_state, actor_input],
|
| 1103 |
-
outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
|
| 1104 |
)
|
| 1105 |
request_finale_button.click(
|
| 1106 |
request_audience_finale,
|
| 1107 |
inputs=[session_state],
|
| 1108 |
-
outputs=[session_state, stage_output, transcript_output, director_output, trace_output],
|
| 1109 |
)
|
| 1110 |
reset_button.click(
|
| 1111 |
reset_show,
|
|
@@ -1118,6 +1139,7 @@ with gr.Blocks(title="AI Puppet Theater") as app:
|
|
| 1118 |
transcript_output,
|
| 1119 |
director_output,
|
| 1120 |
trace_output,
|
|
|
|
| 1121 |
],
|
| 1122 |
)
|
| 1123 |
|
|
|
|
| 27 |
EMPTY_TRANSCRIPT = "No show yet. The transcript will appear here."
|
| 28 |
EMPTY_DIRECTOR_LOG = "No director notes yet."
|
| 29 |
EMPTY_TRACE = "No trace events yet."
|
| 30 |
+
EMPTY_BACKEND = "Active backend: deterministic\nFallback: deterministic safety path enabled"
|
| 31 |
PLAYBACK_DELAY_SECONDS = 0.75
|
| 32 |
PROP_EMOJI = {
|
| 33 |
"rubber duck": "🐤",
|
|
|
|
| 912 |
return "\n".join(f"- {entry}" for entry in session.trace_events)
|
| 913 |
|
| 914 |
|
| 915 |
+
def render_backend_settings(session: TheaterSession | None) -> str:
|
| 916 |
+
backend_name = session.backend_name if session is not None else "deterministic"
|
| 917 |
+
return (
|
| 918 |
+
f"Active backend: {backend_name}\n"
|
| 919 |
+
"Available backends: deterministic\n"
|
| 920 |
+
"LLM backends: not configured\n"
|
| 921 |
+
"Fallback behavior: invalid model output falls back to deterministic actor lines"
|
| 922 |
+
)
|
| 923 |
+
|
| 924 |
+
|
| 925 |
def render_outputs(session: TheaterSession | None):
|
| 926 |
return (
|
| 927 |
render_stage(session),
|
| 928 |
render_transcript(session),
|
| 929 |
render_director_log(session),
|
| 930 |
render_trace(session),
|
| 931 |
+
render_backend_settings(session),
|
| 932 |
)
|
| 933 |
|
| 934 |
|
|
|
|
| 941 |
"No premise yet. Add a premise to raise the curtain.",
|
| 942 |
EMPTY_DIRECTOR_LOG,
|
| 943 |
EMPTY_TRACE,
|
| 944 |
+
EMPTY_BACKEND,
|
| 945 |
)
|
| 946 |
|
| 947 |
session = create_show_from_premise(premise)
|
|
|
|
| 958 |
EMPTY_TRANSCRIPT,
|
| 959 |
EMPTY_DIRECTOR_LOG,
|
| 960 |
EMPTY_TRACE,
|
| 961 |
+
EMPTY_BACKEND,
|
| 962 |
)
|
| 963 |
|
| 964 |
|
| 965 |
def advance_one_beat(session: TheaterSession | None):
|
| 966 |
if session is None:
|
| 967 |
+
return None, EMPTY_STAGE, "Create a show before running a beat.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE, EMPTY_BACKEND
|
| 968 |
|
| 969 |
session = run_one_beat(session)
|
| 970 |
return session, *render_outputs(session)
|
|
|
|
| 972 |
|
| 973 |
def advance_full_act(session: TheaterSession | None):
|
| 974 |
if session is None:
|
| 975 |
+
yield None, EMPTY_STAGE, "Create a show before running the full act.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE, EMPTY_BACKEND
|
| 976 |
return
|
| 977 |
|
| 978 |
if session.beat_index >= session.max_beats:
|
|
|
|
| 989 |
|
| 990 |
def throw_audience_prop(session: TheaterSession | None, prop_name: str):
|
| 991 |
if session is None:
|
| 992 |
+
return None, EMPTY_STAGE, "Create a show before throwing a prop.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE, EMPTY_BACKEND
|
| 993 |
|
| 994 |
session = throw_prop(session, prop_name)
|
| 995 |
return session, *render_outputs(session)
|
|
|
|
| 997 |
|
| 998 |
def summon_audience_actor(session: TheaterSession | None, actor_name: str):
|
| 999 |
if session is None:
|
| 1000 |
+
return None, EMPTY_STAGE, "Create a show before summoning an actor.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE, EMPTY_BACKEND
|
| 1001 |
|
| 1002 |
session = summon_actor(session, actor_name)
|
| 1003 |
return session, *render_outputs(session)
|
|
|
|
| 1005 |
|
| 1006 |
def request_audience_finale(session: TheaterSession | None):
|
| 1007 |
if session is None:
|
| 1008 |
+
return None, EMPTY_STAGE, "Create a show before requesting a finale.", EMPTY_DIRECTOR_LOG, EMPTY_TRACE, EMPTY_BACKEND
|
| 1009 |
|
| 1010 |
session = request_finale(session)
|
| 1011 |
return session, *render_outputs(session)
|
|
|
|
| 1090 |
lines=6,
|
| 1091 |
interactive=False,
|
| 1092 |
)
|
| 1093 |
+
with gr.Accordion("Backend", open=False):
|
| 1094 |
+
backend_output = gr.Textbox(
|
| 1095 |
+
value=EMPTY_BACKEND,
|
| 1096 |
+
label="Model Settings",
|
| 1097 |
+
lines=4,
|
| 1098 |
+
interactive=False,
|
| 1099 |
+
)
|
| 1100 |
|
| 1101 |
create_button.click(
|
| 1102 |
create_show,
|
| 1103 |
inputs=[premise_input, session_state],
|
| 1104 |
+
outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
|
| 1105 |
)
|
| 1106 |
run_one_button.click(
|
| 1107 |
advance_one_beat,
|
| 1108 |
inputs=[session_state],
|
| 1109 |
+
outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
|
| 1110 |
)
|
| 1111 |
run_full_button.click(
|
| 1112 |
advance_full_act,
|
| 1113 |
inputs=[session_state],
|
| 1114 |
+
outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
|
| 1115 |
)
|
| 1116 |
throw_prop_button.click(
|
| 1117 |
throw_audience_prop,
|
| 1118 |
inputs=[session_state, prop_input],
|
| 1119 |
+
outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
|
| 1120 |
)
|
| 1121 |
summon_actor_button.click(
|
| 1122 |
summon_audience_actor,
|
| 1123 |
inputs=[session_state, actor_input],
|
| 1124 |
+
outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
|
| 1125 |
)
|
| 1126 |
request_finale_button.click(
|
| 1127 |
request_audience_finale,
|
| 1128 |
inputs=[session_state],
|
| 1129 |
+
outputs=[session_state, stage_output, transcript_output, director_output, trace_output, backend_output],
|
| 1130 |
)
|
| 1131 |
reset_button.click(
|
| 1132 |
reset_show,
|
|
|
|
| 1139 |
transcript_output,
|
| 1140 |
director_output,
|
| 1141 |
trace_output,
|
| 1142 |
+
backend_output,
|
| 1143 |
],
|
| 1144 |
)
|
| 1145 |
|
puppet_theater/__init__.py
CHANGED
|
@@ -1,14 +1,20 @@
|
|
| 1 |
from puppet_theater.actions import request_finale, summon_actor, throw_prop
|
|
|
|
| 2 |
from puppet_theater.director import BEAT_ARC, run_full_act, run_one_beat
|
| 3 |
-
from puppet_theater.models import Actor, Beat, TheaterSession
|
| 4 |
from puppet_theater.session import create_show_from_premise
|
| 5 |
|
| 6 |
__all__ = [
|
| 7 |
"Actor",
|
|
|
|
| 8 |
"BEAT_ARC",
|
| 9 |
"Beat",
|
|
|
|
|
|
|
| 10 |
"TheaterSession",
|
| 11 |
"create_show_from_premise",
|
|
|
|
|
|
|
| 12 |
"request_finale",
|
| 13 |
"run_full_act",
|
| 14 |
"run_one_beat",
|
|
|
|
| 1 |
from puppet_theater.actions import request_finale, summon_actor, throw_prop
|
| 2 |
+
from puppet_theater.backends import DeterministicBackend, ModelBackend, generate_actor_response, parse_actor_output
|
| 3 |
from puppet_theater.director import BEAT_ARC, run_full_act, run_one_beat
|
| 4 |
+
from puppet_theater.models import Actor, ActorResponse, Beat, TheaterSession
|
| 5 |
from puppet_theater.session import create_show_from_premise
|
| 6 |
|
| 7 |
__all__ = [
|
| 8 |
"Actor",
|
| 9 |
+
"ActorResponse",
|
| 10 |
"BEAT_ARC",
|
| 11 |
"Beat",
|
| 12 |
+
"DeterministicBackend",
|
| 13 |
+
"ModelBackend",
|
| 14 |
"TheaterSession",
|
| 15 |
"create_show_from_premise",
|
| 16 |
+
"generate_actor_response",
|
| 17 |
+
"parse_actor_output",
|
| 18 |
"request_finale",
|
| 19 |
"run_full_act",
|
| 20 |
"run_one_beat",
|
puppet_theater/backends.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from abc import ABC, abstractmethod
|
| 2 |
+
from dataclasses import dataclass
|
| 3 |
+
import json
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
from pydantic import ValidationError
|
| 7 |
+
|
| 8 |
+
from puppet_theater.models import Actor, ActorResponse, TheaterSession
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
MAX_ACTOR_LINE_CHARS = 220
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
@dataclass(frozen=True)
|
| 15 |
+
class BackendGeneration:
|
| 16 |
+
response: ActorResponse
|
| 17 |
+
backend_name: str
|
| 18 |
+
fallback_used: bool
|
| 19 |
+
validation_status: str
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ModelBackend(ABC):
|
| 23 |
+
name: str = "base"
|
| 24 |
+
|
| 25 |
+
@abstractmethod
|
| 26 |
+
def generate_actor_response(
|
| 27 |
+
self,
|
| 28 |
+
session: TheaterSession,
|
| 29 |
+
beat_type: str,
|
| 30 |
+
speaker: Actor,
|
| 31 |
+
prop: str | None,
|
| 32 |
+
) -> ActorResponse | dict[str, Any] | str:
|
| 33 |
+
"""Return raw or structured actor output for one beat."""
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class DeterministicBackend(ModelBackend):
|
| 37 |
+
name = "deterministic"
|
| 38 |
+
|
| 39 |
+
def generate_actor_response(
|
| 40 |
+
self,
|
| 41 |
+
session: TheaterSession,
|
| 42 |
+
beat_type: str,
|
| 43 |
+
speaker: Actor,
|
| 44 |
+
prop: str | None,
|
| 45 |
+
) -> ActorResponse:
|
| 46 |
+
return deterministic_actor_response(session, beat_type, speaker, prop)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def deterministic_actor_response(
|
| 50 |
+
session: TheaterSession,
|
| 51 |
+
beat_type: str,
|
| 52 |
+
speaker: Actor,
|
| 53 |
+
prop: str | None,
|
| 54 |
+
) -> ActorResponse:
|
| 55 |
+
return ActorResponse(
|
| 56 |
+
line=_line_for_beat(session, beat_type, speaker, prop),
|
| 57 |
+
emotion=_emotion_for_beat(beat_type),
|
| 58 |
+
gesture=_gesture_for_beat(beat_type),
|
| 59 |
+
stage_effect=_effect_for_beat(beat_type),
|
| 60 |
+
tool_request=None,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def generate_actor_response(
|
| 65 |
+
session: TheaterSession,
|
| 66 |
+
beat_type: str,
|
| 67 |
+
speaker: Actor,
|
| 68 |
+
prop: str | None,
|
| 69 |
+
backend: ModelBackend | None = None,
|
| 70 |
+
) -> BackendGeneration:
|
| 71 |
+
active_backend = backend or DeterministicBackend()
|
| 72 |
+
raw_output = active_backend.generate_actor_response(session, beat_type, speaker, prop)
|
| 73 |
+
response, validation_status = parse_actor_output(raw_output)
|
| 74 |
+
if response is not None:
|
| 75 |
+
return BackendGeneration(
|
| 76 |
+
response=response,
|
| 77 |
+
backend_name=active_backend.name,
|
| 78 |
+
fallback_used=False,
|
| 79 |
+
validation_status=validation_status,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
fallback_response = deterministic_actor_response(session, beat_type, speaker, prop)
|
| 83 |
+
return BackendGeneration(
|
| 84 |
+
response=fallback_response,
|
| 85 |
+
backend_name=active_backend.name,
|
| 86 |
+
fallback_used=True,
|
| 87 |
+
validation_status=validation_status,
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def parse_actor_output(raw_output: ActorResponse | dict[str, Any] | str) -> tuple[ActorResponse | None, str]:
|
| 92 |
+
parsed = _coerce_actor_output(raw_output)
|
| 93 |
+
if parsed is None:
|
| 94 |
+
return None, "invalid_schema"
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
response = ActorResponse.model_validate(parsed)
|
| 98 |
+
except ValidationError:
|
| 99 |
+
return None, "invalid_required_fields"
|
| 100 |
+
|
| 101 |
+
if not response.line.strip():
|
| 102 |
+
return None, "invalid_empty_line"
|
| 103 |
+
|
| 104 |
+
if len(response.line) > MAX_ACTOR_LINE_CHARS:
|
| 105 |
+
capped_line = response.line[: MAX_ACTOR_LINE_CHARS - 3].rstrip() + "..."
|
| 106 |
+
response = response.model_copy(update={"line": capped_line})
|
| 107 |
+
return response, "valid_line_capped"
|
| 108 |
+
|
| 109 |
+
return response, "valid"
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _coerce_actor_output(raw_output: ActorResponse | dict[str, Any] | str) -> ActorResponse | dict[str, Any] | None:
|
| 113 |
+
if isinstance(raw_output, ActorResponse):
|
| 114 |
+
return raw_output
|
| 115 |
+
if isinstance(raw_output, dict):
|
| 116 |
+
return raw_output
|
| 117 |
+
if isinstance(raw_output, str):
|
| 118 |
+
text = raw_output.strip()
|
| 119 |
+
if not text:
|
| 120 |
+
return None
|
| 121 |
+
try:
|
| 122 |
+
decoded = json.loads(text)
|
| 123 |
+
except json.JSONDecodeError:
|
| 124 |
+
return None
|
| 125 |
+
return decoded if isinstance(decoded, dict) else None
|
| 126 |
+
return None
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _line_for_beat(
|
| 130 |
+
session: TheaterSession,
|
| 131 |
+
beat_type: str,
|
| 132 |
+
speaker: Actor,
|
| 133 |
+
prop: str | None,
|
| 134 |
+
) -> str:
|
| 135 |
+
if prop is not None:
|
| 136 |
+
return f"I shall use this {prop} as evidence, a prop, and possibly a tiny emotional support object."
|
| 137 |
+
if beat_type == "setup":
|
| 138 |
+
return f"I see it clearly: {session.premise}, and somehow I am in charge."
|
| 139 |
+
if beat_type == "denial_or_contradiction":
|
| 140 |
+
return "Absolutely not. The premise is innocent, which is exactly what makes it suspicious."
|
| 141 |
+
if beat_type == "evidence_or_prop":
|
| 142 |
+
return "I found a prop with fingerprints, glitter, and a very dramatic attitude."
|
| 143 |
+
if beat_type == "secret_reveal":
|
| 144 |
+
return f"I confess: {speaker.secret}"
|
| 145 |
+
if beat_type == "chaos_or_intervention":
|
| 146 |
+
return "The audience has interrupted with imaginary confetti, so everyone must panic gracefully."
|
| 147 |
+
return "Curtain call! We solved nothing, learned everything, and bowed before the wobble got worse."
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def _emotion_for_beat(beat_type: str) -> str:
|
| 151 |
+
return {
|
| 152 |
+
"setup": "curious",
|
| 153 |
+
"denial_or_contradiction": "defensive",
|
| 154 |
+
"evidence_or_prop": "suspicious",
|
| 155 |
+
"secret_reveal": "confessional",
|
| 156 |
+
"chaos_or_intervention": "frantic",
|
| 157 |
+
"finale": "triumphant",
|
| 158 |
+
}[beat_type]
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def _gesture_for_beat(beat_type: str) -> str:
|
| 162 |
+
return {
|
| 163 |
+
"setup": "raise_curtain",
|
| 164 |
+
"denial_or_contradiction": "shake_head",
|
| 165 |
+
"evidence_or_prop": "present_prop",
|
| 166 |
+
"secret_reveal": "lean_to_audience",
|
| 167 |
+
"chaos_or_intervention": "flail_politely",
|
| 168 |
+
"finale": "deep_bow",
|
| 169 |
+
}[beat_type]
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
def _effect_for_beat(beat_type: str) -> str:
|
| 173 |
+
return {
|
| 174 |
+
"setup": "warm_spotlight",
|
| 175 |
+
"denial_or_contradiction": "quick_blackout",
|
| 176 |
+
"evidence_or_prop": "prop_table_glow",
|
| 177 |
+
"secret_reveal": "single_spotlight",
|
| 178 |
+
"chaos_or_intervention": "confetti_rustle",
|
| 179 |
+
"finale": "curtain_fall",
|
| 180 |
+
}[beat_type]
|
puppet_theater/director.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
from puppet_theater.models import Beat, TheaterSession
|
| 2 |
|
| 3 |
|
|
@@ -11,28 +12,6 @@ BEAT_ARC = [
|
|
| 11 |
]
|
| 12 |
|
| 13 |
|
| 14 |
-
def _line_for_beat(
|
| 15 |
-
session: TheaterSession,
|
| 16 |
-
beat_type: str,
|
| 17 |
-
speaker_name: str,
|
| 18 |
-
prop: str | None,
|
| 19 |
-
) -> str:
|
| 20 |
-
if prop is not None:
|
| 21 |
-
return f"I shall use this {prop} as evidence, a prop, and possibly a tiny emotional support object."
|
| 22 |
-
if beat_type == "setup":
|
| 23 |
-
return f"I see it clearly: {session.premise}, and somehow I am in charge."
|
| 24 |
-
if beat_type == "denial_or_contradiction":
|
| 25 |
-
return "Absolutely not. The premise is innocent, which is exactly what makes it suspicious."
|
| 26 |
-
if beat_type == "evidence_or_prop":
|
| 27 |
-
return "I found a prop with fingerprints, glitter, and a very dramatic attitude."
|
| 28 |
-
if beat_type == "secret_reveal":
|
| 29 |
-
actor = next(actor for actor in session.actors if actor.name == speaker_name)
|
| 30 |
-
return f"I confess: {actor.secret}"
|
| 31 |
-
if beat_type == "chaos_or_intervention":
|
| 32 |
-
return "The audience has interrupted with imaginary confetti, so everyone must panic gracefully."
|
| 33 |
-
return "Curtain call! We solved nothing, learned everything, and bowed before the wobble got worse."
|
| 34 |
-
|
| 35 |
-
|
| 36 |
def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
|
| 37 |
if session is None:
|
| 38 |
return None
|
|
@@ -47,16 +26,17 @@ def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
|
|
| 47 |
prop = session.latest_prop
|
| 48 |
if prop is not None:
|
| 49 |
speaker.held_prop = prop
|
| 50 |
-
|
| 51 |
session.beat_index += 1
|
| 52 |
|
|
|
|
| 53 |
beat = Beat(
|
| 54 |
speaker=speaker.name,
|
| 55 |
-
line=line,
|
| 56 |
-
emotion=
|
| 57 |
-
gesture=
|
| 58 |
-
stage_effect=
|
| 59 |
-
tool_request=
|
| 60 |
)
|
| 61 |
session.transcript.append(beat)
|
| 62 |
if prop is not None:
|
|
@@ -66,7 +46,18 @@ def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
|
|
| 66 |
session.director_log.append(
|
| 67 |
f"Beat {session.beat_index}/{session.max_beats}: {beat_type} assigned to {speaker.name}."
|
| 68 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
session.trace_events.append(f"beat_added:{session.beat_index}:{beat_type}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
if beat_type == "finale":
|
| 72 |
session.beat_index = session.max_beats
|
|
@@ -84,36 +75,3 @@ def run_full_act(session: TheaterSession | None) -> TheaterSession | None:
|
|
| 84 |
while session.beat_index < session.max_beats:
|
| 85 |
run_one_beat(session)
|
| 86 |
return session
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def _emotion_for_beat(beat_type: str) -> str:
|
| 90 |
-
return {
|
| 91 |
-
"setup": "curious",
|
| 92 |
-
"denial_or_contradiction": "defensive",
|
| 93 |
-
"evidence_or_prop": "suspicious",
|
| 94 |
-
"secret_reveal": "confessional",
|
| 95 |
-
"chaos_or_intervention": "frantic",
|
| 96 |
-
"finale": "triumphant",
|
| 97 |
-
}[beat_type]
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def _gesture_for_beat(beat_type: str) -> str:
|
| 101 |
-
return {
|
| 102 |
-
"setup": "raise_curtain",
|
| 103 |
-
"denial_or_contradiction": "shake_head",
|
| 104 |
-
"evidence_or_prop": "present_prop",
|
| 105 |
-
"secret_reveal": "lean_to_audience",
|
| 106 |
-
"chaos_or_intervention": "flail_politely",
|
| 107 |
-
"finale": "deep_bow",
|
| 108 |
-
}[beat_type]
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def _effect_for_beat(beat_type: str) -> str:
|
| 112 |
-
return {
|
| 113 |
-
"setup": "warm_spotlight",
|
| 114 |
-
"denial_or_contradiction": "quick_blackout",
|
| 115 |
-
"evidence_or_prop": "prop_table_glow",
|
| 116 |
-
"secret_reveal": "single_spotlight",
|
| 117 |
-
"chaos_or_intervention": "confetti_rustle",
|
| 118 |
-
"finale": "curtain_fall",
|
| 119 |
-
}[beat_type]
|
|
|
|
| 1 |
+
from puppet_theater.backends import generate_actor_response
|
| 2 |
from puppet_theater.models import Beat, TheaterSession
|
| 3 |
|
| 4 |
|
|
|
|
| 12 |
]
|
| 13 |
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def run_one_beat(session: TheaterSession | None) -> TheaterSession | None:
|
| 16 |
if session is None:
|
| 17 |
return None
|
|
|
|
| 26 |
prop = session.latest_prop
|
| 27 |
if prop is not None:
|
| 28 |
speaker.held_prop = prop
|
| 29 |
+
backend_generation = generate_actor_response(session, beat_type, speaker, prop)
|
| 30 |
session.beat_index += 1
|
| 31 |
|
| 32 |
+
response = backend_generation.response
|
| 33 |
beat = Beat(
|
| 34 |
speaker=speaker.name,
|
| 35 |
+
line=response.line,
|
| 36 |
+
emotion=response.emotion,
|
| 37 |
+
gesture=response.gesture,
|
| 38 |
+
stage_effect=response.stage_effect,
|
| 39 |
+
tool_request=response.tool_request,
|
| 40 |
)
|
| 41 |
session.transcript.append(beat)
|
| 42 |
if prop is not None:
|
|
|
|
| 46 |
session.director_log.append(
|
| 47 |
f"Beat {session.beat_index}/{session.max_beats}: {beat_type} assigned to {speaker.name}."
|
| 48 |
)
|
| 49 |
+
session.director_log.append(
|
| 50 |
+
"Backend "
|
| 51 |
+
f"{backend_generation.backend_name} returned actor output "
|
| 52 |
+
f"({backend_generation.validation_status}, fallback={backend_generation.fallback_used})."
|
| 53 |
+
)
|
| 54 |
session.trace_events.append(f"beat_added:{session.beat_index}:{beat_type}")
|
| 55 |
+
session.trace_events.append(
|
| 56 |
+
"backend_result:"
|
| 57 |
+
f"{backend_generation.backend_name}:"
|
| 58 |
+
f"fallback={backend_generation.fallback_used}:"
|
| 59 |
+
f"validation={backend_generation.validation_status}"
|
| 60 |
+
)
|
| 61 |
|
| 62 |
if beat_type == "finale":
|
| 63 |
session.beat_index = session.max_beats
|
|
|
|
| 75 |
while session.beat_index < session.max_beats:
|
| 76 |
run_one_beat(session)
|
| 77 |
return session
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
puppet_theater/models.py
CHANGED
|
@@ -1,5 +1,31 @@
|
|
| 1 |
from dataclasses import dataclass, field
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
@dataclass
|
| 5 |
class Actor:
|
|
@@ -37,3 +63,4 @@ class TheaterSession:
|
|
| 37 |
director_log: list[str] = field(default_factory=list)
|
| 38 |
trace_events: list[str] = field(default_factory=list)
|
| 39 |
finale_requested: bool = False
|
|
|
|
|
|
| 1 |
from dataclasses import dataclass, field
|
| 2 |
|
| 3 |
+
from pydantic import BaseModel, Field, field_validator
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ActorResponse(BaseModel):
|
| 7 |
+
line: str = Field(description="Short, stage-ready puppet dialogue.")
|
| 8 |
+
emotion: str
|
| 9 |
+
gesture: str
|
| 10 |
+
stage_effect: str
|
| 11 |
+
tool_request: str | None = None
|
| 12 |
+
|
| 13 |
+
@field_validator("line", "emotion", "gesture", "stage_effect")
|
| 14 |
+
@classmethod
|
| 15 |
+
def require_text(cls, value: str) -> str:
|
| 16 |
+
cleaned = " ".join(value.strip().split())
|
| 17 |
+
if not cleaned:
|
| 18 |
+
raise ValueError("field must not be empty")
|
| 19 |
+
return cleaned
|
| 20 |
+
|
| 21 |
+
@field_validator("tool_request")
|
| 22 |
+
@classmethod
|
| 23 |
+
def clean_optional_text(cls, value: str | None) -> str | None:
|
| 24 |
+
if value is None:
|
| 25 |
+
return None
|
| 26 |
+
cleaned = " ".join(value.strip().split())
|
| 27 |
+
return cleaned or None
|
| 28 |
+
|
| 29 |
|
| 30 |
@dataclass
|
| 31 |
class Actor:
|
|
|
|
| 63 |
director_log: list[str] = field(default_factory=list)
|
| 64 |
trace_events: list[str] = field(default_factory=list)
|
| 65 |
finale_requested: bool = False
|
| 66 |
+
backend_name: str = "deterministic"
|
puppet_theater/prompts.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ACTOR_LINE_PROMPT = """You are writing one short puppet line for AI Puppet Theater.
|
| 2 |
+
|
| 3 |
+
Return only JSON with these fields:
|
| 4 |
+
- line: a short, stage-ready spoken line
|
| 5 |
+
- emotion: one concise emotion label
|
| 6 |
+
- gesture: one concise stage gesture label
|
| 7 |
+
- stage_effect: one concise stage effect label
|
| 8 |
+
- tool_request: null or one concise theatrical tool request
|
| 9 |
+
|
| 10 |
+
Show title: {show_title}
|
| 11 |
+
Premise: {premise}
|
| 12 |
+
Setting: {setting}
|
| 13 |
+
Beat type: {beat_type}
|
| 14 |
+
Speaker: {speaker_name}
|
| 15 |
+
Speaker goal: {speaker_goal}
|
| 16 |
+
Speaker style: {speaker_style}
|
| 17 |
+
Audience action: {audience_action}
|
| 18 |
+
Latest prop: {latest_prop}
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
DIRECTOR_DECISION_PROMPT = """You are the Director for AI Puppet Theater.
|
| 23 |
+
|
| 24 |
+
Choose the next beat for a short puppet scene. Keep the scene tight, rotate speakers,
|
| 25 |
+
respect finale requests, and avoid exposing actor secrets unless the beat calls for it.
|
| 26 |
+
|
| 27 |
+
Return only JSON with these fields:
|
| 28 |
+
- beat_type
|
| 29 |
+
- speaker_name
|
| 30 |
+
- reason_summary
|
| 31 |
+
- should_end
|
| 32 |
+
|
| 33 |
+
Show title: {show_title}
|
| 34 |
+
Premise: {premise}
|
| 35 |
+
Current beat: {beat_index}
|
| 36 |
+
Maximum beats: {max_beats}
|
| 37 |
+
Available actors: {actor_names}
|
| 38 |
+
Recent transcript: {recent_transcript}
|
| 39 |
+
Audience action: {audience_action}
|
| 40 |
+
Finale requested: {finale_requested}
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
CASTING_PROMPT = """You are casting a tiny improv puppet show.
|
| 45 |
+
|
| 46 |
+
Create a small ensemble of puppet actors that can perform the premise quickly.
|
| 47 |
+
Return only JSON with an actors array. Each actor must include:
|
| 48 |
+
- name
|
| 49 |
+
- avatar
|
| 50 |
+
- goal
|
| 51 |
+
- secret
|
| 52 |
+
- speaking_style
|
| 53 |
+
- tools
|
| 54 |
+
|
| 55 |
+
Premise: {premise}
|
| 56 |
+
Preferred actor count: {actor_count}
|
| 57 |
+
"""
|
puppet_theater/session.py
CHANGED
|
@@ -61,6 +61,7 @@ def create_show_from_premise(premise: str) -> TheaterSession:
|
|
| 61 |
|
| 62 |
director_log = [
|
| 63 |
"Director created a deterministic six-beat show plan.",
|
|
|
|
| 64 |
f"Setting selected: {setting}.",
|
| 65 |
"Three puppet actors are waiting for the first beat.",
|
| 66 |
]
|
|
@@ -68,6 +69,7 @@ def create_show_from_premise(premise: str) -> TheaterSession:
|
|
| 68 |
"show_created",
|
| 69 |
"actors_created:3",
|
| 70 |
"director_plan_created",
|
|
|
|
| 71 |
]
|
| 72 |
|
| 73 |
return TheaterSession(
|
|
@@ -84,4 +86,5 @@ def create_show_from_premise(premise: str) -> TheaterSession:
|
|
| 84 |
director_log=director_log,
|
| 85 |
trace_events=trace_events,
|
| 86 |
finale_requested=False,
|
|
|
|
| 87 |
)
|
|
|
|
| 61 |
|
| 62 |
director_log = [
|
| 63 |
"Director created a deterministic six-beat show plan.",
|
| 64 |
+
"Active backend: deterministic.",
|
| 65 |
f"Setting selected: {setting}.",
|
| 66 |
"Three puppet actors are waiting for the first beat.",
|
| 67 |
]
|
|
|
|
| 69 |
"show_created",
|
| 70 |
"actors_created:3",
|
| 71 |
"director_plan_created",
|
| 72 |
+
"backend_active:deterministic",
|
| 73 |
]
|
| 74 |
|
| 75 |
return TheaterSession(
|
|
|
|
| 86 |
director_log=director_log,
|
| 87 |
trace_events=trace_events,
|
| 88 |
finale_requested=False,
|
| 89 |
+
backend_name="deterministic",
|
| 90 |
)
|