File size: 5,432 Bytes
3edeefe
3cf33dd
3edeefe
 
 
3cf33dd
 
 
3edeefe
 
 
823ce16
3edeefe
823ce16
3edeefe
823ce16
3cf33dd
b828530
 
 
 
 
3edeefe
823ce16
 
 
 
 
 
3cf33dd
3edeefe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3cf33dd
3edeefe
 
 
 
 
 
3cf33dd
3edeefe
 
 
 
 
 
3cf33dd
 
 
 
 
 
 
 
 
 
 
 
3edeefe
 
 
3cf33dd
3edeefe
 
 
3cf33dd
 
 
 
823ce16
 
 
 
 
3cf33dd
 
 
823ce16
 
 
 
 
 
3cf33dd
823ce16
 
 
3cf33dd
 
823ce16
 
3cf33dd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3826b11
3cf33dd
 
 
 
3826b11
3cf33dd
3826b11
 
3cf33dd
 
 
 
 
3826b11
 
 
 
3edeefe
3826b11
 
 
3edeefe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
"""
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