Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f7168a0e3 | ||
|
|
d4de39f294 | ||
|
|
d7e940fe2e | ||
|
|
22ee613352 | ||
|
|
6ae7249583 | ||
|
|
b7d0c26ad1 |
@@ -29,3 +29,6 @@ temp/
|
|||||||
|
|
||||||
# Node.js
|
# Node.js
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|
||||||
|
# Dev/testing (not packaged, not committed)
|
||||||
|
fake_room.py
|
||||||
|
|||||||
@@ -96,11 +96,10 @@ python main.py --room-id ROOM_ID [--voice VOICE] [--speed SPEED]
|
|||||||
| 事件 | 变量 |
|
| 事件 | 变量 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `danmaku` 弹幕 | `{uname}` `{msg}` |
|
| `danmaku` 弹幕 | `{uname}` `{msg}` |
|
||||||
| `gift` 普通礼物 | `{viewer_name}` `{gift_name}` `{gift_num}` `{gift_price}` `{gift_coin_type}` |
|
| `gift` 普通礼物 | `{viewer_name}` `{gift_name}` `{gift_count}` `{gift_price}` `{gift_coin_type}` |
|
||||||
| `blind_box` 盲盒礼物 | `{viewer_name}` `{gift_name}` `{gift_num}` `{blind_box_gift}` |
|
| `blind_box` 盲盒礼物 | `{viewer_name}` `{gift_name}` `{gift_count}` `{blind_box_gift}` |
|
||||||
| `enter_room` 进房 | `{viewer_name}` `{user_uid}` |
|
| `enter_room` 进房 | `{viewer_name}` `{user_uid}` |
|
||||||
| `follow` 关注 | `{viewer_name}` `{user_uid}` |
|
| `follow` 关注 | `{viewer_name}` `{user_uid}` |
|
||||||
| `like` 点赞 | `{viewer_name}` `{user_uid}` |
|
|
||||||
| `interact_other` 其他互动 | `{viewer_name}` `{user_uid}` `{interact_type}`(分享/特别关注/互粉) |
|
| `interact_other` 其他互动 | `{viewer_name}` `{user_uid}` `{interact_type}`(分享/特别关注/互粉) |
|
||||||
| `buy_guard` 上舰 | `{ships_viewer_name}` `{ships_level}`(舰长/提督/总督) `{ships_num}` `{ships_price}` |
|
| `buy_guard` 上舰 | `{ships_viewer_name}` `{ships_level}`(舰长/提督/总督) `{ships_num}` `{ships_price}` |
|
||||||
| `guard_toast` 上舰提示 | `{ships_viewer_name}` `{ships_level}` `{guard_days}` `{toast_msg}` |
|
| `guard_toast` 上舰提示 | `{ships_viewer_name}` `{ships_level}` `{guard_days}` `{toast_msg}` |
|
||||||
@@ -111,8 +110,8 @@ python main.py --room-id ROOM_ID [--voice VOICE] [--speed SPEED]
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"templates": {
|
"templates": {
|
||||||
"gift": "感谢由{viewer_name}投喂的{gift_name}",
|
"gift": "感谢由{viewer_name}投喂的{gift_count}个{gift_name}",
|
||||||
"blind_box": "感谢由{viewer_name}投喂的{gift_name}爆出了{blind_box_gift}",
|
"blind_box": "感谢由{viewer_name}投喂的{gift_count}个{gift_name}爆出了{blind_box_gift}",
|
||||||
"enter_room": "欢迎{viewer_name}进入直播间",
|
"enter_room": "欢迎{viewer_name}进入直播间",
|
||||||
"buy_guard": "恭喜{ships_viewer_name}成为尊贵的{ships_level}",
|
"buy_guard": "恭喜{ships_viewer_name}成为尊贵的{ships_level}",
|
||||||
"super_chat": "{viewer_name}的醒目留言:{super_chat_content}"
|
"super_chat": "{viewer_name}的醒目留言:{super_chat_content}"
|
||||||
|
|||||||
@@ -106,13 +106,16 @@ def main() -> int:
|
|||||||
"--hidden-import", "danmaku_handler",
|
"--hidden-import", "danmaku_handler",
|
||||||
"--hidden-import", "tts",
|
"--hidden-import", "tts",
|
||||||
"--hidden-import", "bili_login",
|
"--hidden-import", "bili_login",
|
||||||
|
"--noconfirm",
|
||||||
"--distpath", str(BUILD_DIR / "app"),
|
"--distpath", str(BUILD_DIR / "app"),
|
||||||
str(ROOT / "server.py"),
|
str(ROOT / "server.py"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
exe = BUILD_DIR / "app" / "bililive-tts" / "bililive-tts.exe"
|
exe = BUILD_DIR / "app" / "bililive-tts" / "bililive-tts.exe"
|
||||||
if not exe.exists():
|
internal = BUILD_DIR / "app" / "bililive-tts" / "_internal"
|
||||||
print(f"ERROR: PyInstaller failed, {exe} not found")
|
base_lib = internal / "base_library.zip"
|
||||||
|
if not exe.exists() or not base_lib.exists():
|
||||||
|
print(f"ERROR: PyInstaller build incomplete: exe={exe.exists()}, base_library.zip={base_lib.exists()}")
|
||||||
return 1
|
return 1
|
||||||
print(f" OK: {exe} ({exe.stat().st_size // 1024 // 1024} MB)")
|
print(f" OK: {exe} ({exe.stat().st_size // 1024 // 1024} MB)")
|
||||||
|
|
||||||
|
|||||||
+31
-15
@@ -7,6 +7,7 @@ Rules come from event_rules.json (see event_rules.py).
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import blivedm
|
import blivedm
|
||||||
@@ -20,7 +21,26 @@ from event_rules import (
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
DANMAKU_FORMAT = "{uname}\u8bf4\u3001 {msg}"
|
FAKE_ROOM_PORT = 8081 # fake_room.py default = bot web UI port (8080) + 1
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_apply_fake_room(room_id: int) -> None:
|
||||||
|
"""Connect-time routing: room_id == 1 uses the local fake_room.py (dev only).
|
||||||
|
|
||||||
|
fake_room.py is gitignored and never packaged. For any other room id this
|
||||||
|
returns immediately without ever importing fake_room, so production
|
||||||
|
(which has no fake_room.py) is completely unaffected.
|
||||||
|
"""
|
||||||
|
fake = sys.modules.get("fake_room")
|
||||||
|
if room_id == 1:
|
||||||
|
if fake is None:
|
||||||
|
import importlib
|
||||||
|
fake = importlib.import_module("fake_room")
|
||||||
|
fake.patch_blivedm(FAKE_ROOM_PORT)
|
||||||
|
logger.info("[dev] room 1 -> fake room on port %d", FAKE_ROOM_PORT)
|
||||||
|
elif fake is not None:
|
||||||
|
# previously routed to fake room; switch back to real Bilibili
|
||||||
|
fake.unpatch_blivedm()
|
||||||
|
|
||||||
INTERACT_TYPE_NAMES = {
|
INTERACT_TYPE_NAMES = {
|
||||||
3: "\u5206\u4eab", 4: "\u7279\u522b\u5173\u6ce8", 5: "\u4e92\u7c89",
|
3: "\u5206\u4eab", 4: "\u7279\u522b\u5173\u6ce8", 5: "\u4e92\u7c89",
|
||||||
@@ -32,12 +52,10 @@ class DanmakuHandler(blivedm.BaseHandler):
|
|||||||
|
|
||||||
_CMD_CALLBACK_DICT = blivedm.BaseHandler._CMD_CALLBACK_DICT.copy()
|
_CMD_CALLBACK_DICT = blivedm.BaseHandler._CMD_CALLBACK_DICT.copy()
|
||||||
|
|
||||||
def __init__(self, queue: asyncio.Queue, rules: dict | None = None,
|
def __init__(self, queue: asyncio.Queue, rules: dict | None = None):
|
||||||
message_format: str = DANMAKU_FORMAT):
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._queue = queue
|
self._queue = queue
|
||||||
self._rules = rules or dict(DEFAULT_RULES)
|
self._rules = rules or dict(DEFAULT_RULES)
|
||||||
self._message_format = message_format
|
|
||||||
self._count = 0
|
self._count = 0
|
||||||
|
|
||||||
# ── custom SEND_GIFT callback to capture blind-gift info ──────────
|
# ── custom SEND_GIFT callback to capture blind-gift info ──────────
|
||||||
@@ -62,11 +80,12 @@ class DanmakuHandler(blivedm.BaseHandler):
|
|||||||
def _on_danmaku(self, client: blivedm.BLiveClient, message: DanmakuMessage) -> None:
|
def _on_danmaku(self, client: blivedm.BLiveClient, message: DanmakuMessage) -> None:
|
||||||
if not message.msg.strip():
|
if not message.msg.strip():
|
||||||
return
|
return
|
||||||
text = self._message_format.format(
|
text = event_text(self._rules, "danmaku",
|
||||||
uname=message.uname,
|
uname=message.uname,
|
||||||
msg=message.msg,
|
msg=message.msg,
|
||||||
uid=message.uid,
|
uid=message.uid)
|
||||||
)
|
if not text:
|
||||||
|
return
|
||||||
self._count += 1
|
self._count += 1
|
||||||
if self._count % 20 == 1:
|
if self._count % 20 == 1:
|
||||||
logger.info("Danmaku #%d: %s", self._count, text)
|
logger.info("Danmaku #%d: %s", self._count, text)
|
||||||
@@ -87,12 +106,14 @@ class DanmakuHandler(blivedm.BaseHandler):
|
|||||||
viewer_name=message.uname,
|
viewer_name=message.uname,
|
||||||
gift_name=message.gift_name,
|
gift_name=message.gift_name,
|
||||||
gift_num=message.num,
|
gift_num=message.num,
|
||||||
|
gift_count=message.num,
|
||||||
blind_box_gift=blind_box_gift)
|
blind_box_gift=blind_box_gift)
|
||||||
else:
|
else:
|
||||||
text = event_text(self._rules, "gift",
|
text = event_text(self._rules, "gift",
|
||||||
viewer_name=message.uname,
|
viewer_name=message.uname,
|
||||||
gift_name=message.gift_name,
|
gift_name=message.gift_name,
|
||||||
gift_num=message.num,
|
gift_num=message.num,
|
||||||
|
gift_count=message.num,
|
||||||
gift_price=message.price,
|
gift_price=message.price,
|
||||||
gift_coin_type=message.coin_type)
|
gift_coin_type=message.coin_type)
|
||||||
logger.info("Gift: %s x%d from %s (blind=%s)",
|
logger.info("Gift: %s x%d from %s (blind=%s)",
|
||||||
@@ -133,7 +154,7 @@ class DanmakuHandler(blivedm.BaseHandler):
|
|||||||
logger.info("Super chat: %s (%d yuan): %s", message.uname, message.price, message.message)
|
logger.info("Super chat: %s (%d yuan): %s", message.uname, message.price, message.message)
|
||||||
self._put(text)
|
self._put(text)
|
||||||
|
|
||||||
# ── interact (enter room / follow / like / share) ──────────────────
|
# ── interact (enter room / follow / share) ─────────────────────────
|
||||||
def _on_interact_word_v2(self, client: blivedm.BLiveClient, message: InteractWordV2Message) -> None:
|
def _on_interact_word_v2(self, client: blivedm.BLiveClient, message: InteractWordV2Message) -> None:
|
||||||
msg_type = message.msg_type
|
msg_type = message.msg_type
|
||||||
if msg_type == 1:
|
if msg_type == 1:
|
||||||
@@ -144,10 +165,6 @@ class DanmakuHandler(blivedm.BaseHandler):
|
|||||||
text = event_text(self._rules, "follow",
|
text = event_text(self._rules, "follow",
|
||||||
viewer_name=message.username,
|
viewer_name=message.username,
|
||||||
user_uid=message.uid)
|
user_uid=message.uid)
|
||||||
elif msg_type == 6:
|
|
||||||
text = event_text(self._rules, "like",
|
|
||||||
viewer_name=message.username,
|
|
||||||
user_uid=message.uid)
|
|
||||||
else:
|
else:
|
||||||
text = event_text(self._rules, "interact_other",
|
text = event_text(self._rules, "interact_other",
|
||||||
viewer_name=message.username,
|
viewer_name=message.username,
|
||||||
@@ -173,21 +190,20 @@ class DanmakuClient:
|
|||||||
queue: asyncio.Queue,
|
queue: asyncio.Queue,
|
||||||
cookies: Optional[dict[str, str]] = None,
|
cookies: Optional[dict[str, str]] = None,
|
||||||
rules: dict | None = None,
|
rules: dict | None = None,
|
||||||
message_format: str = DANMAKU_FORMAT,
|
|
||||||
):
|
):
|
||||||
self._room_id = room_id
|
self._room_id = room_id
|
||||||
self._queue = queue
|
self._queue = queue
|
||||||
self._cookies = cookies
|
self._cookies = cookies
|
||||||
self._rules = rules or dict(DEFAULT_RULES)
|
self._rules = rules or dict(DEFAULT_RULES)
|
||||||
self._message_format = message_format
|
|
||||||
self._client: Optional[blivedm.BLiveClient] = None
|
self._client: Optional[blivedm.BLiveClient] = None
|
||||||
self._session: Optional["aiohttp.ClientSession"] = None
|
self._session: Optional["aiohttp.ClientSession"] = None
|
||||||
|
|
||||||
async def start(self) -> None:
|
async def start(self) -> None:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
_maybe_apply_fake_room(self._room_id)
|
||||||
self._session = await build_danmaku_session(self._cookies)
|
self._session = await build_danmaku_session(self._cookies)
|
||||||
self._client = blivedm.BLiveClient(self._room_id, session=self._session)
|
self._client = blivedm.BLiveClient(self._room_id, session=self._session)
|
||||||
handler = DanmakuHandler(self._queue, self._rules, self._message_format)
|
handler = DanmakuHandler(self._queue, self._rules)
|
||||||
self._client.set_handler(handler)
|
self._client.set_handler(handler)
|
||||||
self._client.start()
|
self._client.start()
|
||||||
logger.info("Connected to room %d", self._room_id)
|
logger.info("Connected to room %d", self._room_id)
|
||||||
|
|||||||
+2
-4
@@ -5,7 +5,6 @@
|
|||||||
"blind_box": true,
|
"blind_box": true,
|
||||||
"enter_room": true,
|
"enter_room": true,
|
||||||
"follow": false,
|
"follow": false,
|
||||||
"like": false,
|
|
||||||
"interact_other": false,
|
"interact_other": false,
|
||||||
"buy_guard": true,
|
"buy_guard": true,
|
||||||
"guard_toast": false,
|
"guard_toast": false,
|
||||||
@@ -13,11 +12,10 @@
|
|||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"danmaku": "{uname}说、 {msg}",
|
"danmaku": "{uname}说、 {msg}",
|
||||||
"gift": "感谢由{viewer_name}投喂的{gift_name}",
|
"gift": "感谢由{viewer_name}投喂的{gift_count}个{gift_name}",
|
||||||
"blind_box": "感谢由{viewer_name}投喂的{gift_name}爆出了{blind_box_gift}",
|
"blind_box": "感谢由{viewer_name}投喂的{gift_count}个{gift_name}爆出了{blind_box_gift}",
|
||||||
"enter_room": "欢迎{viewer_name}进入直播间",
|
"enter_room": "欢迎{viewer_name}进入直播间",
|
||||||
"follow": "",
|
"follow": "",
|
||||||
"like": "",
|
|
||||||
"interact_other": "",
|
"interact_other": "",
|
||||||
"buy_guard": "恭喜{ships_viewer_name}成为尊贵的{ships_level}",
|
"buy_guard": "恭喜{ships_viewer_name}成为尊贵的{ships_level}",
|
||||||
"guard_toast": "",
|
"guard_toast": "",
|
||||||
|
|||||||
+3
-5
@@ -20,7 +20,6 @@ DEFAULT_RULES = {
|
|||||||
"blind_box": True,
|
"blind_box": True,
|
||||||
"enter_room": True,
|
"enter_room": True,
|
||||||
"follow": False,
|
"follow": False,
|
||||||
"like": False,
|
|
||||||
"interact_other": False,
|
"interact_other": False,
|
||||||
"buy_guard": True,
|
"buy_guard": True,
|
||||||
"guard_toast": False,
|
"guard_toast": False,
|
||||||
@@ -28,11 +27,10 @@ DEFAULT_RULES = {
|
|||||||
},
|
},
|
||||||
"templates": {
|
"templates": {
|
||||||
"danmaku": "{uname}\u8bf4\u3001 {msg}",
|
"danmaku": "{uname}\u8bf4\u3001 {msg}",
|
||||||
"gift": "\u611f\u8c22\u7531{viewer_name}\u6295\u5582\u7684{gift_name}",
|
"gift": "\u611f\u8c22\u7531{viewer_name}\u6295\u5582\u7684{gift_count}\u4e2a{gift_name}",
|
||||||
"blind_box": "\u611f\u8c22\u7531{viewer_name}\u6295\u5582\u7684{gift_name}\u7206\u51fa\u4e86{blind_box_gift}",
|
"blind_box": "\u611f\u8c22\u7531{viewer_name}\u6295\u5582\u7684{gift_count}\u4e2a{gift_name}\u7206\u51fa\u4e86{blind_box_gift}",
|
||||||
"enter_room": "\u6b22\u8fce{viewer_name}\u8fdb\u5165\u76f4\u64ad\u95f4",
|
"enter_room": "\u6b22\u8fce{viewer_name}\u8fdb\u5165\u76f4\u64ad\u95f4",
|
||||||
"follow": "",
|
"follow": "",
|
||||||
"like": "",
|
|
||||||
"interact_other": "",
|
"interact_other": "",
|
||||||
"buy_guard": "\u606d\u559c{ships_viewer_name}\u6210\u4e3a\u5c0a\u8d35\u7684{ships_level}",
|
"buy_guard": "\u606d\u559c{ships_viewer_name}\u6210\u4e3a\u5c0a\u8d35\u7684{ships_level}",
|
||||||
"guard_toast": "",
|
"guard_toast": "",
|
||||||
@@ -42,7 +40,7 @@ DEFAULT_RULES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ALL_EVENTS = [
|
ALL_EVENTS = [
|
||||||
"danmaku", "gift", "blind_box", "enter_room", "follow", "like",
|
"danmaku", "gift", "blind_box", "enter_room", "follow",
|
||||||
"interact_other", "buy_guard", "guard_toast", "super_chat",
|
"interact_other", "buy_guard", "guard_toast", "super_chat",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -30,8 +30,6 @@ def parse_args() -> argparse.Namespace:
|
|||||||
choices=["f1", "f2", "m1", "m2", "dvd", "imd1", "jgr", "r1"])
|
choices=["f1", "f2", "m1", "m2", "dvd", "imd1", "jgr", "r1"])
|
||||||
parser.add_argument("--speed", "-s", type=int, default=DEFAULT_SPEED)
|
parser.add_argument("--speed", "-s", type=int, default=DEFAULT_SPEED)
|
||||||
parser.add_argument("--sessdata", type=str, default="")
|
parser.add_argument("--sessdata", type=str, default="")
|
||||||
parser.add_argument("--format", "-f", type=str,
|
|
||||||
default="{uname}\u8bf4\u3001 {msg}")
|
|
||||||
parser.add_argument("--no-numbers", action="store_true")
|
parser.add_argument("--no-numbers", action="store_true")
|
||||||
parser.add_argument("--list-devices", action="store_true")
|
parser.add_argument("--list-devices", action="store_true")
|
||||||
parser.add_argument("--debug", "-d", action="store_true")
|
parser.add_argument("--debug", "-d", action="store_true")
|
||||||
@@ -77,7 +75,6 @@ async def main_async(args: argparse.Namespace) -> int:
|
|||||||
room_id=args.room_id, queue=queue,
|
room_id=args.room_id, queue=queue,
|
||||||
cookies={"SESSDATA": args.sessdata} if args.sessdata else None,
|
cookies={"SESSDATA": args.sessdata} if args.sessdata else None,
|
||||||
rules=rules,
|
rules=rules,
|
||||||
message_format=args.format,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_signal():
|
async def handle_signal():
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ class TTSService:
|
|||||||
voice = config.get("voice", "f1")
|
voice = config.get("voice", "f1")
|
||||||
speed = config.get("speed", 100)
|
speed = config.get("speed", 100)
|
||||||
self._volume = config.get("volume", 100)
|
self._volume = config.get("volume", 100)
|
||||||
message_format = config.get("format", "{uname}\u8bf4\u3001 {msg}")
|
|
||||||
convert_numbers = config.get("convert_numbers", True)
|
convert_numbers = config.get("convert_numbers", True)
|
||||||
self.current_config = config
|
self.current_config = config
|
||||||
self.recent_messages = []
|
self.recent_messages = []
|
||||||
@@ -71,7 +70,6 @@ class TTSService:
|
|||||||
self._danmaku_client = DanmakuClient(
|
self._danmaku_client = DanmakuClient(
|
||||||
room_id=room_id, queue=self._queue,
|
room_id=room_id, queue=self._queue,
|
||||||
cookies=self._cookies, rules=event_rules,
|
cookies=self._cookies, rules=event_rules,
|
||||||
message_format=message_format,
|
|
||||||
)
|
)
|
||||||
await self._danmaku_client.start()
|
await self._danmaku_client.start()
|
||||||
self.running = True
|
self.running = True
|
||||||
@@ -169,7 +167,6 @@ async def api_start(request: web.Request) -> web.Response:
|
|||||||
config.setdefault("voice", "f1")
|
config.setdefault("voice", "f1")
|
||||||
config.setdefault("speed", 100)
|
config.setdefault("speed", 100)
|
||||||
config.setdefault("volume", 100)
|
config.setdefault("volume", 100)
|
||||||
config.setdefault("format", "{uname}\u8bf4\u3001 {msg}")
|
|
||||||
config.setdefault("convert_numbers", True)
|
config.setdefault("convert_numbers", True)
|
||||||
if tts_service.running:
|
if tts_service.running:
|
||||||
try:
|
try:
|
||||||
@@ -248,7 +245,6 @@ async def api_rules_get(request: web.Request) -> web.Response:
|
|||||||
|
|
||||||
|
|
||||||
async def api_rules_post(request: web.Request) -> web.Response:
|
async def api_rules_post(request: web.Request) -> web.Response:
|
||||||
global event_rules
|
|
||||||
try:
|
try:
|
||||||
new_rules = await request.json()
|
new_rules = await request.json()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -275,7 +271,11 @@ async def api_rules_post(request: web.Request) -> web.Response:
|
|||||||
save_rules(merged)
|
save_rules(merged)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
return web.json_response({"error": f"Failed to save rules: {e}"}, status=500)
|
return web.json_response({"error": f"Failed to save rules: {e}"}, status=500)
|
||||||
event_rules = merged
|
|
||||||
|
# Mutate in place so running handlers (which hold the same dict reference)
|
||||||
|
# pick up the new rules immediately without restarting TTS.
|
||||||
|
event_rules.clear()
|
||||||
|
event_rules.update(merged)
|
||||||
return web.json_response({"status": "saved", "rules": event_rules})
|
return web.json_response({"status": "saved", "rules": event_rules})
|
||||||
|
|
||||||
|
|
||||||
@@ -311,6 +311,7 @@ def main() -> int:
|
|||||||
datefmt="%H:%M:%S",
|
datefmt="%H:%M:%S",
|
||||||
)
|
)
|
||||||
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
|
||||||
|
|
||||||
app = create_app()
|
app = create_app()
|
||||||
print(f"\n Bilibili Live → Yukkuri TTS Web UI")
|
print(f"\n Bilibili Live → Yukkuri TTS Web UI")
|
||||||
print(f" Open: http://{args.host}:{args.port}\n")
|
print(f" Open: http://{args.host}:{args.port}\n")
|
||||||
|
|||||||
+2
-2
@@ -571,7 +571,7 @@ async function checkExistingLogin() {
|
|||||||
|
|
||||||
const RULE_LABELS = {
|
const RULE_LABELS = {
|
||||||
danmaku: '弹幕', gift: '礼物', blind_box: '盲盒礼物',
|
danmaku: '弹幕', gift: '礼物', blind_box: '盲盒礼物',
|
||||||
enter_room: '进入房间', follow: '关注', like: '点赞',
|
enter_room: '进入房间', follow: '关注',
|
||||||
interact_other: '其他互动', buy_guard: '上舰',
|
interact_other: '其他互动', buy_guard: '上舰',
|
||||||
guard_toast: '上舰提示', super_chat: '醒目留言',
|
guard_toast: '上舰提示', super_chat: '醒目留言',
|
||||||
};
|
};
|
||||||
@@ -634,7 +634,7 @@ async function saveRules() {
|
|||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
if (data.error) { alert('保存失败: ' + data.error); return; }
|
if (data.error) { alert('保存失败: ' + data.error); return; }
|
||||||
renderRules(data.rules);
|
renderRules(data.rules);
|
||||||
alert('规则已保存。重启TTS后生效。');
|
alert('规则已保存并即时生效。');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
alert('保存失败: ' + e.message);
|
alert('保存失败: ' + e.message);
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-2
@@ -189,9 +189,13 @@ function spellWord(word) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── English punctuation → Japanese pause equivalents ──────────────────
|
// ── English punctuation → Japanese pause equivalents ──────────────────
|
||||||
|
// Characters AquesTalk1 can safely pronounce (kana + limited punctuation).
|
||||||
|
// Anything else (!「」・ etc.) is collapsed to a 、 pause in synthesize().
|
||||||
|
const AQTK_SAFE_RE = /[\u3040-\u309F\u30A0-\u30FF\uFF65-\uFF9F\u3001\u3002\uFF1F\u301C\u30FC\u309B\u309C]/;
|
||||||
|
|
||||||
const PUNCT_MAP = {
|
const PUNCT_MAP = {
|
||||||
",": "、", ".": "。", "!": "!", "?": "?",
|
",": "、", ".": "。", "!": "、", "?": "?",
|
||||||
",": "、", "。": "。", "!": "!", "?": "?",
|
",": "、", "。": "。", "!": "、", "?": "?",
|
||||||
" ": "、", // AquesTalk truncates on space; use 、 pause instead
|
" ": "、", // AquesTalk truncates on space; use 、 pause instead
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -269,6 +273,14 @@ function synthesize(kanaText) {
|
|||||||
// Collapse consecutive pauses; strip any residual spaces (AquesTalk truncates on space)
|
// Collapse consecutive pauses; strip any residual spaces (AquesTalk truncates on space)
|
||||||
result = result.replace(/ +/g, "、").replace(/、+/g, "、").replace(/^、|、$/g, "");
|
result = result.replace(/ +/g, "、").replace(/、+/g, "、").replace(/^、|、$/g, "");
|
||||||
|
|
||||||
|
// Replace any remaining chars AquesTalk1 cannot pronounce with a 、 pause
|
||||||
|
// (covers !「」・ etc. that slipped through from user templates)
|
||||||
|
let cleaned = "";
|
||||||
|
for (const ch of result) {
|
||||||
|
cleaned += AQTK_SAFE_RE.test(ch) ? ch : "、";
|
||||||
|
}
|
||||||
|
result = cleaned.replace(/、+/g, "、").replace(/^、|、$/g, "");
|
||||||
|
|
||||||
console.error(`[bridge] SYNTH IN: ${trimmed}`);
|
console.error(`[bridge] SYNTH IN: ${trimmed}`);
|
||||||
console.error(`[bridge] SYNTH OUT: ${result}`);
|
console.error(`[bridge] SYNTH OUT: ${result}`);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user