48 lines
1.4 KiB
Python
48 lines
1.4 KiB
Python
from dataclasses import dataclass
|
|
from typing import Optional
|
|
import json
|
|
|
|
REQUIRED_FIELDS = {
|
|
"name", "age", "voice_suggestion", "location", "identity",
|
|
"situation", "reason_calling", "opening_line", "secret_want",
|
|
"specific_details", "emotional_register"
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class CallerIdentity:
|
|
name: str
|
|
age: int
|
|
voice_suggestion: str
|
|
location: str
|
|
identity: str
|
|
situation: str
|
|
reason_calling: str
|
|
opening_line: str
|
|
secret_want: str
|
|
specific_details: list[str]
|
|
emotional_register: str
|
|
# set after voice validation
|
|
voice_resolved: Optional[str] = None
|
|
|
|
|
|
def parse_batch_response(raw: str) -> list[CallerIdentity]:
|
|
data = json.loads(raw)
|
|
callers = data.get("callers", [])
|
|
result = []
|
|
for c in callers:
|
|
missing = REQUIRED_FIELDS - set(c.keys())
|
|
if missing:
|
|
raise ValueError(f"CallerIdentity missing fields: {missing}")
|
|
result.append(CallerIdentity(**{k: c[k] for k in REQUIRED_FIELDS}))
|
|
return result
|
|
|
|
|
|
def resolve_voice(suggestion: str, roster: list[str]) -> str:
|
|
"""Map sonnet's voice suggestion to a real voice in the roster.
|
|
Case-insensitive exact match; deterministic fallback to first roster entry."""
|
|
if not suggestion or not roster:
|
|
return roster[0] if roster else ""
|
|
lower_map = {v.lower(): v for v in roster}
|
|
return lower_map.get(suggestion.lower(), roster[0])
|