Skip to content

About

Nagi (凪): a PWA that quietly lets you know when a conversation is getting heated. Design stage. / 会話が熱くなってきたことを静かに知らせるPWA

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 

Repository files navigation

凪 / Nagi

会話が熱くなってきたことを、その場で静かに知らせるPWA。

夫婦や家族の話し合いで、片方が感情的になっていく。本人は気づかない。周りが指摘すると火に油。そこに人間ではないものを一つ置いて、事実だけを返す。それが目的です。

状態は3段階。凪 / さざ波 / 時化(しけ)。

  • 最終更新: 2026-09-11
  • ステータス: 設計中。コードなし
  • 公開方針: 当面プライベート

決まっていること

項目 決定
名前 凪 / Nagi
主軸 リアルタイム検知(事後レビューではない)
話者分離 やらない。誰が喋っているかは問わない
端末 1台。テーブルに置くだけ
形態 PWA。ストアには出さない
音声の扱い 端末外に一切出さない。録音もしない

やらないこと

  • 「あなたは感情的になっています」と名指しで言う
  • 音声をサーバーに送る
  • 録音を保存する
  • 片方だけに通知を出す
  • 診断や採点をする

既存調査 (2026-09時点)

部品は全部ある。日本語圏の夫婦向けで、完全ローカルという組み合わせが薄い。

研究

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台構成なのでこれは自動的に満たされる。片方の端末に通知が飛ぶ設計にはしない。

名前がそのままUIになる

「温度が上がってきました」ではなく「さざ波」「時化」。天気予報のように読めて、誰も責めていない。


計測指標

Web Audio APIの範囲で完結する。MLモデルは載せない。

指標 実装 優先度
音量(RMS/dBFS)の逸脱 AnalyserNode 高
同時発話の回数と長さ エネルギー包絡 高
F0(基本周波数)の上昇と変動幅 YIN or 自己相関 高
スペクトル重心 FFTビンの加重平均 中
発話速度 発話区間の密度 低
無音の長さ エネルギー閾値 低

怒鳴ると高周波にエネルギーが寄って声が硬くなるので、スペクトル重心は計算コストの割によく効く。黙り込みも時化に含める。

絶対値は使わない。開始2分のキャリブレーションで取った平常値からの逸脱で見る。


声域バイアス (要注意)

話者を分けない設計の最大の弱点。

平常時のF0は男女で倍近く違う。絶対値で閾値を切ると、低い声の人はほぼ検知されず、高い声の人だけが延々と反応する。

これが厄介なのは、当初の問題意識(いつも片方が感情的になる)と表面的に噛み合って見えること。実際には測定バイアスと本物の傾向が区別できなくなる。データが正しいのか装置が偏っているのか誰にも分からなくなるので、ここは最初から潰しておく。

対策A: 全部を相対値にする

絶対F0を一切使わず、「直近30秒が過去10分の分布のどこにいるか」だけを見る。誰の声かは問わない。実装が一番楽。まずこれで足りるはず。

対策B: F0を2つの帯に分ける

検出したF0を低群と高群に振り分け、それぞれ別のベースラインを持つ。1次元の閾値なので軽い。話者分離ではなく声域の正規化。

Aから始めて、偏りが体感できたらBを足す。


技術メモ

getUserMediaの制約

navigator.mediaDevices.getUserMedia({
  audio: {
    autoGainControl: false,
    noiseSuppression: false,
    echoCancellation: false
  }
})

これは必須。 デフォルトのままだとブラウザのAGCが音量を勝手に平坦化して、音量指標が全部潰れる。

AudioWorklet

音声処理はAudioWorklet側に閉じ込める。Reactには判定結果の状態だけ流す。毎フレームstateを更新すると死ぬので、Worklet内で集計して200msごとにpostMessage程度の粒度にする。

AudioWorkletはHTTPS必須。ローカル開発はlocalhost例外で通る。

AudioContextの開始

ユーザー操作のハンドラ内でしか開始できない。「開始」ボタンのonClickの中で new AudioContext() して resume()。iOSが特に厳しい。

チャタリング防止

移動平均(EWMA)とヒステリシスを入れる。状態が凪とさざ波を往復するとただの雑音になる。

人の声かどうかの判定

1台で環境音を全部拾うので、テレビの怒鳴り声で時化判定が出る。有声音の周期性があるか、F0が人間の範囲に収まるか、程度の簡易判定を一段入れる。完璧でなくていい。明らかな音楽とノイズを弾ければ十分。

キャリブレーション

机の上の置き位置で音量が全然変わるので、2分のキャリブレーションは毎回必須。前回の値は再利用しない。


iOSの関門

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週間触らなければ記録が消える。

「インストールするとデータが残ります」は、ユーザーに正直に説明できるインストール理由になる。


Service Worker / manifest

全アセットをプリキャッシュして完全オフライン動作。ネットワークに一度も触らない設計なので、fetchイベントでキャッシュを返すだけ。オンライン判定もフォールバックもいらない。

manifest:

  • display: standalone
  • orientation: portrait
  • アイコン(波が一本の水平線に落ち着いていくモチーフ)

ホスティング

完全静的。関数も帯域も使わないので、VercelでもCloudflare Pagesでも無料枠で余裕。どちらに転んでも影響が出ないので、手を動かして比べる題材としてちょうどいいかもしれない。


MVPの範囲

  1. マイク許可のオンボーディング(iOSは手動インストール案内つき)
  2. 開始2分のキャリブレーションで平常値を取る
  3. 音量・同時発話・F0の3指標のみ。EWMA + ヒステリシス
  4. 3段階の状態を大きく1つ表示。数字は出さない。根拠を小さく1行だけ添える
  5. 音は鳴らさない。バイブのみ
  6. 時化が2分続いたら「10分休憩しますか」。押すのは必須にしない

スペクトル重心と発話速度は後回しでいい。同時発話の検出だけでも体感はかなり出るはず。


訴求ポイント

音声を端末外に出していないことが、主張ではなく検証可能な事実になる。

DevToolsのNetworkタブを開けば何も飛んでいないのが見える。夫婦喧嘩を録る道具としては、ここがほぼ全て。ランディングページの一番上に置く。

ストアに出さないので発見されにくいのは承知の上。初期は知り合いに配る段階なので問題にならないし、むしろ「会話を録音するアプリ」として審査で揉めるリスクを回避できる。実際には録音していないが、説明コストは確実に発生する。


次のステップ

  1. iPhone実機でマイク許可の挙動を確認する。ここの答えでオンボーディングの形が決まるので、コードを書く前に知っておきたい
  2. 音響パイプラインの検証。1台で自分の声だけ測って、「普通に喋る」「少し声を張る」の2パターンで指標がどれだけ動くか見る。閾値の当たりをつける
  3. 声域バイアスの実測。低い声と高い声で同じ「張り」を出したとき、指標が同じくらい動くか

未決事項

  • 履歴機能を入れるかどうか(リアルタイム主軸なので後回しでもいい)
  • 休憩提案を受け入れたかどうかの記録を取るか
  • セッション終了後に何を見せるか。何も見せないのも選択肢
  • 「時化」の閾値をユーザーが調整できるようにするか

About

Nagi (凪): a PWA that quietly lets you know when a conversation is getting heated. Design stage. / 会話が熱くなってきたことを静かに知らせるPWA

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors