2026-01-26 バックグラウンド通知設計
検討: バックグラウンド通知設計(Gemini Live + Web Notification)
背景
Gemini Live APIを使った音声UI実装において、ユーザーが別アプリを使用中でもタスク完了を確実に通知する必要がある。
利用シーン
[音声指示 30秒] → [待機 5-30分] → [完了通知] → [確認/次の指示]
↓
他のアプリで作業中
エディタ使用中
ブラウザがバックグラウンド
採用方針: Web Notification + 効果音 + Gemini Live音声
基本フロー
[バックグラウンドでタスク完了]
↓
🔔 Web Notification: 「Claude Code - タスク完了」
🔊 効果音: ピロン♪(短い通知音)
↓
[ユーザーが通知をクリック]
↓
[アプリ画面にフォーカス]
↓
🎤 Gemini Live: 詳細を音声で説明
「実装が完了しました。backend/internal/service/claude.goに
新しいストリーミング機能を追加し、3つのテストケースを
追加しています。ビルドとテストは全て成功しました。」
通知タイミングの方針
| イベント | Web通知 | 効果音 | Gemini音声 | 備考 |
|---|---|---|---|---|
| 14分再接続 | ❌ | ❌ | ❌ | 静かに実行(技術的制約なのでユーザーは気にしなくていい) |
| タスク完了 | ✅ | ✅ | ✅ | フォーカス後に音声説明 |
| エラー発生 | ✅ | ✅ | ✅ | フォーカス後に音声説明 |
実装設計
1. Web Notification API の実装
許可リクエスト
// frontend/src/hooks/useNotification.ts
export function useNotification() {
const [permission, setPermission] = useState(Notification.permission);
// アプリ起動時に許可をリクエスト
useEffect(() => {
if (permission === "default") {
Notification.requestPermission().then(setPermission);
}
}, []);
return { permission };
}
通知表示
// frontend/src/lib/notification.ts
interface NotificationOptions {
title: string;
body: string;
icon?: string;
soundUrl?: string; // 効果音のURL
onFocus?: () => void; // フォーカス時のコールバック
}
export async function showNotificationWithSound(options: NotificationOptions) {
const { title, body, icon = "/icon.png", soundUrl, onFocus } = options;
// 1. 許可チェック
if (Notification.permission !== "granted") {
console.warn("Notification permission not granted");
return;
}
// 2. ブラウザ通知を表示
const notification = new Notification(title, {
body,
icon,
requireInteraction: true, // クリックまで消えない
tag: "claude-code-task", // 重複防止
silent: true // ブラウザのデフォルト音を消す
});
// 3. 効果音を再生
if (soundUrl) {
try {
const audio = new Audio(soundUrl);
await audio.play();
} catch (error) {
console.error("Failed to play notification sound:", error);
}
}
// 4. クリックでアプリにフォーカス
notification.onclick = () => {
window.focus();
notification.close();
onFocus?.(); // Gemini音声のトリガー
};
// 5. 自動クローズ(30秒後)
setTimeout(() => notification.close(), 30000);
}
2. Gemini Live 音声説明の実装
タスク完了時の音声生成
// frontend/src/hooks/useGeminiLive.ts
interface TaskCompletionInfo {
status: "success" | "error";
summary: string; // バックエンドから受け取る概要
details?: {
filesChanged: string[];
testsRun?: number;
testsPassed?: number;
errorMessage?: string;
};
}
export function useGeminiLive() {
const [ws, setWs] = useState<WebSocket | null>(null);
const [isConnected, setIsConnected] = useState(false);
// タスク完了時に呼び出す
async function announceTaskCompletion(info: TaskCompletionInfo) {
if (!ws || !isConnected) {
console.warn("Gemini Live not connected");
return;
}
// システム指示に完了情報を含めて音声生成
const prompt = buildCompletionPrompt(info);
// Gemini Liveに送信
ws.send(JSON.stringify({
client_content: {
turns: [{
role: "user",
parts: [{ text: prompt }]
}]
}
}));
}
return {
announceTaskCompletion,
isConnected
};
}
function buildCompletionPrompt(info: TaskCompletionInfo): string {
if (info.status === "success") {
return `
タスクが完了しました。以下の内容を簡潔に音声で説明してください。
概要: ${info.summary}
変更ファイル:
${info.details?.filesChanged.map(f => `- ${f}`).join('\n')}
${info.details?.testsRun ? `テスト実行: ${info.details.testsPassed}/${info.details.testsRun} 成功` : ''}
30秒以内で、ユーザーが次に何をすべきかを含めて説明してください。
`;
} else {
return `
エラーが発生しました。以下の内容を簡潔に音声で説明してください。
エラー概要: ${info.summary}
${info.details?.errorMessage ? `詳細: ${info.details.errorMessage}` : ''}
30秒以内で、解決方法の提案を含めて説明してください。
`;
}
}
3. 統合実装
page.tsx での統合
// frontend/src/app/page.tsx
export default function Home() {
const { permission } = useNotification();
const { announceTaskCompletion, isConnected } = useGeminiLive();
const [shouldAnnounce, setShouldAnnounce] = useState<TaskCompletionInfo | null>(null);
// SSEストリームの完了イベント
const handleStreamComplete = useCallback((result: TaskCompletionInfo) => {
// 1. Web通知 + 効果音
showNotificationWithSound({
title: "Claude Code",
body: result.status === "success"
? "タスクが完了しました"
: "エラーが発生しました",
soundUrl: result.status === "success"
? "/notification-success.mp3"
: "/notification-error.mp3",
onFocus: () => {
// 2. フォーカス後にGemini音声をトリガー
setShouldAnnounce(result);
}
});
}, []);
// アプリにフォーカスが戻ったらGemini音声を再生
useEffect(() => {
if (shouldAnnounce && isConnected) {
announceTaskCompletion(shouldAnnounce);
setShouldAnnounce(null);
}
}, [shouldAnnounce, isConnected]);
return (
<div>
{/* 通知許可のリクエストUI */}
{permission === "default" && (
<div className="notification-banner">
タスク完了通知を受け取るには、ブラウザの通知を許可してください。
</div>
)}
{/* 既存のUI */}
</div>
);
}
4. バックエンドからの完了情報
SSEイベントに完了情報を追加
// backend/internal/service/claude.go
type TaskCompletionInfo struct {
Status string `json:"status"` // "success" or "error"
Summary string `json:"summary"`
FilesChanged []string `json:"files_changed,omitempty"`
TestsRun int `json:"tests_run,omitempty"`
TestsPassed int `json:"tests_passed,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
type StreamEvent struct {
Type string `json:"type"`
SessionID string `json:"session_id"`
Message string `json:"message"`
Completion *TaskCompletionInfo `json:"completion,omitempty"` // 追加
}
func (s *ClaudeService) StreamCommand(ctx context.Context, input string) (<-chan StreamEvent, error) {
eventCh := make(chan StreamEvent)
go func() {
defer close(eventCh)
// Claude CLI実行
output, err := s.executeClaude(ctx, input)
if err != nil {
// エラー時
eventCh <- StreamEvent{
Type: "completion",
Message: "Error occurred",
Completion: &TaskCompletionInfo{
Status: "error",
Summary: parseErrorSummary(err),
ErrorMessage: err.Error(),
},
}
return
}
// 成功時
eventCh <- StreamEvent{
Type: "completion",
Message: "Task completed",
Completion: &TaskCompletionInfo{
Status: "success",
Summary: parseSuccessSummary(output),
FilesChanged: extractChangedFiles(output),
TestsRun: extractTestsRun(output),
TestsPassed: extractTestsPassed(output),
},
}
}()
return eventCh, nil
}
ブラウザ制限への対応
バックグラウンドでの音声再生制限
問題点:
- Chrome/Safari: タブがバックグラウンドの場合、音声再生が制限される
- 解決策: 通知クリック後に音声を再生(採用方針)
実装のポイント:
// ❌ バックグラウンドで音声再生(失敗する可能性)
showNotification();
playGeminiAudio(); // 制限される
// ✅ フォーカス後に音声再生(確実)
showNotification({
onFocus: () => playGeminiAudio() // ユーザーアクション後なので再生可能
});
効果音ファイルの準備
推奨仕様
| 項目 | 値 |
|---|---|
| フォーマット | MP3 |
| サイズ | 10KB以下 |
| 長さ | 0.5-1秒 |
| 配置場所 | /public/notification-success.mp3 |
/public/notification-error.mp3 |
無料素材の例
- 効果音ラボ: https://soundeffect-lab.info/
- Zapsplat (要登録): https://www.zapsplat.com/
推奨音:
- 成功: 明るいピロン音、ベル音
- エラー: 控えめなビープ音
実装の優先順位
フェーズ1: 基本通知(MVP)
- Web Notification API の実装
- 効果音の再生
- 許可リクエストUI
完成イメージ:
[バックグラウンド]
🔔 「タスク完了」
🔊 ピロン♪
フェーズ2: Gemini Live 統合
- Gemini Live WebSocket接続
- タスク完了時の音声生成
- フォーカス後の自動再生
完成イメージ:
[通知クリック]
↓
🎤 「実装が完了しました。3つのファイルを変更しています...」
フェーズ3: 高度な機能
- 音声内容のカスタマイズ
- 通知履歴の管理
- 通知設定のカスタマイズUI
トレードオフ
メリット
-
確実な通知
- Web Notificationで視覚的に通知
- 効果音で聴覚的にも通知
- ブラウザ標準機能で安定動作
-
バックグラウンド制限を回避
- フォーカス後にGemini音声再生
- ユーザーアクション後なので確実
-
段階的実装が可能
- フェーズ1だけでも価値がある
- Gemini Liveは後から追加可能
デメリット
-
初回許可が必要
- ユーザーが通知を許可しないと動作しない
- 許可リクエストUIが必要
-
タブを閉じると動作しない
- ブラウザタブが開いている必要がある
- → 実用上は問題ない(アプリを使用中)
-
音声は通知クリック後
- 通知を無視すると音声が流れない
- → 効果音で気づかせるので軽減
セキュリティとプライバシー
通知内容の制限
通知テキスト:
- 機密情報を含めない
- 「タスク完了」「エラー発生」のみ
音声説明:
- アプリにフォーカス後のみ再生
- ファイル名や詳細はアプリ画面内で表示
次のステップ
1. プロトタイプ実装(フェーズ1)
-
useNotificationフックの実装 -
showNotificationWithSound関数の実装 - 効果音ファイルの配置
- page.tsx での統合
2. バックエンド対応
-
StreamEventにCompletionフィールド追加 - ログからファイル変更を抽出
- テスト結果を抽出
3. Gemini Live 統合(フェーズ2)
- WebSocket接続の実装
- 音声生成プロンプトの作成
- フォーカス後の自動再生
参考資料
開発/検討中/2026-01-26_gemini-live接続時間制限対応.md- Gemini Live実装計画開発/資料/2026-01-25_gemini-live-function-calling.md- Gemini Live API調査- Web Notification API: https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
- Web Audio API: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API