eureka, we have uber TTS, using it three ways!
* read page * tts anything * tts ai generated message modified: uncloseai.js
This commit is contained in:
parent
62674dfc5f
commit
8debf2b8bf
1 changed files with 473 additions and 21 deletions
494
uncloseai.js
494
uncloseai.js
|
|
@ -1,11 +1,3 @@
|
|||
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
|
||||
import hljs from 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js';
|
||||
|
||||
// Configuration
|
||||
const API_URL = "https://hermes.ai.unturf.com/v1/chat/completions";
|
||||
const API_KEY = "dummy-api-key";
|
||||
const MODEL = "NousResearch/Hermes-3-Llama-3.1-8B";
|
||||
|
||||
/* how to use in HTML */
|
||||
|
||||
/*
|
||||
|
|
@ -23,6 +15,15 @@ const MODEL = "NousResearch/Hermes-3-Llama-3.1-8B";
|
|||
</div>
|
||||
*/
|
||||
|
||||
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
|
||||
import hljs from 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js';
|
||||
|
||||
// Configuration
|
||||
const API_URL = "https://hermes.ai.unturf.com/v1/chat/completions";
|
||||
const TTS_API_URL = "https://speech.ai.unturf.com/v1/audio/speech";
|
||||
const API_KEY = "dummy-api-key";
|
||||
const MODEL = "NousResearch/Hermes-3-Llama-3.1-8B";
|
||||
|
||||
// Initialize chat history
|
||||
let chatHistory = [
|
||||
{
|
||||
|
|
@ -31,10 +32,9 @@ let chatHistory = [
|
|||
}
|
||||
];
|
||||
|
||||
// Function to extract text content from the webpage
|
||||
//function extractWebpageContent() {
|
||||
// return document.body.innerText;
|
||||
//}
|
||||
// Cache for TTS Anything
|
||||
let lastTTSInput = '';
|
||||
let lastTTSResult = null;
|
||||
|
||||
// Function to extract text content along with links and metadata from the webpage
|
||||
function extractWebpageContent() {
|
||||
|
|
@ -73,11 +73,318 @@ function extractWebpageContent() {
|
|||
}
|
||||
}
|
||||
|
||||
getTextWithLinks(document.body); // Start with the body element
|
||||
|
||||
getTextWithLinks(document.body);
|
||||
return content.trim();
|
||||
}
|
||||
|
||||
// Function to compute SHA-256 hash
|
||||
async function sha256(message) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(message);
|
||||
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
return hashHex;
|
||||
}
|
||||
|
||||
// Function to open IndexedDB
|
||||
function openDatabase() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('TTSCache', 1);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = event.target.result;
|
||||
db.createObjectStore('audio', { keyPath: 'hash' });
|
||||
};
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve(event.target.result);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Function to get audio from IndexedDB
|
||||
async function getAudioFromCache(hash) {
|
||||
const db = await openDatabase();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction('audio', 'readonly');
|
||||
const store = transaction.objectStore('audio');
|
||||
const request = store.get(hash);
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
resolve(event.target.result ? event.target.result.audioData : null);
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Function to save audio to IndexedDB
|
||||
async function saveAudioToCache(hash, audioData) {
|
||||
const db = await openDatabase();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction('audio', 'readwrite');
|
||||
const store = transaction.objectStore('audio');
|
||||
const request = store.put({ hash, audioData });
|
||||
|
||||
request.onsuccess = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
request.onerror = (event) => {
|
||||
reject(event.target.error);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Function to convert Blob to Base64
|
||||
function blobToBase64(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to convert Base64 to Blob
|
||||
function base64ToBlob(base64) {
|
||||
const byteString = atob(base64.split(',')[1]);
|
||||
const mimeString = base64.split(',')[0].split(':')[1].split(';')[0];
|
||||
const ab = new ArrayBuffer(byteString.length);
|
||||
const ia = new Uint8Array(ab);
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
return new Blob([ab], { type: mimeString });
|
||||
}
|
||||
|
||||
// Function to read text using TTS
|
||||
async function speakText(text, voice = 'alloy') {
|
||||
const textHash = await sha256(text); // Use text for hashing
|
||||
const cachedAudioData = await getAudioFromCache(textHash);
|
||||
|
||||
if (cachedAudioData) {
|
||||
const audioBlob = base64ToBlob(cachedAudioData);
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
return { audio: new Audio(audioUrl), blob: audioBlob };
|
||||
}
|
||||
|
||||
try {
|
||||
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: text
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const audioBlob = await response.blob();
|
||||
const audioData = await blobToBase64(audioBlob);
|
||||
await saveAudioToCache(textHash, audioData); // Cache the audio data
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
return { audio: new Audio(audioUrl), blob: audioBlob };
|
||||
} catch (error) {
|
||||
console.error('Error in TTS:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// New function to process content using Hermes
|
||||
async function processContentWithHermes(content) {
|
||||
const payload = {
|
||||
model: MODEL,
|
||||
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.
|
||||
|
||||
Only summarize pre or code tags containing code.
|
||||
|
||||
How to summarize code: In place of any pre or code tags that have code, summarize the code instead of reading the code.
|
||||
|
||||
Here is a sample to show how code could be explained using English:
|
||||
|
||||
"a code block that shows how to use curl to post a json prompt to the nous research hermes 3 llama 3.1 8b model using a temperature of .5 and asking if the ai to 'Give a Python Fizzbuzz solution in one line of code'"
|
||||
|
||||
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.
|
||||
|
||||
Important remember do not summarize my words, only summarize code!
|
||||
|
||||
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: 14000
|
||||
};
|
||||
|
||||
console.log("Sending payload to Hermes:", JSON.stringify(payload, null, 2));
|
||||
|
||||
try {
|
||||
const response = await fetch(API_URL, {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to generate a title for TTS
|
||||
async function generateTitleForTTS(text) {
|
||||
const response = await fetch(API_URL, {
|
||||
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();
|
||||
}
|
||||
|
||||
// Function to read the entire page using Hermes
|
||||
async function readPageWithHermes() {
|
||||
const content = extractWebpageContent();
|
||||
|
||||
// Create status indicator
|
||||
const statusDiv = document.createElement('div');
|
||||
statusDiv.style.position = 'fixed';
|
||||
statusDiv.style.top = '10px';
|
||||
statusDiv.style.right = '10px';
|
||||
statusDiv.style.padding = '10px';
|
||||
statusDiv.style.background = 'rgba(0,0,0,0.8)';
|
||||
statusDiv.style.color = 'white';
|
||||
statusDiv.style.borderRadius = '5px';
|
||||
statusDiv.style.zIndex = '1000';
|
||||
document.body.appendChild(statusDiv);
|
||||
|
||||
statusDiv.textContent = 'Processing content with Hermes...';
|
||||
|
||||
const processedContent = await processContentWithHermes(content);
|
||||
|
||||
// Update status indicator
|
||||
statusDiv.textContent = 'Converting text to speech...';
|
||||
|
||||
// Add pause/resume button
|
||||
const pauseButton = document.createElement('button');
|
||||
pauseButton.textContent = 'Pause Reading';
|
||||
pauseButton.style.marginLeft = '10px';
|
||||
statusDiv.appendChild(pauseButton);
|
||||
|
||||
let isPaused = false;
|
||||
let currentAudio = null;
|
||||
let currentBlob = null;
|
||||
|
||||
pauseButton.onclick = () => {
|
||||
if (currentAudio) {
|
||||
if (isPaused) {
|
||||
currentAudio.play();
|
||||
pauseButton.textContent = 'Pause Reading';
|
||||
} else {
|
||||
currentAudio.pause();
|
||||
pauseButton.textContent = 'Resume Reading';
|
||||
}
|
||||
isPaused = !isPaused;
|
||||
}
|
||||
};
|
||||
|
||||
const { audio, blob } = await speakText(processedContent);
|
||||
currentAudio = audio;
|
||||
currentBlob = blob;
|
||||
|
||||
statusDiv.textContent = 'Reading page...';
|
||||
statusDiv.appendChild(pauseButton);
|
||||
|
||||
// Add download button
|
||||
const downloadButton = document.createElement('button');
|
||||
downloadButton.textContent = 'Download MP3';
|
||||
downloadButton.onclick = () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(currentBlob);
|
||||
a.download = `${document.title.replace(/\s+/g, '-').toLowerCase()}.mp3`;
|
||||
a.click();
|
||||
};
|
||||
statusDiv.appendChild(downloadButton);
|
||||
|
||||
// Ensure the audio only plays once
|
||||
audio.loop = false;
|
||||
|
||||
// Play the audio
|
||||
await new Promise((resolve) => {
|
||||
audio.onended = () => {
|
||||
statusDiv.remove();
|
||||
resolve();
|
||||
};
|
||||
audio.play();
|
||||
});
|
||||
}
|
||||
|
||||
// Generator function to send a message to the LLM and yield responses
|
||||
async function* sendMessage(message) {
|
||||
chatHistory.push({ role: "user", content: message });
|
||||
|
|
@ -138,12 +445,10 @@ async function handleUserInput() {
|
|||
const chatBox = document.getElementById('chat-box');
|
||||
chatBox.innerHTML += `<p><strong>You:</strong> ${userInput}</p>`;
|
||||
|
||||
// Create a new paragraph for the AI response
|
||||
const aiResponseParagraph = document.createElement('p');
|
||||
aiResponseParagraph.innerHTML = '<strong>AI:</strong> ';
|
||||
chatBox.appendChild(aiResponseParagraph);
|
||||
|
||||
// Create a span element for the actual response content
|
||||
const responseContent = document.createElement('span');
|
||||
aiResponseParagraph.appendChild(responseContent);
|
||||
|
||||
|
|
@ -153,16 +458,159 @@ async function handleUserInput() {
|
|||
const parsedChunk = marked.parse(accumulatedContent);
|
||||
responseContent.innerHTML = parsedChunk;
|
||||
|
||||
// Apply syntax highlighting to code blocks
|
||||
responseContent.querySelectorAll('pre code').forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
}
|
||||
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
|
||||
// Add play/pause button for TTS
|
||||
const playPauseButton = document.createElement('button');
|
||||
playPauseButton.textContent = 'Generate TTS for AI Response';
|
||||
let aiAudio = null;
|
||||
let aiBlob = null;
|
||||
let isPaused = false;
|
||||
|
||||
playPauseButton.onclick = async () => {
|
||||
if (!aiAudio) {
|
||||
playPauseButton.textContent = 'Processing...';
|
||||
playPauseButton.disabled = true; // Disable button while processing
|
||||
const result = await speakText(accumulatedContent);
|
||||
aiAudio = result.audio;
|
||||
aiBlob = result.blob;
|
||||
playPauseButton.textContent = 'Pause AI Response';
|
||||
playPauseButton.disabled = false; // Re-enable button after processing
|
||||
aiAudio.play();
|
||||
|
||||
// Generate title for the MP3 file
|
||||
const title = await generateTitleForTTS(accumulatedContent);
|
||||
|
||||
// Add download button
|
||||
const downloadButton = document.createElement('button');
|
||||
downloadButton.textContent = 'Download MP3';
|
||||
downloadButton.onclick = () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(aiBlob);
|
||||
a.download = `${title}.mp3`;
|
||||
a.click();
|
||||
};
|
||||
chatBox.appendChild(downloadButton);
|
||||
} else {
|
||||
if (isPaused) {
|
||||
aiAudio.play();
|
||||
playPauseButton.textContent = 'Pause AI Response';
|
||||
} else {
|
||||
aiAudio.pause();
|
||||
playPauseButton.textContent = 'Play AI Response';
|
||||
}
|
||||
isPaused = !isPaused;
|
||||
}
|
||||
};
|
||||
chatBox.appendChild(playPauseButton);
|
||||
}
|
||||
|
||||
// Function to initialize the chat interface
|
||||
// Function to open TTS modal
|
||||
async function openTTSModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.style.position = 'fixed';
|
||||
modal.style.top = '0';
|
||||
modal.style.left = '0';
|
||||
modal.style.width = '100%';
|
||||
modal.style.height = '100%';
|
||||
modal.style.backgroundColor = 'rgba(0,0,0,0.5)';
|
||||
modal.style.display = 'flex';
|
||||
modal.style.justifyContent = 'center';
|
||||
modal.style.alignItems = 'center';
|
||||
modal.style.zIndex = '1001';
|
||||
|
||||
const modalContent = document.createElement('div');
|
||||
modalContent.style.backgroundColor = 'white';
|
||||
modalContent.style.padding = '20px';
|
||||
modalContent.style.borderRadius = '5px';
|
||||
modalContent.style.position = 'relative';
|
||||
modal.appendChild(modalContent);
|
||||
|
||||
const closeButton = document.createElement('button');
|
||||
closeButton.textContent = 'X';
|
||||
closeButton.style.position = 'absolute';
|
||||
closeButton.style.top = '10px';
|
||||
closeButton.style.right = '10px';
|
||||
closeButton.onclick = () => document.body.removeChild(modal);
|
||||
modalContent.appendChild(closeButton);
|
||||
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.style.width = '100%';
|
||||
textArea.style.height = '100px';
|
||||
modalContent.appendChild(textArea);
|
||||
|
||||
const playButton = document.createElement('button');
|
||||
playButton.textContent = 'Play Text';
|
||||
playButton.onclick = async () => {
|
||||
const currentText = textArea.value.trim();
|
||||
if (currentText !== lastTTSInput) {
|
||||
playButton.textContent = 'Processing...';
|
||||
playButton.disabled = true; // Disable button while processing
|
||||
lastTTSInput = currentText;
|
||||
lastTTSResult = await speakText(currentText);
|
||||
playButton.textContent = 'Pause Text';
|
||||
playButton.disabled = false; // Re-enable button after processing
|
||||
|
||||
// Generate title for the MP3 file
|
||||
const title = await generateTitleForTTS(currentText);
|
||||
|
||||
// Remove existing download button if present
|
||||
const existingDownloadButton = modalContent.querySelector('button.download');
|
||||
if (existingDownloadButton) {
|
||||
modalContent.removeChild(existingDownloadButton);
|
||||
}
|
||||
|
||||
const downloadButton = document.createElement('button');
|
||||
downloadButton.textContent = 'Download MP3';
|
||||
downloadButton.className = 'download';
|
||||
downloadButton.onclick = () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(lastTTSResult.blob);
|
||||
a.download = `${title}.mp3`;
|
||||
a.click();
|
||||
};
|
||||
modalContent.appendChild(downloadButton);
|
||||
}
|
||||
|
||||
if (lastTTSResult) {
|
||||
if (lastTTSResult.audio.paused) {
|
||||
lastTTSResult.audio.play();
|
||||
playButton.textContent = 'Pause Text';
|
||||
} else {
|
||||
lastTTSResult.audio.pause();
|
||||
playButton.textContent = 'Play Text';
|
||||
}
|
||||
}
|
||||
};
|
||||
modalContent.appendChild(playButton);
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Add button to page
|
||||
function addReadPageButton() {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Read Page';
|
||||
button.onclick = readPageWithHermes;
|
||||
button.style.margin = '10px';
|
||||
|
||||
const ttsButton = document.createElement('button');
|
||||
ttsButton.textContent = 'TTS Anything';
|
||||
ttsButton.onclick = openTTSModal;
|
||||
ttsButton.style.margin = '10px';
|
||||
|
||||
// Insert before the chat container
|
||||
const chatContainer = document.getElementById('chat-container');
|
||||
chatContainer.parentNode.insertBefore(button, chatContainer);
|
||||
chatContainer.parentNode.insertBefore(ttsButton, chatContainer);
|
||||
}
|
||||
|
||||
// Initialize the chat interface
|
||||
function initializeChatInterface() {
|
||||
const pageContent = extractWebpageContent();
|
||||
chatHistory.push({
|
||||
|
|
@ -170,15 +618,18 @@ function initializeChatInterface() {
|
|||
content: `Here's the content of the webpage: ${pageContent}`
|
||||
});
|
||||
|
||||
// Set up marked.js options
|
||||
marked.setOptions({
|
||||
highlight: function(code, lang) {
|
||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
||||
return hljs.highlight(code, { language }).value;
|
||||
}
|
||||
});
|
||||
|
||||
// Add the Read Page button
|
||||
addReadPageButton();
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
document.getElementById("user-input").addEventListener("keydown", function(event) {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
|
|
@ -186,8 +637,9 @@ document.getElementById("user-input").addEventListener("keydown", function(event
|
|||
}
|
||||
});
|
||||
|
||||
// Initialize the chat interface when the page loads
|
||||
// Initialize on page load
|
||||
window.onload = initializeChatInterface;
|
||||
|
||||
// Expose handleUserInput to the global scope
|
||||
// Export functions to global scope
|
||||
window.handleUserInput = handleUserInput;
|
||||
window.readPage = readPageWithHermes;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue