diff --git a/src/tts-modal.js b/src/tts-modal.js index 19f50b0..7d8f8b1 100644 --- a/src/tts-modal.js +++ b/src/tts-modal.js @@ -122,18 +122,22 @@ export function openTTSModal() { if (USE_CUSTOM_STYLING) { textArea.style.cssText = ` width: 100%; - height: 240px; + height: 100px; + resize: vertical; + min-height: 60px; `; } else { textArea.style.cssText = ` width: 100%; - height: 240px; + height: 100px; background: rgba(255,255,255,0.1); color: inherit; border: 1px solid rgba(128,128,128,0.3); border-radius: 8px; backdrop-filter: blur(10px); padding: 0.5rem; + resize: vertical; + min-height: 60px; `; } textArea.placeholder = getUIText("ttsPlaceholder"); diff --git a/src/tts.js b/src/tts.js index 5d5d9ec..7823279 100644 --- a/src/tts.js +++ b/src/tts.js @@ -2,10 +2,30 @@ import { API_KEY, MODEL, setLastTTS, TTS_API_URL } from "./config.js"; import { getSelectedModel, getSelectedModelEndpoint } from "./models.js"; -// Function to read text using TTS +// Simple JavaScript text cleaning for TTS +function cleanTextForTTS(text) { + return text + // Remove markdown formatting + .replace(/\*\*([^*]+)\*\*/g, '$1') // Bold + .replace(/\*([^*]+)\*/g, '$1') // Italic + .replace(/__([^_]+)__/g, '$1') // Bold alt + .replace(/_([^_]+)_/g, '$1') // Italic alt + .replace(/#+\s/g, '') // Headers + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Links + .replace(/`([^`]+)`/g, '$1') // Inline code + .replace(/```[^`]*```/g, '') // Code blocks + // Remove HTML tags + .replace(/<[^>]+>/g, '') + // Remove excess whitespace + .replace(/\n\n+/g, '\n\n') + .replace(/[ \t]+/g, ' ') + .trim(); +} + +// Function to read text using TTS - for Read Page button (uses Hermes) export async function speakText(text, voice = "alloy", rate = 0.9) { try { - // Preprocess the text using the new function to get spoken tokens + // Preprocess the text using Hermes to get spoken tokens const spokenText = await extractSpokenTokens(text); const response = await fetch(TTS_API_URL, { @@ -40,6 +60,46 @@ export async function speakText(text, voice = "alloy", rate = 0.9) { } } +// Direct TTS function for modal - no Hermes preprocessing +export async function speakTextDirect(text, voice = "alloy", rate = 0.9) { + try { + // Clean text with simple JavaScript instead of Hermes + const cleanedText = cleanTextForTTS(text); + + console.log("TTS input preview:", cleanedText.substring(0, 100) + "..."); + + const response = await fetch(TTS_API_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${API_KEY}`, + }, + body: JSON.stringify({ + model: "tts-1", + voice: voice, + input: cleanedText, + }), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const audioBlob = await response.blob(); + const audioUrl = URL.createObjectURL(audioBlob); + const audio = new Audio(audioUrl); + audio.playbackRate = rate; + + // Store the last TTS result + setLastTTS(text, { audio, blob: audioBlob }); + + return { audio, blob: audioBlob, blobUrl: audioUrl }; + } catch (error) { + console.error("Error in TTS:", error); + throw error; + } +} + // Process page content using Hermes export async function processContentWithHermes(content) { const payload = { @@ -105,41 +165,25 @@ Ready? Breath and then return a stream of tokens to be used in a TTS system. } } +// Extract spoken tokens using Hermes - for Read Page functionality export async function extractSpokenTokens(content) { - // Basic content filtering - const filteredContent = content - .replace(/hawk\s*tau/gi, '[filtered]') - .replace(/spit\s*on\s*that/gi, '[filtered]') - .trim(); - - // Skip processing if content is inappropriate or too short - if (filteredContent.length < 10 || filteredContent.includes('[filtered]')) { - console.warn('Content filtered or too short for TTS processing'); - return content; // Return original but won't be processed - } - const payload = { - model: MODEL, + model: getSelectedModel(), messages: [ { role: "system", - content: `Extract the spoken tokens from the content without altering or summarizing it. Ensure that the spoken tokens match the content displayed on the screen. Skip any embedded advertisements. Remove asterisks, markdown symbols, and other characters that should not be spoken, such as **, *, #, or []. Preserve the original text's meaning and structure.`, + content: `Extract the exact main content of the article. Avoid reading ads. Do not change or summarize content. Return the same words verbatim. When you encounter a list or bullets, add . . . to make the TTS pause between items. Try to stay as close to the truth of the original version as possible. Start with the title of the post and then jump into it. Do not mention the TTS stream just do the work! Remember your only goal is to extract the entire & exact main content of the article. Ready? Breath and then return a stream of tokens to be used in a TTS system.`, }, { role: "user", - content: filteredContent, + content: content, }, ], temperature: 0, max_tokens: 76000, }; - // Reduce console spam - only log content preview for long text - const contentPreview = filteredContent.length > 100 - ? filteredContent.substring(0, 100) + "..." - : filteredContent; - - console.log("Processing TTS for content:", contentPreview); + console.log("Processing page content for TTS..."); try { const response = await fetch( @@ -211,10 +255,10 @@ export async function generateTitleForTTS(text) { .substring(0, 50); // Limit length for filesystem compatibility } -// Handle TTS from element (for modal usage) +// Handle TTS from element (for modal usage - uses direct TTS without Hermes) export async function handleTTS(text, voice = "alloy", rate = 0.9) { try { - const result = await speakText(text, voice, rate); + const result = await speakTextDirect(text, voice, rate); return result; } catch (error) { console.error("Error in handleTTS:", error);