@voicefeedback/sdk
Version:
Modern voice feedback SDK with beautiful UI components and AI-powered analysis
245 lines (243 loc) • 9.59 kB
JavaScript
class VoiceFeedback {
constructor(config) {
this.mediaRecorder = null;
this.stream = null;
this.chunks = [];
this.isRecording = false;
this.startTime = 0;
this.config = {
language: 'en',
maxDuration: 300, // 5 minutes default
apiUrl: 'https://quicksass.cool.newstack.be/api', // Fixed: removed /v1 suffix
debug: false,
...config
};
if (!this.config.apiKey) {
throw new Error('VoiceFeedback: API key is required');
}
this.log('VoiceFeedback initialized with config:', this.config);
}
log(...args) {
if (this.config.debug) {
console.log('[VoiceFeedback]', ...args);
}
}
async startRecording() {
if (this.isRecording) {
this.log('Recording already in progress');
return;
}
try {
this.log('Requesting microphone access...');
// Request microphone access
this.stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
});
this.log('Microphone access granted');
// Create MediaRecorder with fallback mime types
const mimeType = this.getSupportedMimeType();
this.log('Using mime type:', mimeType);
this.mediaRecorder = new MediaRecorder(this.stream, { mimeType });
this.chunks = [];
// Set up event listeners
this.mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
this.chunks.push(event.data);
this.log('Audio chunk received:', event.data.size, 'bytes');
}
};
this.mediaRecorder.onstop = () => {
this.log('Recording stopped, processing...');
this.processRecording();
};
this.mediaRecorder.onerror = (event) => {
this.log('MediaRecorder error:', event);
const error = new Error('Recording failed');
this.config.onError?.(error);
};
// Start recording
this.mediaRecorder.start();
this.isRecording = true;
this.startTime = Date.now();
this.log('Recording started');
// Auto-stop after max duration
if (this.config.maxDuration) {
setTimeout(() => {
if (this.isRecording) {
this.log('Auto-stopping recording after max duration');
this.stopRecording();
}
}, this.config.maxDuration * 1000);
}
this.config.onStart?.();
}
catch (error) {
this.log('Failed to start recording:', error);
const err = new Error(`Failed to start recording: ${error instanceof Error ? error.message : String(error)}`);
this.config.onError?.(err);
throw err;
}
}
stopRecording() {
if (!this.isRecording || !this.mediaRecorder) {
this.log('No recording in progress');
return;
}
this.log('Stopping recording...');
this.mediaRecorder.stop();
this.isRecording = false;
// Stop all tracks
if (this.stream) {
this.stream.getTracks().forEach(track => {
track.stop();
this.log('Stopped audio track');
});
}
this.config.onStop?.();
}
getSupportedMimeType() {
const types = [
'audio/webm;codecs=opus',
'audio/webm',
'audio/mp4',
'audio/wav'
];
for (const type of types) {
if (typeof MediaRecorder !== 'undefined' && MediaRecorder.isTypeSupported && MediaRecorder.isTypeSupported(type)) {
return type;
}
}
return 'audio/webm'; // fallback
}
async processRecording() {
if (this.chunks.length === 0) {
const error = new Error('No audio data recorded');
this.config.onError?.(error);
return;
}
try {
// Create blob from recorded chunks
const audioBlob = new Blob(this.chunks, { type: this.getSupportedMimeType() });
const duration = (Date.now() - this.startTime) / 1000;
this.log('Processing audio blob:', audioBlob.size, 'bytes, duration:', duration, 'seconds');
// Prepare form data to match backend expectations
const formData = new FormData();
formData.append('audio', audioBlob, 'recording.webm');
formData.append('duration', duration.toString());
// Send to VoiceFeedback API - use correct endpoint
const apiUrl = `${this.config.apiUrl}/process-audio`;
this.log('Sending request to:', apiUrl);
const response = await fetch(apiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
},
body: formData
});
if (!response.ok) {
let errorMessage = `API request failed: ${response.status} ${response.statusText}`;
try {
const errorData = await response.json();
if (errorData.error && errorData.error.message) {
errorMessage = errorData.error.message;
}
}
catch (e) {
// Fallback to status text if JSON parsing fails
}
this.log('API error response:', response.status, errorMessage);
throw new Error(errorMessage);
}
const responseData = await response.json();
this.log('Raw API response:', responseData);
// Transform backend response to match SDK interface
if (responseData.success && responseData.result) {
const backendResult = responseData.result;
const analysis = backendResult.analysis || {};
const sentiment = analysis.sentiment || {};
const result = {
id: `rec_${Date.now()}`, // Generate client-side ID
transcript: backendResult.transcription || '',
sentiment: sentiment.label || 'neutral',
sentimentScore: sentiment.score || 0,
topics: analysis.keywords || [], // Use keywords as topics for now
emotions: [analysis.tone || 'neutral'],
duration: duration,
language: this.config.language || 'en',
processingTime: backendResult.metadata?.processingTime || 0
};
this.log('Transformed result:', result);
this.config.onComplete?.(result);
}
else {
throw new Error('Invalid response format from server');
}
}
catch (error) {
this.log('Failed to process recording:', error);
const err = new Error(`Failed to process recording: ${error instanceof Error ? error.message : String(error)}`);
this.config.onError?.(err);
}
}
// Static method for quick integration
static async quickStart(apiKey, options) {
const instance = new VoiceFeedback({
apiKey,
debug: true, // Enable debug by default for quick start
...options
});
return instance;
}
// Get recording status
getStatus() {
return {
isRecording: this.isRecording,
duration: this.isRecording ? (Date.now() - this.startTime) / 1000 : 0
};
}
// Check browser compatibility
static isSupported() {
return !!(typeof navigator !== 'undefined' &&
navigator.mediaDevices &&
typeof navigator.mediaDevices.getUserMedia === 'function' &&
typeof window !== 'undefined' &&
typeof window.MediaRecorder !== 'undefined');
}
// Test API key validity
async testApiKey() {
try {
const response = await fetch(`${this.config.apiUrl}/test-api-key`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json'
}
});
const responseData = await response.json();
if (response.ok && responseData.valid) {
return {
valid: true,
message: responseData.message || 'API key is valid and working correctly!'
};
}
else {
return {
valid: false,
message: responseData.error || `API key test failed: ${response.status}`
};
}
}
catch (error) {
return {
valid: false,
message: `API key test error: ${error instanceof Error ? error.message : String(error)}`
};
}
}
}
export { VoiceFeedback, VoiceFeedback as default };
//# sourceMappingURL=index.esm.js.map