cytopa99 commited on
Commit
3c26d39
·
verified ·
1 Parent(s): e25b15a

Upload 47 files

Browse files
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 gateway or not gateway.is_initialized:
458
- yield f"data: {json.dumps({'type': 'error', 'message': '后端未初始化'})}\n\n"
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': 10, 'message': '音频上传完成,开始处理...'})}\n\n"
476
 
477
- # 准备处理数据
478
- process_data = {
479
- "audio_data": audio_content,
480
- "client_config": ext_config,
481
- "segment_duration": 60 # 快速模式使用较短分段
482
- }
483
 
484
- # 使用分段处理模式
485
- segment_index = 0
486
- total_duration = 0
 
 
 
487
 
488
- async for update in gateway.process_request_segmented("upload", process_data):
489
- update_type = update.get("type", "progress")
490
-
491
- if update_type == "progress":
492
- yield f"data: {json.dumps({'type': 'progress', 'progress': update.get('progress', 0), 'message': update.get('message', '')})}\n\n"
493
-
494
- elif update_type == "segment_ready":
495
- segment_data = update.get("segment", {})
496
- audio_data = segment_data.get("audio_data")
497
-
498
- # 二进制数据转base64
499
- if isinstance(audio_data, bytes):
500
- audio_data = base64.b64encode(audio_data).decode('utf-8')
501
-
502
- yield f"data: {json.dumps({'type': 'segment_ready', 'index': segment_index, 'startTime': segment_data.get('start_time', total_duration), 'duration': segment_data.get('duration', 60), 'audioData': audio_data})}\n\n"
503
-
504
- total_duration += segment_data.get("duration", 60)
505
- segment_index += 1
506
-
507
- elif update_type == "complete":
508
- yield f"data: {json.dumps({'type': 'complete', 'totalSegments': segment_index, 'totalDuration': total_duration, 'processingTime': update.get('processing_time', 0)})}\n\n"
509
- break
510
-
511
- elif update_type == "error":
512
- yield f"data: {json.dumps({'type': 'error', 'message': update.get('message', '处理失败')})}\n\n"
513
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
 
515
- logger.info(f"[快速启动] 处理完成: {segment_index}段, 总时长: {total_duration}秒")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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(),
 
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=480
 
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", "url")
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
- if data.get('audio_data') or data.get('audio_path'):
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