cajpany's picture
Upload folder using huggingface_hub
fc9b7e2 verified
Raw
History Blame Contribute Delete
4.34 kB
"""Shared script schema for writers, renderers, mixers, and UI."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any
class Genre(StrEnum):
NOIR = "noir"
WEIRD = "weird"
WESTERN = "western"
ROMANCE = "romance"
COMEDY = "comedy"
HINDI_MELODRAMA = "hindi_melodrama"
class Delivery(StrEnum):
NEUTRAL = "neutral"
SLOW = "slow"
URGENT = "urgent"
WHISPER = "whisper"
BOOMING = "booming"
DEADPAN = "deadpan"
AGITATED = "agitated"
class VoiceID(StrEnum):
"""Locked Kokoro voice roster verified against hexgrad/Kokoro-82M."""
ANNOUNCER = "am_michael"
DETECTIVE = "am_onyx"
INGENUE = "af_bella"
COMIC = "am_puck"
VILLAIN = "am_fenrir"
ELDER = "bm_george"
HINDI_FEMALE = "hf_alpha"
HINDI_MALE = "hm_omega"
@dataclass(frozen=True)
class CastMember:
id: str
name: str
voice: VoiceID
description: str
@dataclass(frozen=True)
class Line:
cast: str
text: str
delivery: Delivery = Delivery.NEUTRAL
@dataclass(frozen=True)
class Scene:
title: str
sfx: list[str]
lines: list[Line]
@dataclass(frozen=True)
class MusicPlan:
genre: Genre
opening_sting: str
bed: str
closing_sting: str
@dataclass(frozen=True)
class Script:
title: str
logline: str
genre: Genre
cast: list[CastMember]
scenes: list[Scene]
music: MusicPlan
estimated_seconds: int = field(default=75)
def validate(self) -> None:
if not self.title.strip():
raise ValueError("script title is required")
if not self.logline.strip():
raise ValueError("script logline is required")
if not 30 <= self.estimated_seconds <= 120:
raise ValueError("estimated_seconds must be between 30 and 120")
if len(self.cast) < 2:
raise ValueError("script must define at least two cast members")
cast_ids = {member.id for member in self.cast}
if len(cast_ids) != len(self.cast):
raise ValueError("cast member ids must be unique")
if not self.scenes:
raise ValueError("script must contain at least one scene")
for scene in self.scenes:
if not scene.lines:
raise ValueError(f"scene '{scene.title}' must contain at least one line")
for line in scene.lines:
if line.cast not in cast_ids:
raise ValueError(f"line references unknown cast id '{line.cast}'")
if not line.text.strip():
raise ValueError("line text cannot be empty")
if self.music.genre != self.genre:
raise ValueError("music genre must match script genre")
def validate_script(script: Script) -> Script:
script.validate()
return script
def script_from_json_dict(payload: dict[str, Any]) -> Script:
"""Build and validate a Script from a JSON-compatible dict."""
script = Script(
title=str(payload["title"]),
logline=str(payload["logline"]),
genre=Genre(payload["genre"]),
cast=[
CastMember(
id=str(member["id"]),
name=str(member["name"]),
voice=VoiceID(member["voice"]),
description=str(member.get("description", "")),
)
for member in payload["cast"]
],
scenes=[
Scene(
title=str(scene["title"]),
sfx=[str(item) for item in scene.get("sfx", [])],
lines=[
Line(
cast=str(line["cast"]),
text=str(line["text"]),
delivery=Delivery(line.get("delivery", Delivery.NEUTRAL)),
)
for line in scene["lines"]
],
)
for scene in payload["scenes"]
],
music=MusicPlan(
genre=Genre(payload["music"]["genre"]),
opening_sting=str(payload["music"]["opening_sting"]),
bed=str(payload["music"]["bed"]),
closing_sting=str(payload["music"]["closing_sting"]),
),
estimated_seconds=int(payload.get("estimated_seconds", 75)),
)
script.validate()
return script