Skip to content

Xilu005 Discover Sfx

src.xil_pipeline.XILU005_discover_SFX

Discover and inspect personally generated Sound Effects.

Two data sources are supported:

  1. ElevenLabs account history (--api, default when accessible): Calls GET /v1/sound-generation/history directly. Requires the API key to have Sound Effects access. If your key is missing it, go to ElevenLabs → Profile → API Keys → edit your key → Endpoints → Sound Effects → Access.

  2. Local SFX library (--local, default fallback): Scans the SFX/ directory and reads ID3/mutagen metadata — prompt text stored in the USLT lyrics tag, duration, file size, bit-rate. Works with no API key required.

Usage:

python XILU005_discover_SFX.py                  # local scan (default)
python XILU005_discover_SFX.py --api             # attempt API (endpoint may not be public)
python XILU005_discover_SFX.py --local           # explicit local scan
python XILU005_discover_SFX.py --search "phone"
python XILU005_discover_SFX.py --verbose
python XILU005_discover_SFX.py --json
python XILU005_discover_SFX.py --sfx-dir SFX/   # override local scan directory

logger module-attribute

logger = get_logger(__name__)

SFX_DIR module-attribute

SFX_DIR = 'SFX'

ELEVENLABS_BASE module-attribute

ELEVENLABS_BASE = 'https://api.elevenlabs.io'

fetch_local_records

fetch_local_records(sfx_dir: str) -> list[dict]

Scan sfx_dir and return one record per .mp3 file.

Source code in src/xil_pipeline/XILU005_discover_SFX.py
def fetch_local_records(sfx_dir: str) -> list[dict]:
    """Scan *sfx_dir* and return one record per ``.mp3`` file."""
    if not os.path.isdir(sfx_dir):
        logger.warning(f"SFX directory not found: {sfx_dir}")
        return []

    records = []
    for fname in sorted(os.listdir(sfx_dir)):
        if not fname.lower().endswith(".mp3"):
            continue
        records.append(_read_local_record(os.path.join(sfx_dir, fname)))
    return records

fetch_api_records

fetch_api_records(api_key: str, max_items: int | None = None) -> list[dict]

Fetch sound-generation history from the ElevenLabs API.

Paginates until all items are retrieved or max_items is reached.

Parameters:

  • api_key (str) –

    ElevenLabs API key with sound_generation permission.

  • max_items (int | None, default: None ) –

    Cap on total records returned. None = no limit.

Returns:

Raises:

  • SystemExit

    When the API key is missing the required permission.

Source code in src/xil_pipeline/XILU005_discover_SFX.py
def fetch_api_records(api_key: str, max_items: int | None = None) -> list[dict]:
    """Fetch sound-generation history from the ElevenLabs API.

    Paginates until all items are retrieved or *max_items* is reached.

    Args:
        api_key: ElevenLabs API key with ``sound_generation`` permission.
        max_items: Cap on total records returned.  ``None`` = no limit.

    Returns:
        List of record dicts.

    Raises:
        SystemExit: When the API key is missing the required permission.
    """
    headers = {"xi-api-key": api_key, "accept": "application/json"}
    records: list[dict] = []
    start_after: str | None = None
    page_size = 100

    while True:
        params: dict = {"page_size": page_size}
        if start_after:
            params["start_after_history_item_id"] = start_after

        resp = httpx.get(
            f"{ELEVENLABS_BASE}/v1/sound-generation/history",
            headers=headers,
            params=params,
            timeout=15,
        )

        if resp.status_code == 401:
            body = resp.json()
            detail = body.get("detail", {}) if isinstance(body, dict) else {}
            status = detail.get("status", "") if isinstance(detail, dict) else ""
            logger.warning("ElevenLabs API: permission denied for sound-generation history.")
            logger.warning("")
            if status in ("missing_permissions", "needs_authorization"):
                logger.warning("    Fix: ElevenLabs dashboard → Profile → API Keys")
                logger.warning("    Edit your key → Endpoints → Sound Effects → set to 'Access'")
                logger.warning("    then re-run without --api to fall back to local scan,")
                logger.warning("    or with --api once the permission is active.")
            else:
                logger.warning(f"    Response: {resp.text[:200]}")
            raise SystemExit(1)

        resp.raise_for_status()
        data = resp.json()

        # Normalise the response — ElevenLabs may use 'history' or 'generations'
        items = data.get("history") or data.get("generations") or data.get("items") or []

        for item in items:
            cfg = item.get("generation_config") or item.get("settings") or {}
            ts = item.get("date_unix") or item.get("created_at_unix")
            char_from = item.get("character_count_change_from", 0)
            char_to = item.get("character_count_change_to", 0)

            records.append({
                "source":       "api",
                "history_item_id": item.get("history_item_id") or item.get("id", ""),
                "prompt":       item.get("text") or item.get("prompt", ""),
                "model_id":     item.get("model_id", ""),
                "date":         _fmt_unix(ts),
                "date_unix":    ts or 0,
                "duration_seconds": cfg.get("duration_seconds"),
                "prompt_influence": cfg.get("prompt_influence"),
                "credits_used": char_to - char_from,
                "filename":     "",
                "path":         "",
            })

            if max_items is not None and len(records) >= max_items:
                return records

        has_more = data.get("has_more", False)
        if not has_more:
            break
        start_after = data.get("last_history_item_id") or (items[-1].get("history_item_id") if items else None)
        if not start_after:
            break

    return records

