liumaolin commited on
Commit
d08a15b
·
1 Parent(s): 61524a8

Refactor project: split `main.py` functionality into modular components under `cli`, `core`, and `config`.

Browse files
src/voice_dialogue/api/server.py CHANGED
@@ -1,43 +1,34 @@
1
  """
2
- 独立的API服务器启动脚本
3
- 可以直接运行此脚本启动API服务器,无需通过main.py
4
- """
5
 
6
- import sys
7
- from pathlib import Path
8
 
9
  import uvicorn
10
 
11
- # 添加项目根目录到Python路径
12
- project_root = Path(__file__).parent.parent
13
- sys.path.insert(0, str(project_root))
14
-
15
- # 加载第三方库
16
- from config.paths import load_third_party
17
-
18
- load_third_party()
19
 
 
 
 
20
 
21
- def run_server(host: str = "0.0.0.0", port: int = 8000, reload: bool = False):
22
- """运行API服务器"""
23
- print(f"""
24
- {"=" * 80}
25
- VoiceDialogue API Server
26
- {"=" * 80}
27
- 服务器地址: http://{host}:{port}
28
- API文档: http://{host}:{port}/docs
29
- ReDoc文档: http://{host}:{port}/redoc
30
- 热重载: {'启用' if reload else '禁用'}
31
- {"=" * 80}
32
- """)
33
 
 
34
  uvicorn.run(
35
- "api.app:app",
36
  host=host,
37
  port=port,
38
  reload=reload,
39
- log_level="info",
40
- access_log=True
41
  )
42
 
43
 
@@ -50,4 +41,4 @@ if __name__ == "__main__":
50
  parser.add_argument("--reload", action="store_true", help="启用热重载")
51
 
52
  args = parser.parse_args()
53
- run_server(args.host, args.port, args.reload)
 
1
  """
2
+ API服务器启动器
 
 
3
 
4
+ 提供API服务器的启动和配置功能
5
+ """
6
 
7
  import uvicorn
8
 
 
 
 
 
 
 
 
 
9
 
10
+ def launch_api_server(host: str = "0.0.0.0", port: int = 8000, reload: bool = False):
11
+ """
12
+ 启动API服务器
13
 
14
+ Args:
15
+ host (str): 服务器主机地址,默认为 "0.0.0.0"
16
+ port (int): 服务器端口,默认为 8000
17
+ reload (bool): 是否启用热重载,默认为 False
18
+ """
19
+ print(f'{"=" * 80}\n正在启动API服务器...\n{"=" * 80}')
20
+ print(f"服务器地址: http://{host}:{port}")
21
+ print(f"API文档: http://{host}:{port}/docs")
22
+ print(f"热重载: {'启用' if reload else '禁用'}")
23
+ print(f'{"=" * 80}')
 
 
24
 
25
+ # 导入并启动FastAPI应用
26
  uvicorn.run(
27
+ "voice_dialogue.api.app:app",
28
  host=host,
29
  port=port,
30
  reload=reload,
31
+ log_level="info"
 
32
  )
33
 
34
 
 
41
  parser.add_argument("--reload", action="store_true", help="启用热重载")
42
 
43
  args = parser.parse_args()
