blivechat/plugins/text-to-speech/tts.py
2024-02-27 23:18:46 +08:00

128 lines
3.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
import dataclasses
import enum
import logging
import queue
import threading
from typing import *
import pyttsx3.voice
import config
logger = logging.getLogger('text-to-speech.' + __name__)
_tts: Optional['Tts'] = None
class Priority(enum.IntEnum):
HIGH = 0
NORMAL = 1
@dataclasses.dataclass
class TtsTask:
priority: Priority
text: str
def init():
global _tts
_tts = Tts()
return _tts.init()
def say(text, priority: Priority = Priority.NORMAL):
logger.debug('%s', text)
task = TtsTask(priority=priority, text=text)
res = _tts.push_task(task)
if not res:
if task.priority == Priority.HIGH:
logger.info('Dropped high priority task: %s', task.text)
else:
logger.debug('Dropped task: %s', task.text)
return res
class Tts:
def __init__(self):
self._worker_thread = threading.Thread(target=self._worker_thread_func, daemon=True)
# COM组件必须在使用它的线程里初始化否则使用时会有问题
self._engine: Optional[pyttsx3.Engine] = None
self._thread_init_event = threading.Event()
cfg = config.get_config()
self._task_queues: List[queue.Queue['TtsTask']] = [
queue.Queue(cfg.max_tts_queue_size) for _ in range(len(Priority))
]
"""任务队列,索引是优先级"""
def init(self):
self._worker_thread.start()
res = self._thread_init_event.wait(10)
if not res:
logger.error('Initializing TTS engine timed out')
return res
def _init_in_worker_thread(self):
logger.info('Initializing TTS engine')
self._engine = pyttsx3.init()
voices = cast(List[pyttsx3.voice.Voice], self._engine.getProperty('voices'))
logger.info('Available voices:\n%s', '\n'.join(map(str, voices)))
cfg = config.get_config()
if cfg.tts_voice_id is not None:
self._engine.setProperty('voice', cfg.tts_voice_id)
self._engine.setProperty('rate', cfg.tts_rate)
self._engine.setProperty('volume', cfg.tts_volume)
self._thread_init_event.set()
# TODO 自己实现队列,合并礼物消息
def push_task(self, task: TtsTask):
q = self._task_queues[task.priority]
try:
q.put_nowait(task)
return True
except queue.Full:
pass
if task.priority != Priority.HIGH:
return False
# 高优先级的尝试降级,挤掉低优先级的任务
q = self._task_queues[Priority.NORMAL]
while True:
try:
q.put_nowait(task)
break
except queue.Full:
try:
task = q.get_nowait()
if task.priority == Priority.HIGH:
logger.info('Dropped high priority task: %s', task.text)
else:
logger.debug('Dropped task: %s', task.text)
except queue.Empty:
pass
return True
def _pop_task(self) -> TtsTask:
while True:
# 按优先级遍历,轮询等待任务
for q in self._task_queues:
try:
return q.get(timeout=0.1)
except queue.Empty:
pass
def _worker_thread_func(self):
self._init_in_worker_thread()
logger.info('Running TTS worker')
while True:
task = self._pop_task()
self._engine.say(task.text)
self._engine.runAndWait()