voice-chat-api / index.html
NitinBot001's picture
Upload 4 files
64ac061 verified
raw
history blame
19.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Voice AI Assistant</title>
<style>
:root {
--primary-color: #4a90e2;
--success-color: #52c41a;
--danger-color: #ff4d4f;
--bg-color: #f0f2f5;
--card-bg: #ffffff;
--text-color: #333333;
--border-color: #d9d9d9;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: var(--text-color);
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
color: white;
margin-bottom: 30px;
}
header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.subtitle {
font-size: 1.1em;
opacity: 0.9;
}
.main-card {
background: var(--card-bg);
border-radius: 20px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
margin-bottom: 30px;
}
.recording-section {
text-align: center;
margin-bottom: 30px;
}
.record-btn {
width: 150px;
height: 150px;
border-radius: 50%;
border: none;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin: 0 auto 20px;
transition: all 0.3s ease;
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.record-btn:hover {
transform: scale(1.05);
box-shadow: 0 8px 30px rgba(102, 126, 234, 0.6);
}
.record-btn.recording {
background: linear-gradient(135deg, #ff4d4f 0%, #ff7875 100%);
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.record-icon {
font-size: 3em;
margin-bottom: 10px;
}
.visualizer {
margin: 20px 0;
height: 100px;
background: #f5f5f5;
border-radius: 10px;
overflow: hidden;
}
.visualizer canvas {
width: 100%;
height: 100%;
}
.status {
font-size: 1.1em;
color: #666;
margin-top: 10px;
}
.status.recording {
color: var(--danger-color);
font-weight: bold;
}
.status.processing {
color: var(--primary-color);
}
.status.success {
color: var(--success-color);
}
.tts-controls {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 10px;
}
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 24px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 24px;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--primary-color);
}
input:checked + .slider:before {
transform: translateX(26px);
}
.voice-select {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: 5px;
background: white;
font-size: 14px;
}
.conversation-display {
margin-top: 30px;
padding: 20px;
background: #f9f9f9;
border-radius: 10px;
}
.user-query, .ai-response {
margin-bottom: 20px;
}
.user-query h3, .ai-response h3 {
color: var(--primary-color);
margin-bottom: 10px;
font-size: 1.1em;
}
.user-query p, .ai-response p {
line-height: 1.6;
color: var(--text-color);
}
.speak-btn {
margin-top: 10px;
padding: 8px 16px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
}
.speak-btn:hover {
background: #3a7bc8;
}
.metadata {
margin-top: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 10px;
}
.metadata h4 {
margin-bottom: 10px;
color: #666;
}
.metadata pre {
font-size: 12px;
color: #666;
white-space: pre-wrap;
word-wrap: break-word;
}
.history-section {
background: var(--card-bg);
border-radius: 20px;
padding: 25px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.history-header h2 {
color: var(--primary-color);
}
.clear-btn {
padding: 8px 16px;
background: var(--danger-color);
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.clear-btn:hover {
background: #ff7875;
}
.history-list {
max-height: 400px;
overflow-y: auto;
}
.history-item {
padding: 15px;
margin-bottom: 10px;
background: #f9f9f9;
border-radius: 10px;
border-left: 4px solid var(--primary-color);
}
.history-item .timestamp {
font-size: 12px;
color: #999;
margin-bottom: 5px;
}
.history-item .query {
font-weight: 500;
margin-bottom: 5px;
}
.history-item .response {
color: #666;
font-size: 14px;
}
.hidden {
display: none !important;
}
.error {
color: var(--danger-color);
padding: 10px;
background: #fff2f0;
border-radius: 5px;
margin-top: 10px;
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
header h1 {
font-size: 2em;
}
.record-btn {
width: 120px;
height: 120px;
}
.record-icon {
font-size: 2.5em;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>🎙️ Voice AI Assistant</h1>
<p class="subtitle">Ask questions using your voice</p>
</header>
<div class="main-card">
<!-- Recording Controls -->
<div class="recording-section">
<button id="recordBtn" class="record-btn">
<span class="record-icon">🎤</span>
<span class="record-text">Start Recording</span>
</button>
<div id="visualizer" class="visualizer hidden">
<canvas id="waveform"></canvas>
</div>
<div id="status" class="status">
Ready to record
</div>
</div>
<!-- TTS Controls -->
<div class="tts-controls">
<label class="switch">
<input type="checkbox" id="autoSpeak" checked>
<span class="slider"></span>
</label>
<span>Auto-speak responses</span>
<select id="voiceSelect" class="voice-select">
<option value="">Default Voice</option>
</select>
</div>
<!-- Current Conversation -->
<div id="currentConversation" class="conversation-display hidden">
<div class="user-query">
<h3>You asked:</h3>
<p id="userText"></p>
</div>
<div class="ai-response">
<h3>AI Response:</h3>
<p id="aiText"></p>
<button id="speakBtn" class="speak-btn">🔊 Speak</button>
</div>
</div>
<!-- Metadata Display -->
<div id="metadata" class="metadata hidden">
<h4>Session Details</h4>
<pre id="metadataContent"></pre>
</div>
</div>
<!-- Conversation History -->
<div class="history-section">
<div class="history-header">
<h2>Conversation History</h2>
<button id="clearHistory" class="clear-btn">Clear All</button>
</div>
<div id="historyList" class="history-list"></div>
</div>
</div>
<script>
class VoiceAIApp {
constructor() {
this.backendUrl = 'http://localhost:8000';
this.mediaRecorder = null;
this.audioChunks = [];
this.isRecording = false;
this.recognition = null;
this.synthesis = window.speechSynthesis;
this.voices = [];
this.currentSession = null;
this.initializeElements();
this.initializeEventListeners();
this.loadVoices();
this.loadHistory();
}
initializeElements() {
this.elements = {
recordBtn: document.getElementById('recordBtn'),
status: document.getElementById('status'),
visualizer: document.getElementById('visualizer'),
waveform: document.getElementById('waveform'),
autoSpeak: document.getElementById('autoSpeak'),
voiceSelect: document.getElementById('voiceSelect'),
currentConversation: document.getElementById('currentConversation'),
userText: document.getElementById('userText'),
aiText: document.getElementById('aiText'),
speakBtn: document.getElementById('speakBtn'),
metadata: document.getElementById('metadata'),
metadataContent: document.getElementById('metadataContent'),
historyList: document.getElementById('historyList'),
clearHistory: document.getElementById('clearHistory')
};
}
initializeEventListeners() {
this.elements.recordBtn.addEventListener('click', () => this.toggleRecording());
this.elements.speakBtn.addEventListener('click', () => this.speakResponse());
this.elements.clearHistory.addEventListener('click', () => this.clearHistory());
// Load voices when they change
this.synthesis.addEventListener('voiceschanged', () => this.loadVoices());
}
loadVoices() {
this.voices = this.synthesis.getVoices();
this.elements.voiceSelect.innerHTML = '<option value="">Default Voice</option>';
this.voices.forEach((voice, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${voice.name} (${voice.lang})`;
this.elements.voiceSelect.appendChild(option);
});
}
async toggleRecording() {
if (this.isRecording) {
this.stopRecording();
} else {
this.startRecording();
}
}
async startRecording() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
// Setup MediaRecorder
const mimeType = 'audio/webm';
this.mediaRecorder = new MediaRecorder(stream, { mimeType });
this.audioChunks = [];
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.audioChunks.push(event.data);
}
};
this.mediaRecorder.onstop = async () => {
const audioBlob = new Blob(this.audioChunks, { type: mimeType });
await this.processAudio(audioBlob);
stream.getTracks().forEach(track => track.stop());
};
this.mediaRecorder.start();
this.isRecording = true;
// Update UI
this.elements.recordBtn.classList.add('recording');
this.elements.recordBtn.querySelector('.record-text').textContent = 'Stop Recording';
this.elements.status.textContent = 'Recording... Speak now';
this.elements.status.className = 'status recording';
this.elements.visualizer.classList.remove('hidden');
// Start visualizer
this.startVisualizer(stream);
} catch (error) {
console.error('Error accessing microphone:', error);
this.showError('Could not access microphone. Please check permissions.');
}
}
stopRecording() {
if (this.mediaRecorder && this.isRecording) {
this.mediaRecorder.stop();
this.isRecording = false;
// Update UI
this.elements.recordBtn.classList.remove('recording');
this.elements.recordBtn.querySelector('.record-text').textContent = 'Start Recording';
this.elements.status.textContent = 'Processing audio...';
this.elements.status.className = 'status processing';
this.elements.visualizer.classList.add('hidden');
}
}
startVisualizer(stream) {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const analyser = audioContext.createAnalyser();
const microphone = audioContext.createMediaStreamSource(stream);
const canvas = this.elements.waveform;
const ctx = canvas.getContext('2d');
analyser.fftSize = 256;
microphone.connect(analyser);
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
const draw = () => {
if (!this.isRecording) return;
requestAnimationFrame(draw);
analyser.getByteFrequencyData(dataArray);
ctx.fillStyle = '#f5f5f5';
ctx.fillRect(0, 0, canvas.width, canvas.height);
const barWidth = (canvas.width / bufferLength) * 2.5;
let barHeight;
let x = 0;
for (let i = 0; i < bufferLength; i++) {
barHeight = (dataArray[i] / 255) * canvas.height;
const gradient = ctx.createLinearGradient(0, 0, 0, canvas.height);
gradient.addColorStop(0, '#667eea');
gradient.addColorStop(1, '#764ba2');
ctx.fillStyle = gradient;
ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
x += barWidth + 1;
}
};
draw();
}
async processAudio(audioBlob) {
try {
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.webm');
const response = await fetch(`${this.backendUrl}/api/process-audio`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
if (data.success) {
this.displayConversation(data);
this.saveToHistory(data);
if (this.elements.autoSpeak.checked) {
this.speakText(data.ai_response);
}
this.elements.status.textContent = 'Success! Response received';
this.elements.status.className = 'status success';
} else {
throw new Error(data.error || 'Processing failed');
}
} catch (error) {
console.error('Error processing audio:', error);
this.showError(`Error: ${error.message}`);
}
}
displayConversation(data) {
this.currentSession = data;
this.elements.userText.textContent = data.user_query;
this.elements.aiText.textContent = data.ai_response;
this.elements.currentConversation.classList.remove('hidden');
// Display metadata
this.elements.metadataContent.textContent = JSON.stringify({
session_id: data.session_id,
timestamp: data.timestamp,
...data.metadata
}, null, 2);
this.elements.metadata.classList.remove('hidden');
}
speakResponse() {
if (this.currentSession) {
this.speakText(this.currentSession.ai_response);
}
}
speakText(text) {
// Cancel any ongoing speech
this.synthesis.cancel();
const utterance = new SpeechSynthesisUtterance(text);
// Set voice if selected
const selectedVoiceIndex = this.elements.voiceSelect.value;
if (selectedVoiceIndex && this.voices[selectedVoiceIndex]) {
utterance.voice = this.voices[selectedVoiceIndex];
}
// Set speech parameters
utterance.rate = 0.9;
utterance.pitch = 1;
utterance.volume = 1;
this.synthesis.speak(utterance);
}
saveToHistory(data) {
// Update history display
this.loadHistory();
}
async loadHistory() {
try {
const response = await fetch(`${this.backendUrl}/api/history`);
const data = await response.json();
this.elements.historyList.innerHTML = '';
data.sessions.reverse().forEach(session => {
const item = document.createElement('div');
item.className = 'history-item';
item.innerHTML = `
<div class="timestamp">${new Date(session.timestamp).toLocaleString()}</div>
<div class="query"><strong>Q:</strong> ${session.user_query}</div>
<div class="response"><strong>A:</strong> ${session.ai_response}</div>
`;
this.elements.historyList.appendChild(item);
});
} catch (error) {
console.error('Error loading history:', error);
}
}
async clearHistory() {
if (confirm('Are you sure you want to clear all conversation history?')) {
try {
await fetch(`${this.backendUrl}/api/history`, {
method: 'DELETE'
});
this.elements.historyList.innerHTML = '';
} catch (error) {
console.error('Error clearing history:', error);
}
}
}
showError(message) {
this.elements.status.textContent = message;
this.elements.status.className = 'status error';
}
}
// Initialize app when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
new VoiceAIApp();
});
</script>
</body>
</html>