print_verbose_local

print_verbose_local(rec: dict) -> None

Print all fields for a local SFX record.

Source code in src/xil_pipeline/XILU005_discover_SFX.py
def print_verbose_local(rec: dict) -> None:
    """Print all fields for a local SFX record."""
    logger.info(f"  File           : {rec['filename']}")
    prompt_display = rec["prompt"] if rec["prompt"] else "— (no prompt tag)"
    logger.info(f"  Prompt         : {prompt_display}")
    if rec["title"]:
        logger.info(f"  Title          : {rec['title']}")
    if rec["duration_seconds"] is not None:
        logger.info(f"  Duration       : {_fmt_duration(rec['duration_seconds'])}")
    if rec["bitrate_kbps"] is not None:
        logger.info(f"  Bitrate        : {rec['bitrate_kbps']} kbps")
    logger.info(f"  Size           : {_fmt_size(rec['size_bytes'])}")
    logger.info("")

print_compact_local

print_compact_local(rec: dict) -> None

Print a compact line for a local SFX record.

Source code in src/xil_pipeline/XILU005_discover_SFX.py
def print_compact_local(rec: dict) -> None:
    """Print a compact line for a local SFX record."""
    dur = _fmt_duration(rec["duration_seconds"])
    size = _fmt_size(rec["size_bytes"])
    prompt = rec["prompt"][:72] + "…" if len(rec["prompt"]) > 72 else rec["prompt"]
    meta = f"{dur:6s}  {size:8s}"
    logger.info(f"  {rec['filename']:<38}  {meta}")
    if prompt:
        logger.info(f"    {prompt}")

print_verbose_api

print_verbose_api(rec: dict) -> None

Print all fields for an API SFX record.

Source code in src/xil_pipeline/XILU005_discover_SFX.py
def print_verbose_api(rec: dict) -> None:
    """Print all fields for an API SFX record."""
    logger.info(f"  Prompt         : {rec['prompt']}")
    logger.info(f"  History ID     : {rec['history_item_id']}")
    if rec["model_id"]:
        logger.info(f"  Model          : {rec['model_id']}")
    logger.info(f"  Created        : {rec['date'] or '—'}")
    if rec["duration_seconds"] is not None:
        logger.info(f"  Duration       : {_fmt_duration(rec['duration_seconds'])}")
    if rec["prompt_influence"] is not None:
        logger.info(f"  Prompt infl.   : {rec['prompt_influence']}")
    logger.info(f"  Credits used   : {rec['credits_used']}")
    logger.info("")

print_compact_api

