micforge 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. micforge/__init__.py +3 -0
  2. micforge/__main__.py +4 -0
  3. micforge/analysis/__init__.py +0 -0
  4. micforge/analysis/stats.py +133 -0
  5. micforge/audio/__init__.py +0 -0
  6. micforge/audio/backend.py +32 -0
  7. micforge/audio/cues.py +110 -0
  8. micforge/audio/device_watch.py +237 -0
  9. micforge/audio/devices.py +160 -0
  10. micforge/audio/engine.py +746 -0
  11. micforge/audio/fileio.py +45 -0
  12. micforge/audio/latency.py +169 -0
  13. micforge/audio/loopback.py +113 -0
  14. micforge/audio/metrics.py +134 -0
  15. micforge/audio/noise_profile.py +250 -0
  16. micforge/audio/recorder.py +100 -0
  17. micforge/audio/ring.py +89 -0
  18. micforge/cli.py +293 -0
  19. micforge/commands/__init__.py +0 -0
  20. micforge/commands/calibrate.py +47 -0
  21. micforge/commands/common.py +69 -0
  22. micforge/commands/devices_cmd.py +86 -0
  23. micforge/commands/diagnostics_cmd.py +68 -0
  24. micforge/commands/doctor.py +127 -0
  25. micforge/commands/files.py +48 -0
  26. micforge/commands/help_cmd.py +449 -0
  27. micforge/commands/latency_test.py +65 -0
  28. micforge/commands/live.py +321 -0
  29. micforge/commands/preset_cmd.py +142 -0
  30. micforge/commands/test_mic.py +98 -0
  31. micforge/commands/wizard.py +70 -0
  32. micforge/config.py +279 -0
  33. micforge/diagnostics.py +308 -0
  34. micforge/dsp/__init__.py +0 -0
  35. micforge/dsp/aec.py +388 -0
  36. micforge/dsp/base.py +30 -0
  37. micforge/dsp/biquad.py +144 -0
  38. micforge/dsp/chain.py +153 -0
  39. micforge/dsp/cofilite.py +427 -0
  40. micforge/dsp/compressor.py +109 -0
  41. micforge/dsp/deesser.py +64 -0
  42. micforge/dsp/denoise.py +181 -0
  43. micforge/dsp/deverb.py +227 -0
  44. micforge/dsp/export_cofilite.py +111 -0
  45. micforge/dsp/gate.py +76 -0
  46. micforge/dsp/leveler.py +74 -0
  47. micforge/dsp/proximity.py +164 -0
  48. micforge/dsp/util.py +80 -0
  49. micforge/errors.py +25 -0
  50. micforge/logging_setup.py +25 -0
  51. micforge/models/cofilite.onnx +0 -0
  52. micforge/presets.py +284 -0
  53. micforge/profile_store.py +41 -0
  54. micforge/ui/__init__.py +0 -0
  55. micforge/ui/dashboard.py +573 -0
  56. micforge/ui/hotkeys.py +169 -0
  57. micforge/ui/keys.py +64 -0
  58. micforge/ui/report.py +88 -0
  59. micforge/ui/settings_gui.py +899 -0
  60. micforge/ui/theme.py +42 -0
  61. micforge/ui/tray.py +276 -0
  62. micforge/ui/tuning.py +138 -0
  63. micforge/ui/widgets.py +109 -0
  64. micforge/util/autostart.py +56 -0
  65. micforge-1.3.0.dist-info/METADATA +334 -0
  66. micforge-1.3.0.dist-info/RECORD +70 -0
  67. micforge-1.3.0.dist-info/WHEEL +5 -0
  68. micforge-1.3.0.dist-info/entry_points.txt +2 -0
  69. micforge-1.3.0.dist-info/licenses/LICENSE +22 -0
  70. micforge-1.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,127 @@
