Merge branch 'main' of https://huggingface.co/spaces/build-small-hackathon/Hackathon-IA-VisualNovel
Browse files- .gitignore +3 -1
- app.py +8 -1
- frontend/index.html +32 -3
- pyproject.toml +2 -0
- uv.lock +189 -1
- visualnovel/config.py +12 -5
- visualnovel/engine.py +16 -1
- visualnovel/orchestrator.py +1 -0
- visualnovel/schemas.py +2 -0
- visualnovel/state.py +1 -0
- visualnovel/tts.py +151 -0
.gitignore
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# Secrets β never commit
|
| 2 |
-
.env
|
| 3 |
.env.local
|
|
|
|
| 4 |
|
| 5 |
# Python
|
| 6 |
__pycache__/
|
|
@@ -29,3 +29,5 @@ memory/
|
|
| 29 |
# macOS
|
| 30 |
.DS_Store
|
| 31 |
.python-version
|
|
|
|
|
|
|
|
|
| 1 |
# Secrets β never commit
|
|
|
|
| 2 |
.env.local
|
| 3 |
+
.env.secret
|
| 4 |
|
| 5 |
# Python
|
| 6 |
__pycache__/
|
|
|
|
| 29 |
# macOS
|
| 30 |
.DS_Store
|
| 31 |
.python-version
|
| 32 |
+
|
| 33 |
+
uv.lock
|
app.py
CHANGED
|
@@ -104,12 +104,19 @@ if not config.USE_MOCK:
|
|
| 104 |
except ImportError:
|
| 105 |
_missing.append("transformers")
|
| 106 |
|
| 107 |
-
if config.IMAGE_BACKEND
|
| 108 |
try:
|
| 109 |
import diffusers # noqa: F401
|
| 110 |
except ImportError:
|
| 111 |
_missing.append("image")
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
if config.LLM_BACKEND == "modal" or config.IMAGE_BACKEND == "modal":
|
| 114 |
try:
|
| 115 |
import modal # noqa: F401
|
|
|
|
| 104 |
except ImportError:
|
| 105 |
_missing.append("transformers")
|
| 106 |
|
| 107 |
+
if config.IMAGE_BACKEND in ("local", "lightning"):
|
| 108 |
try:
|
| 109 |
import diffusers # noqa: F401
|
| 110 |
except ImportError:
|
| 111 |
_missing.append("image")
|
| 112 |
|
| 113 |
+
if config.TTS_BACKEND == "kokoro":
|
| 114 |
+
try:
|
| 115 |
+
import kokoro_onnx # noqa: F401
|
| 116 |
+
import soundfile # noqa: F401
|
| 117 |
+
except ImportError:
|
| 118 |
+
_missing.append("tts")
|
| 119 |
+
|
| 120 |
if config.LLM_BACKEND == "modal" or config.IMAGE_BACKEND == "modal":
|
| 121 |
try:
|
| 122 |
import modal # noqa: F401
|
frontend/index.html
CHANGED
|
@@ -143,6 +143,9 @@
|
|
| 143 |
@keyframes pulse-rec{0%,100%{opacity:1}50%{opacity:.6}}
|
| 144 |
#mic.live{background:var(--rose); animation:pulse-rec 1.2s ease-in-out infinite}
|
| 145 |
#mic.waiting{background:rgba(255,255,255,.12); color:var(--ink-dim); cursor:wait}
|
|
|
|
|
|
|
|
|
|
| 146 |
/* ---- book intro overlay ---- */
|
| 147 |
#intro-book{
|
| 148 |
position:fixed; inset:0; z-index:30; display:flex; align-items:center; justify-content:center;
|
|
@@ -354,6 +357,7 @@
|
|
| 354 |
<input id="say" placeholder="say somethingβ¦" autocomplete="off" />
|
| 355 |
<button id="send">β</button>
|
| 356 |
<button id="mic" class="ghost" title="hold to speak">ποΈ</button>
|
|
|
|
| 357 |
</div>
|
| 358 |
</div>
|
| 359 |
</div>
|
|
@@ -567,6 +571,7 @@ function render(v){
|
|
| 567 |
$("place").textContent = v.place || "";
|
| 568 |
$("speaker").textContent = v.speaker || "";
|
| 569 |
typeOut($("line"), v.dialogue || "");
|
|
|
|
| 570 |
if (v.backdrop_url && v.backdrop_url !== lastBg){
|
| 571 |
const bg = $("backdrop");
|
| 572 |
bg.classList.remove("on");
|
|
@@ -725,6 +730,30 @@ function busy(state){
|
|
| 725 |
document.querySelectorAll(".target-chip").forEach(b => b.disabled = state);
|
| 726 |
}
|
| 727 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
// --- book overlay dismiss ---
|
| 729 |
$("book-close").addEventListener("click", () => {
|
| 730 |
$("intro-book").classList.add("hidden");
|
|
@@ -797,10 +826,10 @@ $("mic").addEventListener("click", async () => {
|
|
| 797 |
try {
|
| 798 |
const file = new File([blob], "speech.webm", { type: "audio/webm" });
|
| 799 |
const { data } = await client.predict("/transcribe", { audio: handle_file(file) });
|
| 800 |
-
micIdle();
|
| 801 |
-
if (data[0]?.text) { $("say").value = data[0].text;
|
| 802 |
} catch (e) { console.error("[stt] transcribe error", e); }
|
| 803 |
-
// fallback: restore input
|
| 804 |
mic.disabled = false; $("say").disabled = false; $("send").disabled = false;
|
| 805 |
};
|
| 806 |
rec.start(100);
|
|
|
|
| 143 |
@keyframes pulse-rec{0%,100%{opacity:1}50%{opacity:.6}}
|
| 144 |
#mic.live{background:var(--rose); animation:pulse-rec 1.2s ease-in-out infinite}
|
| 145 |
#mic.waiting{background:rgba(255,255,255,.12); color:var(--ink-dim); cursor:wait}
|
| 146 |
+
#tts-toggle{min-width:42px; background:transparent; color:var(--ink-dim);
|
| 147 |
+
border:1px solid rgba(255,255,255,.22); font-size:1.1rem}
|
| 148 |
+
#tts-toggle.active{color:var(--gold); border-color:rgba(232,193,114,.5)}
|
| 149 |
/* ---- book intro overlay ---- */
|
| 150 |
#intro-book{
|
| 151 |
position:fixed; inset:0; z-index:30; display:flex; align-items:center; justify-content:center;
|
|
|
|
| 357 |
<input id="say" placeholder="say somethingβ¦" autocomplete="off" />
|
| 358 |
<button id="send">β</button>
|
| 359 |
<button id="mic" class="ghost" title="hold to speak">ποΈ</button>
|
| 360 |
+
<button id="tts-toggle" title="toggle voice output">π</button>
|
| 361 |
</div>
|
| 362 |
</div>
|
| 363 |
</div>
|
|
|
|
| 571 |
$("place").textContent = v.place || "";
|
| 572 |
$("speaker").textContent = v.speaker || "";
|
| 573 |
typeOut($("line"), v.dialogue || "");
|
| 574 |
+
playTTS(v.audio_b64);
|
| 575 |
if (v.backdrop_url && v.backdrop_url !== lastBg){
|
| 576 |
const bg = $("backdrop");
|
| 577 |
bg.classList.remove("on");
|
|
|
|
| 730 |
document.querySelectorAll(".target-chip").forEach(b => b.disabled = state);
|
| 731 |
}
|
| 732 |
|
| 733 |
+
// --- TTS toggle ---
|
| 734 |
+
let ttsEnabled = true;
|
| 735 |
+
const ttsBtn = $("tts-toggle");
|
| 736 |
+
ttsBtn.textContent = "π";
|
| 737 |
+
ttsBtn.classList.add("active");
|
| 738 |
+
ttsBtn.addEventListener("click", () => {
|
| 739 |
+
ttsEnabled = !ttsEnabled;
|
| 740 |
+
ttsBtn.textContent = ttsEnabled ? "π" : "π";
|
| 741 |
+
ttsBtn.classList.toggle("active", ttsEnabled);
|
| 742 |
+
if (!ttsEnabled) { const a = $("vn-audio"); if (a) { a.pause(); a.src = ""; } }
|
| 743 |
+
});
|
| 744 |
+
|
| 745 |
+
function playTTS(audio_b64) {
|
| 746 |
+
if (!ttsEnabled || !audio_b64) return;
|
| 747 |
+
let audio = $("vn-audio");
|
| 748 |
+
if (!audio) {
|
| 749 |
+
audio = document.createElement("audio");
|
| 750 |
+
audio.id = "vn-audio";
|
| 751 |
+
document.body.appendChild(audio);
|
| 752 |
+
}
|
| 753 |
+
audio.src = audio_b64;
|
| 754 |
+
audio.play().catch(() => {}); // swallow autoplay policy errors silently
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
// --- book overlay dismiss ---
|
| 758 |
$("book-close").addEventListener("click", () => {
|
| 759 |
$("intro-book").classList.add("hidden");
|
|
|
|
| 826 |
try {
|
| 827 |
const file = new File([blob], "speech.webm", { type: "audio/webm" });
|
| 828 |
const { data } = await client.predict("/transcribe", { audio: handle_file(file) });
|
| 829 |
+
micIdle(); mic.disabled = false; $("say").disabled = false; $("send").disabled = false;
|
| 830 |
+
if (data[0]?.text) { $("say").value = data[0].text; $("say").focus(); return; }
|
| 831 |
} catch (e) { console.error("[stt] transcribe error", e); }
|
| 832 |
+
// fallback: restore input on error
|
| 833 |
mic.disabled = false; $("say").disabled = false; $("send").disabled = false;
|
| 834 |
};
|
| 835 |
rec.start(100);
|
pyproject.toml
CHANGED
|
@@ -27,11 +27,13 @@ llamacpp = ["llama-cpp-python>=0.3.0"]
|
|
| 27 |
transformers = ["transformers>=4.44", "accelerate>=0.33", "torch>=2.3"]
|
| 28 |
image = ["diffusers>=0.38.0", "transformers>=4.44", "accelerate>=0.33", "torch>=2.3", "safetensors>=0.4", "rembg>=2.0", "peft>=0.11"]
|
| 29 |
stt = ["faster-whisper>=1.0", "sounddevice>=0.5.5"]
|
|
|
|
| 30 |
matting = ["transformers>=4.44", "torch>=2.3"] # BiRefNet for transparent sprites (Phase 2)
|
| 31 |
spaces = ["spaces>=0.30"] # ZeroGPU decorator on HF Spaces
|
| 32 |
modal = ["modal>=1.0"] # cloud GPU via Modal (LLM + painter)
|
| 33 |
dev = ["ruff>=0.6", "pytest>=8.0", "matplotlib>=3.9"]
|
| 34 |
|
|
|
|
| 35 |
[tool.ruff]
|
| 36 |
line-length = 100
|
| 37 |
target-version = "py311"
|
|
|
|
| 27 |
transformers = ["transformers>=4.44", "accelerate>=0.33", "torch>=2.3"]
|
| 28 |
image = ["diffusers>=0.38.0", "transformers>=4.44", "accelerate>=0.33", "torch>=2.3", "safetensors>=0.4", "rembg>=2.0", "peft>=0.11"]
|
| 29 |
stt = ["faster-whisper>=1.0", "sounddevice>=0.5.5"]
|
| 30 |
+
tts = ["kokoro-onnx>=0.3", "soundfile>=0.12"]
|
| 31 |
matting = ["transformers>=4.44", "torch>=2.3"] # BiRefNet for transparent sprites (Phase 2)
|
| 32 |
spaces = ["spaces>=0.30"] # ZeroGPU decorator on HF Spaces
|
| 33 |
modal = ["modal>=1.0"] # cloud GPU via Modal (LLM + painter)
|
| 34 |
dev = ["ruff>=0.6", "pytest>=8.0", "matplotlib>=3.9"]
|
| 35 |
|
| 36 |
+
|
| 37 |
[tool.ruff]
|
| 38 |
line-length = 100
|
| 39 |
target-version = "py311"
|
uv.lock
CHANGED
|
@@ -294,6 +294,15 @@ wheels = [
|
|
| 294 |
{ url = "https://files.pythonhosted.org/packages/5f/af/0a6e1d2a845988039f6c197fa7269b5e9abbe17354fb41cc9d75bb260fcb/av-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a87a42c36e29f75e7dff7281944f2a6876a2c8875e225ccbf6c1ae62748b4caa", size = 22072676, upload-time = "2026-04-18T17:12:31.836Z" },
|
| 295 |
]
|
| 296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
[[package]]
|
| 298 |
name = "brotli"
|
| 299 |
version = "1.2.0"
|
|
@@ -661,6 +670,26 @@ wheels = [
|
|
| 661 |
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
|
| 662 |
]
|
| 663 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
[[package]]
|
| 665 |
name = "ctranslate2"
|
| 666 |
version = "4.7.2"
|
|
@@ -804,6 +833,28 @@ wheels = [
|
|
| 804 |
{ url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" },
|
| 805 |
]
|
| 806 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 807 |
[[package]]
|
| 808 |
name = "fastapi"
|
| 809 |
version = "0.136.3"
|
|
@@ -1165,6 +1216,10 @@ transformers = [
|
|
| 1165 |
{ name = "torch" },
|
| 1166 |
{ name = "transformers" },
|
| 1167 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1168 |
|
| 1169 |
[package.dev-dependencies]
|
| 1170 |
dev = [
|
|
@@ -1180,6 +1235,7 @@ requires-dist = [
|
|
| 1180 |
{ name = "faster-whisper", marker = "extra == 'stt'", specifier = ">=1.0" },
|
| 1181 |
{ name = "gradio", specifier = ">=5.0" },
|
| 1182 |
{ name = "huggingface-hub", specifier = ">=1.17.0" },
|
|
|
|
| 1183 |
{ name = "llama-cpp-python", marker = "extra == 'llamacpp'", specifier = ">=0.3.0" },
|
| 1184 |
{ name = "matplotlib", marker = "extra == 'dev'", specifier = ">=3.9" },
|
| 1185 |
{ name = "modal", marker = "extra == 'modal'", specifier = ">=1.0" },
|
|
@@ -1193,6 +1249,7 @@ requires-dist = [
|
|
| 1193 |
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" },
|
| 1194 |
{ name = "safetensors", marker = "extra == 'image'", specifier = ">=0.4" },
|
| 1195 |
{ name = "sounddevice", marker = "extra == 'stt'", specifier = ">=0.5.5" },
|
|
|
|
| 1196 |
{ name = "spaces", marker = "extra == 'spaces'", specifier = ">=0.30" },
|
| 1197 |
{ name = "torch", marker = "extra == 'image'", specifier = ">=2.3" },
|
| 1198 |
{ name = "torch", marker = "extra == 'matting'", specifier = ">=2.3" },
|
|
@@ -1201,7 +1258,7 @@ requires-dist = [
|
|
| 1201 |
{ name = "transformers", marker = "extra == 'matting'", specifier = ">=4.44" },
|
| 1202 |
{ name = "transformers", marker = "extra == 'transformers'", specifier = ">=4.44" },
|
| 1203 |
]
|
| 1204 |
-
provides-extras = ["llamacpp", "transformers", "image", "stt", "matting", "spaces", "modal", "dev"]
|
| 1205 |
|
| 1206 |
[package.metadata.requires-dev]
|
| 1207 |
dev = [
|
|
@@ -1364,6 +1421,15 @@ wheels = [
|
|
| 1364 |
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
| 1365 |
]
|
| 1366 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1367 |
[[package]]
|
| 1368 |
name = "jinja2"
|
| 1369 |
version = "3.1.6"
|
|
@@ -1376,6 +1442,15 @@ wheels = [
|
|
| 1376 |
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
| 1377 |
]
|
| 1378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1379 |
[[package]]
|
| 1380 |
name = "jsonschema"
|
| 1381 |
version = "4.26.0"
|
|
@@ -1509,6 +1584,30 @@ wheels = [
|
|
| 1509 |
{ url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" },
|
| 1510 |
]
|
| 1511 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1512 |
[[package]]
|
| 1513 |
name = "lazy-loader"
|
| 1514 |
version = "0.5"
|
|
@@ -2337,6 +2436,22 @@ wheels = [
|
|
| 2337 |
{ url = "https://files.pythonhosted.org/packages/e8/b6/f54d676ed93cc2dd2234c3b172ea9c8c3d7d29361e66b1b23dec57a67465/peft-0.19.1-py3-none-any.whl", hash = "sha256:2113f72a81621b5913ef28f9022204c742df111890c5f49d812716a4a301e356", size = 680692, upload-time = "2026-04-16T15:46:42.886Z" },
|
| 2338 |
]
|
| 2339 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2340 |
[[package]]
|
| 2341 |
name = "pillow"
|
| 2342 |
version = "12.2.0"
|
|
@@ -2888,6 +3003,18 @@ wheels = [
|
|
| 2888 |
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
| 2889 |
]
|
| 2890 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2891 |
[[package]]
|
| 2892 |
name = "referencing"
|
| 2893 |
version = "0.37.0"
|
|
@@ -3040,6 +3167,15 @@ wheels = [
|
|
| 3040 |
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
| 3041 |
]
|
| 3042 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3043 |
[[package]]
|
| 3044 |
name = "rich"
|
| 3045 |
version = "15.0.0"
|
|
@@ -3389,6 +3525,19 @@ wheels = [
|
|
| 3389 |
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
|
| 3390 |
]
|
| 3391 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3392 |
[[package]]
|
| 3393 |
name = "semantic-version"
|
| 3394 |
version = "2.10.0"
|
|
@@ -3441,6 +3590,27 @@ wheels = [
|
|
| 3441 |
{ url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" },
|
| 3442 |
]
|
| 3443 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3444 |
[[package]]
|
| 3445 |
name = "spaces"
|
| 3446 |
version = "0.50.2"
|
|
@@ -3495,6 +3665,15 @@ wheels = [
|
|
| 3495 |
{ url = "https://files.pythonhosted.org/packages/57/ea/531a6ea751cbd989da386144810b1b8f529b0aae8c1a9beda8b40966c9c2/synchronicity-0.12.3-py3-none-any.whl", hash = "sha256:e476818cd14102136f41622c619de548f0000c024485fc18521c8fe908ea7574", size = 40982, upload-time = "2026-05-28T12:33:49.125Z" },
|
| 3496 |
]
|
| 3497 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3498 |
[[package]]
|
| 3499 |
name = "tifffile"
|
| 3500 |
version = "2026.3.3"
|
|
@@ -3741,6 +3920,15 @@ wheels = [
|
|
| 3741 |
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
| 3742 |
]
|
| 3743 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3744 |
[[package]]
|
| 3745 |
name = "urllib3"
|
| 3746 |
version = "2.7.0"
|
|
|
|
| 294 |
{ url = "https://files.pythonhosted.org/packages/5f/af/0a6e1d2a845988039f6c197fa7269b5e9abbe17354fb41cc9d75bb260fcb/av-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:a87a42c36e29f75e7dff7281944f2a6876a2c8875e225ccbf6c1ae62748b4caa", size = 22072676, upload-time = "2026-04-18T17:12:31.836Z" },
|
| 295 |
]
|
| 296 |
|
| 297 |
+
[[package]]
|
| 298 |
+
name = "babel"
|
| 299 |
+
version = "2.18.0"
|
| 300 |
+
source = { registry = "https://pypi.org/simple" }
|
| 301 |
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" }
|
| 302 |
+
wheels = [
|
| 303 |
+
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
|
| 304 |
+
]
|
| 305 |
+
|
| 306 |
[[package]]
|
| 307 |
name = "brotli"
|
| 308 |
version = "1.2.0"
|
|
|
|
| 670 |
{ url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" },
|
| 671 |
]
|
| 672 |
|
| 673 |
+
[[package]]
|
| 674 |
+
name = "csvw"
|
| 675 |
+
version = "4.0.0"
|
| 676 |
+
source = { registry = "https://pypi.org/simple" }
|
| 677 |
+
dependencies = [
|
| 678 |
+
{ name = "babel" },
|
| 679 |
+
{ name = "isodate" },
|
| 680 |
+
{ name = "jsonschema" },
|
| 681 |
+
{ name = "language-tags" },
|
| 682 |
+
{ name = "python-dateutil" },
|
| 683 |
+
{ name = "rdflib" },
|
| 684 |
+
{ name = "rfc3986" },
|
| 685 |
+
{ name = "termcolor" },
|
| 686 |
+
{ name = "uritemplate" },
|
| 687 |
+
]
|
| 688 |
+
sdist = { url = "https://files.pythonhosted.org/packages/33/50/ff0bd8c3663dad0dae929775a6078c5bb7f4038d93032369786b1fe6a91c/csvw-4.0.0.tar.gz", hash = "sha256:060e9bedf3c274d0fce6d6f7f892a16cc8c72f39c2bc49b69b5f0858fb2f6217", size = 83025, upload-time = "2026-05-05T06:25:26.787Z" }
|
| 689 |
+
wheels = [
|
| 690 |
+
{ url = "https://files.pythonhosted.org/packages/3a/88/9713d1ecac111742d60e1d9c2c15fec56fd121940f97a73d014dc9a7d521/csvw-4.0.0-py2.py3-none-any.whl", hash = "sha256:df875fcb1505afd15061b5f370268522bf162640de0662a724453dcb4db6a88b", size = 69424, upload-time = "2026-05-05T06:25:24.646Z" },
|
| 691 |
+
]
|
| 692 |
+
|
| 693 |
[[package]]
|
| 694 |
name = "ctranslate2"
|
| 695 |
version = "4.7.2"
|
|
|
|
| 833 |
{ url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" },
|
| 834 |
]
|
| 835 |
|
| 836 |
+
[[package]]
|
| 837 |
+
name = "dlinfo"
|
| 838 |
+
version = "2.0.0"
|
| 839 |
+
source = { registry = "https://pypi.org/simple" }
|
| 840 |
+
sdist = { url = "https://files.pythonhosted.org/packages/85/8e/8f2f94cd40af1b51e8e371a83b385d622170d42f98776441a6118f4dd682/dlinfo-2.0.0.tar.gz", hash = "sha256:88a2bc04f51d01bc604cdc9eb1c3cc0bde89057532ca6a3e71a41f6235433e17", size = 12727, upload-time = "2025-01-16T15:43:10.756Z" }
|
| 841 |
+
wheels = [
|
| 842 |
+
{ url = "https://files.pythonhosted.org/packages/da/90/022c79d6e5e6f843268c10b84d4a021ee3afba0621d3c176d3ff2024bfc8/dlinfo-2.0.0-py3-none-any.whl", hash = "sha256:b32cc18e3ea67c0ca9ca409e5b41eed863bd1363dbc9dd3de90fedf11b61e7bc", size = 3654, upload-time = "2025-01-16T15:43:09.474Z" },
|
| 843 |
+
]
|
| 844 |
+
|
| 845 |
+
[[package]]
|
| 846 |
+
name = "espeakng-loader"
|
| 847 |
+
version = "0.2.4"
|
| 848 |
+
source = { registry = "https://pypi.org/simple" }
|
| 849 |
+
wheels = [
|
| 850 |
+
{ url = "https://files.pythonhosted.org/packages/f8/92/f44ed7f531143c3c6c97d56e2b0f9be8728dc05e18b96d46eb539230ed46/espeakng_loader-0.2.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b77477ae2ddf62a748e04e49714eabb2f3a24f344166200b00539083bd669904", size = 9938387, upload-time = "2025-01-17T01:22:42.064Z" },
|
| 851 |
+
{ url = "https://files.pythonhosted.org/packages/a8/26/258c0cd43b9bc1043301c5f61767d6a6c3b679df82790c9cb43a3277b865/espeakng_loader-0.2.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d27cdca31112226e7299d8562e889d3e38a1e48055c9ee381b45d669072ee59f", size = 9892565, upload-time = "2025-01-17T01:22:40.365Z" },
|
| 852 |
+
{ url = "https://files.pythonhosted.org/packages/de/1e/25ec5ab07528c0fbb215a61800a38eca05c8a99445515a02d7fa5debcb32/espeakng_loader-0.2.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08721baf27d13d461f6be6eed9a65277e70d68234ff484fd8b9897b222cdcb6d", size = 10078484, upload-time = "2025-01-17T01:22:43.373Z" },
|
| 853 |
+
{ url = "https://files.pythonhosted.org/packages/d9/ad/1b768d8daffc2996e07bbcb6f534d8de3202cd75fce1f1c45eced1ce6465/espeakng_loader-0.2.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:d1e798141b46a050cdb75fcf3c17db969bb2c40394f3f4a48910655d547508b9", size = 10037736, upload-time = "2025-01-17T01:22:42.576Z" },
|
| 854 |
+
{ url = "https://files.pythonhosted.org/packages/9d/ed/a3d872fbad4f3a3f3db0e8c31768ab14e77cd77306de16b8b20b1e1df7ea/espeakng_loader-0.2.4-py3-none-win_amd64.whl", hash = "sha256:41f1e08ac9deda2efd1ea9de0b81dab9f5ae3c4b24284f76533d0a7b1dd7abd7", size = 9437292, upload-time = "2025-01-17T01:23:27.463Z" },
|
| 855 |
+
{ url = "https://files.pythonhosted.org/packages/29/64/0b75bc50ec53b4e000bac913625511215aa96124adf5dba8c4baa17c02cd/espeakng_loader-0.2.4-py3-none-win_arm64.whl", hash = "sha256:d7a2928843eaeb2df82f99a370f44e8a630f59b02f9b0d1f168a03c4eeb76b89", size = 9426841, upload-time = "2025-01-17T01:23:21.766Z" },
|
| 856 |
+
]
|
| 857 |
+
|
| 858 |
[[package]]
|
| 859 |
name = "fastapi"
|
| 860 |
version = "0.136.3"
|
|
|
|
| 1216 |
{ name = "torch" },
|
| 1217 |
{ name = "transformers" },
|
| 1218 |
]
|
| 1219 |
+
tts = [
|
| 1220 |
+
{ name = "kokoro-onnx" },
|
| 1221 |
+
{ name = "soundfile" },
|
| 1222 |
+
]
|
| 1223 |
|
| 1224 |
[package.dev-dependencies]
|
| 1225 |
dev = [
|
|
|
|
| 1235 |
{ name = "faster-whisper", marker = "extra == 'stt'", specifier = ">=1.0" },
|
| 1236 |
{ name = "gradio", specifier = ">=5.0" },
|
| 1237 |
{ name = "huggingface-hub", specifier = ">=1.17.0" },
|
| 1238 |
+
{ name = "kokoro-onnx", marker = "extra == 'tts'", specifier = ">=0.3" },
|
| 1239 |
{ name = "llama-cpp-python", marker = "extra == 'llamacpp'", specifier = ">=0.3.0" },
|
| 1240 |
{ name = "matplotlib", marker = "extra == 'dev'", specifier = ">=3.9" },
|
| 1241 |
{ name = "modal", marker = "extra == 'modal'", specifier = ">=1.0" },
|
|
|
|
| 1249 |
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" },
|
| 1250 |
{ name = "safetensors", marker = "extra == 'image'", specifier = ">=0.4" },
|
| 1251 |
{ name = "sounddevice", marker = "extra == 'stt'", specifier = ">=0.5.5" },
|
| 1252 |
+
{ name = "soundfile", marker = "extra == 'tts'", specifier = ">=0.12" },
|
| 1253 |
{ name = "spaces", marker = "extra == 'spaces'", specifier = ">=0.30" },
|
| 1254 |
{ name = "torch", marker = "extra == 'image'", specifier = ">=2.3" },
|
| 1255 |
{ name = "torch", marker = "extra == 'matting'", specifier = ">=2.3" },
|
|
|
|
| 1258 |
{ name = "transformers", marker = "extra == 'matting'", specifier = ">=4.44" },
|
| 1259 |
{ name = "transformers", marker = "extra == 'transformers'", specifier = ">=4.44" },
|
| 1260 |
]
|
| 1261 |
+
provides-extras = ["llamacpp", "transformers", "image", "stt", "tts", "matting", "spaces", "modal", "dev"]
|
| 1262 |
|
| 1263 |
[package.metadata.requires-dev]
|
| 1264 |
dev = [
|
|
|
|
| 1421 |
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
| 1422 |
]
|
| 1423 |
|
| 1424 |
+
[[package]]
|
| 1425 |
+
name = "isodate"
|
| 1426 |
+
version = "0.7.2"
|
| 1427 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1428 |
+
sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" }
|
| 1429 |
+
wheels = [
|
| 1430 |
+
{ url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" },
|
| 1431 |
+
]
|
| 1432 |
+
|
| 1433 |
[[package]]
|
| 1434 |
name = "jinja2"
|
| 1435 |
version = "3.1.6"
|
|
|
|
| 1442 |
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
| 1443 |
]
|
| 1444 |
|
| 1445 |
+
[[package]]
|
| 1446 |
+
name = "joblib"
|
| 1447 |
+
version = "1.5.3"
|
| 1448 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1449 |
+
sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" }
|
| 1450 |
+
wheels = [
|
| 1451 |
+
{ url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" },
|
| 1452 |
+
]
|
| 1453 |
+
|
| 1454 |
[[package]]
|
| 1455 |
name = "jsonschema"
|
| 1456 |
version = "4.26.0"
|
|
|
|
| 1584 |
{ url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" },
|
| 1585 |
]
|
| 1586 |
|
| 1587 |
+
[[package]]
|
| 1588 |
+
name = "kokoro-onnx"
|
| 1589 |
+
version = "0.5.0"
|
| 1590 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1591 |
+
dependencies = [
|
| 1592 |
+
{ name = "espeakng-loader" },
|
| 1593 |
+
{ name = "numpy" },
|
| 1594 |
+
{ name = "onnxruntime" },
|
| 1595 |
+
{ name = "phonemizer-fork" },
|
| 1596 |
+
]
|
| 1597 |
+
sdist = { url = "https://files.pythonhosted.org/packages/6b/18/277bd18aeeceaf5c46a51bb50c1cbdccdad8cab7fd1d58f0173bbeeec708/kokoro_onnx-0.5.0.tar.gz", hash = "sha256:5beb15f085e2828ed8d493f792c079af857103ab2dceaa1e112b1760587ac96a", size = 84570, upload-time = "2026-01-30T03:05:45.6Z" }
|
| 1598 |
+
wheels = [
|
| 1599 |
+
{ url = "https://files.pythonhosted.org/packages/0d/55/0bfcb4aa50033c89e5ac132af3d07fac0543824ce6eaefd4d1bfdcc3795b/kokoro_onnx-0.5.0-py3-none-any.whl", hash = "sha256:4e1c38a296db5dbc1f722f69f5e3c13f2e2877b3e5b145287f56ec057013e357", size = 17448, upload-time = "2026-01-30T03:05:46.931Z" },
|
| 1600 |
+
]
|
| 1601 |
+
|
| 1602 |
+
[[package]]
|
| 1603 |
+
name = "language-tags"
|
| 1604 |
+
version = "1.3.1"
|
| 1605 |
+
source = { registry = "https://pypi.org/simple" }
|
| 1606 |
+
sdist = { url = "https://files.pythonhosted.org/packages/39/d1/b37165bed9016c19e2cdecf4176fbd902a014fb8a87dd44415438e190969/language_tags-1.3.1.tar.gz", hash = "sha256:b15f05505dad3ad296a1782d5a6083fd141309186094d1ab08095348f4203f37", size = 222642, upload-time = "2026-05-08T11:46:24.418Z" }
|
| 1607 |
+
wheels = [
|
| 1608 |
+
{ url = "https://files.pythonhosted.org/packages/0a/21/b48b9e8408b3faec9fb7cb2f68352c7724add35a980c948eaceb29ac41e4/language_tags-1.3.1-py3-none-any.whl", hash = "sha256:f7db7ef8879523019603e09644a86d95ba60595ce5e5ea82d46e8971b0f68f7b", size = 216654, upload-time = "2026-05-08T11:46:17.836Z" },
|
| 1609 |
+
]
|
| 1610 |
+
|
| 1611 |
[[package]]
|
| 1612 |
name = "lazy-loader"
|
| 1613 |
version = "0.5"
|
|
|
|
| 2436 |
{ url = "https://files.pythonhosted.org/packages/e8/b6/f54d676ed93cc2dd2234c3b172ea9c8c3d7d29361e66b1b23dec57a67465/peft-0.19.1-py3-none-any.whl", hash = "sha256:2113f72a81621b5913ef28f9022204c742df111890c5f49d812716a4a301e356", size = 680692, upload-time = "2026-04-16T15:46:42.886Z" },
|
| 2437 |
]
|
| 2438 |
|
| 2439 |
+
[[package]]
|
| 2440 |
+
name = "phonemizer-fork"
|
| 2441 |
+
version = "3.3.2"
|
| 2442 |
+
source = { registry = "https://pypi.org/simple" }
|
| 2443 |
+
dependencies = [
|
| 2444 |
+
{ name = "attrs" },
|
| 2445 |
+
{ name = "dlinfo" },
|
| 2446 |
+
{ name = "joblib" },
|
| 2447 |
+
{ name = "segments" },
|
| 2448 |
+
{ name = "typing-extensions" },
|
| 2449 |
+
]
|
| 2450 |
+
sdist = { url = "https://files.pythonhosted.org/packages/42/fa/9294d2f11890ca49d0bdac7a4da60cbe5686629bfd4987cae0ad75e051cc/phonemizer_fork-3.3.2.tar.gz", hash = "sha256:10e16e827d0443b087062e21b55e805c00989cf1343b2e81e734cae5f6c0cf69", size = 300989, upload-time = "2025-01-30T13:02:31.201Z" }
|
| 2451 |
+
wheels = [
|
| 2452 |
+
{ url = "https://files.pythonhosted.org/packages/64/f1/0dcce21b0ae16a82df4b6583f8f3ad8e55b35f7e98b6bf536a4dd225fa08/phonemizer_fork-3.3.2-py3-none-any.whl", hash = "sha256:97305c76f4183b3825dae8f4c032265fe78c9946ce58c47d4b62161349264b74", size = 82700, upload-time = "2025-01-30T13:02:28.667Z" },
|
| 2453 |
+
]
|
| 2454 |
+
|
| 2455 |
[[package]]
|
| 2456 |
name = "pillow"
|
| 2457 |
version = "12.2.0"
|
|
|
|
| 3003 |
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
| 3004 |
]
|
| 3005 |
|
| 3006 |
+
[[package]]
|
| 3007 |
+
name = "rdflib"
|
| 3008 |
+
version = "7.6.0"
|
| 3009 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3010 |
+
dependencies = [
|
| 3011 |
+
{ name = "pyparsing" },
|
| 3012 |
+
]
|
| 3013 |
+
sdist = { url = "https://files.pythonhosted.org/packages/98/f5/18bb77b7af9526add0c727a3b2048959847dc5fb030913e2918bf384fec3/rdflib-7.6.0.tar.gz", hash = "sha256:6c831288d5e4a5a7ece85d0ccde9877d512a3d0f02d7c06455d00d6d0ea379df", size = 4943826, upload-time = "2026-02-13T07:15:55.938Z" }
|
| 3014 |
+
wheels = [
|
| 3015 |
+
{ url = "https://files.pythonhosted.org/packages/10/c2/6604a71269e0c1bd75656d5a001432d16f2cc5b8c057140ec797155c295e/rdflib-7.6.0-py3-none-any.whl", hash = "sha256:30c0a3ebf4c0e09215f066be7246794b6492e054e782d7ac2a34c9f70a15e0dd", size = 615416, upload-time = "2026-02-13T07:15:46.487Z" },
|
| 3016 |
+
]
|
| 3017 |
+
|
| 3018 |
[[package]]
|
| 3019 |
name = "referencing"
|
| 3020 |
version = "0.37.0"
|
|
|
|
| 3167 |
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
| 3168 |
]
|
| 3169 |
|
| 3170 |
+
[[package]]
|
| 3171 |
+
name = "rfc3986"
|
| 3172 |
+
version = "1.5.0"
|
| 3173 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3174 |
+
sdist = { url = "https://files.pythonhosted.org/packages/79/30/5b1b6c28c105629cc12b33bdcbb0b11b5bb1880c6cfbd955f9e792921aa8/rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835", size = 49378, upload-time = "2021-05-07T23:29:27.183Z" }
|
| 3175 |
+
wheels = [
|
| 3176 |
+
{ url = "https://files.pythonhosted.org/packages/c4/e5/63ca2c4edf4e00657584608bee1001302bbf8c5f569340b78304f2f446cb/rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97", size = 31976, upload-time = "2021-05-07T23:29:25.611Z" },
|
| 3177 |
+
]
|
| 3178 |
+
|
| 3179 |
[[package]]
|
| 3180 |
name = "rich"
|
| 3181 |
version = "15.0.0"
|
|
|
|
| 3525 |
{ url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" },
|
| 3526 |
]
|
| 3527 |
|
| 3528 |
+
[[package]]
|
| 3529 |
+
name = "segments"
|
| 3530 |
+
version = "2.4.0"
|
| 3531 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3532 |
+
dependencies = [
|
| 3533 |
+
{ name = "csvw" },
|
| 3534 |
+
{ name = "regex" },
|
| 3535 |
+
]
|
| 3536 |
+
sdist = { url = "https://files.pythonhosted.org/packages/8b/57/85cac3a8e32370e88fa5fa92812edb6025db7fcbed51452bd56ee1524957/segments-2.4.0.tar.gz", hash = "sha256:bba71f5520ddd54c8aa2f4d765a60618c6862162d6e7356a4a097f2223166f5b", size = 18662, upload-time = "2026-03-07T10:01:28.925Z" }
|
| 3537 |
+
wheels = [
|
| 3538 |
+
{ url = "https://files.pythonhosted.org/packages/be/60/eef9acce946177f92c9aabf432224d87ab908bafafac516a36ab924199f3/segments-2.4.0-py2.py3-none-any.whl", hash = "sha256:4021dc67f201cc03c864c74c618bdb163b1af629da3040babbaa37d8813f3db0", size = 16321, upload-time = "2026-03-07T10:01:27.885Z" },
|
| 3539 |
+
]
|
| 3540 |
+
|
| 3541 |
[[package]]
|
| 3542 |
name = "semantic-version"
|
| 3543 |
version = "2.10.0"
|
|
|
|
| 3590 |
{ url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" },
|
| 3591 |
]
|
| 3592 |
|
| 3593 |
+
[[package]]
|
| 3594 |
+
name = "soundfile"
|
| 3595 |
+
version = "0.14.0"
|
| 3596 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3597 |
+
dependencies = [
|
| 3598 |
+
{ name = "cffi" },
|
| 3599 |
+
{ name = "numpy" },
|
| 3600 |
+
{ name = "typing-extensions" },
|
| 3601 |
+
]
|
| 3602 |
+
sdist = { url = "https://files.pythonhosted.org/packages/d2/db/949331952a6fb1c5b12e9de80fd08747966c2039d1a61db4764fbd3981c2/soundfile-0.14.0.tar.gz", hash = "sha256:ba1c1a2d618bca5c406647c83b89f07cc8810fa506a50622a6993ba130c1de11", size = 47842, upload-time = "2026-06-06T08:58:47.869Z" }
|
| 3603 |
+
wheels = [
|
| 3604 |
+
{ url = "https://files.pythonhosted.org/packages/b1/d1/5e338af9ca6ed0786cd5bb03f6d60de1c325728c1189014f3b59aae7403c/soundfile-0.14.0-py2.py3-none-any.whl", hash = "sha256:8ba81ae3a89fd5ab3bef8a8eb481fbbe794e806309675a89b4df48b8d31908a8", size = 26799, upload-time = "2026-06-06T08:58:33.269Z" },
|
| 3605 |
+
{ url = "https://files.pythonhosted.org/packages/7e/72/c6b21e58d3113596e7e8de0a08d6f1d95173492cfbca0a4db14148cbba2a/soundfile-0.14.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:19be05428da76ed61a4cad29b8e4bcf43a3e5c100089d2ec81dc961eed1b0dd4", size = 1144568, upload-time = "2026-06-06T08:58:35.231Z" },
|
| 3606 |
+
{ url = "https://files.pythonhosted.org/packages/63/7a/dfdd6f8c748988427119f75eb860a3cedd858d1aea1fe28f39ad8559ef22/soundfile-0.14.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d828d35a059626da52f1415b5faee610aeab393319cb3fc4a9aef47b619fc14c", size = 1103726, upload-time = "2026-06-06T08:58:37.948Z" },
|
| 3607 |
+
{ url = "https://files.pythonhosted.org/packages/4a/f8/fc39fad6f879633461d27394cd1ddaf1f769ffa0597dca35872f51b16461/soundfile-0.14.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e85724a90bc99a6e8062c0b4ddf725f53b2a3b70afd4da875e9d2cfc4e92f377", size = 1238050, upload-time = "2026-06-06T08:58:39.932Z" },
|
| 3608 |
+
{ url = "https://files.pythonhosted.org/packages/7b/a2/70fd4432b924684c372df8b0a45708c36c057ef3596c9eb53e0a806b980b/soundfile-0.14.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:1e38bac1853412871318e82a1ba69a8be677619b56025bbfcccdb41b6cafe82d", size = 1315963, upload-time = "2026-06-06T08:58:41.716Z" },
|
| 3609 |
+
{ url = "https://files.pythonhosted.org/packages/d9/34/c9e80783d83eab739a9531fdee03675d53e0bf1b2ccb4bb3af5844675046/soundfile-0.14.0-py2.py3-none-win32.whl", hash = "sha256:0a6ae43c50c71b4e020cc55382925cb89451c1ed1a0c3d0f5d802da269226849", size = 902199, upload-time = "2026-06-06T08:58:43.289Z" },
|
| 3610 |
+
{ url = "https://files.pythonhosted.org/packages/ed/97/b39c18ac1df45e755ca22b8b00e872929da5d107998a207a5e4ac831bfda/soundfile-0.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:299491d3499460fb1b74bb4bd78b57ffc2d243a5fafa7b6ec1b264875c78453e", size = 1021480, upload-time = "2026-06-06T08:58:45.016Z" },
|
| 3611 |
+
{ url = "https://files.pythonhosted.org/packages/f4/83/55c65e61cf457805ce2ec157c1c6ae17715d0851aa2374422de0538838ca/soundfile-0.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:e090704718e124e7c844695236f1fce8d18a5e761eaf7c82dfcd124620805f98", size = 888858, upload-time = "2026-06-06T08:58:46.593Z" },
|
| 3612 |
+
]
|
| 3613 |
+
|
| 3614 |
[[package]]
|
| 3615 |
name = "spaces"
|
| 3616 |
version = "0.50.2"
|
|
|
|
| 3665 |
{ url = "https://files.pythonhosted.org/packages/57/ea/531a6ea751cbd989da386144810b1b8f529b0aae8c1a9beda8b40966c9c2/synchronicity-0.12.3-py3-none-any.whl", hash = "sha256:e476818cd14102136f41622c619de548f0000c024485fc18521c8fe908ea7574", size = 40982, upload-time = "2026-05-28T12:33:49.125Z" },
|
| 3666 |
]
|
| 3667 |
|
| 3668 |
+
[[package]]
|
| 3669 |
+
name = "termcolor"
|
| 3670 |
+
version = "3.3.0"
|
| 3671 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3672 |
+
sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" }
|
| 3673 |
+
wheels = [
|
| 3674 |
+
{ url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" },
|
| 3675 |
+
]
|
| 3676 |
+
|
| 3677 |
[[package]]
|
| 3678 |
name = "tifffile"
|
| 3679 |
version = "2026.3.3"
|
|
|
|
| 3920 |
{ url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" },
|
| 3921 |
]
|
| 3922 |
|
| 3923 |
+
[[package]]
|
| 3924 |
+
name = "uritemplate"
|
| 3925 |
+
version = "4.2.0"
|
| 3926 |
+
source = { registry = "https://pypi.org/simple" }
|
| 3927 |
+
sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" }
|
| 3928 |
+
wheels = [
|
| 3929 |
+
{ url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" },
|
| 3930 |
+
]
|
| 3931 |
+
|
| 3932 |
[[package]]
|
| 3933 |
name = "urllib3"
|
| 3934 |
version = "2.7.0"
|
visualnovel/config.py
CHANGED
|
@@ -44,15 +44,20 @@ USE_MOCK = os.getenv("VN_MOCK", "1") == "1"
|
|
| 44 |
# VN_REAL_STT=1 -> use real Whisper even in mock mode (test STT without full model stack).
|
| 45 |
REAL_STT = os.getenv("VN_REAL_STT", "0") == "1"
|
| 46 |
|
| 47 |
-
#
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
# Image generation backend when not mocking.
|
| 51 |
-
#
|
| 52 |
IMAGE_BACKEND = os.getenv(
|
| 53 |
"VN_IMAGE_BACKEND",
|
| 54 |
-
"modal" if LLM_BACKEND == "modal" else "
|
| 55 |
-
)
|
| 56 |
|
| 57 |
# Set to a .jsonl path to record a full per-turn trace (Open-Trace bonus).
|
| 58 |
TRACE_PATH = os.getenv("VN_TRACE") # None disables tracing
|
|
@@ -76,6 +81,8 @@ WHISPER_SIZE = os.getenv(
|
|
| 76 |
"VN_WHISPER_SIZE", "large-v3-turbo"
|
| 77 |
) # tiny|base|small|medium|large-v3|large-v3-turbo
|
| 78 |
|
|
|
|
|
|
|
| 79 |
# --------------------------------------------------------------------------- #
|
| 80 |
# Generation / budget params
|
| 81 |
# --------------------------------------------------------------------------- #
|
|
|
|
| 44 |
# VN_REAL_STT=1 -> use real Whisper even in mock mode (test STT without full model stack).
|
| 45 |
REAL_STT = os.getenv("VN_REAL_STT", "0") == "1"
|
| 46 |
|
| 47 |
+
# Detect HuggingFace Space runtime β SPACE_ID is always injected by the HF platform.
|
| 48 |
+
_ON_SPACE = bool(os.getenv("SPACE_ID"))
|
| 49 |
+
|
| 50 |
+
# LLM runtime backend when not mocking.
|
| 51 |
+
# Auto-selects transformers on HF Spaces (ZeroGPU); llamacpp everywhere else.
|
| 52 |
+
# Override with VN_LLM_BACKEND=modal for the Modal cloud path.
|
| 53 |
+
LLM_BACKEND = os.getenv("VN_LLM_BACKEND", "transformers" if _ON_SPACE else "llamacpp")
|
| 54 |
|
| 55 |
# Image generation backend when not mocking.
|
| 56 |
+
# "lightning" = SDXL-Lightning (4-step, GPU-local) | "modal" = Modal cloud | "local" = SDXL-Turbo
|
| 57 |
IMAGE_BACKEND = os.getenv(
|
| 58 |
"VN_IMAGE_BACKEND",
|
| 59 |
+
"modal" if LLM_BACKEND == "modal" else "lightning",
|
| 60 |
+
)
|
| 61 |
|
| 62 |
# Set to a .jsonl path to record a full per-turn trace (Open-Trace bonus).
|
| 63 |
TRACE_PATH = os.getenv("VN_TRACE") # None disables tracing
|
|
|
|
| 81 |
"VN_WHISPER_SIZE", "large-v3-turbo"
|
| 82 |
) # tiny|base|small|medium|large-v3|large-v3-turbo
|
| 83 |
|
| 84 |
+
TTS_BACKEND = os.getenv("VN_TTS_BACKEND", "mock") # mock | kokoro
|
| 85 |
+
|
| 86 |
# --------------------------------------------------------------------------- #
|
| 87 |
# Generation / budget params
|
| 88 |
# --------------------------------------------------------------------------- #
|
visualnovel/engine.py
CHANGED
|
@@ -14,6 +14,7 @@ sessions on a busy Space, key engines by session id instead.
|
|
| 14 |
|
| 15 |
from __future__ import annotations
|
| 16 |
|
|
|
|
| 17 |
from pathlib import Path
|
| 18 |
|
| 19 |
from . import config, memory, orchestrator, state
|
|
@@ -23,6 +24,7 @@ from .painter import get_painter
|
|
| 23 |
from .schemas import DirectorOutput, GameState, SetupForm, SpritePresence, Turn, ViewState
|
| 24 |
from .stt import get_stt
|
| 25 |
from .trace import Tracer
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
class Engine:
|
|
@@ -30,6 +32,7 @@ class Engine:
|
|
| 30 |
self.llm = get_llm()
|
| 31 |
self.painter = get_painter()
|
| 32 |
self.stt = get_stt()
|
|
|
|
| 33 |
self.tracer = Tracer(config.TRACE_PATH)
|
| 34 |
self.state: GameState | None = None
|
| 35 |
|
|
@@ -165,7 +168,18 @@ class Engine:
|
|
| 165 |
)
|
| 166 |
)
|
| 167 |
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
return ViewState(
|
| 170 |
speaker=speaker_name,
|
| 171 |
dialogue=out.dialogue,
|
|
@@ -180,6 +194,7 @@ class Engine:
|
|
| 180 |
turn_index=s.turn_index,
|
| 181 |
notifications=notifications or [],
|
| 182 |
intro_text=intro_text,
|
|
|
|
| 183 |
)
|
| 184 |
|
| 185 |
|
|
|
|
| 14 |
|
| 15 |
from __future__ import annotations
|
| 16 |
|
| 17 |
+
import base64
|
| 18 |
from pathlib import Path
|
| 19 |
|
| 20 |
from . import config, memory, orchestrator, state
|
|
|
|
| 24 |
from .schemas import DirectorOutput, GameState, SetupForm, SpritePresence, Turn, ViewState
|
| 25 |
from .stt import get_stt
|
| 26 |
from .trace import Tracer
|
| 27 |
+
from .tts import get_tts
|
| 28 |
|
| 29 |
|
| 30 |
class Engine:
|
|
|
|
| 32 |
self.llm = get_llm()
|
| 33 |
self.painter = get_painter()
|
| 34 |
self.stt = get_stt()
|
| 35 |
+
self.tts = get_tts()
|
| 36 |
self.tracer = Tracer(config.TRACE_PATH)
|
| 37 |
self.state: GameState | None = None
|
| 38 |
|
|
|
|
| 168 |
)
|
| 169 |
)
|
| 170 |
|
| 171 |
+
speaker_ch = s.characters.get(out.speaker)
|
| 172 |
+
speaker_name = speaker_ch.name if speaker_ch else "The wood"
|
| 173 |
+
|
| 174 |
+
audio_b64: str | None = None
|
| 175 |
+
if speaker_ch and speaker_ch.tts_voice_description:
|
| 176 |
+
with collector.measure("tts", turn=turn):
|
| 177 |
+
wav = self.tts.synthesize(
|
| 178 |
+
out.dialogue, speaker_ch.tts_voice_description, speaker_ch.sprite_seed
|
| 179 |
+
)
|
| 180 |
+
if wav:
|
| 181 |
+
audio_b64 = "data:audio/wav;base64," + base64.b64encode(wav).decode()
|
| 182 |
+
|
| 183 |
return ViewState(
|
| 184 |
speaker=speaker_name,
|
| 185 |
dialogue=out.dialogue,
|
|
|
|
| 194 |
turn_index=s.turn_index,
|
| 195 |
notifications=notifications or [],
|
| 196 |
intro_text=intro_text,
|
| 197 |
+
audio_b64=audio_b64,
|
| 198 |
)
|
| 199 |
|
| 200 |
|
visualnovel/orchestrator.py
CHANGED
|
@@ -93,6 +93,7 @@ def init_world(llm: LLMBackend, setup: SetupForm) -> tuple[GameState, DirectorOu
|
|
| 93 |
traits=first.traits,
|
| 94 |
goals=first.goals,
|
| 95 |
sprite_seed=_seed_for(seed, first.id),
|
|
|
|
| 96 |
)
|
| 97 |
},
|
| 98 |
flags=flags,
|
|
|
|
| 93 |
traits=first.traits,
|
| 94 |
goals=first.goals,
|
| 95 |
sprite_seed=_seed_for(seed, first.id),
|
| 96 |
+
tts_voice_description=first.voice, # frozen at creation
|
| 97 |
)
|
| 98 |
},
|
| 99 |
flags=flags,
|
visualnovel/schemas.py
CHANGED
|
@@ -40,6 +40,7 @@ class Character(BaseModel):
|
|
| 40 |
# Discovery tracking (not sent to the LLM β managed by state.apply_directives)
|
| 41 |
discovered_traits: list[int] = Field(default_factory=list) # indices of unlocked traits
|
| 42 |
goal_unlocked: bool = False
|
|
|
|
| 43 |
|
| 44 |
|
| 45 |
class Scene(BaseModel):
|
|
@@ -166,3 +167,4 @@ class ViewState(BaseModel):
|
|
| 166 |
turn_index: int = 0
|
| 167 |
notifications: list[str] = Field(default_factory=list) # popup messages this turn
|
| 168 |
intro_text: str = "" # shown once in the book overlay at game start; empty on subsequent turns
|
|
|
|
|
|
| 40 |
# Discovery tracking (not sent to the LLM β managed by state.apply_directives)
|
| 41 |
discovered_traits: list[int] = Field(default_factory=list) # indices of unlocked traits
|
| 42 |
goal_unlocked: bool = False
|
| 43 |
+
tts_voice_description: str = "" # frozen at first introduction; never updated by directives
|
| 44 |
|
| 45 |
|
| 46 |
class Scene(BaseModel):
|
|
|
|
| 167 |
turn_index: int = 0
|
| 168 |
notifications: list[str] = Field(default_factory=list) # popup messages this turn
|
| 169 |
intro_text: str = "" # shown once in the book overlay at game start; empty on subsequent turns
|
| 170 |
+
audio_b64: str | None = None # data:audio/wav;base64,... β absent when TTS is mocked
|
visualnovel/state.py
CHANGED
|
@@ -63,6 +63,7 @@ def apply_directives(state: GameState, out: DirectorOutput) -> list[str]:
|
|
| 63 |
traits=nc.traits,
|
| 64 |
goals=nc.goals,
|
| 65 |
sprite_seed=_seed_for(state.seed, cid),
|
|
|
|
| 66 |
)
|
| 67 |
if cid not in state.scene.present:
|
| 68 |
state.scene.present.append(cid)
|
|
|
|
| 63 |
traits=nc.traits,
|
| 64 |
goals=nc.goals,
|
| 65 |
sprite_seed=_seed_for(state.seed, cid),
|
| 66 |
+
tts_voice_description=nc.voice, # frozen at creation; never updated
|
| 67 |
)
|
| 68 |
if cid not in state.scene.present:
|
| 69 |
state.scene.present.append(cid)
|
visualnovel/tts.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""The Voice β text to speech.
|
| 2 |
+
|
| 3 |
+
Synthesises NPC dialogue using Kokoro-82M via ONNX (~82M params, no transformers dependency).
|
| 4 |
+
|
| 5 |
+
Design notes:
|
| 6 |
+
- Voice selection reads Character.tts_voice_description (frozen at creation) and scores every
|
| 7 |
+
Kokoro preset by keyword overlap β the best-scoring voice wins. sprite_seed breaks ties so
|
| 8 |
+
two characters with the same description still get different voices.
|
| 9 |
+
- `tts_voice_description` is frozen at character creation (see state.apply_directives and
|
| 10 |
+
orchestrator.init_world), ensuring voice identity is stable for the whole session.
|
| 11 |
+
- Results are cached in memory by SHA-256(text + voice_id + seed) so repeated lines are free.
|
| 12 |
+
|
| 13 |
+
Model files (~337 MB total) live in models/kokoro/ and are downloaded once:
|
| 14 |
+
curl -L -o models/kokoro/kokoro-v1.0.onnx \\
|
| 15 |
+
https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/kokoro-v1.0.onnx
|
| 16 |
+
curl -L -o models/kokoro/voices-v1.0.bin \\
|
| 17 |
+
https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/voices-v1.0.bin
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
|
| 22 |
+
import hashlib
|
| 23 |
+
import io
|
| 24 |
+
from typing import Protocol
|
| 25 |
+
|
| 26 |
+
from . import config
|
| 27 |
+
|
| 28 |
+
# ---------------------------------------------------------------------------
|
| 29 |
+
# Voice profiles β (voice_id, gender, keyword_set)
|
| 30 |
+
# gender: "f" | "m"
|
| 31 |
+
# keyword_set: words in a voice descriptor that suggest this voice
|
| 32 |
+
# ---------------------------------------------------------------------------
|
| 33 |
+
_VOICE_PROFILES: list[tuple[str, str, frozenset[str]]] = [
|
| 34 |
+
# ββ American Female ββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 35 |
+
("af_heart", "f", frozenset({"warm", "bright", "breathless", "tender", "sweet", "laughs", "light"})),
|
| 36 |
+
("af_bella", "f", frozenset({"soft", "gentle", "quiet", "shy", "delicate", "whisper", "whispered"})),
|
| 37 |
+
("af_sarah", "f", frozenset({"clear", "professional", "calm", "steady", "cool", "composed"})),
|
| 38 |
+
("af_sky", "f", frozenset({"airy", "bubbly", "cheerful", "energetic", "lively", "peppy"})),
|
| 39 |
+
("af_jessica", "f", frozenset({"bold", "confident", "sharp", "spirited", "assertive", "direct"})),
|
| 40 |
+
("af_nicole", "f", frozenset({"rich", "smooth", "soothing", "mature", "warm", "velvet"})),
|
| 41 |
+
("af_nova", "f", frozenset({"crisp", "precise", "intelligent", "focused", "analytical"})),
|
| 42 |
+
("af_river", "f", frozenset({"flowing", "peaceful", "serene", "dreamy", "drifts", "trails"})),
|
| 43 |
+
("af_kore", "f", frozenset({"strong", "powerful", "serious", "stern", "commanding"})),
|
| 44 |
+
("af_aoede", "f", frozenset({"melodic", "musical", "lyrical", "poetic", "ethereal", "sings"})),
|
| 45 |
+
("af_alloy", "f", frozenset({"neutral", "balanced", "modern", "even"})),
|
| 46 |
+
# ββ British Female βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 47 |
+
("bf_alice", "f", frozenset({"british", "english", "uk", "posh", "accent", "clipped"})),
|
| 48 |
+
("bf_emma", "f", frozenset({"british", "english", "uk", "homely", "friendly", "warm"})),
|
| 49 |
+
("bf_isabella", "f", frozenset({"british", "english", "uk", "refined", "elegant", "sophisticated"})),
|
| 50 |
+
("bf_lily", "f", frozenset({"british", "english", "uk", "gentle", "soft", "sweet"})),
|
| 51 |
+
# ββ American Male βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 52 |
+
("am_adam", "m", frozenset({"confident", "deep", "steady", "bass", "strong", "rich"})),
|
| 53 |
+
("am_echo", "m", frozenset({"resonant", "full", "commanding", "measured"})),
|
| 54 |
+
("am_eric", "m", frozenset({"friendly", "casual", "warm", "approachable", "cheerful"})),
|
| 55 |
+
("am_fenrir", "m", frozenset({"gruff", "rough", "intense", "dark", "rugged", "growl", "harsh"})),
|
| 56 |
+
("am_liam", "m", frozenset({"young", "energetic", "bright", "enthusiastic", "lively"})),
|
| 57 |
+
("am_michael", "m", frozenset({"mature", "calm", "authoritative", "professorial", "quiet"})),
|
| 58 |
+
("am_onyx", "m", frozenset({"smooth", "velvet", "baritone", "low", "silky", "suave"})),
|
| 59 |
+
("am_puck", "m", frozenset({"playful", "mischievous", "quick", "witty", "impish", "teasing"})),
|
| 60 |
+
# ββ British Male ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 61 |
+
("bm_daniel", "m", frozenset({"british", "english", "uk", "smooth", "refined", "crisp"})),
|
| 62 |
+
("bm_fable", "m", frozenset({"british", "english", "uk", "storyteller", "dramatic", "expressive"})),
|
| 63 |
+
("bm_george", "m", frozenset({"british", "english", "uk", "distinguished", "formal", "noble"})),
|
| 64 |
+
("bm_lewis", "m", frozenset({"british", "english", "uk", "young", "casual", "easy"})),
|
| 65 |
+
]
|
| 66 |
+
|
| 67 |
+
_MALE_GENDER_HINTS = {"man", "male", "his", "him", "he", "boy", "lad", "gentleman"}
|
| 68 |
+
_FEMALE_GENDER_HINTS = {"woman", "female", "her", "she", "girl", "lady"}
|
| 69 |
+
|
| 70 |
+
_MODEL_PATH = config.MODELS_DIR / "kokoro" / "kokoro-v1.0.onnx"
|
| 71 |
+
_VOICES_PATH = config.MODELS_DIR / "kokoro" / "voices-v1.0.bin"
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _pick_voice(voice_description: str, sprite_seed: int) -> str:
|
| 75 |
+
"""Score every Kokoro voice against the description; use sprite_seed to break ties."""
|
| 76 |
+
words = set(voice_description.lower().split())
|
| 77 |
+
|
| 78 |
+
# Detect gender from description
|
| 79 |
+
is_male = bool(words & _MALE_GENDER_HINTS)
|
| 80 |
+
is_female = bool(words & _FEMALE_GENDER_HINTS)
|
| 81 |
+
# If ambiguous, default to female (most anime VN characters)
|
| 82 |
+
target_gender = "m" if is_male and not is_female else "f"
|
| 83 |
+
|
| 84 |
+
candidates = [(vid, kws) for vid, g, kws in _VOICE_PROFILES if g == target_gender]
|
| 85 |
+
|
| 86 |
+
# Score: number of keyword matches, then deterministic tie-break by seed
|
| 87 |
+
scored = [(len(words & kws), vid) for vid, kws in candidates]
|
| 88 |
+
scored.sort(key=lambda x: x[0], reverse=True)
|
| 89 |
+
|
| 90 |
+
# Among voices tied at the top score, pick by sprite_seed
|
| 91 |
+
top_score = scored[0][0]
|
| 92 |
+
top_voices = [vid for score, vid in scored if score == top_score]
|
| 93 |
+
return top_voices[sprite_seed % len(top_voices)]
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
class TTSBackend(Protocol):
|
| 97 |
+
def synthesize(self, text: str, voice_description: str, seed: int) -> bytes | None: ...
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
class MockTTS:
|
| 101 |
+
def synthesize(self, text: str, voice_description: str, seed: int) -> bytes | None:
|
| 102 |
+
return None
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
class KokoroTTS:
|
| 106 |
+
def __init__(self) -> None:
|
| 107 |
+
from kokoro_onnx import Kokoro # noqa: PLC0415
|
| 108 |
+
|
| 109 |
+
if not _MODEL_PATH.exists() or not _VOICES_PATH.exists():
|
| 110 |
+
raise FileNotFoundError(
|
| 111 |
+
f"Kokoro model files missing from {_MODEL_PATH.parent}.\n"
|
| 112 |
+
"Download them with:\n"
|
| 113 |
+
" curl -L -o models/kokoro/kokoro-v1.0.onnx \\\n"
|
| 114 |
+
" https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/kokoro-v1.0.onnx\n"
|
| 115 |
+
" curl -L -o models/kokoro/voices-v1.0.bin \\\n"
|
| 116 |
+
" https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/voices-v1.0.bin"
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
self._kokoro = Kokoro(str(_MODEL_PATH), str(_VOICES_PATH))
|
| 120 |
+
# Filter profiles to voices actually present in the loaded model
|
| 121 |
+
available = set(self._kokoro.get_voices())
|
| 122 |
+
self._available_voices = available
|
| 123 |
+
self._cache: dict[str, bytes] = {}
|
| 124 |
+
|
| 125 |
+
def synthesize(self, text: str, voice_description: str, seed: int) -> bytes | None:
|
| 126 |
+
import soundfile as sf # noqa: PLC0415
|
| 127 |
+
|
| 128 |
+
voice_id = _pick_voice(voice_description, seed)
|
| 129 |
+
# Fallback if the chosen voice isn't in this model build
|
| 130 |
+
if voice_id not in self._available_voices:
|
| 131 |
+
fallback = [vid for vid, g, _ in _VOICE_PROFILES if g == "f" and vid in self._available_voices]
|
| 132 |
+
voice_id = fallback[seed % len(fallback)] if fallback else next(iter(self._available_voices))
|
| 133 |
+
|
| 134 |
+
cache_key = hashlib.sha256(f"{text}\x00{voice_id}\x00{seed}".encode()).hexdigest()
|
| 135 |
+
if cache_key in self._cache:
|
| 136 |
+
return self._cache[cache_key]
|
| 137 |
+
|
| 138 |
+
samples, sample_rate = self._kokoro.create(text, voice=voice_id, speed=1.0, lang="en-us")
|
| 139 |
+
|
| 140 |
+
buf = io.BytesIO()
|
| 141 |
+
sf.write(buf, samples, sample_rate, format="WAV")
|
| 142 |
+
wav_bytes = buf.getvalue()
|
| 143 |
+
|
| 144 |
+
self._cache[cache_key] = wav_bytes
|
| 145 |
+
return wav_bytes
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def get_tts() -> TTSBackend:
|
| 149 |
+
if config.TTS_BACKEND == "kokoro":
|
| 150 |
+
return KokoroTTS()
|
| 151 |
+
return MockTTS()
|