feat: Add some diagrams into live report
This commit is contained in:
parent
cbe27f09d0
commit
5f0764dc0b
@ -294,7 +294,8 @@ class MySQLDataSource(DataSource):
|
||||
"`enabled`, `logo`, `logo_base64`, `time`, `fans_change`, `fans_medal_change`, `guard_change`, "
|
||||
"`danmu`, `box`, `gift`, `sc`, `guard`, "
|
||||
"`danmu_ranking`, `box_ranking`, `box_profit_ranking`, `gift_ranking`, `sc_ranking`, "
|
||||
"`guard_list`, `danmu_cloud` "
|
||||
"`guard_list`, `box_profit_diagram`, `danmu_diagram`, `box_diagram`, `gift_diagram`, "
|
||||
"`sc_diagram`, `guard_diagram`, `danmu_cloud` "
|
||||
"FROM `groups` AS `g` LEFT JOIN `live_report` AS `l` "
|
||||
"ON g.`uid` = l.`uid` AND g.`index` = l.`index` "
|
||||
f"WHERE g.`uid` = {uid} "
|
||||
|
@ -136,6 +136,24 @@ class LiveReport(BaseModel):
|
||||
guard_list = False
|
||||
"""是否展示本场直播开通大航海观众列表。默认:False"""
|
||||
|
||||
box_profit_diagram = False
|
||||
"""是否展示本场直播的盲盒盈亏曲线图。默认:False"""
|
||||
|
||||
danmu_diagram = False
|
||||
"""是否展示本场直播的弹幕互动曲线图。默认:False"""
|
||||
|
||||
box_diagram = False
|
||||
"""是否展示本场直播的盲盒互动曲线图。默认:False"""
|
||||
|
||||
gift_diagram = False
|
||||
"""是否展示本场直播的礼物互动曲线图。默认:False"""
|
||||
|
||||
sc_diagram = False
|
||||
"""是否展示本场直播的 SC(醒目留言)互动曲线图。默认:False"""
|
||||
|
||||
guard_diagram = False
|
||||
"""是否展示本场直播的开通大航海互动曲线图。默认:False"""
|
||||
|
||||
danmu_cloud: Optional[bool] = False
|
||||
"""是否生成本场直播弹幕词云。默认:False。默认:False"""
|
||||
|
||||
@ -143,25 +161,38 @@ class LiveReport(BaseModel):
|
||||
def default(cls):
|
||||
"""
|
||||
获取功能全部开启的默认 LiveReport 实例
|
||||
默认配置:启用直播报告,生成弹幕词云
|
||||
默认配置:启用直播报告,无主播立绘,展示直播时间段和直播时长,展示粉丝变动,展示粉丝团(粉丝勋章数)变动,展示大航海变动
|
||||
展示收到弹幕数、发送弹幕人数,展示收到盲盒数、送出盲盒人数、盲盒盈亏,展示礼物收益、送礼物人数
|
||||
展示 SC(醒目留言)收益、发送 SC(醒目留言)人数,展示开通大航海数
|
||||
展示弹幕排行榜前 3 名,展示盲盒数量排行榜前 3 名,展示盲盒盈亏排行榜前 3 名,展示礼物排行榜前 3 名
|
||||
展示 SC(醒目留言)排行榜前 3 名,展示开通大航海观众列表
|
||||
展示盲盒盈亏曲线图,展示弹幕互动曲线图,展示盲盒互动曲线图,展示礼物互动曲线图,
|
||||
展示 SC(醒目留言)互动曲线图,展示开通大航海互动曲线图,
|
||||
生成弹幕词云
|
||||
"""
|
||||
return LiveReport(enabled=True, logo=None, logo_base64=None,
|
||||
time=True, fans_change=True, fans_medal_change=True,guard_change=True,
|
||||
danmu=True, box=True, gift=True, sc=True, guard=True,
|
||||
danmu_ranking=3, box_ranking=3, box_profit_ranking=3, gift_ranking=3, sc_ranking=3,
|
||||
guard_list=True, danmu_cloud=True)
|
||||
guard_list=True, box_profit_diagram=True,
|
||||
danmu_diagram=True, box_diagram=True, gift_diagram=True, sc_diagram=True, guard_diagram=True,
|
||||
danmu_cloud=True)
|
||||
|
||||
def __str__(self):
|
||||
return (f"启用: {self.enabled}\n直播时长数据: {self.time}\n粉丝变动数据: {self.fans_change}\n"
|
||||
f"粉丝团变动数据: {self.fans_medal_change}\n大航海变动数据: {self.guard_change}\n"
|
||||
f"弹幕相关数据: {self.danmu}\n盲盒相关数据: {self.box}\n礼物相关数据: {self.gift}\n"
|
||||
f"SC相关数据: {self.sc}\n大航海相关数据: {self.guard}\n"
|
||||
f"SC 相关数据: {self.sc}\n大航海相关数据: {self.guard}\n"
|
||||
f"弹幕排行榜: {f'前 {self.danmu_ranking} 名' if self.danmu_ranking else False}\n"
|
||||
f"盲盒数量排行榜: {f'前 {self.box_ranking} 名' if self.box_ranking else False}\n"
|
||||
f"盲盒盈亏排行榜: {f'前 {self.box_profit_ranking} 名' if self.box_profit_ranking else False}\n"
|
||||
f"礼物排行榜: {f'前 {self.gift_ranking} 名' if self.gift_ranking else False}\n"
|
||||
f"SC 排行榜: {f'前 {self.sc_ranking} 名' if self.sc_ranking else False}\n"
|
||||
f"开通大航海观众列表: {self.guard_list}\n生成弹幕词云: {self.danmu_cloud}")
|
||||
f"开通大航海观众列表: {self.guard_list}\n盲盒盈亏曲线图: {self.box_profit_diagram}\n"
|
||||
f"弹幕互动曲线图: {self.danmu_diagram}\n盲盒互动曲线图: {self.box_diagram}\n"
|
||||
f"礼物互动曲线图: {self.gift_diagram}\nSC 互动曲线图: {self.sc_diagram}\n"
|
||||
f"开通大航海互动曲线图: {self.guard_diagram}\n"
|
||||
f"生成弹幕词云: {self.danmu_cloud}")
|
||||
|
||||
|
||||
class DynamicUpdate(BaseModel):
|
||||
|
@ -1,17 +1,11 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import os
|
||||
import time
|
||||
import typing
|
||||
from asyncio import AbstractEventLoop
|
||||
from collections import Counter
|
||||
from io import BytesIO
|
||||
from typing import Optional, Any, Union, List
|
||||
|
||||
import jieba
|
||||
from loguru import logger
|
||||
from pydantic import BaseModel, PrivateAttr
|
||||
from wordcloud import WordCloud
|
||||
|
||||
from .live import LiveDanmaku, LiveRoom
|
||||
from .model import PushTarget
|
||||
@ -24,8 +18,6 @@ from ..utils.utils import get_credential, timestamp_format, get_unames_and_faces
|
||||
if typing.TYPE_CHECKING:
|
||||
from .sender import Bot
|
||||
|
||||
jieba.setLogLevel(jieba.logging.INFO)
|
||||
|
||||
|
||||
class Up(BaseModel):
|
||||
"""
|
||||
@ -77,6 +69,10 @@ class Up(BaseModel):
|
||||
def dispatch(self, name, data):
|
||||
self.__room.dispatch(name, data)
|
||||
|
||||
async def accumulate_and_reset_data(self):
|
||||
await self.__accumulate_data()
|
||||
await self.__reset_data()
|
||||
|
||||
def is_connecting(self):
|
||||
return (self.__room is not None) and (self.__room.get_status() != 2)
|
||||
|
||||
@ -195,8 +191,7 @@ class Up(BaseModel):
|
||||
await redis.hset(f"FansMedalCount:{self.room_id}", live_start_time, fans_medal_count)
|
||||
await redis.hset(f"GuardCount:{self.room_id}", live_start_time, guard_count)
|
||||
|
||||
await self.__accumulate_data()
|
||||
await self.__reset_data()
|
||||
await self.accumulate_and_reset_data()
|
||||
|
||||
# 推送开播消息
|
||||
arg_base = room_info["room_info"]
|
||||
@ -230,7 +225,7 @@ class Up(BaseModel):
|
||||
self.__bot.send_live_off(self, live_off_args)
|
||||
self.__bot.send_live_report(self, live_report_param)
|
||||
|
||||
danmu_items = ["danmu", "danmu_cloud"]
|
||||
danmu_items = ["danmu", "danmu_ranking", "danmu_diagram", "danmu_cloud"]
|
||||
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled(danmu_items):
|
||||
@self.__room.on("DANMU_MSG")
|
||||
async def on_danmu(event):
|
||||
@ -250,8 +245,12 @@ class Up(BaseModel):
|
||||
# 弹幕词云所需弹幕记录
|
||||
if isinstance(base[0][13], str):
|
||||
await redis.rpush(f"RoomDanmu:{self.room_id}", content)
|
||||
await redis.hincrby(f"RoomDanmuTime:{self.room_id}", int(time.time()))
|
||||
|
||||
gift_items = ["box", "gift"]
|
||||
gift_items = [
|
||||
"box", "gift", "box_ranking", "box_profit_ranking", "gift_ranking",
|
||||
"box_profit_diagram", "box_diagram", "gift_diagram"
|
||||
]
|
||||
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled(gift_items):
|
||||
@self.__room.on("SEND_GIFT")
|
||||
async def on_gift(event):
|
||||
@ -270,6 +269,8 @@ class Up(BaseModel):
|
||||
await redis.hincrbyfloat("RoomGiftProfit", self.room_id, price)
|
||||
await redis.zincrby(f"UserGiftProfit:{self.room_id}", uid, price)
|
||||
|
||||
await redis.hincrbyfloat(f"RoomGiftTime:{self.room_id}", int(time.time()), price)
|
||||
|
||||
# 盲盒统计
|
||||
if base["blind_gift"] is not None:
|
||||
box_price = base["total_coin"] / 1000
|
||||
@ -279,10 +280,14 @@ class Up(BaseModel):
|
||||
|
||||
await redis.hincrby("RoomBoxCount", self.room_id, gift_num)
|
||||
await redis.zincrby(f"UserBoxCount:{self.room_id}", uid, gift_num)
|
||||
await redis.hincrbyfloat("RoomBoxProfit", self.room_id, profit)
|
||||
box_profit_after = await redis.hincrbyfloat("RoomBoxProfit", self.room_id, profit)
|
||||
await redis.zincrby(f"UserBoxProfit:{self.room_id}", uid, profit)
|
||||
|
||||
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled("sc"):
|
||||
await redis.rpush(f"RoomBoxProfitRecord:{self.room_id}", box_profit_after)
|
||||
await redis.hincrby(f"RoomBoxTime:{self.room_id}", int(time.time()))
|
||||
|
||||
sc_items = ["sc", "sc_ranking", "sc_diagram"]
|
||||
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled(sc_items):
|
||||
@self.__room.on("SUPER_CHAT_MESSAGE")
|
||||
async def on_sc(event):
|
||||
"""
|
||||
@ -298,7 +303,10 @@ class Up(BaseModel):
|
||||
await redis.hincrby("RoomScProfit", self.room_id, price)
|
||||
await redis.zincrby(f"UserScProfit:{self.room_id}", uid, price)
|
||||
|
||||
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled("guard"):
|
||||
await redis.hincrby(f"RoomScTime:{self.room_id}", int(time.time()), price)
|
||||
|
||||
guard_items = ["guard", "guard_list", "guard_diagram"]
|
||||
if not config.get("ONLY_HANDLE_NECESSARY_EVENT") or self.__any_live_report_item_enabled(guard_items):
|
||||
@self.__room.on("GUARD_BUY")
|
||||
async def on_guard(event):
|
||||
"""
|
||||
@ -320,6 +328,8 @@ class Up(BaseModel):
|
||||
await redis.hincrby(f"Room{type_mapping[guard_type]}Count", self.room_id, month)
|
||||
await redis.zincrby(f"User{type_mapping[guard_type]}Count:{self.room_id}", uid, month)
|
||||
|
||||
await redis.hincrby(f"RoomGuardTime:{self.room_id}", int(time.time()), month)
|
||||
|
||||
if self.__any_dynamic_update_enabled():
|
||||
@self.__room.on("DYNAMIC_UPDATE")
|
||||
async def dynamic_update(event):
|
||||
@ -407,6 +417,13 @@ class Up(BaseModel):
|
||||
# 清空弹幕记录
|
||||
await redis.delete(f"RoomDanmu:{self.room_id}")
|
||||
|
||||
# 清空数据分布
|
||||
await redis.delete(f"RoomDanmuTime:{self.room_id}")
|
||||
await redis.delete(f"RoomBoxTime:{self.room_id}")
|
||||
await redis.delete(f"RoomGiftTime:{self.room_id}")
|
||||
await redis.delete(f"RoomScTime:{self.room_id}")
|
||||
await redis.delete(f"RoomGuardTime:{self.room_id}")
|
||||
|
||||
# 重置弹幕数
|
||||
await redis.hset(f"RoomDanmuCount", self.room_id, 0)
|
||||
await redis.delete(f"UserDanmuCount:{self.room_id}")
|
||||
@ -419,6 +436,9 @@ class Up(BaseModel):
|
||||
await redis.hset(f"RoomBoxProfit", self.room_id, 0)
|
||||
await redis.delete(f"UserBoxProfit:{self.room_id}")
|
||||
|
||||
# 清空盲盒盈亏记录
|
||||
await redis.delete(f"RoomBoxProfitRecord:{self.room_id}")
|
||||
|
||||
# 重置礼物收益
|
||||
await redis.hset(f"RoomGiftProfit", self.room_id, 0)
|
||||
await redis.delete(f"UserGiftProfit:{self.room_id}")
|
||||
@ -459,6 +479,8 @@ class Up(BaseModel):
|
||||
hour, minute = divmod(minute, 60)
|
||||
|
||||
live_report_param.update({
|
||||
"start_timestamp": start_time,
|
||||
"end_timestamp": end_time,
|
||||
"start_time": timestamp_format(start_time, "%m/%d %H:%M:%S"),
|
||||
"end_time": timestamp_format(end_time, "%m/%d %H:%M:%S"),
|
||||
"hour": hour,
|
||||
@ -470,17 +492,16 @@ class Up(BaseModel):
|
||||
if self.__any_live_report_item_enabled(["fans_change", "fans_medal_change", "guard_change"]):
|
||||
room_info = await self.__live_room.get_room_info()
|
||||
|
||||
live_start_time = await redis.hgeti("StartTime", self.room_id)
|
||||
if await redis.hexists(f"FansCount:{self.room_id}", live_start_time):
|
||||
fans_count = await redis.hgeti(f"FansCount:{self.room_id}", live_start_time)
|
||||
if await redis.hexists(f"FansCount:{self.room_id}", start_time):
|
||||
fans_count = await redis.hgeti(f"FansCount:{self.room_id}", start_time)
|
||||
else:
|
||||
fans_count = -1
|
||||
if await redis.hexists(f"FansMedalCount:{self.room_id}", live_start_time):
|
||||
fans_medal_count = await redis.hgeti(f"FansMedalCount:{self.room_id}", live_start_time)
|
||||
if await redis.hexists(f"FansMedalCount:{self.room_id}", start_time):
|
||||
fans_medal_count = await redis.hgeti(f"FansMedalCount:{self.room_id}", start_time)
|
||||
else:
|
||||
fans_medal_count = -1
|
||||
if await redis.hexists(f"GuardCount:{self.room_id}", live_start_time):
|
||||
guard_count = await redis.hgeti(f"GuardCount:{self.room_id}", live_start_time)
|
||||
if await redis.hexists(f"GuardCount:{self.room_id}", start_time):
|
||||
guard_count = await redis.hgeti(f"GuardCount:{self.room_id}", start_time)
|
||||
else:
|
||||
guard_count = -1
|
||||
|
||||
@ -512,21 +533,27 @@ class Up(BaseModel):
|
||||
# 弹幕相关
|
||||
"danmu_count": await redis.hgeti("RoomDanmuCount", self.room_id),
|
||||
"danmu_person_count": await redis.zcard(f"UserDanmuCount:{self.room_id}"),
|
||||
"danmu_diagram": await redis.hgetalltuplei(f"RoomDanmuTime:{self.room_id}"),
|
||||
# 盲盒相关
|
||||
"box_count": await redis.hgeti("RoomBoxCount", self.room_id),
|
||||
"box_person_count": await redis.zcard(f"UserBoxCount:{self.room_id}"),
|
||||
"box_profit": box_profit,
|
||||
"box_beat_percent": percent,
|
||||
"box_profit_diagram": await redis.lrangef1(f"RoomBoxProfitRecord:{self.room_id}", 0, -1),
|
||||
"box_diagram": await redis.hgetalltuplei(f"RoomBoxTime:{self.room_id}"),
|
||||
# 礼物相关
|
||||
"gift_profit": await redis.hgetf1("RoomGiftProfit", self.room_id),
|
||||
"gift_person_count": await redis.zcard(f"UserGiftProfit:{self.room_id}"),
|
||||
"gift_diagram": await redis.hgetalltuplef1(f"RoomGiftTime:{self.room_id}"),
|
||||
# SC(醒目留言)相关
|
||||
"sc_profit": await redis.hgeti("RoomScProfit", self.room_id),
|
||||
"sc_person_count": await redis.zcard(f"UserScProfit:{self.room_id}"),
|
||||
"sc_diagram": await redis.hgetalltuplei(f"RoomScTime:{self.room_id}"),
|
||||
# 大航海相关
|
||||
"captain_count": await redis.hgeti("RoomCaptainCount", self.room_id),
|
||||
"commander_count": await redis.hgeti("RoomCommanderCount", self.room_id),
|
||||
"governor_count": await redis.hgeti("RoomGovernorCount", self.room_id)
|
||||
"governor_count": await redis.hgeti("RoomGovernorCount", self.room_id),
|
||||
"guard_diagram": await redis.hgetalltuplei(f"RoomGuardTime:{self.room_id}")
|
||||
})
|
||||
|
||||
# 弹幕排行
|
||||
@ -649,31 +676,11 @@ class Up(BaseModel):
|
||||
|
||||
# 弹幕词云
|
||||
if self.__any_live_report_item_enabled("danmu_cloud"):
|
||||
all_danmu = " ".join(await redis.lrange(f"RoomDanmu:{self.room_id}", 0, -1))
|
||||
all_danmu = await redis.lrange(f"RoomDanmu:{self.room_id}", 0, -1)
|
||||
|
||||
if len(all_danmu) == 0:
|
||||
live_report_param.update({
|
||||
"danmu_cloud": ""
|
||||
})
|
||||
else:
|
||||
words = list(jieba.cut(all_danmu))
|
||||
counts = dict(Counter(words))
|
||||
|
||||
font_base_path = os.path.dirname(os.path.dirname(__file__))
|
||||
io = BytesIO()
|
||||
word_cloud = WordCloud(width=900,
|
||||
height=450,
|
||||
font_path=f"{font_base_path}/resource/{config.get('DANMU_CLOUD_FONT')}",
|
||||
background_color=config.get("DANMU_CLOUD_BACKGROUND_COLOR"),
|
||||
max_font_size=config.get("DANMU_CLOUD_MAX_FONT_SIZE"),
|
||||
max_words=config.get("DANMU_CLOUD_MAX_WORDS"))
|
||||
word_cloud.generate_from_frequencies(counts)
|
||||
word_cloud.to_image().save(io, format="png")
|
||||
base64_str = base64.b64encode(io.getvalue()).decode()
|
||||
|
||||
live_report_param.update({
|
||||
"danmu_cloud": base64_str
|
||||
})
|
||||
live_report_param.update({
|
||||
"all_danmu": all_danmu
|
||||
})
|
||||
|
||||
return live_report_param
|
||||
|
||||
|
@ -1,13 +1,26 @@
|
||||
import base64
|
||||
import bisect
|
||||
import io
|
||||
import math
|
||||
import os
|
||||
from collections import Counter
|
||||
from io import BytesIO
|
||||
from typing import Union, Tuple, List, Dict, Any
|
||||
|
||||
import jieba
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from matplotlib import pyplot as plt
|
||||
from mpl_toolkits import axisartist
|
||||
from scipy.interpolate import make_interp_spline
|
||||
from wordcloud import WordCloud
|
||||
|
||||
from .PicGenerator import Color, PicGenerator
|
||||
from ..core.model import LiveReport
|
||||
from ..utils.utils import split_list, limit_str_length, mask_round
|
||||
from ..utils import config
|
||||
from ..utils.utils import split_list, limit_str_length, mask_round, timestamp_format
|
||||
|
||||
jieba.setLogLevel(jieba.logging.INFO)
|
||||
|
||||
|
||||
class LiveReportGenerator:
|
||||
@ -261,14 +274,107 @@ class LiveReportGenerator:
|
||||
)
|
||||
pic.draw_img_alpha(pic.auto_size_img_by_limit(guard_list_img, logo_limit))
|
||||
|
||||
# 盲盒盈亏曲线图
|
||||
if model.box_profit_diagram:
|
||||
profits = param.get("box_profit_diagram", [])
|
||||
if profits:
|
||||
profits.insert(0, 0.0)
|
||||
|
||||
pic.draw_section("盲盒盈亏曲线图")
|
||||
|
||||
diagram = cls.__get_box_profit_diagram(profits, pic.width - (margin * 2))
|
||||
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
|
||||
|
||||
# 弹幕互动曲线图
|
||||
if model.danmu_diagram:
|
||||
start = param.get('start_timestamp', 0)
|
||||
end = param.get('end_timestamp', 0)
|
||||
|
||||
times = param.get("danmu_diagram", [])
|
||||
|
||||
if end - start >= 100 and times:
|
||||
pic.draw_section("弹幕互动曲线图")
|
||||
pic.draw_tip("收获弹幕数量在本场直播中的分布情况")
|
||||
|
||||
diagram = cls.__get_interaction_diagram(times, start, end, pic.width - (margin * 2))
|
||||
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
|
||||
|
||||
# 盲盒互动曲线图
|
||||
if model.box_diagram:
|
||||
start = param.get('start_timestamp', 0)
|
||||
end = param.get('end_timestamp', 0)
|
||||
|
||||
times = param.get("box_diagram", [])
|
||||
|
||||
if end - start >= 100 and times:
|
||||
pic.draw_section("盲盒互动曲线图")
|
||||
pic.draw_tip("收获盲盒数量在本场直播中的分布情况")
|
||||
|
||||
diagram = cls.__get_interaction_diagram(times, start, end, pic.width - (margin * 2))
|
||||
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
|
||||
|
||||
# 礼物互动曲线图
|
||||
if model.gift_diagram:
|
||||
start = param.get('start_timestamp', 0)
|
||||
end = param.get('end_timestamp', 0)
|
||||
|
||||
times = param.get("gift_diagram", [])
|
||||
|
||||
if end - start >= 100 and times:
|
||||
pic.draw_section("礼物互动曲线图")
|
||||
pic.draw_tip("收获礼物价值在本场直播中的分布情况")
|
||||
|
||||
diagram = cls.__get_interaction_diagram(times, start, end, pic.width - (margin * 2))
|
||||
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
|
||||
|
||||
# SC(醒目留言)互动曲线图
|
||||
if model.sc_diagram:
|
||||
start = param.get('start_timestamp', 0)
|
||||
end = param.get('end_timestamp', 0)
|
||||
|
||||
times = param.get("sc_diagram", [])
|
||||
|
||||
if end - start >= 100 and times:
|
||||
pic.draw_section("SC(醒目留言)互动曲线图")
|
||||
pic.draw_tip("收获SC(醒目留言)价值在本场直播中的分布情况")
|
||||
|
||||
diagram = cls.__get_interaction_diagram(times, start, end, pic.width - (margin * 2))
|
||||
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
|
||||
|
||||
# 开通大航海互动曲线图
|
||||
if model.guard_diagram:
|
||||
start = param.get('start_timestamp', 0)
|
||||
end = param.get('end_timestamp', 0)
|
||||
|
||||
times = param.get("guard_diagram", [])
|
||||
|
||||
if end - start >= 100 and times:
|
||||
pic.draw_section("开通大航海互动曲线图")
|
||||
pic.draw_tip("收获大航海开通时长在本场直播中的分布情况")
|
||||
|
||||
diagram = cls.__get_interaction_diagram(times, start, end, pic.width - (margin * 2))
|
||||
pic.draw_img_alpha(pic.auto_size_img_by_limit(diagram, logo_limit))
|
||||
|
||||
# 弹幕词云
|
||||
if model.danmu_cloud:
|
||||
base64_str = param.get('danmu_cloud', "")
|
||||
if base64_str != "":
|
||||
all_danmu = param.get('all_danmu', [])
|
||||
|
||||
if all_danmu:
|
||||
pic.draw_section("弹幕词云")
|
||||
|
||||
img_bytes = BytesIO(base64.b64decode(base64_str))
|
||||
img = pic.auto_size_img_by_limit(Image.open(img_bytes), logo_limit)
|
||||
all_danmu_str = " ".join(all_danmu)
|
||||
words = list(jieba.cut(all_danmu_str))
|
||||
counts = dict(Counter(words))
|
||||
|
||||
font_base_path = os.path.dirname(os.path.dirname(__file__))
|
||||
word_cloud = WordCloud(width=900,
|
||||
height=450,
|
||||
font_path=f"{font_base_path}/resource/{config.get('DANMU_CLOUD_FONT')}",
|
||||
background_color=config.get("DANMU_CLOUD_BACKGROUND_COLOR"),
|
||||
max_font_size=config.get("DANMU_CLOUD_MAX_FONT_SIZE"),
|
||||
max_words=config.get("DANMU_CLOUD_MAX_WORDS"))
|
||||
word_cloud.generate_from_frequencies(counts)
|
||||
img = pic.auto_size_img_by_limit(word_cloud.to_image(), logo_limit)
|
||||
pic.draw_img_with_border(img)
|
||||
|
||||
# 底部版权信息,请务必保留此处
|
||||
@ -577,4 +683,206 @@ class LiveReportGenerator:
|
||||
cls.__get_guard_line_pic(pic, width, face_size, faces, unames, counts, icon_map[i], color_map[i])
|
||||
).move_pos(0, -pic.row_space)
|
||||
|
||||
return img.img
|
||||
return img.img
|
||||
|
||||
@classmethod
|
||||
def __insert_zeros(cls, lst: List[float]) -> List[float]:
|
||||
"""
|
||||
在列表正负变化处添加 0.0 值
|
||||
|
||||
Args:
|
||||
lst: 源列表
|
||||
|
||||
Returns:
|
||||
在正负变化处添加 0.0 后的列表
|
||||
"""
|
||||
result = []
|
||||
for i in range(len(lst)):
|
||||
if i == 0:
|
||||
result.append(lst[i])
|
||||
continue
|
||||
if (lst[i - 1] < 0 < lst[i]) or (lst[i] < 0 < lst[i - 1]):
|
||||
result.append(0.0)
|
||||
result.append(lst[i])
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def __smooth_xy(cls, lx: List[Any], ly: List[Any]) -> Tuple[Any, Any]:
|
||||
"""
|
||||
平滑处理折线图数据
|
||||
|
||||
Args:
|
||||
lx: x 轴数据
|
||||
ly: y 轴数据
|
||||
|
||||
Returns:
|
||||
平滑处理后的 x 和 y 轴数据组成的元组
|
||||
"""
|
||||
x = np.array(lx)
|
||||
y = np.array(ly)
|
||||
x_smooth = np.linspace(0, max(x), 300)
|
||||
y_smooth = make_interp_spline(x, y)(x_smooth)
|
||||
return x_smooth, y_smooth
|
||||
|
||||
@classmethod
|
||||
def __get_line_diagram(cls,
|
||||
xs: List[Any],
|
||||
ys: List[Any],
|
||||
xticks: List[Any],
|
||||
yticks: List[Any],
|
||||
xlabels: List[Any],
|
||||
ylabels: List[Any],
|
||||
xlimits: Tuple[Any, Any],
|
||||
ylimits: Tuple[Any, Any],
|
||||
width: int) -> Image:
|
||||
"""
|
||||
绘制折线图
|
||||
|
||||
Args:
|
||||
xs: x 轴数据
|
||||
ys: y 轴数据
|
||||
xticks: x 轴标签点
|
||||
yticks: y 轴标签点
|
||||
xlabels: x 轴标签
|
||||
ylabels: y 轴标签
|
||||
xlimits: x 轴范围
|
||||
ylimits: y 轴范围
|
||||
width: 折线图图片宽度
|
||||
"""
|
||||
fig = plt.figure(figsize=(width / 100, width / 100 * 0.75))
|
||||
ax = axisartist.Subplot(fig, 111)
|
||||
fig.add_axes(ax)
|
||||
ax.axis[:].set_visible(False)
|
||||
ax.axis["x"] = ax.new_floating_axis(0, 0)
|
||||
ax.axis["y"] = ax.new_floating_axis(1, 0)
|
||||
ax.axis["x"].set_axis_direction('top')
|
||||
ax.axis["y"].set_axis_direction('left')
|
||||
ax.axis["x"].set_axisline_style("->", size=2.0)
|
||||
ax.axis["y"].set_axisline_style("->", size=2.0)
|
||||
|
||||
ax.plot(xs, ys, color='red')
|
||||
ax.grid(True, linestyle='--', alpha=0.5)
|
||||
for i in range(len(ys) - 1):
|
||||
if ys[i] >= 0 and ys[i + 1] >= 0:
|
||||
plt.fill_between(xs[i:i + 2], 0, ys[i:i + 2], facecolor='red', alpha=0.3)
|
||||
else:
|
||||
plt.fill_between(xs[i:i + 2], 0, ys[i:i + 2], facecolor='green', alpha=0.3)
|
||||
|
||||
ax.set_xticks(xticks)
|
||||
ax.set_yticks(yticks)
|
||||
|
||||
if xlabels:
|
||||
ax.set_xticklabels(xlabels)
|
||||
if ylabels:
|
||||
ax.set_yticklabels(ylabels)
|
||||
|
||||
ax.set_xlim(xlimits[0], xlimits[1])
|
||||
ax.set_ylim(ylimits[0], ylimits[1])
|
||||
|
||||
buf = io.BytesIO()
|
||||
fig.savefig(buf)
|
||||
buf.seek(0)
|
||||
return Image.open(buf)
|
||||
|
||||
@classmethod
|
||||
def __get_box_profit_diagram(cls, profits: List[float], width: int) -> Image:
|
||||
"""
|
||||
绘制盲盒盈亏曲线图
|
||||
|
||||
Args:
|
||||
profits: 盲盒盈亏记录
|
||||
width: 盲盒盈亏曲线图图片宽度
|
||||
"""
|
||||
profits = cls.__insert_zeros(profits)
|
||||
length = len(profits)
|
||||
indexs = list(range(0, length))
|
||||
|
||||
abs_max = int(max(max(profits), abs(min(profits))))
|
||||
start = -abs_max - (-abs_max % 10)
|
||||
end = abs_max + (-abs_max % 10)
|
||||
step = int((end - start) / 10)
|
||||
|
||||
yticks = list(range(start, end)[::step])
|
||||
yticks.append(end)
|
||||
return cls.__get_line_diagram(
|
||||
indexs, profits, [], yticks, [], [], (-1, length), (start, end), width
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def __calc_interaction_diagram_xy(cls,
|
||||
times: List[Tuple[str, Union[int, float]]],
|
||||
start: int,
|
||||
end: int) -> Tuple[List[int], List[int]]:
|
||||
"""
|
||||
根据互动时间列表计算互动曲线图中 x 轴和 y 轴数据
|
||||
|
||||
Args:
|
||||
times: 互动时间及权重列表
|
||||
start: 直播开始时间戳
|
||||
end: 直播结束时间戳
|
||||
|
||||
Returns:
|
||||
x 轴和 y 轴数据组成的元组
|
||||
"""
|
||||
count = 20
|
||||
step = (end - start) // count
|
||||
|
||||
times = [(int(x[0]), x[1]) for x in times]
|
||||
|
||||
divisions = [(start + i * step, start + (i + 1) * step - 1) for i in range(count)]
|
||||
divisions[-1] = (divisions[-1][0], end)
|
||||
results = [0] * count
|
||||
|
||||
times.sort(key=lambda x: x[0])
|
||||
for i in range(len(times) - 1, -1, -1):
|
||||
if times[i][0] <= end:
|
||||
times = times[:i + 1]
|
||||
break
|
||||
|
||||
for data in times:
|
||||
index = bisect.bisect_left([d[1] for d in divisions], data[0])
|
||||
results[index] += data[1]
|
||||
|
||||
result = [d[0] for d in divisions], results
|
||||
result[0].append(start + count * step)
|
||||
result[1].append(0)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def __get_interaction_diagram(cls,
|
||||
times: List[Tuple[str, Union[int, float]]],
|
||||
start: int,
|
||||
end: int,
|
||||
width: int) -> Image:
|
||||
"""
|
||||
绘制互动曲线图
|
||||
|
||||
Args:
|
||||
times: 互动时间及权重列表
|
||||
start: 直播开始时间戳
|
||||
end: 直播结束时间戳
|
||||
width: 互动曲线图图片宽度
|
||||
"""
|
||||
xs, ys = cls.__calc_interaction_diagram_xy(times, start, end)
|
||||
|
||||
xs = [x - start for x in xs]
|
||||
max_x = max(xs)
|
||||
|
||||
xs, ys = cls.__smooth_xy(xs, ys)
|
||||
max_y = math.ceil(max(ys))
|
||||
|
||||
xticks = [x * max_x // 4 for x in range(1, 4)]
|
||||
xticks.insert(0, 0)
|
||||
xticks.append(max_x)
|
||||
|
||||
xlabels = [timestamp_format(start + x * (end - start) // 4, "%H:%M:%S") for x in range(1, 4)]
|
||||
xlabels.insert(0, timestamp_format(start, "%H:%M:%S"))
|
||||
xlabels.append(timestamp_format(end, "%H:%M:%S"))
|
||||
|
||||
ystep = max(max_y // 10, 1)
|
||||
yticks = list(range(0, max_y)[::ystep])
|
||||
yticks.append(yticks[-1] + ystep)
|
||||
|
||||
return cls.__get_line_diagram(
|
||||
xs, ys, xticks, yticks, xlabels, [], (0, max_x), (0, yticks[-1]), width
|
||||
)
|
||||
|
@ -33,8 +33,16 @@ async def delete(key: str):
|
||||
|
||||
# List
|
||||
|
||||
async def lrange(key: str, start: int, end: int) -> List:
|
||||
return list(map(lambda x: x.decode(), await __redis.lrange(key, start, end)))
|
||||
async def lrange(key: str, start: int, end: int) -> List[str]:
|
||||
return [x.decode() for x in await __redis.lrange(key, start, end)]
|
||||
|
||||
|
||||
async def lrangei(key: str, start: int, end: int) -> List[float]:
|
||||
return [int(x) for x in await __redis.lrange(key, start, end)]
|
||||
|
||||
|
||||
async def lrangef1(key: str, start: int, end: int) -> List[float]:
|
||||
return [float("{:.1f}".format(float(x))) for x in await __redis.lrange(key, start, end)]
|
||||
|
||||
|
||||
async def rpush(key: str, value: Any):
|
||||
@ -61,6 +69,16 @@ async def hgetf1(key: str, hkey: Union[str, int]) -> float:
|
||||
return float("{:.1f}".format(float(result)))
|
||||
|
||||
|
||||
async def hgetalltuplei(key: str) -> List[Tuple[str, int]]:
|
||||
result = await __redis.hgetall(key)
|
||||
return [(x.decode(), int(result[x])) for x in result]
|
||||
|
||||
|
||||
async def hgetalltuplef1(key: str) -> List[Tuple[str, float]]:
|
||||
result = await __redis.hgetall(key)
|
||||
return [(x.decode(), float("{:.1f}".format(float(result[x])))) for x in result]
|
||||
|
||||
|
||||
async def hset(key: str, hkey: Union[str, int], value: Any):
|
||||
await __redis.hset(key, hkey, value)
|
||||
|
||||
@ -87,13 +105,13 @@ async def zrank(key: str, member: str) -> int:
|
||||
|
||||
|
||||
async def zrevrangewithscoresi(key: str, start: int, end: int) -> List[Tuple[str, int]]:
|
||||
return list(map(lambda x: (x[0].decode(), int(x[1])), await __redis.zrevrange(key, start, end, True)))
|
||||
return [(x[0].decode(), int(x[1])) for x in await __redis.zrevrange(key, start, end, True)]
|
||||
|
||||
|
||||
async def zrevrangewithscoresf1(key: str, start: int, end: int) -> List[Tuple[str, float]]:
|
||||
return list(map(
|
||||
lambda x: (x[0].decode(), float("{:.1f}".format(float(x[1])))), await __redis.zrevrange(key, start, end, True)
|
||||
))
|
||||
return [
|
||||
(x[0].decode(), float("{:.1f}".format(float(x[1])))) for x in await __redis.zrevrange(key, start, end, True)
|
||||
]
|
||||
|
||||
|
||||
async def zadd(key: str, member: str, score: Union[int, float]):
|
||||
|
Loading…
x
Reference in New Issue
Block a user