個人開発で挑む「完全ローカルAIアシスタント」3週目ーほぼ完成
Kaiエコシステムと「Kai Lite」
「Kaiエコシステム」は、
日々の創造・暮らし・学びを“自分らしいリズム”で支え、
プライバシーとデータ主権を守ることをテーマにした
ローカルAI&音声アシスタントのプロジェクトです。
多くのAIサービスがクラウド依存・データ収集型へ傾く中、
Kaiは「あなたのデータはあなたのもの」を絶対の約束としています。
本記事でご紹介する Kai Lite は、
このKaiエコシステムの “最小・最速” プロトタイプ。
完全オフライン&プライバシーファーストを徹底し、
「本当に“自分のため”のAIアプリ」として
日々の生活や創造を静かにサポートします。
実はこのアプリ、完全に自分用として作っています。
「大切なリマインダーやメモを誰にも見られず、端末一台だけでコントロールしたい」
そんなごく素朴な願いから生まれたアプリです。
Kai Lite:5つの要点
-
プライバシーファースト音声アシスタント
完全オフライン動作、クラウド共有ゼロ。すべてのデータが端末内にとどまります。 -
自然な音声コマンド
音声認識+パターン解析で、リマインダー・メモ追加やカレンダー確認が可能。 -
ローカルファーストアーキテクチャ
SQLiteストレージ+Flutter製アプリ。機内モードでも動き、インターネット不要。 -
ユーザーデータの完全管理
いつでもデータをエクスポート/削除。マイクON時は分かりやすいビジュアルで通知。 -
将来のKaiエコシステム基盤
プライバシーとユーザーコントロールを守りつつ、Kai Laptop/Desktopとの同期も見据えて設計。
今週の「AI協力者」体験記
Claudeは、構築全体で頼もしい実装パートナーでした。
設計の意思決定から正規表現パターンのデバッグまで、
技術的な壁にぶつかるたびにアイデアをくれ、
実装サイクルの加速に大いに役立ってくれました。
実際に作ったもの(リアルな記録)
試行1:「Alexa級の音声コマンドを目指して」
目標は「完全ローカルでAlexa並みの快適音声コマンド」。
標準的なFlutter音声ライブラリから着手:
dependencies:
speech\_to\_text: ^6.3.0
flutter\_tts: ^3.8.3
permission\_handler: ^11.0.1
音声サービスの基本構造:
class VoiceService {
final SpeechToText _speech = SpeechToText();
final FlutterTts _tts = FlutterTts();
Future<void> initialize() async {
await _speech.initialize();
// Kaiの落ち着いた音声設定
await _tts.setSpeechRate(0.9);
await _tts.setPitch(1.0);
}
}
現実チェック:
丸一日テストした結果、“onDevice: true”でも「Alexa級」の体験には精度と一貫性が大きく不足していると痛感。
結論: 根本的に別アプローチが必要、と判断。
試行2:パターンベース音声コマンド解析に大転換
Claudeのアドバイスで「ミニAlexa」ではなく“パターン解析に特化”する方向にシフト。
VoiceCommandParserを設計し、さまざまな話し方に対応する正規表現をAIに生成してもらいました。
class VoiceCommandParser {
static final Map<String, List<RegExp>> patterns = {
'calendar_add': [
RegExp(r'remind me to (.*?) at (.*)'),
RegExp(r'add (.*?) to calendar at (.*)'),
RegExp(r'schedule (.*?) for (.*)'),
RegExp(r'set reminder (.*?) at (.*)'),
RegExp(r'(.*?) at (.*?) today'),
RegExp(r'(.*?) at (.*?) tomorrow'),
],
'calendar_check': [
RegExp(r"what'?s on my calendar\??"),
RegExp(r"what do i have today\??"),
RegExp(r"show my schedule"),
RegExp(r"any events today\??"),
],
'memo_add': [
RegExp(r'note to self[,:]? (.*)'),
RegExp(r'remember that (.*)'),
RegExp(r'make a note[,:]? (.*)'),
RegExp(r'write down (.*)'),
],
};
static VoiceCommand parse(String input) {
input = input.toLowerCase().trim();
// 各パターンカテゴリをチェック
for (final entry in patterns.entries) {
final intent = entry.key;
final patternList = entry.value;
for (final pattern in patternList) {
final match = pattern.firstMatch(input);
if (match != null) {
return _extractCommand(intent, input, match);
}
}
}
// ファジーマッチングのフォールバック
return _fuzzyMatch(input);
}
}
スマート時間解析も追加
static String? _parseTime(String timeStr) {
// 自然言語から時間へ変換
final conversions = {
'morning': '9:00 AM',
'afternoon': '2:00 PM',
'evening': '6:00 PM',
'night': '9:00 PM',
'noon': '12:00 PM',
'midnight': '12:00 AM',
};
// まず自然言語をチェック
for (final entry in conversions.entries) {
if (timeStr.contains(entry.key)) {
return entry.value;
}
}
// 時刻パターンを解析(例:3pm、3:30pm、15:00)
final timeMatch = RegExp(r'(\d{1,2})(?::(\d{2}))?\s*(am|pm)?', caseSensitive: false).firstMatch(timeStr);
if (timeMatch != null) {
var hour = int.parse(timeMatch.group(1) ?? '0');
final minute = timeMatch.group(2) ?? '00';
var ampm = timeMatch.group(3)?.toUpperCase();
// あいまいな時間の自動補完
if (ampm == null) {
if (hour >= 7 && hour <= 11) {
ampm = 'AM';
} else if (hour >= 1 && hour <= 6) {
ampm = 'PM';
} else if (hour >= 13 && hour <= 23) {
hour = hour - 12;
ampm = 'PM';
}
}
return '${hour}:${minute} ${ampm}';
}
return null;
}
マルチターン会話ハンドラー(AI協力で構築)
class ConversationHandler {
ConversationContext _context = ConversationContext();
Future<void> handleCommand(String input) async {
final command = VoiceCommandParser.parse(input);
if (command.confidence < 0.7) {
await _voice.speak("うまく認識できませんでした。カレンダーイベントを追加するか、メモを作成したいですか?");
return;
}
// 不足情報を順次補完
if (command.intent == 'calendar_add') {
if (command.title == null) {
_context.state = ConversationState.waitingForTitle;
await _voice.speak("何についてリマインドしますか?");
return;
}
if (command.time == null) {
_context.state = ConversationState.waitingForTime;
await _voice.speak("何時にリマインダーを設定しますか?");
return;
}
await _createCalendarEvent(command);
}
}
}
実例とパフォーマンス
- 認識精度: サポート対象パターンで90%以上
- 応答速度: 300ms未満
- メモリ消費: 45MB前後
- バッテリー消費: 1日使って2%未満
コマンド例
ユーザー「明日3時にママに電話するリマインダー」
↓
音声認識 → パターンマッチ → 時刻・日付解析 → タスク登録(SQLite) → TTSで応答
プライバシー設計と実装
「データは絶対に端末から出ない」ことをどう証明するか?
Claudeと協力し、「約束」ではなく見える化&即アクションできる設計にしました。
1. 明快なビジュアルインジケーター
マイクがONのとき、Kaiバブルがパルス表示
AnimatedContainer(
duration: Duration(milliseconds: 300),
decoration: BoxDecoration(
color: _isListening ? Color(0xFF9C7BD9).withOpacity(0.8) : Color(0xFF9C7BD9).withOpacity(0.2),
shape: BoxShape.circle,
),
)
2. ワンタップでデータエクスポート
いつでも全データをJSON形式でエクスポート可能
class DataExportService {
Future<String> exportAllUserData() async {
final tasks = await CalendarService().getAllTasks();
final memos = await MemoService().getAllMemos();
return jsonEncode({
'export_date': DateTime.now().toIso8601String(),
'tasks': tasks.map((t) => t.toMap()).toList(),
'memos': memos.map((m) => m.toMap()).toList(),
});
}
}
3. ワンタップで全データ削除
Future<void> deleteAllUserData() async {
await CalendarService().clearAllTasks();
await MemoService().clearAllMemos();
await SharedPreferences.getInstance().then((prefs) => prefs.clear());
// 確認表示:「すべてのデータが削除されました」
}
気づき:
テストでは「音声精度」よりも“データをエクスポート/削除できる安心感”が大切だと感じました。
コントロール権が自分にあるという実感が信頼につながりました。
実際にオフラインで機能するデータベース設計
AIの力も借りつつ、将来のデバイス同期も想定したSQLite設計:
class Task {
final String id;
final String title;
final DateTime? date;
final String? time;
final bool isCompleted;
// 将来の同期用
final DateTime lastModified;
final String sourceDevice;
final String status; // 'active' | 'deleted'
Task({
required this.id,
required this.title,
this.date,
this.time,
this.isCompleted = false,
required this.lastModified,
this.sourceDevice = 'kai-lite-android',
this.status = 'active',
});
}
なぜこれが機能するか:
- すべて即座にオフライン動作
- クロスデバイス同期向けの設計済み
- ソフト削除&復旧が簡単
- デバイスごとのデータ管理が可能
パフォーマンスデバッグ(学びの記録)
- 問題1:音声処理中のメモリリーク
→ Claudeがdispose漏れを指摘し、適切に廃棄処理を実装
void dispose() {
_speech.stop(); // これを追加
_speech.cancel(); // これも追加
super.dispose();
}
- 問題2:オーバーレイでバッテリードレイン
→ 通話中などは自動で非表示に
void _hideOverlayDuringCalls() {
if (_phoneStateService.isInCall()) {
_overlay.hide();
}
}
- 問題3:大量タスク時のSQLiteパフォーマンス
→ Claudeの提案で複合インデックス追加
await db.execute('''
CREATE INDEX IF NOT EXISTS idx_task_date_status
ON tasks(date, status)
''');
学びと洞察まとめ
技術面:
- SQLiteはモバイル用途でも十分高速
- ユースケースに特化すればローカル音声処理も現実的
- コマンド解析にはパターンマッチングがAIモデルを上回ることも
- Flutterのオーバーレイ管理は要注意
UX面:
- プライバシーは“守り”ではなく“自分を力づけるもの”であるべき
- 視覚的なフィードバックは言葉以上に信頼を生む
- シンプルかつ信頼性の高いコマンドが一番スムーズ
アーキテクチャ面:
- AI協力で開発サイクルが加速する
- 実際のユーザーテストでしか見えない課題も多い
- AIは設計や最適化に強く、人間はUX直感でリードできる
現在のKai Liteの状態
- 15種類以上の音声コマンドパターンが確実に動作
- 完全オフライン(ネット不要、機内モードOK)
- データのエクスポート・削除で“完全所有”を実現
- 300ms未満の音声応答
- 技術スタック:Flutter 3.x/SQLite/speech_to_text(ローカル)/パターンマッチング
おわりに
Kai Liteは「自分専用AIアシスタント」として、現時点でローカル完結型の実装まで到達しました。
今後はKai Laptopの開発に進み、API連携やデバイス間同期といったKaiエコシステムの本格連携も計画しています。
ただし、それは次のチャレンジ――
いったんここで区切りとし、この“等身大の一歩”をまとめておきます。
Discussion