qids-j-webapp

utils — 記録データの読み込みサンプル

QIDS-J webapp が書き出すファイルを読み込む最小サンプルを置いてあります。

v1 と v2 でファイル構成が異なります

バージョン 問卷終了時にダウンロードできるもの
v1(旧実装・live detection) qids-j_recording_*.webmqids-j_landmarks_*.json(.gz) — 特徴点データは録画中にリアルタイムで抽出済み
v2(現行・post-hoc) qids-j_recording_*.webmqids-j_session_*.json(メタ情報 + events + questionSegments + 回答のみ。
frames[] は空)

v2 では、録画中は MediaRecorder のみ動作し、特徴点は問卷終了後にユーザーが 分析ボタンを押した時点で js/extract.mjs が webm を MediaPipe に通して抽出します。 抽出結果は分析ビューアーに直接渡されるため、通常のユーザーが特徴点 JSON を手動で 扱う場面は減りました

特徴点データを手元で解析したい場合の流れ(v2)

  1. 問卷終了後、「録画 (webm)」と「セッションログ (json)」をダウンロード
  2. analyze.html に webm + session.json をまとめてドロップ → 抽出 → ブラウザの DevTools から IndexedDB の qids-j-handoff ストアを見ると、抽出済みの 完全 JSON が入っています。または、
  3. 抽出後の JSON が必要なら analyze 側に将来「JSON エクスポート」ボタンを足すことも可能 (現時点未実装)

既存の v1 landmark JSON は analyze.html でも utils/decode.py でもそのまま読めます — このフォルダ配下のサンプルコードは v1 / v2 どちらの形式にも対応しています。

ファイルフォーマット

{
  "meta": { /* 後述 */ },

  "questionSegments": [
    // 問題ごとの入退出時刻・活動時間・回答履歴のサマリ(16 項目)
    {
      "q": 0,                             // 0-indexed
      "questionNumber": 1,                // 1-indexed(Q1)
      "title": "寝つき",
      "domain": "sleep",
      "enterTimes": [0.0, 45210.5],       // 2 回入ってきた
      "firstAnswerTime": 3210.1,          // 最初に選択肢を押した時刻
      "lastAnswerTime": 4980.7,           // 最後に選び直した時刻
      "finalAnswer": 1,                   // 最終的な選択 (0-3)
      "finalizeTime": 5430.2,             // 「次へ」を押して離れた時刻
      "answerEventCount": 2,              // 選び直し回数
      "activeTimeRanges": [[0.0, 5430.2], [45210.5, 48200.3]],
      "activeDurationMs": 8420.0          // 合計滞在時間
    }
  ],

  "frames": [
    // 通常の検出フレーム(30 fps)
    {
      "t": 123.45,
      "q": 0,
      "pts": [[0.5123, 0.4821, -0.0142], /* ... 478 点 × [x,y,z] */],
      "bs":  { "jawOpen": 0.12, "browInnerUp": 0.33, /* ... 52 項目 */ },
      "mat": [/* 16 floats (4x4 column-major) */]
    },
    // イベントマーカー(pts/bs/mat は持たない)
    { "t": 5432.10, "q": 1, "event": "question_enter" },
    { "t": 5980.12, "q": 1, "event": "answer_selected",   "a": 2 },
    { "t": 6310.45, "q": 1, "event": "answer_selected",   "a": 1 },  // 選び直し
    { "t": 6800.00, "q": 1, "event": "question_finalize", "a": 1 }
  ],

  "result":  { "total": 12, "severity": "中等度", "severityKey": "moderate", "breakdown": {...} },
  "answers": [{ "q": 1, "title": "寝つき", "score": 1 }, /* ... 16 項目 */]
}

meta

