Skip to content

feat(cli): add hyperframes init --timeline to start a project from an OTIO timeline - #7433

Open
scottdmilner wants to merge 1 commit into
heygen-com:mainfrom
scottdmilner:feat/init-timeline
Open

scottdmilner wants to merge 1 commit into
heygen-com:mainfrom
scottdmilner:feat/init-timeline

Conversation

@scottdmilner

@scottdmilner scottdmilner commented Nov 9, 2026 •

Copy link
Copy Markdown

What

Adds hyperframes init --timeline timeline.json, which builds a project from an edit made in another editor. It reads a flat JSON description of the edit (hyperframes-timeline/1) and creates one clip element per cut. Source trims, speed changes, audio tracks, dissolves, freeze frames and image sequences carry over, and the media is copied into assets/.

A new /import-timeline skill ships a Python converter that produces hyperframes-timeline/1 JSON from OpenTimelineIO (OTIO) and the other timeline formats OTIO can read.

~/.venvs/otio/bin/python -I scripts/otio_flatten.py cut.otio --width 1920 --height 1080 > timeline.json
npx hyperframes init my-video --timeline timeline.json --media-root ./footage
cd my-video && npx hyperframes check

New flags: --timeline, --media-root (where relative clip paths resolve) and --link-media (symlink instead of copy; copies on Windows and on disks that refuse links). --timeline can't be combined with --video, --audio, --example, --tailwind or --resolution.

Why

Editors hand off cuts as EDL, Final Cut XML, AAF or OTIO. Today there is no way to bring an existing edit into HyperFrames except rebuilding it by hand. Importing the cut list lets someone start from the edit they already made and add motion graphics, captions and overlays on top.

How

Design decisions

OTIO is the first supported timeline format. It is open source, supported by major video editing softwares (including Davinci Resolve, Adobe Premiere, and Avid Media Composer) and has adapters for the other major timeline formats, so supporting it gets several formats without writing a parser for each one. The converter is tested with native .otio, EDL (otio-cmx3600-adapter), Final Cut Pro 7 XML (otio-fcp-adapter), Final Cut Pro X (otio-fcpx-xml-adapter) and GStreamer (otio-xges-adapter). AAF (otio-aaf-adapter) reads but is untested and carries no media, so it needs relinking first.

A JSON schema plus a Python converter, rather than reading OTIO directly. OTIO only ships Python and C++ bindings. The TypeScript bindings are in progress but not complete, and the HyperFrames CLI is TypeScript. So the boundary is a JSON schema:

  • The schema is JSON Schema draft 2020-12, in packages/core/schemas/timeline-import.json, mirrored to docs/schema/ and served at https://hyperframes.heygen.com/schema/timeline-import.json.
  • The converter (skills/import-timeline/scripts/otio_flatten.py) uses the official opentimelineio package for all parsing and time math, then walks the result and prints flat facts as schema-conforming JSON.
  • The consumer (init --timeline) reads only the JSON and never touches OTIO.

This keeps OTIO's precision and its file-format support without reimplementing either. Because the schema is the contract, any other tool can write the file, and a future TypeScript OTIO binding could replace the Python script without touching init.

Implementation

Core (packages/core/src/timelineImport/, exported as @hyperframes/core/timeline-import)

  • schema.ts parses and validates the file, and enforces the rules JSON Schema can't state: unique clip ids and fps between 1 and 240.
  • emit.ts generates the composition HTML.
  • transitions.ts plans dissolves. Each one is clamped to the handles (footage beyond the trim points) that ffprobe finds. A shortened dissolve is reported, and with no handle it stays a hard cut.
  • mediaOffset.ts maps a clip's position on its source's own clock to seconds into the file. It tries the file's own start timecode, then the converter's mediaOrigin hint, then the value as written, and uses the first that places the clip inside the file.

CLI (packages/cli/src/commands/)

  • init.ts wires up the flags. On failure it removes the directory it created.
  • initTimeline.ts stages media and prints warnings: the first 20, then a count of the rest.
  • initTimelineSequence.ts encodes image sequences to H.264 with ffmpeg. EXR is decoded to display light, and missing frames are held.
  • initTimelineOrigin.ts reads each file's start timecode with ffprobe, including drop-frame timecode and WAV time_reference.

