Add presentation slides for OU process and Mehler formula
Sync to site1 / sync (push) Has been cancelled
Sync to site1 / sync (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env python3
|
||||
"""用火山引擎 v3 单向流式 TTS(/api/v3/tts/unidirectional)把双人对话脚本合成为一个 MP3。
|
||||
|
||||
鉴权:请求头 x-api-key + X-Api-Resource-Id(新版方式,区别于旧版 Bearer;{token})。
|
||||
用法:
|
||||
VOLC_API_KEY=<你的key> python3 synth_volc_v3.py \
|
||||
--script-file hermite_polynomials-script.json \
|
||||
--output-file output/hermite_polynomials-podcast.mp3
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
|
||||
import requests
|
||||
|
||||
ENDPOINT = "https://openspeech.bytedance.com/api/v3/tts/unidirectional"
|
||||
RESOURCE_ID = "volc.service_type.10029"
|
||||
|
||||
# 男女两个大模型情感音色(v2 mars bigtts)
|
||||
VOICE_MALE = "zh_male_beijingxiaoye_emo_v2_mars_bigtts"
|
||||
VOICE_FEMALE = "zh_female_roumeinvyou_emo_v2_mars_bigtts"
|
||||
|
||||
ADDITIONS = json.dumps(
|
||||
{
|
||||
"disable_markdown_filter": True,
|
||||
"enable_language_detector": True,
|
||||
"enable_latex_tn": True,
|
||||
"disable_default_bit_rate": True,
|
||||
"max_length_to_filter_parenthesis": 0,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def synth_line(api_key: str, text: str, speaker: str, retries: int = 3) -> bytes:
|
||||
"""合成一句,返回 mp3 字节。失败抛异常。"""
|
||||
headers = {
|
||||
"x-api-key": api_key,
|
||||
"X-Api-Resource-Id": RESOURCE_ID,
|
||||
"Connection": "keep-alive",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"req_params": {
|
||||
"text": text,
|
||||
"speaker": speaker,
|
||||
"additions": ADDITIONS,
|
||||
"audio_params": {"format": "mp3", "sample_rate": 24000},
|
||||
}
|
||||
}
|
||||
last_err = None
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
r = requests.post(ENDPOINT, headers=headers, json=payload, timeout=60)
|
||||
if r.status_code != 200:
|
||||
last_err = f"HTTP {r.status_code}: {r.text[:200]}"
|
||||
time.sleep(1.0 + attempt)
|
||||
continue
|
||||
# 响应体可能是单个 JSON,也可能是多段(流式)。逐段解析 data 累积。
|
||||
audio = b""
|
||||
text_body = r.text.strip()
|
||||
# 尝试按行分割的多 JSON(流式 unidirectional 常见)
|
||||
chunks = []
|
||||
for line in text_body.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
chunks.append(line)
|
||||
if not chunks:
|
||||
chunks = [text_body]
|
||||
ok = False
|
||||
for c in chunks:
|
||||
try:
|
||||
obj = json.loads(c)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if obj.get("code") not in (0, None):
|
||||
last_err = f"code={obj.get('code')} msg={obj.get('message')}"
|
||||
continue
|
||||
d = obj.get("data")
|
||||
if d:
|
||||
audio += base64.b64decode(d)
|
||||
ok = True
|
||||
if ok and audio:
|
||||
return audio
|
||||
last_err = last_err or "no audio data in response"
|
||||
time.sleep(1.0 + attempt)
|
||||
except Exception as e: # noqa
|
||||
last_err = str(e)
|
||||
time.sleep(1.0 + attempt)
|
||||
raise RuntimeError(f"合成失败: {last_err}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--script-file", required=True)
|
||||
ap.add_argument("--output-file", required=True)
|
||||
args = ap.parse_args()
|
||||
|
||||
api_key = os.getenv("VOLC_API_KEY")
|
||||
if not api_key:
|
||||
print("ERROR: 需要环境变量 VOLC_API_KEY", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
with open(args.script_file, encoding="utf-8") as f:
|
||||
script = json.load(f)
|
||||
lines = script["lines"]
|
||||
total = len(lines)
|
||||
print(f"加载脚本:{script.get('title','')},共 {total} 句")
|
||||
|
||||
os.makedirs(os.path.dirname(args.output_file) or ".", exist_ok=True)
|
||||
|
||||
all_audio = b""
|
||||
ok_count = 0
|
||||
for i, line in enumerate(lines, 1):
|
||||
speaker = VOICE_MALE if line["speaker"] == "male" else VOICE_FEMALE
|
||||
text = line["paragraph"]
|
||||
try:
|
||||
audio = synth_line(api_key, text, speaker)
|
||||
all_audio += audio
|
||||
ok_count += 1
|
||||
print(f"[{i}/{total}] {line['speaker']:6s} ok ({len(audio)} bytes)")
|
||||
except Exception as e: # noqa
|
||||
print(f"[{i}/{total}] {line['speaker']:6s} FAIL: {e}")
|
||||
time.sleep(0.15) # 轻微限速
|
||||
|
||||
if not all_audio:
|
||||
print("ERROR: 没有任何一句合成成功", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
with open(args.output_file, "wb") as f:
|
||||
f.write(all_audio)
|
||||
print(f"\n完成:{ok_count}/{total} 句成功,已写出 {args.output_file}({len(all_audio)} bytes)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user