← Commit history

0.2.4: clips and step pages play every clip in the ::adom-video widget

John Lauer ·0bb9087907 · ·parent 6e27c4b
4 files changed +43−6
crates/adom-aiflow/src/main.rs+2−1
@@ -400,7 +400,8 @@ fn clips_page_of(r: &Run) -> Option<String> { /// with an em-dash or a match of $AIFLOW_BANNED_RE. Returns (what happened, the files). fn publish_clips(r: &Run, dir: &Path, page: &str, push: bool) -> Result<(String, Vec<String>), String> {     let notes: Value = std::fs::read_to_string(dir.join("clip-notes.json")).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or(Value::Null);-    let (md, media) = preboard::clips_page(&r.data, dir, &notes, &now());+    let page_ref = r.data["clipsPage"].as_str().or(r.data["page"].as_str()).map(str::to_string);+    let (md, media) = preboard::clips_page(&r.data, dir, &notes, &now(), page_ref.as_deref());     let stage = dir.join("clips-page");     std::fs::create_dir_all(stage.join("docs").join("clips")).map_err(|e| format!("{}: {e}", stage.display()))?;     // docs/clips.md belongs to the first run that published it (a hidden marker says which). A later
crates/adom-aiflow/src/preboard.rs+25−5
@@ -238,7 +238,9 @@ fn when(iso: &str) -> String { /// docs/clips.md and the media it references: (markdown, [(local source, repo path)]). /// Media refs (video src, image) are repo-root-relative (docs/clips/x-10x.mp4); plain links are /// relative to docs/ (clips/x-action.mp4), which is how the wiki's files viewer resolves them.-pub fn clips_page(data: &Value, dir: &Path, notes: &Value, updated: &str) -> (String, Vec<(PathBuf, String)>) {+/// `page` is the wiki page the clips go to (owner/slug): with it every clip plays in the wiki's+/// `::adom-video` widget; without it, a plain `<video>`.+pub fn clips_page(data: &Value, dir: &Path, notes: &Value, updated: &str, page: Option<&str>) -> (String, Vec<(PathBuf, String)>) {     let t0 = data["clock"]["promptTime"].as_str().and_then(aiflow_run::parse);     let mut caps: Vec<Value> = data["captures"].as_array().cloned().unwrap_or_default().into_iter().filter(|c| c["file"].is_string() && c.get("discarded").map(|d| d.is_null()).unwrap_or(true)).collect();     caps.sort_by(|a, b| a["start"].as_str().unwrap_or("").cmp(b["start"].as_str().unwrap_or("")));@@ -292,11 +294,25 @@ pub fn clips_page(data: &Value, dir: &Path, notes: &Value, updated: &str) -> (St         if let Some(s) = note["shows"].as_str().filter(|s| !s.is_empty()) { b.push(s.to_string()); b.push(String::new()); }         for s in &state { b.push(format!("> {s}")); b.push(String::new()); }         if let Some(rel) = have.get("-10x.mp4") {-            let poster = have.get("-poster.jpg").map(|p| format!(" poster=\"{p}\"")).unwrap_or_default();-            b.push(format!("<video src=\"{rel}\"{poster} controls muted playsinline preload=\"metadata\"></video>"));+            // the wiki's video widget (captions, chapters, the Adom player) when the page is known; a plain+            // <video> otherwise. Widget paths are /<owner>/<slug>/render/<repo path>+            match page {+                Some(pg) => {+                    let poster = have.get("-poster.jpg").map(|p| format!(" poster=\"/{pg}/render/{p}\"")).unwrap_or_default();+                    b.push(format!("::adom-video{{video=\"/{pg}/render/{rel}\"{poster} title=\"Clip {n}: {} (10x)\" muted=\"true\"}}", title.replace('"', "'")));+                }+                None => {+                    let poster = have.get("-poster.jpg").map(|p| format!(" poster=\"{p}\"")).unwrap_or_default();+                    b.push(format!("<video src=\"{rel}\"{poster} controls muted playsinline preload=\"metadata\"></video>"));+                }+            }             b.push(String::new());         }-        if have.contains_key("-action.mp4") { b.push(format!("Motion-only action cut: [docs/clips/{stem}-action.mp4](clips/{stem}-action.mp4)")); b.push(String::new()); }+        if let (Some(pg), true) = (page, have.contains_key("-action.mp4")) {+            b.push(format!("::adom-video{{video=\"/{pg}/render/docs/clips/{stem}-action.mp4\" title=\"Clip {n}: {} (action cut)\" muted=\"true\"}}", title.replace('"', "'")));+            b.push(String::new());+        }+        if have.contains_key("-action.mp4") && page.is_none() { b.push(format!("Motion-only action cut: [docs/clips/{stem}-action.mp4](clips/{stem}-action.mp4)")); b.push(String::new()); }         if let Some(rel) = have.get("-sheet.png") { b.push(format!("Contact sheet (nine frames): ![clip {n} contact sheet]({rel})")); b.push(String::new()); }         if let Some(raw) = clip_local(dir, file) {             let mb = std::fs::metadata(&raw).map(|m| m.len()).unwrap_or(0) as f64 / 1e6;@@ -456,7 +472,11 @@ mod tests {             {"file": "./run/window-1-20260929-010000.mp4", "step": "placement", "visit": 1, "start": "2026-09-29T01:00:00Z", "stop": "2026-09-29T01:10:00Z", "suspect": "8 of 8 flat"},             {"file": "./run/window-2.mp4", "step": "3d", "visit": 1, "start": "2026-09-29T01:30:00Z", "discarded": "editor clip"}]});         let notes = json!({"window-1-20260929-010000": {"step": "Placement", "shows": "parts land", "state": "ok"}});-        let (md, media) = clips_page(&data, &dir, &notes, "2026-09-29T03:00:00Z");+        let (md, media) = clips_page(&data, &dir, &notes, "2026-09-29T03:00:00Z", None);+        let (wmd, _) = clips_page(&data, &dir, &notes, "2026-09-29T03:00:00Z", Some("john/b"));+        assert!(wmd.contains("::adom-video{video=\"/john/b/render/docs/clips/window-1-20260929-010000-10x.mp4\""), "{wmd}");+        assert!(wmd.contains("-action.mp4\" title="), "{wmd}");+        assert!(!wmd.contains("<video"), "{wmd}");         assert!(md.contains("<video src=\"docs/clips/window-1-20260929-010000-10x.mp4\""), "{md}");         assert!(md.contains("preload=\"metadata\""), "{md}");         assert!(md.contains("](clips/window-1-20260929-010000-action.mp4)"));
docs/release-0.2.4.mdadded+4
@@ -0,0 +1,4 @@+# adom-aiflow 0.2.4++- Every clip on the live clips page (`docs/clips.md`) plays in the wiki's `::adom-video` widget (10x cut and action cut, with its poster) when the run has a page; a plain `<video>` only without one.+- Each step's sub-readme on the project page (`steps/<nn>-<step>/README.md`) shows that step's clips in the same widget.
tools/project-steps.py+12
@@ -88,6 +88,18 @@ def sub_readme(run, data, events, step, n, stage):         for f, cap in pics[:16]:             name = os.path.basename(f); shutil.copy(f, os.path.join(d, name))             md += [f"**{cap}**", "", f"![{cap}]({name})", ""]+    # the step's clips (pushed by the clips page as docs/clips/<stem>-10x.mp4), in the wiki's video widget+    page = data.get("projectPage") or data.get("clipsPage") or data.get("page")+    vids = []+    for c in data.get("captures") or []:+        if c.get("step") != step or c.get("discarded") or not c.get("file"): continue+        stem = os.path.splitext(os.path.basename(c["file"]))[0]+        if os.path.isfile(os.path.join(run, f"{stem}-10x.mp4")): vids.append((stem, c.get("visit")))+    if vids and page:+        md += ["## Clips", ""]+        for stem, visit in vids:+            poster = f' poster="/{page}/render/docs/clips/{stem}-poster.jpg"' if os.path.isfile(os.path.join(run, f"{stem}-poster.jpg")) else ""+            md += [f'::adom-video{{video="/{page}/render/docs/clips/{stem}-10x.mp4"{poster} title="{title}, visit {visit} (10x)" muted="true"}}', ""]     t = tokens_by_step(data).get(step)     if t:         md += ["## AI usage (Google Antigravity)", "", f"{t['calls']} model calls: {t['freshIn']:,} fresh input, {t['cachedIn']:,} cached input and {t['out']:,} output tokens.", ""]