添加TTS插件

This commit is contained in:
John Smith 2024-02-27 23:18:46 +08:00
parent 263d4d8e65
commit 5761ae44f2
6 changed files with 368 additions and 0 deletions

2
blcsdk/requirements.txt Normal file
View File

@ -0,0 +1,2 @@
aiohttp~=3.9.0
pyinstaller~=5.13.2

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
import os
from typing import *
BASE_PATH = os.path.realpath(os.getcwd())
LOG_PATH = os.path.join(BASE_PATH, 'log')
_config: Optional['AppConfig'] = None
def init():
global _config
_config = AppConfig()
# TODO 读配置文件
def get_config():
return _config
class AppConfig:
def __init__(self):
self.tts_voice_id: Optional[str] = None
self.tts_rate = 250
self.tts_volume = 1.0
self.max_tts_queue_size = 5
self.template_text = '{author_name} 说:{content}'
self.template_free_gift = '{author_name} 赠送了{num}{gift_name},总价{total_coin}银瓜子'
self.template_paid_gift = '{author_name} 赠送了{num}{gift_name},总价{price}'
self.template_member = '{author_name} 购买了{num}{unit} {guard_name}'
self.template_super_chat = '{author_name} 发送了{price}元的醒目留言:{content}'

View File

@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
import __main__
import logging
import os
import sys
from typing import *
import blcsdk
import blcsdk.models as sdk_models
import config
import tts
logger = logging.getLogger('text-to-speech.' + __name__)
_msg_handler: Optional['MsgHandler'] = None
def init():
global _msg_handler
_msg_handler = MsgHandler()
blcsdk.set_msg_handler(_msg_handler)
def shut_down():
blcsdk.set_msg_handler(None)
class MsgHandler(blcsdk.BaseHandler):
def on_client_stopped(self, client: blcsdk.BlcPluginClient, exception: Optional[Exception]):
logger.info('blivechat disconnected')
__main__.start_shut_down()
def _on_open_plugin_admin_ui(
self, client: blcsdk.BlcPluginClient, message: sdk_models.OpenPluginAdminUiMsg, extra: sdk_models.ExtraData
):
if sys.platform == 'win32':
# TODO 浏览配置文件
os.startfile(config.LOG_PATH)
else:
logger.info('Log path is "%s"', config.LOG_PATH)
def _on_add_text(self, client: blcsdk.BlcPluginClient, message: sdk_models.AddTextMsg, extra: sdk_models.ExtraData):
if extra.is_from_plugin:
return
cfg = config.get_config()
if cfg.template_text == '':
return
text = cfg.template_text.format(
author_name=message.author_name,
content=message.content,
)
tts.say(text)
def _on_add_gift(self, client: blcsdk.BlcPluginClient, message: sdk_models.AddGiftMsg, extra: sdk_models.ExtraData):
if extra.is_from_plugin:
return
cfg = config.get_config()
is_paid_gift = message.total_coin != 0
template = cfg.template_paid_gift if is_paid_gift else cfg.template_free_gift
if template == '':
return
text = template.format(
author_name=message.author_name,
num=message.num,
gift_name=message.gift_name,
price=message.total_coin / 1000,
total_coin=message.total_coin if is_paid_gift else message.total_free_coin,
)
tts.say(text, tts.Priority.HIGH if is_paid_gift else tts.Priority.NORMAL)
def _on_add_member(
self, client: blcsdk.BlcPluginClient, message: sdk_models.AddMemberMsg, extra: sdk_models.ExtraData
):
if extra.is_from_plugin:
return
cfg = config.get_config()
if cfg.template_member == '':
return
if message.privilege_type == sdk_models.GuardLevel.LV1:
guard_name = '舰长'
elif message.privilege_type == sdk_models.GuardLevel.LV2:
guard_name = '提督'
elif message.privilege_type == sdk_models.GuardLevel.LV3:
guard_name = '总督'
else:
guard_name = '未知舰队等级'
text = cfg.template_member.format(
author_name=message.author_name,
num=message.num,
unit=message.unit,
guard_name=guard_name,
)
tts.say(text, tts.Priority.HIGH)
def _on_add_super_chat(
self, client: blcsdk.BlcPluginClient, message: sdk_models.AddSuperChatMsg, extra: sdk_models.ExtraData
):
if extra.is_from_plugin:
return
cfg = config.get_config()
if cfg.template_super_chat == '':
return
text = cfg.template_super_chat.format(
author_name=message.author_name,
price=message.price,
content=message.content,
)
tts.say(text, tts.Priority.HIGH)

View File

@ -0,0 +1,92 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import asyncio
import logging.handlers
import os
import signal
import sys
from typing import *
import blcsdk
import config
import listener
import tts
logger = logging.getLogger('text-to-speech')
shut_down_event: Optional[asyncio.Event] = None
async def main():
try:
if not await init():
return 1
await run()
finally:
await shut_down()
return 0
async def init():
init_signal_handlers()
init_logging()
config.init()
await blcsdk.init()
if not blcsdk.is_sdk_version_compatible():
raise RuntimeError('SDK version is not compatible')
if not tts.init():
return False
listener.init()
return True
def init_signal_handlers():
global shut_down_event
shut_down_event = asyncio.Event()
signums = (signal.SIGINT, signal.SIGTERM)
try:
loop = asyncio.get_running_loop()
for signum in signums:
loop.add_signal_handler(signum, start_shut_down)
except NotImplementedError:
# 不太安全但Windows只能用这个
for signum in signums:
signal.signal(signum, start_shut_down)
def start_shut_down(*_args):
shut_down_event.set()
def init_logging():
filename = os.path.join(config.LOG_PATH, 'text-to-speech.log')
stream_handler = logging.StreamHandler()
file_handler = logging.handlers.TimedRotatingFileHandler(
filename, encoding='utf-8', when='midnight', backupCount=7, delay=True
)
logging.basicConfig(
format='{asctime} {levelname} [{name}]: {message}',
style='{',
level=logging.INFO,
# level=logging.DEBUG,
handlers=[stream_handler, file_handler],
)
async def run():
logger.info('Running event loop')
await shut_down_event.wait()
logger.info('Start to shut down')
async def shut_down():
listener.shut_down()
await blcsdk.shut_down()
if __name__ == '__main__':
sys.exit(asyncio.run(main()))

View File

@ -0,0 +1 @@
pyttsx3==2.90

View File

@ -0,0 +1,127 @@
# -*- 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()