@kubohiroya/turbowarp-realtime-motion-capture 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +373 -0
  2. package/README.ja.md +89 -0
  3. package/README.md +113 -0
  4. package/THIRD_PARTY_NOTICES.md +31 -0
  5. package/config/feature-flags.ts +28 -0
  6. package/config/qr-config.ts +19 -0
  7. package/dist/extension-manifest.json +664 -0
  8. package/dist/turbowarp-realtime-motion-capture.js +82631 -0
  9. package/docs/architecture.ja.md +256 -0
  10. package/docs/architecture.md +294 -0
  11. package/docs/index.html +83 -0
  12. package/docs/turbowarp-extension-api.ja.md +157 -0
  13. package/docs/turbowarp-extension-api.md +1033 -0
  14. package/package.json +105 -0
  15. package/schemas/camera-calibration-v1.json +209 -0
  16. package/schemas/extension-manifest.schema.json +49 -0
  17. package/schemas/performance-dsl-v1.json +72 -0
  18. package/schemas/pose-frame-2d-v1.json +626 -0
  19. package/schemas/pose-frame-2d-v2.json +723 -0
  20. package/schemas/pose-frame-3d-v1.json +714 -0
  21. package/schemas/session-policy-v1.json +138 -0
  22. package/src/avatar/aframe-port.ts +39 -0
  23. package/src/avatar/controller.ts +541 -0
  24. package/src/avatar/kalidokit-adapter.ts +193 -0
  25. package/src/avatar/types.ts +119 -0
  26. package/src/block-definitions.json +810 -0
  27. package/src/calibration/controller.ts +564 -0
  28. package/src/calibration/opencv-backend.ts +347 -0
  29. package/src/calibration/types.ts +53 -0
  30. package/src/config.ts +13 -0
  31. package/src/extension-manifest.ts +182 -0
  32. package/src/extension.ts +913 -0
  33. package/src/frame-sync/controller.ts +416 -0
  34. package/src/frame-sync/detector.ts +278 -0
  35. package/src/frame-sync/pattern-display.ts +161 -0
  36. package/src/frame-sync/pattern.ts +88 -0
  37. package/src/frame-sync/time-source.ts +36 -0
  38. package/src/frame-sync/types.ts +39 -0
  39. package/src/frame-sync/video-frame-pump.ts +116 -0
  40. package/src/fusion/controller.ts +554 -0
  41. package/src/fusion/fuse.ts +385 -0
  42. package/src/fusion/geometry.ts +474 -0
  43. package/src/fusion/glow-stick.ts +173 -0
  44. package/src/fusion/identity.ts +112 -0
  45. package/src/fusion/jitter-buffer.ts +439 -0
  46. package/src/fusion/types.ts +103 -0
  47. package/src/globals.d.ts +57 -0
  48. package/src/index.ts +8 -0
  49. package/src/markers/canvas-sampler.ts +59 -0
  50. package/src/markers/color.ts +53 -0
  51. package/src/markers/sampler.ts +185 -0
  52. package/src/markers/types.ts +49 -0
  53. package/src/pose/controller.ts +388 -0
  54. package/src/pose/pose-frame.ts +128 -0
  55. package/src/pose/tfjs-movenet.ts +34 -0
  56. package/src/pose/types.ts +116 -0
  57. package/src/protocol/codec.ts +248 -0
  58. package/src/protocol/schemas.ts +283 -0
  59. package/src/qr-courier.ts +289 -0
  60. package/src/qr-svg.ts +39 -0
  61. package/src/sprite-skin.ts +112 -0
  62. package/src/webrtc-capability.ts +38 -0
