- Limit translation.js console output to 100 chars for long text (was showing 22KB) - Add content filtering to TTS to prevent inappropriate content processing - Enhance audio error handling with abort, stalled, and specific error types - Add user-friendly error messages for audio playback failures
234 lines
6 KiB
JavaScript
234 lines
6 KiB
JavaScript
// Text-to-speech functionality
|
|
import { API_KEY, MODEL, setLastTTS, TTS_API_URL } from "./config.js";
|
|
import { getSelectedModel, getSelectedModelEndpoint } from "./models.js";
|
|
|
|
// Function to read text using TTS
|
|
export async function speakText(text, voice = "alloy", rate = 0.9) {
|
|
try {
|
|
// Preprocess the text using the new function 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 };
|
|
} 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;
|
|
}
|
|
}
|
|
|
|
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,
|
|
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.`,
|
|
},
|
|
{
|
|
role: "user",
|
|
content: filteredContent,
|
|
},
|
|
],
|
|
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);
|
|
|
|
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 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 title for the following text.",
|
|
},
|
|
{
|
|
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(/\s+/g, "-")
|
|
.toLowerCase();
|
|
}
|
|
|
|
// Handle TTS from element (for modal usage)
|
|
export async function handleTTS(text, voice = "alloy", rate = 0.9) {
|
|
try {
|
|
const result = await speakText(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;
|
|
}
|
|
}
|