Spaces:
Runtime error
Runtime error
| """Phase 11 β variation testing: one body, many publishable hookΓCTA videos | |
| (SPEC_NEW Β§16.6). | |
| Decided with the owner (Β§16.6): each hookΓCTA combination is its **own | |
| publishable video**, and the **body clip is generated ONCE and reused** across | |
| every variation by reference (never regenerated). | |
| The model: | |
| - A `bodies` row = `{avatar_id (startframe), product_id, seed, script}` plus its | |
| generated body segment clip(s). The body's segments live in `segments` with | |
| `video_id = null`, `role = 'body'`, `body_id` set, all on the body's **locked | |
| seed** (identity continuity, Β§11.2). Generated ONCE. | |
| - Around a body we generate a **pool of hooks** and a **pool of CTAs** β each a | |
| short spoken line β its own candidate clip, on the **same seed + startframe** | |
| as the body so the face matches. These live in `segments` with `role` of | |
| `hook_candidate` / `cta_candidate` and `video_id = null`. | |
| - A `variations` row = `(body_id, hook_segment_id, cta_segment_id)` assembled | |
| into its **own `videos` row** (publishable), whose hook/body/cta segments are | |
| COPIES that point at the shared candidate + body clips by reference (no | |
| regeneration). Assembly (Β§11.4) + the approval/publish gate (Β§7.7) are | |
| unchanged β a variation is just a normal `videos` row. | |
| KEY INVARIANT: the body clip is generated once. Every variation's body segments | |
| carry the SAME `file_path` as the shared body segment clip β proven by the test. | |
| Β§12.4 invariant: the render seed lives only in `bodies.seed` / `segments.seed`; | |
| a variation's `videos.tags` carries `{"source":"variation","body_id":...}` and | |
| **never** a `seed` key. | |
| """ | |
| from __future__ import annotations | |
| import itertools | |
| from typing import Any | |
| from psycopg.types.json import Json | |
| from . import db, duration | |
| from .config import get_settings | |
| from .prompts.veo_segment import render_prompt, validate_spoken_text | |
| # ββ Body + pool construction βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _insert_pool_segment(conn, body_id: str, kind: str, role: str, text: str, | |
| avatar: dict[str, Any], seed: int) -> str: | |
| """Insert one POOL segment (body / hook_candidate / cta_candidate). | |
| Pool segments belong to the BODY, not to a video: `video_id` is null, | |
| `body_id` is set, `role` records which pool it is, and the seed is the body's | |
| locked seed (Β§16.6). The prompt is rendered from the avatar's start-frame | |
| template, exactly like flow.insert_segment (Β§11.2). Validates the line first | |
| (prompts.veo_segment.validate_spoken_text + duration.decide_duration). | |
| """ | |
| s = get_settings() | |
| validate_spoken_text(text) | |
| prompt = render_prompt(avatar, text, kind) | |
| dur = duration.decide_duration(text, s.words_per_second, s.tail_budget_s) | |
| row = conn.execute( | |
| """ | |
| insert into segments | |
| (video_id, idx, take, kind, spoken_text, veo_prompt, seed, duration_s, body_id, role) | |
| values (null, %s, %s, %s, %s, %s, %s, %s, %s, %s) | |
| returning id | |
| """, | |
| (0, 1, kind, text, prompt, int(seed), int(dur), body_id, role), | |
| ).fetchone() | |
| return str(row["id"]) | |
| def create_body(avatar_id: str, body_lines: list[str], product_id: str | None = None, | |
| seed: int | None = None) -> str: | |
| """Create a body: one reusable, identity-locked clip set (SPEC_NEW Β§16.6). | |
| Inserts a `bodies` row on a single locked seed (the seed shared by the body | |
| and ALL of its hook/CTA pool clips, Β§11.2), then one `role='body'` segment | |
| per body line β these hold the reusable body clips. Returns the body id. | |
| The body segments belong to the BODY (`video_id` null); a rendered variation | |
| later COPIES them (by file_path reference) into its own video. | |
| """ | |
| avatar = db.fetch_one("select * from avatars where id = %s", (avatar_id,)) | |
| if avatar is None: | |
| raise ValueError(f"no avatar {avatar_id}") | |
| if not body_lines: | |
| raise ValueError("create_body needs at least one body line") | |
| body_seed = int(seed) if seed is not None else duration.random_seed() | |
| # Validate every line up front so a bad line fails before we write anything. | |
| for line in body_lines: | |
| validate_spoken_text(line) | |
| with db.connect() as conn: | |
| brow = conn.execute( | |
| """ | |
| insert into bodies (avatar_id, product_id, seed, script, status) | |
| values (%s, %s, %s, %s, 'draft') | |
| returning id | |
| """, | |
| (avatar_id, product_id, body_seed, Json({"body": body_lines})), | |
| ).fetchone() | |
| body_id = str(brow["id"]) | |
| for line in body_lines: | |
| _insert_pool_segment(conn, body_id, "body", "body", line, avatar, body_seed) | |
| return body_id | |
| def _body_with_avatar(body_id: str) -> tuple[dict[str, Any], dict[str, Any]]: | |
| body = db.fetch_one("select * from bodies where id = %s", (body_id,)) | |
| if body is None: | |
| raise ValueError(f"no body {body_id}") | |
| avatar = db.fetch_one("select * from avatars where id = %s", (body["avatar_id"],)) | |
| if avatar is None: | |
| raise RuntimeError(f"body {body_id} is missing its avatar {body['avatar_id']}") | |
| return body, avatar | |
| def generate_hook_pool(body_id: str, hook_lines: list[str]) -> list[str]: | |
| """Insert a pool of hook candidates around a body (SPEC_NEW Β§16.6). | |
| Each line becomes a `role='hook_candidate'`, `kind='hook'` segment on the | |
| body's locked seed + startframe (so the face matches the body). Returns the | |
| new candidate segment ids. | |
| """ | |
| body, avatar = _body_with_avatar(body_id) | |
| seed = body["seed"] | |
| ids: list[str] = [] | |
| with db.connect() as conn: | |
| for line in hook_lines: | |
| ids.append( | |
| _insert_pool_segment(conn, body_id, "hook", "hook_candidate", line, avatar, seed) | |
| ) | |
| return ids | |
| def generate_cta_pool(body_id: str, cta_lines: list[str]) -> list[str]: | |
| """Insert a pool of CTA candidates around a body (SPEC_NEW Β§16.6). | |
| Each line becomes a `role='cta_candidate'`, `kind='cta'` segment on the | |
| body's locked seed + startframe. Returns the new candidate segment ids. | |
| """ | |
| body, avatar = _body_with_avatar(body_id) | |
| seed = body["seed"] | |
| ids: list[str] = [] | |
| with db.connect() as conn: | |
| for line in cta_lines: | |
| ids.append( | |
| _insert_pool_segment(conn, body_id, "cta", "cta_candidate", line, avatar, seed) | |
| ) | |
| return ids | |
| # ββ Clip generation (body ONCE; pools once each) βββββββββββββββββββββββββββββ | |
| def generate_body_clips(body_id: str) -> list[str]: | |
| """Generate the real clips for a body's segments β body clips ONCE (Β§16.6). | |
| Renders every pool segment under the body (role body / hook_candidate / | |
| cta_candidate) that does not yet have a file_path, via | |
| veo.generate_and_download (which in DRY_RUN copies the fixture clip). The | |
| body clips are produced exactly once here and then REUSED by reference across | |
| all variations (never regenerated). Returns the segment ids generated. | |
| """ | |
| from . import veo | |
| segs = db.fetch_all( | |
| """ | |
| select id, file_path from segments | |
| where body_id = %s | |
| and role in ('body', 'hook_candidate', 'cta_candidate') | |
| order by role, idx | |
| """, | |
| (body_id,), | |
| ) | |
| if not segs: | |
| raise RuntimeError(f"body {body_id} has no body/pool segments to generate") | |
| db.execute("update bodies set status = 'generating' where id = %s", (body_id,)) | |
| generated: list[str] = [] | |
| for seg in segs: | |
| if seg["file_path"]: # already generated β never regenerate (Β§16.6) | |
| continue | |
| veo.generate_and_download(str(seg["id"])) # sets segments.file_path | |
| generated.append(str(seg["id"])) | |
| db.execute("update bodies set status = 'ready' where id = %s", (body_id,)) | |
| return generated | |
| # ββ Variation rendering (reuse the body clip by reference) βββββββββββββββββββ | |
| def _copy_segment(conn, video_id: str, idx: int, src: dict[str, Any], kind: str) -> str: | |
| """Copy a pool/body segment into a variation's video, reusing its clip. | |
| The new segment is a normal (role-null, body_id-null) video segment at `idx`, | |
| carrying the SAME file_path as the source β the shared body / candidate clip | |
| is REUSED by reference, never regenerated (Β§16.6). Prompt/seed/duration are | |
| carried over so the row is a faithful, re-renderable copy. | |
| """ | |
| row = conn.execute( | |
| """ | |
| insert into segments | |
| (video_id, idx, take, kind, spoken_text, veo_prompt, seed, duration_s, | |
| file_path, status, role, body_id) | |
| values (%s, %s, 1, %s, %s, %s, %s, %s, %s, %s, null, null) | |
| returning id | |
| """, | |
| ( | |
| video_id, idx, kind, src["spoken_text"], src["veo_prompt"], src["seed"], | |
| src["duration_s"], src["file_path"], | |
| "qc_pass" if src["file_path"] else "pending", | |
| ), | |
| ).fetchone() | |
| return str(row["id"]) | |
| def build_variation(body_id: str, hook_segment_id: str, cta_segment_id: str) -> dict[str, str]: | |
| """Render one hookΓCTA combination into its own publishable video (Β§16.6). | |
| Creates a NEW `videos` row (avatar + product from the body; tags | |
| `{"source":"variation","body_id":...}` β NO seed key, Β§12.4) and its | |
| idx-ordered segments: | |
| idx 0 = hook, COPIED from the chosen hook_candidate (clip reused) | |
| idx 1..k = body, COPIED from the body's body segments (clips REUSED β | |
| the shared body clip, never regenerated) | |
| idx last = cta, COPIED from the chosen cta_candidate (clip reused) | |
| Inserts a `variations` row linking body/hook/cta/video. Returns | |
| {variation_id, video_id}. The new video is a normal publishable video: | |
| assemble.assemble + the Β§7.7 approval/publish gate work unchanged. | |
| """ | |
| body = db.fetch_one("select * from bodies where id = %s", (body_id,)) | |
| if body is None: | |
| raise ValueError(f"no body {body_id}") | |
| hook = db.fetch_one( | |
| "select * from segments where id = %s and body_id = %s and role = 'hook_candidate'", | |
| (hook_segment_id, body_id), | |
| ) | |
| if hook is None: | |
| raise ValueError(f"no hook candidate {hook_segment_id} under body {body_id}") | |
| cta = db.fetch_one( | |
| "select * from segments where id = %s and body_id = %s and role = 'cta_candidate'", | |
| (cta_segment_id, body_id), | |
| ) | |
| if cta is None: | |
| raise ValueError(f"no cta candidate {cta_segment_id} under body {body_id}") | |
| body_segs = db.fetch_all( | |
| "select * from segments where body_id = %s and role = 'body' order by idx, id", | |
| (body_id,), | |
| ) | |
| if not body_segs: | |
| raise RuntimeError(f"body {body_id} has no body segments") | |
| # Β§12.4: creative levers only β the render seed is NEVER written into tags. | |
| tags = {"source": "variation", "body_id": body_id} | |
| with db.connect() as conn: | |
| vrow = conn.execute( | |
| "insert into videos (avatar_id, product_id, tags, status) values (%s,%s,%s,'draft') returning id", | |
| (body["avatar_id"], body.get("product_id"), Json(tags)), | |
| ).fetchone() | |
| video_id = str(vrow["id"]) | |
| idx = 0 | |
| _copy_segment(conn, video_id, idx, hook, "hook") # idx 0 β hook | |
| for bseg in body_segs: # idx 1..k β body (REUSE) | |
| idx += 1 | |
| _copy_segment(conn, video_id, idx, bseg, "body") | |
| idx += 1 | |
| _copy_segment(conn, video_id, idx, cta, "cta") # idx last β cta | |
| vr = conn.execute( | |
| """ | |
| insert into variations (body_id, hook_segment_id, cta_segment_id, video_id, status) | |
| values (%s, %s, %s, %s, 'ready') | |
| returning id | |
| """, | |
| (body_id, hook_segment_id, cta_segment_id, video_id), | |
| ).fetchone() | |
| variation_id = str(vr["id"]) | |
| return {"variation_id": variation_id, "video_id": video_id} | |
| def build_all_variations(body_id: str) -> list[dict[str, str]]: | |
| """Build every hookΓCTA variation under a body β the cartesian product (Β§16.6). | |
| For each (hook_candidate, cta_candidate) pair in the body's pools, calls | |
| build_variation. Returns the list of {variation_id, video_id} dicts (one | |
| publishable video per combination). | |
| """ | |
| hooks = db.fetch_all( | |
| "select id from segments where body_id = %s and role = 'hook_candidate' order by id", | |
| (body_id,), | |
| ) | |
| ctas = db.fetch_all( | |
| "select id from segments where body_id = %s and role = 'cta_candidate' order by id", | |
| (body_id,), | |
| ) | |
| if not hooks or not ctas: | |
| raise RuntimeError( | |
| f"body {body_id} needs at least one hook candidate and one cta candidate " | |
| f"(have {len(hooks)} hooks, {len(ctas)} ctas)" | |
| ) | |
| out: list[dict[str, str]] = [] | |
| for hook, cta in itertools.product(hooks, ctas): | |
| out.append(build_variation(body_id, str(hook["id"]), str(cta["id"]))) | |
| return out | |