Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>Voice Chat with Chrome Speech API, Ollama, and Kokoro TTS</title>
- <link rel="icon" href="data:,"> <!-- Empty favicon to prevent 404 error -->
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
- <style>
- :root {
- --background-color: #121212;
- --card-color: #1e1e1e;
- --text-color: #e0e0e0;
- --accent-color: #2c7be5;
- --secondary-color: #404040;
- --highlight-color: #60EFFF;
- --success-color: #28a745;
- --danger-color: #dc3545;
- }
- html, body {
- font-family: Arial, sans-serif;
- margin: 0;
- padding: 0;
- height: 100%;
- background-color: var(--background-color);
- color: var(--text-color);
- overflow: hidden;
- }
- #container {
- display: flex;
- flex-direction: column;
- height: 100vh;
- max-width: 1200px;
- margin: 0 auto;
- padding: 20px;
- box-sizing: border-box;
- }
- .header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- }
- h1 {
- margin: 0;
- color: var(--highlight-color);
- font-size: 24px;
- }
- .selector-group {
- display: flex;
- gap: 10px;
- align-items: center;
- }
- .selector-group select {
- flex: 1;
- padding: 8px 12px;
- background-color: var(--card-color);
- color: var(--text-color);
- border: 1px solid var(--secondary-color);
- border-radius: 6px;
- cursor: pointer;
- }
- #settings-btn {
- background: none;
- border: none;
- color: var(--text-color);
- font-size: 20px;
- cursor: pointer;
- padding: 8px;
- transition: color 0.2s;
- }
- #settings-btn:hover {
- color: var(--highlight-color);
- }
- #listening-info {
- background-color: rgba(44, 123, 229, 0.1);
- padding: 10px 15px;
- border-radius: 6px;
- margin-bottom: 15px;
- border-left: 4px solid var(--accent-color);
- font-size: 14px;
- }
- .chat-area {
- flex: 1;
- display: flex;
- position: relative;
- margin-bottom: 20px;
- min-height: 300px;
- overflow: hidden;
- border-radius: 8px;
- border: 1px solid var(--secondary-color);
- background-color: rgba(30, 30, 30, 0.5);
- }
- #output {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- padding: 20px;
- overflow-y: auto;
- background-color: transparent;
- z-index: 3;
- color: var(--text-color);
- text-shadow: 0 0 10px rgba(0, 0, 0, 0.8);
- font-size: 18px;
- line-height: 1.6;
- }
- #text-input {
- width: 100%;
- padding: 15px;
- background-color: var(--card-color);
- color: var(--text-color);
- border: 1px solid var(--secondary-color);
- border-radius: 6px;
- min-height: 80px;
- resize: vertical;
- margin-bottom: 15px;
- box-sizing: border-box;
- }
- .button-group {
- display: flex;
- gap: 10px;
- }
- .btn {
- padding: 12px 24px;
- background-color: var(--accent-color);
- color: var(--text-color);
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 16px;
- transition: background-color 0.2s;
- flex: 1;
- }
- .btn:hover {
- background-color: #1a68d1;
- }
- #voice-btn.recording {
- background-color: var(--success-color);
- }
- #voice-btn:disabled, #send-btn:disabled {
- background-color: var(--secondary-color);
- cursor: not-allowed;
- }
- #status {
- margin-top: 10px;
- font-size: 14px;
- color: #929292;
- }
- #audio-output {
- margin-top: 15px;
- width: 100%;
- }
- /* Modal styling */
- .modal {
- display: none;
- position: fixed;
- z-index: 10;
- left: 0;
- top: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.7);
- }
- .modal-content {
- background-color: var(--card-color);
- margin: 10% auto;
- padding: 20px;
- border: 1px solid var(--secondary-color);
- border-radius: 8px;
- width: 70%;
- max-width: 600px;
- color: var(--text-color);
- }
- .modal-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- }
- .close {
- color: var(--text-color);
- font-size: 24px;
- font-weight: bold;
- cursor: pointer;
- }
- .close:hover {
- color: var(--highlight-color);
- }
- .modal textarea {
- width: 100%;
- padding: 12px;
- height: 200px;
- margin-bottom: 15px;
- background-color: var(--background-color);
- color: var(--text-color);
- border: 1px solid var(--secondary-color);
- border-radius: 6px;
- resize: vertical;
- }
- @media (max-width: 768px) {
- #container {
- padding: 10px;
- }
- .header {
- flex-direction: column;
- align-items: flex-start;
- }
- .selector-group {
- width: 100%;
- margin-top: 10px;
- }
- .modal-content {
- width: 90%;
- margin: 20% auto;
- }
- }
- </style>
- </head>
- <body>
- <div id="container">
- <div class="header">
- <h1>Voice & Text Chat</h1>
- <div class="selector-group">
- <select id="model-selector">
- {% for model in models %}
- <option value="{{ model }}">{{ model }}</option>
- {% endfor %}
- </select>
- <select id="kroko-language-selector">
- <option value="auto">Auto-detect</option>
- <option value="en" selected>English</option>
- <option value="fr">French</option>
- <option value="de">German</option>
- </select>
- <button id="settings-btn" title="Settings"><i class="fas fa-cog"></i></button>
- </div>
- </div>
- <p id="listening-info">Voice recognition is always active. Speak clearly to interrupt at any time.</p>
- <div class="chat-area">
- <div id="output">Welcome! Start speaking or type a message to begin.</div>
- </div>
- <textarea id="text-input" placeholder="Type your message or use voice input"></textarea>
- <div class="button-group">
- <button id="voice-btn" class="btn">Voice Status: Listening</button>
- <button id="send-btn" class="btn">Send Text</button>
- </div>
- <div id="status">Ready</div>
- <audio id="audio-output" controls></audio>
- </div>
- <!-- Settings Modal -->
- <div id="settings-modal" class="modal">
- <div class="modal-content">
- <div class="modal-header">
- <h2>System Prompt Settings</h2>
- <span class="close">×</span>
- </div>
- <p>Set a custom system prompt for the AI model:</p>
- <textarea id="system-prompt" placeholder="Enter a system prompt that defines how the AI should behave...">You are a helpful, friendly AI assistant.</textarea>
- <button id="save-settings" class="btn">Save Settings</button>
- </div>
- </div>
- <script>
- // DOM Elements
- const voiceBtn = document.getElementById('voice-btn');
- const sendBtn = document.getElementById('send-btn');
- const textInput = document.getElementById('text-input');
- const outputDiv = document.getElementById('output');
- const audioOutput = document.getElementById('audio-output');
- const modelSelector = document.getElementById('model-selector');
- const languageSelector = document.getElementById('kroko-language-selector');
- const statusEl = document.getElementById('status');
- const settingsBtn = document.getElementById('settings-btn');
- const settingsModal = document.getElementById('settings-modal');
- const closeModal = document.querySelector('.close');
- const systemPrompt = document.getElementById('system-prompt');
- const saveSettings = document.getElementById('save-settings');
- // State variables
- let isListening = false;
- let recognition = null;
- let isProcessing = false;
- let currentAudio = null;
- let continuousMode = true; // Always listen for speech
- let userSystemPrompt = "You are a helpful, friendly AI assistant.";
- // Map language codes for speech recognition
- const languageMap = {
- 'en': 'en-US',
- 'fr': 'fr-FR',
- 'de': 'de-DE',
- 'auto': ''
- };
- // Initialize Web Speech API
- function initSpeechRecognition() {
- // Check if browser supports speech recognition
- window.SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
- if (!window.SpeechRecognition) {
- updateStatus("Speech recognition not supported in this browser. Please use Chrome.");
- voiceBtn.disabled = true;
- return false;
- }
- // Create speech recognition instance
- recognition = new SpeechRecognition();
- // Configure recognition
- recognition.continuous = true; // Enable continuous listening
- recognition.interimResults = true;
- // Set language based on selector
- const lang = languageSelector.value;
- recognition.lang = languageMap[lang] || 'en-US';
- // Handle results
- recognition.onresult = (event) => {
- let finalTranscript = '';
- let interimTranscript = '';
- for (let i = event.resultIndex; i < event.results.length; i++) {
- const transcript = event.results[i][0].transcript;
- if (event.results[i].isFinal) {
- finalTranscript += transcript;
- } else {
- interimTranscript += transcript;
- }
- }
- // If we have a final transcript and we're not currently processing
- if (finalTranscript && !isProcessing) {
- textInput.value = finalTranscript;
- // Stop any playing audio
- if (currentAudio && !currentAudio.paused) {
- currentAudio.pause();
- currentAudio = null;
- }
- // Process the new input
- sendMessage();
- } else if (finalTranscript && isProcessing) {
- // If we're processing, this is an interruption
- textInput.value = finalTranscript;
- // Stop any playing audio
- if (currentAudio && !currentAudio.paused) {
- currentAudio.pause();
- currentAudio = null;
- }
- // Cancel current processing and handle new input
- handleInterruption(finalTranscript);
- } else if (interimTranscript) {
- // Just update the text input for interim results
- textInput.value = interimTranscript;
- }
- };
- // Handle end of speech recognition service
- recognition.onend = () => {
- console.log("Recognition ended");
- // If we're in continuous mode and not deliberately stopped, restart
- if (continuousMode && isListening) {
- try {
- recognition.start();
- console.log("Restarted continuous listening");
- } catch (error) {
- console.error("Error restarting recognition:", error);
- isListening = false;
- voiceBtn.textContent = "Start Voice Input";
- voiceBtn.classList.remove("recording");
- }
- } else {
- isListening = false;
- voiceBtn.textContent = "Start Voice Input";
- voiceBtn.classList.remove("recording");
- }
- };
- // Handle errors
- recognition.onerror = (event) => {
- console.error("Recognition error:", event.error);
- updateStatus(`Error: ${event.error}`);
- // If it's a network error or other temporary issue, try to restart
- if (event.error === 'network' || event.error === 'aborted') {
- setTimeout(() => {
- if (continuousMode && isListening) {
- try {
- recognition.start();
- console.log("Restarted after error");
- } catch (e) {
- isListening = false;
- voiceBtn.textContent = "Start Voice Input";
- voiceBtn.classList.remove("recording");
- }
- }
- }, 1000);
- } else {
- isListening = false;
- voiceBtn.textContent = "Start Voice Input";
- voiceBtn.classList.remove("recording");
- }
- };
- return true;
- }
- // Handle user interruption
- function handleInterruption(newText) {
- console.log("Handling interruption with: ", newText);
- // Cancel any ongoing request if possible
- isProcessing = false;
- // Process the new text
- sendMessage();
- }
- // Start listening for speech
- function startListening() {
- if (!recognition) {
- if (!initSpeechRecognition()) {
- return;
- }
- }
- try {
- // Update language setting
- const lang = languageSelector.value;
- recognition.lang = languageMap[lang] || 'en-US';
- recognition.start();
- isListening = true;
- voiceBtn.textContent = "Voice Status: Listening";
- voiceBtn.classList.add("recording");
- updateStatus("Listening for speech...");
- } catch (error) {
- console.error("Error starting recognition:", error);
- updateStatus(`Error: ${error.message}`);
- }
- }
- // Stop listening
- function stopListening() {
- if (!isListening) return;
- try {
- recognition.stop();
- isListening = false;
- voiceBtn.textContent = "Voice Status: Paused";
- voiceBtn.classList.remove("recording");
- updateStatus("Ready");
- } catch (error) {
- console.error("Error stopping recognition:", error);
- }
- }
- // Toggle listening mode
- function toggleListening() {
- if (isListening) {
- // If we're in continuous mode, just update the button text
- // but don't actually stop listening
- if (continuousMode) {
- isListening = false;
- voiceBtn.textContent = "Voice Status: Paused";
- voiceBtn.classList.remove("recording");
- updateStatus("Voice input paused (still listening in background)");
- } else {
- stopListening();
- }
- } else {
- startListening();
- }
- }
- // Send message to the server for processing
- async function sendMessage() {
- const text = textInput.value.trim();
- if (!text) return;
- // Disable send button during processing (but keep voice active for interruptions)
- sendBtn.disabled = true;
- textInput.disabled = true;
- isProcessing = true;
- updateStatus("Processing message...");
- outputDiv.textContent = "Thinking...";
- try {
- // Send the text to the server
- const response = await fetch('/process_text', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- text: text,
- model: modelSelector.value,
- language: languageSelector.value,
- system_prompt: userSystemPrompt // Include the custom system prompt
- })
- });
- // If processing was interrupted, don't continue
- if (!isProcessing) {
- console.log("Processing was interrupted, ignoring response");
- return;
- }
- if (!response.ok) {
- throw new Error(`Server returned ${response.status}: ${response.statusText}`);
- }
- const data = await response.json();
- // If processing was interrupted, don't continue
- if (!isProcessing) {
- console.log("Processing was interrupted, ignoring response");
- return;
- }
- if (data.error) {
- throw new Error(data.error);
- }
- // Display the response
- outputDiv.textContent = data.text;
- // Play audio if available and not interrupted
- if (data.audio && isProcessing) {
- // Set up new audio source
- audioOutput.src = `data:audio/wav;base64,${data.audio}`;
- currentAudio = audioOutput;
- // Play the audio
- audioOutput.play().catch(err => {
- console.error("Error playing audio:", err);
- });
- }
- // Clear input for next message
- textInput.value = "";
- } catch (error) {
- console.error("Request error:", error);
- outputDiv.textContent = `Error: ${error.message}`;
- } finally {
- // Re-enable inputs
- sendBtn.disabled = false;
- textInput.disabled = false;
- isProcessing = false;
- updateStatus("Ready");
- }
- }
- // Update status message
- function updateStatus(message) {
- statusEl.textContent = message;
- }
- // Settings modal functions
- function openSettingsModal() {
- settingsModal.style.display = "block";
- systemPrompt.value = userSystemPrompt;
- }
- function closeSettingsModal() {
- settingsModal.style.display = "none";
- }
- function saveSystemPrompt() {
- userSystemPrompt = systemPrompt.value.trim();
- if (!userSystemPrompt) {
- userSystemPrompt = "You are a helpful, friendly AI assistant.";
- }
- closeSettingsModal();
- saveToLocalStorage();
- }
- // Event listeners
- voiceBtn.addEventListener('click', toggleListening);
- sendBtn.addEventListener('click', sendMessage);
- // Settings modal events
- settingsBtn.addEventListener('click', openSettingsModal);
- closeModal.addEventListener('click', closeSettingsModal);
- saveSettings.addEventListener('click', saveSystemPrompt);
- // Close modal if clicked outside the content
- window.addEventListener('click', (event) => {
- if (event.target === settingsModal) {
- closeSettingsModal();
- }
- });
- // Allow pressing Enter to send a message
- textInput.addEventListener('keydown', (event) => {
- if (event.key === 'Enter' && !event.shiftKey) {
- event.preventDefault();
- sendMessage();
- }
- });
- // Update language when selection changes
- languageSelector.addEventListener('change', () => {
- if (recognition) {
- const lang = languageSelector.value;
- recognition.lang = languageMap[lang] || 'en-US';
- }
- });
- // Handle stopping audio playback on interruption
- audioOutput.addEventListener('error', () => {
- console.log("Audio playback error");
- currentAudio = null;
- });
- // Load saved system prompt from localStorage if available
- function loadSettings() {
- const savedPrompt = localStorage.getItem('systemPrompt');
- if (savedPrompt) {
- userSystemPrompt = savedPrompt;
- systemPrompt.value = savedPrompt;
- }
- }
- // Save system prompt to localStorage
- function saveToLocalStorage() {
- localStorage.setItem('systemPrompt', userSystemPrompt);
- }
- // Initialize the app
- document.addEventListener('DOMContentLoaded', () => {
- loadSettings();
- initSpeechRecognition();
- // Start listening immediately in continuous mode
- if (continuousMode) {
- startListening();
- }
- });
- </script>
- </body>
- </html>
Add Comment
Please, Sign In to add comment