Initial commit: 军事科技AI配音系统 v1.0
- Flask backend with MiniMax and CosyVoice TTS engines - Frontend UI with emotion tags, speed/pitch/volume controls - Nginx reverse proxy configuration - Dual engine support (MiniMax speech-2.8-hd, CosyVoice v3.5-flash)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# 应用包初始化
|
||||
@@ -0,0 +1,14 @@
|
||||
from flask import Flask, jsonify
|
||||
from flask_cors import CORS
|
||||
|
||||
def create_app():
|
||||
app = Flask(__name__)
|
||||
CORS(app, resources={r"/api/*": {"origins": "*"}})
|
||||
from app.routes.tts_synthesize import bp as synthesize_bp
|
||||
app.register_blueprint(synthesize_bp)
|
||||
@app.route('/api/health')
|
||||
def health(): return jsonify({'status': 'ok', 'service': '军事科技AI配音系统'})
|
||||
return app
|
||||
|
||||
if __name__ == '__main__':
|
||||
create_app().run(host='0.0.0.0', port=5000, debug=True)
|
||||
@@ -0,0 +1 @@
|
||||
# 路由包初始化
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
健康检查路由
|
||||
"""
|
||||
|
||||
from flask import Blueprint, jsonify
|
||||
|
||||
health_bp = Blueprint('health', __name__)
|
||||
|
||||
@health_bp.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
"""健康检查接口"""
|
||||
return jsonify({
|
||||
'status': 'healthy',
|
||||
'message': '服务运行正常'
|
||||
})
|
||||
@@ -0,0 +1,112 @@
|
||||
from flask import Blueprint, jsonify, request, send_file
|
||||
import requests, os, uuid
|
||||
import logging
|
||||
import dashscope
|
||||
from dashscope.audio.tts_v2 import SpeechSynthesizer
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 从环境变量读取配置(如果不存在则使用默认值)
|
||||
MINIMAX_API_KEY = os.environ.get("MINIMAX_API_KEY", "sk-api-x3IxJC9nlORkopOJ4NOWff0Er4rtDjVrM08O5HOCYnsmBaZ2k1n2HIBLZGAwIDOSTh6bsmC2Nmz_xfyNnEWfhjLTrQPRLLN64Hu89ed0vG6gzwFfE24gfQM")
|
||||
MINIMAX_VOICE_ID = os.environ.get("MINIMAX_VOICE_ID", "lanhao_military_tech")
|
||||
COSYVOICE_API_KEY = os.environ.get("COSYVOICE_API_KEY", "sk-3c386742861b4d2da421030fa6254ef6")
|
||||
COSYVOICE_VOICE_ID = os.environ.get("COSYVOICE_VOICE_ID", "cosyvoice-v3.5-flash-bailian-e859a6e87823419f9c6b3eefb1a97dc3")
|
||||
|
||||
# 配置DashScope WebSocket
|
||||
dashscope.api_key = COSYVOICE_API_KEY
|
||||
dashscope.base_websocket_api_url = 'wss://dashscope.aliyuncs.com/api-ws/v1/inference'
|
||||
|
||||
logger.info(f"Loaded config - MINIMAX_VOICE_ID: {MINIMAX_VOICE_ID}, COSYVOICE_VOICE_ID: {COSYVOICE_VOICE_ID}")
|
||||
|
||||
tts_bp = Blueprint("tts", __name__)
|
||||
|
||||
@tts_bp.route('/synthesize', methods=['POST'])
|
||||
def synthesize():
|
||||
d = request.get_json()
|
||||
t = d.get('text', '')
|
||||
m = d.get('model', 'minimax')
|
||||
if m == 'minimax': return synthesize_minimax(t)
|
||||
elif m == 'cosyvoice': return synthesize_cosyvoice(t)
|
||||
return jsonify({'error': '不支持的模型'}), 400
|
||||
|
||||
def synthesize_minimax(text):
|
||||
if not text:
|
||||
return jsonify({"success": False, "error": "文本不能为空"}), 400
|
||||
|
||||
url = "https://api.minimaxi.com/v1/t2a_v2"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {MINIMAX_API_KEY}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
payload = {
|
||||
"model": "speech-2.8-hd",
|
||||
"text": text,
|
||||
"voice_setting": {"voice_id": MINIMAX_VOICE_ID},
|
||||
"audio_setting": {
|
||||
"sample_rate": 32000,
|
||||
"format": "mp3",
|
||||
"bitrate": 128000,
|
||||
"speed": 1.0,
|
||||
"volume": 1.0,
|
||||
"pitch": 0
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"Calling MiniMax API with voice_id: {MINIMAX_VOICE_ID}")
|
||||
|
||||
try:
|
||||
resp = requests.post(url, headers=headers, json=payload, timeout=60)
|
||||
result = resp.json()
|
||||
logger.info(f"MiniMax response: {result}")
|
||||
|
||||
if result.get("base_resp", {}).get("status_code") == 0:
|
||||
audio_hex = result.get("data", {}).get("audio", "")
|
||||
if audio_hex:
|
||||
filename = f"minimax_{uuid.uuid4().hex[:8]}.mp3"
|
||||
filepath = f"/workspace/military_tech_voice/audio_uploads/{filename}"
|
||||
with open(filepath, "wb") as f: f.write(bytes.fromhex(audio_hex))
|
||||
logger.info(f"Audio saved to: {filepath}")
|
||||
return jsonify({"success": True, "audio_url": f"/api/tts/audio/{filename}"})
|
||||
|
||||
error_msg = result.get("base_resp", {}).get("status_msg", str(result))
|
||||
return jsonify({"success": False, "error": error_msg})
|
||||
except Exception as e:
|
||||
logger.error(f"MiniMax API error: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
def synthesize_cosyvoice(text):
|
||||
if not text:
|
||||
return jsonify({"success": False, "error": "文本不能为空"}), 400
|
||||
|
||||
logger.info(f"Calling CosyVoice WebSocket API with voice_id: {COSYVOICE_VOICE_ID}")
|
||||
|
||||
try:
|
||||
# 使用DashScope SDK的WebSocket方式
|
||||
synthesizer = SpeechSynthesizer(
|
||||
model="cosyvoice-v3-flash",
|
||||
voice=COSYVOICE_VOICE_ID
|
||||
)
|
||||
|
||||
# 调用WebSocket API进行语音合成
|
||||
audio = synthesizer.call(text)
|
||||
|
||||
if audio:
|
||||
filename = f"cosyvoice_{uuid.uuid4().hex[:8]}.mp3"
|
||||
filepath = f"/workspace/military_tech_voice/audio_uploads/{filename}"
|
||||
with open(filepath, "wb") as f:
|
||||
f.write(audio)
|
||||
logger.info(f"CosyVoice audio saved to: {filepath}")
|
||||
return jsonify({"success": True, "audio_url": f"/api/tts/audio/{filename}"})
|
||||
|
||||
return jsonify({"success": False, "error": "未生成音频"})
|
||||
except Exception as e:
|
||||
logger.error(f"CosyVoice WebSocket API error: {e}")
|
||||
return jsonify({"success": False, "error": str(e)}), 500
|
||||
|
||||
@tts_bp.route('/audio/<filename>')
|
||||
def get_audio(filename):
|
||||
filepath = f"/workspace/military_tech_voice/audio_uploads/{filename}"
|
||||
if os.path.exists(filepath): return send_file(filepath, mimetype="audio/mpeg")
|
||||
return jsonify({"error": "文件不存在"}), 404
|
||||
@@ -0,0 +1,55 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
import dashscope
|
||||
from dashscope.audio.tts_v2 import SpeechSynthesizer
|
||||
import os
|
||||
|
||||
cosyvoice_bp = Blueprint('cosyvoice', __name__)
|
||||
|
||||
# CosyVoice配置
|
||||
COSYVOICE_API_KEY = os.environ.get('COSYVOICE_API_KEY', 'sk-3c386742861b4d2da421030fa6254ef6')
|
||||
COSYVOICE_VOICE_ID = os.environ.get('COSYVOICE_VOICE_ID', 'cosyvoice-v3.5-flash-bailian-e859a6e87823419f9c6b3eefb1a97dc3')
|
||||
COSYVOICE_MODEL = 'cosyvoice-v3.5-flash' # 注意:克隆音色必须用这个模型
|
||||
|
||||
def synthesize_cosyvoice(text):
|
||||
"""使用CosyVoice WebSocket SDK合成语音"""
|
||||
try:
|
||||
# 设置API Key
|
||||
dashscope.api_key = COSYVOICE_API_KEY
|
||||
|
||||
# 创建合成器
|
||||
synthesizer = SpeechSynthesizer(
|
||||
model=COSYVOICE_MODEL,
|
||||
voice=COSYVOICE_VOICE_ID
|
||||
)
|
||||
|
||||
# 合成音频
|
||||
audio = synthesizer.call(text)
|
||||
|
||||
if audio:
|
||||
return {
|
||||
"success": True,
|
||||
"audio_data": audio.hex() if isinstance(audio, bytes) else audio,
|
||||
"model": "cosyvoice",
|
||||
"voice_id": COSYVOICE_VOICE_ID
|
||||
}
|
||||
else:
|
||||
return {"success": False, "error": "No audio returned"}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
@cosyvoice_bp.route('/cosyvoice', methods=['POST'])
|
||||
def cosyvoice_synthesize():
|
||||
"""CosyVoice合成接口"""
|
||||
data = request.get_json()
|
||||
text = data.get('text', '')
|
||||
|
||||
if not text:
|
||||
return jsonify({"success": False, "error": "Text is required"}), 400
|
||||
|
||||
result = synthesize_cosyvoice(text)
|
||||
|
||||
if result.get("success"):
|
||||
return jsonify(result)
|
||||
else:
|
||||
return jsonify(result), 500
|
||||
@@ -0,0 +1,10 @@
|
||||
def synthesize_minimax(text):
|
||||
url = "https://api.minimaxi.com/v1/t2a_v2"
|
||||
headers = {"Authorization": f"Bearer {MINIMAX_API_KEY}", "Content-Type": "application/json"}
|
||||
payload = {
|
||||
"model": "speech-02-turbo",
|
||||
"text": text,
|
||||
"voice_setting": {"voice_id": MINIMAX_VOICE_ID}
|
||||
}
|
||||
resp = requests.post(url, headers=headers, json=payload)
|
||||
return jsonify({"success": True, "model": "minimax", "text": text, "response": resp.json()})
|
||||
@@ -0,0 +1,57 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
import base64
|
||||
import os
|
||||
import requests
|
||||
from app.services.minimax_service import synthesize_minimax
|
||||
from app.services.cosyvoice_service import synthesize_cosyvoice
|
||||
|
||||
bp = Blueprint('tts', __name__, url_prefix='/api')
|
||||
|
||||
MINIMAX_API_KEY = os.getenv('MINIMAX_API_KEY', '')
|
||||
MINIMAX_API_URL = 'https://api.minimax.chat/v1/t2a_v2'
|
||||
|
||||
COSYVOICE_API_URL = os.getenv('COSYVOICE_API_URL', 'http://127.0.0.1:5001/v1/audio/synthesis')
|
||||
|
||||
@bp.route('/synthesize', methods=['POST'])
|
||||
def synthesize():
|
||||
data = request.get_json()
|
||||
|
||||
if not data or 'text' not in data:
|
||||
return jsonify({'error': '缺少text参数'}), 400
|
||||
|
||||
text = data.get('text')
|
||||
engine = data.get('engine', 'minimax')
|
||||
speed = data.get('speed', 1.0)
|
||||
pitch = data.get('pitch', 0)
|
||||
volume = data.get('volume', 1.0)
|
||||
instruction = data.get('instruction') # 用于CosyVoice的情绪指令
|
||||
|
||||
if not text.strip():
|
||||
return jsonify({'error': '文本不能为空'}), 400
|
||||
|
||||
if len(text) > 500:
|
||||
return jsonify({'error': '文本不能超过500字'}), 400
|
||||
|
||||
try:
|
||||
if engine == 'cosyvoice':
|
||||
audio_data = synthesize_cosyvoice(
|
||||
text=text,
|
||||
speed=speed,
|
||||
pitch=pitch,
|
||||
volume=volume,
|
||||
instruction=instruction
|
||||
)
|
||||
else:
|
||||
audio_data = synthesize_minimax(
|
||||
text=text,
|
||||
speed=speed,
|
||||
pitch=pitch,
|
||||
volume=volume
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'audio': base64.b64encode(audio_data).decode('utf-8')
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e)}), 500
|
||||
@@ -0,0 +1,43 @@
|
||||
import os
|
||||
import requests
|
||||
|
||||
def synthesize_cosyvoice(text, speed=1.0, pitch=0, volume=1.0, instruction=None):
|
||||
"""调用阿里云百炼CosyVoice API"""
|
||||
api_key = os.getenv('DASHSCOPE_API_KEY', '')
|
||||
if not api_key:
|
||||
raise ValueError('DASHSCOPE_API_KEY未设置')
|
||||
|
||||
url = 'https://dashscope.aliyuncs.com/api/v1/services/audio/tts/SpeechSynthesizer'
|
||||
headers = {
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
payload = {
|
||||
'model': 'cosyvoice-v3.5-flash',
|
||||
'input': {
|
||||
'text': text,
|
||||
'voice': 'cosyvoice-v3.5-flash-bailian-e859a6e87823419f9c6b3eefb1a97dc3',
|
||||
'format': 'mp3',
|
||||
'sample_rate': 32000
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=60)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f'CosyVoice API错误: {response.status_code} - {response.text}')
|
||||
|
||||
result = response.json()
|
||||
|
||||
# 阿里云返回JSON格式,音频数据在 output.audio.url 字段
|
||||
if 'output' in result and 'audio' in result['output']:
|
||||
audio_url = result['output']['audio'].get('url')
|
||||
if audio_url:
|
||||
# 从URL下载实际音频
|
||||
audio_response = requests.get(audio_url, timeout=60)
|
||||
if audio_response.status_code == 200:
|
||||
return audio_response.content
|
||||
raise Exception(f'下载音频失败: {audio_response.status_code}')
|
||||
|
||||
raise Exception(f'CosyVoice返回格式异常: {result}')
|
||||
@@ -0,0 +1,50 @@
|
||||
import os
|
||||
import base64
|
||||
import requests
|
||||
from io import BytesIO
|
||||
|
||||
def synthesize_minimax(text, speed=1.0, pitch=0, volume=1.0):
|
||||
"""调用MiniMax TTS API"""
|
||||
api_key = os.getenv('MINIMAX_API_KEY', '')
|
||||
if not api_key:
|
||||
raise ValueError('MINIMAX_API_KEY未设置')
|
||||
|
||||
# 将音量映射到0-100
|
||||
volume_int = int(volume * 100)
|
||||
|
||||
url = 'https://api.minimax.chat/v1/t2a_v2'
|
||||
headers = {
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
payload = {
|
||||
'model': 'speech-2.8-hd',
|
||||
'text': text,
|
||||
'stream': False,
|
||||
'voice_setting': {
|
||||
'voice_id': 'lanhao_military_tech',
|
||||
'speed': speed,
|
||||
'vol': volume_int,
|
||||
'pitch': pitch,
|
||||
},
|
||||
'audio_setting': {
|
||||
'audio_sample_rate': 32000,
|
||||
'bitrate': 128000,
|
||||
'format': 'mp3'
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(url, json=payload, headers=headers, timeout=60)
|
||||
|
||||
if response.status_code != 200:
|
||||
raise Exception(f'MiniMax API错误: {response.status_code} - {response.text}')
|
||||
|
||||
result = response.json()
|
||||
|
||||
if 'data' in result and 'audio' in result['data']:
|
||||
audio_hex = result['data']['audio']
|
||||
audio_bytes = bytes.fromhex(audio_hex)
|
||||
return audio_bytes
|
||||
|
||||
raise Exception(f'MiniMax返回格式异常: {result}')
|
||||
Reference in New Issue
Block a user