Files

278 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# **システム機能・構成仕様書:Android Dockerビルド&Gitea CI/CD環境およびAI音声アシスタントアプリ (改訂完全統合版)**
## **1. システム概要および運用モデル**
### **1.1 背景と目的**
本仕様書は、Android OS 8.0+ (API Level 26 以上 / Target API Level 34) を対象とした「ハンズフリーAI音声ナビゲーション連携アシスタントアプリ」の設計要件、およびDockerコンテナとGitea Runner (Act Runner) を活用した自動ビルド・CI/CDパイプライン構築要件を統合定義する。
運転中や作業中の手動操作を排除し、Bluetoothヘッドセットや画面タップによる音声入力、AIサーバーとの連携(JSON API)、TTS音声応答(リアクション)、ならびに NaviCon / Google Maps 経由での車載ナビ目的地自動転送を安全かつ低コストで実現する。
### **1.2 システム運用・開発モデル**
1. **ローカル開発環境**: Docker Compose を用いて、ホスト環境に依存せずコンテナ内で Gradle ビルドおよび単体テストを実行可能とする。
2. **CI/CD環境**: Gitea への Push / Pull Request をトリガーとして Gitea Runner 上で同一コンテナ環境を起動し、自動テストおよび APK/AAB の生成を行う。
3. **エミュレータ・実機検証モデル**: Android Studio エミュレータ(Android 8.0〜14)での仮想動作確認を標準とし、最終動作を Bluetooth ヘッドセット(エレコム LBT-HSC41BK-EC 等)および車載ナビ連携環境にて検証する。
## **2. システム要件および技術スタック**
### **2.1 動作環境・開発基盤**
* **ターゲットプラットフォーム**: Linux (x86_64) コンテナ環境(ビルド用)
* **対象端末OS**: Android OS 8.0 (API Level 26) 以上推奨 / Target API Level 34 (Android 14)
* **開発言語・フレームワーク**: Kotlin 1.9+ / Jetpack Compose / Coroutines & Flow / Clean Architecture + MVVM + Hilt
* **コンテナ・CI/CD環境**: Docker Engine 20.10+ / Docker Compose V2 / Gitea Runner (Act Runner)
* **ビルドツール・JDK**: OpenJDK 17 (または AGP 互換 JDK 21) / Gradle (AGP 8.x 対応)
### **2.2 必須権限およびサービス宣言 (AndroidManifest.xml)**
* android.permission.RECORD_AUDIO (マイク音声入力)
* android.permission.INTERNET / ACCESS_NETWORK_STATE (AIサーバー通信・ネットワーク確認)
* android.permission.ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION (GPS現在地情報付与)
* android.permission.MODIFY_AUDIO_SETTINGS (オーディオフォーカス・音量制御)
* android.permission.POST_NOTIFICATIONS (通知出力)
* android.permission.BIND_NOTIFICATION_LISTENER_SERVICE (未読メッセージ・通知本文取得用バインド権限)
* android.permission.SCHEDULE_EXACT_ALARM (アラーム・タイマー設定権限)
* android.permission.FOREGROUND_SERVICE / android.permission.FOREGROUND_SERVICE_MICROPHONE / android.permission.FOREGROUND_SERVICE_LOCATION (Android 14 / API 34 必須フォアグラウンドサービス権限)
## **3. アプリ主要機能仕様**
### **3.1 画面仕様 (Jetpack Compose UI)**
シングルアクティビティ構成のレスポンシブなJetpack Composeレイアウトを採用する。
1. **ステータス表示エリア (Top / Hero Banner)**
* アプリの状態 (Idle, Listening, Processing, Speaking, Error) を色とアイコンで視覚表示。
* Listening 時はマイク入力音量レベル (RmsdB) に同期する波形アニメーション (WaveformIndicator) を描画 (60fps)。
2. **対話・コマンド履歴エリア (Center Scroll Area)**
* ユーザー発話テキストとシステム応答テキスト(目的地情報含む)をタイムスタンプ付きチャット風UI (LazyColumn) で表示。
3. **音声操作コントロールエリア (Bottom Floating Area)**
* マイクボタン (FAB): タップにより音声認識の開始/停止をトグル。
* クリアボタン: 会話履歴のリセット。
4. **パーミッション誘導ダイアログ**
* マイク権限未許可時に許可リクエストを表示。永久拒否時は端末設定画面 (ACTION_APPLICATION_DETAILS_SETTINGS) への移行ダイアログを提示。
### **3.2 音声処理・オーディオ制御仕様**
#### **3.2.1 起動トリガー仕様**
* 常時マイク監視(WakeWord検知)によるバッテリー消費を防ぐため、トリガーを以下に一本化する。
1. 画面上のマイクボタンタップ
2. Bluetoothヘッドセット (LBT-HSC41BK-EC 等) のボタン操作・メディアキーインテント (Intent.ACTION_MEDIA_BUTTON / ACTION_VOICE_COMMAND)
* **バックグラウンド起動制限 (Background Activity Launch restrictions) 対策**:
Android 10+ ではスリープ中・バックグラウンドからのダイレクト Activity 起動が制限されるため、常駐フォアグラウンドサービス (`VoiceAssistantService`) および `MediaSessionCompat` を併用する。Bluetooth メディアボタン押下時は `MediaSessionCompat` の Callback にてイベントを補捉し、フォアグラウンドサービス経由で音声認識および TTS 処理を開始する。
#### **3.2.2 音声認識モジュール (IVoiceRecognizer / VoiceRecognitionManager)**
* **技術方式**: Android標準 android.speech.SpeechRecognizer のラッパーモジュール。
* **インターフェース定義**:
interface IVoiceRecognizer {
val state: StateFlow<VoiceRecognitionState>
val rmsDb: StateFlow<Float>
fun startListening()
fun stopListening()
fun destroy()
}
* **排他制御・フィードバック防止**:
* ITextToSpeech の状態が Speaking の間は startListening() を呼び出し不可とし、スピーカー音声の誤認識ループを防止。
#### **3.2.3 音声合成 (TTS) モジュール (ITextToSpeech / TextToSpeechManager)**
* **技術方式**: Android標準 android.speech.tts.TextToSpeech のラッパーモジュール。
* **状態管理の単一化**:
* 状態変化は val status: StateFlow<TtsStatus> に集約。speak() は Unit を返す。
sealed interface TtsStatus {
object Idle : TtsStatus
object Speaking : TtsStatus
object Completed : TtsStatus
data class Error(val message: String) : TtsStatus
}
interface ITextToSpeech {
val status: StateFlow<TtsStatus>
fun speak(text: String, queueMode: Int = TextToSpeech.QUEUE_FLUSH)
fun stop()
fun destroy()
}
* **オーディオフォーカス制御**:
* speak() 実行時に AudioManager.requestAudioFocus (AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) を呼び出し、他アプリの音量をミュート/減衰。発話完了 (UtteranceProgressListener.onDone) 時に abandonAudioFocus を呼出し解放する。
### **3.3 AIサーバー連携およびコマンドパース仕様**
#### **3.3.1 AIサーバー通信インターフェース仕様 (JSON API)**
* **エンドポイント**: POST /api/v1/navigate
* **認証ヘッダー**: `Authorization: Bearer <API_KEY>` または `X-API-Key: <API_KEY>`
* **通信タイムアウト & リトライ方針**:
* Connect Timeout: 5,000 ms / Read Timeout: 10,000 ms
* ネットワークエラー発生時は Exponential Backoff(初期遅延 1秒、倍率 2、最大 3 回試行)でリトライを実施。
* **リクエスト構造 (App -> Server)**:
```json
{
"user_id": "string",
"query_text": "仕事で行く〇〇会社に設定して",
"current_location": {
"latitude": 35.681236,
"longitude": 139.767125
},
"timestamp": 1700000000
}
```
* **成功レスポンス構造 (Server -> App / HTTP 200)**:
```json
{
"status": "success",
"tts_message": "〇〇会社を目的地にセットします。",
"destination": {
"name": "〇〇会社 本社",
"address": "東京都千代田区丸の内1-1-1",
"latitude": 35.681236,
"longitude": 139.767125
}
}
```
* **エラーレスポンス構造 (Server -> App / HTTP 400, 404, 500)**:
```json
{
"status": "error",
"error_code": "LOCATION_NOT_FOUND",
"message": "該当する目的地を見つけることができませんでした。"
}
```
#### **3.3.2 漢数字・表記揺れコンバータ (KanjiToNumberConverter)**
* 正規表現マッチングの前に、入力テキスト内の漢数字(「一」「二」「三分」「七時」等)および全角数字をアラビア数字(「1」「2」「3分」「7時」)へ事前変換する処理を実行する。
#### **3.3.3 コマンドパターンおよびパラメータ抽出定義**
| コマンド識別子 | 正規表現パターン (前処理後) | 抽出パラメータキー | 実行処理・連携内容 | 音声応答メッセージ例 |
| :---- | :---- | :---- | :---- | :---- |
| NAVIGATE_AI | `.*(ナビ \| 行きたい \| 向かう \| セット).*` | query_text | AIサーバー通信 & NaviCon転送 | tts_message (レスポンス依存) |
| GET_TIME_DATE | `.*(今何時 \| 何時ですか \| 日付 \| 今日は何日).*` | なし | システム日時取得 | 「現在は14時30分です」 |
| SET_ALARM_TIMER | `.*((\d+)\s*(分 \| 秒 \| 時間)タイマー \| (\d+)時にアラーム).*` | timer_val, unit / alarm_hour | アラーム・タイマーインテント | 「5分タイマーをセットしました」 |
| CHANGE_SETTINGS | `.*(音量を \| ダークモード \| ライトモード).*` | setting_type, value | 設定変更 | 「音量を変更しました」 |
| READ_MESSAGES | `.*(メッセージ \| 未読 \| 通知).*` | なし | AppNotificationListenerService 連携 | 「未読メッセージは〇件です」 |
| GET_SYSTEM_INFO | `.*(バッテリー \| 電池 \| Wi-Fi \| ネットワーク).*` | info_type | バッテリー/通信状態取得 | 「バッテリー残量は80%です」 |
| UNKNOWN_FALLBACK | 上記いずれにも未マッチ | なし | ガイダンス応答 | 「申し訳ありません。よく理解できませんでした。」 |
### **3.4 外部ナビ連携仕様 (NaviCon & Google Maps)**
1. **NaviCon連携 (メイン軸 / 完全無料)**:
* AIサーバーから目的地の緯度・経度・地点名を受信後、URLスキームを発行して起動。
* 地点名 (`name`) パラメータは必ず UTF-8 URL エンコード(`URLEncoder.encode(name, "UTF-8")`)を行う。
* フォーマット: `navicon://point?ll={latitude},{longitude}&title={encoded_name}`
2. **Google Maps連携 (フォールバック / 完全無料)**:
* NaviCon未インストール時またはユーザー選択時、標準Intent(`geo:{latitude},{longitude}?q={encoded_name}`)を発行して起動。
3. **未インストール時の自動フォールバック順序**:
* パッケージ管理 (`PackageManager.getPackageInfo("jp.co.denso.navicon.user", 0)`) により NaviCon インストール状態を確認する。
* (1) NaviCon が検出可能 -> NaviCon Intent を発行。
* (2) NaviCon 未検出 且つ Google Maps が利用可能 -> 音声で「NaviConが未検出のため、Google Mapsで表示します」とアナウンスし、Google Maps Intent を発行。
* (3) いずれも未検出時 -> Playストアの NaviCon ページ (`market://details?id=jp.co.denso.navicon.user`) を起動。
### **3.5 未読メッセージ・通知取得サービスおよびフォアグラウンドサービス仕様**
1. **AppNotificationListenerService (未読メッセージ読み上げ)**:
* 他アプリの通知本文を読み上げるため、NotificationListenerService を継承し AndroidManifest.xml に定義する。
2. **VoiceAssistantService (常駐フォアグラウンドサービス)**:
* Android 14 (API 34) 準拠のフォアグラウンドサービスとして定義し、`foregroundServiceType="microphone|location"` を付与する。
* `AndroidManifest.xml` 定義例:
```xml
<service
android:name=".service.VoiceAssistantService"
android:exported="false"
android:foregroundServiceType="microphone|location" />
<service
android:name=".service.AppNotificationListenerService"
android:label="メッセージ読み上げサービス"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
```
## **4. 状態遷移設計 (State Machine Design)**
### **4.1 アプリケーション状態モデル (VoiceAppState)**
sealed interface VoiceAppState {
object Idle : VoiceAppState
data class Listening(val rmsDb: Float = 0f) : VoiceAppState
object Processing : VoiceAppState
data class Speaking(val text: String) : VoiceAppState
data class Error(val message: String, val errorCode: Int? = null) : VoiceAppState
}
### **4.2 状態遷移およびエラー時 3秒自動復帰制御**
* MainViewModel において、VoiceAppState.Error へ遷移した場合は viewModelScope.launch 内で delay(3000) を実行し、ユーザーの手動操作なしで3秒後に自動的に VoiceAppState.Idle へ安全に復帰させる。
stateDiagram-v2
[*] --> Idle
Idle --> Listening : マイクタップ / Bluetoothボタン
Listening --> Processing : 音声入力完了 (onResults)
Listening --> Error : 認識エラー / タイムアウト (onError)
Listening --> Idle : キャンセル操作
Processing --> Speaking : AI応答 / コマンド解析成功 (TTS開始)
Processing --> Error : AI通信エラー / 解析失敗
Speaking --> Idle : TTS再生完了 (onDone)
Speaking --> Error : TTS出力エラー
Error --> Idle : 3秒経過による自動復帰 (delay 3000ms)
## **5. ディレクトリ構造および成果物定義**
### **5.1 プロジェクトディレクトリ構造**
.
├── .gitea/
│ └── workflows/
│ └── build.yaml # Gitea Runner 用 CI/CD ワークフロー定義ファイル
├── docker/
│ ├── Dockerfile # Android ビルド環境定義 Dockerfile (Ubuntu 22.04 + OpenJDK 17 + Android SDK)
│ └── entrypoint.sh # 権限・初期化処理スクリプト
├── app/ # Android アプリソースコード (Kotlin / Jetpack Compose)
│ ├── build.gradle.kts
│ └── src/
│ ├── main/
│ │ ├── java/com/example/voiceapp/
│ │ │ ├── VoiceApplication.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── di/ # Hilt DI モジュール (VoiceModule, DomainModule)
│ │ │ ├── domain/ # UseCases, CommandParser, KanjiToNumberConverter
│ │ │ │ ├── model/ # CommandId, CommandMatchResult, CommandResult
│ │ │ │ ├── executor/ # NavigateAiExecutor, GetTimeDateExecutor, etc.
│ │ │ │ └── converter/ # KanjiToNumberConverter
│ │ │ ├── voice/ # SpeechRecognizer / TTS ラッパー
│ │ │ ├── service/ # VoiceAssistantService, AppNotificationListenerService
│ │ │ └── ui/ # Jetpack Compose UI / ViewModels
│ │ └── AndroidManifest.xml
│ └── test/ # ユニットテスト (JUnit 5, MockK, Turbine)
├── docker-compose.yml # ローカル環境用 Docker 実行定義ファイル
└── README.md # 仕様書・環境構築/実行マニュアル
### **5.2 Gitea CI/CD 実行モデル仕様**
* **Runner 構成**: Gitea Act Runner の Docker Runner モデルを採用。
* **実行環境**: Push / Pull Request 発生時、Runner 上で `docker/Dockerfile` にて生成されるコンテナイメージを起動。
* **自動パイプライン**: コンテナ内で `./gradlew testDebugUnitTest` (単体テスト) および `./gradlew assembleDebug` (Debug APK 生成) を逐次実行し、ビルド成果物をアーティファクトとして保存する。
## **6. 受入・評価基準 (Acceptance Criteria)**
1. **CI/CDパイプラインビルドの成功**: Gitea Runner 上で ./gradlew testDebugUnitTest および ./gradlew assembleDebug が正常終了し、エラーなくAPKが出力されること。
2. **単体テスト網羅性**:
* KanjiToNumberConverterTest: 漢数字(「三分」「7時」「15分」等)の変換正確性を検証。
* CommandParserTest: アラーム、タイマー、NAVIGATE_AI 含む全コマンドの正規表現パラメータ抽出を検証。
* MainViewModelTest: Turbine を用い、正常系フローおよび Error 遷移から3秒後に自動 Idle 復帰する非同期状態遷移を検証。
3. **堅牢性とエラーハンドリング**: モック応答環境において、Listening -> Processing -> Speaking -> Idle への単方向フローが一切クラッシュなく完了し、NaviCon未インストール時もGoogle Mapsへ安全にフォールバックすること。