app
Adom Video Post-Production
Public Made by Adomby adom
AI-driven studio for finishing demo videos: review every captured clip, flag and re-record the weak ones, speed up dead air, narrate, mux and auto-level, validate the final cut, and publish straight to the wiki, driven end to end by your AI.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
//! `adom-video-post manifest {init,add,inspect,remove}` handlers.
//!
//! The demo script calls `add` once per completed step, which is
//! the whole contract between the demo-side bash and adom-video-post's
//! post-processing side. Everything else (`storyboard`, `process`,
//! `voiceover`, `publish`, `concat`) reads the resulting file.
use super::manifest::{parse_speedup_arg, Clip, Manifest, SpeedupEvent};
use std::path::{Path, PathBuf};
/// `manifest init <file> [--title "..."]`
pub fn init(file: &Path, title: &str) -> Result<(), String> {
if file.exists() {
return Err(format!(
"manifest already exists at {}. Hint: delete it first, or use `manifest add` to append.",
file.display()
));
}
let m = Manifest::init(title);
m.save(file)?;
Ok(())
}
/// `manifest add <file> --id step-03 --title "Open BQ25792" ...`
/// All clip fields are optional except `id` and `title`.
#[allow(clippy::too_many_arguments)]
pub fn add(
file: &Path,
id: &str,
title: &str,
description: Option<String>,
raw_path: Option<PathBuf>,
fast_path: Option<PathBuf>,
image_path: Option<PathBuf>,
console_png_path: Option<PathBuf>,
speedups: Vec<String>,
actions: Vec<String>,
warnings: Vec<String>,
narration: Option<String>,
) -> Result<(), String> {
let mut manifest = if file.exists() {
Manifest::load(file)?
} else {
// Auto-init so the user doesn't have to call `init` first.
// This matches how demo scripts naturally work: first `add`
// implicitly creates a fresh manifest.
Manifest::init("Untitled")
};
// Upsert semantics: an existing clip with the same id is replaced
// in-place, preserving its position in the ordered list. Demo
// scripts often call `manifest add` twice per step (once early
// with bare info, once at the end with description/image/speedup
// populated); the second call updates the first. To force-fail
// on duplicates, the caller should use `manifest remove` first.
let existing_index = manifest.clips.iter().position(|c| c.id == id);
let speedup_events: Result<Vec<SpeedupEvent>, String> =
speedups.iter().map(|s| parse_speedup_arg(s)).collect();
// Validate any paths the caller supplied exist — but only warn,
// don't fail. Partial manifests are fine; the UI surfaces
// missing files as warnings on the affected card.
let mut auto_warnings: Vec<String> = Vec::new();
for (name, p) in [
("raw", &raw_path),
("fast", &fast_path),
("image", &image_path),
("console-png", &console_png_path),
] {
if let Some(path) = p {
if !path.exists() {
auto_warnings.push(format!("{} path not found on disk: {}", name, path.display()));
}
}
}
let clip = Clip {
id: id.to_string(),
title: title.to_string(),
description,
raw_path,
fast_path,
poster_path: None,
image_path,
console_png_path,
speedup_events: speedup_events?,
actions,
warnings: warnings.into_iter().chain(auto_warnings).collect(),
narration,
};
if let Some(idx) = existing_index {
manifest.clips[idx] = clip;
} else {
manifest.clips.push(clip);
}
manifest.save(file)?;
Ok(())
}
/// `manifest inspect <file>` — print a human-readable summary.
pub fn inspect(file: &Path) -> Result<(), String> {
let manifest = Manifest::load(file)?;
println!("title: {}", manifest.title);
println!("clips: {}", manifest.clips.len());
for (i, clip) in manifest.clips.iter().enumerate() {
let n = i + 1;
println!(" [{}] {} — {}", n, clip.id, clip.title);
if let Some(p) = &clip.raw_path {
let present = if p.exists() { "✓" } else { "✗ missing" };
println!(" raw: {} ({})", p.display(), present);
}
if let Some(p) = &clip.fast_path {
let present = if p.exists() { "✓" } else { "✗ missing" };
println!(" fast: {} ({})", p.display(), present);
}
if !clip.speedup_events.is_empty() {
let labels: Vec<String> = clip
.speedup_events
.iter()
.map(|e| format!("{}x:{}", e.speed, e.label))
.collect();
println!(" speedup: {}", labels.join(", "));
}
if !clip.warnings.is_empty() {
for w in &clip.warnings {
println!(" ⚠ {}", w);
}
}
}
if let Some(p) = &manifest.final_raw_path {
println!("final raw: {}", p.display());
}
if let Some(p) = &manifest.final_fast_path {
println!("final fast: {}", p.display());
}
if let Some(pub_target) = &manifest.publish {
println!("publish → {}", pub_target.page);
}
Ok(())
}
/// `manifest remove <file> --id step-03`
pub fn remove(file: &Path, id: &str) -> Result<(), String> {
let mut manifest = Manifest::load(file)?;
let before = manifest.clips.len();
manifest.clips.retain(|c| c.id != id);
if manifest.clips.len() == before {
return Err(format!(
"no clip with id {:?} in manifest {}",
id,
file.display()
));
}
manifest.save(file)?;
Ok(())
}