Merge pull request #48 from punkhop/fix/sequence-inspect-readback

engine: sequence.inspect reports clip ends, gain and marker comments
This commit is contained in:
Brandon Thomas 2026-10-06 11:36:53 -04:00 committed by GitHub
commit 7377def476
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 2 deletions

View File

@ -2721,6 +2721,7 @@ pub fn inspect_sequence(s: &Session, id: ItemId, q: &filmcraft_project::Sequence
"items": t.items.iter().map(|i| json!({
"clip": i.id.0, "item": i.item.0, "name": i.name, "start": i.start.0, "duration": i.duration.0,
"startFrame": rate.frame_at(i.start), "durationFrames": rate.frame_at(i.duration), "sourceIn": i.source_in.0, "speed": i.speed,
"end": i.end().0, "endFrame": rate.frame_at(i.end()), "sourceOut": i.source_out().0, "gainDb": i.gain_db,
"enabled": i.enabled, "link": i.link, "label": i.label.name(),
"effects": i.effects.iter().map(|e| json!({"effect": e.effect, "enabled": e.enabled, "masks": e.masks.len(), "params": e.params.iter().map(|(k, p)| (k.clone(), json!({"value": format!("{:?}", p.value), "keyframes": p.keyframes.len()}))).collect::<serde_json::Map<_, _>>()})).collect::<Vec<_>>(),
})).collect::<Vec<_>>(),
@ -2737,7 +2738,7 @@ pub fn inspect_sequence(s: &Session, id: ItemId, q: &filmcraft_project::Sequence
"in": q.mark_in.map(|t| t.0),
"out": q.mark_out.map(|t| t.0),
"split": q.split,
"markers": q.markers.iter().map(|m| json!({"id": m.id.0, "start": m.start.0, "duration": m.duration.0, "kind": format!("{:?}", m.kind), "name": m.name, "color": m.color.name()})).collect::<Vec<_>>(),
"markers": q.markers.iter().map(|m| json!({"id": m.id.0, "start": m.start.0, "duration": m.duration.0, "kind": format!("{:?}", m.kind), "name": m.name, "comment": m.comment, "color": m.color.name()})).collect::<Vec<_>>(),
"video": q.video_tracks.iter().map(tr).collect::<Vec<_>>(),
"audio": q.audio_tracks.iter().map(tr).collect::<Vec<_>>(),
"selection": s.state.selection.iter().map(|c| c.0).collect::<Vec<_>>(),

View File

@ -918,6 +918,8 @@ mod remix_tests;
#[cfg(test)]
mod scopes_tests;
#[cfg(test)]
mod sequence_inspect_tests;
#[cfg(test)]
mod sequence_tools_tests;
#[cfg(test)]
mod settings_tests;

View File

@ -0,0 +1,61 @@
//! `sequence.inspect` is how an agent checks its edits: it has to show where a clip ends in the
//! sequence and in its media, the clip's audio gain and what a marker says.
use serde_json::json;
use crate::Session;
#[test]
fn sequence_inspect_reports_clip_ends_gain_and_marker_comments() {
let mut s = Session::default();
s.execute("file.openDemoProject", json!({})).unwrap();
let rate = s.sequence_rate();
// a clip at 200 %, a clip with gain and a marker with a comment, all made through commands
s.execute("sequence.linkedSelection", json!({"on": false})).unwrap();
let (fast, loud) = {
let q = s.active_sequence().unwrap();
(q.video_tracks[0].items.last().unwrap().id, q.audio_tracks[0].items[0].id)
};
s.execute("timeline.select", json!({"clips": [fast.0]})).unwrap();
s.execute("clip.speedDuration", json!({"speed": 200})).unwrap();
s.execute("timeline.select", json!({"clips": [loud.0]})).unwrap();
s.execute("clip.audioGain", json!({"mode": "set", "db": -6.5})).unwrap();
let marker = s.execute("markers.add", json!({"time": rate.tick_of(12).0, "name": "Beat", "comment": "cut on the downbeat"})).unwrap()["marker"].clone();
let q = s.active_sequence().unwrap().clone();
let js = s.execute("sequence.inspect", json!({})).unwrap();
let m = js["markers"].as_array().unwrap().iter().find(|m| m["id"] == marker).unwrap();
assert_eq!((m["name"].as_str(), m["comment"].as_str()), (Some("Beat"), Some("cut on the downbeat")));
let mut seen = 0;
for (key, tracks) in [("video", &q.video_tracks), ("audio", &q.audio_tracks)] {
for (ti, t) in tracks.iter().enumerate() {
for (ci, c) in t.items.iter().enumerate() {
let j = &js[key][ti]["items"][ci];
assert_eq!(j["clip"], c.id.0);
assert_eq!(j["end"], c.end().0, "{j}");
assert_eq!(j["end"].as_i64(), j["start"].as_i64().zip(j["duration"].as_i64()).map(|(a, b)| a + b));
assert_eq!(j["endFrame"], rate.frame_at(c.end()));
assert_eq!(j["sourceOut"], c.source_out().0, "{j}");
assert_eq!(j["gainDb"], c.gain_db);
// the keys that were there before stay
for old in ["item", "name", "start", "duration", "startFrame", "durationFrames", "sourceIn", "speed", "enabled", "link", "label", "effects"] {
assert!(j.get(old).is_some(), "{old} is still reported");
}
if c.id == fast {
assert_eq!(j["speed"], 2.0);
assert_eq!(
j["sourceOut"].as_i64().unwrap() - j["sourceIn"].as_i64().unwrap(),
2 * j["duration"].as_i64().unwrap(),
"at 200% a clip uses twice its length of media"
);
seen += 1;
}
if c.id == loud {
assert_eq!(j["gainDb"], -6.5);
seen += 1;
}
}
}
}
assert_eq!(seen, 2, "both edited clips were found in the readback");
}

View File

@ -43,7 +43,7 @@ claude mcp add filmcraft-headless -- /abs/path/filmcraft/target/release/filmcraf
| `doc_inspect` | both | the project tree and the active sequence in one call (`project_inspect` + `sequence_inspect`) |
| `render_preview` | both | same as `render_frame` |
| `project_inspect` | both | bins and items with ids, types, durations; active sequence |
| `sequence_inspect` | both | the active sequence: tracks, clips (ticks and frames), effects, transitions, markers, playhead, selection |
| `sequence_inspect` | both | the active sequence: tracks, clips (`start` / `end` and `sourceIn` / `sourceOut` in ticks, frames, `speed`, `gainDb`), effects, transitions, markers (name and `comment`), playhead, selection |
| `media_import` | both | import files by absolute path (`text`, one path per line) |
| `render_frame` | both | PNG of the program frame at `seconds` (headless renders; bridge screenshots the Program monitor) |
| `ui_inspect` | bridge | UI state: tool, workspace, panels, zoom, playback, fps |