会話が熱くなってきたことを、その場で静かに知らせるPWA。
夫婦や家族の話し合いで、片方が感情的になっていく。本人は気づかない。周りが指摘すると火に油。そこに人間ではないものを一つ置いて、事実だけを返す。それが目的です。
状態は3段階。凪 / さざ波 / 時化(しけ)。
- 最終更新: 2026-09-11
- ステータス: 設計中。コードなし
- 公開方針: 当面プライベート
| 項目 | 決定 |
|---|---|
| 名前 | 凪 / Nagi |
| 主軸 | リアルタイム検知(事後レビューではない) |
| 話者分離 | やらない。誰が喋っているかは問わない |
| 端末 | 1台。テーブルに置くだけ |
| 形態 | PWA。ストアには出さない |
| 音声の扱い | 端末外に一切出さない。録音もしない |
- 「あなたは感情的になっています」と名指しで言う
- 音声をサーバーに送る
- 録音を保存する
- 片方だけに通知を出す
- 診断や採点をする
部品は全部ある。日本語圏の夫婦向けで、完全ローカルという組み合わせが薄い。
研究
USCが2017年に、ウェアラブルと音声からカップルの衝突を検知する研究を発表。体温・心拍・発汗・音声・言語内容・声の強さを統合して、衝突エピソードの検知精度が最大86%。言語の手がかりだけだと62.3%まで落ちる。声のトーンと生体情報が効いているということ。研究者自身が「衝突が起きていることは検知できるが、起きる前の予測はまだできていない」と述べている。
論文: Timmons et al., "Using Multimodal Wearable Technology to Detect Conflict among Couples", IEEE Computer, 2017
製品
- Relationship Ref (iOS): AIメディエーター。会話をリアルタイム分析して沈静化の言い方を提案。ゴットマン法とNVCベース
- Selah's Insights (Google AIコンペ): 口論の音声をリアルタイム文字起こしし、遮りや話題そらしを検知するWebアプリ
日本語圏の感情認識エンジン
スワローインキュベートの感情認識SDK、Empathなど。音響特徴量ベースで認識率は7割程度、言語非依存なので多言語対応が可能。ただし今回はクラウドに送らない方針なので採用しない。
音響特徴量のみだと精度は7割前後が上限。研究の数字(音響+生体で86%、言語のみで62.3%)の間のどこかに落ちる。
だから「感情的です」とは言わない。事実だけ出して、解釈は本人に渡す。
声量が平常の1.9倍 直近5分で遮りが6回
これは精度の限界への対処であると同時に、UXとしても正しい。技術的制約と設計要件が同じ方向を向いているので、ここは迷わない。
1台構成なのでこれは自動的に満たされる。片方の端末に通知が飛ぶ設計にはしない。
「温度が上がってきました」ではなく「さざ波」「時化」。天気予報のように読めて、誰も責めていない。
Web Audio APIの範囲で完結する。MLモデルは載せない。
| 指標 | 実装 | 優先度 |
|---|---|---|
| 音量(RMS/dBFS)の逸脱 | AnalyserNode | 高 |
| 同時発話の回数と長さ | エネルギー包絡 | 高 |
| F0(基本周波数)の上昇と変動幅 | YIN or 自己相関 | 高 |
| スペクトル重心 | FFTビンの加重平均 | 中 |
| 発話速度 | 発話区間の密度 | 低 |
| 無音の長さ | エネルギー閾値 | 低 |
怒鳴ると高周波にエネルギーが寄って声が硬くなるので、スペクトル重心は計算コストの割によく効く。黙り込みも時化に含める。
絶対値は使わない。開始2分のキャリブレーションで取った平常値からの逸脱で見る。
話者を分けない設計の最大の弱点。
平常時のF0は男女で倍近く違う。絶対値で閾値を切ると、低い声の人はほぼ検知されず、高い声の人だけが延々と反応する。
これが厄介なのは、当初の問題意識(いつも片方が感情的になる)と表面的に噛み合って見えること。実際には測定バイアスと本物の傾向が区別できなくなる。データが正しいのか装置が偏っているのか誰にも分からなくなるので、ここは最初から潰しておく。
対策A: 全部を相対値にする
絶対F0を一切使わず、「直近30秒が過去10分の分布のどこにいるか」だけを見る。誰の声かは問わない。実装が一番楽。まずこれで足りるはず。
対策B: F0を2つの帯に分ける
検出したF0を低群と高群に振り分け、それぞれ別のベースラインを持つ。1次元の閾値なので軽い。話者分離ではなく声域の正規化。
Aから始めて、偏りが体感できたらBを足す。
navigator.mediaDevices.getUserMedia({
audio: {
autoGainControl: false,
noiseSuppression: false,
echoCancellation: false
}
})これは必須。 デフォルトのままだとブラウザのAGCが音量を勝手に平坦化して、音量指標が全部潰れる。
音声処理はAudioWorklet側に閉じ込める。Reactには判定結果の状態だけ流す。毎フレームstateを更新すると死ぬので、Worklet内で集計して200msごとにpostMessage程度の粒度にする。
AudioWorkletはHTTPS必須。ローカル開発はlocalhost例外で通る。
ユーザー操作のハンドラ内でしか開始できない。「開始」ボタンのonClickの中で new AudioContext() して resume()。iOSが特に厳しい。
移動平均(EWMA)とヒステリシスを入れる。状態が凪とさざ波を往復するとただの雑音になる。
1台で環境音を全部拾うので、テレビの怒鳴り声で時化判定が出る。有声音の周期性があるか、F0が人間の範囲に収まるか、程度の簡易判定を一段入れる。完璧でなくていい。明らかな音楽とノイズを弾ければ十分。
机の上の置き位置で音量が全然変わるので、2分のキャリブレーションは毎回必須。前回の値は再利用しない。
Androidはほぼ素直に動く。問題はiOS。コードを書く前に実機で確認する。
マイク許可が永続しない可能性がある ホーム画面から起動するstandaloneモードのPWAでは、起動のたびに許可ダイアログが出る挙動が長く続いていた。Safariのタブで開いたときと扱いが違う。改善されている可能性もあるので実機確認が必要。毎回聞かれるなら、それを前提にしたオンボーディング(「起動したら許可を押してから机に置く」)にする。
getUserMedia自体はiOS 14.3以降でstandaloneでも動く それ以前の端末は切る。
インストール導線が手動
iOSには beforeinstallprompt がない。「共有ボタンからホーム画面に追加」を図で案内する画面を自作する。ここで脱落するので、Safariで開いたときだけ出る専用バナーを用意する。
Screen Wake Lock API はSafari 16.4以降 画面が消えるとマイクが止まる。セッション中は必ず取得する。
履歴を残すならIndexedDB。
ホーム画面に追加済みのPWAは、iOSの7日間ストレージ削除ポリシーの対象外になる。ただのブラウザタブだと1週間触らなければ記録が消える。
「インストールするとデータが残ります」は、ユーザーに正直に説明できるインストール理由になる。
全アセットをプリキャッシュして完全オフライン動作。ネットワークに一度も触らない設計なので、fetchイベントでキャッシュを返すだけ。オンライン判定もフォールバックもいらない。
manifest:
display: standaloneorientation: portrait- アイコン(波が一本の水平線に落ち着いていくモチーフ)
完全静的。関数も帯域も使わないので、VercelでもCloudflare Pagesでも無料枠で余裕。どちらに転んでも影響が出ないので、手を動かして比べる題材としてちょうどいいかもしれない。
- マイク許可のオンボーディング(iOSは手動インストール案内つき)
- 開始2分のキャリブレーションで平常値を取る
- 音量・同時発話・F0の3指標のみ。EWMA + ヒステリシス
- 3段階の状態を大きく1つ表示。数字は出さない。根拠を小さく1行だけ添える
- 音は鳴らさない。バイブのみ
- 時化が2分続いたら「10分休憩しますか」。押すのは必須にしない
スペクトル重心と発話速度は後回しでいい。同時発話の検出だけでも体感はかなり出るはず。
音声を端末外に出していないことが、主張ではなく検証可能な事実になる。
DevToolsのNetworkタブを開けば何も飛んでいないのが見える。夫婦喧嘩を録る道具としては、ここがほぼ全て。ランディングページの一番上に置く。
ストアに出さないので発見されにくいのは承知の上。初期は知り合いに配る段階なので問題にならないし、むしろ「会話を録音するアプリ」として審査で揉めるリスクを回避できる。実際には録音していないが、説明コストは確実に発生する。
- iPhone実機でマイク許可の挙動を確認する。ここの答えでオンボーディングの形が決まるので、コードを書く前に知っておきたい
- 音響パイプラインの検証。1台で自分の声だけ測って、「普通に喋る」「少し声を張る」の2パターンで指標がどれだけ動くか見る。閾値の当たりをつける
- 声域バイアスの実測。低い声と高い声で同じ「張り」を出したとき、指標が同じくらい動くか
- 履歴機能を入れるかどうか(リアルタイム主軸なので後回しでもいい)
- 休憩提案を受け入れたかどうかの記録を取るか
- セッション終了後に何を見せるか。何も見せないのも選択肢
- 「時化」の閾値をユーザーが調整できるようにするか