File size: 25,471 Bytes
90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 898b35b 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 271bd15 90f10d2 898b35b 90f10d2 898b35b 90f10d2 898b35b 90f10d2 898b35b 90f10d2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 | """Misc commands: list, info, remove, cleanup, auth, path, doctor."""
from pathlib import Path
import getpass
import json
import os
import sys
import textwrap
import time
import urllib.error
import urllib.request
from vlib import ctx
from vlib.ui import _confirm, _fail, _noninteractive, _note, _ok, _say, _step, _term_width, _trunc, _warn
from vlib.net import _get_hf_token, _hf_auth_headers
from vlib.tensors import read_safetensors
from vlib.sources import _base_paths
from vlib.registry import _iter_voices, _read_meta, _registry_resolve, log_operation
def cmd_list(args):
if getattr(args, "target", None) == "base" or getattr(args, "list_base", False):
_list_bases()
return
voices = [d for d in _iter_voices() if (d / "voice.json").exists()]
if not voices:
_say(" No voices stored yet. Use 'voice get <model>' to download one, or 'voice add <file> <json>' for local files.")
return
tw = _term_width()
w_name = 26 if tw >= 80 else max(14, tw - 54)
w_src = 40 if tw >= 80 else max(18, tw - w_name - 16)
w_type = 8
_say(f" {_trunc('Name', w_name):<{w_name}} {_trunc('Source', w_src):<{w_src}} {_trunc('Type', w_type):<{w_type}}")
_say(f" {'β'*w_name} {'β'*w_src} {'β'*w_type}")
for d in voices:
meta = _read_meta(d / "voice.json") or {}
name = meta.get("name", d.name)
src = meta.get("source_hf_model") or meta.get("source") or "β"
typ = meta.get("type", "voice")
_say(f" {_trunc(name, w_name):<{w_name}} {_trunc(src, w_src):<{w_src}} {_trunc(typ, w_type):<{w_type}}")
def _list_bases():
base_root = ctx.VOICES_DIR / "base"
if not base_root.exists() or not any(base_root.rglob("*.json")):
_say(" No bases cached yet. Use 'voice delta <voice>' to create one.")
return
tw = _term_width()
w_base = 40 if tw >= 80 else max(20, tw - 30)
_say(f" {_trunc('Base', w_base):<{w_base}} {'Bytes':<18} {'Fetched':<12}")
_say(f" {'β'*w_base} {'β'*18} {'β'*12}")
seen = set()
for jpath in sorted(base_root.rglob("*.json")):
try:
meta = json.loads(jpath.read_text())
except Exception:
continue
parts = jpath.relative_to(base_root).parts
base_id = f"{parts[0]}/{Path(parts[1]).stem}" if len(parts) == 2 else f"{parts[0]}/{parts[1]}"
if base_id in seen:
continue
seen.add(base_id)
b = meta.get("bytes")
b_str = f"{b} ({b/1e6:.1f} MB)" if isinstance(b, int) else "β"
fetched = meta.get("downloaded_at", "β")[:10]
_say(f" {_trunc(base_id, w_base):<{w_base}} {_trunc(b_str, 18):<18} {_trunc(fetched, 12):<12}")
def _dec_str(v):
"""Display coercion: GGUF-derived metadata sometimes holds raw byte
lists (e.g. [108, 108, 97, 109, 97]); render them as text, never dumps."""
if v is None:
return None
if isinstance(v, (bytes, bytearray)):
try:
return bytes(v).decode("ascii", "replace")
except Exception:
return str(v)
if isinstance(v, list) and v and all(isinstance(x, int) for x in v):
try:
return bytes(v).decode("ascii", "replace")
except Exception:
return str(v)
return v if isinstance(v, str) else str(v)
def _voice_tensors(meta):
"""Normalize voice metadata to a (tensors, synthetic) pair.
Flat-only files (tensor_name/shape/dtype, no tensors list) become a
synthetic 1-entry list so readers share one code path. Bare-string
entries (hand-written json) are coerced so readers never traceback."""
claims = meta.get("tensors")
if isinstance(claims, list) and claims:
fixed = [({"name": e} if isinstance(e, str) else e) for e in claims]
return [e for e in fixed if isinstance(e, dict)], False
if meta.get("tensor_name"):
return [{"name": meta.get("tensor_name"), "shape": meta.get("shape"),
"dtype": meta.get("dtype")}], True
return [], False
def _verify_voice_file(path, meta):
"""Cross-check voice.json claims against the artifact's own header.
Returns warning strings (empty = agrees). Never raises: an unreadable
file is a note, not a failure."""
out = []
if str(path).endswith(".gguf"):
return out # quant type names differ; nothing truthful to compare
try:
header, tens = read_safetensors(str(path))
except Exception:
return [" ? Could not read the artifact header; showing voice.json as-is."]
names = [k for k in tens if k != "delta.voice.marker"]
claims, _synthetic = _voice_tensors(meta)
if claims:
for entry in claims:
tname = entry.get("name")
if tname not in tens:
out.append(f" ! MISMATCH tensor: voice.json lists '{tname}', file doesn't hold it.")
continue
info = tens[tname]
claimed_shape = entry.get("shape")
if claimed_shape is not None and list(claimed_shape) != list(info["shape"]):
out.append(f" ! MISMATCH shape '{tname}': voice.json says {list(claimed_shape)}, "
f"file holds {list(info['shape'])} β file wins.")
claimed_dtype = (entry.get("dtype") or "").upper()
if claimed_dtype and claimed_dtype != str(info["dtype"]).upper():
out.append(f" ! MISMATCH dtype '{tname}': voice.json says {entry.get('dtype')}, "
f"file holds {info['dtype']} β file wins.")
claimed_names = {e.get("name") for e in claims}
for extra in names:
if extra not in claimed_names:
out.append(f" ! File holds '{extra}' which voice.json doesn't list.")
if not names:
out.append(" ? No tensors found in the artifact.")
has_marker = "delta.voice.marker" in tens
if meta.get("type") == "delta" and not has_marker:
out.append(" ! voice.json claims delta but the file has no delta marker.")
if meta.get("type") != "delta" and has_marker:
out.append(" ! File carries a delta marker but voice.json type is "
f"'{meta.get('type', 'voice')}'.")
return out
# No tensor claims at all (minimal legacy json): only the delta-marker
# check below is truthful; shape/dtype have nothing to compare against.
if not names:
return [" ? No tensors found in the artifact."]
has_marker = "delta.voice.marker" in tens
if meta.get("type") == "delta" and not has_marker:
out.append(" ! voice.json claims delta but the file has no delta marker.")
if meta.get("type") != "delta" and has_marker:
out.append(" ! File carries a delta marker but voice.json type is "
f"'{meta.get('type', 'voice')}'.")
return out
def cmd_info(args):
voicename = args.voicename.strip()
voice_path, voice_meta = _registry_resolve(voicename)
if voice_path is None or not voice_path.exists():
bps = _base_paths(voicename)
if bps and bps[0].exists():
meta = _read_meta(bps[1]) or {}
_say(f" Name: {voicename}")
_say(f" Type: base")
_say(f" Source: https://huggingface.co/{meta.get('source_hf_model', voicename)}")
_say(f" Tensor: {meta.get('tensor_name', 'β')}")
_say(f" Shape: {meta.get('shape', 'β')}")
_say(f" Dtype: {meta.get('dtype', 'β')}")
_say(f" File: {bps[0]}")
return
_fail(f" β Voice '{voicename}' not found. Try: voice list")
meta = _read_meta(Path(voice_path).parent / "voice.json") or voice_meta or {}
name = meta.get("name", voicename)
source = meta.get("source_hf_model") or meta.get("source") or "β"
typ = meta.get("type", "voice")
_say(f" Name: {name}")
if meta.get("family") or meta.get("architecture"):
_arch = _dec_str(meta.get("architecture"))
_fam = _dec_str(meta.get("family"))
_say(f" Family: {_fam or 'β'}" + (f" ({_arch})" if _arch else ""))
tensors, _synthetic = _voice_tensors(meta)
only = tensors[0] if len(tensors) == 1 else {}
if len(tensors) != 1:
for entry in tensors:
_say(f" - {entry.get('name')}: {entry.get('shape')} Β· {entry.get('dtype')}")
else:
_say(f" Tensor: {meta.get('tensor_name') or only.get('name', 'β')}")
_say(f" Shape: {meta.get('shape') or only.get('shape', 'β')}")
_say(f" Dtype: {meta.get('dtype') or only.get('dtype', 'β')}")
b = meta.get("bytes")
if isinstance(b, int):
_say(f" Bytes: {b} ({b/1e6:.1f} MB)")
if meta.get("method"):
_say(f" Method: {meta.get('method')}")
if meta.get("pulse"):
_say(f" Pulse: {meta.get('pulse')}")
if meta.get("donor"):
_say(f" Donor: {meta.get('donor')}")
chain = meta.get("chain")
if isinstance(chain, list) and chain:
_say(f" Chain: {' β '.join(str(c) for c in chain)}")
cfg = meta.get("config")
if isinstance(cfg, dict) and cfg:
knobs = " Β· ".join(f"{k} {cfg[k]}" for k in
("density", "epsilon", "lambda", "rescale", "seed")
if k in cfg)
extra = " ".join(f"{k} {v}" for k, v in cfg.items()
if k not in ("density", "epsilon", "lambda", "rescale", "seed"))
recipe = " Β· ".join(p for p in (knobs, extra) if p)
chash = meta.get("config_hash")
_say(f" Config: {recipe}" + (f" (#{chash})" if chash else ""))
if meta.get("base"):
_say(f" Base: {meta.get('base')}")
if meta.get("license"):
_say(f" License: {_trunc(meta['license'], 80)}")
_say(f" Saved: {meta.get('downloaded_at', meta.get('fetched_at', 'β'))}")
_say(f" File: {voice_path}")
for w in _verify_voice_file(str(voice_path), meta):
_warn(w)
def cmd_remove(args):
name = args.voice.strip().lower()
target = next((d for d in _iter_voices() if d.name.lower() == name), None)
if target is None:
_fail(f" β Voice '{args.voice}' not found. Try: voice list")
if _noninteractive(args) and not getattr(args, "yes", False):
_fail(f" β Can't remove '{target.name}' in non-interactive mode without --yes.")
if not _confirm(f" Remove voice '{target.name}'? [y/N]: ", args):
_say(" Cancelled.")
sys.exit(1)
import shutil
shutil.rmtree(target)
log_operation({"op": "remove", "voice": target.name, "output": str(target), "status": "ok"})
_ok(f"Removed '{target.name}'.")
def cmd_cleanup(args):
parts_dir = ctx.VOICES_DIR / ".parts"
if not parts_dir.exists() or not any(parts_dir.iterdir()):
_say("No incomplete downloads to clean.")
return
removed, reclaimed = 0, 0
import shutil
force = bool(getattr(args, "force", False))
for p in sorted(parts_dir.iterdir()):
try:
if p.is_dir():
lock = p / ".lock"
if lock.exists() and not force:
try:
if time.time() - lock.stat().st_mtime < 600:
_note(f"Skipping {p.name}: download in progress or stalled β use --force to clear")
continue
except Exception:
pass
sz = sum(f.stat().st_size for f in p.rglob("*") if f.is_file())
shutil.rmtree(p)
removed += 1
reclaimed += sz
_ok(f"Removed {p.name}/ ({sz/1e6:.1f} MB)")
else:
sz = p.stat().st_size if p.is_file() else 0
p.unlink()
removed += 1
reclaimed += sz
_ok(f"Removed {p.name}")
except Exception as e:
if ctx.VERBOSE:
_warn(f"Could not remove {p}: {e}")
_ok(f"Cleaned {removed} entry(s), reclaimed {reclaimed/1e6:.1f} MB.")
try:
if not any(parts_dir.iterdir()):
parts_dir.rmdir()
except Exception:
pass
def cmd_auth(args):
tok_path = ctx.HF_TOKEN_PATH
if getattr(args, "clear", False):
if not tok_path.exists():
_say(" No HF token stored β nothing to clear.")
return
try:
tok_path.unlink(missing_ok=True)
_ok("Removed stored HF token.")
except Exception as e:
_fail(f" β Could not remove token: {e}")
return
if getattr(args, "show", False):
tok = _get_hf_token()
if not tok or not tok.startswith("hf_") or len(tok) < 10:
_say(" No valid HF token stored. Run: voice auth")
return
masked = tok[:4] + "Β·" * (len(tok) - 8) + tok[-4:] if len(tok) > 8 else "Β·" * len(tok)
src = "environment" if os.environ.get("HF_TOKEN") or os.environ.get("HF_HUB_TOKEN") else str(tok_path)
_say(f" β HF token present: {masked} ({src})")
return
if not sys.stdin.isatty():
_fail(" β voice auth needs an interactive terminal (stdin is piped).")
_say(" Paste your HF token (hf_β¦): it stays on this machine, 0600, never logged.")
try:
try:
tok = getpass.getpass(" Paste HF token: ").strip()
except Exception:
tok = input(" Paste HF token: ").strip()
except (EOFError, KeyboardInterrupt):
_say(" β Cancelled.")
sys.exit(1)
if not tok:
_say(" β No token entered.")
sys.exit(1)
if not tok.startswith("hf_") or len(tok) < 10:
_fail(" β That doesn't look like an HF token (should start with hf_β¦).")
_step("Verifyingβ¦")
try:
req = urllib.request.Request("https://huggingface.co/api/whoami-v2",
headers=_hf_auth_headers("https://huggingface.co/api/whoami-v2", {"Authorization": f"Bearer {tok}"}))
with urllib.request.urlopen(req, timeout=15) as resp:
data = json.loads(resp.read())
_ok(f"Verified as {data.get('name') or data.get('id') or 'ok'}.")
except urllib.error.HTTPError as e:
if e.code in (401, 403):
_fail(" β Token rejected (401). Check it at https://huggingface.co/settings/tokens")
_warn(f"Verification skipped (HTTP {e.code}), saving anyway.")
except Exception:
_warn("Could not verify now β saving anyway.")
try:
ctx.VOICES_DIR.mkdir(parents=True, exist_ok=True)
try:
os.chmod(ctx.VOICES_DIR, 0o700)
except Exception:
pass
tmp = tok_path.with_suffix(".tmp")
# atomic 0600: create with restricted perms, avoid 0644 race
fd = os.open(str(tmp), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
try:
os.write(fd, (tok.strip() + "\n").encode("utf-8"))
try:
os.fsync(fd)
except Exception:
pass
finally:
os.close(fd)
try:
os.chmod(tmp, 0o600)
except Exception:
pass
tmp.replace(tok_path)
try:
os.chmod(tok_path, 0o600)
except Exception:
pass
_ok(f"Saved to {tok_path} (0600).")
except Exception as e:
_fail(f" β Could not save token: {e}")
def cmd_path(_args):
script = _entry_script()
if script is None:
_fail(" β Cannot locate the voice entry script in this interpreter. Run voice.py directly.")
script = Path(script).resolve()
vlib_src = script.parent / "vlib"
if sys.platform.startswith("win"):
import shutil as _sh
bindir = Path(os.environ.get("LOCALAPPDATA", "")) / "voice-cli"
if str(bindir) == "voice-cli":
_fail(" β Couldn't find a Windows app-data folder to install into.")
bindir.mkdir(parents=True, exist_ok=True)
_copy_tree(vlib_src, bindir / "vlib")
_sh.copy2(str(script), str(bindir / "voice.py"))
if _sh.which("voice"):
_ok("Voice updated on your PATH.")
else:
_ok("Voice is ready. Add this to your PATH (PowerShell):")
_say(f' setx PATH "%PATH%;{bindir}"')
return
import shutil
existing = shutil.which("voice")
if existing:
try:
same = Path(existing).resolve() == script
except Exception:
same = False
if same:
_ok("Voice is already on your PATH.")
return
# Stale install: relink only when the old entry is our symlink
# (points at a voice.py); never touch a foreign binary.
try:
old_target = os.readlink(existing)
except OSError:
_fail(f" β 'voice' on PATH isn't ours ({existing}). Remove or rename it, then retry.")
if Path(old_target).name != "voice.py":
_fail(f" β '{existing}' isn't a voice install (points at {old_target}). Remove it (e.g. sudo mv {existing} {existing}.bak), then retry.")
try:
Path(existing).unlink()
Path(existing).symlink_to(str(script))
except (PermissionError, OSError):
_fail(f" β Couldn't relink {existing}. Try: sudo ln -sf {script} {existing}")
_ok(f"Voice updated: {existing} now points at {script}. Open a new terminal and run: voice")
return
candidates = [Path("/usr/local/bin"), Path.home() / ".local" / "bin", Path("/usr/bin"), Path("/bin")]
on_path = next((d for d in candidates if d.is_dir() and str(d) in os.environ.get("PATH", "").split(":")), None)
if on_path is None:
_fail(" β No writable directory on PATH found. Add one and retry.")
target = on_path / "voice"
try:
target.symlink_to(str(script))
except FileExistsError:
target.unlink()
target.symlink_to(str(script))
except (PermissionError, OSError):
_fail(f" β Couldn't create a link in {on_path}. Try: sudo ln -s {script} {target}")
_ok(f"Voice is ready at {target}. Open a new terminal and run: voice")
def cmd_doctor(_args):
import platform
import shutil
_say(f" voice {ctx.VERSION} β doctor")
_say(f" Python {platform.python_version()} ({sys.executable})")
uv = shutil.which("uv")
_say(f" uv {'found: ' + uv if uv else 'NOT FOUND β curl -LsSf https://astral.sh/uv/install.sh | sh'}")
venv = ctx.VOICES_DIR / ".venv"
if (venv / "bin" / "python").exists():
_say(f" venv {venv} present")
else:
_say(f" venv not created yet (created on first get/cast/delta)")
try:
free = shutil.disk_usage(ctx.VOICES_DIR if ctx.VOICES_DIR.exists() else Path.home()).free
_say(f" disk free {free/1e9:.1f} GB")
except Exception:
pass
tok = _get_hf_token()
_say(f" HF token {'present' if tok else 'not present'}")
if ctx.OPERATIONS_LOG.exists():
try:
n = sum(1 for _ in open(ctx.OPERATIONS_LOG))
_say(f" operations logged: {n} ({ctx.OPERATIONS_LOG})")
except Exception:
pass
if ctx.REPORTS_JSONL.exists():
try:
n = sum(1 for _ in open(ctx.REPORTS_JSONL))
pending = len(list(ctx.REPORTS_OUTBOX.glob("*.json"))) if ctx.REPORTS_OUTBOX.exists() else 0
_say(f" reports: {n} local, {pending} pending in outbox ({ctx.REPORTS_JSONL})")
if pending > 5000:
_warn(f" outbox >5000 pending β run: voice report --sync --yes")
# headroom
try:
sz = ctx.REPORTS_JSONL.stat().st_size
if sz > 50 * 1024 * 1024:
_warn(f" reports.jsonl {sz/1e6:.1f} MB β consider archiving")
except Exception:
pass
except Exception:
pass
else:
_say(f" reports: none yet β run: voice report")
_step("Checking connectivityβ¦")
try:
req = urllib.request.Request("https://huggingface.co/api/whoami-v2" if tok else "https://huggingface.co",
headers=_hf_auth_headers("https://huggingface.co"))
with urllib.request.urlopen(req, timeout=3) as resp:
_say(f" connectivity huggingface.co ok ({resp.status})")
except Exception as e:
_warn(f"connectivity failed: {e}")
# check push target
try:
_say(f" community reports: {ctx.REPORTS_DATASET_ID} (share prompt on voice report)")
except Exception:
pass
def _print_version():
import platform
_say(f" voice {ctx.VERSION}")
_say(f" Python {platform.python_version()} ({sys.executable})")
for mod in ("gguf", "numpy", "huggingface_hub", "hf_xet"):
try:
m = __import__(mod)
_say(f" {mod} {getattr(m, '__version__', 'installed')}")
except Exception:
_say(f" {mod} not installed")
def _print_completion(shell):
cmds = ["get", "cast", "delta", "graft", "chorus", "add", "list", "info", "remove", "cleanup", "auth", "report", "path", "doctor"]
voices = [d.name for d in _iter_voices() if (d / "voice.json").exists()]
vlist = " ".join(voices)
clist = " ".join(cmds)
if shell == "bash":
print(textwrap.dedent(f"""\
_voice_completions() {{
local cur="${{COMP_WORDS[COMP_CWORD]}}"
local cmds="{clist}"
if [[ $COMP_CWORD -eq 1 ]]; then
COMPREPLY=($(compgen -W "$cmds --help --version --completion" -- "$cur"))
return
fi
case "${{COMP_WORDS[1]}}" in
cast|delta|graft|chorus|info|remove) COMPREPLY=($(compgen -W "{vlist}" -- "$cur")) ;;
list) COMPREPLY=($(compgen -W "base --base" -- "$cur")) ;;
*) COMPREPLY=() ;;
esac
}}
complete -F _voice_completions voice
"""))
elif shell == "zsh":
print(textwrap.dedent(f"""\
#compdef voice
_voice() {{
local -a cmds; cmds=({clist})
local -a voices; voices=({vlist})
_arguments -C "1: :($cmds)" "*::arg:->args"
case $line[1] in
cast|delta|graft|chorus|info|remove) _describe voices voices ;;
list) _values "target" base ;;
esac
}}
compdef _voice voice
"""))
elif shell == "fish":
print(textwrap.dedent(f"""\
complete -c voice -f
complete -c voice -n "__fish_use_subcommand" -a "{clist}" -d "voice command"
complete -c voice -n "__fish_seen_subcommand_from cast delta graft chorus info remove" -a "{vlist}"
"""))
def _copy_tree(src, dst):
"""Refresh-copy a directory (Windows PATH install needs vlib/ beside voice.py)."""
import shutil as _sh
if Path(src).is_dir():
_sh.copytree(str(src), str(dst), dirs_exist_ok=True,
ignore=_sh.ignore_patterns("__pycache__"))
return dst
def _entry_script():
"""Absolute path of the CLI entry (voice.py), or None if unknown.
The split moved commands into vlib/, so __file__ here is the module β
resolve via __main__ instead. Single seam for every entry-path need."""
_main = getattr(sys.modules.get("__main__"), "__file__", None)
if not _main:
return None
return os.path.realpath(_main)
def _ensure_venv(cmd):
if os.environ.get("VOICE_NO_VENV") in ("1", "true", "True"):
return
if cmd not in ("get", "cast", "delta"):
return
venv = ctx.VOICES_DIR / ".venv"
venv_py = venv / "bin" / "python"
try:
if venv_py.exists() and (sys.prefix == str(venv) or Path(sys.executable).resolve().parent.parent == venv.resolve()):
return
except Exception:
pass
import shutil
if shutil.which("uv") is None:
_fail(" β uv not found. Install it, then retry:\n curl -LsSf https://astral.sh/uv/install.sh | sh")
if venv_py.exists():
try:
_main = _entry_script()
if not _main:
_warn(" venv re-exec skipped: entry script unknown, continuing in-process.")
return
os.execv(str(venv_py), [str(venv_py), _main, *sys.argv[1:]])
except Exception as e:
if ctx.VERBOSE:
_warn(f"could not re-exec into venv: {e}")
return
_step(f"Creating a virtual environment at {venv}β¦")
import subprocess
try:
subprocess.run(["uv", "venv", str(venv), "--python", sys.executable], capture_output=True, check=True)
subprocess.run(["uv", "pip", "install", "--quiet", "--python", str(venv_py),
"gguf", "numpy", "huggingface_hub[hf_xet]", "hf_xet"], capture_output=True, check=True)
except Exception as e:
_fail(f" β Failed to create venv: {e}")
_main = _entry_script()
if not _main:
_warn(" venv re-exec skipped: entry script unknown, continuing in-process.")
return
try:
os.execv(str(venv_py), [str(venv_py), _main, *sys.argv[1:]])
except Exception as e:
_fail(f" β Could not re-exec into venv: {e}")
|