👶

Gemini + Macのカメラとマイクで育児をリアルタイムにサポートするAIを作った

に公開

やったこと

Macのカメラとマイクで育児中の映像を30秒ごとに録画して、Gemini APIで解析して、音声でアドバイスを返してくれるCLIアプリを作りました。

「耳元にいる育児コーチ」みたいなイメージです。

背景

育児中、つい「ダメ!」「やめて!」と否定的な言葉が出てしまうことがあります。「こっちにしてみよう」みたいに言い換えたいけど、余裕がないときほど気づけない。

じゃあAIに横から見てもらって、必要なときだけ声をかけてもらえばいいのでは?というのが出発点です。

構成

Mac カメラ + マイク
    ↓ ffmpeg (30秒録画) + sounddevice (音声録音)
  動画ファイル (.mp4)
    ↓ 無音検出 (volumedetect)
    ↓ 無音ならスキップ
  Gemini API (gemini-3-flash-preview)
    ↓ JSON 構造化出力
  macOS say コマンド (音声出力)

30秒ごとに録画 → 解析 → 必要なときだけ音声でアドバイスするサイクルです。

実装の詳細

録画: ffmpeg + sounddevice の並行録画

映像はffmpegのavfoundation、音声はsounddeviceで別々に録っています。なぜ分けているかというと、avfoundationの音声キャプチャには96kHzの音声ドロップ問題があって、そのまま使うと音が途切れることがあるからです。

def record(duration=30, output_dir=".", video_device=DEFAULT_VIDEO, audio_keyword="MacBook"):
    vid = _find_video_index(video_device)
    aud = _find_audio_device_id(audio_keyword)

    # 音声を先に開始(sounddeviceはほぼ即座に録音開始)
    t0 = time.monotonic()
    audio_thread = threading.Thread(target=audio_worker)
    audio_thread.start()

    # 映像キャプチャ開始(ffmpegは初期化に0.5〜1.5秒かかる)
    video_proc = subprocess.run([
        "ffmpeg", "-f", "avfoundation",
        "-thread_queue_size", "4096",
        "-framerate", "30", "-video_size", "640x480",
        "-i", f"{vid}:none",
        "-t", str(duration),
        "-c:v", "libx264", "-preset", "ultrafast",
        "-y", video_path,
    ], capture_output=True, text=True)
    t1 = time.monotonic()

    audio_thread.join()

ポイントは音声を先に開始していること。ffmpegは初期化に0.5〜1.5秒かかるので、音声側がその分だけ先に始まります。結合時にこのオフセットを計算してトリミングすることで同期を取っています。

# ffmpegの初期化時間分だけ音声の先頭をスキップ
offset = max(0.0, (t1 - t0) - duration)

subprocess.run([
    "ffmpeg",
    "-i", video_path,
    "-ss", f"{offset:.3f}", "-i", audio_path,
    "-c:v", "copy", "-c:a", "aac", "-b:a", "128k",
    "-shortest", "-y", output_path,
])

この同期処理は地味にハマったところで、オフセット補正なしだと映像と音声が0.5〜1秒ズレて、Geminiの解析精度にも影響が出ます。

デバイス検出の自動化

カメラとマイクのデバイスインデックスは環境によって変わるので、名前で自動検出するようにしています。

def _find_video_index(device_name: str) -> str:
    """avfoundationのビデオデバイス一覧から名前でインデックスを検索"""
    result = subprocess.run(
        ["ffmpeg", "-f", "avfoundation", "-list_devices", "true", "-i", ""],
        capture_output=True, text=True,
    )
    # stderr から "[0] FaceTime HD Camera" のようなパターンを探す
    for line in result.stderr.splitlines():
        m = re.search(r'\[(\d+)\]\s+(.+)', line)
        if m and device_name in m.group(2):
            return m.group(1)

音声側はsounddeviceのAPIでデバイス一覧を取得して、キーワードマッチで探しています。「MacBook」で引っかかるので大抵のMacではそのまま動きます。

無音スキップ

子どもが寝ていたり部屋が静かなときはAPIに送る意味がないです。ffmpegのvolumedetectフィルタで平均音量を取得して、閾値(デフォルト-50dB)以下ならスキップしています。

def detect_audio_level(video_path: str) -> float:
    result = subprocess.run(
        ["ffmpeg", "-i", video_path,
         "-af", "volumedetect",
         "-f", "null", "/dev/null"],
        capture_output=True, text=True,
    )
    match = re.search(r"mean_volume:\s*([-\d.]+)\s*dB", result.stderr)
    return float(match.group(1)) if match else -91.0

実際に使ってみると、この無音スキップだけでAPIコールが半分以下になりました。子どもが寝ている時間や、別の部屋にいる時間が意外と長いので。

Gemini APIによる解析

動画をGemini APIにアップロードして、構造化されたJSONで結果を受け取ります。

def analyze(video_path: str, api_key: str) -> dict:
    client = genai.Client(api_key=api_key)

    # 動画をアップロード
    video_file = client.files.upload(file=video_path)

    # 処理完了を待つ
    while video_file.state and video_file.state.name != "ACTIVE":
        time.sleep(3)
        video_file = client.files.get(name=video_file.name)

    # 構造化出力で解析
    response = client.models.generate_content(
        model="gemini-3-flash-preview",
        contents=[video_file, SYSTEM_PROMPT],
        config={
            "response_mime_type": "application/json",
            "response_json_schema": RESPONSE_SCHEMA,
        },
    )

    # プライバシー保護: クラウド上の動画を即削除
    client.files.delete(name=video_file.name)

    return json.loads(response.text)

response_json_schemaを指定することで、Geminiのレスポンスを確実にパースできます。自由形式のテキストだとパース失敗のハンドリングが面倒なので、構造化出力はかなり助かっています。