Skill (skills/import-timeline/): SKILL.md, otio_flatten.py, otio_flatten_test.py and scripts/tests/fixtures/.

Behaviors worth knowing

  • Precision: times are written to the microsecond. A cut at frame 10 of 29.97 fps is 0.333667 s, and rounding to 0.334 s renders one frame late. Clip starts round down and source in-points round up, so the cut frame lands on the new clip and shows the right source frame. NTSC rates stay exact (30000/1001).
  • Warnings: everything skipped or approximated is reported and nothing is saved in the project. The exit code is 0 if anything imported, and non-zero only when nothing could be.
  • Missing media: clips whose files can't be found are dropped and named. If none are found, the import stops and points at --media-root.

Not supported

These are imported as hard cuts or skipped, each with a warning: wipes and other transition types, audio transitions, effects, color corrections, markers, generated media, reverse playback, remote URLs and editor-specific metadata.

Catalog and docs

The skill is registered in skills-manifest.json, and the published skill count goes from 9 to 10. The skill list is updated in README.md, AGENTS.md, CLAUDE.md, docs/guides/skills.mdx and both _shared template files. The count and catalog references are also updated in scripts/check-core-skills.mjs, scripts/sync-schemas.ts and the two skill-lifecycle files. New docs are docs/guides/import-from-otio.mdx and docs/reference/timeline-import.mdx, plus a docs/packages/cli.mdx entry.

Test plan

Automated tests

Three small hand-written fixtures live in skills/import-timeline/scripts/tests/fixtures/, about 18 KB in all, and none is taken from a real production. cut.otio has a trim, a 2x speed change, a dissolve, a second lane with a 12-image sequence and an audio track. cut.edl is a two-cut EDL. Each has an expected-output file (cut.expected.json, cut-edl.expected.json) that the converter's output must match exactly. Media is generated with ffmpeg when the test runs, so no media is committed. Every other test builds its input in code or in a temp directory.

