Commit ·
29b2e23
1
Parent(s): bed519a
Better speaker casting and strip stage directions
Browse files
app.py
CHANGED
|
@@ -145,6 +145,20 @@ STYLE:
|
|
| 145 |
- Speakers should reference what the other person said, react naturally, and build on previous points
|
| 146 |
- Include personality — people joke, digress slightly, use analogies, get passionate about topics
|
| 147 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
FORMAT RULES:
|
| 149 |
- Start with a title on the FIRST LINE in this format: "Title: Your Script Title Here"
|
| 150 |
- Then a blank line, then the dialogue
|
|
@@ -155,6 +169,44 @@ FORMAT RULES:
|
|
| 155 |
- Output ONLY the title and script — no stage directions, no commentary, no preamble"""
|
| 156 |
|
| 157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
def generate_script_from_prompt(prompt: str) -> tuple[list[dict], int, str]:
|
| 159 |
"""Returns (turns, num_speakers, title)."""
|
| 160 |
system = SCRIPT_SYSTEM_PROMPT.format(max_words=SCRIPT_MAX_WORDS)
|
|
@@ -176,6 +228,12 @@ def generate_script_from_prompt(prompt: str) -> tuple[list[dict], int, str]:
|
|
| 176 |
raw = "\n".join(lines[1:])
|
| 177 |
|
| 178 |
turns = parse_script_to_turns(raw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
turns = turns[:MAX_TURNS]
|
| 180 |
total_words = sum(len(t["text"].split()) for t in turns)
|
| 181 |
while total_words > MAX_SCRIPT_WORDS and turns:
|
|
|
|
| 145 |
- Speakers should reference what the other person said, react naturally, and build on previous points
|
| 146 |
- Include personality — people joke, digress slightly, use analogies, get passionate about topics
|
| 147 |
|
| 148 |
+
CASTING (IMPORTANT):
|
| 149 |
+
- Before writing, identify EVERY character in the scenario — including any who enter, interrupt, or arrive later (parents, bosses, narrators, bystanders, etc.)
|
| 150 |
+
- If the prompt mentions someone at all, they get their own Speaker number (up to 4 max)
|
| 151 |
+
- Example: "Two kids argue until their mom walks in" = 3 speakers, not 2
|
| 152 |
+
- Example: "A detective interviews a suspect while a lawyer objects" = 3 speakers
|
| 153 |
+
- Assign Speaker numbers in order of first appearance
|
| 154 |
+
|
| 155 |
+
STRICT NO-NO's (VibeVoice reads these LITERALLY as spoken words — never use them):
|
| 156 |
+
- NO bracketed stage directions: [whispering], [sighs], [laughs], [door slams], [pause], [music], etc.
|
| 157 |
+
- NO parenthetical emotion cues: (softly), (angrily), (laughing), (sarcastically), etc.
|
| 158 |
+
- NO asterisk actions: *laughs*, *sighs*, *door opens*, etc.
|
| 159 |
+
- NO scene headings, sound effects, or narration lines
|
| 160 |
+
- Convey emotion through WORD CHOICE and natural speech only (e.g., actually type "hahaha" or "ugh" or "whoa" as part of the dialogue itself)
|
| 161 |
+
|
| 162 |
FORMAT RULES:
|
| 163 |
- Start with a title on the FIRST LINE in this format: "Title: Your Script Title Here"
|
| 164 |
- Then a blank line, then the dialogue
|
|
|
|
| 169 |
- Output ONLY the title and script — no stage directions, no commentary, no preamble"""
|
| 170 |
|
| 171 |
|
| 172 |
+
# Strip bracketed stage directions, parenthetical cues, and asterisk actions.
|
| 173 |
+
# VibeVoice reads these literally, so we defensively remove them even if the LLM sneaks them in.
|
| 174 |
+
_STAGE_DIRECTION_PATTERNS = [
|
| 175 |
+
re.compile(r"\[[^\]]*\]"), # [whispering], [sighs], [door slams]
|
| 176 |
+
re.compile(r"\*[^*\n]+\*"), # *laughs*, *sighs*
|
| 177 |
+
]
|
| 178 |
+
# Common parenthetical emotion/action cues — only strip short ones that look like directions,
|
| 179 |
+
# not legitimate asides like "(which, by the way, is huge)".
|
| 180 |
+
_PAREN_CUE_WORDS = {
|
| 181 |
+
"softly", "angrily", "laughing", "laughs", "sighs", "sighing", "whispers", "whispering",
|
| 182 |
+
"shouts", "shouting", "sarcastically", "sarcastic", "nervously", "excitedly",
|
| 183 |
+
"quietly", "loudly", "pauses", "pause", "crying", "sobbing", "giggling", "chuckling",
|
| 184 |
+
"sternly", "coldly", "warmly", "mockingly", "sadly", "happily", "angry", "sad",
|
| 185 |
+
"clears throat", "beat", "aside", "muttering", "mutters", "groans", "groaning",
|
| 186 |
+
}
|
| 187 |
+
_PAREN_PATTERN = re.compile(r"\(([^)\n]{1,40})\)")
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
def sanitize_dialogue(text: str) -> str:
|
| 191 |
+
"""Remove stage directions VibeVoice would read as literal words."""
|
| 192 |
+
for pat in _STAGE_DIRECTION_PATTERNS:
|
| 193 |
+
text = pat.sub("", text)
|
| 194 |
+
|
| 195 |
+
def _paren_filter(m):
|
| 196 |
+
inside = m.group(1).strip().lower().rstrip(".!?")
|
| 197 |
+
if inside in _PAREN_CUE_WORDS:
|
| 198 |
+
return ""
|
| 199 |
+
# Also strip single-word parentheticals ending in -ly (adverbs)
|
| 200 |
+
if " " not in inside and inside.endswith("ly"):
|
| 201 |
+
return ""
|
| 202 |
+
return m.group(0) # keep legitimate asides
|
| 203 |
+
|
| 204 |
+
text = _PAREN_PATTERN.sub(_paren_filter, text)
|
| 205 |
+
# Collapse whitespace the stripping may have introduced
|
| 206 |
+
text = re.sub(r"\s{2,}", " ", text).strip()
|
| 207 |
+
return text
|
| 208 |
+
|
| 209 |
+
|
| 210 |
def generate_script_from_prompt(prompt: str) -> tuple[list[dict], int, str]:
|
| 211 |
"""Returns (turns, num_speakers, title)."""
|
| 212 |
system = SCRIPT_SYSTEM_PROMPT.format(max_words=SCRIPT_MAX_WORDS)
|
|
|
|
| 228 |
raw = "\n".join(lines[1:])
|
| 229 |
|
| 230 |
turns = parse_script_to_turns(raw)
|
| 231 |
+
# Scrub stage directions from each turn, drop any turn that becomes empty
|
| 232 |
+
turns = [
|
| 233 |
+
{"speaker": t["speaker"], "text": sanitize_dialogue(t["text"])}
|
| 234 |
+
for t in turns
|
| 235 |
+
]
|
| 236 |
+
turns = [t for t in turns if t["text"].strip()]
|
| 237 |
turns = turns[:MAX_TURNS]
|
| 238 |
total_words = sum(len(t["text"].split()) for t in turns)
|
| 239 |
while total_words > MAX_SCRIPT_WORDS and turns:
|