スキーマはこんな感じです。

RESPONSE_SCHEMA = {
    "type": "object",
    "properties": {
        "situation": {"type": "string", "description": "現在の状況の要約"},
        "feedback": {"type": "string", "description": "親への具体的な声がけアドバイス"},
        "evidence": {"type": "string", "description": "アドバイスの根拠(発達心理学ベース)"},
        "has_feedback": {"type": "boolean", "description": "アドバイスが必要かどうか"},
    },
    "required": ["situation", "feedback", "evidence", "has_feedback"],
}

has_feedbackfalseのときは何も言わない。常にアドバイスが飛んでくるとさすがにウザいので、必要なときだけ話す設計にしています。

プライバシーへの配慮

育児の映像という性質上、プライバシーには気を使っています。

  • Gemini APIの有料枠を使うことで、データがモデル学習に使用されない設定にしている
  • 解析完了後、クラウド上の動画ファイルは即座に削除(client.files.delete()
  • ローカルでも、フィードバックがなかった動画は即削除
  • フィードバックありの動画だけfeedback/に保存(振り返り用)

メインループ

全体の流れをまとめるとこうなります。

while True:
    # 1. 30秒間録画
    video_path = record(duration=args.duration)

    # 2. 無音チェック
    mean_vol = detect_audio_level(video_path)
    if mean_vol <= args.silence_threshold:
        os.remove(video_path)
        continue

    # 3. Geminiで解析
    result = analyze(video_path, api_key)

    # 4. フィードバックがあれば音声出力
    if result.get("has_feedback"):
        shutil.move(video_path, feedback_dir)
        speak(result["feedback"])  # macOS say コマンド
    else:
        os.remove(video_path)

各サイクルのログはJSON形式でlogs/に保存していて、後から「いつどんなアドバイスがあったか」を振り返れるようにしています。

プロンプト設計

プロンプトではAIの役割を4つに絞っています。

  1. 言い換え — 親の否定語を肯定語に変換する案を出す
  2. 警告 — 子どもの危険行動を短く伝える
  3. 賞賛 — 良い対応をさりげなく褒める
  4. 見守り — 問題なければ黙る

「さりげなく褒める」がポイントで、大げさに褒めるより「今の声掛け、いいですね」くらいの方が自然に続けられます。

アドバイスの根拠(evidence)も返させていて、「発達心理学的にこういう効果がある」という裏付けがあると納得感があります。ログに残しておくと後から読み返すのも面白いです。

音声出力

現状はmacOSのsayコマンドで読み上げています。

def speak(text: str) -> None:
    subprocess.run(["say", "-v", "Kyoko", "-r", "200", text])

実装コスト・APIコストがゼロなので、まずはこれで十分でした。音声の品質はそこまで良くないですが、短いアドバイスを読み上げるだけなので実用上は問題ありません。

使い方

cd realtime_advice_app
pip install -r requirements.txt
cp .env.example .env  # GEMINI_API_KEY を設定
make start

オプション:

  • --duration 30 — 録画秒数
  • --silence-threshold -50.0 — 無音判定の閾値(dB)
  • --interval 0 — サイクル間の追加待機秒数

今後の展望

v2: Multimodal Live APIでリアルタイム化

現在のv1は30秒のバッチ処理なので、どうしてもレスポンスに遅延があります。子どもが危ないことをしている場面で30秒待つのは実用的ではないです。

v2ではGoogleのMultimodal Live API(WebSocketベースの双方向ストリーミング)を使って、1〜2秒のレスポンスを目指しています。

主な変更点:

  • ffmpegによるバッチ録画 → OpenCVで1〜2fpsのフレーム送信
  • macOSのsayコマンド → PyAudioによるストリーム再生
  • Barge-in対応 — 親が話し始めたらAIが即座に発話を停止する

特にBarge-inは大きくて、v1だとAIが喋っている最中は状況が変わっても30秒待つしかないのが課題でした。v2では親が話し始めた瞬間にAIが黙って聞き役に回る設計にする予定です。

ウェアラブルデバイスとの統合

さらに先の話ですが、個人的に楽しみにしているのがウェアラブルデバイスとの統合です。

GoogleのProject Aura(Xrealと共同開発のAIグラス、2026年発売予定)は、カメラとマイクを搭載したメガネ型デバイスで、Android XRアプリが動作します。Project Astraの技術(低レイテンシのマルチモーダルAI、空間認識、プロアクティブな応答)と組み合わせると、今回作ったような「カメラで状況を見て音声でアドバイスする」というパイプラインが、デバイスネイティブでサポートされる可能性が高いと思っています。

今はMacの前にいるときしか使えませんが、メガネのカメラから親の視界をそのまま送れるようになれば、リビングでもキッチンでも公園でも動く育児コーチになります。「子どもが手を伸ばしている物体を空間認識で特定して警告する」みたいなことも、技術的にはもう射程圏内です。

自分で1からパイプラインを組まなくても、プラットフォーム側がマルチモーダルAI+ウェアラブルの統合を進めてくれるのはありがたいし、その上でドメイン固有のプロンプトや体験を作り込むことに集中できる時代が来そうで楽しみです。

まとめ

  • Geminiの動画理解 + JSON構造化出力は思った以上に実用的だった
  • ffmpegのvolumedetectによる無音スキップでAPIコストを半分以下に削減
  • 映像と音声の同期(avfoundationの96kHz問題回避 + オフセット補正)は地味にハマるポイント
  • 「必要なときだけ話す」設計が一番大事
  • プラットフォーム側の進化(Project Aura / Astra)でこの手のアプリはネイティブにサポートされていきそう

Discussion