docs: 消除 Gitea 模棱两可 Unicode 告警(全角标点紧邻 ASCII 时改为半角)
依据 Gitea v1.27.3 modules/charset/escape_stream.go 的判定规则 (analyzeDetectResults:仅当左右相邻字符都不是非 ASCII 时才标记, 且 _common 表恒定生效),把说明文档中会被标记的全角标点 (,:;?!() 等)改为半角,并保证成对括号宽度一致。 中文语境中的全角标点(左右均为中文)保持原样,不动代码与契约语义。
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# DSH.md -- opendance-android(OpenDance Studio 项目三:Android 辅助 App)
|
||||
# DSH.md -- opendance-android(OpenDance Studio 项目三:Android 辅助 App)
|
||||
|
||||
## 0. 本文件的作用
|
||||
|
||||
@@ -28,7 +28,7 @@ WebSocket 推给 Windows 客户端。
|
||||
| 相机 | 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) |
|
||||
| UI | Jetpack Compose(BOM 2024.10.01, Material3) |
|
||||
|
||||
## 3. 单次会话加载范围
|
||||
|
||||
@@ -64,7 +64,7 @@ opendance-android/
|
||||
├── gradlew / gradle/wrapper/ # Gradle 8.9 wrapper
|
||||
├── project.yaml / DSH.md
|
||||
├── tasks/ # 001~009 单任务说明书
|
||||
├── contracts/ # 只读契约快照(来源 opendance-web v0.1.0)
|
||||
├── contracts/ # 只读契约快照(来源 opendance-web v0.1.0)
|
||||
├── app/
|
||||
│ ├── build.gradle.kts # 依赖、签名、R8、ABI
|
||||
│ ├── proguard-rules.pro # MediaPipe / OkHttp 的 keep 规则
|
||||
@@ -121,7 +121,7 @@ bash build/verify.sh # 一键跑完上述全部并附加产物校
|
||||
**opendance-web 已通过验收的 TypeScript 实现**生成的 239 字节金标准向量,
|
||||
断言 Kotlin 实现产出**逐位相同**的字节。
|
||||
|
||||
这条测试同时守护三端:TypeScript(服务端参考实现)、GDScript(Windows 端)、
|
||||
这条测试同时守护三端:TypeScript(服务端参考实现)、GDScript(Windows 端)、
|
||||
Kotlin(本端)。任何一端私自改动字段布局都会立刻失败。
|
||||
|
||||
生成金标准向量的方法见 `tasks/004-keypoint-serializer.md`。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# opendance-android
|
||||
|
||||
OpenDance Studio **阶段三**:Android 辅助 App(Kotlin + CameraX + MediaPipe 端侧推理)。
|
||||
OpenDance Studio **阶段三**: Android 辅助 App(Kotlin + CameraX + MediaPipe 端侧推理)。
|
||||
|
||||
用手机摄像头做**端侧**姿态推理,把 33 个骨骼关键点通过 WebSocket 推给 Windows 客户端,
|
||||
由后者驱动 VRM 角色并 DTW 实时评分。
|
||||
@@ -9,17 +9,17 @@ OpenDance Studio **阶段三**:Android 辅助 App(Kotlin + CameraX + MediaPi
|
||||
|
||||
## 状态
|
||||
|
||||
**阶段三已完成**:T-AND-001 -> T-AND-009 全部落地。
|
||||
**阶段三已完成**: T-AND-001 -> T-AND-009 全部落地。
|
||||
|
||||
| 验收项 | 结果 |
|
||||
|--|--|
|
||||
| 单元测试 | OK **30 项全部通过**(JVM,无需设备) |
|
||||
| 单元测试 | OK **30 项全部通过**(JVM,无需设备) |
|
||||
| **跨端金标准** | OK Kotlin 序列化器与 Web 端 TypeScript 实现**逐位一致** |
|
||||
| `./gradlew assembleRelease` | OK 生成**已签名** APK(28.6 MB) |
|
||||
| 签名校验 | OK `apksigner` 退出码 0,v2 方案,CN=OpenDance Studio Dev |
|
||||
| 包信息 | OK `com.opendance.studio` v0.1.0,minSdk 29(Android 10+)/ targetSdk 34 |
|
||||
| 模型随包分发 | OK `assets/pose_landmarker_lite.task` 5.7 MB(Stored 未压缩) |
|
||||
| ABI 覆盖 | OK arm64-v8a + armeabi-v7a(debug 额外含 x86/x86_64) |
|
||||
| `./gradlew assembleRelease` | OK 生成**已签名** APK(28.6 MB) |
|
||||
| 签名校验 | OK `apksigner` 退出码 0, v2 方案,CN=OpenDance Studio Dev |
|
||||
| 包信息 | OK `com.opendance.studio` v0.1.0, minSdk 29(Android 10+)/ targetSdk 34 |
|
||||
| 模型随包分发 | OK `assets/pose_landmarker_lite.task` 5.7 MB(Stored 未压缩) |
|
||||
| ABI 覆盖 | OK arm64-v8a + armeabi-v7a(debug 额外含 x86/x86_64) |
|
||||
|
||||
## 快速开始
|
||||
|
||||
@@ -33,7 +33,7 @@ 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),
|
||||
第一次上手请看下文的「初次部署(从零跑起来)」一节。
|
||||
|
||||
## 初次部署(从零跑起来)
|
||||
@@ -46,14 +46,14 @@ bash build/package-release.sh # 打发布包
|
||||
|
||||
| 项 | 要求 | 依据 |
|
||||
|--|--|--|
|
||||
| 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 过滤 |
|
||||
| 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 模拟器**。
|
||||
- release 包只打包 `arm64-v8a` + `armeabi-v7a`, **无法安装到 x86/x86_64 模拟器**。
|
||||
- debug 包额外含 `x86`/`x86_64`,可以装进模拟器,但模拟器相机是虚拟场景,
|
||||
CameraX 取到的是合成画面,MediaPipe 的姿态识别效果不能代表真机,
|
||||
也不能作为「第一次跑通」的验收依据。
|
||||
@@ -79,7 +79,7 @@ sdk.dir=<Android SDK 路径>
|
||||
`build/build-apk.sh` 在该文件缺失时会依据 `ANDROID_HOME` / `ANDROID_SDK_ROOT`
|
||||
自动生成它;也可以手动创建。**不要提交它**,每台机器路径不同。
|
||||
|
||||
签名材料同样**不入库**(`.gitignore` 已忽略 `keystore/`、`*.jks`、`*.keystore`、
|
||||
签名材料同样**不入库**(`.gitignore` 已忽略 `keystore/`、`*.jks`、`*.keystore`、
|
||||
`keystore.properties`):
|
||||
|
||||
- 仓库里**没有私钥**,只有生成脚本与说明。
|
||||
@@ -88,7 +88,7 @@ sdk.dir=<Android SDK 路径>
|
||||
(含 `storeFile`、`keyAlias` 与两个口令字段)。**请自行备份,且不要提交**。
|
||||
- `app/build.gradle.kts` 的签名取值优先级:
|
||||
`keystore.properties` -> 环境变量(`KEYSTORE_PATH` / `KEYSTORE_PASSWORD` /
|
||||
`KEY_ALIAS` / `KEY_PASSWORD`)-> 回退 debug 签名。
|
||||
`KEY_ALIAS` / `KEY_PASSWORD`)-> 回退 debug 签名。
|
||||
回退只为让 CI 也能产出已签名 APK;正式发布必须提供自己的 keystore。
|
||||
|
||||
### 3. 首次构建
|
||||
@@ -140,9 +140,9 @@ adb install -r app/build/outputs/apk/release/app-release.apk
|
||||
|
||||
| 界面字段 | 设置项 | 默认值 | 说明 |
|
||||
|--|--|--|--|
|
||||
| IP 地址 | `host` | RFC 5737 文档网段占位地址(源码为 `192.0.2.100`,**非真实主机**) | 填 Windows 电脑的局域网 IP |
|
||||
| IP 地址 | `host` | RFC 5737 文档网段占位地址(源码为 `192.0.2.100`, **非真实主机**) | 填 Windows 电脑的局域网 IP |
|
||||
| 端口 | `port` | `8765` | 与 Windows 客户端监听端口一致 |
|
||||
| 目标帧率(FPS) | `targetFps` | `30` | 取值范围 5~60 |
|
||||
| 目标帧率(FPS) | `targetFps` | `30` | 取值范围 5~60 |
|
||||
| 分辨率 | `resolution` | `640x480` | 可选 `480x360` / `640x480` / `1280x720` |
|
||||
| 镜像画面 | `mirror` | 开 | 只影响显示与 `flags.bit0`,发送的是未镜像坐标 |
|
||||
| 仅上半身模式 | `upperBodyOnly` | 关 | 下半身置信度置 0,接收端插值 |
|
||||
@@ -183,10 +183,10 @@ IP 地址:192.0.2.10 # RFC 5737 文档示例网段,实际填你电脑
|
||||
| # | 命令 | 预期输出 |
|
||||
|--|--|--|
|
||||
| 1 | `./gradlew testDebugUnitTest` | `BUILD SUCCESSFUL`,全部用例通过;报告在 `app/build/reports/tests/testDebugUnitTest/index.html` |
|
||||
| 2 | `bash build/build-apk.sh debug` | 末尾打印 `OK Debug APK:app/build/outputs/apk/debug/app-debug.apk` 及其大小与签名信息 |
|
||||
| 3 | `bash build/build-apk.sh all` | debug 与 release 均 `OK`;release 显示 `已签名(APK Signature Scheme v2)` |
|
||||
| 2 | `bash build/build-apk.sh debug` | 末尾打印 `OK Debug APK: app/build/outputs/apk/debug/app-debug.apk` 及其大小与签名信息 |
|
||||
| 3 | `bash build/build-apk.sh all` | debug 与 release 均 `OK`; 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 未压缩) |
|
||||
| 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` 必须通过:
|
||||
@@ -211,7 +211,7 @@ IP 地址:192.0.2.10 # RFC 5737 文档示例网段,实际填你电脑
|
||||
| 相机 | CameraX 1.4.0 |
|
||||
| 推理 | MediaPipe Tasks Vision 0.10.35 |
|
||||
| 通信 | OkHttp 4.12.0 |
|
||||
| UI | Jetpack Compose(BOM 2024.10.01,Material3) |
|
||||
| UI | Jetpack Compose(BOM 2024.10.01, Material3) |
|
||||
|
||||
## 数据流
|
||||
|
||||
@@ -235,7 +235,7 @@ app/src/main/java/com/opendance/studio/
|
||||
|
||||
## 依赖的契约(opendance-web v0.1.0 冻结)
|
||||
|
||||
- [`contracts/websocket-keypoints.md`](contracts/websocket-keypoints.md) - 239 字节关键点帧(本端为**发送方**)
|
||||
- [`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) - 关于页必须展示的开源声明
|
||||
|
||||
@@ -244,7 +244,7 @@ app/src/main/java/com/opendance/studio/
|
||||
|
||||
## 跨端一致性:金标准字节向量测试
|
||||
|
||||
本仓库最重要的测试不是「函数跑通了」,而是**与另两端逐位一致**:
|
||||
本仓库最重要的测试不是「函数跑通了」,而是**与另两端逐位一致**:
|
||||
|
||||
`KeypointFrameTest.goldenVectorMatchesWebImplementation` 用 opendance-web 已通过验收的
|
||||
TypeScript 实现生成 239 字节金标准向量,断言 Kotlin 实现产出完全相同的字节。
|
||||
@@ -252,12 +252,12 @@ TypeScript 实现生成 239 字节金标准向量,断言 Kotlin 实现产出
|
||||
|
||||
## 已知限制
|
||||
|
||||
1. Godot(Windows 端)的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
|
||||
1. Godot(Windows 端)的 `WebSocketPeer` 不暴露握手请求路径,因此**无法校验 path 是否为 `/pose`**,
|
||||
本端仍按契约连接到 `/pose`,互操作不受影响。
|
||||
2. **未在真实 Android 设备上安装运行**(本环境无设备也无模拟器)。
|
||||
已验证到:构建通过、签名有效、包信息正确、模型与 4 个 ABI 的 native 库均正确打包。
|
||||
3. 主机发现依赖手机与电脑处于同一 WiFi;路由器开启 AP 隔离时无法互访(见 README.txt FAQ)。
|
||||
4. release 包只含 arm64-v8a + armeabi-v7a。x86/x86_64 仅供模拟器,会占掉约 27 MB,
|
||||
4. release 包只含 arm64-v8a + armeabi-v7a。x86/x86_64 仅供模拟器,会占掉约 27 MB,
|
||||
对真机用户没有价值;debug 包保留全部 4 个 ABI 以便模拟器调试。
|
||||
|
||||
## 第三方开源声明
|
||||
|
||||
+17
-17
@@ -18,7 +18,7 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评
|
||||
方式二:用 adb 安装
|
||||
adb install -r app-release.apk
|
||||
|
||||
要求:Android 10(API 29)或更高版本。
|
||||
要求:Android 10(API 29)或更高版本。
|
||||
|
||||
--
|
||||
二、初次部署(从零跑起来)
|
||||
@@ -32,7 +32,7 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评
|
||||
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
|
||||
compileSdk 34 / minSdk 29(Android 10+)/ targetSdk 34
|
||||
设备 需要真机(Android 10 及以上、带摄像头)
|
||||
|
||||
真机与模拟器:
|
||||
@@ -58,7 +58,7 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评
|
||||
生成 keystore/opendance-release.jks 与 keystore.properties(含 storeFile、
|
||||
keyAlias 与两个口令字段)。请自行备份,且不要提交。
|
||||
取值优先级:keystore.properties -> 环境变量(KEYSTORE_PATH /
|
||||
KEYSTORE_PASSWORD / KEY_ALIAS / KEY_PASSWORD)-> 回退 debug 签名
|
||||
KEYSTORE_PASSWORD / KEY_ALIAS / KEY_PASSWORD)-> 回退 debug 签名
|
||||
(仅供 CI 产出已签名 APK,正式发布必须自备 keystore)。
|
||||
|
||||
3) 首次构建
|
||||
@@ -70,7 +70,7 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评
|
||||
bash build/build-apk.sh release # 仅 release
|
||||
bash build/build-apk.sh all # 两者都构建(含产物校验)
|
||||
|
||||
或直接用 Gradle:
|
||||
或直接用 Gradle:
|
||||
./gradlew assembleDebug
|
||||
./gradlew assembleRelease
|
||||
|
||||
@@ -93,15 +93,15 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评
|
||||
5) 配置与联通
|
||||
|
||||
本 App 只连接 Windows 客户端,不访问 Web 服务端。
|
||||
地址来自 AppSettings.webSocketUrl():ws://<host>:<port>/pose
|
||||
地址来自 AppSettings.webSocketUrl(): ws://<host>:<port>/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)
|
||||
目标帧率 targetFps 30(5~60)
|
||||
分辨率 resolution 640x480(可选 480x360 / 1280x720)
|
||||
镜像画面 mirror 开
|
||||
仅上半身模式 upperBodyOnly 关
|
||||
|
||||
@@ -115,7 +115,7 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评
|
||||
|
||||
6) 第一次跑通
|
||||
|
||||
手机与电脑连同一局域网 -> 电脑启动 Windows 客户端(监听 8765)->
|
||||
手机与电脑连同一局域网 -> 电脑启动 Windows 客户端(监听 8765)->
|
||||
App 填地址并测试连接 -> 点「开始推流」并授予相机权限 -> 站到画面中央。
|
||||
|
||||
成功标志:
|
||||
@@ -132,8 +132,8 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评
|
||||
报告:app/build/reports/tests/testDebugUnitTest/index.html
|
||||
|
||||
bash build/build-apk.sh all
|
||||
预期:debug 与 release 均显示 "OK ... APK",release 显示
|
||||
"已签名(APK Signature Scheme v2)"
|
||||
预期:debug 与 release 均显示 "OK ... APK", release 显示
|
||||
"已签名(APK Signature Scheme v2)"
|
||||
|
||||
$ANDROID_HOME/build-tools/34.0.0/apksigner verify app/build/outputs/apk/release/app-release.apk
|
||||
预期:退出码 0(无输出即通过)
|
||||
@@ -154,7 +154,7 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评
|
||||
|
||||
Q: 点「开始推流」闪退(或提示 MediaPipe 初始化失败)
|
||||
A: MediaPipe 原生库加载 / 静态初始化失败抛出的是 Error 子类
|
||||
(UnsatisfiedLinkError、ExceptionInInitializerError),不是 Exception;
|
||||
(UnsatisfiedLinkError、ExceptionInInitializerError),不是 Exception;
|
||||
常见诱因是 ABI 不匹配(例如把只含 arm 的 release 包装到 x86 模拟器)
|
||||
或模型资源缺失。处理:改用与设备 ABI 匹配的包(真机用 release、
|
||||
模拟器调试用 debug);确认 APK 内含 assets/pose_landmarker_lite.task。
|
||||
@@ -167,7 +167,7 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评
|
||||
--
|
||||
|
||||
1. 在 Windows 电脑上启动 OpenDance Studio 客户端
|
||||
(Windows 客户端会自动监听 8765 端口)
|
||||
(Windows 客户端会自动监听 8765 端口)
|
||||
|
||||
2. 确保手机与电脑连在同一个 WiFi
|
||||
|
||||
@@ -193,9 +193,9 @@ WebSocket 推送给 Windows 客户端,由后者驱动 VRM 角色并实时评
|
||||
--
|
||||
|
||||
分辨率
|
||||
低(480x360) 最省电,适合老机型
|
||||
中(640x480) 推荐,识别精度与功耗平衡
|
||||
高(1280x720) 最清晰,但推理更慢、更耗电
|
||||
低(480x360) 最省电,适合老机型
|
||||
中(640x480) 推荐,识别精度与功耗平衡
|
||||
高(1280x720) 最清晰,但推理更慢、更耗电
|
||||
|
||||
目标帧率
|
||||
默认 30 FPS。低端机若「识别帧率」明显低于目标值,可降到 20 或 15。
|
||||
@@ -302,8 +302,8 @@ Android Studio 路径:
|
||||
gradle/wrapper/ Gradle 8.9 wrapper
|
||||
project.yaml 任务清单与状态
|
||||
DSH.md 本项目 DSH 执行约束
|
||||
tasks/ 单任务说明书(T-AND-001 ~ 009)
|
||||
contracts/ 只读契约快照(来源 opendance-web v0.1.0)
|
||||
tasks/ 单任务说明书(T-AND-001 ~ 009)
|
||||
contracts/ 只读契约快照(来源 opendance-web v0.1.0)
|
||||
app/src/main/java/com/opendance/studio/
|
||||
MainActivity.kt 三页导航
|
||||
model/ 239 字节帧编解码 + 33 点拓扑
|
||||
|
||||
+3
-3
@@ -11,8 +11,8 @@
|
||||
- 版权:Copyright 2023 Google LLC
|
||||
- 许可证:Apache License 2.0
|
||||
- 源链接:https://github.com/google-ai-edge/mediapipe
|
||||
- 用途:**本应用的端侧姿态推理**(MediaPipe Tasks Vision Android,
|
||||
模型文件 `pose_landmarker_lite.task`)
|
||||
- 用途:**本应用的端侧姿态推理**(MediaPipe Tasks Vision Android,
|
||||
模型文件 `pose_landmarker_lite.task`)
|
||||
|
||||
## 2. Three.js
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
与 OkHttp,均为 Apache License 2.0;对应许可证文本随依赖包分发,
|
||||
可在各依赖的 Maven 构件中找到。
|
||||
3. 随包分发的模型文件 `pose_landmarker_lite.task` 来自 MediaPipe
|
||||
(Copyright 2023 Google LLC,Apache License 2.0)。
|
||||
(Copyright 2023 Google LLC, Apache License 2.0)。
|
||||
4. **未使用 SMPL 模型及其衍生拓扑**,统一采用 MediaPipe 33 点拓扑,
|
||||
以规避 SMPL 的非商用许可限制。
|
||||
5. **本应用只在设备本地进行推理,画面不离开手机**;通过网络发送的只有
|
||||
|
||||
@@ -5,7 +5,7 @@ Web 服务端是唯一权威定义,本文件是另两端消费的**稳定视
|
||||
|
||||
## 1. 通用约定
|
||||
|
||||
- 所有资源返回 `id: number`(BIGINT,超出 2^53 前不会发生)。
|
||||
- 所有资源返回 `id: number`(BIGINT,超出 2^53 前不会发生)。
|
||||
- 所有时间为 ISO 8601 UTC 字符串,例:`2026-09-14T10:00:00.000Z`。
|
||||
- 列表接口统一分页响应:
|
||||
|
||||
@@ -117,7 +117,7 @@ rejected 已驳回
|
||||
```
|
||||
0. 环境变量 DEFAULT_MODEL_ID 指定,且该模型存在且已发布
|
||||
1. 源码内写死的默认模型种子(web-server/src/config/default-model.ts 的
|
||||
DEFAULT_MODEL_SEED):按 **sourceSha256(上传压缩包的 sha256)** 匹配已发布模型,
|
||||
DEFAULT_MODEL_SEED):按 **sourceSha256(上传压缩包的 sha256)** 匹配已发布模型,
|
||||
换库/重建库后仍然有效;没有压缩包时退回按产物 sha256,最后按名称兜底。
|
||||
⚠ 不能用产物 sha256 当主键:实测 Assimp 的 GLB 导出不是字节级确定的,
|
||||
同一份 zip 每次转换得到的文件 sha256 都不同(BIN chunk 有十几字节浮动)
|
||||
@@ -145,7 +145,7 @@ rejected 已驳回
|
||||
| `comment` | string\|null | 审核意见 |
|
||||
| `createdAt` | string | |
|
||||
|
||||
## 6. `CatalogBundle`(客户端一次性拉取,`GET /api/catalog/export`)
|
||||
## 6. `CatalogBundle`(客户端一次性拉取,`GET /api/catalog/export`)
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
+11
-11
@@ -3,11 +3,11 @@
|
||||
## 环境
|
||||
|
||||
- 类型:MySQL 兼容实例(外部提供,含 MariaDB 10.5+),TypeORM
|
||||
使用 `mysql` driver(`mysql2`)连接,SQL 语法限制在 MySQL 5.7 / MariaDB 10.5+
|
||||
使用 `mysql` driver(`mysql2`)连接,SQL 语法限制在 MySQL 5.7 / MariaDB 10.5+
|
||||
共同子集内,不使用 MySQL 8 专有语法(窗口函数 CTE 递归除外)。
|
||||
- 数据库名:由部署方提供,通过 `DB_NAME` 注入(本文档示例统一用 `opendance`)
|
||||
- 数据库名:由部署方提供,通过 `DB_NAME` 注入(本文档示例统一用 `opendance`)
|
||||
- 字符集:`utf8mb4` / `utf8mb4_general_ci`(沿用现有实例默认排序规则)
|
||||
- 迁移工具:TypeORM migration,**不使用 `DB_SYNCHRONIZE`**
|
||||
- 迁移工具:TypeORM migration, **不使用 `DB_SYNCHRONIZE`**
|
||||
|
||||
## 连接信息(由部署方提供)
|
||||
|
||||
@@ -25,10 +25,10 @@ DB_LOGGING=false
|
||||
|
||||
## 约定
|
||||
|
||||
- 所有表名使用 `snake_case`,复数形式(`songs` / `motions` / `models`)
|
||||
- 所有表名使用 `snake_case`,复数形式(`songs` / `motions` / `models`)
|
||||
- 主键统一为 `BIGINT AUTO_INCREMENT`,字段名 `id`
|
||||
- 时间字段统一 `DATETIME`,`created_at` / `updated_at`,`updated_at` 由应用层维护
|
||||
- 软删除统一使用 `deleted_at DATETIME NULL`(`@DeleteDateColumn`)
|
||||
- 时间字段统一 `DATETIME`, `created_at` / `updated_at`, `updated_at` 由应用层维护
|
||||
- 软删除统一使用 `deleted_at DATETIME NULL`(`@DeleteDateColumn`)
|
||||
- 布尔字段使用 `TINYINT(1)`
|
||||
- 来源(出处)字段为 5 元组,任何资产入库必须齐备:
|
||||
`source_type` / `source_name` / `source_url` / `license` / `author`
|
||||
@@ -36,24 +36,24 @@ DB_LOGGING=false
|
||||
- 每个 migration 必须可逆(提供 `down` 方法)
|
||||
- 迁移文件名格式:`<timestamp>-<kebab-name>.ts`
|
||||
|
||||
## 增量迁移(v0.2.0 / v0.3.0)
|
||||
## 增量迁移(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)
|
||||
## 表清单(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 |
|
||||
|
||||
## 状态机(`status` 列,v0.1.0)
|
||||
## 状态机(`status` 列,v0.1.0)
|
||||
|
||||
```
|
||||
draft ──submit──▶ pending ──approve──▶ published
|
||||
|
||||
+29
-29
@@ -1,21 +1,21 @@
|
||||
# contracts/model-formats.md - 模型与动作格式契约 v0.4.0
|
||||
|
||||
## 1. 角色模型(models)
|
||||
## 1. 角色模型(models)
|
||||
|
||||
**系统以 GLB / VRM 为主力格式**(两个客户端都原生加载),其余格式一律在服务端转换。
|
||||
|
||||
| 格式 | 扩展名 | 支持级别 | 服务端行为 |
|
||||
|--|--|--|--|
|
||||
| glTF Binary | `.glb` | **主力(P0)** | **直接导入**,不重编码 |
|
||||
| VRM | `.vrm` | **主力(P0)** | **直接导入**;= GLB + VRM 扩展(humanoid / 表情 / springBone / meta) |
|
||||
| glTF | `.gltf` (+ `.bin`, 贴图) | P1 | 直接导入(分离文件在 zip 内可用;单文件上传会缺.bin) |
|
||||
| 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 |
|
||||
| MMD 模型 | `.pmx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM);单文件上传无贴图 |
|
||||
| FBX | `.fbx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM) |
|
||||
| FBX | `.fbx` | P2(输入) | 服务端转换(优先 GLB、其次 VRM) |
|
||||
|
||||
### 1.1 上传与转换策略
|
||||
|
||||
**`.glb` / `.vrm` / `.gltf` 直接导入**(不重编码,零损失);**`.pmx` / `.fbx` 由服务端转换**:
|
||||
**`.glb` / `.vrm` / `.gltf` 直接导入**(不重编码,零损失);**`.pmx` / `.fbx` 由服务端转换**:
|
||||
**优先 GLB,其次 VRM**。
|
||||
|
||||
#### 单文件上传
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
#### 压缩包上传(自动搜索)
|
||||
|
||||
不再"只找 `.pmx`",而是**按格式优先级自动搜索包内可用模型**:
|
||||
不再"只找 `.pmx`",而是**按格式优先级自动搜索包内可用模型**:
|
||||
|
||||
```
|
||||
.vrm ->.glb ->.gltf (直接导入,优先)
|
||||
@@ -38,7 +38,7 @@
|
||||
同一优先级内:`MODEL_CONVERT_PMX` 指定的文件名优先,其次目录最浅,最后文件最大。
|
||||
压缩包原件以 `model-archive` 资产留存并写入 `models.source_asset_id`。
|
||||
|
||||
#### 转换与校验(PMX / FBX)
|
||||
#### 转换与校验(PMX / FBX)
|
||||
|
||||
| 步骤 | 规则 |
|
||||
|--|--|
|
||||
@@ -46,7 +46,7 @@
|
||||
| 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 张贴图`) |
|
||||
| 说明 | 每次尝试都写进 `models.conversion_note`(例如 `Hatsune Miku.pmx -> GLB,已内嵌 2 张贴图`) |
|
||||
| 失败 | 包内没有可用模型、Assimp 不可用、两条路径都失败 -> **HTTP 422**,不产生模型记录 |
|
||||
|
||||
#### 入库后(自动)
|
||||
@@ -63,14 +63,14 @@
|
||||
| 项 | 规则 |
|
||||
|--|--|
|
||||
| 确定性 | **转换产物不是字节级确定的**:同一份 zip 连续转换 3 次得到 3 个不同 sha256(大小相同,BIN chunk 有十几字节浮动)。因此默认模型种子以**上传压缩包的 sha256** 为匹配键,产物 sha256 只作记录 |
|
||||
| 安全 | 解压前逐条校验路径,拒绝绝对路径与 `..`(zip-slip);拒绝加密条目;只支持 STORE / DEFLATE |
|
||||
| 安全 | 解压前逐条校验路径,拒绝绝对路径与 `..`(zip-slip);拒绝加密条目;只支持 STORE / DEFLATE |
|
||||
|
||||
**VRM 优先**的选取顺序见 `data-models.md` 4.1。
|
||||
|
||||
VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)或
|
||||
`extensions.VRMC_vrm`(1.0),写入 `models.vrm_version`。
|
||||
VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)或
|
||||
`extensions.VRMC_vrm`(1.0),写入 `models.vrm_version`。
|
||||
|
||||
## 2. 动作数据(motions)
|
||||
## 2. 动作数据(motions)
|
||||
|
||||
| 格式 | 扩展名 | 预处理支持 | 说明 |
|
||||
|--|--|--|--|
|
||||
@@ -82,7 +82,7 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
|
||||
|
||||
### 2.1 预处理产物(冻结)
|
||||
|
||||
- 格式:NDJSON(`application/x-ndjson`),每行一帧,UTF-8 无 BOM
|
||||
- 格式:NDJSON(`application/x-ndjson`),每行一帧,UTF-8 无 BOM
|
||||
- 一行结构:
|
||||
|
||||
```json
|
||||
@@ -93,9 +93,9 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
|
||||
- `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 轴向上、右手系、骨盆为原点**,
|
||||
- 坐标系换算:所有 3D 动作数据统一转到 **Y 轴向上、右手系、骨盆为原点**,
|
||||
再投影到 MediaPipe 归一化图像空间:
|
||||
`x_img = 0.5 + x_world * scale`,`y_img = 0.5 - y_world * scale`,`z_img = -z_world * scale`,
|
||||
`x_img = 0.5 + x_world * scale`, `y_img = 0.5 - y_world * scale`, `z_img = -z_world * scale`,
|
||||
`scale = 0.35 / max(|x|,|y|,|z|)`(按整段动作的最大范围自适应,保证不出画)
|
||||
- 缺失关节(BVH 中不存在对应骨骼):`confidence = 0.0`,坐标沿用最近可用父节点
|
||||
|
||||
@@ -122,18 +122,18 @@ VRM 版本识别:读取 GLB 的 JSON chunk,检查 `extensions.VRM`(0.x)
|
||||
未映射到的点按 `skeleton-topology.md` 5 的层级由父节点推算,
|
||||
并置 `confidence` 降低(`0.5`),表示估算而非实测。
|
||||
|
||||
### 2.3 VMD(MikuMikuDance)解析约定
|
||||
### 2.3 VMD(MikuMikuDance)解析约定
|
||||
|
||||
VMD 是二进制格式:文件头 `Vocaloid Motion Data 0002`(30 字节)+ 模型名(20 字节,
|
||||
Shift-JIS)+ 骨骼关键帧数组(每条 **111 字节**:15 字节骨骼名、uint32 帧号、
|
||||
VMD 是二进制格式:文件头 `Vocaloid Motion Data 0002`(30 字节)+ 模型名(20 字节,
|
||||
Shift-JIS)+ 骨骼关键帧数组(每条 **111 字节**: 15 字节骨骼名、uint32 帧号、
|
||||
float32x3 位移、float32x4 旋转四元数、64 字节贝塞尔插值参数)+ morph / 相机 /
|
||||
灯光 / 自阴影 / IK / 可见性段落。**VMD 不含骨骼长度**,只有旋转(少数骨骼带位移),
|
||||
因此服务端必须自带一套标准 MMD 骨架才能做 FK。
|
||||
|
||||
处理链路:`vmd-parser.ts`(二进制 + Shift-JIS + 贝塞尔插值)-> `mmd-skeleton.ts`
|
||||
(标准骨架)-> `mmd-pose.ts`(FK + 腿部 IK)-> `retarget.ts` 的
|
||||
(标准骨架)-> `mmd-pose.ts`(FK + 腿部 IK)-> `retarget.ts` 的
|
||||
`worldFramesToKeypointFrames()`(与 BVH 完全共用骨盆归零 / 自适应缩放 / 图像空间投影 /
|
||||
9 个关节角)。固定 **30 fps**(MMD 帧率恒定,上传时填写的 `fps` 被忽略)。
|
||||
9 个关节角)。固定 **30 fps**(MMD 帧率恒定,上传时填写的 `fps` 被忽略)。
|
||||
|
||||
| MediaPipe 点 | MMD 标准骨骼 | 说明 |
|
||||
|--|--|--|
|
||||
@@ -149,11 +149,11 @@ float32x3 位移、float32x4 旋转四元数、64 字节贝塞尔插值参数)
|
||||
骨骼名按 `mmd-skeleton.ts` 的别名表归一(含全角 `IK` / 半角 `IK` 两种写法);
|
||||
未纳入标准骨架的骨骼(裙摆、物理、表情等)被忽略但会计数并写入预处理日志。
|
||||
|
||||
**腿部驱动判定**:`足IK` 轨道存在非零位移 -> 走 IK(用两骨骼解析 IK 把踝拉到 IK
|
||||
**腿部驱动判定**: `足IK` 轨道存在非零位移 -> 走 IK(用两骨骼解析 IK 把踝拉到 IK
|
||||
目标点,膝盖朝角色正面弯,不可达时收敛为伸直);否则走 FK(部分动作数据把腿烘焙成
|
||||
FK 旋转)。两份示例文件分别命中两种路径。
|
||||
|
||||
**坐标系**:MMD 原生是左手系(正面为 -Z),本项目通路是右手系(正面为 +Z、+X 为角色
|
||||
**坐标系**: MMD 原生是左手系(正面为 -Z),本项目通路是右手系(正面为 +Z、+X 为角色
|
||||
左侧)。VMD 的四元数与位移统一做 Z 镜像(`(x,y,z,w) -> (-x,-y,z,w)`、位移 z 取反)后
|
||||
送入标准骨架。该选择由真实数据对照实验确定:腿部烘焙为 FK 的样本中,`zMirrored` 下
|
||||
膝盖朝前占比 94.1%,不转换时仅 5.9%(见 `test/mmd-vmd.spec.ts`)。
|
||||
@@ -167,15 +167,15 @@ FK 旋转)。两份示例文件分别命中两种路径。
|
||||
|
||||
| 项 | 值 |
|
||||
|--|--|
|
||||
| 单文件上限 | 512 MiB(`UPLOAD_MAX_BYTES` 可覆盖) |
|
||||
| 单文件上限 | 512 MiB(`UPLOAD_MAX_BYTES` 可覆盖) |
|
||||
| 允许扩展名 | 上表全部;其他一律 422 |
|
||||
| 存储路径 | `${STORAGE_DIR}/{kind}/{yyyy}/{mm}/{sha256前2位}/{sha256}.{ext}` |
|
||||
| 去重 | 相同 sha256 复用同一 `assets` 记录,不重复落盘 |
|
||||
| 文件名 | 服务端不使用原始文件名做路径,只存 `assets.original_name` |
|
||||
|
||||
## 4. 开源依赖与许可证(引用 `open-source-credits.md`)
|
||||
## 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 预览
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
| 33 | neck | `midpoint(left_shoulder(11), right_shoulder(12))` |
|
||||
| 34 | pelvis | `midpoint(left_hip(23), right_hip(24))` |
|
||||
|
||||
## 3. 父节点表(`parent[i]`,-1 表示世界根)
|
||||
## 3. 父节点表(`parent[i]`, -1 表示世界根)
|
||||
|
||||
对称树,便于骨骼重定向与关节角计算:
|
||||
|
||||
@@ -91,16 +91,16 @@
|
||||
|
||||
## 4. 坐标系
|
||||
|
||||
- **归一化图像坐标**:`x, y ∈ [0, 1]`,原点左上角,`y` 向下;`x` 相对图像宽,`y` 相对图像高。
|
||||
- **深度**:`z` 与 `x` 同尺度,原点位于髋中心,**负值朝向相机**。
|
||||
- **镜像**:Android 前置摄像头默认输出镜像画面;是否镜像由帧头 `flags.bit0` 声明,
|
||||
接收端据此决定是否翻转 `x`(`x' = 1 - x`)。
|
||||
- **置信度**:`visibility ∈ [0, 1]`,落盘/传输为 `uint8`(`round(v * 255)`)。
|
||||
- **归一化图像坐标**: `x, y ∈ [0, 1]`,原点左上角,`y` 向下;`x` 相对图像宽,`y` 相对图像高。
|
||||
- **深度**: `z` 与 `x` 同尺度,原点位于髋中心,**负值朝向相机**。
|
||||
- **镜像**: Android 前置摄像头默认输出镜像画面;是否镜像由帧头 `flags.bit0` 声明,
|
||||
接收端据此决定是否翻转 `x`(`x' = 1 - x`)。
|
||||
- **置信度**: `visibility ∈ [0, 1]`,落盘/传输为 `uint8`(`round(v * 255)`)。
|
||||
归一化比例统一使用 `32767`(见 `websocket-keypoints.md`)。
|
||||
|
||||
## 5. 派生特征(DTW 输入,Windows 端实现,服务端预处理产物同构)
|
||||
|
||||
服务端 T-WEB-006 预处理产出的关键点序列与 Android 端输出的帧**结构完全一致**,
|
||||
服务端 T-WEB-006 预处理产出的关键点序列与 Android 端输出的帧**结构完全一致**,
|
||||
因此 Windows 端 DTW 引擎只有一条输入通路。
|
||||
|
||||
关节角特征(弧度,余弦值便于插值):
|
||||
|
||||
@@ -10,17 +10,17 @@ ws://<windows-host>:8765/pose
|
||||
```
|
||||
|
||||
- Windows 客户端监听,Android App 主动连接。
|
||||
- 子协议:`opendance.pose.v1`(`Sec-WebSocket-Protocol`)
|
||||
- 子协议:`opendance.pose.v1`(`Sec-WebSocket-Protocol`)
|
||||
- 二进制帧传输关键点,文本帧传输控制消息(JSON)。
|
||||
- 默认 30 FPS;发送端允许丢帧,接收端必须按 `frame_id` 检测丢包并插值补齐。
|
||||
|
||||
## 2. 二进制帧:239 字节(冻结)
|
||||
|
||||
**小端序(little-endian)**。
|
||||
**小端序(little-endian)**。
|
||||
|
||||
```
|
||||
偏移 长度 字段 类型 说明
|
||||
0 2 magic u16 LE 固定 0x4F44(ASCII "OD")
|
||||
0 2 magic u16 LE 固定 0x4F44(ASCII "OD")
|
||||
2 1 version u8 固定 0x01
|
||||
3 1 flags u8 bit0 = 镜像画面(mirrored)
|
||||
bit1 = 本帧含低置信度点(<0.5)
|
||||
@@ -32,7 +32,7 @@ ws://<windows-host>:8765/pose
|
||||
合计 239
|
||||
```
|
||||
|
||||
### 单个关键点(7 字节,`i` 为 0..32)
|
||||
### 单个关键点(7 字节,`i` 为 0..32)
|
||||
|
||||
```
|
||||
偏移 长度 字段 类型 换算
|
||||
@@ -43,7 +43,7 @@ ws://<windows-host>:8765/pose
|
||||
```
|
||||
|
||||
- 点索引与坐标语义见 `skeleton-topology.md`。
|
||||
- 编码:`x = clamp(round(x_norm * 32767), -32768, 32767)`,`y`、`z` 同理。
|
||||
- 编码:`x = clamp(round(x_norm * 32767), -32768, 32767)`, `y`、`z` 同理。
|
||||
- 低置信度点(`conf < 128`)坐标无意义,接收端必须插值,不得直接使用。
|
||||
- `i16` 为有符号数:`0x8000` 表示 `-32768`。
|
||||
|
||||
@@ -68,7 +68,7 @@ func decode(buf: PackedByteArray) -> Dictionary:
|
||||
return {"flags": flags, "frame_id": frame_id, "points": pts}
|
||||
```
|
||||
|
||||
## 3. 文本控制帧(JSON,UTF-8)
|
||||
## 3. 文本控制帧(JSON, UTF-8)
|
||||
|
||||
### 3.1 `hello` - Android -> Windows,连接后立即发送
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# T-AND-001 Android Studio 项目骨架
|
||||
|
||||
- **状态**:done
|
||||
- **状态**: done
|
||||
- **依赖**:无
|
||||
|
||||
## 目标
|
||||
@@ -22,9 +22,9 @@ 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) |
|
||||
| `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 设置 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# T-AND-002 CameraX 采集
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-001
|
||||
- **状态**: done
|
||||
- **依赖**: T-AND-001
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -17,7 +17,7 @@ app/src/main/java/com/opendance/studio/data/AppSettings.kt
|
||||
|
||||
## 交付物
|
||||
|
||||
`camera/CameraController.kt`:Preview + ImageAnalysis 绑定、分辨率档位、前后摄切换。
|
||||
`camera/CameraController.kt`: Preview + ImageAnalysis 绑定、分辨率档位、前后摄切换。
|
||||
|
||||
## 关键实现
|
||||
|
||||
@@ -31,7 +31,7 @@ app/src/main/java/com/opendance/studio/data/AppSettings.kt
|
||||
|
||||
## 隐私
|
||||
|
||||
相机帧只在内存中流转到推理器,**不落盘、不上传**。离开运行页即 `unbindAll()`,
|
||||
相机帧只在内存中流转到推理器,**不落盘、不上传**。离开运行页即 `unbindAll()`,
|
||||
不在后台占用相机。
|
||||
|
||||
## 验收
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# T-AND-003 MediaPipe Pose 端侧推理
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-002
|
||||
- **状态**: done
|
||||
- **依赖**: T-AND-002
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -17,7 +17,7 @@ app/src/main/java/com/opendance/studio/pose/PoseDetector.kt
|
||||
|
||||
## 交付物
|
||||
|
||||
`pose/PoseDetector.kt` + `app/src/main/assets/pose_landmarker_lite.task`(5.7 MB)。
|
||||
`pose/PoseDetector.kt` + `app/src/main/assets/pose_landmarker_lite.task`(5.7 MB)。
|
||||
|
||||
## 关键实现
|
||||
|
||||
@@ -32,7 +32,7 @@ app/src/main/java/com/opendance/studio/pose/PoseDetector.kt
|
||||
|
||||
## 坐标与镜像语义
|
||||
|
||||
- 发送**未镜像的原始 MediaPipe 坐标**(契约 4)
|
||||
- 发送**未镜像的原始 MediaPipe 坐标**(契约 4)
|
||||
- 是否镜像由帧头 `flags.bit0` 声明,接收端据此决定如何还原
|
||||
- 叠加层绘制时才取 `1 - x`,与前置摄像头预览的镜像观感一致
|
||||
|
||||
@@ -40,7 +40,7 @@ app/src/main/java/com/opendance/studio/pose/PoseDetector.kt
|
||||
|
||||
## 上半身模式
|
||||
|
||||
`upperBodyOnly = true` 时,髋部及以下点置信度置 0 并置 `flags.bit2`,
|
||||
`upperBodyOnly = true` 时,髋部及以下点置信度置 0 并置 `flags.bit2`,
|
||||
接收端会插值。用于带宽/算力不足时降级。
|
||||
|
||||
## 验收
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# T-AND-004 33 点 -> 239 字节帧
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-003(数据结构)、T-AND-001
|
||||
- **状态**: done
|
||||
- **依赖**: T-AND-003(数据结构)、T-AND-001
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -25,7 +25,7 @@ app/src/main/java/com/opendance/studio/model/SkeletonTopology.kt
|
||||
2 1 version u8 = 1
|
||||
3 1 flags u8 bit0 镜像 | bit1 低置信度 | bit2 上半身
|
||||
4 4 frame_id u32 LE
|
||||
8 231 keypoints 33 x 7B:s16 x, s16 y, s16 z, u8 conf
|
||||
8 231 keypoints 33 x 7B:s16 x, s16 y, s16 z, u8 conf
|
||||
合计 239
|
||||
```
|
||||
|
||||
@@ -58,7 +58,7 @@ console.log(buf.toString('hex'));
|
||||
|
||||
- `ByteBuffer.order(LITTLE_ENDIAN)`,避免手动移位出错
|
||||
- 定点换算 `round(v x 32767)` 并 `coerceIn(-32768, 32767)`
|
||||
- 置信度 `round(v x 255)`,阈值 0.5 含端点(`>=`)
|
||||
- 置信度 `round(v x 255)`,阈值 0.5 含端点(`>=`)
|
||||
- 输入超范围时裁剪而非抛异常(相机抖动可能产生轻微越界值)
|
||||
- 并行 FloatArray 存储,30 FPS 循环内零对象分配
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# T-AND-005 WebSocket 推流 + 自动重连
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-004
|
||||
- **状态**: done
|
||||
- **依赖**: T-AND-004
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -17,7 +17,7 @@ app/src/main/java/com/opendance/studio/net/PoseWebSocketClient.kt
|
||||
|
||||
## 交付物
|
||||
|
||||
`net/PoseWebSocketClient.kt`:OkHttp WebSocket 封装 + 状态流 + 统计。
|
||||
`net/PoseWebSocketClient.kt`: OkHttp WebSocket 封装 + 状态流 + 统计。
|
||||
|
||||
## 关键实现
|
||||
|
||||
@@ -25,7 +25,7 @@ app/src/main/java/com/opendance/studio/net/PoseWebSocketClient.kt
|
||||
|--|--|--|
|
||||
| 子协议 | 请求头 `Sec-WebSocket-Protocol: opendance.pose.v1` | 契约 1 |
|
||||
| 二进制帧 | `socket.send(ByteString)` | 契约要求生产链路用二进制 |
|
||||
| 控制帧 | `send(text)`,JSON | hello / stats / bye |
|
||||
| 控制帧 | `send(text)`, JSON | hello / stats / bye |
|
||||
| 自动重连 | 指数退避 1s -> 2s -> 4s... 上限 15s,连接成功即重置 | 避免抖动时打爆对端 |
|
||||
| 非连接态发送 | 直接丢弃并计入 `droppedFrames` | **绝不阻塞相机线程** |
|
||||
| 心跳 | OkHttp `pingInterval(15s)` | 及时发现半开连接 |
|
||||
@@ -37,7 +37,7 @@ app/src/main/java/com/opendance/studio/net/PoseWebSocketClient.kt
|
||||
- **hello**(连接后立即发):protocol / device / appVersion / cameraFacing /
|
||||
targetFps / resolution
|
||||
- **welcome**(收):记录服务端 sessionId
|
||||
- **config**(收):服务端可下发 mirror / targetFps / upperBodyOnly / resolution,
|
||||
- **config**(收):服务端可下发 mirror / targetFps / upperBodyOnly / resolution,
|
||||
通过 `onRemoteConfig` 回调
|
||||
- **stats**(每秒发):fps / dropped / latencyMs / confidenceAvg
|
||||
- **bye**(主动断开时发)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# T-AND-006 连接页 + 主机发现
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-005
|
||||
- **状态**: done
|
||||
- **依赖**: T-AND-005
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -27,7 +27,7 @@ HTTP 端点发现。因此:
|
||||
|
||||
1. 取手机在 WiFi 网的 IPv4 地址(排除 `rmnet*` 蜂窝网卡与 `169.254.*` 链路本地地址)
|
||||
2. 推导 `/24` 网段
|
||||
3. 并发 TCP 探测 `<局域网网段>.1-254:8765`(并发 32、单机超时 300ms)
|
||||
3. 并发 TCP 探测 `<局域网网段>.1-254:8765`(并发 32、单机超时 300ms)
|
||||
4. 以 `Flow` 形式边扫边产出,按延迟升序展示--最快响应的最可能是目标机
|
||||
|
||||
整段扫描约 2~3 秒。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# T-AND-007 运行页 + 状态指示
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-003 / 005 / 006
|
||||
- **状态**: done
|
||||
- **依赖**: T-AND-003 / 005 / 006
|
||||
|
||||
## 目标
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# T-AND-008 关于页 + 开源声明
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-006
|
||||
- **状态**: done
|
||||
- **依赖**: T-AND-006
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -18,7 +18,7 @@ app/src/main/java/com/opendance/studio/ui/Credits.kt
|
||||
|
||||
## 交付物
|
||||
|
||||
- `ui/Credits.kt`:15 项声明数据(与契约表逐条对应)
|
||||
- `ui/Credits.kt`: 15 项声明数据(与契约表逐条对应)
|
||||
- `ui/AboutScreen.kt`:版本卡片 + 隐私说明 + 可滚动声明列表
|
||||
|
||||
## 内容要点
|
||||
@@ -30,7 +30,7 @@ app/src/main/java/com/opendance/studio/ui/Credits.kt
|
||||
|
||||
## 验收
|
||||
|
||||
`CreditsTest`(JVM 单元测试)断言:
|
||||
`CreditsTest`(JVM 单元测试)断言:
|
||||
条目数 >= 10、文档要求的 10 项全部在列、每条有许可证与 https 链接、
|
||||
已知项目的许可证正确、本端自身依赖(MediaPipe/OkHttp/CameraX/Compose)均在列、
|
||||
项目名不重复、契约版本为 0.1.0。
|
||||
|
||||
+10
-10
@@ -1,7 +1,7 @@
|
||||
# T-AND-009 APK 打包
|
||||
|
||||
- **状态**:done
|
||||
- **依赖**:T-AND-001 ~ 008
|
||||
- **状态**: done
|
||||
- **依赖**: T-AND-001 ~ 008
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -21,7 +21,7 @@ app/build.gradle.kts
|
||||
|
||||
| 文件 | 说明 |
|
||||
|--|--|
|
||||
| `build/gen-keystore.sh` | 生成发布签名密钥库(**不入库**) |
|
||||
| `build/gen-keystore.sh` | 生成发布签名密钥库(**不入库**) |
|
||||
| `build/build-apk.sh` | 构建 + 校验产物(魔数、签名方案) |
|
||||
| `build/verify.sh` | 单元测试 -> 构建 -> 产物校验 一条龙 |
|
||||
|
||||
@@ -30,7 +30,7 @@ app/build.gradle.kts
|
||||
`app/build.gradle.kts` 按优先级读取签名信息:
|
||||
|
||||
```
|
||||
1. keystore.properties(build/gen-keystore.sh 生成,已 gitignore)
|
||||
1. keystore.properties(build/gen-keystore.sh 生成,已 gitignore)
|
||||
2. 环境变量 KEYSTORE_PATH / KEYSTORE_PASSWORD / KEY_ALIAS / KEY_PASSWORD
|
||||
3. 两者都缺失 -> 回退 debug 签名,保证 CI 上 assembleRelease 也能产出已签名 APK
|
||||
```
|
||||
@@ -46,7 +46,7 @@ app/build.gradle.kts
|
||||
4. 生成 app-release.apk
|
||||
```
|
||||
|
||||
## 验收(文档 6.3 T-AND-009)
|
||||
## 验收(文档 6.3 T-AND-009)
|
||||
|
||||
| 验收项 | 结果 |
|
||||
|--|--|
|
||||
@@ -58,17 +58,17 @@ app/build.gradle.kts
|
||||
## 实测产物
|
||||
|
||||
```
|
||||
app-release.apk 55.5 MiB apksigner: Verifies(v2 方案)
|
||||
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
|
||||
minSdk 29(Android 10) targetSdk 34
|
||||
ABI: arm64-v8a, armeabi-v7a, x86, x86_64
|
||||
assets/pose_landmarker_lite.task 5.7 MB(Stored 未压缩)
|
||||
assets/pose_landmarker_lite.task 5.7 MB(Stored 未压缩)
|
||||
```
|
||||
|
||||
## 环境记录
|
||||
|
||||
- JDK 17(本地安装路径,示例 `<JDK 17 路径>`)
|
||||
- Android SDK:`platforms;android-34`、`build-tools;34.0.0`
|
||||
- JDK 17(本地安装路径,示例 `<JDK 17 路径>`)
|
||||
- Android SDK: `platforms;android-34`、`build-tools;34.0.0`
|
||||
- 构建期修复的问题:`proguard-rules.pro` 缺失导致 R8 配置告警(已补全 keep 规则)、
|
||||
androidTest 的 `androidx.test.runner` 依赖补齐
|
||||
|
||||
Reference in New Issue
Block a user