mirror of
https://github.com/storytold/filmcraft.git
synced 2026-11-02 18:09:21 +00:00
Refactor filmcraft-cli for strict Clippy checks
This commit is contained in:
+229
-210
@@ -316,219 +316,238 @@ async fn cli() {
|
||||
match cmd {
|
||||
"help" | "-h" => out!("{HELP}"),
|
||||
"version" => outln!("filmcraft-cli {}", env!("CARGO_PKG_VERSION")),
|
||||
"probe" => {
|
||||
let path = a.pos(1).unwrap_or_else(|| usage("probe <media> [--image-sequence]"));
|
||||
match probe::probe(path, a.flag("--image-sequence")) {
|
||||
Ok(v) => print(&a, &v),
|
||||
Err(e) => fail(e),
|
||||
}
|
||||
}
|
||||
"bench-decode" => {
|
||||
let path = a.pos(1).unwrap_or_else(|| usage("bench-decode <media>"));
|
||||
let n: i64 = a.opt("--frames").and_then(|v| v.parse().ok()).unwrap_or(120);
|
||||
let src = filmcraft_codecs::open_bytes(path, read_file(path).await.unwrap_or_else(|e| fail(e)).into()).unwrap_or_else(|e| fail(e));
|
||||
let rate = src.info().frame_rate();
|
||||
let t0 = std::time::Instant::now();
|
||||
for f in 0..n {
|
||||
src.video_frame(filmcraft_media::FrameRequest::full(rate.tick_of(f))).unwrap_or_else(|e| fail(e));
|
||||
}
|
||||
let dt = t0.elapsed().as_secs_f64();
|
||||
outln!("{n} frames in {dt:.2}s → {:.1} fps (sequential, via Mp4Source)", n as f64 / dt);
|
||||
let t1 = std::time::Instant::now();
|
||||
let f = src.video_frame(filmcraft_media::FrameRequest::full(rate.tick_of(n / 2))).unwrap_or_else(|e| fail(e));
|
||||
let (w, h, _) = f.to_linear_f32_decimated(2);
|
||||
outln!("½-res linear conversion {w}x{h}: {:.1} ms", t1.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
"commands" | "describe" => {
|
||||
let mut b = Backend::open(&a);
|
||||
let list = b.exec("command.list", json!({})).await.unwrap_or_else(|e| fail(e));
|
||||
let list = list.as_array().cloned().unwrap_or_default();
|
||||
if cmd == "describe" {
|
||||
let id = a.pos(1).unwrap_or_else(|| usage("describe <id>"));
|
||||
match list.iter().find(|c| c["id"] == id) {
|
||||
Some(c) => print(&a, c),
|
||||
None => fail(format!("unknown command `{id}` (try `filmcraft-cli commands {id}`)")),
|
||||
}
|
||||
return;
|
||||
}
|
||||
let f = a.pos(1).map(str::to_ascii_lowercase).unwrap_or_default();
|
||||
let hit = |c: &Value| {
|
||||
let id = c["id"].as_str().unwrap_or("").to_ascii_lowercase();
|
||||
let label = c["label"].as_str().unwrap_or("").to_ascii_lowercase();
|
||||
f.is_empty() || id.contains(&f) || label.contains(&f)
|
||||
};
|
||||
let list: Vec<Value> = list.into_iter().filter(hit).collect();
|
||||
if a.flag("--json") {
|
||||
print(&a, &Value::Array(list));
|
||||
} else {
|
||||
for c in &list {
|
||||
let s = |k: &str| c[k].as_str().unwrap_or("").to_string();
|
||||
outln!("{:<32} {:<34} {:<14} {}", s("id"), s("label"), s("shortcut"), s("params"));
|
||||
}
|
||||
}
|
||||
}
|
||||
"exec" => {
|
||||
let id = a.pos(1).unwrap_or_else(|| usage("exec <id> [key=value ...]"));
|
||||
let params = a.params_from(2).unwrap_or_else(|e| usage(e));
|
||||
let mut b = Backend::open(&a);
|
||||
match b.exec(id, params).await {
|
||||
Ok(v) => print(&a, &v),
|
||||
Err(e) => fail(format!("{id}: {e}")),
|
||||
}
|
||||
b.finish(&a).await;
|
||||
}
|
||||
"run" => {
|
||||
let script = a.pos(1).unwrap_or_else(|| usage("run <script.jsonl | ->"));
|
||||
let text = if script == "-" {
|
||||
std::io::read_to_string(std::io::stdin()).unwrap_or_else(|e| fail(e))
|
||||
} else {
|
||||
read_text(script).await.unwrap_or_else(|e| fail(format!("{script}: {e}")))
|
||||
};
|
||||
let mut b = Backend::open(&a);
|
||||
let mut failed = 0;
|
||||
for (n, line) in text.lines().enumerate().filter(|(_, l)| !l.trim().is_empty() && !l.trim_start().starts_with('#')) {
|
||||
let v: Value = serde_json::from_str(line).unwrap_or_else(|e| usage(format!("line {}: {e}", n + 1)));
|
||||
let id = v["id"].as_str().unwrap_or_else(|| usage(format!("line {}: missing \"id\"", n + 1)));
|
||||
match b.exec(id, v.get("params").cloned().unwrap_or(json!({}))).await {
|
||||
Ok(r) => outln!("{}", json!({"line": n + 1, "id": id, "ok": true, "result": r})),
|
||||
Err(e) => {
|
||||
outln!("{}", json!({"line": n + 1, "id": id, "ok": false, "error": e}));
|
||||
failed += 1;
|
||||
if !a.flag("--keep-going") {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
b.finish(&a).await;
|
||||
if failed > 0 {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
"inspect" => {
|
||||
let mut b = Backend::open(&a);
|
||||
let what = a.pos(1).unwrap_or("all");
|
||||
let mut get = async |id: &str| b.exec(id, json!({})).await.unwrap_or_else(|e| fail(e));
|
||||
let v = match what {
|
||||
"project" => get("project.inspect").await,
|
||||
"sequence" => get("sequence.inspect").await,
|
||||
"all" => json!({"project": get("project.inspect").await, "sequence": get("sequence.inspect").await}),
|
||||
other => usage(format!("inspect: unknown `{other}` (project | sequence)")),
|
||||
};
|
||||
print(&a, &v);
|
||||
}
|
||||
"import" => {
|
||||
let paths: Vec<String> = a.positionals.get(1..).unwrap_or_default().iter().map(|p| absolute(p)).collect();
|
||||
if paths.is_empty() {
|
||||
usage("import <file>...");
|
||||
}
|
||||
let mut b = Backend::open(&a);
|
||||
match b.exec("file.import", json!({"paths": paths})).await {
|
||||
Ok(v) => print(&a, &v),
|
||||
Err(e) => fail(format!("import: {e}")),
|
||||
}
|
||||
b.finish(&a).await;
|
||||
}
|
||||
"export" => {
|
||||
if a.flag("--list-presets") {
|
||||
let mut b = Backend::open(&a);
|
||||
let q = a.pos(1).map(|q| json!({"query": q})).unwrap_or(json!({}));
|
||||
match b.exec("export.presets.list", q).await {
|
||||
Ok(v) => print(&a, &v["presets"]),
|
||||
Err(e) => fail(format!("export: {e}")),
|
||||
}
|
||||
return;
|
||||
}
|
||||
let out = a.pos(1).unwrap_or_else(|| usage("export <out> [--preset name | --format f]"));
|
||||
let mut p = json!({"path": out, "wait": true});
|
||||
match (a.opt("--preset"), a.opt("--format")) {
|
||||
(Some(preset), f) => {
|
||||
p["preset"] = json!(preset);
|
||||
if let Some(f) = f {
|
||||
p["format"] = json!(f);
|
||||
}
|
||||
}
|
||||
(None, f) => {
|
||||
let format = f.or_else(|| format_for(out)).unwrap_or_else(|| usage("export: give --preset or --format (unknown extension)"));
|
||||
p["format"] = json!(format);
|
||||
}
|
||||
}
|
||||
for (k, key) in [("--scale", "scale"), ("--quality", "quality")] {
|
||||
if let Some(v) = a.opt(k) {
|
||||
p[key] = parse_value(v);
|
||||
}
|
||||
}
|
||||
if let Some(r) = a.opt("--range") {
|
||||
p["range"] = json!(r);
|
||||
}
|
||||
set_custom_bounds(&mut p, a.opt("--start"), a.opt("--end"));
|
||||
if let Some(js) = a.opt("--settings") {
|
||||
p["settings"] = serde_json::from_str(js).unwrap_or_else(|e| usage(format!("--settings: {e}")));
|
||||
}
|
||||
if a.flag("--no-audio") {
|
||||
p["audio"] = json!(false);
|
||||
}
|
||||
if let Some(v) = a.opt("--gpu-rendering") {
|
||||
p["gpuRendering"] = json!(v); // off | auto (validated by the export command)
|
||||
}
|
||||
let mut b = Backend::open(&a);
|
||||
let t0 = std::time::Instant::now();
|
||||
let r = if a.flag("--queue") {
|
||||
let mut q = p.clone();
|
||||
q["start"] = json!(true);
|
||||
b.exec("export.queue.add", q).await.and_then(|v| {
|
||||
let failed: Vec<&Value> = v["items"].as_array().map(|i| i.iter().filter(|x| x["status"] != "done").collect()).unwrap_or_default();
|
||||
if failed.is_empty() { Ok(v) } else { Err(format!("queue: {}", Value::Array(failed.into_iter().cloned().collect()))) }
|
||||
})
|
||||
} else {
|
||||
b.exec("file.exportMedia", p).await
|
||||
};
|
||||
match r {
|
||||
Ok(v) => {
|
||||
print(&a, &v);
|
||||
let written = written_paths(&v, out).join(", ");
|
||||
check_offline(&a, &offline_media(&v), &format!("export: {written}"));
|
||||
diag(format_args!("exported {written} in {:.1}s", t0.elapsed().as_secs_f64()));
|
||||
}
|
||||
Err(e) => fail(format!("export: {e}")),
|
||||
}
|
||||
}
|
||||
"render" => {
|
||||
if a.opt("--bridge").is_some() {
|
||||
usage("render is headless; with --bridge use the MCP `render_frame` tool or `exec` ui commands");
|
||||
}
|
||||
let Backend::Local(mut s) = Backend::open(&a) else { usage("render is headless") };
|
||||
let secs: f64 = finite_opt(&a, "--seconds").unwrap_or(0.0);
|
||||
let scale: f32 = finite_opt(&a, "--scale").unwrap_or(1.0);
|
||||
let out = a.opt("--out").unwrap_or("frame.png");
|
||||
s.set_playhead(filmcraft_time::Tick::from_seconds_f64(secs));
|
||||
let t0 = std::time::Instant::now();
|
||||
let (img, offline) = render_at_playhead(&s, scale).unwrap_or_else(|e| fail(e));
|
||||
let dt = t0.elapsed();
|
||||
let png = filmcraft_automation::png_rgba(img.w as u32, img.h as u32, img.over_black_rgba8(), 0).unwrap_or_else(|e| fail(e));
|
||||
write_file(out, png).await.unwrap_or_else(|e| fail(format!("{out}: {e}")));
|
||||
let offline: Vec<Value> = offline.iter().filter_map(|o| serde_json::to_value(o).ok()).collect();
|
||||
check_offline(&a, &offline, &format!("render: {out}"));
|
||||
diag(format_args!("rendered {}x{} in {:.1} ms → {out}", img.w, img.h, dt.as_secs_f64() * 1000.0));
|
||||
}
|
||||
"mcp" => {
|
||||
if let Some(opt) = a.unknown_opt(&["--bridge", "--project", "--demo", "--data-dir"]) {
|
||||
usage(format!("unknown mcp option `{opt}`"));
|
||||
}
|
||||
let server = match a.opt("--bridge") {
|
||||
Some(addr) => filmcraft_automation::FilmcraftMcp::bridge(addr).unwrap_or_else(|e| usage(e)),
|
||||
None => match Backend::open(&a) {
|
||||
Backend::Local(s) => filmcraft_automation::FilmcraftMcp::headless(*s),
|
||||
Backend::Bridge(_) => usage("mcp: use --bridge ADDR to drive a running app"),
|
||||
},
|
||||
};
|
||||
if let Err(e) = server.serve_stdio().await {
|
||||
fail(format!("mcp: {e}"));
|
||||
}
|
||||
}
|
||||
"probe" => cli_probe(&a).await,
|
||||
"bench-decode" => cli_bench_decode(&a).await,
|
||||
"commands" | "describe" => cli_commands(&a, cmd).await,
|
||||
"exec" => cli_exec(&a).await,
|
||||
"run" => cli_run(&a).await,
|
||||
"inspect" => cli_inspect(&a).await,
|
||||
"import" => cli_import(&a).await,
|
||||
"export" => cli_export(&a).await,
|
||||
"render" => cli_render(&a).await,
|
||||
"mcp" => cli_mcp(&a).await,
|
||||
other => usage(format!("unknown subcommand `{other}`")),
|
||||
}
|
||||
}
|
||||
async fn cli_probe(a: &Args) {
|
||||
let path = a.pos(1).unwrap_or_else(|| usage("probe <media> [--image-sequence]"));
|
||||
match probe::probe(path, a.flag("--image-sequence")) {
|
||||
Ok(v) => print(a, &v),
|
||||
Err(e) => fail(e),
|
||||
}
|
||||
}
|
||||
|
||||
async fn cli_bench_decode(a: &Args) {
|
||||
let path = a.pos(1).unwrap_or_else(|| usage("bench-decode <media>"));
|
||||
let n: i64 = a.opt("--frames").and_then(|v| v.parse().ok()).unwrap_or(120);
|
||||
let src = filmcraft_codecs::open_bytes(path, read_file(path).await.unwrap_or_else(|e| fail(e)).into()).unwrap_or_else(|e| fail(e));
|
||||
let rate = src.info().frame_rate();
|
||||
let t0 = std::time::Instant::now();
|
||||
for f in 0..n {
|
||||
src.video_frame(filmcraft_media::FrameRequest::full(rate.tick_of(f))).unwrap_or_else(|e| fail(e));
|
||||
}
|
||||
let dt = t0.elapsed().as_secs_f64();
|
||||
outln!("{n} frames in {dt:.2}s → {:.1} fps (sequential, via Mp4Source)", n as f64 / dt);
|
||||
let t1 = std::time::Instant::now();
|
||||
let f = src.video_frame(filmcraft_media::FrameRequest::full(rate.tick_of(n / 2))).unwrap_or_else(|e| fail(e));
|
||||
let (w, h, _) = f.to_linear_f32_decimated(2);
|
||||
outln!("½-res linear conversion {w}x{h}: {:.1} ms", t1.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
|
||||
async fn cli_commands(a: &Args, cmd: &str) {
|
||||
let mut b = Backend::open(a);
|
||||
let list = b.exec("command.list", json!({})).await.unwrap_or_else(|e| fail(e));
|
||||
let list = list.as_array().cloned().unwrap_or_default();
|
||||
if cmd == "describe" {
|
||||
let id = a.pos(1).unwrap_or_else(|| usage("describe <id>"));
|
||||
match list.iter().find(|c| c["id"] == id) {
|
||||
Some(c) => print(a, c),
|
||||
None => fail(format!("unknown command `{id}` (try `filmcraft-cli commands {id}`)")),
|
||||
}
|
||||
return;
|
||||
}
|
||||
let f = a.pos(1).map(str::to_ascii_lowercase).unwrap_or_default();
|
||||
let hit = |c: &Value| {
|
||||
let id = c["id"].as_str().unwrap_or("").to_ascii_lowercase();
|
||||
let label = c["label"].as_str().unwrap_or("").to_ascii_lowercase();
|
||||
f.is_empty() || id.contains(&f) || label.contains(&f)
|
||||
};
|
||||
let list: Vec<Value> = list.into_iter().filter(hit).collect();
|
||||
if a.flag("--json") {
|
||||
print(a, &Value::Array(list));
|
||||
} else {
|
||||
for c in &list {
|
||||
let s = |k: &str| c[k].as_str().unwrap_or("").to_string();
|
||||
outln!("{:<32} {:<34} {:<14} {}", s("id"), s("label"), s("shortcut"), s("params"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cli_exec(a: &Args) {
|
||||
let id = a.pos(1).unwrap_or_else(|| usage("exec <id> [key=value ...]"));
|
||||
let params = a.params_from(2).unwrap_or_else(|e| usage(e));
|
||||
let mut b = Backend::open(a);
|
||||
match b.exec(id, params).await {
|
||||
Ok(v) => print(a, &v),
|
||||
Err(e) => fail(format!("{id}: {e}")),
|
||||
}
|
||||
b.finish(a).await;
|
||||
}
|
||||
|
||||
async fn cli_run(a: &Args) {
|
||||
let script = a.pos(1).unwrap_or_else(|| usage("run <script.jsonl | ->"));
|
||||
let text = if script == "-" {
|
||||
std::io::read_to_string(std::io::stdin()).unwrap_or_else(|e| fail(e))
|
||||
} else {
|
||||
read_text(script).await.unwrap_or_else(|e| fail(format!("{script}: {e}")))
|
||||
};
|
||||
let mut b = Backend::open(a);
|
||||
let mut failed = 0;
|
||||
for (n, line) in text.lines().enumerate().filter(|(_, l)| !l.trim().is_empty() && !l.trim_start().starts_with('#')) {
|
||||
let v: Value = serde_json::from_str(line).unwrap_or_else(|e| usage(format!("line {}: {e}", n + 1)));
|
||||
let id = v["id"].as_str().unwrap_or_else(|| usage(format!("line {}: missing \"id\"", n + 1)));
|
||||
match b.exec(id, v.get("params").cloned().unwrap_or(json!({}))).await {
|
||||
Ok(r) => outln!("{}", json!({"line": n + 1, "id": id, "ok": true, "result": r})),
|
||||
Err(e) => {
|
||||
outln!("{}", json!({"line": n + 1, "id": id, "ok": false, "error": e}));
|
||||
failed += 1;
|
||||
if !a.flag("--keep-going") {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
b.finish(a).await;
|
||||
if failed > 0 {
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async fn cli_inspect(a: &Args) {
|
||||
let mut b = Backend::open(a);
|
||||
let what = a.pos(1).unwrap_or("all");
|
||||
let mut get = async |id: &str| b.exec(id, json!({})).await.unwrap_or_else(|e| fail(e));
|
||||
let v = match what {
|
||||
"project" => get("project.inspect").await,
|
||||
"sequence" => get("sequence.inspect").await,
|
||||
"all" => json!({"project": get("project.inspect").await, "sequence": get("sequence.inspect").await}),
|
||||
other => usage(format!("inspect: unknown `{other}` (project | sequence)")),
|
||||
};
|
||||
print(a, &v);
|
||||
}
|
||||
|
||||
async fn cli_import(a: &Args) {
|
||||
let paths: Vec<String> = a.positionals.get(1..).unwrap_or_default().iter().map(|p| absolute(p)).collect();
|
||||
if paths.is_empty() {
|
||||
usage("import <file>...");
|
||||
}
|
||||
let mut b = Backend::open(a);
|
||||
match b.exec("file.import", json!({"paths": paths})).await {
|
||||
Ok(v) => print(a, &v),
|
||||
Err(e) => fail(format!("import: {e}")),
|
||||
}
|
||||
b.finish(a).await;
|
||||
}
|
||||
|
||||
async fn cli_export(a: &Args) {
|
||||
if a.flag("--list-presets") {
|
||||
let mut b = Backend::open(a);
|
||||
let q = a.pos(1).map(|q| json!({"query": q})).unwrap_or(json!({}));
|
||||
match b.exec("export.presets.list", q).await {
|
||||
Ok(v) => print(a, &v["presets"]),
|
||||
Err(e) => fail(format!("export: {e}")),
|
||||
}
|
||||
return;
|
||||
}
|
||||
let out = a.pos(1).unwrap_or_else(|| usage("export <out> [--preset name | --format f]"));
|
||||
let mut p = json!({"path": out, "wait": true});
|
||||
match (a.opt("--preset"), a.opt("--format")) {
|
||||
(Some(preset), f) => {
|
||||
p["preset"] = json!(preset);
|
||||
if let Some(f) = f {
|
||||
p["format"] = json!(f);
|
||||
}
|
||||
}
|
||||
(None, f) => {
|
||||
let format = f.or_else(|| format_for(out)).unwrap_or_else(|| usage("export: give --preset or --format (unknown extension)"));
|
||||
p["format"] = json!(format);
|
||||
}
|
||||
}
|
||||
for (k, key) in [("--scale", "scale"), ("--quality", "quality")] {
|
||||
if let Some(v) = a.opt(k) {
|
||||
p[key] = parse_value(v);
|
||||
}
|
||||
}
|
||||
if let Some(r) = a.opt("--range") {
|
||||
p["range"] = json!(r);
|
||||
}
|
||||
set_custom_bounds(&mut p, a.opt("--start"), a.opt("--end"));
|
||||
if let Some(js) = a.opt("--settings") {
|
||||
p["settings"] = serde_json::from_str(js).unwrap_or_else(|e| usage(format!("--settings: {e}")));
|
||||
}
|
||||
if a.flag("--no-audio") {
|
||||
p["audio"] = json!(false);
|
||||
}
|
||||
if let Some(v) = a.opt("--gpu-rendering") {
|
||||
p["gpuRendering"] = json!(v); // off | auto (validated by the export command)
|
||||
}
|
||||
let mut b = Backend::open(a);
|
||||
let t0 = std::time::Instant::now();
|
||||
let r = if a.flag("--queue") {
|
||||
let mut q = p.clone();
|
||||
q["start"] = json!(true);
|
||||
b.exec("export.queue.add", q).await.and_then(|v| {
|
||||
let failed: Vec<&Value> = v["items"].as_array().map(|i| i.iter().filter(|x| x["status"] != "done").collect()).unwrap_or_default();
|
||||
if failed.is_empty() { Ok(v) } else { Err(format!("queue: {}", Value::Array(failed.into_iter().cloned().collect()))) }
|
||||
})
|
||||
} else {
|
||||
b.exec("file.exportMedia", p).await
|
||||
};
|
||||
match r {
|
||||
Ok(v) => {
|
||||
print(a, &v);
|
||||
let written = written_paths(&v, out).join(", ");
|
||||
check_offline(a, &offline_media(&v), &format!("export: {written}"));
|
||||
diag(format_args!("exported {written} in {:.1}s", t0.elapsed().as_secs_f64()));
|
||||
}
|
||||
Err(e) => fail(format!("export: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn cli_render(a: &Args) {
|
||||
if a.opt("--bridge").is_some() {
|
||||
usage("render is headless; with --bridge use the MCP `render_frame` tool or `exec` ui commands");
|
||||
}
|
||||
let Backend::Local(mut s) = Backend::open(a) else { usage("render is headless") };
|
||||
let secs: f64 = finite_opt(a, "--seconds").unwrap_or(0.0);
|
||||
let scale: f32 = finite_opt(a, "--scale").unwrap_or(1.0);
|
||||
let out = a.opt("--out").unwrap_or("frame.png");
|
||||
s.set_playhead(filmcraft_time::Tick::from_seconds_f64(secs));
|
||||
let t0 = std::time::Instant::now();
|
||||
let (img, offline) = render_at_playhead(&s, scale).unwrap_or_else(|e| fail(e));
|
||||
let dt = t0.elapsed();
|
||||
let png = filmcraft_automation::png_rgba(img.w as u32, img.h as u32, img.over_black_rgba8(), 0).unwrap_or_else(|e| fail(e));
|
||||
write_file(out, png).await.unwrap_or_else(|e| fail(format!("{out}: {e}")));
|
||||
let offline: Vec<Value> = offline.iter().filter_map(|o| serde_json::to_value(o).ok()).collect();
|
||||
check_offline(a, &offline, &format!("render: {out}"));
|
||||
diag(format_args!("rendered {}x{} in {:.1} ms → {out}", img.w, img.h, dt.as_secs_f64() * 1000.0));
|
||||
}
|
||||
|
||||
async fn cli_mcp(a: &Args) {
|
||||
if let Some(opt) = a.unknown_opt(&["--bridge", "--project", "--demo", "--data-dir"]) {
|
||||
usage(format!("unknown mcp option `{opt}`"));
|
||||
}
|
||||
let server = match a.opt("--bridge") {
|
||||
Some(addr) => filmcraft_automation::FilmcraftMcp::bridge(addr).unwrap_or_else(|e| usage(e)),
|
||||
None => match Backend::open(a) {
|
||||
Backend::Local(s) => filmcraft_automation::FilmcraftMcp::headless(*s),
|
||||
Backend::Bridge(_) => usage("mcp: use --bridge ADDR to drive a running app"),
|
||||
},
|
||||
};
|
||||
if let Err(e) = server.serve_stdio().await {
|
||||
fail(format!("mcp: {e}"));
|
||||
}
|
||||
}
|
||||
|
||||
/// The files an export wrote, from its result: `path` of a direct export, or each queued item's
|
||||
/// `path` (the format may have changed the extension: `out.mp4` exported as APV is `out.mp4.mov`).
|
||||
|
||||
@@ -308,8 +308,12 @@ fn phone_vfr_clip_extract_gain_title_to_h264_mp4() {
|
||||
let r = m.run("file.exportMedia", json!({"path": out, "format": "h264", "range": "entire", "wait": true}));
|
||||
assert_eq!(r["result"]["frames"], 89, "{r}");
|
||||
|
||||
verify_phone_export(&ffmpeg, &ffprobe, &src, &out);
|
||||
}
|
||||
|
||||
fn verify_phone_export(ffmpeg: &Path, ffprobe: &Path, src: &str, out: &str) {
|
||||
// container and streams
|
||||
let j = probe(&ffprobe, &out);
|
||||
let j = probe(ffprobe, out);
|
||||
let (v, a) = (streams(&j, "video"), streams(&j, "audio"));
|
||||
assert_eq!((v.len(), a.len()), (1, 1), "{j}");
|
||||
assert_eq!((v[0]["codec_name"].as_str(), v[0]["width"].as_u64(), v[0]["height"].as_u64()), (Some("h264"), Some(320), Some(240)));
|
||||
@@ -322,8 +326,8 @@ fn phone_vfr_clip_extract_gain_title_to_h264_mp4() {
|
||||
|
||||
// picture: frame k of the export is frame k (before the cut) or k + 31 (after) of the source
|
||||
// as an editor plays it (CFR 30, a dropped frame holding the previous one)
|
||||
let src_frames = frames(&ffmpeg, &src, Some("fps=30"), "gray");
|
||||
let got = frames(&ffmpeg, &out, None, "gray");
|
||||
let src_frames = frames(ffmpeg, src, Some("fps=30"), "gray");
|
||||
let got = frames(ffmpeg, out, None, "gray");
|
||||
assert!(src_frames.len() >= 120, "{} source frames", src_frames.len());
|
||||
assert_eq!(got.len(), 89);
|
||||
for k in [20, 32, 40, 44, 45, 50, 70, 88] {
|
||||
@@ -359,11 +363,11 @@ fn phone_vfr_clip_extract_gain_title_to_h264_mp4() {
|
||||
}
|
||||
|
||||
// sound: beeps where the flashes are (A/V sync across the extract), 6 dB down
|
||||
let src_pcm = pcm(&ffmpeg, &src, 0);
|
||||
let got_pcm = pcm(&ffmpeg, &out, 0);
|
||||
let src_pcm = pcm(ffmpeg, src, 0);
|
||||
let got_pcm = pcm(ffmpeg, out, 0);
|
||||
let beeps = onsets(&got_pcm);
|
||||
assert_eq!(beeps.len(), 2, "beeps at {beeps:?}");
|
||||
for (beep, flash) in beeps.iter().zip([30.0 / 30.0, 59.0 / 30.0]) {
|
||||
for (beep, flash) in beeps.iter().zip([1.0, 59.0 / 30.0]) {
|
||||
assert!((beep - flash).abs() <= 1.0 / 30.0, "beep at {beep:.3} s, flash at {flash:.3} s");
|
||||
}
|
||||
let change = db(rms(&got_pcm, 1.01, 1.09) / rms(&src_pcm, 1.01, 1.09));
|
||||
|
||||
Reference in New Issue
Block a user