diff --git a/DSH.md b/DSH.md index e77fc90..0361cc1 100644 --- a/DSH.md +++ b/DSH.md @@ -1,4 +1,4 @@ -# DSH.md —— opendance-android(OpenDance Studio 项目三:Android 辅助 App) +# DSH.md -- opendance-android(OpenDance Studio 项目三:Android 辅助 App) ## 0. 本文件的作用 @@ -17,12 +17,12 @@ WebSocket 推给 Windows 客户端。 ws://:8765/pose ``` -**画面不离开设备** —— 只有关键点坐标经网络发送。 +**画面不离开设备** -- 只有关键点坐标经网络发送。 ## 2. 技术栈 | 组件 | 选型 | -|---|---| +|--|--| | 语言 | Kotlin 2.0.21 | | 构建 | AGP 8.7.3 + Gradle 8.9 + JDK 17 | | 相机 | CameraX 1.4.0 (Apache 2.0) | @@ -50,7 +50,7 @@ WebSocket 推给 Windows 客户端。 ## 4. 上下文预算 | 项 | 建议值 | -|---|---| +|--|--| | 单任务文件数 | ≤ 6 | | 单任务代码行数 | ≤ 1500 | | 单任务 Token | ≤ 12K | @@ -91,7 +91,7 @@ opendance-android/ 也是与「上传视频做云端推理」方案的本质区别。 2. **239 字节帧格式冻结**。任何字段改动都要先改 `opendance-web` 的 `contracts/websocket-keypoints.md` 并提升版本。 - 本端序列化器有金标准字节向量测试守护(见 §8)。 + 本端序列化器有金标准字节向量测试守护(见 8)。 3. **33 点顺序冻结**。MediaPipe Pose 的原始索引顺序**就是**契约顺序, 不得重排或插入。 4. **镜像语义**:发送**未镜像**的原始坐标,是否镜像由 `flags.bit0` 声明; @@ -103,14 +103,18 @@ opendance-android/ ## 7. 验证(每任务执行) ```bash -export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 # AGP 8.7 需要 JDK 17 -export ANDROID_HOME=/path/to/android-sdk +export JAVA_HOME= # AGP 8.7 需要 JDK 17 +export ANDROID_HOME= ./gradlew testDebugUnitTest # JVM 单元测试,无需设备 bash build/build-apk.sh all # debug + release APK,并校验签名与产物 bash build/verify.sh # 一键跑完上述全部并附加产物校验 ``` +首次部署(前置条件、本地配置、安装到手机、配置联通、自检清单、常见问题) +见 `README.md` 的「初次部署(从零跑起来)」与 `README.txt` 对应小节。 +文档中的地址、路径、口令一律使用占位符,真实值不得写入仓库。 + ## 8. 跨端一致性验证(本项目最重要的测试) `KeypointFrameTest.goldenVectorMatchesWebImplementation` 使用 @@ -125,7 +129,7 @@ Kotlin(本端)。任何一端私自改动字段布局都会立刻失败。 ## 9. 上下文超标时的拆分策略 | 超标原因 | 拆分方式 | -|---|---| +|--|--| | 单任务涉及多个界面 | 按界面拆(连接/运行/关于) | | 单文件过大 | 按类拆,先生成接口再分实现 | | 契约全文加载 | 改用锚点引用 | diff --git a/README.md b/README.md index c76ce37..b4348e5 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,11 @@ OpenDance Studio **阶段三**:Android 辅助 App(Kotlin + CameraX + MediaPi ## 状态 -**阶段三已完成**:T-AND-001 → T-AND-009 全部落地。 +**阶段三已完成**:T-AND-001 -> T-AND-009 全部落地。 | 验收项 | 结果 | -|---|---| -| 单元测试 | ✓ **25 项全部通过**(JVM,无需设备) | +|--|--| +| 单元测试 | ✓ **30 项全部通过**(JVM,无需设备) | | **跨端金标准** | ✓ Kotlin 序列化器与 Web 端 TypeScript 实现**逐位一致** | | `./gradlew assembleRelease` | ✓ 生成**已签名** APK(28.6 MB) | | 签名校验 | ✓ `apksigner` 退出码 0,v2 方案,CN=OpenDance Studio Dev | @@ -28,17 +28,184 @@ export JAVA_HOME=/path/to/jdk17 # AGP 8.7.3 要求 export ANDROID_HOME=/path/to/android-sdk bash build/gen-keystore.sh # 生成发布签名(首次;不入库,请自行备份) -bash build/verify.sh # 单元测试 → 构建 → 产物校验 +bash build/verify.sh # 单元测试 -> 构建 -> 产物校验 bash build/build-apk.sh all # 仅构建 debug + release bash build/package-release.sh # 打发布包 ``` -详细使用说明见 [`README.txt`](README.txt),工程约束见 [`DSH.md`](DSH.md)。 +详细使用说明见 [`README.txt`](README.txt),工程约束见 [`DSH.md`](DSH.md), +第一次上手请看下文的「初次部署(从零跑起来)」一节。 + +## 初次部署(从零跑起来) + +本节面向**第一次拿到本仓库**、要在真机上把 App 跑起来的人。 +下文出现的地址、路径、口令一律是**占位符或文档示例值**,请替换为你自己的值, +并确保这些值**不要提交进仓库**。 + +### 1. 前置条件 + +| 项 | 要求 | 依据 | +|--|--|--| +| JDK | 17 或更高(AGP 8.7.3 要求;`build/build-apk.sh` 会校验主版本 >= 17) | `gradle.properties` / `build/build-apk.sh` | +| Gradle | 8.9,仓库自带 wrapper(`./gradlew`),本机无需另装 | `gradle/wrapper/gradle-wrapper.properties` | +| Android SDK | `compileSdk 34` / `minSdk 29`(Android 10+)/ `targetSdk 34`;需 `platforms;android-34` 与 `build-tools;34.0.0` | `app/build.gradle.kts` | +| 设备 | **需要真机**(Android 10 及以上、带摄像头);模拟器只作有限调试 | `app/build.gradle.kts` 的 ABI 过滤 | + +真机与模拟器的差别: + +- release 包只打包 `arm64-v8a` + `armeabi-v7a`,**无法安装到 x86/x86_64 模拟器**。 +- debug 包额外含 `x86`/`x86_64`,可以装进模拟器,但模拟器相机是虚拟场景, + CameraX 取到的是合成画面,MediaPipe 的姿态识别效果不能代表真机, + 也不能作为「第一次跑通」的验收依据。 +- 结论:**姿态识别与联调验收请用真机**。 + +权限:运行页需要**相机权限**(运行时授予)。另外 `INTERNET`、`ACCESS_NETWORK_STATE`、 +`ACCESS_WIFI_STATE` 为普通权限,安装时自动授予,用于 WebSocket 推流与局域网扫描。 + +### 2. 获取代码与本地配置 + +```bash +git clone https:////opendance-android.git +cd opendance-android +``` + +`local.properties` 是**本地文件**(已在 `.gitignore` 中,不会入库), +只需写 Android SDK 路径: + +```properties +sdk.dir= +``` + +`build/build-apk.sh` 在该文件缺失时会依据 `ANDROID_HOME` / `ANDROID_SDK_ROOT` +自动生成它;也可以手动创建。**不要提交它**,每台机器路径不同。 + +签名材料同样**不入库**(`.gitignore` 已忽略 `keystore/`、`*.jks`、`*.keystore`、 +`keystore.properties`): + +- 仓库里**没有私钥**,只有生成脚本与说明。 +- 首次发布前执行 `bash build/gen-keystore.sh`,生成 + `keystore/opendance-release.jks` 与 `keystore.properties` + (含 `storeFile`、`keyAlias` 与两个口令字段)。**请自行备份,且不要提交**。 +- `app/build.gradle.kts` 的签名取值优先级: + `keystore.properties` -> 环境变量(`KEYSTORE_PATH` / `KEYSTORE_PASSWORD` / + `KEY_ALIAS` / `KEY_PASSWORD`)-> 回退 debug 签名。 + 回退只为让 CI 也能产出已签名 APK;正式发布必须提供自己的 keystore。 + +### 3. 首次构建 + +```bash +export JAVA_HOME= +export ANDROID_HOME= + +# 方式一:仓库脚本(推荐,构建后自动校验产物) +bash build/build-apk.sh debug # 仅 debug +bash build/build-apk.sh release # 仅 release +bash build/build-apk.sh all # 两者都构建 + +# 方式二:直接调用 Gradle +./gradlew assembleDebug +./gradlew assembleRelease +``` + +产物路径与文件名(固定): + +| 变体 | 产物 | 说明 | +|--|--|--| +| debug | `app/build/outputs/apk/debug/app-debug.apk` | applicationId 带 `.debug` 后缀,含 x86/x86_64 | +| release | `app/build/outputs/apk/release/app-release.apk` | 仅 arm64-v8a + armeabi-v7a,开启 R8 | + +发布整包(APK + `THIRD_PARTY.md` + `README.txt` + `contracts/`)用 +`bash build/package-release.sh`,输出在 `dist-release/`。 + +### 4. 安装到手机 + +```bash +# 用 adb(手机需开启 USB 调试) +adb install -r app/build/outputs/apk/debug/app-debug.apk +adb install -r app/build/outputs/apk/release/app-release.apk +``` + +也可以直接把 APK 拷到手机,在系统设置中允许「安装未知来源应用」后点击安装。 + +首次进入运行页会弹出**相机权限**请求;拒绝后运行页显示 +「需要相机权限才能进行姿态识别」,可点「授予相机权限」重新申请, +或到系统「设置 -> 应用 -> OpenDance -> 权限」中手动开启。 + +### 5. 配置与联通(填 Windows 客户端地址) + +本 App **只连接 Windows 客户端,不访问 Web 服务端**:地址由 +`AppSettings.webSocketUrl()` 拼成 `ws://:/pose`,子协议 `opendance.pose.v1`。 + +连接页字段(对应 `data/AppSettings.kt`): + +| 界面字段 | 设置项 | 默认值 | 说明 | +|--|--|--|--| +| IP 地址 | `host` | RFC 5737 文档网段占位地址(源码为 `192.0.2.100`,**非真实主机**) | 填 Windows 电脑的局域网 IP | +| 端口 | `port` | `8765` | 与 Windows 客户端监听端口一致 | +| 目标帧率(FPS) | `targetFps` | `30` | 取值范围 5~60 | +| 分辨率 | `resolution` | `640x480` | 可选 `480x360` / `640x480` / `1280x720` | +| 镜像画面 | `mirror` | 开 | 只影响显示与 `flags.bit0`,发送的是未镜像坐标 | +| 仅上半身模式 | `upperBodyOnly` | 关 | 下半身置信度置 0,接收端插值 | + +填写示例(占位符,请替换): + +``` +IP 地址:192.0.2.10 # RFC 5737 文档示例网段,实际填你电脑的局域网 IP +端口: 8765 +``` + +不想手填 IP 时,可点「扫描局域网」:App 取本机 WiFi IPv4 地址后推导 `/24` 网段, +以 32 并发、单机 300ms 超时探测该网段内开放 8765 端口的主机(约 2~3 秒), +结果按延迟升序展示,点候选即可填入。也可以先点「测试连接」确认端口可达。 + +### 6. 第一次跑通 + +数据流是**单向**的:`手机 -> Windows 客户端`;手机端不连 Web 服务端。 + +1. 手机与 Windows 电脑连到**同一个局域网 / WiFi**(路由器不要开 AP 隔离)。 +2. 在电脑上启动 OpenDance Studio Windows 客户端,确认它在监听 8765 端口。 +3. 手机打开 App,连接页填入电脑 IP(或用「扫描局域网」自动发现),点「测试连接」。 +4. 点「开始推流」,首次会申请相机权限,允许后进入运行页。 +5. 站到画面中央,屏幕上出现骨架叠加。 + +成功标志: + +| 位置 | 可观察现象 | +|--|--| +| App 运行页 | 「连接状态」为**已连接**(或短暂「连接中」后转「已连接」) | +| App 运行页 | 「目标」显示 `ws://:<端口>/pose`;服务端返回 welcome 后出现「会话」ID | +| App 运行页 | 「已发送」帧数持续增长;「发送帧率」「识别帧率」大于 0;「推理耗时」为毫秒数 | +| App 运行页 | 「人体识别」为**已识别**,叠加层出现骨架(黄点 = 置信度好,红点 = 置信度低) | +| Windows 客户端 | 显示已连接并收到关键点,VRM 模型随动作实时动起来 | + +### 7. 首次部署自检清单 + +| # | 命令 | 预期输出 | +|--|--|--| +| 1 | `./gradlew testDebugUnitTest` | `BUILD SUCCESSFUL`,全部用例通过;报告在 `app/build/reports/tests/testDebugUnitTest/index.html` | +| 2 | `bash build/build-apk.sh debug` | 末尾打印 `✓ Debug APK:app/build/outputs/apk/debug/app-debug.apk` 及其大小与签名信息 | +| 3 | `bash build/build-apk.sh all` | debug 与 release 均 `✓`;release 显示 `已签名(APK Signature Scheme v2)` | +| 4 | `$ANDROID_HOME/build-tools/34.0.0/apksigner verify app/build/outputs/apk/release/app-release.apk` | 退出码 0(无输出即通过) | +| 5 | `unzip -l app/build/outputs/apk/release/app-release.apk \| grep pose_landmarker_lite.task` | 列出 `assets/pose_landmarker_lite.task`(约 5.7 MB,Stored 未压缩) | +| 6 | `adb install -r app/build/outputs/apk/debug/app-debug.apk` | `Success` | + +其中 `KeypointFrameTest.goldenVectorMatchesWebImplementation` 必须通过: +它断言本端 Kotlin 序列化器与 Web 端 TypeScript 实现产出**逐位相同**的 239 字节帧。 + +### 8. 常见问题(首次部署) + +| 现象 | 可能原因 | 处理 | +|--|--|--| +| 相机黑屏,或一直停在「需要相机权限才能进行姿态识别」 | 相机权限被拒绝或已被永久拒绝;也可能相机被其他 App 占用 | 在运行页点「授予相机权限」;若不再弹窗,去系统「设置 -> 应用 -> OpenDance -> 权限」手动开启,并关闭占用相机的其它 App | +| 相机预览正常但「人体识别」一直是「未识别到人体」 | 光线不足、人不在画面内、或分辨率/模式不合适 | 站到画面中央并改善光照;确认分辨率不是过低;观察「推理耗时」是否异常大 | +| 「连接状态」一直「已断开 / 重连中」,「测试连接」失败 | IP 或端口填错;Windows 客户端未启动;防火墙拦截入站 8765;手机与电脑不在同一网段(AP 隔离) | 核对连接页「本机地址」与电脑 IP 是否同网段;启动客户端;在 Windows 防火墙放行 OpenDance 的 TCP 8765 入站;关闭路由器 AP 隔离 | +| 「扫描局域网」扫不到主机 | 手机未连 WiFi、路由器 AP 隔离、或客户端未监听 8765 | 确认手机连的是 WiFi 而非蜂窝;改为手动填 IP;检查客户端监听端口与防火墙 | +| 点「开始推流」立即闪退,或提示 MediaPipe 初始化失败 | MediaPipe 原生库加载 / 静态初始化失败抛出的是 **Error** 子类(`UnsatisfiedLinkError`、`ExceptionInInitializerError`),不是 `Exception`;常见诱因是 ABI 不匹配(例如把只含 arm 的 release 包装到 x86 模拟器)或模型资源缺失 | 改用与设备 ABI 匹配的包(真机用 release,模拟器调试用 debug);确认 APK 内含 `assets/pose_landmarker_lite.task`。仓库已在 `PoseDetector.initialize()` 中 `catch Throwable` 兜底并把原因显示在运行页;`CrashLogger` 会把未捕获堆栈写入应用私有目录,并在连接页展示,可截图反馈,或用 `adb logcat -s PoseDetector` 查看 | ## 技术栈 | 组件 | 选型 | -|---|---| +|--|--| | 语言 | Kotlin 2.0.21 | | 构建 | AGP 8.7.3 + Gradle 8.9 + JDK 17 | | 相机 | CameraX 1.4.0 | @@ -68,12 +235,12 @@ app/src/main/java/com/opendance/studio/ ## 依赖的契约(opendance-web v0.1.0 冻结) -- [`contracts/websocket-keypoints.md`](contracts/websocket-keypoints.md) — 239 字节关键点帧(本端为**发送方**) -- [`contracts/skeleton-topology.md`](contracts/skeleton-topology.md) — MediaPipe 33 点索引与坐标语义 -- [`contracts/open-source-credits.md`](contracts/open-source-credits.md) — 关于页必须展示的开源声明 +- [`contracts/websocket-keypoints.md`](contracts/websocket-keypoints.md) - 239 字节关键点帧(本端为**发送方**) +- [`contracts/skeleton-topology.md`](contracts/skeleton-topology.md) - MediaPipe 33 点索引与坐标语义 +- [`contracts/open-source-credits.md`](contracts/open-source-credits.md) - 关于页必须展示的开源声明 `contracts/` 是**只读快照**。任何跨端契约变更必须先回到 -[`opendance-web`](https://gitea.kasugano.cn/zijunchiang/opendance-web) 修改并提升版本。 +[`opendance-web`](https:////opendance-web) 修改并提升版本。 ## 跨端一致性:金标准字节向量测试 diff --git a/README.txt b/README.txt index ec7b9c3..2f33010 100644 --- a/README.txt +++ b/README.txt @@ -1,15 +1,15 @@ ================================================================ -OpenDance Studio —— Android 辅助 App v0.1.0 使用说明 +OpenDance Studio -- Android 辅助 App v0.1.0 使用说明 ================================================================ -手机端姿态采集器:用摄像头做**端侧**姿态推理,把 33 个骨骼关键点通过 +手机端姿态采集器:用摄像头做端侧姿态推理,把 33 个骨骼关键点通过 WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评分。 隐私:画面不会离开手机。通过网络发送的只有 33 个关键点坐标。 ----------------------------------------------------------------- +-- 一、安装 ----------------------------------------------------------------- +-- 方式一(推荐):直接安装随附的 app-release.apk - 在手机上允许「安装未知来源应用」 @@ -20,14 +20,156 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评 要求:Android 10(API 29)或更高版本。 ----------------------------------------------------------------- -二、快速开始 ----------------------------------------------------------------- +-- +二、初次部署(从零跑起来) +-- + +本节面向第一次部署的人,给出从零到跑通的最短路径。 +下文地址、路径、口令一律是占位符,请替换成你自己的值,且不要提交进仓库。 + +1) 前置条件 + + JDK 17 或更高 AGP 8.7.3 要求,构建脚本会校验主版本 >= 17 + Gradle 8.9 仓库自带 gradlew,本机无需另装 + Android SDK platforms;android-34 与 build-tools;34.0.0 + compileSdk 34 / minSdk 29(Android 10+)/ targetSdk 34 + 设备 需要真机(Android 10 及以上、带摄像头) + + 真机与模拟器: + release 包只含 arm64-v8a + armeabi-v7a,装不到 x86/x86_64 模拟器; + debug 包含 x86/x86_64,可装进模拟器,但相机是虚拟场景,姿态识别 + 效果不能代表真机,不能作为验收依据。联调验收请用真机。 + + 权限:相机权限为运行时授予;INTERNET / ACCESS_NETWORK_STATE / + ACCESS_WIFI_STATE 为普通权限,安装时自动授予,用于推流与局域网扫描。 + +2) 获取代码与本地配置 + + git clone https:////opendance-android.git + cd opendance-android + + local.properties 是本地文件(已在 .gitignore 中,不入库),内容: + sdk.dir= + + build/build-apk.sh 在缺失时会依据 ANDROID_HOME / ANDROID_SDK_ROOT 自动生成。 + + 签名材料不入库:仓库只有生成脚本,没有私钥。首次发布前执行 + bash build/gen-keystore.sh + 生成 keystore/opendance-release.jks 与 keystore.properties(含 storeFile、 + keyAlias 与两个口令字段)。请自行备份,且不要提交。 + 取值优先级:keystore.properties -> 环境变量(KEYSTORE_PATH / + KEYSTORE_PASSWORD / KEY_ALIAS / KEY_PASSWORD)-> 回退 debug 签名 + (仅供 CI 产出已签名 APK,正式发布必须自备 keystore)。 + +3) 首次构建 + + export JAVA_HOME= + export ANDROID_HOME= + + bash build/build-apk.sh debug # 仅 debug + bash build/build-apk.sh release # 仅 release + bash build/build-apk.sh all # 两者都构建(含产物校验) + + 或直接用 Gradle: + ./gradlew assembleDebug + ./gradlew assembleRelease + + 产物(固定路径): + app/build/outputs/apk/debug/app-debug.apk + app/build/outputs/apk/release/app-release.apk + + 发布整包(APK + THIRD_PARTY.md + README.txt + contracts/): + bash build/package-release.sh -> dist-release/ + +4) 安装到手机 + + adb install -r app/build/outputs/apk/debug/app-debug.apk + adb install -r app/build/outputs/apk/release/app-release.apk + + 也可把 APK 拷到手机,允许「安装未知来源应用」后点击安装。 + 首次进入运行页会申请相机权限;拒绝后可在运行页点「授予相机权限」 + 重新申请,或到系统设置里手动开启。 + +5) 配置与联通 + + 本 App 只连接 Windows 客户端,不访问 Web 服务端。 + 地址来自 AppSettings.webSocketUrl():ws://:/pose + 子协议 opendance.pose.v1。 + + 连接页字段与默认值(见 data/AppSettings.kt): + IP 地址 host 默认是 RFC 5737 文档网段占位地址 + (源码为 192.0.2.100,非真实主机) + 端口 port 8765 + 目标帧率 targetFps 30(5~60) + 分辨率 resolution 640x480(可选 480x360 / 1280x720) + 镜像画面 mirror 开 + 仅上半身模式 upperBodyOnly 关 + + 填写示例(占位符,请替换成你电脑的局域网 IP): + IP 地址:192.0.2.10 + 端口: 8765 + + 不想手填可点「扫描局域网」:App 由本机 WiFi IPv4 推导 /24 网段, + 以 32 并发、单机 300ms 超时探测开放 8765 的主机(约 2~3 秒), + 按延迟升序展示,点候选即填入。也可先点「测试连接」确认端口可达。 + +6) 第一次跑通 + + 手机与电脑连同一局域网 -> 电脑启动 Windows 客户端(监听 8765)-> + App 填地址并测试连接 -> 点「开始推流」并授予相机权限 -> 站到画面中央。 + + 成功标志: + App 运行页 「连接状态」为「已连接」,「目标」显示 ws://:<端口>/pose + App 运行页 服务端返回 welcome 后出现「会话」ID + App 运行页 「已发送」持续增长,「发送帧率」「识别帧率」大于 0 + App 运行页 「人体识别」为「已识别」,出现骨架叠加(黄点好、红点置信度低) + Windows 端 显示已连接并收到关键点,VRM 模型随动作实时动起来 + +7) 自检清单 + + ./gradlew testDebugUnitTest + 预期:BUILD SUCCESSFUL,全部用例通过 + 报告:app/build/reports/tests/testDebugUnitTest/index.html + + bash build/build-apk.sh all + 预期:debug 与 release 均显示 "✓ ... APK",release 显示 + "已签名(APK Signature Scheme v2)" + + $ANDROID_HOME/build-tools/34.0.0/apksigner verify app/build/outputs/apk/release/app-release.apk + 预期:退出码 0(无输出即通过) + + adb install -r app/build/outputs/apk/debug/app-debug.apk + 预期:Success + +8) 常见问题 + + Q: 相机黑屏 / 一直提示需要相机权限 + A: 权限被拒绝或已被永久拒绝,或相机被其他应用占用。在运行页点 + 「授予相机权限」;不再弹窗时到系统「设置 -> 应用 -> OpenDance -> 权限」 + 手动开启相机,并关闭占用相机的其它 App。 + + Q: 连不上 Windows 客户端 / 扫描不到主机 + A: 核对 IP 与端口、确认客户端已启动并监听 8765、在 Windows 防火墙放行 + OpenDance 的 TCP 8765 入站、确认手机与电脑同一 WiFi 且路由器未开 AP 隔离。 + + Q: 点「开始推流」闪退(或提示 MediaPipe 初始化失败) + A: MediaPipe 原生库加载 / 静态初始化失败抛出的是 Error 子类 + (UnsatisfiedLinkError、ExceptionInInitializerError),不是 Exception; + 常见诱因是 ABI 不匹配(例如把只含 arm 的 release 包装到 x86 模拟器) + 或模型资源缺失。处理:改用与设备 ABI 匹配的包(真机用 release、 + 模拟器调试用 debug);确认 APK 内含 assets/pose_landmarker_lite.task。 + 仓库已在 PoseDetector.initialize() 中 catch Throwable 兜底并把原因显示在 + 运行页;CrashLogger 会把未捕获堆栈写入应用私有目录,并在连接页展示, + 可截图反馈,或用 adb logcat -s PoseDetector 查看。 + +-- +三、快速开始 +-- 1. 在 Windows 电脑上启动 OpenDance Studio 客户端 (Windows 客户端会自动监听 8765 端口) -2. 确保手机与电脑连在**同一个 WiFi** +2. 确保手机与电脑连在同一个 WiFi 3. 打开本应用,进入「连接设置」页 @@ -46,9 +188,9 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评 9. 在 Windows 客户端上确认已连接,即可开始跟练 ----------------------------------------------------------------- -三、推流参数 ----------------------------------------------------------------- +-- +四、推流参数 +-- 分辨率 低(480×360) 最省电,适合老机型 @@ -60,16 +202,16 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评 镜像画面 默认开启。前置摄像头的常见用法。 - 注意:本应用发送的是**未镜像的原始坐标**,是否镜像由帧头 flags.bit0 声明, + 注意:本应用发送的是未镜像的原始坐标,是否镜像由帧头 flags.bit0 声明, Windows 客户端据此正确还原左右。因此无论开关如何,动作方向都不会错。 仅上半身模式 下半身关键点置信度置 0(显示为红点),Windows 端会插值。 带宽或算力不足时开启可明显降负载。 ----------------------------------------------------------------- -四、运行页状态说明 ----------------------------------------------------------------- +-- +五、运行页状态说明 +-- 连接状态 未连接 / 连接中 / 已连接 / 重连中(第 N 次)/ 已断开 目标 正在连接的 WebSocket 地址 @@ -83,12 +225,12 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评 平均置信度 < 70% 时转黄,提示改善光线或减少遮挡 最近错误 连接或推理的错误原因 -网络中断时会**自动重连**(1s → 2s → 4s … 最长 15s 退避), +网络中断时会自动重连(1s -> 2s -> 4s... 最长 15s 退避), 无需手动操作。 ----------------------------------------------------------------- -五、常见问题 ----------------------------------------------------------------- +-- +六、常见问题 +-- Q: 扫描不到电脑 A: 1) 确认手机与电脑在同一 WiFi(不是手机热点) @@ -101,7 +243,7 @@ A: 查看运行页的「已发送」是否在增长。若为 0,说明相机或 若「人体识别」一直是「未识别到人体」,请站到画面中央并保证光线充足。 Q: 帧率很低 / 已丢弃帧数持续增长 -A: 依次尝试:降低分辨率 → 降低目标帧率 → 开启「仅上半身模式」→ 关闭其他后台应用。 +A: 依次尝试:降低分辨率 -> 降低目标帧率 -> 开启「仅上半身模式」-> 关闭其他后台应用。 Q: 关键点抖动明显 A: 正常现象,Windows 端有 DTW 带状对齐与平滑处理,不要求逐帧完全一致。 @@ -110,9 +252,9 @@ A: 正常现象,Windows 端有 DTW 带状对齐与平滑处理,不要求逐 Q: 应用退到后台会怎样? A: 返回键或离开运行页会立即停止推流并释放相机,不会在后台占用摄像头。 ----------------------------------------------------------------- -六、从源码构建 ----------------------------------------------------------------- +-- +七、从源码构建 +-- 环境要求: JDK 17 AGP 8.7.3 要求(JDK 25 未经测试) @@ -130,7 +272,7 @@ A: 返回键或离开运行页会立即停止推流并释放相机,不会在 # 构建并校验产物 bash build/build-apk.sh all - # 一键验证(单元测试 → 构建 → 产物校验) + # 一键验证(单元测试 -> 构建 -> 产物校验) bash build/verify.sh 产物: @@ -138,23 +280,23 @@ A: 返回键或离开运行页会立即停止推流并释放相机,不会在 app/build/outputs/apk/release/app-release.apk Android Studio 路径: - 打开本目录 → Build → Generate Signed Bundle / APK → APK → 选择 keystore → release + 打开本目录 -> Build -> Generate Signed Bundle / APK -> APK -> 选择 keystore -> release ----------------------------------------------------------------- -七、测试 ----------------------------------------------------------------- +-- +八、测试 +-- ./gradlew testDebugUnitTest -19 项单元测试(JVM,无需设备),核心是**金标准字节向量测试**: +30 项单元测试(JVM,无需设备),核心是金标准字节向量测试: 用 opendance-web 已通过验收的 TypeScript 实现产出的 239 字节向量, -断言本端 Kotlin 实现逐位相同 —— 这是三端契约一致性的硬证据。 +断言本端 Kotlin 实现逐位相同 -- 这是三端契约一致性的硬证据。 测试报告:app/build/reports/tests/testDebugUnitTest/index.html ----------------------------------------------------------------- -八、目录结构 ----------------------------------------------------------------- +-- +九、目录结构 +-- settings.gradle.kts / build.gradle.kts 工程与插件配置 gradle/wrapper/ Gradle 8.9 wrapper @@ -173,9 +315,9 @@ Android Studio 路径: app/src/main/assets/pose_landmarker_lite.task MediaPipe 模型 build/ 打包与验证脚本 ----------------------------------------------------------------- -九、许可 ----------------------------------------------------------------- +-- +十、许可 +-- 本应用代码以 MIT 许可发布。第三方开源项目声明见 THIRD_PARTY.md (应用内「关于」页同样可查看)。 diff --git a/app/src/main/java/com/opendance/studio/data/AppSettings.kt b/app/src/main/java/com/opendance/studio/data/AppSettings.kt index a5589a7..f1b1091 100644 --- a/app/src/main/java/com/opendance/studio/data/AppSettings.kt +++ b/app/src/main/java/com/opendance/studio/data/AppSettings.kt @@ -55,7 +55,7 @@ class AppSettings(context: Context) { companion object { const val PREFS_NAME = "opendance_settings" - const val DEFAULT_HOST = "192.168.1.100" + const val DEFAULT_HOST = "192.0.2.100" const val DEFAULT_PORT = 8765 const val DEFAULT_FPS = 30 diff --git a/app/src/main/java/com/opendance/studio/net/HostDiscovery.kt b/app/src/main/java/com/opendance/studio/net/HostDiscovery.kt index d92dd8c..e0430db 100644 --- a/app/src/main/java/com/opendance/studio/net/HostDiscovery.kt +++ b/app/src/main/java/com/opendance/studio/net/HostDiscovery.kt @@ -35,7 +35,7 @@ object HostDiscovery { /** 发现结果 */ data class Candidate(val ip: String, val port: Int, val latencyMs: Long) - /** 手机自身的 WiFi IPv4 地址(形如 192.168.1.23),无网络时返回 null */ + /** 手机自身的 WiFi IPv4 地址(形如 192.0.2.23),无网络时返回 null */ suspend fun localIpv4Address(): String? = withContext(Dispatchers.IO) { try { val candidates = mutableListOf() @@ -71,7 +71,7 @@ object HostDiscovery { (a == 192 && b == 168) } - /** 由本机地址推导 /24 前缀,如 192.168.1.23 → "192.168.1" */ + /** 由本机地址推导 /24 前缀,如 192.0.2.23 -> "192.0.2" */ fun subnetPrefix(ip: String): String? { val parts = ip.split(".") if (parts.size != 4) return null diff --git a/contracts/data-models.md b/contracts/data-models.md index d4d7c13..978f5f3 100644 --- a/contracts/data-models.md +++ b/contracts/data-models.md @@ -1,4 +1,4 @@ -# contracts/data-models.md — 数据模型契约 v0.4.0 +# contracts/data-models.md - 数据模型契约 v0.4.0 Web 服务端是唯一权威定义,本文件是另两端消费的**稳定视图**。 字段名在 JSON 中统一 `camelCase`;数据库列名 `snake_case`(见 `database.md`)。 @@ -16,13 +16,13 @@ Web 服务端是唯一权威定义,本文件是另两端消费的**稳定视 - 错误响应统一: ```json -{ "statusCode": 400, "message": "…", "error": "Bad Request", "details": {} } +{ "statusCode": 400, "message": "...", "error": "Bad Request", "details": {} } ``` - **来源(出处)五元组**是必填项,出现在 `songs` / `motions` / `models` 三处: | 字段 | 类型 | 说明 | -|---|---|---| +|--|--|--| | `sourceType` | enum | 见 1.1 | | `sourceName` | string | 来源名称,如「官方曲库」「作者名」 | | `sourceUrl` | string | 来源链接,必须为 http(s) | @@ -52,7 +52,7 @@ rejected 已驳回 ## 2. `Song` | 字段 | 类型 | 必填 | 说明 | -|---|---|---|---| +|--|--|--|--| | `id` | number | 读 | | | `title` | string | ✅ | 曲名,≤200 | | `artist` | string | ✅ | 演唱者,≤200 | @@ -71,7 +71,7 @@ rejected 已驳回 ## 3. `Motion`(动作数据) | 字段 | 类型 | 必填 | 说明 | -|---|---|---|---| +|--|--|--|--| | `id` | number | 读 | | | `songId` | number\|null | | 关联曲目 | | `name` | string | ✅ | 动作名称,≤200 | @@ -82,7 +82,7 @@ rejected 已驳回 | `frameCount` | number\|null | | 原始帧数 | | `durationMs` | number\|null | | | | `keypointFormat` | string\|null | 读 | 预处理后固定为 `mediapipe33@0.1.0` | -| `keypointUrl` | string\|null | 读 | 预处理产物(NDJSON 或 .npz 的 JSON 封装) | +| `keypointUrl` | string\|null | 读 | 预处理产物(NDJSON 或.npz 的 JSON 封装) | | `keypointFrames` | number\|null | 读 | 关键点帧数 | | `preprocessState` | enum | 读 | `none` / `running` / `done` / `failed` | | `preprocessError` | string\|null | 读 | | @@ -93,19 +93,19 @@ rejected 已驳回 ## 4. `Model`(角色模型,VRM 优先) | 字段 | 类型 | 必填 | 说明 | -|---|---|---|---| +|--|--|--|--| | `id` | number | 读 | | | `name` | string | ✅ | 模型名称,≤200 | -| `format` | enum | ✅ | `glb` / `vrm`(主力)/ `gltf` / `fbx`(历史数据)。上传的 PMX/FBX 会被服务端转换成 glb 或 vrm 再入库,见 model-formats.md §1.1 | +| `format` | enum | ✅ | `glb` / `vrm`(主力)/ `gltf` / `fbx`(历史数据)。上传的 PMX/FBX 会被服务端转换成 glb 或 vrm 再入库,见 model-formats.md 1.1 | | `vrmVersion` | string\|null | 读 | `0.x` / `1.0`,非 VRM 为 null | | `isPrimary` | boolean | | 全局默认模型;**同一时刻全库至多一个 `true`** | | `fileUrl` | string | 读 | | | `fileSize` | number | 读 | | | `thumbnailUrl` | string\|null | 读 | | -| `conversionNote` | string\|null | 读 | 由压缩包转换而来时的说明(`Hatsune Miku.pmx → GLB,已内嵌 2 张贴图`);直传模型为 null | -| `fileSha256` | string\|null | 读 | 模型文件(转换产物)的 sha256 —— **每次转换都会变**,仅作记录 | +| `conversionNote` | string\|null | 读 | 由压缩包转换而来时的说明(`Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图`);直传模型为 null | +| `fileSha256` | string\|null | 读 | 模型文件(转换产物)的 sha256 -- **每次转换都会变**,仅作记录 | | `sourceSha256` | string\|null | 读 | 上传压缩包的 sha256(稳定);默认模型匹配以它为准,非压缩包上传为 null | -| `stats` | object\|null | 读 | 模型分析结果:面数 / 顶点数 / 贴图 / **骨骼树** / humanoid 覆盖 / 可驱动性,见 model-formats.md §1.1 | +| `stats` | object\|null | 读 | 模型分析结果:面数 / 顶点数 / 贴图 / **骨骼树** / humanoid 覆盖 / 可驱动性,见 model-formats.md 1.1 | | `source*` | | ✅ | 来源五元组 | | `status` | `PublishStatus` | | | | `createdAt` / `updatedAt` | string | 读 | | @@ -124,7 +124,7 @@ rejected 已驳回 2. models 中 format = 'vrm' 且 status = 'published' 且 is_primary = 1 3. models 中 format = 'vrm' 且 status = 'published',按 id 升序取第一个 4. models 中 status = 'published',按 (format='vrm' DESC, id ASC) 取第一个 -5. 无可用模型 → 客户端使用内置占位模型 +5. 无可用模型 -> 客户端使用内置占位模型 ``` 第 0/1 步是 v0.3.0 新增:上传 MMD 模型压缩包并转换成功后,服务端会把该模型 @@ -135,7 +135,7 @@ rejected 已驳回 ## 5. `ReviewRecord`(审核流水) | 字段 | 类型 | 说明 | -|---|---|---| +|--|--|--| | `id` | number | | | `targetType` | enum | `song` / `motion` / `model` | | `targetId` | number | | diff --git a/contracts/database.md b/contracts/database.md index 7860b4e..161f404 100644 --- a/contracts/database.md +++ b/contracts/database.md @@ -1,11 +1,11 @@ -# contracts/database.md — 数据库契约 v0.1.0 +# contracts/database.md - 数据库契约 v0.1.0 ## 环境 -- 类型:MySQL 兼容实例(外部提供)。**当前部署实际为 MariaDB 10.11.19**,TypeORM +- 类型:MySQL 兼容实例(外部提供,含 MariaDB 10.5+),TypeORM 使用 `mysql` driver(`mysql2`)连接,SQL 语法限制在 MySQL 5.7 / MariaDB 10.5+ 共同子集内,不使用 MySQL 8 专有语法(窗口函数 CTE 递归除外)。 -- 数据库名:`dpendance`(由部署方提供,通过 `DB_NAME` 注入;文档默认值 `opendance` 可覆盖) +- 数据库名:由部署方提供,通过 `DB_NAME` 注入(本文档示例统一用 `opendance`) - 字符集:`utf8mb4` / `utf8mb4_general_ci`(沿用现有实例默认排序规则) - 迁移工具:TypeORM migration,**不使用 `DB_SYNCHRONIZE`** @@ -14,11 +14,11 @@ host / port / user / password 一律通过环境变量注入,**不入库、不入仓**。 ```bash -DB_HOST=192.168.150.10 +DB_HOST=<数据库主机> DB_PORT=3306 -DB_USER=dpendance -DB_PASS=<部署方提供> -DB_NAME=dpendance +DB_USER=opendance_app +DB_PASS=<数据库口令> +DB_NAME=opendance DB_SYNCHRONIZE=false DB_LOGGING=false ``` @@ -39,17 +39,17 @@ DB_LOGGING=false ## 增量迁移(v0.2.0 / v0.3.0) | 迁移 | 变更 | -|---|---| -| `AddModelConversionFields` | `models` 增加 `source_asset_id BIGINT NULL`(上传的压缩包,用于溯源)与 `conversion_note VARCHAR(500) NULL`(转换说明,见 model-formats.md §1.1) | +|--|--| +| `AddModelConversionFields` | `models` 增加 `source_asset_id BIGINT NULL`(上传的压缩包,用于溯源)与 `conversion_note VARCHAR(500) NULL`(转换说明,见 model-formats.md 1.1) | | `AddModelStats` | `models` 增加 `stats TEXT NULL`(模型分析结果:面数、骨骼树、humanoid 覆盖、可驱动性),供审核界面展示 | ## 表清单(v0.1.0) | 表 | 用途 | 任务 | -|---|---|---| +|--|--|--| | `songs` | 曲目元数据 | T-WEB-003 | | `assets` | 上传文件元数据(去重、sha256、物理路径) | T-WEB-004 | -| `motions` | 动作数据(BVH/FBX…)+ 来源 + 预处理产物 | T-WEB-004 / 006 | +| `motions` | 动作数据(BVH/FBX...)+ 来源 + 预处理产物 | T-WEB-004 / 006 | | `models` | 角色模型(VRM 优先)+ 来源 + 压缩包转换溯源 | T-WEB-005 | | `review_records` | 上传审核流水 | T-WEB-007 | diff --git a/contracts/model-formats.md b/contracts/model-formats.md index ad0a74b..c8db38d 100644 --- a/contracts/model-formats.md +++ b/contracts/model-formats.md @@ -1,15 +1,15 @@ -# contracts/model-formats.md — 模型与动作格式契约 v0.4.0 +# contracts/model-formats.md - 模型与动作格式契约 v0.4.0 ## 1. 角色模型(models) **系统以 GLB / VRM 为主力格式**(两个客户端都原生加载),其余格式一律在服务端转换。 | 格式 | 扩展名 | 支持级别 | 服务端行为 | -|---|---|---|---| +|--|--|--|--| | glTF Binary | `.glb` | **主力(P0)** | **直接导入**,不重编码 | | VRM | `.vrm` | **主力(P0)** | **直接导入**;= GLB + VRM 扩展(humanoid / 表情 / springBone / meta) | -| glTF | `.gltf` (+ `.bin`, 贴图) | P1 | 直接导入(分离文件在 zip 内可用;单文件上传会缺 .bin) | -| MMD 压缩包 | `.zip` | P1 | **自动搜索包内可用模型**:优先直接导入 `.vrm`/`.glb`/`.gltf`,否则转换 `.pmx`/`.fbx`,见 §1.1 | +| glTF | `.gltf` (+ `.bin`, 贴图) | P1 | 直接导入(分离文件在 zip 内可用;单文件上传会缺.bin) | +| MMD 压缩包 | `.zip` | P1 | **自动搜索包内可用模型**:优先直接导入 `.vrm`/`.glb`/`.gltf`,否则转换 `.pmx`/`.fbx`,见 1.1 | | MMD 模型 | `.pmx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM);单文件上传无贴图 | | FBX | `.fbx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM) | @@ -21,18 +21,18 @@ #### 单文件上传 | 输入 | 行为 | -|---|---| +|--|--| | `.glb` / `.vrm` | 直接导入;`.glb` 若含 `VRMC_vrm`/`VRM` 扩展会被识别为 VRM | | `.gltf` | 直接导入(外部 `.bin` / 贴图不在同一文件里,会缺资源) | -| `.pmx` / `.fbx` | 转换:`ASSIMP_BIN` 导出 → 回读校验(见下) | +| `.pmx` / `.fbx` | 转换:`ASSIMP_BIN` 导出 -> 回读校验(见下) | #### 压缩包上传(自动搜索) 不再"只找 `.pmx`",而是**按格式优先级自动搜索包内可用模型**: ``` -.vrm → .glb → .gltf (直接导入,优先) -.pmx → .fbx (需要转换) +.vrm ->.glb ->.gltf (直接导入,优先) +.pmx ->.fbx (需要转换) ``` 同一优先级内:`MODEL_CONVERT_PMX` 指定的文件名优先,其次目录最浅,最后文件最大。 @@ -41,31 +41,31 @@ #### 转换与校验(PMX / FBX) | 步骤 | 规则 | -|---|---| -| 1. GLB 直出 | `assimp export → .glb`,成功后把 Assimp 写成外部 URI 的贴图**内嵌**进 BIN chunk | -| 2. glTF 打包 | 上一步失败时 `assimp export → .gltf`,由 `gltf-packer.ts` 把 `.gltf` + `.bin` + 贴图自行合成 GLB | +|--|--| +| 1. GLB 直出 | `assimp export ->.glb`,成功后把 Assimp 写成外部 URI 的贴图**内嵌**进 BIN chunk | +| 2. glTF 打包 | 上一步失败时 `assimp export ->.gltf`,由 `gltf-packer.ts` 把 `.gltf` + `.bin` + 贴图自行合成 GLB | | 3. VRM 封装 | `MODEL_CONVERT_FORMAT=vrm` 时,在 GLB 之上补 `VRMC_vrm` 扩展(humanoid 骨骼来自骨骼名映射,见 `model-inspector.ts` 的候选表,与 Godot 客户端一致);**humanoid 不全则回退交付 GLB**,缺失骨骼写进 `conversion_note` | | 回读校验 | 每一步产物都要 `assimp info` 回读。Assimp 会"写出损坏文件"(实测 FBX 导出退出码 11、产物回读报 `FBX-DOM no Objects dictionary found`),只看退出码会把坏文件入库 | -| 说明 | 每次尝试都写进 `models.conversion_note`(例如 `Hatsune Miku.pmx → GLB,已内嵌 2 张贴图`) | -| 失败 | 包内没有可用模型、Assimp 不可用、两条路径都失败 → **HTTP 422**,不产生模型记录 | +| 说明 | 每次尝试都写进 `models.conversion_note`(例如 `Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图`) | +| 失败 | 包内没有可用模型、Assimp 不可用、两条路径都失败 -> **HTTP 422**,不产生模型记录 | #### 入库后(自动) | 项 | 说明 | -|---|---| +|--|--| | 状态 | **一律 `pending`(待审核)**:模型必须审核通过后才可能被客户端消费,见 `database.md` 状态机 | | 分析 | `model-inspector.ts` 计算面数 / 顶点数 / 贴图 / **骨骼树** / humanoid 覆盖 / 可驱动性,写入 `models.stats`,供审核界面展示 | | 缩略图 | `model-thumbnail.ts` 纯 TS 光栅化生成 PNG,写入 `models.thumbnail_asset_id`(失败不影响入库) | -| 默认模型 | 只在**审核通过并勾选"同时设为默认"**(或 `POST /api/models/:id/primary`)时设置,并把身份写进源码 `src/config/default-model.ts`,见 `data-models.md` §4.1 | +| 默认模型 | 只在**审核通过并勾选"同时设为默认"**(或 `POST /api/models/:id/primary`)时设置,并把身份写进源码 `src/config/default-model.ts`,见 `data-models.md` 4.1 | #### 其它约定 | 项 | 规则 | -|---|---| +|--|--| | 确定性 | **转换产物不是字节级确定的**:同一份 zip 连续转换 3 次得到 3 个不同 sha256(大小相同,BIN chunk 有十几字节浮动)。因此默认模型种子以**上传压缩包的 sha256** 为匹配键,产物 sha256 只作记录 | | 安全 | 解压前逐条校验路径,拒绝绝对路径与 `..`(zip-slip);拒绝加密条目;只支持 STORE / DEFLATE | -**VRM 优先**的选取顺序见 `data-models.md` §4.1。 +**VRM 优先**的选取顺序见 `data-models.md` 4.1。 VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)或 `extensions.VRMC_vrm`(1.0),写入 `models.vrm_version`。 @@ -73,9 +73,9 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x) ## 2. 动作数据(motions) | 格式 | 扩展名 | 预处理支持 | 说明 | -|---|---|---|---| -| BVH | `.bvh` | ✅ 内置纯 TS 解析器 | 文本格式,服务端直接解析 → FK → MediaPipe33 | -| VMD | `.vmd` | ✅ 内置纯 TS 解析器 | MikuMikuDance 动作数据(二进制,Shift-JIS 骨骼名 + 四元数关键帧);服务端按标准 MMD 骨架做 FK + 腿部 IK 求解 → MediaPipe33,见 §2.3 | +|--|--|--|--| +| BVH | `.bvh` | ✅ 内置纯 TS 解析器 | 文本格式,服务端直接解析 -> FK -> MediaPipe33 | +| VMD | `.vmd` | ✅ 内置纯 TS 解析器 | MikuMikuDance 动作数据(二进制,Shift-JIS 骨骼名 + 四元数关键帧);服务端按标准 MMD 骨架做 FK + 腿部 IK 求解 -> MediaPipe33,见 2.3 | | FBX | `.fbx` | ⚠️ 需 Assimp | 服务端调用 `assimp` 可执行文件导出为 BVH/glTF 后再解析;未安装则 `preprocessState=failed` | | glTF/GLB | `.gltf` / `.glb` | ⚠️ 需 Assimp | 同上 | | 关键点 JSON | `.json` | ✅ 直通校验 | 已是 `mediapipe33@0.1.0` 结构,仅做校验与归一化 | @@ -86,11 +86,11 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x) - 一行结构: ```json -{"frame":0,"timeMs":0.0,"points":[[0.5,0.5,0.0,1.0],"…33 项"],"angles":[1.0,"…9 项"]} +{"frame":0,"timeMs":0.0,"points":[[0.5,0.5,0.0,1.0],"...33 项"],"angles":[1.0,"...9 项"]} ``` - `points[i] = [x, y, z, confidence]`,语义与 `websocket-keypoints.md` 解码后完全一致 -- `angles` 为 `skeleton-topology.md` §5 定义的 9 个关节角余弦,顺序固定: +- `angles` 为 `skeleton-topology.md` 5 定义的 9 个关节角余弦,顺序固定: `[shoulder_L, shoulder_R, elbow_L, elbow_R, hip_L, hip_R, knee_L, knee_R, spine]` - 帧率:保留源帧率;`fps` 写入 `motions.fps`,缺省 30 - 坐标系换算:所有 3D 动作数据统一转到 **Y 轴向上、右手系、骨盆为原点**, @@ -102,7 +102,7 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x) ### 2.2 BVH 关节名映射(默认表,可在 `web-server/src/preprocess/joint-map.ts` 扩展) | MediaPipe 点 | 常见 BVH 关节名(不区分大小写,按序匹配) | -|---|---| +|--|--| | 0 nose | `Head`, `Neck1`, `Head_Nub` | | 11 left_shoulder | `LeftShoulder`, `LeftArm`, `LShoulder` | | 12 right_shoulder | `RightShoulder`, `RightArm`, `RShoulder` | @@ -119,7 +119,7 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x) | 31 left_foot_index | `LeftToeBase`, `LeftToe`, `LToe` | | 32 right_foot_index | `RightToeBase`, `RightToe`, `RToe` | -未映射到的点按 `skeleton-topology.md` §5 的层级由父节点推算, +未映射到的点按 `skeleton-topology.md` 5 的层级由父节点推算, 并置 `confidence` 降低(`0.5`),表示估算而非实测。 ### 2.3 VMD(MikuMikuDance)解析约定 @@ -130,13 +130,13 @@ float32×3 位移、float32×4 旋转四元数、64 字节贝塞尔插值参数 灯光 / 自阴影 / IK / 可见性段落。**VMD 不含骨骼长度**,只有旋转(少数骨骼带位移), 因此服务端必须自带一套标准 MMD 骨架才能做 FK。 -处理链路:`vmd-parser.ts`(二进制 + Shift-JIS + 贝塞尔插值)→ `mmd-skeleton.ts` -(标准骨架)→ `mmd-pose.ts`(FK + 腿部 IK)→ `retarget.ts` 的 +处理链路:`vmd-parser.ts`(二进制 + Shift-JIS + 贝塞尔插值)-> `mmd-skeleton.ts` +(标准骨架)-> `mmd-pose.ts`(FK + 腿部 IK)-> `retarget.ts` 的 `worldFramesToKeypointFrames()`(与 BVH 完全共用骨盆归零 / 自适应缩放 / 图像空间投影 / 9 个关节角)。固定 **30 fps**(MMD 帧率恒定,上传时填写的 `fps` 被忽略)。 | MediaPipe 点 | MMD 标准骨骼 | 说明 | -|---|---|---| +|--|--|--| | 0 nose | `頭` | 取头骨根部再上移,近似面部中心 | | 11 / 12 left/right_shoulder | `左腕` / `右腕` | 肩关节(不是 `左肩` 骨骼) | | 13 / 14 left/right_elbow | `左ひじ` / `右ひじ` | | @@ -149,12 +149,12 @@ float32×3 位移、float32×4 旋转四元数、64 字节贝塞尔插值参数 骨骼名按 `mmd-skeleton.ts` 的别名表归一(含全角 `IK` / 半角 `IK` 两种写法); 未纳入标准骨架的骨骼(裙摆、物理、表情等)被忽略但会计数并写入预处理日志。 -**腿部驱动判定**:`足IK` 轨道存在非零位移 → 走 IK(用两骨骼解析 IK 把踝拉到 IK +**腿部驱动判定**:`足IK` 轨道存在非零位移 -> 走 IK(用两骨骼解析 IK 把踝拉到 IK 目标点,膝盖朝角色正面弯,不可达时收敛为伸直);否则走 FK(部分动作数据把腿烘焙成 FK 旋转)。两份示例文件分别命中两种路径。 **坐标系**:MMD 原生是左手系(正面为 -Z),本项目通路是右手系(正面为 +Z、+X 为角色 -左侧)。VMD 的四元数与位移统一做 Z 镜像(`(x,y,z,w) → (-x,-y,z,w)`、位移 z 取反)后 +左侧)。VMD 的四元数与位移统一做 Z 镜像(`(x,y,z,w) -> (-x,-y,z,w)`、位移 z 取反)后 送入标准骨架。该选择由真实数据对照实验确定:腿部烘焙为 FK 的样本中,`zMirrored` 下 膝盖朝前占比 94.1%,不转换时仅 5.9%(见 `test/mmd-vmd.spec.ts`)。 @@ -166,7 +166,7 @@ FK 旋转)。两份示例文件分别命中两种路径。 ## 3. 上传限制 | 项 | 值 | -|---|---| +|--|--| | 单文件上限 | 512 MiB(`UPLOAD_MAX_BYTES` 可覆盖) | | 允许扩展名 | 上表全部;其他一律 422 | | 存储路径 | `${STORAGE_DIR}/{kind}/{yyyy}/{mm}/{sha256前2位}/{sha256}.{ext}` | @@ -175,7 +175,7 @@ FK 旋转)。两份示例文件分别命中两种路径。 ## 4. 开源依赖与许可证(引用 `open-source-credits.md`) -- `bvh`(MIT)— BVH 解析参考实现 -- `Assimp`(BSD-3-Clause)— FBX/glTF 转换,**可选外部依赖** -- `@pixiv/three-vrm`(MIT)— 管理端 VRM 预览 -- `three`(MIT)— 管理端 3D 预览 +- `bvh`(MIT)- BVH 解析参考实现 +- `Assimp`(BSD-3-Clause)- FBX/glTF 转换,**可选外部依赖** +- `@pixiv/three-vrm`(MIT)- 管理端 VRM 预览 +- `three`(MIT)- 管理端 3D 预览 diff --git a/contracts/open-source-credits.md b/contracts/open-source-credits.md index 4a6d166..473d251 100644 --- a/contracts/open-source-credits.md +++ b/contracts/open-source-credits.md @@ -1,10 +1,10 @@ -# contracts/open-source-credits.md — 开源声明锚点 v0.1.0 +# contracts/open-source-credits.md - 开源声明锚点 v0.1.0 三端发布产物中的 `THIRD_PARTY.md` 与各自「关于」页面必须包含下表全部条目。 本文件是**唯一来源**,任一端新增依赖须回到 `opendance-web` 更新此表。 | # | 项目 | 许可证 | 源链接 | 使用端 | -|---|---|---|---|---| +|--|--|--|--|--| | 1 | MediaPipe | Apache License 2.0 | https://github.com/google-ai-edge/mediapipe | Android | | 2 | Three.js | MIT | https://github.com/mrdoob/three.js | Web 管理端 | | 3 | @pixiv/three-vrm | MIT | https://github.com/pixiv/three-vrm | Web 管理端 | @@ -21,7 +21,7 @@ | 14 | Jetpack Compose | Apache License 2.0 | https://developer.android.com/jetpack/compose | Android | | 15 | mysql2 | MIT | https://github.com/sidorares/node-mysql2 | Web 服务端 | -> 注意:文档模板中列出的 10 项为**最低要求**;第 11–15 项为实际引入的构建期/运行时依赖, +> 注意:文档模板中列出的 10 项为**最低要求**;第 11-15 项为实际引入的构建期/运行时依赖, > 一并声明更完整。发布时必须保留各许可证副本(见 `web-server/LICENSE-THIRD-PARTY/`)。 ## 版权声明补充 diff --git a/contracts/rest-api.yaml b/contracts/rest-api.yaml index 66dc71b..e0b9557 100644 --- a/contracts/rest-api.yaml +++ b/contracts/rest-api.yaml @@ -262,7 +262,7 @@ paths: parameters: [{ $ref: '#/components/parameters/Id' }] post: tags: [motions] - summary: 触发 BVH/FBX → MediaPipe33 关键点序列预处理(同步执行) + summary: 触发 BVH/FBX -> MediaPipe33 关键点序列预处理(同步执行) description: | 同步执行并返回最终状态:成功时 preprocessState=done; 失败时返回 422,且 Motion.preprocessError 记录可读原因。 @@ -285,7 +285,7 @@ paths: '200': description: | application/x-ndjson,每行一个帧对象: - {"frame":0,"timeMs":0,"points":[[x,y,z,conf], …33],"angles":[9 个余弦]} + {"frame":0,"timeMs":0,"points":[[x,y,z,conf], ...33],"angles":[9 个余弦]} content: application/x-ndjson: schema: { type: string } @@ -323,11 +323,11 @@ paths: - `.glb` / `.vrm` / `.gltf`:**直接导入**,不重编码 - `.pmx` / `.fbx`:服务端转换,**优先 GLB、其次 VRM** - - `.zip`:**自动搜索包内可用模型** —— `.vrm` → `.glb` → `.gltf`(直接导入) - 优先,其次 `.pmx` → `.fbx`(转换)。压缩包原件作为 `model-archive` 资产留存。 + - `.zip`:**自动搜索包内可用模型** -- `.vrm` -> `.glb` -> `.gltf`(直接导入) + 优先,其次 `.pmx` -> `.fbx`(转换)。压缩包原件作为 `model-archive` 资产留存。 - 转换链路(`model-convert.ts`):`assimp export → .glb` →(失败)`→ .gltf` 后 - 由服务端自行打包成 GLB →(`MODEL_CONVERT_FORMAT=vrm` 时)在 GLB 之上补 + 转换链路(`model-convert.ts`):`assimp export -> .glb` ->(失败)`-> .gltf` 后 + 由服务端自行打包成 GLB ->(`MODEL_CONVERT_FORMAT=vrm` 时)在 GLB 之上补 `VRMC_vrm` 扩展产出 VRM 1.0。每一步都 `assimp info` **回读校验**, 并把过程写进 `conversionNote`;贴图会内嵌进 GLB,保证单文件可用。 @@ -488,7 +488,7 @@ paths: - $ref: '#/components/parameters/Id' post: tags: [reviews] - summary: 审核通过 → published + summary: 审核通过 -> published requestBody: content: application/json: @@ -510,7 +510,7 @@ paths: - $ref: '#/components/parameters/Id' post: tags: [reviews] - summary: 撤回审核 → draft + summary: 撤回审核 -> draft requestBody: content: application/json: @@ -552,7 +552,7 @@ paths: - $ref: '#/components/parameters/Id' post: tags: [reviews] - summary: 审核驳回 → rejected + summary: 审核驳回 -> rejected requestBody: required: true content: @@ -664,7 +664,7 @@ components: type: object properties: connected: { type: boolean } - server: { type: string, description: '如 MariaDB 10.11.19' } + server: { type: string, description: '如 MariaDB 10.5+' } database: { type: string } migrationsPending: { type: integer } @@ -795,13 +795,13 @@ components: fileSha256: type: string nullable: true - description: 模型文件(转换产物)的 sha256 —— 每次转换都会变(Assimp 导出非字节级确定),仅作记录 + description: 模型文件(转换产物)的 sha256 -- 每次转换都会变(Assimp 导出非字节级确定),仅作记录 stats: type: object nullable: true description: >- 模型分析结果(面数 / 顶点数 / 贴图 / 骨骼树 / humanoid 覆盖 / 可驱动性), - 审核界面据此判断"能不能驱动与评分"。结构见 contracts/model-formats.md §1.1 + 审核界面据此判断"能不能驱动与评分"。结构见 contracts/model-formats.md 1.1 sourceSha256: type: string nullable: true diff --git a/contracts/skeleton-topology.md b/contracts/skeleton-topology.md index c4ba13c..d8c9db0 100644 --- a/contracts/skeleton-topology.md +++ b/contracts/skeleton-topology.md @@ -1,11 +1,11 @@ -# contracts/skeleton-topology.md — 骨骼拓扑契约 v0.1.0 +# contracts/skeleton-topology.md - 骨骼拓扑契约 v0.1.0 **唯一拓扑:MediaPipe Pose 33 点。禁止引入 SMPL 及其衍生拓扑(许可风险)。** ## 1. 33 点定义与索引 | idx | name | 中文 | 说明 | -|---|---|---|---| +|--|--|--|--| | 0 | nose | 鼻 | | | 1 | left_eye_inner | 左眼内 | | | 2 | left_eye | 左眼 | | @@ -43,7 +43,7 @@ ## 2. 虚拟关节(由 33 点派生,索引 33/34,仅用于层级与角度特征) | idx | name | 计算 | -|---|---|---| +|--|--|--| | 33 | neck | `midpoint(left_shoulder(11), right_shoulder(12))` | | 34 | pelvis | `midpoint(left_hip(23), right_hip(24))` | diff --git a/contracts/websocket-keypoints.md b/contracts/websocket-keypoints.md index 4930921..fd40f48 100644 --- a/contracts/websocket-keypoints.md +++ b/contracts/websocket-keypoints.md @@ -1,6 +1,6 @@ -# contracts/websocket-keypoints.md — 关键点传输契约 v0.1.0 +# contracts/websocket-keypoints.md - 关键点传输契约 v0.1.0 -Android 辅助 App(发送端)→ Windows 客户端(接收端)。 +Android 辅助 App(发送端)-> Windows 客户端(接收端)。 服务端不参与实时链路,但 T-WEB-006 预处理产出的序列必须与此结构同构。 ## 1. 端点 @@ -28,7 +28,7 @@ ws://:8765/pose bit3-7 保留,必须为 0 4 4 frame_id u32 LE 单调递增,从 0 开始 8 231 keypoints 33 × 7B 见下 --------------------------------------------------------------- +-- 合计 239 ``` @@ -70,7 +70,7 @@ func decode(buf: PackedByteArray) -> Dictionary: ## 3. 文本控制帧(JSON,UTF-8) -### 3.1 `hello` — Android → Windows,连接后立即发送 +### 3.1 `hello` - Android -> Windows,连接后立即发送 ```json { @@ -84,25 +84,25 @@ func decode(buf: PackedByteArray) -> Dictionary: } ``` -### 3.2 `welcome` — Windows → Android +### 3.2 `welcome` - Windows -> Android ```json { "type": "welcome", "protocol": "opendance.pose.v1", "sessionId": "uuid", "expectedFps": 30 } ``` -### 3.3 `config` — Windows → Android,运行中调整 +### 3.3 `config` - Windows -> Android,运行中调整 ```json { "type": "config", "mirror": true, "targetFps": 30, "upperBodyOnly": false, "resolution": "640x480" } ``` -### 3.4 `stats` — Android → Windows,每秒一次 +### 3.4 `stats` - Android -> Windows,每秒一次 ```json { "type": "stats", "fps": 29.6, "dropped": 3, "latencyMs": 42, "confidenceAvg": 0.93 } ``` -### 3.5 `bye` — 双向 +### 3.5 `bye` - 双向 ```json { "type": "bye", "reason": "user_stop" } @@ -117,7 +117,7 @@ func decode(buf: PackedByteArray) -> Dictionary: "frameId": 1024, "timestampMs": 1730000000000, "flags": { "mirrored": false, "upperBodyOnly": false }, - "points": [[0.51, 0.22, -0.03, 0.99], "…共 33 项,顺序固定 [x, y, z, conf]"] + "points": [[0.51, 0.22, -0.03, 0.99], "...共 33 项,顺序固定 [x, y, z, conf]"] } ``` diff --git a/project.yaml b/project.yaml index 4e587bc..febffb0 100644 --- a/project.yaml +++ b/project.yaml @@ -5,7 +5,7 @@ default_branch: main platform: Android 10+(minSdk 29 / targetSdk 34) contracts_version: 0.1.0 -# 契约变更流程(文档 §7.2): +# 契约变更流程(文档 7.2): # 任何跨端契约变更必须先在 opendance-web 修改 contracts/ 并提升版本, # 再在本项目创建适配任务。本项目内的 contracts/ 只是只读快照。 @@ -34,7 +34,7 @@ tasks: status: done - id: T-AND-004 file: tasks/004-keypoint-serializer.md - scope: 33 点 → 239 字节帧 + scope: 33 点 -> 239 字节帧 status: done - id: T-AND-005 file: tasks/005-websocket-client.md diff --git a/tasks/001-scaffold.md b/tasks/001-scaffold.md index a5a3ed4..f744597 100644 --- a/tasks/001-scaffold.md +++ b/tasks/001-scaffold.md @@ -39,7 +39,7 @@ JDK 17 / Gradle 8.9 / AGP 8.7.3 / Kotlin 2.0.21 / Compose BOM 2024.10.01 ## 验收 ```bash -export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 +export JAVA_HOME= ./gradlew assembleDebug ``` diff --git a/tasks/003-mediapipe-pose.md b/tasks/003-mediapipe-pose.md index 768b5bc..ec097c1 100644 --- a/tasks/003-mediapipe-pose.md +++ b/tasks/003-mediapipe-pose.md @@ -11,7 +11,7 @@ ``` DSH.md -contracts/skeleton-topology.md # 锚点:§1 33 点、§4 坐标系 +contracts/skeleton-topology.md # 锚点:1 33 点、4 坐标系 app/src/main/java/com/opendance/studio/pose/PoseDetector.kt ``` @@ -22,7 +22,7 @@ app/src/main/java/com/opendance/studio/pose/PoseDetector.kt ## 关键实现 | 项 | 说明 | -|---|---| +|--|--| | 运行模式 | `LIVE_STREAM` + `detectAsync`,不阻塞相机线程 | | 模型 | `pose_landmarker_lite.task`(随 APK 分发,`noCompress "task"` 保证可读) | | 置信度阈值 | detection / presence / tracking 均 0.5 | @@ -32,7 +32,7 @@ app/src/main/java/com/opendance/studio/pose/PoseDetector.kt ## 坐标与镜像语义 -- 发送**未镜像的原始 MediaPipe 坐标**(契约 §4) +- 发送**未镜像的原始 MediaPipe 坐标**(契约 4) - 是否镜像由帧头 `flags.bit0` 声明,接收端据此决定如何还原 - 叠加层绘制时才取 `1 - x`,与前置摄像头预览的镜像观感一致 diff --git a/tasks/004-keypoint-serializer.md b/tasks/004-keypoint-serializer.md index cb0874c..7544444 100644 --- a/tasks/004-keypoint-serializer.md +++ b/tasks/004-keypoint-serializer.md @@ -1,4 +1,4 @@ -# T-AND-004 33 点 → 239 字节帧 +# T-AND-004 33 点 -> 239 字节帧 - **状态**:done - **依赖**:T-AND-003(数据结构)、T-AND-001 @@ -11,8 +11,8 @@ ``` DSH.md -contracts/websocket-keypoints.md # 锚点:§2 二进制帧(冻结) -contracts/skeleton-topology.md # 锚点:§1 33 点、§4 坐标系 +contracts/websocket-keypoints.md # 锚点:2 二进制帧(冻结) +contracts/skeleton-topology.md # 锚点:1 33 点、4 坐标系 app/src/main/java/com/opendance/studio/model/KeypointFrame.kt app/src/main/java/com/opendance/studio/model/SkeletonTopology.kt ``` @@ -21,7 +21,7 @@ app/src/main/java/com/opendance/studio/model/SkeletonTopology.kt ``` 偏移 长度 字段 -0 2 magic u16 = 0x4F44 ('OD'),小端序 → 字节序为 44 4F +0 2 magic u16 = 0x4F44 ('OD'),小端序 -> 字节序为 44 4F 2 1 version u8 = 1 3 1 flags u8 bit0 镜像 | bit1 低置信度 | bit2 上半身 4 4 frame_id u32 LE @@ -44,7 +44,7 @@ const { encodeBinaryFrame } = require('./dist/preprocess/keypoint-codec.js'); const points = [], conf = []; for (let i = 0; i < 33; i++) { points.push([(i%33)/32, 1-(i%33)/32, ((i%7)-3)/10]); - conf.push(i%2===0 ? 1.0 : 0.5); + conf.push(i%2===0? 1.0: 0.5); } const buf = encodeBinaryFrame(42, points.map((p,i)=>[...p,conf[i]]), 0b101); console.log(buf.toString('hex')); @@ -52,7 +52,7 @@ console.log(buf.toString('hex')); ``` 固定输入:`xs = (i%33)/32`、`ys = 1-(i%33)/32`、`zs = ((i%7)-3)/10`、 -`confidence = i%2==0 ? 1.0 : 0.5`、`frameId = 42`、`flags = 0b101`。 +`confidence = i%2==0? 1.0: 0.5`、`frameId = 42`、`flags = 0b101`。 ## 关键实现 diff --git a/tasks/005-websocket-client.md b/tasks/005-websocket-client.md index 84d1d1f..44ac829 100644 --- a/tasks/005-websocket-client.md +++ b/tasks/005-websocket-client.md @@ -11,7 +11,7 @@ ``` DSH.md -contracts/websocket-keypoints.md # 锚点:§1 端点与子协议、§3 控制帧 +contracts/websocket-keypoints.md # 锚点:1 端点与子协议、3 控制帧 app/src/main/java/com/opendance/studio/net/PoseWebSocketClient.kt ``` @@ -22,11 +22,11 @@ app/src/main/java/com/opendance/studio/net/PoseWebSocketClient.kt ## 关键实现 | 项 | 做法 | 理由 | -|---|---|---| -| 子协议 | 请求头 `Sec-WebSocket-Protocol: opendance.pose.v1` | 契约 §1 | +|--|--|--| +| 子协议 | 请求头 `Sec-WebSocket-Protocol: opendance.pose.v1` | 契约 1 | | 二进制帧 | `socket.send(ByteString)` | 契约要求生产链路用二进制 | | 控制帧 | `send(text)`,JSON | hello / stats / bye | -| 自动重连 | 指数退避 1s → 2s → 4s … 上限 15s,连接成功即重置 | 避免抖动时打爆对端 | +| 自动重连 | 指数退避 1s -> 2s -> 4s... 上限 15s,连接成功即重置 | 避免抖动时打爆对端 | | 非连接态发送 | 直接丢弃并计入 `droppedFrames` | **绝不阻塞相机线程** | | 心跳 | OkHttp `pingInterval(15s)` | 及时发现半开连接 | | 读超时 | 0(不超时) | WebSocket 长连接 | diff --git a/tasks/006-connect-ui.md b/tasks/006-connect-ui.md index 8e4cfa5..7d918ee 100644 --- a/tasks/006-connect-ui.md +++ b/tasks/006-connect-ui.md @@ -27,15 +27,15 @@ HTTP 端点发现。因此: 1. 取手机在 WiFi 网的 IPv4 地址(排除 `rmnet*` 蜂窝网卡与 `169.254.*` 链路本地地址) 2. 推导 `/24` 网段 -3. 并发 TCP 探测 `192.168.x.1-254:8765`(并发 32、单机超时 300ms) -4. 以 `Flow` 形式边扫边产出,按延迟升序展示——最快响应的最可能是目标机 +3. 并发 TCP 探测 `<局域网网段>.1-254:8765`(并发 32、单机超时 300ms) +4. 以 `Flow` 形式边扫边产出,按延迟升序展示--最快响应的最可能是目标机 整段扫描约 2~3 秒。 ## 可配置项 | 项 | 说明 | -|---|---| +|--|--| | IP / 端口 | 默认 8765,与契约一致 | | 分辨率 | 480×360 / 640×480 / 1280×720 | | 目标帧率 | 写入 hello 的 targetFps | diff --git a/tasks/007-running-ui.md b/tasks/007-running-ui.md index 0e74de4..224ed7a 100644 --- a/tasks/007-running-ui.md +++ b/tasks/007-running-ui.md @@ -28,7 +28,7 @@ CameraX(ImageAnalysis) ──ImageProxy──▶ MediaPipe PoseLandmarker(端 ## 状态指示(本任务的验收重点) | 指标 | 含义 | -|---|---| +|--|--| | 连接状态 | 未连接 / 连接中 / 已连接 / 重连中(第 N 次)/ 已断开 | | 目标 | 完整 WS 地址 | | 会话 | 服务端 welcome 回来的 sessionId | @@ -39,7 +39,7 @@ CameraX(ImageAnalysis) ──ImageProxy──▶ MediaPipe PoseLandmarker(端 | 平均置信度 | < 70% 时转黄,提示光线或遮挡问题 | | 最近错误 | 连接或推理的错误原因 | -叠加层把 **低置信度点画成红色**,用户能直接看出是哪个部位没识别到—— +叠加层把 **低置信度点画成红色**,用户能直接看出是哪个部位没识别到-- 比只显示一个错误码有用得多。 ## 其他 diff --git a/tasks/009-build-apk.md b/tasks/009-build-apk.md index 4220fbc..ce8ddfb 100644 --- a/tasks/009-build-apk.md +++ b/tasks/009-build-apk.md @@ -20,10 +20,10 @@ app/build.gradle.kts ## 交付物 | 文件 | 说明 | -|---|---| +|--|--| | `build/gen-keystore.sh` | 生成发布签名密钥库(**不入库**) | | `build/build-apk.sh` | 构建 + 校验产物(魔数、签名方案) | -| `build/verify.sh` | 单元测试 → 构建 → 产物校验 一条龙 | +| `build/verify.sh` | 单元测试 -> 构建 -> 产物校验 一条龙 | ## 签名设计 @@ -32,7 +32,7 @@ app/build.gradle.kts ``` 1. keystore.properties(build/gen-keystore.sh 生成,已 gitignore) 2. 环境变量 KEYSTORE_PATH / KEYSTORE_PASSWORD / KEY_ALIAS / KEY_PASSWORD -3. 两者都缺失 → 回退 debug 签名,保证 CI 上 assembleRelease 也能产出已签名 APK +3. 两者都缺失 -> 回退 debug 签名,保证 CI 上 assembleRelease 也能产出已签名 APK ``` 正式发布必须走 1 或 2;回退路径仅为让 CI/验收可跑通,并在构建日志中给出提示。 @@ -41,16 +41,16 @@ app/build.gradle.kts ``` 1. Android Studio 打开 opendance-android/ -2. Build → Generate Signed Bundle / APK -3. 选择 APK → 选择或创建 keystore → 选择 release +2. Build -> Generate Signed Bundle / APK +3. 选择 APK -> 选择或创建 keystore -> 选择 release 4. 生成 app-release.apk ``` -## 验收(文档 §6.3 T-AND-009) +## 验收(文档 6.3 T-AND-009) | 验收项 | 结果 | -|---|---| -| Android Studio 可 Build → Generate Signed APK | ✅ 工程为标准 AGP 结构,`keystore.properties` 就位即可 | +|--|--| +| Android Studio 可 Build -> Generate Signed APK | ✅ 工程为标准 AGP 结构,`keystore.properties` 就位即可 | | `./gradlew assembleRelease` 成功生成签名 APK | ✅ 见下 | | APK 在 Android 10+ 设备可安装运行 | ⚠️ 本环境无 Android 设备/模拟器,未实机安装 | | 关于页包含开源项目声明 | ✅ `CreditsTest` 覆盖,且声明随包分发 | @@ -68,7 +68,7 @@ app-release.apk 55.5 MiB apksigner: Verifies(v2 方案) ## 环境记录 -- JDK 17(`/usr/lib/jvm/java-17-openjdk-amd64`) +- JDK 17(本地安装路径,示例 ``) - Android SDK:`platforms;android-34`、`build-tools;34.0.0` - 构建期修复的问题:`proguard-rules.pro` 缺失导致 R8 配置告警(已补全 keep 规则)、 androidTest 的 `androidx.test.runner` 依赖补齐