|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<meta charset="utf-8" /> |
|
<meta name="viewport" content="width=device-width" /> |
|
<title>Voice Recorder</title> |
|
<style> |
|
.card { |
|
max-width: 600px; |
|
margin: 2rem auto; |
|
padding: 2rem; |
|
border-radius: 10px; |
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); |
|
text-align: center; |
|
} |
|
|
|
.button { |
|
padding: 1rem 2rem; |
|
margin: 0.5rem; |
|
border: none; |
|
border-radius: 5px; |
|
cursor: pointer; |
|
font-size: 1rem; |
|
} |
|
|
|
.record { |
|
background-color: #ff4444; |
|
color: white; |
|
} |
|
|
|
.record.recording { |
|
background-color: #aa0000; |
|
} |
|
|
|
.status { |
|
margin-top: 1rem; |
|
font-style: italic; |
|
} |
|
|
|
.response { |
|
margin-top: 1rem; |
|
padding: 1rem; |
|
background-color: #f5f5f5; |
|
border-radius: 5px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="card"> |
|
<h1>Voice Recorder</h1> |
|
<button id="recordButton" class="button record">Start Recording</button> |
|
<div id="status" class="status">Click to start recording</div> |
|
<div id="response" class="response"></div> |
|
</div> |
|
|
|
<script> |
|
class VoiceRecorder { |
|
constructor() { |
|
this.mediaRecorder = null; |
|
this.audioChunks = []; |
|
this.isRecording = false; |
|
this.recordButton = document.getElementById('recordButton'); |
|
this.statusDiv = document.getElementById('status'); |
|
this.responseDiv = document.getElementById('response'); |
|
|
|
this.recordButton.addEventListener('click', () => this.toggleRecording()); |
|
|
|
|
|
this.ELEVEN_LABS_API_KEY = 'sk_f54a899fc3dd71448fc4a1e2c07f15aafa93580cdc50d853'; |
|
} |
|
|
|
async setupMediaRecorder() { |
|
try { |
|
const stream = await navigator.mediaDevices.getUserMedia({ |
|
audio: { |
|
sampleRate: 16000, |
|
channelCount: 1 |
|
} |
|
}); |
|
|
|
this.mediaRecorder = new MediaRecorder(stream, { |
|
mimeType: 'audio/webm', |
|
audioBitsPerSecond: 128000 |
|
}); |
|
|
|
this.mediaRecorder.ondataavailable = (event) => { |
|
this.audioChunks.push(event.data); |
|
}; |
|
|
|
this.mediaRecorder.onstop = async () => { |
|
const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' }); |
|
await this.convertAndSend(audioBlob); |
|
this.audioChunks = []; |
|
}; |
|
|
|
} catch (error) { |
|
console.error('Error accessing microphone:', error); |
|
this.statusDiv.textContent = 'Error accessing microphone. Please ensure microphone permissions are granted.'; |
|
} |
|
} |
|
|
|
async toggleRecording() { |
|
if (!this.mediaRecorder) { |
|
await this.setupMediaRecorder(); |
|
} |
|
|
|
if (!this.isRecording) { |
|
this.startRecording(); |
|
} else { |
|
this.stopRecording(); |
|
} |
|
} |
|
|
|
startRecording() { |
|
this.mediaRecorder.start(); |
|
this.isRecording = true; |
|
this.recordButton.textContent = 'Stop Recording'; |
|
this.recordButton.classList.add('recording'); |
|
this.statusDiv.textContent = 'Recording...'; |
|
} |
|
|
|
stopRecording() { |
|
this.mediaRecorder.stop(); |
|
this.isRecording = false; |
|
this.recordButton.textContent = 'Start Recording'; |
|
this.recordButton.classList.remove('recording'); |
|
this.statusDiv.textContent = 'Processing audio...'; |
|
} |
|
|
|
async convertAndSend(audioBlob) { |
|
try { |
|
|
|
const arrayBuffer = await audioBlob.arrayBuffer(); |
|
const audioContext = new AudioContext(); |
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); |
|
|
|
|
|
const wavBuffer = this.audioBufferToWav(audioBuffer); |
|
const wavBlob = new Blob([wavBuffer], { type: 'audio/wav' }); |
|
|
|
await this.sendToElevenLabs(wavBlob); |
|
} catch (error) { |
|
console.error('Error converting audio:', error); |
|
this.statusDiv.textContent = 'Error converting audio. Please try again.'; |
|
} |
|
} |
|
|
|
audioBufferToWav(audioBuffer) { |
|
const numberOfChannels = 1; |
|
const sampleRate = 16000; |
|
const format = 1; |
|
const bitDepth = 16; |
|
|
|
const length = audioBuffer.length * numberOfChannels * (bitDepth / 8); |
|
const buffer = new ArrayBuffer(44 + length); |
|
const view = new DataView(buffer); |
|
|
|
|
|
const writeString = (view, offset, string) => { |
|
for (let i = 0; i < string.length; i++) { |
|
view.setUint8(offset + i, string.charCodeAt(i)); |
|
} |
|
}; |
|
|
|
writeString(view, 0, 'RIFF'); |
|
view.setUint32(4, 36 + length, true); |
|
writeString(view, 8, 'WAVE'); |
|
writeString(view, 12, 'fmt '); |
|
view.setUint32(16, 16, true); |
|
view.setUint16(20, format, true); |
|
view.setUint16(22, numberOfChannels, true); |
|
view.setUint32(24, sampleRate, true); |
|
view.setUint32(28, sampleRate * numberOfChannels * (bitDepth / 8), true); |
|
view.setUint16(32, numberOfChannels * (bitDepth / 8), true); |
|
view.setUint16(34, bitDepth, true); |
|
writeString(view, 36, 'data'); |
|
view.setUint32(40, length, true); |
|
|
|
|
|
const channelData = audioBuffer.getChannelData(0); |
|
let offset = 44; |
|
for (let i = 0; i < channelData.length; i++) { |
|
const sample = Math.max(-1, Math.min(1, channelData[i])); |
|
view.setInt16(offset, sample < 0 ? sample * 0x8000 : sample * 0x7FFF, true); |
|
offset += 2; |
|
} |
|
|
|
return buffer; |
|
} |
|
|
|
async sendToElevenLabs(wavBlob) { |
|
try { |
|
const formData = new FormData(); |
|
formData.append('file', wavBlob, 'recording.wav'); |
|
|
|
const response = await fetch('https://api.elevenlabs.io/v2/speech-recognition', { |
|
method: 'POST', |
|
headers: { |
|
'xi-api-key': this.ELEVEN_LABS_API_KEY, |
|
'Accept': 'application/json' |
|
}, |
|
body: formData |
|
}); |
|
|
|
if (!response.ok) { |
|
const errorText = await response.text(); |
|
throw new Error(`HTTP error! status: ${response.status}, ${errorText}`); |
|
} |
|
|
|
const data = await response.json(); |
|
this.responseDiv.textContent = `Transcription: ${data.text}`; |
|
this.statusDiv.textContent = 'Transcription complete'; |
|
|
|
} catch (error) { |
|
console.error('Error sending to Eleven Labs:', error); |
|
this.statusDiv.textContent = 'Error sending to Eleven Labs API. Please check your API key and try again.'; |
|
} |
|
} |
|
} |
|
|
|
|
|
window.addEventListener('load', () => { |
|
new VoiceRecorder(); |
|
}); |
|
</script> |
|
</body> |
|
</html> |