voice-break-api / VoiceAnalysis.tsx
fabiodeluca77's picture
Upload 6 files
723fe33 verified
// VoiceAnalysis.tsx - Componente React completo per Lovable
// Questo file può essere copiato direttamente in Lovable
import { useState, useRef } from 'react';
import { Mic, Square, Loader2, Wind, Droplet, Mountain, Flame } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress';
// Configurazione API - CAMBIA QUESTO URL dopo il deploy su HF
const API_BASE_URL = 'https://YOUR-USERNAME-voice-break-api.hf.space';
interface AnalysisResult {
element: string;
confidence: number;
prosody_score: {
aria: number;
acqua: number;
terra: number;
fuoco: number;
};
features: {
rms: number;
pitch: number;
duration: number;
};
}
export default function VoiceAnalysis() {
const [isRecording, setIsRecording] = useState(false);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const [analysis, setAnalysis] = useState<AnalysisResult | null>(null);
const [error, setError] = useState<string | null>(null);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
const chunksRef = useRef<Blob[]>([]);
// Funzione per iniziare la registrazione
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
});
const mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm'
});
mediaRecorderRef.current = mediaRecorder;
chunksRef.current = [];
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
chunksRef.current.push(e.data);
}
};
mediaRecorder.start();
setIsRecording(true);
setError(null);
} catch (err) {
console.error('Errore accesso microfono:', err);
setError('Impossibile accedere al microfono. Verifica i permessi del browser.');
}
};
// Funzione per fermare la registrazione e analizzare
const stopRecordingAndAnalyze = async () => {
const mediaRecorder = mediaRecorderRef.current;
if (!mediaRecorder) return;
mediaRecorder.onstop = async () => {
setIsRecording(false);
setIsAnalyzing(true);
// Crea blob audio
const audioBlob = new Blob(chunksRef.current, { type: 'audio/webm' });
// Ferma tutti i track del microfono
mediaRecorder.stream.getTracks().forEach(track => track.stop());
// Invia all'API per analisi
await analyzeVoice(audioBlob);
};
mediaRecorder.stop();
};
// Funzione per chiamare l'API di analisi
const analyzeVoice = async (audioBlob: Blob) => {
try {
const formData = new FormData();
formData.append('audio', audioBlob, 'voice.webm');
// Recupera baseline se esiste
const baselineStr = localStorage.getItem('voice_baseline');
if (baselineStr) {
const baseline = JSON.parse(baselineStr);
formData.append('baseline_rms', baseline.rms_baseline);
formData.append('baseline_pitch', baseline.pitch_baseline);
}
const response = await fetch(`${API_BASE_URL}/api/analyze-voice`, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`API Error: ${response.statusText}`);
}
const result: AnalysisResult = await response.json();
setAnalysis(result);
} catch (err) {
console.error('Errore analisi:', err);
setError('Errore durante l\'analisi vocale. Riprova.');
} finally {
setIsAnalyzing(false);
}
};
// Helper per ottenere icona elemento
const getElementIcon = (element: string) => {
switch (element) {
case 'aria': return <Wind className="w-12 h-12" />;
case 'acqua': return <Droplet className="w-12 h-12" />;
case 'terra': return <Mountain className="w-12 h-12" />;
case 'fuoco': return <Flame className="w-12 h-12" />;
default: return null;
}
};
// Helper per ottenere colore elemento
const getElementColor = (element: string) => {
switch (element) {
case 'aria': return 'text-cyan-500 bg-cyan-50';
case 'acqua': return 'text-blue-500 bg-blue-50';
case 'terra': return 'text-amber-700 bg-amber-50';
case 'fuoco': return 'text-red-500 bg-red-50';
default: return 'text-gray-500 bg-gray-50';
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-purple-50 via-white to-blue-50 p-8">
<div className="max-w-4xl mx-auto space-y-8">
{/* Header */}
<div className="text-center space-y-2">
<h1 className="text-4xl font-bold bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent">
Voice Break 🎙️
</h1>
<p className="text-gray-600">
Scopri l'elemento della tua voce attraverso l'analisi prosodica
</p>
</div>
{/* Recording Card */}
<Card className="shadow-xl">
<CardHeader>
<CardTitle>Registra la tua voce</CardTitle>
<CardDescription>
Clicca il pulsante e parla per 3-5 secondi
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
{/* Recording Button */}
<div className="flex justify-center">
<Button
size="lg"
onClick={isRecording ? stopRecordingAndAnalyze : startRecording}
disabled={isAnalyzing}
className={`w-32 h-32 rounded-full transition-all ${
isRecording
? 'bg-red-500 hover:bg-red-600 animate-pulse'
: 'bg-purple-500 hover:bg-purple-600'
}`}
>
{isRecording ? (
<Square className="w-12 h-12" />
) : isAnalyzing ? (
<Loader2 className="w-12 h-12 animate-spin" />
) : (
<Mic className="w-12 h-12" />
)}
</Button>
</div>
{/* Status Text */}
<div className="text-center">
{isRecording && (
<p className="text-red-500 font-medium animate-pulse">
🔴 Registrando... Clicca per fermare
</p>
)}
{isAnalyzing && (
<p className="text-purple-500 font-medium">
🔍 Analisi in corso...
</p>
)}
{!isRecording && !isAnalyzing && !analysis && (
<p className="text-gray-500">
Clicca il microfono per iniziare
</p>
)}
</div>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-red-700 text-sm">{error}</p>
</div>
)}
</CardContent>
</Card>
{/* Analysis Results */}
{analysis && (
<Card className="shadow-xl animate-in fade-in-50 duration-700">
<CardHeader>
<CardTitle>Risultato Analisi</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Elemento Dominante */}
<div className={`rounded-xl p-8 ${getElementColor(analysis.element)} transition-all`}>
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium opacity-70 mb-1">
Elemento Dominante
</p>
<p className="text-3xl font-bold capitalize">
{analysis.element}
</p>
<p className="text-sm mt-2 opacity-70">
Confidenza: {(analysis.confidence * 100).toFixed(1)}%
</p>
</div>
<div className="opacity-70">
{getElementIcon(analysis.element)}
</div>
</div>
</div>
{/* Distribuzione Percentuali */}
<div className="space-y-4">
<h3 className="font-semibold text-gray-700">Distribuzione Elementi</h3>
{Object.entries(analysis.prosody_score).map(([element, score]) => (
<div key={element} className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
<div className={`w-3 h-3 rounded-full ${
element === 'aria' ? 'bg-cyan-500' :
element === 'acqua' ? 'bg-blue-500' :
element === 'terra' ? 'bg-amber-600' :
'bg-red-500'
}`} />
<span className="font-medium capitalize">{element}</span>
</div>
<span className="text-sm font-bold">{score.toFixed(1)}%</span>
</div>
<Progress
value={score}
className="h-2"
/>
</div>
))}
</div>
{/* Feature Tecniche */}
<div className="grid grid-cols-3 gap-4 pt-4 border-t">
<div className="text-center">
<p className="text-xs text-gray-500 mb-1">RMS Energy</p>
<p className="text-lg font-bold">{analysis.features.rms.toFixed(4)}</p>
</div>
<div className="text-center">
<p className="text-xs text-gray-500 mb-1">Pitch (Hz)</p>
<p className="text-lg font-bold">{analysis.features.pitch.toFixed(1)}</p>
</div>
<div className="text-center">
<p className="text-xs text-gray-500 mb-1">Durata (s)</p>
<p className="text-lg font-bold">{analysis.features.duration.toFixed(1)}</p>
</div>
</div>
</CardContent>
</Card>
)}
</div>
</div>
);
}