Spaces:
Sleeping
Sleeping
Download src/mss/cli.py from metzgerdev/vocal-semantic-search: direct link, hf CLI and curl.
- Browser
- Download file 8.86 kB
-
https://huggingface.co/spaces/metzgerdev/vocal-semantic-search/resolve/main/src/mss/cli.py
- Command line
-
hf download hf://spaces/metzgerdev/vocal-semantic-search/src/mss/cli.py
-
curl -L -o cli.py https://huggingface.co/spaces/metzgerdev/vocal-semantic-search/resolve/main/src/mss/cli.py
8.86 kB
| """Command line interface for the voice library search engine.""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| from . import DEFAULT_MODEL, __version__ | |
| DEFAULT_DIR = "data/voices" | |
| DEFAULT_MODELS = "models" | |
| def cmd_fetch(args) -> int: | |
| from .clap import ClapEncoder | |
| from .voices import (VoiceCatalog, download_previews, embed_voices, fetch_voices) | |
| root = Path(args.dir).expanduser() | |
| print(f"Fetching up to {args.limit} voices from the ElevenLabs Voice Library...") | |
| voices = fetch_voices(limit=args.limit, | |
| progress=lambda n, t: print(f"\r {n}/{t}", end="", flush=True)) | |
| print(f"\nDownloading previews...") | |
| errors = download_previews(voices, root / "previews", | |
| progress=lambda i, t: print(f"\r {i}/{t}", end="", flush=True)) | |
| print("\nEmbedding (runs locally, nothing is uploaded)...") | |
| encoder = ClapEncoder(model_id=args.model) | |
| kept, vecs, embed_errors = embed_voices( | |
| voices, encoder, windows=args.windows, | |
| progress=lambda i, t: print(f"\r {i}/{t}", end="", flush=True)) | |
| catalog = VoiceCatalog(root=root, model_id=encoder.model_id, voices=kept, vectors=vecs) | |
| catalog.save() | |
| print(f"\n{len(kept)} voices embedded -> {root}") | |
| from .evaluate import embedding_health | |
| health = embedding_health(vecs) | |
| if health.get("count", 0) >= 2: | |
| print(f"Embedding spread: mean pairwise cosine " | |
| f"{health['mean_pairwise_cosine']:.3f} (std {health['std_pairwise_cosine']:.3f})") | |
| if health.get("collapsed"): | |
| print("WARNING: vectors have collapsed -- this checkpoint's tower is\n" | |
| " degenerate and retrieval will be meaningless.", file=sys.stderr) | |
| failed = len(errors) + len(embed_errors) | |
| if failed: | |
| print(f"{failed} voice(s) failed", file=sys.stderr) | |
| for e in (errors + embed_errors)[:5]: | |
| print(f" {e}", file=sys.stderr) | |
| return 0 | |
| def _load(args): | |
| from .clap import ClapEncoder | |
| from .voices import VoiceCatalog | |
| catalog = VoiceCatalog.load(Path(args.dir).expanduser()) | |
| if not len(catalog) or catalog.vectors is None: | |
| print(f"no voices at {args.dir} -- run `mss fetch` first", file=sys.stderr) | |
| return None, None | |
| return catalog, ClapEncoder(model_id=catalog.model_id) | |
| def cmd_search(args) -> int: | |
| from .voices import search_voices | |
| catalog, encoder = _load(args) | |
| if catalog is None: | |
| return 1 | |
| from .voices import matching_indices | |
| filters = {k: getattr(args, k) for k in ("gender", "age", "accent", "language") | |
| if getattr(args, k, None)} | |
| # computed once and reused for both the search and the scope line below | |
| keep = matching_indices(catalog, filters) | |
| hits = search_voices(catalog, encoder, args.query, top_n=args.top_n, keep=keep) | |
| if not hits and filters: | |
| print(f"no voices match {filters}", file=sys.stderr) | |
| return 1 | |
| if args.json: | |
| print(json.dumps([{"rank": h.rank, "score": round(h.score, 6), | |
| "name": h.name, "metadata": h.metadata} | |
| for h in hits], indent=2, ensure_ascii=False)) | |
| return 0 | |
| scope = f"{len(catalog)} voices" | |
| if filters: | |
| scope = (f"{len(keep)} of {len(catalog)} voices " | |
| f"matching {', '.join(f'{k}={v}' for k, v in filters.items())}") | |
| print(f'\n"{args.query}" -> top {len(hits)} of {scope}\n') | |
| for h in hits: | |
| m = h.metadata | |
| attrs = "/".join(str(m.get(k) or "?") for k in ("gender", "age", "use_case")) | |
| print(f" {h.rank:>2}. {h.score:.4f} {h.name[:42]:44s} {attrs}") | |
| print() | |
| return 0 | |
| def cmd_eval(args) -> int: | |
| from .voices import attribute_retrieval, linear_probe | |
| catalog, encoder = _load(args) | |
| if catalog is None: | |
| return 1 | |
| print(f"{len(catalog)} voices, model {catalog.model_id}\n") | |
| print("linear probe (is the attribute decodable at all?):") | |
| for attr in ("gender", "age", "use_case", "language"): | |
| r = linear_probe(catalog, attr) | |
| print(f" {r['attribute']:9s} acc={r['accuracy']:.3f} " | |
| f"majority={r['majority_baseline']:.3f} lift={r['lift']:+.3f} | " | |
| f"balanced={r['balanced_accuracy']:.3f} chance={r['chance']:.3f} " | |
| f"lift={r['balanced_lift']:+.3f}") | |
| print("\nretrieval precision@10:") | |
| for attr, value, query in [ | |
| ("gender", "female", "a recording of a woman speaking"), | |
| ("gender", "male", "a recording of a man speaking"), | |
| ("age", "old", "a recording of an elderly person speaking"), | |
| ]: | |
| r = attribute_retrieval(catalog, encoder, attr, value, query) | |
| print(f" {r['target']:14s} P@10={r['precision_at_k']:.2f} " | |
| f"(prevalence {r['prevalence']:.2f})") | |
| return 0 | |
| def cmd_info(args) -> int: | |
| from .evaluate import embedding_health | |
| from .voices import VoiceCatalog | |
| catalog = VoiceCatalog.load(Path(args.dir).expanduser()) | |
| if not len(catalog): | |
| print(f"No voices at {Path(args.dir).expanduser()}") | |
| return 1 | |
| have_audio = sum(1 for v in catalog.voices if catalog.preview_path(v)) | |
| print(f"Catalog : {catalog.root}") | |
| print(f"Voices : {len(catalog)} ({have_audio} with preview audio)") | |
| print(f"Model : {catalog.model_id}") | |
| if catalog.vectors is not None: | |
| print(f"Vectors : {catalog.vectors.shape[0]} x {catalog.vectors.shape[1]} float32") | |
| health = embedding_health(catalog.vectors) | |
| if health.get("count", 0) >= 2: | |
| print(f"Spread : mean pairwise cosine {health['mean_pairwise_cosine']:.3f}" | |
| f" (std {health['std_pairwise_cosine']:.3f})") | |
| if health.get("collapsed"): | |
| print("WARNING : vectors have collapsed -- retrieval is meaningless.") | |
| return 0 | |
| def cmd_export(args) -> int: | |
| from .export_onnx import export | |
| try: | |
| export(args.dest, model_id=args.model, keep_fp32=args.keep_fp32) | |
| except ImportError as exc: | |
| print(f"missing export dependency: {exc}\n" | |
| 'install with: uv pip install -e ".[export]"', file=sys.stderr) | |
| return 1 | |
| return 0 | |
| def cmd_serve(args) -> int: | |
| from .api import serve | |
| serve(args.dir, host=args.host, port=args.port, models=args.models) | |
| return 0 | |
| def main(argv: list[str] | None = None) -> int: | |
| p = argparse.ArgumentParser( | |
| prog="mss", | |
| description="Semantic search over the ElevenLabs Voice Library (CLAP).") | |
| p.add_argument("--version", action="version", version=f"%(prog)s {__version__}") | |
| sub = p.add_subparsers(dest="command", required=True) | |
| def add_dir(sp): | |
| sp.add_argument("-d", "--dir", default=DEFAULT_DIR, | |
| help=f"voice catalog directory (default: {DEFAULT_DIR})") | |
| f = sub.add_parser("fetch", help="fetch voices, download previews, embed them") | |
| add_dir(f) | |
| f.add_argument("--limit", type=int, default=1000) | |
| f.add_argument("--model", default=DEFAULT_MODEL) | |
| f.add_argument("--windows", type=int, default=2) | |
| f.set_defaults(func=cmd_fetch) | |
| s = sub.add_parser("search", help="text query -> ranked voices") | |
| s.add_argument("query") | |
| add_dir(s) | |
| s.add_argument("-n", "--top-n", type=int, default=10) | |
| s.add_argument("--json", action="store_true") | |
| # Filters, not query words: the label is exact where CLAP is 0.90 at best | |
| # on gender and at chance on accent and language. | |
| for attr in ("gender", "age", "accent", "language"): | |
| s.add_argument(f"--{attr}", default=None, help=f"restrict to this {attr}") | |
| s.set_defaults(func=cmd_search) | |
| e = sub.add_parser("eval", help="zero-shot, linear probe and retrieval metrics") | |
| add_dir(e) | |
| e.set_defaults(func=cmd_eval) | |
| n = sub.add_parser("info", help="show catalog status") | |
| add_dir(n) | |
| n.set_defaults(func=cmd_info) | |
| x = sub.add_parser("export", help="export the text tower to int8 ONNX") | |
| x.add_argument("--dest", default=DEFAULT_MODELS, | |
| help=f"output directory (default: {DEFAULT_MODELS})") | |
| x.add_argument("--model", default=DEFAULT_MODEL) | |
| x.add_argument("--keep-fp32", action="store_true", | |
| help="keep the intermediate fp32 graph") | |
| x.set_defaults(func=cmd_export) | |
| v = sub.add_parser("serve", help="run the API backing the web UI") | |
| add_dir(v) | |
| v.add_argument("--host", default="127.0.0.1") | |
| v.add_argument("--port", type=int, default=8787) | |
| v.add_argument("--models", default=DEFAULT_MODELS, | |
| help=f"ONNX export directory, if built (default: {DEFAULT_MODELS})") | |
| v.set_defaults(func=cmd_serve) | |
| args = p.parse_args(argv) | |
| return args.func(args) | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |