/** * Audio Generation Module * UPDATED: Panel-based generation (no chapter markers) * Generates audio for N blocks starting from the selected starting block */ // ============================================ // Panel-Based Generation // ============================================ async function generateFromPanel() { const textBlocks = getTextBlocks(); if (textBlocks.length === 0) { alert('No text blocks found to generate audio for.'); return; } if (!panelState.startingBlockId) { alert('No starting block selected.'); return; } const startIdx = getTextBlockIndex(panelState.startingBlockId); if (startIdx < 0) { alert('Starting block not found. Please select a valid block.'); return; } const count = panelState.blockCount || 10; const voice = panelState.voice || 'af_heart'; const endIdx = Math.min(startIdx + count, textBlocks.length); const blocksToGenerate = []; for (let i = startIdx; i < endIdx; i++) { const blockEl = textBlocks[i]; const textarea = blockEl.querySelector('.md-block-textarea'); const content = textarea ? textarea.value : ''; if (!content.trim()) continue; // Skip image content if (content.trim().startsWith('![') && content.trim().indexOf('](') !== -1) continue; const ttsText = (blockEl.dataset.ttsText && blockEl.dataset.ttsText.trim()) ? blockEl.dataset.ttsText : content; blocksToGenerate.push({ id: blockEl.id, text: ttsText, element: blockEl }); } if (blocksToGenerate.length === 0) { alert('No speakable text blocks found in the selected range.'); return; } // Disable generate button const genBtn = document.getElementById('ampGenerateBtn'); if (genBtn) genBtn.disabled = true; showLoader(`Generating Audio...`, `Processing ${blocksToGenerate.length} blocks`); let successCount = 0; let errorCount = 0; for (let i = 0; i < blocksToGenerate.length; i++) { const blockInfo = blocksToGenerate[i]; document.getElementById('loadingSubtext').textContent = `Block ${i + 1} of ${blocksToGenerate.length}`; try { const response = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: blockInfo.text, voice: voice, block_id: null }) }); const data = await response.json(); if (data.error) { console.error(`Block ${blockInfo.id} error:`, data.error); errorCount++; continue; } // Store audio data in editorBlocks const blockData = editorBlocks.find(b => b.id === blockInfo.id); if (blockData) { blockData.audio_data = data.audio_data; blockData.audio_format = data.audio_format; blockData.transcription = data.transcription; } // Update visual indicator const indicator = blockInfo.element.querySelector('.audio-indicator'); if (indicator) { indicator.classList.remove('no-audio'); indicator.classList.add('has-audio'); indicator.title = 'Audio generated'; } successCount++; } catch (error) { console.error(`Block ${blockInfo.id} error:`, error); errorCount++; } } hideLoader(); // Re-enable generate button if (genBtn) genBtn.disabled = false; if (errorCount > 0) { showNotification(`Generated ${successCount} blocks, ${errorCount} failed`, 'warning'); } else { showNotification(`Generated audio for ${successCount} blocks!`, 'success'); } // Update workflow to show audio is ready if (successCount > 0) { updateWorkflowProgress('audio-ready'); // Advance starting block advanceStartingBlockAfterGeneration(endIdx - startIdx); } } // ============================================ // Single Block Generation (from toolbar button) // ============================================ async function generateSingleBlockAudio(blockId) { const block = document.getElementById(blockId); if (!block) { console.error('Block not found:', blockId); return; } const blockType = block.dataset.blockType || 'paragraph'; if (blockType === 'image') { alert('Cannot generate audio for image blocks.'); return; } const textarea = block.querySelector('.md-block-textarea'); const content = textarea ? textarea.value : ''; if (!content.trim()) { alert('No text content to generate audio for.'); return; } if (content.trim().startsWith('![') && content.trim().indexOf('](') !== -1) { alert('Cannot generate audio for image blocks.'); return; } const ttsText = (block.dataset.ttsText && block.dataset.ttsText.trim()) ? block.dataset.ttsText : content; const voice = panelState.voice || 'af_heart'; showLoader('Generating Audio...', 'Creating speech and timestamps'); try { const response = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: ttsText, voice: voice, block_id: null }) }); const data = await response.json(); if (data.error) { throw new Error(data.error); } const blockData = editorBlocks.find(b => b.id === blockId); if (blockData) { blockData.audio_data = data.audio_data; blockData.audio_format = data.audio_format; blockData.transcription = data.transcription; } const indicator = block.querySelector('.audio-indicator'); if (indicator) { indicator.classList.remove('no-audio'); indicator.classList.add('has-audio'); indicator.title = 'Audio generated'; } hideLoader(); showNotification('Audio generated successfully!', 'success'); updateWorkflowProgress('audio-ready'); updatePanelUI(); } catch (error) { hideLoader(); console.error('Generation error:', error); alert('Failed to generate audio: ' + error.message); } } // Keep old function name for backward compatibility function generateBlockAudio(blockId) { generateSingleBlockAudio(blockId); }