mirror of
https://github.com/xfgryujk/blivechat.git
synced 2024-12-25 20:30:28 +08:00
添加TTS插件
This commit is contained in:
parent
263d4d8e65
commit
5761ae44f2
2
blcsdk/requirements.txt
Normal file
2
blcsdk/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
aiohttp~=3.9.0
|
||||
pyinstaller~=5.13.2
|
33
plugins/text-to-speech/config.py
Normal file
33
plugins/text-to-speech/config.py
Normal 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}'
|
113
plugins/text-to-speech/listener.py
Normal file
113
plugins/text-to-speech/listener.py
Normal 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)
|
92
plugins/text-to-speech/main.py
Normal file
92
plugins/text-to-speech/main.py
Normal 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()))
|
1
plugins/text-to-speech/requirements.txt
Normal file
1
plugins/text-to-speech/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
pyttsx3==2.90
|
127
plugins/text-to-speech/tts.py
Normal file
127
plugins/text-to-speech/tts.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user