Suite Tests Covers
packages/core/src/timelineImport/timelineImport.test.ts 25 schema parsing, the rules JSON Schema can't state (unique ids, fps range), HTML emission, lint-clean output
packages/core/src/timelineImport/transitions.test.ts 36 dissolve planning, handle clamping, fades from and to nothing, audio and non-dissolve transitions, fades kept inside the composition (replays the emitted tweens through real GSAP and checks their start times)
packages/core/src/timelineImport/mediaOffset.test.ts 17 file timecode, mediaOrigin hint and plain-seconds fallbacks, rounding up
packages/cli/src/commands/initTimeline.test.ts 70 staging, media lookup and relinking, --link-media, warnings and their cap, duplicate audio, missing media, timecode offsets
packages/cli/src/commands/initTimelineOrigin.test.ts 20 ffprobe timecode and WAV time_reference, including drop-frame
packages/cli/src/commands/initTimelineSequence.test.ts 11 image-sequence listing, missing and held frames
packages/cli/src/commands/initTimelineSequence.encode.test.ts 14 encodes real sequences with ffmpeg and decodes the output to check the frame count
packages/cli/src/commands/init.timeline.test.ts 19 the flags, the flags that can't be combined, cleanup on failure
packages/cli/src/commands/timelineImportSchema.test.ts 99 the schema file and the TypeScript parser agree; the format side never mentions OTIO
packages/cli/src/commands/initTimelineFixtures.test.ts 3 stages each fixture's expected JSON against ffmpeg-made media, then lints the project; checks the dissolve becomes a fade that starts inside the film
skills/import-timeline/scripts/otio_flatten_test.py 87 the converter: placement, trims, speed, nested stacks, transitions, image sequences (names checked against the library's own), bundles (including a reused folder whose files differ at the same size), .otio, EDL and the other adapters, output validated against the schema; the fixtures above flatten to their expected JSON

The Python tests run with python3 -I otio_flatten_test.py in a venv with opentimelineio and its adapters. A new CI job, Test: import-timeline converter, installs pinned versions (opentimelineio 0.18.1 and the EDL, Final Cut Pro 7, Final Cut Pro X, GStreamer and ALE adapters, plus jsonschema) and runs them when a skill or the timeline schema changes. The job fails if any test is skipped, so a missing adapter can't pass silently, and the aggregate Test check requires it. The TypeScript fixture test runs in the existing CLI test shards.

Real timelines

Checked against their reference renders:

  • Animal Logic ALab trailer (ASWF Digital Production Example Library): converted and imported with init, then rendered and compared with the reference render. Cuts are exact, SSIM is 0.942 and PSNR 43.4 dB, and audio is aligned at 0 ms (6 dB hot, because the AAF lists the stems twice).

  • AWS Picchu edit, 4096x2048 (ASWF Digital Production Example Library) (.otioz bundle): per-clip audio offsets match the reference for 75 of 88 clips. The render was 1024x512 at draft quality. Known differences: compound clips are skipped (black near 36.5 s and 410 s), and the editor's fades and one transform are not applied. A mismatch near 100 s comes from the OTIO itself: the clip's in-point is 46 frames from where the reference shows it, and ours matches the OTIO's frame exactly.

Clip placement check:

darbyjohnston/otio-oc-examples: all 12 files were flattened, and every clip's placement was compared with the OTIO library's own. All 12 place every clip identically.

File Rate Clips Notes
ASC_StEM2_239_4K_24_100nits_Rec709_Stereo_ProRes422HQ 24 198
STEM2_4k_ctm_ACES_239 24 198 its EXR sequences say a missing frame should show black; the importer holds the previous image instead and warns once, with a count of the clips
StEM2_MultipleMediaRefs 24 198 each clip lists a full-resolution image sequence and a proxy .mov, with the proxy selected; the import uses the selected one and ignores the clip colors
Chimera_3840x2160_2997fps_10bit_422 29.97 6
ElFuente_4096x2160_60fps_10bit_420 60 5
ElFuente_4096x2160_60fps_10bit_420_remix1 60 10 4 dissolves and a lane of PNG overlays
Meridian_UHD4k5994_HDR_P3PQ 59.94 83
SolLevante_Animatics_ver1_H264 23.976 63
SolLevante_HDR10_r2020_ST2084_UHD_24fps_1000nit 24 144 72 video clips and 72 audio clips, all cut from one .mov file
sollevante_lp_vdm_16b_p3d65_pq_20200218_3840x2160 24 72 whole film as one TIFF sequence with no available range
SPARKS_HDR10_4K_2020_PQ_1000nits 59.94 39 whole film as one TIFF sequence with no available range; same cuts as the next row
SPARKS_P3_PQ_4000nit 59.94 39 whole film as one EXR sequence with no available range; same cuts as the row above

End-to-end init check: a small timeline of two ffmpeg test clips with a dissolve and a tone track went through init --timeline, then hyperframes lint (0 errors, 0 warnings) and hyperframes check (passed). The dissolve's incoming clip starts early on its handle and the fade tween matches it.

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

@mintlify

mintlify Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Preview approval required

This pull request comes from a fork, so Mintlify won't build a preview until someone with write access approves commit c1b0765.

Approve preview

const used = new Set<string>(RESERVED_IDS);
const ids = new Map<string, string>();
for (const clip of clips) {
const base = clip.id.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "") || "clip";

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved

…an OTIO timeline

`init --timeline timeline.json` builds a project from an edit made in another
editor. It reads a flat `hyperframes-timeline/1` JSON file and creates one clip
per cut: source trims, speed changes, audio tracks, dissolves, freeze frames and
image sequences carry over, and media is copied into `assets/`. New flags are
`--timeline`, `--media-root` and `--link-media`.

A new `/import-timeline` skill ships a Python converter that writes the JSON from
OpenTimelineIO and the formats its adapters read. The schema lives in
`packages/core/schemas/timeline-import.json`; the emitter, dissolve planner and
media-offset resolver are in `@hyperframes/core/timeline-import`.

Tests: core and CLI unit tests, converter tests with hand-written fixtures, an
ffmpeg-backed `init` fixture test, and a CI job for the Python converter.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants