1use std::ffi::OsString;
8
9#[derive(Clone, Copy, PartialEq, Eq)]
11pub enum Group {
12 Pipeline,
13 Utility,
14}
15
16pub type NativeFn = fn(args: &[OsString]) -> anyhow::Result<i32>;
21
22pub struct CommandSpec {
23 pub name: &'static str,
24 pub py_module: &'static str,
27 pub description: &'static str,
28 pub group: Group,
29 pub hint: &'static str,
30 pub run: NativeFn,
31 pub command: fn() -> clap::Command,
33}
34
35macro_rules! cmd {
36 ($module:ident, $name:literal, $py:literal, $desc:literal, $group:ident) => {
37 cmd!($module, $name, $py, $desc, $group, "")
38 };
39 ($module:ident, $name:literal, $py:literal, $desc:literal, $group:ident, $hint:literal) => {
40 CommandSpec {
41 name: $name,
42 py_module: $py,
43 description: $desc,
44 group: Group::$group,
45 hint: $hint,
46 run: crate::cmd::$module::run,
47 command: crate::cmd::$module::command,
48 }
49 };
50}
51
52#[rustfmt::skip] pub const COMMANDS: &[CommandSpec] = &[
55 cmd!(init, "init", "xil_pipeline.xil_init", "workspace scaffolding", Pipeline),
56 cmd!(use_cmd, "use", "xil_pipeline.xil_use", "set / show the active show context", Utility, "(multi-show workspaces)"),
57 cmd!(scan, "scan", "xil_pipeline.XILP000_script_scanner", "pre-flight script scanner", Pipeline),
58 cmd!(parse, "parse", "xil_pipeline.XILP001_script_parser", "script parser", Pipeline),
59 cmd!(cues, "cues", "xil_pipeline.XILP006_cues_ingester", "cues sheet ingestion", Pipeline),
60 cmd!(produce, "produce", "xil_pipeline.XILP002_producer", "voice stem generation", Pipeline),
61 cmd!(assemble, "assemble", "xil_pipeline.XILP003_audio_assembly", "master audio assembly", Pipeline),
62 cmd!(studio_onboard, "studio-onboard", "xil_pipeline.XILP004_studio_onboard", "ElevenLabs Studio project onboarding", Pipeline),
63 cmd!(daw, "daw", "xil_pipeline.XILP005_daw_export", "DAW layer export", Pipeline),
64 cmd!(migrate, "migrate", "xil_pipeline.XILP007_stem_migrator", "stem migration", Pipeline),
65 cmd!(cleanup, "cleanup", "xil_pipeline.XILP008_stale_stem_cleanup", "stale stem cleanup", Pipeline),
66 cmd!(import, "import", "xil_pipeline.XILP010_studio_import", "Studio export import", Pipeline),
67 cmd!(regen, "regen", "xil_pipeline.XILP009_script_regenerator", "script regeneration", Pipeline),
68 cmd!(master, "master", "xil_pipeline.XILP011_master_export", "final master MP3 export", Pipeline),
69 cmd!(publish, "publish", "xil_pipeline.XILP012_publish", "social media post draft generator", Pipeline),
70 cmd!(voices, "voices", "xil_pipeline.XILU001_discover_voices_T2S", "voice discovery", Utility, "(before parse/produce)"),
71 cmd!(csv_join, "csv-join", "xil_pipeline.XILU003_csv_sfx_join", "CSV + SFX/cast annotation join", Utility, "(after parse)"),
72 cmd!(sfx, "sfx", "xil_pipeline.XILU002_generate_SFX", "standalone SFX generation", Utility, "(after cues/parse)"),
73 cmd!(sample, "sample", "xil_pipeline.XILU004_sample_voices_T2S", "voice sample generation", Utility, "(after voices/cast config)"),
74 cmd!(sfx_lib, "sfx-lib", "xil_pipeline.XILU005_discover_SFX", "SFX library discovery", Utility, "(any time)"),
75 cmd!(splice, "splice", "xil_pipeline.XILU006_splice_parsed", "parsed JSON splice utility", Utility, "(advanced)"),
76 cmd!(mp3_hash, "mp3-hash", "xil_pipeline.XILU007_mp3_hash", "recursive MP3 SHA-256 hash log", Utility, "(integrity / audit)"),
77 cmd!(stem_log, "stem-log", "xil_pipeline.XILU008_stem_log_report", "parse daily logs → chronological stem generation CSV", Utility, "(integrity / audit)"),
78 cmd!(gui, "gui", "xil_pipeline.xil_gui", "web dashboard (requires [gui] extra)", Utility, "(pip install xil-pipeline[gui])"),
79 cmd!(migrate_workspace, "migrate-workspace", "xil_pipeline.XILU009_migrate_workspace", "migrate pre-0.1.8 workspace to normalized layout", Utility, "(run once per workspace)"),
80 cmd!(db_profile, "db-profile", "xil_pipeline.XILU010_db_profile", "profile MP3 loudness: peak, average, and minimum dBFS", Utility, "(audio level analysis)"),
81 cmd!(sfx_csv, "sfx-csv", "xil_pipeline.XILU011_sfx_csv", "flatten sfx_<tag>.json configs to CSV — one row per effect", Utility, "(debug / audit)"),
82 cmd!(parsed_csv, "parsed-csv", "xil_pipeline.XILU012_parsed_csv", "export parsed_<tag>.json entries to CSV — one row per entry", Utility, "(debug / audit)"),
83 cmd!(sfx_hydrate, "sfx-hydrate", "xil_pipeline.XILU013_sfx_hydrate", "write pipe-hint source fields from parsed JSON into the SFX config", Utility, "(after parse, before produce)"),
84 cmd!(sfx_restore, "sfx-restore", "xil_pipeline.XILU020_sfx_restore", "reapply journaled timeline sound edits onto the SFX config", Utility, "(recover timeline sound edits)"),
85 cmd!(sfx_impact, "sfx-impact", "xil_pipeline.XILU021_sfx_impact", "report which source-backed cues duration_seconds is clipping short", Utility, "(before changing clip durations)"),
86 cmd!(sfx_match, "sfx-match", "xil_pipeline.XILU022_sfx_match", "find existing library assets for cues whose source file is missing", Utility, "(when produce reports missing SFX sources)"),
87 cmd!(episode_summary, "episode-summary", "xil_pipeline.XILU014_episode_summary", "write one-row-per-episode summary CSV (lines, words, TTS chars)", Utility, "(any time)"),
88 cmd!(stem_verify, "stem-verify", "xil_pipeline.XILU015_stem_verify", "scan stems folder → JSON report with file attributes and optional Whisper transcripts", Utility, "(after produce / import)"),
89 cmd!(stem_compare, "stem-compare", "xil_pipeline.XILU016_stem_compare", "cross-reference a stem-verify transcript report against the parsed script", Utility, "(after stem-verify)"),
90 cmd!(remove_show, "remove-show", "xil_pipeline.XILU017_remove_show", "remove all workspace files for a show (--dry-run safe)", Utility, "(workspace management)"),
91 cmd!(remove_episode, "remove-episode", "xil_pipeline.XILU018_remove_episode", "remove workspace files for one episode, preserving the source script (--dry-run safe)", Utility, "(workspace management)"),
92 cmd!(status, "status", "xil_pipeline.XILU019_episode_status", "make-style staleness check of an episode's pipeline artifacts (report only)", Utility, "(workspace management)"),
93 cmd!(setup, "setup", "xil_pipeline.XILU023_setup", "create and verify local ML worker venvs (chatterbox, whisper, mmaudio)", Utility, "(local GPU TTS setup)"),
94];
95
96pub fn find(name: &str) -> Option<&'static CommandSpec> {
97 COMMANDS.iter().find(|c| c.name == name)
98}
99
100pub fn module_tag(module: &str) -> &str {
102 let Some(start) = module.find("XIL") else {
103 return "";
104 };
105 let rest = &module[start..];
106 let mut end = 3;
108 let bytes = rest.as_bytes();
109 if bytes.len() < 4 || !matches!(bytes[3], b'P' | b'U') {
110 return "";
111 }
112 end += 1;
113 while end < bytes.len() && bytes[end].is_ascii_digit() {
114 end += 1;
115 }
116 if end == 4 {
117 return "";
118 }
119 &rest[..end]
120}
121
122pub fn help_text() -> String {
124 let width = COMMANDS.iter().map(|c| c.name.len()).max().unwrap_or(0);
125 let mut out = String::from("Usage: xil <command> [args...]\n\n");
126 for (label, group) in [
127 ("Pipeline Stages (recommended order):", Group::Pipeline),
128 ("Utilities:", Group::Utility),
129 ] {
130 out.push_str(label);
131 out.push('\n');
132 for c in COMMANDS.iter().filter(|c| c.group == group) {
133 let tag = module_tag(c.py_module);
134 let tag_str = if tag.is_empty() {
135 " ".repeat(8)
136 } else {
137 format!("{tag:<8}")
138 };
139 let suffix = if c.hint.is_empty() {
140 String::new()
141 } else {
142 format!(" {}", c.hint)
143 };
144 out.push_str(&format!(
145 " {:<width$} {tag_str} {}{suffix}\n",
146 c.name, c.description
147 ));
148 }
149 out.push('\n');
150 }
151 out.push_str("Run 'xil <command> --help' for command-specific options.\n");
152 out
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn table_has_all_39_python_commands() {
161 assert_eq!(COMMANDS.len(), 39);
163 }
164
165 #[test]
166 fn names_are_unique() {
167 let mut names: Vec<_> = COMMANDS.iter().map(|c| c.name).collect();
168 names.sort_unstable();
169 names.dedup();
170 assert_eq!(names.len(), COMMANDS.len());
171 }
172
173 #[test]
174 fn module_tag_extracts_xilp_and_xilu() {
175 assert_eq!(module_tag("xil_pipeline.XILP000_script_scanner"), "XILP000");
176 assert_eq!(module_tag("xil_pipeline.XILU022_sfx_match"), "XILU022");
177 assert_eq!(module_tag("xil_pipeline.xil_init"), "");
178 assert_eq!(module_tag("xil_pipeline.xil_gui"), "");
179 }
180
181 #[test]
182 fn help_matches_python_layout_for_first_lines() {
183 let text = help_text();
184 let lines: Vec<_> = text.lines().collect();
185 assert_eq!(lines[0], "Usage: xil <command> [args...]");
186 assert_eq!(lines[1], "");
187 assert_eq!(lines[2], "Pipeline Stages (recommended order):");
188 assert_eq!(
189 lines[3],
190 " init workspace scaffolding"
191 );
192 assert_eq!(
193 lines[4],
194 " scan XILP000 pre-flight script scanner"
195 );
196 assert_eq!(
197 lines.last().copied(),
198 Some("Run 'xil <command> --help' for command-specific options.")
199 );
200 }
201}