弹幕字号按画面短边自动定,竖屏成片不再字大到铺满整行

1. 字号基准从「画面高度」改成「画面短边」
   以前合并时只用高度比换算字号(字号 × 视频高 ÷ 弹幕文件高)。横屏弹幕文件
   (1920x1080、60 号)配竖屏视频(720x1280)时会被放大到 71 号,而画面只有
   720 宽,一条 10 字弹幕就铺满整行。横竖屏互换时高度不是约束文字宽度的维度,
   短边才是:横屏取高、竖屏取宽。

2. 「弹幕字号」填 0 的含义从「跟随弹幕文件」改成「自动按画面分辨率」
   取画布短边的 5.56%(60/1080)。于是 1920x1080 → 60、1280x720 → 40、
   960x540 → 30,横屏行为与以前完全一致;720x1280 → 40(以前 60)、
   640x1280 → 36。填大于 0 仍是精确像素覆盖。

3. 录制端竖屏档位预设 720x1280 的字号从 60 改成 40
   原来横屏三档都是短边的 5.56%,只有竖屏这档是 8.33%,比横屏大 50%。
   改完四档口径一致,新录的弹幕文件和成片也对得上。

4. 坐标系和字号拆开
   unifyDanmuSpace 只负责把 PlayRes 统一到视频分辨率,字号统一交给
   applyFontSize 按最终画布算,避免两处各改一次。

5. .gitignore 加上 /avatar_cache
   头像缓存挪到程序目录后,程序正好跑在仓库里,缓存目录会被 git 看见,
   git add . 会把那 336 个 PNG 一起提交进去。

已验证:
- 新增 TestShortSide / TestAutoFontSize / TestSubConfigsMatchAutoFontSize /
  TestApplyFontSize;TestUnifyDanmuSpace 改成只校验 PlayRes,并且新增断言
  确认它不再动字号。
- 反向验证:把短边规则临时退回「按高度」,TestAutoFontSize 报
  「720x1280 应为 40,实际 71」「1080x1920 应为 60,实际 107」,
  TestApplyFontSize、TestSubConfigsMatchAutoFontSize 同步失败。
- 端到端:造 720x1280 竖屏视频 + 1920x1080/60 号的 jsonl,字号填 0 真跑一次合并,
  日志打出「竖屏画布 720x1280,弹幕字号自动取 40」;同一输入再用旧规则跑一次对照,
  抽同一时刻的帧量墨迹高度:改前 46px、改后 25px(1.84 倍)。