print_compact_api(rec: dict) -> None

Print a compact summary line for an API SFX record.

Source code in src/xil_pipeline/XILU005_discover_SFX.py
def print_compact_api(rec: dict) -> None:
    """Print a compact summary line for an API SFX record."""
    dur = _fmt_duration(rec["duration_seconds"]) if rec["duration_seconds"] else ""
    prompt = rec["prompt"][:72] + "…" if len(rec["prompt"]) > 72 else rec["prompt"]
    logger.info(f"  {rec['date']}  {rec['history_item_id']}  {dur}")
    if prompt:
        logger.info(f"    {prompt}")

export_kit

export_kit(records: list[dict], output_dir: str = '.') -> tuple[str, str, str]

Generate SFX inventory JSON, pipe-hint cheatsheet, and copy the reference doc.

Parameters:

  • records (list[dict]) –

    Local SFX records from fetch_local_records().

  • output_dir (str, default: '.' ) –

    Directory to write output files into.

Returns:

  • tuple[str, str, str]

    Tuple of (json_path, hints_path, markdown_path).

Source code in src/xil_pipeline/XILU005_discover_SFX.py
def export_kit(records: list[dict], output_dir: str = ".") -> tuple[str, str, str]:
    """Generate SFX inventory JSON, pipe-hint cheatsheet, and copy the reference doc.

    Args:
        records: Local SFX records from ``fetch_local_records()``.
        output_dir: Directory to write output files into.

    Returns:
        Tuple of (json_path, hints_path, markdown_path).
    """
    os.makedirs(output_dir, exist_ok=True)

    # Write JSON inventory
    json_path = os.path.join(output_dir, "sfx_inventory.json")
    with open(json_path, "w", encoding="utf-8") as f:
        _json.dump(records, f, indent=2)
        f.write("\n")

    # Write pipe-hint cheatsheet
    hints_path = _write_pipe_hints_md(records, output_dir)

    # Copy the scriptwriter reference doc
    ref_src = os.path.join(os.path.dirname(__file__), "..", "..", "docs", "claude-scriptwriter-reference.md")
    ref_src = os.path.normpath(ref_src)
    md_path = os.path.normpath(os.path.join(output_dir, "claude-scriptwriter-reference.md"))
    def _same(a: str, b: str) -> bool:
        try:
            return os.path.exists(a) and os.path.exists(b) and os.path.samefile(a, b)
        except OSError:
            return False

    if os.path.exists(ref_src):
        if not _same(ref_src, md_path):
            shutil.copy2(ref_src, md_path)
    else:
        # Fallback: check relative to CWD (for editable installs)
        cwd_ref = os.path.normpath(os.path.join("docs", "claude-scriptwriter-reference.md"))
        if os.path.exists(cwd_ref):
            if not _same(cwd_ref, md_path):
                shutil.copy2(cwd_ref, md_path)
            else:
                md_path = cwd_ref  # already in place
        else:
            logger.warning(f"Reference doc not found at {ref_src} or {cwd_ref}")
            md_path = ""

    return json_path, hints_path, md_path

get_parser

get_parser() -> argparse.ArgumentParser
Source code in src/xil_pipeline/XILU005_discover_SFX.py
def get_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="xil-sfx-lib",
        description="Discover personally generated Sound Effects from the ElevenLabs account or local SFX/ directory",
    )
    mode = parser.add_mutually_exclusive_group()
    mode.add_argument(
        "--api",
        action="store_true",
        help="Query ElevenLabs /v1/sound-generation/history (requires sound_generation permission)",
    )
    mode.add_argument(
        "--local",
        action="store_true",
        help="Scan local SFX/ directory only (no API key needed)",
    )
    parser.add_argument(
        "--sfx-dir",
        default=SFX_DIR,
        metavar="DIR",
        help=f"Local SFX directory to scan (default: {SFX_DIR}/)",
    )
    parser.add_argument(
        "--search",
        metavar="TEXT",
        help="Case-insensitive substring filter on the prompt/filename",
    )
    parser.add_argument(
        "--all",
        action="store_true",
        help="(API mode) Paginate through the full account history; default: most recent 100",
    )
    parser.add_argument(
        "--verbose", "-v",
        action="store_true",
        help="Print all fields for each record",
    )
    parser.add_argument(
        "--json",
        action="store_true",
        help="Output results as a JSON array",
    )
    parser.add_argument(
        "--export-kit",
        metavar="DIR",
        nargs="?",
        const=".",
        default=None,
        help="Export SFX inventory JSON + scriptwriter reference doc to DIR (default: current directory)",
    )
    return parser

