Fix TTS cleanText function to properly extract text from HTML

The cleanText() function was using broken regex patterns that resulted in mangled HTML fragments being returned instead of clean text. This caused TTS to only speak the last segment of responses. Changes: - Replace broken regex HTML parsing with DOMParser (browser native) - Properly extract text content while preserving all readable text - Replace <pre> and <code> tags with placeholder before text extraction - Add fallback for edge cases where DOMParser might fail Before: cleanText output was '</pre> distribution in version.</p>' After: cleanText output is 'Linux Distribution...See code attached...version.' Fixes #901 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

Frederic Thomas committed Jan 14, 2026 at 13:57 UTC 9b6be5214cedc9a08cf4d488fe67db39803afc50
1 file changed +22 -48
webui/components/chat/speech/speech-store.js
+22 -48
@@ -499,59 +499,33 @@ const model = {
499 const codePlaceholder = SUB + "code" + SUB;
500 const tablePlaceholder = SUB + "table" + SUB;
501
502 - // Helper function to handle both closed and unclosed patterns
503 - // replacement can be a string or null (to remove)
504 - function handlePatterns(
505 - inputText,
506 - closedPattern,
507 - unclosedPattern,
508 - replacement
509 - ) {
510 - // Process closed patterns first
511 - let processed = inputText.replace(closedPattern, replacement || "");
512 -
513 - // If the text changed, it means we found and replaced closed patterns
514 - if (processed !== inputText) {
515 - return processed;
516 - } else {
517 - // No closed patterns found, check for unclosed ones
518 - const unclosedMatch = inputText.match(unclosedPattern);
519 - if (unclosedMatch) {
520 - // Replace the unclosed pattern
521 - return inputText.replace(unclosedPattern, replacement || "");
522 - }
523 - }
524 -
525 - // No patterns found, return original
526 - return inputText;
527 - }
528 -
529 - // Handle code blocks
530 - text = handlePatterns(
531 - text,
532 - /```(?:[a-zA-Z0-9]*\n)?[\s\S]*?```/g, // closed code blocks
533 - /```(?:[a-zA-Z0-9]*\n)?[\s\S]*$/g, // unclosed code blocks
534 - codePlaceholder
535 - );
502 + // Handle code blocks BEFORE HTML parsing (markdown code blocks)
503 + text = text.replace(/```(?:[a-zA-Z0-9]*\n)?[\s\S]*?```/g, codePlaceholder); // closed code blocks
504 + text = text.replace(/```(?:[a-zA-Z0-9]*\n)?[\s\S]*$/g, codePlaceholder); // unclosed code blocks
505
506 // Replace inline code ticks with content preserved
507 text = text.replace(/`([^`]*)`/g, "$1"); // remove backticks but keep content
508
540 - // Handle HTML tags
541 - text = handlePatterns(
542 - text,
543 - /<[a-zA-Z][a-zA-Z0-9]*>.*?<\/[a-zA-Z][a-zA-Z0-9]*>/gs, // closed HTML tags
544 - /<[a-zA-Z][a-zA-Z0-9]*>[\s\S]*$/g, // unclosed HTML tags
545 - "" // remove HTML tags completely
546 - );
509 + // Parse HTML using browser's DOMParser to properly extract text content
510 + try {
511 + const parser = new DOMParser();
512 + // Wrap in a div to handle fragments
513 + const doc = parser.parseFromString(`<div>${text}</div>`, 'text/html');
514
548 - // Handle self-closing HTML tags
549 - text = handlePatterns(
550 - text,
551 - /<[a-zA-Z][a-zA-Z0-9]*(\/| [^>]*\/>)/g, // complete self-closing tags
552 - /<[a-zA-Z][a-zA-Z0-9]* [^>]*$/g, // incomplete self-closing tags
553 - ""
554 - );
515 + // Replace <pre> and <code> tags with placeholder before extracting text
516 + doc.querySelectorAll('pre, code').forEach(el => {
517 + el.textContent = codePlaceholder;
518 + });
519 +
520 + // Extract text content (this strips all HTML tags properly)
521 + text = doc.body.textContent || "";
522 + } catch (e) {
523 + // Fallback: simple tag stripping if DOMParser fails
524 + console.warn("[Speech Store] DOMParser failed, using fallback:", e);
525 + text = text.replace(/<pre[^>]*>[\s\S]*?<\/pre>/gi, codePlaceholder);
526 + text = text.replace(/<code[^>]*>[\s\S]*?<\/code>/gi, codePlaceholder);
527 + text = text.replace(/<[^>]+>/g, ''); // strip remaining tags
528 + }
529
530 // Remove markdown links: [label](url) → label
531 text = text.replace(/\[([^\]]+)\]\([^\)]+\)/g, "$1");