- gofmt / go vet / go build / GOOS=linux go build / go test 全过。
This commit is contained in:
tamakyi
2026-09-26 00:59:17 +08:00
parent aa47dd342d
commit cbe5dfa982
8 changed files with 181 additions and 40 deletions
+112 -13
View File
@@ -123,32 +123,131 @@ func TestRelayoutFixesConcatenatedSegmentConflict(t *testing.T) {
}
}
// 弹幕坐标系(PlayRes)和视频分辨率不一致时统一到视频坐标系:
// 跟随文件时字号按高度比换算,用户填了字号就按成片像素直接用
// 画布短边:横屏取高、竖屏取宽
func TestShortSide(t *testing.T) {
cases := []struct {
w, h, want int
}{
{1920, 1080, 1080},
{1080, 1920, 1080},
{720, 1280, 720},
{1280, 720, 720},
{1080, 1080, 1080},
{0, 1080, 0},
{1920, -1, 0},
}
for _, c := range cases {
if got := shortSide(c.w, c.h); got != c.want {
t.Errorf("shortSide(%d,%d) 应为 %d,实际 %d", c.w, c.h, c.want, got)
}
}
}
// 自动字号 = 画布短边的 5.56%。竖屏必须按宽度算 ——
// 按高度算的话 720x1280 会得到 71,一条弹幕就铺满整行。
func TestAutoFontSize(t *testing.T) {
cases := []struct {
vw, vh, want int
}{
{1920, 1080, 60},
{1280, 720, 40},
{960, 540, 30},
{720, 1280, 40},
{640, 1280, 36},
{1080, 1920, 60},
{1080, 1080, 60}, // 方形按宽算
{0, 1080, 0}, // 拿不到尺寸
{1920, 0, 0},
}
for _, c := range cases {
if got := autoFontSize(c.vw, c.vh); got != c.want {
t.Errorf("autoFontSize(%d,%d) 应为 %d,实际 %d", c.vw, c.vh, c.want, got)
}
}
}
// 录制端的档位预设必须和自动字号规则一致,否则录出来的弹幕文件和成片对不上
func TestSubConfigsMatchAutoFontSize(t *testing.T) {
for key, cfg := range subConfigs {
want := autoFontSize(cfg.PlayResX, cfg.PlayResY)
if cfg.FontSize != want {
t.Errorf("档位 %d(%dx%d)字号应为 %d,实际 %d",
key, cfg.PlayResX, cfg.PlayResY, want, cfg.FontSize)
}
}
}
// 主流程:先把坐标系统一到视频分辨率,再按画布定字号
func TestApplyFontSize(t *testing.T) {
// 横屏弹幕文件 + 竖屏成片:以前按高度比会放大到 71,现在按短边取 40
meta := danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 60}
unifyDanmuSpace(&meta, 720, 1280)
applyFontSize(&meta, 0)
if meta.FontSize != 40 {
t.Errorf("竖屏成片字号应为 40,实际 %d", meta.FontSize)
}
// 横屏成片和弹幕文件一致时保持原样
meta = danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 60}
unifyDanmuSpace(&meta, 1920, 1080)
applyFontSize(&meta, 0)
if meta.FontSize != 60 {
t.Errorf("1080p 横屏字号应为 60,实际 %d", meta.FontSize)
}
// 竖屏弹幕文件 + 同尺寸竖屏成片:老文件写的 60 要归一到 40
meta = danmuMeta{PlayResX: 720, PlayResY: 1280, FontSize: 60}
unifyDanmuSpace(&meta, 720, 1280)
applyFontSize(&meta, 0)
if meta.FontSize != 40 {
t.Errorf("竖屏弹幕文件应归一到 40,实际 %d", meta.FontSize)
}
// 设置里填了数就按填的来(成片像素)
meta = danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 60}
unifyDanmuSpace(&meta, 720, 1280)
applyFontSize(&meta, 33)
if meta.FontSize != 33 {
t.Errorf("填了 33 就该用 33,实际 %d", meta.FontSize)
}
// 拿不到画布尺寸时保留弹幕文件里的字号
meta = danmuMeta{FontSize: 48}
applyFontSize(&meta, 0)
if meta.FontSize != 48 {
t.Errorf("拿不到尺寸时应保留 48,实际 %d", meta.FontSize)
}
// 低分辨率也别算出 0 号字
meta = danmuMeta{PlayResX: 4, PlayResY: 4, FontSize: 30}
applyFontSize(&meta, 0)
if meta.FontSize < 1 {
t.Errorf("字号不能小于 1,实际 %d", meta.FontSize)
}
}
// 弹幕坐标系(PlayRes)和视频分辨率不一致时统一到视频坐标系;
// 字号不在这里改,由 applyFontSize 按最终画布决定
func TestUnifyDanmuSpace(t *testing.T) {
cases := []struct {
name string
resX, resY, fontSize int
vw, vh int
override int
wantChanged bool
wantFontSize int
}{
{"坐标系一致不动", 1920, 1080, 60, 1920, 1080, 0, false, 60},
{"960x540 弹幕配 720x1280 视频(跟随文件)", 960, 540, 30, 720, 1280, 0, true, 71},
{"同上但用户填了 42", 960, 540, 30, 720, 1280, 42, true, 42},
{"手机档 720x1280 弹幕配 540x960 视频", 720, 1280, 60, 540, 960, 0, true, 45},
{"探测不到视频尺寸就不动", 960, 540, 30, 0, 0, 0, false, 30},
{"相同宽高比但不同分辨率也要按视频走", 1280, 720, 40, 1920, 1080, 0, true, 60},
{"坐标系一致不动", 1920, 1080, 60, 1920, 1080, false},
{"960x540 弹幕配 720x1280 视频", 960, 540, 30, 720, 1280, true},
{"探测不到视频尺寸就不动", 960, 540, 30, 0, 0, false},
{"相同宽高比但不同分辨率也要按视频走", 1280, 720, 40, 1920, 1080, true},
}
for _, c := range cases {
meta := danmuMeta{PlayResX: c.resX, PlayResY: c.resY, FontSize: c.fontSize}
changed := unifyDanmuSpace(&meta, c.vw, c.vh, c.override)
changed := unifyDanmuSpace(&meta, c.vw, c.vh)
if changed != c.wantChanged {
t.Errorf("%s:changed 应为 %v,实际 %v", c.name, c.wantChanged, changed)
}
if meta.FontSize != c.wantFontSize {
t.Errorf("%s:字号应为 %d,实际 %d", c.name, c.wantFontSize, meta.FontSize)
if meta.FontSize != c.fontSize {
t.Errorf("%s:统一坐标系不该动字号,应为 %d,实际 %d", c.name, c.fontSize, meta.FontSize)
}
if changed {
if meta.PlayResX != c.vw || meta.PlayResY != c.vh {