Add OpenClaw wake word support and README

This commit is contained in:
Yu-ga
2026-01-31 09:29:02 +09:00
parent 0086f54589
commit 2af34e028e
5 changed files with 198 additions and 19 deletions
+87
View File
@@ -0,0 +1,87 @@
# OpenClaw Assistant
OpenClaw専用のAndroid音声アシスタントアプリ。
## 機能
- 🎤 **ウェイクワード「OpenClaw」** - 音声だけで起動
- 🏠 **ホームボタン長押し** - システムアシスタントとして動作
- 🔄 **連続会話** - セッションを維持して自然な対話
- 🔒 **プライバシー重視** - 設定は暗号化保存
## セットアップ
### 1. アプリのインストール
Android Studioでビルドするか、Releasesからapkをダウンロード。
### 2. 設定
1. アプリを開く
2. 右上の⚙️から設定画面へ
3. 以下を入力:
- **Webhook URL** (必須): OpenClawのエンドポイント
- **認証トークン** (任意): Bearer認証用
- **Picovoice Access Key**: https://console.picovoice.ai で無料取得
### 3. ウェイクワード「OpenClaw」の設定
1. [Picovoice Console](https://console.picovoice.ai) にログイン
2. **Porcupine****Custom Keywords**
3. 「OpenClaw」と入力してキーワード作成
4. Android用の `.ppn` ファイルをダウンロード
5. `app/src/main/assets/openclaw_android.ppn` として配置
6. アプリを再ビルド
### 4. システムアシスタントとして設定(任意)
1. 端末の設定 → アプリ → デフォルトアプリ → アシスタントアプリ
2. 「OpenClaw Assistant」を選択
3. ホームボタン長押しで起動可能に
## OpenClaw側の設定
### リクエスト形式
```json
POST /your-webhook-endpoint
Content-Type: application/json
Authorization: Bearer <token>
{
"message": "ユーザーの発話テキスト",
"session_id": "uuid-xxx-xxx",
"user_id": "optional"
}
```
### レスポンス形式
以下のいずれかの形式でOK
```json
{"response": "応答テキスト"}
{"text": "応答テキスト"}
{"message": "応答テキスト"}
```
## 技術スタック
- Kotlin + Jetpack Compose
- VoiceInteractionService
- Picovoice Porcupine (ホットワード検知)
- Android SpeechRecognizer
- TextToSpeech
- OkHttp + Gson
- EncryptedSharedPreferences
## 必要な権限
- `RECORD_AUDIO` - 音声認識
- `INTERNET` - API通信
- `FOREGROUND_SERVICE` - 常時聴取
- `POST_NOTIFICATIONS` - 通知表示
## ライセンス
MIT License
+19
View File
@@ -0,0 +1,19 @@
# OpenClaw Wake Word File
このフォルダに `openclaw_android.ppn` ファイルを配置してください。
## 作成方法
1. https://console.picovoice.ai にアクセス
2. アカウント作成(無料)
3. **Porcupine****Custom Keywords** を選択
4. 「OpenClaw」と入力してキーワードを作成
5. **Android** プラットフォームを選択
6. `.ppn` ファイルをダウンロード
7. ファイル名を `openclaw_android.ppn` に変更
8. このフォルダに配置
## 注意
- カスタムキーワードファイルがない場合、フォールバックとして「Porcupine」が使用されます
- Picovoice Access Keyは別途設定画面で入力が必要です
@@ -179,8 +179,8 @@ fun MainScreen(
// ホットワード
ActionCard(
icon = Icons.Default.Mic,
title = "ウェイクワード「Porcupine",
description = if (hotwordEnabled) "常時聴取中" else "タップで有効化",
title = "ウェイクワード「OpenClaw",
description = if (hotwordEnabled) "「OpenClaw」と呼んでください" else "タップで有効化",
showSwitch = true,
switchValue = hotwordEnabled,
onSwitchChange = { enabled ->
@@ -21,10 +21,12 @@ import com.openclaw.assistant.speech.TTSManager
import ai.picovoice.porcupine.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.collectLatest
import java.io.File
import java.io.FileOutputStream
/**
* ホットワード検知サービス
* バックグラウンドで常時聴取し、ウェイクワードを検知したら音声認識開始
* バックグラウンドで常時聴取し、ウェイクワード「OpenClaw」を検知したら音声認識開始
*/
class HotwordService : Service() {
@@ -32,6 +34,9 @@ class HotwordService : Service() {
private const val TAG = "HotwordService"
private const val NOTIFICATION_ID = 1001
private const val CHANNEL_ID = "hotword_channel"
// カスタムウェイクワードファイル名(assetsに配置)
private const val WAKE_WORD_FILE = "openclaw_android.ppn"
fun start(context: Context) {
val intent = Intent(context, HotwordService::class.java)
@@ -114,7 +119,7 @@ class HotwordService : Service() {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(getString(R.string.notification_title))
.setContentText(getString(R.string.notification_text))
.setContentText("「OpenClaw」と呼んでください")
.setSmallIcon(R.drawable.ic_mic)
.setContentIntent(pendingIntent)
.setOngoing(true)
@@ -122,6 +127,33 @@ class HotwordService : Service() {
.build()
}
/**
* カスタムキーワードファイルをassetsから内部ストレージにコピー
*/
private fun copyKeywordFile(): String? {
return try {
val outputFile = File(filesDir, WAKE_WORD_FILE)
// 既にコピー済みならスキップ
if (outputFile.exists()) {
return outputFile.absolutePath
}
// assetsからコピー
assets.open(WAKE_WORD_FILE).use { input ->
FileOutputStream(outputFile).use { output ->
input.copyTo(output)
}
}
Log.d(TAG, "Keyword file copied to: ${outputFile.absolutePath}")
outputFile.absolutePath
} catch (e: Exception) {
Log.e(TAG, "Failed to copy keyword file", e)
null
}
}
private fun startHotwordDetection() {
val accessKey = settings.picovoiceAccessKey
if (accessKey.isBlank()) {
@@ -131,23 +163,54 @@ class HotwordService : Service() {
}
try {
// Porcupine(ホットワード検知エンジン)を初期化
// 組み込みキーワード "Hey Google" の代わりに "Porcupine" を使用
// カスタムキーワードを使う場合は .ppn ファイルをassetsに配置
porcupineManager = PorcupineManager.Builder()
// カスタムキーワードファイルをコピー
val keywordPath = copyKeywordFile()
val builder = PorcupineManager.Builder()
.setAccessKey(accessKey)
.setKeyword(Porcupine.BuiltInKeyword.PORCUPINE) // "Porcupine" がウェイクワード
.setSensitivity(0.7f)
.build(this) { keywordIndex ->
Log.d(TAG, "Hotword detected! Index: $keywordIndex")
onHotwordDetected()
}
if (keywordPath != null) {
// カスタムキーワード「OpenClaw」を使用
Log.d(TAG, "Using custom keyword: OpenClaw")
builder.setKeywordPath(keywordPath)
} else {
// フォールバック: ビルトインキーワード「Porcupine」を使用
Log.w(TAG, "Custom keyword not found, falling back to 'Porcupine'")
builder.setKeyword(Porcupine.BuiltInKeyword.PORCUPINE)
}
porcupineManager = builder.build(this) { keywordIndex ->
Log.d(TAG, "Hotword 'OpenClaw' detected!")
onHotwordDetected()
}
porcupineManager?.start()
Log.d(TAG, "Hotword detection started")
} catch (e: PorcupineException) {
Log.e(TAG, "Failed to start Porcupine", e)
// カスタムキーワードで失敗した場合、ビルトインで再試行
tryFallbackKeyword(accessKey)
}
}
private fun tryFallbackKeyword(accessKey: String) {
try {
Log.d(TAG, "Trying fallback keyword 'Porcupine'")
porcupineManager = PorcupineManager.Builder()
.setAccessKey(accessKey)
.setKeyword(Porcupine.BuiltInKeyword.PORCUPINE)
.setSensitivity(0.7f)
.build(this) { keywordIndex ->
Log.d(TAG, "Hotword detected (fallback)!")
onHotwordDetected()
}
porcupineManager?.start()
Log.d(TAG, "Fallback hotword detection started")
} catch (e: PorcupineException) {
Log.e(TAG, "Fallback also failed", e)
stopSelf()
}
}
@@ -169,13 +232,21 @@ class HotwordService : Service() {
// ホットワード検知を一時停止
porcupineManager?.stop()
// 確認音(オプション)
// playConfirmationSound()
// 確認音を鳴らす(オプション)
playConfirmationBeep()
// 音声認識開始
startCommandListening()
}
private fun playConfirmationBeep() {
// 短いビープ音で応答(TTSで代用)
scope.launch {
// 「はい」と短く応答
ttsManager.speak("はい")
}
}
private fun startCommandListening() {
scope.launch {
var recognizedText: String? = null
@@ -206,8 +277,10 @@ class HotwordService : Service() {
private fun sendToOpenClaw(message: String) {
if (!settings.isConfigured()) {
Log.e(TAG, "Webhook not configured")
ttsManager.speakQueued("設定が必要です")
resumeHotwordDetection()
scope.launch {
ttsManager.speak("設定が必要です")
resumeHotwordDetection()
}
return
}
@@ -230,7 +303,7 @@ class HotwordService : Service() {
},
onFailure = { error ->
Log.e(TAG, "API error", error)
ttsManager.speakQueued("エラーが発生しました")
ttsManager.speak("エラーが発生しました")
resumeHotwordDetection()
}
)
+1 -1
View File
@@ -3,7 +3,7 @@
<string name="notification_channel_name">Hotword Detection</string>
<string name="notification_channel_description">常時音声検知サービス</string>
<string name="notification_title">OpenClaw Assistant</string>
<string name="notification_text">ウェイクワードを待機中…</string>
<string name="notification_text">「OpenClaw」と呼んでください</string>
<string name="settings_title">設定</string>
<string name="webhook_url_label">Webhook URL</string>
<string name="webhook_url_hint">https://your-openclaw-endpoint.com/webhook</string>