feat: Bilibili Live Danmaku -> Yukkuri TTS Reader
- Python main entry with CLI (--room-id, --voice, --speed) - blivedm integration for real-time danmaku receiving - Chinese-to-Japanese katakana conversion (pypinyin + mapping table) - Node.js persistent bridge for aquestalk.js TTS synthesis - Audio playback via sounddevice - Message queue for sequential TTS playback
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""Bilibili Live Danmaku Handler.
|
||||
|
||||
Connects to a Bilibili live room and puts incoming danmaku messages into an asyncio queue.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import blivedm
|
||||
from blivedm.models.web import DanmakuMessage
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DANMAKU_FORMAT = "{uname}: {msg}"
|
||||
|
||||
|
||||
class DanmakuHandler(blivedm.BaseHandler):
|
||||
"""Handles incoming danmaku from a Bilibili live room."""
|
||||
|
||||
def __init__(self, queue: asyncio.Queue, message_format: str = DANMAKU_FORMAT):
|
||||
super().__init__()
|
||||
self._queue = queue
|
||||
self._message_format = message_format
|
||||
|
||||
def _on_danmaku(self, client: blivedm.BLiveClient, message: DanmakuMessage) -> None:
|
||||
if not message.msg.strip():
|
||||
return
|
||||
text = self._message_format.format(
|
||||
uname=message.uname,
|
||||
msg=message.msg,
|
||||
uid=message.uid,
|
||||
)
|
||||
logger.info("Danmaku: %s", text)
|
||||
try:
|
||||
self._queue.put_nowait(text)
|
||||
except asyncio.QueueFull:
|
||||
logger.warning("Danmaku queue full, dropping: %s", text)
|
||||
|
||||
def _on_gift(self, client: blivedm.BLiveClient, message) -> None:
|
||||
logger.debug("Gift: %s x%d from %s", message.gift_name, message.num, message.uname)
|
||||
|
||||
def _on_interact_word_v2(self, client: blivedm.BLiveClient, message) -> None:
|
||||
logger.debug("Interact: %s (type=%d)", message.uname, message.msg_type)
|
||||
|
||||
def on_client_stopped(self, client, exception: Optional[Exception]) -> None:
|
||||
if exception is not None:
|
||||
logger.error("Client stopped with exception: %s", exception)
|
||||
else:
|
||||
logger.info("Client stopped normally.")
|
||||
|
||||
|
||||
class DanmakuClient:
|
||||
"""Manages the BLiveClient connection for a single room."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
room_id: int,
|
||||
queue: asyncio.Queue,
|
||||
sessdata: str = "",
|
||||
message_format: str = DANMAKU_FORMAT,
|
||||
):
|
||||
self._room_id = room_id
|
||||
self._queue = queue
|
||||
self._sessdata = sessdata
|
||||
self._message_format = message_format
|
||||
self._client: Optional[blivedm.BLiveClient] = None
|
||||
|
||||
async def start(self) -> None:
|
||||
"""Connect to the live room and start receiving danmaku."""
|
||||
import http.cookies
|
||||
import aiohttp
|
||||
|
||||
session = aiohttp.ClientSession()
|
||||
if self._sessdata:
|
||||
cookies = http.cookies.SimpleCookie()
|
||||
cookies["SESSDATA"] = self._sessdata
|
||||
cookies["SESSDATA"]["domain"] = "bilibili.com"
|
||||
session.cookie_jar.update_cookies(cookies)
|
||||
|
||||
self._client = blivedm.BLiveClient(
|
||||
self._room_id,
|
||||
session=session,
|
||||
)
|
||||
|
||||
handler = DanmakuHandler(self._queue, self._message_format)
|
||||
self._client.set_handler(handler)
|
||||
self._client.start()
|
||||
|
||||
logger.info("Connected to room %d", self._room_id)
|
||||
|
||||
async def stop(self) -> None:
|
||||
"""Stop the client and clean up."""
|
||||
if self._client is not None:
|
||||
logger.info("Disconnecting from room %d...", self._room_id)
|
||||
await self._client.stop_and_close()
|
||||
self._client = None
|
||||
|
||||
@property
|
||||
def room_id(self) -> int:
|
||||
return self._room_id
|
||||
Reference in New Issue
Block a user