init
This commit is contained in:
9 files changed
+902
No files matched your search
@@ -0,0 +1,3 @@
|
||||
MIMO_API_KEY=tp-67
|
||||
# optional
|
||||
MIMO_BASE_URL=https://token-plan-sgp.xiaomimimo.com/v1
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/python
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=python
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/#use-with-ide
|
||||
.pdm.toml
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
### Python Patch ###
|
||||
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
|
||||
poetry.toml
|
||||
|
||||
# ruff
|
||||
.ruff_cache/
|
||||
|
||||
# LSP config files
|
||||
pyrightconfig.json
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/python
|
||||
|
||||
|
||||
mimotts.config.json
|
||||
@@ -0,0 +1,80 @@
|
||||
# mimotts-cli
|
||||
|
||||
Tiny CLI for Xiaomi MiMo TTS.
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
uv pip install -e .
|
||||
```
|
||||
|
||||
Or run without activating:
|
||||
|
||||
```bash
|
||||
uv run mimotts --help
|
||||
```
|
||||
|
||||
## Auth
|
||||
|
||||
Create `.env`:
|
||||
|
||||
```bash
|
||||
MIMO_API_KEY=your_key_here
|
||||
# optional
|
||||
MIMO_BASE_URL=https://api.xiaomimimo.com/v1
|
||||
```
|
||||
|
||||
## Configure a built-in voice
|
||||
|
||||
```bash
|
||||
uv run mimotts config --preset moli
|
||||
```
|
||||
|
||||
Interactive:
|
||||
|
||||
```bash
|
||||
uv run mimotts config
|
||||
```
|
||||
|
||||
Voice names included here:
|
||||
|
||||
- `mimo_default`
|
||||
- `default_zh`
|
||||
- `default_en`
|
||||
- `bingtang`
|
||||
- `moli`
|
||||
- `suda`
|
||||
- `baihua`
|
||||
- `mia`
|
||||
- `chloe`
|
||||
- `milo`
|
||||
- `dean`
|
||||
|
||||
The Chinese display names are also accepted for the v2.5 voices: `冰糖`, `茉莉`, `苏打`, `白桦`.
|
||||
|
||||
## Configure a designed voice
|
||||
|
||||
```bash
|
||||
uv run mimotts config --design "young warm male voice, clear, calm, slight southern accent"
|
||||
```
|
||||
|
||||
## Start a session
|
||||
|
||||
```bash
|
||||
uv run mimotts session
|
||||
```
|
||||
|
||||
Type text, press Enter, get audio. Each generation is saved under `workspace/<session-id>/` with a JSONL history file.
|
||||
|
||||
## Generate once
|
||||
|
||||
```bash
|
||||
uv run mimotts say "hello from this cursed chat-completion tts api" --play
|
||||
```
|
||||
|
||||
## Export an old generated file
|
||||
|
||||
```bash
|
||||
uv run mimotts export workspace/latest.jsonl --index -1 --to out.wav
|
||||
```
|
||||
@@ -0,0 +1,19 @@
|
||||
[project]
|
||||
name = "mimotts-cli"
|
||||
version = "0.1.0"
|
||||
description = "Tiny Xiaomi MiMo TTS CLI"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = [
|
||||
"httpx>=0.27",
|
||||
"python-dotenv>=1.0",
|
||||
"rich>=13.7",
|
||||
"typer>=0.12",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
mimotts = "mimotts_cli.main:app"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
Whitespace-only changes.
@@ -0,0 +1,414 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import uuid
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
|
||||
import httpx
|
||||
import typer
|
||||
from dotenv import load_dotenv
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
from rich.prompt import Confirm, Prompt
|
||||
from rich.table import Table
|
||||
|
||||
app = typer.Typer(no_args_is_help=True, add_completion=False)
|
||||
console = Console()
|
||||
|
||||
DEFAULT_BASE_URL = "https://api.xiaomimimo.com/v1"
|
||||
DEFAULT_CONFIG = Path("mimotts.config.json")
|
||||
DEFAULT_WORKSPACE = Path("workspace")
|
||||
|
||||
# Values from public docs/community examples. For v2.5, Chinese display names
|
||||
# appear to be accepted by the official model. ASCII aliases are mapped below.
|
||||
PRESET_VOICES: dict[str, str] = {
|
||||
"mimo_default": "mimo_default",
|
||||
"default_zh": "default_zh",
|
||||
"default_en": "default_en",
|
||||
"bingtang": "冰糖",
|
||||
"moli": "茉莉",
|
||||
"suda": "苏打",
|
||||
"baihua": "白桦",
|
||||
"mia": "Mia",
|
||||
"chloe": "Chloe",
|
||||
"milo": "Milo",
|
||||
"dean": "Dean",
|
||||
"冰糖": "冰糖",
|
||||
"茉莉": "茉莉",
|
||||
"苏打": "苏打",
|
||||
"白桦": "白桦",
|
||||
}
|
||||
|
||||
AudioFormat = Literal["wav", "mp3", "pcm16"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TTSConfig:
|
||||
mode: Literal["preset", "design"] = "preset"
|
||||
model: str = "mimo-v2.5-tts"
|
||||
voice: str | None = "mimo_default"
|
||||
design_prompt: str | None = None
|
||||
audio_format: AudioFormat = "wav"
|
||||
text_role: Literal["assistant", "user"] = "assistant"
|
||||
style_prefix: str = ""
|
||||
|
||||
@staticmethod
|
||||
def load(path: Path) -> "TTSConfig":
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return TTSConfig(**data)
|
||||
|
||||
def save(self, path: Path) -> None:
|
||||
path.write_text(
|
||||
json.dumps(asdict(self), ensure_ascii=False, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def now_id() -> str:
|
||||
return datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
|
||||
def utc_iso() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def need_api_key() -> str:
|
||||
load_dotenv()
|
||||
key = os.getenv("MIMO_API_KEY")
|
||||
if not key:
|
||||
raise typer.BadParameter("Missing MIMO_API_KEY. Put it in .env or export it.")
|
||||
return key
|
||||
|
||||
|
||||
def base_url() -> str:
|
||||
load_dotenv()
|
||||
return os.getenv("MIMO_BASE_URL", DEFAULT_BASE_URL).rstrip("/")
|
||||
|
||||
|
||||
def config_or_die(path: Path) -> TTSConfig:
|
||||
if not path.exists():
|
||||
raise typer.BadParameter(f"No config at {path}. Run: mimotts config")
|
||||
return TTSConfig.load(path)
|
||||
|
||||
|
||||
def audio_ext(fmt: str) -> str:
|
||||
return "wav" if fmt == "pcm16" else fmt
|
||||
|
||||
|
||||
def build_payload(cfg: TTSConfig, text: str) -> dict[str, Any]:
|
||||
final_text = f"{cfg.style_prefix}{text}" if cfg.style_prefix else text
|
||||
|
||||
if cfg.mode == "design":
|
||||
# VoiceDesign is directed by a user message; it should not receive audio.voice.
|
||||
messages = [
|
||||
{"role": "user", "content": cfg.design_prompt or "Design a natural friendly voice."},
|
||||
{"role": "assistant", "content": final_text},
|
||||
]
|
||||
return {
|
||||
"model": cfg.model,
|
||||
"messages": messages,
|
||||
"audio": {"format": cfg.audio_format},
|
||||
}
|
||||
|
||||
return {
|
||||
"model": cfg.model,
|
||||
"messages": [{"role": cfg.text_role, "content": final_text}],
|
||||
"audio": {
|
||||
"format": cfg.audio_format,
|
||||
"voice": cfg.voice or "mimo_default",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def extract_audio_b64(data: dict[str, Any]) -> str:
|
||||
# Expected OpenAI-ish shape:
|
||||
# choices[0].message.audio.data
|
||||
try:
|
||||
audio = data["choices"][0]["message"]["audio"]
|
||||
if isinstance(audio, dict) and isinstance(audio.get("data"), str):
|
||||
return audio["data"]
|
||||
except (KeyError, IndexError, TypeError):
|
||||
pass
|
||||
|
||||
# Be forgiving, because providers love "compatible" APIs until they don't.
|
||||
def walk(obj: Any) -> str | None:
|
||||
if isinstance(obj, dict):
|
||||
if isinstance(obj.get("data"), str) and (
|
||||
"audio" in obj or obj.get("type") in {"audio", "output_audio"}
|
||||
):
|
||||
return obj["data"]
|
||||
for value in obj.values():
|
||||
found = walk(value)
|
||||
if found:
|
||||
return found
|
||||
elif isinstance(obj, list):
|
||||
for value in obj:
|
||||
found = walk(value)
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
found = walk(data)
|
||||
if found:
|
||||
return found
|
||||
|
||||
raise RuntimeError("Could not find base64 audio in response.")
|
||||
|
||||
|
||||
def request_tts(cfg: TTSConfig, text: str) -> tuple[bytes, dict[str, Any]]:
|
||||
key = need_api_key()
|
||||
url = f"{base_url()}/chat/completions"
|
||||
headers = {
|
||||
"api-key": key,
|
||||
"content-type": "application/json",
|
||||
}
|
||||
payload = build_payload(cfg, text)
|
||||
|
||||
with httpx.Client(timeout=120) as client:
|
||||
res = client.post(url, headers=headers, json=payload)
|
||||
if res.status_code >= 400:
|
||||
raise RuntimeError(f"HTTP {res.status_code}: {res.text[:2000]}")
|
||||
data = res.json()
|
||||
|
||||
b64 = extract_audio_b64(data)
|
||||
return base64.b64decode(b64), data
|
||||
|
||||
|
||||
def play_file(path: Path) -> None:
|
||||
system = platform.system().lower()
|
||||
try:
|
||||
if system == "windows":
|
||||
os.startfile(str(path)) # type: ignore[attr-defined]
|
||||
elif system == "darwin":
|
||||
subprocess.Popen(["open", str(path)])
|
||||
else:
|
||||
opener = shutil.which("xdg-open") or shutil.which("gio")
|
||||
if not opener:
|
||||
console.print("[yellow]No xdg-open/gio found; not playing.[/yellow]")
|
||||
return
|
||||
cmd = [opener, str(path)] if Path(opener).name != "gio" else [opener, "open", str(path)]
|
||||
subprocess.Popen(cmd)
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]Could not play {path}: {e}[/yellow]")
|
||||
|
||||
|
||||
def append_jsonl(path: Path, obj: dict[str, Any]) -> None:
|
||||
with path.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(obj, ensure_ascii=False) + "\n")
|
||||
|
||||
|
||||
def save_generation(
|
||||
cfg: TTSConfig,
|
||||
text: str,
|
||||
audio: bytes,
|
||||
response: dict[str, Any],
|
||||
session_dir: Path,
|
||||
history_path: Path,
|
||||
) -> Path:
|
||||
idx = sum(1 for _ in history_path.open("r", encoding="utf-8")) if history_path.exists() else 0
|
||||
ext = audio_ext(cfg.audio_format)
|
||||
audio_path = session_dir / f"{idx + 1:04d}.{ext}"
|
||||
audio_path.write_bytes(audio)
|
||||
|
||||
append_jsonl(
|
||||
history_path,
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"created_at": utc_iso(),
|
||||
"text": text,
|
||||
"audio_path": str(audio_path),
|
||||
"config": asdict(cfg),
|
||||
"response_model": response.get("model"),
|
||||
"usage": response.get("usage"),
|
||||
},
|
||||
)
|
||||
return audio_path
|
||||
|
||||
|
||||
@app.command("voices")
|
||||
def voices() -> None:
|
||||
"""List built-in voice aliases."""
|
||||
table = Table(title="MiMo TTS voices", box=box.SIMPLE)
|
||||
table.add_column("alias")
|
||||
table.add_column("sent as")
|
||||
for alias, value in PRESET_VOICES.items():
|
||||
table.add_row(alias, value)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@app.command("config")
|
||||
def configure(
|
||||
path: Path = typer.Option(DEFAULT_CONFIG, "--config", "-c"),
|
||||
preset: str | None = typer.Option(None, "--preset", help="Voice alias/name, e.g. moli, mia, default_zh"),
|
||||
design: str | None = typer.Option(None, "--design", help="Natural-language voice design prompt"),
|
||||
fmt: AudioFormat = typer.Option("wav", "--format"),
|
||||
style: str = typer.Option("", "--style", help="Optional prefix like '(开心)' or '(台湾腔 变慢)'"),
|
||||
text_role: Literal["assistant", "user"] = typer.Option("assistant", "--text-role"),
|
||||
start: bool = typer.Option(False, "--start", help="Immediately enter session after saving"),
|
||||
) -> None:
|
||||
"""Create/update voice config."""
|
||||
if design and preset:
|
||||
raise typer.BadParameter("Pick either --design or --preset, not both.")
|
||||
|
||||
if not design and not preset:
|
||||
console.print(Panel("Choose [bold]preset[/bold] for built-ins, or [bold]design[/bold] for natural-language voice design."))
|
||||
mode = Prompt.ask("mode", choices=["preset", "design"], default="preset")
|
||||
if mode == "design":
|
||||
design = Prompt.ask("describe the voice")
|
||||
else:
|
||||
voices()
|
||||
preset = Prompt.ask("voice alias", default="mimo_default")
|
||||
|
||||
if design:
|
||||
cfg = TTSConfig(
|
||||
mode="design",
|
||||
model="mimo-v2.5-tts-voicedesign",
|
||||
voice=None,
|
||||
design_prompt=design,
|
||||
audio_format=fmt,
|
||||
text_role=text_role,
|
||||
style_prefix=style,
|
||||
)
|
||||
else:
|
||||
voice = PRESET_VOICES.get(preset or "mimo_default", preset or "mimo_default")
|
||||
cfg = TTSConfig(
|
||||
mode="preset",
|
||||
model="mimo-v2.5-tts",
|
||||
voice=voice,
|
||||
design_prompt=None,
|
||||
audio_format=fmt,
|
||||
text_role=text_role,
|
||||
style_prefix=style,
|
||||
)
|
||||
|
||||
cfg.save(path)
|
||||
console.print(f"[green]Saved config:[/green] {path}")
|
||||
|
||||
if start:
|
||||
run_session(path=path, workspace=DEFAULT_WORKSPACE, play=False)
|
||||
|
||||
|
||||
@app.command("say")
|
||||
def say(
|
||||
text: str = typer.Argument(...),
|
||||
path: Path = typer.Option(DEFAULT_CONFIG, "--config", "-c"),
|
||||
out: Path | None = typer.Option(None, "--out", "-o"),
|
||||
workspace: Path = typer.Option(DEFAULT_WORKSPACE, "--workspace", "-w"),
|
||||
play: bool = typer.Option(False, "--play"),
|
||||
) -> None:
|
||||
"""Generate one audio file."""
|
||||
cfg = config_or_die(path)
|
||||
session_dir = workspace / now_id()
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
history_path = session_dir / "history.jsonl"
|
||||
|
||||
console.print("[dim]generating...[/dim]")
|
||||
audio, response = request_tts(cfg, text)
|
||||
|
||||
if out is None:
|
||||
out = save_generation(cfg, text, audio, response, session_dir, history_path)
|
||||
else:
|
||||
out.parent.mkdir(parents=True, exist_ok=True)
|
||||
out.write_bytes(audio)
|
||||
append_jsonl(
|
||||
history_path,
|
||||
{
|
||||
"id": str(uuid.uuid4()),
|
||||
"created_at": utc_iso(),
|
||||
"text": text,
|
||||
"audio_path": str(out),
|
||||
"config": asdict(cfg),
|
||||
"response_model": response.get("model"),
|
||||
"usage": response.get("usage"),
|
||||
},
|
||||
)
|
||||
|
||||
latest = workspace / "latest.jsonl"
|
||||
latest.parent.mkdir(parents=True, exist_ok=True)
|
||||
latest.write_text(str(history_path) + "\n", encoding="utf-8")
|
||||
|
||||
console.print(f"[green]wrote[/green] {out}")
|
||||
if play:
|
||||
play_file(out)
|
||||
|
||||
|
||||
@app.command("session")
|
||||
def run_session(
|
||||
path: Path = typer.Option(DEFAULT_CONFIG, "--config", "-c"),
|
||||
workspace: Path = typer.Option(DEFAULT_WORKSPACE, "--workspace", "-w"),
|
||||
play: bool = typer.Option(False, "--play"),
|
||||
) -> None:
|
||||
"""Interactive TTS session. Type text, press Enter. Ctrl-C exits."""
|
||||
cfg = config_or_die(path)
|
||||
session_dir = workspace / now_id()
|
||||
session_dir.mkdir(parents=True, exist_ok=True)
|
||||
history_path = session_dir / "history.jsonl"
|
||||
|
||||
latest = workspace / "latest.jsonl"
|
||||
latest.parent.mkdir(parents=True, exist_ok=True)
|
||||
latest.write_text(str(history_path) + "\n", encoding="utf-8")
|
||||
|
||||
console.print(Panel.fit(
|
||||
f"[bold]MiMo TTS session[/bold]\n"
|
||||
f"mode={cfg.mode} model={cfg.model} voice={cfg.voice or '<designed>'}\n"
|
||||
f"history={history_path}\n"
|
||||
f"Ctrl-C to exit.",
|
||||
border_style="green",
|
||||
))
|
||||
|
||||
while True:
|
||||
try:
|
||||
text = Prompt.ask("\ntext").strip()
|
||||
if not text:
|
||||
continue
|
||||
if text in {":q", ":quit", "exit"}:
|
||||
break
|
||||
|
||||
console.print("[dim]generating...[/dim]")
|
||||
audio, response = request_tts(cfg, text)
|
||||
audio_path = save_generation(cfg, text, audio, response, session_dir, history_path)
|
||||
console.print(f"[green]wrote[/green] {audio_path}")
|
||||
if play:
|
||||
play_file(audio_path)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[dim]bye[/dim]")
|
||||
break
|
||||
except Exception as e:
|
||||
console.print(f"[red]error:[/red] {e}")
|
||||
|
||||
|
||||
@app.command("export")
|
||||
def export(
|
||||
history: Path = typer.Argument(..., help="history.jsonl, or workspace/latest.jsonl"),
|
||||
index: int = typer.Option(-1, "--index", "-i", help="0-based index; negative works like Python"),
|
||||
to: Path = typer.Option(..., "--to", "-o"),
|
||||
) -> None:
|
||||
"""Copy one generated audio file from JSONL history."""
|
||||
if history.name == "latest.jsonl":
|
||||
target = history.read_text(encoding="utf-8").strip()
|
||||
history = Path(target)
|
||||
|
||||
rows = [json.loads(line) for line in history.read_text(encoding="utf-8").splitlines() if line.strip()]
|
||||
if not rows:
|
||||
raise typer.BadParameter("history is empty")
|
||||
|
||||
row = rows[index]
|
||||
src = Path(row["audio_path"])
|
||||
to.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(src, to)
|
||||
console.print(f"[green]exported[/green] {src} -> {to}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
@@ -0,0 +1,205 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "annotated-doc"
|
||||
version = "0.0.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.4.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.16.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpcore"
|
||||
version = "1.0.9"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.28.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "certifi" },
|
||||
{ name = "httpcore" },
|
||||
{ name = "idna" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markdown-it-py"
|
||||
version = "4.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "mdurl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mdurl"
|
||||
version = "0.1.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mimotts-cli"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "httpx" },
|
||||
{ name = "python-dotenv" },
|
||||
{ name = "rich" },
|
||||
{ name = "typer" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "httpx", specifier = ">=0.27" },
|
||||
{ name = "python-dotenv", specifier = ">=1.0" },
|
||||
{ name = "rich", specifier = ">=13.7" },
|
||||
{ name = "typer", specifier = ">=0.12" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rich"
|
||||
version = "15.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "markdown-it-py" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shellingham"
|
||||
version = "1.5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typer"
|
||||
version = "0.25.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "annotated-doc" },
|
||||
{ name = "click" },
|
||||
{ name = "rich" },
|
||||
{ name = "shellingham" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
|
||||
]
|
||||
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
Reference in new issue
Block a user