File size: 10,618 Bytes
8f823b0
511ff0c
e80f558
7b64dcd
 
824183a
7b64dcd
8f823b0
 
511ff0c
 
824183a
511ff0c
 
 
bfefeb3
 
 
 
 
 
511ff0c
 
 
 
 
 
 
7b64dcd
 
 
 
 
 
025ca3f
bba0d84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b64dcd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
516d7b8
7b64dcd
516d7b8
7b64dcd
 
 
 
 
 
bba0d84
7b64dcd
 
 
 
cf355e6
 
 
 
 
 
025ca3f
7b64dcd
bba0d84
 
ef0d09e
7b64dcd
 
 
 
bba0d84
7b64dcd
 
e80f558
 
 
 
7b64dcd
 
 
 
 
 
cf355e6
bba0d84
cf355e6
511ff0c
cf355e6
 
511ff0c
8f823b0
cf355e6
511ff0c
cf355e6
 
8f823b0
cf355e6
 
 
511ff0c
cf355e6
 
 
 
 
511ff0c
cf355e6
 
511ff0c
cf355e6
 
511ff0c
cf355e6
 
 
 
511ff0c
cf355e6
 
 
 
 
511ff0c
cf355e6
8f823b0
cf355e6
 
 
 
511ff0c
cf355e6
 
 
 
 
 
 
511ff0c
cf355e6
 
 
 
511ff0c
cf355e6
 
511ff0c
cf355e6
 
 
 
 
 
 
 
511ff0c
cf355e6
 
 
 
 
 
511ff0c
cf355e6
 
 
 
bba0d84
 
8f823b0
 
cf355e6
 
511ff0c
8f823b0
 
 
cf355e6
8f823b0
 
 
bba0d84
8f823b0
 
bba0d84
8f823b0
 
bba0d84
8f823b0
 
 
 
 
 
 
cf355e6
8f823b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cf355e6
 
8f823b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bba0d84
8f823b0
7b64dcd
 
cf355e6
 
 
511ff0c
cf355e6
 
 
 
 
 
 
 
 
 
 
 
 
15e98c9
cf355e6
 
 
 
 
 
 
8f823b0
 
 
511ff0c
8f823b0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7b64dcd
 
 
824183a
7b64dcd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
import argparse
import sys
import time
import typing
from pathlib import Path
import multiprocessing

import uvicorn

HERE = Path(__file__).parent
lib_path = HERE / "src"
if lib_path.exists() and lib_path.as_posix() not in sys.path:
    sys.path.insert(0, lib_path.as_posix())

from voice_dialogue.core.constants import (
    audio_frames_queue,
    user_voice_queue,
    transcribed_text_queue,
    text_input_queue,
    audio_output_queue
)
from voice_dialogue.services.audio.capture import EchoCancellingAudioCapture
from voice_dialogue.services.audio.generator import TTSAudioGenerator
from voice_dialogue.services.audio.generators.models import tts_config_registry
from voice_dialogue.services.audio.player import AudioStreamPlayer
from voice_dialogue.services.speech.monitor import SpeechStateMonitor
from voice_dialogue.services.speech.recognizer import ASRWorker
from voice_dialogue.services.text.generator import LLMResponseGenerator

language: typing.Literal['zh', 'en'] = 'en'


