Skip to content

Xilu001 Discover Voices T2S

src.xil_pipeline.XILU001_discover_voices_T2S

Discover and inspect ElevenLabs voices available in this workspace.

Lists all voices returned by the API with enriched metadata drawn from labels, sharing, verified_languages, and other fields exposed by the SDK. Useful for finding voice IDs and assessing voice suitability before configuring cast files.

Usage:

python XILU001_discover_voices_T2S.py
python XILU001_discover_voices_T2S.py --category professional
python XILU001_discover_voices_T2S.py --category cloned generated
python XILU001_discover_voices_T2S.py --search tina
python XILU001_discover_voices_T2S.py --verbose
python XILU001_discover_voices_T2S.py --json
python XILU001_discover_voices_T2S.py --id WtA85syCrJwasGeHGH2p

Categories returned by the API: premade, cloned, generated, professional (Professional Voice Clone / PVC).

Permissions note: all workspace voices (including PVCs copied from the voice library) return permission_on_resource = 'admin' — no access barriers exist. PVCs show is_owner = False but remain fully usable.

logger module-attribute

logger = get_logger(__name__)

CATEGORY_LABELS module-attribute

CATEGORY_LABELS = {'premade': 'Premade', 'cloned': 'Instant Clone', 'generated': 'Generated', 'professional': 'Professional Clone (PVC)'}

SHARING_CATEGORY_LABELS module-attribute

SHARING_CATEGORY_LABELS = {'professional': 'Professional Clone', 'high_quality': 'High Quality'}

build_voice_record

build_voice_record(v) -> dict

Extract all relevant fields from a Voice SDK object into a plain dict.

Source code in src/xil_pipeline/XILU001_discover_voices_T2S.py
def build_voice_record(v) -> dict:
    """Extract all relevant fields from a Voice SDK object into a plain dict."""
    labels = v.labels or {}
    sharing = v.sharing

    # Library (sharing) metadata — richer description/name for copied voices
    library_name = sharing.name if sharing else None
    library_desc = sharing.description if sharing else None
    sharing_cat = SHARING_CATEGORY_LABELS.get(sharing.category, sharing.category) if sharing else None
    notice_days = sharing.notice_period if sharing else None

    # Prefer local description; fall back to library description
    description = v.description or library_desc or ""

    verified_langs = v.verified_languages or []
    lang_str = _fmt_languages(verified_langs)

    return {
        "voice_id":          v.voice_id,
        "name":              v.name,
        "library_name":      library_name,
        "category":          v.category,
        "sharing_category":  sharing_cat,
        "description":       description,
        "gender":            labels.get("gender", ""),
        "age":               labels.get("age", ""),
        "accent":            labels.get("accent", ""),
        "descriptive":       labels.get("descriptive", ""),
        "use_case":          labels.get("use_case", ""),
        "language":          labels.get("language", ""),
        "verified_languages": lang_str,
        "verified_lang_count": len(set(vl.language for vl in verified_langs)),
        "high_quality_models": v.high_quality_base_model_ids or [],
        "is_owner":          v.is_owner,
        "is_bookmarked":     v.is_bookmarked,
        "permission":        v.permission_on_resource,
        "created_at":        _fmt_unix(v.created_at_unix),
        "notice_days":       notice_days,
    }

print_verbose

print_verbose(rec: dict) -> None

Print all fields for a single voice record.