main

main() -> None

CLI entry point for SFX discovery.

Source code in src/xil_pipeline/XILU005_discover_SFX.py
def main() -> None:
    """CLI entry point for SFX discovery."""
    configure_logging()
    with run_banner():
        args = get_parser().parse_args()

        api_key = os.environ.get("ELEVENLABS_API_KEY", "")
        # Default to local scan — the /v1/sound-generation/history endpoint appears
        # to be internal-only and is not accessible via public API keys regardless
        # of permission settings.  Pass --api explicitly to attempt it anyway.
        use_api = args.api

        if use_api and not api_key:
            logger.warning("ELEVENLABS_API_KEY not set.")
            raise SystemExit(1)

        # --- Fetch records ---
        if use_api:
            max_items = None if args.all else 100
            try:
                records = fetch_api_records(api_key, max_items=max_items)
                data_source = "API"
            except SystemExit:
                if args.api:
                    raise  # user explicitly asked for API — propagate the error
                logger.info("")
                logger.info("  Falling back to local SFX/ directory scan.")
                logger.info("")
                records = fetch_local_records(args.sfx_dir)
                data_source = f"local ({args.sfx_dir}/)"
        else:
            records = fetch_local_records(args.sfx_dir)
            data_source = f"local ({args.sfx_dir}/)"

        # --- Search filter ---
        if args.search:
            q = args.search.lower()
            records = [
                r for r in records
                if q in r.get("prompt", "").lower()
                or q in r.get("filename", "").lower()
            ]

        # --- Sort ---
        if records and records[0].get("date_unix"):
            records.sort(key=lambda r: r.get("date_unix", 0), reverse=True)
        else:
            records.sort(key=lambda r: r.get("filename", "").lower())

        # --- Export kit ---
        if args.export_kit is not None:
            json_path, hints_path, md_path = export_kit(records, args.export_kit)
            logger.info(f"\n--- Export kit ({len(records)} assets) ---\n")
            logger.info(f"  JSON inventory : {json_path}")
            logger.info(f"  Pipe-hint cheatsheet : {hints_path}")
            if md_path:
                logger.info(f"  Reference doc  : {md_path}")
            logger.info("")
            logger.info("  Attach all three files to your Claude project as knowledge files.")
            return

        # --- Output ---
        if args.json:
            print(_json.dumps(records, indent=2))
            return

        logger.info(f"\n--- ElevenLabs Sound Effects  [{data_source}]  ({len(records)} items) ---\n")

        if not records:
            logger.info("  No sound-effect records found.")
            if args.search:
                logger.info(f"  (search filter: {args.search!r})")
            return

        if args.verbose:
            for rec in records:
                if rec["source"] == "local":
                    print_verbose_local(rec)
                else:
                    print_verbose_api(rec)
        else:
            for rec in records:
                if rec["source"] == "local":
                    print_compact_local(rec)
                else:
                    print_compact_api(rec)
            logger.info("")

            if data_source.startswith("local"):
                total_size = sum(r.get("size_bytes", 0) for r in records)
                logger.info(f"  Total size: {total_size / (1024*1024):.1f} MB  ({len(records)} files)")
            else:
                total_credits = sum(r.get("credits_used", 0) for r in records)
                logger.info(f"  Total credits used: {total_credits:,}")

            logger.info("")
            logger.info("  Use --verbose for full details, --json for machine-readable output,")
            logger.info("  --search <text> to filter, --local / --api to select data source.")