Instructions to use AGofficial/MyName_RPG with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use AGofficial/MyName_RPG with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf AGofficial/MyName_RPG # Run inference directly in the terminal: llama cli -hf AGofficial/MyName_RPG
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf AGofficial/MyName_RPG # Run inference directly in the terminal: llama cli -hf AGofficial/MyName_RPG
Build from source code
git clone https://github.com/ggml-org/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-app # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama serve -hf AGofficial/MyName_RPG # Run inference directly in the terminal: ./build/bin/llama cli -hf AGofficial/MyName_RPG
Use Docker
docker model run hf.co/AGofficial/MyName_RPG
- Llama new
- LM Studio
- Jan
- Ollama
How to use AGofficial/MyName_RPG with Ollama:
ollama run hf.co/AGofficial/MyName_RPG
- Unsloth Desktop
- Docker Model Runner
How to use AGofficial/MyName_RPG with Docker Model Runner:
docker model run hf.co/AGofficial/MyName_RPG
- Lemonade
How to use AGofficial/MyName_RPG with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull AGofficial/MyName_RPG
Run and chat with the model
lemonade run user.MyName_RPG-{{QUANT_TAG}}List all available models
lemonade list
- Atomic Chat
Upload 96 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +16 -0
- backgrounds/bussiness_room.jpeg +3 -0
- characters/black_haired_girl.png +3 -0
- characters/bussinessman.png +3 -0
- characters/pink_haired_girl.png +3 -0
- dream_chat_engine/README.md +9 -0
- dream_chat_engine/__init__.py +17 -0
- dream_chat_engine/actions.py +68 -0
- dream_chat_engine/assets.py +54 -0
- dream_chat_engine/character.py +17 -0
- dream_chat_engine/config.py +37 -0
- dream_chat_engine/demo.py +38 -0
- dream_chat_engine/dialogue.py +155 -0
- dream_chat_engine/engine.py +399 -0
- dream_chat_engine/renderer.py +272 -0
- kokoro/README.md +117 -0
- kokoro/VOICES.md +161 -0
- kokoro/config.json +150 -0
- kokoro/kokoro-v1_0.pth +3 -0
- kokoro/requirements.txt +5 -0
- kokoro/sample.py +368 -0
- kokoro/voices/af_alloy.pt +3 -0
- kokoro/voices/af_aoede.pt +3 -0
- kokoro/voices/af_bella.pt +3 -0
- kokoro/voices/af_heart.pt +3 -0
- kokoro/voices/af_jessica.pt +3 -0
- kokoro/voices/af_kore.pt +3 -0
- kokoro/voices/af_nicole.pt +3 -0
- kokoro/voices/af_nova.pt +3 -0
- kokoro/voices/af_river.pt +3 -0
- kokoro/voices/af_sarah.pt +3 -0
- kokoro/voices/af_sky.pt +3 -0
- kokoro/voices/am_adam.pt +3 -0
- kokoro/voices/am_echo.pt +3 -0
- kokoro/voices/am_eric.pt +3 -0
- kokoro/voices/am_fenrir.pt +3 -0
- kokoro/voices/am_liam.pt +3 -0
- kokoro/voices/am_michael.pt +3 -0
- kokoro/voices/am_onyx.pt +3 -0
- kokoro/voices/am_puck.pt +3 -0
- kokoro/voices/am_santa.pt +3 -0
- kokoro/voices/bf_alice.pt +3 -0
- kokoro/voices/bf_emma.pt +3 -0
- kokoro/voices/bf_isabella.pt +3 -0
- kokoro/voices/bf_lily.pt +3 -0
- kokoro/voices/bm_daniel.pt +3 -0
- kokoro/voices/bm_fable.pt +3 -0
- kokoro/voices/bm_george.pt +3 -0
- kokoro/voices/bm_lewis.pt +3 -0
- kokoro/voices/ef_dora.pt +3 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,19 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
backgrounds/bussiness_room.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
characters/black_haired_girl.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
characters/bussinessman.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
characters/pink_haired_girl.png filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
llm/dolphin.gguf filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
outputs/dream_chat_engine_preview.jpg filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
outputs/kokoro_sample.wav filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
outputs/kokoro_test.wav filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
outputs/tts/am_michael_1781378852989.wav filter=lfs diff=lfs merge=lfs -text
|
| 45 |
+
outputs/tts/am_michael_1781378862368.wav filter=lfs diff=lfs merge=lfs -text
|
| 46 |
+
outputs/tts/am_michael_1781378874715.wav filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
outputs/tts/am_michael_1781378894011.wav filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
outputs/tts/am_michael_1781378899096.wav filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
outputs/tts/am_michael_1781378909364.wav filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
outputs/tts/am_michael_1781378932377.wav filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
outputs/tts/am_michael_1781379206195.wav filter=lfs diff=lfs merge=lfs -text
|
backgrounds/bussiness_room.jpeg
ADDED
|
Git LFS Details
|
characters/black_haired_girl.png
ADDED
|
Git LFS Details
|
characters/bussinessman.png
ADDED
|
Git LFS Details
|
characters/pink_haired_girl.png
ADDED
|
Git LFS Details
|
dream_chat_engine/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dream Chat Engine
|
| 2 |
+
|
| 3 |
+
A small Python visual chat engine with a RenPy-like script flow.
|
| 4 |
+
|
| 5 |
+
Run the demo from the project root:
|
| 6 |
+
|
| 7 |
+
```powershell
|
| 8 |
+
python dream_chat_engine/demo.py
|
| 9 |
+
```
|
dream_chat_engine/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A tiny RenPy-like visual chat engine for Python games."""
|
| 2 |
+
|
| 3 |
+
from .actions import Ask, Generate, GeneratedReply, Hide, Say, SetBackground, Show
|
| 4 |
+
from .config import EngineConfig
|
| 5 |
+
from .engine import DreamChatEngine
|
| 6 |
+
|
| 7 |
+
__all__ = [
|
| 8 |
+
"Ask",
|
| 9 |
+
"DreamChatEngine",
|
| 10 |
+
"EngineConfig",
|
| 11 |
+
"Generate",
|
| 12 |
+
"GeneratedReply",
|
| 13 |
+
"Hide",
|
| 14 |
+
"Say",
|
| 15 |
+
"SetBackground",
|
| 16 |
+
"Show",
|
| 17 |
+
]
|
dream_chat_engine/actions.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Script actions used by Dream Chat Engine demos and games."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass, field
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import Any, Callable, Iterable, Literal, Sequence
|
| 8 |
+
|
| 9 |
+
AssetPath = str | Path
|
| 10 |
+
Position = Literal["left", "center", "right"] | float
|
| 11 |
+
InputRule = Literal["any", "letters"]
|
| 12 |
+
DialogueHistory = list[dict[str, str]]
|
| 13 |
+
ReplyProducer = Callable[[dict[str, str], DialogueHistory], str | Iterable[str]]
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
@dataclass(frozen=True)
|
| 17 |
+
class SetBackground:
|
| 18 |
+
image: AssetPath
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass(frozen=True)
|
| 22 |
+
class Show:
|
| 23 |
+
name: str
|
| 24 |
+
image: AssetPath
|
| 25 |
+
position: Position = "center"
|
| 26 |
+
tag: str | None = None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
@dataclass(frozen=True)
|
| 30 |
+
class Hide:
|
| 31 |
+
tag: str
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass(frozen=True)
|
| 35 |
+
class Say:
|
| 36 |
+
speaker: str
|
| 37 |
+
text: str
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclass(frozen=True)
|
| 41 |
+
class GeneratedReply:
|
| 42 |
+
text: str
|
| 43 |
+
actions: Sequence[Any] = ()
|
| 44 |
+
variables: dict[str, str] = field(default_factory=dict)
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
ReplyPostprocessor = Callable[
|
| 48 |
+
[str, dict[str, str], DialogueHistory],
|
| 49 |
+
str | GeneratedReply,
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@dataclass(frozen=True)
|
| 54 |
+
class Generate:
|
| 55 |
+
speaker: str
|
| 56 |
+
producer: ReplyProducer
|
| 57 |
+
postprocess: ReplyPostprocessor | None = None
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@dataclass(frozen=True)
|
| 61 |
+
class Ask:
|
| 62 |
+
variable: str
|
| 63 |
+
prompt: str
|
| 64 |
+
speaker: str = "Stranger"
|
| 65 |
+
response_speaker: str | None = None
|
| 66 |
+
max_chars: int | None = None
|
| 67 |
+
input_rule: InputRule = "any"
|
| 68 |
+
min_chars: int = 0
|
dream_chat_engine/assets.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Asset loading helpers for backgrounds and character sprites."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Iterable
|
| 7 |
+
|
| 8 |
+
from PIL import Image
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class AssetNotFoundError(FileNotFoundError):
|
| 12 |
+
"""Raised when an asset cannot be resolved from the project folders."""
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class AssetManager:
|
| 16 |
+
def __init__(self, project_root: str | Path) -> None:
|
| 17 |
+
self.project_root = Path(project_root).resolve()
|
| 18 |
+
self.characters_dir = self.project_root / "characters"
|
| 19 |
+
self.backgrounds_dir = self.project_root / "backgrounds"
|
| 20 |
+
|
| 21 |
+
def load_background(self, asset: str | Path) -> Image.Image:
|
| 22 |
+
path = self.resolve(asset, (self.backgrounds_dir, self.project_root))
|
| 23 |
+
return Image.open(path).convert("RGBA")
|
| 24 |
+
|
| 25 |
+
def load_character(self, asset: str | Path) -> Image.Image:
|
| 26 |
+
path = self.resolve(asset, (self.characters_dir, self.project_root))
|
| 27 |
+
image = Image.open(path).convert("RGBA")
|
| 28 |
+
return self._crop_alpha(image)
|
| 29 |
+
|
| 30 |
+
def resolve(self, asset: str | Path, roots: Iterable[Path]) -> Path:
|
| 31 |
+
asset_path = Path(asset)
|
| 32 |
+
if asset_path.is_absolute() and asset_path.exists():
|
| 33 |
+
return asset_path
|
| 34 |
+
|
| 35 |
+
candidates: list[Path] = []
|
| 36 |
+
for root in roots:
|
| 37 |
+
candidates.append(root / asset_path)
|
| 38 |
+
if asset_path.suffix:
|
| 39 |
+
continue
|
| 40 |
+
candidates.extend(sorted(root.glob(f"{asset_path.name}.*")))
|
| 41 |
+
|
| 42 |
+
for candidate in candidates:
|
| 43 |
+
if candidate.exists() and candidate.is_file():
|
| 44 |
+
return candidate.resolve()
|
| 45 |
+
|
| 46 |
+
searched = ", ".join(str(root) for root in roots)
|
| 47 |
+
raise AssetNotFoundError(f"Could not find asset {asset!r} in {searched}.")
|
| 48 |
+
|
| 49 |
+
@staticmethod
|
| 50 |
+
def _crop_alpha(image: Image.Image) -> Image.Image:
|
| 51 |
+
bbox = image.getbbox()
|
| 52 |
+
if bbox is None:
|
| 53 |
+
return image
|
| 54 |
+
return image.crop(bbox)
|
dream_chat_engine/character.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Runtime character sprite model."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
from .actions import Position
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
@dataclass
|
| 13 |
+
class CharacterSprite:
|
| 14 |
+
tag: str
|
| 15 |
+
name: str
|
| 16 |
+
image: Image.Image
|
| 17 |
+
position: Position = "center"
|
dream_chat_engine/config.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Configuration values for the Dream Chat Engine window and renderer."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class EngineConfig:
|
| 10 |
+
title: str = "Dream Chat Engine"
|
| 11 |
+
width: int = 1280
|
| 12 |
+
height: int = 720
|
| 13 |
+
min_width: int = 800
|
| 14 |
+
min_height: int = 450
|
| 15 |
+
fps: int = 30
|
| 16 |
+
|
| 17 |
+
text_speed: float = 42.0
|
| 18 |
+
max_input_chars: int = 240
|
| 19 |
+
|
| 20 |
+
character_max_height_ratio: float = 0.92
|
| 21 |
+
character_max_width_ratio: float = 0.46
|
| 22 |
+
|
| 23 |
+
dialogue_margin: int = 24
|
| 24 |
+
dialogue_height: int = 188
|
| 25 |
+
dialogue_radius: int = 8
|
| 26 |
+
dialogue_alpha: int = 205
|
| 27 |
+
dialogue_padding_x: int = 30
|
| 28 |
+
dialogue_padding_y: int = 18
|
| 29 |
+
|
| 30 |
+
name_font_size: int = 28
|
| 31 |
+
text_font_size: int = 30
|
| 32 |
+
prompt_font_size: int = 23
|
| 33 |
+
line_spacing: int = 8
|
| 34 |
+
|
| 35 |
+
auto_advance: bool = False
|
| 36 |
+
end_speaker: str = "Demo"
|
| 37 |
+
end_message: str | None = "End of demo."
|
dream_chat_engine/demo.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hardcoded demo game for Dream Chat Engine."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import sys
|
| 7 |
+
|
| 8 |
+
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
| 9 |
+
if str(PROJECT_ROOT) not in sys.path:
|
| 10 |
+
sys.path.insert(0, str(PROJECT_ROOT))
|
| 11 |
+
|
| 12 |
+
from dream_chat_engine import Ask, DreamChatEngine, EngineConfig, Hide, Say, SetBackground, Show
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def build_script():
|
| 16 |
+
return [
|
| 17 |
+
SetBackground("bussiness_room.jpeg"),
|
| 18 |
+
Show("Mira", "pink_haired_girl.png", position="left", tag="mira"),
|
| 19 |
+
Say("Mira", "Hey. You made it. The room, the textbox, and the sprite layer are all live."),
|
| 20 |
+
Show("Mr. Vale", "bussinessman.png", position="right", tag="vale"),
|
| 21 |
+
Say("Mr. Vale", "Characters are loaded from the characters folder and kept flush with the bottom of the screen."),
|
| 22 |
+
Say("Mira", "The text prints itself out, but a click or the space bar can hurry it along."),
|
| 23 |
+
Ask("first_reply", "What do you say back?"),
|
| 24 |
+
Say("Mr. Vale", "Good. That reply was typed freely, stored, and then played as a Stranger line."),
|
| 25 |
+
Hide("vale"),
|
| 26 |
+
Show("Nyx", "black_haired_girl.png", position="center", tag="nyx"),
|
| 27 |
+
Say("Nyx", "That is enough for a first test. Drop more actions into demo.py and it becomes a tiny visual novel."),
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def main() -> None:
|
| 32 |
+
config = EngineConfig(title="Dream Chat Engine Demo")
|
| 33 |
+
engine = DreamChatEngine(build_script(), project_root=PROJECT_ROOT, config=config)
|
| 34 |
+
engine.run()
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
if __name__ == "__main__":
|
| 38 |
+
main()
|
dream_chat_engine/dialogue.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dialogue and keyboard-input state."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from dataclasses import dataclass
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@dataclass
|
| 9 |
+
class DialogueState:
|
| 10 |
+
text_speed: float
|
| 11 |
+
mode: str = "idle"
|
| 12 |
+
speaker: str = ""
|
| 13 |
+
full_text: str = ""
|
| 14 |
+
visible_text: str = ""
|
| 15 |
+
prompt: str = ""
|
| 16 |
+
input_text: str = ""
|
| 17 |
+
caret_index: int = 0
|
| 18 |
+
awaiting_stream: bool = False
|
| 19 |
+
_loading_elapsed: float = 0.0
|
| 20 |
+
_revealed_chars: float = 0.0
|
| 21 |
+
|
| 22 |
+
def start_say(self, speaker: str, text: str) -> None:
|
| 23 |
+
self.mode = "say"
|
| 24 |
+
self.speaker = speaker
|
| 25 |
+
self.full_text = text
|
| 26 |
+
self.visible_text = ""
|
| 27 |
+
self.prompt = ""
|
| 28 |
+
self.input_text = ""
|
| 29 |
+
self.caret_index = 0
|
| 30 |
+
self.awaiting_stream = False
|
| 31 |
+
self._loading_elapsed = 0.0
|
| 32 |
+
self._revealed_chars = 0.0
|
| 33 |
+
|
| 34 |
+
def start_stream(self, speaker: str) -> None:
|
| 35 |
+
self.start_say(speaker, "")
|
| 36 |
+
self.awaiting_stream = True
|
| 37 |
+
|
| 38 |
+
def start_loading(self, speaker: str) -> None:
|
| 39 |
+
self.mode = "loading"
|
| 40 |
+
self.speaker = speaker
|
| 41 |
+
self.full_text = ""
|
| 42 |
+
self.visible_text = "."
|
| 43 |
+
self.prompt = ""
|
| 44 |
+
self.input_text = ""
|
| 45 |
+
self.caret_index = 0
|
| 46 |
+
self.awaiting_stream = True
|
| 47 |
+
self._loading_elapsed = 0.0
|
| 48 |
+
self._revealed_chars = 0.0
|
| 49 |
+
|
| 50 |
+
def start_input(self, speaker: str, prompt: str) -> None:
|
| 51 |
+
self.mode = "input"
|
| 52 |
+
self.speaker = speaker
|
| 53 |
+
self.full_text = ""
|
| 54 |
+
self.visible_text = ""
|
| 55 |
+
self.prompt = prompt
|
| 56 |
+
self.input_text = ""
|
| 57 |
+
self.caret_index = 0
|
| 58 |
+
self.awaiting_stream = False
|
| 59 |
+
self._loading_elapsed = 0.0
|
| 60 |
+
self._revealed_chars = 0.0
|
| 61 |
+
|
| 62 |
+
def update(self, delta_seconds: float) -> None:
|
| 63 |
+
if self.mode == "loading":
|
| 64 |
+
self._loading_elapsed += delta_seconds
|
| 65 |
+
phase = int(self._loading_elapsed / 0.36) % 4
|
| 66 |
+
self.visible_text = "." * phase
|
| 67 |
+
return
|
| 68 |
+
|
| 69 |
+
if self.mode != "say" or self.is_complete:
|
| 70 |
+
return
|
| 71 |
+
|
| 72 |
+
self._revealed_chars = min(
|
| 73 |
+
len(self.full_text),
|
| 74 |
+
self._revealed_chars + self.text_speed * delta_seconds,
|
| 75 |
+
)
|
| 76 |
+
self.visible_text = self.full_text[: int(self._revealed_chars)]
|
| 77 |
+
|
| 78 |
+
@property
|
| 79 |
+
def is_complete(self) -> bool:
|
| 80 |
+
return (
|
| 81 |
+
self.mode == "say"
|
| 82 |
+
and not self.awaiting_stream
|
| 83 |
+
and self.visible_text == self.full_text
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
def reveal_all(self) -> None:
|
| 87 |
+
if self.mode == "say":
|
| 88 |
+
self.visible_text = self.full_text
|
| 89 |
+
self._revealed_chars = float(len(self.full_text))
|
| 90 |
+
|
| 91 |
+
def append_stream_text(self, text: str) -> None:
|
| 92 |
+
if self.mode == "say" and text:
|
| 93 |
+
self.full_text += text
|
| 94 |
+
|
| 95 |
+
def finish_stream(self) -> None:
|
| 96 |
+
self.awaiting_stream = False
|
| 97 |
+
|
| 98 |
+
def insert_text(self, text: str, max_chars: int) -> None:
|
| 99 |
+
if self.mode != "input" or not text:
|
| 100 |
+
return
|
| 101 |
+
allowed = "".join(ch for ch in text if ch >= " " and ch != "\x7f")
|
| 102 |
+
if not allowed:
|
| 103 |
+
return
|
| 104 |
+
remaining = max_chars - len(self.input_text)
|
| 105 |
+
if remaining <= 0:
|
| 106 |
+
return
|
| 107 |
+
allowed = allowed[:remaining]
|
| 108 |
+
self.input_text = (
|
| 109 |
+
self.input_text[: self.caret_index]
|
| 110 |
+
+ allowed
|
| 111 |
+
+ self.input_text[self.caret_index :]
|
| 112 |
+
)
|
| 113 |
+
self.caret_index += len(allowed)
|
| 114 |
+
|
| 115 |
+
def backspace(self) -> None:
|
| 116 |
+
if self.mode == "input" and self.caret_index > 0:
|
| 117 |
+
self.input_text = (
|
| 118 |
+
self.input_text[: self.caret_index - 1]
|
| 119 |
+
+ self.input_text[self.caret_index :]
|
| 120 |
+
)
|
| 121 |
+
self.caret_index -= 1
|
| 122 |
+
|
| 123 |
+
def delete(self) -> None:
|
| 124 |
+
if self.mode == "input" and self.caret_index < len(self.input_text):
|
| 125 |
+
self.input_text = (
|
| 126 |
+
self.input_text[: self.caret_index]
|
| 127 |
+
+ self.input_text[self.caret_index + 1 :]
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
def move_caret(self, amount: int) -> None:
|
| 131 |
+
if self.mode == "input":
|
| 132 |
+
self.caret_index = max(0, min(len(self.input_text), self.caret_index + amount))
|
| 133 |
+
|
| 134 |
+
def move_caret_home(self) -> None:
|
| 135 |
+
if self.mode == "input":
|
| 136 |
+
self.caret_index = 0
|
| 137 |
+
|
| 138 |
+
def move_caret_end(self) -> None:
|
| 139 |
+
if self.mode == "input":
|
| 140 |
+
self.caret_index = len(self.input_text)
|
| 141 |
+
|
| 142 |
+
def display_input_with_caret(self) -> str:
|
| 143 |
+
return (
|
| 144 |
+
self.input_text[: self.caret_index]
|
| 145 |
+
+ "|"
|
| 146 |
+
+ self.input_text[self.caret_index :]
|
| 147 |
+
)
|
| 148 |
+
|
| 149 |
+
def submit_input(self) -> str:
|
| 150 |
+
response = self.input_text.strip()
|
| 151 |
+
self.mode = "idle"
|
| 152 |
+
self.prompt = ""
|
| 153 |
+
self.input_text = ""
|
| 154 |
+
self.caret_index = 0
|
| 155 |
+
return response
|
dream_chat_engine/engine.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Window loop and script runner for Dream Chat Engine."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import queue
|
| 6 |
+
import threading
|
| 7 |
+
import time
|
| 8 |
+
import tkinter as tk
|
| 9 |
+
from collections import OrderedDict
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any, Sequence
|
| 12 |
+
|
| 13 |
+
from PIL import Image, ImageTk
|
| 14 |
+
|
| 15 |
+
from .actions import Ask, Generate, GeneratedReply, Hide, Say, SetBackground, Show
|
| 16 |
+
from .assets import AssetManager
|
| 17 |
+
from .character import CharacterSprite
|
| 18 |
+
from .config import EngineConfig
|
| 19 |
+
from .dialogue import DialogueState
|
| 20 |
+
from .renderer import SceneRenderer
|
| 21 |
+
|
| 22 |
+
ScriptAction = SetBackground | Show | Hide | Say | Generate | Ask
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class DreamChatEngine:
|
| 26 |
+
def __init__(
|
| 27 |
+
self,
|
| 28 |
+
script: Sequence[ScriptAction],
|
| 29 |
+
project_root: str | Path,
|
| 30 |
+
config: EngineConfig | None = None,
|
| 31 |
+
voice_manager: object | None = None,
|
| 32 |
+
) -> None:
|
| 33 |
+
self.script = list(script)
|
| 34 |
+
self.config = config or EngineConfig()
|
| 35 |
+
self.assets = AssetManager(project_root)
|
| 36 |
+
self.dialogue = DialogueState(text_speed=self.config.text_speed)
|
| 37 |
+
self.renderer = SceneRenderer(self.config)
|
| 38 |
+
self.voice_manager = voice_manager
|
| 39 |
+
self.background: Image.Image | None = None
|
| 40 |
+
self.characters: OrderedDict[str, CharacterSprite] = OrderedDict()
|
| 41 |
+
self.variables: dict[str, str] = {}
|
| 42 |
+
self.history: list[dict[str, str]] = []
|
| 43 |
+
|
| 44 |
+
self._index = 0
|
| 45 |
+
self._active_ask: Ask | None = None
|
| 46 |
+
self._generation_queue: queue.Queue[dict[str, Any]] | None = None
|
| 47 |
+
self._generation_thread: threading.Thread | None = None
|
| 48 |
+
self._speech_handle: Any | None = None
|
| 49 |
+
self._demo_finished = False
|
| 50 |
+
self._last_tick = time.perf_counter()
|
| 51 |
+
|
| 52 |
+
self._root: tk.Tk | None = None
|
| 53 |
+
self._canvas: tk.Canvas | None = None
|
| 54 |
+
self._photo: ImageTk.PhotoImage | None = None
|
| 55 |
+
|
| 56 |
+
def run(self) -> None:
|
| 57 |
+
self._root = tk.Tk()
|
| 58 |
+
self._root.title(self.config.title)
|
| 59 |
+
self._root.geometry(f"{self.config.width}x{self.config.height}")
|
| 60 |
+
self._root.minsize(self.config.min_width, self.config.min_height)
|
| 61 |
+
|
| 62 |
+
self._canvas = tk.Canvas(self._root, highlightthickness=0, bg="black")
|
| 63 |
+
self._canvas.pack(fill=tk.BOTH, expand=True)
|
| 64 |
+
self._canvas.focus_set()
|
| 65 |
+
|
| 66 |
+
self._root.bind("<Key>", self._on_key)
|
| 67 |
+
self._root.bind("<Button-1>", self._on_click)
|
| 68 |
+
self._canvas.bind("<Button-1>", self._on_click)
|
| 69 |
+
|
| 70 |
+
self._advance_script()
|
| 71 |
+
self._tick()
|
| 72 |
+
self._root.mainloop()
|
| 73 |
+
|
| 74 |
+
def _tick(self) -> None:
|
| 75 |
+
now = time.perf_counter()
|
| 76 |
+
delta = now - self._last_tick
|
| 77 |
+
self._last_tick = now
|
| 78 |
+
self.dialogue.update(delta)
|
| 79 |
+
self._drain_generation_queue()
|
| 80 |
+
self._maybe_auto_advance()
|
| 81 |
+
self._render()
|
| 82 |
+
|
| 83 |
+
if self._root is not None:
|
| 84 |
+
delay_ms = max(1, int(1000 / self.config.fps))
|
| 85 |
+
self._root.after(delay_ms, self._tick)
|
| 86 |
+
|
| 87 |
+
def _render(self) -> None:
|
| 88 |
+
if self._canvas is None:
|
| 89 |
+
return
|
| 90 |
+
|
| 91 |
+
width = max(1, self._canvas.winfo_width())
|
| 92 |
+
height = max(1, self._canvas.winfo_height())
|
| 93 |
+
if width <= 1 or height <= 1:
|
| 94 |
+
width, height = self.config.width, self.config.height
|
| 95 |
+
|
| 96 |
+
frame = self.renderer.compose(
|
| 97 |
+
(width, height),
|
| 98 |
+
self.background,
|
| 99 |
+
self.characters.values(),
|
| 100 |
+
self.dialogue,
|
| 101 |
+
)
|
| 102 |
+
self._photo = ImageTk.PhotoImage(frame)
|
| 103 |
+
self._canvas.delete("all")
|
| 104 |
+
self._canvas.create_image(0, 0, anchor=tk.NW, image=self._photo)
|
| 105 |
+
|
| 106 |
+
def _advance_script(self) -> None:
|
| 107 |
+
self._active_ask = None
|
| 108 |
+
self._speech_handle = None
|
| 109 |
+
|
| 110 |
+
while self._index < len(self.script):
|
| 111 |
+
action = self.script[self._index]
|
| 112 |
+
self._index += 1
|
| 113 |
+
|
| 114 |
+
if isinstance(action, SetBackground):
|
| 115 |
+
self.background = self.assets.load_background(action.image)
|
| 116 |
+
continue
|
| 117 |
+
|
| 118 |
+
if isinstance(action, Show):
|
| 119 |
+
tag = action.tag or action.name
|
| 120 |
+
self.characters[tag] = CharacterSprite(
|
| 121 |
+
tag=tag,
|
| 122 |
+
name=action.name,
|
| 123 |
+
image=self.assets.load_character(action.image),
|
| 124 |
+
position=action.position,
|
| 125 |
+
)
|
| 126 |
+
continue
|
| 127 |
+
|
| 128 |
+
if isinstance(action, Hide):
|
| 129 |
+
self.characters.pop(action.tag, None)
|
| 130 |
+
continue
|
| 131 |
+
|
| 132 |
+
if isinstance(action, Say):
|
| 133 |
+
speaker = self._format(action.speaker)
|
| 134 |
+
text = self._format(action.text)
|
| 135 |
+
self.dialogue.start_say(speaker, text)
|
| 136 |
+
self._record_line(speaker, text)
|
| 137 |
+
self._speech_handle = self._speak(speaker, text)
|
| 138 |
+
return
|
| 139 |
+
|
| 140 |
+
if isinstance(action, Generate):
|
| 141 |
+
self._start_generated_reply(action)
|
| 142 |
+
return
|
| 143 |
+
|
| 144 |
+
if isinstance(action, Ask):
|
| 145 |
+
self._active_ask = action
|
| 146 |
+
self.dialogue.start_input(
|
| 147 |
+
self._format(action.speaker),
|
| 148 |
+
self._format(action.prompt),
|
| 149 |
+
)
|
| 150 |
+
return
|
| 151 |
+
|
| 152 |
+
if self.config.end_message is not None and not self._demo_finished:
|
| 153 |
+
self._demo_finished = True
|
| 154 |
+
self.dialogue.start_say(self.config.end_speaker, self.config.end_message)
|
| 155 |
+
else:
|
| 156 |
+
self.close()
|
| 157 |
+
|
| 158 |
+
def _on_click(self, _event: tk.Event) -> None:
|
| 159 |
+
if self.dialogue.mode == "input":
|
| 160 |
+
return
|
| 161 |
+
self._continue_or_skip()
|
| 162 |
+
|
| 163 |
+
def _on_key(self, event: tk.Event) -> None:
|
| 164 |
+
keysym = event.keysym
|
| 165 |
+
|
| 166 |
+
if self.dialogue.mode == "input":
|
| 167 |
+
self._handle_input_key(event)
|
| 168 |
+
return
|
| 169 |
+
|
| 170 |
+
if keysym in {"space", "Return"}:
|
| 171 |
+
self._continue_or_skip()
|
| 172 |
+
|
| 173 |
+
def _continue_or_skip(self) -> None:
|
| 174 |
+
if self.config.auto_advance or self.dialogue.mode == "loading":
|
| 175 |
+
return
|
| 176 |
+
if not self._speech_is_done():
|
| 177 |
+
return
|
| 178 |
+
if self.dialogue.mode != "say":
|
| 179 |
+
return
|
| 180 |
+
|
| 181 |
+
if not self.dialogue.is_complete:
|
| 182 |
+
self.dialogue.reveal_all()
|
| 183 |
+
return
|
| 184 |
+
|
| 185 |
+
self._advance_script()
|
| 186 |
+
|
| 187 |
+
def _handle_input_key(self, event: tk.Event) -> None:
|
| 188 |
+
keysym = event.keysym
|
| 189 |
+
|
| 190 |
+
if keysym == "Return":
|
| 191 |
+
self._submit_input()
|
| 192 |
+
elif keysym == "BackSpace":
|
| 193 |
+
self.dialogue.backspace()
|
| 194 |
+
elif keysym == "Delete":
|
| 195 |
+
self.dialogue.delete()
|
| 196 |
+
elif keysym == "Left":
|
| 197 |
+
self.dialogue.move_caret(-1)
|
| 198 |
+
elif keysym == "Right":
|
| 199 |
+
self.dialogue.move_caret(1)
|
| 200 |
+
elif keysym == "Home":
|
| 201 |
+
self.dialogue.move_caret_home()
|
| 202 |
+
elif keysym == "End":
|
| 203 |
+
self.dialogue.move_caret_end()
|
| 204 |
+
elif event.char:
|
| 205 |
+
self._insert_input_text(event.char)
|
| 206 |
+
|
| 207 |
+
def _insert_input_text(self, text: str) -> None:
|
| 208 |
+
max_chars = self.config.max_input_chars
|
| 209 |
+
if self._active_ask is not None and self._active_ask.max_chars is not None:
|
| 210 |
+
max_chars = self._active_ask.max_chars
|
| 211 |
+
|
| 212 |
+
if self._active_ask is not None and self._active_ask.input_rule == "letters":
|
| 213 |
+
text = "".join(ch for ch in text if ch.isalpha())
|
| 214 |
+
|
| 215 |
+
self.dialogue.insert_text(text, max_chars)
|
| 216 |
+
|
| 217 |
+
def _submit_input(self) -> None:
|
| 218 |
+
if self._active_ask is None:
|
| 219 |
+
return
|
| 220 |
+
|
| 221 |
+
response = self.dialogue.submit_input()
|
| 222 |
+
if len(response) < self._active_ask.min_chars:
|
| 223 |
+
self.dialogue.start_input(
|
| 224 |
+
self._format(self._active_ask.speaker),
|
| 225 |
+
self._format(self._active_ask.prompt),
|
| 226 |
+
)
|
| 227 |
+
return
|
| 228 |
+
|
| 229 |
+
if not response:
|
| 230 |
+
response = "..."
|
| 231 |
+
|
| 232 |
+
self.variables[self._active_ask.variable] = response
|
| 233 |
+
response_speaker = self._active_ask.response_speaker or self._active_ask.speaker
|
| 234 |
+
speaker = self._format(response_speaker)
|
| 235 |
+
self.dialogue.start_say(speaker, response)
|
| 236 |
+
self._record_line(speaker, response)
|
| 237 |
+
self._speech_handle = self._speak(speaker, response)
|
| 238 |
+
self._active_ask = None
|
| 239 |
+
|
| 240 |
+
def _start_generated_reply(self, action: Generate) -> None:
|
| 241 |
+
speaker = self._format(action.speaker)
|
| 242 |
+
self.dialogue.start_loading(speaker)
|
| 243 |
+
self._speech_handle = None
|
| 244 |
+
self._generation_queue = queue.Queue()
|
| 245 |
+
|
| 246 |
+
variables = dict(self.variables)
|
| 247 |
+
history = [dict(item) for item in self.history]
|
| 248 |
+
|
| 249 |
+
def worker() -> None:
|
| 250 |
+
try:
|
| 251 |
+
reply = action.producer(variables, history)
|
| 252 |
+
if isinstance(reply, str):
|
| 253 |
+
raw_text = reply
|
| 254 |
+
else:
|
| 255 |
+
raw_text = "".join(str(chunk) for chunk in reply if chunk)
|
| 256 |
+
|
| 257 |
+
generated = self._postprocess_generated(action, raw_text, variables, history)
|
| 258 |
+
prepared_audio = self._prepare_speech(speaker, generated.text)
|
| 259 |
+
self._generation_queue.put(
|
| 260 |
+
{
|
| 261 |
+
"text": generated.text,
|
| 262 |
+
"actions": list(generated.actions),
|
| 263 |
+
"variables": dict(generated.variables),
|
| 264 |
+
"prepared_audio": prepared_audio,
|
| 265 |
+
}
|
| 266 |
+
)
|
| 267 |
+
except Exception as exc:
|
| 268 |
+
self._generation_queue.put(
|
| 269 |
+
{
|
| 270 |
+
"text": f"I must pause. {exc}",
|
| 271 |
+
"actions": [],
|
| 272 |
+
"variables": {},
|
| 273 |
+
"prepared_audio": None,
|
| 274 |
+
}
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
self._generation_thread = threading.Thread(target=worker, daemon=True)
|
| 278 |
+
self._generation_thread.start()
|
| 279 |
+
|
| 280 |
+
def _drain_generation_queue(self) -> None:
|
| 281 |
+
if self._generation_queue is None:
|
| 282 |
+
return
|
| 283 |
+
|
| 284 |
+
try:
|
| 285 |
+
payload = self._generation_queue.get_nowait()
|
| 286 |
+
except queue.Empty:
|
| 287 |
+
return
|
| 288 |
+
|
| 289 |
+
speaker = self.dialogue.speaker
|
| 290 |
+
text = str(payload.get("text") or "").strip()
|
| 291 |
+
if not text:
|
| 292 |
+
text = "We will discuss that in due time."
|
| 293 |
+
|
| 294 |
+
self.variables.update(payload.get("variables") or {})
|
| 295 |
+
self.insert_actions(payload.get("actions") or [])
|
| 296 |
+
|
| 297 |
+
self.dialogue.start_say(speaker, text)
|
| 298 |
+
self._record_line(speaker, text)
|
| 299 |
+
self._speech_handle = self._speak(
|
| 300 |
+
speaker,
|
| 301 |
+
text,
|
| 302 |
+
prepared_audio=payload.get("prepared_audio"),
|
| 303 |
+
)
|
| 304 |
+
self._generation_queue = None
|
| 305 |
+
self._generation_thread = None
|
| 306 |
+
|
| 307 |
+
def _postprocess_generated(
|
| 308 |
+
self,
|
| 309 |
+
action: Generate,
|
| 310 |
+
raw_text: str,
|
| 311 |
+
variables: dict[str, str],
|
| 312 |
+
history: list[dict[str, str]],
|
| 313 |
+
) -> GeneratedReply:
|
| 314 |
+
text = raw_text.strip()
|
| 315 |
+
if action.postprocess is None:
|
| 316 |
+
return GeneratedReply(text=text)
|
| 317 |
+
|
| 318 |
+
processed = action.postprocess(text, variables, history)
|
| 319 |
+
if isinstance(processed, GeneratedReply):
|
| 320 |
+
return processed
|
| 321 |
+
return GeneratedReply(text=str(processed).strip())
|
| 322 |
+
|
| 323 |
+
def insert_actions(self, actions: Sequence[ScriptAction]) -> None:
|
| 324 |
+
if not actions:
|
| 325 |
+
return
|
| 326 |
+
self.script[self._index : self._index] = list(actions)
|
| 327 |
+
|
| 328 |
+
def _maybe_auto_advance(self) -> None:
|
| 329 |
+
if not self.config.auto_advance:
|
| 330 |
+
return
|
| 331 |
+
if self.dialogue.mode != "say":
|
| 332 |
+
return
|
| 333 |
+
if not self.dialogue.is_complete:
|
| 334 |
+
return
|
| 335 |
+
if not self._speech_is_done():
|
| 336 |
+
return
|
| 337 |
+
self._advance_script()
|
| 338 |
+
|
| 339 |
+
def _record_line(self, speaker: str, text: str) -> None:
|
| 340 |
+
self.history.append({"speaker": speaker, "text": text})
|
| 341 |
+
|
| 342 |
+
def _prepare_speech(self, speaker: str, text: str) -> Any | None:
|
| 343 |
+
if self.voice_manager is None:
|
| 344 |
+
return None
|
| 345 |
+
prepare = getattr(self.voice_manager, "prepare", None)
|
| 346 |
+
if prepare is None:
|
| 347 |
+
return None
|
| 348 |
+
try:
|
| 349 |
+
return prepare(speaker, text)
|
| 350 |
+
except Exception:
|
| 351 |
+
return None
|
| 352 |
+
|
| 353 |
+
def _speak(self, speaker: str, text: str, prepared_audio: Any | None = None) -> Any | None:
|
| 354 |
+
if self.voice_manager is None:
|
| 355 |
+
return None
|
| 356 |
+
speak = getattr(self.voice_manager, "speak", None)
|
| 357 |
+
if speak is None:
|
| 358 |
+
return None
|
| 359 |
+
try:
|
| 360 |
+
return speak(speaker, text, prepared_audio=prepared_audio)
|
| 361 |
+
except Exception:
|
| 362 |
+
return None
|
| 363 |
+
|
| 364 |
+
def _speech_is_done(self) -> bool:
|
| 365 |
+
if self._speech_handle is None:
|
| 366 |
+
return True
|
| 367 |
+
is_done = getattr(self._speech_handle, "is_done", None)
|
| 368 |
+
if is_done is not None:
|
| 369 |
+
try:
|
| 370 |
+
return bool(is_done())
|
| 371 |
+
except Exception:
|
| 372 |
+
return True
|
| 373 |
+
done = getattr(self._speech_handle, "done", None)
|
| 374 |
+
if done is not None:
|
| 375 |
+
return bool(done)
|
| 376 |
+
return True
|
| 377 |
+
|
| 378 |
+
def close(self) -> None:
|
| 379 |
+
if self.voice_manager is not None:
|
| 380 |
+
stop = getattr(self.voice_manager, "stop", None)
|
| 381 |
+
if stop is not None:
|
| 382 |
+
try:
|
| 383 |
+
stop()
|
| 384 |
+
except Exception:
|
| 385 |
+
pass
|
| 386 |
+
if self._root is not None:
|
| 387 |
+
self._root.destroy()
|
| 388 |
+
self._root = None
|
| 389 |
+
|
| 390 |
+
def _format(self, value: str) -> str:
|
| 391 |
+
try:
|
| 392 |
+
return value.format_map(_SafeVariables(self.variables))
|
| 393 |
+
except (KeyError, ValueError):
|
| 394 |
+
return value
|
| 395 |
+
|
| 396 |
+
|
| 397 |
+
class _SafeVariables(dict[str, str]):
|
| 398 |
+
def __missing__(self, key: str) -> str:
|
| 399 |
+
return "{" + key + "}"
|
dream_chat_engine/renderer.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pillow renderer used by the Tkinter window."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Iterable
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 9 |
+
|
| 10 |
+
from .actions import Position
|
| 11 |
+
from .character import CharacterSprite
|
| 12 |
+
from .config import EngineConfig
|
| 13 |
+
from .dialogue import DialogueState
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class SceneRenderer:
|
| 17 |
+
def __init__(self, config: EngineConfig) -> None:
|
| 18 |
+
self.config = config
|
| 19 |
+
self._stage_cache_key: tuple[object, ...] | None = None
|
| 20 |
+
self._stage_cache_image: Image.Image | None = None
|
| 21 |
+
self._font_cache: dict[tuple[int, bool], ImageFont.ImageFont] = {}
|
| 22 |
+
|
| 23 |
+
def compose(
|
| 24 |
+
self,
|
| 25 |
+
size: tuple[int, int],
|
| 26 |
+
background: Image.Image | None,
|
| 27 |
+
characters: Iterable[CharacterSprite],
|
| 28 |
+
dialogue: DialogueState,
|
| 29 |
+
) -> Image.Image:
|
| 30 |
+
width, height = size
|
| 31 |
+
frame = self._stage(size, background, tuple(characters)).copy()
|
| 32 |
+
self._draw_dialogue(frame, dialogue)
|
| 33 |
+
return frame.convert("RGB")
|
| 34 |
+
|
| 35 |
+
def _stage(
|
| 36 |
+
self,
|
| 37 |
+
size: tuple[int, int],
|
| 38 |
+
background: Image.Image | None,
|
| 39 |
+
characters: tuple[CharacterSprite, ...],
|
| 40 |
+
) -> Image.Image:
|
| 41 |
+
width, height = size
|
| 42 |
+
key = (
|
| 43 |
+
width,
|
| 44 |
+
height,
|
| 45 |
+
id(background),
|
| 46 |
+
tuple((char.tag, id(char.image), char.position) for char in characters),
|
| 47 |
+
)
|
| 48 |
+
if key == self._stage_cache_key and self._stage_cache_image is not None:
|
| 49 |
+
return self._stage_cache_image
|
| 50 |
+
|
| 51 |
+
frame = Image.new("RGBA", size, (19, 21, 26, 255))
|
| 52 |
+
if background is not None:
|
| 53 |
+
frame.alpha_composite(self._cover(background, size), (0, 0))
|
| 54 |
+
|
| 55 |
+
for character in characters:
|
| 56 |
+
self._paste_character(frame, character)
|
| 57 |
+
|
| 58 |
+
self._stage_cache_key = key
|
| 59 |
+
self._stage_cache_image = frame
|
| 60 |
+
return frame
|
| 61 |
+
|
| 62 |
+
def _cover(self, image: Image.Image, size: tuple[int, int]) -> Image.Image:
|
| 63 |
+
width, height = size
|
| 64 |
+
scale = max(width / image.width, height / image.height)
|
| 65 |
+
new_size = (max(1, int(image.width * scale)), max(1, int(image.height * scale)))
|
| 66 |
+
resized = image.resize(new_size, Image.Resampling.LANCZOS)
|
| 67 |
+
left = (resized.width - width) // 2
|
| 68 |
+
top = (resized.height - height) // 2
|
| 69 |
+
return resized.crop((left, top, left + width, top + height))
|
| 70 |
+
|
| 71 |
+
def _paste_character(self, frame: Image.Image, character: CharacterSprite) -> None:
|
| 72 |
+
width, height = frame.size
|
| 73 |
+
max_height = int(height * self.config.character_max_height_ratio)
|
| 74 |
+
max_width = int(width * self.config.character_max_width_ratio)
|
| 75 |
+
scale = min(max_height / character.image.height, max_width / character.image.width)
|
| 76 |
+
new_size = (
|
| 77 |
+
max(1, int(character.image.width * scale)),
|
| 78 |
+
max(1, int(character.image.height * scale)),
|
| 79 |
+
)
|
| 80 |
+
sprite = character.image.resize(new_size, Image.Resampling.LANCZOS)
|
| 81 |
+
anchor_x = self._position_to_x(character.position, width)
|
| 82 |
+
left = int(anchor_x - sprite.width / 2)
|
| 83 |
+
left = max(-sprite.width // 3, min(width - sprite.width * 2 // 3, left))
|
| 84 |
+
top = height - sprite.height
|
| 85 |
+
frame.alpha_composite(sprite, (left, top))
|
| 86 |
+
|
| 87 |
+
@staticmethod
|
| 88 |
+
def _position_to_x(position: Position, width: int) -> float:
|
| 89 |
+
if isinstance(position, float):
|
| 90 |
+
return width * position
|
| 91 |
+
if position == "left":
|
| 92 |
+
return width * 0.28
|
| 93 |
+
if position == "right":
|
| 94 |
+
return width * 0.72
|
| 95 |
+
return width * 0.5
|
| 96 |
+
|
| 97 |
+
def _draw_dialogue(self, frame: Image.Image, dialogue: DialogueState) -> None:
|
| 98 |
+
if dialogue.mode not in {"say", "input", "loading"}:
|
| 99 |
+
return
|
| 100 |
+
|
| 101 |
+
draw = ImageDraw.Draw(frame, "RGBA")
|
| 102 |
+
width, height = frame.size
|
| 103 |
+
margin = min(self.config.dialogue_margin, max(8, width // 30))
|
| 104 |
+
panel_height = min(self.config.dialogue_height, height - margin * 2)
|
| 105 |
+
left = margin
|
| 106 |
+
top = height - panel_height - margin
|
| 107 |
+
right = width - margin
|
| 108 |
+
bottom = height - margin
|
| 109 |
+
|
| 110 |
+
draw.rounded_rectangle(
|
| 111 |
+
(left, top, right, bottom),
|
| 112 |
+
radius=self.config.dialogue_radius,
|
| 113 |
+
fill=(0, 0, 0, self.config.dialogue_alpha),
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
name_font = self._font(self.config.name_font_size, bold=True)
|
| 117 |
+
text_font = self._font(self.config.text_font_size, bold=False)
|
| 118 |
+
prompt_font = self._font(self.config.prompt_font_size, bold=False)
|
| 119 |
+
pad_x = self.config.dialogue_padding_x
|
| 120 |
+
pad_y = self.config.dialogue_padding_y
|
| 121 |
+
text_left = left + pad_x
|
| 122 |
+
text_right = right - pad_x
|
| 123 |
+
name_y = top + pad_y
|
| 124 |
+
body_y = name_y + self.config.name_font_size + 14
|
| 125 |
+
|
| 126 |
+
if dialogue.speaker:
|
| 127 |
+
draw.text((text_left, name_y), dialogue.speaker, font=name_font, fill=(142, 220, 255, 255))
|
| 128 |
+
|
| 129 |
+
if dialogue.mode == "input":
|
| 130 |
+
if dialogue.prompt:
|
| 131 |
+
prompt_bottom = self._draw_wrapped(
|
| 132 |
+
draw,
|
| 133 |
+
dialogue.prompt,
|
| 134 |
+
prompt_font,
|
| 135 |
+
(232, 237, 243, 235),
|
| 136 |
+
(text_left, body_y, text_right, bottom - 62),
|
| 137 |
+
)
|
| 138 |
+
else:
|
| 139 |
+
prompt_bottom = body_y
|
| 140 |
+
self._draw_wrapped(
|
| 141 |
+
draw,
|
| 142 |
+
dialogue.display_input_with_caret(),
|
| 143 |
+
text_font,
|
| 144 |
+
(255, 255, 255, 255),
|
| 145 |
+
(text_left, prompt_bottom + 12, text_right, bottom - pad_y),
|
| 146 |
+
)
|
| 147 |
+
return
|
| 148 |
+
|
| 149 |
+
if dialogue.mode == "loading":
|
| 150 |
+
self._draw_wrapped(
|
| 151 |
+
draw,
|
| 152 |
+
dialogue.visible_text,
|
| 153 |
+
text_font,
|
| 154 |
+
(255, 255, 255, 255),
|
| 155 |
+
(text_left, body_y, text_right, bottom - pad_y),
|
| 156 |
+
)
|
| 157 |
+
return
|
| 158 |
+
|
| 159 |
+
self._draw_wrapped(
|
| 160 |
+
draw,
|
| 161 |
+
dialogue.visible_text,
|
| 162 |
+
text_font,
|
| 163 |
+
(255, 255, 255, 255),
|
| 164 |
+
(text_left, body_y, text_right, bottom - pad_y),
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
if dialogue.is_complete and not self.config.auto_advance:
|
| 168 |
+
indicator_font = self._font(24, bold=True)
|
| 169 |
+
draw.text((right - 48, bottom - 42), ">", font=indicator_font, fill=(255, 255, 255, 220))
|
| 170 |
+
|
| 171 |
+
def _draw_wrapped(
|
| 172 |
+
self,
|
| 173 |
+
draw: ImageDraw.ImageDraw,
|
| 174 |
+
text: str,
|
| 175 |
+
font: ImageFont.ImageFont,
|
| 176 |
+
fill: tuple[int, int, int, int],
|
| 177 |
+
box: tuple[int, int, int, int],
|
| 178 |
+
) -> int:
|
| 179 |
+
left, top, right, bottom = box
|
| 180 |
+
line_height = self._line_height(font) + self.config.line_spacing
|
| 181 |
+
lines = self._wrap_text(draw, text, font, max(1, right - left))
|
| 182 |
+
max_lines = max(1, (bottom - top) // line_height)
|
| 183 |
+
if len(lines) > max_lines:
|
| 184 |
+
lines = lines[-max_lines:]
|
| 185 |
+
|
| 186 |
+
y = top
|
| 187 |
+
for line in lines:
|
| 188 |
+
if y + line_height > bottom + self.config.line_spacing:
|
| 189 |
+
break
|
| 190 |
+
draw.text((left, y), line, font=font, fill=fill)
|
| 191 |
+
y += line_height
|
| 192 |
+
return y
|
| 193 |
+
|
| 194 |
+
def _wrap_text(
|
| 195 |
+
self,
|
| 196 |
+
draw: ImageDraw.ImageDraw,
|
| 197 |
+
text: str,
|
| 198 |
+
font: ImageFont.ImageFont,
|
| 199 |
+
max_width: int,
|
| 200 |
+
) -> list[str]:
|
| 201 |
+
if not text:
|
| 202 |
+
return [""]
|
| 203 |
+
|
| 204 |
+
lines: list[str] = []
|
| 205 |
+
for raw_line in text.splitlines() or [""]:
|
| 206 |
+
words = raw_line.split(" ")
|
| 207 |
+
current = ""
|
| 208 |
+
for word in words:
|
| 209 |
+
candidate = word if not current else f"{current} {word}"
|
| 210 |
+
if self._text_width(draw, candidate, font) <= max_width:
|
| 211 |
+
current = candidate
|
| 212 |
+
continue
|
| 213 |
+
if current:
|
| 214 |
+
lines.append(current)
|
| 215 |
+
current = self._break_long_word(draw, word, font, max_width, lines)
|
| 216 |
+
lines.append(current)
|
| 217 |
+
return lines
|
| 218 |
+
|
| 219 |
+
def _break_long_word(
|
| 220 |
+
self,
|
| 221 |
+
draw: ImageDraw.ImageDraw,
|
| 222 |
+
word: str,
|
| 223 |
+
font: ImageFont.ImageFont,
|
| 224 |
+
max_width: int,
|
| 225 |
+
lines: list[str],
|
| 226 |
+
) -> str:
|
| 227 |
+
current = ""
|
| 228 |
+
for char in word:
|
| 229 |
+
candidate = current + char
|
| 230 |
+
if self._text_width(draw, candidate, font) <= max_width:
|
| 231 |
+
current = candidate
|
| 232 |
+
else:
|
| 233 |
+
if current:
|
| 234 |
+
lines.append(current)
|
| 235 |
+
current = char
|
| 236 |
+
return current
|
| 237 |
+
|
| 238 |
+
@staticmethod
|
| 239 |
+
def _text_width(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont) -> int:
|
| 240 |
+
bbox = draw.textbbox((0, 0), text, font=font)
|
| 241 |
+
return bbox[2] - bbox[0]
|
| 242 |
+
|
| 243 |
+
@staticmethod
|
| 244 |
+
def _line_height(font: ImageFont.ImageFont) -> int:
|
| 245 |
+
bbox = font.getbbox("Ag")
|
| 246 |
+
return bbox[3] - bbox[1]
|
| 247 |
+
|
| 248 |
+
def _font(self, size: int, bold: bool) -> ImageFont.ImageFont:
|
| 249 |
+
key = (size, bold)
|
| 250 |
+
if key in self._font_cache:
|
| 251 |
+
return self._font_cache[key]
|
| 252 |
+
|
| 253 |
+
font_names = (
|
| 254 |
+
("segoeuib.ttf", "arialbd.ttf") if bold else ("segoeui.ttf", "arial.ttf")
|
| 255 |
+
)
|
| 256 |
+
candidates: list[str | Path] = []
|
| 257 |
+
windows_fonts = Path("C:/Windows/Fonts")
|
| 258 |
+
for name in font_names:
|
| 259 |
+
candidates.append(name)
|
| 260 |
+
candidates.append(windows_fonts / name)
|
| 261 |
+
|
| 262 |
+
for candidate in candidates:
|
| 263 |
+
try:
|
| 264 |
+
font = ImageFont.truetype(str(candidate), size=size)
|
| 265 |
+
self._font_cache[key] = font
|
| 266 |
+
return font
|
| 267 |
+
except OSError:
|
| 268 |
+
continue
|
| 269 |
+
|
| 270 |
+
font = ImageFont.load_default()
|
| 271 |
+
self._font_cache[key] = font
|
| 272 |
+
return font
|
kokoro/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
base_model:
|
| 6 |
+
- yl4579/StyleTTS2-LJSpeech
|
| 7 |
+
pipeline_tag: text-to-speech
|
| 8 |
+
---
|
| 9 |
+
**Kokoro** is an open-weight TTS model with 82 million parameters. Despite its lightweight architecture, it delivers comparable quality to larger models while being significantly faster and more cost-efficient. With Apache-licensed weights, Kokoro can be deployed anywhere from production environments to personal projects.
|
| 10 |
+
|
| 11 |
+
<audio controls><source src="https://huggingface.co/hexgrad/Kokoro-82M/resolve/main/samples/HEARME.wav" type="audio/wav"></audio>
|
| 12 |
+
|
| 13 |
+
🐈 **GitHub**: https://github.com/hexgrad/kokoro
|
| 14 |
+
|
| 15 |
+
🚀 **Demo**: https://hf.co/spaces/hexgrad/Kokoro-TTS
|
| 16 |
+
|
| 17 |
+
> [!NOTE]
|
| 18 |
+
> As of April 2025, the market rate of Kokoro served over API is **under $1 per million characters of text input**, or under $0.06 per hour of audio output. (On average, 1000 characters of input is about 1 minute of output.) Sources: [ArtificialAnalysis/Replicate at 65 cents per M chars](https://artificialanalysis.ai/text-to-speech/model-family/kokoro#price) and [DeepInfra at 80 cents per M chars](https://deepinfra.com/hexgrad/Kokoro-82M).
|
| 19 |
+
>
|
| 20 |
+
> This is an Apache-licensed model, and Kokoro has been deployed in numerous projects and commercial APIs. We welcome the deployment of the model in real use cases.
|
| 21 |
+
|
| 22 |
+
> [!CAUTION]
|
| 23 |
+
> Fake websites like kokorottsai_com (snapshot: https://archive.ph/nRRnk) and kokorotts_net (snapshot: https://archive.ph/60opa) are likely scams masquerading under the banner of a popular model.
|
| 24 |
+
>
|
| 25 |
+
> Any website containing "kokoro" in its root domain (e.g. kokorottsai_com, kokorotts_net) is **NOT owned by and NOT affiliated with this model page or its author**, and attempts to imply otherwise are red flags.
|
| 26 |
+
|
| 27 |
+
- [Releases](#releases)
|
| 28 |
+
- [Usage](#usage)
|
| 29 |
+
- [EVAL.md](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/EVAL.md) ↗️
|
| 30 |
+
- [SAMPLES.md](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/SAMPLES.md) ↗️
|
| 31 |
+
- [VOICES.md](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md) ↗️
|
| 32 |
+
- [Model Facts](#model-facts)
|
| 33 |
+
- [Training Details](#training-details)
|
| 34 |
+
- [Creative Commons Attribution](#creative-commons-attribution)
|
| 35 |
+
- [Acknowledgements](#acknowledgements)
|
| 36 |
+
|
| 37 |
+
### Releases
|
| 38 |
+
|
| 39 |
+
| Model | Published | Training Data | Langs & Voices | SHA256 |
|
| 40 |
+
| ----- | --------- | ------------- | -------------- | ------ |
|
| 41 |
+
| **v1.0** | **2025 Jan 27** | **Few hundred hrs** | [**8 & 54**](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md) | `496dba11` |
|
| 42 |
+
| [v0.19](https://huggingface.co/hexgrad/kLegacy/tree/main/v0.19) | 2024 Dec 25 | <100 hrs | 1 & 10 | `3b0c392f` |
|
| 43 |
+
|
| 44 |
+
| Training Costs | v0.19 | v1.0 | **Total** |
|
| 45 |
+
| -------------- | ----- | ---- | ----- |
|
| 46 |
+
| in A100 80GB GPU hours | 500 | 500 | **1000** |
|
| 47 |
+
| average hourly rate | $0.80/h | $1.20/h | **$1/h** |
|
| 48 |
+
| in USD | $400 | $600 | **$1000** |
|
| 49 |
+
|
| 50 |
+
### Usage
|
| 51 |
+
You can run this basic cell on [Google Colab](https://colab.research.google.com/). [Listen to samples](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/SAMPLES.md). For more languages and details, see [Advanced Usage](https://github.com/hexgrad/kokoro?tab=readme-ov-file#advanced-usage).
|
| 52 |
+
```py
|
| 53 |
+
!pip install -q kokoro>=0.9.2 soundfile
|
| 54 |
+
!apt-get -qq -y install espeak-ng > /dev/null 2>&1
|
| 55 |
+
from kokoro import KPipeline
|
| 56 |
+
from IPython.display import display, Audio
|
| 57 |
+
import soundfile as sf
|
| 58 |
+
import torch
|
| 59 |
+
pipeline = KPipeline(lang_code='a')
|
| 60 |
+
text = '''
|
| 61 |
+
[Kokoro](/kˈOkəɹO/) is an open-weight TTS model with 82 million parameters. Despite its lightweight architecture, it delivers comparable quality to larger models while being significantly faster and more cost-efficient. With Apache-licensed weights, [Kokoro](/kˈOkəɹO/) can be deployed anywhere from production environments to personal projects.
|
| 62 |
+
'''
|
| 63 |
+
generator = pipeline(text, voice='af_heart')
|
| 64 |
+
for i, (gs, ps, audio) in enumerate(generator):
|
| 65 |
+
print(i, gs, ps)
|
| 66 |
+
display(Audio(data=audio, rate=24000, autoplay=i==0))
|
| 67 |
+
sf.write(f'{i}.wav', audio, 24000)
|
| 68 |
+
```
|
| 69 |
+
Under the hood, `kokoro` uses [`misaki`](https://pypi.org/project/misaki/), a G2P library at https://github.com/hexgrad/misaki
|
| 70 |
+
|
| 71 |
+
### Model Facts
|
| 72 |
+
|
| 73 |
+
**Architecture:**
|
| 74 |
+
- StyleTTS 2: https://arxiv.org/abs/2306.07691
|
| 75 |
+
- ISTFTNet: https://arxiv.org/abs/2203.02395
|
| 76 |
+
- Decoder only: no diffusion, no encoder release
|
| 77 |
+
|
| 78 |
+
**Architected by:** Li et al @ https://github.com/yl4579/StyleTTS2
|
| 79 |
+
|
| 80 |
+
**Trained by**: `@rzvzn` on Discord
|
| 81 |
+
|
| 82 |
+
**Languages:** Multiple
|
| 83 |
+
|
| 84 |
+
**Model SHA256 Hash:** `496dba118d1a58f5f3db2efc88dbdc216e0483fc89fe6e47ee1f2c53f18ad1e4`
|
| 85 |
+
|
| 86 |
+
### Training Details
|
| 87 |
+
|
| 88 |
+
**Data:** Kokoro was trained exclusively on **permissive/non-copyrighted audio data** and IPA phoneme labels. Examples of permissive/non-copyrighted audio include:
|
| 89 |
+
- Public domain audio
|
| 90 |
+
- Audio licensed under Apache, MIT, etc
|
| 91 |
+
- Synthetic audio<sup>[1]</sup> generated by closed<sup>[2]</sup> TTS models from large providers<br/>
|
| 92 |
+
[1] https://copyright.gov/ai/ai_policy_guidance.pdf<br/>
|
| 93 |
+
[2] No synthetic audio from open TTS models or "custom voice clones"
|
| 94 |
+
|
| 95 |
+
**Total Dataset Size:** A few hundred hours of audio
|
| 96 |
+
|
| 97 |
+
**Total Training Cost:** About $1000 for 1000 hours of A100 80GB vRAM
|
| 98 |
+
|
| 99 |
+
### Creative Commons Attribution
|
| 100 |
+
|
| 101 |
+
The following CC BY audio was part of the dataset used to train Kokoro v1.0.
|
| 102 |
+
|
| 103 |
+
| Audio Data | Duration Used | License | Added to Training Set After |
|
| 104 |
+
| ---------- | ------------- | ------- | --------------------------- |
|
| 105 |
+
| [Koniwa](https://github.com/koniwa/koniwa) `tnc` | <1h | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/deed.ja) | v0.19 / 22 Nov 2024 |
|
| 106 |
+
| [SIWIS](https://datashare.ed.ac.uk/handle/10283/2353) | <11h | [CC BY 4.0](https://datashare.ed.ac.uk/bitstream/handle/10283/2353/license_text) | v0.19 / 22 Nov 2024 |
|
| 107 |
+
|
| 108 |
+
### Acknowledgements
|
| 109 |
+
|
| 110 |
+
- 🛠️ [@yl4579](https://huggingface.co/yl4579) for architecting StyleTTS 2.
|
| 111 |
+
- 🏆 [@Pendrokar](https://huggingface.co/Pendrokar) for adding Kokoro as a contender in the TTS Spaces Arena.
|
| 112 |
+
- 📊 Thank you to everyone who contributed synthetic training data.
|
| 113 |
+
- ❤️ Special thanks to all compute sponsors.
|
| 114 |
+
- 👾 Discord server: https://discord.gg/QuGxSWBfQy
|
| 115 |
+
- 🪽 Kokoro is a Japanese word that translates to "heart" or "spirit". It is also the name of an [AI in the Terminator franchise](https://terminator.fandom.com/wiki/Kokoro).
|
| 116 |
+
|
| 117 |
+
<img src="https://static0.gamerantimages.com/wordpress/wp-content/uploads/2024/08/terminator-zero-41-1.jpg" width="400" alt="kokoro" />
|
kokoro/VOICES.md
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Voices
|
| 2 |
+
|
| 3 |
+
- 🇺🇸 [American English](#american-english): 11F 9M
|
| 4 |
+
- 🇬🇧 [British English](#british-english): 4F 4M
|
| 5 |
+
- 🇯🇵 [Japanese](#japanese): 4F 1M
|
| 6 |
+
- 🇨🇳 [Mandarin Chinese](#mandarin-chinese): 4F 4M
|
| 7 |
+
- 🇪🇸 [Spanish](#spanish): 1F 2M
|
| 8 |
+
- 🇫🇷 [French](#french): 1F
|
| 9 |
+
- 🇮🇳 [Hindi](#hindi): 2F 2M
|
| 10 |
+
- 🇮🇹 [Italian](#italian): 1F 1M
|
| 11 |
+
- 🇧🇷 [Brazilian Portuguese](#brazilian-portuguese): 1F 2M
|
| 12 |
+
|
| 13 |
+
For each voice, the given grades are intended to be estimates of the **quality and quantity** of its associated training data, both of which impact overall inference quality.
|
| 14 |
+
|
| 15 |
+
Subjectively, voices will sound better or worse to different people.
|
| 16 |
+
|
| 17 |
+
Support for non-English languages may be absent or thin due to weak G2P and/or lack of training data. Some languages are only represented by a small handful or even just one voice (French).
|
| 18 |
+
|
| 19 |
+
Most voices perform best on a "goldilocks range" of 100-200 tokens out of ~500 possible. Voices may perform worse at the extremes:
|
| 20 |
+
- **Weakness** on short utterances, especially less than 10-20 tokens. Root cause could be lack of short-utterance training data and/or model architecture. One possible inference mitigation is to bundle shorter utterances together.
|
| 21 |
+
- **Rushing** on long utterances, especially over 400 tokens. You can chunk down to shorter utterances or adjust the `speed` parameter to mitigate this.
|
| 22 |
+
|
| 23 |
+
**Target Quality**
|
| 24 |
+
- How high quality is the reference voice? This grade may be impacted by audio quality, artifacts, compression, & sample rate.
|
| 25 |
+
- How well do the text labels match the audio? Text/audio misalignment (e.g. from hallucinations) will lower this grade.
|
| 26 |
+
|
| 27 |
+
**Training Duration**
|
| 28 |
+
- How much audio was seen during training? Smaller durations result in a lower overall grade.
|
| 29 |
+
- 10 hours <= **HH hours** < 100 hours
|
| 30 |
+
- 1 hour <= H hours < 10 hours
|
| 31 |
+
- 10 minutes <= MM minutes < 100 minutes
|
| 32 |
+
- 1 minute <= _M minutes_ 🤏 < 10 minutes
|
| 33 |
+
|
| 34 |
+
### American English
|
| 35 |
+
|
| 36 |
+
- `lang_code='a'` in [`misaki[en]`](https://github.com/hexgrad/misaki)
|
| 37 |
+
- espeak-ng `en-us` fallback
|
| 38 |
+
|
| 39 |
+
| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 |
|
| 40 |
+
| ---- | ------ | -------------- | ----------------- | ------------- | ------ |
|
| 41 |
+
| **af\_heart** | 🚺❤️ | | | **A** | `0ab5709b` |
|
| 42 |
+
| af_alloy | 🚺 | B | MM minutes | C | `6d877149` |
|
| 43 |
+
| af_aoede | 🚺 | B | H hours | C+ | `c03bd1a4` |
|
| 44 |
+
| af_bella | 🚺🔥 | **A** | **HH hours** | **A-** | `8cb64e02` |
|
| 45 |
+
| af_jessica | 🚺 | C | MM minutes | D | `cdfdccb8` |
|
| 46 |
+
| af_kore | 🚺 | B | H hours | C+ | `8bfbc512` |
|
| 47 |
+
| af_nicole | 🚺🎧 | B | **HH hours** | B- | `c5561808` |
|
| 48 |
+
| af_nova | 🚺 | B | MM minutes | C | `e0233676` |
|
| 49 |
+
| af_river | 🚺 | C | MM minutes | D | `e149459b` |
|
| 50 |
+
| af_sarah | 🚺 | B | H hours | C+ | `49bd364e` |
|
| 51 |
+
| af_sky | 🚺 | B | _M minutes_ 🤏 | C- | `c799548a` |
|
| 52 |
+
| am_adam | 🚹 | D | H hours | F+ | `ced7e284` |
|
| 53 |
+
| am_echo | 🚹 | C | MM minutes | D | `8bcfdc85` |
|
| 54 |
+
| am_eric | 🚹 | C | MM minutes | D | `ada66f0e` |
|
| 55 |
+
| am_fenrir | 🚹 | B | H hours | C+ | `98e507ec` |
|
| 56 |
+
| am_liam | 🚹 | C | MM minutes | D | `c8255075` |
|
| 57 |
+
| am_michael | 🚹 | B | H hours | C+ | `9a443b79` |
|
| 58 |
+
| am_onyx | 🚹 | C | MM minutes | D | `e8452be1` |
|
| 59 |
+
| am_puck | 🚹 | B | H hours | C+ | `dd1d8973` |
|
| 60 |
+
| am_santa | 🚹 | C | _M minutes_ 🤏 | D- | `7f2f7582` |
|
| 61 |
+
|
| 62 |
+
### British English
|
| 63 |
+
|
| 64 |
+
- `lang_code='b'` in [`misaki[en]`](https://github.com/hexgrad/misaki)
|
| 65 |
+
- espeak-ng `en-gb` fallback
|
| 66 |
+
|
| 67 |
+
| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 |
|
| 68 |
+
| ---- | ------ | -------------- | ----------------- | ------------- | ------ |
|
| 69 |
+
| bf_alice | 🚺 | C | MM minutes | D | `d292651b` |
|
| 70 |
+
| bf_emma | 🚺 | B | **HH hours** | B- | `d0a423de` |
|
| 71 |
+
| bf_isabella | 🚺 | B | MM minutes | C | `cdd4c370` |
|
| 72 |
+
| bf_lily | 🚺 | C | MM minutes | D | `6e09c2e4` |
|
| 73 |
+
| bm_daniel | 🚹 | C | MM minutes | D | `fc3fce4e` |
|
| 74 |
+
| bm_fable | 🚹 | B | MM minutes | C | `d44935f3` |
|
| 75 |
+
| bm_george | 🚹 | B | MM minutes | C | `f1bc8122` |
|
| 76 |
+
| bm_lewis | 🚹 | C | H hours | D+ | `b5204750` |
|
| 77 |
+
|
| 78 |
+
### Japanese
|
| 79 |
+
|
| 80 |
+
- `lang_code='j'` in [`misaki[ja]`](https://github.com/hexgrad/misaki)
|
| 81 |
+
- Total Japanese training data: H hours
|
| 82 |
+
|
| 83 |
+
| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 | CC BY |
|
| 84 |
+
| ---- | ------ | -------------- | ----------------- | ------------- | ------ | ----- |
|
| 85 |
+
| jf_alpha | 🚺 | B | H hours | C+ | `1bf4c9dc` | |
|
| 86 |
+
| jf_gongitsune | 🚺 | B | MM minutes | C | `1b171917` | [gongitsune](https://github.com/koniwa/koniwa/blob/master/source/tnc/tnc__gongitsune.txt) |
|
| 87 |
+
| jf_nezumi | 🚺 | B | _M minutes_ 🤏 | C- | `d83f007a` | [nezuminoyomeiri](https://github.com/koniwa/koniwa/blob/master/source/tnc/tnc__nezuminoyomeiri.txt) |
|
| 88 |
+
| jf_tebukuro | 🚺 | B | MM minutes | C | `0d691790` | [tebukurowokaini](https://github.com/koniwa/koniwa/blob/master/source/tnc/tnc__tebukurowokaini.txt) |
|
| 89 |
+
| jm_kumo | 🚹 | B | _M minutes_ 🤏 | C- | `98340afd` | [kumonoito](https://github.com/koniwa/koniwa/blob/master/source/tnc/tnc__kumonoito.txt) |
|
| 90 |
+
|
| 91 |
+
### Mandarin Chinese
|
| 92 |
+
|
| 93 |
+
- `lang_code='z'` in [`misaki[zh]`](https://github.com/hexgrad/misaki)
|
| 94 |
+
- Total Mandarin Chinese training data: H hours
|
| 95 |
+
|
| 96 |
+
| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 |
|
| 97 |
+
| ---- | ------ | -------------- | ----------------- | ------------- | ------ |
|
| 98 |
+
| zf_xiaobei | 🚺 | C | MM minutes | D | `9b76be63` |
|
| 99 |
+
| zf_xiaoni | 🚺 | C | MM minutes | D | `95b49f16` |
|
| 100 |
+
| zf_xiaoxiao | 🚺 | C | MM minutes | D | `cfaf6f2d` |
|
| 101 |
+
| zf_xiaoyi | 🚺 | C | MM minutes | D | `b5235dba` |
|
| 102 |
+
| zm_yunjian | 🚹 | C | MM minutes | D | `76cbf8ba` |
|
| 103 |
+
| zm_yunxi | 🚹 | C | MM minutes | D | `dbe6e1ce` |
|
| 104 |
+
| zm_yunxia | 🚹 | C | MM minutes | D | `bb2b03b0` |
|
| 105 |
+
| zm_yunyang | 🚹 | C | MM minutes | D | `5238ac22` |
|
| 106 |
+
|
| 107 |
+
### Spanish
|
| 108 |
+
|
| 109 |
+
- `lang_code='e'` in [`misaki[en]`](https://github.com/hexgrad/misaki)
|
| 110 |
+
- espeak-ng `es`
|
| 111 |
+
|
| 112 |
+
| Name | Traits | SHA256 |
|
| 113 |
+
| ---- | ------ | ------ |
|
| 114 |
+
| ef_dora | 🚺 | `d9d69b0f` |
|
| 115 |
+
| em_alex | 🚹 | `5eac53f7` |
|
| 116 |
+
| em_santa | 🚹 | `aa8620cb` |
|
| 117 |
+
|
| 118 |
+
### French
|
| 119 |
+
|
| 120 |
+
- `lang_code='f'` in [`misaki[en]`](https://github.com/hexgrad/misaki)
|
| 121 |
+
- espeak-ng `fr-fr`
|
| 122 |
+
- Total French training data: <11 hours
|
| 123 |
+
|
| 124 |
+
| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 | CC BY |
|
| 125 |
+
| ---- | ------ | -------------- | ----------------- | ------------- | ------ | ----- |
|
| 126 |
+
| ff_siwis | 🚺 | B | <11 hours | B- | `8073bf2d` | [SIWIS](https://datashare.ed.ac.uk/handle/10283/2353) |
|
| 127 |
+
|
| 128 |
+
### Hindi
|
| 129 |
+
|
| 130 |
+
- `lang_code='h'` in [`misaki[en]`](https://github.com/hexgrad/misaki)
|
| 131 |
+
- espeak-ng `hi`
|
| 132 |
+
- Total Hindi training data: H hours
|
| 133 |
+
|
| 134 |
+
| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 |
|
| 135 |
+
| ---- | ------ | -------------- | ----------------- | ------------- | ------ |
|
| 136 |
+
| hf_alpha | 🚺 | B | MM minutes | C | `06906fe0` |
|
| 137 |
+
| hf_beta | 🚺 | B | MM minutes | C | `63c0a1a6` |
|
| 138 |
+
| hm_omega | 🚹 | B | MM minutes | C | `b55f02a8` |
|
| 139 |
+
| hm_psi | 🚹 | B | MM minutes | C | `2f0f055c` |
|
| 140 |
+
|
| 141 |
+
### Italian
|
| 142 |
+
|
| 143 |
+
- `lang_code='i'` in [`misaki[en]`](https://github.com/hexgrad/misaki)
|
| 144 |
+
- espeak-ng `it`
|
| 145 |
+
- Total Italian training data: H hours
|
| 146 |
+
|
| 147 |
+
| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 |
|
| 148 |
+
| ---- | ------ | -------------- | ----------------- | ------------- | ------ |
|
| 149 |
+
| if_sara | 🚺 | B | MM minutes | C | `6c0b253b` |
|
| 150 |
+
| im_nicola | 🚹 | B | MM minutes | C | `234ed066` |
|
| 151 |
+
|
| 152 |
+
### Brazilian Portuguese
|
| 153 |
+
|
| 154 |
+
- `lang_code='p'` in [`misaki[en]`](https://github.com/hexgrad/misaki)
|
| 155 |
+
- espeak-ng `pt-br`
|
| 156 |
+
|
| 157 |
+
| Name | Traits | SHA256 |
|
| 158 |
+
| ---- | ------ | ------ |
|
| 159 |
+
| pf_dora | 🚺 | `07e4ff98` |
|
| 160 |
+
| pm_alex | 🚹 | `cf0ba8c5` |
|
| 161 |
+
| pm_santa | 🚹 | `d4210316` |
|
kokoro/config.json
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"istftnet": {
|
| 3 |
+
"upsample_kernel_sizes": [20, 12],
|
| 4 |
+
"upsample_rates": [10, 6],
|
| 5 |
+
"gen_istft_hop_size": 5,
|
| 6 |
+
"gen_istft_n_fft": 20,
|
| 7 |
+
"resblock_dilation_sizes": [
|
| 8 |
+
[1, 3, 5],
|
| 9 |
+
[1, 3, 5],
|
| 10 |
+
[1, 3, 5]
|
| 11 |
+
],
|
| 12 |
+
"resblock_kernel_sizes": [3, 7, 11],
|
| 13 |
+
"upsample_initial_channel": 512
|
| 14 |
+
},
|
| 15 |
+
"dim_in": 64,
|
| 16 |
+
"dropout": 0.2,
|
| 17 |
+
"hidden_dim": 512,
|
| 18 |
+
"max_conv_dim": 512,
|
| 19 |
+
"max_dur": 50,
|
| 20 |
+
"multispeaker": true,
|
| 21 |
+
"n_layer": 3,
|
| 22 |
+
"n_mels": 80,
|
| 23 |
+
"n_token": 178,
|
| 24 |
+
"style_dim": 128,
|
| 25 |
+
"text_encoder_kernel_size": 5,
|
| 26 |
+
"plbert": {
|
| 27 |
+
"hidden_size": 768,
|
| 28 |
+
"num_attention_heads": 12,
|
| 29 |
+
"intermediate_size": 2048,
|
| 30 |
+
"max_position_embeddings": 512,
|
| 31 |
+
"num_hidden_layers": 12,
|
| 32 |
+
"dropout": 0.1
|
| 33 |
+
},
|
| 34 |
+
"vocab": {
|
| 35 |
+
";": 1,
|
| 36 |
+
":": 2,
|
| 37 |
+
",": 3,
|
| 38 |
+
".": 4,
|
| 39 |
+
"!": 5,
|
| 40 |
+
"?": 6,
|
| 41 |
+
"—": 9,
|
| 42 |
+
"…": 10,
|
| 43 |
+
"\"": 11,
|
| 44 |
+
"(": 12,
|
| 45 |
+
")": 13,
|
| 46 |
+
"“": 14,
|
| 47 |
+
"”": 15,
|
| 48 |
+
" ": 16,
|
| 49 |
+
"\u0303": 17,
|
| 50 |
+
"ʣ": 18,
|
| 51 |
+
"ʥ": 19,
|
| 52 |
+
"ʦ": 20,
|
| 53 |
+
"ʨ": 21,
|
| 54 |
+
"ᵝ": 22,
|
| 55 |
+
"\uAB67": 23,
|
| 56 |
+
"A": 24,
|
| 57 |
+
"I": 25,
|
| 58 |
+
"O": 31,
|
| 59 |
+
"Q": 33,
|
| 60 |
+
"S": 35,
|
| 61 |
+
"T": 36,
|
| 62 |
+
"W": 39,
|
| 63 |
+
"Y": 41,
|
| 64 |
+
"ᵊ": 42,
|
| 65 |
+
"a": 43,
|
| 66 |
+
"b": 44,
|
| 67 |
+
"c": 45,
|
| 68 |
+
"d": 46,
|
| 69 |
+
"e": 47,
|
| 70 |
+
"f": 48,
|
| 71 |
+
"h": 50,
|
| 72 |
+
"i": 51,
|
| 73 |
+
"j": 52,
|
| 74 |
+
"k": 53,
|
| 75 |
+
"l": 54,
|
| 76 |
+
"m": 55,
|
| 77 |
+
"n": 56,
|
| 78 |
+
"o": 57,
|
| 79 |
+
"p": 58,
|
| 80 |
+
"q": 59,
|
| 81 |
+
"r": 60,
|
| 82 |
+
"s": 61,
|
| 83 |
+
"t": 62,
|
| 84 |
+
"u": 63,
|
| 85 |
+
"v": 64,
|
| 86 |
+
"w": 65,
|
| 87 |
+
"x": 66,
|
| 88 |
+
"y": 67,
|
| 89 |
+
"z": 68,
|
| 90 |
+
"ɑ": 69,
|
| 91 |
+
"ɐ": 70,
|
| 92 |
+
"ɒ": 71,
|
| 93 |
+
"æ": 72,
|
| 94 |
+
"β": 75,
|
| 95 |
+
"ɔ": 76,
|
| 96 |
+
"ɕ": 77,
|
| 97 |
+
"ç": 78,
|
| 98 |
+
"ɖ": 80,
|
| 99 |
+
"ð": 81,
|
| 100 |
+
"ʤ": 82,
|
| 101 |
+
"ə": 83,
|
| 102 |
+
"ɚ": 85,
|
| 103 |
+
"ɛ": 86,
|
| 104 |
+
"ɜ": 87,
|
| 105 |
+
"ɟ": 90,
|
| 106 |
+
"ɡ": 92,
|
| 107 |
+
"ɥ": 99,
|
| 108 |
+
"ɨ": 101,
|
| 109 |
+
"ɪ": 102,
|
| 110 |
+
"ʝ": 103,
|
| 111 |
+
"ɯ": 110,
|
| 112 |
+
"ɰ": 111,
|
| 113 |
+
"ŋ": 112,
|
| 114 |
+
"ɳ": 113,
|
| 115 |
+
"ɲ": 114,
|
| 116 |
+
"ɴ": 115,
|
| 117 |
+
"ø": 116,
|
| 118 |
+
"ɸ": 118,
|
| 119 |
+
"θ": 119,
|
| 120 |
+
"œ": 120,
|
| 121 |
+
"ɹ": 123,
|
| 122 |
+
"ɾ": 125,
|
| 123 |
+
"ɻ": 126,
|
| 124 |
+
"ʁ": 128,
|
| 125 |
+
"ɽ": 129,
|
| 126 |
+
"ʂ": 130,
|
| 127 |
+
"ʃ": 131,
|
| 128 |
+
"ʈ": 132,
|
| 129 |
+
"ʧ": 133,
|
| 130 |
+
"ʊ": 135,
|
| 131 |
+
"ʋ": 136,
|
| 132 |
+
"ʌ": 138,
|
| 133 |
+
"ɣ": 139,
|
| 134 |
+
"ɤ": 140,
|
| 135 |
+
"χ": 142,
|
| 136 |
+
"ʎ": 143,
|
| 137 |
+
"ʒ": 147,
|
| 138 |
+
"ʔ": 148,
|
| 139 |
+
"ˈ": 156,
|
| 140 |
+
"ˌ": 157,
|
| 141 |
+
"ː": 158,
|
| 142 |
+
"ʰ": 162,
|
| 143 |
+
"ʲ": 164,
|
| 144 |
+
"↓": 169,
|
| 145 |
+
"→": 171,
|
| 146 |
+
"↗": 172,
|
| 147 |
+
"↘": 173,
|
| 148 |
+
"ᵻ": 177
|
| 149 |
+
}
|
| 150 |
+
}
|
kokoro/kokoro-v1_0.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:496dba118d1a58f5f3db2efc88dbdc216e0483fc89fe6e47ee1f2c53f18ad1e4
|
| 3 |
+
size 327212226
|
kokoro/requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Use Python 3.10, 3.11, or 3.12. Kokoro 0.9.4 is not published for Python 3.13+.
|
| 2 |
+
kokoro>=0.9.4
|
| 3 |
+
soundfile>=0.12.1
|
| 4 |
+
misaki[ja]>=0.9.4
|
| 5 |
+
misaki[zh]>=0.9.4
|
kokoro/sample.py
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import os
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
REPO_ID = "hexgrad/Kokoro-82M"
|
| 9 |
+
SAMPLE_RATE = 24000
|
| 10 |
+
|
| 11 |
+
ROOT = Path(__file__).resolve().parent
|
| 12 |
+
ASSET_DIR = ROOT
|
| 13 |
+
CONFIG_FILE = ASSET_DIR / "config.json"
|
| 14 |
+
MODEL_FILE = ASSET_DIR / "kokoro-v1_0.pth"
|
| 15 |
+
VOICE_DIR = ASSET_DIR / "voices"
|
| 16 |
+
|
| 17 |
+
LANG_NAMES = {
|
| 18 |
+
"a": "American English",
|
| 19 |
+
"b": "British English",
|
| 20 |
+
"e": "Spanish",
|
| 21 |
+
"f": "French",
|
| 22 |
+
"h": "Hindi",
|
| 23 |
+
"i": "Italian",
|
| 24 |
+
"j": "Japanese",
|
| 25 |
+
"p": "Brazilian Portuguese",
|
| 26 |
+
"z": "Mandarin Chinese",
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
LANG_ALIASES = {
|
| 30 |
+
"auto": "auto",
|
| 31 |
+
"a": "a",
|
| 32 |
+
"en-us": "a",
|
| 33 |
+
"us": "a",
|
| 34 |
+
"american": "a",
|
| 35 |
+
"b": "b",
|
| 36 |
+
"en-gb": "b",
|
| 37 |
+
"gb": "b",
|
| 38 |
+
"british": "b",
|
| 39 |
+
"e": "e",
|
| 40 |
+
"es": "e",
|
| 41 |
+
"spanish": "e",
|
| 42 |
+
"f": "f",
|
| 43 |
+
"fr": "f",
|
| 44 |
+
"fr-fr": "f",
|
| 45 |
+
"french": "f",
|
| 46 |
+
"h": "h",
|
| 47 |
+
"hi": "h",
|
| 48 |
+
"hindi": "h",
|
| 49 |
+
"i": "i",
|
| 50 |
+
"it": "i",
|
| 51 |
+
"italian": "i",
|
| 52 |
+
"j": "j",
|
| 53 |
+
"ja": "j",
|
| 54 |
+
"japanese": "j",
|
| 55 |
+
"p": "p",
|
| 56 |
+
"pt": "p",
|
| 57 |
+
"pt-br": "p",
|
| 58 |
+
"portuguese": "p",
|
| 59 |
+
"brazilian-portuguese": "p",
|
| 60 |
+
"z": "z",
|
| 61 |
+
"zh": "z",
|
| 62 |
+
"zh-cn": "z",
|
| 63 |
+
"mandarin": "z",
|
| 64 |
+
"chinese": "z",
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def parse_args() -> argparse.Namespace:
|
| 69 |
+
parser = argparse.ArgumentParser(
|
| 70 |
+
description="Generate WAV audio with local Kokoro-82M model and voices."
|
| 71 |
+
)
|
| 72 |
+
parser.add_argument(
|
| 73 |
+
"text_args",
|
| 74 |
+
nargs="*",
|
| 75 |
+
help="Text to synthesize. If omitted, uses --text, --text-file, stdin, or a short sample.",
|
| 76 |
+
)
|
| 77 |
+
parser.add_argument("-t", "--text", help="Text to synthesize.")
|
| 78 |
+
parser.add_argument("--text-file", type=Path, help="UTF-8 text file to synthesize.")
|
| 79 |
+
parser.add_argument(
|
| 80 |
+
"-v",
|
| 81 |
+
"--voice",
|
| 82 |
+
default="af_heart",
|
| 83 |
+
help="Voice name, .pt path, or comma-separated blend. Example: af_heart or af_heart,af_bella",
|
| 84 |
+
)
|
| 85 |
+
parser.add_argument(
|
| 86 |
+
"-o",
|
| 87 |
+
"--output",
|
| 88 |
+
type=Path,
|
| 89 |
+
default=Path("kokoro_sample.wav"),
|
| 90 |
+
help="Output WAV path.",
|
| 91 |
+
)
|
| 92 |
+
parser.add_argument("--speed", type=float, default=1.0, help="Speech speed multiplier.")
|
| 93 |
+
parser.add_argument(
|
| 94 |
+
"-l",
|
| 95 |
+
"--lang",
|
| 96 |
+
default="auto",
|
| 97 |
+
help="Language code or alias. Default auto-infers from the first voice letter.",
|
| 98 |
+
)
|
| 99 |
+
parser.add_argument(
|
| 100 |
+
"--device",
|
| 101 |
+
choices=["auto", "cpu", "cuda", "mps"],
|
| 102 |
+
default="auto",
|
| 103 |
+
help="Torch device to use.",
|
| 104 |
+
)
|
| 105 |
+
parser.add_argument(
|
| 106 |
+
"--split-pattern",
|
| 107 |
+
default=r"\n+",
|
| 108 |
+
help="Regex used to split text into sections before synthesis.",
|
| 109 |
+
)
|
| 110 |
+
parser.add_argument(
|
| 111 |
+
"--gap",
|
| 112 |
+
type=float,
|
| 113 |
+
default=0.12,
|
| 114 |
+
help="Seconds of silence to insert between generated chunks.",
|
| 115 |
+
)
|
| 116 |
+
parser.add_argument("--list-voices", action="store_true", help="Print local voices and exit.")
|
| 117 |
+
parser.add_argument("--verbose", action="store_true", help="Print generated chunks and phonemes.")
|
| 118 |
+
return parser.parse_args()
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
def require_assets() -> None:
|
| 122 |
+
missing = [p for p in (CONFIG_FILE, MODEL_FILE, VOICE_DIR) if not p.exists()]
|
| 123 |
+
if missing:
|
| 124 |
+
formatted = "\n".join(f" - {p}" for p in missing)
|
| 125 |
+
raise SystemExit(f"Missing Kokoro asset(s):\n{formatted}")
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def require_python_version() -> None:
|
| 129 |
+
if not ((3, 10) <= sys.version_info[:2] < (3, 13)):
|
| 130 |
+
version = ".".join(map(str, sys.version_info[:3]))
|
| 131 |
+
raise SystemExit(
|
| 132 |
+
"Kokoro's current PyPI runtime requires Python 3.10, 3.11, or 3.12.\n"
|
| 133 |
+
f"You are running Python {version}. Use Python 3.12 for this script."
|
| 134 |
+
)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def available_voices() -> list[str]:
|
| 138 |
+
if not VOICE_DIR.exists():
|
| 139 |
+
return []
|
| 140 |
+
return sorted(path.stem for path in VOICE_DIR.glob("*.pt"))
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def print_voices() -> None:
|
| 144 |
+
voices = available_voices()
|
| 145 |
+
print(f"{len(voices)} local voices in {VOICE_DIR}:")
|
| 146 |
+
for code, language in LANG_NAMES.items():
|
| 147 |
+
group = [voice for voice in voices if voice.startswith(code)]
|
| 148 |
+
if group:
|
| 149 |
+
print(f"\n{code} - {language}")
|
| 150 |
+
print(" " + ", ".join(group))
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def resolve_voice_files(voice_arg: str) -> list[Path]:
|
| 154 |
+
voices = [part.strip() for part in voice_arg.split(",") if part.strip()]
|
| 155 |
+
if not voices:
|
| 156 |
+
raise SystemExit("No voice was provided.")
|
| 157 |
+
|
| 158 |
+
resolved: list[Path] = []
|
| 159 |
+
for voice in voices:
|
| 160 |
+
candidate = Path(voice).expanduser()
|
| 161 |
+
if candidate.suffix == ".pt":
|
| 162 |
+
if candidate.exists():
|
| 163 |
+
resolved.append(candidate.resolve())
|
| 164 |
+
continue
|
| 165 |
+
local_by_name = VOICE_DIR / candidate.name
|
| 166 |
+
if local_by_name.exists():
|
| 167 |
+
resolved.append(local_by_name)
|
| 168 |
+
continue
|
| 169 |
+
|
| 170 |
+
local = VOICE_DIR / f"{voice}.pt"
|
| 171 |
+
if local.exists():
|
| 172 |
+
resolved.append(local)
|
| 173 |
+
continue
|
| 174 |
+
|
| 175 |
+
raise SystemExit(f"Unknown voice '{voice}'. Run `python kokoro/sample.py --list-voices`.")
|
| 176 |
+
|
| 177 |
+
return resolved
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def normalize_lang(value: str) -> str:
|
| 181 |
+
key = (value or "auto").lower()
|
| 182 |
+
lang = LANG_ALIASES.get(key, key)
|
| 183 |
+
if lang != "auto" and lang not in LANG_NAMES:
|
| 184 |
+
valid = ", ".join(sorted(LANG_NAMES))
|
| 185 |
+
raise SystemExit(f"Unknown language '{value}'. Valid codes: {valid}, or auto.")
|
| 186 |
+
return lang
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def infer_lang(voice_files: list[Path], requested: str) -> str:
|
| 190 |
+
lang = normalize_lang(requested)
|
| 191 |
+
if lang != "auto":
|
| 192 |
+
return lang
|
| 193 |
+
|
| 194 |
+
inferred = {path.stem[0].lower() for path in voice_files if path.stem}
|
| 195 |
+
inferred &= set(LANG_NAMES)
|
| 196 |
+
if len(inferred) == 1:
|
| 197 |
+
return next(iter(inferred))
|
| 198 |
+
if len(inferred) > 1:
|
| 199 |
+
langs = ", ".join(sorted(inferred))
|
| 200 |
+
raise SystemExit(f"Voice blend spans languages ({langs}); pass --lang explicitly.")
|
| 201 |
+
raise SystemExit("Could not infer language from voice name; pass --lang explicitly.")
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def read_text(args: argparse.Namespace) -> str:
|
| 205 |
+
parts: list[str] = []
|
| 206 |
+
if args.text:
|
| 207 |
+
parts.append(args.text)
|
| 208 |
+
if args.text_file:
|
| 209 |
+
parts.append(args.text_file.read_text(encoding="utf-8"))
|
| 210 |
+
if args.text_args:
|
| 211 |
+
parts.append(" ".join(args.text_args))
|
| 212 |
+
if parts:
|
| 213 |
+
return "\n".join(parts)
|
| 214 |
+
if not sys.stdin.isatty():
|
| 215 |
+
return sys.stdin.read()
|
| 216 |
+
return "Kokoro is ready to generate speech with any local voice."
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
def same_path(entry: str, target: Path) -> bool:
|
| 220 |
+
try:
|
| 221 |
+
return Path(entry or ".").resolve() == target
|
| 222 |
+
except OSError:
|
| 223 |
+
return False
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def import_runtime():
|
| 227 |
+
# The local asset directory is named "kokoro", so temporarily remove it
|
| 228 |
+
# and its parent from sys.path to avoid shadowing the installed package.
|
| 229 |
+
removed: list[tuple[int, str]] = []
|
| 230 |
+
for index, entry in reversed(list(enumerate(sys.path))):
|
| 231 |
+
if same_path(entry, ROOT) or same_path(entry, ROOT.parent):
|
| 232 |
+
removed.append((index, entry))
|
| 233 |
+
sys.path.pop(index)
|
| 234 |
+
|
| 235 |
+
try:
|
| 236 |
+
import numpy as np
|
| 237 |
+
import soundfile as sf
|
| 238 |
+
import torch
|
| 239 |
+
from kokoro import KModel, KPipeline
|
| 240 |
+
except Exception as exc:
|
| 241 |
+
install = (
|
| 242 |
+
"Missing Kokoro runtime dependency.\n"
|
| 243 |
+
"Install Python packages with:\n"
|
| 244 |
+
" python -m pip install -r kokoro/requirements.txt\n\n"
|
| 245 |
+
"Use Python 3.10, 3.11, or 3.12. Python 3.13+ is too new for Kokoro's current PyPI package.\n"
|
| 246 |
+
"For Japanese/Chinese voices, requirements.txt includes the misaki extras."
|
| 247 |
+
)
|
| 248 |
+
raise RuntimeError(install) from exc
|
| 249 |
+
finally:
|
| 250 |
+
for index, entry in sorted(removed):
|
| 251 |
+
sys.path.insert(index, entry)
|
| 252 |
+
|
| 253 |
+
return np, sf, torch, KModel, KPipeline
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def select_device(torch, requested: str) -> str:
|
| 257 |
+
if requested == "cuda" and not torch.cuda.is_available():
|
| 258 |
+
raise SystemExit("CUDA was requested, but torch.cuda.is_available() is false.")
|
| 259 |
+
if requested == "mps":
|
| 260 |
+
has_mps = hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
|
| 261 |
+
if not has_mps:
|
| 262 |
+
raise SystemExit("MPS was requested, but torch.backends.mps.is_available() is false.")
|
| 263 |
+
if os.environ.get("PYTORCH_ENABLE_MPS_FALLBACK") != "1":
|
| 264 |
+
raise SystemExit("Set PYTORCH_ENABLE_MPS_FALLBACK=1 before using --device mps.")
|
| 265 |
+
if requested != "auto":
|
| 266 |
+
return requested
|
| 267 |
+
if torch.cuda.is_available():
|
| 268 |
+
return "cuda"
|
| 269 |
+
has_mps = hasattr(torch.backends, "mps") and torch.backends.mps.is_available()
|
| 270 |
+
if has_mps and os.environ.get("PYTORCH_ENABLE_MPS_FALLBACK") == "1":
|
| 271 |
+
return "mps"
|
| 272 |
+
return "cpu"
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def load_voice_tensor(torch, voice_files: list[Path]):
|
| 276 |
+
packs = [torch.load(str(path), weights_only=True) for path in voice_files]
|
| 277 |
+
if len(packs) == 1:
|
| 278 |
+
return packs[0]
|
| 279 |
+
return torch.mean(torch.stack(packs), dim=0)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def language_dependency_hint(lang: str, exc: Exception) -> RuntimeError:
|
| 283 |
+
suffix = ""
|
| 284 |
+
if lang in {"e", "f", "h", "i", "p"}:
|
| 285 |
+
suffix = "\nInstall espeak-ng and make sure it is available on PATH."
|
| 286 |
+
elif lang == "j":
|
| 287 |
+
suffix = "\nInstall Japanese support with: python -m pip install \"misaki[ja]>=0.9.4\""
|
| 288 |
+
elif lang == "z":
|
| 289 |
+
suffix = "\nInstall Chinese support with: python -m pip install \"misaki[zh]>=0.9.4\""
|
| 290 |
+
return RuntimeError(f"Failed to initialize {LANG_NAMES.get(lang, lang)} pipeline.{suffix}")
|
| 291 |
+
|
| 292 |
+
|
| 293 |
+
def synthesize(args: argparse.Namespace) -> None:
|
| 294 |
+
require_assets()
|
| 295 |
+
require_python_version()
|
| 296 |
+
voice_files = resolve_voice_files(args.voice)
|
| 297 |
+
lang = infer_lang(voice_files, args.lang)
|
| 298 |
+
text = read_text(args).strip()
|
| 299 |
+
if not text:
|
| 300 |
+
raise SystemExit("No text to synthesize.")
|
| 301 |
+
|
| 302 |
+
np, sf, torch, KModel, KPipeline = import_runtime()
|
| 303 |
+
device = select_device(torch, args.device)
|
| 304 |
+
voice_tensor = load_voice_tensor(torch, voice_files)
|
| 305 |
+
|
| 306 |
+
model = KModel(repo_id=REPO_ID, config=str(CONFIG_FILE), model=str(MODEL_FILE))
|
| 307 |
+
model = model.to(device).eval()
|
| 308 |
+
|
| 309 |
+
try:
|
| 310 |
+
pipeline = KPipeline(lang_code=lang, repo_id=REPO_ID, model=model)
|
| 311 |
+
except Exception as exc:
|
| 312 |
+
raise language_dependency_hint(lang, exc) from exc
|
| 313 |
+
|
| 314 |
+
audios = []
|
| 315 |
+
generator = pipeline(
|
| 316 |
+
text,
|
| 317 |
+
voice=voice_tensor,
|
| 318 |
+
speed=args.speed,
|
| 319 |
+
split_pattern=args.split_pattern,
|
| 320 |
+
)
|
| 321 |
+
|
| 322 |
+
for index, result in enumerate(generator):
|
| 323 |
+
if args.verbose:
|
| 324 |
+
print(f"[{index}] {result.graphemes}")
|
| 325 |
+
print(f" phonemes: {result.phonemes}")
|
| 326 |
+
audio = result.audio
|
| 327 |
+
if audio is None:
|
| 328 |
+
continue
|
| 329 |
+
if hasattr(audio, "detach"):
|
| 330 |
+
audio = audio.detach().cpu().numpy()
|
| 331 |
+
audios.append(np.asarray(audio, dtype=np.float32))
|
| 332 |
+
|
| 333 |
+
if not audios:
|
| 334 |
+
raise SystemExit("Kokoro did not generate any audio. Check the text and language.")
|
| 335 |
+
|
| 336 |
+
if len(audios) == 1 or args.gap <= 0:
|
| 337 |
+
merged = np.concatenate(audios)
|
| 338 |
+
else:
|
| 339 |
+
silence = np.zeros(max(0, int(SAMPLE_RATE * args.gap)), dtype=np.float32)
|
| 340 |
+
chunks = []
|
| 341 |
+
for index, audio in enumerate(audios):
|
| 342 |
+
if index:
|
| 343 |
+
chunks.append(silence)
|
| 344 |
+
chunks.append(audio)
|
| 345 |
+
merged = np.concatenate(chunks)
|
| 346 |
+
|
| 347 |
+
output = args.output.expanduser()
|
| 348 |
+
output.parent.mkdir(parents=True, exist_ok=True)
|
| 349 |
+
sf.write(str(output), merged, SAMPLE_RATE)
|
| 350 |
+
|
| 351 |
+
voice_names = ",".join(path.stem for path in voice_files)
|
| 352 |
+
print(f"Wrote {output} using voice={voice_names}, lang={lang}, device={device}")
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def main() -> None:
|
| 356 |
+
args = parse_args()
|
| 357 |
+
if args.list_voices:
|
| 358 |
+
require_assets()
|
| 359 |
+
print_voices()
|
| 360 |
+
return
|
| 361 |
+
try:
|
| 362 |
+
synthesize(args)
|
| 363 |
+
except RuntimeError as exc:
|
| 364 |
+
raise SystemExit(str(exc)) from exc
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
if __name__ == "__main__":
|
| 368 |
+
main()
|
kokoro/voices/af_alloy.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6d877149dd8b348fbad12e5845b7e43d975390e9f3b68a811d1d86168bef5aa3
|
| 3 |
+
size 523425
|
kokoro/voices/af_aoede.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c03bd1a4c3716c2d8eaa3d50022f62d5c31cfbd6e15933a00b17fefe13841cc4
|
| 3 |
+
size 523425
|
kokoro/voices/af_bella.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8cb64e02fcc8de0327a8e13817e49c76c945ecf0052ceac97d3081480e8e48d6
|
| 3 |
+
size 523425
|
kokoro/voices/af_heart.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0ab5709b8ffab19bfd849cd11d98f75b60af7733253ad0d67b12382a102cb4ff
|
| 3 |
+
size 523425
|
kokoro/voices/af_jessica.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cdfdccb8cc975aa34ee6b89642963b0064237675de0e41a30ae64cc958dd4e87
|
| 3 |
+
size 523435
|
kokoro/voices/af_kore.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8bfbc512321c3db49dff984ac675fa5ac7eaed5a96cc31104d3a9080e179d69d
|
| 3 |
+
size 523420
|
kokoro/voices/af_nicole.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c5561808bcf5250fe8c5f5de32caf2d94f27e57e95befdb098c5c85991d4c5da
|
| 3 |
+
size 523430
|
kokoro/voices/af_nova.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e0233676ddc21908c37a1f102f6b88a59e4e5c1bd764983616eb9eda629dbcd2
|
| 3 |
+
size 523420
|
kokoro/voices/af_river.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e149459bd9c084416b74756b9bd3418256a8b839088abb07d463730c369dab8f
|
| 3 |
+
size 523425
|
kokoro/voices/af_sarah.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:49bd364ea3be9eb3e9685e8f9a15448c4883112a7c0ff7ab139fa4088b08cef9
|
| 3 |
+
size 523425
|
kokoro/voices/af_sky.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c799548aed06e0cb0d655a85a01b48e7f10484d71663f9a3045a5b9362e8512c
|
| 3 |
+
size 523351
|
kokoro/voices/am_adam.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ced7e284aba12472891be1da3ab34db84cc05cc02b5889535796dbf2d8b0cb34
|
| 3 |
+
size 523420
|
kokoro/voices/am_echo.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8bcfdc852bc985fb45c396c561e571ffb9183930071f962f1b50df5c97b161e8
|
| 3 |
+
size 523420
|
kokoro/voices/am_eric.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ada66f0eefff34ec921b1d7474d7ac8bec00cd863c170f1c534916e9b8212aae
|
| 3 |
+
size 523420
|
kokoro/voices/am_fenrir.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:98e507eca1db08230ae3b6232d59c10aec9630022d19accac4f5d12fcec3c37a
|
| 3 |
+
size 523430
|
kokoro/voices/am_liam.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c82550757ddb31308b97f30040dda8c2d609a9e2de6135848d0a948368138518
|
| 3 |
+
size 523420
|
kokoro/voices/am_michael.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9a443b79a4b22489a5b0ab7c651a0bcd1a30bef675c28333f06971abbd47bd37
|
| 3 |
+
size 523435
|
kokoro/voices/am_onyx.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e8452be16cd0f6da7b4579eaf7b1e4506e92524882053d86d72b96b9a7fed584
|
| 3 |
+
size 523420
|
kokoro/voices/am_puck.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:dd1d8973f4ce4b7d8ae407c77a435f485dabc052081b80ea75c4f30b84f36223
|
| 3 |
+
size 523420
|
kokoro/voices/am_santa.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7f2f7582fa2b1f160e90aafe6d0b442a685e773608b6667e545d743b073e97a7
|
| 3 |
+
size 523425
|
kokoro/voices/bf_alice.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d292651b6af6c0d81705c2580dcb4463fccc0ff7b8d618a471dbb4e45655b3f3
|
| 3 |
+
size 523425
|
kokoro/voices/bf_emma.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d0a423deabf4a52b4f49318c51742c54e21bb89bbbe9a12141e7758ddb5da701
|
| 3 |
+
size 523420
|
kokoro/voices/bf_isabella.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:cdd4c37003805104d1d08fb1e05855c8fb2c68de24ca6e71f264a30aaa59eefd
|
| 3 |
+
size 523440
|
kokoro/voices/bf_lily.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6e09c2e481e2d53004d7e5ae7d3a325369e130a6f45c35a6002de75084be9285
|
| 3 |
+
size 523420
|
kokoro/voices/bm_daniel.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fc3fce4e9c12ed4dbc8fa9680cfe51ee190a96444ce7c3ad647549a30823fc5d
|
| 3 |
+
size 523430
|
kokoro/voices/bm_fable.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d44935f3135257a9064df99f007fc1342ff1aa767552b4a4fa4c3b2e6e59079c
|
| 3 |
+
size 523425
|
kokoro/voices/bm_george.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f1bc812213dc59774769e5c80004b13eeb79bd78130b11b2d7f934542dab811b
|
| 3 |
+
size 523430
|
kokoro/voices/bm_lewis.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b5204750dcba01029d2ac9cec17aec3b20a6d64073c579d694a23cb40effbd0e
|
| 3 |
+
size 523425
|
kokoro/voices/ef_dora.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d9d69b0f8a2b87a345f269d89639f89dfbd1a6c9da0c498ae36dd34afcf35530
|
| 3 |
+
size 523420
|