Skip to content

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 (mp4 or xml) from the timeline UI.
  • useExportFlow (studio-frontend/jaduCutV2/hooks/export/useExportFlow.ts):
  • Flushes pending overlay saves (cancelPendingSaves).
  • POST /jaduCutV2/storyVideo/export with { projectId, storyId, format }.
  • Receives { exportId }.
  • Starts polling GET /jaduCutV2/storyVideo/export/:exportId via exportPollingService (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 localStorage key jaducutv2_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 jaduCutV2ExportJobs document with status QUEUED.
  • Returns 200 + exportId immediately.
  • Logs a TIMELINE_EXPORT project 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:

  1. Read storyvideos (scenes, shots, dialogues, timing).
  2. Read jaduCutV2StoryVideos overlay (settings, text clips, custom tracks).
  3. Batch-resolve dialogue audio URLs from asset-gen jobs (assetGenJobId → URL).
  4. Call buildRenderSpec() — flat, track-tagged timeline:
  5. visual clips from shots
  6. dialogue audio clips
  7. custom audio clips
  8. text overlays
  9. ordered tracks[]
  10. POST ${PYTHON_BACKEND_BASE_URL}/jaduCutV2/dispatchExportJob with:
{
  "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_routerJaduCutV2ExportService.dispatch_export_job:

  1. Load the job from jaduCutV2ExportJobs by exportJobId.
  2. Reject with 409 if the job is already PROCESSING or has a live in-process background task (jaducutv2_export_task_registry).
  3. Spawn a background asyncio task for rendering.
  4. Return 200 to studio-backend right away.

Background job — shared steps (both formats)

JaduCutV2ExportService._process_export_async:

  1. mark_processing — status → PROCESSING, set startedAt.
  2. Branch on format (MP4 or XML — see below).
  3. On success:
  4. mark_completed — status → COMPLETED, set outputUrl.
  5. append_export_to_story — push a ref onto jaduCutV2StoryVideos.exports[].
  6. On failure → mark_failed with 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_complex graph: scale/pad visuals, overlay text, mix audio.
  • Uses amix normalize=0 so 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 public outputUrl.

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.tracks order sent by studio-backend (no re-sorting).
  • Maps visual clips → MediaAsset + MediaClip on video tracks.
  • Maps dialogue clips and custom audio → audio tracks.
  • Text overlays are dropped — XML export is text-less.
  • Produces a JaduCutProject with timeline, assets, clips, and track order.

JaduCutV2XmlExporter.export_xml (jaducutv2_xml_exporter.py)

  1. Walk the project timeline and collect the asset map.
  2. Download media files into a temp directory.
  3. Build FCP 7 XML via JaduCutV2XmlBuilder (Resolve-compatible still handling).
  4. Zip the directory and upload to B2 as jaducut_exports/jaducut_export_{exportId}.zip.
  5. 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 RGB for JPEG (no alpha) or for PNG when mode is not RGB/RGBA.
  • Rename entry.filename to match the output extension.
  • Set entry.dimensions from 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