{
  "sessionStart": "2026-04-17T05:12:30.123Z",
  "videoWidth": 640,
  "videoHeight": 480,
  "runtime": "@mediapipe/tasks-vision@0.10.14",
  "modelUrl": "https://.../face_landmarker.task",
  "targetFps": 30,
  "mirrored": true,
  "pointCount": 478,
  "blendshapeCount": 52,
  "ptsFormat": "array of [x, y, z] normalized",
  "matFormat": "16 floats, 4x4 facial transformation matrix, column-major",
  "timeBase": "performance.now() ms relative to recording start",
  "eventTypes": ["question_enter", "answer_selected", "question_finalize"]
}

特定の問題のフレームだけ抜き出す

import gzip, json
doc = json.load(gzip.open("qids-j_landmarks_...json.gz", "rt", encoding="utf-8"))

def frames_of_question(doc, q_index):
    """Q(q_index+1) に滞在していた間の検出フレームを返す。"""
    seg = next((s for s in doc["questionSegments"] if s["q"] == q_index), None)
    if not seg:
        return []
    ranges = seg["activeTimeRanges"]
    return [
        f for f in doc["frames"]
        if "pts" in f and any(a <= f["t"] < b for (a, b) in ranges)
    ]

# 例: Q1(寝つき)回答中のフレーム
q0_frames = frames_of_question(doc, 0)
print(f"Q1 detection frames: {len(q0_frames)}")

# ついでに Q1 にかかった合計時間
seg = doc["questionSegments"][0]
print(f"Q1 滞在時間: {seg['activeDurationMs']/1000:.1f}s, "
      f"最初に答えを選ぶまで: {(seg['firstAnswerTime'] - seg['enterTimes'][0])/1000:.1f}s")

座標系

blendshape の例(52 項目)

MediaPipe FaceLandmarker は ARKit 準拠の名前で blendshape スコア(0〜1)を返します:

_neutral
browDownLeft, browDownRight, browInnerUp, browOuterUpLeft, browOuterUpRight
cheekPuff, cheekSquintLeft, cheekSquintRight
eyeBlinkLeft, eyeBlinkRight, eyeLookDownLeft, eyeLookDownRight,
eyeLookInLeft, eyeLookInRight, eyeLookOutLeft, eyeLookOutRight,
eyeLookUpLeft, eyeLookUpRight, eyeSquintLeft, eyeSquintRight,
eyeWideLeft, eyeWideRight
jawForward, jawLeft, jawOpen, jawRight
mouthClose, mouthDimpleLeft, mouthDimpleRight, mouthFrownLeft, mouthFrownRight,
mouthFunnel, mouthLeft, mouthLowerDownLeft, mouthLowerDownRight,
mouthPressLeft, mouthPressRight, mouthPucker, mouthRight, mouthRollLower,
mouthRollUpper, mouthShrugLower, mouthShrugUpper,
mouthSmileLeft, mouthSmileRight, mouthStretchLeft, mouthStretchRight,
mouthUpperUpLeft, mouthUpperUpRight
noseSneerLeft, noseSneerRight

使い方

Python

# 最低限
python utils/decode.py qids-j_landmarks_20260417_142530.json.gz

# pandas / numpy を入れておくと DataFrame 化までしてくれる
pip install pandas numpy
python utils/decode.py qids-j_landmarks_20260417_142530.json.gz

pandas を使う場合、decode.pyto_dataframe()

t | q | pt0_x pt0_y pt0_z ... pt477_z | bs_jawOpen ... | mat0 ... mat15

という形の DataFrame を返すので、時系列分析や機械学習用の特徴量作成にそのまま使えます。

Node.js (>= 18)

node utils/decode.mjs qids-j_landmarks_20260417_142530.json.gz

decode.mjs は追加の npm 依存なしで動きます。特定 blendshape の時系列を取り出したいときは:

import { blendshapeSeries } from './utils/decode.mjs';
const smile = blendshapeSeries(doc, 'mouthSmileLeft');
// => [[t1, 0.12], [t2, 0.14], ...]

手動展開(zcat / gunzip)

gunzip qids-j_landmarks_20260417_142530.json.gz      # → .json が残る
# または
zcat  qids-j_landmarks_20260417_142530.json.gz | jq .meta

サイズの目安