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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
//! Storyboard subcommand — first phase of post-production review.
//!
//! Reads a manifest file describing a multi-clip video review,
//! opens a Hydrogen webview tab hosting the UI, and blocks until
//! /shutdown. With `--watch` (default), the manifest file is
//! poll-watched for mtime changes so the UI live-updates as the
//! user's demo script appends new clips during a run.
//!
//! The manifest schema and manifest-CRUD subcommands live in the
//! sibling modules; this file is the `run()` glue + the `impl Routes`
//! wiring.
pub mod manifest;
pub mod manifest_cli;
pub mod routes;
use crate::webapp::{run_webapp, WebappConfig};
use manifest::Manifest;
/// Pre-load placeholder icon — see the comment on
/// `voiceover::TAB_ICON_PLACEHOLDER`. Once the storyboard UI
/// finishes loading, Hydrogen overrides this with the favicon
/// served from `src/storyboard/icon.svg`.
const TAB_ICON_PLACEHOLDER: &str = "mdi:filmstrip-box-multiple";
use std::path::PathBuf;
use std::sync::atomic::AtomicU64;
use std::sync::{Arc, RwLock};
use std::time::{Duration, SystemTime};
pub struct StoryboardConfig {
pub manifest_path: PathBuf,
pub port: u16,
pub tab_name: String,
pub panel_id_override: Option<String>,
/// Poll the manifest file for mtime changes and live-update the
/// UI when it changes. Default true; disable with `--no-watch`.
pub watch: bool,
/// Skip the Hydrogen webview tab open; HTTP server only. Use
/// when reviewing in pup or driving via browser_eval.
pub no_open: bool,
}
pub fn run(cfg: StoryboardConfig) -> Result<(), String> {
if !cfg.manifest_path.exists() {
return Err(format!(
"manifest not found: {}. Hint: create one with `adom-video-post manifest init`.",
cfg.manifest_path.display()
));
}
let initial = Manifest::load(&cfg.manifest_path)?;
let title = if initial.title.is_empty() {
"Storyboard".to_string()
} else {
initial.title.clone()
};
// Shared state: the manifest itself under RwLock so the file
// watcher thread can swap it out on reload, plus a version
// counter the UI polls via /state to know when to re-fetch.
let manifest = Arc::new(RwLock::new(initial));
let version = Arc::new(AtomicU64::new(1));
let manifest_mtime = Arc::new(std::sync::Mutex::new(
std::fs::metadata(&cfg.manifest_path)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH),
));
// Spawn a file-watcher thread if --watch is on. This is a
// 500ms-poll-mtime loop rather than inotify because (a) we
// only need to react to `manifest add` which writes atomically
// via rename, (b) inotify on a renamed target is fiddly, and
// (c) 500ms is well within the "feels live" window.
if cfg.watch {
let manifest_for_thread = Arc::clone(&manifest);
let version_for_thread = Arc::clone(&version);
let mtime_for_thread = Arc::clone(&manifest_mtime);
let path_for_thread = cfg.manifest_path.clone();
std::thread::spawn(move || loop {
std::thread::sleep(Duration::from_millis(500));
let new_mtime = match std::fs::metadata(&path_for_thread)
.and_then(|m| m.modified())
{
Ok(t) => t,
Err(_) => continue,
};
let mut last = mtime_for_thread.lock().unwrap();
if new_mtime == *last {
continue;
}
// File changed — reload and bump the version.
match Manifest::load(&path_for_thread) {
Ok(fresh) => {
*manifest_for_thread.write().unwrap() = fresh;
version_for_thread
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
*last = new_mtime;
}
Err(e) => {
eprintln!("storyboard: manifest reload failed: {}", e);
}
}
});
}
let routes = Arc::new(routes::StoryboardRoutes {
manifest,
version,
manifest_path: cfg.manifest_path.clone(),
});
let webapp_cfg = WebappConfig {
port: cfg.port,
tab_name: cfg.tab_name,
panel_id_override: cfg.panel_id_override,
display_icon: TAB_ICON_PLACEHOLDER.to_string(),
subcommand: "storyboard".to_string(),
title,
no_open: cfg.no_open,
};
run_webapp(webapp_cfg, routes)
}