1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! Inspection commands; ordinary denoising retains its positional CLI.
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "dnoise")]
struct Commands {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Execute a JSON batch manifest, continuing past per-file errors.
Batch {
manifest: PathBuf,
/// Replace existing outputs only after successful processing.
#[arg(long)]
force: bool,
/// Estimate without writing acquisitions.
#[arg(long)]
dry_run: bool,
/// Skip full input/output decoding checks for every job; keep structural checks.
#[arg(long, alias = "no-validation", conflicts_with = "validate")]
skip_validation: bool,
/// Enable full validation for every job, overriding manifest settings.
#[arg(long, conflicts_with = "skip_validation")]
validate: bool,
/// Save results JSON (must not exist); results are also printed on stdout.
#[arg(long)]
report: Option<PathBuf>,
},
/// Validate SQLite metadata and every decoded type-2 frame without writing.
Validate { input: PathBuf },
/// Print dnoise processing history; absence does not prove the input is raw.
Metadata { input: PathBuf },
}
pub fn dispatch() -> Option<anyhow::Result<()>> {
let name = std::env::args_os().nth(1)?;
if name != "validate" && name != "metadata" && name != "batch" {
return None;
}
Some((|| {
match Commands::parse().command {
Command::Batch {
manifest,
force,
dry_run,
skip_validation,
validate,
report,
} => {
let mut batch = dnoise::batch::Manifest::load(&manifest)?;
for job in &mut batch.jobs {
if skip_validation {
job.config.skip_validation = Some(true);
} else if validate {
job.config.skip_validation = Some(false);
}
}
if !dry_run {
dnoise::batch::preflight(&batch.jobs, force)?;
}
if let Some(path) = &report {
dnoise::output::check_disjoint(&manifest, path)?;
for job in &batch.jobs {
dnoise::output::check_disjoint(&job.input, path)?;
dnoise::output::check_disjoint(&job.output, path)?;
}
if path.exists() {
anyhow::bail!("report already exists: {}", path.display());
}
}
let mut results = Vec::new();
let mut failures = 0;
for job in &batch.jobs {
let result = (|| -> anyhow::Result<serde_json::Value> {
let resolved = job.config.resolve(&job.input)?;
let stages = resolved.stages();
let options = dnoise::RunOptions {
force,
dry_run,
crop: (!resolved.crop.is_empty()).then_some(&resolved.crop),
crop_only: resolved.crop_only,
frame_batch_size: job.config.frame_batch_size,
skip_validation: job.config.skip_validation.unwrap_or(false),
..Default::default()
};
let stats = job.config.in_thread_pool(|| {
dnoise::denoise_with_options(
&job.input,
&job.output,
&resolved.filter,
&stages,
&options,
|_| {},
)
})?;
Ok(dnoise::provenance::report(
&job.input,
&resolved.filter,
&stages,
&options,
&stats,
))
})();
match result {
Ok(run)=>results.push(serde_json::json!({"input":job.input,"output":job.output,"success":true,"run":run})),
Err(e)=>{failures+=1;results.push(serde_json::json!({"input":job.input,"output":job.output,"success":false,"error":e.to_string()}));}
}
}
let result =
serde_json::json!({"schema_version":1,"failures":failures,"jobs":results});
let json = serde_json::to_string_pretty(&result)?;
if let Some(path) = report {
use std::io::Write;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)?;
file.write_all(json.as_bytes())?;
}
println!("{json}");
if failures > 0 {
anyhow::bail!("{failures} batch job(s) failed; see results");
}
}
Command::Validate { input } => {
let result = dnoise::validation::inspect(&input, true)?;
println!(
"Valid type-2 {} acquisition: {} frames, {} points, {} binary bytes",
dnoise::detect_acquisition(&input)?,
result.frames,
result.points,
result.binary_bytes
);
}
Command::Metadata { input } => match dnoise::provenance::read(&input)? {
Some(history) => println!("{}", serde_json::to_string_pretty(&history)?),
None => println!(
"No dnoise history found (older versions or removed sidecars cannot be detected)."
),
},
}
Ok(())
})())
}