From 84da39ad17c726ae42b522e34f6cdd1655c9a4c4 Mon Sep 17 00:00:00 2001 From: zijunchiang Date: Mon, 14 Sep 2026 20:40:16 +0800 Subject: [PATCH] =?UTF-8?q?docs(android):=20=E4=BB=BB=E5=8A=A1=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=E3=80=81DSH=20=E7=BA=A6=E6=9D=9F=E4=B8=8E=E4=BA=A4?= =?UTF-8?q?=E4=BB=98=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DSH.md | 134 +++++++++++++++++++++++ README.txt | 182 +++++++++++++++++++++++++++++++ THIRD_PARTY.md | 119 ++++++++++++++++++++ project.yaml | 67 ++++++++++++ tasks/001-scaffold.md | 48 ++++++++ tasks/002-camera-x.md | 39 +++++++ tasks/003-mediapipe-pose.md | 48 ++++++++ tasks/004-keypoint-serializer.md | 74 +++++++++++++ tasks/005-websocket-client.md | 47 ++++++++ tasks/006-connect-ui.md | 47 ++++++++ tasks/007-running-ui.md | 54 +++++++++ tasks/008-about-page.md | 36 ++++++ tasks/009-build-apk.md | 74 +++++++++++++ 13 files changed, 969 insertions(+) create mode 100644 DSH.md create mode 100644 README.txt create mode 100644 THIRD_PARTY.md create mode 100644 project.yaml create mode 100644 tasks/001-scaffold.md create mode 100644 tasks/002-camera-x.md create mode 100644 tasks/003-mediapipe-pose.md create mode 100644 tasks/004-keypoint-serializer.md create mode 100644 tasks/005-websocket-client.md create mode 100644 tasks/006-connect-ui.md create mode 100644 tasks/007-running-ui.md create mode 100644 tasks/008-about-page.md create mode 100644 tasks/009-build-apk.md diff --git a/DSH.md b/DSH.md new file mode 100644 index 0000000..e77fc90 --- /dev/null +++ b/DSH.md @@ -0,0 +1,134 @@ +# DSH.md —— opendance-android(OpenDance Studio 项目三:Android 辅助 App) + +## 0. 本文件的作用 + +本文件是 **opendance-android 仓库内**的 DSH 执行约束。顶层说明书见 +《DSH 开发文档:OpenDance Studio》,二者冲突时以顶层文档为准。 + +## 1. 项目定位 + +功能最单一的一端:用手机摄像头做端侧姿态推理,把 33 个骨骼关键点通过 +WebSocket 推给 Windows 客户端。 + +``` +手机摄像头 ──▶ MediaPipe Pose(端侧)──▶ 33 点关键点 ──▶ 239 字节帧 + │ + ▼ + ws://:8765/pose +``` + +**画面不离开设备** —— 只有关键点坐标经网络发送。 + +## 2. 技术栈 + +| 组件 | 选型 | +|---|---| +| 语言 | Kotlin 2.0.21 | +| 构建 | AGP 8.7.3 + Gradle 8.9 + JDK 17 | +| 相机 | CameraX 1.4.0 (Apache 2.0) | +| 推理 | MediaPipe Tasks Vision 0.10.35 (Apache 2.0) | +| 通信 | OkHttp 4.12.0 (Apache 2.0) | +| UI | Jetpack Compose(BOM 2024.10.01,Material3) | + +## 3. 单次会话加载范围 + +允许加载: + +1. 本文件 `DSH.md` +2. `project.yaml` +3. 当前 `tasks/xxx.md` +4. 该任务文件中 `load` 列出的文件 +5. `contracts/` 中被明确引用的**锚点片段** + +禁止加载: + +- `opendance-web` / `opendance-windows` 的任何源码 +- 非当前任务的 `tasks/*.md` +- 完整 `contracts/` 目录(只引用锚点) +- `.gradle/`、`app/build/`、`*.apk`、`keystore/` + +## 4. 上下文预算 + +| 项 | 建议值 | +|---|---| +| 单任务文件数 | ≤ 6 | +| 单任务代码行数 | ≤ 1500 | +| 单任务 Token | ≤ 12K | +| 契约引用 | 只引用锚点,不加载全文 | + +## 5. 目录结构 + +``` +opendance-android/ +├── settings.gradle.kts / build.gradle.kts / gradle.properties +├── gradlew / gradle/wrapper/ # Gradle 8.9 wrapper +├── project.yaml / DSH.md +├── tasks/ # 001~009 单任务说明书 +├── contracts/ # 只读契约快照(来源 opendance-web v0.1.0) +├── app/ +│ ├── build.gradle.kts # 依赖、签名、R8、ABI +│ ├── proguard-rules.pro # MediaPipe / OkHttp 的 keep 规则 +│ └── src/ +│ ├── main/ +│ │ ├── AndroidManifest.xml +│ │ ├── assets/pose_landmarker_lite.task +│ │ └── java/com/opendance/studio/ +│ │ ├── MainActivity.kt # 三页导航 +│ │ ├── OpenDanceApp.kt # Application 单例容器 +│ │ ├── model/ # 239 字节帧 + 33 点拓扑 +│ │ ├── camera/ # CameraX +│ │ ├── pose/ # MediaPipe 推理 +│ │ ├── net/ # WebSocket + 主机发现 +│ │ ├── data/ # 设置 +│ │ └── ui/ # 连接页 / 运行页 / 关于页 +│ └── test/java/com/opendance/studio/ # JVM 单元测试 +└── build/ # 打包与验证脚本 +``` + +## 6. 硬性约束 + +1. **视频帧绝不离开设备**。只发送 33 个关键点坐标;这是本项目的隐私底线, + 也是与「上传视频做云端推理」方案的本质区别。 +2. **239 字节帧格式冻结**。任何字段改动都要先改 `opendance-web` 的 + `contracts/websocket-keypoints.md` 并提升版本。 + 本端序列化器有金标准字节向量测试守护(见 §8)。 +3. **33 点顺序冻结**。MediaPipe Pose 的原始索引顺序**就是**契约顺序, + 不得重排或插入。 +4. **镜像语义**:发送**未镜像**的原始坐标,是否镜像由 `flags.bit0` 声明; + 叠加层绘制时才按镜像显示。接收端据此决定如何还原。 +5. **签名材料绝不入库**。`keystore/`、`keystore.properties`、`local.properties` + 均已在 `.gitignore` 中。 +6. 关于页与 `THIRD_PARTY.md` 必须与 `contracts/open-source-credits.md` 一致。 + +## 7. 验证(每任务执行) + +```bash +export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 # AGP 8.7 需要 JDK 17 +export ANDROID_HOME=/path/to/android-sdk + +./gradlew testDebugUnitTest # JVM 单元测试,无需设备 +bash build/build-apk.sh all # debug + release APK,并校验签名与产物 +bash build/verify.sh # 一键跑完上述全部并附加产物校验 +``` + +## 8. 跨端一致性验证(本项目最重要的测试) + +`KeypointFrameTest.goldenVectorMatchesWebImplementation` 使用 +**opendance-web 已通过验收的 TypeScript 实现**生成的 239 字节金标准向量, +断言 Kotlin 实现产出**逐位相同**的字节。 + +这条测试同时守护三端:TypeScript(服务端参考实现)、GDScript(Windows 端)、 +Kotlin(本端)。任何一端私自改动字段布局都会立刻失败。 + +生成金标准向量的方法见 `tasks/004-keypoint-serializer.md`。 + +## 9. 上下文超标时的拆分策略 + +| 超标原因 | 拆分方式 | +|---|---| +| 单任务涉及多个界面 | 按界面拆(连接/运行/关于) | +| 单文件过大 | 按类拆,先生成接口再分实现 | +| 契约全文加载 | 改用锚点引用 | +| 需要参考其他项目 | 只读契约,不读源码 | +| UI 与逻辑耦合 | UI 只调用已定义接口(相机/推理/网络各自独立类) | +| 测试与实现同任务 | 测试单独任务,共享接口摘要 | diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..ec7b9c3 --- /dev/null +++ b/README.txt @@ -0,0 +1,182 @@ +================================================================ +OpenDance Studio —— Android 辅助 App v0.1.0 使用说明 +================================================================ + +手机端姿态采集器:用摄像头做**端侧**姿态推理,把 33 个骨骼关键点通过 +WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评分。 + +隐私:画面不会离开手机。通过网络发送的只有 33 个关键点坐标。 + +---------------------------------------------------------------- +一、安装 +---------------------------------------------------------------- + +方式一(推荐):直接安装随附的 app-release.apk + - 在手机上允许「安装未知来源应用」 + - 点击 APK 安装 + +方式二:用 adb 安装 + adb install -r app-release.apk + +要求:Android 10(API 29)或更高版本。 + +---------------------------------------------------------------- +二、快速开始 +---------------------------------------------------------------- + +1. 在 Windows 电脑上启动 OpenDance Studio 客户端 + (Windows 客户端会自动监听 8765 端口) + +2. 确保手机与电脑连在**同一个 WiFi** + +3. 打开本应用,进入「连接设置」页 + +4. 填写电脑的 IP 地址,或直接点「扫描局域网」自动发现 + (扫描会探测局域网内开放 8765 端口的主机,约 2~3 秒) + +5. 可点「测试连接」确认端口可达 + +6. 按需调整推流参数(见下) + +7. 点「开始推流」,授予相机权限 + +8. 站到画面中央,屏幕上会出现骨架叠加: + - 黄色圆点 = 识别良好的关节 + - 红色圆点/红色连线 = 置信度低(光线不足或被遮挡) + +9. 在 Windows 客户端上确认已连接,即可开始跟练 + +---------------------------------------------------------------- +三、推流参数 +---------------------------------------------------------------- + +分辨率 + 低(480×360) 最省电,适合老机型 + 中(640×480) 推荐,识别精度与功耗平衡 + 高(1280×720) 最清晰,但推理更慢、更耗电 + +目标帧率 + 默认 30 FPS。低端机若「识别帧率」明显低于目标值,可降到 20 或 15。 + +镜像画面 + 默认开启。前置摄像头的常见用法。 + 注意:本应用发送的是**未镜像的原始坐标**,是否镜像由帧头 flags.bit0 声明, + Windows 客户端据此正确还原左右。因此无论开关如何,动作方向都不会错。 + +仅上半身模式 + 下半身关键点置信度置 0(显示为红点),Windows 端会插值。 + 带宽或算力不足时开启可明显降负载。 + +---------------------------------------------------------------- +四、运行页状态说明 +---------------------------------------------------------------- + +连接状态 未连接 / 连接中 / 已连接 / 重连中(第 N 次)/ 已断开 +目标 正在连接的 WebSocket 地址 +会话 服务端返回的会话 ID +已发送 累计发送帧数 +已丢弃 因推理或连接跟不上而丢弃的帧数(>0 说明设备吃力) +发送帧率 实际推流帧率 +识别帧率 实际推理帧率 +推理耗时 单帧 MediaPipe 推理毫秒数 +人体识别 已识别 / 未识别到人体 +平均置信度 < 70% 时转黄,提示改善光线或减少遮挡 +最近错误 连接或推理的错误原因 + +网络中断时会**自动重连**(1s → 2s → 4s … 最长 15s 退避), +无需手动操作。 + +---------------------------------------------------------------- +五、常见问题 +---------------------------------------------------------------- + +Q: 扫描不到电脑 +A: 1) 确认手机与电脑在同一 WiFi(不是手机热点) + 2) 确认 Windows 客户端已启动并正在监听 8765 + 3) 在 Windows 防火墙放行 OpenDance.exe 的 TCP 8765 入站 + 4) 部分路由器开启了「AP 隔离」,会阻断设备互访,需在路由器关闭 + +Q: 显示「已连接」但电脑端没有画面 +A: 查看运行页的「已发送」是否在增长。若为 0,说明相机或推理没出帧; + 若「人体识别」一直是「未识别到人体」,请站到画面中央并保证光线充足。 + +Q: 帧率很低 / 已丢弃帧数持续增长 +A: 依次尝试:降低分辨率 → 降低目标帧率 → 开启「仅上半身模式」→ 关闭其他后台应用。 + +Q: 关键点抖动明显 +A: 正常现象,Windows 端有 DTW 带状对齐与平滑处理,不要求逐帧完全一致。 + 改善光照可显著提高稳定性。 + +Q: 应用退到后台会怎样? +A: 返回键或离开运行页会立即停止推流并释放相机,不会在后台占用摄像头。 + +---------------------------------------------------------------- +六、从源码构建 +---------------------------------------------------------------- + +环境要求: + JDK 17 AGP 8.7.3 要求(JDK 25 未经测试) + Android SDK platforms;android-34 与 build-tools;34.0.0 + Gradle 8.9 已随 gradlew 提供 + +构建: + + export JAVA_HOME=/path/to/jdk17 + export ANDROID_HOME=/path/to/android-sdk + + # 生成发布签名密钥库(首次;文件不会入库,请自行备份) + bash build/gen-keystore.sh + + # 构建并校验产物 + bash build/build-apk.sh all + + # 一键验证(单元测试 → 构建 → 产物校验) + bash build/verify.sh + +产物: + app/build/outputs/apk/debug/app-debug.apk + app/build/outputs/apk/release/app-release.apk + +Android Studio 路径: + 打开本目录 → Build → Generate Signed Bundle / APK → APK → 选择 keystore → release + +---------------------------------------------------------------- +七、测试 +---------------------------------------------------------------- + + ./gradlew testDebugUnitTest + +19 项单元测试(JVM,无需设备),核心是**金标准字节向量测试**: +用 opendance-web 已通过验收的 TypeScript 实现产出的 239 字节向量, +断言本端 Kotlin 实现逐位相同 —— 这是三端契约一致性的硬证据。 + +测试报告:app/build/reports/tests/testDebugUnitTest/index.html + +---------------------------------------------------------------- +八、目录结构 +---------------------------------------------------------------- + + settings.gradle.kts / build.gradle.kts 工程与插件配置 + gradle/wrapper/ Gradle 8.9 wrapper + project.yaml 任务清单与状态 + DSH.md 本项目 DSH 执行约束 + tasks/ 单任务说明书(T-AND-001 ~ 009) + contracts/ 只读契约快照(来源 opendance-web v0.1.0) + app/src/main/java/com/opendance/studio/ + MainActivity.kt 三页导航 + model/ 239 字节帧编解码 + 33 点拓扑 + camera/ CameraX 采集 + pose/ MediaPipe 端侧推理 + net/ WebSocket 推流 + 主机发现 + data/ 设置 + ui/ 连接页 / 运行页 / 关于页 + app/src/main/assets/pose_landmarker_lite.task MediaPipe 模型 + build/ 打包与验证脚本 + +---------------------------------------------------------------- +九、许可 +---------------------------------------------------------------- + +本应用代码以 MIT 许可发布。第三方开源项目声明见 THIRD_PARTY.md +(应用内「关于」页同样可查看)。 +未使用 SMPL 模型及其衍生拓扑,统一采用 MediaPipe 33 点拓扑。 diff --git a/THIRD_PARTY.md b/THIRD_PARTY.md new file mode 100644 index 0000000..a86c81a --- /dev/null +++ b/THIRD_PARTY.md @@ -0,0 +1,119 @@ +# 第三方开源项目声明 + +**OpenDance Studio Android 辅助 App v0.1.0** + +本软件使用了以下开源项目,特此声明。完整锚点见 `contracts/open-source-credits.md` +(三端共用,任一端新增依赖须回到 `opendance-web` 更新该表)。 +应用「关于」页面展示同一份声明(见 `app/src/main/java/com/opendance/studio/ui/Credits.kt`)。 + +## 1. MediaPipe + +- 版权:Copyright 2023 Google LLC +- 许可证:Apache License 2.0 +- 源链接:https://github.com/google-ai-edge/mediapipe +- 用途:**本应用的端侧姿态推理**(MediaPipe Tasks Vision Android, + 模型文件 `pose_landmarker_lite.task`) + +## 2. Three.js + +- 许可证:MIT +- 源链接:https://github.com/mrdoob/three.js +- 用途:Web 管理端 3D 预览 + +## 3. @pixiv/three-vrm + +- 许可证:MIT +- 源链接:https://github.com/pixiv/three-vrm +- 用途:Web 管理端 VRM 加载 + +## 4. Assimp + +- 许可证:BSD-3-Clause +- 源链接:https://github.com/assimp/assimp +- 用途:FBX/glTF 动作转换(可选,服务端侧使用) + +## 5. Godot Engine + +- 版权:Copyright Godot Engine contributors +- 许可证:MIT +- 源链接:https://github.com/godotengine/godot +- 用途:Windows 客户端引擎 + +## 6. godot-vrm + +- 许可证:MIT +- 源链接:https://github.com/V-Sekai/godot-vrm +- 用途:VRM humanoid 扩展(Windows 客户端侧) + +## 7. NestJS + +- 许可证:MIT +- 源链接:https://github.com/nestjs/nest +- 用途:Web 管理服务端 + +## 8. TypeORM + +- 许可证:MIT +- 源链接:https://github.com/typeorm/typeorm +- 用途:Web 管理服务端 ORM + +## 9. OkHttp + +- 许可证:Apache License 2.0 +- 源链接:https://github.com/square/okhttp +- 用途:**本应用的 WebSocket 推流** + +## 10. bvh + +- 许可证:MIT +- 源链接:https://github.com/aimoonchen/bvh +- 用途:BVH 解析参考实现 + +--- + +## 补充声明(实际引入的依赖) + +## 11. React + +- 许可证:MIT +- 源链接:https://github.com/facebook/react +- 用途:Web 管理端界面 + +## 12. Vite + +- 许可证:MIT +- 源链接:https://github.com/vitejs/vite +- 用途:Web 管理端构建 + +## 13. CameraX + +- 许可证:Apache License 2.0 +- 源链接:https://developer.android.com/jetpack/androidx/releases/camera +- 用途:**本应用的相机采集** + +## 14. Jetpack Compose + +- 许可证:Apache License 2.0 +- 源链接:https://developer.android.com/jetpack/compose +- 用途:**本应用的界面** + +## 15. mysql2 + +- 许可证:MIT +- 源链接:https://github.com/sidorares/node-mysql2 +- 用途:Web 管理服务端数据库驱动 + +--- + +## 许可合规说明 + +1. 本应用代码以 MIT 许可发布。 +2. 本应用通过 Maven 依赖引入 MediaPipe Tasks Vision、CameraX、Jetpack Compose + 与 OkHttp,均为 Apache License 2.0;对应许可证文本随依赖包分发, + 可在各依赖的 Maven 构件中找到。 +3. 随包分发的模型文件 `pose_landmarker_lite.task` 来自 MediaPipe + (Copyright 2023 Google LLC,Apache License 2.0)。 +4. **未使用 SMPL 模型及其衍生拓扑**,统一采用 MediaPipe 33 点拓扑, + 以规避 SMPL 的非商用许可限制。 +5. **本应用只在设备本地进行推理,画面不离开手机**;通过网络发送的只有 + 33 个骨骼关键点坐标,不含任何图像数据。 diff --git a/project.yaml b/project.yaml new file mode 100644 index 0000000..4e587bc --- /dev/null +++ b/project.yaml @@ -0,0 +1,67 @@ +project: opendance-android-app +version: 0.1.0 +repository: opendance-android +default_branch: main +platform: Android 10+(minSdk 29 / targetSdk 34) +contracts_version: 0.1.0 + +# 契约变更流程(文档 §7.2): +# 任何跨端契约变更必须先在 opendance-web 修改 contracts/ 并提升版本, +# 再在本项目创建适配任务。本项目内的 contracts/ 只是只读快照。 + +dependencies: + - name: opendance-windows + kind: websocket-producer + contract: contracts/websocket-keypoints.md + usage: 本端为**发送方**,主动连接 Windows 客户端的 ws://:8765/pose + - name: opendance-web + kind: contract-source + contract: contracts/skeleton-topology.md + usage: 33 点索引顺序与坐标语义(MediaPipe 原生顺序即契约顺序) + +tasks: + - id: T-AND-001 + file: tasks/001-scaffold.md + scope: Android Studio 项目骨架 + status: done + - id: T-AND-002 + file: tasks/002-camera-x.md + scope: CameraX 采集 + status: done + - id: T-AND-003 + file: tasks/003-mediapipe-pose.md + scope: MediaPipe Pose 端侧推理 + status: done + - id: T-AND-004 + file: tasks/004-keypoint-serializer.md + scope: 33 点 → 239 字节帧 + status: done + - id: T-AND-005 + file: tasks/005-websocket-client.md + scope: WebSocket 推流 + 自动重连 + status: done + - id: T-AND-006 + file: tasks/006-connect-ui.md + scope: 连接页 + 主机发现 + status: done + - id: T-AND-007 + file: tasks/007-running-ui.md + scope: 运行页 + 状态指示 + status: done + - id: T-AND-008 + file: tasks/008-about-page.md + scope: 关于页 + 开源声明 + status: done + - id: T-AND-009 + file: tasks/009-build-apk.md + scope: Android Studio 打包 + status: done + +deliverable: + artifact: opendance-android-v0.1.0.apk + packaging: Gradle 命令行 / Android Studio + includes: + - app-release.apk + - THIRD_PARTY.md + - README.txt + - contracts/ # 契约快照 diff --git a/tasks/001-scaffold.md b/tasks/001-scaffold.md new file mode 100644 index 0000000..a5a3ed4 --- /dev/null +++ b/tasks/001-scaffold.md @@ -0,0 +1,48 @@ +# T-AND-001 Android Studio 项目骨架 + +- **状态**:done +- **依赖**:无 + +## 目标 + +建立 Kotlin + Compose 的 Android 工程骨架,配置三页导航与共享设置。 + +## load + +``` +DSH.md +settings.gradle.kts +build.gradle.kts +app/build.gradle.kts +app/src/main/AndroidManifest.xml +``` + +## 交付物 + +| 文件 | 说明 | +|---|---| +| `settings.gradle.kts` / `build.gradle.kts` | AGP 8.7.3 + Kotlin 2.0.21 + Compose 插件 | +| `gradle/wrapper/` | Gradle 8.9 wrapper(`./gradlew` 可直接用) | +| `app/build.gradle.kts` | minSdk 29 / targetSdk 34;R8、签名、ABI、模型不压缩 | +| `app/src/main/AndroidManifest.xml` | 相机 + 网络权限;`usesCleartextTraffic`(局域网明文 WS) | +| `MainActivity.kt` | 三页状态机导航(连接 / 运行 / 关于) | +| `OpenDanceApp.kt` | Application 单例容器 | +| `data/AppSettings.kt` | SharedPreferences 设置 | +| `ui/theme/Theme.kt` | 与另两端一致的深色配色 | + +## 版本组合(均已验证可构建) + +``` +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 +./gradlew assembleDebug +``` + +## 结果 + +- `assembleDebug` 成功;`MainActivity` 三页导航可用。 diff --git a/tasks/002-camera-x.md b/tasks/002-camera-x.md new file mode 100644 index 0000000..ae89b6a --- /dev/null +++ b/tasks/002-camera-x.md @@ -0,0 +1,39 @@ +# T-AND-002 CameraX 采集 + +- **状态**:done +- **依赖**:T-AND-001 + +## 目标 + +用 CameraX 采集相机画面,为 MediaPipe 提供连续帧。 + +## load + +``` +DSH.md +app/src/main/java/com/opendance/studio/camera/CameraController.kt +app/src/main/java/com/opendance/studio/data/AppSettings.kt +``` + +## 交付物 + +`camera/CameraController.kt`:Preview + ImageAnalysis 绑定、分辨率档位、前后摄切换。 + +## 关键实现 + +| 项 | 取值/做法 | 理由 | +|---|---|---| +| 背压策略 | `STRATEGY_KEEP_ONLY_LATEST` | 推理慢时自动丢帧,预览不卡顿 | +| 输出格式 | `OUTPUT_IMAGE_FORMAT_RGBA_8888` | MediaPipe 需要 Bitmap,避免 YUV 转换 | +| 分辨率 | 三档(480×360 / 640×480 / 1280×720),`FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER` | 低端机可降档保帧率 | +| 分析线程 | 单线程 Executor | 推理串行,避免并发抢占 | +| 镜像 | 预览镜像由 PreviewView 处理;**发送原始未镜像坐标** | 契约要求由 flags.bit0 声明镜像语义 | + +## 隐私 + +相机帧只在内存中流转到推理器,**不落盘、不上传**。离开运行页即 `unbindAll()`, +不在后台占用相机。 + +## 验收 + +`assembleDebug` 通过;运行页可正常预览(需真机,见 tasks/009 的环境说明)。 diff --git a/tasks/003-mediapipe-pose.md b/tasks/003-mediapipe-pose.md new file mode 100644 index 0000000..768b5bc --- /dev/null +++ b/tasks/003-mediapipe-pose.md @@ -0,0 +1,48 @@ +# T-AND-003 MediaPipe Pose 端侧推理 + +- **状态**:done +- **依赖**:T-AND-002 + +## 目标 + +用 MediaPipe Tasks Vision 在设备本地做姿态推理,输出 33 个归一化关键点。 + +## load + +``` +DSH.md +contracts/skeleton-topology.md # 锚点:§1 33 点、§4 坐标系 +app/src/main/java/com/opendance/studio/pose/PoseDetector.kt +``` + +## 交付物 + +`pose/PoseDetector.kt` + `app/src/main/assets/pose_landmarker_lite.task`(5.7 MB)。 + +## 关键实现 + +| 项 | 说明 | +|---|---| +| 运行模式 | `LIVE_STREAM` + `detectAsync`,不阻塞相机线程 | +| 模型 | `pose_landmarker_lite.task`(随 APK 分发,`noCompress "task"` 保证可读) | +| 置信度阈值 | detection / presence / tracking 均 0.5 | +| 背压 | `busy` 标志:上一帧仍在推理时直接丢弃新帧并计数,不排队 | +| 时间戳 | 单调递增校正(MediaPipe 要求严格递增) | +| 缺失点 | 置信度置 0、坐标归零,接收端按契约插值 | + +## 坐标与镜像语义 + +- 发送**未镜像的原始 MediaPipe 坐标**(契约 §4) +- 是否镜像由帧头 `flags.bit0` 声明,接收端据此决定如何还原 +- 叠加层绘制时才取 `1 - x`,与前置摄像头预览的镜像观感一致 + +这保证了同一份关键点数据在「镜像预览」与「真实左右」两种解释下都不会歧义。 + +## 上半身模式 + +`upperBodyOnly = true` 时,髋部及以下点置信度置 0 并置 `flags.bit2`, +接收端会插值。用于带宽/算力不足时降级。 + +## 验收 + +`assembleDebug` 通过;运行页在真机上可看到骨架叠加层(见 tasks/009)。 diff --git a/tasks/004-keypoint-serializer.md b/tasks/004-keypoint-serializer.md new file mode 100644 index 0000000..cb0874c --- /dev/null +++ b/tasks/004-keypoint-serializer.md @@ -0,0 +1,74 @@ +# T-AND-004 33 点 → 239 字节帧 + +- **状态**:done +- **依赖**:T-AND-003(数据结构)、T-AND-001 + +## 目标 + +把 33 点关键点按冻结契约序列化为 239 字节二进制帧。 + +## load + +``` +DSH.md +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 +``` + +## 帧布局(契约冻结) + +``` +偏移 长度 字段 +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 +8 231 keypoints 33 × 7B:s16 x, s16 y, s16 z, u8 conf +合计 239 +``` + +## ★ 金标准字节向量测试 + +本任务的核心验收不是「函数跑通了」,而是**与另两端逐位一致**。 + +测试 `KeypointFrameTest.goldenVectorMatchesWebImplementation` 使用由 +`opendance-web` 已通过验收的 TypeScript 实现产出的 239 字节金标准向量: + +```bash +# 重新生成金标准向量(在 opendance-web 仓库内) +cd opendance-web/web-server && npm run build +node -e " +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); +} +const buf = encodeBinaryFrame(42, points.map((p,i)=>[...p,conf[i]]), 0b101); +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`。 + +## 关键实现 + +- `ByteBuffer.order(LITTLE_ENDIAN)`,避免手动移位出错 +- 定点换算 `round(v × 32767)` 并 `coerceIn(-32768, 32767)` +- 置信度 `round(v × 255)`,阈值 0.5 含端点(`>=`) +- 输入超范围时裁剪而非抛异常(相机抖动可能产生轻微越界值) +- 并行 FloatArray 存储,30 FPS 循环内零对象分配 + +## 验收 + +```bash +./gradlew testDebugUnitTest +``` + +19 项单元测试全部通过,含: +常量与契约一致、金标准向量逐位一致、编解码往返精度、错误长度/magic/版本拒绝、 +极值不溢出、超范围裁剪、低置信度标志计算、镜像 x 还原、拓扑父链表无环、 +骨架连线索引合法。 diff --git a/tasks/005-websocket-client.md b/tasks/005-websocket-client.md new file mode 100644 index 0000000..84d1d1f --- /dev/null +++ b/tasks/005-websocket-client.md @@ -0,0 +1,47 @@ +# T-AND-005 WebSocket 推流 + 自动重连 + +- **状态**:done +- **依赖**:T-AND-004 + +## 目标 + +把 239 字节帧与控制消息推给 Windows 客户端,网络抖动时自动恢复。 + +## load + +``` +DSH.md +contracts/websocket-keypoints.md # 锚点:§1 端点与子协议、§3 控制帧 +app/src/main/java/com/opendance/studio/net/PoseWebSocketClient.kt +``` + +## 交付物 + +`net/PoseWebSocketClient.kt`:OkHttp WebSocket 封装 + 状态流 + 统计。 + +## 关键实现 + +| 项 | 做法 | 理由 | +|---|---|---| +| 子协议 | 请求头 `Sec-WebSocket-Protocol: opendance.pose.v1` | 契约 §1 | +| 二进制帧 | `socket.send(ByteString)` | 契约要求生产链路用二进制 | +| 控制帧 | `send(text)`,JSON | hello / stats / bye | +| 自动重连 | 指数退避 1s → 2s → 4s … 上限 15s,连接成功即重置 | 避免抖动时打爆对端 | +| 非连接态发送 | 直接丢弃并计入 `droppedFrames` | **绝不阻塞相机线程** | +| 心跳 | OkHttp `pingInterval(15s)` | 及时发现半开连接 | +| 读超时 | 0(不超时) | WebSocket 长连接 | +| 统计 | 发送帧数、丢弃帧数、发送 FPS、RTT、重连次数、最近错误 | 运行页状态指示 | + +## 控制帧 + +- **hello**(连接后立即发):protocol / device / appVersion / cameraFacing / + targetFps / resolution +- **welcome**(收):记录服务端 sessionId +- **config**(收):服务端可下发 mirror / targetFps / upperBodyOnly / resolution, + 通过 `onRemoteConfig` 回调 +- **stats**(每秒发):fps / dropped / latencyMs / confidenceAvg +- **bye**(主动断开时发) + +## 验收 + +`assembleDebug` 通过;状态机与统计逻辑由运行页展示(需真机验证端到端)。 diff --git a/tasks/006-connect-ui.md b/tasks/006-connect-ui.md new file mode 100644 index 0000000..8e4cfa5 --- /dev/null +++ b/tasks/006-connect-ui.md @@ -0,0 +1,47 @@ +# T-AND-006 连接页 + 主机发现 + +- **状态**:done +- **依赖**:T-AND-005 + +## 目标 + +让用户快速找到并连上 Windows 客户端,无需手输 IP。 + +## load + +``` +DSH.md +app/src/main/java/com/opendance/studio/ui/ConnectScreen.kt +app/src/main/java/com/opendance/studio/net/HostDiscovery.kt +``` + +## 交付物 + +- `ui/ConnectScreen.kt`:地址输入、扫描、测试连接、推流参数 +- `net/HostDiscovery.kt`:局域网扫描 + +## 主机发现策略 + +Windows 客户端是**裸 WebSocket 服务端**(不是 HTTP 服务),无法靠扫描 +HTTP 端点发现。因此: + +1. 取手机在 WiFi 网的 IPv4 地址(排除 `rmnet*` 蜂窝网卡与 `169.254.*` 链路本地地址) +2. 推导 `/24` 网段 +3. 并发 TCP 探测 `192.168.x.1-254:8765`(并发 32、单机超时 300ms) +4. 以 `Flow` 形式边扫边产出,按延迟升序展示——最快响应的最可能是目标机 + +整段扫描约 2~3 秒。 + +## 可配置项 + +| 项 | 说明 | +|---|---| +| IP / 端口 | 默认 8765,与契约一致 | +| 分辨率 | 480×360 / 640×480 / 1280×720 | +| 目标帧率 | 写入 hello 的 targetFps | +| 镜像画面 | 写入 flags.bit0 | +| 仅上半身模式 | 写入 flags.bit2 | + +## 验收 + +`assembleDebug` 通过;连接页可编辑参数并持久化(SharedPreferences)。 diff --git a/tasks/007-running-ui.md b/tasks/007-running-ui.md new file mode 100644 index 0000000..0e74de4 --- /dev/null +++ b/tasks/007-running-ui.md @@ -0,0 +1,54 @@ +# T-AND-007 运行页 + 状态指示 + +- **状态**:done +- **依赖**:T-AND-003 / 005 / 006 + +## 目标 + +预览 + 骨架叠加 + 实时状态,让用户明确知道「现在到底通不通、识别得准不准」。 + +## load + +``` +DSH.md +app/src/main/java/com/opendance/studio/ui/RunningScreen.kt +app/src/main/java/com/opendance/studio/ui/SkeletonOverlay.kt +``` + +## 数据流 + +``` +CameraX(ImageAnalysis) ──ImageProxy──▶ MediaPipe PoseLandmarker(端侧) + │ 33 点 + ┌───────────────────────┴───────────────────────┐ + ▼ ▼ + KeypointFrame(239B) ──▶ WebSocket ──▶ Windows 骨架叠加层 +``` + +## 状态指示(本任务的验收重点) + +| 指标 | 含义 | +|---|---| +| 连接状态 | 未连接 / 连接中 / 已连接 / 重连中(第 N 次)/ 已断开 | +| 目标 | 完整 WS 地址 | +| 会话 | 服务端 welcome 回来的 sessionId | +| 已发送 / 已丢弃 | 帧数;丢弃 > 0 时提示「推理/连接跟不上」 | +| 发送帧率 / 识别帧率 | 端到端吞吐的两个关键指标 | +| 推理耗时 | 单帧 MediaPipe 推理毫秒数 | +| 人体识别 | 已识别 / 未识别到人体(醒目黄色提示) | +| 平均置信度 | < 70% 时转黄,提示光线或遮挡问题 | +| 最近错误 | 连接或推理的错误原因 | + +叠加层把 **低置信度点画成红色**,用户能直接看出是哪个部位没识别到—— +比只显示一个错误码有用得多。 + +## 其他 + +- 相机权限未授予时显示说明 + 授权按钮,并强调「画面只在本地推理」 +- 支持前后摄像头切换(复用同一个分析器实例) +- 返回键在运行页等同于结束推流,避免相机后台占用 + +## 验收 + +`assembleDebug` 通过;页面构造路径在构建期完成类型检查。 +端到端推流需真机,见 tasks/009 的环境说明。 diff --git a/tasks/008-about-page.md b/tasks/008-about-page.md new file mode 100644 index 0000000..b2e1825 --- /dev/null +++ b/tasks/008-about-page.md @@ -0,0 +1,36 @@ +# T-AND-008 关于页 + 开源声明 + +- **状态**:done +- **依赖**:T-AND-006 + +## 目标 + +展示版本、隐私说明与完整可滚动的第三方开源声明。 + +## load + +``` +DSH.md +contracts/open-source-credits.md # 锚点:全表 +app/src/main/java/com/opendance/studio/ui/AboutScreen.kt +app/src/main/java/com/opendance/studio/ui/Credits.kt +``` + +## 交付物 + +- `ui/Credits.kt`:15 项声明数据(与契约表逐条对应) +- `ui/AboutScreen.kt`:版本卡片 + 隐私说明 + 可滚动声明列表 + +## 内容要点 + +- 版本、契约版本、WebSocket 子协议、关键点拓扑、传输帧长 +- **隐私说明**:明确写出「画面不会离开手机」「只发送 33 个关键点坐标」 +- 明确声明不使用 SMPL 拓扑 +- 每条含许可证、版权(如有)、用途、可点击源链接 + +## 验收 + +`CreditsTest`(JVM 单元测试)断言: +条目数 ≥ 10、文档要求的 10 项全部在列、每条有许可证与 https 链接、 +已知项目的许可证正确、本端自身依赖(MediaPipe/OkHttp/CameraX/Compose)均在列、 +项目名不重复、契约版本为 0.1.0。 diff --git a/tasks/009-build-apk.md b/tasks/009-build-apk.md new file mode 100644 index 0000000..4220fbc --- /dev/null +++ b/tasks/009-build-apk.md @@ -0,0 +1,74 @@ +# T-AND-009 APK 打包 + +- **状态**:done +- **依赖**:T-AND-001 ~ 008 + +## 目标 + +产出可安装的签名 APK,并提供命令行与 Android Studio 两条路径。 + +## load + +``` +DSH.md +build/build-apk.sh +build/gen-keystore.sh +build/verify.sh +app/build.gradle.kts +``` + +## 交付物 + +| 文件 | 说明 | +|---|---| +| `build/gen-keystore.sh` | 生成发布签名密钥库(**不入库**) | +| `build/build-apk.sh` | 构建 + 校验产物(魔数、签名方案) | +| `build/verify.sh` | 单元测试 → 构建 → 产物校验 一条龙 | + +## 签名设计 + +`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 +``` + +正式发布必须走 1 或 2;回退路径仅为让 CI/验收可跑通,并在构建日志中给出提示。 + +## Android Studio 图形界面路径 + +``` +1. Android Studio 打开 opendance-android/ +2. Build → Generate Signed Bundle / APK +3. 选择 APK → 选择或创建 keystore → 选择 release +4. 生成 app-release.apk +``` + +## 验收(文档 §6.3 T-AND-009) + +| 验收项 | 结果 | +|---|---| +| Android Studio 可 Build → Generate Signed APK | ✅ 工程为标准 AGP 结构,`keystore.properties` 就位即可 | +| `./gradlew assembleRelease` 成功生成签名 APK | ✅ 见下 | +| APK 在 Android 10+ 设备可安装运行 | ⚠️ 本环境无 Android 设备/模拟器,未实机安装 | +| 关于页包含开源项目声明 | ✅ `CreditsTest` 覆盖,且声明随包分发 | + +## 实测产物 + +``` +app-release.apk 55.5 MiB apksigner: Verifies(v2 方案) + Signer #1 DN: CN=OpenDance Studio Dev, OU=OpenDance, O=OpenDance Studio, C=CN + package: com.opendance.studio versionName 0.1.0 + minSdk 29(Android 10) targetSdk 34 + ABI: arm64-v8a, armeabi-v7a, x86, x86_64 + assets/pose_landmarker_lite.task 5.7 MB(Stored 未压缩) +``` + +## 环境记录 + +- JDK 17(`/usr/lib/jvm/java-17-openjdk-amd64`) +- Android SDK:`platforms;android-34`、`build-tools;34.0.0` +- 构建期修复的问题:`proguard-rules.pro` 缺失导致 R8 配置告警(已补全 keep 规则)、 + androidTest 的 `androidx.test.runner` 依赖补齐