JaduCutV2 Export (Python)¶
Render service for the Studio V2 timeline editor. studio-backend builds a
render-ready RenderSpec from story content + the editor overlay; this module
downloads media assets and produces MP4 or FCP 7 XML (ZIP). Python does
not read storyvideos or merge timeline state — that lives in
studio-backend (src/jaduCutV2/export/).
Flow: Frontend → studio-backend → python-backend¶
sequenceDiagram
participant FE as studio-frontend
participant BE as studio-backend
participant PY as python-backend
participant B2 as B2 storage
FE->>BE: POST /jaduCutV2/storyVideo/export
BE-->>FE: 200 { exportId } (job QUEUED)
BE->>BE: buildAndDispatchExport (async)
BE->>BE: buildRenderSpec(story + overlay)
BE->>PY: POST /jaduCutV2/dispatchExportJob
PY-->>BE: 200 (accepted; render runs in background)
PY->>PY: PROCESSING → render → COMPLETED
PY->>B2: upload output
PY->>PY: append jaduCutV2StoryVideos.exports[]
loop Poll every 15s
FE->>BE: GET /jaduCutV2/storyVideo/export/:exportId
BE-->>FE: status + outputUrl when done
end
Step 1 — Frontend starts export¶
- User picks a format (
mp4orxml) from the timeline UI. useExportFlow(studio-frontend/jaduCutV2/hooks/export/useExportFlow.ts):- Flushes pending overlay saves (
cancelPendingSaves). POST /jaduCutV2/storyVideo/exportwith{ projectId, storyId, format }.- Receives
{ exportId }. - Starts polling
GET /jaduCutV2/storyVideo/export/:exportIdviaexportPollingService(15s interval, up to ~10 min). - On
COMPLETED, shows a success toast with a download action (outputUrl). - On page reload mid-export, resumes polling from
localStoragekeyjaducutv2_export_${storyId}.
Step 2 — studio-backend creates the job (sync)¶
- Validates no other active export for that story (uniqueness is per-story, not per-project).
- Creates a
jaduCutV2ExportJobsdocument with statusQUEUED. - Returns 200 + exportId immediately.
- Logs a
TIMELINE_EXPORTproject activity.
Step 3 — studio-backend builds spec and dispatches (async)¶
Fire-and-forget via buildAndDispatchExport
(studio-backend/src/jaduCutV2/export/utils/dispatchExport.ts), which runs
after the HTTP response is sent:
- Read
storyvideos(scenes, shots, dialogues, timing). - Read
jaduCutV2StoryVideosoverlay (settings, text clips, custom tracks). - Batch-resolve dialogue audio URLs from asset-gen jobs (
assetGenJobId→ URL). - Call
buildRenderSpec()— flat, track-tagged timeline: - visual clips from shots
- dialogue audio clips
- custom audio clips
- text overlays
- ordered
tracks[] POST ${PYTHON_BACKEND_BASE_URL}/jaduCutV2/dispatchExportJobwith:
{
"exportJobId": "<uuid>",
"format": "mp4",
"renderSpec": { ... }
}
Auth header: X-API-KEY: PYTHON_BACKEND_API_KEY.
Failure handling (studio-backend marks the job FAILED):
- Spec build or dispatch throws before Python accepts the job.
- Python returns a non-2xx response (Python never started processing).
Step 4 — Frontend polls until done¶
Polls GET /jaduCutV2/storyVideo/export/:exportId until status is
COMPLETED or FAILED.
| Status | Meaning |
|---|---|
QUEUED |
Job created; spec build / dispatch may still be in progress |
PROCESSING |
Python accepted the job and is rendering |
COMPLETED |
Output uploaded; outputUrl available |
FAILED |
Error in dispatch (BE) or render (PY); see errorMessage |
What python-backend receives¶
Endpoint: POST /jaduCutV2/dispatchExportJob
Caller: studio-backend only (not the browser).
Auth: X-API-KEY.
Request body (DispatchExportRequest in app/schemas/jaducutv2_render_spec.py;
wire casing is camelCase):
| Field | Description |
|---|---|
exportJobId |
Same id created by studio-backend |
format |
"mp4" or "xml" |
renderSpec |
Flat, render-ready timeline |
renderSpec fields:
| Field | Contents |
|---|---|
width, height, fps |
Canvas settings |
tracks[] |
Ordered lanes (video, dialogue, audio, text) |
visualElements[] |
Video/image clips + text overlays (kind: "clip" | "text") |
dialogueClips[] |
Scene dialogue audio (volume 0.0–1.0) |
audioClips[] |
Custom-lane audio (SFX / uploads) |
There is no top-level duration — Python derives total length from element
end times. Text position / size arrive as percent (0–100). All media
file_url fields are remote URLs that Python downloads before rendering.
RenderSpec merge logic and TypeScript types:
studio-backend/src/jaduCutV2/export/ and src/shared/jaduCutV2Types.ts.
Python processing flow¶
Dispatch (returns immediately)¶
jaducutv2_export_router → JaduCutV2ExportService.dispatch_export_job:
- Load the job from
jaduCutV2ExportJobsbyexportJobId. - Reject with 409 if the job is already
PROCESSINGor has a live in-process background task (jaducutv2_export_task_registry). - Spawn a background
asynciotask for rendering. - Return 200 to studio-backend right away.
Background job — shared steps (both formats)¶
JaduCutV2ExportService._process_export_async:
mark_processing— status →PROCESSING, setstartedAt.- Branch on
format(MP4 or XML — see below). - On success:
mark_completed— status →COMPLETED, setoutputUrl.append_export_to_story— push a ref ontojaduCutV2StoryVideos.exports[].- On failure →
mark_failedwith the error message.
MP4 path (format: "mp4")¶
RenderSpec
→ spec_to_render_inputs() # map to FFmpeg DTOs
→ download_render_input_assets() # HTTP URLs → local temp files
→ JaduCutV2VideoRenderer.render_video()
→ upload .mp4 to B2
→ outputUrl
spec_to_render_inputs (jaducutv2_spec_render_mapper.py)¶
| Input | Processing |
|---|---|
visualElements where kind=clip |
→ VisualClipInfo (geometry/timing already in pixels) |
visualElements where kind=text |
→ TextOverlayInfo; percent position/size converted to pixels; stroke applied from constants |
dialogueClips + audioClips |
Merged into one flat AudioClipInfo[] for FFmpeg amix |
| (no top-level duration) | Derived as max(startTime + duration) across all visual, dialogue, and audio elements |
tracks[] is ignored for MP4 — rendering uses flat compositing, not lane order.
download_render_input_assets (jaducutv2_render_assets_downloader.py)¶
- Collects unique
http(s)://URLs from visual clips and audio clips. - Downloads in parallel via
SharedHelpers.download_assets_parallel. - Replaces remote URLs with local temp file paths before FFmpeg runs.
JaduCutV2VideoRenderer.render_video (jaducutv2_video_renderer.py)¶
- Builds an FFmpeg
filter_complexgraph: scale/pad visuals, overlay text, mix audio. - Uses
amix normalize=0so multiple audio clips keep their intended volume (without normalization, N clips would attenuate to roughly 1/N loudness). - Writes a temp MP4, uploads to B2 at
jaducut_exports/jaducut_export_{exportId}.mp4, returns the publicoutputUrl.
XML path (format: "xml")¶
RenderSpec
→ spec_to_xml_project() # map to JaduCutProject (FCP 7 structure)
→ JaduCutV2XmlExporter.export_xml()
→ download assets into temp dir
→ JaduCutV2XmlBuilder.build() # FCP 7 XML
→ zip (XML + media files)
→ upload .zip to B2
→ outputUrl
spec_to_xml_project (jaducutv2_spec_xml_adapter.py)¶
- Lays out tracks in the exact
spec.tracksorder sent by studio-backend (no re-sorting). - Maps visual clips →
MediaAsset+MediaClipon video tracks. - Maps dialogue clips and custom audio → audio tracks.
- Text overlays are dropped — XML export is text-less.
- Produces a
JaduCutProjectwith timeline, assets, clips, and track order.
JaduCutV2XmlExporter.export_xml (jaducutv2_xml_exporter.py)¶
- Walk the project timeline and collect the asset map.
- Download media files into a temp directory.
- Build FCP 7 XML via
JaduCutV2XmlBuilder(Resolve-compatible still handling). - Zip the directory and upload to B2 as
jaducut_exports/jaducut_export_{exportId}.zip. - Clean up temp files.
Image normalization (jaducutv2_export_image_normalizer)¶
Still images are normalized for both MP4 and XML export via
app/jaducutv2/jaducutv2_export_image_normalizer.py. DaVinci Resolve (and Premiere
via FCP 7 XML) and FFmpeg's image loop input reliably accept only PNG/JPEG
stills in RGB/RGBA.
Supported inputs (via Pillow + plugins): PNG, JPEG, WebP, AVIF (pillow-avif),
HEIF/HEIC (pillow-heif), GIF (first frame). If Pillow cannot open a file,
FFmpeg is used as a one-frame fallback before export fails.
MP4 path: jaducutv2_render_assets_downloader.py normalizes each downloaded
clipType=image asset to PNG before FFmpeg runs.
XML path: jaducutv2_xml_exporter._normalize_image_asset delegates to the
same normalizer (JPEG preserved when already JPEG; otherwise PNG).
Fast path — return the file unchanged when all of the following hold:
| Check | Requirement |
|---|---|
| Format | PNG or JPEG (PIL img.format) |
| Extension | .png, .jpg, or .jpeg |
| Color mode | RGB or RGBA |
| Extension ↔ bytes | Extension matches actual format (e.g. .png file is really PNG) |
Records entry.dimensions for JaduCutV2XmlBuilder.
Slow path — re-encode when any fast-path check fails:
- Target format: keep JPEG if the source is already JPEG; otherwise default to PNG.
- Color mode: convert to
RGBfor JPEG (no alpha) or for PNG when mode is not RGB/RGBA. - Rename
entry.filenameto match the output extension. - Set
entry.dimensionsfrom the converted image.
Video and audio assets skip normalization — they are moved into the zip as downloaded.
Job status ownership¶
| Status | Written by | When |
|---|---|---|
QUEUED |
studio-backend | Export endpoint returns |
PROCESSING |
python-backend | Render starts |
COMPLETED |
python-backend | Output uploaded |
FAILED |
studio-backend or python-backend | Dispatch/spec errors (BE) or render errors (PY) |
Mongo collections touched by Python:
| Collection | Purpose |
|---|---|
jaduCutV2ExportJobs |
Status, outputUrl, timestamps, errorMessage |
jaduCutV2StoryVideos.exports[] |
Append export ref on success |
studio-backend also finalizes stale jobs stuck in QUEUED / PROCESSING for
more than 10 minutes (JADUCUTV2_EXPORT_STALE_JOB_MS).
Module layout¶
app/jaducutv2/
├── jaducutv2_export_router.py POST /jaduCutV2/dispatchExportJob
├── jaducutv2_export_service.py Orchestration + format branch
├── jaducutv2_export_strategy.py Mongo job read/write
├── jaducutv2_export_task_registry.py In-process duplicate-dispatch guard
├── jaducutv2_export_types.py RenderInputs / ExportJobView DTOs
├── jaducutv2_spec_render_mapper.py RenderSpec → MP4 inputs
├── jaducutv2_spec_xml_adapter.py RenderSpec → JaduCutProject
├── jaducutv2_render_assets_downloader.py
├── jaducutv2_video_renderer.py FFmpeg MP4 renderer
├── jaducutv2_xml_exporter.py XML zip + B2 upload
└── jaducutv2_xml_builder.py FCP 7 XML generation
app/schemas/
├── jaducutv2_render_spec.py Wire contract (RenderSpec, DispatchExportRequest)
└── jaducutv2_export_schema.py Job status enums
app/models/
├── jaducutv2_export_jobs_model.py
└── jaducutv2_story_videos_model.py