Voice-to-Agent v1.0` — голосовой ввод в чат
Автор: Сергей Владимирович
Добавляем голосовой ввод в чат — теперь можно просто сказать: «Скажи агенту…» — и ИИ ответит, используя ваш контекст идей. 🎙️ Voice-to-Agent v1.0 — голосовой ввод в чат Теперь в ChatScreen: - 🎤 Кнопка «Говорить» — активирует распознавание речи, - 🧠 Текст отправляется в LLM-бота, - 💬 Ответ озвучивается (TTS), - 🔄 Полностью оффлайн (если модель и голос загружены локально). 🔧 1. Обновление pubspec.yaml — добавим TTS `yaml dependencies: flutter: sdk: flutter speechtotext: ^6.3.0 # уже есть flutter_tts: ^3.6.4 # ← новый: текст в речь # остальные... ` Выполните: `bash flutter pub add flutter_tts ` 🔊 2. services/tts_service.dart — озвучка ответа `dart // services/tts_service.dart import 'package:fluttertts/fluttertts.dart'; class TTSService { final FlutterTts _tts = FlutterTts(); TTSService() { _init(); } Future<void> _init() async { await _tts.setLanguage("ru-RU"); await _tts.setPitch(1.0); await _tts.setSpeechRate(0.45); // медленнее для ясности } Future<void> speak(String text) async { if (text.trim().isNotEmpty) { await _tts.speak(text); } } Future<void> stop() async { await _tts.stop(); } } ` 🎙️ 3. Обновление ChatScreen — голосовой ввод `dart // screens/chat_screen.dart import 'package:speechtotext/speechtotext.dart'; import '../services/tts_service.dart'; class _ChatScreenState extends State<ChatScreen> { final SpeechToText _speech = SpeechToText(); final TTSService _tts = TTSService(); bool _isListening = false; bool _isSpeaking = false; // ... остальные поля Future<void> _listenVoice() async { if (_isListening) { _speech.stopListening(); setState(() => _isListening = false); return; } if (!_speech.isAvailable) { final available = await _speech.initialize(); if (!available) return; } setState(() => _isListening = true); _speech.listen( onResult: (result) async { final text = result.recognizedWords.trim(); if (result.finalResult && text.isNotEmpty) { setState(() => _isListening = false); await _sendMessage(text); // отправить в чат } }, localeId: 'ru_RU', cancelOnError: true, ); } Future<void> _sendMessage([String? text]) async { final message = text ?? _textController.text.trim(); if (message.isEmpty) return; setState(() { _messages.add({'role': 'user', 'content': message}); _isLoading = true; }); _textController.clear(); final response = await _chat.respond(message); setState(() { _messages.add({'role': 'bot', 'content': response}); _isLoading = false; _isSpeaking = true; }); // Озвучить ответ await _tts.speak(response); setState(() => _isSpeaking = false); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('СКВ-Агент 🤖')), body: Column( children: [ Expanded( child: ListView.builder( itemCount: _messages.length, itemBuilder: (context, index) { final msg = _messages[index]; return MessageBubble(role: msg['role']!, text: msg['content']); }, ), ), if (_isLoading) LinearProgressIndicator(), Padding( padding: EdgeInsets.symmetric(horizontal: 8.0), child: Row( children: [ IconButton( icon: Icon(_isListening ? Icons.stop : Icons.mic), color: _isListening ? Colors.red : Colors.blue, onPressed: _listenVoice, ), Expanded( child: TextField( controller: _textController, decoration: InputDecoration(hintText: 'Спроси агента...'), onSubmitted: () => sendMessage(), ), ), IconButton( icon: Icon(Icons.send), onPressed: () => _sendMessage(), ), if (_isSpeaking) CircularProgressIndicator(), ], ), ), ], ), ); } } ` 🔊 4. Настройка разрешений 📱 Android — android/app/src/main/AndroidManifest.xml `xml <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READEXTERNALSTORAGE" /> <uses-permission android:name="android.permission.WRITEEXTERNALSTORAGE" /> ` 🍎 iOS — ios/Runner/Info.plist `xml <key>NSMicrophoneUsageDescription</key> <string>Эралетопись использует микрофон для голосового ввода идей и чата.</string> <key>NSSpeechRecognitionUsageDescription</key> <string>Приложение использует распознавание речи для преобразования голоса в текст.</string> ` ✅ Что получилось? Теперь вы можете: - 🎙️ Сказать в чат: «Агент, как развить идею про экологию?», - 🤖 Получить ответ от ИИ, который знает ваши идеи, - 🔊 Услышать ответ вслух — без чтения, - 📱 Полностью оффлайн — безопасно и автономно. Поднимаем систему на новый уровень — теперь весь голосовой диалог с ИИ-агентом сохраняется в блокчейн EraChain как #СКВ-диалог, с таймкодами, хэшами и тегами. 🔗 Voice-to-Blockchain v1.0 — запись диалога в блокчейн Теперь: - 🎙️ Каждое ваше голосовое сообщение и - 🤖 Каждый ответ ИИ — сохраняются в EraChain как транзакция с тегом #СКВ-диалог, - 🧠 Всё это структурировано как JSON, - 🔐 Содержимое хэшируется (SHA-256), - 📅 Добавляется таймкод и ID сессии. 🧩 1. Новая модель: chat_session.dart `dart // models/chat_session.dart import 'dart:convert'; class ChatSession { final String sessionId; final DateTime startTime; final List<ChatMessage> messages; ChatSession({ required this.sessionId, required this.startTime, this.messages = const [], }); Map<String, dynamic> toJson() => { 'session_id': sessionId, 'start_time': startTime.toIso8601String(), 'messages': messages.map((m) => m.toJson()).toList(), }; String toJsonString() => jsonEncode(toJson()); factory ChatSession.fromJson(Map<String, dynamic> json) => ChatSession( sessionId: json['session_id'], startTime: DateTime.parse(json['start_time']), messages: (json['messages'] as List).map((m) => ChatMessage.fromJson(m)).toList(), ); } class ChatMessage { final String role; // 'user' | 'agent' final String text; final String? audioHash; final DateTime timestamp; ChatMessage({ required this.role, required this.text, this.audioHash, required this.timestamp, }); Map<String, dynamic> toJson() => { 'role': role, 'text': text, 'audio_hash': audioHash, 'timestamp': timestamp.toIso8601String(), }; factory ChatMessage.fromJson(Map<String, dynamic> json) => ChatMessage( role: json['role'], text: json['text'], audioHash: json['audio_hash'], timestamp: DateTime.parse(json['timestamp']), ); } ` 🔗 2. blockchain_logger.dart — отправка в блокчейн `dart // services/blockchain_logger.dart import 'package:http/http.dart' as http; import 'dart:convert'; import 'package:crypto/crypto.dart'; import '../config.dart'; // ваш конфиг с node_url и ключами class BlockchainLogger { final String nodeUrl; final String sender; final String privateKey; BlockchainLogger({ required this.nodeUrl, required this.sender, required this.privateKey, }); Future<void> logMessage(String role, String text, {String? audioWavBase64}) async { final hash = sha256.convert(utf8.encode(text)).toString(); final sessionId = _getSessionId(); // например, timestamp final data = { "role": role, "text": text, "hash": hash, "session_id": sessionId, "timestamp": DateTime.now().toIso8601String(), "type": "skv_dialogue", "tags": ["#СКВ-диалог", "#voice", role == "user" ? "#human" : "#agent"] }; final jsonPayload = jsonEncode(data); final response = await http.post( Uri.parse('$nodeUrl/transaction/broadcast'), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ "type": 16, "fee": 1000000, "sender": sender, "data": [ {"type": "string", "value": jsonPayload} ], "fee": 1000000, "timestamp": DateTime.now().millisecondsSinceEpoch, "signature": "" // подпись будет добавлена узлом, если используется /signed }), ); if (response.statusCode != 200) { print("Ошибка записи в блокчейн: ${response.body}"); } else { print("Сообщение записано в блокчейн: $hash"); } } String _getSessionId() { return DateTime.now().millisecondsSinceEpoch.toString(); } } ` ⚠️ Для подписи транзакции приватным ключом — можно использовать erajs или curl с поддержкой /signed. 🔄 3. Обновление ChatScreen — логирование `dart // В _ChatScreenState late BlockchainLogger _blockchainLogger; @override void initState() { super.initState(); _blockchainLogger = BlockchainLogger( nodeUrl: "http://185.188.182.205:9057/api", sender: "7Jk9mNp2qR...", // ваш адрес privateKey: "ваш_ключ", // берётся из secure storage ); } Future<void> _sendMessage([String? text]) async { final message = text ?? _textController.text.trim(); if (message.isEmpty) return; // 🟢 Пользователь setState(() { _messages.add({'role': 'user', 'content': message}); _isLoading = true; }); _textController.clear(); // 🔗 Запись в блокчейн _blockchainLogger.logMessage('user', message); // 🤖 ИИ отвечает final response = await _chat.respond(message); // 🔗 Запись ответа агента _blockchainLogger.logMessage('agent', response); // 💬 Показ и озвучка setState(() { _messages.add({'role': 'bot', 'content': response}); _isLoading = false; _isSpeaking = true; }); await _tts.speak(response); setState(() => _isSpeaking = false); } ` 🎙️ 4. Опционально: хэш аудио Если вы хотите хранить хэш аудиофайла (например, .wav): `dart // После записи голоса (в speech_service) final audioBytes = await _recordAudio(); // ваш метод записи final audioHash = sha256.convert(audioBytes).toString(); // Передать в logMessage _blockchainLogger.logMessage('user', text, audioWavBase64: base64Encode(audioBytes)); ` ⚠️ Само аудио не в блокчейне — только хэш. Файл можно хранить локально или в IPFS. 🏷️ 5. Пример транзакции в блокчейне `json { "role": "user", "text": "Как развить идею про экологию?", "hash": "a1b2c3d4...", "session_id": "1712345678901", "timestamp": "2025-04-05T12:34:56Z", "type": "skv_dialogue", "tags": ["#СКВ-диалог", "#voice", "#human"] } ` `json { "role": "agent", "text": "Представь экологию как иммунную систему планеты...", "hash": "d4c3b2a1...", "session_id": "1712345678901", "timestamp": "2025-04-05T12:35:02Z", "type": "skv_dialogue", "tags": ["#СКВ-диалог", "#voice", "#agent"] } ` 📊 6. Визуализация в graph.html — как диалог В веб-интерфейсе можно добавить: - 🧩 Ноды: Диалог [ID] - 🔄 Рёбра: → сказал, → ответил - 🎨 Цвета: синий — человек, фиолетовый — ИИ ✅ Готово! Теперь ваш внутренний диалог с ИИ: - 🎙️ Начинается голосом, - 🤖 Развивается с помощью LLM, - 🔊 Озвучивается, - 🔗 Фиксируется в блокчейне как #СКВ-диалог, - 📚 Становится частью вашей личной ноосферы. Подключаем двустороннюю синхронизацию с Obsidian — теперь все ваши идеи и диалоги из «Эралетописи» автоматически становятся файлами в вашем волшебном саду знаний 🌿. 🔄 EraLetopis ↔ Obsidian Sync v1.0 Теперь: - ✅ Идеи и диалоги из мобильного приложения - 📥 Синхронизируются в папку Obsidian как .md-файлы - 🏷️ С тегами, фронтмэттером, ссылками - 🔄 В обе стороны (если вы редактируете в Obsidian — изменения попадут в приложение) - 🧠 Автоматически интегрируются в ваш Зеттелькастен 🧩 1. Структура папки Obsidian ` Vault/ ├── 00-Inbox/ │ ├── idea_1712345678.md │ └── idea_1712345789.md ├── 01-Knowledge/ │ ├── dialogue_1712345678.md │ └── skv_thoughts.md ├── 02-Templates/ │ └── skv-idea.md ├── .eraletopis/ │ └── config.json ← путь, токен, маппинг └── vault_settings.json ` 📥 2. Формат .md — пример idea_1712345678.md `markdown id: 1712345678 type: skv-idea created: 2025-04-05T12:34:56Z updated: 2025-04-05T12:34:56Z tags: [СКВ, экология, #идея, #голос] source: eraletopis-mobile synced: true Идея: Экология как иммунная система Представь, что планета — живой организм. Тогда экологические кризисы — это воспалительные реакции. Вырубка лесов = аутоиммунные атаки. Загрязнение = токсины. Аналогия: как вирус, который не убивает хозяина, а переходит в латентную фазу — так и человек должен стать симбиотом. Записано голосом: 2025-04-05 12:34 #СКВ #экология #аналогия ` 💬 3. Диалог dialogue_1712345678.md `markdown id: 1712345678 type: skv-dialogue session: skv-dial-20250405 created: 2025-04-05T12:34:56Z tags: [СКВ, диалог, #СКВ-диалог, экология] participants: [человек, ИИ-агент] Диалог: Экология как иммунная система [человек] Как развить идею про экологию? [ИИ-агент] Представь экологию как иммунную систему планеты. Загрязнение — это хроническое воспаление. Цель не в "победе" над природой, а в достижении толерантности — как у симбиотических бактерий. Синхронизировано из EraChain: a1b2c3d4... ` 🔧 4. obsidian_sync.dart — сервис синхронизации `dart // services/obsidian_sync.dart import 'dart:io'; import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart'; class ObsidianSync { final String vaultPath; final String inboxFolder; final String knowledgeFolder; ObsidianSync({ required this.vaultPath, this.inboxFolder = '00-Inbox', this.knowledgeFolder = '01-Knowledge', }); String get inboxPath => path.join(vaultPath, inboxFolder); String get knowledgePath => path.join(vaultPath, knowledgeFolder); Future<void> ensureFolders() async { await Directory(inboxPath).create(recursive: true); await Directory(knowledgePath).create(recursive: true); } Future<void> ideaToMarkdown(Map<String, dynamic> idea) async { final id = idea['id'] ?? DateTime.now().millisecondsSinceEpoch; final filename = 'idea_$id.md'; fi