Skip to content

feat(danmaku): merge duplicate danmaku, zh conversion, local file import, and renderer perf - #3255

Draft
NihilDigit wants to merge 10 commits into
open-ani:mainfrom
NihilDigit:feat/danmaku-improvements
Draft

feat(danmaku): merge duplicate danmaku, zh conversion, local file import, and renderer perf#3255
NihilDigit wants to merge 10 commits into
open-ani:mainfrom
NihilDigit:feat/danmaku-improvements

Conversation

@NihilDigit

Copy link
Copy Markdown
Member

WIP

🤖 Generated with Claude Code

NihilDigit and others added 10 commits August 1, 2026 18:59
seek 时 repopulate 会一次性对整个时间窗口的弹幕做 TextMeasurer.measure,
在主线程上形成测量风暴. 现在每放置 20 条就在帧边界让出一次
(没有 MonotonicFrameClock 时退化为 yield), 把单帧的测量量限制在有界范围内.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
描边和填充的排版结果完全一致, 之前对同一段文本做了两次 TextMeasurer.measure.
现在只测量一次, 光栅化时用 drawText 的 drawStyle 覆盖分两遍绘制.

同时把位图边距从固定的 height/2 改为按实际需要计算 (描边宽度的一半 +
阴影模糊半径和偏移, 再加 2px 余量). 默认样式下一条 w×h 弹幕的位图从
(w+h)×(2h) 缩小到 (w+8)×(h+8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
内容和样式相同的弹幕现在共用同一份排版结果和光栅化位图, 刷屏弹幕只会测量和
光栅化一次. 缓存按 (文本, 解析后的 TextStyle, 描边颜色/宽度, 阴影) 为 key,
每个 DanmakuHostState 一份, 256 条 LRU. 拖动样式滑块也不会再一次性产生
整整一屏的孤儿位图.

StyledDanmaku 直接持有 entry 引用, 绘制路径上只有一次字段读取, 无查表无分配.

顺带把 trySend 里的 StyledDanmaku 构造移进 Dispatchers.Main.immediate:
TextMeasurer 和缓存都不是线程安全的, 现在测量统一在 UI 线程上做.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Modifier.alpha 会给整个 Canvas 开一层全屏离屏合成层. 改成把 alpha 传给每次
drawImage, 省掉这层合成, 同时为将来的逐条弹幕 alpha (淡入淡出) 留出空间.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
参考 pakku.js, 但只按"归一化之后完全相同"判定重复, 不做编辑距离/拼音相似度.

- DanmakuTextNormalizer: 全角转半角, 折叠空白, 小写化, 去掉结尾语气标点.
  全部由标点组成的弹幕归一化后为空串, 视为独一无二, 不参与合并.
- DanmakuMerger: 在时间窗口 (默认 40s, 以簇内第一条为锚点) 内把归一化文本
  相同的弹幕合并成一条, 保留第一条的时间/颜色/位置, 文本后追加 ×N.
- 作为 TimeBasedDanmakuSession.create 的预处理步骤, 纯 list-in/list-out.
- 设置项 DanmakuFilterConfig.enableMerge (默认关闭), 经
  GetDanmakuPreprocessConfigFlowUseCase 流到 EpisodeDanmakuLoader,
  播放器内弹幕设置面板新增「合并重复弹幕」开关.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
三态设置: 关闭 / 转换为简体 / 转换为繁体.

- ZhConversionTable: 从 OpenCC 的 TSCharacters/STCharacters 抽取的单字映射表
  (繁->简 2966 组, 简->繁 2712 组), 以成对字符串形式内嵌, 只做单字映射不分词.
- 在 DanmakuPreprocessor 中于合并之前做转换, 这样同一句话的简繁两种写法可以合并.
- 过滤时把正则也转换成同一种字形 (转换表只含 CJK 字符, 不影响正则元字符),
  于是用简体写的屏蔽词也能拦住繁体弹幕.
- DanmakuCollection.at 的参数由 Flow<List<String>> 改为 Flow<DanmakuFilterSpec>,
  GetDanmakuRegexFilterListFlowUseCase 相应改名为 GetDanmakuFilterSpecFlowUseCase.
- 设置项 DanmakuFilterConfig.zhConversion (默认 NONE), 播放器内弹幕设置面板
  新增「简繁转换」下拉选择.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…hadow

- DanmakuFilterer: 会话内长期持有, 按 pattern 缓存已编译的 Regex, 弹幕列表每次变化
  不再重新编译一遍正则; 同时缓存归一化后的关键词.
- 纯文本关键词屏蔽 (upstream open-ani#919): 归一化后按子串匹配, 忽略大小写/全半角/结尾标点,
  并跟随简繁转换设置. 存在 DanmakuFilterConfig.keywordBlocklist,
  播放器内「正则管理」侧边栏和全局设置的过滤器分组都可以增删.
- DanmakuConfigSerializer 补上 DanmakuStyle.shadow 的序列化 (旧配置里没有该字段,
  默认为 null, 可正常反序列化). strokeColor 之前已经序列化了.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
单字表转不对需要看上下文的字, 比如 "头发" 会变成 "頭發", "饼干" 会变成 "餅幹".
现在在单字表之上加一层词组最长匹配.

- ZhPhraseConversionTable: 从 OpenCC 的 STPhrases/TSPhrases 生成. OpenCC 原始词典里
  绝大多数条目是冗余的 (逐字转换本来就对), 只保留"逐字转换结果与词组结果不一致"的
  纠正性条目, 简->繁 49174 条筛剩 9734 条, 繁->简 477 条筛剩 410 条.
  按词组首字分桶, 桶内按长度倒序排列, 顺序扫到的第一个匹配即最长匹配.
- ZhConverter: 每个位置先查词组表取最长匹配, 没匹配上再回落单字表. 查词组走
  regionMatches, 匹配不上的位置不产生分配, 保持"整句都不用转换就不分配"的快路径.
  公开 API 不变.
- scripts/generate-zh-phrase-table.py: 可重复运行的生成脚本. 过滤针对已提交的单字表
  进行 (脚本只读不改它), 否则会漏掉或多留条目.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds DanmakuFileParser in danmaku/api: a pure, IO-free parser for the two
danmaku file formats users are likely to have on disk. Format is detected from
the content (first non-whitespace char) rather than the file extension.

Mode mapping is shared by both formats: 1-3 and 6 map to NORMAL, 4 to BOTTOM,
5 to TOP; 7/8/9 (advanced/code/BAS danmaku) are skipped since the renderer
cannot draw them. Parsing is best-effort — a malformed entry is skipped and
counted, only an unrecognizable file or a file with zero usable danmaku fails.

Also adds DanmakuServiceId.LocalFile and DanmakuProviderId.LocalFile so
imported danmaku can occupy their own slot in the loader.

Test fixtures are a 152-entry sample of a real Bilibili XML (spam-heavy, mixed
modes and colors, full-width and Japanese text) plus the same data converted to
dandanplay JSON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds an 「导入弹幕文件…」 chip to the per-episode danmaku source list. Picking a
file (FileKit, same launcher pattern as the avatar picker) reads and parses it,
then hands the danmaku to EpisodeDanmakuLoader.setImportedDanmaku.

The import occupies its own DanmakuProviderId.LocalFile slot in DanmakuLoader,
so it lands in the source list as a normal source: it gets an enable toggle, a
time-shift adjustment, a count on its chip, and it goes through the same
sanitize/zh-convert/merge/sort pipeline as fetched danmaku. Re-importing
replaces the previous file; switching episodes clears it.

Imported danmaku stay in memory for the session — they are excluded from the
Room write-back so a test file can't poison the offline cache.

Part of open-ani#1856

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant