155 lines
5.8 KiB
JavaScript
155 lines
5.8 KiB
JavaScript
export function createNotesRecorder(options) {
|
|
options = options || {};
|
|
var recorder = null;
|
|
var recTimer = null;
|
|
var recPaused = false;
|
|
var recActive = false;
|
|
|
|
function start() {
|
|
if (recActive) return;
|
|
if (typeof options.ensureEditor === 'function') options.ensureEditor();
|
|
if (typeof AudioRecorder === 'undefined') { updateStatus('Recorder unavailable', 'err'); return; }
|
|
recorder = new AudioRecorder();
|
|
recorder.start().then(function() {
|
|
recActive = true; recPaused = false;
|
|
setUI('recording');
|
|
startTimer();
|
|
}).catch(function(err) {
|
|
updateStatus('Mic denied: ' + (err && err.message || ''), 'err');
|
|
});
|
|
}
|
|
|
|
function togglePause() {
|
|
if (!recActive || !recorder || !recorder.mediaRecorder) return;
|
|
if (!recPaused) {
|
|
try { recorder.mediaRecorder.pause(); } catch (e) {}
|
|
recPaused = true; stopTimer(); setUI('paused');
|
|
} else {
|
|
try { recorder.mediaRecorder.resume(); } catch (e) {
|
|
try {
|
|
if (recorder.stream && recorder.stream.active) {
|
|
var mime = MediaRecorder.isTypeSupported('audio/webm;codecs=opus') ? 'audio/webm;codecs=opus' : 'audio/webm';
|
|
recorder.mediaRecorder = new MediaRecorder(recorder.stream, { mimeType: mime, audioBitsPerSecond: 32000 });
|
|
recorder.mediaRecorder.ondataavailable = function(ev) { if (ev.data.size > 0) recorder.chunks.push(ev.data); };
|
|
recorder.mediaRecorder.start(1000);
|
|
}
|
|
} catch (e2) {}
|
|
}
|
|
recPaused = false; startTimer(); setUI('recording');
|
|
}
|
|
}
|
|
|
|
function stop(silent) {
|
|
if (!recActive || !recorder) {
|
|
if (!silent) setUI('idle');
|
|
return;
|
|
}
|
|
recActive = false; recPaused = false;
|
|
stopTimer(true);
|
|
if (silent) {
|
|
try { recorder.mediaRecorder && recorder.mediaRecorder.state !== 'inactive' && recorder.mediaRecorder.stop(); } catch (e) {}
|
|
try { recorder.stream && recorder.stream.getTracks().forEach(function(t) { t.stop(); }); } catch (e) {}
|
|
setUI('idle');
|
|
return;
|
|
}
|
|
setUI('processing');
|
|
updateStatus('Transcribing...', 'saving');
|
|
|
|
recorder.stop().then(function(blob) {
|
|
if (!blob || blob.size === 0) { updateStatus('Nothing recorded', 'err'); setUI('idle'); return; }
|
|
if (typeof transcribeAudio !== 'function') { updateStatus('Transcription unavailable', 'err'); setUI('idle'); return; }
|
|
return transcribeAudio(blob).then(function(resp) {
|
|
if (!resp || !resp.success || !resp.text) {
|
|
var msg = (resp && (resp.error || (resp.noProvider ? 'No STT provider configured' : null))) || 'Transcription failed';
|
|
updateStatus(msg, 'err'); setUI('idle'); return;
|
|
}
|
|
updateStatus('Generating note...', 'saving');
|
|
var modelEl = document.getElementById('notes-model-select');
|
|
var selectedModel = modelEl ? modelEl.value : '';
|
|
return options.noteFromVoice({ transcript: resp.text, model: selectedModel || undefined })
|
|
.then(function(data) {
|
|
setUI('idle');
|
|
if (!data.success) { updateStatus(data.error || 'Generation failed', 'err'); return; }
|
|
if (typeof options.applyGeneratedNote === 'function') options.applyGeneratedNote(data.title || 'Voice note', data.body || '');
|
|
updateStatus('Generated - autosaving...', 'ok');
|
|
if (typeof options.markDirty === 'function') options.markDirty();
|
|
if (typeof options.scheduleAutosave === 'function') options.scheduleAutosave();
|
|
});
|
|
});
|
|
}).catch(function(err) {
|
|
setUI('idle');
|
|
updateStatus(err.message || 'Recording failed', 'err');
|
|
});
|
|
}
|
|
|
|
function isActive() {
|
|
return recActive;
|
|
}
|
|
|
|
function startTimer() {
|
|
var el = getEl('notes-rec-timer');
|
|
if (recTimer || !el) return;
|
|
recTimer = createTimerLite(el);
|
|
recTimer.start();
|
|
}
|
|
|
|
function stopTimer(reset) {
|
|
if (recTimer) recTimer.stop();
|
|
if (reset && recTimer) { recTimer.reset(); recTimer = null; }
|
|
}
|
|
|
|
function createTimerLite(el) {
|
|
var s = 0, iv = null;
|
|
function paint() { el.textContent = String(Math.floor(s/60)).padStart(2,'0') + ':' + String(s%60).padStart(2,'0'); }
|
|
return {
|
|
start: function() { paint(); if (!iv) iv = setInterval(function() { s++; paint(); }, 1000); },
|
|
stop: function() { if (iv) { clearInterval(iv); iv = null; } },
|
|
reset: function() { s = 0; paint(); }
|
|
};
|
|
}
|
|
|
|
function setUI(state) {
|
|
var startBtn = getEl('btn-note-rec-start');
|
|
var pause = getEl('btn-note-rec-pause');
|
|
var stopBtn = getEl('btn-note-rec-stop');
|
|
var ind = getEl('notes-rec-indicator');
|
|
var stateEl = getEl('notes-rec-state');
|
|
var dot = ind ? ind.querySelector('.pulse-dot') : null;
|
|
if (!startBtn) return;
|
|
|
|
var idle = (state === 'idle');
|
|
var recording = (state === 'recording');
|
|
var paused = (state === 'paused');
|
|
var processing = (state === 'processing');
|
|
|
|
startBtn.classList.toggle('hidden', !idle);
|
|
pause.classList.toggle('hidden', !(recording || paused));
|
|
stopBtn.classList.toggle('hidden', !(recording || paused));
|
|
ind.classList.toggle('hidden', idle);
|
|
|
|
if (pause) pause.innerHTML = paused ? '<i class="fas fa-play"></i> Resume' : '<i class="fas fa-pause"></i> Pause';
|
|
if (stateEl) stateEl.textContent = processing ? 'Processing...' : (paused ? 'Paused' : 'Recording');
|
|
if (dot) dot.classList.toggle('paused', paused || processing);
|
|
|
|
startBtn.disabled = !idle;
|
|
pause.disabled = processing;
|
|
stopBtn.disabled = processing;
|
|
}
|
|
|
|
function getEl(id) {
|
|
if (typeof options.getEl === 'function') return options.getEl(id);
|
|
return document.getElementById(id);
|
|
}
|
|
|
|
function updateStatus(text, kind) {
|
|
if (typeof options.updateStatus === 'function') options.updateStatus(text, kind);
|
|
}
|
|
|
|
return {
|
|
start: start,
|
|
togglePause: togglePause,
|
|
stop: stop,
|
|
isActive: isActive,
|
|
setUI: setUI
|
|
};
|
|
}
|