Initial commit: Voice controlled device application, specs and Gitea Actions workflow

This commit is contained in:
2026-08-10 08:26:00 +00:00
commit d449469116
69 changed files with 3209 additions and 0 deletions
+277
View File
@@ -0,0 +1,277 @@
# **システム機能・構成仕様書: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へ安全にフォールバックすること。