添加自动翻译前端

This commit is contained in:
John Smith 2020-02-06 17:39:56 +08:00
parent 0c9560f8ca
commit d3e9300fa4
12 changed files with 198 additions and 32 deletions

View File

@ -6,13 +6,16 @@ import json
import logging
import random
import time
import uuid
from typing import *
import aiohttp
import tornado.websocket
import blivedm.blivedm as blivedm
import config
import models.avatar
import models.translate
logger = logging.getLogger(__name__)
@ -25,6 +28,7 @@ class Command(enum.IntEnum):
ADD_MEMBER = 4
ADD_SUPER_CHAT = 5
DEL_SUPER_CHAT = 6
UPDATE_TRANSLATION = 7
_http_session = aiohttp.ClientSession()
@ -94,6 +98,7 @@ class Room(blivedm.BLiveClient):
def __init__(self, room_id):
super().__init__(room_id, session=_http_session, heartbeat_interval=10)
self.clients: List['ChatHandler'] = []
self.auto_translate_count = 0
def stop_and_close(self):
if self.is_running:
@ -110,6 +115,14 @@ class Room(blivedm.BLiveClient):
except tornado.websocket.WebSocketClosedError:
pass
def send_message_if(self, can_send_func: Callable[['ChatHandler'], bool], cmd, data):
body = json.dumps({'cmd': cmd, 'data': data})
for client in filter(can_send_func, self.clients):
try:
client.write_message(body)
except tornado.websocket.WebSocketClosedError:
pass
async def _on_receive_danmaku(self, danmaku: blivedm.DanmakuMessage):
asyncio.ensure_future(self.__on_receive_danmaku(danmaku))
@ -122,6 +135,19 @@ class Room(blivedm.BLiveClient):
author_type = 1 # 舰队
else:
author_type = 0
need_translate = self._need_translate(danmaku.msg)
if need_translate:
translation = models.translate.get_translation_from_cache(danmaku.msg)
if translation is None:
# 没有缓存,需要后面异步翻译后通知
translation = ''
else:
need_translate = False
else:
translation = ''
id_ = uuid.uuid4().hex
# 为了节省带宽用list而不是dict
self.send_message(Command.ADD_TEXT, [
# 0: avatarUrl
@ -145,15 +171,24 @@ class Room(blivedm.BLiveClient):
# 9: isMobileVerified
1 if danmaku.mobile_verify else 0,
# 10: medalLevel
0 if danmaku.room_id != self.room_id else danmaku.medal_level
0 if danmaku.room_id != self.room_id else danmaku.medal_level,
# 11: id
id_,
# 12: translation
translation
])
if need_translate:
await self._translate_and_response(danmaku.msg, id_)
async def _on_receive_gift(self, gift: blivedm.GiftMessage):
avatar_url = models.avatar.process_avatar_url(gift.face)
models.avatar.update_avatar_cache(gift.uid, avatar_url)
if gift.coin_type != 'gold': # 丢人
return
id_ = uuid.uuid4().hex
self.send_message(Command.ADD_GIFT, {
'id': id_,
'avatarUrl': avatar_url,
'timestamp': gift.timestamp,
'authorName': gift.uname,
@ -164,7 +199,9 @@ class Room(blivedm.BLiveClient):
asyncio.ensure_future(self.__on_buy_guard(message))
async def __on_buy_guard(self, message: blivedm.GuardBuyMessage):
id_ = uuid.uuid4().hex
self.send_message(Command.ADD_MEMBER, {
'id': id_,
'avatarUrl': await models.avatar.get_avatar_url(message.uid),
'timestamp': message.start_time,
'authorName': message.username
@ -173,20 +210,59 @@ class Room(blivedm.BLiveClient):
async def _on_super_chat(self, message: blivedm.SuperChatMessage):
avatar_url = models.avatar.process_avatar_url(message.face)
models.avatar.update_avatar_cache(message.uid, avatar_url)
need_translate = self._need_translate(message.message)
if need_translate:
translation = models.translate.get_translation_from_cache(message.message)
if translation is None:
# 没有缓存,需要后面异步翻译后通知
translation = ''
else:
need_translate = False
else:
translation = ''
id_ = str(message.id)
self.send_message(Command.ADD_SUPER_CHAT, {
'id': id_,
'avatarUrl': avatar_url,
'timestamp': message.start_time,
'authorName': message.uname,
'price': message.price,
'content': message.message,
'id': message.id
'translation': translation
})
if need_translate:
asyncio.ensure_future(self._translate_and_response(message.message, id_))
async def _on_super_chat_delete(self, message: blivedm.SuperChatDeleteMessage):
self.send_message(Command.ADD_SUPER_CHAT, {
'ids': message.ids
'ids': list(map(str, message.ids))
})
def _need_translate(self, text):
return (
config.get_config().enable_translate
and self.auto_translate_count > 0
and models.translate.need_translate(text)
)
async def _translate_and_response(self, text, msg_id):
translation = await models.translate.translate(text)
if translation is None:
return
self.send_message_if(
lambda client: client.auto_translate,
Command.UPDATE_TRANSLATION,
[
# 0: id
msg_id,
# 1: translation
translation
]
)
class RoomManager:
def __init__(self):
@ -200,6 +276,8 @@ class RoomManager:
room = self._rooms[room_id]
room.clients.append(client)
logger.info('%d clients in room %s', len(room.clients), room_id)
if client.auto_translate:
room.auto_translate_count += 1
if client.application.settings['debug']:
await client.send_test_message()
@ -210,6 +288,8 @@ class RoomManager:
room = self._rooms[room_id]
room.clients.remove(client)
logger.info('%d clients in room %s', len(room.clients), room_id)
if client.auto_translate:
room.auto_translate_count -= 1
if not room.clients:
self._del_room(room_id)
@ -243,6 +323,7 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
super().__init__(*args, **kwargs)
self._close_on_timeout_future = None
self.room_id = None
self.auto_translate = False
def open(self):
logger.info('Websocket connected %s', self.request.remote_ip)
@ -268,6 +349,11 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
return
self.room_id = int(body['data']['roomId'])
logger.info('Client %s is joining room %d', self.request.remote_ip, self.room_id)
try:
cfg = body['data']['config']
self.auto_translate = cfg['autoTranslate']
except KeyError:
pass
asyncio.ensure_future(room_manager.add_client(self.room_id, self))
self._close_on_timeout_future.cancel()
@ -320,32 +406,43 @@ class ChatHandler(tornado.websocket.WebSocketHandler):
# 9: isMobileVerified
1,
# 10: medalLevel
0
0,
# 11: id
uuid.uuid4().hex,
# 12: translation
''
]
member_data = base_data.copy()
member_data = {
**base_data,
'id': uuid.uuid4().hex
}
gift_data = {
**base_data,
'id': uuid.uuid4().hex,
'totalCoin': 450000
}
sc_data = {
**base_data,
'id': str(random.randint(1, 65535)),
'price': 30,
'content': 'The quick brown fox jumps over the lazy dog',
'id': random.randint(1, 65535)
'translation': ''
}
self.send_message(Command.ADD_TEXT, text_data)
text_data[2] = '主播'
text_data[3] = 3
text_data[4] = "I can eat glass, it doesn't hurt me."
text_data[11] = uuid.uuid4().hex
self.send_message(Command.ADD_TEXT, text_data)
self.send_message(Command.ADD_MEMBER, member_data)
self.send_message(Command.ADD_SUPER_CHAT, sc_data)
sc_data['id'] = str(random.randint(1, 65535))
sc_data['price'] = 100
sc_data['content'] = '敏捷的棕色狐狸跳过了懒狗'
sc_data['id'] = random.randint(1, 65535)
self.send_message(Command.ADD_SUPER_CHAT, sc_data)
# self.send_message(Command.DEL_SUPER_CHAT, {'ids': [sc_data['id']]})
self.send_message(Command.ADD_GIFT, gift_data)
gift_data['id'] = uuid.uuid4().hex
gift_data['totalCoin'] = 1245000
self.send_message(Command.ADD_GIFT, gift_data)

View File

@ -30,6 +30,7 @@ def get_config():
class AppConfig:
def __init__(self):
self.database_url = 'sqlite:///data/database.db'
self.enable_translate = True
def load(self, path):
config = configparser.ConfigParser()
@ -37,6 +38,7 @@ class AppConfig:
try:
app_section = config['app']
self.database_url = app_section['database_url']
self.enable_translate = app_section.getboolean('enable_translate')
except (KeyError, ValueError):
logger.exception('Failed to load config:')
return False

View File

@ -1,8 +1,11 @@
[app]
# See https://docs.sqlalchemy.org/en/13/core/engines.html#database-urls
database_url = sqlite:///data/database.db
# Enable auto translate to Japanese
enable_translate = true
# DON'T modify this section
[DEFAULT]
database_url = sqlite:///data/database.db
enable_translate = true

View File

@ -13,7 +13,9 @@ export const DEFAULT_CONFIG = {
blockNotMobileVerified: true,
blockKeywords: '',
blockUsers: '',
blockMedalLevel: 0
blockMedalLevel: 0,
autoTranslate: false
}
export function setLocalConfig (config) {

View File

@ -13,7 +13,7 @@
<text-message :key="message.id" v-if="message.type === MESSAGE_TYPE_TEXT"
class="style-scope yt-live-chat-item-list-renderer"
:avatarUrl="message.avatarUrl" :time="message.time" :authorName="message.authorName"
:authorType="message.authorType" :content="message.content" :privilegeType="message.privilegeType"
:authorType="message.authorType" :content="getShowContent(message)" :privilegeType="message.privilegeType"
:repeated="message.repeated"
></text-message>
<legacy-paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_MEMBER"
@ -24,7 +24,7 @@
<paid-message :key="message.id" v-else-if="message.type === MESSAGE_TYPE_SUPER_CHAT"
class="style-scope yt-live-chat-item-list-renderer"
:price="message.price" :avatarUrl="message.avatarUrl" :authorName="message.authorName"
:time="message.time" :content="message.content"
:time="message.time" :content="getShowContent(message)"
></paid-message>
</template>
</div>
@ -118,6 +118,13 @@ export default {
this.clearMessages()
},
methods: {
getShowContent(message) {
if (message.translation) {
return `${message.content}${message.translation}`
}
return message.content
},
addMessage(message) {
this.addMessages([message])
},
@ -212,6 +219,29 @@ export default {
this.scrollToBottom()
}
},
updateMessage(id, newValuesObj) {
//
this.forEachRecentMessage(999999999, message => {
if (message.id !== id) {
return true
}
for (let name in newValuesObj) {
message[name] = newValuesObj[name]
}
return false
})
//
for (let message of this.paidMessages) {
if (message.id !== id) {
continue
}
for (let name in newValuesObj) {
message[name] = newValuesObj[name]
}
break
}
// TODO
},
enqueueMessages(messages) {
if (this.lastEnqueueTime) {

View File

@ -28,6 +28,9 @@ export default {
blockUsers: 'Block users',
blockMedalLevel: 'Block medal level lower than',
advanced: 'Advanced',
autoTranslate: 'Auto translate messages to Japanese',
roomUrl: 'Room URL',
copy: 'Copy',
enterRoom: 'Enter room',

View File

@ -28,6 +28,9 @@ export default {
blockUsers: 'ブロックユーザー',
blockMedalLevel: 'ブロック勲章等級がx未満',
advanced: 'アドバンス',
autoTranslate: '自動翻訳コメントから日本語へ',
roomUrl: 'ルームのURL',
copy: 'コピー',
enterRoom: 'ルームに入る',

View File

@ -28,6 +28,9 @@ export default {
blockUsers: '屏蔽用户',
blockMedalLevel: '屏蔽当前直播间勋章等级低于',
advanced: '高级',
autoTranslate: '自动翻译弹幕到日语',
roomUrl: '房间URL',
copy: '复制',
enterRoom: '进入房间',

View File

@ -8,7 +8,7 @@ export function mergeConfig (config, defaultConfig) {
export function toBool (val) {
if (typeof val === 'string') {
return val !== 'false' && val !== ''
return ['false', 'no', 'off', '0', ''].indexOf(val.toLowerCase()) === -1
}
return !!val
}

View File

@ -50,6 +50,12 @@
<el-slider v-model="form.blockMedalLevel" show-input :min="0" :max="20"></el-slider>
</el-form-item>
</el-tab-pane>
<el-tab-pane :label="$t('home.advanced')">
<el-form-item :label="$t('home.autoTranslate')">
<el-switch v-model="form.autoTranslate"></el-switch>
</el-form-item>
</el-tab-pane>
</el-tabs>
<el-divider></el-divider>

View File

@ -15,6 +15,7 @@ const COMMAND_ADD_GIFT = 3
const COMMAND_ADD_MEMBER = 4
const COMMAND_ADD_SUPER_CHAT = 5
const COMMAND_DEL_SUPER_CHAT = 6
const COMMAND_UPDATE_TRANSLATION = 7
export default {
name: 'Room',
@ -28,9 +29,7 @@ export default {
websocket: null,
retryCount: 0,
isDestroying: false,
heartbeatTimerId: null,
nextId: 0,
heartbeatTimerId: null
}
},
computed: {
@ -42,8 +41,8 @@ export default {
}
},
created() {
this.wsConnect()
this.updateConfig()
this.wsConnect()
},
beforeDestroy() {
this.isDestroying = true
@ -70,6 +69,7 @@ export default {
cfg.blockNewbie = toBool(cfg.blockNewbie)
cfg.blockNotMobileVerified = toBool(cfg.blockNotMobileVerified)
cfg.blockMedalLevel = toInt(cfg.blockMedalLevel, config.DEFAULT_CONFIG.blockMedalLevel)
cfg.autoTranslate = toBool(cfg.autoTranslate)
this.config = cfg
},
@ -94,7 +94,10 @@ export default {
this.websocket.send(JSON.stringify({
cmd: COMMAND_JOIN_ROOM,
data: {
roomId: parseInt(this.$route.params.roomId)
roomId: parseInt(this.$route.params.roomId),
config: {
autoTranslate: this.config.autoTranslate
}
}
}))
},
@ -125,13 +128,15 @@ export default {
authorLevel: data[7],
isNewbie: !!data[8],
isMobileVerified: !!data[9],
medalLevel: data[10]
medalLevel: data[10],
id: data[11],
translation: data[12]
}
if (!this.config.showDanmaku || !this.filterTextMessage(data) || this.mergeSimilarText(data.content)) {
break
}
message = {
id: `text_${this.nextId++}`,
id: data.id,
type: constants.MESSAGE_TYPE_TEXT,
avatarUrl: data.avatarUrl,
time: new Date(data.timestamp * 1000),
@ -139,7 +144,8 @@ export default {
authorType: data.authorType,
content: data.content,
privilegeType: data.privilegeType,
repeated: 1
repeated: 1,
translation: data.translation
}
break
case COMMAND_ADD_GIFT: {
@ -154,7 +160,7 @@ export default {
break
}
message = {
id: `gift_${this.nextId++}`,
id: data.id,
type: constants.MESSAGE_TYPE_SUPER_CHAT,
avatarUrl: data.avatarUrl,
authorName: data.authorName,
@ -169,7 +175,7 @@ export default {
break
}
message = {
id: `member_${this.nextId++}`,
id: data.id,
type: constants.MESSAGE_TYPE_MEMBER,
avatarUrl: data.avatarUrl,
time: new Date(data.timestamp * 1000),
@ -186,7 +192,7 @@ export default {
break
}
message = {
id: `sc_${data.id}`,
id: data.id,
type: constants.MESSAGE_TYPE_SUPER_CHAT,
avatarUrl: data.avatarUrl,
authorName: data.authorName,
@ -197,10 +203,19 @@ export default {
break
case COMMAND_DEL_SUPER_CHAT:
for (let id of data.ids) {
id = `sc_${id}`
this.$refs.renderer.delMessage(id)
}
break
case COMMAND_UPDATE_TRANSLATION:
if (!this.config.autoTranslate) {
break
}
data = {
id: data[0],
translation: data[1]
}
this.$refs.renderer.updateMessage(data.id, {translation: data.translation})
break
}
if (message) {
this.$refs.renderer.addMessage(message)

View File

@ -216,7 +216,8 @@ let textMessageTemplate = {
authorType: constants.AUTHRO_TYPE_NORMAL,
content: '',
privilegeType: 0,
repeated: 1
repeated: 1,
translation: ''
}
let legacyPaidMessageTemplate = {
id: 0,
@ -236,20 +237,21 @@ let paidMessageTemplate = {
authorName: '',
price: 0,
time: time,
content: ''
content: '',
translation: ''
}
let nextId = 0
const EXAMPLE_MESSAGES = [
{
...textMessageTemplate,
id: nextId++,
id: (nextId++).toString(),
authorName: 'mob路人',
content: '8888888888',
repeated: 12
},
{
...textMessageTemplate,
id: nextId++,
id: (nextId++).toString(),
authorName: 'member舰长',
authorType: constants.AUTHRO_TYPE_MEMBER,
content: '草',
@ -258,34 +260,34 @@ const EXAMPLE_MESSAGES = [
},
{
...textMessageTemplate,
id: nextId++,
id: (nextId++).toString(),
authorName: 'admin房管',
authorType: constants.AUTHRO_TYPE_ADMIN,
content: 'kksk'
},
{
...legacyPaidMessageTemplate,
id: nextId++,
id: (nextId++).toString(),
authorName: '少年Pi',
content: 'Welcome 少年Pi!'
},
{
...paidMessageTemplate,
id: nextId++,
id: (nextId++).toString(),
authorName: '无火的残渣',
price: 66600,
content: 'Sent 小电视飞船x100'
},
{
...textMessageTemplate,
id: nextId++,
id: (nextId++).toString(),
authorName: 'streamer主播',
authorType: constants.AUTHRO_TYPE_OWNER,
content: '老板大气,老板身体健康'
},
{
...paidMessageTemplate,
id: nextId++,
id: (nextId++).toString(),
authorName: '夏色祭保護協会会長',
price: 30,
content: '言いたいことがあるんだよ!'