1
+ """`doctor`: environment, device and performance checks with fix-it hints."""
2
+ from __future__ import annotations
3
+
4
+ import platform
5
+ import sys
6
+ import time
7
+
8
+ import numpy as np
9
+ from rich import box
10
+ from rich.table import Table
11
+
12
+ from ..audio.backend import get_sd
13
+ from ..audio.devices import detect_virtual_output, find_device, list_devices
14
+ from ..config import AppConfig
15
+ from ..dsp.chain import process_offline
16
+ from ..errors import MicforgeError
17
+ from ..ui.theme import console
18
+ from .common import make_chain
19
+
20
+ OK, WARN, BAD = "[green]✔[/]", "[yellow]![/]", "[red]✖[/]"
21
+
22
+ CABLE_HELP = {
23
+ "win32": "Install VB-CABLE (free): https://vb-audio.com/Cable/ - reboot, then re-run `micforge setup`.",
24
+ "darwin": "Install BlackHole 2ch: `brew install blackhole-2ch` - then re-run `micforge setup`.",
25
+ "linux": "Create a virtual sink: `pactl load-module module-null-sink sink_name=micforge` "
26
+ "- Discord then uses 'Monitor of micforge' as its mic.",
27
+ }
28
+
29
+
30
+ def run_doctor(app: AppConfig, export: bool = False, export_path: str | None = None) -> int:
31
+ t = Table(box=box.SIMPLE_HEAD, header_style="bold cyan", show_lines=False)
32
+ t.add_column("", width=2)
33
+ t.add_column("check")
34
+ t.add_column("result")
35
+ problems = 0
36
+
37
+ def row(mark: str, name: str, result: str) -> None:
38
+ t.add_row(mark, name, result)
39
+
40
+ row(OK if sys.version_info >= (3, 10) else BAD, "python", f"{platform.python_version()} on {platform.system()}")
41
+ try:
42
+ import scipy
43
+ row(OK, "numpy / scipy", f"{np.__version__} / {scipy.__version__}")
44
+ except ImportError:
45
+ row(BAD, "scipy", "missing - pip install scipy")
46
+ problems += 1
47
+
48
+ try:
49
+ sd = get_sd()
50
+ row(OK, "PortAudio", str(sd.get_portaudio_version()[1]))
51
+ except MicforgeError as exc:
52
+ row(BAD, "PortAudio", f"{exc} {exc.hint or ''}")
53
+ console.print(t)
54
+ return 1
55
+
56
+ pool = list_devices(all_apis=True)
57
+ try:
58
+ mic = find_device(app.input_device, "input", pool)
59
+ row(OK, "microphone", f"{mic.name} [{mic.hostapi}]")
60
+ try:
61
+ sd.check_input_settings(device=mic.index, channels=1, samplerate=app.sample_rate)
62
+ row(OK, f"mic @ {app.sample_rate} Hz", "supported")
63
+ except Exception as exc: # noqa: BLE001
64
+ row(WARN, f"mic @ {app.sample_rate} Hz", f"not accepted ({exc}); WASAPI auto-convert may still work, or try 44100")
65
+ except MicforgeError as exc:
66
+ row(BAD, "microphone", str(exc))
67
+ problems += 1
68
+
69
+ cable = detect_virtual_output(pool) if app.output_device is None else None
70
+ if app.output_device is not None:
71
+ try:
72
+ cable = find_device(app.output_device, "output", pool)
73
+ except MicforgeError as exc:
74
+ row(BAD, "virtual cable", str(exc))
75
+ problems += 1
76
+ if cable:
77
+ row(OK, "virtual cable", cable.name)
78
+ elif app.output_device is None:
79
+ row(BAD, "virtual cable", "none found - " + CABLE_HELP.get(sys.platform if sys.platform in CABLE_HELP else "linux", ""))
80
+ problems += 1
81
+
82
+ try:
83
+ hp = find_device(app.monitor_device, "output", pool)
84
+ row(OK, "headphones / monitor", hp.name)
85
+ except MicforgeError as exc:
86
+ row(WARN, "headphones / monitor", str(exc))
87
+
88
+ from ..dsp.cofilite import find_model_file
89
+ model_file = find_model_file(app.denoise_model)
90
+ if model_file:
91
+ row(OK, "denoise model", f"CoFi-Lite ({model_file.name}, {model_file.stat().st_size / 1024:.0f} KB) [<100MB RAM]")
92
+ else:
93
+ row(OK, "denoise model", "Spectral Wiener (classical fallback)")
94
+
95
+ chain, _ = make_chain(app, app.preset)
96
+ x = (np.random.default_rng(0).standard_normal(app.sample_rate * 2) * 0.05).astype(np.float32)
97
+ process_offline(chain, x[: app.sample_rate // 2], app.block_size) # warm-up (numpy/FFT plan caches)
98
+ chain, _ = make_chain(app, app.preset)
99
+ t0 = time.perf_counter()
100
+ process_offline(chain, x, app.block_size)
101
+ rt = (x.shape[0] / app.sample_rate) / (time.perf_counter() - t0)
102
+ row(OK if rt > 5 else WARN if rt > 2 else BAD, "dsp speed", f"{rt:.0f}x realtime ({100 / rt:.1f}% of one core)")
103
+ if rt <= 2:
104
+ problems += 1
105
+ row(OK, "latency (dsp only)", f"{chain.latency_samples / app.sample_rate * 1000:.1f} ms")
106
+
107
+ console.print(t)
108
+ console.print(
109
+ "[bold]Discord checklist:[/] Input Device = cable's recording end · Noise Suppression: None · "
110
+ "Automatic Gain Control: off · Echo Cancellation: off · Input Sensitivity: manual, low."
111
+ )
112
+ console.print(
113
+ "[bold]Windows tip:[/] Sound settings → your mic → Properties → Advanced: turn off 'Audio Enhancements' "
114
+ "and exclusive mode, or other apps can fight for the mic."
115
+ )
116
+ console.print(
117
+ "[dim]📖 Troubleshooting guide:[/] [bold cyan]docs/TROUBLESHOOTING.md[/]"
118
+ )
119
+
120
+ if export or export_path:
121
+ from pathlib import Path
122
+ from ..diagnostics import collect_diagnostics, export_diagnostics_zip
123
+ report = collect_diagnostics(app)
124
+ out_zip = export_diagnostics_zip(report, Path(export_path) if export_path else None)
125
+ console.print(f"\n[bold green]✔ Diagnostics exported to:[/] [white]{out_zip}[/]")
126
+
127
+ return 1 if problems else 0
@@ -0,0 +1,48 @@
1
+ """`process` and `analyze`: offline work on WAV files."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from rich.progress import Progress, SpinnerColumn, TextColumn
7
+
8
+ from ..analysis.stats import analyze, band_levels_db, diagnose
9
+ from ..audio.fileio import read_wav, write_wav
10
+ from ..config import AppConfig
11
+ from ..dsp.chain import process_offline
12
+ from ..errors import MicforgeError
13
+ from ..ui.report import full_report
14
+ from ..ui.theme import console
15
+ from .common import make_chain
16
+
17
+
18
+ def run_process(app: AppConfig, src: str, dst: str, preset: str | None) -> None:
19
+ preset = preset or app.preset
20
+ if not Path(src).is_file():
21
+ raise MicforgeError(f"File not found: {src}")
22
+ x, sr = read_wav(src, target_rate=app.sample_rate)
23
+ chain, _ = make_chain(app, preset)
24
+ dn = chain.stage("denoise")
25
+ if dn is not None and getattr(dn, "profile", None) is None:
26
+ try: # no saved profile: use the quietest 1 s of the file as the noise reference
27
+ import numpy as np
28
+ fr = int(0.1 * sr)
29
+ n = x.shape[0] // fr
30
+ lv = np.mean(x[: n * fr].reshape(n, fr) ** 2, axis=1)
31
+ i = int(np.argmin(np.convolve(lv, np.ones(10) / 10, mode="valid")))
32
+ dn.learn(x[i * fr : i * fr + 10 * fr]) # type: ignore[attr-defined]
33
+ except (ValueError, IndexError):
34
+ pass
35
+ with Progress(SpinnerColumn(), TextColumn("processing…"), console=console, transient=True):
36
+ y = process_offline(chain, x, app.block_size)
37
+ write_wav(dst, y, sr)
38
+ console.print(f"[green]✔ Wrote {dst}[/] (preset: {preset})")
39
+ b, a = analyze(x, sr), analyze(y, sr)
40
+ console.print(full_report(b, a, band_levels_db(x, sr), band_levels_db(y, sr), diagnose(a)))
41
+
42
+
43
+ def run_analyze(app: AppConfig, src: str) -> None:
44
+ if not Path(src).is_file():
45
+ raise MicforgeError(f"File not found: {src}")
46
+ x, sr = read_wav(src)
47
+ s = analyze(x, sr)
48
+ console.print(full_report(s, None, findings=diagnose(s)))
@@ -0,0 +1,449 @@
1
+ """Interactive, dynamic command-line help and usage reference for Micforge."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from typing import Any
6
+
7
+ from rich import box
8
+ from rich.markup import escape
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+ from rich.text import Text
12
+
13
+ from .. import __version__
14
+ from ..ui.theme import console
15
+
16
+
17
+ # Enhanced metadata enriching argparse introspection with practical examples and tips
18
+ COMMAND_METADATA: dict[str, dict[str, Any]] = {
19
+ "run": {
20
+ "category": "🎙️ Live Real-Time Audio",
21
+ "headline": "Start the real-time DSP rack, virtual cable routing, and live HUD.",
22
+ "examples": [
23
+ ("Start streaming with default settings", "micforge run"),
24
+ ("Use gaming preset with 128-sample low latency", "micforge run -p gaming --block 128"),
25
+ ("Listen to your processed voice in headphones", "micforge run --monitor"),
26
+ ("Run silently in system tray without terminal", "micforge run --tray"),
27
+ ],
28
+ "tips": [
29
+ "Press [Tab] anytime during live streaming to toggle the ultra-clean Gamer HUD.",
30
+ "Press [b] to toggle bypass and [x] to mute your microphone.",
31
+ "In Discord or OBS, remember to select 'CABLE Output' as your recording mic.",
32
+ ],
33
+ },
34
+ "loopback": {
35
+ "category": "🎙️ Live Real-Time Audio",
36
+ "headline": "Hear yourself live with instant A/B raw vs. processed comparison.",
37
+ "examples": [
38
+ ("Start loopback monitor on default headphones", "micforge loopback"),
39
+ ("Test warm broadcast preset", "micforge loopback -p broadcast"),
40
+ ("Specify headphones device explicitly", "micforge loopback --out \"Headphones\""),
41
+ ],
42
+ "tips": [
43
+ "Use headphones to prevent feedback howling.",
44
+ "Press [b] to toggle instant A/B comparison between raw mic and processed audio.",
45
+ ],
46
+ },
47
+ "test": {
48
+ "category": "🎙️ Live Real-Time Audio",
49
+ "headline": "Record a voice sample, show before/after spectral metrics, and play both.",
50
+ "examples": [
51
+ ("Record 8 seconds test sample", "micforge test"),
52
+ ("Quick 4-second test with podcast preset", "micforge test -p podcast -s 4"),
53
+ ],
54
+ "tips": [
55
+ "Speak naturally with background noise (fans/typing) to judge noise reduction quality.",
56
+ ],
57
+ },
58
+ "setup": {
59
+ "category": "🛠️ Hardware, Calibration & Latency",
60
+ "headline": "Interactive setup wizard to select microphone, virtual cable, and headphones.",
61
+ "examples": [
62
+ ("Launch interactive device setup", "micforge setup"),
63
+ ],
64
+ "tips": [
65
+ "On Windows, always pick the WASAPI endpoints for the lowest latency and clock stability.",
66
+ ],
67
+ },
68
+ "devices": {
69
+ "category": "🛠️ Hardware, Calibration & Latency",
70
+ "headline": "List all detected audio hardware, default endpoints, and host APIs.",
71
+ "examples": [
72
+ ("List standard devices", "micforge devices"),
73
+ ("List all audio host APIs (WASAPI, MME, DirectSound)", "micforge devices --all"),
74
+ ],
75
+ "tips": [
76
+ "Use device index numbers (e.g. --mic 1) or matching substrings (e.g. --mic Realtek).",
77
+ ],
78
+ },
79
+ "calibrate": {
80
+ "category": "🛠️ Hardware, Calibration & Latency",
81
+ "headline": "Learn your room silence for 3–5s to configure noise floor and gate threshold.",
82
+ "examples": [
83
+ ("Perform standard room calibration", "micforge calibrate"),
84
+ ("Calibrate for 6 seconds", "micforge calibrate -s 6"),
85
+ ("Reset calibration back to factory baseline", "micforge calibrate --reset"),
86
+ ],
87
+ "tips": [
88
+ "Stay silent and keep PC fans running normally while calibrating.",
89
+ ],
90
+ },
91
+ "profile": {
92
+ "category": "🛠️ Hardware, Calibration & Latency",
93
+ "headline": "Manage voice-aware noise profiles and acoustic environments.",
94
+ "examples": [
95
+ ("List all available noise profiles", "micforge profile list"),
96
+ ("Inspect fan noise profile details", "micforge profile show fan_on"),
97
+ ("Switch active environment to mechanical keyboard", "micforge profile switch mechanical_keyboard"),
98
+ ("Trigger immediate quiet-room relearn", "micforge profile relearn"),
99
+ ],
100
+ "tips": [
101
+ "Continuous learning adapts slowly (≤1 dB/s) and never learns when you speak.",
102
+ "Switch to 'fan_on' during heavy gaming sessions when PC fans ramp up.",
103
+ ],
104
+ },
105
+ "latency": {
106
+ "category": "🛠️ Hardware, Calibration & Latency",
107
+ "headline": "Measure physical end-to-end loopback latency with logarithmic chirp.",
108
+ "examples": [
109
+ ("Run standard loopback latency benchmark", "micforge latency"),
110
+ ("Benchmark specific mic and headphones", "micforge latency --mic 1 --out 2"),
111
+ ],
112
+ "tips": [
113
+ "Uses a 60ms acoustic chirp cross-correlation with sub-millisecond precision.",
114
+ "Provides tailored tuning tips for buffer sizes and Windows WASAPI settings.",
115
+ ],
116
+ },
117
+ "settings": {
118
+ "category": "🎛️ Tuning, Presets & Studio GUI",
119
+ "headline": "Open native multi-tab Control Center (Simple/Advanced DSP, Hotkeys, Devices).",
120
+ "examples": [
121
+ ("Open Settings GUI", "micforge settings"),
122
+ ("Alias command", "micforge hotkeys"),
123
+ ],
124
+ "tips": [
125
+ "Features Simple Mode (4 macro knobs: Clarity, Warmth, Noise Removal, Room Cleanup).",
126
+ "Includes one-click '📦 Export Diagnostics (.zip)' in the Routing & System tab.",
127
+ ],
128
+ },
129
+ "hotkeys": {
130
+ "category": "🎛️ Tuning, Presets & Studio GUI",
131
+ "headline": "Alias for 'settings' — configure global hotkeys, audio cues, and startup.",
132
+ "examples": [
133
+ ("Open Settings and Hotkeys GUI", "micforge hotkeys"),
134
+ ],
135
+ "tips": [
136
+ "Hotkeys work system-wide even when full-screen games or Discord have focus.",
137
+ ],
138
+ },
139
+ "preset": {
140
+ "category": "🎛️ Tuning, Presets & Studio GUI",
141
+ "headline": "List, inspect, clone, export, and manage DSP chain presets.",
142
+ "examples": [
143
+ ("List all available presets", "micforge preset list"),
144
+ ("Inspect 'discord' preset settings", "micforge preset show discord"),
145
+ ("Clone 'gaming' into a new custom preset", "micforge preset save my_stream --from gaming"),
146
+ ("Export custom preset to JSON file", "micforge preset export my_stream ~/preset.json"),
147
+ ("Import preset from JSON file", "micforge preset import ~/preset.json"),
148
+ ],
149
+ "tips": [
150
+ "Built-in presets (discord, gaming, podcast, noisy_room) are immutable and safe.",
151
+ ],
152
+ },
153
+ "presets": {
154
+ "category": "🎛️ Tuning, Presets & Studio GUI",
155
+ "headline": "Quick alias to list presets or print preset details.",
156
+ "examples": [
157
+ ("List all presets", "micforge presets"),
158
+ ("Print preset configuration", "micforge presets podcast"),
159
+ ],
160
+ "tips": [],
161
+ },
162
+ "process": {
163
+ "category": "📁 Offline File Audio Processing",
164
+ "headline": "Process an offline WAV audio file through the 11-stage DSP rack.",
165
+ "examples": [
166
+ ("Process raw recording", "micforge process raw.wav clean.wav"),
167
+ ("Process with broadcast preset", "micforge process voice.wav out.wav -p broadcast"),
168
+ ],
169
+ "tips": [
170
+ "Supports 16-bit, 24-bit, and 32-bit float WAV audio at any sample rate.",
171
+ ],
172
+ },
173
+ "analyze": {
174
+ "category": "📁 Offline File Audio Processing",
175
+ "headline": "Analyze a WAV audio file (loudness, SNR, clarity, spectral profile).",
176
+ "examples": [
177
+ ("Inspect audio quality of recording", "micforge analyze recording.wav"),
178
+ ],
179
+ "tips": [
180
+ "Reports RMS energy, peak ceiling, dynamic crest factor, and acoustic suggestions.",
181
+ ],
182
+ },
183
+ "doctor": {
184
+ "category": "🩺 Diagnostics, Models & System",
185
+ "headline": "Health check for audio drivers, buffer speed, and redacted .zip diagnostics.",
186
+ "examples": [
187
+ ("Run doctor health assessment", "micforge doctor"),
188
+ ("Export complete redacted diagnostic .zip bundle", "micforge doctor --export"),
189
+ ("Export to specific file destination", "micforge doctor --export ~/diag.zip"),
190
+ ],
191
+ "tips": [
192
+ "Automatically sanitizes passwords, tokens, API keys, URLs, and home paths.",
193
+ "Links directly to docs/TROUBLESHOOTING.md for instant fix instructions.",
194
+ ],
195
+ },
196
+ "model": {
197
+ "category": "🩺 Diagnostics, Models & System",
198
+ "headline": "Inspect active CoFi-Lite neural speech model or export PyTorch weights to ONNX.",
199
+ "examples": [
200
+ ("Show active neural model status & RAM footprint", "micforge model info"),
201
+ ("Export PyTorch checkpoint to ONNX", "micforge model export checkpoint.tar model.onnx"),
202
+ ],
203
+ "tips": [
204
+ "Micforge bundles an ultra-compact ~83k parameter CoFi-Lite model requiring <100MB RAM.",
205
+ ],
206
+ },
207
+ "version": {
208
+ "category": "🩺 Diagnostics, Models & System",
209
+ "headline": "Display Micforge version, model status, and runtime environment.",
210
+ "examples": [
211
+ ("Show version info", "micforge version"),
212
+ ],
213
+ "tips": [],
214
+ },
215
+ "help": {
216
+ "category": "🩺 Diagnostics, Models & System",
217
+ "headline": "Display dynamic, categorized command reference and deep-dive usage guides.",
218
+ "examples": [
219
+ ("Show complete categorized command table", "micforge help"),
220
+ ("Deep dive into a specific command", "micforge help run"),
221
+ ("Inspect latency test options", "micforge help latency"),
222
+ ],
223
+ "tips": [
224
+ "Type 'micforge help <command>' anytime to see full arguments, flags, and pro tips.",
225
+ ],
226
+ },
227
+ }
228
+
229
+ CATEGORIES_ORDER = [
230
+ "🎙️ Live Real-Time Audio",
231
+ "🛠️ Hardware, Calibration & Latency",
232
+ "🎛️ Tuning, Presets & Studio GUI",
233
+ "📁 Offline File Audio Processing",
234
+ "🩺 Diagnostics, Models & System",
235
+ ]
236
+
237
+
238
+ def _extract_parser_info(parser: argparse.ArgumentParser) -> dict[str, Any]:
239
+ """Dynamically introspect an argparse ArgumentParser."""
240
+ subparsers_action = None
241
+ for action in parser._actions:
242
+ if isinstance(action, argparse._SubParsersAction):
243
+ subparsers_action = action
244
+ break
245
+
246
+ commands_info: dict[str, dict[str, Any]] = {}
247
+ if not subparsers_action:
248
+ return commands_info
249
+
250
+ for cmd_name, subparser in subparsers_action.choices.items():
251
+ # Get positional arguments and options
252
+ positionals: list[dict[str, Any]] = []
253
+ options: list[dict[str, Any]] = []
254
+
255
+ for action in subparser._actions:
256
+ if isinstance(action, (argparse._HelpAction, argparse._SubParsersAction)):
257
+ continue
258
+
259
+ info = {
260
+ "dest": action.dest,
261
+ "help": action.help or "",
262
+ "default": action.default if action.default != "==SUPPRESS==" else None,
263
+ "choices": list(action.choices) if action.choices else None,
264
+ "type": action.type.__name__ if hasattr(action.type, "__name__") else None,
265
+ }
266
+
267
+ if action.option_strings:
268
+ info["flags"] = "/".join(action.option_strings)
269
+ options.append(info)
270
+ else:
271
+ info["name"] = action.dest
272
+ positionals.append(info)
273
+
274
+ commands_info[cmd_name] = {
275
+ "help": subparser.description or subparser.format_help().split("\n")[0] or "",
276
+ "positionals": positionals,
277
+ "options": options,
278
+ "parser": subparser,
279
+ }
280
+
281
+ return commands_info
282
+
283
+
284
+ def run_help(command_name: str | None = None) -> int:
285
+ """Entry point for `micforge help [command]`."""
286
+ from ..cli import build_parser
287
+
288
+ parser = build_parser()
289
+ discovered_commands = _extract_parser_info(parser)
290
+
291
+ # 1. SPECIFIC COMMAND DEEP DIVE
292
+ if command_name:
293
+ cmd_key = command_name.lower().strip()
294
+ if cmd_key not in discovered_commands:
295
+ console.print(f"\n[bold red]✖ Unknown command:[/] '[bold white]{command_name}[/]'")
296
+ # Suggest matching commands
297
+ matches = [c for c in discovered_commands if cmd_key in c or c in cmd_key]
298
+ if matches:
299
+ console.print(f"[yellow] Did you mean:[/] [cyan]{', '.join(matches)}[/]")
300
+ console.print("[dim] Run [bold white]micforge help[/] to see all available commands.\n[/]")
301
+ return 1
302
+
303
+ _print_command_deep_dive(cmd_key, discovered_commands[cmd_key])
304
+ return 0
305
+
306
+ # 2. COMPLETE CATEGORIZED COMMAND DIRECTORY
307
+ _print_all_commands_overview(discovered_commands)
308
+ return 0
309
+
310
+
311
+ def _print_all_commands_overview(discovered_commands: dict[str, dict[str, Any]]) -> None:
312
+ """Print the complete categorized reference table."""
313
+ banner = Text()
314
+ banner.append("🎙️ MICFORGE COMMAND REFERENCE ", style="bold cyan")
315
+ banner.append(f"v{__version__}\n", style="bold green")
316
+ banner.append("Real-time local DSP rack, AI speech enhancement, diagnostics, and routing.", style="dim")
317
+ console.print(Panel(banner, box=box.ROUNDED, border_style="cyan"))
318
+
319
+ # Group commands into categories
320
+ categorized: dict[str, list[str]] = {cat: [] for cat in CATEGORIES_ORDER}
321
+ uncategorized: list[str] = []
322
+
323
+ for cmd_name in sorted(discovered_commands.keys()):
324
+ meta = COMMAND_METADATA.get(cmd_name)
325
+ cat = meta.get("category") if meta else None
326
+ if cat and cat in categorized:
327
+ categorized[cat].append(cmd_name)
328
+ else:
329
+ uncategorized.append(cmd_name)
330
+
331
+ if uncategorized:
332
+ categorized["Other Commands"] = uncategorized
333
+
334
+ for category, cmds in categorized.items():
335
+ if not cmds:
336
+ continue
337
+
338
+ table = Table(
339
+ title=f"[bold cyan]{category}[/]",
340
+ title_justify="left",
341
+ box=box.SIMPLE_HEAD,
342
+ header_style="bold white",
343
+ show_lines=False,
344
+ padding=(0, 1),
345
+ )
346
+ table.add_column("Command", style="bold yellow", width=14)
347
+ table.add_column("Description", style="white", width=42)
348
+ table.add_column("Typical Example", style="dim cyan")
349
+
350
+ for cmd_name in cmds:
351
+ meta = COMMAND_METADATA.get(cmd_name, {})
352
+ headline = meta.get("headline")
353
+ if not headline:
354
+ headline = discovered_commands[cmd_name]["help"] or "Execute command"
355
+
356
+ examples = meta.get("examples", [])
357
+ example_cmd = examples[0][1] if examples else f"micforge {cmd_name}"
358
+
359
+ table.add_row(cmd_name, headline, example_cmd)
360
+
361
+ console.print(table)
362
+ console.print()
363
+
364
+ # Helpful Footer
365
+ console.print(
366
+ "[bold cyan]Tip:[/] Type [bold white]micforge help <command>[/] "
367
+ "(e.g. [green]micforge help run[/], [green]micforge help latency[/], [green]micforge help profile[/]) "
368
+ "for detailed flags, syntax, and options.\n"
369
+ )
370
+
371
+
372
+ def _print_command_deep_dive(cmd_name: str, info: dict[str, Any]) -> None:
373
+ """Print an exhaustive, beautiful deep dive for a single command."""
374
+ meta = COMMAND_METADATA.get(cmd_name, {})
375
+ headline = meta.get("headline") or info.get("help") or "Command reference"
376
+ category = meta.get("category", "General")
377
+
378
+ # 1. Header Banner
379
+ header = Text()
380
+ header.append(f"COMMAND: micforge {cmd_name}\n", style="bold cyan")
381
+ header.append(f"{headline}\n", style="bold white")
382
+ header.append(f"Category: {category}", style="dim")
383
+ console.print(Panel(header, box=box.ROUNDED, border_style="cyan"))
384
+
385
+ # 2. Syntax / Usage Line
386
+ positionals = info.get("positionals", [])
387
+ options = info.get("options", [])
388
+
389
+ syntax_parts = [f"micforge {cmd_name}"]
390
+ if options:
391
+ syntax_parts.append("[options]")
392
+ for pos in positionals:
393
+ syntax_parts.append(f"<{pos['dest']}>")
394
+
395
+ syntax_str = " ".join(syntax_parts)
396
+ console.print(f"[bold cyan]Usage:[/] [bold green]{escape(syntax_str)}[/]\n")
397
+
398
+ # 3. Positional Arguments (if any)
399
+ if positionals:
400
+ pos_table = Table(title="[bold]Positional Arguments[/]", box=box.SIMPLE_HEAD, header_style="bold cyan")
401
+ pos_table.add_column("Argument", style="bold yellow", width=18)
402
+ pos_table.add_column("Choices / Type", style="dim", width=20)
403
+ pos_table.add_column("Description")
404
+
405
+ for pos in positionals:
406
+ choices_str = ", ".join(map(str, pos["choices"])) if pos.get("choices") else (pos.get("type") or "string")
407
+ pos_table.add_row(
408
+ pos["dest"],
409
+ f"[dim]{escape(choices_str)}[/]",
410
+ escape(pos.get("help", "")),
411
+ )
412
+
413
+ console.print(pos_table)
414
+ console.print()
415
+
416
+ # 4. Optional Flags (if any)
417
+ if options:
418
+ opt_table = Table(title="[bold]Options & Flags[/]", box=box.SIMPLE_HEAD, header_style="bold cyan")
419
+ opt_table.add_column("Flag", style="bold yellow", width=22)
420
+ opt_table.add_column("Default", style="dim", width=16)
421
+ opt_table.add_column("Description")
422
+
423
+ for opt in options:
424
+ default_val = str(opt["default"]) if opt.get("default") is not None and opt["default"] is not False else "-"
425
+ opt_table.add_row(
426
+ opt["flags"],
427
+ f"[dim]{escape(default_val)}[/]",
428
+ escape(opt.get("help", "")),
429
+ )
430
+
431
+ console.print(opt_table)
432
+ console.print()
433
+
434
+ # 5. Practical Copy-Paste Examples
435
+ examples = meta.get("examples", [])
436
+ if examples:
437
+ console.print("[bold cyan]Practical Examples:[/]")
438
+ for desc, ex_code in examples:
439
+ console.print(f" • [dim]{desc}:[/]")
440
+ console.print(f" [bold white]{escape(ex_code)}[/]")
441
+ console.print()
442
+
443
+ # 6. Pro Tips & Best Practices
444
+ tips = meta.get("tips", [])
445
+ if tips:
446
+ console.print("[bold cyan]Pro Tips & Best Practices:[/]")
447
+ for tip in tips:
448
+ console.print(f" 💡 {escape(tip)}")
449
+ console.print()