diff options
Diffstat (limited to 'lib/project/src')
| -rw-r--r-- | lib/project/src/producer/cli.py | 13 | ||||
| -rw-r--r-- | lib/project/src/producer/config.py | 3 | ||||
| -rw-r--r-- | lib/project/src/producer/pipeline.py | 26 |
3 files changed, 19 insertions, 23 deletions
diff --git a/lib/project/src/producer/cli.py b/lib/project/src/producer/cli.py index 52cd1bd..4788fb5 100644 --- a/lib/project/src/producer/cli.py +++ b/lib/project/src/producer/cli.py @@ -13,9 +13,9 @@ import tomllib from rich.console import Console from . import __version__ -from .audio import ffmpeg_run, loudness, measure +from .audio import ffmpeg_run from .config import PROFILES, Settings, resolve, toml -from .pipeline import check_target, process, targets_for +from .pipeline import check_target, process, report_for, targets_for from .progress import Display from .setup import data_dir, ensure_ffmpeg @@ -24,7 +24,7 @@ def parser(): app = argparse.ArgumentParser(prog="producer", description="Local speech cleanup and podcast mastering. Originals are never overwritten.") app.add_argument("--version", action="version", version=__version__) sub = app.add_subparsers(dest="command", required=True) - for name in ("process", "preview", "config", "analyze"): + for name in ("process", "preview", "config"): cmd = sub.add_parser(name) cmd.add_argument("--config", type=Path, help="TOML configuration file") cmd.add_argument("--profile", choices=PROFILES, default=None) @@ -104,11 +104,6 @@ def main(): print(json.dumps(asdict(settings), indent=2) if args.json else toml(settings), end="\n" if args.json else "") return with Display() as display: - if args.command == "analyze": - stats = measure(args.inputs[0], display.update) - stats["loudness"] = loudness(ensure_ffmpeg(), args.inputs[0], settings, stats["duration_seconds"], display.update) - print(json.dumps(stats, indent=2, allow_nan=False)) - return if args.command == "process": if args.output and len(args.inputs) != 1: raise ValueError("--output requires exactly one input") @@ -156,7 +151,7 @@ def main(): output = output_dir / f"{source.stem}.{profile}.preview.wav" if output.resolve() == original: raise ValueError("Preview output would overwrite the original") - targets = [output, output.with_suffix(".report.json")] + targets = [output, report_for(output)] resolved = {target.resolve() for target in targets} if len(resolved) < len(targets) or resolved & claimed: raise ValueError("Preview outputs have duplicate names") diff --git a/lib/project/src/producer/config.py b/lib/project/src/producer/config.py index ef0cd49..b1d84cc 100644 --- a/lib/project/src/producer/config.py +++ b/lib/project/src/producer/config.py @@ -12,7 +12,6 @@ class Settings: device: str = "auto" denoise_strength: float = 0.85 fft_reduction_db: float = 8.0 - channel: str = "auto" highpass: bool = True highpass_hz: float = 70.0 lowpass: bool = False @@ -97,7 +96,7 @@ def resolve(path: Path | None = None, profile: str | None = None, raise ValueError(f"{key} must be between {RANGES[key][0]} and {RANGES[key][1]}") for key, options in { "denoiser": ("deepfilter", "fft", "none"), "device": ("auto", "cpu", "cuda"), - "channel": ("auto", "left", "right", "mix"), "hum_hz": (0, 50, 60), + "hum_hz": (0, 50, 60), "sample_rate": (44100, 48000), "mp3_bitrate": (128, 160, 192, 224, 256, 320), }.items(): if merged[key] not in options: diff --git a/lib/project/src/producer/pipeline.py b/lib/project/src/producer/pipeline.py index 8fb5484..d7c2ced 100644 --- a/lib/project/src/producer/pipeline.py +++ b/lib/project/src/producer/pipeline.py @@ -31,9 +31,14 @@ def filters(settings: Settings) -> list[str]: return result or ["anull"] +def report_for(destination: Path) -> Path: + """Diagnostic reports live in a logs/ directory, out of the way of outputs.""" + return destination.parent / "logs" / destination.with_suffix(".report.json").name + + def targets_for(destination: Path, settings: Settings) -> list[Path]: """Every path a successful run publishes: master WAV, report, optional MP3.""" - targets = [destination, destination.with_suffix(".report.json")] + targets = [destination, report_for(destination)] if settings.mp3: targets.append(destination.with_suffix(".mp3")) return targets @@ -66,18 +71,17 @@ def process(source: Path, destination: Path, settings: Settings, progress, raise ValueError(f"Input must be an existing WAV file: {source}") if destination.suffix.lower() != ".wav": raise ValueError("Master output must have a .wav extension") - report_path = destination.with_suffix(".report.json") + report_path = report_for(destination) for target in targets_for(destination, settings): check_target(source, target, overwrite) ffmpeg = ensure_ffmpeg() original = measure(source, progress, "Analyzing original") if original["sample_peak_dbfs"] < -100: raise ValueError("Recording is silent or too quiet to master safely") - channel = settings.channel - if original["channels"] == 2 and channel == "auto": - raise ValueError("Stereo input: select --channel left, right, or mix explicitly. Auto downmix could cancel your voice.") - if original["channels"] == 1 and channel == "right": - raise ValueError("Cannot select the right channel of a mono recording") + if original["channels"] == 2: + raise ValueError("Stereo input: downmix to mono first, for example " + "'ffmpeg -i input.wav -ac 1 mono.wav'. An automatic " + "downmix could cancel your voice.") duration = original["duration_seconds"] warnings = [] if original["near_full_scale_samples"]: @@ -85,13 +89,11 @@ def process(source: Path, destination: Path, settings: Settings, progress, if max(abs(x) for x in original["dc_offset"]) > 0.01: warnings.append("Significant input DC offset detected; enable highpass filtering to remove it.") destination.parent.mkdir(parents=True, exist_ok=True) + report_path.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix=".producer-", dir=destination.parent) as temporary: work = Path(temporary) prepared, cleaned, shaped = [work / name for name in ("prepared.wav", "cleaned.wav", "shaped.wav")] input_filters = [] - if original["channels"] == 2: - input_filters.append({"left": "pan=mono|c0=c0", "right": "pan=mono|c0=c1", - "mix": "pan=mono|c0=0.5*c0+0.5*c1"}[channel]) if settings.highpass: input_filters.append(f"highpass=f={settings.highpass_hz}:p=2") if settings.hum_hz: @@ -110,8 +112,8 @@ def process(source: Path, destination: Path, settings: Settings, progress, cleaned = prepared clean_stats = measure(cleaned, progress, "Measuring cleaned voice") if clean_stats["sample_peak_dbfs"] < -100: - raise ValueError("The selected audio is silent or too quiet to master safely; " - "check --channel, the mix, and cleanup settings") + raise ValueError("The audio is silent or too quiet to master safely; " + "check the denoise and cleanup settings") gain = 0.0 if settings.leveling: speech_db = clean_stats["speech_level_estimate_dbfs"] |
