product

Screenplay ↔ Scene Studio integration — design doc

Status: design / not built. Decision doc for product_tasks efbc2b48. Recommends Option C+ (labeled breakdown store + picker) as phase 1, with the full re-key (Option B) reserved as a later phase only if truly parallel rendering is ever needed. Background: REVERSE_SCREENPLAY_ENGINE.md (the "To Scene Studio" hand-off) + SCENE_STUDIO_ARCHITECTURE.md.

The problem

The Reverse Screenplay Engine re-segments one chapter into N screenplay scenes. Scene Studio is built around one breakdown per chapterscene_projects.episode_id is UNIQUE. So when you hand a screenplay scene to Scene Studio, it has to overwrite that chapter's breakdown.

We already softened this (PR #783, migration 414): the "To Scene Studio" hand-off now confirms before replacing and snapshots the outgoing shots into scene_projects.breakdown_versions. But that's a guard, not a model — you still can't keep, say, a hand-made breakdown and breakdowns for two different screenplay scenes of the same chapter, all addressable.

Goal: let a chapter hold multiple coexisting, labeled breakdowns; make the screenplay hand-off additive instead of destructive.

What the data already supports (and what doesn't)

  • scene_shots FKs to scene_project_id (the PK), not episode_id — so multiple shot sets per chapter work at the row level for free.
  • scene_projects.breakdown_versions JSONB (migration 414) already stores snapshot breakdowns.
  • scene_projects.episode_id UNIQUE hardwires one project per chapter.
  • ~60 touch points assume it: ~22 exported scene-studio.ts functions keyed by episodeId, ~13 .eq('episode_id', …).maybeSingle() / upsert(…, { onConflict: 'episode_id' }) query sites, ~24 /:episodeId routes, and the frontend (?ep=<episodeId>, getSceneProject(episodeId)).

Option B — full re-key (the "correct" but heavy path)

Drop episode_id UNIQUE; make project_id the first-class key; add scene_projects.screenplay_scene_id (nullable FK). Each screenplay scene spawns its own full project — its own shots, keyframes, clips, animatic — all live simultaneously.

Touches (~40–60): rename/re-signature ~22 service functions (getSceneProject(episodeId)(projectId), etc.), rewrite ~13 query sites, change ~24 routes from /:episodeId to /:projectId (breaking URL change, ?ep=?project=), a schema migration with a data backfill (assign every existing project a stable key + a screenplay_scene_id = null), and a frontend project-list picker. ~2–3 weeks; high blast radius on a central, heavily-used feature.

When it's worth it: only if you need to render multiple breakdowns of the same chapter in parallel (keep N sets of live keyframes/clips at once). For the actual workflow — you work one animatic at a time — this is over-built.

Keep episode_id UNIQUE and the entire existing plumbing. Promote the already-present breakdown_versions column into a labeled multi-breakdown store, and add a picker in Scene Studio to switch the active breakdown. The one in scene_shots is "active" (rendered); the rest sit in breakdown_versions. Switching is lossless because we snapshot each shot's rendered assets, not just its text.

Schema (1 trivial additive migration — 415)

ALTER TABLE public.scene_projects
  ADD COLUMN IF NOT EXISTS active_breakdown JSONB;  -- metadata of the live breakdown

active_breakdown = { id, label, source, screenplay_scene_id } for whatever is currently in scene_shots (null on legacy projects → treated as an unlabeled "Hand-made" breakdown). No constraint changes, no re-key, no breaking URL.

Each breakdown_versions[] entry becomes a full, restorable breakdown:

{
  "id": "uuid",                       // app-generated, stable
  "label": "INT. SERVER ROOM - NIGHT",// human label
  "source": "manual" | "screenplay",
  "screenplay_scene_id": "uuid|null",
  "style_anchor": "…",
  "characters": [ … ],                // canonical cast at snapshot time
  "shots": [ {                        // FULL shot state, incl. rendered assets
    "idx", "shot_type", "visual_prompt", "speaker_name", "dialogue", "duration_s",
    "keyframe_url", "clip_url", "audio_url", "lip_sync_url",
    "keyframe_versions", "clip_versions", "place_key", "camera_motion",
    "transition", "trim_start_s", "trim_end_s"
  } ],
  "created_at", "updated_at"
}

The key upgrade vs. migration 414: the snapshot must capture *keyframe_url / clip_url / audio_url / _versions per shot — so flipping breakdowns preserves rendered work. (Today's snapshot grabs only shot text.)

Backend (scene-studio.ts + routes)

New functions (all keyed by episodeId, so they fit the existing model):

  • listBreakdowns(episodeId)[{ id, label, source, screenplay_scene_id, shot_count, active, created_at }] (the active one from active_breakdown + a scene_shots count; the rest from breakdown_versions).
  • switchBreakdown(episodeId, breakdownId) → snapshot the current active into breakdown_versions, restore the chosen entry's full shots + style + characters into scene_shots / scene_projects, set active_breakdown, and remove the chosen entry from the store. Non-destructive both ways.
  • deleteBreakdown(episodeId, breakdownId) → drop an inactive breakdown.
  • Enhance the existing generateSceneBreakdown snapshot to capture full rendered shot state + write a label/source/id.

New routes: GET /projects/:episodeId/breakdowns, POST /projects/:episodeId/breakdowns/:breakdownId/switch, DELETE /projects/:episodeId/breakdowns/:breakdownId.

The screenplay hand-off becomes purely additive

sendSceneToStudio stops being destructive: instead of confirm-before-replace, it always adds a new labeled breakdown (label = the scene's slug line, source='screenplay', screenplay_scene_id set) and makes it active, snapshotting the prior active into the store. The confirm dialog + conflict signal from PR #783 go away — replaced by "added breakdown N to chapter X." (Optionally surface "→ in Scene Studio (N breakdowns)" back on the screenplay scene.)

Frontend (scene-studio/page.tsx)

A Breakdowns picker at the top of the project view: a list/dropdown of the chapter's breakdowns with a source badge (✋ Hand-made / 🎬 Screenplay: "<slug>") and the active one marked. Selecting one calls switch and reloads. Everything downstream (keyframes, clips, assemble) is unchanged — it always operates on the active breakdown.

Touches (~8–12): 1 trivial migration, ~3 new service functions + an enhanced

snapshot, 3 new routes, a picker component + handlers, and the screenplay hand-off simplification. ~1 focused session. No breaking change.

Recommendation

Ship Option C+ as phase 1. It delivers coexisting, labeled, losslessly- switchable breakdowns per chapter and makes the screenplay hand-off additive — the actual ask — at ~15% of B's cost and risk, with no breaking change and a one-column migration. Keep Option B in reserve: if a real need for simultaneous parallel rendering of multiple breakdowns ever appears, the C+ breakdown_versions entries are already the seed of the per-project rows B would create, so C+ is a stepping stone, not a dead end.

Open questions

  • Active-asset capture completeness: confirm every render path writes its asset URLs onto scene_shots (not elsewhere) so the snapshot is faithful; audit generateSceneKeyframes / generateShotClip / lipSyncShot / assembleSceneAnimatic write targets.
  • Animatic per breakdown: scene_projects.video_url / animatic_versions are project-level (one chapter). Should a switched breakdown also stash/restore its assembled video_url? (Likely yes — add video_url + music_url to the snapshot.)
  • Label source for manual breakdowns: default to the chapter title / "Hand-made"; let the admin rename.
  • Cap: breakdown_versions is capped at 10 in the service — fine for this, but make the cap explicit in the UI (oldest drops).
SCREENPLAY SCENE STUDIO INTEGRATION — Docs | HiveJournal