Initial release: DictIA v0.8.14-alpha (fork de Speakr, AGPL-3.0)
This commit is contained in:
450
static/js/modules/composables/reprocess.js
Normal file
450
static/js/modules/composables/reprocess.js
Normal file
@@ -0,0 +1,450 @@
|
||||
/**
|
||||
* Reprocessing composable
|
||||
* Handles reprocessing transcription and summary
|
||||
*/
|
||||
|
||||
import * as IncognitoStorage from '../db/incognito-storage.js';
|
||||
|
||||
export function useReprocess(state, utils) {
|
||||
const { nextTick } = Vue;
|
||||
|
||||
const {
|
||||
showReprocessModal, showResetModal, reprocessType,
|
||||
reprocessRecording, recordingToReset, selectedRecording,
|
||||
recordings, asrReprocessOptions, summaryReprocessPromptSource,
|
||||
summaryReprocessSelectedTagId, summaryReprocessCustomPrompt,
|
||||
availableTags, processingProgress, processingMessage,
|
||||
currentlyProcessingFile, uploadQueue
|
||||
} = state;
|
||||
|
||||
const { showToast, setGlobalError, onChatComplete } = utils;
|
||||
|
||||
// Store for active polling intervals
|
||||
const reprocessingPolls = new Map();
|
||||
|
||||
// =========================================
|
||||
// Reprocess Modal
|
||||
// =========================================
|
||||
|
||||
const openReprocessModal = (type, recording = null) => {
|
||||
reprocessType.value = type;
|
||||
reprocessRecording.value = recording || selectedRecording.value;
|
||||
showReprocessModal.value = true;
|
||||
|
||||
// Reset options
|
||||
if (type === 'transcription') {
|
||||
asrReprocessOptions.language = '';
|
||||
asrReprocessOptions.min_speakers = '';
|
||||
asrReprocessOptions.max_speakers = '';
|
||||
} else {
|
||||
summaryReprocessPromptSource.value = 'default';
|
||||
summaryReprocessSelectedTagId.value = '';
|
||||
summaryReprocessCustomPrompt.value = '';
|
||||
}
|
||||
};
|
||||
|
||||
const closeReprocessModal = () => {
|
||||
showReprocessModal.value = false;
|
||||
reprocessRecording.value = null;
|
||||
reprocessType.value = null;
|
||||
};
|
||||
|
||||
const confirmReprocess = openReprocessModal;
|
||||
const cancelReprocess = closeReprocessModal;
|
||||
|
||||
// =========================================
|
||||
// Reset Status
|
||||
// =========================================
|
||||
|
||||
const confirmReset = (recording) => {
|
||||
recordingToReset.value = recording;
|
||||
showResetModal.value = true;
|
||||
};
|
||||
|
||||
const cancelReset = () => {
|
||||
showResetModal.value = false;
|
||||
recordingToReset.value = null;
|
||||
};
|
||||
|
||||
const executeReset = async () => {
|
||||
if (!recordingToReset.value) return;
|
||||
|
||||
const recordingId = recordingToReset.value.id;
|
||||
|
||||
// Close the modal first
|
||||
cancelReset();
|
||||
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const response = await fetch(`/recording/${recordingId}/reset_status`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to reset recording status');
|
||||
|
||||
// Update recording status in list
|
||||
const index = recordings.value.findIndex(r => r.id === recordingId);
|
||||
if (index !== -1) {
|
||||
recordings.value[index].status = 'FAILED';
|
||||
}
|
||||
|
||||
if (selectedRecording.value?.id === recordingId) {
|
||||
selectedRecording.value.status = 'FAILED';
|
||||
}
|
||||
|
||||
showToast('Recording status reset to FAILED', 'fa-undo');
|
||||
} catch (error) {
|
||||
setGlobalError(`Failed to reset status: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const executeReprocess = async () => {
|
||||
if (!reprocessRecording.value || !reprocessType.value) return;
|
||||
|
||||
const recordingId = reprocessRecording.value.id;
|
||||
const type = reprocessType.value;
|
||||
|
||||
closeReprocessModal();
|
||||
|
||||
if (type === 'transcription') {
|
||||
await reprocessTranscription(
|
||||
recordingId,
|
||||
asrReprocessOptions.language,
|
||||
asrReprocessOptions.min_speakers,
|
||||
asrReprocessOptions.max_speakers
|
||||
);
|
||||
} else {
|
||||
await reprocessSummary(
|
||||
recordingId,
|
||||
summaryReprocessPromptSource.value,
|
||||
summaryReprocessSelectedTagId.value,
|
||||
summaryReprocessCustomPrompt.value
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Transcription Reprocessing
|
||||
// =========================================
|
||||
|
||||
const reprocessTranscription = async (recordingId, language, minSpeakers, maxSpeakers) => {
|
||||
if (!recordingId) {
|
||||
setGlobalError('No recording ID provided for reprocessing.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const requestBody = {
|
||||
language: language || '' // Always send language - empty string means auto-detect
|
||||
};
|
||||
if (minSpeakers && minSpeakers !== '') requestBody.min_speakers = parseInt(minSpeakers);
|
||||
if (maxSpeakers && maxSpeakers !== '') requestBody.max_speakers = parseInt(maxSpeakers);
|
||||
|
||||
const response = await fetch(`/recording/${recordingId}/reprocess_transcription`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to start transcription reprocessing');
|
||||
|
||||
// Update recording status in list
|
||||
const index = recordings.value.findIndex(r => r.id === recordingId);
|
||||
if (index !== -1) {
|
||||
recordings.value[index].status = 'PROCESSING';
|
||||
}
|
||||
|
||||
if (selectedRecording.value?.id === recordingId) {
|
||||
selectedRecording.value.status = 'PROCESSING';
|
||||
}
|
||||
|
||||
showToast('Transcription reprocessing started', 'fa-sync-alt');
|
||||
|
||||
// Start polling for progress
|
||||
startReprocessingPoll(recordingId);
|
||||
} catch (error) {
|
||||
setGlobalError(`Failed to start transcription reprocessing: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Summary Reprocessing
|
||||
// =========================================
|
||||
|
||||
const reprocessSummary = async (recordingId, promptSource, selectedTagId, customPrompt) => {
|
||||
if (!recordingId) {
|
||||
setGlobalError('No recording ID provided for reprocessing.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
const requestBody = { reprocess_summary: true };
|
||||
|
||||
if (promptSource === 'tag' && selectedTagId) {
|
||||
const selectedTag = availableTags.value.find(t => t.id == selectedTagId);
|
||||
if (selectedTag && selectedTag.custom_prompt) {
|
||||
requestBody.custom_prompt = selectedTag.custom_prompt;
|
||||
}
|
||||
} else if (promptSource === 'custom' && customPrompt) {
|
||||
requestBody.custom_prompt = customPrompt;
|
||||
}
|
||||
|
||||
const response = await fetch(`/recording/${recordingId}/reprocess_summary`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to start summary reprocessing');
|
||||
|
||||
// Update recording status in list
|
||||
const index = recordings.value.findIndex(r => r.id === recordingId);
|
||||
if (index !== -1) {
|
||||
recordings.value[index].status = 'SUMMARIZING';
|
||||
}
|
||||
|
||||
if (selectedRecording.value?.id === recordingId) {
|
||||
selectedRecording.value.status = 'SUMMARIZING';
|
||||
}
|
||||
|
||||
showToast('Summary reprocessing started', 'fa-sync-alt');
|
||||
|
||||
// Start polling for progress
|
||||
startReprocessingPoll(recordingId);
|
||||
} catch (error) {
|
||||
setGlobalError(`Failed to start summary reprocessing: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Generate Summary
|
||||
// =========================================
|
||||
|
||||
const generateSummary = async () => {
|
||||
if (!selectedRecording.value) return;
|
||||
|
||||
try {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
|
||||
|
||||
// Check if this is an incognito recording
|
||||
if (selectedRecording.value.incognito === true) {
|
||||
// Use incognito summary endpoint - generate synchronously
|
||||
const response = await fetch('/api/recordings/incognito/summary', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
transcription: selectedRecording.value.transcription
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to generate summary');
|
||||
|
||||
// Update the incognito recording with the new summary
|
||||
selectedRecording.value.summary = data.summary;
|
||||
selectedRecording.value.summary_html = data.summary_html;
|
||||
|
||||
// Update sessionStorage
|
||||
IncognitoStorage.updateIncognitoRecording({
|
||||
summary: data.summary,
|
||||
summary_html: data.summary_html
|
||||
});
|
||||
|
||||
showToast('Summary generated', 'fa-file-alt');
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular recording - use existing flow
|
||||
const response = await fetch(`/recording/${selectedRecording.value.id}/generate_summary`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Failed to start summary generation');
|
||||
|
||||
selectedRecording.value.status = 'SUMMARIZING';
|
||||
|
||||
const recordingInList = recordings.value.find(r => r.id === selectedRecording.value.id);
|
||||
if (recordingInList) {
|
||||
recordingInList.status = 'SUMMARIZING';
|
||||
}
|
||||
|
||||
showToast('Summary generation started', 'fa-file-alt');
|
||||
|
||||
// Start polling for progress
|
||||
startReprocessingPoll(selectedRecording.value.id);
|
||||
} catch (error) {
|
||||
setGlobalError(`Failed to generate summary: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
// =========================================
|
||||
// Progress Polling
|
||||
// =========================================
|
||||
|
||||
const startReprocessingPoll = (recordingId) => {
|
||||
// Stop existing poll if any
|
||||
stopReprocessingPoll(recordingId);
|
||||
|
||||
// Track if we've already fetched full data for SUMMARIZING status
|
||||
let hasFetchedForSummarizing = false;
|
||||
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
// Use lightweight status-only endpoint
|
||||
const response = await fetch(`/recording/${recordingId}/status`);
|
||||
if (!response.ok) throw new Error('Status check failed');
|
||||
|
||||
const statusData = await response.json();
|
||||
|
||||
// Update status in recordings list
|
||||
const index = recordings.value.findIndex(r => r.id === recordingId);
|
||||
|
||||
if (index !== -1) {
|
||||
// Create new object to ensure Vue reactivity
|
||||
recordings.value[index] = {
|
||||
...recordings.value[index],
|
||||
status: statusData.status
|
||||
};
|
||||
}
|
||||
|
||||
// Update selectedRecording with new object reference for reactivity
|
||||
if (selectedRecording.value?.id === recordingId) {
|
||||
selectedRecording.value = {
|
||||
...selectedRecording.value,
|
||||
status: statusData.status
|
||||
};
|
||||
}
|
||||
|
||||
// Check if summarization has started (fetch transcript) or processing is complete
|
||||
if (statusData.status === 'SUMMARIZING' || statusData.status === 'COMPLETED') {
|
||||
// Only fetch once when status first becomes SUMMARIZING
|
||||
const shouldFetch = (statusData.status === 'SUMMARIZING' && !hasFetchedForSummarizing) ||
|
||||
statusData.status === 'COMPLETED';
|
||||
|
||||
if (shouldFetch) {
|
||||
// Mark that we've fetched for SUMMARIZING
|
||||
if (statusData.status === 'SUMMARIZING') {
|
||||
hasFetchedForSummarizing = true;
|
||||
}
|
||||
|
||||
// Only stop polling if COMPLETED, keep polling during SUMMARIZING
|
||||
if (statusData.status === 'COMPLETED') {
|
||||
stopReprocessingPoll(recordingId);
|
||||
}
|
||||
|
||||
// Fetch the full recording with updated data
|
||||
const fullResponse = await fetch(`/api/recordings/${recordingId}`);
|
||||
|
||||
if (fullResponse.ok) {
|
||||
const fullData = await fullResponse.json();
|
||||
|
||||
// Update in recordings list first
|
||||
const currentIndex = recordings.value.findIndex(r => r.id === recordingId);
|
||||
if (currentIndex !== -1) {
|
||||
recordings.value[currentIndex] = fullData;
|
||||
}
|
||||
|
||||
// Always update selectedRecording if it's the current recording
|
||||
if (selectedRecording.value?.id === recordingId) {
|
||||
selectedRecording.value = fullData;
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
|
||||
if (statusData.status === 'COMPLETED') {
|
||||
showToast('Processing completed!', 'fa-check-circle');
|
||||
// Refresh token budget after LLM operations complete
|
||||
if (onChatComplete) onChatComplete();
|
||||
}
|
||||
}
|
||||
} else if (statusData.status === 'FAILED') {
|
||||
stopReprocessingPoll(recordingId);
|
||||
|
||||
// Fetch full recording data to get error details for display
|
||||
try {
|
||||
const failedResponse = await fetch(`/api/recordings/${recordingId}`);
|
||||
if (failedResponse.ok) {
|
||||
const failedData = await failedResponse.json();
|
||||
|
||||
// Update in recordings list
|
||||
const currentIndex = recordings.value.findIndex(r => r.id === recordingId);
|
||||
if (currentIndex !== -1) {
|
||||
recordings.value[currentIndex] = failedData;
|
||||
}
|
||||
|
||||
// Update selectedRecording to show error in transcription panel
|
||||
if (selectedRecording.value?.id === recordingId) {
|
||||
selectedRecording.value = failedData;
|
||||
await nextTick();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch error details:', err);
|
||||
}
|
||||
|
||||
showToast('Processing failed', 'fa-exclamation-circle');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Polling error:', error);
|
||||
stopReprocessingPoll(recordingId);
|
||||
}
|
||||
}, 3000);
|
||||
|
||||
reprocessingPolls.set(recordingId, pollInterval);
|
||||
};
|
||||
|
||||
const stopReprocessingPoll = (recordingId) => {
|
||||
if (reprocessingPolls.has(recordingId)) {
|
||||
clearInterval(reprocessingPolls.get(recordingId));
|
||||
reprocessingPolls.delete(recordingId);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
// Reprocess modal
|
||||
openReprocessModal,
|
||||
closeReprocessModal,
|
||||
confirmReprocess,
|
||||
cancelReprocess,
|
||||
executeReprocess,
|
||||
|
||||
// Reset status
|
||||
confirmReset,
|
||||
cancelReset,
|
||||
executeReset,
|
||||
|
||||
// Transcription
|
||||
reprocessTranscription,
|
||||
|
||||
// Summary
|
||||
reprocessSummary,
|
||||
generateSummary,
|
||||
|
||||
// Polling
|
||||
startReprocessingPoll,
|
||||
stopReprocessingPoll
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user