44
+ launch_api_server(args.host, args.port, args.reload)
src/voice_dialogue/cli/__init__.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 命令行界面模块
3
+
4
+ 提供命令行参数处理和相关功能
5
+ """
6
+
7
+ from .args import create_argument_parser
8
+
9
+ __all__ = ['create_argument_parser']
src/voice_dialogue/cli/args.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ 命令行参数处理模块
3
+
4
+ 提供命令行参数解析和配置功能
5
+ """
6
+
7
+ import argparse
8
+
9
+ from voice_dialogue.config.speaker_config import update_argument_parser_speaker_choices
10
+
11
+
12
+ def create_argument_parser():
13
+ """创建命令行参数解析器"""
14
+ # 动态获取可用说话人列表
15
+ available_speakers = update_argument_parser_speaker_choices()
16
+
17
+ parser = argparse.ArgumentParser(
18
+ description="VoiceDialogue - 语音对话系统",
19
+ formatter_class=argparse.RawDescriptionHelpFormatter,
20
+ epilog=f"""
21
+ 示例用法:
22
+ # 启动命令行模式(默认)
23
+ python main.py
24
+
25
+ # 启动命令行模式并指定参数
26
+ python main.py --mode cli --language zh --speaker 沈逸
27
+
28
+ # 启动API服务器
29
+ python main.py --mode api
30
+
31
+ # 启动API服务器并指定端口
32
+ python main.py --mode api --port 9000
33
+
34
+ # 启动API服务器并启用热重载(开发模式)
35
+ python main.py --mode api --port 8000 --reload
36
+
37
+ 支持的说话人:
38
+ {', '.join(available_speakers)}
39
+ """
40
+ )
41
+
42
+ # 运行模式选择
43
+ parser.add_argument(
44
+ '--mode', '-m',
45
+ choices=['cli', 'api'],
46
+ default='cli',
47
+ help='运行模式: cli=命令行模式, api=API服务器模式 (默认: cli)'
48
+ )
49
+
50
+ # 命令行模式参数
51
+ cli_group = parser.add_argument_group('命令行模式参数')
52
+ cli_group.add_argument(
53
+ '--language', '-l',
54
+ choices=['zh', 'en'],
55
+ default='zh',
56
+ help='用户语言: zh=中文, en=英文 (默认: zh)'
57
+ )
58
+ cli_group.add_argument(
59
+ '--speaker', '-s',
60
+ choices=available_speakers,
61
+ default='沈逸' if '沈逸' in available_speakers else (available_speakers[0] if available_speakers else '沈逸'),
62
+ help='TTS说话人 (默认: 沈逸)'
63
+ )
64
+
65
+ # API服务器模式参数
66
+ api_group = parser.add_argument_group('API服务器模式参数')
67
+ api_group.add_argument(
68
+ '--host',
69
+ default='0.0.0.0',
70
+ help='服务器主机地址 (默认: 0.0.0.0)'
71
+ )
72
+ api_group.add_argument(
73
+ '--port', '-p',
74
+ type=int,
75
+ default=8000,
76
+ help='服务器端口 (默认: 8000)'
77
+ )
78
+ api_group.add_argument(
79
+ '--reload',
80
+ action='store_true',
81
+ help='启用热重载(开发模式)'
82
+ )
83
+
84
+ return parser
src/voice_dialogue/config/speaker_config.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ TTS说话人配置管理
3
+
4
+ 提供说话人配置的查找、映射和管理功能
5
+ """
6
+
7
+ from voice_dialogue.services.audio.generators.models import tts_config_registry
8
+
9
+
10
+ def get_tts_config_by_speaker_name(speaker_name: str):
11
+ """
12
+ 根据说话人名称获取TTS配置
13
+
14
+ 支持中文名称和英文名称,优先匹配中文名称映射,
15
+ 如果找不到则直接使用英文名称搜索
16
+
17
+ Args:
18
+ speaker_name (str): 说话人名称
19
+
20
+ Returns:
21
+ BaseTTSConfig: TTS配置,如果找不到则返回None
22
+ """
23
+ # 中文名称到英文名称的映射(保持向后兼容)
24
+ chinese_to_english_mapping = {
25
+ '罗翔': 'Luo Xiang',
26
+ '马保国': 'Ma Baoguo',
27
+ '沈逸': 'Shen Yi',
28
+ '杨幂': 'Yang Mi',
29
+ '周杰伦': 'Zhou Jielun',
30
+ '马云': 'Ma Yun',
31
+ }
32
+
33
+ # 首先尝试中文名称映射
34
+ english_name = chinese_to_english_mapping.get(speaker_name, speaker_name)
35
+
36
+ # 获取所有可用配置
37
+ all_configs = tts_config_registry.get_all_configs()
38
+
39
+ # 搜索匹配的配置
40
+ for config in all_configs:
41
+ if config.character_name == english_name:
42
+ return config
43
+
44
+ # 如果通过映射找不到,尝试直接匹配输入的名称
45
+ if speaker_name != english_name:
46
+ for config in all_configs:
47
+ if config.character_name == speaker_name:
48
+ return config
49
+
50
+ return None
51
+
52
+
53
+ def get_available_speaker_names():
54
+ """
55
+ 获取所有可用的说话人名称列表
56
+
57
+ Returns:
58
+ list[str]: 包含中文显示名称和英文原始名称的列表
59
+ """
60
+ # 中文显示名称映射
61
+ english_to_chinese_mapping = {
62
+ 'Luo Xiang': '罗翔',
63
+ 'Ma Baoguo': '马保国',
64
+ 'Shen Yi': '沈逸',
65
+ 'Yang Mi': '杨幂',
66
+ 'Zhou Jielun': '周杰伦',
67
+ 'Ma Yun': '马云',
68
+ }
69
+
70
+ all_configs = tts_config_registry.get_all_configs()
71
+ speaker_names = []
72
+
73
+ for config in all_configs:
74
+ # 优先显示中文名称
75
+ chinese_name = english_to_chinese_mapping.get(config.character_name)
76
+ if chinese_name:
77
+ speaker_names.append(chinese_name)
78
+ else:
79
+ # 如果没有中文映射,使用英文原名
80
+ speaker_names.append(config.character_name)
81
+
82
+ return sorted(speaker_names)
83
+
84
+
85
+ def update_argument_parser_speaker_choices():
86
+ """
87
+ 动态更新命令行参数解析器中的说话人选项
88
+
89
+ Returns:
90
+ list[str]: 可用的说话人选择列表
91
+ """
92
+ return get_available_speaker_names()
src/voice_dialogue/{main.py → core/launcher.py} RENAMED
@@ -1,27 +1,25 @@
1
- import argparse
2
- import time
3
- import typing
4
- from pathlib import Path
 
5
 
6
- import uvicorn
7
 
8
- from core.constants import (
 
9
  audio_frames_queue,
10
  user_voice_queue,
11
  transcribed_text_queue,
12
  text_input_queue,
13
  audio_output_queue
14
  )
15
- from services.audio.capture import EchoCancellingAudioCapture
16
- from services.audio.generator import TTSAudioGenerator
17
- from services.audio.generators.models import tts_config_registry
18
- from services.audio.player import AudioStreamPlayer
19
- from services.speech.monitor import SpeechStateMonitor
20
- from services.speech.recognizer import ASRWorker
21
- from services.text.generator import LLMResponseGenerator
22
-
23
- HERE = Path(__file__).parent
24
- language: typing.Literal['zh', 'en'] = 'en'
25
 
26
 
27
  def launch_system(
@@ -57,14 +55,16 @@ def launch_system(
57
  Note:
58
  该函数会阻塞运行,直到系统被外部停止或发生异常
59
  """
 
