""" Hook → DramaBox scene drafting via an LLM. The frontend's "Adapt to my niche" step POSTs to /api/tools/scene; this turns a viral-hook structure plus the creator's niche into a directed DramaBox scene. Provider chain: AWS Bedrock (primary) → Pollinations (fallback). If both fail, the endpoint errors and the frontend falls back to its offline template engine. """ from __future__ import annotations import json import os import urllib.request from steps.lang._shared import bedrock_converse, log_llm_call # Bedrock model for scene drafting. Defaults to the BEDROCK_MODEL env the rest # of the backend already uses — the dub pipeline's translation fallback runs on # it, so it's a known-good id on this Space. (A hardcoded default here shadowed # that env and hit Bedrock "Operation not allowed".) Override: SCENE_BEDROCK_MODEL. SCENE_MODEL = os.getenv("SCENE_BEDROCK_MODEL") or os.getenv("BEDROCK_MODEL") # Pollinations fallback uses the keyless anonymous text endpoint. The keyed # gen.pollinations.ai route (build_client) blocks the Space's requests; the # anonymous text route stays open. Override the model via env POLLEN_TEXT_MODEL. POLLEN_TEXT_URL = "https://text.pollinations.ai/openai" POLLEN_TEXT_MODEL = os.getenv("POLLEN_TEXT_MODEL", "openai") _TEMPERATURE = 0.8 _SYSTEM_PROMPT = """You are a scriptwriter for DramaBox, a text-to-speech engine that performs short, emotionally directed monologues. Write ONE scene from the given viral hook and the creator's niche. FORMAT — follow exactly: - Structure: , "" "" - Text inside double quotes is SPOKEN aloud, verbatim. - Text outside quotes is a STAGE DIRECTION, never spoken (e.g. She sighs deeply. A long pause.). - Phonetic sounds may go inside quotes: "Hahaha", "Mmmmm", "Ugh". - NEVER put Ahem, Pfft, Sigh, Gasp or Cough inside quotes — use a stage direction. CONTENT: - Follow the hook's structure, rewritten specifically for the creator's niche — do not just slot the topic in. - About 3 short sentences — one performable take, roughly 10-20 seconds spoken. - Output ONLY the scene text. No preamble, no markdown, no surrounding quotes.""" def _build_user_prompt( hook_pattern: str, hook_title: str, register: str, register_label: str, niche: str, ) -> str: return ( f'Viral hook: "{hook_title}"\n' f"Hook structure: {hook_pattern}\n" f"Emotional register: {register_label} ({register})\n" f"Creator's niche / topic: {niche}\n\n" "Write the DramaBox scene now." ) def _clean(scene: str) -> str: """Strip whitespace and a markdown fence if the model wrapped its output.""" s = (scene or "").strip() if s.startswith("```"): s = s.split("\n", 1)[-1].rsplit("```", 1)[0].strip() return s def _bedrock_scene(user_prompt: str) -> str: """Primary provider — AWS Bedrock.""" raw = bedrock_converse( _SYSTEM_PROMPT, user_prompt, temperature=_TEMPERATURE, step="dramabox_scene", model_id=SCENE_MODEL, ) return _clean(raw) def _pollinations_scene(user_prompt: str) -> str: """Fallback provider — Pollinations' keyless anonymous text endpoint.""" payload = json.dumps({ "model": POLLEN_TEXT_MODEL, "temperature": _TEMPERATURE, "messages": [ {"role": "system", "content": _SYSTEM_PROMPT}, {"role": "user", "content": user_prompt}, ], }).encode("utf-8") req = urllib.request.Request( POLLEN_TEXT_URL, data=payload, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=60) as resp: data = json.loads(resp.read().decode("utf-8")) raw = (data["choices"][0]["message"]["content"] or "").strip() log_llm_call( step="dramabox_scene", provider="pollinations-text", model=POLLEN_TEXT_MODEL, system_prompt=_SYSTEM_PROMPT, user_prompt=user_prompt, response=raw, temperature=_TEMPERATURE, ) return _clean(raw) def write_scene( *, hook_pattern: str, hook_title: str, register: str, register_label: str, niche: str, ) -> str: """Draft a directed DramaBox scene — Bedrock primary, Pollinations fallback. Raises if both providers fail; the caller surfaces a 500 and the frontend falls back to its offline template engine. """ user_prompt = _build_user_prompt( hook_pattern, hook_title, register, register_label, niche ) # Primary — Bedrock. bedrock_err = "unknown error" try: scene = _bedrock_scene(user_prompt) if scene: return scene bedrock_err = "returned an empty scene" except Exception as e: # noqa: BLE001 bedrock_err = str(e) print(f"[scene] Bedrock unavailable ({bedrock_err}) — trying Pollinations.") # Fallback — Pollinations. try: scene = _pollinations_scene(user_prompt) except Exception as e: # noqa: BLE001 print(f"[scene] Pollinations also failed ({e}).") raise RuntimeError( f"Bedrock failed [{bedrock_err}]; Pollinations failed [{e}]" ) if not scene: raise RuntimeError( f"Bedrock failed [{bedrock_err}]; Pollinations returned an empty scene" ) return scene