Download app/services/voice_cache.py from grimshaw/pocket: direct link, hf CLI and curl.
- Browser
- Download file 5.27 kB
-
https://huggingface.co/spaces/grimshaw/pocket/resolve/main/app/services/voice_cache.py
- Command line
-
hf download hf://spaces/grimshaw/pocket/app/services/voice_cache.py
-
curl -L -o voice_cache.py https://huggingface.co/spaces/grimshaw/pocket/resolve/main/app/services/voice_cache.py
5.27 kB
| """Pure filename and path logic for per-model cached voice states. | |
| Kept separate from tts.py so it can be exercised without loading pocket-tts. | |
| """ | |
| import re | |
| from pathlib import Path | |
| from app.config import Config | |
| # Substituted away when sanitizing path-derived tags. Includes filesystem-illegal | |
| # characters AND `.`, since `parse_safetensors_name` uses `.` to delimit | |
| # <stem>.<tag>.safetensors — a tag containing a dot would mis-parse. | |
| _TAG_ILLEGAL_CHARS = re.compile(r'[\\/:*?"<>|.]') | |
| _CONFIG_EXTENSIONS = ('.yaml', '.yml', '.json') | |
| def active_model_tag(raw_model: str) -> str: | |
| """Normalize a language identifier for use as a filename tag. | |
| Handles three input shapes: | |
| - plain language identifier (e.g. 'english_2026-04') → unchanged | |
| - aliased identifier (e.g. 'english') → canonical alias target | |
| - custom --model-path value (e.g. r'C:\\models\\english.yaml') → | |
| reduced to the file stem ('english'), since path separators and | |
| Windows drive colons are illegal in filenames and would crash | |
| safetensors serialization (issue #13). | |
| """ | |
| canonical = Config.LEGACY_MODEL_ALIASES.get(raw_model, raw_model) | |
| # If the value looks like a file path (separators present, or it ends in | |
| # a known config extension), reduce it to a filesystem-safe stem. | |
| if '/' in canonical or '\\' in canonical or canonical.lower().endswith(_CONFIG_EXTENSIONS): | |
| last = canonical.replace('\\', '/').rsplit('/', 1)[-1] | |
| for ext in _CONFIG_EXTENSIONS: | |
| if last.lower().endswith(ext): | |
| last = last[: -len(ext)] | |
| break | |
| canonical = _TAG_ILLEGAL_CHARS.sub('_', last) | |
| return canonical | |
| def known_model_tags() -> set[str]: | |
| """Filename tags we treat as model identifiers during parsing. | |
| Includes both the raw supported languages and the alias-target canonicals. | |
| """ | |
| return set(Config.SUPPORTED_LANGUAGES) | set(Config.LEGACY_MODEL_ALIASES.values()) | |
| def parse_safetensors_name(filename: str, tags: set[str]) -> tuple[str, str | None]: | |
| """Split `stem.model_tag.safetensors` into (stem, tag). | |
| Returns (stem, None) when the filename is unlabeled or the final segment | |
| is not a recognized tag. | |
| """ | |
| base = Path(filename).stem # strips .safetensors | |
| if '.' in base: | |
| stem, tag = base.rsplit('.', 1) | |
| if tag in tags: | |
| return stem, tag | |
| return base, None | |
| def resolve_voice_path( | |
| voice_id: str, | |
| active_model: str, | |
| voices_dir: Path | None, | |
| cache_dir: Path | None, | |
| ) -> Path | str: | |
| """Resolve a voice identifier to its on-disk source. | |
| Preference order (within each directory, the canonical-tagged filename | |
| is checked first; if `active_model` is an alias, the alias-tagged | |
| filename is checked as a fallback so files written under either name | |
| resolve correctly): | |
| 1. cache_dir/<voice_id>.<canonical_tag>.safetensors | |
| 2. cache_dir/<voice_id>.<active_model>.safetensors (alias fallback) | |
| 3. voices_dir/<voice_id>.<canonical_tag>.safetensors | |
| 4. voices_dir/<voice_id>.<active_model>.safetensors (alias fallback) | |
| 5. voices_dir/<voice_id>.{wav,mp3,flac} | |
| 6. voices_dir/<voice_id>.safetensors (legacy unlabeled) | |
| 7. The bare voice_id string — pocket-tts will resolve (e.g. built-ins). | |
| New caches are always written using the canonical tag (see | |
| `_save_cloned_state` in tts.py), so the alias-tagged paths exist only | |
| for files placed by external tools or by users running an older version. | |
| """ | |
| canonical_tag = active_model_tag(active_model) | |
| candidates = [f'{voice_id}.{canonical_tag}.safetensors'] | |
| if active_model != canonical_tag: | |
| candidates.append(f'{voice_id}.{active_model}.safetensors') | |
| for directory in (cache_dir, voices_dir): | |
| if not directory: | |
| continue | |
| for name in candidates: | |
| p = directory / name | |
| if p.exists(): | |
| return p | |
| if voices_dir: | |
| for ext in ('.wav', '.mp3', '.flac'): | |
| p = voices_dir / f'{voice_id}{ext}' | |
| if p.exists(): | |
| return p | |
| p = voices_dir / f'{voice_id}.safetensors' | |
| if p.exists(): | |
| return p | |
| return voice_id | |
| AUDIO_EXTENSIONS = ('.wav', '.mp3', '.flac') | |
| def list_voice_stems( | |
| voices_dir: Path | None, | |
| cache_dir: Path | None, | |
| ) -> list[str]: | |
| """Return the unique voice stems present across voices_dir and cache_dir.""" | |
| stems: set[str] = set() | |
| tags = known_model_tags() | |
| for directory in (voices_dir, cache_dir): | |
| if not directory or not directory.is_dir(): | |
| continue | |
| for ext in AUDIO_EXTENSIONS: | |
| for f in directory.glob(f'*{ext}'): | |
| stems.add(f.stem) | |
| for f in directory.glob('*.safetensors'): | |
| stem, _tag = parse_safetensors_name(f.name, tags) | |
| stems.add(stem) | |
| return sorted(stems) | |
| def cache_is_stale(cache_path: Path, source_path: Path) -> bool: | |
| """True when `source_path` exists and is newer than `cache_path`.""" | |
| if not source_path.exists(): | |
| return False | |
| if not cache_path.exists(): | |
| return False | |
| return source_path.stat().st_mtime > cache_path.stat().st_mtime | |