Source code in src/xil_pipeline/XILU001_discover_voices_T2S.py
def print_verbose(rec: dict) -> None:
    """Print all fields for a single voice record."""
    cat_label = CATEGORY_LABELS.get(rec["category"], rec["category"] or "?")
    if rec["sharing_category"]:
        cat_label = f"{cat_label} / {rec['sharing_category']}"

    logger.info(f"  Name         : {rec['name']}")
    if rec["library_name"] and rec["library_name"] != rec["name"]:
        logger.info(f"  Library name : {rec['library_name']}")
    logger.info(f"  Voice ID     : {rec['voice_id']}")
    logger.info(f"  Category     : {cat_label}")
    if rec["description"]:
        logger.info(f"  Description  : {rec['description']}")
    logger.info(f"  Gender       : {rec['gender'] or '—'}")
    logger.info(f"  Age          : {rec['age'] or '—'}")
    logger.info(f"  Accent       : {rec['accent'] or '—'}")
    logger.info(f"  Tone/style   : {rec['descriptive'] or '—'}")
    logger.info(f"  Use case     : {rec['use_case'] or '—'}")
    logger.info(f"  Language     : {rec['language'] or '—'}")
    if rec["verified_languages"]:
        logger.info(f"  Verified langs: {rec['verified_languages']} ({rec['verified_lang_count']} total)")
    if rec["high_quality_models"]:
        logger.info(f"  HQ models    : {', '.join(rec['high_quality_models'])}")
    logger.info(f"  Owner        : {'Yes' if rec['is_owner'] else 'No (library copy)'}")
    logger.info(f"  Bookmarked   : {'Yes' if rec['is_bookmarked'] else 'No'}")
    logger.info(f"  Permission   : {rec['permission'] or 'none'}")
    logger.info(f"  Created      : {rec['created_at'] or '—'}")
    if rec["notice_days"]:
        logger.info(f"  Notice period: {rec['notice_days']} days")
    logger.info("")

print_compact

print_compact(rec: dict) -> None

Print a single compact summary line for a voice.

Source code in src/xil_pipeline/XILU001_discover_voices_T2S.py
def print_compact(rec: dict) -> None:
    """Print a single compact summary line for a voice."""
    cat = CATEGORY_LABELS.get(rec["category"], rec["category"] or "?")
    gender = rec["gender"] or "?"
    age = rec["age"] or "?"
    accent = rec["accent"] or "?"
    tone = rec["descriptive"] or "?"
    langs = f" | langs: {rec['verified_languages']}" if rec["verified_languages"] else ""
    desc = f" | {rec['description'][:60]}" if rec["description"] else ""
    logger.info(
        f"  {rec['name']:<28} {rec['voice_id']}  [{cat}]"
        f"\n    {gender}, {age}, {accent}, {tone}{langs}{desc}"
    )

update_cast

update_cast(cast_path: str, records_by_id: dict, dry_run: bool = False) -> None

Back-fill cast JSON fields from API voice metadata.

For each cast member whose voice_id is not "TBD", looks up the voice in records_by_id and updates:

  • role — set to the voice description if currently "TBD"
  • language_code — set from labels.language if currently null

full_name is intentionally left unchanged (it is the character name, not the voice name).

Parameters:

  • cast_path (str) –

    Path to the cast JSON file to update.

  • records_by_id (dict) –

    Mapping of voice_id → voice record dict from :func:build_voice_record.

  • dry_run (bool, default: False ) –

    When True, print the diff but do not write the file.

Source code in src/xil_pipeline/XILU001_discover_voices_T2S.py
def update_cast(cast_path: str, records_by_id: dict, dry_run: bool = False) -> None:
    """Back-fill cast JSON fields from API voice metadata.

    For each cast member whose ``voice_id`` is not ``"TBD"``, looks up
    the voice in *records_by_id* and updates:

    - ``role`` — set to the voice description if currently ``"TBD"``
    - ``language_code`` — set from ``labels.language`` if currently ``null``

    ``full_name`` is intentionally left unchanged (it is the character
    name, not the voice name).

    Args:
        cast_path: Path to the cast JSON file to update.
        records_by_id: Mapping of ``voice_id`` → voice record dict from
            :func:`build_voice_record`.
        dry_run: When ``True``, print the diff but do not write the file.
    """
    with open(cast_path, encoding="utf-8") as f:
        cast_data = _json.load(f)

    changes: list[str] = []

    for key, member in cast_data.get("cast", {}).items():
        vid = member.get("voice_id", "TBD")
        if vid == "TBD":
            logger.info(f"  {key}: voice_id is TBD — skipping")
            continue
        rec = records_by_id.get(vid)
        if rec is None:
            logger.info(f"  {key} ({vid}): not found in workspace voices — skipping")
            continue

        # role: fill if still "TBD"
        if member.get("role") == "TBD" and rec.get("description"):
            old = member["role"]
            member["role"] = rec["description"]
            changes.append(f"  {key}.role: {old!r}{member['role']!r}")

        # language_code: fill if null/missing
        if not member.get("language_code") and rec.get("language"):
            member["language_code"] = rec["language"]
            changes.append(f"  {key}.language_code: null → {member['language_code']!r}")

    if not changes:
        logger.info("  No updates needed — cast file is already fully populated.")
        return

    logger.info(f"  {'(dry run) ' if dry_run else ''}Changes ({len(changes)}):")
    for c in changes:
        logger.info(c)

    if not dry_run:
        with open(cast_path, "w", encoding="utf-8") as f:
            _json.dump(cast_data, f, indent=2, ensure_ascii=False)
            f.write("\n")
        logger.info(f"  Written: {cast_path}")

