Skip to content

Commit 10b71ec

Browse files
committed
Ensures consistent speaker format handling with updated parsing and validation.
1 parent 6f9c9d0 commit 10b71ec

File tree

2 files changed

+83
-41
lines changed

2 files changed

+83
-41
lines changed

generate_podcast.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def _parse_script_segments(self, script_text: str) -> List[Tuple[str, str]]:
273273
if not line:
274274
continue
275275

276-
match = re.match(r"^(\w+)\s*:\s*(.+)$", line)
276+
match = re.match(r"^\s*([^:]+?)\s*:\s*(.+)$", line)
277277

278278
if match:
279279
# This is a new speaker line.
@@ -345,7 +345,15 @@ def _ffmpeg_convert_inline_audio_chunks(audio_chunks: List[bytes], mime_type: st
345345

346346

347347
def validate_speakers(script_text: str, app_settings: Dict[str, Any]) -> Tuple[List[str], List[str]]:
348-
script_speakers = set(re.findall(r"^\s*(\w+)\s*:", script_text, re.MULTILINE))
348+
# Only extract speakers from lines that are actual speaker declarations
349+
# (not continuation lines within a dialogue block)
350+
raw_speakers = []
351+
for line in script_text.splitlines():
352+
match = re.match(r"^\s*([^:]+?)\s*:\s*(.+)$", line)
353+
if match:
354+
raw_speakers.append(match.group(1).strip())
355+
script_speakers = set(raw_speakers)
356+
349357
if not script_speakers:
350358
return [], []
351359
provider_name = app_settings.get("tts_provider", "gemini").lower()

templates/index.html

Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
.script-preview { /* New style for the formatted script display */
113113
width: 100%;
114114
min-height: 100px; /* Give it a minimum height */
115+
max-height: 300px; /* Add a max-height to enable scrolling */
115116
padding: 10px;
116117
border-radius: 4px;
117118
border: 1px solid var(--input-border-color);
@@ -125,6 +126,10 @@
125126
.script-preview strong { /* Style for speaker names */
126127
color: var(--success-text);
127128
}
129+
.script-preview .invalid-speaker {
130+
color: var(--error-text);
131+
font-weight: bold;
132+
}
128133
button, .button {
129134
display: inline-block;
130135
padding: 10px 15px;
@@ -442,7 +447,8 @@ <h2>Generate HTML Demo</h2>
442447
<h2>About Podcast Generator</h2>
443448
<p>Version: <span id="app-version"></span></p>
444449
<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>
446452
<hr>
447453
<h3>Core Technologies</h3>
448454
<ul style="text-align: left;">
@@ -552,71 +558,99 @@ <h3>Asset Credits</h3>
552558
}
553559

554560
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');
556562
}
557563

558564
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)];
560578

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;
564583
return;
565584
}
566585

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));
570589

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+
}
573596

574-
if (uniqueSpeakers.length > 2) {
597+
if (provider === 'gemini' && uniqueSpeakers.length > 2) {
575598
speakerValidationErrorDiv.textContent = `Gemini TTS supports a maximum of 2 speakers. Found ${uniqueSpeakers.length}: ${uniqueSpeakers.join(', ')}.`;
576599
speakerValidationErrorDiv.style.display = 'block';
577600
generateBtn.disabled = true;
578-
} else {
579-
speakerValidationErrorDiv.style.display = 'none';
580-
generateBtn.disabled = false;
601+
return;
581602
}
603+
604+
speakerValidationErrorDiv.style.display = 'none';
605+
generateBtn.disabled = false;
582606
}
583607

584-
// Function to update the formatted script preview
585608
function updateFormattedScriptPreview() {
586609
const instructionText = instructionContainer.style.display !== 'none' ? instructionTextarea.value : '';
587610
const scriptText = scriptTextarea.value;
588611

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+
592616
let htmlLines = [];
593-
let instructionBlockEnded = false;
594617

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*:(.*)$/);
596628
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-
}
603629

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+
}
606641

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>');
613647
}
614648

615649
htmlLines.push(processedLine);
616-
}
650+
});
617651

618652
formattedScriptPreview.innerHTML = htmlLines.join('<br>');
619-
validateSpeakersInUI(); // Also validate speakers on every update
653+
validateSpeakersInUI();
620654
}
621655

622656
// --- Accordion Logic ---
@@ -747,8 +781,8 @@ <h3>Asset Credits</h3>
747781
const scriptText = scriptTextarea.value; // Use the actual textarea value
748782
const lines = scriptText.split('\n');
749783
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;
752786
}).filter(Boolean);
753787
const uniqueSpeakers = [...new Set(speakersInScript)];
754788

0 commit comments

Comments
 (0)