|
112 | 112 | .script-preview { /* New style for the formatted script display */ |
113 | 113 | width: 100%; |
114 | 114 | min-height: 100px; /* Give it a minimum height */ |
| 115 | + max-height: 300px; /* Add a max-height to enable scrolling */ |
115 | 116 | padding: 10px; |
116 | 117 | border-radius: 4px; |
117 | 118 | border: 1px solid var(--input-border-color); |
|
125 | 126 | .script-preview strong { /* Style for speaker names */ |
126 | 127 | color: var(--success-text); |
127 | 128 | } |
| 129 | + .script-preview .invalid-speaker { |
| 130 | + color: var(--error-text); |
| 131 | + font-weight: bold; |
| 132 | + } |
128 | 133 | button, .button { |
129 | 134 | display: inline-block; |
130 | 135 | padding: 10px 15px; |
@@ -442,7 +447,8 @@ <h2>Generate HTML Demo</h2> |
442 | 447 | <h2>About Podcast Generator</h2> |
443 | 448 | <p>Version: <span id="app-version"></span></p> |
444 | 449 | <p>This application is an open-source project designed to simplify the creation of audio podcasts from text scripts.</p> |
445 | | - <p>If this application is useful to you, you can support its development: <a href="https://buymeacoffee.com/laurentftech" target="_blank">❤️ Buy Me a Coffee</a></p> |
| 450 | + <p>If this application is useful to you, you can support its development:</p> |
| 451 | + <p>❤️ <a href="https://buymeacoffee.com/laurentftech" target="_blank">Buy Me a Coffee</a></p> |
446 | 452 | <hr> |
447 | 453 | <h3>Core Technologies</h3> |
448 | 454 | <ul style="text-align: left;"> |
@@ -552,71 +558,99 @@ <h3>Asset Credits</h3> |
552 | 558 | } |
553 | 559 |
|
554 | 560 | function cleanScript(text) { |
555 | | - return text.split('\n').filter(line => /^\s*\w+\s*:\s*.+/.test(line)).join('\n'); |
| 561 | + return text.split('\n').filter(line => /^\s*([^:]+?)\s*:\s*.+/.test(line)).join('\n'); |
556 | 562 | } |
557 | 563 |
|
558 | 564 | function validateSpeakersInUI() { |
559 | | - const provider = currentSettings.tts_provider || 'elevenlabs'; // Always use saved settings for main UI validation |
| 565 | + const provider = currentSettings.tts_provider || 'elevenlabs'; |
| 566 | + const scriptText = scriptTextarea.value; |
| 567 | + |
| 568 | + // Split into lines and only process lines that start with "Speaker:" |
| 569 | + const lines = scriptText.split('\n'); |
| 570 | + const speakersInScript = []; |
| 571 | + for (const line of lines) { |
| 572 | + const match = line.match(/^\s*([^:]+?)\s*:/); |
| 573 | + if (match) { |
| 574 | + speakersInScript.push(match[1].trim()); |
| 575 | + } |
| 576 | + } |
| 577 | + const uniqueSpeakers = [...new Set(speakersInScript)]; |
560 | 578 |
|
561 | | - if (provider !== 'gemini') { |
562 | | - speakerValidationErrorDiv.style.display = 'none'; |
563 | | - generateBtn.disabled = false; |
| 579 | + if (uniqueSpeakers.length === 0 && scriptText.trim().length > 0) { |
| 580 | + speakerValidationErrorDiv.textContent = "No valid speakers found. Use the 'Speaker: Dialogue' format."; |
| 581 | + speakerValidationErrorDiv.style.display = 'block'; |
| 582 | + generateBtn.disabled = true; |
564 | 583 | return; |
565 | 584 | } |
566 | 585 |
|
567 | | - const instructionText = instructionContainer.style.display !== 'none' ? instructionTextarea.value : ''; |
568 | | - const scriptText = scriptTextarea.value; |
569 | | - const combinedText = (instructionText.trim() ? instructionText + '\n' : '') + scriptText; |
| 586 | + const speakerVoices = (provider === 'elevenlabs') ? currentSettings.speaker_voices_elevenlabs : currentSettings.speaker_voices; |
| 587 | + const validSpeakers = new Set(Object.keys(speakerVoices || {})); |
| 588 | + const invalidSpeakers = uniqueSpeakers.filter(s => !validSpeakers.has(s)); |
570 | 589 |
|
571 | | - const speakersInScript = (combinedText.match(/^\s*(\w+)\s*:/gm) || []).map(s => s.match(/^\s*(\w+)\s*:/)[1]); |
572 | | - const uniqueSpeakers = [...new Set(speakersInScript)]; |
| 590 | + if (invalidSpeakers.length > 0) { |
| 591 | + speakerValidationErrorDiv.textContent = `The following speakers are not configured: ${invalidSpeakers.join(', ')}. Please configure them in Settings.`; |
| 592 | + speakerValidationErrorDiv.style.display = 'block'; |
| 593 | + generateBtn.disabled = true; |
| 594 | + return; |
| 595 | + } |
573 | 596 |
|
574 | | - if (uniqueSpeakers.length > 2) { |
| 597 | + if (provider === 'gemini' && uniqueSpeakers.length > 2) { |
575 | 598 | speakerValidationErrorDiv.textContent = `Gemini TTS supports a maximum of 2 speakers. Found ${uniqueSpeakers.length}: ${uniqueSpeakers.join(', ')}.`; |
576 | 599 | speakerValidationErrorDiv.style.display = 'block'; |
577 | 600 | generateBtn.disabled = true; |
578 | | - } else { |
579 | | - speakerValidationErrorDiv.style.display = 'none'; |
580 | | - generateBtn.disabled = false; |
| 601 | + return; |
581 | 602 | } |
| 603 | + |
| 604 | + speakerValidationErrorDiv.style.display = 'none'; |
| 605 | + generateBtn.disabled = false; |
582 | 606 | } |
583 | 607 |
|
584 | | - // Function to update the formatted script preview |
585 | 608 | function updateFormattedScriptPreview() { |
586 | 609 | const instructionText = instructionContainer.style.display !== 'none' ? instructionTextarea.value : ''; |
587 | 610 | const scriptText = scriptTextarea.value; |
588 | 611 |
|
589 | | - // Combine instruction and script for preview |
590 | | - const combinedText = (instructionText.trim() ? instructionText + '\n' : '') + scriptText; |
591 | | - const lines = combinedText.split('\n'); |
| 612 | + const provider = currentSettings.tts_provider || 'elevenlabs'; |
| 613 | + const speakerVoices = (provider === 'elevenlabs') ? currentSettings.speaker_voices_elevenlabs : currentSettings.speaker_voices; |
| 614 | + const validSpeakers = new Set(Object.keys(speakerVoices || {})); |
| 615 | + |
592 | 616 | let htmlLines = []; |
593 | | - let instructionBlockEnded = false; |
594 | 617 |
|
595 | | - for (const line of lines) { |
| 618 | + // Process instruction lines: just italicize them |
| 619 | + if (instructionText.trim()) { |
| 620 | + instructionText.split('\n').forEach(line => { |
| 621 | + htmlLines.push(`<em>${line.replace(/\[(.*?)\]/g, '<em>[$1]</em>')}</em>`); |
| 622 | + }); |
| 623 | + } |
| 624 | + |
| 625 | + // Process script lines: apply speaker validation |
| 626 | + scriptText.split('\n').forEach(line => { |
| 627 | + const speakerMatch = line.match(/^\s*([^:]+?)\s*:(.*)$/); |
596 | 628 | let processedLine = line; |
597 | | - |
598 | | - // Check if this line contains a speaker (marks the end of the instruction block) |
599 | | - const isSpeakerLine = /^\s*\w+\s*:/.test(line); |
600 | | - if (isSpeakerLine) { |
601 | | - instructionBlockEnded = true; |
602 | | - } |
603 | 629 |
|
604 | | - // Italicize bracketed text first, everywhere. |
605 | | - processedLine = processedLine.replace(/\[(.*?)\]/g, '<em>[$1]</em>'); |
| 630 | + if (speakerMatch) { |
| 631 | + const speakerName = speakerMatch[1].trim(); |
| 632 | + const speakerTag = speakerMatch[0].substring(0, speakerMatch[0].indexOf(':') + 1); |
| 633 | + const dialoguePart = speakerMatch[2]; |
| 634 | + |
| 635 | + let styledSpeakerTag; |
| 636 | + if (validSpeakers.has(speakerName)) { |
| 637 | + styledSpeakerTag = `<strong>${speakerTag}</strong>`; |
| 638 | + } else { |
| 639 | + styledSpeakerTag = `<span class="invalid-speaker">${speakerTag}</span>`; |
| 640 | + } |
606 | 641 |
|
607 | | - if (isSpeakerLine) { |
608 | | - // Bold speaker names |
609 | | - processedLine = processedLine.replace(/^(\s*)(\w+\s*):/m, '$1<strong>$2:</strong>'); |
610 | | - } else if (!instructionBlockEnded && line.trim() !== '') { |
611 | | - // If it's an instruction line (before any speaker) and not empty, wrap the whole line in italics. |
612 | | - processedLine = `<em>${processedLine}</em>`; |
| 642 | + processedLine = line.replace(/^\s*([^:]+?)\s*:/, styledSpeakerTag); |
| 643 | + processedLine = processedLine.replace(/\[(.*?)\]/g, '<em>[$1]</em>'); |
| 644 | + |
| 645 | + } else { |
| 646 | + processedLine = line.replace(/\[(.*?)\]/g, '<em>[$1]</em>'); |
613 | 647 | } |
614 | 648 |
|
615 | 649 | htmlLines.push(processedLine); |
616 | | - } |
| 650 | + }); |
617 | 651 |
|
618 | 652 | formattedScriptPreview.innerHTML = htmlLines.join('<br>'); |
619 | | - validateSpeakersInUI(); // Also validate speakers on every update |
| 653 | + validateSpeakersInUI(); |
620 | 654 | } |
621 | 655 |
|
622 | 656 | // --- Accordion Logic --- |
@@ -747,8 +781,8 @@ <h3>Asset Credits</h3> |
747 | 781 | const scriptText = scriptTextarea.value; // Use the actual textarea value |
748 | 782 | const lines = scriptText.split('\n'); |
749 | 783 | const speakersInScript = lines.map(line => { |
750 | | - const match = line.match(/^\s*(\w+)\s*:/); |
751 | | - return match ? match[1] : null; |
| 784 | + const match = line.match(/^\s*([^:]+?)\s*:/); |
| 785 | + return match ? match[1].trim() : null; |
752 | 786 | }).filter(Boolean); |
753 | 787 | const uniqueSpeakers = [...new Set(speakersInScript)]; |
754 | 788 |
|
|
0 commit comments