🤖

個人開発で挑む「完全ローカルAIアシスタント」3週目ーほぼ完成

に公開

Kaiエコシステムと「Kai Lite」

Kaiエコシステム」は、
日々の創造・暮らし・学びを“自分らしいリズム”で支え、
プライバシーとデータ主権を守ることをテーマにした
ローカルAI&音声アシスタントのプロジェクトです。

多くのAIサービスがクラウド依存・データ収集型へ傾く中、
Kaiは「あなたのデータはあなたのもの」を絶対の約束としています。

https://youtube.com/shorts/u6rP2D4xVG8?feature=share

本記事でご紹介する 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