From 8debf2b8bf29433710296079910f675f097aeb28 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 10 Nov 2024 17:43:33 +0000 Subject: [PATCH] eureka, we have uber TTS, using it three ways! * read page * tts anything * tts ai generated message modified: uncloseai.js --- uncloseai.js | 494 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 473 insertions(+), 21 deletions(-) diff --git a/uncloseai.js b/uncloseai.js index 8cc6408..f40edd4 100644 --- a/uncloseai.js +++ b/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"; */ +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 += `

You: ${userInput}

`; - // Create a new paragraph for the AI response const aiResponseParagraph = document.createElement('p'); aiResponseParagraph.innerHTML = 'AI: '; 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;