- Add bin/api/ package with generate_image() and synthesize_tts() - Refactor bin/commands/image.py to call bin.api.image.generate_image - Refactor bin/commands/voice.py to call bin.api.voice.synthesize_tts - API layer is free of Click CLI dependencies, usable by external scripts
96 lines
2.6 KiB
Python
96 lines
2.6 KiB
Python
"""Voice / TTS utilities via Volcengine (ByteDance) WebSocket API."""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import click
|
|
|
|
from bin.api.voice import synthesize_tts
|
|
from bin.config import config
|
|
from bin.utils import handle_errors
|
|
|
|
_VOICE_CHOICES = [
|
|
"zh_female_cancan_mars_bigtts",
|
|
"zh_female_shuangkuaisisi_moon_bigtts",
|
|
"zh_male_wennuanahu_moon_bigtts",
|
|
"zh_male_sunwukong_moon_bigtts",
|
|
]
|
|
|
|
_FORMAT_CHOICES = ["mp3", "wav", "pcm"]
|
|
|
|
|
|
def _get_credentials():
|
|
"""Get Volcengine credentials from config."""
|
|
appid = config.get("volc_appid")
|
|
access_token = config.get("volc_access_token")
|
|
if not appid or not access_token:
|
|
return None, None
|
|
return appid, access_token
|
|
|
|
|
|
def _synthesize_sync(
|
|
text: str,
|
|
speaker: str,
|
|
fmt: str,
|
|
speech_rate: int,
|
|
output_path: str,
|
|
) -> str:
|
|
"""Synchronous wrapper around async WebSocket TTS."""
|
|
appid, access_token = _get_credentials()
|
|
if not appid or not access_token:
|
|
raise click.UsageError(
|
|
"Volcengine credentials not set. Run:\n"
|
|
" mytoolkit env set volc_appid <appid>\n"
|
|
" mytoolkit env set volc_access_token <token>"
|
|
)
|
|
|
|
return synthesize_tts(
|
|
appid=appid,
|
|
token=access_token,
|
|
text=text,
|
|
speaker=speaker,
|
|
fmt=fmt,
|
|
speech_rate=speech_rate,
|
|
output_path=output_path,
|
|
)
|
|
|
|
|
|
@click.group()
|
|
def voice():
|
|
"""Voice synthesis (TTS) commands."""
|
|
pass
|
|
|
|
|
|
@voice.command("tts")
|
|
@click.argument("text", required=False)
|
|
@click.option("-f", "--file", type=click.Path(exists=True), help="从文件读取文本")
|
|
@click.option(
|
|
"-v",
|
|
"--voice",
|
|
"speaker",
|
|
type=click.Choice(_VOICE_CHOICES),
|
|
default="zh_male_wennuanahu_moon_bigtts",
|
|
help="音色",
|
|
)
|
|
@click.option("--format", "fmt", type=click.Choice(_FORMAT_CHOICES), default="mp3", help="输出格式")
|
|
@click.option(
|
|
"--speed",
|
|
type=click.IntRange(-50, 100),
|
|
default=0,
|
|
help="语速 (-50=0.5倍, 0=正常, 100=2倍)",
|
|
)
|
|
@click.option("-o", "--output", help="输出文件路径")
|
|
@handle_errors
|
|
def tts_cmd(text, file, speaker, fmt, speed, output):
|
|
"""使用豆包/火山引擎语音 API 将文本转为语音。"""
|
|
if file:
|
|
text = Path(file).read_text(encoding="utf-8").strip()
|
|
elif not text:
|
|
raise click.UsageError("必须提供 text 或使用 -f/--file 从文件读取")
|
|
|
|
click.echo(f"Synthesizing: {text[:40]}{'...' if len(text) > 40 else ''}")
|
|
|
|
save_path = output or f"output.{fmt}"
|
|
result = _synthesize_sync(text, speaker, fmt, speed, save_path)
|
|
click.echo(os.path.abspath(result))
|