Implement lazy audio loading to fix large response truncation

This commit is contained in:
Ashim Kumar
2026-05-23 07:45:02 +06:00
parent 36a842dc60
commit e0e3b65c75
3 changed files with 139 additions and 164 deletions

View File

@@ -1,7 +1,6 @@
/**
* Audiobook Maker Pro v4.2 - Main Application
* UPDATED: Publishing support, thumbnails, sections terminology
* FIXED: Edit/rename in new archive UI, republish populates existing data
* UPDATED: Lazy audio loading to avoid large response truncation
*/
// ============================================
@@ -20,7 +19,7 @@ let ttsEditModal = null;
let publishModal = null;
let publishingProjectId = null;
let currentWorkflowStage = 'upload';
let allArchiveProjects = []; // Cache for republish dialog
let allArchiveProjects = [];
// ============================================
// Initialization
@@ -398,7 +397,7 @@ function updateWorkflowProgress(stage) {
}
// ============================================
// Helper: Switch to Editor Tab
// Helpers
// ============================================
function switchToEditorTab() {
@@ -409,10 +408,6 @@ function switchToEditorTab() {
}
}
// ============================================
// Helper: Start from scratch
// ============================================
function startFromScratch() {
document.getElementById('uploadSection').style.display = 'none';
document.getElementById('editorSection').style.display = 'block';
@@ -432,10 +427,6 @@ function startFromScratch() {
}
}
// ============================================
// Loading Overlay
// ============================================
function showLoader(text = 'Processing...', subtext = 'Please wait') {
const overlay = document.getElementById('loadingOverlay');
if(overlay) {
@@ -591,7 +582,6 @@ async function openProjectArchive() {
const response = await fetch('/api/projects');
const data = await response.json();
// Cache for republish dialog
allArchiveProjects = data.projects || [];
const container = document.getElementById('projectList');
@@ -616,7 +606,6 @@ async function openProjectArchive() {
: '';
const canPublish = project.audio_count > 0;
const safeNameAttr = escapeHtml(project.name).replace(/'/g, "'");
return `
<div class="project-item-v2" id="project-item-${project.id}">
@@ -694,7 +683,7 @@ async function openProjectArchive() {
}
// ============================================
// Rename Functionality (FIXED for new UI)
// Rename
// ============================================
function startEditProjectName(projectId) {
@@ -755,18 +744,15 @@ async function saveProjectName(projectId) {
return;
}
// Update the text in the cached element
const textEl = document.getElementById(`project-name-text-${projectId}`);
if (textEl) textEl.textContent = newName;
// Update cached project list
const cached = allArchiveProjects.find(p => p.id === projectId);
if (cached) cached.name = newName;
cancelEditProjectName(projectId);
showNotification('Project renamed successfully', 'success');
// Update header if currently loaded project was renamed
if (currentProject.id === projectId) {
currentProject.name = newName;
const nameInput = document.getElementById('projectName');
@@ -820,13 +806,12 @@ async function uploadThumbnail(projectId, inputEl) {
}
// ============================================
// Publishing Functions (FIXED to populate existing data)
// Publishing
// ============================================
function openPublishDialog(projectId) {
publishingProjectId = projectId;
// Find project in cache to populate existing data
const project = allArchiveProjects.find(p => p.id === projectId);
if (!publishModal) {
@@ -871,7 +856,6 @@ function openPublishDialog(projectId) {
publishModal = new bootstrap.Modal(document.getElementById('publishModal'));
}
// Populate with existing data (FIX: was always empty before)
document.getElementById('pub-name').value = project ? project.name : '';
document.getElementById('pub-author').value = project ? (project.author || '') : '';
document.getElementById('pub-description').value = project ? (project.description || '') : '';
@@ -926,14 +910,15 @@ async function unpublishProject(projectId) {
}
// ============================================
// Project Load / Delete
// Project Load / Delete - LAZY AUDIO LOADING
// ============================================
async function loadProject(projectId) {
showLoader('Loading project...');
showLoader('Loading project...', 'Fetching metadata');
if(archiveModal) archiveModal.hide();
try {
// Step 1: Load lightweight metadata (no audio_data)
const response = await fetch(`/api/projects/${projectId}`);
const data = await response.json();
@@ -955,27 +940,93 @@ async function loadProject(projectId) {
renderProjectInEditor(data);
}
let hasAudio = false;
// Step 2: Find blocks that need audio fetched
const audioBlocks = [];
for (const ch of data.chapters) {
for (const bl of ch.blocks) {
if (bl.audio_data && bl.block_type !== 'image') {
hasAudio = true;
break;
if (bl.has_audio && bl.block_type !== 'image') {
audioBlocks.push(bl.id);
}
}
if (hasAudio) break;
}
updateWorkflowProgress(hasAudio ? 'audio-ready' : 'edit');
updateWorkflowProgress(audioBlocks.length > 0 ? 'audio-ready' : 'edit');
hideLoader();
showNotification('Project loaded successfully!', 'success');
if (audioBlocks.length === 0) {
showNotification('Project loaded successfully!', 'success');
return;
}
// Step 3: Lazy-load audio in background, parallel batches
showNotification(`Project loaded. Fetching ${audioBlocks.length} audio blocks in background...`, 'info');
loadAudioBlocksInBackground(projectId, audioBlocks);
} catch (error) {
hideLoader();
console.error('Load project error:', error);
alert('Failed to load project: ' + error.message);
}
}
async function loadAudioBlocksInBackground(projectId, blockIds) {
const BATCH_SIZE = 5;
let loaded = 0;
let failed = 0;
async function fetchOne(blockId) {
try {
const resp = await fetch(`/api/projects/${projectId}/audio/${blockId}`);
const data = await resp.json();
if (data.error || !data.audio_data) {
failed++;
return;
}
// Update editorBlocks state by db_id
const blockData = editorBlocks.find(b => b.db_id === blockId);
if (blockData) {
blockData.audio_data = data.audio_data;
blockData.audio_format = data.audio_format;
// Update DOM indicator (green dot)
const blockEl = document.getElementById(blockData.id);
if (blockEl) {
const indicator = blockEl.querySelector('.audio-indicator');
if (indicator) {
indicator.classList.remove('no-audio');
indicator.classList.add('has-audio');
indicator.title = 'Audio loaded';
}
}
}
loaded++;
} catch (e) {
failed++;
console.warn(`Failed to load audio for block ${blockId}:`, e);
}
}
for (let i = 0; i < blockIds.length; i += BATCH_SIZE) {
const batch = blockIds.slice(i, i + BATCH_SIZE);
await Promise.all(batch.map(fetchOne));
}
const msg = failed > 0
? `Loaded ${loaded}/${blockIds.length} audio blocks (${failed} failed)`
: `All ${loaded} audio blocks loaded ✓`;
showNotification(msg, failed > 0 ? 'warning' : 'success');
if (typeof updatePanelUI === 'function') {
updatePanelUI();
}
}
async function deleteProject(projectId) {
if (!confirm('Are you sure you want to delete this project? This action cannot be undone.')) return;

View File

@@ -1,10 +1,6 @@
/**
* Markdown Editor Module
* UPDATED: Data-driven section markers (stored in editorBlocks array)
* REMOVED: duplicate renderDocumentBlocks (now only in pdf-handler.js)
* FIXED: repairAllNewBlockLines no longer removes lines after section-dividers
* FIXED: removeSection is data-driven, no orphan dividers
* ADDED: addSectionAtLine function for Custom Section Marker button
* UPDATED: Lazy audio loading support — tracks db_id for each block
*/
// ============================================
@@ -15,7 +11,6 @@ let editorBlocks = [];
let activeBlockId = null;
let isToolbarClick = false;
// Panel state
let panelState = {
startingBlockId: null,
blockCount: 10,
@@ -31,7 +26,6 @@ function initMarkdownEditor() {
const editor = document.getElementById('markdownEditor');
editor.addEventListener('click', function(e) {
// Pick mode: clicking a block sets it as starting block
if (panelState.pickMode) {
const blockEl = e.target.closest('.md-block');
if (blockEl && !blockEl.classList.contains('editing')) {
@@ -66,7 +60,7 @@ function initMarkdownEditor() {
});
restorePanelSettings();
console.log('📝 Markdown editor initialized (data-driven sections)');
console.log('📝 Markdown editor initialized (lazy audio loading)');
}
// ============================================
@@ -81,7 +75,6 @@ function updatePanelUI() {
const textBlocks = getTextBlocks();
const totalBlocks = textBlocks.length;
// Update total blocks stat with NULL checks
const totalEl = document.getElementById('ampTotalBlocks');
if (totalEl) totalEl.textContent = totalBlocks;
@@ -96,7 +89,6 @@ function updatePanelUI() {
const remainEl = document.getElementById('ampRemainingBlocks');
if (remainEl) remainEl.textContent = (totalBlocks - genCount);
// Validate starting block
if (!panelState.startingBlockId || !document.getElementById(panelState.startingBlockId)) {
if (textBlocks.length > 0) {
panelState.startingBlockId = textBlocks[0].id;
@@ -302,7 +294,7 @@ function advanceStartingBlockAfterGeneration(generatedCount) {
}
// ============================================
// DATA-DRIVEN Section Marker System
// Section Markers
// ============================================
function makeSectionStart(blockId, title = null) {
@@ -399,7 +391,7 @@ function renderDocumentOutline() {
}
// ============================================
// Block Merge & Split Logic
// Block Merge & Split
// ============================================
function mergeBlockUp(blockId) {
@@ -575,6 +567,8 @@ function addBlock(type = 'paragraph', content = '', afterElement = null, images
editorBlocks.push({
id: blockId,
db_id: null, // Database ID — set when loaded from server
has_audio: false, // Server-reported audio presence
type: type,
content: content,
images: images,
@@ -1112,6 +1106,8 @@ function renderProjectInEditor(projectData) {
blockData.audio_format = block.audio_format;
blockData.transcription = block.transcription;
blockData.tts_text = block.tts_text;
blockData.db_id = block.id; // Track DB ID for lazy audio loading
blockData.has_audio = !!block.has_audio; // Server-reported audio presence
}
const blockEl = document.getElementById(blockId);
@@ -1119,13 +1115,14 @@ function renderProjectInEditor(projectData) {
blockEl.dataset.ttsText = block.tts_text;
}
if (block.audio_data && block.block_type !== 'image') {
// Show audio indicator based on has_audio flag (audio will be lazy-loaded)
if (block.has_audio && block.block_type !== 'image') {
if (blockEl) {
const indicator = blockEl.querySelector('.audio-indicator');
if (indicator) {
indicator.classList.remove('no-audio');
indicator.classList.add('has-audio');
indicator.title = 'Audio generated';
indicator.title = 'Audio loading...';
}
}
}
@@ -1143,7 +1140,7 @@ function renderProjectInEditor(projectData) {
let foundStart = false;
for (const tb of textBlocks) {
const data = editorBlocks.find(b => b.id === tb.id);
if (!data || !data.audio_data) {
if (!data || !data.has_audio) {
panelState.startingBlockId = tb.id;
foundStart = true;
break;
@@ -1153,12 +1150,11 @@ function renderProjectInEditor(projectData) {
panelState.startingBlockId = textBlocks[textBlocks.length - 1].id;
}
// যুক্ত করা হলো: প্রজেক্ট লোড হলেও প্যানেলের ব্লক কাউন্ট যেন মোট ব্লকের সমান থাকে
if (textBlocks.length > 0) {
panelState.blockCount = textBlocks.length;
panelState.blockCount = textBlocks.length;
}
updatePanelUI();
renderDocumentOutline();
checkEmptyEditor();
}
}