60
 
61
  threads = []
62
- #
 
63
  audio_frame_probe = EchoCancellingAudioCapture(audio_frames_queue=audio_frames_queue)
64
  audio_frame_probe.start()
65
  threads.append(audio_frame_probe)
66
 
67
- #
68
  user_voice_checker = SpeechStateMonitor(
69
  audio_frame_queue=audio_frames_queue,
70
  user_voice_queue=user_voice_queue,
@@ -72,14 +72,16 @@ def launch_system(
72
  user_voice_checker.start()
73
  threads.append(user_voice_checker)
74
 
75
- #
76
  whisper_worker = ASRWorker(
77
- user_voice_queue=user_voice_queue, transcribed_text_queue=transcribed_text_queue,
 
78
  language=user_language
79
  )
80
  whisper_worker.start()
81
  threads.append(whisper_worker)
82
 
 
83
  answer_generator_worker = LLMResponseGenerator(
84
  user_question_queue=transcribed_text_queue,
85
  generated_answer_queue=text_input_queue
@@ -87,13 +89,14 @@ def launch_system(
87
  answer_generator_worker.start()
88
  threads.append(answer_generator_worker)
89
 
90
- # 动态获取TTS配置,而不是使用固定映射
91
- tts_speaker_config = _get_tts_config_by_speaker_name(speaker)
92
  if tts_speaker_config is None:
93
  # 如果找不到指定说话人,列出所有可用说话人并抛出异常
94
- available_speakers = _get_available_speaker_names()
95
  raise ValueError(f"不支持的TTS说话人: {speaker}。可用说话人: {', '.join(available_speakers)}")
96
 
 
97
  audio_generator_worker = TTSAudioGenerator(
98
  text_input_queue=text_input_queue,
99
  audio_output_queue=audio_output_queue,
@@ -102,243 +105,17 @@ def launch_system(
102
  audio_generator_worker.start()
103
  threads.append(audio_generator_worker)
104
 
 
105
  audio_playing_worker = AudioStreamPlayer(audio_playing_queue=audio_output_queue)
106
  audio_playing_worker.start()
107
  threads.append(audio_playing_worker)
108
 
 
109
  while not all([thread.is_ready for thread in threads]):
110
  time.sleep(0.1)
111
 
112
- # audio_frame_probe.start_record()
113
  print(f'{"=" * 80}\n服务启动成功\n{"=" * 80}')
 
 
114
  for thread in threads:
115
  thread.join()
116
-
117
-
118
- def _get_tts_config_by_speaker_name(speaker_name: str):
119
- """
120
- 根据说话人名称获取TTS配置
121
-
122
- 支持中文名称和英文名称,优先匹配中文名称映射,
123
- 如果找不到则直接使用英文名称搜索
124
-
125
- Args:
126
- speaker_name (str): 说话人名称
127
-
128
- Returns:
129
- BaseTTSConfig: TTS配置,如果找不到则返回None
130
- """
131
- # 中文名称到英文名称的映射(保持向后兼容)
132
- chinese_to_english_mapping = {
133
- '罗翔': 'Luo Xiang',
134
- '马保国': 'Ma Baoguo',
135
- '沈逸': 'Shen Yi',
136
- '杨幂': 'Yang Mi',
137
- '周杰伦': 'Zhou Jielun',
138
- '马云': 'Ma Yun',
139
- }
140
-
141
- # 首先尝试中文名称映射
142
- english_name = chinese_to_english_mapping.get(speaker_name, speaker_name)
143
-
144
- # 获取所有可用配置
145
- all_configs = tts_config_registry.get_all_configs()
146
-
147
- # 搜索匹配的配��
148
- for config in all_configs:
149
- if config.character_name == english_name:
150
- return config
151
-
152
- # 如果通过映射找不到,尝试直接匹配输入的名称
153
- if speaker_name != english_name:
154
- for config in all_configs:
155
- if config.character_name == speaker_name:
156
- return config
157
-
158
- return None
159
-
160
-
161
- def _get_available_speaker_names():
162
- """
163
- 获取所有可用的说话人名称列表
164
-
165
- Returns:
166
- list[str]: 包含中文显示名称和英文原始名称的列表
167
- """
168
- # 中文显示名称映射
169
- english_to_chinese_mapping = {
170
- 'Luo Xiang': '罗翔',
171
- 'Ma Baoguo': '马保国',
172
- 'Shen Yi': '沈逸',
173
- 'Yang Mi': '杨幂',
174
- 'Zhou Jielun': '周杰伦',
175
- 'Ma Yun': '马云',
176
- }
177
-
178
- all_configs = tts_config_registry.get_all_configs()
179
- speaker_names = []
180
-
181
- for config in all_configs:
182
- # 优先显示中文名称
183
- chinese_name = english_to_chinese_mapping.get(config.character_name)
184
- if chinese_name:
185
- speaker_names.append(chinese_name)
186
- else:
187
- # 如果没有中文映射,使用英文原名
188
- speaker_names.append(config.character_name)
189
-
190
- return sorted(speaker_names)
191
-
192
-
193
- def _update_argument_parser_speaker_choices():
194
- """
195
- 动态更新命令行参数解析器中的说话人选项
196
-
197
- Returns:
198
- list[str]: 可用的说话人选择列表
199
- """
200
- return _get_available_speaker_names()
201
-
202
-
203
- def create_argument_parser():
204
- """创建命令行参数解析器"""
205
- # 动态获取可用说话人列表
206
- available_speakers = _update_argument_parser_speaker_choices()
207
-
208
- parser = argparse.ArgumentParser(
209
- description="VoiceDialogue - 语音对话系统",
210
- formatter_class=argparse.RawDescriptionHelpFormatter,
211
- epilog=f"""
212
- 示例用法:
213
- # 启动命令行模式(默认)
214
- python main.py
215
-
216
- # 启动命令行模式并指定参数
217
- python main.py --mode cli --language zh --speaker 沈逸
218
-
219
- # 启动API服务器
220
- python main.py --mode api
221
-
222
- # 启动API服务器并指定端口
223
- python main.py --mode api --port 9000
224
-
225
- # 启动API服务器并启用热重载(开发模式)
226
- python main.py --mode api --port 8000 --reload
227
-
228
- 支持的说话人:
229
- {', '.join(available_speakers)}
230
- """
231
- )
232
-
233
- # 运行模式选择
234
- parser.add_argument(
235
- '--mode', '-m',
236
- choices=['cli', 'api'],
237
- default='cli',
238
- help='运行模式: cli=命令行模式, api=API服务器模式 (默认: cli)'
239
- )
240
-
241
- # 命令行模式参数
242
- cli_group = parser.add_argument_group('命令行模式参数')
243
- cli_group.add_argument(
244
- '--language', '-l',
245
- choices=['zh', 'en'],
246
- default='zh',
247
- help='用户语言: zh=中文, en=英文 (默认: zh)'
248
- )
249
- cli_group.add_argument(
250
- '--speaker', '-s',
251
- choices=available_speakers,
252
- default='沈逸' if '沈逸' in available_speakers else (available_speakers[0] if available_speakers else '沈逸'),
253
- help='TTS说话人 (默认: 沈逸)'
254
- )
255
-
256
- # API服务器模式参数
257
- api_group = parser.add_argument_group('API服务器模式参数')
258
- api_group.add_argument(
259
- '--host',
260
- default='0.0.0.0',
261
- help='服务器主机地址 (默认: 0.0.0.0)'
262
- )
263
- api_group.add_argument(
264
- '--port', '-p',
265
- type=int,
266
- default=8000,
267
- help='服务器端口 (默认: 8000)'
268
- )
269
- api_group.add_argument(
270
- '--reload',
271
- action='store_true',
272
- help='启用热重载(开发模式)'
273
- )
274
-
275
- return parser
276
-
277
-
278
- def launch_api_server(host: str = "0.0.0.0", port: int = 8000, reload: bool = False):
279
- """
280
- 启动API服务器
281
-
282
- Args:
283
- host (str): 服务器主机地址,默认为 "0.0.0.0"
284
- port (int): 服务器端口,默认为 8000
285
- reload (bool): 是否启用热重载,默认为 False
286
- """
287
- print(f'{"=" * 80}\n正在启动API服务器...\n{"=" * 80}')
288
- print(f"服务器地址: http://{host}:{port}")
289
- print(f"API文档: http://{host}:{port}/docs")
290
- print(f"热重载: {'启用' if reload else '禁用'}")
291
- print(f'{"=" * 80}')
292
-
293
- # 导入并启动FastAPI应用
294
- uvicorn.run(
295
- "api.app:app",
296
- host=host,
297
- port=port,
298
- reload=reload,
299
- log_level="info"
300
- )
301
-
302
-
303
- def main():
304
- """
305
- 主程序入口函数
306
-
307
- 根据命令行参数选择启动模式:
308
- - cli: 启动命令行语音对话系统
309
- - api: 启动HTTP API服务器
310
- """
311
- parser = create_argument_parser()
312
- args = parser.parse_args()
313
-
314
- print(f"""
315
- {"=" * 80}
316
- VoiceDialogue - 语音对话系统
317
- {"=" * 80}
318
- 运行模式: {args.mode.upper()}
319
- {"=" * 80}
320
- """)
321
-
322
- try:
323
- if args.mode == 'cli':
324
- print(f"语言设置: {args.language}")
325
- print(f"说话人: {args.speaker}")
326
- print("正在启动命令行语音对话系统...")
327
- launch_system(args.language, args.speaker)
328
-
329
- elif args.mode == 'api':
330
- launch_api_server(
331
- host=args.host,
332
- port=args.port,
333
- reload=args.reload
334
- )
335
-
336
- except KeyboardInterrupt:
337
- print("\n程序被用户中断")
338
- except Exception as e:
339
- print(f"程序运行出错: {e}")
340
- raise
341
-
342
-
343
- if __name__ == '__main__':
344
- main()
 
1
+ """
2
+ 语音对话系统启动器
3
+
4
+ 负责启动和协调语音对话系统的所有组件
5
+ """
6
 
7
+ import time
8
 
9
+ from voice_dialogue.config.speaker_config import get_tts_config_by_speaker_name, get_available_speaker_names
10
+ from voice_dialogue.core.constants import (
11
  audio_frames_queue,
12
  user_voice_queue,
13
  transcribed_text_queue,
14
  text_input_queue,
15
  audio_output_queue
16
  )
17
+ from voice_dialogue.services.audio.capture import EchoCancellingAudioCapture
18
+ from voice_dialogue.services.audio.generator import TTSAudioGenerator
19
+ from voice_dialogue.services.audio.player import AudioStreamPlayer
20
+ from voice_dialogue.services.speech.monitor import SpeechStateMonitor
21
+ from voice_dialogue.services.speech.recognizer import ASRWorker
22
+ from voice_dialogue.services.text.generator import LLMResponseGenerator
 
 
 
 
23
 
24
 
25
  def launch_system(
 
55
  Note:
56
  该函数会阻塞运行,直到系统被外部停止或发生异常
57
  """
58
+ # 导入speaker配置相关功能
59
 
60
  threads = []
61
+
62
+ # 音频采集
63
  audio_frame_probe = EchoCancellingAudioCapture(audio_frames_queue=audio_frames_queue)
64
  audio_frame_probe.start()
65
  threads.append(audio_frame_probe)
66
 
67
+ # 语音状态监测
68
  user_voice_checker = SpeechStateMonitor(
69
  audio_frame_queue=audio_frames_queue,
70
  user_voice_queue=user_voice_queue,
 
72
  user_voice_checker.start()
73
  threads.append(user_voice_checker)
74
 
75
+ # 语音识别
76
  whisper_worker = ASRWorker(
77
+ user_voice_queue=user_voice_queue,
78
+ transcribed_text_queue=transcribed_text_queue,
79
  language=user_language
80
  )
81
  whisper_worker.start()
82
  threads.append(whisper_worker)
83
 
84
+ # 文本生成
85
  answer_generator_worker = LLMResponseGenerator(
86
  user_question_queue=transcribed_text_queue,
87
  generated_answer_queue=text_input_queue
 
89
  answer_generator_worker.start()
90
  threads.append(answer_generator_worker)
91
 
92
+ # 动态获取TTS配置
93
+ tts_speaker_config = get_tts_config_by_speaker_name(speaker)
94
  if tts_speaker_config is None:
95
  # 如果找不到指定说话人,列出所有可用说话人并抛出异常
96
+ available_speakers = get_available_speaker_names()
97
  raise ValueError(f"不支持的TTS说话人: {speaker}。可用说话人: {', '.join(available_speakers)}")
98
 
99
+ # 语音合成
100
  audio_generator_worker = TTSAudioGenerator(
101
  text_input_queue=text_input_queue,
102
  audio_output_queue=audio_output_queue,
 
105
  audio_generator_worker.start()
106
  threads.append(audio_generator_worker)
107
 
108
+ # 音频播放
109
  audio_playing_worker = AudioStreamPlayer(audio_playing_queue=audio_output_queue)
110
  audio_playing_worker.start()
111
  threads.append(audio_playing_worker)
112
 
113
+ # 等待所有线程准备就绪
114
  while not all([thread.is_ready for thread in threads]):
115
  time.sleep(0.1)
116
 
 
117
  print(f'{"=" * 80}\n服务启动成功\n{"=" * 80}')
118
+
119
+ # 等待所有线程结束
120
  for thread in threads:
121
  thread.join()