Skip to content

Xilp011 Master Export

src.xil_pipeline.XILP011_master_export

XILP011 — Final Master MP3 Export.

Overlays the five DAW layer WAV files produced by XILP005 into a single stereo MP3 file suitable for podcast distribution.

Output format
  • Stereo, 48 kHz sample rate
  • VBR MP3, quality target 145–185 kbps (LAME VBR quality ~2)
  • Filename: S01E01_<slug>_YYYY-MM-DD.mp3

Usage:

python XILP011_master_export.py --episode S02E03 --dry-run
python XILP011_master_export.py --episode S02E03
python XILP011_master_export.py --episode S02E03 --show "Night Owls"

No ElevenLabs API calls are made — this stage is safe to run freely.

logger module-attribute

logger = get_logger(__name__)

MASTERS_DIR module-attribute

MASTERS_DIR = str(get_workspace_root() / 'masters')

DAW_DIR module-attribute

DAW_DIR = str(get_workspace_root() / 'daw')

LAYER_SUFFIXES module-attribute

LAYER_SUFFIXES = ('dialogue', 'ambience', 'music', 'sfx', 'vintage_filter')

SAMPLE_RATE module-attribute

SAMPLE_RATE = 48000

load_layer_wavs

load_layer_wavs(daw_dir: str, tag: str) -> list[tuple[str, str]]

Locate the four layer WAV files for an episode.

Returns:

  • list[tuple[str, str]]

    List of (layer_name, file_path) tuples for layers that exist on disk.

Source code in src/xil_pipeline/XILP011_master_export.py
def load_layer_wavs(daw_dir: str, tag: str) -> list[tuple[str, str]]:
    """Locate the four layer WAV files for an episode.

    Returns:
        List of (layer_name, file_path) tuples for layers that exist on disk.
    """
    found = []
    for suffix in LAYER_SUFFIXES:
        fname = f"{tag}_layer_{suffix}.wav"
        path = os.path.join(daw_dir, fname)
        if os.path.exists(path):
            found.append((suffix, path))
    return found

mix_layers

mix_layers(layer_paths: list[tuple[str, str]]) -> AudioSegment

Overlay all layer WAVs into a single AudioSegment.

All layers are assumed to be the same duration and aligned at t=0 (as produced by XILP005).

Source code in src/xil_pipeline/XILP011_master_export.py
def mix_layers(layer_paths: list[tuple[str, str]]) -> AudioSegment:
    """Overlay all layer WAVs into a single AudioSegment.

    All layers are assumed to be the same duration and aligned at t=0
    (as produced by XILP005).
    """
    combined = None
    for name, path in layer_paths:
        seg = AudioSegment.from_wav(path)
        if combined is None:
            combined = seg
        else:
            combined = combined.overlay(seg)
    return combined

export_master

export_master(combined: AudioSegment, output_path: str, show_name: str, tag: str, title: str | None = None, artist: str | None = None) -> None

Export the mixed audio as a stereo 48 kHz VBR MP3.

Uses LAME VBR quality 2 which targets ~170–210 kbps ABR, producing a VBR stream in the 145–185 kbps range for spoken-word content.

Source code in src/xil_pipeline/XILP011_master_export.py
def export_master(
    combined: AudioSegment,
    output_path: str,
    show_name: str,
    tag: str,
    title: str | None = None,
    artist: str | None = None,
) -> None:
    """Export the mixed audio as a stereo 48 kHz VBR MP3.

    Uses LAME VBR quality 2 which targets ~170–210 kbps ABR, producing
    a VBR stream in the 145–185 kbps range for spoken-word content.
    """
    # Ensure stereo
    if combined.channels == 1:
        combined = combined.set_channels(2)

    # Resample to 48 kHz
    combined = combined.set_frame_rate(SAMPLE_RATE)

    combined.export(
        output_path,
        format="mp3",
        parameters=[
            "-q:a", "2",          # LAME VBR quality (lower = higher bitrate)
            "-ar", str(SAMPLE_RATE),
        ],
    )

    # Write ID3 metadata
    tag_mp3(
        output_path,
        show=show_name,
        title=title or tag,
        artist=artist,
    )

get_parser

