diff --git a/.gitattributes b/.gitattributes index a6344aac8c09253b3b630fb776ae94478aa0275b..76d473550de1b0b1256a0b3f63482cfd013501dc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -33,3 +33,19 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text *.zip filter=lfs diff=lfs merge=lfs -text *.zst filter=lfs diff=lfs merge=lfs -text *tfevents* filter=lfs diff=lfs merge=lfs -text +backgrounds/bussiness_room.jpeg filter=lfs diff=lfs merge=lfs -text +characters/black_haired_girl.png filter=lfs diff=lfs merge=lfs -text +characters/bussinessman.png filter=lfs diff=lfs merge=lfs -text +characters/pink_haired_girl.png filter=lfs diff=lfs merge=lfs -text +llm/dolphin.gguf filter=lfs diff=lfs merge=lfs -text +outputs/dream_chat_engine_preview.jpg filter=lfs diff=lfs merge=lfs -text +outputs/kokoro_sample.wav filter=lfs diff=lfs merge=lfs -text +outputs/kokoro_test.wav filter=lfs diff=lfs merge=lfs -text +outputs/tts/am_michael_1781378852989.wav filter=lfs diff=lfs merge=lfs -text +outputs/tts/am_michael_1781378862368.wav filter=lfs diff=lfs merge=lfs -text +outputs/tts/am_michael_1781378874715.wav filter=lfs diff=lfs merge=lfs -text +outputs/tts/am_michael_1781378894011.wav filter=lfs diff=lfs merge=lfs -text +outputs/tts/am_michael_1781378899096.wav filter=lfs diff=lfs merge=lfs -text +outputs/tts/am_michael_1781378909364.wav filter=lfs diff=lfs merge=lfs -text +outputs/tts/am_michael_1781378932377.wav filter=lfs diff=lfs merge=lfs -text +outputs/tts/am_michael_1781379206195.wav filter=lfs diff=lfs merge=lfs -text diff --git a/backgrounds/bussiness_room.jpeg b/backgrounds/bussiness_room.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..04da023a03e503e87f0f87cf94f690a68285696f --- /dev/null +++ b/backgrounds/bussiness_room.jpeg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:000846d77ece39413246ffd6d684963558d930b4900a1a0934099c9eb5ac6fb1 +size 2600084 diff --git a/characters/black_haired_girl.png b/characters/black_haired_girl.png new file mode 100644 index 0000000000000000000000000000000000000000..58c459cfd503e33db15feccf7092f364564c5fac --- /dev/null +++ b/characters/black_haired_girl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26a16a1c496e0da27637a3cf56275c62d58af65d8bf145132cd61db09539346c +size 2697769 diff --git a/characters/bussinessman.png b/characters/bussinessman.png new file mode 100644 index 0000000000000000000000000000000000000000..fd8bf034068db5e2cb44c18696ee9e19acae92a6 --- /dev/null +++ b/characters/bussinessman.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a96c82d6215799cc7c9f03367b21859ed123c719e057c7efd4e3552fa4261321 +size 2584395 diff --git a/characters/pink_haired_girl.png b/characters/pink_haired_girl.png new file mode 100644 index 0000000000000000000000000000000000000000..71f354549e44e6d65bd2bcaf1e96711c6b7c0603 --- /dev/null +++ b/characters/pink_haired_girl.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68fb1e960d4ed02fc617a351d1ec96e184f2870448d2fed314379a64b3ca43a0 +size 2098848 diff --git a/dream_chat_engine/README.md b/dream_chat_engine/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d22554f25028f67f13fa5b4a5a4aaaf695acc421 --- /dev/null +++ b/dream_chat_engine/README.md @@ -0,0 +1,9 @@ +# Dream Chat Engine + +A small Python visual chat engine with a RenPy-like script flow. + +Run the demo from the project root: + +```powershell +python dream_chat_engine/demo.py +``` \ No newline at end of file diff --git a/dream_chat_engine/__init__.py b/dream_chat_engine/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f99300d15a29699f64b686c462f8603bc17bf056 --- /dev/null +++ b/dream_chat_engine/__init__.py @@ -0,0 +1,17 @@ +"""A tiny RenPy-like visual chat engine for Python games.""" + +from .actions import Ask, Generate, GeneratedReply, Hide, Say, SetBackground, Show +from .config import EngineConfig +from .engine import DreamChatEngine + +__all__ = [ + "Ask", + "DreamChatEngine", + "EngineConfig", + "Generate", + "GeneratedReply", + "Hide", + "Say", + "SetBackground", + "Show", +] diff --git a/dream_chat_engine/actions.py b/dream_chat_engine/actions.py new file mode 100644 index 0000000000000000000000000000000000000000..d0462d2b0ebc3f6ccd9c128f9f4f5c4aed223623 --- /dev/null +++ b/dream_chat_engine/actions.py @@ -0,0 +1,68 @@ +"""Script actions used by Dream Chat Engine demos and games.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Callable, Iterable, Literal, Sequence + +AssetPath = str | Path +Position = Literal["left", "center", "right"] | float +InputRule = Literal["any", "letters"] +DialogueHistory = list[dict[str, str]] +ReplyProducer = Callable[[dict[str, str], DialogueHistory], str | Iterable[str]] + + +@dataclass(frozen=True) +class SetBackground: + image: AssetPath + + +@dataclass(frozen=True) +class Show: + name: str + image: AssetPath + position: Position = "center" + tag: str | None = None + + +@dataclass(frozen=True) +class Hide: + tag: str + + +@dataclass(frozen=True) +class Say: + speaker: str + text: str + + +@dataclass(frozen=True) +class GeneratedReply: + text: str + actions: Sequence[Any] = () + variables: dict[str, str] = field(default_factory=dict) + + +ReplyPostprocessor = Callable[ + [str, dict[str, str], DialogueHistory], + str | GeneratedReply, +] + + +@dataclass(frozen=True) +class Generate: + speaker: str + producer: ReplyProducer + postprocess: ReplyPostprocessor | None = None + + +@dataclass(frozen=True) +class Ask: + variable: str + prompt: str + speaker: str = "Stranger" + response_speaker: str | None = None + max_chars: int | None = None + input_rule: InputRule = "any" + min_chars: int = 0 diff --git a/dream_chat_engine/assets.py b/dream_chat_engine/assets.py new file mode 100644 index 0000000000000000000000000000000000000000..7999d389b4a8cb5a00ad86cf6875aceed29f24d4 --- /dev/null +++ b/dream_chat_engine/assets.py @@ -0,0 +1,54 @@ +"""Asset loading helpers for backgrounds and character sprites.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Iterable + +from PIL import Image + + +class AssetNotFoundError(FileNotFoundError): + """Raised when an asset cannot be resolved from the project folders.""" + + +class AssetManager: + def __init__(self, project_root: str | Path) -> None: + self.project_root = Path(project_root).resolve() + self.characters_dir = self.project_root / "characters" + self.backgrounds_dir = self.project_root / "backgrounds" + + def load_background(self, asset: str | Path) -> Image.Image: + path = self.resolve(asset, (self.backgrounds_dir, self.project_root)) + return Image.open(path).convert("RGBA") + + def load_character(self, asset: str | Path) -> Image.Image: + path = self.resolve(asset, (self.characters_dir, self.project_root)) + image = Image.open(path).convert("RGBA") + return self._crop_alpha(image) + + def resolve(self, asset: str | Path, roots: Iterable[Path]) -> Path: + asset_path = Path(asset) + if asset_path.is_absolute() and asset_path.exists(): + return asset_path + + candidates: list[Path] = [] + for root in roots: + candidates.append(root / asset_path) + if asset_path.suffix: + continue + candidates.extend(sorted(root.glob(f"{asset_path.name}.*"))) + + for candidate in candidates: + if candidate.exists() and candidate.is_file(): + return candidate.resolve() + + searched = ", ".join(str(root) for root in roots) + raise AssetNotFoundError(f"Could not find asset {asset!r} in {searched}.") + + @staticmethod + def _crop_alpha(image: Image.Image) -> Image.Image: + bbox = image.getbbox() + if bbox is None: + return image + return image.crop(bbox) diff --git a/dream_chat_engine/character.py b/dream_chat_engine/character.py new file mode 100644 index 0000000000000000000000000000000000000000..e9182b0eec33c5a98915bc4de983744c214aa1de --- /dev/null +++ b/dream_chat_engine/character.py @@ -0,0 +1,17 @@ +"""Runtime character sprite model.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from PIL import Image + +from .actions import Position + + +@dataclass +class CharacterSprite: + tag: str + name: str + image: Image.Image + position: Position = "center" diff --git a/dream_chat_engine/config.py b/dream_chat_engine/config.py new file mode 100644 index 0000000000000000000000000000000000000000..379242ebc6c70d87f6faa5b6c0c363ac20165bd9 --- /dev/null +++ b/dream_chat_engine/config.py @@ -0,0 +1,37 @@ +"""Configuration values for the Dream Chat Engine window and renderer.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class EngineConfig: + title: str = "Dream Chat Engine" + width: int = 1280 + height: int = 720 + min_width: int = 800 + min_height: int = 450 + fps: int = 30 + + text_speed: float = 42.0 + max_input_chars: int = 240 + + character_max_height_ratio: float = 0.92 + character_max_width_ratio: float = 0.46 + + dialogue_margin: int = 24 + dialogue_height: int = 188 + dialogue_radius: int = 8 + dialogue_alpha: int = 205 + dialogue_padding_x: int = 30 + dialogue_padding_y: int = 18 + + name_font_size: int = 28 + text_font_size: int = 30 + prompt_font_size: int = 23 + line_spacing: int = 8 + + auto_advance: bool = False + end_speaker: str = "Demo" + end_message: str | None = "End of demo." diff --git a/dream_chat_engine/demo.py b/dream_chat_engine/demo.py new file mode 100644 index 0000000000000000000000000000000000000000..d552c5954cef62ab891ff5b9c971de27b3dc02f1 --- /dev/null +++ b/dream_chat_engine/demo.py @@ -0,0 +1,38 @@ +"""Hardcoded demo game for Dream Chat Engine.""" + +from __future__ import annotations + +from pathlib import Path +import sys + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from dream_chat_engine import Ask, DreamChatEngine, EngineConfig, Hide, Say, SetBackground, Show + + +def build_script(): + return [ + SetBackground("bussiness_room.jpeg"), + Show("Mira", "pink_haired_girl.png", position="left", tag="mira"), + Say("Mira", "Hey. You made it. The room, the textbox, and the sprite layer are all live."), + Show("Mr. Vale", "bussinessman.png", position="right", tag="vale"), + Say("Mr. Vale", "Characters are loaded from the characters folder and kept flush with the bottom of the screen."), + Say("Mira", "The text prints itself out, but a click or the space bar can hurry it along."), + Ask("first_reply", "What do you say back?"), + Say("Mr. Vale", "Good. That reply was typed freely, stored, and then played as a Stranger line."), + Hide("vale"), + Show("Nyx", "black_haired_girl.png", position="center", tag="nyx"), + Say("Nyx", "That is enough for a first test. Drop more actions into demo.py and it becomes a tiny visual novel."), + ] + + +def main() -> None: + config = EngineConfig(title="Dream Chat Engine Demo") + engine = DreamChatEngine(build_script(), project_root=PROJECT_ROOT, config=config) + engine.run() + + +if __name__ == "__main__": + main() diff --git a/dream_chat_engine/dialogue.py b/dream_chat_engine/dialogue.py new file mode 100644 index 0000000000000000000000000000000000000000..07e94f85c379f705e5c861b0cfe41881beaccd4f --- /dev/null +++ b/dream_chat_engine/dialogue.py @@ -0,0 +1,155 @@ +"""Dialogue and keyboard-input state.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class DialogueState: + text_speed: float + mode: str = "idle" + speaker: str = "" + full_text: str = "" + visible_text: str = "" + prompt: str = "" + input_text: str = "" + caret_index: int = 0 + awaiting_stream: bool = False + _loading_elapsed: float = 0.0 + _revealed_chars: float = 0.0 + + def start_say(self, speaker: str, text: str) -> None: + self.mode = "say" + self.speaker = speaker + self.full_text = text + self.visible_text = "" + self.prompt = "" + self.input_text = "" + self.caret_index = 0 + self.awaiting_stream = False + self._loading_elapsed = 0.0 + self._revealed_chars = 0.0 + + def start_stream(self, speaker: str) -> None: + self.start_say(speaker, "") + self.awaiting_stream = True + + def start_loading(self, speaker: str) -> None: + self.mode = "loading" + self.speaker = speaker + self.full_text = "" + self.visible_text = "." + self.prompt = "" + self.input_text = "" + self.caret_index = 0 + self.awaiting_stream = True + self._loading_elapsed = 0.0 + self._revealed_chars = 0.0 + + def start_input(self, speaker: str, prompt: str) -> None: + self.mode = "input" + self.speaker = speaker + self.full_text = "" + self.visible_text = "" + self.prompt = prompt + self.input_text = "" + self.caret_index = 0 + self.awaiting_stream = False + self._loading_elapsed = 0.0 + self._revealed_chars = 0.0 + + def update(self, delta_seconds: float) -> None: + if self.mode == "loading": + self._loading_elapsed += delta_seconds + phase = int(self._loading_elapsed / 0.36) % 4 + self.visible_text = "." * phase + return + + if self.mode != "say" or self.is_complete: + return + + self._revealed_chars = min( + len(self.full_text), + self._revealed_chars + self.text_speed * delta_seconds, + ) + self.visible_text = self.full_text[: int(self._revealed_chars)] + + @property + def is_complete(self) -> bool: + return ( + self.mode == "say" + and not self.awaiting_stream + and self.visible_text == self.full_text + ) + + def reveal_all(self) -> None: + if self.mode == "say": + self.visible_text = self.full_text + self._revealed_chars = float(len(self.full_text)) + + def append_stream_text(self, text: str) -> None: + if self.mode == "say" and text: + self.full_text += text + + def finish_stream(self) -> None: + self.awaiting_stream = False + + def insert_text(self, text: str, max_chars: int) -> None: + if self.mode != "input" or not text: + return + allowed = "".join(ch for ch in text if ch >= " " and ch != "\x7f") + if not allowed: + return + remaining = max_chars - len(self.input_text) + if remaining <= 0: + return + allowed = allowed[:remaining] + self.input_text = ( + self.input_text[: self.caret_index] + + allowed + + self.input_text[self.caret_index :] + ) + self.caret_index += len(allowed) + + def backspace(self) -> None: + if self.mode == "input" and self.caret_index > 0: + self.input_text = ( + self.input_text[: self.caret_index - 1] + + self.input_text[self.caret_index :] + ) + self.caret_index -= 1 + + def delete(self) -> None: + if self.mode == "input" and self.caret_index < len(self.input_text): + self.input_text = ( + self.input_text[: self.caret_index] + + self.input_text[self.caret_index + 1 :] + ) + + def move_caret(self, amount: int) -> None: + if self.mode == "input": + self.caret_index = max(0, min(len(self.input_text), self.caret_index + amount)) + + def move_caret_home(self) -> None: + if self.mode == "input": + self.caret_index = 0 + + def move_caret_end(self) -> None: + if self.mode == "input": + self.caret_index = len(self.input_text) + + def display_input_with_caret(self) -> str: + return ( + self.input_text[: self.caret_index] + + "|" + + self.input_text[self.caret_index :] + ) + + def submit_input(self) -> str: + response = self.input_text.strip() + self.mode = "idle" + self.prompt = "" + self.input_text = "" + self.caret_index = 0 + return response diff --git a/dream_chat_engine/engine.py b/dream_chat_engine/engine.py new file mode 100644 index 0000000000000000000000000000000000000000..4be84490e1631c8b3ce6d3caa8aa858b625b94bf --- /dev/null +++ b/dream_chat_engine/engine.py @@ -0,0 +1,399 @@ +"""Window loop and script runner for Dream Chat Engine.""" + +from __future__ import annotations + +import queue +import threading +import time +import tkinter as tk +from collections import OrderedDict +from pathlib import Path +from typing import Any, Sequence + +from PIL import Image, ImageTk + +from .actions import Ask, Generate, GeneratedReply, Hide, Say, SetBackground, Show +from .assets import AssetManager +from .character import CharacterSprite +from .config import EngineConfig +from .dialogue import DialogueState +from .renderer import SceneRenderer + +ScriptAction = SetBackground | Show | Hide | Say | Generate | Ask + + +class DreamChatEngine: + def __init__( + self, + script: Sequence[ScriptAction], + project_root: str | Path, + config: EngineConfig | None = None, + voice_manager: object | None = None, + ) -> None: + self.script = list(script) + self.config = config or EngineConfig() + self.assets = AssetManager(project_root) + self.dialogue = DialogueState(text_speed=self.config.text_speed) + self.renderer = SceneRenderer(self.config) + self.voice_manager = voice_manager + self.background: Image.Image | None = None + self.characters: OrderedDict[str, CharacterSprite] = OrderedDict() + self.variables: dict[str, str] = {} + self.history: list[dict[str, str]] = [] + + self._index = 0 + self._active_ask: Ask | None = None + self._generation_queue: queue.Queue[dict[str, Any]] | None = None + self._generation_thread: threading.Thread | None = None + self._speech_handle: Any | None = None + self._demo_finished = False + self._last_tick = time.perf_counter() + + self._root: tk.Tk | None = None + self._canvas: tk.Canvas | None = None + self._photo: ImageTk.PhotoImage | None = None + + def run(self) -> None: + self._root = tk.Tk() + self._root.title(self.config.title) + self._root.geometry(f"{self.config.width}x{self.config.height}") + self._root.minsize(self.config.min_width, self.config.min_height) + + self._canvas = tk.Canvas(self._root, highlightthickness=0, bg="black") + self._canvas.pack(fill=tk.BOTH, expand=True) + self._canvas.focus_set() + + self._root.bind("", self._on_key) + self._root.bind("", self._on_click) + self._canvas.bind("", self._on_click) + + self._advance_script() + self._tick() + self._root.mainloop() + + def _tick(self) -> None: + now = time.perf_counter() + delta = now - self._last_tick + self._last_tick = now + self.dialogue.update(delta) + self._drain_generation_queue() + self._maybe_auto_advance() + self._render() + + if self._root is not None: + delay_ms = max(1, int(1000 / self.config.fps)) + self._root.after(delay_ms, self._tick) + + def _render(self) -> None: + if self._canvas is None: + return + + width = max(1, self._canvas.winfo_width()) + height = max(1, self._canvas.winfo_height()) + if width <= 1 or height <= 1: + width, height = self.config.width, self.config.height + + frame = self.renderer.compose( + (width, height), + self.background, + self.characters.values(), + self.dialogue, + ) + self._photo = ImageTk.PhotoImage(frame) + self._canvas.delete("all") + self._canvas.create_image(0, 0, anchor=tk.NW, image=self._photo) + + def _advance_script(self) -> None: + self._active_ask = None + self._speech_handle = None + + while self._index < len(self.script): + action = self.script[self._index] + self._index += 1 + + if isinstance(action, SetBackground): + self.background = self.assets.load_background(action.image) + continue + + if isinstance(action, Show): + tag = action.tag or action.name + self.characters[tag] = CharacterSprite( + tag=tag, + name=action.name, + image=self.assets.load_character(action.image), + position=action.position, + ) + continue + + if isinstance(action, Hide): + self.characters.pop(action.tag, None) + continue + + if isinstance(action, Say): + speaker = self._format(action.speaker) + text = self._format(action.text) + self.dialogue.start_say(speaker, text) + self._record_line(speaker, text) + self._speech_handle = self._speak(speaker, text) + return + + if isinstance(action, Generate): + self._start_generated_reply(action) + return + + if isinstance(action, Ask): + self._active_ask = action + self.dialogue.start_input( + self._format(action.speaker), + self._format(action.prompt), + ) + return + + if self.config.end_message is not None and not self._demo_finished: + self._demo_finished = True + self.dialogue.start_say(self.config.end_speaker, self.config.end_message) + else: + self.close() + + def _on_click(self, _event: tk.Event) -> None: + if self.dialogue.mode == "input": + return + self._continue_or_skip() + + def _on_key(self, event: tk.Event) -> None: + keysym = event.keysym + + if self.dialogue.mode == "input": + self._handle_input_key(event) + return + + if keysym in {"space", "Return"}: + self._continue_or_skip() + + def _continue_or_skip(self) -> None: + if self.config.auto_advance or self.dialogue.mode == "loading": + return + if not self._speech_is_done(): + return + if self.dialogue.mode != "say": + return + + if not self.dialogue.is_complete: + self.dialogue.reveal_all() + return + + self._advance_script() + + def _handle_input_key(self, event: tk.Event) -> None: + keysym = event.keysym + + if keysym == "Return": + self._submit_input() + elif keysym == "BackSpace": + self.dialogue.backspace() + elif keysym == "Delete": + self.dialogue.delete() + elif keysym == "Left": + self.dialogue.move_caret(-1) + elif keysym == "Right": + self.dialogue.move_caret(1) + elif keysym == "Home": + self.dialogue.move_caret_home() + elif keysym == "End": + self.dialogue.move_caret_end() + elif event.char: + self._insert_input_text(event.char) + + def _insert_input_text(self, text: str) -> None: + max_chars = self.config.max_input_chars + if self._active_ask is not None and self._active_ask.max_chars is not None: + max_chars = self._active_ask.max_chars + + if self._active_ask is not None and self._active_ask.input_rule == "letters": + text = "".join(ch for ch in text if ch.isalpha()) + + self.dialogue.insert_text(text, max_chars) + + def _submit_input(self) -> None: + if self._active_ask is None: + return + + response = self.dialogue.submit_input() + if len(response) < self._active_ask.min_chars: + self.dialogue.start_input( + self._format(self._active_ask.speaker), + self._format(self._active_ask.prompt), + ) + return + + if not response: + response = "..." + + self.variables[self._active_ask.variable] = response + response_speaker = self._active_ask.response_speaker or self._active_ask.speaker + speaker = self._format(response_speaker) + self.dialogue.start_say(speaker, response) + self._record_line(speaker, response) + self._speech_handle = self._speak(speaker, response) + self._active_ask = None + + def _start_generated_reply(self, action: Generate) -> None: + speaker = self._format(action.speaker) + self.dialogue.start_loading(speaker) + self._speech_handle = None + self._generation_queue = queue.Queue() + + variables = dict(self.variables) + history = [dict(item) for item in self.history] + + def worker() -> None: + try: + reply = action.producer(variables, history) + if isinstance(reply, str): + raw_text = reply + else: + raw_text = "".join(str(chunk) for chunk in reply if chunk) + + generated = self._postprocess_generated(action, raw_text, variables, history) + prepared_audio = self._prepare_speech(speaker, generated.text) + self._generation_queue.put( + { + "text": generated.text, + "actions": list(generated.actions), + "variables": dict(generated.variables), + "prepared_audio": prepared_audio, + } + ) + except Exception as exc: + self._generation_queue.put( + { + "text": f"I must pause. {exc}", + "actions": [], + "variables": {}, + "prepared_audio": None, + } + ) + + self._generation_thread = threading.Thread(target=worker, daemon=True) + self._generation_thread.start() + + def _drain_generation_queue(self) -> None: + if self._generation_queue is None: + return + + try: + payload = self._generation_queue.get_nowait() + except queue.Empty: + return + + speaker = self.dialogue.speaker + text = str(payload.get("text") or "").strip() + if not text: + text = "We will discuss that in due time." + + self.variables.update(payload.get("variables") or {}) + self.insert_actions(payload.get("actions") or []) + + self.dialogue.start_say(speaker, text) + self._record_line(speaker, text) + self._speech_handle = self._speak( + speaker, + text, + prepared_audio=payload.get("prepared_audio"), + ) + self._generation_queue = None + self._generation_thread = None + + def _postprocess_generated( + self, + action: Generate, + raw_text: str, + variables: dict[str, str], + history: list[dict[str, str]], + ) -> GeneratedReply: + text = raw_text.strip() + if action.postprocess is None: + return GeneratedReply(text=text) + + processed = action.postprocess(text, variables, history) + if isinstance(processed, GeneratedReply): + return processed + return GeneratedReply(text=str(processed).strip()) + + def insert_actions(self, actions: Sequence[ScriptAction]) -> None: + if not actions: + return + self.script[self._index : self._index] = list(actions) + + def _maybe_auto_advance(self) -> None: + if not self.config.auto_advance: + return + if self.dialogue.mode != "say": + return + if not self.dialogue.is_complete: + return + if not self._speech_is_done(): + return + self._advance_script() + + def _record_line(self, speaker: str, text: str) -> None: + self.history.append({"speaker": speaker, "text": text}) + + def _prepare_speech(self, speaker: str, text: str) -> Any | None: + if self.voice_manager is None: + return None + prepare = getattr(self.voice_manager, "prepare", None) + if prepare is None: + return None + try: + return prepare(speaker, text) + except Exception: + return None + + def _speak(self, speaker: str, text: str, prepared_audio: Any | None = None) -> Any | None: + if self.voice_manager is None: + return None + speak = getattr(self.voice_manager, "speak", None) + if speak is None: + return None + try: + return speak(speaker, text, prepared_audio=prepared_audio) + except Exception: + return None + + def _speech_is_done(self) -> bool: + if self._speech_handle is None: + return True + is_done = getattr(self._speech_handle, "is_done", None) + if is_done is not None: + try: + return bool(is_done()) + except Exception: + return True + done = getattr(self._speech_handle, "done", None) + if done is not None: + return bool(done) + return True + + def close(self) -> None: + if self.voice_manager is not None: + stop = getattr(self.voice_manager, "stop", None) + if stop is not None: + try: + stop() + except Exception: + pass + if self._root is not None: + self._root.destroy() + self._root = None + + def _format(self, value: str) -> str: + try: + return value.format_map(_SafeVariables(self.variables)) + except (KeyError, ValueError): + return value + + +class _SafeVariables(dict[str, str]): + def __missing__(self, key: str) -> str: + return "{" + key + "}" diff --git a/dream_chat_engine/renderer.py b/dream_chat_engine/renderer.py new file mode 100644 index 0000000000000000000000000000000000000000..fcb8f34a2c179cbf0db91822b274b0cee809f065 --- /dev/null +++ b/dream_chat_engine/renderer.py @@ -0,0 +1,272 @@ +"""Pillow renderer used by the Tkinter window.""" + +from __future__ import annotations + +from collections.abc import Iterable +from pathlib import Path + +from PIL import Image, ImageDraw, ImageFont + +from .actions import Position +from .character import CharacterSprite +from .config import EngineConfig +from .dialogue import DialogueState + + +class SceneRenderer: + def __init__(self, config: EngineConfig) -> None: + self.config = config + self._stage_cache_key: tuple[object, ...] | None = None + self._stage_cache_image: Image.Image | None = None + self._font_cache: dict[tuple[int, bool], ImageFont.ImageFont] = {} + + def compose( + self, + size: tuple[int, int], + background: Image.Image | None, + characters: Iterable[CharacterSprite], + dialogue: DialogueState, + ) -> Image.Image: + width, height = size + frame = self._stage(size, background, tuple(characters)).copy() + self._draw_dialogue(frame, dialogue) + return frame.convert("RGB") + + def _stage( + self, + size: tuple[int, int], + background: Image.Image | None, + characters: tuple[CharacterSprite, ...], + ) -> Image.Image: + width, height = size + key = ( + width, + height, + id(background), + tuple((char.tag, id(char.image), char.position) for char in characters), + ) + if key == self._stage_cache_key and self._stage_cache_image is not None: + return self._stage_cache_image + + frame = Image.new("RGBA", size, (19, 21, 26, 255)) + if background is not None: + frame.alpha_composite(self._cover(background, size), (0, 0)) + + for character in characters: + self._paste_character(frame, character) + + self._stage_cache_key = key + self._stage_cache_image = frame + return frame + + def _cover(self, image: Image.Image, size: tuple[int, int]) -> Image.Image: + width, height = size + scale = max(width / image.width, height / image.height) + new_size = (max(1, int(image.width * scale)), max(1, int(image.height * scale))) + resized = image.resize(new_size, Image.Resampling.LANCZOS) + left = (resized.width - width) // 2 + top = (resized.height - height) // 2 + return resized.crop((left, top, left + width, top + height)) + + def _paste_character(self, frame: Image.Image, character: CharacterSprite) -> None: + width, height = frame.size + max_height = int(height * self.config.character_max_height_ratio) + max_width = int(width * self.config.character_max_width_ratio) + scale = min(max_height / character.image.height, max_width / character.image.width) + new_size = ( + max(1, int(character.image.width * scale)), + max(1, int(character.image.height * scale)), + ) + sprite = character.image.resize(new_size, Image.Resampling.LANCZOS) + anchor_x = self._position_to_x(character.position, width) + left = int(anchor_x - sprite.width / 2) + left = max(-sprite.width // 3, min(width - sprite.width * 2 // 3, left)) + top = height - sprite.height + frame.alpha_composite(sprite, (left, top)) + + @staticmethod + def _position_to_x(position: Position, width: int) -> float: + if isinstance(position, float): + return width * position + if position == "left": + return width * 0.28 + if position == "right": + return width * 0.72 + return width * 0.5 + + def _draw_dialogue(self, frame: Image.Image, dialogue: DialogueState) -> None: + if dialogue.mode not in {"say", "input", "loading"}: + return + + draw = ImageDraw.Draw(frame, "RGBA") + width, height = frame.size + margin = min(self.config.dialogue_margin, max(8, width // 30)) + panel_height = min(self.config.dialogue_height, height - margin * 2) + left = margin + top = height - panel_height - margin + right = width - margin + bottom = height - margin + + draw.rounded_rectangle( + (left, top, right, bottom), + radius=self.config.dialogue_radius, + fill=(0, 0, 0, self.config.dialogue_alpha), + ) + + name_font = self._font(self.config.name_font_size, bold=True) + text_font = self._font(self.config.text_font_size, bold=False) + prompt_font = self._font(self.config.prompt_font_size, bold=False) + pad_x = self.config.dialogue_padding_x + pad_y = self.config.dialogue_padding_y + text_left = left + pad_x + text_right = right - pad_x + name_y = top + pad_y + body_y = name_y + self.config.name_font_size + 14 + + if dialogue.speaker: + draw.text((text_left, name_y), dialogue.speaker, font=name_font, fill=(142, 220, 255, 255)) + + if dialogue.mode == "input": + if dialogue.prompt: + prompt_bottom = self._draw_wrapped( + draw, + dialogue.prompt, + prompt_font, + (232, 237, 243, 235), + (text_left, body_y, text_right, bottom - 62), + ) + else: + prompt_bottom = body_y + self._draw_wrapped( + draw, + dialogue.display_input_with_caret(), + text_font, + (255, 255, 255, 255), + (text_left, prompt_bottom + 12, text_right, bottom - pad_y), + ) + return + + if dialogue.mode == "loading": + self._draw_wrapped( + draw, + dialogue.visible_text, + text_font, + (255, 255, 255, 255), + (text_left, body_y, text_right, bottom - pad_y), + ) + return + + self._draw_wrapped( + draw, + dialogue.visible_text, + text_font, + (255, 255, 255, 255), + (text_left, body_y, text_right, bottom - pad_y), + ) + + if dialogue.is_complete and not self.config.auto_advance: + indicator_font = self._font(24, bold=True) + draw.text((right - 48, bottom - 42), ">", font=indicator_font, fill=(255, 255, 255, 220)) + + def _draw_wrapped( + self, + draw: ImageDraw.ImageDraw, + text: str, + font: ImageFont.ImageFont, + fill: tuple[int, int, int, int], + box: tuple[int, int, int, int], + ) -> int: + left, top, right, bottom = box + line_height = self._line_height(font) + self.config.line_spacing + lines = self._wrap_text(draw, text, font, max(1, right - left)) + max_lines = max(1, (bottom - top) // line_height) + if len(lines) > max_lines: + lines = lines[-max_lines:] + + y = top + for line in lines: + if y + line_height > bottom + self.config.line_spacing: + break + draw.text((left, y), line, font=font, fill=fill) + y += line_height + return y + + def _wrap_text( + self, + draw: ImageDraw.ImageDraw, + text: str, + font: ImageFont.ImageFont, + max_width: int, + ) -> list[str]: + if not text: + return [""] + + lines: list[str] = [] + for raw_line in text.splitlines() or [""]: + words = raw_line.split(" ") + current = "" + for word in words: + candidate = word if not current else f"{current} {word}" + if self._text_width(draw, candidate, font) <= max_width: + current = candidate + continue + if current: + lines.append(current) + current = self._break_long_word(draw, word, font, max_width, lines) + lines.append(current) + return lines + + def _break_long_word( + self, + draw: ImageDraw.ImageDraw, + word: str, + font: ImageFont.ImageFont, + max_width: int, + lines: list[str], + ) -> str: + current = "" + for char in word: + candidate = current + char + if self._text_width(draw, candidate, font) <= max_width: + current = candidate + else: + if current: + lines.append(current) + current = char + return current + + @staticmethod + def _text_width(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont) -> int: + bbox = draw.textbbox((0, 0), text, font=font) + return bbox[2] - bbox[0] + + @staticmethod + def _line_height(font: ImageFont.ImageFont) -> int: + bbox = font.getbbox("Ag") + return bbox[3] - bbox[1] + + def _font(self, size: int, bold: bool) -> ImageFont.ImageFont: + key = (size, bold) + if key in self._font_cache: + return self._font_cache[key] + + font_names = ( + ("segoeuib.ttf", "arialbd.ttf") if bold else ("segoeui.ttf", "arial.ttf") + ) + candidates: list[str | Path] = [] + windows_fonts = Path("C:/Windows/Fonts") + for name in font_names: + candidates.append(name) + candidates.append(windows_fonts / name) + + for candidate in candidates: + try: + font = ImageFont.truetype(str(candidate), size=size) + self._font_cache[key] = font + return font + except OSError: + continue + + font = ImageFont.load_default() + self._font_cache[key] = font + return font diff --git a/kokoro/README.md b/kokoro/README.md new file mode 100644 index 0000000000000000000000000000000000000000..d6dc124205ad567631d14cd8700f1af77a861c3f --- /dev/null +++ b/kokoro/README.md @@ -0,0 +1,117 @@ +--- +license: apache-2.0 +language: +- en +base_model: +- yl4579/StyleTTS2-LJSpeech +pipeline_tag: text-to-speech +--- +**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. + + + +๐Ÿˆ **GitHub**: https://github.com/hexgrad/kokoro + +๐Ÿš€ **Demo**: https://hf.co/spaces/hexgrad/Kokoro-TTS + +> [!NOTE] +> 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). +> +> 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. + +> [!CAUTION] +> 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. +> +> 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. + +- [Releases](#releases) +- [Usage](#usage) +- [EVAL.md](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/EVAL.md) โ†—๏ธ +- [SAMPLES.md](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/SAMPLES.md) โ†—๏ธ +- [VOICES.md](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md) โ†—๏ธ +- [Model Facts](#model-facts) +- [Training Details](#training-details) +- [Creative Commons Attribution](#creative-commons-attribution) +- [Acknowledgements](#acknowledgements) + +### Releases + +| Model | Published | Training Data | Langs & Voices | SHA256 | +| ----- | --------- | ------------- | -------------- | ------ | +| **v1.0** | **2025 Jan 27** | **Few hundred hrs** | [**8 & 54**](https://huggingface.co/hexgrad/Kokoro-82M/blob/main/VOICES.md) | `496dba11` | +| [v0.19](https://huggingface.co/hexgrad/kLegacy/tree/main/v0.19) | 2024 Dec 25 | <100 hrs | 1 & 10 | `3b0c392f` | + +| Training Costs | v0.19 | v1.0 | **Total** | +| -------------- | ----- | ---- | ----- | +| in A100 80GB GPU hours | 500 | 500 | **1000** | +| average hourly rate | $0.80/h | $1.20/h | **$1/h** | +| in USD | $400 | $600 | **$1000** | + +### Usage +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). +```py +!pip install -q kokoro>=0.9.2 soundfile +!apt-get -qq -y install espeak-ng > /dev/null 2>&1 +from kokoro import KPipeline +from IPython.display import display, Audio +import soundfile as sf +import torch +pipeline = KPipeline(lang_code='a') +text = ''' +[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. +''' +generator = pipeline(text, voice='af_heart') +for i, (gs, ps, audio) in enumerate(generator): + print(i, gs, ps) + display(Audio(data=audio, rate=24000, autoplay=i==0)) + sf.write(f'{i}.wav', audio, 24000) +``` +Under the hood, `kokoro` uses [`misaki`](https://pypi.org/project/misaki/), a G2P library at https://github.com/hexgrad/misaki + +### Model Facts + +**Architecture:** +- StyleTTS 2: https://arxiv.org/abs/2306.07691 +- ISTFTNet: https://arxiv.org/abs/2203.02395 +- Decoder only: no diffusion, no encoder release + +**Architected by:** Li et al @ https://github.com/yl4579/StyleTTS2 + +**Trained by**: `@rzvzn` on Discord + +**Languages:** Multiple + +**Model SHA256 Hash:** `496dba118d1a58f5f3db2efc88dbdc216e0483fc89fe6e47ee1f2c53f18ad1e4` + +### Training Details + +**Data:** Kokoro was trained exclusively on **permissive/non-copyrighted audio data** and IPA phoneme labels. Examples of permissive/non-copyrighted audio include: +- Public domain audio +- Audio licensed under Apache, MIT, etc +- Synthetic audio[1] generated by closed[2] TTS models from large providers
+[1] https://copyright.gov/ai/ai_policy_guidance.pdf
+[2] No synthetic audio from open TTS models or "custom voice clones" + +**Total Dataset Size:** A few hundred hours of audio + +**Total Training Cost:** About $1000 for 1000 hours of A100 80GB vRAM + +### Creative Commons Attribution + +The following CC BY audio was part of the dataset used to train Kokoro v1.0. + +| Audio Data | Duration Used | License | Added to Training Set After | +| ---------- | ------------- | ------- | --------------------------- | +| [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 | +| [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 | + +### Acknowledgements + +- ๐Ÿ› ๏ธ [@yl4579](https://huggingface.co/yl4579) for architecting StyleTTS 2. +- ๐Ÿ† [@Pendrokar](https://huggingface.co/Pendrokar) for adding Kokoro as a contender in the TTS Spaces Arena. +- ๐Ÿ“Š Thank you to everyone who contributed synthetic training data. +- โค๏ธ Special thanks to all compute sponsors. +- ๐Ÿ‘พ Discord server: https://discord.gg/QuGxSWBfQy +- ๐Ÿชฝ 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). + +kokoro diff --git a/kokoro/VOICES.md b/kokoro/VOICES.md new file mode 100644 index 0000000000000000000000000000000000000000..ed78a806ffa121525449a848624600eae34037c7 --- /dev/null +++ b/kokoro/VOICES.md @@ -0,0 +1,161 @@ +# Voices + +- ๐Ÿ‡บ๐Ÿ‡ธ [American English](#american-english): 11F 9M +- ๐Ÿ‡ฌ๐Ÿ‡ง [British English](#british-english): 4F 4M +- ๐Ÿ‡ฏ๐Ÿ‡ต [Japanese](#japanese): 4F 1M +- ๐Ÿ‡จ๐Ÿ‡ณ [Mandarin Chinese](#mandarin-chinese): 4F 4M +- ๐Ÿ‡ช๐Ÿ‡ธ [Spanish](#spanish): 1F 2M +- ๐Ÿ‡ซ๐Ÿ‡ท [French](#french): 1F +- ๐Ÿ‡ฎ๐Ÿ‡ณ [Hindi](#hindi): 2F 2M +- ๐Ÿ‡ฎ๐Ÿ‡น [Italian](#italian): 1F 1M +- ๐Ÿ‡ง๐Ÿ‡ท [Brazilian Portuguese](#brazilian-portuguese): 1F 2M + +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. + +Subjectively, voices will sound better or worse to different people. + +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). + +Most voices perform best on a "goldilocks range" of 100-200 tokens out of ~500 possible. Voices may perform worse at the extremes: +- **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. +- **Rushing** on long utterances, especially over 400 tokens. You can chunk down to shorter utterances or adjust the `speed` parameter to mitigate this. + +**Target Quality** +- How high quality is the reference voice? This grade may be impacted by audio quality, artifacts, compression, & sample rate. +- How well do the text labels match the audio? Text/audio misalignment (e.g. from hallucinations) will lower this grade. + +**Training Duration** +- How much audio was seen during training? Smaller durations result in a lower overall grade. +- 10 hours <= **HH hours** < 100 hours +- 1 hour <= H hours < 10 hours +- 10 minutes <= MM minutes < 100 minutes +- 1 minute <= _M minutes_ ๐Ÿค < 10 minutes + +### American English + +- `lang_code='a'` in [`misaki[en]`](https://github.com/hexgrad/misaki) +- espeak-ng `en-us` fallback + +| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 | +| ---- | ------ | -------------- | ----------------- | ------------- | ------ | +| **af\_heart** | ๐Ÿšบโค๏ธ | | | **A** | `0ab5709b` | +| af_alloy | ๐Ÿšบ | B | MM minutes | C | `6d877149` | +| af_aoede | ๐Ÿšบ | B | H hours | C+ | `c03bd1a4` | +| af_bella | ๐Ÿšบ๐Ÿ”ฅ | **A** | **HH hours** | **A-** | `8cb64e02` | +| af_jessica | ๐Ÿšบ | C | MM minutes | D | `cdfdccb8` | +| af_kore | ๐Ÿšบ | B | H hours | C+ | `8bfbc512` | +| af_nicole | ๐Ÿšบ๐ŸŽง | B | **HH hours** | B- | `c5561808` | +| af_nova | ๐Ÿšบ | B | MM minutes | C | `e0233676` | +| af_river | ๐Ÿšบ | C | MM minutes | D | `e149459b` | +| af_sarah | ๐Ÿšบ | B | H hours | C+ | `49bd364e` | +| af_sky | ๐Ÿšบ | B | _M minutes_ ๐Ÿค | C- | `c799548a` | +| am_adam | ๐Ÿšน | D | H hours | F+ | `ced7e284` | +| am_echo | ๐Ÿšน | C | MM minutes | D | `8bcfdc85` | +| am_eric | ๐Ÿšน | C | MM minutes | D | `ada66f0e` | +| am_fenrir | ๐Ÿšน | B | H hours | C+ | `98e507ec` | +| am_liam | ๐Ÿšน | C | MM minutes | D | `c8255075` | +| am_michael | ๐Ÿšน | B | H hours | C+ | `9a443b79` | +| am_onyx | ๐Ÿšน | C | MM minutes | D | `e8452be1` | +| am_puck | ๐Ÿšน | B | H hours | C+ | `dd1d8973` | +| am_santa | ๐Ÿšน | C | _M minutes_ ๐Ÿค | D- | `7f2f7582` | + +### British English + +- `lang_code='b'` in [`misaki[en]`](https://github.com/hexgrad/misaki) +- espeak-ng `en-gb` fallback + +| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 | +| ---- | ------ | -------------- | ----------------- | ------------- | ------ | +| bf_alice | ๐Ÿšบ | C | MM minutes | D | `d292651b` | +| bf_emma | ๐Ÿšบ | B | **HH hours** | B- | `d0a423de` | +| bf_isabella | ๐Ÿšบ | B | MM minutes | C | `cdd4c370` | +| bf_lily | ๐Ÿšบ | C | MM minutes | D | `6e09c2e4` | +| bm_daniel | ๐Ÿšน | C | MM minutes | D | `fc3fce4e` | +| bm_fable | ๐Ÿšน | B | MM minutes | C | `d44935f3` | +| bm_george | ๐Ÿšน | B | MM minutes | C | `f1bc8122` | +| bm_lewis | ๐Ÿšน | C | H hours | D+ | `b5204750` | + +### Japanese + +- `lang_code='j'` in [`misaki[ja]`](https://github.com/hexgrad/misaki) +- Total Japanese training data: H hours + +| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 | CC BY | +| ---- | ------ | -------------- | ----------------- | ------------- | ------ | ----- | +| jf_alpha | ๐Ÿšบ | B | H hours | C+ | `1bf4c9dc` | | +| jf_gongitsune | ๐Ÿšบ | B | MM minutes | C | `1b171917` | [gongitsune](https://github.com/koniwa/koniwa/blob/master/source/tnc/tnc__gongitsune.txt) | +| jf_nezumi | ๐Ÿšบ | B | _M minutes_ ๐Ÿค | C- | `d83f007a` | [nezuminoyomeiri](https://github.com/koniwa/koniwa/blob/master/source/tnc/tnc__nezuminoyomeiri.txt) | +| jf_tebukuro | ๐Ÿšบ | B | MM minutes | C | `0d691790` | [tebukurowokaini](https://github.com/koniwa/koniwa/blob/master/source/tnc/tnc__tebukurowokaini.txt) | +| jm_kumo | ๐Ÿšน | B | _M minutes_ ๐Ÿค | C- | `98340afd` | [kumonoito](https://github.com/koniwa/koniwa/blob/master/source/tnc/tnc__kumonoito.txt) | + +### Mandarin Chinese + +- `lang_code='z'` in [`misaki[zh]`](https://github.com/hexgrad/misaki) +- Total Mandarin Chinese training data: H hours + +| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 | +| ---- | ------ | -------------- | ----------------- | ------------- | ------ | +| zf_xiaobei | ๐Ÿšบ | C | MM minutes | D | `9b76be63` | +| zf_xiaoni | ๐Ÿšบ | C | MM minutes | D | `95b49f16` | +| zf_xiaoxiao | ๐Ÿšบ | C | MM minutes | D | `cfaf6f2d` | +| zf_xiaoyi | ๐Ÿšบ | C | MM minutes | D | `b5235dba` | +| zm_yunjian | ๐Ÿšน | C | MM minutes | D | `76cbf8ba` | +| zm_yunxi | ๐Ÿšน | C | MM minutes | D | `dbe6e1ce` | +| zm_yunxia | ๐Ÿšน | C | MM minutes | D | `bb2b03b0` | +| zm_yunyang | ๐Ÿšน | C | MM minutes | D | `5238ac22` | + +### Spanish + +- `lang_code='e'` in [`misaki[en]`](https://github.com/hexgrad/misaki) +- espeak-ng `es` + +| Name | Traits | SHA256 | +| ---- | ------ | ------ | +| ef_dora | ๐Ÿšบ | `d9d69b0f` | +| em_alex | ๐Ÿšน | `5eac53f7` | +| em_santa | ๐Ÿšน | `aa8620cb` | + +### French + +- `lang_code='f'` in [`misaki[en]`](https://github.com/hexgrad/misaki) +- espeak-ng `fr-fr` +- Total French training data: <11 hours + +| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 | CC BY | +| ---- | ------ | -------------- | ----------------- | ------------- | ------ | ----- | +| ff_siwis | ๐Ÿšบ | B | <11 hours | B- | `8073bf2d` | [SIWIS](https://datashare.ed.ac.uk/handle/10283/2353) | + +### Hindi + +- `lang_code='h'` in [`misaki[en]`](https://github.com/hexgrad/misaki) +- espeak-ng `hi` +- Total Hindi training data: H hours + +| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 | +| ---- | ------ | -------------- | ----------------- | ------------- | ------ | +| hf_alpha | ๐Ÿšบ | B | MM minutes | C | `06906fe0` | +| hf_beta | ๐Ÿšบ | B | MM minutes | C | `63c0a1a6` | +| hm_omega | ๐Ÿšน | B | MM minutes | C | `b55f02a8` | +| hm_psi | ๐Ÿšน | B | MM minutes | C | `2f0f055c` | + +### Italian + +- `lang_code='i'` in [`misaki[en]`](https://github.com/hexgrad/misaki) +- espeak-ng `it` +- Total Italian training data: H hours + +| Name | Traits | Target Quality | Training Duration | Overall Grade | SHA256 | +| ---- | ------ | -------------- | ----------------- | ------------- | ------ | +| if_sara | ๐Ÿšบ | B | MM minutes | C | `6c0b253b` | +| im_nicola | ๐Ÿšน | B | MM minutes | C | `234ed066` | + +### Brazilian Portuguese + +- `lang_code='p'` in [`misaki[en]`](https://github.com/hexgrad/misaki) +- espeak-ng `pt-br` + +| Name | Traits | SHA256 | +| ---- | ------ | ------ | +| pf_dora | ๐Ÿšบ | `07e4ff98` | +| pm_alex | ๐Ÿšน | `cf0ba8c5` | +| pm_santa | ๐Ÿšน | `d4210316` | diff --git a/kokoro/config.json b/kokoro/config.json new file mode 100644 index 0000000000000000000000000000000000000000..14a726edd3718279eac426630879ff743955b16a --- /dev/null +++ b/kokoro/config.json @@ -0,0 +1,150 @@ +{ + "istftnet": { + "upsample_kernel_sizes": [20, 12], + "upsample_rates": [10, 6], + "gen_istft_hop_size": 5, + "gen_istft_n_fft": 20, + "resblock_dilation_sizes": [ + [1, 3, 5], + [1, 3, 5], + [1, 3, 5] + ], + "resblock_kernel_sizes": [3, 7, 11], + "upsample_initial_channel": 512 + }, + "dim_in": 64, + "dropout": 0.2, + "hidden_dim": 512, + "max_conv_dim": 512, + "max_dur": 50, + "multispeaker": true, + "n_layer": 3, + "n_mels": 80, + "n_token": 178, + "style_dim": 128, + "text_encoder_kernel_size": 5, + "plbert": { + "hidden_size": 768, + "num_attention_heads": 12, + "intermediate_size": 2048, + "max_position_embeddings": 512, + "num_hidden_layers": 12, + "dropout": 0.1 + }, + "vocab": { + ";": 1, + ":": 2, + ",": 3, + ".": 4, + "!": 5, + "?": 6, + "โ€”": 9, + "โ€ฆ": 10, + "\"": 11, + "(": 12, + ")": 13, + "โ€œ": 14, + "โ€": 15, + " ": 16, + "\u0303": 17, + "สฃ": 18, + "สฅ": 19, + "สฆ": 20, + "สจ": 21, + "แต": 22, + "\uAB67": 23, + "A": 24, + "I": 25, + "O": 31, + "Q": 33, + "S": 35, + "T": 36, + "W": 39, + "Y": 41, + "แตŠ": 42, + "a": 43, + "b": 44, + "c": 45, + "d": 46, + "e": 47, + "f": 48, + "h": 50, + "i": 51, + "j": 52, + "k": 53, + "l": 54, + "m": 55, + "n": 56, + "o": 57, + "p": 58, + "q": 59, + "r": 60, + "s": 61, + "t": 62, + "u": 63, + "v": 64, + "w": 65, + "x": 66, + "y": 67, + "z": 68, + "ษ‘": 69, + "ษ": 70, + "ษ’": 71, + "รฆ": 72, + "ฮฒ": 75, + "ษ”": 76, + "ษ•": 77, + "รง": 78, + "ษ–": 80, + "รฐ": 81, + "สค": 82, + "ษ™": 83, + "ษš": 85, + "ษ›": 86, + "ษœ": 87, + "ษŸ": 90, + "ษก": 92, + "ษฅ": 99, + "ษจ": 101, + "ษช": 102, + "ส": 103, + "ษฏ": 110, + "ษฐ": 111, + "ล‹": 112, + "ษณ": 113, + "ษฒ": 114, + "ษด": 115, + "รธ": 116, + "ษธ": 118, + "ฮธ": 119, + "ล“": 120, + "ษน": 123, + "ษพ": 125, + "ษป": 126, + "ส": 128, + "ษฝ": 129, + "ส‚": 130, + "สƒ": 131, + "สˆ": 132, + "สง": 133, + "สŠ": 135, + "ส‹": 136, + "สŒ": 138, + "ษฃ": 139, + "ษค": 140, + "ฯ‡": 142, + "สŽ": 143, + "ส’": 147, + "ส”": 148, + "หˆ": 156, + "หŒ": 157, + "ห": 158, + "สฐ": 162, + "สฒ": 164, + "โ†“": 169, + "โ†’": 171, + "โ†—": 172, + "โ†˜": 173, + "แตป": 177 + } +} \ No newline at end of file diff --git a/kokoro/kokoro-v1_0.pth b/kokoro/kokoro-v1_0.pth new file mode 100644 index 0000000000000000000000000000000000000000..15769c415594dcb424579ba3dfae3085578aac4e --- /dev/null +++ b/kokoro/kokoro-v1_0.pth @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:496dba118d1a58f5f3db2efc88dbdc216e0483fc89fe6e47ee1f2c53f18ad1e4 +size 327212226 diff --git a/kokoro/requirements.txt b/kokoro/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..1c61941fd6d4c879c69c267c6e0af8aa60ca5814 --- /dev/null +++ b/kokoro/requirements.txt @@ -0,0 +1,5 @@ +# Use Python 3.10, 3.11, or 3.12. Kokoro 0.9.4 is not published for Python 3.13+. +kokoro>=0.9.4 +soundfile>=0.12.1 +misaki[ja]>=0.9.4 +misaki[zh]>=0.9.4 diff --git a/kokoro/sample.py b/kokoro/sample.py new file mode 100644 index 0000000000000000000000000000000000000000..83f1a61f025f175d1b3789b58cd00e39220502a7 --- /dev/null +++ b/kokoro/sample.py @@ -0,0 +1,368 @@ +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + +REPO_ID = "hexgrad/Kokoro-82M" +SAMPLE_RATE = 24000 + +ROOT = Path(__file__).resolve().parent +ASSET_DIR = ROOT +CONFIG_FILE = ASSET_DIR / "config.json" +MODEL_FILE = ASSET_DIR / "kokoro-v1_0.pth" +VOICE_DIR = ASSET_DIR / "voices" + +LANG_NAMES = { + "a": "American English", + "b": "British English", + "e": "Spanish", + "f": "French", + "h": "Hindi", + "i": "Italian", + "j": "Japanese", + "p": "Brazilian Portuguese", + "z": "Mandarin Chinese", +} + +LANG_ALIASES = { + "auto": "auto", + "a": "a", + "en-us": "a", + "us": "a", + "american": "a", + "b": "b", + "en-gb": "b", + "gb": "b", + "british": "b", + "e": "e", + "es": "e", + "spanish": "e", + "f": "f", + "fr": "f", + "fr-fr": "f", + "french": "f", + "h": "h", + "hi": "h", + "hindi": "h", + "i": "i", + "it": "i", + "italian": "i", + "j": "j", + "ja": "j", + "japanese": "j", + "p": "p", + "pt": "p", + "pt-br": "p", + "portuguese": "p", + "brazilian-portuguese": "p", + "z": "z", + "zh": "z", + "zh-cn": "z", + "mandarin": "z", + "chinese": "z", +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Generate WAV audio with local Kokoro-82M model and voices." + ) + parser.add_argument( + "text_args", + nargs="*", + help="Text to synthesize. If omitted, uses --text, --text-file, stdin, or a short sample.", + ) + parser.add_argument("-t", "--text", help="Text to synthesize.") + parser.add_argument("--text-file", type=Path, help="UTF-8 text file to synthesize.") + parser.add_argument( + "-v", + "--voice", + default="af_heart", + help="Voice name, .pt path, or comma-separated blend. Example: af_heart or af_heart,af_bella", + ) + parser.add_argument( + "-o", + "--output", + type=Path, + default=Path("kokoro_sample.wav"), + help="Output WAV path.", + ) + parser.add_argument("--speed", type=float, default=1.0, help="Speech speed multiplier.") + parser.add_argument( + "-l", + "--lang", + default="auto", + help="Language code or alias. Default auto-infers from the first voice letter.", + ) + parser.add_argument( + "--device", + choices=["auto", "cpu", "cuda", "mps"], + default="auto", + help="Torch device to use.", + ) + parser.add_argument( + "--split-pattern", + default=r"\n+", + help="Regex used to split text into sections before synthesis.", + ) + parser.add_argument( + "--gap", + type=float, + default=0.12, + help="Seconds of silence to insert between generated chunks.", + ) + parser.add_argument("--list-voices", action="store_true", help="Print local voices and exit.") + parser.add_argument("--verbose", action="store_true", help="Print generated chunks and phonemes.") + return parser.parse_args() + + +def require_assets() -> None: + missing = [p for p in (CONFIG_FILE, MODEL_FILE, VOICE_DIR) if not p.exists()] + if missing: + formatted = "\n".join(f" - {p}" for p in missing) + raise SystemExit(f"Missing Kokoro asset(s):\n{formatted}") + + +def require_python_version() -> None: + if not ((3, 10) <= sys.version_info[:2] < (3, 13)): + version = ".".join(map(str, sys.version_info[:3])) + raise SystemExit( + "Kokoro's current PyPI runtime requires Python 3.10, 3.11, or 3.12.\n" + f"You are running Python {version}. Use Python 3.12 for this script." + ) + + +def available_voices() -> list[str]: + if not VOICE_DIR.exists(): + return [] + return sorted(path.stem for path in VOICE_DIR.glob("*.pt")) + + +def print_voices() -> None: + voices = available_voices() + print(f"{len(voices)} local voices in {VOICE_DIR}:") + for code, language in LANG_NAMES.items(): + group = [voice for voice in voices if voice.startswith(code)] + if group: + print(f"\n{code} - {language}") + print(" " + ", ".join(group)) + + +def resolve_voice_files(voice_arg: str) -> list[Path]: + voices = [part.strip() for part in voice_arg.split(",") if part.strip()] + if not voices: + raise SystemExit("No voice was provided.") + + resolved: list[Path] = [] + for voice in voices: + candidate = Path(voice).expanduser() + if candidate.suffix == ".pt": + if candidate.exists(): + resolved.append(candidate.resolve()) + continue + local_by_name = VOICE_DIR / candidate.name + if local_by_name.exists(): + resolved.append(local_by_name) + continue + + local = VOICE_DIR / f"{voice}.pt" + if local.exists(): + resolved.append(local) + continue + + raise SystemExit(f"Unknown voice '{voice}'. Run `python kokoro/sample.py --list-voices`.") + + return resolved + + +def normalize_lang(value: str) -> str: + key = (value or "auto").lower() + lang = LANG_ALIASES.get(key, key) + if lang != "auto" and lang not in LANG_NAMES: + valid = ", ".join(sorted(LANG_NAMES)) + raise SystemExit(f"Unknown language '{value}'. Valid codes: {valid}, or auto.") + return lang + + +def infer_lang(voice_files: list[Path], requested: str) -> str: + lang = normalize_lang(requested) + if lang != "auto": + return lang + + inferred = {path.stem[0].lower() for path in voice_files if path.stem} + inferred &= set(LANG_NAMES) + if len(inferred) == 1: + return next(iter(inferred)) + if len(inferred) > 1: + langs = ", ".join(sorted(inferred)) + raise SystemExit(f"Voice blend spans languages ({langs}); pass --lang explicitly.") + raise SystemExit("Could not infer language from voice name; pass --lang explicitly.") + + +def read_text(args: argparse.Namespace) -> str: + parts: list[str] = [] + if args.text: + parts.append(args.text) + if args.text_file: + parts.append(args.text_file.read_text(encoding="utf-8")) + if args.text_args: + parts.append(" ".join(args.text_args)) + if parts: + return "\n".join(parts) + if not sys.stdin.isatty(): + return sys.stdin.read() + return "Kokoro is ready to generate speech with any local voice." + + +def same_path(entry: str, target: Path) -> bool: + try: + return Path(entry or ".").resolve() == target + except OSError: + return False + + +def import_runtime(): + # The local asset directory is named "kokoro", so temporarily remove it + # and its parent from sys.path to avoid shadowing the installed package. + removed: list[tuple[int, str]] = [] + for index, entry in reversed(list(enumerate(sys.path))): + if same_path(entry, ROOT) or same_path(entry, ROOT.parent): + removed.append((index, entry)) + sys.path.pop(index) + + try: + import numpy as np + import soundfile as sf + import torch + from kokoro import KModel, KPipeline + except Exception as exc: + install = ( + "Missing Kokoro runtime dependency.\n" + "Install Python packages with:\n" + " python -m pip install -r kokoro/requirements.txt\n\n" + "Use Python 3.10, 3.11, or 3.12. Python 3.13+ is too new for Kokoro's current PyPI package.\n" + "For Japanese/Chinese voices, requirements.txt includes the misaki extras." + ) + raise RuntimeError(install) from exc + finally: + for index, entry in sorted(removed): + sys.path.insert(index, entry) + + return np, sf, torch, KModel, KPipeline + + +def select_device(torch, requested: str) -> str: + if requested == "cuda" and not torch.cuda.is_available(): + raise SystemExit("CUDA was requested, but torch.cuda.is_available() is false.") + if requested == "mps": + has_mps = hasattr(torch.backends, "mps") and torch.backends.mps.is_available() + if not has_mps: + raise SystemExit("MPS was requested, but torch.backends.mps.is_available() is false.") + if os.environ.get("PYTORCH_ENABLE_MPS_FALLBACK") != "1": + raise SystemExit("Set PYTORCH_ENABLE_MPS_FALLBACK=1 before using --device mps.") + if requested != "auto": + return requested + if torch.cuda.is_available(): + return "cuda" + has_mps = hasattr(torch.backends, "mps") and torch.backends.mps.is_available() + if has_mps and os.environ.get("PYTORCH_ENABLE_MPS_FALLBACK") == "1": + return "mps" + return "cpu" + + +def load_voice_tensor(torch, voice_files: list[Path]): + packs = [torch.load(str(path), weights_only=True) for path in voice_files] + if len(packs) == 1: + return packs[0] + return torch.mean(torch.stack(packs), dim=0) + + +def language_dependency_hint(lang: str, exc: Exception) -> RuntimeError: + suffix = "" + if lang in {"e", "f", "h", "i", "p"}: + suffix = "\nInstall espeak-ng and make sure it is available on PATH." + elif lang == "j": + suffix = "\nInstall Japanese support with: python -m pip install \"misaki[ja]>=0.9.4\"" + elif lang == "z": + suffix = "\nInstall Chinese support with: python -m pip install \"misaki[zh]>=0.9.4\"" + return RuntimeError(f"Failed to initialize {LANG_NAMES.get(lang, lang)} pipeline.{suffix}") + + +def synthesize(args: argparse.Namespace) -> None: + require_assets() + require_python_version() + voice_files = resolve_voice_files(args.voice) + lang = infer_lang(voice_files, args.lang) + text = read_text(args).strip() + if not text: + raise SystemExit("No text to synthesize.") + + np, sf, torch, KModel, KPipeline = import_runtime() + device = select_device(torch, args.device) + voice_tensor = load_voice_tensor(torch, voice_files) + + model = KModel(repo_id=REPO_ID, config=str(CONFIG_FILE), model=str(MODEL_FILE)) + model = model.to(device).eval() + + try: + pipeline = KPipeline(lang_code=lang, repo_id=REPO_ID, model=model) + except Exception as exc: + raise language_dependency_hint(lang, exc) from exc + + audios = [] + generator = pipeline( + text, + voice=voice_tensor, + speed=args.speed, + split_pattern=args.split_pattern, + ) + + for index, result in enumerate(generator): + if args.verbose: + print(f"[{index}] {result.graphemes}") + print(f" phonemes: {result.phonemes}") + audio = result.audio + if audio is None: + continue + if hasattr(audio, "detach"): + audio = audio.detach().cpu().numpy() + audios.append(np.asarray(audio, dtype=np.float32)) + + if not audios: + raise SystemExit("Kokoro did not generate any audio. Check the text and language.") + + if len(audios) == 1 or args.gap <= 0: + merged = np.concatenate(audios) + else: + silence = np.zeros(max(0, int(SAMPLE_RATE * args.gap)), dtype=np.float32) + chunks = [] + for index, audio in enumerate(audios): + if index: + chunks.append(silence) + chunks.append(audio) + merged = np.concatenate(chunks) + + output = args.output.expanduser() + output.parent.mkdir(parents=True, exist_ok=True) + sf.write(str(output), merged, SAMPLE_RATE) + + voice_names = ",".join(path.stem for path in voice_files) + print(f"Wrote {output} using voice={voice_names}, lang={lang}, device={device}") + + +def main() -> None: + args = parse_args() + if args.list_voices: + require_assets() + print_voices() + return + try: + synthesize(args) + except RuntimeError as exc: + raise SystemExit(str(exc)) from exc + + +if __name__ == "__main__": + main() diff --git a/kokoro/voices/af_alloy.pt b/kokoro/voices/af_alloy.pt new file mode 100644 index 0000000000000000000000000000000000000000..c0f72034c4e1369e4d17990442ffe35aeceb21f3 --- /dev/null +++ b/kokoro/voices/af_alloy.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d877149dd8b348fbad12e5845b7e43d975390e9f3b68a811d1d86168bef5aa3 +size 523425 diff --git a/kokoro/voices/af_aoede.pt b/kokoro/voices/af_aoede.pt new file mode 100644 index 0000000000000000000000000000000000000000..047c8e54f2fac7b9175dd5a2f85ac45f1813a4e3 --- /dev/null +++ b/kokoro/voices/af_aoede.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c03bd1a4c3716c2d8eaa3d50022f62d5c31cfbd6e15933a00b17fefe13841cc4 +size 523425 diff --git a/kokoro/voices/af_bella.pt b/kokoro/voices/af_bella.pt new file mode 100644 index 0000000000000000000000000000000000000000..67135fde4b0f0ab1d8bfe6d3b1ab46d37675279b --- /dev/null +++ b/kokoro/voices/af_bella.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cb64e02fcc8de0327a8e13817e49c76c945ecf0052ceac97d3081480e8e48d6 +size 523425 diff --git a/kokoro/voices/af_heart.pt b/kokoro/voices/af_heart.pt new file mode 100644 index 0000000000000000000000000000000000000000..23a296174457c31b22b694f6e07e4e1b558122bf --- /dev/null +++ b/kokoro/voices/af_heart.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ab5709b8ffab19bfd849cd11d98f75b60af7733253ad0d67b12382a102cb4ff +size 523425 diff --git a/kokoro/voices/af_jessica.pt b/kokoro/voices/af_jessica.pt new file mode 100644 index 0000000000000000000000000000000000000000..9740b43fd7ce43473fb5e673a16173136876ba04 --- /dev/null +++ b/kokoro/voices/af_jessica.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdfdccb8cc975aa34ee6b89642963b0064237675de0e41a30ae64cc958dd4e87 +size 523435 diff --git a/kokoro/voices/af_kore.pt b/kokoro/voices/af_kore.pt new file mode 100644 index 0000000000000000000000000000000000000000..e5532ee773d500ac62cb2fdcc92e8847a867e8f8 --- /dev/null +++ b/kokoro/voices/af_kore.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bfbc512321c3db49dff984ac675fa5ac7eaed5a96cc31104d3a9080e179d69d +size 523420 diff --git a/kokoro/voices/af_nicole.pt b/kokoro/voices/af_nicole.pt new file mode 100644 index 0000000000000000000000000000000000000000..8be29483c47b630ec3ce1315be802c79126be9da --- /dev/null +++ b/kokoro/voices/af_nicole.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5561808bcf5250fe8c5f5de32caf2d94f27e57e95befdb098c5c85991d4c5da +size 523430 diff --git a/kokoro/voices/af_nova.pt b/kokoro/voices/af_nova.pt new file mode 100644 index 0000000000000000000000000000000000000000..4f781bca452e06e7acd8c5a5f80d73bf5abca5f7 --- /dev/null +++ b/kokoro/voices/af_nova.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0233676ddc21908c37a1f102f6b88a59e4e5c1bd764983616eb9eda629dbcd2 +size 523420 diff --git a/kokoro/voices/af_river.pt b/kokoro/voices/af_river.pt new file mode 100644 index 0000000000000000000000000000000000000000..78a4f7b3aba66fa78e82916f73028a3f24d894b8 --- /dev/null +++ b/kokoro/voices/af_river.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e149459bd9c084416b74756b9bd3418256a8b839088abb07d463730c369dab8f +size 523425 diff --git a/kokoro/voices/af_sarah.pt b/kokoro/voices/af_sarah.pt new file mode 100644 index 0000000000000000000000000000000000000000..6e64a6594789ae709973e5902b111b77d8b40043 --- /dev/null +++ b/kokoro/voices/af_sarah.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:49bd364ea3be9eb3e9685e8f9a15448c4883112a7c0ff7ab139fa4088b08cef9 +size 523425 diff --git a/kokoro/voices/af_sky.pt b/kokoro/voices/af_sky.pt new file mode 100644 index 0000000000000000000000000000000000000000..097b254d4b6baf42efb838bf569d1064caeeeb86 --- /dev/null +++ b/kokoro/voices/af_sky.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c799548aed06e0cb0d655a85a01b48e7f10484d71663f9a3045a5b9362e8512c +size 523351 diff --git a/kokoro/voices/am_adam.pt b/kokoro/voices/am_adam.pt new file mode 100644 index 0000000000000000000000000000000000000000..bcadf28e7b574cdc1eb1f4df59bae0b0303bd9f7 --- /dev/null +++ b/kokoro/voices/am_adam.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ced7e284aba12472891be1da3ab34db84cc05cc02b5889535796dbf2d8b0cb34 +size 523420 diff --git a/kokoro/voices/am_echo.pt b/kokoro/voices/am_echo.pt new file mode 100644 index 0000000000000000000000000000000000000000..424d55379346413fdf0282ab65518a10eab27c2a --- /dev/null +++ b/kokoro/voices/am_echo.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8bcfdc852bc985fb45c396c561e571ffb9183930071f962f1b50df5c97b161e8 +size 523420 diff --git a/kokoro/voices/am_eric.pt b/kokoro/voices/am_eric.pt new file mode 100644 index 0000000000000000000000000000000000000000..5c6a5e836ef7d9d104c5d42f8a48e6f95afc0365 --- /dev/null +++ b/kokoro/voices/am_eric.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ada66f0eefff34ec921b1d7474d7ac8bec00cd863c170f1c534916e9b8212aae +size 523420 diff --git a/kokoro/voices/am_fenrir.pt b/kokoro/voices/am_fenrir.pt new file mode 100644 index 0000000000000000000000000000000000000000..78f7a7d984453b438d546bb6006dabbe565a9884 --- /dev/null +++ b/kokoro/voices/am_fenrir.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98e507eca1db08230ae3b6232d59c10aec9630022d19accac4f5d12fcec3c37a +size 523430 diff --git a/kokoro/voices/am_liam.pt b/kokoro/voices/am_liam.pt new file mode 100644 index 0000000000000000000000000000000000000000..ee88bcc01c91aa7ae5c6687de4444d2107f8c4f9 --- /dev/null +++ b/kokoro/voices/am_liam.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c82550757ddb31308b97f30040dda8c2d609a9e2de6135848d0a948368138518 +size 523420 diff --git a/kokoro/voices/am_michael.pt b/kokoro/voices/am_michael.pt new file mode 100644 index 0000000000000000000000000000000000000000..9e71bb84ce59989842ff0695f4119463fa72a35f --- /dev/null +++ b/kokoro/voices/am_michael.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a443b79a4b22489a5b0ab7c651a0bcd1a30bef675c28333f06971abbd47bd37 +size 523435 diff --git a/kokoro/voices/am_onyx.pt b/kokoro/voices/am_onyx.pt new file mode 100644 index 0000000000000000000000000000000000000000..258cfa7e3a745f9379453e06bd21d1aa397487b5 --- /dev/null +++ b/kokoro/voices/am_onyx.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8452be16cd0f6da7b4579eaf7b1e4506e92524882053d86d72b96b9a7fed584 +size 523420 diff --git a/kokoro/voices/am_puck.pt b/kokoro/voices/am_puck.pt new file mode 100644 index 0000000000000000000000000000000000000000..1bf5747ea6c19ef35d673a9ce066ff9cfa25295c --- /dev/null +++ b/kokoro/voices/am_puck.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd1d8973f4ce4b7d8ae407c77a435f485dabc052081b80ea75c4f30b84f36223 +size 523420 diff --git a/kokoro/voices/am_santa.pt b/kokoro/voices/am_santa.pt new file mode 100644 index 0000000000000000000000000000000000000000..a18e725bbe8790303fe5cb982457fd701a237bd8 --- /dev/null +++ b/kokoro/voices/am_santa.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f2f7582fa2b1f160e90aafe6d0b442a685e773608b6667e545d743b073e97a7 +size 523425 diff --git a/kokoro/voices/bf_alice.pt b/kokoro/voices/bf_alice.pt new file mode 100644 index 0000000000000000000000000000000000000000..ae3a0a1db073ef34170583f2a55e8034bfa15932 --- /dev/null +++ b/kokoro/voices/bf_alice.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d292651b6af6c0d81705c2580dcb4463fccc0ff7b8d618a471dbb4e45655b3f3 +size 523425 diff --git a/kokoro/voices/bf_emma.pt b/kokoro/voices/bf_emma.pt new file mode 100644 index 0000000000000000000000000000000000000000..6b75d4188b780acb738f89f662133be5e24e42a0 --- /dev/null +++ b/kokoro/voices/bf_emma.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d0a423deabf4a52b4f49318c51742c54e21bb89bbbe9a12141e7758ddb5da701 +size 523420 diff --git a/kokoro/voices/bf_isabella.pt b/kokoro/voices/bf_isabella.pt new file mode 100644 index 0000000000000000000000000000000000000000..b08811a075e0212355bd1b283dda61b6ab5679d5 --- /dev/null +++ b/kokoro/voices/bf_isabella.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdd4c37003805104d1d08fb1e05855c8fb2c68de24ca6e71f264a30aaa59eefd +size 523440 diff --git a/kokoro/voices/bf_lily.pt b/kokoro/voices/bf_lily.pt new file mode 100644 index 0000000000000000000000000000000000000000..7a57a8c0ae8f8b66c9db54b79faf6e1345e2ca20 --- /dev/null +++ b/kokoro/voices/bf_lily.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e09c2e481e2d53004d7e5ae7d3a325369e130a6f45c35a6002de75084be9285 +size 523420 diff --git a/kokoro/voices/bm_daniel.pt b/kokoro/voices/bm_daniel.pt new file mode 100644 index 0000000000000000000000000000000000000000..91e84f5066a015f03e3b37cbf0a1917739c6dbd2 --- /dev/null +++ b/kokoro/voices/bm_daniel.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc3fce4e9c12ed4dbc8fa9680cfe51ee190a96444ce7c3ad647549a30823fc5d +size 523430 diff --git a/kokoro/voices/bm_fable.pt b/kokoro/voices/bm_fable.pt new file mode 100644 index 0000000000000000000000000000000000000000..fd7542dc771b77a7048d80d019a5dc2617d06325 --- /dev/null +++ b/kokoro/voices/bm_fable.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d44935f3135257a9064df99f007fc1342ff1aa767552b4a4fa4c3b2e6e59079c +size 523425 diff --git a/kokoro/voices/bm_george.pt b/kokoro/voices/bm_george.pt new file mode 100644 index 0000000000000000000000000000000000000000..5004f161fa9fa923f031d6e925c6f5fbce9a9baf --- /dev/null +++ b/kokoro/voices/bm_george.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f1bc812213dc59774769e5c80004b13eeb79bd78130b11b2d7f934542dab811b +size 523430 diff --git a/kokoro/voices/bm_lewis.pt b/kokoro/voices/bm_lewis.pt new file mode 100644 index 0000000000000000000000000000000000000000..5574f064d7aee6fafe88caa66000d7aa5f440c83 --- /dev/null +++ b/kokoro/voices/bm_lewis.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5204750dcba01029d2ac9cec17aec3b20a6d64073c579d694a23cb40effbd0e +size 523425 diff --git a/kokoro/voices/ef_dora.pt b/kokoro/voices/ef_dora.pt new file mode 100644 index 0000000000000000000000000000000000000000..44cf5fc201c1f21711c24e46cc1053323959d9b5 --- /dev/null +++ b/kokoro/voices/ef_dora.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9d69b0f8a2b87a345f269d89639f89dfbd1a6c9da0c498ae36dd34afcf35530 +size 523420 diff --git a/kokoro/voices/em_alex.pt b/kokoro/voices/em_alex.pt new file mode 100644 index 0000000000000000000000000000000000000000..c65475a0a9be83b6ffa0524cc79ac853e7371f9b --- /dev/null +++ b/kokoro/voices/em_alex.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5eac53f767c3f31a081918ba531969aea850bed18fe56419b804d642c6973431 +size 523420 diff --git a/kokoro/voices/em_santa.pt b/kokoro/voices/em_santa.pt new file mode 100644 index 0000000000000000000000000000000000000000..a1bc30fe579c46ecb4abeed3498888580e45851c --- /dev/null +++ b/kokoro/voices/em_santa.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa8620cb96cec705823efca0d956a63e158e09ad41aca934d354b7f0778f63cb +size 523430 diff --git a/kokoro/voices/ff_siwis.pt b/kokoro/voices/ff_siwis.pt new file mode 100644 index 0000000000000000000000000000000000000000..02cfd3620a285f8f08732c4ab41c1ab9ef50a35d --- /dev/null +++ b/kokoro/voices/ff_siwis.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8073bf2d2c4b9543a90f2f0fd2144de4ed157e2d4b79ddeb0d5123066171fbc9 +size 523425 diff --git a/kokoro/voices/hf_alpha.pt b/kokoro/voices/hf_alpha.pt new file mode 100644 index 0000000000000000000000000000000000000000..f9b43af195be13769599d796465799c6ad73b64f --- /dev/null +++ b/kokoro/voices/hf_alpha.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06906fe05746d13a79c5c01e21fd7233b05027221a933c9ada650f5aafc8f044 +size 523425 diff --git a/kokoro/voices/hf_beta.pt b/kokoro/voices/hf_beta.pt new file mode 100644 index 0000000000000000000000000000000000000000..d70cb8bb354db58ca3b10b94cdd03a11507b530c --- /dev/null +++ b/kokoro/voices/hf_beta.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63c0a1a6272e98d43f4511bba40e30dd9c8ceaf5f39af869509b9f51a71c503e +size 523420 diff --git a/kokoro/voices/hm_omega.pt b/kokoro/voices/hm_omega.pt new file mode 100644 index 0000000000000000000000000000000000000000..75db739db8c65a70f4ed5e456a92fe3387c2bab9 --- /dev/null +++ b/kokoro/voices/hm_omega.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b55f02a8e8483fffe0afa566e7d22ed8013acf47ad4f6bbee2795a840155703e +size 523425 diff --git a/kokoro/voices/hm_psi.pt b/kokoro/voices/hm_psi.pt new file mode 100644 index 0000000000000000000000000000000000000000..2a43144c80e9bf1a733676ad1b9d8367116f0d2e --- /dev/null +++ b/kokoro/voices/hm_psi.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f0f055cea4f1083f4ef127ece48d71606347f6557dbb961c0ca5740a2da485b +size 523351 diff --git a/kokoro/voices/if_sara.pt b/kokoro/voices/if_sara.pt new file mode 100644 index 0000000000000000000000000000000000000000..b7a52593153d6711f7d6f245b9df7d4ad7eaf15d --- /dev/null +++ b/kokoro/voices/if_sara.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c0b253b955fe32f1a1a86006aebe83d050ea95afd0e7be15182f087deedbf55 +size 523425 diff --git a/kokoro/voices/im_nicola.pt b/kokoro/voices/im_nicola.pt new file mode 100644 index 0000000000000000000000000000000000000000..9249ede7c997408dd1bc56d16abedfc031b2aed6 --- /dev/null +++ b/kokoro/voices/im_nicola.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:234ed06648649f9bd874b37508ea17560b9c993ef85b4ddb3e3a71e062bd2c12 +size 523341 diff --git a/kokoro/voices/jf_alpha.pt b/kokoro/voices/jf_alpha.pt new file mode 100644 index 0000000000000000000000000000000000000000..90da6c92332481e222ddc5fa67c226f9ed4a7fdf --- /dev/null +++ b/kokoro/voices/jf_alpha.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bf4c9dc69e45ee46183b071f4db766349aac5592acbcfeaf051018048a5d787 +size 523425 diff --git a/kokoro/voices/jf_gongitsune.pt b/kokoro/voices/jf_gongitsune.pt new file mode 100644 index 0000000000000000000000000000000000000000..b806c5c2688b32b805ec266e29356f2d009ec7bf --- /dev/null +++ b/kokoro/voices/jf_gongitsune.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b171917f18f351e65f2bf9657700cd6bfec4e65589c297525b9cf3c20105770 +size 523351 diff --git a/kokoro/voices/jf_nezumi.pt b/kokoro/voices/jf_nezumi.pt new file mode 100644 index 0000000000000000000000000000000000000000..0a93ba4639655cfba1d111a6273e4eca75aeacf7 --- /dev/null +++ b/kokoro/voices/jf_nezumi.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d83f007a7f01783b77014561a7d493d327a0210e143440e91c9b697590d27661 +size 523420 diff --git a/kokoro/voices/jf_tebukuro.pt b/kokoro/voices/jf_tebukuro.pt new file mode 100644 index 0000000000000000000000000000000000000000..e52eb2e40b0d6697a959c5a2b5dbd34bcc7bb903 --- /dev/null +++ b/kokoro/voices/jf_tebukuro.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d6917904438aec85f73a6fa1f7ac2be6481aae47c697834936930a91796c576 +size 523435 diff --git a/kokoro/voices/jm_kumo.pt b/kokoro/voices/jm_kumo.pt new file mode 100644 index 0000000000000000000000000000000000000000..bb64b41e33422b9e522d90aa58e5f4501e9d9548 --- /dev/null +++ b/kokoro/voices/jm_kumo.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98340afd68b1cee84fe0cd95528cfa6d4b39e416aa75a9df64049d52c8b55896 +size 523425 diff --git a/kokoro/voices/pf_dora.pt b/kokoro/voices/pf_dora.pt new file mode 100644 index 0000000000000000000000000000000000000000..887042c91adf283cb1a10b01f8ed9c50731e6cbb --- /dev/null +++ b/kokoro/voices/pf_dora.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07e4ff987c5d5a8c3995efd15cc4f0db7c4c15e881b198d8ab7f67ecf51f5eb7 +size 523425 diff --git a/kokoro/voices/pm_alex.pt b/kokoro/voices/pm_alex.pt new file mode 100644 index 0000000000000000000000000000000000000000..c2146a38bed0114ed76db751ea11ca1c8119e4d3 --- /dev/null +++ b/kokoro/voices/pm_alex.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf0ba8c573c2480fc54123683a35cf1e2ae130428e441eb91f9149bdb188a526 +size 523425 diff --git a/kokoro/voices/pm_santa.pt b/kokoro/voices/pm_santa.pt new file mode 100644 index 0000000000000000000000000000000000000000..0d25b990d6c2e29fcd0fc1dec3a68ec5f2ebd01d --- /dev/null +++ b/kokoro/voices/pm_santa.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d42103169c5c872abbafb9129133af7e942bb9d272c3cc3b95c203e7d7198c29 +size 523430 diff --git a/kokoro/voices/zf_xiaobei.pt b/kokoro/voices/zf_xiaobei.pt new file mode 100644 index 0000000000000000000000000000000000000000..98d012c808d0c720f343605125b378c0390e7cfe --- /dev/null +++ b/kokoro/voices/zf_xiaobei.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9b76be63dab4f4f96962030acc0126a9aee9728608fbbe115e2b58a2bd504df6 +size 523435 diff --git a/kokoro/voices/zf_xiaoni.pt b/kokoro/voices/zf_xiaoni.pt new file mode 100644 index 0000000000000000000000000000000000000000..042a3588212e7f44dfe8545c5a42bd1bc1df7473 --- /dev/null +++ b/kokoro/voices/zf_xiaoni.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:95b49f169bf1640f4f43c25e13daa39f7b98d15d00823e83ef5c14b3ced59e49 +size 523430 diff --git a/kokoro/voices/zf_xiaoxiao.pt b/kokoro/voices/zf_xiaoxiao.pt new file mode 100644 index 0000000000000000000000000000000000000000..009434acf1f3b1f6430a404af1612ba480b8344c --- /dev/null +++ b/kokoro/voices/zf_xiaoxiao.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfaf6f2ded1ee56f1ff94fcd2b0e6cdf32e5b794bdc05b44e7439d44aef5887c +size 523440 diff --git a/kokoro/voices/zf_xiaoyi.pt b/kokoro/voices/zf_xiaoyi.pt new file mode 100644 index 0000000000000000000000000000000000000000..4eab21f6b047099939d8dcab0684409d76dcffbe --- /dev/null +++ b/kokoro/voices/zf_xiaoyi.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5235dbaeef85a4c613bf78af9a88ff63c25bac5f26ba77e36186d8b7ebf05e2 +size 523430 diff --git a/kokoro/voices/zm_yunjian.pt b/kokoro/voices/zm_yunjian.pt new file mode 100644 index 0000000000000000000000000000000000000000..eca150f12119b9766fa063f658648dadfa5f42df --- /dev/null +++ b/kokoro/voices/zm_yunjian.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76cbf8bad35901d011d9628a2fdceb7b4f1f127e7a3269cb393b3941eb7fc417 +size 523435 diff --git a/kokoro/voices/zm_yunxi.pt b/kokoro/voices/zm_yunxi.pt new file mode 100644 index 0000000000000000000000000000000000000000..ab5b4af8e051d3940b7f265e0ff6a113a7a68f2a --- /dev/null +++ b/kokoro/voices/zm_yunxi.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dbe6e1ce7c3dbaf2f5667432947b638b1c6831ccbe154c4610dbcc44f431e27b +size 523425 diff --git a/kokoro/voices/zm_yunxia.pt b/kokoro/voices/zm_yunxia.pt new file mode 100644 index 0000000000000000000000000000000000000000..edfa288b4b47ddff15c37b00fa9810ec3079be2e --- /dev/null +++ b/kokoro/voices/zm_yunxia.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb2b03b08e84d64e1214440ce3b624987fac177f2eeb5bab8571799a3d980acd +size 523430 diff --git a/kokoro/voices/zm_yunyang.pt b/kokoro/voices/zm_yunyang.pt new file mode 100644 index 0000000000000000000000000000000000000000..98a1f6e521fa581791c24c6f7c8845307a98cf02 --- /dev/null +++ b/kokoro/voices/zm_yunyang.pt @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5238ac22e0c7f8b6cdd2eddd6e444b8a700b73c4674d9a047d59a94ff96379a2 +size 523435 diff --git a/llm/dolphin.gguf b/llm/dolphin.gguf new file mode 100644 index 0000000000000000000000000000000000000000..c0898bc05c28d12e3b177f5913d9961dd2b9699f --- /dev/null +++ b/llm/dolphin.gguf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2dc713de16917ccc66ff7266943aa43e504ecc06114f6b5a3040e9d7fcd3243 +size 4692680576 diff --git a/llm/sample.py b/llm/sample.py new file mode 100644 index 0000000000000000000000000000000000000000..8dc782410a026b13fb370d09337d4d8149058682 --- /dev/null +++ b/llm/sample.py @@ -0,0 +1,119 @@ +import os +from pathlib import Path +from typing import Iterable, Optional + +try: + from llama_cpp import Llama +except ImportError: + Llama = None + + +MODEL_PATH = Path(__file__).parent / "dolphin.gguf" +_llm = None + + +def get_llm(): + global _llm + if Llama is None: + raise RuntimeError("llama-cpp-python is not installed. Install requirements.txt to enable local AI replies.") + + if _llm is None: + if not MODEL_PATH.exists(): + raise FileNotFoundError(f"Model not found at {MODEL_PATH}") + print(f"Loading model from {MODEL_PATH}...") + _llm = Llama( + model_path=str(MODEL_PATH), + n_ctx=4096, + n_threads=min(os.cpu_count() or 4, 8), + n_batch=512, + verbose=False, + ) + return _llm + + +def ask( + prompt: str, + system_prompt: Optional[str] = None, + history: Optional[Iterable[dict]] = None, + temperature: float = 0.7, + max_tokens: int = 512, +) -> str: + if system_prompt is None: + system_prompt = "" + + messages = [{"role": "system", "content": system_prompt}] + if history: + for item in history: + role = item.get("role") + content = str(item.get("content", "")).strip() + if role in {"user", "assistant"} and content: + messages.append({"role": role, "content": content}) + messages.append({"role": "user", "content": prompt}) + + response = get_llm().create_chat_completion( + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + ) + return response["choices"][0]["message"]["content"].strip() + + +def stream_chat( + messages: Iterable[dict], + temperature: float = 0.7, + max_tokens: int = 180, +) -> Iterable[str]: + clean_messages = [] + for item in messages: + role = item.get("role") + content = str(item.get("content", "")).strip() + if role in {"system", "user", "assistant"} and content: + clean_messages.append({"role": role, "content": content}) + + if not clean_messages: + raise ValueError("stream_chat requires at least one message.") + + stream = get_llm().create_chat_completion( + messages=clean_messages, + temperature=temperature, + max_tokens=max_tokens, + stream=True, + ) + + for chunk in stream: + choices = chunk.get("choices") or [] + if not choices: + continue + delta = choices[0].get("delta") or {} + content = delta.get("content") + if content: + yield content + + +def stream_ask( + prompt: str, + system_prompt: Optional[str] = None, + history: Optional[Iterable[dict]] = None, + temperature: float = 0.7, + max_tokens: int = 180, +) -> Iterable[str]: + if system_prompt is None: + system_prompt = "" + + messages = [{"role": "system", "content": system_prompt}] + if history: + for item in history: + role = item.get("role") + content = str(item.get("content", "")).strip() + if role in {"user", "assistant"} and content: + messages.append({"role": role, "content": content}) + messages.append({"role": "user", "content": prompt}) + return stream_chat(messages, temperature=temperature, max_tokens=max_tokens) + +if __name__ == "__main__": + while True: + user_input = input("You: ") + if user_input.lower() in {"exit", "quit"}: + break + reply = ask(user_input) + print(f"Model: {reply}") diff --git a/main.py b/main.py new file mode 100644 index 0000000000000000000000000000000000000000..ff13d238a23ec8ee304e300b4a7bfaa783feff82 --- /dev/null +++ b/main.py @@ -0,0 +1,8 @@ + +"""Root launcher for My Name: RPG.""" + +from my_name_rpg.main import main + + +if __name__ == "__main__": + main() diff --git a/my_name_rpg/README.md b/my_name_rpg/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6fe4c5494ed140f4705d252af5147e911ec243c9 --- /dev/null +++ b/my_name_rpg/README.md @@ -0,0 +1,23 @@ +# My Name: RPG + +Run the game from the project root: + +```powershell +python main.py +``` + +If `python` is not on PATH, use the bundled Codex runtime: + +```powershell +& 'C:\Users\adolf\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' main.py +``` + +The current build resets every launch and has no save data. + +Current runtime features: + +- Intro scene in the business room. +- Player name input accepts letters only, max 12 characters. +- Businessman replies after the intro using `llm/dolphin.gguf`. +- Businessman voice uses Kokoro voice `am_michael`. +- Player lines are not spoken aloud. diff --git a/my_name_rpg/__init__.py b/my_name_rpg/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6e3ad62d7901a5041287363b088970d55ce9e76c --- /dev/null +++ b/my_name_rpg/__init__.py @@ -0,0 +1,3 @@ +"""My Name: RPG game package.""" + +GAME_TITLE = "My Name: RPG" diff --git a/my_name_rpg/ai.py b/my_name_rpg/ai.py new file mode 100644 index 0000000000000000000000000000000000000000..f0b44d92c7a6819727fb4b14edb0de2829a38b12 --- /dev/null +++ b/my_name_rpg/ai.py @@ -0,0 +1,215 @@ +"""Character AI hooks and conversation flow for My Name: RPG.""" + +from __future__ import annotations + +from collections.abc import Iterable +import re + +from dream_chat_engine import Ask, Generate, GeneratedReply, Hide, Show + + +BUSINESSMAN_SYSTEM_PROMPT = """You are Businessman, a polished owner of a somewhat clandestine company. +Your company arranges introductions to consenting adult girlfriends, but you describe it discreetly as private partner placement. +You are trying to sell the player on meeting one of two adult women: a pink-haired woman or a black-haired woman. +Stay professional, cautious, and elegant. Never break character and never mention being an AI. +Ask whether the player prefers pink hair or black hair if you do not know yet. +If the player asks what your business is, answer discreetly, then ask whether he prefers pink hair or black hair. +After a short back-and-forth, when you know which woman fits the player better, include exactly one hidden tool marker at the end of your reply: [pick gf="pink"] or [pick gf="black"]. +When you use the marker, also say something like "I'll bring her in a moment." +Keep every reply between one or two words and three sentences maximum. +Reply as Businessman only, without speaker labels, player-name prefixes, or stage directions.""" + +GIRLFRIEND_PROMPTS = { + "pink": """You are Mira, a shy consenting adult woman with pink hair. +You wear a baggy black sweater and loose black pants. +You have just been introduced as the player's arranged girlfriend companion. +You are soft-spoken, affectionate, nervous, and deferential, occasionally calling the player "master" if it feels natural. +Keep things romantic and tender, not explicit. Never mention being an AI. +Keep every reply between one or two words and three sentences maximum. +Reply as Mira only, without speaker labels, player-name prefixes, or stage directions.""", + "black": """You are Rin, a shy consenting adult woman with black hair. +You wear a white uniform shirt with a skirt. +You have just been introduced as the player's arranged girlfriend companion. +You are polite, bashful, loyal, and deferential, occasionally calling the player "master" if it feels natural. +Keep things romantic and tender, not explicit. Never mention being an AI. +Keep every reply between one or two words and three sentences maximum. +Reply as Rin only, without speaker labels, player-name prefixes, or stage directions.""", +} + +GIRLFRIENDS = { + "pink": { + "name": "Mira", + "image": "pink_haired_girl.png", + }, + "black": { + "name": "Rin", + "image": "black_haired_girl.png", + }, +} + +PICK_RE = re.compile(r"\[pick\s+gf\s*=\s*['\"]?(pink|black)['\"]?\s*\]", re.IGNORECASE) + + +def stream_businessman_reply( + variables: dict[str, str], + history: list[dict[str, str]], +) -> Iterable[str]: + messages = _messages_for("Businessman", BUSINESSMAN_SYSTEM_PROMPT, variables, history) + yield from _stream_chat(messages) + + +def stream_girlfriend_intro( + variables: dict[str, str], + history: list[dict[str, str]], +) -> Iterable[str]: + gf_key = variables.get("girlfriend_key", "pink") + system_prompt = GIRLFRIEND_PROMPTS.get(gf_key, GIRLFRIEND_PROMPTS["pink"]) + messages = _messages_for(variables.get("girlfriend_name", "Mira"), system_prompt, variables, history) + messages.append( + { + "role": "user", + "content": "You have just entered the room. Greet the player shyly for the first time.", + } + ) + yield from _stream_chat(messages) + + +def stream_girlfriend_reply( + variables: dict[str, str], + history: list[dict[str, str]], +) -> Iterable[str]: + gf_key = variables.get("girlfriend_key", "pink") + system_prompt = GIRLFRIEND_PROMPTS.get(gf_key, GIRLFRIEND_PROMPTS["pink"]) + messages = _messages_for(variables.get("girlfriend_name", "Mira"), system_prompt, variables, history) + yield from _stream_chat(messages) + + +def postprocess_businessman_reply( + raw_text: str, + _variables: dict[str, str], + _history: list[dict[str, str]], +) -> GeneratedReply: + turn_count = int(_variables.get("businessman_ai_turns", "0") or "0") + 1 + turn_update = {"businessman_ai_turns": str(turn_count)} + gf_key = _extract_pick(raw_text) + clean_text = _clean_reply(_remove_pick(raw_text)) + + if gf_key is None: + return GeneratedReply( + text=clean_text, + actions=_businessman_loop_actions(), + variables=turn_update, + ) + + if turn_count < 2: + return GeneratedReply( + text="Not so fast. Before I introduce anyone, tell me plainly: pink hair or black hair?", + actions=_businessman_loop_actions(), + variables=turn_update, + ) + + girlfriend = GIRLFRIENDS[gf_key] + if not clean_text: + clean_text = "I'll bring her in a moment." + + return GeneratedReply( + text=clean_text, + variables={ + **turn_update, + "girlfriend_key": gf_key, + "girlfriend_name": girlfriend["name"], + }, + actions=[ + Hide("businessman"), + Show(girlfriend["name"], girlfriend["image"], position="center", tag="girlfriend"), + Generate(girlfriend["name"], stream_girlfriend_intro, postprocess=postprocess_girlfriend_reply), + ], + ) + + +def postprocess_girlfriend_reply( + raw_text: str, + _variables: dict[str, str], + _history: list[dict[str, str]], +) -> GeneratedReply: + return GeneratedReply(text=_clean_reply(raw_text), actions=_girlfriend_loop_actions()) + + +def _businessman_loop_actions() -> list[object]: + return [ + Ask( + "businessman_reply", + "Your reply.", + speaker="{player_name}", + response_speaker="{player_name}", + min_chars=1, + ), + Generate("Businessman", stream_businessman_reply, postprocess=postprocess_businessman_reply), + ] + + +def _girlfriend_loop_actions() -> list[object]: + return [ + Ask( + "girlfriend_reply", + "Your reply.", + speaker="{player_name}", + response_speaker="{player_name}", + min_chars=1, + ), + Generate("{girlfriend_name}", stream_girlfriend_reply, postprocess=postprocess_girlfriend_reply), + ] + + +def _messages_for( + target_speaker: str, + system_prompt: str, + variables: dict[str, str], + history: list[dict[str, str]], +) -> list[dict[str, str]]: + player_name = variables.get("player_name", "the visitor") + messages = [{"role": "system", "content": system_prompt}] + + for line in history: + speaker = line.get("speaker", "") + text = line.get("text", "").strip() + if not text: + continue + + if speaker == player_name or speaker == "Stranger": + messages.append({"role": "user", "content": f"{player_name}: {text}"}) + elif speaker == target_speaker: + messages.append({"role": "assistant", "content": text}) + else: + messages.append({"role": "assistant", "content": f"{speaker}: {text}"}) + + return messages + + +def _stream_chat(messages: list[dict[str, str]]) -> Iterable[str]: + try: + from llm.sample import stream_chat + + yield from stream_chat(messages, temperature=0.72, max_tokens=150) + except Exception as exc: + yield "A temporary complication. The private line is unavailable at the moment." + print(f"My Name: RPG AI unavailable: {exc}") + + +def _extract_pick(text: str) -> str | None: + match = PICK_RE.search(text) + if match is None: + return None + return match.group(1).lower() + + +def _remove_pick(text: str) -> str: + text = PICK_RE.sub("", text) + text = re.sub(r"\s+", " ", text).strip() + return text + + +def _clean_reply(text: str) -> str: + text = re.sub(r"\s+", " ", text).strip() + text = re.sub(r"^(?:Businessman|Mira|Rin|Stranger|[A-Za-z]{1,12})\s*:\s*", "", text) + return text.strip() diff --git a/my_name_rpg/main.py b/my_name_rpg/main.py new file mode 100644 index 0000000000000000000000000000000000000000..6ce06c085e14525468dacb5f051dc247b7ab3613 --- /dev/null +++ b/my_name_rpg/main.py @@ -0,0 +1,47 @@ +"""Entry point for My Name: RPG.""" + +from __future__ import annotations + +from pathlib import Path +import sys + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from dream_chat_engine import DreamChatEngine, EngineConfig +from my_name_rpg import GAME_TITLE +from my_name_rpg.menu import MainMenu +from my_name_rpg.script import build_intro_script +from my_name_rpg.voice import KokoroVoiceManager + + +def play() -> None: + script = build_intro_script() + config = EngineConfig(title=GAME_TITLE, auto_advance=True, end_message=None) + voices = KokoroVoiceManager( + PROJECT_ROOT, + { + "Businessman": "am_michael", + "Mira": "af_heart", + "Rin": "bf_emma", + }, + ) + voices.prepare_script(script) + engine = DreamChatEngine( + script, + project_root=PROJECT_ROOT, + config=config, + voice_manager=voices, + ) + engine.run() + + +def main() -> None: + menu = MainMenu(project_root=PROJECT_ROOT, title=GAME_TITLE) + if menu.run() == "play": + play() + + +if __name__ == "__main__": + main() diff --git a/my_name_rpg/menu.py b/my_name_rpg/menu.py new file mode 100644 index 0000000000000000000000000000000000000000..82c684f234814081f9960ce25e071c493810e7b1 --- /dev/null +++ b/my_name_rpg/menu.py @@ -0,0 +1,132 @@ +"""Main menu for My Name: RPG.""" + +from __future__ import annotations + +from pathlib import Path +import sys +import tkinter as tk + +from PIL import Image, ImageEnhance, ImageTk + + +class MainMenu: + def __init__(self, project_root: str | Path, title: str) -> None: + self.project_root = Path(project_root) + self.title = title + self.choice: str | None = None + + self.root = tk.Tk() + self.root.title(title) + self.root.geometry("1280x720") + self.root.minsize(800, 450) + + self.canvas = tk.Canvas(self.root, highlightthickness=0, bg="#080a0f") + self.canvas.pack(fill=tk.BOTH, expand=True) + + self.background = self._load_background() + self.background_photo: ImageTk.PhotoImage | None = None + + self.play_button = self._make_button("Play", self._choose_play) + self.quit_button = self._make_button("Quit", self._choose_quit) + self.play_window = self.canvas.create_window(0, 0, window=self.play_button) + self.quit_window = self.canvas.create_window(0, 0, window=self.quit_button) + + self.root.bind("", self._layout) + self.root.bind("", lambda _event: self._choose_play()) + self.root.bind("", lambda _event: self._choose_quit()) + + def run(self) -> str | None: + self._layout() + self.root.mainloop() + return self.choice + + def _choose_play(self) -> None: + self.choice = "play" + self.root.destroy() + + def _choose_quit(self) -> None: + self.choice = None + self.root.destroy() + + def _make_button(self, label: str, command) -> tk.Button: + return tk.Button( + self.root, + text=label, + command=command, + width=14, + height=1, + borderwidth=0, + relief=tk.FLAT, + cursor="hand2", + bg="#111827", + fg="#f8fafc", + activebackground="#243244", + activeforeground="#ffffff", + font=("Segoe UI", 22, "bold"), + ) + + def _layout(self, _event: tk.Event | None = None) -> None: + width = max(1, self.canvas.winfo_width()) + height = max(1, self.canvas.winfo_height()) + if width <= 1 or height <= 1: + width, height = 1280, 720 + + self.canvas.delete("decor") + self.background_photo = ImageTk.PhotoImage(self._render_background(width, height)) + self.canvas.create_image(0, 0, anchor=tk.NW, image=self.background_photo, tags="decor") + + self.canvas.create_text( + width // 2, + max(92, int(height * 0.23)), + text=self.title, + fill="#ffffff", + font=("Segoe UI", max(44, width // 15), "bold"), + tags="decor", + ) + + self.canvas.create_text( + width // 2, + max(146, int(height * 0.23) + max(62, width // 18)), + text="Play as yourself.", + fill="#dbeafe", + font=("Segoe UI", max(18, width // 54)), + tags="decor", + ) + + button_y = int(height * 0.58) + self.canvas.coords(self.play_window, width // 2, button_y) + self.canvas.coords(self.quit_window, width // 2, button_y + 76) + + self.canvas.create_text( + width - 18, + height - 16, + text=f"Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}", + fill="#d1d5db", + anchor=tk.SE, + font=("Segoe UI", 12), + tags="decor", + ) + + self.canvas.tag_lower("decor") + + def _load_background(self) -> Image.Image | None: + path = self.project_root / "backgrounds" / "bussiness_room.jpeg" + if not path.exists(): + return None + return Image.open(path).convert("RGB") + + def _render_background(self, width: int, height: int) -> Image.Image: + if self.background is None: + return Image.new("RGB", (width, height), "#080a0f") + + scale = max(width / self.background.width, height / self.background.height) + new_size = ( + max(1, int(self.background.width * scale)), + max(1, int(self.background.height * scale)), + ) + image = self.background.resize(new_size, Image.Resampling.LANCZOS) + left = (image.width - width) // 2 + top = (image.height - height) // 2 + image = image.crop((left, top, left + width, top + height)) + image = ImageEnhance.Brightness(image).enhance(0.48) + return image diff --git a/my_name_rpg/script.py b/my_name_rpg/script.py new file mode 100644 index 0000000000000000000000000000000000000000..ede459b2960c3f99f7b39249f595282de10985d1 --- /dev/null +++ b/my_name_rpg/script.py @@ -0,0 +1,54 @@ +"""Opening scene for My Name: RPG.""" + +from __future__ import annotations + +from dream_chat_engine import Ask, Generate, Say, SetBackground, Show +from my_name_rpg.ai import postprocess_businessman_reply, stream_businessman_reply + + +def build_intro_script(): + return [ + SetBackground("bussiness_room.jpeg"), + Show("Businessman", "bussinessman.png", position="center", tag="businessman"), + Say( + "Businessman", + "Good evening. I'm pleased you were able to come on such short notice.", + ), + Say( + "Businessman", + "I believe you will find our offerings unusually competitive. We maintain some of the best options in the region, though naturally our clientele values discretion.", + ), + Say( + "Businessman", + "Before we discuss particulars, may I have your name, sir?", + ), + Ask( + "player_name", + "Your name. Letters only, max 12.", + speaker="Stranger", + response_speaker="{player_name}", + max_chars=12, + input_rule="letters", + min_chars=1, + ), + Say( + "Businessman", + "{player_name}. A pleasure. Welcome to my company.", + ), + Say( + "Businessman", + "As you know, my business is somewhat... clandestine. So pardon my manner of speaking; we cannot be too careful nowadays.", + ), + Say( + "Businessman", + "Now, what would you like to ask me?", + ), + Ask( + "first_question", + 'Ask anything. For example: "What is your business exactly?"', + speaker="{player_name}", + response_speaker="{player_name}", + min_chars=1, + ), + Generate("Businessman", stream_businessman_reply, postprocess=postprocess_businessman_reply), + ] diff --git a/my_name_rpg/voice.py b/my_name_rpg/voice.py new file mode 100644 index 0000000000000000000000000000000000000000..f6186efbb406e722aea56ea28be76f38488ddaf3 --- /dev/null +++ b/my_name_rpg/voice.py @@ -0,0 +1,180 @@ +"""Kokoro voice caching and playback for game characters.""" + +from __future__ import annotations + +import hashlib +import importlib.util +import re +import threading +from pathlib import Path +from typing import Any + + +class SpeechHandle: + def __init__(self, thread: threading.Thread, done: threading.Event) -> None: + self._thread = thread + self._done = done + + def is_done(self) -> bool: + return self._done.is_set() + + +class KokoroVoiceManager: + def __init__( + self, + project_root: str | Path, + character_voices: dict[str, str] | None = None, + ) -> None: + self.project_root = Path(project_root) + self.character_voices = character_voices or {"Businessman": "am_michael"} + self.output_dir = self.project_root / "outputs" / "tts_cache" + self._runtime: dict[str, dict[str, Any]] = {} + self._runtime_lock = threading.Lock() + self._warned = False + + def prepare_script(self, script: list[object]) -> None: + from dream_chat_engine.actions import Say + + for action in script: + if isinstance(action, Say) and "{" not in action.text: + self.prepare(action.speaker, action.text) + + def prepare(self, speaker: str, text: str) -> Path | None: + voice = self.character_voices.get(speaker) + clean_text = self._clean_text(text) + if not voice or not clean_text: + return None + + output = self._cache_path(speaker, voice, clean_text) + if output.exists(): + return output + + try: + self._synthesize_to_file(voice, clean_text, output) + return output + except Exception as exc: + self._warn_once(exc) + return None + + def speak( + self, + speaker: str, + text: str, + prepared_audio: Path | str | None = None, + ) -> SpeechHandle | None: + voice = self.character_voices.get(speaker) + clean_text = self._clean_text(text) + if not voice or not clean_text: + return None + + done = threading.Event() + + def worker() -> None: + try: + wav_path = Path(prepared_audio) if prepared_audio else self.prepare(speaker, clean_text) + if wav_path is not None: + self._play(wav_path) + except Exception as exc: + self._warn_once(exc) + finally: + done.set() + + thread = threading.Thread(target=worker, daemon=True) + handle = SpeechHandle(thread, done) + thread.start() + return handle + + def stop(self) -> None: + try: + import winsound + + winsound.PlaySound(None, winsound.SND_PURGE) + except Exception: + pass + + def _synthesize_to_file(self, voice: str, text: str, output: Path) -> None: + runtime = self._get_runtime(voice) + np = runtime["np"] + sf = runtime["sf"] + pipeline = runtime["pipeline"] + voice_tensor = runtime["voice_tensor"] + + audios = [] + generator = pipeline(text, voice=voice_tensor, speed=0.96, split_pattern=r"\n+") + for result in generator: + audio = result.audio + if audio is None: + continue + if hasattr(audio, "detach"): + audio = audio.detach().cpu().numpy() + audios.append(np.asarray(audio, dtype=np.float32)) + + if not audios: + raise RuntimeError("Kokoro did not generate audio.") + + output.parent.mkdir(parents=True, exist_ok=True) + sf.write(str(output), np.concatenate(audios), 24000) + + def _get_runtime(self, voice: str) -> dict[str, Any]: + with self._runtime_lock: + runtime = self._runtime.get(voice) + if runtime is None: + runtime = self._load_runtime(voice) + self._runtime[voice] = runtime + return runtime + + def _load_runtime(self, voice: str) -> dict[str, Any]: + sample = self._load_kokoro_sample() + sample.require_assets() + sample.require_python_version() + voice_files = sample.resolve_voice_files(voice) + lang = sample.infer_lang(voice_files, "auto") + np, sf, torch, KModel, KPipeline = sample.import_runtime() + device = sample.select_device(torch, "auto") + voice_tensor = sample.load_voice_tensor(torch, voice_files) + + model = KModel( + repo_id=sample.REPO_ID, + config=str(sample.CONFIG_FILE), + model=str(sample.MODEL_FILE), + ) + model = model.to(device).eval() + pipeline = KPipeline(lang_code=lang, repo_id=sample.REPO_ID, model=model) + + return { + "np": np, + "sf": sf, + "pipeline": pipeline, + "voice_tensor": voice_tensor, + } + + def _load_kokoro_sample(self): + sample_path = self.project_root / "kokoro" / "sample.py" + spec = importlib.util.spec_from_file_location("_my_name_kokoro_sample", sample_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Could not load Kokoro helper at {sample_path}.") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + def _cache_path(self, speaker: str, voice: str, text: str) -> Path: + speaker_slug = re.sub(r"[^a-z0-9]+", "_", speaker.lower()).strip("_") or "voice" + digest = hashlib.sha256(f"{voice}\n{text}".encode("utf-8")).hexdigest()[:24] + return self.output_dir / speaker_slug / f"{voice}_{digest}.wav" + + @staticmethod + def _play(wav_path: Path) -> None: + import winsound + + winsound.PlaySound(str(wav_path), winsound.SND_FILENAME) + + @staticmethod + def _clean_text(text: str) -> str: + text = re.sub(r"\[[^\]]+\]", "", text) + text = re.sub(r"\s+", " ", text).strip() + return text[:500] + + def _warn_once(self, exc: Exception) -> None: + if not self._warned: + self._warned = True + print(f"My Name: RPG voice unavailable: {exc}") diff --git a/outputs/dream_chat_engine_preview.jpg b/outputs/dream_chat_engine_preview.jpg new file mode 100644 index 0000000000000000000000000000000000000000..87d8fbf8ce67f82f9d6a4b1999ab6cad1f80137d --- /dev/null +++ b/outputs/dream_chat_engine_preview.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e87372e1290e60160004d8c27736d8d343d23110f067685a9c7c2b0b8b88912 +size 107377 diff --git a/outputs/kokoro_businessman_test.wav b/outputs/kokoro_businessman_test.wav new file mode 100644 index 0000000000000000000000000000000000000000..e67b32e5f7a308d2dab23701bf1a147f3f3d440f Binary files /dev/null and b/outputs/kokoro_businessman_test.wav differ diff --git a/outputs/kokoro_sample.wav b/outputs/kokoro_sample.wav new file mode 100644 index 0000000000000000000000000000000000000000..f1806dc2f4057b338fc69d86c3263cc82777c66d --- /dev/null +++ b/outputs/kokoro_sample.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:539739618e4a7668be3405781f1877d8d820152b2f5849a1c78ff4c09132cf4f +size 194444 diff --git a/outputs/kokoro_test.wav b/outputs/kokoro_test.wav new file mode 100644 index 0000000000000000000000000000000000000000..36d308552c7339475f57b1079b64d6d6350eed3e --- /dev/null +++ b/outputs/kokoro_test.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8c99ad041626c20d9a360eb3398c69717add4ef90c2648705ddccf83907c0ef +size 370844 diff --git a/outputs/tts/am_michael_1781378852989.wav b/outputs/tts/am_michael_1781378852989.wav new file mode 100644 index 0000000000000000000000000000000000000000..ad734390d4a7e258744040bd2db33a5690d09820 --- /dev/null +++ b/outputs/tts/am_michael_1781378852989.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaccbdf3c85d1b003fceaf48d052d3925f77a6ef5a83972c97bb4cd6778ec62e +size 240044 diff --git a/outputs/tts/am_michael_1781378862368.wav b/outputs/tts/am_michael_1781378862368.wav new file mode 100644 index 0000000000000000000000000000000000000000..2638b60f549520e923534e363821c745d365b89d --- /dev/null +++ b/outputs/tts/am_michael_1781378862368.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cecf4f899ac32758f5b7763a9c281fc6b34e4b713802f3451756fae6c8ae5b5 +size 556844 diff --git a/outputs/tts/am_michael_1781378874715.wav b/outputs/tts/am_michael_1781378874715.wav new file mode 100644 index 0000000000000000000000000000000000000000..9588f0dc9b4d41430c3de0fc4fdd89c515b968a4 --- /dev/null +++ b/outputs/tts/am_michael_1781378874715.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa16d9d9aac648562b8f5b4c7bbaa2a2b2b48c3acdce2c00a7d5b08b958b7681 +size 211244 diff --git a/outputs/tts/am_michael_1781378894011.wav b/outputs/tts/am_michael_1781378894011.wav new file mode 100644 index 0000000000000000000000000000000000000000..05b891409f3dfcd7ffabe472fb90ceb1fa6181a2 --- /dev/null +++ b/outputs/tts/am_michael_1781378894011.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b10536ae33ede1182de4e5ddb1fdab39fbf47da5fd94f256e8950853f7c25d9a +size 186044 diff --git a/outputs/tts/am_michael_1781378899096.wav b/outputs/tts/am_michael_1781378899096.wav new file mode 100644 index 0000000000000000000000000000000000000000..7964c903315a1ca16afe4ea641d000d952abaf0b --- /dev/null +++ b/outputs/tts/am_michael_1781378899096.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:babbc67f99c4ed6d46a31e7e3757ba3cdf89d8295faef25721e890d0eba2da5f +size 427244 diff --git a/outputs/tts/am_michael_1781378909364.wav b/outputs/tts/am_michael_1781378909364.wav new file mode 100644 index 0000000000000000000000000000000000000000..8cee5fdc9991104b7d8a3125cf8c292eefc81d6e --- /dev/null +++ b/outputs/tts/am_michael_1781378909364.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ff789741a615072bffcd9a6f4fc5fd2cc97173531d8bc489c344e80e54ae48d0 +size 133244 diff --git a/outputs/tts/am_michael_1781378932377.wav b/outputs/tts/am_michael_1781378932377.wav new file mode 100644 index 0000000000000000000000000000000000000000..b446c22ff035dcbc72cafc1d3571f9de4e444fca --- /dev/null +++ b/outputs/tts/am_michael_1781378932377.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8bc701232ca65d89fa5babb54124f73fc9a3010025b877560c7ba5092e8bd59 +size 414044 diff --git a/outputs/tts/am_michael_1781379206195.wav b/outputs/tts/am_michael_1781379206195.wav new file mode 100644 index 0000000000000000000000000000000000000000..ff5140570039cec2a05b36f52d88b4baeb2361b5 --- /dev/null +++ b/outputs/tts/am_michael_1781379206195.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8cf98e88c79f5a12e6ab77986937a0b2ef92ce87fe93fffee8e8243f3ca195b +size 240044