Spaces:
Runtime error
Runtime error
Upload 47 files
Browse files- app.py +139 -45
- backend/.env.example +2 -1
- backend/modules/__init__.py +30 -0
- backend/modules/router.py +3 -15
- backend/modules/stream_processor.py +576 -0
app.py
CHANGED
|
@@ -31,6 +31,8 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'backend'))
|
|
| 31 |
# 导入后端模块
|
| 32 |
from modules.gateway import GradioAPIGateway, GatewayConfig
|
| 33 |
from modules.groq_client import GroqConfig
|
|
|
|
|
|
|
| 34 |
from modules.logging_config import setup_logging, get_component_logger, Component
|
| 35 |
|
| 36 |
# 配置日志
|
|
@@ -39,13 +41,14 @@ logger = get_component_logger(Component.SYSTEM)
|
|
| 39 |
|
| 40 |
from contextlib import asynccontextmanager
|
| 41 |
|
| 42 |
-
# 全局
|
| 43 |
gateway: Optional[GradioAPIGateway] = None
|
|
|
|
| 44 |
|
| 45 |
@asynccontextmanager
|
| 46 |
async def lifespan(app: FastAPI):
|
| 47 |
"""应用生命周期管理"""
|
| 48 |
-
global gateway
|
| 49 |
|
| 50 |
# 启动时初始化
|
| 51 |
logger.info("初始化 Universal Fast Dubbing 后端...")
|
|
@@ -58,6 +61,13 @@ async def lifespan(app: FastAPI):
|
|
| 58 |
llm_model=os.getenv("LLM_MODEL", "llama3-8b-8192")
|
| 59 |
) if groq_api_key else None
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
# 配置网关
|
| 62 |
gateway_config = GatewayConfig(
|
| 63 |
temp_dir=os.getenv("TEMP_DIR", "temp/gateway"),
|
|
@@ -69,14 +79,28 @@ async def lifespan(app: FastAPI):
|
|
| 69 |
gateway = GradioAPIGateway(config=gateway_config, groq_config=groq_config)
|
| 70 |
await gateway.initialize()
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
logger.info("后端初始化完成")
|
| 73 |
|
| 74 |
yield # 应用运行期间
|
| 75 |
|
| 76 |
# 关闭时清理
|
| 77 |
if gateway:
|
| 78 |
-
# 这里可以添加清理逻辑
|
| 79 |
pass
|
|
|
|
|
|
|
| 80 |
|
| 81 |
# 创建 FastAPI 应用
|
| 82 |
app = FastAPI(
|
|
@@ -452,10 +476,10 @@ async def quick_start_dubbing(
|
|
| 452 |
"""
|
| 453 |
|
| 454 |
async def event_generator() -> AsyncGenerator[str, None]:
|
| 455 |
-
"""SSE 事件生成器"""
|
| 456 |
try:
|
| 457 |
-
if not
|
| 458 |
-
yield f"data: {json.dumps({'type': 'error', 'message': '
|
| 459 |
return
|
| 460 |
|
| 461 |
# 解析客户端配置
|
|
@@ -468,55 +492,125 @@ async def quick_start_dubbing(
|
|
| 468 |
|
| 469 |
logger.info(f"[快速启动] 收到音频文件: {audio.filename}")
|
| 470 |
|
| 471 |
-
# 读取音频数据
|
| 472 |
audio_content = await audio.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
|
| 474 |
# 发送初始进度
|
| 475 |
-
yield f"data: {json.dumps({'type': 'progress', 'progress':
|
| 476 |
|
| 477 |
-
#
|
| 478 |
-
|
| 479 |
-
"
|
| 480 |
-
"client_config": ext_config,
|
| 481 |
-
"segment_duration": 60 # 快速模式使用较短分段
|
| 482 |
-
}
|
| 483 |
|
| 484 |
-
#
|
| 485 |
-
|
| 486 |
-
|
|
|
|
|
|
|
|
|
|
| 487 |
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
|
| 515 |
-
logger.info(f"[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
|
| 517 |
except Exception as e:
|
| 518 |
-
logger.error(f"[
|
| 519 |
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
|
| 521 |
return StreamingResponse(
|
| 522 |
event_generator(),
|
|
|
|
| 31 |
# 导入后端模块
|
| 32 |
from modules.gateway import GradioAPIGateway, GatewayConfig
|
| 33 |
from modules.groq_client import GroqConfig
|
| 34 |
+
from modules.siliconflow_client import SiliconFlowConfig
|
| 35 |
+
from modules.stream_processor import StreamProcessor, StreamConfig
|
| 36 |
from modules.logging_config import setup_logging, get_component_logger, Component
|
| 37 |
|
| 38 |
# 配置日志
|
|
|
|
| 41 |
|
| 42 |
from contextlib import asynccontextmanager
|
| 43 |
|
| 44 |
+
# 全局实例
|
| 45 |
gateway: Optional[GradioAPIGateway] = None
|
| 46 |
+
stream_processor: Optional[StreamProcessor] = None
|
| 47 |
|
| 48 |
@asynccontextmanager
|
| 49 |
async def lifespan(app: FastAPI):
|
| 50 |
"""应用生命周期管理"""
|
| 51 |
+
global gateway, stream_processor
|
| 52 |
|
| 53 |
# 启动时初始化
|
| 54 |
logger.info("初始化 Universal Fast Dubbing 后端...")
|
|
|
|
| 61 |
llm_model=os.getenv("LLM_MODEL", "llama3-8b-8192")
|
| 62 |
) if groq_api_key else None
|
| 63 |
|
| 64 |
+
# 配置 SiliconFlow
|
| 65 |
+
siliconflow_api_key = os.getenv("SILICONFLOW_API_KEY")
|
| 66 |
+
siliconflow_config = SiliconFlowConfig(
|
| 67 |
+
api_key=siliconflow_api_key,
|
| 68 |
+
tts_model=os.getenv("SILICONFLOW_TTS_MODEL", "fishaudio/fish-speech-1.5")
|
| 69 |
+
) if siliconflow_api_key else None
|
| 70 |
+
|
| 71 |
# 配置网关
|
| 72 |
gateway_config = GatewayConfig(
|
| 73 |
temp_dir=os.getenv("TEMP_DIR", "temp/gateway"),
|
|
|
|
| 79 |
gateway = GradioAPIGateway(config=gateway_config, groq_config=groq_config)
|
| 80 |
await gateway.initialize()
|
| 81 |
|
| 82 |
+
# 初始化流式处理器
|
| 83 |
+
stream_config = StreamConfig(
|
| 84 |
+
temp_dir=os.getenv("TEMP_DIR", "temp/stream"),
|
| 85 |
+
max_segment_duration=float(os.getenv("MAX_SEGMENT_DURATION", "120")),
|
| 86 |
+
first_segment_target=float(os.getenv("FIRST_SEGMENT_TARGET", "30"))
|
| 87 |
+
)
|
| 88 |
+
stream_processor = StreamProcessor(
|
| 89 |
+
config=stream_config,
|
| 90 |
+
groq_config=groq_config,
|
| 91 |
+
siliconflow_config=siliconflow_config
|
| 92 |
+
)
|
| 93 |
+
await stream_processor.initialize()
|
| 94 |
+
|
| 95 |
logger.info("后端初始化完成")
|
| 96 |
|
| 97 |
yield # 应用运行期间
|
| 98 |
|
| 99 |
# 关闭时清理
|
| 100 |
if gateway:
|
|
|
|
| 101 |
pass
|
| 102 |
+
if stream_processor:
|
| 103 |
+
stream_processor.cleanup()
|
| 104 |
|
| 105 |
# 创建 FastAPI 应用
|
| 106 |
app = FastAPI(
|
|
|
|
| 476 |
"""
|
| 477 |
|
| 478 |
async def event_generator() -> AsyncGenerator[str, None]:
|
| 479 |
+
"""SSE 事件生成器 - 使用新的流式处理器"""
|
| 480 |
try:
|
| 481 |
+
if not stream_processor or not stream_processor.is_initialized:
|
| 482 |
+
yield f"data: {json.dumps({'type': 'error', 'message': '流式处理器未初始化'})}\n\n"
|
| 483 |
return
|
| 484 |
|
| 485 |
# 解析客户端配置
|
|
|
|
| 492 |
|
| 493 |
logger.info(f"[快速启动] 收到音频文件: {audio.filename}")
|
| 494 |
|
| 495 |
+
# 读取音频数据并保存到临时文件
|
| 496 |
audio_content = await audio.read()
|
| 497 |
+
temp_audio_path = f"temp/stream/upload_{os.urandom(4).hex()}.wav"
|
| 498 |
+
os.makedirs(os.path.dirname(temp_audio_path), exist_ok=True)
|
| 499 |
+
|
| 500 |
+
with open(temp_audio_path, 'wb') as f:
|
| 501 |
+
f.write(audio_content)
|
| 502 |
|
| 503 |
# 发送初始进度
|
| 504 |
+
yield f"data: {json.dumps({'type': 'progress', 'progress': 5, 'message': '音频上传完成,开始处理...'})}\n\n"
|
| 505 |
|
| 506 |
+
# 使用流式处理器处理
|
| 507 |
+
async for update in stream_processor.process_stream(temp_audio_path, ext_config):
|
| 508 |
+
yield f"data: {json.dumps(update)}\n\n"
|
|
|
|
|
|
|
|
|
|
| 509 |
|
| 510 |
+
# 清理临时文件
|
| 511 |
+
try:
|
| 512 |
+
if os.path.exists(temp_audio_path):
|
| 513 |
+
os.remove(temp_audio_path)
|
| 514 |
+
except:
|
| 515 |
+
pass
|
| 516 |
|
| 517 |
+
except Exception as e:
|
| 518 |
+
logger.error(f"[快速启动] 错误: {e}")
|
| 519 |
+
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
| 520 |
+
|
| 521 |
+
return StreamingResponse(
|
| 522 |
+
event_generator(),
|
| 523 |
+
media_type="text/event-stream",
|
| 524 |
+
headers={
|
| 525 |
+
"Cache-Control": "no-cache",
|
| 526 |
+
"Connection": "keep-alive",
|
| 527 |
+
"X-Accel-Buffering": "no",
|
| 528 |
+
}
|
| 529 |
+
)
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
@app.post("/api/upload_and_process")
|
| 533 |
+
async def upload_and_process(
|
| 534 |
+
audio: UploadFile = File(...),
|
| 535 |
+
client_config: str = Form(default="{}")
|
| 536 |
+
):
|
| 537 |
+
"""
|
| 538 |
+
上传音频并流式处理 API
|
| 539 |
+
|
| 540 |
+
完整流程:
|
| 541 |
+
扩展上传音频 → 语音识别 (Whisper V3 带时间戳) → 带时间戳分段异步处理
|
| 542 |
+
→ 翻译 + 角色识别 (Llama 3) → 语音合成 (Edge-TTS / SiliconFlow)
|
| 543 |
+
→ 音频同步对齐 (时间戳匹配) → 配音音频分段输出
|
| 544 |
+
|
| 545 |
+
参数:
|
| 546 |
+
audio: 音频文件(来自扩展端下载或录制)
|
| 547 |
+
client_config: 客户端配置JSON,包含:
|
| 548 |
+
- asrProvider: ASR提供商 (groq/siliconflow)
|
| 549 |
+
- ttsProvider: TTS提供商 (edge-tts/siliconflow)
|
| 550 |
+
- voiceMale: 男声语音模型
|
| 551 |
+
- voiceFemale: 女声语音模型
|
| 552 |
+
- syncOffset: 同步偏移量(毫秒)
|
| 553 |
+
|
| 554 |
+
返回:
|
| 555 |
+
SSE 事件流
|
| 556 |
+
"""
|
| 557 |
+
|
| 558 |
+
async def event_generator() -> AsyncGenerator[str, None]:
|
| 559 |
+
"""SSE 事件生成器"""
|
| 560 |
+
temp_audio_path = None
|
| 561 |
+
|
| 562 |
+
try:
|
| 563 |
+
if not stream_processor or not stream_processor.is_initialized:
|
| 564 |
+
yield f"data: {json.dumps({'type': 'error', 'message': '流式处理器未初始化'})}\n\n"
|
| 565 |
+
return
|
| 566 |
+
|
| 567 |
+
# 解析客户端配置
|
| 568 |
+
ext_config = {}
|
| 569 |
+
if client_config and client_config != "{}":
|
| 570 |
+
try:
|
| 571 |
+
ext_config = json.loads(client_config)
|
| 572 |
+
logger.info(f"[上传处理] 客户端配置: {list(ext_config.keys())}")
|
| 573 |
+
except json.JSONDecodeError as e:
|
| 574 |
+
logger.warning(f"[上传处理] 配置解析失败: {e}")
|
| 575 |
+
|
| 576 |
+
# 获取文件信息
|
| 577 |
+
filename = audio.filename or "unknown"
|
| 578 |
+
content_type = audio.content_type or "audio/mpeg"
|
| 579 |
|
| 580 |
+
logger.info(f"[上传处理] 收到文件: {filename}, 类型: {content_type}")
|
| 581 |
+
|
| 582 |
+
# 保存上传的音频到临时文件
|
| 583 |
+
file_ext = os.path.splitext(filename)[1] or ".mp3"
|
| 584 |
+
temp_audio_path = f"temp/stream/upload_{os.urandom(4).hex()}{file_ext}"
|
| 585 |
+
os.makedirs(os.path.dirname(temp_audio_path), exist_ok=True)
|
| 586 |
+
|
| 587 |
+
audio_content = await audio.read()
|
| 588 |
+
file_size_mb = len(audio_content) / (1024 * 1024)
|
| 589 |
+
|
| 590 |
+
with open(temp_audio_path, 'wb') as f:
|
| 591 |
+
f.write(audio_content)
|
| 592 |
+
|
| 593 |
+
logger.info(f"[上传处理] 音频已保存: {temp_audio_path}, 大小: {file_size_mb:.2f}MB")
|
| 594 |
+
|
| 595 |
+
# 发送初始进度
|
| 596 |
+
yield f"data: {json.dumps({'type': 'progress', 'progress': 5, 'message': f'音频上传完成 ({file_size_mb:.1f}MB),开始处理...'})}\n\n"
|
| 597 |
+
|
| 598 |
+
# 使用流式处理器处理
|
| 599 |
+
async for update in stream_processor.process_stream(temp_audio_path, ext_config):
|
| 600 |
+
yield f"data: {json.dumps(update)}\n\n"
|
| 601 |
|
| 602 |
except Exception as e:
|
| 603 |
+
logger.error(f"[上传处理] 错误: {e}", exc_info=True)
|
| 604 |
yield f"data: {json.dumps({'type': 'error', 'message': str(e)})}\n\n"
|
| 605 |
+
|
| 606 |
+
finally:
|
| 607 |
+
# 清理临时文件
|
| 608 |
+
if temp_audio_path:
|
| 609 |
+
try:
|
| 610 |
+
if os.path.exists(temp_audio_path):
|
| 611 |
+
os.remove(temp_audio_path)
|
| 612 |
+
except Exception as e:
|
| 613 |
+
logger.warning(f"[上传处理] 清理临时文件失败: {e}")
|
| 614 |
|
| 615 |
return StreamingResponse(
|
| 616 |
event_generator(),
|
backend/.env.example
CHANGED
|
@@ -25,7 +25,8 @@ PORT=7860
|
|
| 25 |
# ============================================================================
|
| 26 |
# 处理配置
|
| 27 |
# ============================================================================
|
| 28 |
-
MAX_SEGMENT_DURATION=
|
|
|
|
| 29 |
MAX_CONCURRENT_WORKERS=3
|
| 30 |
CACHE_DURATION=3600
|
| 31 |
|
|
|
|
| 25 |
# ============================================================================
|
| 26 |
# 处理配置
|
| 27 |
# ============================================================================
|
| 28 |
+
MAX_SEGMENT_DURATION=120
|
| 29 |
+
FIRST_SEGMENT_TARGET=30
|
| 30 |
MAX_CONCURRENT_WORKERS=3
|
| 31 |
CACHE_DURATION=3600
|
| 32 |
|
backend/modules/__init__.py
CHANGED
|
@@ -112,6 +112,23 @@ from .performance_monitor import (
|
|
| 112 |
track_performance,
|
| 113 |
)
|
| 114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
__all__ = [
|
| 116 |
# 统一错误响应
|
| 117 |
"ErrorCode",
|
|
@@ -148,6 +165,14 @@ __all__ = [
|
|
| 148 |
"GroqAuthError",
|
| 149 |
"GroqConnectionError",
|
| 150 |
"RetryStats",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
# TTS 生成器
|
| 152 |
"TTSGenerator",
|
| 153 |
"TTSConfig",
|
|
@@ -192,4 +217,9 @@ __all__ = [
|
|
| 192 |
"OperationTracker",
|
| 193 |
"get_performance_monitor",
|
| 194 |
"track_performance",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
]
|
|
|
|
| 112 |
track_performance,
|
| 113 |
)
|
| 114 |
|
| 115 |
+
from .stream_processor import (
|
| 116 |
+
StreamProcessor,
|
| 117 |
+
StreamConfig,
|
| 118 |
+
ProcessingStage,
|
| 119 |
+
SegmentData,
|
| 120 |
+
)
|
| 121 |
+
|
| 122 |
+
from .siliconflow_client import (
|
| 123 |
+
SiliconFlowClient,
|
| 124 |
+
SiliconFlowConfig,
|
| 125 |
+
SiliconFlowError,
|
| 126 |
+
SiliconFlowRateLimitError,
|
| 127 |
+
SiliconFlowTimeoutError,
|
| 128 |
+
SiliconFlowAuthError,
|
| 129 |
+
SiliconFlowConnectionError,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
__all__ = [
|
| 133 |
# 统一错误响应
|
| 134 |
"ErrorCode",
|
|
|
|
| 165 |
"GroqAuthError",
|
| 166 |
"GroqConnectionError",
|
| 167 |
"RetryStats",
|
| 168 |
+
# SiliconFlow 客户端
|
| 169 |
+
"SiliconFlowClient",
|
| 170 |
+
"SiliconFlowConfig",
|
| 171 |
+
"SiliconFlowError",
|
| 172 |
+
"SiliconFlowRateLimitError",
|
| 173 |
+
"SiliconFlowTimeoutError",
|
| 174 |
+
"SiliconFlowAuthError",
|
| 175 |
+
"SiliconFlowConnectionError",
|
| 176 |
# TTS 生成器
|
| 177 |
"TTSGenerator",
|
| 178 |
"TTSConfig",
|
|
|
|
| 217 |
"OperationTracker",
|
| 218 |
"get_performance_monitor",
|
| 219 |
"track_performance",
|
| 220 |
+
# 流式处理器
|
| 221 |
+
"StreamProcessor",
|
| 222 |
+
"StreamConfig",
|
| 223 |
+
"ProcessingStage",
|
| 224 |
+
"SegmentData",
|
| 225 |
]
|
backend/modules/router.py
CHANGED
|
@@ -93,7 +93,7 @@ class RequestRouter:
|
|
| 93 |
路由处理请求
|
| 94 |
|
| 95 |
参数:
|
| 96 |
-
mode: 处理模式 ("record", "upload"
|
| 97 |
data: 请求数据
|
| 98 |
session_id: 会话ID(可选)
|
| 99 |
|
|
@@ -108,20 +108,8 @@ class RequestRouter:
|
|
| 108 |
|
| 109 |
logger.info(f"[{session_id}] 路由请求: mode={mode}")
|
| 110 |
|
| 111 |
-
#
|
| 112 |
-
|
| 113 |
-
return await self._handle_audio_upload(data, session_id)
|
| 114 |
-
|
| 115 |
-
# URL模式:返回错误,提示需要音频数据
|
| 116 |
-
if mode == "url":
|
| 117 |
-
raise RouterError(
|
| 118 |
-
"URL模式需要扩展端先获取音频后上传。"
|
| 119 |
-
"请确保扩展端音频拦截功能正常工作,或使用录制模式。"
|
| 120 |
-
)
|
| 121 |
-
|
| 122 |
-
# 其他情况
|
| 123 |
-
raise RouterError("需要提供 audio_data 或 audio_path")
|
| 124 |
-
|
| 125 |
|
| 126 |
async def _handle_audio_upload(
|
| 127 |
self,
|
|
|
|
| 93 |
路由处理请求
|
| 94 |
|
| 95 |
参数:
|
| 96 |
+
mode: 处理模式 ("record", "upload")
|
| 97 |
data: 请求数据
|
| 98 |
session_id: 会话ID(可选)
|
| 99 |
|
|
|
|
| 108 |
|
| 109 |
logger.info(f"[{session_id}] 路由请求: mode={mode}")
|
| 110 |
|
| 111 |
+
# 所有模式都走录制/上传处理
|
| 112 |
+
return await self._handle_audio_upload(data, session_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
async def _handle_audio_upload(
|
| 115 |
self,
|
backend/modules/stream_processor.py
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
流式异步处理模块
|
| 3 |
+
|
| 4 |
+
实现完整的流式配音处理流程:
|
| 5 |
+
扩展上传音频 → 语音识别 (Whisper V3 带时间戳) → 带时间戳分段异步处理
|
| 6 |
+
→ 翻译 + 角色识别 (Llama 3) → 语音合成 (Edge-TTS / SiliconFlow)
|
| 7 |
+
→ 音频同步对齐 (时间戳匹配) → 配音音频分段输出
|
| 8 |
+
|
| 9 |
+
支持:
|
| 10 |
+
- 流式 SSE 输出
|
| 11 |
+
- 分段并行处理
|
| 12 |
+
- 第一段完成即可开始播放
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
import os
|
| 16 |
+
import asyncio
|
| 17 |
+
import logging
|
| 18 |
+
import time
|
| 19 |
+
import uuid
|
| 20 |
+
import base64
|
| 21 |
+
from typing import Dict, Any, Optional, List, AsyncGenerator
|
| 22 |
+
from dataclasses import dataclass, field
|
| 23 |
+
from enum import Enum
|
| 24 |
+
|
| 25 |
+
from .groq_client import GroqClient, GroqConfig, GroqError
|
| 26 |
+
from .siliconflow_client import SiliconFlowClient, SiliconFlowConfig
|
| 27 |
+
from .tts_generator import TTSGenerator, TTSConfig
|
| 28 |
+
from .audio_sync import AudioSyncEngine, SyncConfig
|
| 29 |
+
|
| 30 |
+
logger = logging.getLogger(__name__)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class ProcessingStage(Enum):
|
| 34 |
+
"""处理阶段枚举"""
|
| 35 |
+
INIT = "init"
|
| 36 |
+
ASR = "asr"
|
| 37 |
+
TRANSLATE = "translate"
|
| 38 |
+
TTS = "tts"
|
| 39 |
+
SYNC = "sync"
|
| 40 |
+
COMPLETE = "complete"
|
| 41 |
+
ERROR = "error"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclass
|
| 45 |
+
class StreamConfig:
|
| 46 |
+
"""
|
| 47 |
+
流式处理配置
|
| 48 |
+
|
| 49 |
+
属性:
|
| 50 |
+
temp_dir: 临时文件目录
|
| 51 |
+
max_segment_duration: 每段最大时长(秒)
|
| 52 |
+
first_segment_target: 首段目标时长(秒),用于快速开始播放
|
| 53 |
+
parallel_tts: 是否并行生成TTS
|
| 54 |
+
max_tts_workers: TTS最大并发数
|
| 55 |
+
"""
|
| 56 |
+
temp_dir: str = "temp/stream"
|
| 57 |
+
max_segment_duration: float = 120.0 # 2分钟
|
| 58 |
+
first_segment_target: float = 30.0 # 首段30秒,快速开始
|
| 59 |
+
parallel_tts: bool = True
|
| 60 |
+
max_tts_workers: int = 3
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@dataclass
|
| 64 |
+
class SegmentData:
|
| 65 |
+
"""
|
| 66 |
+
分段数据
|
| 67 |
+
|
| 68 |
+
属性:
|
| 69 |
+
index: 分段索引
|
| 70 |
+
start_time: 开始时间(秒)
|
| 71 |
+
end_time: 结束时间(秒)
|
| 72 |
+
original_text: 原文
|
| 73 |
+
translated_text: 译文
|
| 74 |
+
role: 角色标签
|
| 75 |
+
audio_path: 生成的音频路径
|
| 76 |
+
audio_data: 音频二进制数据
|
| 77 |
+
"""
|
| 78 |
+
index: int
|
| 79 |
+
start_time: float
|
| 80 |
+
end_time: float
|
| 81 |
+
original_text: str = ""
|
| 82 |
+
translated_text: str = ""
|
| 83 |
+
role: str = "MALE"
|
| 84 |
+
audio_path: Optional[str] = None
|
| 85 |
+
audio_data: Optional[bytes] = None
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
class StreamProcessor:
|
| 89 |
+
"""
|
| 90 |
+
流式异步处理器
|
| 91 |
+
|
| 92 |
+
实现完整的配音处理流程,支持流式输出。
|
| 93 |
+
|
| 94 |
+
使用示例:
|
| 95 |
+
processor = StreamProcessor()
|
| 96 |
+
await processor.initialize()
|
| 97 |
+
|
| 98 |
+
async for update in processor.process_stream(audio_path, config):
|
| 99 |
+
if update['type'] == 'segment_ready':
|
| 100 |
+
# 播放分段音频
|
| 101 |
+
play_audio(update['audio_data'])
|
| 102 |
+
"""
|
| 103 |
+
|
| 104 |
+
def __init__(
|
| 105 |
+
self,
|
| 106 |
+
config: Optional[StreamConfig] = None,
|
| 107 |
+
groq_config: Optional[GroqConfig] = None,
|
| 108 |
+
siliconflow_config: Optional[SiliconFlowConfig] = None
|
| 109 |
+
):
|
| 110 |
+
"""
|
| 111 |
+
初始化流式处理器
|
| 112 |
+
|
| 113 |
+
参数:
|
| 114 |
+
config: 流式处理配置
|
| 115 |
+
groq_config: Groq 客户端配置
|
| 116 |
+
siliconflow_config: SiliconFlow 客户端配置
|
| 117 |
+
"""
|
| 118 |
+
self.config = config or StreamConfig()
|
| 119 |
+
self._groq_config = groq_config
|
| 120 |
+
self._siliconflow_config = siliconflow_config
|
| 121 |
+
|
| 122 |
+
# 子模块
|
| 123 |
+
self.groq_client: Optional[GroqClient] = None
|
| 124 |
+
self.siliconflow_client: Optional[SiliconFlowClient] = None
|
| 125 |
+
self.tts_generator: Optional[TTSGenerator] = None
|
| 126 |
+
self.audio_sync: Optional[AudioSyncEngine] = None
|
| 127 |
+
|
| 128 |
+
self._initialized = False
|
| 129 |
+
self._temp_files: List[str] = []
|
| 130 |
+
|
| 131 |
+
# 确保临时目录存在
|
| 132 |
+
os.makedirs(self.config.temp_dir, exist_ok=True)
|
| 133 |
+
|
| 134 |
+
logger.info("流式处理器配置完成")
|
| 135 |
+
|
| 136 |
+
async def initialize(self) -> None:
|
| 137 |
+
"""初始化所有子模块"""
|
| 138 |
+
if self._initialized:
|
| 139 |
+
return
|
| 140 |
+
|
| 141 |
+
logger.info("初始化流式处理器...")
|
| 142 |
+
|
| 143 |
+
# 初始化 Groq 客户端(ASR + LLM)
|
| 144 |
+
if self._groq_config:
|
| 145 |
+
self.groq_client = GroqClient(self._groq_config)
|
| 146 |
+
await self.groq_client.initialize()
|
| 147 |
+
|
| 148 |
+
# 初始化 SiliconFlow 客户端(可选的 ASR + TTS)
|
| 149 |
+
if self._siliconflow_config:
|
| 150 |
+
self.siliconflow_client = SiliconFlowClient(self._siliconflow_config)
|
| 151 |
+
await self.siliconflow_client.initialize()
|
| 152 |
+
|
| 153 |
+
# 初始化 TTS 生成器
|
| 154 |
+
tts_config = TTSConfig(temp_dir=os.path.join(self.config.temp_dir, "tts"))
|
| 155 |
+
self.tts_generator = TTSGenerator(tts_config)
|
| 156 |
+
|
| 157 |
+
# 初始化音频同步引擎
|
| 158 |
+
sync_config = SyncConfig(temp_dir=os.path.join(self.config.temp_dir, "sync"))
|
| 159 |
+
self.audio_sync = AudioSyncEngine(sync_config)
|
| 160 |
+
|
| 161 |
+
self._initialized = True
|
| 162 |
+
logger.info("流式处理器初始化完成")
|
| 163 |
+
|
| 164 |
+
def _ensure_initialized(self) -> None:
|
| 165 |
+
"""确保处理器已初始化"""
|
| 166 |
+
if not self._initialized:
|
| 167 |
+
raise RuntimeError("流式处理器未初始化,请先调用 initialize()")
|
| 168 |
+
|
| 169 |
+
async def process_stream(
|
| 170 |
+
self,
|
| 171 |
+
audio_path: str,
|
| 172 |
+
client_config: Optional[Dict[str, Any]] = None
|
| 173 |
+
) -> AsyncGenerator[Dict[str, Any], None]:
|
| 174 |
+
"""
|
| 175 |
+
流式处理音频文件
|
| 176 |
+
|
| 177 |
+
完整流程:
|
| 178 |
+
1. ASR 语音识别(带时间戳)
|
| 179 |
+
2. 按时间戳分段
|
| 180 |
+
3. 并行处理每段:翻译 → TTS → 同步
|
| 181 |
+
4. 流式输出每段结果
|
| 182 |
+
|
| 183 |
+
参数:
|
| 184 |
+
audio_path: 音频文件路径
|
| 185 |
+
client_config: 客户端配置
|
| 186 |
+
|
| 187 |
+
生成:
|
| 188 |
+
处理更新字典,包含:
|
| 189 |
+
- type: 消息类型 (progress/segment_ready/complete/error)
|
| 190 |
+
- 其他字段根据类型不同
|
| 191 |
+
"""
|
| 192 |
+
self._ensure_initialized()
|
| 193 |
+
|
| 194 |
+
session_id = str(uuid.uuid4())[:8]
|
| 195 |
+
start_time = time.time()
|
| 196 |
+
|
| 197 |
+
logger.info(f"[{session_id}] 开始流式处理: {audio_path}")
|
| 198 |
+
|
| 199 |
+
try:
|
| 200 |
+
# 1. 发送初始进度
|
| 201 |
+
yield {
|
| 202 |
+
'type': 'progress',
|
| 203 |
+
'stage': ProcessingStage.INIT.value,
|
| 204 |
+
'progress': 5,
|
| 205 |
+
'message': '正在分析音频...'
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
# 2. ASR 语音识别
|
| 209 |
+
yield {
|
| 210 |
+
'type': 'progress',
|
| 211 |
+
'stage': ProcessingStage.ASR.value,
|
| 212 |
+
'progress': 10,
|
| 213 |
+
'message': '语音识别中...'
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
asr_result = await self._do_asr(audio_path, client_config)
|
| 217 |
+
|
| 218 |
+
if not asr_result.get('segments'):
|
| 219 |
+
yield {
|
| 220 |
+
'type': 'error',
|
| 221 |
+
'message': '语音识别结果为空'
|
| 222 |
+
}
|
| 223 |
+
return
|
| 224 |
+
|
| 225 |
+
source_language = asr_result.get('language', 'unknown')
|
| 226 |
+
total_duration = asr_result.get('duration', 0)
|
| 227 |
+
|
| 228 |
+
logger.info(
|
| 229 |
+
f"[{session_id}] ASR完成: 语言={source_language}, "
|
| 230 |
+
f"时长={total_duration:.1f}s, 片段数={len(asr_result['segments'])}"
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
yield {
|
| 234 |
+
'type': 'progress',
|
| 235 |
+
'stage': ProcessingStage.ASR.value,
|
| 236 |
+
'progress': 25,
|
| 237 |
+
'message': f'识别完成: {source_language}, {len(asr_result["segments"])} 个片段'
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
# 3. 智能分组(按时间戳分段,优化首段快速输出)
|
| 241 |
+
segment_groups = self._group_segments_for_streaming(
|
| 242 |
+
asr_result['segments'],
|
| 243 |
+
total_duration
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
logger.info(f"[{session_id}] 分组完成: {len(segment_groups)} 组")
|
| 247 |
+
|
| 248 |
+
# 4. 流式处理每个分组
|
| 249 |
+
total_groups = len(segment_groups)
|
| 250 |
+
processed_groups = 0
|
| 251 |
+
|
| 252 |
+
for group_index, group in enumerate(segment_groups):
|
| 253 |
+
group_start_time = group[0].get('start', 0) if group else 0
|
| 254 |
+
group_end_time = group[-1].get('end', 0) if group else 0
|
| 255 |
+
|
| 256 |
+
yield {
|
| 257 |
+
'type': 'progress',
|
| 258 |
+
'stage': ProcessingStage.TRANSLATE.value,
|
| 259 |
+
'progress': 25 + (group_index / total_groups) * 60,
|
| 260 |
+
'message': f'处理第 {group_index + 1}/{total_groups} 段...'
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
# 4.1 翻译当前分组
|
| 264 |
+
translated_segments = await self._translate_segments(
|
| 265 |
+
group,
|
| 266 |
+
source_language,
|
| 267 |
+
client_config
|
| 268 |
+
)
|
| 269 |
+
|
| 270 |
+
# 4.2 TTS 生成
|
| 271 |
+
yield {
|
| 272 |
+
'type': 'progress',
|
| 273 |
+
'stage': ProcessingStage.TTS.value,
|
| 274 |
+
'progress': 25 + (group_index / total_groups) * 60 + 20,
|
| 275 |
+
'message': f'生成配音 {group_index + 1}/{total_groups}...'
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
tts_results = await self._generate_tts(
|
| 279 |
+
translated_segments,
|
| 280 |
+
client_config
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
# 4.3 音频同步
|
| 284 |
+
yield {
|
| 285 |
+
'type': 'progress',
|
| 286 |
+
'stage': ProcessingStage.SYNC.value,
|
| 287 |
+
'progress': 25 + (group_index / total_groups) * 60 + 40,
|
| 288 |
+
'message': f'同步音频 {group_index + 1}/{total_groups}...'
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
synced_audio = await self._sync_audio(
|
| 292 |
+
tts_results,
|
| 293 |
+
translated_segments,
|
| 294 |
+
group_end_time - group_start_time,
|
| 295 |
+
client_config
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
# 4.4 读取音频数据
|
| 299 |
+
audio_data = None
|
| 300 |
+
if synced_audio and os.path.exists(synced_audio):
|
| 301 |
+
with open(synced_audio, 'rb') as f:
|
| 302 |
+
audio_data = f.read()
|
| 303 |
+
|
| 304 |
+
# 4.5 输出分段结果
|
| 305 |
+
processed_groups += 1
|
| 306 |
+
|
| 307 |
+
yield {
|
| 308 |
+
'type': 'segment_ready',
|
| 309 |
+
'index': group_index,
|
| 310 |
+
'start_time': group_start_time,
|
| 311 |
+
'end_time': group_end_time,
|
| 312 |
+
'duration': group_end_time - group_start_time,
|
| 313 |
+
'audio_data': base64.b64encode(audio_data).decode('utf-8') if audio_data else None,
|
| 314 |
+
'segments': [
|
| 315 |
+
{
|
| 316 |
+
'original': seg.get('text', ''),
|
| 317 |
+
'translated': seg.get('cn', ''),
|
| 318 |
+
'role': seg.get('role', 'MALE'),
|
| 319 |
+
'start': seg.get('start', 0),
|
| 320 |
+
'end': seg.get('end', 0)
|
| 321 |
+
}
|
| 322 |
+
for seg in translated_segments
|
| 323 |
+
]
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
logger.info(
|
| 327 |
+
f"[{session_id}] 分组 {group_index + 1} 完成: "
|
| 328 |
+
f"{group_start_time:.1f}s - {group_end_time:.1f}s"
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
# 5. 处理完成
|
| 332 |
+
processing_time = time.time() - start_time
|
| 333 |
+
|
| 334 |
+
yield {
|
| 335 |
+
'type': 'complete',
|
| 336 |
+
'total_segments': processed_groups,
|
| 337 |
+
'total_duration': total_duration,
|
| 338 |
+
'processing_time': processing_time,
|
| 339 |
+
'source_language': source_language
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
logger.info(
|
| 343 |
+
f"[{session_id}] 流式处理完成: "
|
| 344 |
+
f"{processed_groups} 段, 耗时 {processing_time:.1f}s"
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
except Exception as e:
|
| 348 |
+
logger.error(f"[{session_id}] 流式处理失败: {e}")
|
| 349 |
+
yield {
|
| 350 |
+
'type': 'error',
|
| 351 |
+
'message': str(e)
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
async def _do_asr(
|
| 355 |
+
self,
|
| 356 |
+
audio_path: str,
|
| 357 |
+
client_config: Optional[Dict[str, Any]] = None
|
| 358 |
+
) -> Dict[str, Any]:
|
| 359 |
+
"""
|
| 360 |
+
执行语音识别
|
| 361 |
+
|
| 362 |
+
根据配置选择 Groq Whisper 或 SiliconFlow SenseVoice
|
| 363 |
+
"""
|
| 364 |
+
# 获取 ASR 提供商配置
|
| 365 |
+
asr_provider = 'groq' # 默认使用 Groq
|
| 366 |
+
if client_config:
|
| 367 |
+
asr_provider = client_config.get('asrProvider', 'groq')
|
| 368 |
+
|
| 369 |
+
if asr_provider == 'siliconflow' and self.siliconflow_client:
|
| 370 |
+
logger.info("使用 SiliconFlow SenseVoice 进行语音识别")
|
| 371 |
+
return await self.siliconflow_client.transcribe(audio_path)
|
| 372 |
+
elif self.groq_client:
|
| 373 |
+
logger.info("使用 Groq Whisper V3 进行语音识别")
|
| 374 |
+
return await self.groq_client.transcribe(audio_path)
|
| 375 |
+
else:
|
| 376 |
+
raise RuntimeError("没有可用的 ASR 服务")
|
| 377 |
+
|
| 378 |
+
def _group_segments_for_streaming(
|
| 379 |
+
self,
|
| 380 |
+
segments: List[Dict[str, Any]],
|
| 381 |
+
total_duration: float
|
| 382 |
+
) -> List[List[Dict[str, Any]]]:
|
| 383 |
+
"""
|
| 384 |
+
将 ASR 片段分组用于流式处理
|
| 385 |
+
|
| 386 |
+
策略:
|
| 387 |
+
1. 首段尽量短(30秒内),快速开始播放
|
| 388 |
+
2. 后续段按最大时长分组
|
| 389 |
+
3. 尽量在句子边界分割
|
| 390 |
+
"""
|
| 391 |
+
if not segments:
|
| 392 |
+
return []
|
| 393 |
+
|
| 394 |
+
groups = []
|
| 395 |
+
current_group = []
|
| 396 |
+
current_duration = 0
|
| 397 |
+
is_first_group = True
|
| 398 |
+
|
| 399 |
+
# 首段目标时长
|
| 400 |
+
target_duration = self.config.first_segment_target if is_first_group else self.config.max_segment_duration
|
| 401 |
+
|
| 402 |
+
for seg in segments:
|
| 403 |
+
seg_start = seg.get('start', 0)
|
| 404 |
+
seg_end = seg.get('end', 0)
|
| 405 |
+
seg_duration = seg_end - seg_start
|
| 406 |
+
|
| 407 |
+
# 检查是否需要开始新组
|
| 408 |
+
if current_group and current_duration + seg_duration > target_duration:
|
| 409 |
+
groups.append(current_group)
|
| 410 |
+
current_group = []
|
| 411 |
+
current_duration = 0
|
| 412 |
+
is_first_group = False
|
| 413 |
+
target_duration = self.config.max_segment_duration
|
| 414 |
+
|
| 415 |
+
current_group.append(seg)
|
| 416 |
+
current_duration += seg_duration
|
| 417 |
+
|
| 418 |
+
# 添加最后一组
|
| 419 |
+
if current_group:
|
| 420 |
+
groups.append(current_group)
|
| 421 |
+
|
| 422 |
+
return groups
|
| 423 |
+
|
| 424 |
+
async def _translate_segments(
|
| 425 |
+
self,
|
| 426 |
+
segments: List[Dict[str, Any]],
|
| 427 |
+
source_language: str,
|
| 428 |
+
client_config: Optional[Dict[str, Any]] = None
|
| 429 |
+
) -> List[Dict[str, Any]]:
|
| 430 |
+
"""
|
| 431 |
+
翻译片段并识别角色
|
| 432 |
+
"""
|
| 433 |
+
if not self.groq_client:
|
| 434 |
+
# 如果没有 Groq 客户端,返回原始片段
|
| 435 |
+
return [
|
| 436 |
+
{
|
| 437 |
+
**seg,
|
| 438 |
+
'cn': seg.get('text', ''),
|
| 439 |
+
'role': 'MALE'
|
| 440 |
+
}
|
| 441 |
+
for seg in segments
|
| 442 |
+
]
|
| 443 |
+
|
| 444 |
+
# 合并文本进行翻译
|
| 445 |
+
text = ' '.join(seg.get('text', '') for seg in segments)
|
| 446 |
+
|
| 447 |
+
translation_result = await self.groq_client.translate(
|
| 448 |
+
text,
|
| 449 |
+
source_language,
|
| 450 |
+
segments
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
# 合并翻译结果到原始片段
|
| 454 |
+
translated_segments = translation_result.get('segments', [])
|
| 455 |
+
|
| 456 |
+
result = []
|
| 457 |
+
for i, seg in enumerate(segments):
|
| 458 |
+
translated = translated_segments[i] if i < len(translated_segments) else {}
|
| 459 |
+
result.append({
|
| 460 |
+
**seg,
|
| 461 |
+
'cn': translated.get('cn', seg.get('text', '')),
|
| 462 |
+
'role': translated.get('role', 'MALE')
|
| 463 |
+
})
|
| 464 |
+
|
| 465 |
+
return result
|
| 466 |
+
|
| 467 |
+
async def _generate_tts(
|
| 468 |
+
self,
|
| 469 |
+
segments: List[Dict[str, Any]],
|
| 470 |
+
client_config: Optional[Dict[str, Any]] = None
|
| 471 |
+
) -> List[Optional[str]]:
|
| 472 |
+
"""
|
| 473 |
+
生成 TTS 音频
|
| 474 |
+
|
| 475 |
+
根据配置选择 Edge-TTS 或 SiliconFlow TTS
|
| 476 |
+
"""
|
| 477 |
+
# 获取 TTS 提供商配置
|
| 478 |
+
tts_provider = 'edge-tts' # 默认使用 Edge-TTS
|
| 479 |
+
if client_config:
|
| 480 |
+
tts_provider = client_config.get('ttsProvider', 'edge-tts')
|
| 481 |
+
|
| 482 |
+
# 准备 TTS 输入
|
| 483 |
+
tts_segments = [
|
| 484 |
+
{
|
| 485 |
+
'cn': seg.get('cn', ''),
|
| 486 |
+
'role': seg.get('role', 'MALE')
|
| 487 |
+
}
|
| 488 |
+
for seg in segments
|
| 489 |
+
]
|
| 490 |
+
|
| 491 |
+
if tts_provider == 'siliconflow' and self.siliconflow_client:
|
| 492 |
+
logger.info("使用 SiliconFlow TTS 生成配音")
|
| 493 |
+
# 使用 SiliconFlow TTS
|
| 494 |
+
results = []
|
| 495 |
+
for seg in tts_segments:
|
| 496 |
+
if seg['cn']:
|
| 497 |
+
output_path = os.path.join(
|
| 498 |
+
self.config.temp_dir,
|
| 499 |
+
f"tts_{uuid.uuid4().hex[:8]}.mp3"
|
| 500 |
+
)
|
| 501 |
+
await self.siliconflow_client.synthesize(
|
| 502 |
+
seg['cn'],
|
| 503 |
+
seg['role'],
|
| 504 |
+
output_path
|
| 505 |
+
)
|
| 506 |
+
results.append(output_path)
|
| 507 |
+
self._temp_files.append(output_path)
|
| 508 |
+
else:
|
| 509 |
+
results.append(None)
|
| 510 |
+
return results
|
| 511 |
+
else:
|
| 512 |
+
logger.info("使用 Edge-TTS 生成配音")
|
| 513 |
+
# 使用 Edge-TTS
|
| 514 |
+
return await self.tts_generator.generate(tts_segments, client_config)
|
| 515 |
+
|
| 516 |
+
async def _sync_audio(
|
| 517 |
+
self,
|
| 518 |
+
tts_paths: List[Optional[str]],
|
| 519 |
+
segments: List[Dict[str, Any]],
|
| 520 |
+
target_duration: float,
|
| 521 |
+
client_config: Optional[Dict[str, Any]] = None
|
| 522 |
+
) -> Optional[str]:
|
| 523 |
+
"""
|
| 524 |
+
同步音频到时间戳
|
| 525 |
+
"""
|
| 526 |
+
# 过滤有效的 TTS 路径
|
| 527 |
+
valid_paths = []
|
| 528 |
+
valid_segments = []
|
| 529 |
+
|
| 530 |
+
for i, (path, seg) in enumerate(zip(tts_paths, segments)):
|
| 531 |
+
if path and os.path.exists(path):
|
| 532 |
+
valid_paths.append(path)
|
| 533 |
+
valid_segments.append({
|
| 534 |
+
'start': seg.get('start', 0),
|
| 535 |
+
'end': seg.get('end', 0)
|
| 536 |
+
})
|
| 537 |
+
|
| 538 |
+
if not valid_paths:
|
| 539 |
+
logger.warning("没有有效的 TTS 音频")
|
| 540 |
+
return None
|
| 541 |
+
|
| 542 |
+
# 调用音频同步引擎
|
| 543 |
+
return await self.audio_sync.align(
|
| 544 |
+
valid_paths,
|
| 545 |
+
valid_segments,
|
| 546 |
+
target_duration,
|
| 547 |
+
client_config
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
def cleanup(self) -> int:
|
| 551 |
+
"""清理临时文件"""
|
| 552 |
+
cleaned = 0
|
| 553 |
+
|
| 554 |
+
for path in self._temp_files:
|
| 555 |
+
try:
|
| 556 |
+
if os.path.exists(path):
|
| 557 |
+
os.remove(path)
|
| 558 |
+
cleaned += 1
|
| 559 |
+
except Exception as e:
|
| 560 |
+
logger.warning(f"清理临时文件失败 {path}: {e}")
|
| 561 |
+
|
| 562 |
+
self._temp_files.clear()
|
| 563 |
+
|
| 564 |
+
if self.tts_generator:
|
| 565 |
+
cleaned += self.tts_generator.cleanup()
|
| 566 |
+
|
| 567 |
+
if self.audio_sync:
|
| 568 |
+
cleaned += self.audio_sync.cleanup()
|
| 569 |
+
|
| 570 |
+
logger.info(f"流式处理器清理完成: {cleaned} 个文件")
|
| 571 |
+
return cleaned
|
| 572 |
+
|
| 573 |
+
@property
|
| 574 |
+
def is_initialized(self) -> bool:
|
| 575 |
+
"""检查处理器是否已初始化"""
|
| 576 |
+
return self._initialized
|