Real-time speech-to-text using OpenAI Whisper (faster-whisper). Features browser audio capture, WebSocket streaming, and customizable display settings. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
205 lines
6.4 KiB
JavaScript
205 lines
6.4 KiB
JavaScript
/**
|
|
* Live Captions - Recordings Panel
|
|
* Handles viewing and managing saved recordings
|
|
*/
|
|
|
|
const Recordings = {
|
|
// Current state
|
|
recordings: [],
|
|
currentRecording: null,
|
|
|
|
// DOM elements
|
|
elements: {},
|
|
|
|
/**
|
|
* Initialize the recordings panel
|
|
*/
|
|
init() {
|
|
this.cacheElements();
|
|
this.bindEvents();
|
|
},
|
|
|
|
/**
|
|
* Cache DOM element references
|
|
*/
|
|
cacheElements() {
|
|
this.elements = {
|
|
btnRecordings: document.getElementById('btn-recordings'),
|
|
btnClose: document.getElementById('btn-close-recordings'),
|
|
btnBackToList: document.getElementById('btn-back-to-list'),
|
|
btnDelete: document.getElementById('btn-delete-recording'),
|
|
panel: document.getElementById('recordings-panel'),
|
|
overlay: document.getElementById('overlay'),
|
|
recordingsList: document.getElementById('recordings-list'),
|
|
recordingViewer: document.getElementById('recording-viewer'),
|
|
viewerFilename: document.getElementById('viewer-filename'),
|
|
viewerContent: document.getElementById('viewer-content'),
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Bind event listeners
|
|
*/
|
|
bindEvents() {
|
|
this.elements.btnRecordings.addEventListener('click', () => this.openPanel());
|
|
this.elements.btnClose.addEventListener('click', () => this.closePanel());
|
|
this.elements.btnBackToList.addEventListener('click', () => this.showList());
|
|
this.elements.btnDelete.addEventListener('click', () => this.deleteCurrentRecording());
|
|
|
|
// Close on overlay click (but check if it's not settings panel)
|
|
this.elements.overlay.addEventListener('click', () => {
|
|
if (!this.elements.panel.classList.contains('hidden')) {
|
|
this.closePanel();
|
|
}
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Open the recordings panel
|
|
*/
|
|
openPanel() {
|
|
this.elements.panel.classList.remove('hidden');
|
|
this.elements.overlay.classList.remove('hidden');
|
|
this.showList();
|
|
this.loadRecordings();
|
|
},
|
|
|
|
/**
|
|
* Close the recordings panel
|
|
*/
|
|
closePanel() {
|
|
this.elements.panel.classList.add('hidden');
|
|
this.elements.overlay.classList.add('hidden');
|
|
this.currentRecording = null;
|
|
},
|
|
|
|
/**
|
|
* Show the recordings list view
|
|
*/
|
|
showList() {
|
|
this.elements.recordingsList.classList.remove('hidden');
|
|
this.elements.recordingViewer.classList.add('hidden');
|
|
},
|
|
|
|
/**
|
|
* Show the recording viewer
|
|
*/
|
|
showViewer() {
|
|
this.elements.recordingsList.classList.add('hidden');
|
|
this.elements.recordingViewer.classList.remove('hidden');
|
|
},
|
|
|
|
/**
|
|
* Load recordings from the API
|
|
*/
|
|
async loadRecordings() {
|
|
this.elements.recordingsList.innerHTML = '<p class="recordings-empty">Loading recordings...</p>';
|
|
|
|
try {
|
|
const response = await fetch('/api/recordings');
|
|
if (!response.ok) throw new Error('Failed to load recordings');
|
|
|
|
this.recordings = await response.json();
|
|
this.renderRecordingsList();
|
|
} catch (error) {
|
|
console.error('Error loading recordings:', error);
|
|
this.elements.recordingsList.innerHTML =
|
|
'<p class="recordings-empty">Failed to load recordings</p>';
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Render the recordings list
|
|
*/
|
|
renderRecordingsList() {
|
|
if (this.recordings.length === 0) {
|
|
this.elements.recordingsList.innerHTML =
|
|
'<p class="recordings-empty">No recordings yet.<br>Enable auto-save and record some captions!</p>';
|
|
return;
|
|
}
|
|
|
|
const html = this.recordings.map(recording => `
|
|
<div class="recording-item" data-filename="${recording.filename}">
|
|
<div class="recording-info">
|
|
<span class="recording-date">${recording.date}</span>
|
|
<span class="recording-meta">${this.formatFileSize(recording.size)}</span>
|
|
</div>
|
|
<span class="recording-arrow">›</span>
|
|
</div>
|
|
`).join('');
|
|
|
|
this.elements.recordingsList.innerHTML = html;
|
|
|
|
// Bind click events to items
|
|
this.elements.recordingsList.querySelectorAll('.recording-item').forEach(item => {
|
|
item.addEventListener('click', () => {
|
|
const filename = item.dataset.filename;
|
|
this.viewRecording(filename);
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Format file size in human-readable format
|
|
*/
|
|
formatFileSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
},
|
|
|
|
/**
|
|
* View a specific recording
|
|
*/
|
|
async viewRecording(filename) {
|
|
try {
|
|
const response = await fetch(`/api/recordings/${encodeURIComponent(filename)}`);
|
|
if (!response.ok) throw new Error('Failed to load recording');
|
|
|
|
const data = await response.json();
|
|
this.currentRecording = filename;
|
|
this.elements.viewerFilename.textContent = filename;
|
|
this.elements.viewerContent.textContent = data.content;
|
|
this.showViewer();
|
|
} catch (error) {
|
|
console.error('Error loading recording:', error);
|
|
alert('Failed to load recording');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Delete the currently viewed recording
|
|
*/
|
|
async deleteCurrentRecording() {
|
|
if (!this.currentRecording) return;
|
|
|
|
if (!confirm('Are you sure you want to delete this recording?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`/api/recordings/${encodeURIComponent(this.currentRecording)}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to delete recording');
|
|
|
|
// Remove from local list
|
|
this.recordings = this.recordings.filter(r => r.filename !== this.currentRecording);
|
|
this.currentRecording = null;
|
|
|
|
// Go back to list
|
|
this.showList();
|
|
this.renderRecordingsList();
|
|
} catch (error) {
|
|
console.error('Error deleting recording:', error);
|
|
alert('Failed to delete recording');
|
|
}
|
|
}
|
|
};
|
|
|
|
// Initialize when DOM is ready
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
Recordings.init();
|
|
});
|