@@ -0,0 +1,256 @@
1
+ # アーキテクチャ
2
+
3
+ [利用ガイド](../README.ja.md) | [TurboWarp機能拡張API](turbowarp-extension-api.ja.md) |
4
+ [English](architecture.md)
5
+
6
+ ## ビルド出力
7
+
8
+ このプロジェクトは実行時の動作と互換性メタデータを分離し、リポジトリに保存された同じソース定義から両方を生成します。
9
+
10
+ ```text
11
+ src/index.ts + src/extension.ts
12
+ -> vite-plugin-turbowarp-extension
13
+ -> dist/<extension>.js
14
+
15
+ src/config.ts + src/block-definitions.json
16
+ -> extension-api-manifest Viteプラグイン
17
+ -> dist/extension-manifest.json
18
+ ```
19
+
20
+ manifestプラグインはViteのビルド後フェーズで実行されます。これにより、JavaScriptプラグインの単一出力検証を維持しながら、TurboWarpバンドルの完成後にだけmanifestを追加します。
21
+
22
+ ## 拡張機能API manifest v1
23
+
24
+ `schemas/extension-manifest.schema.json`が規範となるJSON Schemaです。`formatVersion`は`1`で、互換性のないmanifest形式を導入するときに変更する必要があります。
25
+
26
+ v1契約は次の情報を含みます。
27
+
28
+ - TurboWarp拡張機能のID
29
+ - 各ブロックのopcodeとブロック種類
30
+ - 各引数のID、引数種類、任意のメニュー参照
31
+ - 各メニューのIDとReporterブロックを受け付けるかどうか
32
+
33
+ ブロック、引数、メニューは、シリアライズ前に識別子で並べ替えられます。テキスト、説明、既定値、静的メニュー項目は、保存済みプロジェクトのAPI参照を識別しないため、意図的に除外しています。そのため互換性チェッカーは、API変更とドキュメントまたはローカライズの変更を区別できます。
34
+
35
+ manifestとTurboWarp blockがpackageの公開APIです。source levelのcontroller/portは内部の実装境界で、
36
+ npm exportではありません。読み込み順、lifecycle値、error、data contractは
37
+ [機能拡張APIリファレンス](turbowarp-extension-api.ja.md)を参照してください。
38
+
39
+ ## 差分の検出
40
+
41
+ `dist/`はリリース成果物としてコミットされます。`npm run check:dist`は両方のファイルを再ビルドし、`dist/`配下に変更、削除、未追跡ファイルがある場合に失敗します。これにより、ローカル検証とCIの両方でmanifestとバンドルの差分を検出できます。
42
+
43
+ ## offer QRの縦切り
44
+
45
+ `config/feature-flags.ts`の`qrCourierPairing`は起動時固定・既定OFFです。有効時は
46
+ `kubohiroyaWebRtcCapability` version 2を要求し、`createOffer(peer)`の完了後に
47
+ `getOffer(peer)`でpairing codeを取得します。別機能拡張のprivate instanceには依存しません。
48
+
49
+ printable ASCIIのpairing codeを変更せず、version付き`twmp-qr/1` partへ格納します。
50
+ session ID、peer/message識別子、0始まりの順序、原文長、SHA-256 digestを全partに含めます。
51
+ byte modeで保守的に容量計算し、QR Version 40を上限とします。入力は128 KiB、64 part、
52
+ chunkごとに4096文字までです。
53
+
54
+ 表示はrenderer上だけの一時SVG skinです。現在のdrawable skinを保持し、VM costumeを追加せず
55
+ QR skinへ切り替えます。明示終了、project停止、extension dispose、表示target削除で元skinを
56
+ 復元し、一時resourceとQR dataを破棄します。
57
+
58
+ ## 固定version application contract codec
59
+
60
+ `protocolV1Codec`は独立した起動時固定・既定OFF flagです。最大1 MiBのJSONをparseし、rootの
61
+ `schema`と`version`から明示対応した5種類のv1 TypeBox schemaだけへdispatchします。
62
+ version推測やfallbackは行いません。decode成功時はcompact JSONを1件保持し、失敗時は保持値を
63
+ 消去して、最初の診断をJSON Pointer pathとmessageとして公開します。
64
+
65
+ 契約は本packageが所有します。正本は`src/protocol/schemas.ts`、`pnpm run schemas`が`schemas/`配下の
66
+ 配布用JSON Schemaを生成し、repository checkは、生成物が定義からdriftした場合、file名が宣言した
67
+ `version` literalや`$id`と食い違う場合、dispatch対象のversionに対応するfileが無い場合に失敗します。
68
+ applicationは本packageと配布された`schemas/`を通して契約を利用します。本packageがapplication
69
+ repositoryから契約定義を読むことはなく、依存方向はapplication → extensionの一方向に保たれます。
70
+
71
+ 契約はversionを切って追加し、公開済みversionを書き換えません。`protocolSchemas`はschema識別子と
72
+ versionの2段でdispatchするため、`twrmc/pose-frame-2d`はv1とv2を受理し、v1利用者はv2 payloadを
73
+ 拒否し続けます。PoseFrame2D v2は人物ごとに最大4件のサイリウムmarkerを追加します。各markerは、
74
+ 一意な色の発光体を観測したCOCO-17 keypoint、`#RRGGBB`の色、patch内で色が占めた割合を持ちます。
75
+ 色はkeypointと同一の映像frame・同一のcapture timestampの観測なので、別messageではなくpose frame
76
+ 内で運び、受信側での時刻対応付けを不要にします。
77
+
78
+ TypeBoxのtuple/array制約でCOCO-17順序、6人上限、matrix size、各数値境界を保証します。
79
+ 別の再帰key guardにより、WebRTC offer/answer、SDP、ICE/DTLS material、credential fieldを
80
+ 永続化前に拒否します。SessionPolicyはapplication validationとして`expiresAt > issuedAt`と
81
+ 未期限切れも検証します。
82
+
83
+ PoseFrame2Dの`captureTimestampUs`とPoseFrame3Dの`timestampUs`は、別実装の同期済みlocal
84
+ time serviceから受け取る不透明値です。この機能拡張はclock同期、offset推定、probe、ping、
85
+ pongを実装せず、受け取ったtimestampを変更せずprotocolへ格納します。
86
+
87
+ ## camera calibration workflow
88
+
89
+ `cameraCalibrationV1`は起動時固定・既定OFFです。camera/calibration ID、inner cornerが縦横
90
+ 3〜20のchessboard、meter単位のsquare sizeを検証してから、named Camera Source leaseを取得
91
+ します。最初の実frame解像度をsession中は固定し、途中変更は検出前に拒否します。
92
+
93
+ 明示的なsample要求ごとに一度だけvideoから一時canvasへcopyします。exact pinしたOpenCV.js
94
+ 4.12 WebAssembly backendはbundle内に含め、最初のsampleまたはsolveで遅延初期化します。
95
+ 完全なchessboardを検出してsubpixel精度へ補正し、board coverageと
96
+ Laplacian sharpnessから品質を評価します。quality 0.2未満、および保持viewのいずれかと正規化
97
+ RMS corner変位0.015未満のviewは拒否します。8〜40の多様なsampleを保持し、継続的なCPU
98
+ sampling loopや別の参照solverは実装しません。
99
+
100
+ `calibrateCamera`がintrinsic matrix、distortion vector、RMS reprojection errorを求めます。
101
+ 最後のsampleを舞台world board配置として予約し、そのrotation/translationを反転してrow-major
102
+ 4×4 `worldFromCameraMatrix`を作ります。設定RMS上限またはCameraCalibration v1境界を超える結果は
103
+ 最後の有効profileを置き換えません。
104
+
105
+ cancel、project reload、extension disposeではleaseと一時sampleを解放し、最後の検証済みprofile
106
+ はmemoryに維持します。明示cleanupだけがprofileも消去します。importもexact v1 schemaで検証し、
107
+ pairing-secret keyを再帰的に拒否します。offline会場LANで唯一のproduction backendを使えるよう
108
+ 非圧縮bundle約11 MB増を受け入れ、実camera/board幾何精度とWebAssembly起動はbrowser E2Eで
109
+ 検証します。
110
+
111
+ ## WebGPU MoveNet MultiPoseの縦切り
112
+
113
+ `webgpuMoveNetMultiPose`は独立した起動時固定・既定OFF flagです。TensorFlow.jsではWebGPU
114
+ backendだけをimportし、`setBackend("webgpu")`の結果を検証してから、trackingとbounding-box
115
+ trackerを有効にした`MULTIPOSE_LIGHTNING`を生成します。backendがWebGPU以外ならfail closedし、
116
+ CPU/WASM/WebGL推論fallbackは行いません。
117
+
118
+ controllerはCamera Sourceから`{cameraId: "pose"}`のleaseを取得し、media captureを所有しません。
119
+ 同時に呼ばれた推論blockは1つのPromiseを共有するため、detector実行は重ならず、古いframe要求を
120
+ 蓄積しません。成功時は最新video frameから最大6人を推定し、model tracking IDと17個すべての
121
+ 名前付きCOCO keypointを必須として、`twrmc/pose-frame-2d` version 1へserializeします。
122
+
123
+ 停止時は実行中の初期化/推論を待ち、detectorをdisposeし、camera leaseと最新frameを解放します。
124
+ TensorFlow.js backendはprocess全体で共有されるためresetせず、本機能が所有するmodel resourceは
125
+ detectorのdisposeで解放します。
126
+
127
+ ## PoseFrame3D avatar retarget
128
+
129
+ `avatarRetargetV1`は独立した起動時固定・既定OFF flagです。runtime key
130
+ `turbowarpAFrameCapability`へ`requireVersion(1)`を呼び、TurboWarp-A-Frame capabilityの公開同期
131
+ scene操作7種だけを利用します。A-Frame DOM、Three.js `object3D`、GLTF内部boneへはアクセス
132
+ しません。capability v1は`@kubohiroya/turbowarp-aframe@0.3.0`で公開済みです。
133
+
134
+ asset登録では宣言的template JSONをA-Frameへ送り、検証済みrig mappingを保持します。各boneは
135
+ 対応するKalidokit pose rig出力、`{avatar}`を含むselector、任意Euler offset degreeで定義します。
136
+ 適用時は対応するexact-v1 PoseFrame3DとPoseFrame2Dの両方を要求します。PoseFrame3Dの`personId`と
137
+ PoseFrame2Dの`trackingId`が一致するpersonだけを結合しますが、これは時刻alignmentではありません。
138
+
139
+ adapterは両方のCOCO-17 recordを、exact pinした`kalidokit@1.1.5`が要求する33 positionへ
140
+ 決定論的に変換します。screen座標にはPoseFrame2Dの`frameWidth`/`frameHeight`を使い、world座標は
141
+ 外部serviceの値を維持します。不足するBlazePose face/hand/foot pointは低visibilityで中点補間
142
+ または複製します。`runtime: "tfjs"`、`enableLegs: true`のKalidokit `Pose.solve`だけをrotation
143
+ solverとし、radian出力をA-Frame degreeへ変換します。hips結果にroot scale/offsetを適用し、
144
+ 自前rotation fallbackは持ちません。joint/personがbinding threshold未満なら該当transformだけを
145
+ skipし、直前値を維持します。
146
+
147
+ 最大6 person IDを一意なtemplate instanceへbindします。recognition遷移は設定可能なA-Frame
148
+ eventで通知し、application側がPerformance DSLのstart/end effectへ接続できます。1人の
149
+ capability失敗は`partial`診断へ集約し、他avatarを継続します。rebind、明示reset、project
150
+ lifecycle reset、disposeでは可能ならend eventを送り、生成instanceと一時状態をcleanupします。
151
+
152
+ PoseFrame3Dは別実装の3D serviceから届くexact v1境界dataです。`timestampUs`は不透明値として
153
+ recognition event dataへcopyするだけです。frame alignment、履歴保持/query、triangulation、
154
+ 3D solveは行いません。Kalidokitは上流でdeprecatedでありnative BlazePose landmarkを想定するため、
155
+ このCOCO-17拡張は明示的な精度制約です。release前に対象GLTF rigを実browserで検証します。
156
+
157
+ ## フレーム同期パターンの縦切り
158
+
159
+ `frameSyncPatternV1`は独立した起動時固定・既定OFF flagです。fusion application向けに、
160
+ 「ある出来事の後、各カメラPCがそれを写したフレームを記録し終えるまで何ms遅れるか」だけを答えます。
161
+
162
+ 表示側は画面全体のoverlayに、黒地の4×4パネルを描きます。12セルが4096msで一周するミリ秒
163
+ カウンタ、4セルがそのカウンタから導くcheck bitです。露光が画面のリフレッシュをまたぐと2つの
164
+ codeが混ざりますが、check bitがその読み取りを拒否するので、誤った時刻は通りません。1つの
165
+ animation frameで描いた内容は次のリフレッシュで画面に出るため、符号化する時刻は現在の時計に
166
+ 実測したリフレッシュ間隔を1つ足した値です。残るプロジェクタ遅延は全カメラ共通なので、
167
+ カメラ間のoffsetでは相殺されます。
168
+
169
+ カメラ側は名前付きのCamera Source leaseを取得し、`getUserMedia`は呼びません。届いたframeは
170
+ 240×180の輝度bufferへ縮小します。bufferはdecoderが同期的に読み終えるため再利用します。
171
+ キャリブレーションは実際のパターンに対して2段階で走ります。前半60%で画素ごとの輝度min/maxを
172
+ 記録し、高レンジ画素の最大連結領域のうちパネル形状のものをbounding boxとして採用します。
173
+ 後半でセルごとの明暗レベルを学習し、復号成功率を測ります。投影は不均一なのでレベルはセル単位で
174
+ 持ち、学習済みレベルの中間に落ちた読み取りは捨てます。信用できないlatencyを返す代わりに、
175
+ `panel-not-found`、`low-contrast`、`decode-unstable`で失敗します。受け付ける窓は6.2秒からです。
176
+ 最も遅いセルの変化周期2048msと、レベル学習に割り当てる窓の割合から導いた下限で、全セルを
177
+ 両方のレベルで観測できない窓は、後からlow contrastとして失敗する代わりに最初に拒否します。
178
+ どちらの段階も共有時計で終了するため、カメラが止まってもcontrollerが待ち続けることはありません。
179
+ また、キャリブレーション中に停止した場合は、camera faultとして記録せずに終了します。
180
+
181
+ 復号できたframeはobservationとしてqueueに入ります。timestampは外部の同期時刻サービスから
182
+ 読み取った不透明な値で、frameがアプリケーションへ届いた時点で取得します。センサの露光時刻が
183
+ 必要な呼び出し側のために、browserが報告するframe ageは別に公開します。clock probe、latency
184
+ サンプル、カメラ別の集計レポートはWebRTC機能拡張側の責務なので、clock・offset・ping・pongの
185
+ ロジックはここには置きません。
186
+ ## 多視点3D pose fusion
187
+
188
+ `poseFusion3D`は独立した起動時固定・既定OFF flagです。bufferへ入力するPoseFrame2D JSONは
189
+ fusion appがWebRTC data channelで受信したものであり、本機能拡張はtransportもclockも所有しません。
190
+
191
+ cameraごとにtimestamp順のring bufferを持ちます。slot数は設定delayとjitter windowから算出し、
192
+ 16〜600 frameに制限します。buffer対象cameraは最大16台です。jitter window内で順序が入れ替わった
193
+ frameは、ringの短い側をずらしてtimestamp位置へ挿入するため、通常の順序どおりの追加はO(1)の
194
+ ままです。timestampの重複、最新frameからjitter windowより古い到着、満杯ringの最古frameより
195
+ 古い到着はdropとして計上し、bufferしません。ここではcameraごとのclock offsetを推定しません。
196
+ capture timestampは別実装の同期済みlocal time serviceが与える不透明値のまま扱います。
197
+
198
+ frameをbufferするのは、その`cameraId`のcalibration profileが読み込み済みで、かつそのprofileが
199
+ frameを説明できる場合だけです。`calibrationId`や解像度が一致しないframeは、誤ったintrinsicで
200
+ そのまま三角測量されてしまうため拒否します。これらは重複や遅延到着と同様にdropとして計上し、
201
+ throwしません。ingestはdata channelのhot pathであり、設定を誤ったpeer 1台で実行中のscriptを
202
+ 止めるべきではないからです。calibration済みcameraしかbufferしないので、未知のcamera IDが
203
+ ring bufferを占有することもありません。
204
+
205
+ `fuse PoseFrame3D at buffered delay`は、最新のbuffered timestampから設定delayを引いた1つの過去の
206
+ 瞬間を解決し、全cameraをその瞬間で再sampleします。前後のframeで挟めたkeypointは線形補間し、
207
+ 片側がocclusionのkeypointは低信頼値を混ぜず見えている側の観測を採用します。挟めないcamera、
208
+ またはjitter windowの2倍より広い間隔しかないcameraは、最大1 jitter windowだけ直近frameを保持し、
209
+ それを超える場合は寄与しません。
210
+
211
+ camera間の対応付けは、異なるcameraの追跡人物のすべての組について、共有する確信のあるkeypoint
212
+ での2視点reprojection誤差の平均をcostとし、共有keypointは4点以上・最大12点で打ち切ります。costの
213
+ 小さい組から貪欲にmergeし、同一cameraの2視点が1人になるmergeは拒否します。2視点の三角測量は
214
+ 2本の視線の最短距離の中点を閉形式で求め、この二乗オーダーの段を反復解法から外します。3視点
215
+ 以上はscore重み付き線形解法(Jacobiは相対収束判定)を使います。16 camera×6人の上限で1回の統合
216
+ は約80 ms、4 camera×2人では約1 msです。
217
+
218
+ 2台以上のcameraが覆うclusterは、keypointごとにcheirality判定とreprojection判定付きで三角測量
219
+ します。全視点が一致しない場合は、2視点ごとの仮解に対してreprojection閾値内に収まる視点数を
220
+ 数え、最大の一致集合で三角測量し直します。これにより少数の誤検出はkeypointを引きずらずに
221
+ 捨てられます(視点が2つの解に均等に割れる場合は原理的に区別できません)。pixel観測はprofileの
222
+ OpenCV rational modelで歪み補正するため、係数0/4/5/8個に対応し、それ以外はprofile読み込み時
223
+ に拒否します。
224
+
225
+ registryはcameraとtracking IDの重なりから`person-N`のidentityを維持し、keypointごとに最後に
226
+ 三角測量できた位置を保持します。確信のある視点が2つ未満のkeypointはその位置を保持してscore `0`
227
+ を返し、実測値と保持値を利用側が区別できるようにします。組み立てたframeは保持する前に、
228
+ pinnedのPoseFrame3D v1 schemaで検証します。
229
+
230
+ bufferが空、覆うcameraが2台未満、多視点で見えた人物がいない場合は想定内の一時状態として
231
+ `false`を返し、直前の統合結果を保持したままerror codeを公開します。不正なJSON、他contractの
232
+ schema、不正なcalibration profileはerrorになります。停止ボタン、project reload、extension dispose
233
+ ではbufferと統合結果を解放し、明示cleanupでは読み込み済みcalibration profileも解放します。
234
+ `PROJECT_RUN_STOP`では解放しません。runtimeはthread queueが空になるたびにこのeventを出すため、
235
+ hat scriptでframeをbufferするevent駆動のprojectがmessageの合間にjitter bufferを失ってしまいます。
236
+ camera leaseと一時skinはこのeventでも解放します。
237
+
238
+ ## サイリウムによる識別と向きの補正
239
+
240
+ `glowStickMarkers`は独立した起動時固定・既定OFF flagです。camera側では推論のたびに、追跡中の各
241
+ 人物について設定したkeypointごとに1つのpatchを、keypointと同一の映像frameから、直後に破棄する
242
+ 一時canvas経由でsampleします。patchはhueで評価します。彩度または明度が閾値未満のpixelは無視し、
243
+ 残りのhueを円環平均するため、0度をまたぐ赤は赤のまま扱えます。色が占める割合が閾値を超えた場合
244
+ だけmarkerとして採用し、keypointごとに最も強い観測をPoseFrame2D v2のmarkerにします。sampleを
245
+ 止めている間、frameはv1のままです。
246
+
247
+ fusion側では、performance DSLが演者の色を与え、演者がサイリウムを持つkeypointはfusion側の設定と
248
+ します(その契約は持ち手を記述しないため)。cameraのsampleごとに、観測の強い順で貪欲に割り当て、
249
+ 1人の演者が同じcameraで2人を占めることはありません。
250
+
251
+ 割り当ては2つの効果を持ちます。1つは識別です。統合された人物は演者IDを`personId`とするため、
252
+ occlusion、再入場、tracking ID変化をまたいで同一性が保たれ、対応付けでは別演者の視点の統合を拒否し、
253
+ 同一演者の視点はreprojection costより先に統合します。もう1つは向きの補正です。指定keypointの
254
+ 左右反転側で色が見つかった場合、そのcameraは背面を腹面として読んでいるため、三角測量の前に
255
+ その視点の左右keypointを入れ替えます。補正しなければ、その視点は手足を体の反対側へ引きずるか、
256
+ reprojection判定で落ちてしまいます。
@@ -0,0 +1,294 @@
1
+ # Architecture
2
+
3
+ [User guide](../README.md) | [TurboWarp extension API](turbowarp-extension-api.md) |
4
+ [日本語](architecture.ja.md)
5
+
6
+ ## Build outputs
7
+
8
+ The project keeps runtime behavior and compatibility metadata separate while generating both from
9
+ the same checked-in source definitions.
10
+
11
+ ```text
12
+ src/index.ts + src/extension.ts
13
+ -> vite-plugin-turbowarp-extension
14
+ -> dist/<extension>.js
15
+
16
+ src/config.ts + src/block-definitions.json
17
+ -> extension-api-manifest Vite plugin
18
+ -> dist/extension-manifest.json
19
+ ```
20
+
21
+ The manifest plugin runs in Vite's post-build phase. This preserves the JavaScript plugin's
22
+ single-output validation and adds the manifest only after the TurboWarp bundle is complete.
23
+
24
+ ## Extension API manifest v1
25
+
26
+ `schemas/extension-manifest.schema.json` is the normative JSON Schema. `formatVersion` is `1` and
27
+ must change when an incompatible manifest shape is introduced.
28
+
29
+ The v1 contract contains:
30
+
31
+ - the TurboWarp extension ID;
32
+ - each block opcode and block type;
33
+ - each argument ID, argument type, and optional menu reference;
34
+ - each menu ID and whether it accepts reporter blocks.
35
+
36
+ Blocks, arguments, and menus are sorted by their identifiers before serialization. Text,
37
+ descriptions, default values, and static menu items are intentionally excluded because they do not
38
+ identify saved-project API references. A compatibility checker can therefore distinguish API
39
+ changes from documentation or localization changes.
40
+
41
+ The manifest and TurboWarp blocks are the package's public API. Source-level controllers and ports
42
+ are internal implementation seams and are not npm exports. See the
43
+ [extension API reference](turbowarp-extension-api.md) for loading order, lifecycle values, errors,
44
+ and data contracts.
45
+
46
+ ## Drift detection
47
+
48
+ `dist/` is committed as a release artifact. `npm run check:dist` rebuilds both files and fails when
49
+ Git reports any modified, deleted, or untracked file below `dist/`. This catches manifest and bundle
50
+ drift in local checks and CI.
51
+
52
+ ## Offer QR vertical slice
53
+
54
+ `config/feature-flags.ts` keeps `qrCourierPairing` startup-fixed and default OFF. When enabled, the
55
+ extension requires `kubohiroyaWebRtcCapability` version 2, calls `createOffer(peer)`, and retrieves
56
+ the completed pairing code with `getOffer(peer)`. It never reaches into another extension's private
57
+ instance.
58
+
59
+ The unchanged printable-ASCII pairing code is wrapped in versioned `twmp-qr/1` parts. Every part
60
+ contains a random session ID, peer and message identity, zero-based ordering, total source length,
61
+ and a SHA-256 digest. A conservative byte-mode capacity check bounds each QR to Version 40. The
62
+ wire input is limited to 128 KiB, 64 parts, and 4096 characters per chunk.
63
+
64
+ Display uses renderer-only SVG skins. The current drawable skin is retained, a generated skin is
65
+ attached without creating a VM costume, and cleanup restores the retained skin before destroying
66
+ the temporary resource. Project stop, extension disposal, explicit cleanup, and displayed-target
67
+ removal all release sensitive QR data.
68
+
69
+ ## WebGPU MoveNet MultiPose vertical slice
70
+
71
+ `webgpuMoveNetMultiPose` is an independent startup-fixed, default-OFF flag. Startup imports only the
72
+ TensorFlow.js WebGPU backend, explicitly calls `setBackend("webgpu")`, verifies the selected backend,
73
+ and then creates `MoveNet` with `MULTIPOSE_LIGHTNING`, tracking enabled, and bounding-box tracking.
74
+ Any non-WebGPU result fails closed; there is no CPU, WASM, or WebGL inference fallback.
75
+
76
+ The controller acquires `{cameraId: "pose"}` through Camera Source and never owns media capture.
77
+ Concurrent inference block calls share one promise, so detector invocations do not overlap and old
78
+ frame requests do not accumulate. Each successful call reads the latest video frame, requests no
79
+ more than six poses, requires model tracking IDs and all 17 named COCO keypoints, and serializes the
80
+ result to the `twrmc/pose-frame-2d` version 1 contract.
81
+
82
+ Stopping waits for in-flight initialization/inference, disposes the detector, releases the camera
83
+ lease, and clears the last frame. The TensorFlow.js backend is process-global and is not reset,
84
+ because doing so would invalidate resources owned by other extensions; disposing the detector
85
+ releases this feature's model resources.
86
+
87
+ ## Owned application contracts and their codec
88
+
89
+ `protocolV1Codec` is an independent startup-fixed, default-OFF flag. The codec parses at most 1 MiB
90
+ of JSON, reads the root `schema` and `version`, and dispatches only explicitly supported TypeBox
91
+ schemas. It performs no version inference or fallback. A successful decode retains one compact JSON
92
+ value; any failed decode clears it and exposes the first diagnostic as a JSON Pointer path and
93
+ message.
94
+
95
+ This package owns the contracts. `src/protocol/schemas.ts` is the source of truth, `pnpm run schemas`
96
+ generates the published JSON Schemas under `schemas/`, and the repository check fails when a
97
+ generated file drifts from its definition, when a file name disagrees with the `version` literal or
98
+ `$id` it declares, or when a dispatched version has no published file. Applications consume these
99
+ definitions through this package and through the published `schemas/` directory; nothing here reads
100
+ contract definitions from an application repository, which keeps the dependency pointing from the
101
+ application to the extension.
102
+
103
+ Contracts are versioned, never edited in place. `protocolSchemas` dispatches by schema identifier and
104
+ then by version, so `twrmc/pose-frame-2d` accepts v1 and v2 while a v1 consumer still rejects a v2
105
+ payload. PoseFrame2D v2 adds up to four glow stick markers per person, each naming the COCO-17
106
+ keypoint where a uniquely colored light was observed, its `#RRGGBB` color, and the patch coverage
107
+ that produced it. The color is observed on the same video frame and at the same capture timestamp as
108
+ the keypoints, so it travels inside the pose frame instead of a second message that a receiver would
109
+ have to time-align.
110
+
111
+ TypeBox tuple and array constraints enforce COCO-17 ordering, six-person limits, matrix sizes, and
112
+ all bounded values. A separate recursive key guard rejects WebRTC offers, answers, SDP, ICE/DTLS
113
+ material, and credential fields before persistence, even if a later schema accidentally permits a
114
+ nested extension point. SessionPolicy also enforces `expiresAt > issuedAt` and rejects expired
115
+ policies at application-validation time.
116
+
117
+ PoseFrame2D `captureTimestampUs` and PoseFrame3D `timestampUs` are opaque values from a separate
118
+ synchronized local time service. This extension carries the supplied timestamp unchanged and does
119
+ not implement clock initialization, offset estimation, probes, ping, or pong logic.
120
+
121
+ ## Camera calibration workflow
122
+
123
+ `cameraCalibrationV1` is startup-fixed and default OFF. A session validates the camera and
124
+ calibration identifiers plus a 3–20 by 3–20 inner-corner chessboard and a square size in meters,
125
+ then obtains a named Camera Source lease. The actual first frame dimensions become immutable for
126
+ the session; later resolution changes fail before detection.
127
+
128
+ Each explicitly requested sample performs one temporary video-to-canvas copy. The exact-pinned
129
+ OpenCV.js 4.12 WebAssembly backend is bundled but lazily initialized by the first sample or solve.
130
+ It detects the complete chessboard, refines corners to subpixel
131
+ precision, and scores board coverage plus Laplacian sharpness. Quality below 0.2 and views within
132
+ 0.015 normalized RMS corner displacement of any retained view are rejected. The controller retains
133
+ 8–40 diverse samples and does not run a continuous CPU sampling loop or a second reference solver.
134
+
135
+ `calibrateCamera` produces the intrinsic matrix, distortion vector, and RMS reprojection error. The
136
+ last sample is deliberately reserved as the stage-world board placement. Its returned rotation and
137
+ translation are inverted into the row-major 4 by 4 `worldFromCameraMatrix`. Results above the
138
+ configured RMS threshold or outside CameraCalibration v1 bounds do not replace the last valid
139
+ profile.
140
+
141
+ Cancel, project reload, and extension disposal release the lease and temporary samples while
142
+ keeping the last validated profile in memory. Explicit cleanup clears that profile too. Import uses
143
+ the same exact v1 schema and recursively rejects pairing-secret keys. The 11 MB uncompressed bundle
144
+ increase is accepted to keep the sole production backend available on an offline venue LAN; real
145
+ camera/board geometry and WebAssembly startup remain browser E2E responsibilities.
146
+
147
+ ## PoseFrame3D avatar retargeting
148
+
149
+ `avatarRetargetV1` is an independent startup-fixed, default-OFF flag. It requires runtime key
150
+ `turbowarpAFrameCapability`, calls `requireVersion(1)`, and uses only the seven public synchronous
151
+ scene operations from the TurboWarp-A-Frame capability. The consumer never accesses A-Frame DOM,
152
+ Three.js `object3D`, or GLTF bone internals. Capability v1 is published in
153
+ `@kubohiroya/turbowarp-aframe@0.3.0`.
154
+
155
+ An asset registration sends declarative template JSON to A-Frame and retains a validated rig map.
156
+ Each bone maps one supported Kalidokit pose rig output to a selector containing `{avatar}`, plus
157
+ optional Euler offset degrees. Applying a frame requires corresponding exact-v1 PoseFrame3D and
158
+ PoseFrame2D values. A person is joined only by PoseFrame3D `personId` equal to PoseFrame2D
159
+ `trackingId`; this is not temporal alignment.
160
+
161
+ The adapter maps both COCO-17 records deterministically to the 33 positions required by
162
+ exact-pinned `kalidokit@1.1.5`. Screen coordinates use PoseFrame2D `frameWidth` and `frameHeight`;
163
+ world coordinates retain the external service coordinate values. Missing BlazePose face, hand, and
164
+ foot points are midpoint-interpolated or duplicated with reduced visibility. Kalidokit `Pose.solve`
165
+ with `runtime: "tfjs"` and `enableLegs: true` is the sole rotation solver. Its radians are converted
166
+ to A-Frame degrees, and its hips result drives the configured root scale and offset. There is no
167
+ custom rotation fallback. Joint or person confidence below the binding threshold skips only that
168
+ transform and preserves its prior value.
169
+
170
+ At most six person IDs bind to unique template instances. Recognition transitions use configurable
171
+ A-Frame events, allowing the application to connect Performance DSL start/end effects without this
172
+ adapter owning effect execution. Per-person capability failures are collected as `partial` state so
173
+ other avatars continue. Rebind, explicit reset, project lifecycle reset, and disposal emit end when
174
+ possible, delete created instances, and clear temporary state.
175
+
176
+ PoseFrame3D is exact v1 boundary data from a separate 3D service. Its `timestampUs` is opaque and is
177
+ only copied into recognition event data. This extension performs no frame alignment, history
178
+ retention/query, triangulation, or 3D solve. Kalidokit is deprecated upstream and expects native
179
+ BlazePose landmarks; the deterministic COCO-17 expansion is therefore an explicit accuracy
180
+ constraint, and intended GLTF rigs require real-browser validation before release.
181
+
182
+ ## Frame sync pattern vertical slice
183
+
184
+ `frameSyncPatternV1` is startup-fixed and default OFF. It answers one question for the fusion
185
+ application: how long after an event each camera computer finishes recording the frame that shows
186
+ it.
187
+
188
+ The display side paints a full-screen overlay with a 4 by 4 panel on black. Twelve cells hold a
189
+ millisecond counter that wraps every 4096 ms; four hold check bits derived from the counter. A
190
+ camera exposure that straddles a display refresh mixes two codes, and the check bits reject that
191
+ reading instead of letting a wrong time through. What is drawn during one animation frame reaches
192
+ the screen at the next refresh, so the encoded time is the current clock reading plus one measured
193
+ refresh interval; the remaining projector delay is common to every camera and cancels out of the
194
+ per-camera offsets.
195
+
196
+ The camera side takes a named Camera Source lease and never calls `getUserMedia`. Each delivered
197
+ frame is downscaled to a 240 by 180 luminance buffer that is reused between callbacks because the
198
+ decoder reads it synchronously. Calibration runs in two phases on the live pattern: the first 60%
199
+ of the window records per-pixel minimum and maximum luminance and takes the largest connected
200
+ high-range region whose bounding box is panel shaped, and the rest learns each cell's own light and
201
+ dark level and measures how often readings decode. Uneven projection is why levels are per cell, and
202
+ a reading that lands between a cell's learned levels is discarded. Calibration fails with
203
+ `panel-not-found`, `low-contrast`, or `decode-unstable` rather than returning untrusted latencies.
204
+ The accepted window starts at 6.2 seconds, derived from the slowest cell's 2048 ms change period and
205
+ the share of the window the levels phase gets, so a window that cannot see every cell at both levels
206
+ is refused up front instead of failing later as low contrast. Both phases end on the shared clock, so
207
+ a stalled camera never leaves the controller waiting, and stopping mid-calibration settles the
208
+ pending run without recording a camera fault.
209
+
210
+ Decoded frames are queued as observations. Timestamps are opaque readings from the external
211
+ synchronized time service, taken when the frame reached the application; the browser-reported frame
212
+ age is exposed separately for callers that want the sensor exposure moment instead. Clock probing,
213
+ latency samples, and the aggregated per-camera report belong to the WebRTC extension, so no clock,
214
+ offset, ping, or pong logic lives here.
215
+ ## Multi-camera 3D pose fusion
216
+
217
+ `poseFusion3D` is an independent startup-fixed, default-OFF flag. The fusion app feeds the buffer
218
+ with PoseFrame2D JSON received over WebRTC data channels; this extension owns neither the transport
219
+ nor the clock.
220
+
221
+ Every camera gets its own timestamp-ordered ring buffer. Slot count is derived from the configured
222
+ delay and jitter window, bounded to 16 to 600 frames, and at most 16 cameras are buffered. An
223
+ in-window reorder is inserted at its timestamp position by shifting the shorter side of the ring, so
224
+ the common in-order append stays O(1). A duplicate timestamp, an arrival older than the newest frame
225
+ minus the jitter window, and an arrival older than the oldest retained frame of a full ring are
226
+ counted as dropped instead of buffered. None of this estimates a per-camera clock offset: capture
227
+ timestamps stay opaque values from the separate synchronized local time service.
228
+
229
+ A frame is only buffered when a calibration profile for its `cameraId` is loaded and that profile
230
+ describes it: a mismatched `calibrationId` or frame size would otherwise be triangulated silently
231
+ with the wrong intrinsics. Such frames, like duplicates and late arrivals, are counted as dropped
232
+ rather than thrown, because ingest runs on the data-channel hot path and one misconfigured peer must
233
+ not break a running project script. Because only calibrated cameras are buffered, stray camera IDs
234
+ cannot occupy the ring buffers either.
235
+
236
+ `fuse PoseFrame3D at buffered delay` resolves the newest buffered timestamp minus the configured
237
+ delay, and every camera is resampled at that single past instant. A keypoint bracketed by two frames
238
+ is interpolated linearly; a keypoint that is occluded on one side of the bracket keeps the visible
239
+ observation instead of blending a low-confidence estimate into it; a camera without a bracket, or
240
+ with a bracket wider than twice the jitter window, holds its nearest frame for at most one jitter
241
+ window and otherwise contributes nothing.
242
+
243
+ Cross-camera association scores every pair of tracked persons from different cameras by the mean
244
+ two-view reprojection error over their shared confident keypoints, requiring at least four shared
245
+ keypoints and stopping at twelve. Pairs are merged greedily from the lowest cost, and a merge that
246
+ would place two views of the same camera in one person is rejected. Two-view triangulation uses the
247
+ closed-form midpoint of both viewing rays, which keeps this quadratic stage off the iterative
248
+ solver; the general case still uses the score-weighted linear solver with a relative Jacobi
249
+ convergence threshold. One fusion at the 16 camera by 6 person limit measures about 80 ms, against
250
+ about 1 ms for four cameras and two performers.
251
+
252
+ Each cluster covered by at least two cameras is triangulated per keypoint with a cheirality check
253
+ and a reprojection check. When the full view set does not agree, every two-view seed is scored by
254
+ how many views fall inside the reprojection threshold, and the largest consensus set is
255
+ re-triangulated; a minority of wrong detections is therefore discarded instead of dragging the
256
+ keypoint away from the truth, while views split evenly between two consistent answers stay
257
+ ambiguous. Pixel observations are undistorted with the OpenCV rational model of the profile, so 0,
258
+ 4, 5, or 8 coefficients are supported and anything else is rejected when the profile is loaded.
259
+
260
+ A registry keeps stable `person-N` identifiers by camera and tracking-ID overlap, and keeps the last
261
+ triangulated position of every keypoint. A keypoint left with fewer than two confident views holds
262
+ that last position and reports score `0`, so consumers can distinguish measured from held values.
263
+ The assembled frame is checked against the pinned PoseFrame3D v1 schema before it is retained.
264
+
265
+ Empty buffers, fewer than two covering cameras, and an instant with no multi-camera person are
266
+ expected transient states: they report `false`, keep the last fused frame, and expose an error code.
267
+ Invalid JSON, a foreign schema, and an invalid calibration profile throw. The stop button, project
268
+ reload, and extension disposal clear buffers and fused results; explicit cleanup also clears the
269
+ loaded calibration profiles. `PROJECT_RUN_STOP` deliberately does not: the runtime emits it whenever
270
+ the thread queue empties, and an event-driven fusion project that buffers frames from hat scripts
271
+ would otherwise lose its jitter buffer between messages. Camera leases and temporary skins are still
272
+ released there.
273
+
274
+ ## Glow stick assisted identity and orientation
275
+
276
+ `glowStickMarkers` is an independent startup-fixed, default-OFF flag. On the camera side, each
277
+ inference samples one patch per configured keypoint of every tracked person from the same video
278
+ frame, in one temporary canvas that is released immediately. A patch is scored by hue: pixels below
279
+ the saturation or value threshold are ignored, the remaining hues are averaged circularly so a
280
+ wrapping red stays red, and a patch qualifies only when enough of it carried the color. The strongest
281
+ observation per keypoint becomes a PoseFrame2D v2 marker; the frame stays v1 while sampling is off.
282
+
283
+ On the fusion side the Performance DSL supplies the performer colors, and a fusion-side setting says
284
+ which keypoint each performer carries the light at, because that contract does not describe the
285
+ carrying hand. Each camera sample is assigned greedily from the strongest observation, and one
286
+ performer never claims two people in the same camera.
287
+
288
+ An assignment does two things. It fixes identity: the fused person takes the performer's identifier,
289
+ so `personId` survives occlusion, re-entry, and tracking-ID churn, and the association step refuses
290
+ to merge views of different performers while merging views of the same performer before any
291
+ reprojection cost is considered. It also fixes orientation: a color found on the mirror of the
292
+ performer's keypoint means that camera labelled a back view as a front view, so the view's left and
293
+ right keypoints are swapped before triangulation. Without that correction the swapped view pulls
294
+ every limb across the body or fails the reprojection gate entirely.
@@ -0,0 +1,83 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>TurboWarp Realtime Motion Capture documentation</title>
7
+ <style>
8
+ body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;max-width:900px;margin:0 auto;padding:2rem;line-height:1.6;color:#222}
9
+ code{background:#f3f3f3;padding:.1em .3em;border-radius:.25em}
10
+ pre{background:#f6f8fa;padding:1rem;overflow:auto;border-radius:.5rem}
11
+ table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:.5rem;text-align:left}
12
+ nav a{margin-right:1rem}
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <h1>TurboWarp Realtime Motion Capture</h1>
17
+ <p>Composite blocks for realtime motion capture camera and fusion applications.</p>
18
+
19
+ <nav>
20
+ <a href="https://github.com/kubohiroya/turbowarp-realtime-motion-capture#readme">User guide</a>
21
+ <a href="https://github.com/kubohiroya/turbowarp-realtime-motion-capture/blob/main/docs/turbowarp-extension-api.md">Extension API</a>
22
+ <a href="https://github.com/kubohiroya/turbowarp-realtime-motion-capture/blob/main/docs/turbowarp-extension-api.ja.md">API(日本語)</a>
23
+ <a href="https://github.com/kubohiroya/turbowarp-realtime-motion-capture/blob/main/docs/architecture.md">Architecture</a>
24
+ </nav>
25
+
26
+ <h2>Install and configure</h2>
27
+ <p>Load the provider extensions you use first, then load the version-pinned bundle as an unsandboxed custom extension:</p>
28
+ <pre>https://cdn.jsdelivr.net/npm/@kubohiroya/turbowarp-realtime-motion-capture@0.2.0/dist/turbowarp-realtime-motion-capture.js</pre>
29
+ <p>All feature groups are OFF by default. Set the complete startup configuration before loading the bundle:</p>
30
+ <pre><code>globalThis.__TWMP_FEATURE_FLAGS__ = {
31
+ qrCourierPairing: true,
32
+ webgpuMoveNetMultiPose: true,
33
+ protocolV1Codec: true,
34
+ cameraCalibrationV1: true,
35
+ avatarRetargetV1: true,
36
+ frameSyncPatternV1: true,
37
+ poseFusion3D: true,
38
+ glowStickMarkers: true,
39
+ };
40
+ globalThis.__TWMP_QR_CONFIG__ = {errorCorrectionLevel: "M"};</code></pre>
41
+ <p>Omit features you do not use. Reload the project and extension after changing this startup-fixed object.</p>
42
+
43
+ <h2>Offer QR blocks</h2>
44
+ <h3><code>prepare offer QR for peer [PEER]</code></h3>
45
+ <p>Creates a named WebRTC offer and prepares one or more QR courier parts.</p>
46
+ <h3><code>show offer QR part [INDEX] on this sprite</code></h3>
47
+ <p>Displays a one-based part as a temporary, lossless SVG sprite skin.</p>
48
+ <h3><code>end offer QR display</code></h3>
49
+ <p>Restores original skins and discards sensitive temporary session data.</p>
50
+ <p>The extension exposes blocks across eight independently enabled feature groups. See the <a href="https://github.com/kubohiroya/turbowarp-realtime-motion-capture/blob/main/docs/turbowarp-extension-api.md#block-reference">generated block reference</a> for every opcode, argument, type, and default.</p>
51
+
52
+ <h2>Enablement and safety</h2>
53
+ <p>The startup-fixed <code>qrCourierPairing</code> flag is OFF by default. The extension requires the version 2 TurboWarp WebRTC runtime capability and must run unsandboxed.</p>
54
+ <p>Pairing QR codes can expose local addressing and ICE credentials. Display them only inside a trusted venue and remove courier photographs after pairing.</p>
55
+ <h2>WebGPU MoveNet MultiPose</h2>
56
+ <p>The independent <code>webgpuMoveNetMultiPose</code> flag is OFF by default. When enabled, the extension leases frames from TurboWarp Camera Source, explicitly requires the TensorFlow.js <code>webgpu</code> backend, and returns up to six tracked COCO-17 poses as PoseFrame2D version 1 JSON.</p>
57
+ <p>No CPU, WASM, or WebGL inference fallback is provided. The first model load generally requires network access; prepare the browser cache before an offline venue run.</p>
58
+
59
+ <h2>Application contract codec</h2>
60
+ <p>The independent <code>protocolV1Codec</code> flag is OFF by default. When enabled, blocks validate and round-trip SessionPolicy, CameraCalibration, PoseFrame2D (v1 and v2), PoseFrame3D, and Performance DSL JSON. This extension owns those contracts and publishes their generated JSON Schemas; applications depend on the extension, never the reverse.</p>
61
+ <p>Unknown schemas, versions, fields, malformed COCO-17 tuples, and pairing credential keys fail closed. Diagnostic reporters expose the JSON Pointer path and message.</p>
62
+ <p>Pose timestamps are opaque values supplied by a separate synchronized local time service. This extension carries them unchanged and implements no clock probes, offset estimation, ping, or pong logic.</p>
63
+
64
+ <h2>Camera calibration</h2>
65
+ <p>The independent <code>cameraCalibrationV1</code> flag is OFF by default. A high-level state machine leases the named Camera Source stream, fixes its real resolution, accepts diverse chessboard samples, and exports an exact CameraCalibration v1 profile.</p>
66
+ <p>The bundled, exact-pinned OpenCV.js 4.12 WebAssembly backend is the sole solver. It estimates intrinsic, distortion, and world-from-camera values and rejects insufficient samples, resolution changes, weak views, excessive reprojection RMS, unknown profile versions, and pairing credentials.</p>
67
+
68
+ <h2>PoseFrame3D avatar retargeting</h2>
69
+ <p>The independent <code>avatarRetargetV1</code> flag is OFF by default. The adapter requires TurboWarp-A-Frame 0.3.0 scene capability v1 and uses only its public template, selector transform, event, count, and delete operations.</p>
70
+ <p>Corresponding external PoseFrame3D and PoseFrame2D v1 values can drive up to six declarative rigs. The adapter matches person IDs, expands COCO-17 to BlazePose-33, and uses exact-pinned Kalidokit 1.1.5 as its only rotation solver before sending public selector transforms to A-Frame.</p>
71
+ <p>The deterministic expansion uses reduced-visibility duplicates for unavailable hand, foot, and face landmarks and is less precise than native BlazePose input. Kalidokit is deprecated upstream, so intended GLTF rigs require real-browser validation. This extension provides no custom solver fallback and does not align frames, query history, triangulate, or solve 3D data.</p>
72
+ <h2>Multi-camera 3D pose fusion</h2>
73
+ <p>The independent <code>poseFusion3D</code> flag is OFF by default. When enabled, blocks buffer PoseFrame2D JSON per camera in timestamp-ordered ring buffers, resample every camera at one past instant behind a configured delay, and triangulate the synchronized 2D sets into PoseFrame3D version 1 JSON.</p>
74
+ <p>Out-of-order frames inside the jitter window are reordered, while duplicates, late arrivals, and frames that no loaded calibration profile describes are counted as dropped. Occluded keypoints are filled from the visible side of their bracket, disagreeing views are resolved by the largest consensus set, and a keypoint without two confident views holds its last triangulated position with score zero.</p>
75
+
76
+ <h2>Glow stick markers</h2>
77
+ <p>The independent <code>glowStickMarkers</code> flag is OFF by default. When enabled, the camera app samples the keypoints you name for a uniquely colored glow stick on the same video frame as the pose and reports PoseFrame2D version 2.</p>
78
+ <p>The fusion app maps those colors to performers with the Performance DSL palette. An identified person keeps the performer's identifier across occlusion and re-entry, views of different performers never merge, and a color seen on the mirrored keypoint tells the fusion that the camera read a back view as a front view, so that view's left and right labels are swapped before triangulation.</p>
79
+
80
+ <h2>Development</h2>
81
+ <p>The public API is the TurboWarp extension and its generated manifest. Source-level controllers are internal and are not npm exports. See the <a href="https://github.com/kubohiroya/turbowarp-realtime-motion-capture#readme">repository README</a> and <a href="https://github.com/kubohiroya/turbowarp-realtime-motion-capture/blob/main/docs/architecture.md">architecture documentation</a> for build, test, release, and compatibility details.</p>
82
+ </body>
83
+ </html>