← Commit history

0.1.35: compose concat lists use absolute paths; recut and compose recover clip files the manifest lost

John Lauer ·f96f12c152 ·7d ago ·parent 1eb7daa
8 files changed +68−23
Cargo.lock+10−10
@@ -4,7 +4,7 @@ version = 4  [[package]] name = "adom-aiflow"-version = "0.1.34"+version = "0.1.35" dependencies = [  "aiflow-analyze",  "aiflow-board",@@ -23,7 +23,7 @@ dependencies = [  [[package]] name = "aiflow-analyze"-version = "0.1.34"+version = "0.1.35" dependencies = [  "serde",  "serde_json",@@ -32,7 +32,7 @@ dependencies = [  [[package]] name = "aiflow-board"-version = "0.1.34"+version = "0.1.35" dependencies = [  "serde",  "serde_json",@@ -40,7 +40,7 @@ dependencies = [  [[package]] name = "aiflow-bridge"-version = "0.1.34"+version = "0.1.35" dependencies = [  "serde",  "serde_json",@@ -48,7 +48,7 @@ dependencies = [  [[package]] name = "aiflow-copper"-version = "0.1.34"+version = "0.1.35" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -58,7 +58,7 @@ dependencies = [  [[package]] name = "aiflow-grid"-version = "0.1.34"+version = "0.1.35" dependencies = [  "aiflow-board",  "serde",@@ -67,7 +67,7 @@ dependencies = [  [[package]] name = "aiflow-place"-version = "0.1.34"+version = "0.1.35" dependencies = [  "aiflow-board",  "serde",@@ -76,7 +76,7 @@ dependencies = [  [[package]] name = "aiflow-pours"-version = "0.1.34"+version = "0.1.35" dependencies = [  "aiflow-board",  "aiflow-copper",@@ -86,7 +86,7 @@ dependencies = [  [[package]] name = "aiflow-router"-version = "0.1.34"+version = "0.1.35" dependencies = [  "aiflow-board",  "aiflow-grid",@@ -96,7 +96,7 @@ dependencies = [  [[package]] name = "aiflow-run"-version = "0.1.34"+version = "0.1.35" dependencies = [  "serde",  "serde_json",
Cargo.toml+1−1
@@ -14,7 +14,7 @@ members = [ ]  [workspace.package]-version = "0.1.34"+version = "0.1.35" edition = "2021" license = "MIT" repository = "https://wiki.adom.inc/adom/adom-aiflow"
bin/adom-aiflow
⋯ 1 unchanged line ⋯
crates/adom-aiflow/src/main.rs+46−5
@@ -484,6 +484,29 @@ fn step_shown(step: &str) -> String { }  +/// A clip stopped twice loses its file on the second stop (the recorder answers "nothing running"),+/// though the ledger kept the first stop's file under the same recording id. Put it back.+fn recover_capture_files(r: &mut Run, dir: &Path) -> usize {+    let mut by_rid: std::collections::HashMap<String, String> = Default::default();+    for e in &r.ledger() {+        if e["event"] == "clip-stop" {+            if let (Some(rid), Some(f)) = (e["recordingId"].as_str(), e["file"].as_str()) {+                let local = dir.join(Path::new(f).file_name().unwrap_or_default());+                if local.is_file() { by_rid.insert(rid.to_string(), local.display().to_string()); }+            }+        }+    }+    let mut n = 0;+    if let Some(caps) = r.data["captures"].as_array_mut() {+        for c in caps.iter_mut() {+            if c["file"].as_str().is_some() { continue; }+            if let Some(f) = c["recordingId"].as_str().and_then(|rid| by_rid.get(rid)).cloned() { c["file"] = json!(f); n += 1; }+        }+    }+    if n > 0 { r.mark(&format!("recovered {n} clip file(s) from the ledger")); }+    n+}+ /// Stop the running clip of the current step, pull it, cut it. finish and deliver call this so /// no recorder outlives the run (a clip left running once recorded nine hours of a static editor). fn stop_running_clip(r: &mut Run, br: &Bridge, dir: &Path, why: &str) -> Option<String> {@@ -655,10 +678,25 @@ fn compose_run(r: &mut Run, dir: &Path, outp: &Path, max_seconds: f64, still_sec     for e in &led { if e["event"] == "artifact" && e["kind"] == "clipAction" { act.insert(e["step"].as_str().unwrap_or("").to_string(), (e["realSeconds"].as_f64().unwrap_or(0.0), e["seconds"].as_f64().unwrap_or(0.0))); } }     let t0 = r.data["clock"]["promptTime"].as_str().and_then(aiflow_run::parse).unwrap_or(0);     let caps = r.data["captures"].as_array().cloned().unwrap_or_default();+    // a clip stopped twice loses its file on the second stop (the recorder answers "nothing running");+    // the ledger kept the first stop's file under the same recording id, so recover it from there+    let mut ledger_file: std::collections::HashMap<String, String> = Default::default();+    for e in &led {+        if e["event"] == "clip-stop" {+            if let (Some(rid), Some(f)) = (e["recordingId"].as_str(), e["file"].as_str()) {+                let local = dir.join(Path::new(f).file_name().unwrap_or_default());+                if local.is_file() { ledger_file.insert(rid.to_string(), local.display().to_string()); }+            }+        }+    }     // the pieces, in the order they happened: clips by their start, drawings by the moment the AI registered them     let mut pieces: Vec<(u64, Piece)> = Vec::new();     for (i, c) in caps.iter().enumerate() {         if c.get("discarded").is_some() || c.get("suspect").is_some() { continue; }+        let recovered = c["recordingId"].as_str().and_then(|rid| ledger_file.get(rid)).cloned();+        let mut c = c.clone();+        if c["file"].as_str().is_none() { if let Some(f) = &recovered { c["file"] = json!(f); } }+        let c = &c;         let (Some(f), Some(step)) = (c["file"].as_str(), c["step"].as_str()) else { continue };         if step == "3d" && c["tour"].as_str() != Some("3d") { continue; }         let visit = c["visit"].as_u64().unwrap_or(1);@@ -772,8 +810,8 @@ fn compose_run(r: &mut Run, dir: &Path, outp: &Path, max_seconds: f64, still_sec     if parts.is_empty() { err("nothing to compose: every segment failed to render", &[]); }     let vlist = workdir.join("video.txt");     let alist = workdir.join("audio.txt");-    std::fs::write(&vlist, parts.iter().map(|(v, _)| format!("file '{}'\n", v.display())).collect::<String>()).unwrap();-    std::fs::write(&alist, parts.iter().map(|(_, a)| format!("file '{}'\n", a.display())).collect::<String>()).unwrap();+    std::fs::write(&vlist, parts.iter().map(|(v, _)| format!("file '{}'\n", std::fs::canonicalize(v).unwrap_or_else(|_| v.clone()).display())).collect::<String>()).unwrap();+    std::fs::write(&alist, parts.iter().map(|(_, a)| format!("file '{}'\n", std::fs::canonicalize(a).unwrap_or_else(|_| a.clone()).display())).collect::<String>()).unwrap();     let vcat = workdir.join("video.mp4");     let acat = workdir.join("audio.m4a");     let okv = std::process::Command::new("ffmpeg").args(["-v", "error", "-y", "-f", "concat", "-safe", "0", "-i", vlist.to_str().unwrap(), "-c", "copy", vcat.to_str().unwrap()]).status().map(|s| s.success()).unwrap_or(false);@@ -964,7 +1002,7 @@ fn compose_pair(a: &mut Run, b: &Run, dir: &Path, outp: &Path, max_seconds: f64,                         if run_ffmpeg(&["-i", &v.tenx, "-an", "-vf", &chain, "-r", "30", "-pix_fmt", "yuv420p", "-c:v", "libx264", "-preset", "veryfast", "-crf", "20", piece.to_str().unwrap()]) { pieces.push(piece); }                     }                     let list = workdir.join(format!("col-{si}-{side}.txt"));-                    std::fs::write(&list, pieces.iter().map(|p| format!("file '{}'\n", p.display())).collect::<String>()).unwrap();+                    std::fs::write(&list, pieces.iter().map(|p| format!("file '{}'\n", std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf()).display())).collect::<String>()).unwrap();                     let cat = workdir.join(format!("cat-{si}-{side}.mp4"));                     let _ = run_ffmpeg(&["-f", "concat", "-safe", "0", "-i", list.to_str().unwrap(), "-c", "copy", cat.to_str().unwrap()]);                     // exactly t_step: the slower column holds its last frame for the rest (a side-by-side has to wait)@@ -1054,8 +1092,8 @@ fn audio_for(workdir: &Path, k: usize, tiers: &[String], vlen: f64, has_tts: boo fn assemble(workdir: &Path, parts: &[(PathBuf, PathBuf)], outp: &Path) {     let vlist = workdir.join("video.txt");     let alist = workdir.join("audio.txt");-    std::fs::write(&vlist, parts.iter().map(|(v, _)| format!("file '{}'\n", v.display())).collect::<String>()).unwrap();-    std::fs::write(&alist, parts.iter().map(|(_, a)| format!("file '{}'\n", a.display())).collect::<String>()).unwrap();+    std::fs::write(&vlist, parts.iter().map(|(v, _)| format!("file '{}'\n", std::fs::canonicalize(v).unwrap_or_else(|_| v.clone()).display())).collect::<String>()).unwrap();+    std::fs::write(&alist, parts.iter().map(|(_, a)| format!("file '{}'\n", std::fs::canonicalize(a).unwrap_or_else(|_| a.clone()).display())).collect::<String>()).unwrap();     let vcat = workdir.join("video.mp4");     let acat = workdir.join("audio.m4a");     let okv = run_ffmpeg(&["-f", "concat", "-safe", "0", "-i", vlist.to_str().unwrap(), "-c", "copy", vcat.to_str().unwrap()]);@@ -2247,6 +2285,8 @@ fn main() {         Cmd::Recut => {             thread(&cli);             let mut r = load_run(&cli);+            let recovered = recover_capture_files(&mut r, &dir);+            if recovered > 0 { eprintln!("recovered {recovered} clip file(s) the manifest had lost"); }             let caps = r.data["captures"].as_array().cloned().unwrap_or_default();             let mut n = 0;             for c in &caps {@@ -2263,6 +2303,7 @@ fn main() {         Cmd::Compose { out, max_seconds, still_seconds, with, script } => {             thread(&cli);             let mut r = load_run(&cli);+            if recover_capture_files(&mut r, &dir) > 0 { r.save().unwrap(); }             let outp = if Path::new(out).is_absolute() { PathBuf::from(out) } else { dir.join(out) };             if let Some(other) = with {                 let b = Run::open(other).unwrap_or_else(|| err(&format!("no run at {}", other.display()), &["The other run needs its run.json, run.jsonl and its clips' 10x cuts in that directory.".into()]));
docs/release-0.1.35.mdadded+7
@@ -0,0 +1,7 @@+# AI Flow 0.1.35++Two `compose` fixes found while delivering the buck molecule run.++- **Concat lists use absolute paths.** `compose` wrote run-relative paths into ffmpeg's concat lists, and ffmpeg resolves those against the list's own folder, so any relative `--run` (the default `./aiflow-run` included) failed with "could not assemble the video". The lists now carry absolute paths.+- **Clips the manifest lost come back.** A clip stopped twice lost its file on the second stop (the recorder answers "nothing running"), although the ledger kept the first stop's file under the same recording id. `recut` and `compose` now recover those files from the ledger and save them to the run. On the buck run this brought back 5 of 14 clips, and the final video went from 3 step visits to 6.+- The `aiflow-live-clips` skill now says plainly that plain links resolve from the doc's folder while media sources are repo-root-relative.
package.json+1−1
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.34",+  "version": "0.1.35",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",
page.json+2−5
@@ -1,7 +1,7 @@ {   "slug": "adom-aiflow",   "type": "app",-  "version": "0.1.34",+  "version": "0.1.35",   "title": "AI Flow",   "description": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. One Rust binary with a crate per step (placement helpers, a grid router with Kelvin taps, pours with keepouts, KiCad's DRC gate, live landing through the KiCad Bridge, copper measurement, current and thermal analysis) and a finish line that refuses an unfinished board. Every command answers with hints for the AI; every turn, its thinking time and every rework loop go into run.jsonl, so Claude, Codex and any other engine are compared on the same flow. KiCad today; Altium, Fusion and Adom's own web apps next.",   "summary": "Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board. The AI thinks its way from placement through routing, pours, current and thermal analysis to a delivered video; the binary does the fast, deterministic parts of every step, hands back hints, and keeps a ledger of every turn, every return to an earlier step, and the clock from the prompt to done.",@@ -85,8 +85,5 @@   "author": {     "name": "John Lauer",     "email": "[email protected]"-  },-  "confirmed_user_skills": [-    "aiflow-simulate"-  ]+  } }\ No newline at end of file
skills/aiflow-live-clips/SKILL.md+1−1
@@ -44,7 +44,7 @@ Wrap the push in `adom-aiflow exec -- python3 tools/clips_subreadme.py ...` so i  - **Publish with `adom-wiki repo push --files`**, not the files blob API (it does not update the git repo the Files tab reads). - **Media paths are repo-root-relative**, even inside `docs/clips.md`: `docs/clips/<stem>-10x.mp4`, `docs/clips/<stem>-sheet.png`. The files viewer prepends `files/` to the ref as written; a path relative to the doc's own folder (`clips/x.mp4`) points at the wrong place. Use the same form for plain links to the action cuts.-- **Doc links are relative** (`docs/clips.md` from the README), never absolute URLs.+- **Doc links are relative** (`docs/clips.md` from the README), never absolute URLs. A plain link (not an image or a video `src`) resolves from the doc's own folder, so from `docs/clips.md` the action cut is `[...](clips/<stem>-action.mp4)`; checked on the rendered page, it lands on `files/docs/clips/...`. - **Push only what changed** plus `docs/clips.md`; pushing the whole run folder is slow and bloats the page. - **Scrub before every push**: the script refuses a payload that names the in-house fab's product or carries an em-dash. Say "our own in-house PCB fab". - **Rate limits**: one push per step is fine; do not push in a loop.