def launch_system(
        user_language: str,
        speaker: str
) -> None:
    """
    启动完整的语音对话系统

    该函数负责启动并协调语音对话系统的所有组件,包括音频采集、语音识别、
    文本生成、语音合成和音频播放等功能模块。系统采用多线程架构,各组件
    通过队列进行数据传递和通信。

    系统工作流程:
    1. 音频采集:EchoCancellingAudioCapture 采集用户语音并进行回声消除
    2. 语音监测:SpeechStateMonitor 检测用户是否在说话
    3. 语音识别:ASRWorker 将用户语音转换为文本
    4. 文本生成:LLMResponseGenerator 基于用户问题生成AI回答
    5. 语音合成:TTSAudioGenerator 将AI回答转换为语音
    6. 音频播放:AudioStreamPlayer 播放生成的语音

    Args:
        user_language (str): 用户语言,支持 'zh'(中文)和 'en'(英文)
        speaker (str): 语音合成使用的说话人,支持:
                      '罗翔', '马保国', '沈逸', '杨幂', '周杰伦', '马云'

    Raises:
        ValueError: 当指定的说话人不在支持列表中时抛出异常

    Returns:
        None: 函数会一直运行直到所有线程结束

    Note:
        该函数会阻塞运行,直到系统被外部停止或发生异常
    """

    threads = []
    #
    audio_frame_probe = EchoCancellingAudioCapture(audio_frames_queue=audio_frames_queue)
    audio_frame_probe.start()
    threads.append(audio_frame_probe)

    #
    user_voice_checker = SpeechStateMonitor(
        audio_frame_queue=audio_frames_queue,
        user_voice_queue=user_voice_queue,
    )
    user_voice_checker.start()
    threads.append(user_voice_checker)

    #
    whisper_worker = ASRWorker(
        user_voice_queue=user_voice_queue, transcribed_text_queue=transcribed_text_queue,
        language=user_language
    )
    whisper_worker.start()
    threads.append(whisper_worker)

    answer_generator_worker = LLMResponseGenerator(
        user_question_queue=transcribed_text_queue,
        generated_answer_queue=text_input_queue
    )
    answer_generator_worker.start()
    threads.append(answer_generator_worker)

    # 动态获取TTS配置,而不是使用固定映射
    tts_speaker_config = _get_tts_config_by_speaker_name(speaker)
    if tts_speaker_config is None:
        # 如果找不到指定说话人,列出所有可用说话人并抛出异常
        available_speakers = _get_available_speaker_names()
        raise ValueError(f"不支持的TTS说话人: {speaker}。可用说话人: {', '.join(available_speakers)}")

    audio_generator_worker = TTSAudioGenerator(
        text_input_queue=text_input_queue,
        audio_output_queue=audio_output_queue,
        tts_config=tts_speaker_config
    )
    audio_generator_worker.start()
    threads.append(audio_generator_worker)

    audio_playing_worker = AudioStreamPlayer(audio_playing_queue=audio_output_queue)
    audio_playing_worker.start()
    threads.append(audio_playing_worker)

    while not all([thread.is_ready for thread in threads]):
        time.sleep(0.1)

    # audio_frame_probe.start_record()
    print(f'{"=" * 80}\n服务启动成功\n{"=" * 80}')
    for thread in threads:
        thread.join()


def _get_tts_config_by_speaker_name(speaker_name: str):
    """
    根据说话人名称获取TTS配置

    支持中文名称和英文名称,优先匹配中文名称映射,
    如果找不到则直接使用英文名称搜索

    Args:
        speaker_name (str): 说话人名称

    Returns:
        BaseTTSConfig: TTS配置,如果找不到则返回None
    """
    # 中文名称到英文名称的映射(保持向后兼容)
    chinese_to_english_mapping = {
        '罗翔': 'Luo Xiang',
        '马保国': 'Ma Baoguo',
        '沈逸': 'Shen Yi',
        '杨幂': 'Yang Mi',
        '周杰伦': 'Zhou Jielun',
        '马云': 'Ma Yun',
    }

    # 首先尝试中文名称映射
    english_name = chinese_to_english_mapping.get(speaker_name, speaker_name)

    # 获取所有可用配置
    all_configs = tts_config_registry.get_all_configs()

    # 搜索匹配的配置
    for config in all_configs:
        if config.character_name == english_name:
            return config

    # 如果通过映射找不到,尝试直接匹配输入的名称
    if speaker_name != english_name:
        for config in all_configs:
            if config.character_name == speaker_name:
                return config

    return None


def _get_available_speaker_names():
    """
    获取所有可用的说话人名称列表

    Returns:
        list[str]: 包含中文显示名称和英文原始名称的列表
    """
    # 中文显示名称映射
    english_to_chinese_mapping = {
        'Luo Xiang': '罗翔',
        'Ma Baoguo': '马保国',
        'Shen Yi': '沈逸',
        'Yang Mi': '杨幂',
        'Zhou Jielun': '周杰伦',
        'Ma Yun': '马云',
    }

    all_configs = tts_config_registry.get_all_configs()
    speaker_names = []

    for config in all_configs:
        # 优先显示中文名称
        chinese_name = english_to_chinese_mapping.get(config.character_name)
        if chinese_name:
            speaker_names.append(chinese_name)
        else:
            # 如果没有中文映射,使用英文原名
            speaker_names.append(config.character_name)

    return sorted(speaker_names)


