Skip to content

Commit 57f6d71

Browse files
committed
feat(v0.22.0): add stable export source contract
session: 7b54e560 What: add render_source.v1 fields, composition compile, recorder source validate/export/snapshot, and MP4 verify-export frame checks. Why: split AI authoring from recorder input so export uses a fixed contract, preventing clip-order/state bugs such as later clips rendering with the wrong background while keeping nf export as the compatibility wrapper. Co-Authored-By: Claude Opus 4.7
1 parent bd4ed99 commit 57f6d71

19 files changed

Lines changed: 1235 additions & 16 deletions

File tree

CLAUDE.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@ Example projects live under `examples/`; runtime projects are copied to `~/.next
2323
`target/debug/nf composition show --project=<slug> --composition=<slug> [--clip=<id>] [--track=<id>] [--item=<id>] [--field=<path>]` — read raw composition JSON or one clip/track/item field.
2424
`target/debug/nf composition patch --project=<slug> --composition=<slug> [--clip=<id>] --track=<id> [--item=<id>] --field=<path> --value=<json-or-string>` — patch one track or clip item field such as `params.title`, `style.x`, or `time.start`.
2525
`target/debug/nf composition validate --project=<slug> --composition=<slug>` — validate component registry, files, mount/update exports, import-free ABI, used tracks, and observed params.
26+
`target/debug/nf composition compile --project=<slug> --composition=<slug> --out=<render_source.json>` — compile AI-authored composition JSON into stable `nf.render_source.v1` recorder input.
2627
`target/debug/nf verify --project=<slug> --composition=<slug> [--out=<json>] [--screenshot-dir=<dir>]` — verify AI-authored composition JSON: component ABI, compiled source, overlap intent, anchor guide, ASCII timeline, layout/text checks, and typical screenshot commands.
28+
`target/debug/nf-recorder validate-source --source=<render_source.json>` — validate recorder input contract without reading project storage or desktop state.
29+
`target/debug/nf-recorder export --source=<render_source.json> --profile=draft|standard|final|final-fast --output=<mp4> [--diagnostics=<json>]` — export directly from render source through the recorder module.
30+
`target/debug/nf-recorder snapshot-source --source=<render_source.json> --t-ms=<ms> --output=<png>` — sample one render-source frame through the same recorder HTML/runtime path.
2731
`target/debug/nf export --project=<slug> --composition=<slug> --profile=draft|standard|final|final-fast --out=<mp4>` — export a composition to MP4 with a named quality/speed profile.
2832
`target/debug/nf export --project=<slug> --composition=<slug> --profile=draft --diagnostics --out=<mp4>` — export and write a sibling diagnostics JSON with frame timings, slow spans, and top slow frames.
2933
`target/debug/nf export --project=<slug> --composition=<slug> --fps=30|60 --resolution=720p|1080p|4k --parallel=<1-8> --events --out=<mp4>` — override export settings and stream recorder progress JSONL before the final summary JSON.
34+
`target/debug/nf verify-export --source=<render_source.json> --video=<mp4> --out=<report.json>` — sample exported MP4 clip frames and fail on magenta-background or blank-frame regressions.
3035
`target/debug/nf export-status --job-id=<id>` — read desktop export job status, including progress and diagnostics summary/path when available.
3136
`target/debug/nf export-cancel --job-id=<id>` — cancel a running desktop export job and stop its recorder process group.
3237
`./scripts/check-structure.sh` — verify repository skeleton: root allowlist, no nested spec git, no tracked generated artifacts.
@@ -44,9 +49,9 @@ Do not write generated videos, screenshots, node_modules, Cargo targets, or one-
4449

4550
## Current Focus
4651

47-
v0.21.0 makes composition authoring clip-first: a composition is the whole video, each top-level clip is one video segment, and each clip owns local anchors, tracks, TTS, subtitle_timeline, and subtitles.
52+
v0.22.0 hardens export contracts: AI writes `composition.json`, `nf composition compile` emits `render_source.v1`, `nf-recorder` accepts only that source contract, and `nf verify-export` checks final MP4 frames against clip windows.
4853

4954
Specs and acceptance scenarios:
5055

51-
- `spec/versions/v0.21.0/spec.json`
52-
- `spec/bdd/clip-first-composition/feature.json`
56+
- `spec/bdd/clip-first-export/feature.json`
57+
- `spec/bdd/clip-first-export/contracts/render-source-v1.md`

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/nf-cli/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ directories.workspace = true
1919
interprocess.workspace = true
2020
nf-project = { path = "../nf-project" }
2121
nf-recorder = { path = "../nf-recorder" }
22+
png = "0.17"
2223
serde.workspace = true
2324
serde_json.workspace = true
2425
thiserror.workspace = true

crates/nf-cli/src/commands/compositions.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
use nf_project::{JsonStorage, Storage, validate_composition_components};
1+
use std::fs;
2+
3+
use nf_project::{
4+
JsonStorage, Storage, compile_composition_source, validate_composition_components,
5+
};
26
use serde_json::{Value, json};
37

48
use crate::commands::{CompositionCommand, CompositionSubcommand, print_json, send_ipc};
@@ -30,7 +34,34 @@ pub fn dispatch(args: CompositionCommand) -> Result<(), NfError> {
3034
}),
3135
),
3236
CompositionSubcommand::Validate(args) => validate(args.project, args.composition),
37+
CompositionSubcommand::Compile(args) => compile(args.project, args.composition, args.out),
38+
}
39+
}
40+
41+
fn compile(project: String, composition: String, out: std::path::PathBuf) -> Result<(), NfError> {
42+
let storage = JsonStorage::new(JsonStorage::default_root()?);
43+
storage.load_project(&project)?;
44+
if !storage.composition_exists(&project, &composition)? {
45+
return Err(NfError::ValidationFailed(format!(
46+
"composition not found: {project}/{composition}"
47+
)));
48+
}
49+
let composition_json = storage.load_composition(&project, &composition)?;
50+
let compiled = compile_composition_source(&storage, &project, &composition_json)?;
51+
if let Some(parent) = out.parent().filter(|parent| !parent.as_os_str().is_empty()) {
52+
fs::create_dir_all(parent).map_err(|err| NfError::StorageFailed(err.to_string()))?;
3353
}
54+
let bytes = serde_json::to_vec_pretty(&compiled.source)?;
55+
fs::write(&out, bytes).map_err(|err| NfError::StorageFailed(err.to_string()))?;
56+
print_json(&json!({
57+
"project": project,
58+
"composition": composition,
59+
"out": out.display().to_string(),
60+
"schema_version": compiled.source.get("schema_version").and_then(Value::as_str),
61+
"duration_ms": compiled.source.get("duration_ms").and_then(Value::as_u64),
62+
"tracks": compiled.source.get("tracks").and_then(Value::as_array).map(Vec::len).unwrap_or(0),
63+
"warnings": compiled.warnings
64+
}))
3465
}
3566

3667
fn validate(project: String, composition: String) -> Result<(), NfError> {

crates/nf-cli/src/commands/mod.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub mod poster_import;
1919
pub mod projects;
2020
pub mod utility;
2121
pub mod verify;
22+
pub mod verify_export;
2223

2324
#[derive(Debug, Parser)]
2425
#[command(
@@ -213,6 +214,26 @@ COMMON ERRORS:
213214
- invalid timeline words -> exit 2 · hint: timeline words must use word/start_ms/end_ms"#
214215
)]
215216
PosterImport(PosterImportArgs),
217+
#[command(
218+
name = "verify-export",
219+
about = "Verify exported MP4 against render_source.v1 clip windows",
220+
long_about = r#"Verify a final exported MP4 by sampling clip windows from the render_source.v1 contract.
221+
222+
USAGE:
223+
nf verify-export --source=<render_source.json> --video=<out.mp4> --out=<report.json>
224+
225+
EXAMPLES:
226+
nf verify-export --source=tmp/showreel.render_source.json --video=tmp/showreel.mp4 --out=tmp/showreel.verify-export.json
227+
228+
EXPECTED JSON:
229+
{"ok":true,"checks":[{"clip":"intro","t_ms":2600,"status":"pass"}],"frames_dir":"tmp/verify-export-frames"}
230+
231+
COMMON ERRORS:
232+
- source invalid -> exit 2 · hint: run `nf-recorder validate-source --source=<json>`
233+
- video missing -> exit 2 · hint: run `nf-recorder export --source=<json> --profile=draft -o <mp4>`
234+
- ffmpeg missing -> exit 2 · hint: install ffmpeg to enable frame sampling"#
235+
)]
236+
VerifyExport(VerifyExportArgs),
216237
#[command(
217238
name = "export-status",
218239
about = "Read a running desktop export job status",
@@ -441,14 +462,17 @@ USAGE:
441462
nf composition show --project=<slug> --composition=<slug> [--track=<id>] [--field=<path>]
442463
nf composition patch --project=<slug> --composition=<slug> --track=<id> --field=<path> --value=<json-or-string>
443464
nf composition validate --project=<slug> --composition=<slug>
465+
nf composition compile --project=<slug> --composition=<slug> --out=<render_source.json>
444466
445467
EXAMPLES:
446468
nf composition show --project=v2-showcase --composition=showreel-24s --track=final-title --field=params.title
447469
nf composition patch --project=v2-showcase --composition=showreel-24s --track=final-title --field=params.title --value='NEXTFRAME LIVE EDIT'
448470
nf composition validate --project=v2-showcase --composition=showreel-24s
471+
nf composition compile --project=v2-showcase --composition=showreel-clip-first --out=tmp/showreel.render_source.json
449472
450473
EXPECTED JSON:
451474
{"composition":{...},"source":{...},"warnings":[]}
475+
{"project":"v2-showcase","composition":"showreel-clip-first","out":"tmp/showreel.render_source.json","schema_version":"nf.render_source.v1","duration_ms":16400,"warnings":[]}
452476
{"ok":true,"components":[...],"errors":[]}
453477
454478
COMMON ERRORS:
@@ -966,6 +990,8 @@ pub enum CompositionSubcommand {
966990
Patch(CompositionPatchArgs),
967991
#[command(about = "Validate composition component registry and component ABI")]
968992
Validate(CompositionValidateArgs),
993+
#[command(about = "Compile composition.json into stable render_source.v1 JSON")]
994+
Compile(CompositionCompileArgs),
969995
}
970996

971997
#[derive(Debug, Args)]
@@ -1054,6 +1080,34 @@ pub struct CompositionValidateArgs {
10541080
pub composition: String,
10551081
}
10561082

1083+
#[derive(Debug, Args)]
1084+
pub struct CompositionCompileArgs {
1085+
#[arg(
1086+
long,
1087+
value_name = "SLUG",
1088+
help = "Project slug, for example v2-showcase"
1089+
)]
1090+
pub project: String,
1091+
#[arg(
1092+
long,
1093+
value_name = "SLUG",
1094+
help = "Composition slug, for example showreel-clip-first"
1095+
)]
1096+
pub composition: String,
1097+
#[arg(long, value_name = "JSON", help = "Output render_source.v1 JSON path")]
1098+
pub out: PathBuf,
1099+
}
1100+
1101+
#[derive(Debug, Args)]
1102+
pub struct VerifyExportArgs {
1103+
#[arg(long, value_name = "JSON", help = "Input render_source.v1 JSON path")]
1104+
pub source: PathBuf,
1105+
#[arg(long, value_name = "MP4", help = "Exported MP4 path to inspect")]
1106+
pub video: PathBuf,
1107+
#[arg(long, value_name = "JSON", help = "Output verification report JSON")]
1108+
pub out: PathBuf,
1109+
}
1110+
10571111
#[derive(Debug, Subcommand)]
10581112
pub enum ProjectSubcommand {
10591113
#[command(

0 commit comments

Comments
 (0)