Skip to content

Instantly share code, notes, and snippets.

@z80oolong
Last active September 7, 2026 13:44
Show Gist options
  • Select an option

  • Save z80oolong/f95f5e98cd4874bd6f7173f03cdf6fce to your computer and use it in GitHub Desktop.

Select an option

Save z80oolong/f95f5e98cd4874bd6f7173f03cdf6fce to your computer and use it in GitHub Desktop.
VOICEVOX 関連の短いプログラム若しくはファイル等
#!/usr/bin/env python3
"""VVM 内の metas から speaker_uuid を読み取り、character_info を生成する。
デフォルト:
character_info/<uuid>/metas.json (内容は {})
--full:
metas.json / policy.md / portrait.png / icons/<style_id>.png
--voice-samples:
voice_samples/<style_id>_00N.wav を合成
優先: voicevox_core Python API
代替: voicevox-core-cli
voicevox-core-cli [-x lib] [-d dict] [-v vvm] [-i style_id] [-o out.wav] TEXT
"""
from __future__ import annotations
import argparse
import hashlib
import io
import json
import re
import shutil
import struct
import subprocess
import sys
import tempfile
import zipfile
from pathlib import Path
from typing import Any, Iterable, Protocol
_UUID_RE = re.compile(
r"^[0-9a-fA-F]{8}-"
r"[0-9a-fA-F]{4}-"
r"[0-9a-fA-F]{4}-"
r"[0-9a-fA-F]{4}-"
r"[0-9a-fA-F]{12}$"
)
ENGINE_METAS_CONTENT = "{}\n"
# 識別用の近似色(公式テーマの完全再現ではない)
IMAGE_COLORS_BY_NAME: dict[str, tuple[int, int, int]] = {
"四国めたん": (0xDF, 0x4C, 0x94),
"ずんだもん": (0x33, 0xA6, 0x5E),
"春日部つむぎ": (0xFF, 0x99, 0x14),
"雨晴はう": (0x2C, 0xA9, 0xE1),
"波音リツ": (0x67, 0x31, 0x53),
"玄野武宏": (0x50, 0x96, 0x6A),
"白上虎太郎": (0x99, 0xD0, 0x2B),
"青山龍星": (0x38, 0x6C, 0xB0),
"冥鳴ひまり": (0xA4, 0x5A, 0xAA),
"九州そら": (0x69, 0x64, 0xAD),
"もち子さん": (0x57, 0x58, 0x55),
"剣崎雌雄": (0xBB, 0xBF, 0xE5),
"WhiteCUL": (0x1D, 0x86, 0xAE),
"後鬼": (0x21, 0x85, 0xDA),
"No.7": (0x77, 0x77, 0x77),
"ちび式じい": (0x89, 0xC3, 0xEB),
"櫻歌ミコ": (0xE9, 0xA3, 0x68),
"小夜/SAYO": (0xFF, 0x66, 0x87),
"ナースロボ_タイプT": (0x00, 0xB0, 0x50),
"†聖騎士 紅桜†": (0xF1, 0x61, 0x52),
"雀松朱司": (0xE3, 0x70, 0x6B),
"麒ヶ島宗麟": (0xE3, 0xD3, 0x5A),
"春歌ナナ": (0xFF, 0xC0, 0xCB),
"猫使アル": (0xF5, 0xA9, 0xB8),
"猫使ビィ": (0xA9, 0xD0, 0xF5),
"中国うさぎ": (0xFC, 0x4E, 0x32),
"栗田まろん": (0xC8, 0xA6, 0x72),
"あいえるたん": (0xEE, 0x7B, 0x2B),
"満別花丸": (0xE6, 0xF5, 0xB0),
"琴詠ニア": (0xFB, 0x80, 0x28),
"Voidoll": (0x6C, 0x7A, 0x89),
"ぞん子": (0x7D, 0x3C, 0x98),
"中部つるぎ": (0x5D, 0x6D, 0x7E),
"離途": (0x5D, 0x4E, 0x37),
"黒沢冴白": (0xD5, 0xD8, 0xDC),
"ユーレイちゃん": (0xD2, 0xB4, 0xDE),
"東北ずん子": (0x31, 0x67, 0x45),
"東北きりたん": (0x9F, 0x6F, 0x55),
"東北イタコ": (0x00, 0x88, 0x99),
"あんこもん": (0x6E, 0x2C, 0x00),
}
IMAGE_COLORS_BY_UUID: dict[str, tuple[int, int, int]] = {
"7ffcb7ce-00ec-4bdc-82cd-45a8889e43ff": IMAGE_COLORS_BY_NAME["四国めたん"],
"388f246b-8c41-4ac1-8e2d-5d79f3ff56d9": IMAGE_COLORS_BY_NAME["ずんだもん"],
"35b2c544-660e-401e-b503-0e14c635303a": IMAGE_COLORS_BY_NAME["春日部つむぎ"],
"3474ee95-c274-47f9-aa1a-8322163d96f1": IMAGE_COLORS_BY_NAME["雨晴はう"],
"8eaad775-3119-417e-8cf4-2a10bfd592c8": IMAGE_COLORS_BY_NAME["冥鳴ひまり"],
}
DEFAULT_SAMPLE_TEXTS = [
"こんにちは。",
"音声サンプルです。",
"よろしくお願いします。",
]
POLICY_TEMPLATE = """\
# {name}
本ディレクトリの画像は公式イラストではありません。
識別用に自動生成したダミー画像です。
voice_samples がある場合、その WAV は VOICEVOX CORE により当該 VVM から
合成したサンプルです。利用時は各キャラクター/音声ライブラリの公式利用規約に
従い、必要なクレジットを表記してください。
- VOICEVOX: https://voicevox.hiroshiba.jp/
- 利用規約は公式サイトおよび各音声ライブラリの案内を参照
speaker_uuid: `{uuid}`
"""
# ---------------------------------------------------------------------------
# VVM / metas
# ---------------------------------------------------------------------------
def _find_metas_member(names: Iterable[str]) -> str | None:
candidates = [
n for n in names if not n.endswith("/") and Path(n).name == "metas.json"
]
if not candidates:
return None
candidates.sort(key=lambda n: (n.count("/"), len(n)))
return candidates[0]
def load_speakers_from_vvm(vvm_path: Path) -> list[dict[str, Any]]:
if not vvm_path.is_file():
raise FileNotFoundError(f"VVM not found: {vvm_path}")
with zipfile.ZipFile(vvm_path, "r") as zf:
member = _find_metas_member(zf.namelist())
if member is None:
raise FileNotFoundError(f"metas.json not found in archive: {vvm_path}")
raw = json.loads(zf.read(member).decode("utf-8"))
if isinstance(raw, list):
speakers = raw
elif isinstance(raw, dict):
if "speakers" in raw and isinstance(raw["speakers"], list):
speakers = raw["speakers"]
elif "speaker_uuid" in raw or "uuid" in raw:
speakers = [raw]
else:
raise ValueError(
f"Unexpected metas.json structure in {vvm_path}: "
f"top-level keys={list(raw.keys())!r}"
)
else:
raise ValueError(f"Unexpected metas.json type in {vvm_path}: {type(raw)}")
if not speakers:
raise ValueError(f"No speakers in metas.json of {vvm_path}")
return speakers
def extract_speaker_records(speakers: list[dict[str, Any]]) -> list[dict[str, Any]]:
seen: set[str] = set()
result: list[dict[str, Any]] = []
for sp in speakers:
if not isinstance(sp, dict):
continue
uuid = sp.get("speaker_uuid") or sp.get("uuid")
name = str(sp.get("name") or "")
if not uuid or not isinstance(uuid, str):
continue
uuid = uuid.strip()
if not _UUID_RE.match(uuid):
print(f"warning: skip invalid uuid: {uuid!r}", file=sys.stderr)
continue
if uuid in seen:
continue
seen.add(uuid)
style_ids: list[int] = []
styles = sp.get("styles") or []
if isinstance(styles, list):
for st in styles:
if not isinstance(st, dict):
continue
sid = st.get("id")
if isinstance(sid, int):
style_ids.append(sid)
elif isinstance(sid, str) and sid.isdigit():
style_ids.append(int(sid))
if not style_ids:
style_ids = [0]
result.append({"uuid": uuid, "name": name, "style_ids": style_ids})
if not result:
raise ValueError("No valid speaker_uuid found in metas")
return result
# ---------------------------------------------------------------------------
# Colors / dummy images
# ---------------------------------------------------------------------------
def color_for(uuid: str, name: str = "") -> tuple[int, int, int]:
if uuid in IMAGE_COLORS_BY_UUID:
return IMAGE_COLORS_BY_UUID[uuid]
if name and name in IMAGE_COLORS_BY_NAME:
return IMAGE_COLORS_BY_NAME[name]
if name:
for key, rgb in IMAGE_COLORS_BY_NAME.items():
if key in name or name in key:
return rgb
digest = hashlib.sha256(uuid.encode("utf-8")).digest()
return (64 + digest[0] % 128, 64 + digest[1] % 128, 64 + digest[2] % 128)
def luminance(rgb: tuple[int, int, int]) -> float:
r, g, b = rgb
return 0.299 * r + 0.587 * g + 0.114 * b
def text_color_for_bg(rgb: tuple[int, int, int]) -> tuple[int, int, int]:
return (20, 20, 20) if luminance(rgb) >= 140 else (245, 245, 245)
def initial_char(name: str) -> str:
for ch in name.strip():
if ch.isascii() and ch.isalnum():
return ch.upper()
o = ord(ch)
if (
0x3040 <= o <= 0x30FF
or 0x4E00 <= o <= 0x9FFF
or 0x3400 <= o <= 0x4DBF
or 0xFF66 <= o <= 0xFF9D
):
return ch
return "?"
def _load_font(size: int, font_path: Path | None):
from PIL import ImageFont
if font_path is not None:
if not font_path.is_file():
raise FileNotFoundError(f"Font not found: {font_path}")
return ImageFont.truetype(str(font_path), size=size)
for path in (
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
"/usr/share/fonts/truetype/fonts-japanese-gothic.ttf",
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
"C:/Windows/Fonts/msgothic.ttc",
"/System/Library/Fonts/Hiragino Sans GB.ttc",
):
if Path(path).is_file():
try:
return ImageFont.truetype(path, size=size)
except OSError:
continue
return ImageFont.load_default()
def render_icon(
path: Path,
ch: str,
rgb: tuple[int, int, int],
font_path: Path | None,
size: int = 256,
) -> None:
from PIL import Image, ImageDraw
img = Image.new("RGB", (size, size), rgb)
draw = ImageDraw.Draw(img)
font = _load_font(max(size // 2, 32), font_path)
fg = text_color_for_bg(rgb)
bbox = draw.textbbox((0, 0), ch, font=font)
tw, th = bbox[2] - bbox[0], bbox[3] - bbox[1]
xy = ((size - tw) / 2 - bbox[0], (size - th) / 2 - bbox[1])
draw.text(xy, ch, fill=fg, font=font)
path.parent.mkdir(parents=True, exist_ok=True)
img.save(path, "PNG")
def render_portrait(
path: Path,
name: str,
uuid: str,
rgb: tuple[int, int, int],
font_path: Path | None,
width: int = 256,
height: int = 512,
) -> None:
from PIL import Image, ImageDraw
img = Image.new("RGB", (width, height), rgb)
draw = ImageDraw.Draw(img)
fg = text_color_for_bg(rgb)
font_name = _load_font(max(width // 10, 18), font_path)
font_uuid = _load_font(max(width // 14, 14), font_path)
font_small = _load_font(max(width // 18, 12), font_path)
def center_text(y: float, text: str, font) -> None:
bbox = draw.textbbox((0, 0), text, font=font)
tw = bbox[2] - bbox[0]
x = (width - tw) / 2 - bbox[0]
draw.text((x, y), text, fill=fg, font=font)
center_text(height * 0.35, name or "(unknown)", font_name)
center_text(height * 0.72, uuid[:8], font_uuid)
center_text(height * 0.88, "dummy", font_small)
path.parent.mkdir(parents=True, exist_ok=True)
img.save(path, "PNG")
# ---------------------------------------------------------------------------
# Voice samples
# ---------------------------------------------------------------------------
def make_silent_wav(
path: Path,
*,
duration_sec: float = 0.3,
sample_rate: int = 24000,
) -> None:
nframes = int(sample_rate * duration_sec)
data = b"\x00\x00" * nframes
buf = io.BytesIO()
buf.write(b"RIFF")
buf.write(struct.pack("<I", 36 + len(data)))
buf.write(b"WAVEfmt ")
buf.write(
struct.pack(
"<IHHIIHH",
16,
1,
1,
sample_rate,
sample_rate * 2,
2,
16,
)
)
buf.write(b"data")
buf.write(struct.pack("<I", len(data)))
buf.write(data)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(buf.getvalue())
class TtsBackend(Protocol):
def tts(self, text: str, style_id: int) -> bytes: ...
def close(self) -> None: ...
class PythonCoreSynthesizer:
"""voicevox_core Python API。"""
def __init__(
self,
vvm: Path,
onnxruntime: Path,
open_jtalk_dict: Path,
) -> None:
from voicevox_core.blocking import (
Onnxruntime,
OpenJtalk,
Synthesizer,
VoiceModelFile,
)
if not vvm.is_file():
raise FileNotFoundError(f"VVM not found: {vvm}")
if not onnxruntime.is_file():
raise FileNotFoundError(f"ONNX Runtime not found: {onnxruntime}")
if not open_jtalk_dict.is_dir():
raise FileNotFoundError(f"OpenJTalk dict not found: {open_jtalk_dict}")
ort = Onnxruntime.load_once(filename=str(onnxruntime))
self._synthesizer = Synthesizer(ort, OpenJtalk(str(open_jtalk_dict)))
self._model_cm = VoiceModelFile.open(str(vvm))
self._model = self._model_cm.__enter__()
self._synthesizer.load_voice_model(self._model)
def tts(self, text: str, style_id: int) -> bytes:
return self._synthesizer.tts(text, style_id)
def close(self) -> None:
try:
self._model_cm.__exit__(None, None, None)
except Exception:
pass
class CliCoreSynthesizer:
"""voicevox-core-cli 経由の TTS。
Usage:
voicevox-core-cli [-x lib] [-d dict] [-v vvm] [-i speaker_id] [-o out.wav] TEXT
"""
def __init__(
self,
cli: Path,
vvm: Path,
onnxruntime: Path | None = None,
open_jtalk_dict: Path | None = None,
) -> None:
if cli.is_file():
self._cli = cli
else:
found = shutil.which(str(cli))
if not found:
raise FileNotFoundError(f"voicevox-core-cli not found: {cli}")
self._cli = Path(found)
if not vvm.is_file():
raise FileNotFoundError(f"VVM not found: {vvm}")
self._vvm = vvm
self._onnxruntime = onnxruntime
self._open_jtalk_dict = open_jtalk_dict
def tts(self, text: str, style_id: int) -> bytes:
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tmp:
out = Path(tmp.name)
try:
cmd: list[str] = [str(self._cli)]
if self._onnxruntime is not None:
cmd.extend(["-x", str(self._onnxruntime)])
if self._open_jtalk_dict is not None:
cmd.extend(["-d", str(self._open_jtalk_dict)])
cmd.extend(
[
"-v",
str(self._vvm),
"-i",
str(style_id),
"-o",
str(out),
text,
]
)
proc = subprocess.run(cmd, check=False, capture_output=True)
if proc.returncode != 0:
err = (proc.stderr or b"").decode("utf-8", errors="replace")
out_s = (proc.stdout or b"").decode("utf-8", errors="replace")
raise RuntimeError(
f"voicevox-core-cli failed (exit {proc.returncode}): "
f"{err or out_s}"
)
if not out.is_file() or out.stat().st_size == 0:
raise RuntimeError("voicevox-core-cli produced no WAV output")
return out.read_bytes()
finally:
out.unlink(missing_ok=True)
def close(self) -> None:
pass
def resolve_core_cli(explicit: Path | None) -> Path | None:
if explicit is not None:
if explicit.is_file():
return explicit
found = shutil.which(str(explicit))
return Path(found) if found else None
for name in ("voicevox-core-cli", "voicevox_core_cli"):
found = shutil.which(name)
if found:
return Path(found)
return None
def create_tts_backend(
vvm: Path,
*,
onnxruntime: Path | None,
open_jtalk_dict: Path | None,
core_cli: Path | None,
) -> tuple[TtsBackend, str]:
"""戻り値: (backend, \"python\"|\"cli\")"""
try:
import voicevox_core # noqa: F401
has_python = True
except ImportError:
has_python = False
if has_python:
if not onnxruntime or not open_jtalk_dict:
raise RuntimeError(
"Python voicevox_core is available but requires "
"--onnxruntime and --open-jtalk-dict"
)
return (
PythonCoreSynthesizer(vvm, onnxruntime, open_jtalk_dict),
"python",
)
cli_path = resolve_core_cli(core_cli)
if cli_path is None:
raise RuntimeError(
"voicevox_core Python package not found, and voicevox-core-cli "
"not found. Install one of them, pass --core-cli, "
"or use --voice-placeholder."
)
print(f"note: voicevox_core not found; using CLI: {cli_path}", file=sys.stderr)
return (
CliCoreSynthesizer(
cli_path,
vvm,
onnxruntime=onnxruntime,
open_jtalk_dict=open_jtalk_dict,
),
"cli",
)
def generate_voice_samples(
dest_dir: Path,
style_ids: list[int],
texts: list[str],
*,
synthesizer: TtsBackend | None,
placeholder: bool,
dry_run: bool,
) -> None:
samples_dir = dest_dir / "voice_samples"
if not dry_run:
samples_dir.mkdir(parents=True, exist_ok=True)
for sid in style_ids:
for i, text in enumerate(texts, start=1):
out = samples_dir / f"{sid}_{i:03d}.wav"
print(f" sample {out.name} text={text!r}")
if dry_run:
continue
if placeholder or synthesizer is None:
make_silent_wav(out)
continue
try:
out.write_bytes(synthesizer.tts(text, sid))
except Exception as e:
print(
f"warning: tts failed style_id={sid}: {e}; silent wav",
file=sys.stderr,
)
make_silent_wav(out)
# ---------------------------------------------------------------------------
# character_info 生成
# ---------------------------------------------------------------------------
def ensure_character_info(
character_info_dir: Path,
records: list[dict[str, Any]],
*,
full: bool,
voice_samples: bool,
sample_texts: list[str],
synthesizer: TtsBackend | None,
voice_placeholder: bool,
font_path: Path | None,
overwrite: bool,
dry_run: bool,
) -> tuple[int, int]:
created = 0
skipped = 0
if full:
try:
from PIL import Image # noqa: F401
except ImportError as e:
raise RuntimeError(
"--full requires Pillow. Install with: pip install Pillow"
) from e
if not dry_run:
character_info_dir.mkdir(parents=True, exist_ok=True)
for rec in records:
uuid = rec["uuid"]
name = rec["name"]
style_ids = rec["style_ids"]
dest_dir = character_info_dir / uuid
metas_path = dest_dir / "metas.json"
label = f"{uuid}" + (f" ({name})" if name else "")
if metas_path.is_file() and not overwrite:
print(f"skip {label}")
skipped += 1
continue
tags = []
if full:
tags.append("full")
if voice_samples:
tags.append("voice")
suffix = f" [{','.join(tags)}]" if tags else ""
print(f"{'would create' if dry_run else 'create'} {label}{suffix}")
if dry_run:
if voice_samples:
generate_voice_samples(
dest_dir,
style_ids,
sample_texts,
synthesizer=None,
placeholder=True,
dry_run=True,
)
created += 1
continue
dest_dir.mkdir(parents=True, exist_ok=True)
metas_path.write_text(ENGINE_METAS_CONTENT, encoding="utf-8")
if full:
(dest_dir / "policy.md").write_text(
POLICY_TEMPLATE.format(name=name or uuid, uuid=uuid),
encoding="utf-8",
)
rgb = color_for(uuid, name)
ch = initial_char(name)
render_portrait(dest_dir / "portrait.png", name, uuid, rgb, font_path)
icons_dir = dest_dir / "icons"
for sid in style_ids:
render_icon(icons_dir / f"{sid}.png", ch, rgb, font_path)
if voice_samples:
generate_voice_samples(
dest_dir,
style_ids,
sample_texts,
synthesizer=synthesizer,
placeholder=voice_placeholder or synthesizer is None,
dry_run=False,
)
created += 1
return created, skipped
# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------
def main() -> int:
parser = argparse.ArgumentParser(
description=(
"Create character_info from VVM metas. "
"Default: empty metas.json. "
"--full: policy + dummy images. "
"--voice-samples: WAV via voicevox_core or voicevox-core-cli."
)
)
parser.add_argument("--vvm", type=Path, required=True, help="Path to .vvm")
parser.add_argument(
"--character-info-dir",
type=Path,
required=True,
help="Output character_info directory",
)
parser.add_argument(
"--full",
action="store_true",
help="policy.md + portrait.png + icons (requires Pillow)",
)
parser.add_argument(
"--font",
type=Path,
default=None,
help="TrueType/OpenType font for dummy images (CJK recommended)",
)
parser.add_argument(
"--voice-samples",
action="store_true",
help="Generate voice_samples (Python API, else voicevox-core-cli)",
)
parser.add_argument(
"--onnxruntime",
type=Path,
default=None,
help="ONNX Runtime shared library (-x for CLI / Python API)",
)
parser.add_argument(
"--open-jtalk-dict",
type=Path,
default=None,
help="OpenJTalk dictionary directory (-d for CLI / Python API)",
)
parser.add_argument(
"--core-cli",
type=Path,
default=None,
help="Path or name of voicevox-core-cli (fallback when no Python API)",
)
parser.add_argument(
"--sample-text",
action="append",
dest="sample_texts",
default=None,
metavar="TEXT",
help="Sample phrase (repeatable). Default: 3 built-in phrases",
)
parser.add_argument(
"--voice-placeholder",
action="store_true",
help="Write silent WAV without calling CORE",
)
parser.add_argument("--overwrite", action="store_true")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--list-only", action="store_true")
args = parser.parse_args()
vvm_path = args.vvm.resolve()
try:
speakers = load_speakers_from_vvm(vvm_path)
records = extract_speaker_records(speakers)
except (OSError, ValueError, json.JSONDecodeError, zipfile.BadZipFile) as e:
print(f"error: {e}", file=sys.stderr)
return 1
if args.list_only:
for rec in records:
styles = ",".join(str(s) for s in rec["style_ids"])
print(f"{rec['uuid']}\t{rec['name']}\tstyles={styles}")
return 0
texts = args.sample_texts if args.sample_texts else list(DEFAULT_SAMPLE_TEXTS)
font_path = args.font.resolve() if args.font else None
synthesizer: TtsBackend | None = None
backend_kind = "none"
if args.voice_samples and not args.voice_placeholder and not args.dry_run:
try:
synthesizer, backend_kind = create_tts_backend(
vvm_path,
onnxruntime=(
args.onnxruntime.resolve() if args.onnxruntime else None
),
open_jtalk_dict=(
args.open_jtalk_dict.resolve() if args.open_jtalk_dict else None
),
core_cli=args.core_cli,
)
print(f"note: TTS backend={backend_kind}", file=sys.stderr)
except Exception as e:
print(f"error: failed to init TTS backend: {e}", file=sys.stderr)
return 1
try:
created, skipped = ensure_character_info(
args.character_info_dir.resolve(),
records,
full=args.full,
voice_samples=args.voice_samples,
sample_texts=texts,
synthesizer=synthesizer,
voice_placeholder=args.voice_placeholder,
font_path=font_path,
overwrite=args.overwrite,
dry_run=args.dry_run,
)
except (RuntimeError, FileNotFoundError, OSError) as e:
print(f"error: {e}", file=sys.stderr)
return 1
finally:
if synthesizer is not None:
synthesizer.close()
print(
f"done: {len(records)} uuid(s), created={created}, skipped={skipped}, "
f"full={args.full}, voice_samples={args.voice_samples}, "
f"backend={backend_kind}, dir={args.character_info_dir}"
)
return 0
if __name__ == "__main__":
sys.exit(main())
#!/bin/bash
# VOICEVOX Engine wrapper script for Homebrew
# Automatically supplies default paths and options.
UV_PATH="@UV_BIN@/uv"
RUN_PATH="@LIBEXEC@/run/run"
DEFAULT_VOICELIB="@VOICEVOX_CORE_LIB@/voicevox"
DEFAULT_RUNTIME="@VOICEVOX_CORE_LIB@/voicevox"
DEFAULT_HOST="0.0.0.0"
DEFAULT_PORT="50021"
has_voicelib=0
has_runtime=0
has_host=0
has_port=0
for arg in "$@"; do
case "$arg" in
"--voicelib_dir") has_voicelib=1 ;;
"--runtime_dir") has_runtime=1 ;;
"--host") has_host=1 ;;
"--port") has_port=1 ;;
esac
done
args=("$@")
[[ $has_voicelib -eq 0 ]] && args+=("--voicelib_dir" "$DEFAULT_VOICELIB")
[[ $has_runtime -eq 0 ]] && args+=("--runtime_dir" "$DEFAULT_RUNTIME")
[[ $has_host -eq 0 ]] && args+=("--host" "$DEFAULT_HOST")
[[ $has_port -eq 0 ]] && args+=("--port" "$DEFAULT_PORT")
export VIRTUAL_ENV="@LIBEXEC@/.venv"
"$UV_PATH" "run" "--active" "@LIBEXEC@/run/generate_filemap.py" "--target_dir" "@SHARE@/voicevox/character_info"
exec "$UV_PATH" "run" "--active" "$RUN_PATH" "${args[@]}"
/*
CLI より VOICEVOX CORE の C API 経由でテキスト音声合成を行い、
音声ファイルを出力する。簡単なテスト目的のプログラムである。
Copyright (C) 2026 z80oolong (zool@zool.jpn.org)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include "voicevox_core.h"
#define HOMEBREW_PREFIX "/home/linuxbrew/.linuxbrew"
#ifndef VOICEVOX_CORE_PREFIX
#define VOICEVOX_CORE_PREFIX HOMEBREW_PREFIX "/opt/voicevox-core"
#endif
#ifndef DEFAULT_ONNXRUNTIME_PATH
#define DEFAULT_ONNXRUNTIME_PATH VOICEVOX_CORE_PREFIX "/lib/voicevox/libonnxruntime.so"
#endif
#ifndef DEFAULT_OPENJTALK_DIC_DIR
#define DEFAULT_OPENJTALK_DIC_DIR VOICEVOX_CORE_PREFIX "/share/voicevox/dic"
#endif
#ifndef DEFAULT_VVM_MODEL_PATH
#define DEFAULT_VVM_MODEL_PATH VOICEVOX_CORE_PREFIX "/share/voicevox/model/sample.vvm"
#endif
#define DEFAULT_SPEAKER_ID 0
#define DEFAULT_OUTPUT_FILE "./audio.wav"
static void print_usage(const char *progname) {
fprintf(stderr, "Usage: %s [-x libonnxruntime_so] [-d open_jtalk_dic_dir] [-v vvm_model] [-i speaker_id] [-o output_wav] TEXT\n", progname);
fprintf(stderr, " -x : ONNX Runtime .so ファイル (default: %s)\n", DEFAULT_ONNXRUNTIME_PATH);
fprintf(stderr, " -d : OpenJTalk辞書ディレクトリ (default: %s)\n", DEFAULT_OPENJTALK_DIC_DIR);
fprintf(stderr, " -v : VVMモデルファイル (.vvm) (default: %s)\n", DEFAULT_VVM_MODEL_PATH);
fprintf(stderr, " -i : 話者ID (default: %d)\n", DEFAULT_SPEAKER_ID);
fprintf(stderr, " -o : 出力WAVファイル (default: %s)\n", DEFAULT_OUTPUT_FILE);
fprintf(stderr, " TEXT: 合成するテキスト\n");
}
int main(int argc, char *argv[]) {
const char *onnx_path = DEFAULT_ONNXRUNTIME_PATH;
const char *dic_dir = DEFAULT_OPENJTALK_DIC_DIR;
const char *vvm_path = DEFAULT_VVM_MODEL_PATH;
VoicevoxStyleId speaker_id = DEFAULT_SPEAKER_ID;
const char *output_file = DEFAULT_OUTPUT_FILE;
char *text = NULL;
int opt;
while ((opt = getopt(argc, argv, "x:d:v:i:o:h")) != -1) {
switch (opt) {
case 'x': onnx_path = optarg; break;
case 'd': dic_dir = optarg; break;
case 'v': vvm_path = optarg; break;
case 'i': speaker_id = (VoicevoxStyleId)atoi(optarg); break;
case 'o': output_file = optarg; break;
case 'h':
print_usage(argv[0]);
return 0;
default:
print_usage(argv[0]);
return 1;
}
}
if (optind >= argc) {
fprintf(stderr, "Error: TEXT が指定されていません。\n");
print_usage(argv[0]);
return 1;
}
text = argv[optind];
VoicevoxResultCode result = VOICEVOX_RESULT_OK;
const VoicevoxOnnxruntime *onnxruntime = NULL;
OpenJtalkRc *open_jtalk = NULL;
VoicevoxSynthesizer *synthesizer = NULL;
VoicevoxVoiceModelFile *model = NULL;
uint8_t *wav = NULL;
size_t wav_size = 0;
/* ONNX Runtime ロード */
VoicevoxLoadOnnxruntimeOptions ort_opts = voicevox_make_default_load_onnxruntime_options();
ort_opts.filename = onnx_path;
result = voicevox_onnxruntime_load_once(ort_opts, &onnxruntime);
if (result != VOICEVOX_RESULT_OK) {
fprintf(stderr, "ONNX Runtime ロード失敗: %s\n", voicevox_error_result_to_message(result));
goto cleanup;
}
/* OpenJTalk */
result = voicevox_open_jtalk_rc_new(dic_dir, &open_jtalk);
if (result != VOICEVOX_RESULT_OK) {
fprintf(stderr, "OpenJTalk 初期化失敗: %s\n", voicevox_error_result_to_message(result));
goto cleanup;
}
/* Synthesizer */
VoicevoxInitializeOptions init_opts = voicevox_make_default_initialize_options();
result = voicevox_synthesizer_new(onnxruntime, open_jtalk, init_opts, &synthesizer);
if (result != VOICEVOX_RESULT_OK) {
fprintf(stderr, "Synthesizer 作成失敗: %s\n", voicevox_error_result_to_message(result));
goto cleanup;
}
/* VVMモデルロード */
result = voicevox_voice_model_file_open(vvm_path, &model);
if (result != VOICEVOX_RESULT_OK) {
fprintf(stderr, "VVMモデルロード失敗 (%s): %s\n", vvm_path, voicevox_error_result_to_message(result));
goto cleanup;
}
/* VVMモデル適用(新旧両対応) */
#ifdef ENABLE_OLD_API
/* 旧API(引数2つ) */
result = voicevox_synthesizer_load_voice_model(synthesizer, model);
#else
/* 新API(引数3つ) */
VoicevoxLoadVoiceModelOptions load_opts = voicevox_make_default_load_voice_model_options();
result = voicevox_synthesizer_load_voice_model(synthesizer, model, load_opts);
#endif
if (result != VOICEVOX_RESULT_OK) {
fprintf(stderr, "モデル適用失敗: %s\n", voicevox_error_result_to_message(result));
goto cleanup;
}
/* 音声合成 */
VoicevoxTtsOptions tts_opts = voicevox_make_default_tts_options();
result = voicevox_synthesizer_tts(synthesizer, text, speaker_id, tts_opts, &wav_size, &wav);
if (result != VOICEVOX_RESULT_OK) {
fprintf(stderr, "音声合成失敗: %s\n", voicevox_error_result_to_message(result));
goto cleanup;
}
/* WAVファイル出力 */
FILE *fp = fopen(output_file, "wb");
if (fp == NULL) {
fprintf(stderr, "出力ファイルを開けません: %s\n", output_file);
goto cleanup;
}
if (fwrite(wav, 1, wav_size, fp) != wav_size) {
fprintf(stderr, "ファイル書き込み中にエラーが発生しました: %s\n", output_file);
fclose(fp);
goto cleanup;
}
fclose(fp);
printf("音声合成完了: %s (%zu bytes)\n", output_file, wav_size);
cleanup:
if (wav) voicevox_wav_free(wav);
if (model) voicevox_voice_model_file_delete(model);
if (synthesizer) voicevox_synthesizer_delete(synthesizer);
if (open_jtalk) voicevox_open_jtalk_rc_delete(open_jtalk);
return (result == VOICEVOX_RESULT_OK) ? 0 : 1;
}
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CLI より VOICEVOX CORE の Python API 経由でテキスト音声合成を行い、
音声ファイルを出力する。簡単なテスト目的のプログラムである。
なお、本プログラムは C API 用のプログラムの移植版である。
Copyright (C) 2026 z80oolong (zool@zool.jpn.org)
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import argparse
import sys
from pathlib import Path
from voicevox_core.blocking import (
Onnxruntime,
OpenJtalk,
Synthesizer,
VoiceModelFile,
)
def main():
parser = argparse.ArgumentParser(
description="VOICEVOX CORE Python API でテキストを音声合成してWAV出力"
)
parser.add_argument("text", nargs="?", help="合成するテキスト")
parser.add_argument("-x", "--onnxruntime", default=None,
help="ONNX Runtime .so ファイルのパス")
parser.add_argument("-d", "--dict-dir", default=None,
help="OpenJTalk辞書ディレクトリ")
parser.add_argument("-v", "--vvm", default=None,
help="VVMモデルファイル (.vvm)")
parser.add_argument("-i", "--speaker-id", type=int, default=0,
help="話者ID (style_id)")
parser.add_argument("-o", "--output", default="audio.wav",
help="出力WAVファイルパス")
args = parser.parse_args()
if not args.text:
print("Error: TEXT が指定されていません。", file=sys.stderr)
parser.print_usage()
sys.exit(1)
HOMEBREW_PREFIX = Path("/home/linuxbrew/.linuxbrew")
VOICEVOX_CORE_PREFIX = HOMEBREW_PREFIX / "opt/voicevox-core"
onnx_path = args.onnxruntime or str(VOICEVOX_CORE_PREFIX / "lib/voicevox/libonnxruntime.so")
dict_dir = args.dict_dir or str(VOICEVOX_CORE_PREFIX / "share/voicevox/dic")
vvm_path = args.vvm or str(VOICEVOX_CORE_PREFIX / "share/voicevox/model/sample.vvm")
output_path = Path(args.output)
print(f"ONNX Runtime: {onnx_path}")
print(f"OpenJTalk辞書: {dict_dir}")
print(f"VVMモデル: {vvm_path}")
print(f"話者ID: {args.speaker_id}")
print(f"出力ファイル: {output_path}")
print(f"テキスト: {args.text}")
try:
ort = Onnxruntime.load_once(filename=onnx_path)
open_jtalk = OpenJtalk(dict_dir)
synthesizer = Synthesizer(ort, open_jtalk)
with VoiceModelFile.open(vvm_path) as model:
# 新API対応(on_existing を明示)
synthesizer.load_voice_model(model, on_existing="ERROR") # "RELOAD" や "SKIP" も可
wav_bytes = synthesizer.tts(
text=args.text,
style_id=args.speaker_id,
)
output_path.write_bytes(wav_bytes)
print(f"[OK] 音声合成完了: {output_path} ({len(wav_bytes):,} bytes)")
except Exception as e:
print(f"[ERR] エラー: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
prefix=@prefix@
exec_prefix=${prefix}
libdir=${prefix}/lib
includedir=${prefix}/include
Name: voicevox_core
Description: VOICEVOX CORE Library
Version: 0.16.4
URL: https://voicevox.hiroshiba.jp/
Libs: -L${libdir} -lvoicevox_core -ldl
Cflags: -I${includedir}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment