This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(sample): log skeleton events in experiment dumps

The experiment logger was originally object-only — every event wrote
"skeleton": null regardless of what the pose pipeline produced. That
made it impossible to measure pose A/Bs from the captured JSON.

ExperimentEvent now carries an optional Skeleton, the JSON writer
serialises the 12 landmarks (or null per-joint) plus frame dims, and
App.kt collects skeletonFlow alongside customObjectFlow into the same
event buffer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+48 -9
+13
sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/App.kt
··· 447 447 } 448 448 } 449 449 450 + LaunchedEffect(experimentRunning) { 451 + if (!experimentRunning) return@LaunchedEffect 452 + skeletonRepository.skeletonFlow.drop(1).collect { skel -> 453 + experimentEvents.add( 454 + ExperimentEvent( 455 + wallMs = Clock.System.now().toEpochMilliseconds(), 456 + skeleton = skel, 457 + ) 458 + ) 459 + experimentCapturedCount = experimentEvents.size 460 + } 461 + } 462 + 450 463 // Auto-mode driver: when the activity was launched with an 451 464 // ExperimentLaunchSpec (via intent extras → AppActivity → CompositionLocal), 452 465 // automatically pick the model, wait until the orchestrator's wall-clock
+35 -9
sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/ExperimentLogger.kt
··· 2 2 3 3 import androidx.compose.runtime.Composable 4 4 import com.performancecoachlab.posedetection.recording.AnalysisObject 5 + import com.performancecoachlab.posedetection.skeleton.Skeleton 5 6 6 7 /** 7 8 * Persists experiment-mode capture buffers from the sample app to a platform- ··· 26 27 27 28 internal data class ExperimentEvent( 28 29 val wallMs: Long, 29 - val objects: List<AnalysisObject>, 30 + val objects: List<AnalysisObject> = emptyList(), 31 + val skeleton: Skeleton? = null, 30 32 ) 31 33 32 - /** 33 - * Hand-builds the experiment log JSON document. Format matches the schema in 34 - * the closed-loop training plan: device + model identification, start/stop 35 - * wall-clock bracket, and an `events` array. The `skeleton` field per event 36 - * is reserved but always emitted as null since this MVP compares object 37 - * detection models only. 38 - */ 39 34 internal fun buildExperimentLogJson( 40 35 deviceModel: String, 41 36 modelName: String, ··· 56 51 if (idx > 0) sb.append(',') 57 52 sb.append('{') 58 53 sb.append("\"wall_ms\":").append(e.wallMs).append(',') 59 - sb.append("\"skeleton\":null,") 54 + sb.append("\"skeleton\":") 55 + val s = e.skeleton 56 + if (s == null) { 57 + sb.append("null") 58 + } else { 59 + sb.append('{') 60 + sb.append("\"w\":").append(jsonNumber(s.width)).append(',') 61 + sb.append("\"h\":").append(jsonNumber(s.height)).append(',') 62 + sb.append("\"joints\":{") 63 + val joints = listOf( 64 + "leftShoulder" to s.leftShoulder, 65 + "rightShoulder" to s.rightShoulder, 66 + "leftElbow" to s.leftElbow, 67 + "rightElbow" to s.rightElbow, 68 + "leftWrist" to s.leftWrist, 69 + "rightWrist" to s.rightWrist, 70 + "leftHip" to s.leftHip, 71 + "rightHip" to s.rightHip, 72 + "leftKnee" to s.leftKnee, 73 + "rightKnee" to s.rightKnee, 74 + "leftAnkle" to s.leftAnkle, 75 + "rightAnkle" to s.rightAnkle, 76 + ) 77 + joints.forEachIndexed { i, (name, c) -> 78 + if (i > 0) sb.append(',') 79 + sb.append('"').append(name).append("\":") 80 + if (c == null) sb.append("null") 81 + else sb.append('[').append(jsonNumber(c.x)).append(',').append(jsonNumber(c.y)).append(']') 82 + } 83 + sb.append("}}") 84 + } 85 + sb.append(',') 60 86 sb.append("\"objects\":[") 61 87 e.objects.forEachIndexed { i, obj -> 62 88 if (i > 0) sb.append(',')