提交qiming-mcp-proxy
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# Voice CLI Server Configuration - 单节点语音转录服务
|
||||
|
||||
server:
|
||||
# Server binding address (0.0.0.0 for all interfaces)
|
||||
host: "0.0.0.0"
|
||||
# Server port
|
||||
port: 8080
|
||||
# Maximum file size for uploads (in bytes) - 200MB default
|
||||
max_file_size: 209715200
|
||||
# Enable CORS for web browser access
|
||||
cors_enabled: true
|
||||
|
||||
whisper:
|
||||
# Default model to use for transcription
|
||||
default_model: "base"
|
||||
# Directory to store whisper models
|
||||
models_dir: "./models"
|
||||
# Automatically download models when needed
|
||||
auto_download: true
|
||||
# List of supported whisper models
|
||||
supported_models:
|
||||
- "tiny"
|
||||
- "tiny.en"
|
||||
- "base"
|
||||
- "base.en"
|
||||
- "small"
|
||||
- "small.en"
|
||||
- "medium"
|
||||
- "medium.en"
|
||||
- "large-v1"
|
||||
- "large-v2"
|
||||
- "large-v3"
|
||||
|
||||
# Audio processing settings
|
||||
audio_processing:
|
||||
supported_formats: ["mp3", "wav", "flac", "m4a", "ogg", "aac", "opus", "amr", "wma", "aiff", "caf", "mp4", "mov", "avi", "mkv", "webm", "3gp", "flv", "wmv", "mpeg", "mxf"]
|
||||
auto_convert: true
|
||||
conversion_timeout: 60 # seconds
|
||||
temp_file_cleanup: true
|
||||
temp_file_retention: 300 # 5 minutes
|
||||
|
||||
# Worker-based concurrency settings
|
||||
workers:
|
||||
transcription_workers: 3 # Number of worker threads for transcription
|
||||
channel_buffer_size: 100 # Channel buffer size for task queue
|
||||
worker_timeout: 3600 # Worker processing timeout in seconds
|
||||
|
||||
logging:
|
||||
# Log level (trace, debug, info, warn, error)
|
||||
level: "info"
|
||||
# Directory for log files
|
||||
log_dir: "./logs"
|
||||
# Maximum size per log file
|
||||
max_file_size: "100MB"
|
||||
# Maximum number of log files to keep
|
||||
max_files: 30
|
||||
|
||||
daemon:
|
||||
# PID file for daemon mode
|
||||
pid_file: "./voice-cli-server.pid"
|
||||
# Log file for daemon output
|
||||
log_file: "./logs/server-daemon.log"
|
||||
# Working directory for daemon
|
||||
work_dir: "./"
|
||||
|
||||
# Async task management configuration
|
||||
task_management:
|
||||
|
||||
# Apalis worker configuration
|
||||
max_concurrent_tasks: 4 # Max concurrent async tasks (should match or be less than transcription_workers)
|
||||
retry_attempts: 2 # Number of retry attempts for failed tasks
|
||||
task_timeout_seconds: 3600 # Task processing timeout in seconds (0 to disable)
|
||||
catch_panic: true # Catch panics in execution and pipe them as errors
|
||||
task_retention_minutes: 1440 # task retention in minutes (1440 minutes = 24 hours)
|
||||
|
||||
# SQLite database configuration
|
||||
sqlite_db_path: "./data/tasks.db" # Path to SQLite database file
|
||||
429
qiming-mcp-proxy/voice-cli/templates/tts_service.py.template
Normal file
429
qiming-mcp-proxy/voice-cli/templates/tts_service.py.template
Normal file
@@ -0,0 +1,429 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
TTS服务模块 - 使用index-tts库进行语音合成
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import asyncio
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import indextts
|
||||
INDEX_TTS_AVAILABLE = True
|
||||
logger.info("IndexTTS library imported successfully")
|
||||
except ImportError as e:
|
||||
INDEX_TTS_AVAILABLE = False
|
||||
logger.warning(f"IndexTTS library not available: {e}")
|
||||
|
||||
try:
|
||||
import torch
|
||||
import torchaudio
|
||||
import numpy as np
|
||||
import soundfile as sf
|
||||
AUDIO_LIBS_AVAILABLE = True
|
||||
logger.info("Audio processing libraries imported successfully")
|
||||
except ImportError as e:
|
||||
AUDIO_LIBS_AVAILABLE = False
|
||||
logger.warning(f"Audio processing libraries not available: {e}")
|
||||
|
||||
class TTSService:
|
||||
"""TTS服务类 - 使用IndexTTS库进行语音合成"""
|
||||
|
||||
def __init__(self, model_path: Optional[str] = None):
|
||||
"""
|
||||
初始化TTS服务
|
||||
|
||||
Args:
|
||||
model_path: TTS模型路径,如果为None则使用默认模型
|
||||
"""
|
||||
self.model_path = model_path
|
||||
self.model = None
|
||||
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
|
||||
if not INDEX_TTS_AVAILABLE:
|
||||
logger.warning("IndexTTS not available, using mock implementation")
|
||||
|
||||
if not AUDIO_LIBS_AVAILABLE:
|
||||
logger.warning("Audio processing libraries not available, using mock implementation")
|
||||
|
||||
logger.info(f"TTS service initialized (device: {self.device})")
|
||||
|
||||
def _setup_environment(self):
|
||||
"""设置Python环境"""
|
||||
logger.info("TTS environment setup complete")
|
||||
|
||||
def load_model(self, model_name: str = "default"):
|
||||
"""
|
||||
加载TTS模型
|
||||
|
||||
Args:
|
||||
model_name: 模型名称
|
||||
"""
|
||||
try:
|
||||
if INDEX_TTS_AVAILABLE and AUDIO_LIBS_AVAILABLE:
|
||||
# 使用真实的IndexTTS库
|
||||
# IndexTTS 需要语音提示文件,我们使用一个默认的或从模型路径加载
|
||||
from indextts.infer import IndexTTS
|
||||
model_dir = self.model_path or "checkpoints"
|
||||
config_path = f"{model_dir}/config.yaml"
|
||||
self.model = IndexTTS(
|
||||
model_dir=model_dir,
|
||||
cfg_path=config_path
|
||||
)
|
||||
logger.info(f"IndexTTS model config loaded successfully: {model_name}")
|
||||
else:
|
||||
# Mock实现
|
||||
self.model = f"mock_model_{model_name}"
|
||||
logger.info(f"Mock IndexTTS model loaded: {model_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load TTS model: {e}")
|
||||
raise
|
||||
|
||||
def synthesize_sync(
|
||||
self,
|
||||
text: str,
|
||||
output_path: str,
|
||||
model: Optional[str] = None,
|
||||
speed: float = 1.0,
|
||||
pitch: int = 0,
|
||||
volume: float = 1.0,
|
||||
format: str = "mp3"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
同步语音合成
|
||||
|
||||
Args:
|
||||
text: 要合成的文本
|
||||
output_path: 输出文件路径
|
||||
model: 模型名称
|
||||
speed: 语速 (0.5-2.0)
|
||||
pitch: 音调 (-20到20)
|
||||
volume: 音量 (0.5-2.0)
|
||||
format: 输出格式
|
||||
|
||||
Returns:
|
||||
包含合成结果的字典
|
||||
"""
|
||||
try:
|
||||
# 确保模型已加载
|
||||
if self.model is None:
|
||||
self.load_model(model or "default")
|
||||
|
||||
# 验证参数
|
||||
if not text.strip():
|
||||
raise ValueError("Text cannot be empty")
|
||||
|
||||
if not (0.5 <= speed <= 2.0):
|
||||
raise ValueError("Speed must be between 0.5 and 2.0")
|
||||
|
||||
if not (-20 <= pitch <= 20):
|
||||
raise ValueError("Pitch must be between -20 and 20")
|
||||
|
||||
if not (0.5 <= volume <= 2.0):
|
||||
raise ValueError("Volume must be between 0.5 and 2.0")
|
||||
|
||||
# 确保输出目录存在
|
||||
output_dir = Path(output_path).parent
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
if INDEX_TTS_AVAILABLE and AUDIO_LIBS_AVAILABLE:
|
||||
# 使用真实的TTS库进行合成
|
||||
try:
|
||||
# 合成音频
|
||||
logger.info(f"Starting TTS synthesis for text: {text[:50]}...")
|
||||
|
||||
# 使用TTS进行合成
|
||||
self.model.infer(
|
||||
audio_prompt="reference_voice.wav",
|
||||
text=text,
|
||||
output_path=output_path
|
||||
)
|
||||
|
||||
logger.info(f"TTS synthesis completed successfully")
|
||||
logger.info(f"TTS synthesis completed in {time.time() - start_time:.2f}s")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TTS synthesis failed: {e}")
|
||||
# 回退到Mock实现
|
||||
return self._mock_synthesize(text, output_path, speed, pitch, volume, format)
|
||||
else:
|
||||
# 使用Mock实现
|
||||
return self._mock_synthesize(text, output_path, speed, pitch, volume, format)
|
||||
|
||||
# 检查输出文件是否存在
|
||||
if not Path(output_path).exists():
|
||||
raise FileNotFoundError(f"Output file not created: {output_path}")
|
||||
|
||||
file_size = Path(output_path).stat().st_size
|
||||
duration = len(text) * 0.1 # 估算时长
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"output_path": output_path,
|
||||
"file_size": file_size,
|
||||
"duration": duration,
|
||||
"text_length": len(text),
|
||||
"parameters": {
|
||||
"speed": speed,
|
||||
"pitch": pitch,
|
||||
"volume": volume,
|
||||
"format": format
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"TTS synthesis failed: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"output_path": None,
|
||||
"file_size": 0
|
||||
}
|
||||
|
||||
def _mock_synthesize(
|
||||
self,
|
||||
text: str,
|
||||
output_path: str,
|
||||
speed: float = 1.0,
|
||||
pitch: int = 0,
|
||||
volume: float = 1.0,
|
||||
format: str = "mp3"
|
||||
) -> Dict[str, Any]:
|
||||
"""Mock TTS合成实现 - 使用真实音频库生成音频"""
|
||||
try:
|
||||
import time
|
||||
start_time = time.time()
|
||||
|
||||
# 使用真实音频库生成音频
|
||||
if AUDIO_LIBS_AVAILABLE:
|
||||
try:
|
||||
# 生成真实音频数据
|
||||
sample_rate = 22050
|
||||
base_duration = max(1.0, len(text) * 0.05) # 基础时长 + 每字符0.05秒
|
||||
duration = base_duration / speed # 根据语速调整
|
||||
|
||||
# 根据文本生成不同频率的正弦波
|
||||
base_freq = 220.0 + pitch * 5 # 基础频率 + 音调调整
|
||||
text_hash = hash(text)
|
||||
freq_variation = (text_hash % 100) + 50
|
||||
frequency = base_freq + freq_variation
|
||||
|
||||
# 生成时间轴
|
||||
t = np.linspace(0, duration, int(sample_rate * duration), False)
|
||||
|
||||
# 生成正弦波
|
||||
sine_wave = np.sin(2 * np.pi * frequency * t)
|
||||
|
||||
# 添加包络使其更像语音
|
||||
envelope = np.exp(-t * 1.5)
|
||||
audio_data = sine_wave * envelope
|
||||
|
||||
# 添加少量噪声
|
||||
noise = np.random.normal(0, 0.005, audio_data.shape)
|
||||
audio_data = audio_data + noise
|
||||
|
||||
# 应用音量调整
|
||||
audio_data = audio_data * volume
|
||||
|
||||
# 归一化
|
||||
audio_data = audio_data / np.max(np.abs(audio_data)) * 0.8
|
||||
|
||||
# 转换为torch张量
|
||||
audio_tensor = torch.from_numpy(audio_data).float()
|
||||
if audio_tensor.dim() == 1:
|
||||
audio_tensor = audio_tensor.unsqueeze(0)
|
||||
|
||||
# 保存音频文件
|
||||
if format.lower() == "wav":
|
||||
torchaudio.save(output_path, audio_tensor, sample_rate)
|
||||
elif format.lower() == "mp3":
|
||||
# 先保存为WAV
|
||||
temp_wav = output_path.replace('.mp3', '.wav')
|
||||
torchaudio.save(temp_wav, audio_tensor, sample_rate)
|
||||
|
||||
# 尝试转换为MP3
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.run([
|
||||
'ffmpeg', '-y', '-i', temp_wav,
|
||||
'-codec:a', 'libmp3lame', '-qscale:a', '2',
|
||||
output_path
|
||||
], check=True, capture_output=True)
|
||||
Path(temp_wav).unlink(missing_ok=True)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
logger.warning("ffmpeg not available, using WAV format instead")
|
||||
Path(temp_wav).rename(output_path)
|
||||
else:
|
||||
torchaudio.save(output_path, audio_tensor, sample_rate)
|
||||
|
||||
actual_duration = duration
|
||||
logger.info(f"Real audio synthesis completed in {time.time() - start_time:.2f}s")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Real audio synthesis failed: {e}")
|
||||
# 回退到简单mock
|
||||
return self._simple_mock_synthesize(text, output_path, speed, pitch, volume, format)
|
||||
else:
|
||||
# 没有音频库,使用简单mock
|
||||
return self._simple_mock_synthesize(text, output_path, speed, pitch, volume, format)
|
||||
|
||||
# 验证文件
|
||||
if not Path(output_path).exists():
|
||||
raise FileNotFoundError(f"Output file not created: {output_path}")
|
||||
|
||||
file_size = Path(output_path).stat().st_size
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"output_path": output_path,
|
||||
"file_size": file_size,
|
||||
"duration": actual_duration,
|
||||
"text_length": len(text),
|
||||
"parameters": {
|
||||
"speed": speed,
|
||||
"pitch": pitch,
|
||||
"volume": volume,
|
||||
"format": format
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Mock TTS synthesis failed: {e}")
|
||||
raise Exception(f"Mock TTS synthesis failed: {e}")
|
||||
|
||||
def _simple_mock_synthesize(
|
||||
self,
|
||||
text: str,
|
||||
output_path: str,
|
||||
speed: float = 1.0,
|
||||
pitch: int = 0,
|
||||
volume: float = 1.0,
|
||||
format: str = "mp3"
|
||||
) -> Dict[str, Any]:
|
||||
"""简单Mock TTS合成实现"""
|
||||
try:
|
||||
# 创建模拟音频文件
|
||||
with open(output_path, 'wb') as f:
|
||||
# 根据文本长度生成模拟数据
|
||||
mock_data_size = max(1024, len(text) * 16) # 基础1KB + 每字符16字节
|
||||
f.write(b'\x00' * mock_data_size)
|
||||
|
||||
# 模拟处理时间
|
||||
import time
|
||||
time.sleep(0.1)
|
||||
|
||||
duration = max(1.0, len(text) * 0.05) # 基础1秒 + 每字符0.05秒
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"output_path": output_path,
|
||||
"file_size": Path(output_path).stat().st_size,
|
||||
"duration": duration,
|
||||
"text_length": len(text),
|
||||
"parameters": {
|
||||
"speed": speed,
|
||||
"pitch": pitch,
|
||||
"volume": volume,
|
||||
"format": format
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Simple mock TTS synthesis failed: {e}")
|
||||
|
||||
async def synthesize_async(
|
||||
self,
|
||||
text: str,
|
||||
output_path: str,
|
||||
model: Optional[str] = None,
|
||||
speed: float = 1.0,
|
||||
pitch: int = 0,
|
||||
volume: float = 1.0,
|
||||
format: str = "mp3"
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
异步语音合成
|
||||
|
||||
Args:
|
||||
text: 要合成的文本
|
||||
output_path: 输出文件路径
|
||||
model: 模型名称
|
||||
speed: 语速
|
||||
pitch: 音调
|
||||
volume: 音量
|
||||
format: 输出格式
|
||||
|
||||
Returns:
|
||||
包含合成结果的字典
|
||||
"""
|
||||
# 在线程池中执行同步合成
|
||||
loop = asyncio.get_event_loop()
|
||||
result = await loop.run_in_executor(
|
||||
None,
|
||||
self.synthesize_sync,
|
||||
text, output_path, model, speed, pitch, volume, format
|
||||
)
|
||||
return result
|
||||
|
||||
def main():
|
||||
"""命令行接口"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="TTS Service CLI")
|
||||
parser.add_argument("text", help="Text to synthesize")
|
||||
parser.add_argument("--output", "-o", help="Output file path")
|
||||
parser.add_argument("--model", "-m", help="Model name")
|
||||
parser.add_argument("--speed", "-s", type=float, default=1.0, help="Speech speed (0.5-2.0)")
|
||||
parser.add_argument("--pitch", "-p", type=int, default=0, help="Pitch (-20 to 20)")
|
||||
parser.add_argument("--volume", "-v", type=float, default=1.0, help="Volume (0.5-2.0)")
|
||||
parser.add_argument("--format", "-f", default="mp3", help="Output format")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 如果没有指定输出路径,使用临时文件
|
||||
if not args.output:
|
||||
with tempfile.NamedTemporaryFile(suffix=f".{args.format}", delete=False) as f:
|
||||
args.output = f.name
|
||||
|
||||
try:
|
||||
# 初始化TTS服务
|
||||
tts_service = TTSService()
|
||||
|
||||
# 执行合成
|
||||
result = tts_service.synthesize_sync(
|
||||
text=args.text,
|
||||
output_path=args.output,
|
||||
model=args.model,
|
||||
speed=args.speed,
|
||||
pitch=args.pitch,
|
||||
volume=args.volume,
|
||||
format=args.format
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
print(f"Synthesis completed successfully!")
|
||||
print(f"Output file: {result['output_path']}")
|
||||
print(f"File size: {result['file_size']} bytes")
|
||||
print(f"Duration: {result['duration']} seconds")
|
||||
else:
|
||||
print(f"Synthesis failed: {result['error']}")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user