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:
2026-06-30 21:00:20 +08:00
commit d6edc69c65
17 changed files with 1551 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
# 应用包初始化
+14
View File
@@ -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)
+1
View File
@@ -0,0 +1 @@
# 路由包初始化
+15
View File
@@ -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': '服务运行正常'
})
+112
View File
@@ -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
+55
View File
@@ -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
+10
View File
@@ -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()})
+57
View File
@@ -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
View File
+43
View File
@@ -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}')
+50
View File
@@ -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}')
View File