a
This commit is contained in:
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
批量转换脚本 - 转换整个文件夹
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from tts_convert import convert_file, VOICES
|
||||
|
||||
|
||||
async def batch_convert(input_dir: str, output_dir: str, lang: str = 'auto'):
|
||||
"""批量转换文件夹"""
|
||||
input_path = Path(input_dir)
|
||||
output_path = Path(output_dir)
|
||||
|
||||
if not input_path.exists():
|
||||
print(f"❌ 输入目录不存在: {input_dir}")
|
||||
return
|
||||
|
||||
# 创建输出目录
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 查找所有文本文件
|
||||
text_files = list(input_path.glob('*.txt')) + list(input_path.glob('*.md'))
|
||||
|
||||
if not text_files:
|
||||
print(f"⚠️ 未找到文本文件: {input_dir}")
|
||||
return
|
||||
|
||||
print(f"📝 找到 {len(text_files)} 个文件")
|
||||
print('=' * 60)
|
||||
|
||||
# 逐个转换
|
||||
success = 0
|
||||
failed = 0
|
||||
|
||||
for i, file in enumerate(text_files, 1):
|
||||
output_file = output_path / file.with_suffix('.mp3').name
|
||||
|
||||
print(f"\n[{i}/{len(text_files)}] {file.name}")
|
||||
|
||||
try:
|
||||
await convert_file(str(file), lang, str(output_file))
|
||||
success += 1
|
||||
except Exception as e:
|
||||
print(f"❌ 失败: {e}")
|
||||
failed += 1
|
||||
|
||||
print('\n' + '=' * 60)
|
||||
print(f"✅ 完成: {success} 成功, {failed} 失败")
|
||||
print(f"📁 输出目录: {output_path.absolute()}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='批量 TTS 转换')
|
||||
parser.add_argument('input_dir', help='输入文件夹')
|
||||
parser.add_argument('--output', '-o', default='output', help='输出文件夹')
|
||||
parser.add_argument('--lang', '-l', default='auto',
|
||||
choices=['auto', 'zh', 'zh-hk', 'en', 'en-gb', 'jp', 'kr'],
|
||||
help='语言代码')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(batch_convert(args.input_dir, args.output, args.lang))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
流式 TTS 服务器 - 实时音频流
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import argparse
|
||||
import edge_tts
|
||||
from pathlib import Path
|
||||
from flask import Flask, Response, send_file
|
||||
import threading
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# 全局配置
|
||||
VOICES = {
|
||||
'zh': 'zh-CN-XiaoxiaoNeural',
|
||||
'zh-hk': 'zh-HK-HiuMaanNeural',
|
||||
'en': 'en-US-AriaNeural',
|
||||
'en-gb': 'en-GB-SoniaNeural',
|
||||
'jp': 'ja-JP-NanamiNeural',
|
||||
'kr': 'ko-KR-SunHiNeural',
|
||||
}
|
||||
|
||||
DEFAULT_TEXT = """
|
||||
Welcome to TTS Streamer. This is a test message.
|
||||
欢迎使用文本转音频流服务。这是测试消息。
|
||||
"""
|
||||
|
||||
|
||||
def detect_language(text: str) -> str:
|
||||
"""检测语言"""
|
||||
if any('\u3040' <= c <= '\u309f' or '\u30a0' <= c <= '\u30ff' for c in text[:100]):
|
||||
return 'jp'
|
||||
if any('\uac00' <= c <= '\ud7af' for c in text[:100]):
|
||||
return 'kr'
|
||||
if any('\u4e00' <= c <= '\u9fff' for c in text[:100]):
|
||||
return 'zh'
|
||||
return 'en'
|
||||
|
||||
|
||||
async def generate_audio_stream(text: str, voice_code: str = 'auto'):
|
||||
"""生成音频流"""
|
||||
if voice_code == 'auto':
|
||||
voice_code = detect_language(text)
|
||||
|
||||
voice = VOICES.get(voice_code, VOICES['zh'])
|
||||
communicate = edge_tts.Communicate(text, voice)
|
||||
|
||||
async for chunk in communicate.stream():
|
||||
if chunk["type"] == "audio":
|
||||
yield chunk["data"]
|
||||
|
||||
|
||||
def run_async_stream(text: str, voice_code: str):
|
||||
"""在同步上下文中运行异步流"""
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
async def stream_generator():
|
||||
async for chunk in generate_audio_stream(text, voice_code):
|
||||
yield chunk
|
||||
|
||||
# 转换为同步生成器
|
||||
gen = stream_generator()
|
||||
while True:
|
||||
try:
|
||||
chunk = loop.run_until_complete(gen.__anext__())
|
||||
yield chunk
|
||||
except StopAsyncIteration:
|
||||
break
|
||||
|
||||
|
||||
@app.route('/stream')
|
||||
def stream_audio():
|
||||
"""音频流端点"""
|
||||
text = app.config.get('TEXT', DEFAULT_TEXT)
|
||||
voice = app.config.get('VOICE', 'auto')
|
||||
|
||||
return Response(
|
||||
run_async_stream(text, voice),
|
||||
mimetype='audio/mpeg',
|
||||
headers={
|
||||
'Content-Type': 'audio/mpeg',
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""首页"""
|
||||
text = app.config.get('TEXT', DEFAULT_TEXT)[:200]
|
||||
return f"""
|
||||
<h1>🔊 TTS Streamer</h1>
|
||||
<p>当前文本: {text}...</p>
|
||||
<p>点击播放: <a href="/stream">/stream</a></p>
|
||||
<hr>
|
||||
<p>或使用播放器直接打开: http://服务器IP:端口/stream</p>
|
||||
"""
|
||||
|
||||
|
||||
def serve_file(input_file: str, port: int = 8080, voice: str = 'auto'):
|
||||
"""提供文件流服务"""
|
||||
# 读取文件
|
||||
path = Path(input_file)
|
||||
if not path.exists():
|
||||
print(f"❌ 文件不存在: {input_file}")
|
||||
return
|
||||
|
||||
with open(input_file, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
# 配置
|
||||
app.config['TEXT'] = text
|
||||
app.config['VOICE'] = voice
|
||||
|
||||
print(f"🎙️ 准备播放: {input_file}")
|
||||
print(f"🌐 访问地址:")
|
||||
print(f" http://0.0.0.0:{port}/stream")
|
||||
print(f" http://0.0.0.0:{port}/ (网页)")
|
||||
print(f"\n📝 语言: {detect_language(text)}")
|
||||
print("按 Ctrl+C 停止服务")
|
||||
|
||||
# 启动服务
|
||||
app.run(host='0.0.0.0', port=port, threaded=True)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='TTS 流服务器')
|
||||
parser.add_argument('input', help='输入文本文件')
|
||||
parser.add_argument('--port', '-p', type=int, default=8080, help='端口 (默认: 8080)')
|
||||
parser.add_argument('--voice', '-v', default='auto', help='语音代码')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
serve_file(args.input, args.port, args.voice)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Executable
+106
@@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TTS 转换脚本 - 支持多语言
|
||||
支持:中文(zh)、英文(en)、日文(jp)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import edge_tts
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 语音配置
|
||||
VOICES = {
|
||||
'zh': 'zh-CN-XiaoxiaoNeural', # 中文-晓晓
|
||||
'zh-hk': 'zh-HK-HiuMaanNeural', # 粤语
|
||||
'en': 'en-US-AriaNeural', # 英语-美音
|
||||
'en-gb': 'en-GB-SoniaNeural', # 英语-英音
|
||||
'jp': 'ja-JP-NanamiNeural', # 日语
|
||||
'kr': 'ko-KR-SunHiNeural', # 韩语
|
||||
}
|
||||
|
||||
# 默认语速
|
||||
DEFAULT_RATE = "+0%"
|
||||
|
||||
|
||||
async def convert_text_to_speech(text: str, voice: str, output_file: str, rate: str = DEFAULT_RATE):
|
||||
"""将文本转换为语音"""
|
||||
communicate = edge_tts.Communicate(text, voice, rate=rate)
|
||||
await communicate.save(output_file)
|
||||
print(f"✅ 已保存: {output_file}")
|
||||
|
||||
|
||||
async def convert_file(input_file: str, lang: str = 'auto', output_file: str = None, rate: str = DEFAULT_RATE):
|
||||
"""转换文件"""
|
||||
input_path = Path(input_file)
|
||||
|
||||
if not input_path.exists():
|
||||
print(f"❌ 文件不存在: {input_file}")
|
||||
sys.exit(1)
|
||||
|
||||
# 读取文本
|
||||
with open(input_file, 'r', encoding='utf-8') as f:
|
||||
text = f.read()
|
||||
|
||||
# 自动检测语言(简单判断)
|
||||
if lang == 'auto':
|
||||
lang = detect_language(text)
|
||||
print(f"🔍 检测到语言: {lang}")
|
||||
|
||||
# 获取语音
|
||||
voice = VOICES.get(lang, VOICES['zh'])
|
||||
|
||||
# 确定输出文件名
|
||||
if output_file is None:
|
||||
output_file = str(input_path.with_suffix('.mp3'))
|
||||
|
||||
# 转换
|
||||
print(f"🎙️ 使用语音: {voice}")
|
||||
print(f"📝 转换: {input_file} -> {output_file}")
|
||||
await convert_text_to_speech(text, voice, output_file, rate)
|
||||
|
||||
return output_file
|
||||
|
||||
|
||||
def detect_language(text: str) -> str:
|
||||
"""简单语言检测"""
|
||||
# 日文假名
|
||||
if any('\u3040' <= c <= '\u309f' or '\u30a0' <= c <= '\u30ff' for c in text[:100]):
|
||||
return 'jp'
|
||||
# 韩文
|
||||
if any('\uac00' <= c <= '\ud7af' for c in text[:100]):
|
||||
return 'kr'
|
||||
# 中文
|
||||
if any('\u4e00' <= c <= '\u9fff' for c in text[:100]):
|
||||
return 'zh'
|
||||
# 默认英文
|
||||
return 'en'
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='文本转语音工具')
|
||||
parser.add_argument('input', help='输入文本文件')
|
||||
parser.add_argument('--lang', '-l', default='auto',
|
||||
choices=['auto', 'zh', 'zh-hk', 'en', 'en-gb', 'jp', 'kr'],
|
||||
help='语言代码 (默认: auto)')
|
||||
parser.add_argument('--output', '-o', help='输出音频文件')
|
||||
parser.add_argument('--rate', '-r', default=DEFAULT_RATE,
|
||||
help='语速调整,如 "+10%" 或 "-10%"')
|
||||
parser.add_argument('--list-voices', action='store_true',
|
||||
help='列出所有可用语音')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.list_voices:
|
||||
print("可用语音:")
|
||||
for code, voice in VOICES.items():
|
||||
print(f" {code}: {voice}")
|
||||
return
|
||||
|
||||
# 运行转换
|
||||
asyncio.run(convert_file(args.input, args.lang, args.output, args.rate))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user