Update project

This commit is contained in:
2026-01-10 14:20:54 +00:00
parent 2e13c90d5f
commit f563eb2a24
+192 -90
View File
@@ -217,7 +217,6 @@
const NOTE_BODY_PLAIN_FIELD = 'body_plain';
const NOTE_USERNAME_FIELD = 'Username';
let mediaRecorder = null;
let recordedChunks = [];
let recordedBlob = null;
let titleRecognition = null;
let noteRecognition = null;
@@ -647,105 +646,172 @@
ensureUserLogged();
}
// --- Audio recording setup with auto-stop on silence ---
// --- Audio recording setup with segmented silence handling ---
let audioContext = null;
let analyser = null;
let silenceStart = null;
let recordingStream = null;
let silenceCheckInterval = null;
let lastSoundAt = 0;
let stoppingSegment = false;
const SILENCE_THRESHOLD = 0.01; // Volume threshold for silence
const SILENCE_DURATION = 3000; // 3 seconds
function checkSilence() {
if (!analyser) return;
const SEGMENT_SILENCE_MS = 2000; // pause a segment after 2s of silence
const SESSION_SILENCE_MS = 3500; // end entire session after 3.5s of silence
let recordingSessionActive = false;
let segmentActive = false;
let currentSegmentChunks = [];
let recordedSegments = [];
recordedBlob = null; // reuse existing variable for merged preview
let hasHeardSpeech = false;
let sessionStartAt = 0;
function computeRms() {
if (!analyser) return 0;
const bufferLength = analyser.frequencyBinCount;
const dataArray = new Uint8Array(bufferLength);
analyser.getByteTimeDomainData(dataArray);
// Calculate average volume
let sum = 0;
for (let i = 0; i < bufferLength; i++) {
const normalized = (dataArray[i] - 128) / 128;
sum += normalized * normalized;
}
const rms = Math.sqrt(sum / bufferLength);
if (rms < SILENCE_THRESHOLD) {
if (!silenceStart) {
silenceStart = Date.now();
} else if (Date.now() - silenceStart >= SILENCE_DURATION) {
stopRecording();
}
} else {
silenceStart = null;
}
return Math.sqrt(sum / bufferLength);
}
async function setupRecorder() {
let stream;
async function ensureStream() {
if (recordingStream) return recordingStream;
try {
stream = await navigator.mediaDevices.getUserMedia({
recordingStream = await navigator.mediaDevices.getUserMedia({
audio: {
autoGainControl: true
}
autoGainControl: true,
},
});
} catch (err) {
console.error('Microphone access failed:', err);
document.getElementById('recordStatus').textContent = 'Microphone access denied or unavailable.';
throw err;
}
recordedChunks = [];
recordedBlob = null;
// Setup audio context for silence detection
audioContext = new (window.AudioContext || window.webkitAudioContext)();
analyser = audioContext.createAnalyser();
const source = audioContext.createMediaStreamSource(stream);
const source = audioContext.createMediaStreamSource(recordingStream);
source.connect(analyser);
analyser.fftSize = 2048;
return recordingStream;
}
mediaRecorder = new MediaRecorder(stream);
function startSilenceMonitor() {
if (silenceCheckInterval) return;
silenceCheckInterval = setInterval(checkSilence, 120);
}
function stopSilenceMonitor() {
if (silenceCheckInterval) {
clearInterval(silenceCheckInterval);
silenceCheckInterval = null;
}
}
function cleanupStream() {
stopSilenceMonitor();
if (audioContext) {
audioContext.close();
audioContext = null;
}
mediaRecorder = null;
if (recordingStream) {
recordingStream.getTracks().forEach((t) => t.stop());
recordingStream = null;
}
analyser = null;
}
function updateCombinedPreview() {
const audioEl = document.getElementById('audioPreview');
if (!recordedSegments.length) {
recordedBlob = null;
audioEl.src = '';
audioEl.classList.add('hidden');
document.getElementById('audioPlayer').classList.add('hidden');
document.getElementById('recordStatus').textContent = '';
return;
}
const type = recordedSegments[0]?.blob?.type || 'audio/webm';
recordedBlob = new Blob(recordedSegments.map((s) => s.blob), { type });
audioEl.src = URL.createObjectURL(recordedBlob);
audioEl.volume = 1.0;
audioEl.classList.remove('hidden');
document.getElementById('audioPlayer').classList.remove('hidden');
document.getElementById('recordStatus').textContent = `Segments recorded: ${recordedSegments.length}`;
}
function startSegment() {
if (!recordingStream) return;
currentSegmentChunks = [];
mediaRecorder = new MediaRecorder(recordingStream);
const recorderRef = mediaRecorder;
segmentActive = true;
stoppingSegment = false;
lastSoundAt = Date.now();
mediaRecorder.ondataavailable = (e) => {
if (e.data && e.data.size > 0) recordedChunks.push(e.data);
if (e.data && e.data.size > 0) currentSegmentChunks.push(e.data);
};
mediaRecorder.onstop = () => {
if (silenceCheckInterval) {
clearInterval(silenceCheckInterval);
silenceCheckInterval = null;
if (currentSegmentChunks.length) {
const blob = new Blob(currentSegmentChunks, { type: recorderRef.mimeType });
recordedSegments.push({ blob, name: `segment-${recordedSegments.length + 1}.webm` });
updateCombinedPreview();
}
silenceStart = null;
if (audioContext) {
audioContext.close();
audioContext = null;
segmentActive = false;
if (mediaRecorder === recorderRef) {
mediaRecorder = null;
}
stream.getTracks().forEach(track => track.stop());
recordedBlob = new Blob(recordedChunks, { type: mediaRecorder.mimeType });
const audioEl = document.getElementById('audioPreview');
audioEl.src = URL.createObjectURL(recordedBlob);
audioEl.volume = 1.0;
audioEl.classList.remove('hidden');
document.getElementById('audioPlayer').classList.remove('hidden');
document.getElementById('recordStatus').textContent = '';
mediaRecorder = null; // allow fresh recorder next start
stoppingSegment = false;
};
mediaRecorder.start();
document.getElementById('recordStatus').textContent = `Recording segment ${recordedSegments.length + 1}`;
}
function stopSegment() {
return new Promise((resolve) => {
if (stoppingSegment) {
resolve();
return;
}
if (mediaRecorder && mediaRecorder.state === 'recording') {
stoppingSegment = true;
const onStop = () => {
mediaRecorder.removeEventListener('stop', onStop);
resolve();
};
mediaRecorder.addEventListener('stop', onStop);
mediaRecorder.stop();
} else {
segmentActive = false;
stoppingSegment = false;
resolve();
}
});
}
async function startRecording() {
const recordBtn = document.getElementById('recordBtn');
const stopBtn = document.getElementById('stopBtn');
try {
if (!mediaRecorder) await setupRecorder();
silenceStart = null;
recordedChunks = [];
if (recordingSessionActive) return;
await ensureStream();
recordedSegments = [];
recordedBlob = null;
mediaRecorder.start();
document.getElementById('recordStatus').textContent = 'Recording…';
segmentActive = false;
hasHeardSpeech = false;
sessionStartAt = Date.now();
recordingSessionActive = true;
lastSoundAt = sessionStartAt;
startSegment();
startSilenceMonitor();
recordBtn.disabled = true;
stopBtn.disabled = false;
stopBtn.classList.remove('hidden');
// Start silence detection
silenceCheckInterval = setInterval(checkSilence, 100);
} catch (err) {
console.error('Failed to start recording:', err);
document.getElementById('recordStatus').textContent = 'Recording not available.';
@@ -755,29 +821,60 @@
}
}
function stopRecording() {
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
if (silenceCheckInterval) {
clearInterval(silenceCheckInterval);
silenceCheckInterval = null;
async function endRecordingSession(reason) {
recordingSessionActive = false;
stopSilenceMonitor();
await stopSegment();
cleanupStream();
const recordBtn = document.getElementById('recordBtn');
const stopBtn = document.getElementById('stopBtn');
recordBtn.disabled = false;
stopBtn.disabled = true;
stopBtn.classList.add('hidden');
if (reason) {
document.getElementById('recordStatus').textContent = reason;
}
hasHeardSpeech = false;
}
async function stopRecording() {
await endRecordingSession('Recording stopped.');
}
function checkSilence() {
if (!recordingSessionActive || !analyser) return;
const now = Date.now();
const rms = computeRms();
if (rms >= SILENCE_THRESHOLD) {
hasHeardSpeech = true;
lastSoundAt = now;
if (!segmentActive) {
startSegment();
}
document.getElementById('recordBtn').disabled = false;
const stopBtn = document.getElementById('stopBtn');
stopBtn.disabled = true;
stopBtn.classList.add('hidden');
return;
}
// If we have not yet heard speech, allow up to SESSION_SILENCE_MS before ending
if (!hasHeardSpeech) {
if (now - sessionStartAt >= SESSION_SILENCE_MS) {
endRecordingSession('Stopped after silence.');
}
return;
}
if (segmentActive && !stoppingSegment && now - lastSoundAt >= SEGMENT_SILENCE_MS) {
stopSegment();
}
if (!segmentActive && now - lastSoundAt >= SESSION_SILENCE_MS) {
endRecordingSession('Stopped after silence.');
}
}
function clearAudio() {
if (silenceCheckInterval) {
clearInterval(silenceCheckInterval);
silenceCheckInterval = null;
}
if (mediaRecorder && mediaRecorder.state === 'recording') {
mediaRecorder.stop();
}
recordedChunks = [];
async function clearAudio() {
await endRecordingSession('Audio cleared.');
recordedSegments = [];
recordedBlob = null;
const audioEl = document.getElementById('audioPreview');
audioEl.src = '';
@@ -788,15 +885,13 @@
document.getElementById('currentTime').textContent = '0:00';
document.getElementById('durationTime').textContent = '0:00';
document.getElementById('playPauseBtn').textContent = '▶';
document.getElementById('recordBtn').disabled = false;
const stopBtn = document.getElementById('stopBtn');
stopBtn.disabled = true;
stopBtn.classList.add('hidden');
document.getElementById('recordStatus').textContent = '';
}
document.getElementById('recordBtn').addEventListener('click', startRecording);
document.getElementById('stopBtn').addEventListener('click', stopRecording);
document.getElementById('stopBtn').addEventListener('click', () => {
stopRecording();
});
// Audio options menu
const audioMenuContainer = document.getElementById('audioMenuContainer');
const audioMenuBtn = document.getElementById('audioMenuBtn');
@@ -853,10 +948,10 @@
e.preventDefault();
audioMenu.classList.toggle('hidden');
});
removeAudioBtn.addEventListener('click', (e) => {
removeAudioBtn.addEventListener('click', async (e) => {
e.preventDefault();
audioMenu.classList.add('hidden');
clearAudio();
await clearAudio();
});
document.addEventListener('click', (e) => {
if (!audioMenuContainer.contains(e.target)) {
@@ -1217,16 +1312,23 @@
}
const noteRecord = await pb.collection(NOTES_COLLECTION).create(payload);
let attachmentRecord = null;
if (recordedBlob) {
const file = new File([recordedBlob], 'note-audio.webm', { type: recordedBlob.type || 'audio/webm' });
const fd = new FormData();
fd.append(ATTACHMENTS_FILE_FIELD, file);
fd.append(ATTACHMENTS_PBID_FIELD, noteRecord.id);
attachmentRecord = await pb.collection(ATTACHMENTS_COLLECTION).create(fd);
const attachmentRecords = [];
if (recordedSegments && recordedSegments.length) {
for (let i = 0; i < recordedSegments.length; i++) {
const seg = recordedSegments[i];
const blobToUpload = seg.blob || seg;
const fileName = seg.name || `note-audio-seg-${i + 1}.webm`;
const file = new File([blobToUpload], fileName, { type: blobToUpload.type || 'audio/webm' });
const fd = new FormData();
fd.append(ATTACHMENTS_FILE_FIELD, file);
fd.append(ATTACHMENTS_PBID_FIELD, noteRecord.id);
const createdAtt = await pb.collection(ATTACHMENTS_COLLECTION).create(fd);
attachmentRecords.push(createdAtt);
}
}
statusEl.textContent = `Saved note ${noteRecord.id}` + (attachmentRecord ? ` with audio ${attachmentRecord.id}` : '');
clearAudio();
const attachmentMsg = attachmentRecords.length ? ` with ${attachmentRecords.length} audio segment(s)` : '';
statusEl.textContent = `Saved note ${noteRecord.id}${attachmentMsg}`;
await clearAudio();
document.getElementById('noteText').value = '';
document.getElementById('noteTitle').value = '';
document.getElementById('jobNote').value = 'no';