Инструменты пользователя

Инструменты сайта


software:development:demo:cms:ucms:appendix:js_speech_chat_bot_eva_v2_comment

Код примера AI ассистента Eva v2

AI_PRO_Eva_v2.php
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Eva AI Pro Fixed</title>
    <style>
        /* ОСНОВНЫЕ СТИЛИ И ГАБАРИТЫ */
        :root { --bg: #ffffff; --accent: #007bff; --bot-msg: #f1f3f5; --user-msg: #e7f3ff; --text: #212529; --eva-active: #28a745; --warn: #dc3545; }
        body { margin: 0; padding: 0; background: #e9ecef; font-family: 'Segoe UI', sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; overflow: hidden; }
        .container { width: 600px; height: 350px; background: var(--bg); border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); display: flex; flex-direction: column; overflow: hidden; border: 2px solid transparent; transition: 0.3s; position: relative; }
        .active-listening { border-color: var(--eva-active); box-shadow: 0 0 20px rgba(40, 167, 69, 0.2); }
 
        /* ШАПКА */
        .header { background: #fff; padding: 6px 15px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #eee; height: 35px; }
        #monitor { font-size: 10px; font-weight: 900; color: var(--eva-active); text-transform: uppercase; letter-spacing: 1px; }
        #live-transcript { font-size: 12px; color: #adb5bd; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 350px; font-style: italic; }

        /* ОКНО ЧАТА */
        #chat-log { flex: 1; overflow-y: auto; padding: 15px; display: flex; flex-direction: column; gap: 10px; background: #fafafa; scroll-behavior: smooth; }
        .msg { padding: 8px 14px; border-radius: 14px; max-width: 80%; font-size: 13px; line-height: 1.4; color: var(--text); box-shadow: 0 2px 5px rgba(0,0,0,0.02); }
        .user-msg { background: var(--user-msg); align-self: flex-end; color: #004085; border-bottom-right-radius: 2px; }
        .bot-msg { background: var(--bot-msg); align-self: flex-start; border-bottom-left-radius: 2px; border: 1px solid #dee2e6; }

        /* ФУТЕР */
        .footer { padding: 10px; background: #fff; border-top: 1px solid #eee; }
        .vol-wrap { width: 100%; height: 4px; background: #eee; border-radius: 2px; margin-bottom: 10px; overflow: hidden; }
        #vol-bar { width: 0%; height: 100%; background: var(--eva-active); transition: 0.05s; }
        .controls { display: flex; gap: 8px; align-items: center; }
        button { border: none; border-radius: 10px; cursor: pointer; font-weight: bold; transition: 0.2s; outline: none; }
        #main-btn { background: var(--accent); color: white; flex-grow: 1; height: 36px; font-size: 11px; text-transform: uppercase; }
        #main-btn.stop { background: var(--warn); }
        .p-btn { background: #f8f9fa; color: #495057; width: 32px; height: 32px; border: 1px solid #dee2e6; }
        .talk-mode-ui { background: #6f42c1 !important; }
        #learning-tip { position: absolute; top: 40px; left: 50%; transform: translateX(-50%); background: var(--warn); color: white; padding: 4px 12px; border-radius: 20px; font-size: 10px; font-weight: bold; z-index: 10; display: none; }
    </style>
</head>
<body>
 
<div class="container" id="main-box">
    <div id="learning-tip">НЕ ЗАПОМИНАЙ — ОТМЕНА</div>
    <div class="header">
        <div id="monitor">OFFLINE</div>
        <div id="live-transcript">Готов к запуску</div>
    </div>
    <div id="chat-log"></div>
    <div class="footer">
        <div class="vol-wrap"><div id="vol-bar"></div></div>
        <div class="controls">
            <button id="main-btn">Запустить систему</button>
            <div class="player-btns">
                <button class="p-btn" onclick="music.prev()"></button>
                <button class="p-btn" onclick="music.toggle()"></button>
                <button class="p-btn" onclick="music.next()"></button>
            </div>
        </div>
    </div>
    <audio id="audio-player"></audio>
</div>
 
<script>
/** --- ПЕРЕМЕННЫЕ --- **/
const monitor = document.getElementById('monitor'), transcriptUI = document.getElementById('live-transcript');
const chatLog = document.getElementById('chat-log'), mainBtn = document.getElementById('main-btn');
const volBar = document.getElementById('vol-bar'), player = document.getElementById('audio-player');
const mainBox = document.getElementById('main-box'), tip = document.getElementById('learning-tip');
const synth = window.speechSynthesis;
 
let isLive = false, isSpeaking = false, isLearning = false, freeTalk = false, isEvaActive = false, currentContext = null;
let lastQ = "", responses = []; 
const playlist = ["music/Святки.mp3", "music/Зажечь огни.mp3", "music/Когда я стану старой бабкой.mp3" ]; 
let trackIdx = 0;
 
/** --- ПЕРЕЗАГРУЗКА БАЗЫ --- **/
    async function reloadEvaBase() {
        responses = [];       
        currentContext = null; 
        lastQ = "";            
        isLearning = false;    
 
        try {
            // Читаем файл base.json
            const response = await fetch('./base.json?v=' + Date.now()); // Добавили ?v=время
            const data = await response.json();
 
            // Если в json есть ключ "responses", берем его, если нет — берем весь массив
            responses = data.responses || (Array.isArray(data) ? data : []);
 
            console.log("База загружена, ОЗУ чистое");
            if(typeof updateStats === "function") updateStats(); // Обновить счетчики, если функция есть
        } catch (e) {
            console.error("Файл base.json не найден, создана пустая база");
            responses = [];
        }
    }
 
 
/** --- 1. ЗАГРУЗКА ПРИ СТАРТЕ --- **/
async function init() {
    try {
        const res = await fetch('./base.json?v=' + Date.now());
        if (!res.ok) throw new Error("Файл не найден на сервере (404)");
 
        const text = await res.text();
        console.log("Сырые данные из файла:", text); // СМОТРИТЕ ЭТО В КОНСОЛИ (F12)
 
        if (!text || text.trim() === "") {
            console.warn("Файл base.json пуст");
            responses = [];
        } else {
            const data = JSON.parse(text);
 
            // ПРОВЕРКА: если это массив — берем как есть, 
            // если объект с ключом responses — берем ключ, иначе — пустой массив
            if (Array.isArray(data)) {
                responses = data;
            } else if (data && typeof data === 'object' && data.responses) {
                responses = data.responses;
            } else {
                responses = [];
            }
        }
 
        console.log("Итоговый массив responses:", responses);
        isBaseLoaded = true; // Важный флаг для сохранения
        logUI("READY");
    } catch (e) { 
        console.error("Критическая ошибка при чтении JSON:", e.message); 
        responses = [];
        logUI("ERROR: JSON CORRUPT"); // Покажем ошибку в UI, чтобы вы знали
    }
}
 
 
 
// Вызываем загрузку сразу
init();
 
 
/** --- 2. ФУНКЦИИ --- **/
const logUI = (txt) => { monitor.innerText = txt; tip.style.display = isLearning ? "block" : "none"; };
 
function normalize(t) {
    if (!t) return "";
    return t.toLowerCase().trim().replace(/[.?!,]/g, '')
            .replace(/\b(я|мне|сейчас|уже|пришел|пришла|ева)\b/g, '')
            .replace(/\s+/g, ' ').trim();
}
 
function processResponse(text) {
    if (!text) return { audio: "", screen: "" };
    let t = text.trim();
    const stressMap = {"звонит": "звонит", "договор": "договор", "начала": "начала", "ева": "Ева"};
    for (let [w, c] of Object.entries(stressMap)) t = t.replace(new RegExp(w, "gi"), c);
    let screenT = t.replace(/\+/g, '');
    screenT = screenT.charAt(0).toUpperCase() + screenT.slice(1);
    if (!/[.!?]$/.test(screenT)) screenT += ".";
    return { audio: t, screen: screenT };
}
 
function addMsg(txt, cls) {
    const d = document.createElement('div'); d.className = 'msg ' + cls; d.innerText = txt;
    chatLog.appendChild(d); chatLog.scrollTop = chatLog.scrollHeight;
}
 
/** --- 3. ПЛЕЕР --- **/
const music = {
    play: () => {
        if(playlist.length > 0) {
            player.src = playlist[trackIdx]; player.play();
            let name = playlist[trackIdx].split('/').pop().replace('.mp3','');
            speak("Играет " + name); logUI("MUSIC");
        }
    },
    toggle: () => player.paused ? player.play() : player.pause(),
    next: () => { trackIdx = (trackIdx + 1) % playlist.length; music.play(); },
    prev: () => { trackIdx = (trackIdx - 1 + playlist.length) % playlist.length; music.play(); },
    stop: () => { player.pause(); player.currentTime = 0; log("MUSIC STOPPED"); }
};
 
/** --- 4. ГОЛОС --- **/
function speak(txt, lang = 'ru-RU', onFinished = null) {
    if (synth.speaking) synth.cancel();
    const processed = processResponse(txt);
    const ut = new SpeechSynthesisUtterance(lang === 'ru-RU' ? processed.audio : txt);
    ut.lang = lang;
    const voices = synth.getVoices();
    ut.voice = voices.find(v => v.name.includes('Google') && v.lang.includes(lang.split('-')[0])) || voices[0];
    ut.rate = 1.0; ut.pitch = 1.1;
 
    ut.onstart = () => { 
        isSpeaking = true; 
        logUI("ЕВА ГОВОРИТ"); 
        try { rec.stop(); } catch(e){} 
    };
 
    ut.onend = () => { 
        isSpeaking = false; 
        logUI("ЕВА СЛУШАЕТ"); 
 
        // Сначала выполняем callback (встречный вопрос), если он есть
        if (typeof onFinished === "function") {
            onFinished();
        } else {
            // Если callback нет, просто включаем микрофон
            if (isLive) startRec(); 
        }
    };
 
    synth.speak(ut);
    return processed.screen;
}
 
/** --- 5. СЛУХ (ФИКС UNDEFINED И СКОРОСТИ) --- **/
const SpeechRec = window.webkitSpeechRecognition || window.SpeechRecognition;
const rec = new SpeechRec();
rec.lang = 'ru-RU'; rec.continuous = true; rec.interimResults = true;
 
function startRec() { if (isLive && !isSpeaking) try { rec.start(); } catch(e){} }
 
rec.onresult = (e) => {
    let interim = '', final = '';
    for (let i = e.resultIndex; i < e.results.length; ++i) {
        if (e.results[i] && e.results[i][0]) { // ФИКС: Тройная проверка
            const txt = e.results[i][0].transcript;
            if (e.results[i].isFinal) final += txt; else interim += txt;
        }
    }
    const heard = (final || interim).toLowerCase().trim();
    if (heard) transcriptUI.innerText = heard;
 
    if (!freeTalk && !isEvaActive && !isLearning && !isSpeaking) {
        if (heard.includes("ева") || heard.includes("еву")) {
            isEvaActive = true; rec.stop(); speak("Ау?"); return;
        }
    }
    if (final.trim() !== "") handle(final.toLowerCase().trim());
};
 
/** --- 6. ЛОГИКА --- **/
function handle(text) {
    if (!text || isSpeaking) return;
 
    // Отмена обучения
    if (isLearning && text.includes("не запоминай")|| text.includes("не отвечай")|| text.includes("забудь")) {
        isLearning = false; isEvaActive = false; speak("Отменяю."); return;
    }
 
    // Запомни вопрос
    if (text.includes("запомни вопрос")) {
        let q = text.replace("запомни вопрос", "").trim();
        if(q) { responses.push({ type: "user_note", content: q }); speak("Записала."); }
        return;
    }
 
    // Перевод
    if (text.includes("переведи на английский")) {
        let trans = text.replace("переведи на английский", "").trim();
        if(trans) { speak(trans, 'en-US'); addMsg("Eng: " + trans, 'bot-msg'); }
        return;
    }
 
    // Время и дата
    if (text.includes("скажи время")) { speak(`Сейчас ${new Date().getHours()} ${new Date().getMinutes()}`); return; }
    if (text.includes("дата")|| text.includes("какое сегодня число")) { speak(`Сегодня ${new Date().toLocaleDateString('ru-RU')}`); return; }
 
    // Управление
    if (text.includes("давай поговорим")|| text.includes("даваай поболтаем")|| text.includes("поболтаем")) { freeTalk = true; mainBtn.classList.add('talk-mode-ui'); speak("Давай."); return; }
    if (text.includes("стоп разговор")) { freeTalk = false; mainBtn.classList.remove('talk-mode-ui'); speak("Ок, замолкаю..."); return; }
    if (text.includes("музыку")) { music.play(); return; }
    if (text.includes("стоп") || text.includes("пауза")) { player.pause(); speak("Пауза"); return; }
 
            // ОБУЧЕНИЕ
    if (isLearning) {
        // Создаем стандартизированный объект новой записи
        const newEntry = { 
            type: "qa", 
            questions: [lastQ], 
            answers: [text], 
            sub: [] // Автоматическое создание пустого массива для веток
        };
 
        if (currentContext) {
            // Если мы находимся внутри ветки (в контексте)
            // На всякий случай проверяем существование sub у родителя
            if (!currentContext.sub) currentContext.sub = []; 
 
            currentContext.sub.push(newEntry);
            logUI("ДОБАВЛЕНА ВЕТКА (SUB)");
        } else {
            // Если мы в корневом уровне
            let entry = responses.find(r => r.questions && r.questions.includes(lastQ));
            if (entry) {
                // Если вопрос уже есть, просто добавляем новый вариант ответа
                entry.answers.push(text);
                // Проверяем, есть ли у существующей записи поле sub (для старых баз)
                if (!entry.sub) entry.sub = []; 
            } else {
                // Если вопроса нет, добавляем полностью новый объект
                responses.push(newEntry);
            }
            logUI("ЗАПИСАНО В КОРЕНЬ");
        }
 
        isLearning = false; 
        isEvaActive = false; 
        currentContext = null; // Сброс контекста после успешного обучения
        speak("Запомнила"); 
        addMsg(text, 'user-msg'); 
        if(typeof updateStats === "function") updateStats(); // Вызывать только если она есть
        return;
    }
 
 
    if (!freeTalk && !isEvaActive) return;
 
// В начале функции обработки текста
const clean = normalize(text.replace(/ева|еву/g, ''));
if (!clean && isEvaActive) return;
 
// 1. СНАЧАЛА ИЩЕМ, ЕСТЬ ЛИ ТАКОЙ ВОПРОС В БАЗЕ
let found = responses.find(r => 
    r.questions && r.questions.some(q => normalize(q) === clean)
);
 
// 2. ЕСЛИ МЫ В РЕЖИМЕ ОБУЧЕНИЯ (отвечаем на "Как мне ответить?")
if (isLearning) {
    const newEntry = { 
        type: "qa", 
        questions: [lastQ], 
        answers: [text], 
        sub: [] 
    };
 
    if (currentContext) {
        if (!currentContext.sub) currentContext.sub = []; 
        currentContext.sub.push(newEntry);
    } else {
        // Если такой вопрос уже был в базе, просто добавим новый вариант ответа
        if (found) {
            found.answers.push(text);
        } else {
            responses.push(newEntry);
        }
    }
 
    isLearning = false; 
    isEvaActive = false; 
    currentContext = null;
    speak("Запомнила"); 
    addMsg(text, 'user-msg'); 
    updateStats();
    return;
}
 
// 3. ЕСЛИ МЫ ПРОСТО ГОВОРИМ (ПОИСК ОТВЕТА)
addMsg(text, 'user-msg');
logUI("ОБРАБОТКА");
 
if (found) {
    const randomAnswer = found.answers[Math.floor(Math.random() * found.answers.length)];
    speak(randomAnswer);
    addMsg(randomAnswer, 'eva-msg');
    currentContext = found; 
    isEvaActive = true;
    logUI("ОТВЕТ НАЙДЕН");
} else {
    // Если ничего не нашли — включаем обучение
    lastQ = clean;
    isLearning = true;
    speak("Как мне отвечать на это?");
    logUI("ОБУЧЕНИЕ...");
}
 
 
 
 
    // ПОИСК: Сначала ищем в sub текущего контекста, если он есть
    let entry = null;
    if (currentContext && currentContext.sub) {
        entry = currentContext.sub.find(r => r.questions && r.questions.some(q => normalize(q) === clean));
    }
 
    // Если в ветке не нашли, ищем в основном массиве
    if (!entry) {
        entry = responses.find(r => r.questions && r.questions.some(q => normalize(q) === clean || clean.includes(normalize(q))));
    }
 
        if (entry) {
        const ans = entry.answers[Math.floor(Math.random() * entry.answers.length)];
        isEvaActive = false;
 
        // 1. Говорим основной ответ и используем callback для встречного вопроса
        const screenText = speak(ans, 'ru-RU', () => {
            // ЭТОТ БЛОК ВЫПОЛНИТСЯ, КОГДА ЕВА ДОГОВОРИТ ОСНОВНОЙ ТЕКСТ
 
            // Встречный вопрос (30%)
            if (Math.random() > 0.7) {
                let qNodes = responses.filter(r => r.type === "qa");
                if(qNodes.length > 0) {
                    setTimeout(() => {
                        let randEntry = qNodes[Math.floor(Math.random() * qNodes.length)];
 
                        // Устанавливаем контекст на встречный вопрос, чтобы ответ попал в его sub
                        currentContext = randEntry; 
 
                        // Берем первый вопрос из массива (или саму строку, если это строка)
                        const nextQ = Array.isArray(randEntry.questions) ? randEntry.questions[0] : randEntry.questions;
 
                        addMsg(nextQ, 'bot-msg');
                        speak(nextQ); // Озвучиваем вопрос (без callback, чтобы не зациклить)
                        logUI("ИНИЦИАТИВА ЕВЫ");
                    }, 1000); // Короткая пауза для естественности
                }
            } else {
                // Если вопроса нет, просто возвращаемся в режим прослушивания
                if (isLive) startRec();
            }
        });
 
        // Выводим основной текст на экран (обработанный через processResponse внутри speak)
        addMsg(screenText, 'bot-msg'); 
 
        // 2. УСТАНОВКА КОНТЕКСТА: Если у этого ответа есть продолжение (sub), фиксируем его
        if (entry.sub && entry.sub.length > 0) {
            currentContext = entry;
            logUI("В КОНТЕКСТЕ: " + entry.questions[0]);
        } else {
            currentContext = null; // Выход из ветки, если продолжения нет
        }
 
    } else {
        // ... далее ваш старый блок обучения (lastQ = clean; и т.д.)
 
        lastQ = clean; 
        isLearning = true; 
        logUI("ОБУЧЕНИЕ");
        speak("Я не знаю ответа. Как мне ответить?");
    }
}
 
/** --- 7. ПЕРИФЕРИЯ --- **/
async function initMeter() {
    try {
        const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
        const ctx = new AudioContext(), src = ctx.createMediaStreamSource(stream), ans = ctx.createAnalyser();
        src.connect(ans);
        const data = new Uint8Array(ans.frequencyBinCount);
        function update() {
            if (!isLive) return;
            ans.getByteFrequencyData(data);
            volBar.style.width = Math.min(100, (data.reduce((a,b)=>a+b, 0) / data.length) * 5) + "%";
            requestAnimationFrame(update);
        }
        update();
    } catch(e){ logUI("MIC ERROR"); }
}
 
mainBtn.onclick = () => {
    if (!isLive) {
        isLive = true; 
        mainBtn.innerText = "ВЫЙТИ И СОХРАНИТЬ"; 
        mainBtn.classList.add('stop');
        mainBox.classList.add('active-listening'); 
        initMeter(); 
        startRec();
    } else {
        // ЗАЩИТА: Если база почему-то пуста, не даем сохранить
        if (!responses || responses.length === 0) {
            alert("Внимание! База пуста. Сохранение отменено, чтобы не испортить файл.");
            return;
        }
 
        const blob = new Blob([JSON.stringify(responses, null, 4)], {type:'application/json'});
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a'); 
        a.href = url; 
        a.download = "base.json"; // Просто имя, без точек и слэшей
 
        document.body.appendChild(a); 
        a.click(); 
 
        // Очистка и перезагрузка
        setTimeout(() => {
            URL.revokeObjectURL(url);
            document.body.removeChild(a);
            location.reload();
        }, 1000);
    }
};
 
 
rec.onend = () => { if (isLive && !isSpeaking) startRec(); };
window.speechSynthesis.onvoiceschanged = () => synth.getVoices();
    // Вызываем очистку и загрузку сразу при старте
    reloadEvaBase();
</script>
</body>
</html>
Только авторизованные участники могут оставлять комментарии.
software/development/demo/cms/ucms/appendix/js_speech_chat_bot_eva_v2_comment.txt · Последнее изменение: VladPolskiy

Если не указано иное, содержимое этой вики предоставляется на условиях следующей лицензии: Public Domain
Public Domain Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki