| | |
| | | ) |
| | | |
| | | def handle_opus(self, opus_data: bytes): |
| | | self.last_stream_had_audio = True |
| | | if not getattr(self, "_logged_first_opus", False): |
| | | self._logged_first_opus = True |
| | | logger.bind(tag=TAG).info(f"首帧opus已入队 bytes={len(opus_data)}") |
| | | logger.bind(tag=TAG).debug(f"推送数据到队列里面帧数~~ {len(opus_data)}") |
| | | self.tts_audio_queue.put((SentenceType.MIDDLE, opus_data, None, getattr(self, 'current_sentence_id', None))) |
| | | |
| | | def _run_text_to_speak(self, text, output_file=None): |
| | | """Generate TTS with a hard timeout so LAST/stop is not blocked forever. |
| | | |
| | | Edge TTS (and some HTTP TTS APIs) can hang past asyncio.wait_for cancel. |
| | | Run synthesis in a daemon thread and abandon it if the join times out. |
| | | """ |
| | | result = {"data": None, "error": None} |
| | | |
| | | def worker(): |
| | | try: |
| | | result["data"] = asyncio.run(self.text_to_speak(text, output_file)) |
| | | except Exception as e: |
| | | result["error"] = e |
| | | |
| | | worker_thread = threading.Thread(target=worker, name="tts-synth", daemon=True) |
| | | worker_thread.start() |
| | | worker_thread.join(timeout=self.tts_timeout) |
| | | if worker_thread.is_alive(): |
| | | logger.bind(tag=TAG).error( |
| | | f"TTS timed out after {self.tts_timeout}s: {text}" |
| | | ) |
| | | raise TimeoutError(f"TTS timed out after {self.tts_timeout}s") |
| | | if result["error"] is not None: |
| | | raise result["error"] |
| | | return result["data"] |
| | | |
| | | def handle_audio_file(self, file_audio: bytes, text): |
| | | self.before_stop_play_files.append((file_audio, text)) |
| | |
| | | # 使用正则一次性替换,避免重复遍历和部分匹配问题 |
| | | if self._correct_words_pattern: |
| | | text = self._correct_words_pattern.sub(lambda m: self.correct_words[m.group(0)], text) |
| | | max_repeat_time = 5 |
| | | # 先合成 opus,再下发 sentence_start。先发字幕再合成时,设备会进入说话中却收不到音频。 |
| | | max_repeat_time = 2 |
| | | opus_frames = {"n": 0} |
| | | pending_opus = [] |
| | | self.last_stream_had_audio = False |
| | | |
| | | def _count_opus(data): |
| | | opus_frames["n"] += 1 |
| | | pending_opus.append(bytes(data) if not isinstance(data, bytes) else data) |
| | | |
| | | def _flush_ready_audio(): |
| | | sid = getattr(self, "current_sentence_id", None) |
| | | self.tts_audio_queue.put((SentenceType.FIRST, None, original_text, sid)) |
| | | for frame in pending_opus: |
| | | if opus_handler: |
| | | opus_handler(frame) |
| | | else: |
| | | self.tts_audio_queue.put((SentenceType.MIDDLE, frame, None, sid)) |
| | | self.last_stream_had_audio = True |
| | | pending_opus.clear() |
| | | |
| | | counted_handler = _count_opus |
| | | if self.delete_audio_file: |
| | | # 需要删除文件的直接转为音频数据 |
| | | audio_bytes = None |
| | | while max_repeat_time > 0: |
| | | try: |
| | | audio_bytes = asyncio.run(self.text_to_speak(text, None)) |
| | | audio_bytes = self._run_text_to_speak(text, None) |
| | | if audio_bytes: |
| | | # 使用原始文本用于显示/上报 |
| | | self.tts_audio_queue.put((SentenceType.FIRST, None, original_text, getattr(self, 'current_sentence_id', None))) |
| | | encode_rate = getattr(self.opus_encoder, "sample_rate", None) or self.conn.sample_rate |
| | | audio_bytes_to_data_stream( |
| | | audio_bytes, |
| | | file_type=self.audio_file_type, |
| | | is_opus=True, |
| | | callback=opus_handler, |
| | | sample_rate=self.conn.sample_rate, |
| | | callback=counted_handler, |
| | | sample_rate=encode_rate, |
| | | opus_encoder=self.opus_encoder, |
| | | ) |
| | | break |
| | |
| | | f"语音生成失败{5 - max_repeat_time + 1}次: {original_text},错误: {e}" |
| | | ) |
| | | max_repeat_time -= 1 |
| | | if max_repeat_time > 0: |
| | | if max_repeat_time > 0 and opus_frames["n"] > 0: |
| | | logger.bind(tag=TAG).info( |
| | | f"语音生成成功: {original_text},重试{5 - max_repeat_time}次" |
| | | f"语音生成成功: {original_text},opus帧={opus_frames['n']},重试{5 - max_repeat_time}次" |
| | | ) |
| | | _flush_ready_audio() |
| | | elif max_repeat_time > 0: |
| | | logger.bind(tag=TAG).error( |
| | | f"语音生成无opus帧: {original_text},bytes={0 if not audio_bytes else len(audio_bytes)}" |
| | | ) |
| | | else: |
| | | logger.bind(tag=TAG).error( |
| | |
| | | try: |
| | | while not os.path.exists(tmp_file) and max_repeat_time > 0: |
| | | try: |
| | | asyncio.run(self.text_to_speak(text, tmp_file)) |
| | | self._run_text_to_speak(text, tmp_file) |
| | | except Exception as e: |
| | | logger.bind(tag=TAG).warning( |
| | | f"语音生成失败{5 - max_repeat_time + 1}次: {original_text},错误: {e}" |
| | |
| | | logger.bind(tag=TAG).info( |
| | | f"语音生成成功: {original_text}:{tmp_file},重试{5 - max_repeat_time}次" |
| | | ) |
| | | self._process_audio_file_stream(tmp_file, callback=counted_handler) |
| | | if opus_frames["n"] > 0: |
| | | _flush_ready_audio() |
| | | else: |
| | | logger.bind(tag=TAG).error( |
| | | f"语音生成无opus帧: {original_text}:{tmp_file}" |
| | | ) |
| | | else: |
| | | logger.bind(tag=TAG).error( |
| | | f"语音生成失败: {original_text},请检查网络或服务是否正常" |
| | | ) |
| | | self.tts_audio_queue.put((SentenceType.FIRST, None, original_text, getattr(self, 'current_sentence_id', None))) |
| | | self._process_audio_file_stream(tmp_file, callback=opus_handler) |
| | | except Exception as e: |
| | | logger.bind(tag=TAG).error(f"Failed to generate TTS file: {e}") |
| | | return None |
| | |
| | | # 需要删除文件的直接转为音频数据 |
| | | while max_repeat_time > 0: |
| | | try: |
| | | audio_bytes = asyncio.run(self.text_to_speak(text, None)) |
| | | audio_bytes = self._run_text_to_speak(text, None) |
| | | if audio_bytes: |
| | | audio_datas = [] |
| | | audio_bytes_to_data_stream( |
| | |
| | | try: |
| | | while not os.path.exists(tmp_file) and max_repeat_time > 0: |
| | | try: |
| | | asyncio.run(self.text_to_speak(text, tmp_file)) |
| | | self._run_text_to_speak(text, tmp_file) |
| | | except Exception as e: |
| | | logger.bind(tag=TAG).warning( |
| | | f"语音生成失败{5 - max_repeat_time + 1}次: {original_text},错误: {e}" |
| | |
| | | |
| | | async def open_audio_channels(self, conn): |
| | | self.conn = conn |
| | | if not hasattr(self, "_open_lock"): |
| | | self._open_lock = threading.Lock() |
| | | |
| | | # 根据conn的sample_rate创建编码器,如果子类已经创建则不覆盖(IndexTTS接口返回为24kHZ-待重采样处理) |
| | | if not hasattr(self, 'opus_encoder') or self.opus_encoder is None: |
| | | self.opus_encoder = opus_encoder_utils.OpusEncoderUtils( |
| | | sample_rate=conn.sample_rate, channels=1, frame_size_ms=60 |
| | | with self._open_lock: |
| | | encoder = getattr(self, "opus_encoder", None) |
| | | encoder_rate = getattr(encoder, "sample_rate", None) if encoder is not None else None |
| | | if encoder is None or encoder_rate != conn.sample_rate: |
| | | self.opus_encoder = opus_encoder_utils.OpusEncoderUtils( |
| | | sample_rate=conn.sample_rate, channels=1, frame_size_ms=60 |
| | | ) |
| | | |
| | | if getattr(self, "_channels_opened", False): |
| | | play = getattr(self, "audio_play_priority_thread", None) |
| | | text_th = getattr(self, "tts_priority_thread", None) |
| | | if play and play.is_alive() and text_th and text_th.is_alive(): |
| | | return |
| | | |
| | | self.tts_priority_thread = threading.Thread( |
| | | target=self.tts_text_priority_thread, daemon=True |
| | | ) |
| | | |
| | | # tts 消化线程 |
| | | self.tts_priority_thread = threading.Thread( |
| | | target=self.tts_text_priority_thread, daemon=True |
| | | ) |
| | | self.tts_priority_thread.start() |
| | | |
| | | # 音频播放 消化线程 |
| | | self.audio_play_priority_thread = threading.Thread( |
| | | target=self._audio_play_priority_thread, daemon=True |
| | | ) |
| | | self.audio_play_priority_thread.start() |
| | | self.tts_priority_thread.start() |
| | | self.audio_play_priority_thread = threading.Thread( |
| | | target=self._audio_play_priority_thread, daemon=True |
| | | ) |
| | | self.audio_play_priority_thread.start() |
| | | self._channels_opened = True |
| | | |
| | | def store_tts_text(self, sentence_id, text): |
| | | """存储指定 sentence_id 对应的文本,用于流式TTS获取正确的字幕文本 |
| | |
| | | self.tts_text_buff = [] |
| | | self.is_first_sentence = True |
| | | self.tts_audio_first_sentence = True |
| | | self._logged_first_opus = False |
| | | self.last_stream_had_audio = False |
| | | elif ContentType.TEXT == message.content_type: |
| | | self.tts_text_buff.append(message.content_detail) |
| | | segment_text = self._get_segment_text() |
| | | if segment_text: |
| | | self.to_tts_stream(segment_text, opus_handler=self.handle_opus) |
| | | try: |
| | | self.to_tts_stream(segment_text, opus_handler=self.handle_opus) |
| | | except Exception as e: |
| | | logger.bind(tag=TAG).error(f"TTS分段合成失败: {e}") |
| | | elif ContentType.FILE == message.content_type: |
| | | self._process_remaining_text_stream(opus_handler=self.handle_opus) |
| | | tts_file = message.content_file |
| | |
| | | tts_file, callback=self.handle_opus |
| | | ) |
| | | if message.sentence_type == SentenceType.LAST: |
| | | self._process_remaining_text_stream(opus_handler=self.handle_opus) |
| | | self.tts_audio_queue.put( |
| | | (message.sentence_type, [], message.content_detail, message.sentence_id) |
| | | ) |
| | | try: |
| | | self._process_remaining_text_stream(opus_handler=self.handle_opus) |
| | | except Exception as e: |
| | | logger.bind(tag=TAG).error( |
| | | f"处理剩余TTS文本失败: {e}", |
| | | ) |
| | | if getattr(self, "last_stream_had_audio", False): |
| | | self.tts_audio_queue.put( |
| | | (message.sentence_type, [], message.content_detail, message.sentence_id) |
| | | ) |
| | | else: |
| | | logger.bind(tag=TAG).error("跳过TTS stop:本句没有opus音频") |
| | | |
| | | except queue.Empty: |
| | | continue |
| | |
| | | sendAudioMessage(self.conn, sentence_type, audio_datas, text, sentence_id), |
| | | self.conn.loop, |
| | | ) |
| | | future.result() |
| | | future.result(timeout=20) |
| | | |
| | | # 记录输出和报告 |
| | | if self.conn.max_output_size > 0 and text: |