voice_system.js
Различия
Показаны различия между двумя версиями страницы.
| voice_system.js [2026/06/07 11:10] – создано - внешнее изменение 127.0.0.1 | voice_system.js [2026/06/07 17:21] (текущий) – удалено VladPolskiy | ||
|---|---|---|---|
| Строка 1: | Строка 1: | ||
| - | /** | ||
| - | * ============================================================================ | ||
| - | * ЕВА - ИНТЕЛЛЕКТУАЛЬНЫЙ ГОЛОСОВОЙ ПОМОЩНИК | ||
| - | * Файл: logic.js | ||
| - | * Назначение: | ||
| - | | ||
| - | * Версия: | ||
| - | * ============================================================================ | ||
| - | */ | ||
| - | |||
| - | // ============================================================================ | ||
| - | // БЛОК 1: ИНИЦИАЛИЗАЦИЯ И СИНХРОНИЗАЦИЯ ГЛОБАЛЬНЫХ ПЕРЕМЕННЫХ СЕССИИ | ||
| - | // ============================================================================ | ||
| - | |||
| - | // Флаг ожидания выбора пользователя: | ||
| - | window.isWaitingDecision = window.isWaitingDecision || false; | ||
| - | |||
| - | // Временное хранилище вопроса пользователя, | ||
| - | window.pendingQuestion | ||
| - | |||
| - | // Хранилище вопроса для механизма прямого интерактивного пошагового обучения | ||
| - | window.lastQ | ||
| - | |||
| - | // Флаг блокировки: | ||
| - | window.isSpeakingWiki | ||
| - | |||
| - | // Флаг активности речевого синтезатора: | ||
| - | window.isEvaSpeaking | ||
| - | |||
| - | // Глобальный объект для хранения структурированных массивов данных из баз JSON | ||
| - | window.allData | ||
| - | |||
| - | // Глобальное время ожидания проактивности Евы по умолчанию (в миллисекундах) | ||
| - | window.IDLE_TIME | ||
| - | |||
| - | |||
| - | // ============================================================================ | ||
| - | // БЛОК 2: ВСПОМОГАТЕЛЬНЫЕ И СЛУЖЕБНЫЕ МЕТОДЫ (ПОЛИФИЛЫ РЕЧЕВОГО ТРАКТА) | ||
| - | // ============================================================================ | ||
| - | |||
| - | /** | ||
| - | * Проверка и регистрация глобальной функции синтеза речи. | ||
| - | * Интегрировано с локальным движком Piper TTS через бэкенд на Synology NAS. | ||
| - | * @param {string} text - Строка текста, | ||
| - | */ | ||
| - | if (typeof window.speak !== ' | ||
| - | window.speak = function(text) { | ||
| - | console.log(" | ||
| - | | ||
| - | if (!text) return; | ||
| - | |||
| - | // Прерываем любой текущий звук Piper, чтобы фразы не накладывались друг на друга | ||
| - | if (window.evaAudioPlayer) { | ||
| - | window.evaAudioPlayer.pause(); | ||
| - | } | ||
| - | |||
| - | // Включаем глобальный флаг активности речи | ||
| - | window.isEvaSpeaking = true; | ||
| - | |||
| - | // Формируем запрос к вашему рабочему бэкенду tts.php на Synology | ||
| - | const ttsApiUrl = `/ | ||
| - | window.evaAudioPlayer = new Audio(ttsApiUrl); | ||
| - | |||
| - | // Функция сброса флага при окончании звука или ошибке | ||
| - | const resetSpeechState = () => { | ||
| - | window.isEvaSpeaking = false; | ||
| - | }; | ||
| - | |||
| - | // Назначаем триггеры изменения глобального состояния активности речи | ||
| - | window.evaAudioPlayer.onended = resetSpeechState; | ||
| - | window.evaAudioPlayer.onerror = resetSpeechState; | ||
| - | |||
| - | // Передаем объект аудио в плеер браузера | ||
| - | window.evaAudioPlayer.play().catch((err) => { | ||
| - | console.warn(" | ||
| - | resetSpeechState(); | ||
| - | }); | ||
| - | }; | ||
| - | } | ||
| - | |||
| - | |||
| - | |||
| - | // ============================================================================ | ||
| - | // БЛОК 3: УПРАВЛЕНИЕ ЖИЗНЕННЫМ ЦИКЛОМ ПРИЛОЖЕНИЯ (МЕНЕДЖЕР ЗАПУСКА STARTAPP) | ||
| - | // ============================================================================ | ||
| - | |||
| - | /** | ||
| - | * Главная асинхронная функция инициализации Евы. | ||
| - | * Выполняет каскадную загрузку локальных баз знаний из папки data/и подготавливает UI. | ||
| - | */ | ||
| - | async function startApp() { | ||
| - | try { | ||
| - | console.log(" | ||
| - | | ||
| - | // Асинхронно запрашиваем чтение всех файлов через внешний модуль Database из папки data/ | ||
| - | const allData = await Database.loadAll(); | ||
| - | | ||
| - | // Проверяем валидность структуры полученного объекта и наличие основной базы | ||
| - | if (allData && allData.base) { | ||
| - | // КРИТИЧЕСКИЙ ШАГ: Дублируем данные в глобальное окно для доступности из любой точки системы | ||
| - | window.allData = allData; | ||
| - | | ||
| - | // Передаем загруженные массивы в аналитический модуль нейросети (ИИ-мозг) | ||
| - | AI.setData(allData); | ||
| - | | ||
| - | // ПРИНУДИТЕЛЬНОЕ ОБНОВЛЕНИЕ ЛИЧНОСТИ ИЗ SETTINGS.JSON | ||
| - | if (allData.settings) { | ||
| - | if (allData.settings.voice && typeof EvaConfig !== ' | ||
| - | // Заменяем сухие захардкоженные параметры голоса в config.js на серверные ползунки | ||
| - | EvaConfig.voice.pitch = allData.settings.voice.pitch; | ||
| - | EvaConfig.voice.rate = allData.settings.voice.rate; | ||
| - | EvaConfig.voice.volume = allData.settings.voice.volume; | ||
| - | } | ||
| - | | ||
| - | // СИНХРОНИЗАЦИЯ ВСЕХ 8 КИБЕР-ЭМОЦИЙ И ШЕПОТА ИЗ АДМИНКИ НА ЛЕТУ | ||
| - | if (allData.settings.emotions && typeof EvaConfig !== ' | ||
| - | for (let emoKey in allData.settings.emotions) { | ||
| - | if (EvaConfig.emotions[emoKey]) { | ||
| - | EvaConfig.emotions[emoKey].pitch = allData.settings.emotions[emoKey].pitch; | ||
| - | EvaConfig.emotions[emoKey].rate = allData.settings.emotions[emoKey].rate; | ||
| - | EvaConfig.emotions[emoKey].volume = allData.settings.emotions[emoKey].volume; | ||
| - | EvaConfig.emotions[emoKey].prefix = allData.settings.emotions[emoKey].prefix; | ||
| - | } else { | ||
| - | // Если эмоции не было в базовом коде (например, | ||
| - | EvaConfig.emotions[emoKey] = allData.settings.emotions[emoKey]; | ||
| - | } | ||
| - | } | ||
| - | } | ||
| - | } | ||
| - | |||
| - | // Обновляем счетчик записей на графическом интерфейсе пользователя | ||
| - | Visual.updateLog(`READY (${allData.base.length})`); | ||
| - | | ||
| - | // Переключаем визуальное состояние аватара в режим дыхания/ | ||
| - | Visual.setState(' | ||
| - | | ||
| - | // Если кнопка инициализации найдена в DOM, активируем её неоновую подсветку | ||
| - | if (window.mainBtn) window.mainBtn.className = ' | ||
| - | | ||
| - | console.log(" | ||
| - | } | ||
| - | } catch (e) { | ||
| - | // Логируем критическую ошибку (например, | ||
| - | console.error(" | ||
| - | | ||
| - | // Выводим тревожный статус на главный экран приложения | ||
| - | Visual.updateLog(" | ||
| - | } | ||
| - | } | ||
| - | |||
| - | |||
| - | // ============================================================================ | ||
| - | // БЛОК 4: ИНТЕГРАЦИЯ И ПАРСИНГ РЕЗУЛЬТАТОВ ИЗ СЕТИ ИНТЕРНЕТ (WIKIPEDIA) | ||
| - | // ============================================================================ | ||
| - | |||
| - | /** | ||
| - | * Обработчик успешного получения данных из Wikipedia. | ||
| - | * Выполняет вывод текста на экран, чанк-анимацию бегущей строки и запускает озвучку. | ||
| - | * @param {string} responseText - Текст статьи, | ||
| - | * @param {string|null} incomingQuestion - Вопрос, | ||
| - | */ | ||
| - | function onWikiResultReceived(responseText, | ||
| - | // Активируем системные блокировки, | ||
| - | window.isSpeakingWiki = true; | ||
| - | window.isWaitingDecision = true; | ||
| - | |||
| - | // Вычисляем итоговый вопрос: | ||
| - | const finalQuestion = incomingQuestion || window.pendingQuestion; | ||
| - | console.log(" | ||
| - | // 💥 ИНТЕГРАЦИЯ СЕТЕВЫХ НАСТРОЕК: | ||
| - | const currentDecisionHints = window.allData? | ||
| - | | ||
| - | // Принудительно выводим актуальные подсказки на верхний статус-бар экрана Евы | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.updateHints(true, | ||
| - | } | ||
| - | |||
| - | // Добавляем полноценный блок ответа Евы в графическое окно диалога чата | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.addMsg(responseText, | ||
| - | } | ||
| - | | ||
| - | // РЕАЛИЗАЦИЯ БЕГУЩЕЙ СТРОКИ: | ||
| - | const words = responseText.split(" | ||
| - | let currentWordIndex = 0; | ||
| - | |||
| - | // Создаем цикличный интервал для симуляции синхронного чтения текста Евой на экране | ||
| - | const textTickerInterval = setInterval(() => { | ||
| - | // Условие остановки: | ||
| - | // 💥 АППАРАТНАЯ ЗАЩИТА: | ||
| - | if (currentWordIndex >= words.length || (!window.isSpeakingWiki && !window.speechSynthesis.speaking)) { | ||
| - | clearInterval(textTickerInterval); | ||
| - | | ||
| - | // Сбрасываем текст живой плашки в исходное состояние тишины | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.setLiveText("" | ||
| - | } | ||
| - | return; | ||
| - | } | ||
| - | |||
| - | |||
| - | // Вырезаем порцию из 4-х слов для плавного отображения на экране | ||
| - | const chunk = words.slice(currentWordIndex, | ||
| - | const tickerText = chunk.join(" | ||
| - | |||
| - | // Отправляем сформированную порцию слов в текстовое поле живого вывода (синяя плашка статус-бара) | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.setLiveText(tickerText); | ||
| - | } | ||
| - | |||
| - | // Сдвигаем курсор индекса вперед на размер обработанной порции | ||
| - | currentWordIndex += 4; | ||
| - | }, 1500); // Оптимальный интервал чтения порции в полторы секунды | ||
| - | |||
| - | // Передаем полную статью на озвучивание в TTS-движок голосовой системы | ||
| - | speak(responseText); | ||
| - | |||
| - | // Валидация данных для запуска фонового автоматического сохранения в энциклопедическую базу wiki_base.json | ||
| - | if (finalQuestion && finalQuestion.length > 0 && responseText.length > 10) { | ||
| - | console.log(" | ||
| - | // Вызываем функцию фонового экспорта данных в базу знаний | ||
| - | if (typeof autoSaveToWiki === ' | ||
| - | autoSaveToWiki(finalQuestion, | ||
| - | } else { | ||
| - | console.warn(" | ||
| - | } | ||
| - | } else { | ||
| - | // Если статья пустая или некорректная, | ||
| - | window.isSpeakingWiki = false; | ||
| - | window.isWaitingDecision = false; | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.updateHints(false); | ||
| - | } | ||
| - | } | ||
| - | } | ||
| - | |||
| - | |||
| - | // ============================================================================ | ||
| - | // БЛОК 5: ИНТЕРФЕЙСНЫЙ ОБРАБОТЧИК ГЛАВНОЙ УПРАВЛЯЮЩЕЙ КНОПКИ | ||
| - | // ============================================================================ | ||
| - | |||
| - | if (window.mainBtn) { | ||
| - | /** | ||
| - | * Обработка нажатия на центральную неоновую кнопку управления Евой. | ||
| - | * Реализует два режима: | ||
| - | */ | ||
| - | window.mainBtn.onclick = async () => { | ||
| - | | ||
| - | // РЕЖИМ А: Приложение спало (window.isLive === false). Запускаем сессию. | ||
| - | if (!window.isLive) { | ||
| - | window.isLive = true; // Переводим систему в статус "В эфире" | ||
| - | window.freeTalk = false; // По умолчанию включаем режим обращения по имени | ||
| - | | ||
| - | // Меняем текстовое содержание и стиль кнопки на стоп-режим (красный неон) | ||
| - | window.mainBtn.innerText = " | ||
| - | window.mainBtn.className = ' | ||
| - | | ||
| - | // Инициализируем аудио-контекст захвата голоса и запускаем анализ частот микрофона | ||
| - | if (typeof initVoiceSystem === ' | ||
| - | if (typeof startMicLevel === ' | ||
| - | |||
| - | // Извлекаем случайное приветствие Евы из конфигурационного файла проекта | ||
| - | const hello = EvaConfig.getRandomGreeting(); | ||
| - | speak(hello); | ||
| - | Visual.addMsg(hello, | ||
| - | | ||
| - | // Запускаем таймер слежения за активностью пользователя | ||
| - | resetIdleTimer(); | ||
| - | | ||
| - | } else { | ||
| - | // РЕЖИМ Б: Приложение активно. Пользователь завершает сессию. Сортируем память. | ||
| - | window.isLive = false; | ||
| - | window.mainBtn.innerText = " | ||
| - | |||
| - | // 💥 МГНОВЕННЫЙ ЗАПУСК АВТОМАТИЧЕСКОГО СЕРВЕРНОГО БЭКАПА 5 БАЗ ДАННЫХ | ||
| - | try { | ||
| - | console.log(" | ||
| - | const backupResponse = await fetch(' | ||
| - | if (backupResponse.ok) { | ||
| - | const backupData = await backupResponse.json(); | ||
| - | if (backupData.status === ' | ||
| - | console.log(" | ||
| - | } | ||
| - | } | ||
| - | } catch (backupErr) { | ||
| - | console.warn(" | ||
| - | } | ||
| - | |||
| - | // Сбрасываем фоновые таймеры и деактивируем медиа-потоки записи звука | ||
| - | if (window.idleTimer) clearTimeout(window.idleTimer); | ||
| - | if (window.rec) try { window.rec.stop(); | ||
| - | |||
| - | // ИНТЕЛЛЕКТУАЛЬНАЯ СОРТИРОВКА: | ||
| - | const wikiData = AI.memory.filter(item => item.isWiki === true); | ||
| - | const baseData = AI.memory.filter(item => item.isWiki !== true); | ||
| - | |||
| - | // Очищаем временные маркеры ' | ||
| - | wikiData.forEach(item => delete item.isWiki); | ||
| - | |||
| - | try { | ||
| - | console.log(" | ||
| - | | ||
| - | // Асинхронно отправляем массив базовых знаний на обработку скрипту save.php (в папку data/) | ||
| - | const saveBase = fetch(' | ||
| - | method: ' | ||
| - | headers: { ' | ||
| - | body: JSON.stringify(baseData) | ||
| - | }); | ||
| - | |||
| - | // Одновременно отправляем энциклопедические данные скрипту save_wiki.php (в папку data/) | ||
| - | const saveWiki = fetch(' | ||
| - | method: ' | ||
| - | headers: { ' | ||
| - | body: JSON.stringify(wikiData) | ||
| - | }); | ||
| - | |||
| - | // Синхронизируем завершение обоих сетевых запросов | ||
| - | const [res1, res2] = await Promise.all([saveBase, | ||
| - | |||
| - | // Проверяем успешность ответов сервера (HTTP статус 200) | ||
| - | if (res1.ok && res2.ok) { | ||
| - | window.mainBtn.innerText = " | ||
| - | // Выполняем мягкую перезагрузку страницы через 1 секунду для обновления кэша данных | ||
| - | setTimeout(() => location.reload(), | ||
| - | } else { | ||
| - | throw new Error(" | ||
| - | } | ||
| - | } catch (err) { | ||
| - | console.error(" | ||
| - | alert(" | ||
| - | | ||
| - | // Возвращаем интерфейс в рабочее состояние для предотвращения потери сессии | ||
| - | window.mainBtn.innerText = " | ||
| - | window.isLive = true; | ||
| - | } | ||
| - | } | ||
| - | }; | ||
| - | } | ||
| - | |||
| - | |||
| - | // ============================================================================ | ||
| - | // БЛОК 6: ГЛАВНЫЙ АСИНХРОННЫЙ КОНВЕЙЕР ОБРАБОТКИ РЕЧИ И КОМАНД | ||
| - | // ============================================================================ | ||
| - | |||
| - | /** | ||
| - | * Объединенный монолитный обработчик распознанной речи. | ||
| - | * Проводит входную строку через каскад фильтров: | ||
| - | * @param {string} text - Распознанная строка текста от голосового движка. | ||
| - | */ | ||
| - | async function handleSpeech(text) { | ||
| - | // Предохранитель А: Если Ева говорит в данный момент, | ||
| - | if (window.isEvaSpeaking) return; | ||
| - | | ||
| - | // Предохранитель Б: Отсекаем пустые срабатывания микрофона или шумы | ||
| - | if (!text || text.trim().length < 1) return; | ||
| - | |||
| - | // Сбрасываем внутренний таймер инициативы, | ||
| - | if (typeof resetIdleTimer === ' | ||
| - | |||
| - | // ПРИНУДИТЕЛЬНЫЙ ВЫВОД: Отображаем фразу пользователя в чате для исключения эффекта зависания | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.addMsg(text, | ||
| - | } | ||
| - | |||
| - | // Очищаем текстовую плашку предварительного (живого) распознавания речевого потока | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.setLiveText("" | ||
| - | } | ||
| - | |||
| - | // Приводим строку к нижнему регистру и очищаем от концевых пробелов для точного сравнения | ||
| - | const cleanRaw = text.toLowerCase().trim(); | ||
| - | console.log(" | ||
| - | |||
| - | // ------------------------------------------------------------------------ | ||
| - | // ШАГ 6.0: ПЕРЕКЛЮЧЕНИЕ РЕЖИМОВ ОБЩЕНИЯ (Мгновенная реакция) | ||
| - | // ------------------------------------------------------------------------ | ||
| - | | ||
| - | // Переход в режим свободной беседы (без постоянного повторения имени " | ||
| - | if (cleanRaw.includes(" | ||
| - | window.freeTalk = true; | ||
| - | clearTimeout(window.idleTimer); | ||
| - | if (Visual.container) Visual.container.classList.add(' | ||
| - | if (window.mainBtn) window.mainBtn.className = ' | ||
| - | | ||
| - | const resp = Tools.humanize(window.allData? | ||
| - | speak(resp); | ||
| - | Visual.addMsg(resp, | ||
| - | resetIdleTimer(); | ||
| - | return; // Прерываем конвейер | ||
| - | } | ||
| - | |||
| - | // Возврат в строгий режим (Ева реагирует только тогда, когда зовут по имени) | ||
| - | if (cleanRaw.includes(" | ||
| - | window.freeTalk = false; | ||
| - | if (Visual.container) Visual.container.classList.remove(' | ||
| - | if (window.mainBtn) window.mainBtn.className = ' | ||
| - | | ||
| - | const resp = Tools.humanize(window.allData? | ||
| - | speak(resp); | ||
| - | Visual.addMsg(resp, | ||
| - | resetIdleTimer(); | ||
| - | return; // Прерываем конвейер | ||
| - | } | ||
| - | |||
| - | // ------------------------------------------------------------------------ | ||
| - | // ШАГ 6.1: ПЕРЕХВАТ СОСТОЯНИЯ ОЖИДАНИЯ ВЫБОРА (window.isWaitingDecision) | ||
| - | // ------------------------------------------------------------------------ | ||
| - | if (window.isWaitingDecision) { | ||
| - | |||
| - | // Перехват 1.1: Полный сброс операции и закрытие модальных окон обучения | ||
| - | if (cleanRaw.includes(" | ||
| - | window.isWaitingDecision = false; | ||
| - | window.pendingQuestion | ||
| - | window.isSpeakingWiki | ||
| - | window.isEvaSpeaking | ||
| - | |||
| - | // Прячем оранжевые текстовые подсказки управления | ||
| - | if (typeof Visual !== ' | ||
| - | |||
| - | // Закрываем любые диалоговые окна обучения через поиск CSS селекторов в DOM | ||
| - | const windows = document.querySelectorAll(' | ||
| - | windows.forEach(win => { | ||
| - | win.style.display = ' | ||
| - | win.classList.remove(' | ||
| - | }); | ||
| - | |||
| - | // Возвращаем аватар в режим ожидания и озвучиваем отмену | ||
| - | if (typeof Visual !== ' | ||
| - | | ||
| - | // Динамическое считывание фразы отмены из настроек settings.json | ||
| - | const resp = Tools.humanize(window.allData? | ||
| - | speak(resp); | ||
| - | Visual.addMsg(resp, | ||
| - | return; | ||
| - | } | ||
| - | // ———————————————————————————————————————————————————————————————————————— | ||
| - | // Перехват 1.2: Команда принудительного асинхронного поиска в Wikipedia | ||
| - | // ———————————————————————————————————————————————————————————————————————— | ||
| - | if (cleanRaw.includes(" | ||
| - | if (window.pendingQuestion && window.pendingQuestion.length > 0) { | ||
| - | window.isWaitingDecision = false; | ||
| - | | ||
| - | // Сбрасываем интерфейсные маркеры и переводим аватар в режим размышления | ||
| - | if (typeof Visual !== ' | ||
| - | if (typeof Visual !== ' | ||
| - | | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.setLiveText(window.allData? | ||
| - | } | ||
| - | |||
| - | try { | ||
| - | let wikiAnswer = null; | ||
| - | // Каскадный поиск доступного метода интеграции с API Википедии | ||
| - | // 💥 УМНАЯ МОДУЛЬНАЯ МАРШРУТИЗАЦИЯ: | ||
| - | // Tools.searchWiki сам заглянет в settings.json и выберет правильную папку навыка! | ||
| - | if (typeof Tools !== ' | ||
| - | wikiAnswer = await Tools.searchWiki(window.pendingQuestion); | ||
| - | } else { | ||
| - | console.error(" | ||
| - | } | ||
| - | |||
| - | |||
| - | // Если статья была успешно найдена и запущена внутри onWikiResultReceived (Блок 4) | ||
| - | if (window.isSpeakingWiki === true || (wikiAnswer && wikiAnswer.trim().length > 5)) { | ||
| - | console.log(" | ||
| - | | ||
| - | // Дополнительная подстраховка: | ||
| - | if (window.isSpeakingWiki === false && wikiAnswer && wikiAnswer.trim().length > 5) { | ||
| - | speak(wikiAnswer); | ||
| - | Visual.addMsg(wikiAnswer, | ||
| - | } | ||
| - | } else { | ||
| - | // Сюда система придет только если Википедия действительно сбойнула (ошибка 404 или пусто) | ||
| - | window.isLearning = true; | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.updateHints(true, | ||
| - | } | ||
| - | const resp = window.allData? | ||
| - | speak(resp); | ||
| - | Visual.addMsg(resp, | ||
| - | } | ||
| - | } catch (e) { | ||
| - | // ВОССТАНОВЛЕННЫЙ БЛОК ОБРАБОТКИ ОШИБОК: | ||
| - | console.error(" | ||
| - | window.isSpeakingWiki = false; | ||
| - | window.isWaitingDecision = false; | ||
| - | } | ||
| - | | ||
| - | // Возвращаем аватар в режим прослушивания | ||
| - | if (typeof Visual !== ' | ||
| - | } else { | ||
| - | speak(" | ||
| - | } | ||
| - | return; | ||
| - | } | ||
| - | |||
| - | // ———————————————————————————————————————————————————————————————————————— | ||
| - | // Перехват 1.3: Команда ручного обучения текущему вопросу фразами " | ||
| - | // ———————————————————————————————————————————————————————————————————————— | ||
| - | if (cleanRaw.includes(" | ||
| - | // Вырезаем системные триггеры, | ||
| - | let userAnswer = cleanRaw.replace(/ | ||
| - | | ||
| - | if (userAnswer.length > 1) { | ||
| - | // Записываем пару Вопрос-> | ||
| - | AI.learn(window.pendingQuestion, | ||
| - | window.isWaitingDecision = false; | ||
| - | | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.updateHints(false); | ||
| - | } | ||
| - | | ||
| - | // Динамическое считывание фразы успешного обучения из настроек settings.json | ||
| - | const resp = Tools.humanize(window.allData? | ||
| - | speak(resp); | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.addMsg(resp, | ||
| - | } | ||
| - | } else { | ||
| - | // Если пользователь не назвал сам текст ответа, | ||
| - | speak(" | ||
| - | } | ||
| - | return; // Прерываем речевой конвейер на этом шаге | ||
| - | } | ||
| - | |||
| - | // ———————————————————————————————————————————————————————————————————————— | ||
| - | // Перехват 1.4: Команда принудительного обращения к локальной нейросети Ollama | ||
| - | // ———————————————————————————————————————————————————————————————————————— | ||
| - | if (cleanRaw.includes(" | ||
| - | if (window.pendingQuestion && window.pendingQuestion.length > 0) { | ||
| - | window.isWaitingDecision = false; | ||
| - | |||
| - | // Гасим подсказки, | ||
| - | if (typeof Visual !== ' | ||
| - | if (typeof Visual !== ' | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.setLiveText(" | ||
| - | } | ||
| - | |||
| - | (async () => { | ||
| - | try { | ||
| - | // Отправляем сохраненный вопрос в ollama.php | ||
| - | const response = await fetch(' | ||
| - | method: | ||
| - | headers: | ||
| - | body: | ||
| - | }); | ||
| - | |||
| - | const data = await response.json(); | ||
| - | let aiResponse = data.message? | ||
| - | |||
| - | if (typeof Tools !== ' | ||
| - | aiResponse = Tools.humanize(aiResponse); | ||
| - | } | ||
| - | |||
| - | // 💾 Сохраняем диалог в базу знаний ai_base.json через save_ai.php | ||
| - | fetch(' | ||
| - | method: | ||
| - | headers: | ||
| - | body: | ||
| - | }) | ||
| - | .then(res => res.json()) | ||
| - | .then(saveResult => console.log(" | ||
| - | .catch(err => console.error(" | ||
| - | |||
| - | // Озвучка и вывод в чат речевого конвейера | ||
| - | if (typeof Visual !== ' | ||
| - | speak(aiResponse); | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.addMsg(aiResponse, | ||
| - | } | ||
| - | |||
| - | window.pendingQuestion = ""; | ||
| - | |||
| - | } catch (error) { | ||
| - | console.error(" | ||
| - | window.isWaitingDecision = true; | ||
| - | if (typeof Visual !== ' | ||
| - | speak(" | ||
| - | } | ||
| - | })(); | ||
| - | } else { | ||
| - | speak(" | ||
| - | } | ||
| - | return; | ||
| - | } | ||
| - | } | ||
| - | // ------------------------------------------------------------------------ | ||
| - | // ШАГ 6.2: РЕЖИМ ПРЯМОГО ПОШАГОВОГО ИНТЕРАКТИВНОГО ОБУЧЕНИЯ (window.isLearning) | ||
| - | // ------------------------------------------------------------------------ | ||
| - | if (window.isLearning) { | ||
| - | // Проверяем команду выхода из жесткого пошагового режима обучения фраз | ||
| - | if (cleanRaw.includes(" | ||
| - | window.isLearning = false; | ||
| - | if (typeof Visual !== ' | ||
| - | | ||
| - | // Динамическое считывание фразы сброса проехали (forgot) из админки личности Евы | ||
| - | speak(Tools.humanize(window.allData? | ||
| - | return; | ||
| - | } | ||
| - | | ||
| - | // Обучаем систему: | ||
| - | AI.learn(window.lastQ, | ||
| - | window.isLearning = false; | ||
| - | if (typeof Visual !== ' | ||
| - | | ||
| - | // Динамическое считывание короткой фразы успеха (learned) для пошагового обучения | ||
| - | speak(Tools.humanize(window.allData? | ||
| - | return; | ||
| - | } | ||
| - | |||
| - | // ------------------------------------------------------------------------ | ||
| - | // ШАГ 6.3: ИНСТРУМЕНТЫ АВТОМАТИЗАЦИИ (МАТЕМАТИКА, | ||
| - | // ------------------------------------------------------------------------ | ||
| - | | ||
| - | // Проверка 3.1: Поиск совпадений в текстовой матрице характера личности Евы из tools.js | ||
| - | const personalResp = Tools.processPersonality(cleanRaw); | ||
| - | if (personalResp && !window.isWaitingDecision) { | ||
| - | speak(personalResp); | ||
| - | Visual.addMsg(personalResp, | ||
| - | return; | ||
| - | } | ||
| - | |||
| - | // Проверка 3.2: Математический калькулятор (вычисление регулярными выражениями фраз типа " | ||
| - | const mathResult = Tools.calculate(cleanRaw); | ||
| - | if (mathResult) { | ||
| - | speak(mathResult); | ||
| - | Visual.addMsg(mathResult, | ||
| - | return; | ||
| - | } | ||
| - | |||
| - | // Проверка 3.3: Запрос текущего системного времени или даты с веб-сервера | ||
| - | if (cleanRaw.includes(" | ||
| - | const timeStr = await Tools.getServerDateTime(); | ||
| - | speak(timeStr); | ||
| - | Visual.addMsg(timeStr, | ||
| - | return; | ||
| - | } | ||
| - | |||
| - | // ------------------------------------------------------------------------ | ||
| - | // ШАГ 6.4: ПРОВЕРКА НАЛИЧИЯ ИМЕНИ (АКТИВАЦИОННЫЙ ФИЛЬТР ВХОДНОГО ПОТОКА) | ||
| - | // ------------------------------------------------------------------------ | ||
| - | // Система реагирует, | ||
| - | const hasName = cleanRaw.includes(" | ||
| - | if (!hasName) { | ||
| - | // Если имя не произнесено, | ||
| - | Visual.setLiveText(text); | ||
| - | return; | ||
| - | } | ||
| - | |||
| - | // Вычленяем чистый запрос для передачи в ассоциативный ИИ-мозг (ai.js) | ||
| - | const cleanForAI = text.replace(/ | ||
| - | if (cleanForAI.length < 1) return; // Выходим, | ||
| - | | ||
| - | // Рендерим статус размышлений аватара на верхней панели | ||
| - | Visual.setState(' | ||
| - | |||
| - | // ------------------------------------------------------------------------ | ||
| - | // ШАГ 6.5: СЕМАНТИЧЕСКИЙ АССОЦИАТИВНЫЙ ПОИСК В БАЗАХ ЗНАНИЙ (AI.THINK) | ||
| - | // ------------------------------------------------------------------------ | ||
| - | |||
| - | // Каскад 1: Ищем в основной ассоциативной базе знаний | ||
| - | let aiResult = (typeof AI !== ' | ||
| - | |||
| - | // Каскад 2: Если в основной базе пусто — ищем в базе ИИ (ai_base.json) | ||
| - | if ((!aiResult || !aiResult.found) && window.allData? | ||
| - | // Пробегаем по массиву вопросов ai_base.json, | ||
| - | const aiBaseMatch = window.allData.aiBase.find(item => | ||
| - | item.questions && item.questions.some(q => q.toLowerCase().trim() === cleanForAI.toLowerCase()) | ||
| - | ); | ||
| - | |||
| - | if (aiBaseMatch) { | ||
| - | console.log(" | ||
| - | aiResult = { | ||
| - | found: { answers: aiBaseMatch.answers || ["По памяти." | ||
| - | clean: cleanForAI | ||
| - | }; | ||
| - | } | ||
| - | } | ||
| - | |||
| - | |||
| - | |||
| - | // Третий каскад: | ||
| - | if ((!aiResult || !aiResult.found) && window.allData? | ||
| - | // Пробегаем по массиву вопросов wiki-базы, | ||
| - | const wikiMatch = window.allData.wiki.find(item => | ||
| - | item.questions && item.questions.some(q => q.toLowerCase().trim() === cleanForAI.toLowerCase()) | ||
| - | ); | ||
| - | | ||
| - | if (wikiMatch) { | ||
| - | // ЗАЩИТНЫЙ МЕХАНИЗМ: | ||
| - | console.log(" | ||
| - | const fallbackAnswers = wikiMatch.answers || [wikiMatch.text || wikiMatch.description || "По записям." | ||
| - | aiResult = { | ||
| - | found: { answers: fallbackAnswers || ["По записям." | ||
| - | clean: cleanForAI | ||
| - | }; | ||
| - | } | ||
| - | } | ||
| - | |||
| - | // ВЫВОД РЕЗУЛЬТАТОВ АНАЛИЗА БАЗ ДАННЫХ | ||
| - | if (aiResult && aiResult.found && aiResult.found.answers && aiResult.found.answers.length > 0) { | ||
| - | // Реакция А: Ответ НАЙДЕН. Выбираем случайный вариант из массива ответов для живости диалога | ||
| - | let response = aiResult.found.answers[Math.floor(Math.random() * aiResult.found.answers.length)]; | ||
| - | |||
| - | // Прогоняем сухой ответ через фильтр лингвистического очеловечивания и обращения по имени | ||
| - | if (typeof Tools !== ' | ||
| - | response = Tools.humanize(response); | ||
| - | } | ||
| - | |||
| - | // Переводим аватар в статус генерации реплики | ||
| - | Visual.setState(' | ||
| - | speak(response); | ||
| - | Visual.addMsg(response, | ||
| - | | ||
| - | // Фиксируем контекст текущей ноды для обеспечения поддержки древовидных sub-веток | ||
| - | if (typeof AI !== ' | ||
| - | | ||
| - | } else { | ||
| - | // ============================================================================ | ||
| - | // 💥 РЕАКЦИЯ Б: ОТВЕТ АБСОЛЮТНО НЕ НАЙДЕН В ПАМЯТИ ИИ ЕВЫ | ||
| - | // Активируем шлюзы интерактивного диалога обучения и ожидания решения | ||
| - | // ============================================================================ | ||
| - | window.isWaitingDecision = true; | ||
| - | window.pendingQuestion | ||
| - | | ||
| - | // Запоминаем текущий чистый вопрос для режима прямого пошагового обучения | ||
| - | window.lastQ = cleanForAI; | ||
| - | | ||
| - | // Считываем строку оранжевых подсказок выбора действий из папки data/ | ||
| - | const currentDecisionHints = window.allData? | ||
| - | | ||
| - | // Обновляем оранжевые подсказки действий на верхнем статус-баре экрана Евы | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.updateHints(true, | ||
| - | } | ||
| - | | ||
| - | // Считываем главный предустановленный вопрос Евы из папки data/ | ||
| - | const prompt = window.allData? | ||
| - | | ||
| - | // Запускаем генерацию речи ИИ через TTS речевую систему voice_system.js | ||
| - | speak(prompt); | ||
| - | | ||
| - | // Выводим текстовое облако сообщения ассистента в центральный чат приложения | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.addMsg(prompt, | ||
| - | } | ||
| - | } | ||
| - | } | ||
| - | |||
| - | |||
| - | // ============================================================================ | ||
| - | // БЛОК 7: АВТОНОМНЫЙ ТАЙМЕР ИНИЦИАТИВЫ И ПРОАКТИВНОСТИ ЕВЫ (СКУКА) | ||
| - | // ============================================================================ | ||
| - | |||
| - | /** | ||
| - | * Сброс и перезапуск таймера неактивности пользователя. | ||
| - | * Если пользователь молчит, | ||
| - | */ | ||
| - | function resetIdleTimer() { | ||
| - | // Стираем предыдущий запущенный таймер активности | ||
| - | clearTimeout(window.idleTimer); | ||
| - | | ||
| - | // Защитные условия: | ||
| - | if (!window.isLive || window.isLearning || window.isWaitingDecision) return; | ||
| - | | ||
| - | // Запускаем новый таймер ожидания на базе глобальной конфигурации window.IDLE_TIME | ||
| - | window.idleTimer = setTimeout(() => { | ||
| - | // Проверяем, | ||
| - | if (!window.speechSynthesis.speaking && Math.random() <= 0.3) { | ||
| - | | ||
| - | // Запрашиваем из ИИ случайную заготовку вопроса для пользователя | ||
| - | const entry = AI.getEvaQuestionEntry(); | ||
| - | if (entry) { | ||
| - | // Выбираем случайный вариант вопроса, | ||
| - | const text = Tools.humanize(entry.answers[Math.floor(Math.random() * entry.answers.length)]); | ||
| - | AI.currentContext = entry; // Запоминаем контекст для анализа ответа пользователя | ||
| - | | ||
| - | speak(text); | ||
| - | Visual.addMsg(text, | ||
| - | } | ||
| - | // Рекурсивно перезапускаем таймер для следующего шага проверки | ||
| - | resetIdleTimer(); | ||
| - | } else { | ||
| - | // Если условия рандома не прошли, | ||
| - | resetIdleTimer(); | ||
| - | } | ||
| - | }, window.IDLE_TIME); | ||
| - | } | ||
| - | |||
| - | |||
| - | // ============================================================================ | ||
| - | // БЛОК 8: СЕРВЕРНОЕ АВТОМАТИЧЕСКОЕ СОХРАНЕНИЕ ДАННЫХ И СБРОС БЛОКИРОВОК | ||
| - | // ============================================================================ | ||
| - | |||
| - | /** | ||
| - | * Асинхронное автоматическое сохранение полученных из интернета знаний на server. | ||
| - | * Выполняет обновление оперативной памяти, | ||
| - | * @param {string} question - Вопрос пользователя. | ||
| - | * @param {string} answer - Текст найденной статьи из Wikipedia. | ||
| - | */ | ||
| - | async function autoSaveToWiki(question, | ||
| - | console.log(" | ||
| - | |||
| - | // Создаем каноничную структуру сущности знаний для базы данных | ||
| - | const newEntry = { | ||
| - | type: " | ||
| - | questions: [question.toLowerCase().trim()], | ||
| - | answers: [answer], | ||
| - | sub: [] | ||
| - | }; | ||
| - | |||
| - | // Проверяем существование массива и пушим новый элемент в оперативную память браузера | ||
| - | if (!window.allData.wiki) window.allData.wiki = []; | ||
| - | window.allData.wiki.push(newEntry); | ||
| - | |||
| - | console.log(" | ||
| - | | ||
| - | // Асинхронно передаем массив модулю сохранения и дожидаемся ответа от сервера | ||
| - | const success = await Database.saveWiki(window.allData.wiki); | ||
| - | | ||
| - | // Анализируем маркер успешности проведения дисковой операции записи сервером | ||
| - | if (success) { | ||
| - | console.log(" | ||
| - | | ||
| - | // --- КРИТИЧЕСКИЙ СБРОС ВСЕХ СИСТЕМНЫХ БЛОКИРОВОК ДЛЯ ПОДДЕРЖАНИЯ ДИАЛОГА --- | ||
| - | window.isWaitingDecision = false; // Разблокируем шлюзы обработки новых фраз (Слон, Бублик) | ||
| - | window.pendingQuestion | ||
| - | window.isSpeakingWiki | ||
| - | window.isEvaSpeaking | ||
| - | |||
| - | // --- ИНТЕРФЕЙСНАЯ ОЧИСТКА ЭКРАНА --- | ||
| - | // Полностью очищаем текстовую строку от застрявших серых/ | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.setLiveText("" | ||
| - | } | ||
| - | |||
| - | // Деактивируем оранжевую строку подсказок команд | ||
| - | if (typeof Visual !== ' | ||
| - | Visual.updateHints(false); | ||
| - | } | ||
| - | |||
| - | // Автоматически закрываем всплывающие формы обучения диалогов изменением инлайновых CSS свойств | ||
| - | const windows = document.querySelectorAll(' | ||
| - | windows.forEach(win => { | ||
| - | win.style.display = ' | ||
| - | win.classList.remove(' | ||
| - | }); | ||
| - | |||
| - | if (typeof Visual !== ' | ||
| - | Visual.setState(' | ||
| - | } | ||
| - | |||
| - | // --- ПРОАКТИВНАЯ АКТИВАЦИЯ МИКРОФОНА --- | ||
| - | // Если сессия продолжается, | ||
| - | if (window.isLive && window.rec) { | ||
| - | try { | ||
| - | window.rec.start(); | ||
| - | console.log(" | ||
| - | } catch(e) { | ||
| - | console.log(" | ||
| - | } | ||
| - | } | ||
| - | } else { | ||
| - | console.error(" | ||
| - | } | ||
| - | } | ||
| - | |||
| - | |||
| - | // ============================================================================ | ||
| - | // БЛОК 9: ТОЧКА ВХОДА И РЕГИСТРАЦИЯ КОМПОНЕНТОВ СИСТЕМЫ | ||
| - | // ============================================================================ | ||
| - | |||
| - | // Автоматический выпуск первичного каскада инициализации при подключении скрипта | ||
| - | startApp(); | ||
| - | |||
| - | // ГЛОБАЛЬНАЯ РЕГИСТРАЦИЯ: | ||
| - | window.handleSpeech = handleSpeech; | ||
voice_system.js.1780819800.txt.gz · Последнее изменение: — 127.0.0.1