get_parser

get_parser() -> argparse.ArgumentParser
Source code in src/xil_pipeline/XILU001_discover_voices_T2S.py
def get_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="xil-voices",
        description="List ElevenLabs voices with enriched metadata",
    )
    parser.add_argument(
        "--category", nargs="+",
        metavar="CAT",
        help="Filter by category: premade cloned generated professional",
    )
    parser.add_argument(
        "--search",
        metavar="TEXT",
        help="Case-insensitive substring filter on name or description",
    )
    parser.add_argument(
        "--id",
        metavar="VOICE_ID",
        help="Show full detail for a single voice ID",
    )
    parser.add_argument(
        "--verbose", "-v", action="store_true",
        help="Print all fields for each voice",
    )
    parser.add_argument(
        "--json", action="store_true",
        help="Output results as JSON array",
    )
    parser.add_argument(
        "--update-cast",
        metavar="CAST_JSON",
        help="Back-fill role and language_code in a cast JSON from API voice metadata",
    )
    parser.add_argument(
        "--dry-run", action="store_true",
        help="With --update-cast: show changes without writing the file",
    )
    return parser

main

main() -> None

CLI entry point for voice discovery.

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

        client = ElevenLabs(api_key=os.environ.get("ELEVENLABS_API_KEY"))
        response = client.voices.get_all()
        voices = response.voices

        records = [build_voice_record(v) for v in voices]
        records_by_id = {r["voice_id"]: r for r in records}

        # --update-cast: enrich a cast JSON from API metadata, then exit
        if args.update_cast:
            logger.info(f"\n--- Updating cast file: {args.update_cast} ---")
            update_cast(args.update_cast, records_by_id, dry_run=args.dry_run)
            return

        # --id: single voice detail
        if args.id:
            matches = [r for r in records if r["voice_id"] == args.id]
            if not matches:
                logger.info(f"Voice ID {args.id!r} not found in your workspace.")
                return
            print_verbose(matches[0])
            return

        # Category filter
        if args.category:
            cats = {c.lower() for c in args.category}
            records = [r for r in records if (r["category"] or "").lower() in cats]

        # Search filter
        if args.search:
            q = args.search.lower()
            records = [
                r for r in records
                if q in (r["name"] or "").lower()
                or q in (r["description"] or "").lower()
                or q in (r["library_name"] or "").lower()
            ]

        # Sort: bookmarked first, then by name
        records.sort(key=lambda r: (not r["is_bookmarked"], r["name"].lower()))

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

        # Summary header
        cat_counts: dict[str, int] = {}
        for r in records:
            cat_counts[r["category"] or "unknown"] = cat_counts.get(r["category"] or "unknown", 0) + 1

        logger.info(f"\n--- ElevenLabs Voices ({len(records)} shown) ---")
        for cat, count in sorted(cat_counts.items()):
            logger.info(f"  {CATEGORY_LABELS.get(cat, cat)}: {count}")
        logger.info("")

        if args.verbose:
            for rec in records:
                print_verbose(rec)
        else:
            for rec in records:
                print_compact(rec)
            logger.info("")
            logger.info("  Use --verbose for full details, --json for machine-readable output,")
            logger.info("  --id <VOICE_ID> for a single voice, --category / --search to filter.")