- Pre-clean HTML before sending to Hermes (remove scripts, styles, ads, iframes) - Decode HTML entities ( & etc.) before Hermes processing - Simplified, clearer system prompt focused on extraction tasks - Post-process Hermes output with cleanTextForTTS() for final cleanup - Reduces Hermes workload and improves extraction quality - Three-stage pipeline: JS pre-clean → Hermes extraction → JS post-clean
313 lines
8.6 KiB
JavaScript
313 lines
8.6 KiB
JavaScript
// Text-to-speech functionality
|
|
import { API_KEY, MODEL, setLastTTS, TTS_API_URL } from "./config.js";
|
|
import { getSelectedModel, getSelectedModelEndpoint } from "./models.js";
|
|
|
|
// 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 Hermes to get spoken tokens
|
|
const spokenText = await extractSpokenTokens(text);
|
|
|
|
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: spokenText,
|
|
}),
|
|
});
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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 = {
|
|
model: getSelectedModel(),
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
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: content,
|
|
},
|
|
],
|
|
temperature: 0,
|
|
max_tokens: 76000,
|
|
};
|
|
|
|
console.log(
|
|
"Sending payload to LLM for spoken tokens:",
|
|
JSON.stringify(payload, null, 2),
|
|
);
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`${getSelectedModelEndpoint()}/chat/completions`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${API_KEY}`,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
},
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error(`HTTP error! status: ${response.status}`, errorText);
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.choices[0].message.content;
|
|
} catch (error) {
|
|
console.error("Error in processContentWithHermes:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Extract spoken tokens using Hermes - for Read Page functionality
|
|
export async function extractSpokenTokens(content) {
|
|
// Pre-clean content with JavaScript to help Hermes
|
|
const preCleanedContent = content
|
|
// Remove script and style tags completely
|
|
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
// Remove common ad/tracking elements
|
|
.replace(/<(ins|iframe|object|embed)[^>]*>[\s\S]*?<\/\1>/gi, '')
|
|
// Clean up HTML entities
|
|
.replace(/ /g, ' ')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, "'")
|
|
// Remove excessive whitespace
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
|
|
const payload = {
|
|
model: getSelectedModel(),
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content: `You are extracting the main article content for text-to-speech. The input has been pre-cleaned of scripts and ads.
|
|
|
|
Your task:
|
|
1. Find and extract the main article content (title, subtitle, body text)
|
|
2. Preserve the exact wording - do not summarize or paraphrase
|
|
3. Skip navigation menus, sidebars, footers, and advertisements
|
|
4. For lists or bullet points, add "..." between items for natural pauses
|
|
5. Remove any remaining HTML tags but keep the text inside them
|
|
6. Start with the article title, then the main content
|
|
|
|
Return only the article text ready for TTS, nothing else.`,
|
|
},
|
|
{
|
|
role: "user",
|
|
content: preCleanedContent,
|
|
},
|
|
],
|
|
temperature: 0,
|
|
max_tokens: 76000,
|
|
};
|
|
|
|
console.log("Processing page content for TTS...");
|
|
|
|
try {
|
|
const response = await fetch(
|
|
`${getSelectedModelEndpoint()}/chat/completions`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${API_KEY}`,
|
|
},
|
|
body: JSON.stringify(payload),
|
|
},
|
|
);
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error(`HTTP error! status: ${response.status}`, errorText);
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Post-process Hermes output with JavaScript cleaning
|
|
const finalContent = cleanTextForTTS(data.choices[0].message.content);
|
|
return finalContent;
|
|
} catch (error) {
|
|
console.error("Error in extractSpokenTokens:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Function to generate a title for TTS
|
|
export async function generateTitleForTTS(text) {
|
|
const response = await fetch(
|
|
`${getSelectedModelEndpoint()}/chat/completions`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Authorization: `Bearer ${API_KEY}`,
|
|
},
|
|
body: JSON.stringify({
|
|
model: MODEL,
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content: "Generate a concise 5-10 word filename title for the following text. Return only the title words, no quotes, no explanation. Focus on the main topic or key concept. Example: 'Machine Learning Basics Tutorial' or 'Recipe For Chocolate Chip Cookies' or 'JavaScript Array Methods Guide'.",
|
|
},
|
|
{
|
|
role: "user",
|
|
content: text,
|
|
},
|
|
],
|
|
temperature: 0.5,
|
|
max_tokens: 60,
|
|
}),
|
|
},
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
return data.choices[0].message.content
|
|
.trim()
|
|
.replace(/['"]/g, "") // Remove quotes
|
|
.replace(/[^\w\s-]/g, "") // Remove special characters except spaces and hyphens
|
|
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
|
.replace(/-+/g, "-") // Replace multiple hyphens with single
|
|
.toLowerCase()
|
|
.substring(0, 50); // Limit length for filesystem compatibility
|
|
}
|
|
|
|
// 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 speakTextDirect(text, voice, rate);
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Error in handleTTS:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Download audio blob as file
|
|
export function downloadAudio(blobUrl, filename = "tts-audio.mp3") {
|
|
try {
|
|
const a = document.createElement("a");
|
|
a.href = blobUrl;
|
|
a.download = filename;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
} catch (error) {
|
|
console.error("Error downloading audio:", error);
|
|
throw error;
|
|
}
|
|
}
|