Spaces:
Running on Zero
Running on Zero
| """ | |
| 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: <speaker description>, "<dialogue>" <stage action> "<more dialogue>" | |
| - 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 | |