AGofficial commited on
Commit
e95022e
·
verified ·
1 Parent(s): 791589f

Upload 96 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +16 -0
  2. backgrounds/bussiness_room.jpeg +3 -0
  3. characters/black_haired_girl.png +3 -0
  4. characters/bussinessman.png +3 -0
  5. characters/pink_haired_girl.png +3 -0
  6. dream_chat_engine/README.md +9 -0
  7. dream_chat_engine/__init__.py +17 -0
  8. dream_chat_engine/actions.py +68 -0
  9. dream_chat_engine/assets.py +54 -0
  10. dream_chat_engine/character.py +17 -0
  11. dream_chat_engine/config.py +37 -0
  12. dream_chat_engine/demo.py +38 -0
  13. dream_chat_engine/dialogue.py +155 -0
  14. dream_chat_engine/engine.py +399 -0
  15. dream_chat_engine/renderer.py +272 -0
  16. kokoro/README.md +117 -0
  17. kokoro/VOICES.md +161 -0
  18. kokoro/config.json +150 -0
  19. kokoro/kokoro-v1_0.pth +3 -0
  20. kokoro/requirements.txt +5 -0
  21. kokoro/sample.py +368 -0
  22. kokoro/voices/af_alloy.pt +3 -0
  23. kokoro/voices/af_aoede.pt +3 -0
  24. kokoro/voices/af_bella.pt +3 -0
  25. kokoro/voices/af_heart.pt +3 -0
  26. kokoro/voices/af_jessica.pt +3 -0
  27. kokoro/voices/af_kore.pt +3 -0
  28. kokoro/voices/af_nicole.pt +3 -0
  29. kokoro/voices/af_nova.pt +3 -0
  30. kokoro/voices/af_river.pt +3 -0
  31. kokoro/voices/af_sarah.pt +3 -0
  32. kokoro/voices/af_sky.pt +3 -0
  33. kokoro/voices/am_adam.pt +3 -0
  34. kokoro/voices/am_echo.pt +3 -0
  35. kokoro/voices/am_eric.pt +3 -0
  36. kokoro/voices/am_fenrir.pt +3 -0
  37. kokoro/voices/am_liam.pt +3 -0
  38. kokoro/voices/am_michael.pt +3 -0
  39. kokoro/voices/am_onyx.pt +3 -0
  40. kokoro/voices/am_puck.pt +3 -0
  41. kokoro/voices/am_santa.pt +3 -0
  42. kokoro/voices/bf_alice.pt +3 -0
  43. kokoro/voices/bf_emma.pt +3 -0
  44. kokoro/voices/bf_isabella.pt +3 -0
  45. kokoro/voices/bf_lily.pt +3 -0
  46. kokoro/voices/bm_daniel.pt +3 -0
  47. kokoro/voices/bm_fable.pt +3 -0
  48. kokoro/voices/bm_george.pt +3 -0
  49. kokoro/voices/bm_lewis.pt +3 -0
  50. 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

  • SHA256: 000846d77ece39413246ffd6d684963558d930b4900a1a0934099c9eb5ac6fb1
  • Pointer size: 132 Bytes
  • Size of remote file: 2.6 MB
characters/black_haired_girl.png ADDED

Git LFS Details

  • SHA256: 26a16a1c496e0da27637a3cf56275c62d58af65d8bf145132cd61db09539346c
  • Pointer size: 132 Bytes
  • Size of remote file: 2.7 MB
characters/bussinessman.png ADDED

Git LFS Details

  • SHA256: a96c82d6215799cc7c9f03367b21859ed123c719e057c7efd4e3552fa4261321
  • Pointer size: 132 Bytes
  • Size of remote file: 2.58 MB
characters/pink_haired_girl.png ADDED

Git LFS Details

  • SHA256: 68fb1e960d4ed02fc617a351d1ec96e184f2870448d2fed314379a64b3ca43a0
  • Pointer size: 132 Bytes
  • Size of remote file: 2.1 MB
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