app
AI Flow
Public Made by Adomby adom
Adom's AI Flow: a tool to help the AI follow all of the steps it takes to build a board.
← All Pull Requests
0.2.4: clips in the ::adom-video widget #39
Merges
clips-video-widget-0.2.4 → mainBase 6e27c4b6470275bb8c2404dd835dbe33847b7272 (Publish 0.2.3). John (2026-10-18): every video clip on the wiki in the ::adom-video widget. clips page (docs/clips.md) uses ::adom-video for the 10x and action cuts when the run has a page; step sub-readmes show their clips in the widget. Tests: workspace pass (clips_page test extended); preflight no problems.
Diff Skip to comments
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, ¬es, &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, ¬es, &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 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") { b.push(format!("Motion-only action cut: [docs/clips/{stem}-action.mp4](clips/{stem}-action.mp4)")); 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): ")); 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, ¬es, "2026-09-29T03:00:00Z");+ let (md, media) = clips_page(&data, &dir, ¬es, "2026-09-29T03:00:00Z", None);+ let (wmd, _) = clips_page(&data, &dir, ¬es, "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"", ""]+ # 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.", ""]
Comments
Log in to comment.
No comments yet.