v6.8: Untrack generated VoiceDesign registry, add presets seed
config/voicedesign_voices.json is generated on every container start from the
voice library's designed voices, so it carries the user's own character design
prompts and made the working tree permanently dirty. It is now gitignored.
The 8 bundled vd_* presets were only ever stored in that file, so untracking it
alone would leave a fresh clone with no built-in voices at all. They now live in
config/voicedesign_voices.presets.json, which is tracked:
- generate_voices.py seeds the registry from the presets file when no vd_*
entries survive in the generated one (i.e. on a fresh checkout).
- run_voicedesign_server.py starts from the presets file when the registry does
not exist yet, instead of crashing on the missing path. Hot-reload picks up
the real registry as soon as the voice-clone container writes it.
Also documents the v6.8 engine fixes from 9836261 in the README changelog, which
that commit did not touch.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
9836261d28
commit
6198f15f64
6
.gitignore
vendored
6
.gitignore
vendored
@ -18,6 +18,12 @@ config/faster-qwen3-tts.code-workspace
|
||||
config/voices.json
|
||||
config/voices.json*
|
||||
|
||||
# Generated at container startup from the voice library's designed voices.
|
||||
# Contains the user's own character design prompts, so it stays local; the
|
||||
# bundled vd_* presets live in voicedesign_voices.presets.json, which IS
|
||||
# tracked and seeds this file on a fresh checkout.
|
||||
config/voicedesign_voices.json
|
||||
|
||||
# Converted private audio (M4A → WAV, generated at runtime)
|
||||
config/converted/
|
||||
|
||||
|
||||
@ -344,6 +344,14 @@ The first request after container startup can be slower because CUDA graph captu
|
||||
|
||||
## Changelog
|
||||
|
||||
### v6.8 — 2026-07-29
|
||||
**Fix: Stale Speaker Embeddings and Unregistered Voice Design Voices**
|
||||
- **Stale `.pt` Embeddings:** A speaker embedding is a cache baked from one specific pairing of reference audio and transcript. Re-recording a voice replaced those sources but left the old `.pt` in place, so the server kept cloning from an embedding whose audio tokens no longer matched its transcript — generation ignored the requested text and emitted unrelated filler. `generate_voices.py` now treats an embedding older than its reference audio or transcript as absent and regenerates it.
|
||||
- **Designed Voices Were Never Registered:** Voices created with VoiceDesign were not written into `voicedesign_voices.json` at all, so the server knew only its 8 bundled presets and silently substituted one of them for every custom voice. The registry is now generated from the voice library's `.meta.json` files, and an unknown voice returns 404 instead of a different character.
|
||||
- **Instruct Merging:** A per-request `instruct` is now appended to the voice's own description rather than replacing it, so directing a line's emotion no longer discards the character's identity and re-rolls a new voice.
|
||||
- **VoiceDesign Hot-Reload:** The VoiceDesign server picks up registry changes while running, matching the voice-clone server. Per-voice `temperature`/`top_p`/`top_k` set in the registry are honoured and preserved across regeneration.
|
||||
- **Registry No Longer Tracked:** `config/voicedesign_voices.json` is generated from your own voice library and carries your design prompts, so it is now gitignored. The bundled presets live in the tracked `config/voicedesign_voices.presets.json`, which seeds the registry on a fresh checkout.
|
||||
|
||||
### v6.7 — 2026-06-26
|
||||
**Feature: Native Speed Control and Word-Level Timestamps**
|
||||
- **Speed Parameter:** The `speed` parameter in the OpenAI `SpeechRequest` schema is now fully supported. Audio tempo is natively adjusted using `ffmpeg` without affecting pitch, and works for both streaming and non-streaming responses.
|
||||
|
||||
@ -166,6 +166,10 @@ else:
|
||||
# which hot-reloads this file.
|
||||
|
||||
VOICEDESIGN_OUTPUT = "/config/voicedesign_voices.json"
|
||||
# The generated registry carries the user's own design prompts, so it is not
|
||||
# tracked in git. The bundled vd_* presets are, in this seed file — it is the
|
||||
# only copy of them a fresh clone gets.
|
||||
VOICEDESIGN_PRESETS = "/config/voicedesign_voices.presets.json"
|
||||
VOICEDESIGN_NOTE_PREFIX = "Voice Design:"
|
||||
|
||||
_LANG_BY_FLAG = {
|
||||
@ -175,15 +179,27 @@ _LANG_BY_FLAG = {
|
||||
}
|
||||
|
||||
|
||||
def _build_voicedesign_registry():
|
||||
def _load_json_dict(path):
|
||||
try:
|
||||
with open(VOICEDESIGN_OUTPUT, encoding="utf-8") as f:
|
||||
existing = json.load(f)
|
||||
with open(path, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
existing = {}
|
||||
return {}
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
# Keep the bundled vd_* presets; rebuild every app-managed entry.
|
||||
|
||||
def _build_voicedesign_registry():
|
||||
existing = _load_json_dict(VOICEDESIGN_OUTPUT)
|
||||
|
||||
# Keep the bundled vd_* presets; rebuild every app-managed entry. On a
|
||||
# fresh checkout the registry does not exist yet, so seed the presets from
|
||||
# the tracked file — without them the server has no built-in voices at all.
|
||||
registry = {k: v for k, v in existing.items() if k.startswith("vd_")}
|
||||
if not registry:
|
||||
registry = {
|
||||
k: v for k, v in _load_json_dict(VOICEDESIGN_PRESETS).items()
|
||||
if k.startswith("vd_")
|
||||
}
|
||||
added = 0
|
||||
|
||||
for scan_dir in SCAN_DIRS:
|
||||
|
||||
@ -422,7 +422,20 @@ def main():
|
||||
_load_model_kwargs = args
|
||||
|
||||
voices_file_path = args.voices
|
||||
with open(args.voices) as f:
|
||||
# The registry is generated by the voice-clone container and is not tracked
|
||||
# in git, so on a fresh checkout it may not exist yet. Start from the
|
||||
# bundled presets instead of crashing — hot-reload picks up the real
|
||||
# registry as soon as it appears.
|
||||
startup_file = args.voices
|
||||
if not os.path.exists(startup_file):
|
||||
startup_file = os.path.join(
|
||||
os.path.dirname(args.voices) or ".", "voicedesign_voices.presets.json"
|
||||
)
|
||||
logger.warning(
|
||||
"%s not found, falling back to bundled presets %s",
|
||||
args.voices, startup_file,
|
||||
)
|
||||
with open(startup_file) as f:
|
||||
voices = json.load(f)
|
||||
try:
|
||||
last_voices_mtime = os.path.getmtime(args.voices)
|
||||
@ -430,7 +443,7 @@ def main():
|
||||
last_voices_mtime = 0.0
|
||||
default_voice = next(iter(voices), None)
|
||||
_build_voice_list()
|
||||
logger.info("Loaded %d voices from %s", len(voices), args.voices)
|
||||
logger.info("Loaded %d voices from %s", len(voices), startup_file)
|
||||
|
||||
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user