两个缺陷,都由上一轮的验收机制暴露:
1) 编码取整与契约参考实现不一致(T-WIN-022)
KeypointFrame.encode() 用 GDScript 的 roundi(半数远离零),而契约参考实现
(Web 的 keypoint-codec.ts)与**生产发送方 Android**(Kotlin Math.round)都是
「半数朝 +∞」:-2.5 分别是 -3 与 -2。实测在 12 个跨实现向量里 1 例 33 字节不同。
本端是接收方、encode() 只用于自测与协议回归,对产品无影响;但它让「跨实现一致性」
只能停留在宽松口径("差异仅限 .5 取整边界"),而宽松口径会退化成没人看的已知差异。
修复:新增 _round_like_js(v) = floor(v + 0.5),用于 4 处取整(坐标 ×3 + 置信度);
夹紧顺序对任意实数与参考实现等价。
收紧:accept-frame-vectors.sh 的编码口径改为「必须为 0 项差异」。
新增单测(+11 断言):_round_like_js 的 5 个取值、端到端 -0.5/32767 → 0(roundi
会给 -1,是有效判别用例),以及 test_reference_vectors_decode_and_encode_exactly
—— 直接读锁定的参考向量逐值解码 + 用 f32InputPoints 重编码与 f32Hex 逐字节比对,
让 verify.sh(只跑源码、无需 pck)也覆盖「Web ↔ Windows」这条边。
2) 产物里夹带开发/验收工具(T-WIN-023)
export_filter="all_resources" 且 exclude_filter 没有 build/*,于是每次新增
构建/验收脚本都被编译进 pck —— 包括**故意不合规**的坏服务端夹具 probe-ws-bad.gd
与 frame-vectors.json。实测 OpenDance.pck 315,464 B →(排除后)145,400 B,
即 54% 的 pck 是测试夹具。已发布的 v0.2.3 同样带着这些残留。
修复:exclude_filter 增加 build/*;
新增产物级防线 build/integration/list-pck.gd(遍历包内条目、标出
res://build/ tests/ _scratch/ dist-release/ 与 *.md 残留,有残留即退出码 1),
并接进 accept-release.sh(打印条目总数并断言「无开发残留」)——否则下次加个新
工具目录又会重演。顺带确认 pck 没有把上一次导出的 pck 自嵌套进去。
实测(v0.2.4 发布包,三条产物级验收全绿):
* accept-release.sh:pck 58 项无开发残留 / PE 版本资源 0.2.4 / pck 启动 180 帧无错 /
运行时探针(包内版本号 0.2.4、AboutScreen 显示值、15 项关键资源、契约表比对)
* accept-ws.sh:客户端 30 通过 / 0 失败 + 服务端侧 16 项事实
* accept-frame-vectors.sh:解码 12/12、396 点 0 差异,
**编码器字节与参考实现完全一致(0 项差异)**
* verify.sh:785 通过 / 0 失败(原 774)
踩坑:给 -0.5/32767 这类值写断言时,第一版按"理想有理数"期望(2.5/32767 → 3)而
失败 —— PackedVector3Array 存的是 float32,乘回 32767 后略小于半整数,与 JS 对同一
float32 输入的行为其实完全一致。断言必须基于实际存储精度,或改用有效判别用例。
155 lines
7.1 KiB
Bash
Executable File
155 lines
7.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ============================================================
|
||
# OpenDance Studio Windows 客户端 —— **关键点帧跨实现一致性验收**(T-WIN-021)
|
||
#
|
||
# opendance-web 的 TypeScript 参考实现(契约 §2)× 发布包里的 GDScript 解码器
|
||
#
|
||
# 为什么需要它:`web-server/src/preprocess/keypoint-codec.ts` 是契约 §2 的
|
||
# **参考实现**(其 `decodeBinaryFrame` 的注释就是「Windows 端参考实现(T-WIN-004)」)。
|
||
# Android 的 Kotlin 实现已用「金标准字节向量」与它对齐;而**本端的 GDScript 解码器
|
||
# 此前只跟自己比过**(自己编码、自己解码)。本脚本把这条缺失的边补上:
|
||
#
|
||
# 1. 用**参考实现**重新生成向量,与仓库里锁定的 `build/integration/frame-vectors.json`
|
||
# 比对 —— 参考实现一旦变化,这里必须失败(否则"锁定"就没有意义);
|
||
# 2. 用**发布包里的 pck** 解码同一批字节,与参考实现的解码结果**逐值精确比对**
|
||
# (float32 表示精度内);
|
||
# 3. 附带比对两边**编码器**的字节:本端不是发送方(`encode()` 只用于自测),
|
||
# 因此只归类记录 —— 但差异必须**只出现在量化前恰好落在 .5 的用例**上。
|
||
#
|
||
# 用法:
|
||
# GODOT_BIN=/path/to/godot bash build/accept-frame-vectors.sh dist-release/opendance-windows-v0.2.3.zip
|
||
# ... --skip-regen # 不重新生成向量(没有 web 仓 / 没装 node_modules 时)
|
||
#
|
||
# 环境变量:
|
||
# WEB_REPO opendance-web 仓库路径(默认 ../opendance-web)
|
||
# WORK_DIR 工作目录(默认 mktemp -d)
|
||
# ============================================================
|
||
set -uo pipefail
|
||
|
||
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||
cd "$PROJECT_DIR"
|
||
|
||
TARGET="${1:-}"
|
||
SKIP_REGEN=""
|
||
for arg in "$@"; do
|
||
case "$arg" in
|
||
--skip-regen) SKIP_REGEN=1 ;;
|
||
"$TARGET") : ;;
|
||
*) echo "✗ 未知参数:$arg(只支持 --skip-regen)" >&2; exit 2 ;;
|
||
esac
|
||
done
|
||
|
||
WEB_REPO="${WEB_REPO:-$PROJECT_DIR/../opendance-web}"
|
||
WORK_DIR="${WORK_DIR:-$(mktemp -d)}"
|
||
mkdir -p "$WORK_DIR"
|
||
VECTORS="$PROJECT_DIR/build/integration/frame-vectors.json"
|
||
REPORT="$WORK_DIR/frame-vectors-report.json"
|
||
|
||
FAILED=0
|
||
log() { printf '\033[1;34m[accept-fv]\033[0m %s\n' "$*"; }
|
||
fail() { printf '\033[1;31m[accept-fv] ✗ %s\033[0m\n' "$*" >&2; FAILED=1; }
|
||
|
||
if [ -z "$TARGET" ] || [ ! -e "$TARGET" ]; then
|
||
echo "用法:GODOT_BIN=... bash build/accept-frame-vectors.sh <发布包 zip 或已解压目录> [--skip-regen]" >&2
|
||
exit 2
|
||
fi
|
||
[ -f "$VECTORS" ] || { fail "找不到锁定的向量文件:$VECTORS"; exit 1; }
|
||
|
||
# ---- 1. 用参考实现重新生成并比对(确保向量仍与参考实现一致)----
|
||
log "参考实现:$WEB_REPO/web-server/src/preprocess/keypoint-codec.ts"
|
||
if [ -n "$SKIP_REGEN" ]; then
|
||
log "跳过重新生成(--skip-regen)"
|
||
elif [ ! -f "$WEB_REPO/web-server/scripts/gen-frame-vectors.mts" ]; then
|
||
fail "找不到生成脚本($WEB_REPO/web-server/scripts/gen-frame-vectors.mts)—— 用 WEB_REPO 指定仓库,或 --skip-regen"
|
||
elif [ ! -d "$WEB_REPO/web-server/node_modules" ]; then
|
||
fail "web 仓没有 node_modules,无法运行参考实现(先在那边 npm install,或 --skip-regen)"
|
||
else
|
||
log "用参考实现重新生成向量并比对…"
|
||
REGEN="$WORK_DIR/frame-vectors.regen.json"
|
||
if ( cd "$WEB_REPO/web-server" && timeout 300 npx vite-node scripts/gen-frame-vectors.mts -- --out "$REGEN" >"$WORK_DIR/regen.log" 2>&1 ); then
|
||
if diff -q "$VECTORS" "$REGEN" >/dev/null; then
|
||
echo " ✓ 锁定的向量与参考实现重新生成的结果逐字节一致"
|
||
else
|
||
fail "锁定的向量已与参考实现不一致(参考实现变了却没同步向量)"
|
||
diff "$VECTORS" "$REGEN" | head -20 | sed 's/^/ /'
|
||
fi
|
||
else
|
||
fail "参考实现生成失败(见 $WORK_DIR/regen.log)"
|
||
tail -10 "$WORK_DIR/regen.log" | sed 's/^/ /'
|
||
fi
|
||
fi
|
||
|
||
# ---- 2. 取包内 pck ----
|
||
if [ -f "$TARGET" ]; then
|
||
log "解压发布包…"
|
||
if ! unzip -qq -o "$TARGET" -d "$WORK_DIR" 2>/dev/null; then
|
||
command -v 7z >/dev/null 2>&1 && 7z x -y -o"$WORK_DIR" "$TARGET" >/dev/null || { fail "解压失败"; exit 1; }
|
||
fi
|
||
PACKAGE_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)"
|
||
else
|
||
PACKAGE_DIR="$TARGET"
|
||
fi
|
||
PCK="$PACKAGE_DIR/OpenDance.pck"
|
||
[ -f "$PCK" ] || { fail "找不到包内 pck:$PCK"; exit 1; }
|
||
log "包内 pck:$PCK"
|
||
|
||
GODOT_BIN="${GODOT_BIN:-}"
|
||
if [ -z "$GODOT_BIN" ]; then
|
||
for candidate in godot godot4 Godot; do
|
||
command -v "$candidate" >/dev/null 2>&1 && { GODOT_BIN="$candidate"; break; }
|
||
done
|
||
fi
|
||
[ -n "$GODOT_BIN" ] || { fail "未找到 Godot(用 GODOT_BIN=<路径> 指定,需 4.7.x)"; exit 1; }
|
||
|
||
# ---- 3. 用包内解码器逐值比对 ----
|
||
log "包内解码器 × 参考向量…"
|
||
PROBE_LOG="$WORK_DIR/probe.log"
|
||
rm -f "$REPORT"
|
||
if timeout 300 "$GODOT_BIN" --headless --main-pack "$PCK" \
|
||
--script "$PROJECT_DIR/build/integration/probe-frame-vectors.gd" \
|
||
-- --vectors "$VECTORS" --report "$REPORT" >"$PROBE_LOG" 2>&1; then
|
||
PROBE_OK=1
|
||
else
|
||
PROBE_OK=0
|
||
fi
|
||
grep -vE "^Godot Engine|^$" "$PROBE_LOG" | sed 's/^/ /'
|
||
[ "$PROBE_OK" = 1 ] || fail "解码比对未通过"
|
||
|
||
# ---- 4. 报告核对 ----
|
||
if [ ! -f "$REPORT" ]; then
|
||
fail "探针没有产出报告($REPORT)"
|
||
exit 1
|
||
fi
|
||
log "报告核对:"
|
||
node -e '
|
||
const fs = require("node:fs");
|
||
const r = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
|
||
let bad = 0;
|
||
const say = (ok, text) => { console.log(` ${ok ? "✓" : "✗"} ${text}`); if (!ok) bad = 1; };
|
||
say(r.cases > 0 && r.decodedCases === r.cases, `解码用例 ${r.decodedCases}/${r.cases}`);
|
||
say(r.pointsCompared === r.cases * 33, `逐点比对 ${r.pointsCompared} 个(期望 ${r.cases * 33})`);
|
||
say(Array.isArray(r.decodeFailures) && r.decodeFailures.length === 0,
|
||
`解码差异 ${r.decodeFailures?.length ?? "?"} 项(必须为 0)`);
|
||
if (r.decodeFailures?.length) for (const f of r.decodeFailures.slice(0, 5)) console.log(` · ${f.case}:${f.problem}`);
|
||
// 编码器差异:v0.2.4 起本端 `_round_like_js`(floor(x+0.5))已与参考实现的
|
||
// `Math.round` 对齐,因此**必须为 0**。原先允许「仅限 .5 取整边界」的宽松口径已收紧:
|
||
// 宽松口径下,规则性偏差会长期以「已知差异」的身份留在日志里,没人会再看它。
|
||
const diffs = r.encodeDifferences ?? [];
|
||
say(diffs.length === 0,
|
||
diffs.length === 0
|
||
? "编码器字节与参考实现完全一致(0 项差异)"
|
||
: `编码器差异 ${diffs.length} 项(必须为 0):${diffs.map((d) => d.case + "(" + d.diffBytes + "B," + (d.halfValueInput ? "取整边界" : "非取整边界") + ")").join("、")}`);
|
||
process.exit(bad);
|
||
' "$REPORT" || fail "报告核对未通过"
|
||
|
||
echo ""
|
||
if [ "$FAILED" -ne 0 ]; then
|
||
echo "============================================================"
|
||
echo " 关键点帧跨实现一致性验收失败(工作目录:$WORK_DIR)"
|
||
echo "============================================================"
|
||
exit 1
|
||
fi
|
||
echo "============================================================"
|
||
echo " 关键点帧跨实现一致性验收全部通过(工作目录:$WORK_DIR)"
|
||
echo "============================================================"
|