github-actions[bot] commited on
Commit
3edeefe
Β·
1 Parent(s): 8bfce29

deploy: switch to chatterbox requirements @ c5db0d8

Browse files
steps/lang/_shared.py CHANGED
@@ -95,12 +95,15 @@ def parse_json_array(raw: str) -> list:
95
  raise ValueError(f"Could not parse JSON array from LLM response:\n{raw[:200]}")
96
 
97
 
98
- def bedrock_converse(system_prompt: str, user_text: str, temperature: float = 0.1, step: str = "bedrock") -> str:
99
- """Make a single Bedrock converse call and return the raw response text."""
 
 
 
100
  import boto3
101
 
102
  region = os.getenv("AWS_REGION", "us-east-1")
103
- model_id = os.getenv("BEDROCK_MODEL", "qwen.qwen3-next-80b-a3b")
104
 
105
  client = boto3.client("bedrock-runtime", region_name=region)
106
  response = client.converse(
 
95
  raise ValueError(f"Could not parse JSON array from LLM response:\n{raw[:200]}")
96
 
97
 
98
+ def bedrock_converse(system_prompt: str, user_text: str, temperature: float = 0.1, step: str = "bedrock", model_id=None) -> str:
99
+ """Make a single Bedrock converse call and return the raw response text.
100
+
101
+ model_id: optional override; defaults to the BEDROCK_MODEL env var.
102
+ """
103
  import boto3
104
 
105
  region = os.getenv("AWS_REGION", "us-east-1")
106
+ model_id = model_id or os.getenv("BEDROCK_MODEL", "qwen.qwen3-next-80b-a3b")
107
 
108
  client = boto3.client("bedrock-runtime", region_name=region)
109
  response = client.converse(
tools_api/router.py CHANGED
@@ -13,10 +13,11 @@ from typing import Optional
13
 
14
  from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
15
  from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse
 
16
 
17
  from server import limiter, _download_url, _is_allowed_video_host
18
 
19
- from . import audio_cleanup, dramabox, subtitles, voice_clone
20
  from .storage import (
21
  file_url,
22
  new_run_dir,
@@ -285,6 +286,44 @@ async def audio_cleanup_endpoint(
285
  })
286
 
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  # ── File download ────────────────────────────────────────────────────
289
 
290
  @router.get("/file/{run_id}/{filename}")
 
13
 
14
  from fastapi import APIRouter, File, Form, HTTPException, Request, UploadFile
15
  from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse
16
+ from pydantic import BaseModel
17
 
18
  from server import limiter, _download_url, _is_allowed_video_host
19
 
20
+ from . import audio_cleanup, dramabox, scene_writer, subtitles, voice_clone
21
  from .storage import (
22
  file_url,
23
  new_run_dir,
 
286
  })
287
 
288
 
289
+ # ── Scene writer ─────────────────────────────────────────────────────
290
+
291
+ class SceneRequest(BaseModel):
292
+ hook_pattern: str
293
+ hook_title: str = ""
294
+ register: str = ""
295
+ register_label: str = ""
296
+ niche: str
297
+
298
+
299
+ @router.post("/scene")
300
+ @limiter.limit("30/hour")
301
+ async def scene_endpoint(request: Request, body: SceneRequest):
302
+ """Draft a directed DramaBox scene from a viral hook + the creator's niche."""
303
+ niche = body.niche.strip()
304
+ hook_pattern = body.hook_pattern.strip()
305
+ if not niche:
306
+ raise HTTPException(400, "niche is required")
307
+ if not hook_pattern:
308
+ raise HTTPException(400, "hook_pattern is required")
309
+ if len(niche) > 400:
310
+ raise HTTPException(400, "niche exceeds 400 char limit")
311
+
312
+ try:
313
+ scene = await asyncio.to_thread(
314
+ scene_writer.write_scene,
315
+ hook_pattern=hook_pattern,
316
+ hook_title=body.hook_title.strip(),
317
+ register=body.register.strip(),
318
+ register_label=body.register_label.strip(),
319
+ niche=niche,
320
+ )
321
+ except Exception as e: # noqa: BLE001
322
+ raise HTTPException(500, f"Scene generation failed: {e}")
323
+
324
+ return JSONResponse({"scene": scene})
325
+
326
+
327
  # ── File download ────────────────────────────────────────────────────
328
 
329
  @router.get("/file/{run_id}/{filename}")
tools_api/scene_writer.py ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hook β†’ DramaBox scene drafting via AWS Bedrock (Amazon Nova).
3
+
4
+ The frontend's "Adapt to my niche" step POSTs to /api/tools/scene; this turns a
5
+ viral-hook structure plus the creator's niche into a directed DramaBox scene.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import os
10
+
11
+ from steps.lang._shared import bedrock_converse
12
+
13
+ # Amazon Nova Micro β€” the quickest/cheapest Nova text model. Override via env;
14
+ # if Bedrock asks for an inference profile, set "us.amazon.nova-micro-v1:0".
15
+ SCENE_MODEL = os.getenv("SCENE_BEDROCK_MODEL", "amazon.nova-micro-v1:0")
16
+
17
+ _SYSTEM_PROMPT = """You are a scriptwriter for DramaBox, a text-to-speech engine that performs short, emotionally directed monologues.
18
+
19
+ Write ONE scene from the given viral hook and the creator's niche.
20
+
21
+ FORMAT β€” follow exactly:
22
+ - Structure: <speaker description>, "<dialogue>" <stage action> "<more dialogue>"
23
+ - Text inside double quotes is SPOKEN aloud, verbatim.
24
+ - Text outside quotes is a STAGE DIRECTION, never spoken (e.g. She sighs deeply. A long pause.).
25
+ - Phonetic sounds may go inside quotes: "Hahaha", "Mmmmm", "Ugh".
26
+ - NEVER put Ahem, Pfft, Sigh, Gasp or Cough inside quotes β€” use a stage direction.
27
+
28
+ CONTENT:
29
+ - Follow the hook's structure, rewritten specifically for the creator's niche β€” do not just slot the topic in.
30
+ - About 3 short sentences β€” one performable take, roughly 10-20 seconds spoken.
31
+ - Output ONLY the scene text. No preamble, no markdown, no surrounding quotes."""
32
+
33
+
34
+ def write_scene(
35
+ *,
36
+ hook_pattern: str,
37
+ hook_title: str,
38
+ register: str,
39
+ register_label: str,
40
+ niche: str,
41
+ ) -> str:
42
+ """Draft a directed DramaBox scene with Bedrock Nova. Raises on empty output."""
43
+ user_prompt = (
44
+ f'Viral hook: "{hook_title}"\n'
45
+ f"Hook structure: {hook_pattern}\n"
46
+ f"Emotional register: {register_label} ({register})\n"
47
+ f"Creator's niche / topic: {niche}\n\n"
48
+ "Write the DramaBox scene now."
49
+ )
50
+ raw = bedrock_converse(
51
+ _SYSTEM_PROMPT,
52
+ user_prompt,
53
+ temperature=0.8,
54
+ step="dramabox_scene",
55
+ model_id=SCENE_MODEL,
56
+ )
57
+ scene = (raw or "").strip()
58
+ # Strip a markdown fence if the model wrapped its output.
59
+ if scene.startswith("```"):
60
+ scene = scene.split("\n", 1)[-1].rsplit("```", 1)[0].strip()
61
+ if not scene:
62
+ raise RuntimeError("Scene model returned an empty response.")
63
+ return scene