def _update_argument_parser_speaker_choices():
    """
    动态更新命令行参数解析器中的说话人选项

    Returns:
        list[str]: 可用的说话人选择列表
    """
    return _get_available_speaker_names()


def create_argument_parser():
    """创建命令行参数解析器"""
    # 动态获取可用说话人列表
    available_speakers = _update_argument_parser_speaker_choices()

    parser = argparse.ArgumentParser(
        description="VoiceDialogue - 语音对话系统",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=f"""
示例用法:
  # 启动命令行模式(默认)
  python main.py

  # 启动命令行模式并指定参数
  python main.py --mode cli --language zh --speaker 沈逸

  # 启动API服务器
  python main.py --mode api

  # 启动API服务器并指定端口
  python main.py --mode api --port 9000

  # 启动API服务器并启用热重载(开发模式)
  python main.py --mode api --port 8000 --reload

支持的说话人:
  {', '.join(available_speakers)}
        """
    )

    # 运行模式选择
    parser.add_argument(
        '--mode', '-m',
        choices=['cli', 'api'],
        default='cli',
        help='运行模式: cli=命令行模式, api=API服务器模式 (默认: cli)'
    )

    # 命令行模式参数
    cli_group = parser.add_argument_group('命令行模式参数')
    cli_group.add_argument(
        '--language', '-l',
        choices=['zh', 'en'],
        default='zh',
        help='用户语言: zh=中文, en=英文 (默认: zh)'
    )
    cli_group.add_argument(
        '--speaker', '-s',
        choices=available_speakers,
        default='沈逸' if '沈逸' in available_speakers else (available_speakers[0] if available_speakers else '沈逸'),
        help='TTS说话人 (默认: 沈逸)'
    )

    # API服务器模式参数
    api_group = parser.add_argument_group('API服务器模式参数')
    api_group.add_argument(
        '--host',
        default='0.0.0.0',
        help='服务器主机地址 (默认: 0.0.0.0)'
    )
    api_group.add_argument(
        '--port', '-p',
        type=int,
        default=8000,
        help='服务器端口 (默认: 8000)'
    )
    api_group.add_argument(
        '--reload',
        action='store_true',
        help='启用热重载(开发模式)'
    )

    return parser


def launch_api_server(host: str = "0.0.0.0", port: int = 8000, reload: bool = False):
    """
    启动API服务器

    Args:
        host (str): 服务器主机地址,默认为 "0.0.0.0"
        port (int): 服务器端口,默认为 8000
        reload (bool): 是否启用热重载,默认为 False
    """
    print(f'{"=" * 80}\n正在启动API服务器...\n{"=" * 80}')
    print(f"服务器地址: http://{host}:{port}")
    print(f"API文档: http://{host}:{port}/docs")
    print(f"热重载: {'启用' if reload else '禁用'}")
    print(f'{"=" * 80}')

    # 导入并启动FastAPI应用
    uvicorn.run(
        "voice_dialogue.api.app:app",
        host=host,
        port=port,
        reload=reload,
        log_level="info"
    )


def main():
    """
    主程序入口函数

    根据命令行参数选择启动模式:
    - cli: 启动命令行语音对话系统
    - api: 启动HTTP API服务器
    """
    parser = create_argument_parser()
    args = parser.parse_args()

    print(f"""
{"=" * 80}
VoiceDialogue - 语音对话系统
{"=" * 80}
运行模式: {args.mode.upper()}
{"=" * 80}
    """)

    try:
        if args.mode == 'cli':
            print(f"语言设置: {args.language}")
            print(f"说话人: {args.speaker}")
            print("正在启动命令行语音对话系统...")
            launch_system(args.language, args.speaker)

        elif args.mode == 'api':
            launch_api_server(
                host=args.host,
                port=args.port,
                reload=args.reload
            )

    except KeyboardInterrupt:
        print("\n程序被用户中断")
    except Exception as e:
        print(f"程序运行出错: {e}")
        raise


if __name__ == '__main__':
    multiprocessing.freeze_support()
    main()