get_parser() -> argparse.ArgumentParser
Source code in src/xil_pipeline/XILP011_master_export.py
def get_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="xil-master",
        description="Final Master MP3 Export — mix DAW layers into a single podcast-ready MP3",
    )
    tag_group = parser.add_mutually_exclusive_group(required=True)
    tag_group.add_argument("--episode",
                           help="Episode tag (e.g. S02E03) — derives DAW layer paths")
    tag_group.add_argument("--tag",
                           help="Raw tag for non-episodic content (e.g. V01C03, D01)")
    parser.add_argument(
        "--show", default=None,
        help="Show name override (default: from project.json)",
    )
    parser.add_argument(
        "--daw-dir", default=None,
        help="DAW layer directory (default: daw/<TAG>/)",
    )
    parser.add_argument(
        "--output", default=None,
        help="Output MP3 path (default: masters/<TAG>_<slug>_<date>.mp3)",
    )
    parser.add_argument(
        "--dry-run", action="store_true",
        help="Show what would be exported without writing files",
    )
    return parser

main

main() -> None

CLI entry point for final master MP3 export.

Source code in src/xil_pipeline/XILP011_master_export.py
def main() -> None:
    """CLI entry point for final master MP3 export."""
    configure_logging()
    with run_banner():
        args = get_parser().parse_args()

        slug = resolve_slug(args.show)
        tag = args.episode or args.tag

        # Load cast config for metadata (title, artist)
        p = derive_paths(slug, tag)
        daw_dir = args.daw_dir or p["daw"]
        cast_path = p["cast"]
        parsed_path = p["parsed"]
        show_name = None
        episode_title = None
        artist = None
        if os.path.exists(cast_path):
            with open(cast_path, encoding="utf-8") as f:
                cast_cfg = CastConfiguration(**json.load(f))
            show_name = cast_cfg.show
            episode_title = cast_cfg.title
            artist = cast_cfg.artist

        # Fallback: fill missing ID3 fields from parsed JSON script header
        if (show_name is None or episode_title is None) and os.path.exists(parsed_path):
            with open(parsed_path, encoding="utf-8") as f:
                parsed = json.load(f)
            if show_name is None:
                show_name = parsed.get("show")
            if episode_title is None:
                episode_title = parsed.get("title")
            if show_name or episode_title:
                logger.info("  Metadata sourced from parsed JSON (cast config fields absent)")

        show_name = show_name or "Sample Show"
        today = datetime.date.today().isoformat()

        # Derive output path
        if args.output:
            output_path = args.output
        else:
            os.makedirs(MASTERS_DIR, exist_ok=True)
            output_path = os.path.join(
                MASTERS_DIR, f"{tag}_{slug}_{today}.mp3"
            )

        # Find layer WAVs
        layers = load_layer_wavs(daw_dir, tag)
        missing = [s for s in LAYER_SUFFIXES if s not in {l[0] for l in layers}]

        logger.info(f"  Episode    : {tag}")
        logger.info(f"  Show       : {show_name} (slug: {slug})")
        logger.info(f"  DAW dir    : {daw_dir}")
        logger.info(f"  Output     : {output_path}")
        logger.info(f"  Format     : Stereo, {SAMPLE_RATE} Hz, VBR MP3 (~145-185 kbps)")
        logger.info(f"  Layers     : {len(layers)}/{len(LAYER_SUFFIXES)} found")
        for name, path in layers:
            logger.info(f"    [{name:>9s}] {path}")
        if missing:
            logger.info(f"  Missing    : {', '.join(missing)}")
        logger.info("")

        if not layers:
            logger.warning("No layer WAVs found. Run XILP005 first.")
            return

        if args.dry_run:
            logger.info("--- Dry run — no files written ---")
            return

        # Mix and export
        logger.info("--- Mixing layers ---")
        combined = mix_layers(layers)
        duration_s = len(combined) / 1000.0
        minutes = int(duration_s // 60)
        seconds = duration_s % 60

        logger.info(f"  Duration   : {minutes}:{seconds:05.2f}")
        logger.info("--- Exporting master MP3 ---")

        title = f"{show_name}{episode_title}" if episode_title else tag
        export_master(
            combined, output_path,
            show_name=show_name,
            tag=tag,
            title=title,
            artist=artist,
        )

        file_size_mb = os.path.getsize(output_path) / (1024 * 1024)
        logger.info(f"  Written    : {output_path} ({file_size_mb:.1f} MB)")
        logger.info("--- Done! ---")