diff --git a/package.json b/package.json index 16a29bc..e705bc7 100644 --- a/package.json +++ b/package.json @@ -7,32 +7,40 @@ "start": "electron .", "build": "electron-builder", "prod": "NODE_ENV=production electron .", - "build:mac": "PYTHON_PATH=/opt/anaconda3/envs/quiz/bin/python electron-builder build --mac --x64 --arm64", + "build:mac": "electron-builder build --mac --x64 --arm64", "build:win": "electron-builder build --win", "build:linux": "electron-builder build --linux", "clean": "rm -rf dist node_modules", - "rebuild": "npm run clean && npm cache clean --force && npm install && npm run patch-dmg-builder && DEBUG=electron-builder PYTHON_PATH=/opt/anaconda3/envs/quiz/bin/python npm run build:mac", + "rebuild": "npm run clean && npm cache clean --force && npm install && npm run patch-dmg-builder && DEBUG=electron-builder npm run build:mac", "patch-dmg-builder": "node patch-dmg-builder.js" }, "author": "Your Name", "license": "ISC", "devDependencies": { "electron": "^32.1.2", - "electron-builder": "^25.0.5" + "electron-builder": "^25.1.8" }, "dependencies": { "axios": "^1.7.7", "dotenv": "^10.0.0", + "electron-store": "^8.1.0", "groq-sdk": "^0.7.0", "reveal.js": "^5.1.0", "ws": "^8.18.0" }, "build": { - "appId": "com.yourdomain.streamsage", + "appId": "com.quizzible.streamsage", "productName": "StreamSage", "mac": { - "category": "public.app-category.productivity" + "category": "public.app-category.productivity", + "hardenedRuntime": true, + "entitlements": "build/entitlements.mac.plist", + "entitlementsInherit": "build/entitlements.mac.plist", + "target": ["dmg"], + "artifactName": "${productName}.${ext}" }, + "icon": "assets/icon.icns", + "files": [ "src/**/*", "StreamSage/**/*", @@ -45,8 +53,13 @@ "filter": [ "**/*" ] + }, + { + "from": "src/assets/", + "to": "assets" } ], + "afterPack": "./after-pack.js" } } diff --git a/src/S_16.png b/src/S_16.png new file mode 100644 index 0000000..d7f95ba Binary files /dev/null and b/src/S_16.png differ diff --git a/src/S_210.png b/src/S_210.png new file mode 100644 index 0000000..efd0d09 Binary files /dev/null and b/src/S_210.png differ diff --git a/src/S_32.png b/src/S_32.png new file mode 100644 index 0000000..f8a2b76 Binary files /dev/null and b/src/S_32.png differ diff --git a/src/S_64.png b/src/S_64.png new file mode 100644 index 0000000..f5557c6 Binary files /dev/null and b/src/S_64.png differ diff --git a/src/assets/beep.mp3 b/src/assets/beep.mp3 new file mode 100644 index 0000000..9b6561c Binary files /dev/null and b/src/assets/beep.mp3 differ diff --git a/src/assets/hey.m4a b/src/assets/hey.m4a new file mode 100644 index 0000000..b3d07e2 Binary files /dev/null and b/src/assets/hey.m4a differ diff --git a/src/assets/icon.icns b/src/assets/icon.icns new file mode 100644 index 0000000..3e58f42 Binary files /dev/null and b/src/assets/icon.icns differ diff --git a/src/download_512.png b/src/download_512.png new file mode 100644 index 0000000..04b4bd8 Binary files /dev/null and b/src/download_512.png differ diff --git a/src/icon.icns b/src/icon.icns new file mode 100644 index 0000000..3e58f42 Binary files /dev/null and b/src/icon.icns differ diff --git a/streamsage_extension/icons/icon16.png b/src/icon16.png similarity index 100% rename from streamsage_extension/icons/icon16.png rename to src/icon16.png diff --git a/src/index.html b/src/index.html index 4d0559c..b4139d0 100644 --- a/src/index.html +++ b/src/index.html @@ -1,8 +1,8 @@ - - + +
Mic: Unmuted
Press (M) to mute, (R) to record
+
+ + +
Idle
- - \ No newline at end of file diff --git a/src/main.js b/src/main.js index a03866e..54ac771 100644 --- a/src/main.js +++ b/src/main.js @@ -4,6 +4,7 @@ const path = require('path'); const fs = require('fs'); const dotenv = require('dotenv'); const WebSocket = require('ws'); +const store = require('./store'); // Load environment variables from .env file dotenv.config(); @@ -37,7 +38,7 @@ async function listDevices() } function createTray() { - tray = new Tray(path.join(__dirname, 'icon16.png')); // Replace with your icon path + tray = new Tray(path.join(__dirname, 'S_16.png')); // Replace with your icon path const contextMenu = Menu.buildFromTemplate([ { label: 'Help', @@ -79,10 +80,25 @@ function createWindow() { nodeIntegration: true, // Enable Node.js integration contextIsolation: false, // Disable context isolation + // Add these permissions + webSecurity: true, + allowRunningInsecureContent: false, + enableRemoteModule: true, devTools: true // Enable developer tools }, show: false, }); + // Request microphone permission + mainWindow.webContents.session.setPermissionRequestHandler((webContents, permission, callback) => { + const allowedPermissions = ['media', 'microphone']; + if (allowedPermissions.includes(permission)) { + callback(true); + } + else + { + callback(false); + } + }); // Load the main HTML file into the window mainWindow.loadFile('src/index.html'); @@ -105,8 +121,11 @@ function createWindow() return false; }); - // Initialize the backend processor - initializeBackendProcessor(); + // Initialize backend processor if API key exists + const apiKey = store.get('groqApiKey'); + if (apiKey) { + initializeBackendProcessor(); + } // Enable the console window for debugging // mainWindow.webContents.openDevTools(); @@ -126,8 +145,8 @@ function handleSaveAudio(arrayBuffer, fileName) return; // Exit the function if the data is invalid } - // Construct the file path for saving the audio - const filePath = path.join(__dirname, '..', 'StreamSage', fileName); + // Construct the file path for saving the audio using app.getPath('userData') + const filePath = path.join(app.getPath('userData'), fileName); const buffer = Buffer.from(arrayBuffer); // Convert ArrayBuffer to Buffer // Write the buffer to the specified file @@ -166,8 +185,8 @@ function handleSaveQuestion(arrayBuffer) return; // Exit the function if the data is invalid } - // Construct the file path for saving the user question audio - const filePath = path.join(__dirname, '..', 'StreamSage', 'user_question.wav'); + // Construct the file path for saving the user question audio using app.getPath('userData') + const filePath = path.join(app.getPath('userData'), 'user_question.wav'); const buffer = Buffer.from(arrayBuffer); // Convert ArrayBuffer to Buffer // Write the buffer to the specified file @@ -326,7 +345,7 @@ ipcMain.on('save-question', (event, arrayBuffer) => // }); // Clean up on app quit -const filePathToDelete = path.join(__dirname, `../StreamSage/${screenAudio_filename}`); // Define the file path to delete +const filePathToDelete = path.join(app.getPath('userData'), screenAudio_filename); // Define the file path to delete app.on('before-quit', () => { @@ -368,4 +387,13 @@ function sendToggleCommand(isMuted) } } ); -} \ No newline at end of file +} + +// Handle API key updates +ipcMain.on('api-key-updated', (event, apiKey) => { + if (backendProcessor) { + backendProcessor.updateApiKey(apiKey); + } else { + initializeBackendProcessor(); + } +}); \ No newline at end of file diff --git a/src/main_backend.js b/src/main_backend.js index 364b5fd..427e9df 100644 --- a/src/main_backend.js +++ b/src/main_backend.js @@ -1,30 +1,61 @@ const { Groq } = require('groq-sdk'); const { recordAudioWithSox } = require('./mic_record'); -// Initialize the Groq client -const groq = new Groq(); const { MacTTS, DeepGram } = require('./tts'); const fs_promises = require('fs').promises; const path = require('path'); const { spawn } = require('child_process'); -const dotenv = require('dotenv'); +// const dotenv = require('dotenv'); const { exec } = require('child_process'); +const store = require('./store'); +const { app } = require('electron'); const fs = require("fs"); // Load environment variables from .env file -dotenv.config({ path: path.resolve(__dirname, '..', '.env') }); - +// dotenv.config({ path: path.resolve(__dirname, '..', '.env') }); +function createLogger() +{ + const logFile = path.join(app.getPath('userData'), 'backend.log'); + + return (message, tag = '') => { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] [${tag || 'BACKEND'}] ${message}\n`; + + // Log to console + console.log(logMessage); + + // Append to file + try { + fs.appendFileSync(logFile, logMessage); + } catch (error) { + console.error('Failed to write to log file:', error); + } + }; +} +const log = createLogger(); class BackendProcessor { constructor(mainWindow) { this.mainWindow = mainWindow; - this.file_to_watch = path.join(__dirname, "../StreamSage/recorded_audio.webm"); - this.user_question_file = "user_question.wav"; + // Use app.getPath to get the user data directory and store files there + const userDataPath = app.getPath('userData'); + this.file_to_watch = path.join(userDataPath, "recorded_audio.webm"); + this.api_key = store.get('groqApiKey'); + + log('Initializing BackendProcessor'); + + if (!this.api_key) + { + log('No API key found', 'CONFIG'); + this.mainWindow.webContents.send('show-api-key-prompt'); + return; + } + this.user_question_file = path.join(userDataPath, "user_question.wav"); this.user_question = ""; this.voice_enabled = true; this.muted = true; @@ -38,13 +69,26 @@ class BackendProcessor this.debug_enabled = true; this.prev_modified_time = 0; - this.transcriber = new Groq({ apiKey: process.env.GROQ_API_KEY }); - this.chat_client = new Groq({ apiKey: process.env.GROQ_API_KEY2 }); + this.transcriber = new Groq({ apiKey: this.api_key }); + this.chat_client = new Groq({ apiKey: this.api_key }); this.tts = process.platform === 'darwin' ? new MacTTS() : new DeepGram(); + log(`Initialized with API key, watching file: ${this.file_to_watch}`, 'CONFIG'); + + // Set up audio paths properly for both development and production + if (app.isPackaged) { + // In production, use the resources path + this.audioPath = path.join(process.resourcesPath, 'assets', 'beep.mp3'); + } else { + // In development, use the src path + this.audioPath = path.join(__dirname, 'assets', 'beep.mp3'); + } + + log(`Audio path set to: ${this.audioPath}`, 'CONFIG'); } async sendStatusToRenderer(status) { + log(`Status update: ${status}`, 'UI'); this.mainWindow.webContents.send('update-status', status); } log(message, tag = "") @@ -58,33 +102,38 @@ class BackendProcessor // Record audio from mic async trigger_mic_recording(outputFile, sampleRate = 16000, device = 1) { - console.log('Triggering mic recording...'); + log('Triggering mic recording...', 'AUDIO'); try { await recordAudioWithSox(outputFile, sampleRate, device); - console.log('Mic recording completed successfully.'); + log('Mic recording completed successfully', 'AUDIO'); } catch (error) { - console.error('An error occurred during mic recording:', error); + log(`Mic recording error: ${error.message}`, 'ERROR'); throw error; // Re-throw the error to be caught in the main function } } async groq_transcribe(filename) { - // Create a transcription job - const transcription = await this.transcriber.audio.transcriptions.create({ - file: fs.createReadStream(filename), // Required path to audio file - replace with your audio file! - model: "distil-whisper-large-v3-en", // Required model to use for transcription - prompt: "Specify context or spelling", // Optional - response_format: "json", // Optional - language: "en", // Optional - temperature: 0.0, // Optional - }); - // Log the transcribed text - return transcription; + log(`Starting transcription for file: ${filename}`, 'GROQ'); + try { + const transcription = await this.transcriber.audio.transcriptions.create({ + file: fs.createReadStream(filename), // Required path to audio file - replace with your audio file! + model: "distil-whisper-large-v3-en", // Required model to use for transcription + prompt: "Specify context or spelling", // Optional + response_format: "json", // Optional + language: "en", // Optional + temperature: 0.0, // Optional + }); + log('Transcription completed successfully', 'GROQ'); + return transcription; + } catch (error) { + log(`Transcription error: ${error.message}`, 'ERROR'); + throw error; + } } async transcribeAudio(filename) @@ -185,11 +234,12 @@ class BackendProcessor async run() { + log('Starting main processing loop', 'SYSTEM'); while (this.should_run) { await new Promise(resolve => setTimeout(resolve, 500)); // Check every 0.5 seconds - console.log("is_recording: " + this.is_recording); + log(`Recording status: ${this.is_recording}`, 'STATUS'); // Check if the recording is currently active // Log a message indicating that recording is active and we are waiting for a stop command this.log("Recording is ON... Waiting for stop command", "main"); @@ -261,7 +311,7 @@ class BackendProcessor //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// if (this.muted) { - this.log("Muted, skipping processing", "main"); + log('System muted, skipping processing', 'STATUS'); continue; } this.playTingSound(); @@ -304,7 +354,7 @@ class BackendProcessor this.sendStatusToRenderer("Answering...") await this.tts.generateSpeech(answer); console.log("TTS generated speech"); - await this.tts.playAudio(); + // await this.tts.playAudio(); console.log("TTS played audio"); this.mainWindow.webContents.send('answer-play'); } @@ -313,32 +363,34 @@ class BackendProcessor } playTingSound() { - const audioPath = path.join(__dirname, 'assets', 'beep.mp3'); + log('Playing notification sound', 'AUDIO'); + log(`Using audio file: ${this.audioPath}`, 'AUDIO'); - if (process.platform === 'darwin') { - // For macOS, use afplay - exec(`afplay "${audioPath}"`, (error) => { - if (error) { - console.error('Error playing sound:', error); - } - }); - } else if (process.platform === 'win32') { - // For Windows, you might use PowerShell - exec(`powershell -c (New-Object Media.SoundPlayer "${audioPath}").PlaySync()`, (error) => { - if (error) { - console.error('Error playing sound:', error); - } - }); - } else { - // For Linux, you might use paplay or another command - console.log('Sound playback not implemented for this platform'); + if (process.platform === 'darwin') + { + exec(`/usr/bin/afplay "${this.audioPath}"`, (error) => { + if (error) { + log(`Error playing sound: ${error.message}`, 'ERROR'); + } else { + log('Sound played successfully', 'AUDIO'); + } + }); + } else if (process.platform === 'win32') + { + exec(`powershell -c (New-Object Media.SoundPlayer "${this.audioPath}").PlaySync()`, (error) => { + if (error) { + log(`Error playing sound: ${error.message}`, 'ERROR'); + } else { + log('Sound played successfully', 'AUDIO'); + } + }); } } // Methods to handle commands from main process setMute(isMuted) { this.muted = isMuted; - this.log(`Microphone ${this.muted ? 'muted' : 'unmuted'}`, "handle_commands"); + log(`Microphone ${this.muted ? 'muted' : 'unmuted'}`, 'CONTROL'); // this.updateStatus(); } @@ -354,7 +406,14 @@ class BackendProcessor this.voice_enabled = isEnabled; this.log(`Voice output ${this.voice_enabled ? 'enabled' : 'disabled'}`, "handle_commands"); } - + updateApiKey(newApiKey) + { + log('Updating API key', 'CONFIG'); + store.set('groqApiKey', newApiKey); + this.transcriber = new Groq({ apiKey: newApiKey }); + this.chat_client = new Groq({ apiKey: newApiKey }); + log('API key updated successfully', 'CONFIG'); + } // processQuestion() // { // this.generate_answer = true; diff --git a/src/mic_record.js b/src/mic_record.js index bbdd9fc..c36074d 100644 --- a/src/mic_record.js +++ b/src/mic_record.js @@ -1,4 +1,7 @@ const { exec } = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const { app } = require('electron'); /** * Records audio using SoX and saves it to a specified output file. @@ -9,44 +12,58 @@ const { exec } = require('child_process'); */ function recordAudioWithSox(outputFile, sampleRate = 16000, device = 1) { + const logFile = path.join(app.getPath('userData'), 'mic_record.log'); + console.log("logFile", logFile) + const logMessage = (msg) => + { + console.log(msg); + fs.appendFileSync(logFile, msg + '\n'); + }; + + logMessage(`MIC ON : outputFile ${outputFile}`); + const escapedPath = `"${outputFile}"`; + return new Promise((resolve, reject) => { let cmd; if (process.platform === 'darwin') { - cmd = `sox -d ${outputFile} rate ${sampleRate} silence 1 0.1 3% 1 2.0 3%`; + cmd = `/opt/homebrew/bin/sox -d ${escapedPath} rate ${sampleRate} silence 1 0.1 1% 1 2.0 1%`; } else if (process.platform === 'win32') { - cmd = `sox -t waveaudio ${device} ${outputFile} rate ${sampleRate} silence 1 0.1 3% 1 3.0 3%`; + cmd = `/opt/homebrew/bin/sox -t waveaudio ${device} ${escapedPath} rate ${sampleRate} silence 1 0.1 3% 1 3.0 3%`; } else { - console.log('Not implemented for this platform:', process.platform); - reject(new Error('Not implemented for this platform:', process.platform)); + const errMsg = `Not implemented for this platform: ${process.platform}`; + logMessage(errMsg); + reject(new Error(errMsg)); } - console.log(`Starting audio recording with command: ${cmd}`); + logMessage(`Starting audio recording with command: ${cmd}`); exec(cmd, { stdio: 'inherit' }, (error, stdout, stderr) => { if (error) { - console.error(`Error occurred during recording: ${error.message}`); + const errMsg = `Error occurred during recording: ${error.message}`; + logMessage(errMsg); reject(error); return; } if (stderr) { - console.warn(`Warning: ${stderr}`); + logMessage(`Warning: ${stderr}`); } if (stdout) { - console.log(`SoX output: ${stdout}`); + logMessage(`SoX output: ${stdout}`); } - console.log(`Recording completed successfully. Output saved to: ${outputFile}`); - console.log(`Recording parameters: Sample rate: ${sampleRate}Hz, Silence duration: 3s`); + logMessage(`Recording completed successfully. Output saved to: ${escapedPath}`); + logMessage(`Recording parameters: Sample rate: ${sampleRate}Hz, Silence duration: 3s`); resolve(); }); + logMessage(`Recording completed successfully. Output saved to: ${escapedPath}`); }); } diff --git a/src/renderer.js b/src/renderer.js index 6c6878d..88dea4d 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -1,4 +1,5 @@ const { ipcRenderer } = require('electron'); +const store = require('./store'); const { startRecording, stopRecording } = require('./screenCapture'); const { startMicrophoneRecording, stopMicrophoneRecording } = require('./micCapture'); @@ -240,4 +241,39 @@ updateStatus('Idle'); initVisualization(); // Add click event listener to the canvas -canvas.addEventListener('click', handleCanvasClick); \ No newline at end of file +canvas.addEventListener('click', handleCanvasClick); + +// Handle saving API key +document.getElementById('saveApiKey').addEventListener('click', () => { + const apiKey = document.getElementById('apiKeyInput').value.trim(); + if (apiKey) { + store.set('groqApiKey', apiKey); + ipcRenderer.send('api-key-updated', apiKey); + + // Show success message + const button = document.getElementById('saveApiKey'); + button.textContent = 'Saved!'; + setTimeout(() => { + button.textContent = 'Save'; + }, 1000); + } +}); + +async function requestMicrophoneAccess() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + console.log('Microphone access granted'); + stream.getTracks().forEach(track => track.stop()); // Stop the test stream + } catch (error) { + console.error('Error accessing microphone:', error); + } +} +// Load saved API key when app starts +window.addEventListener('DOMContentLoaded', async () => { + await requestMicrophoneAccess(); + + const savedApiKey = store.get('groqApiKey'); + if (savedApiKey) { + document.getElementById('apiKeyInput').value = savedApiKey; + } +}); \ No newline at end of file diff --git a/src/store.js b/src/store.js new file mode 100644 index 0000000..1ea6c13 --- /dev/null +++ b/src/store.js @@ -0,0 +1,10 @@ +// store.js +const Store = require('electron-store'); + +const store = new Store({ + defaults: { + groqApiKey: null + } +}); + +module.exports = store; \ No newline at end of file diff --git a/src/tts.js b/src/tts.js index 8e43c15..73071db 100644 --- a/src/tts.js +++ b/src/tts.js @@ -1,182 +1,230 @@ -// Import required libraries (you may need to install these via npm) const dotenv = require('dotenv'); const axios = require('axios'); const fs = require('fs'); const { exec } = require('child_process'); +const path = require('path'); +const { app } = require('electron'); dotenv.config(); -const DEEPGRAM_API_KEY = process.env.DEEPGRAM_API_KEY; -const COQUI_LOCAL_URL = process.env.COQUI_LOCAL_URL; -const PIPER_LOCAL_URL = process.env.PIPER_LOCAL_URL; +// Setup logging +function createLogger() +{ + const logFile = path.join(app.getPath('userData'), 'mic_record.log'); + + return (message) => { + const timestamp = new Date().toISOString(); + const logMessage = `[${timestamp}] [TTS] ${message}\n`; + + // Log to console + console.log(logMessage); + + // Append to file + try { + fs.appendFileSync(logFile, logMessage); + } catch (error) { + console.error('Failed to write to log file:', error); + } + }; +} + +const log = createLogger(); class SpeechGenerator { - constructor() { - if (this.constructor === SpeechGenerator) { - throw new Error("Abstract classes can't be instantiated."); + constructor() + { + if (this.constructor === SpeechGenerator) { + throw new Error("Abstract classes can't be instantiated."); + } } - } - async generateSpeech(text, filename = "audio.mp3") { - throw new Error("Method 'generateSpeech()' must be implemented."); - } + async generateSpeech(text, filename = "audio.mp3") { + throw new Error("Method 'generateSpeech()' must be implemented."); + } - async playAudio(filename) { - throw new Error("Method 'playAudio()' must be implemented."); - } + async playAudio(filename) { + throw new Error("Method 'playAudio()' must be implemented."); + } - stopAudio() { - throw new Error("Method 'stopAudio()' must be implemented."); - } + stopAudio() { + throw new Error("Method 'stopAudio()' must be implemented."); + } } -class DeepGram extends SpeechGenerator { - constructor() { - super(); - // Initialize DeepGram client here - } - - async generateSpeech(text, filename = "audio.mp3") { - try { - // Implement DeepGram API call here - console.log(`Generated speech saved to ${filename}`); - return filename; - } catch (error) { - console.error(`Exception: ${error}`); - return null; - } - } - - async playAudio(filename) { - // Implement audio playback (this might require a browser environment or additional libraries) - console.log(`Playing audio: ${filename}`); - } - - stopAudio() { - // Implement stop audio functionality - console.log("Audio playback stopped"); - } -} +class DeepGram extends SpeechGenerator +{ + constructor() + { + super(); + log('DeepGram TTS initialized'); + } -class Coqui extends SpeechGenerator { - constructor(url = COQUI_LOCAL_URL) { - super(); - this.url = url; - } - - async generateSpeech(text, filename = "audio.mp3") { - try { - const response = await axios.post(this.url, null, { params: { text } }); - if (response.status === 200) { - fs.writeFileSync(filename, response.data); - console.log(`Audio saved to ${filename}`); - return filename; - } else { - console.error(`Error: ${response.status}`); - console.error(response.data); - return null; - } - } catch (error) { - console.error(`Exception: ${error}`); - return null; - } - } - - async playAudio(filename) { - // Implement audio playback (this might require a browser environment or additional libraries) - console.log(`Playing audio: ${filename}`); - } - - stopAudio() { - // Implement stop audio functionality - console.log("Audio playback stopped"); - } -} + async generateSpeech(text, filename = "audio.mp3") + { + try { + log(`Attempting to generate speech via DeepGram: "${text.substring(0, 50)}..."`); + // Implement DeepGram API call here + log(`Generated speech saved to ${filename}`); + return filename; + } catch (error) { + log(`DeepGram error: ${error.message}`); + return null; + } + } + + async playAudio(filename) + { + log(`Playing audio: ${filename}`); + } -class Piper extends SpeechGenerator { - constructor(url = PIPER_LOCAL_URL) { - super(); - this.url = url; - } - - async generateSpeech(text, filename = "audio.mp3") { - try { - const response = await axios.post(this.url, null, { params: { text } }); - if (response.status === 200) { - fs.writeFileSync(filename, response.data); - console.log(`Audio saved to ${filename}`); - return filename; - } else { - console.error(`Error: ${response.status}`); - console.error(response.data); - return null; - } - } catch (error) { - console.error(`Exception: ${error}`); - return null; - } - } - - async playAudio(filename) { - // Implement audio playback (this might require a browser environment or additional libraries) - console.log(`Playing audio: ${filename}`); - } - - stopAudio() { - // Implement stop audio functionality - console.log("Audio playback stopped"); - } + stopAudio() + { + log("Stopping DeepGram audio playback"); + } } -class MacTTS extends SpeechGenerator { - constructor() { - super(); - this.process = null; - this.defaultFilename = "audio.aiff"; // Default filename - } - - async generateSpeech(text, filename = this.defaultFilename) { - return new Promise((resolve, reject) => { - exec(`say -o ${filename} "${text}"`, (error) => { - if (error) { - console.error(`Error occurred while trying to generate speech: ${error}`); - reject(null); - } else { - console.log(`Audio saved to ${filename}`); - resolve(filename); +class Coqui extends SpeechGenerator +{ + constructor(url = COQUI_LOCAL_URL) + { + super(); + this.url = url; + log(`Coqui TTS initialized with URL: ${url}`); + } + + async generateSpeech(text, filename = "audio.mp3") + { + try { + log(`Generating speech via Coqui: "${text.substring(0, 50)}..."`); + const response = await axios.post(this.url, null, { params: { text } }); + + if (response.status === 200) { + fs.writeFileSync(filename, response.data); + log(`Coqui audio saved to ${filename}`); + return filename; + } else { + log(`Coqui error: ${response.status} - ${response.statusText}`); + return null; + } + } catch (error) { + log(`Coqui exception: ${error.message}`); + return null; } - }); - }); - } - - async playAudio(filename = this.defaultFilename) { - return new Promise((resolve, reject) => { - this.process = exec(`afplay ${filename}`, (error) => { - if (error) { - console.error(`Error playing audio: ${error}`); - reject(error); - } else { - console.log("Audio played successfully"); - resolve(); + } + + async playAudio(filename) + { + log(`Playing Coqui audio: ${filename}`); + } + + stopAudio() + { + log("Stopping Coqui audio playback"); + } +} + +class Piper extends SpeechGenerator +{ + constructor(url = PIPER_LOCAL_URL) + { + super(); + this.url = url; + log(`Piper TTS initialized with URL: ${url}`); + } + + async generateSpeech(text, filename = "audio.mp3") + { + try { + log(`Generating speech via Piper: "${text.substring(0, 50)}..."`); + const response = await axios.post(this.url, null, { params: { text } }); + + if (response.status === 200) { + fs.writeFileSync(filename, response.data); + log(`Piper audio saved to ${filename}`); + return filename; + } else { + log(`Piper error: ${response.status} - ${response.statusText}`); + return null; + } + } catch (error) { + log(`Piper exception: ${error.message}`); + return null; } - }); - }); - } + } + + async playAudio(filename) + { + log(`Playing Piper audio: ${filename}`); + } - stopAudio() { - if (this.process) { - this.process.kill(); - console.log("Audio playback stopped"); + stopAudio() + { + log("Stopping Piper audio playback"); } - } } -// ... (other functions like recordAudio, transcribeAudio, chatWithModel, etc.) +class MacTTS extends SpeechGenerator +{ + constructor() + { + super(); + this.process = null; + this.defaultFilename = "audio.aiff"; + log('MacTTS initialized'); + } + + async generateSpeech(text, filename = this.defaultFilename) + { + return new Promise((resolve, reject) => { + log(`Generating speech for text: "${text.substring(0, 50)}..."`); + log(`Output filename: ${filename}`); + + exec(`/usr/bin/say "${text}"`, (error) => { + if (error) { + const errorMsg = `Error generating speech: ${error.message}`; + log(errorMsg); + reject(error); + } else { + log(`Speech generated successfully: ${filename}`); + resolve(filename); + } + }); + }); + } + + async playAudio(filename = this.defaultFilename) + { + return new Promise((resolve, reject) => { + log(`Playing audio file: ${filename}`); + + this.process = exec(`afplay "${filename}"`, (error) => { + if (error) { + const errorMsg = `Error playing audio: ${error.message}`; + log(errorMsg); + reject(error); + } else { + log('Audio playback completed successfully'); + resolve(); + } + }); + }); + } + + stopAudio() + { + if (this.process) { + log('Stopping audio playback'); + this.process.kill(); + log('Audio playback stopped'); + } else { + log('No audio playback to stop'); + } + } +} module.exports = { - DeepGram, - Coqui, - Piper, - MacTTS, - // ... (export other functions as needed) + DeepGram, + Coqui, + Piper, + MacTTS }; \ No newline at end of file diff --git a/streamsage_extension/background.js b/streamsage_extension/background.js deleted file mode 100644 index 7ee28de..0000000 --- a/streamsage_extension/background.js +++ /dev/null @@ -1,158 +0,0 @@ -// Ping the server every 30 seconds to keep the connection alive -setInterval(() => { - if (socket.readyState === WebSocket.OPEN) { - socket.send(JSON.stringify({ action: 'ping' })); - console.log('Ping sent to server'); - } -}, 30000); // 30 seconds - -chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) -{ - if (request.action === "toggleMedia") - { - chrome.tabs.query({active: true, currentWindow: true}, function(tabs) - { - if (tabs.length === 0) { - console.warn('No active tabs found.'); - return; - } - - const activeTab = tabs[0]; - console.log('Executing toggleMediaPlayback in active tab:', activeTab.id); - - chrome.scripting.executeScript( - { - target: {tabId: activeTab.id}, - function: toggleMediaPlayback - }, - () => { - if (chrome.runtime.lastError) { - console.error('executeScript error:', chrome.runtime.lastError); - } else { - console.log('Successfully executed toggleMediaPlayback in tab:', activeTab.id); - } - } - ); - }); - } - else if (request.action === "updateStatus") - { - updateStatus(request.status) - } -}); - -function toggleMediaPlayback() -{ - console.log('toggleMediaPlayback function invoked.'); - const mediaElements = document.querySelectorAll('video, audio'); - - if (mediaElements.length === 0) - { - alert('No media elements found on this page.'); - console.log('No media elements found on this page.'); - return; - } - - mediaElements.forEach(function(media) - { - if (media.paused) - { - media.play().then(() => { - console.log('Media played:', media); - }).catch(error => { - console.error('Error playing media:', error); - }); - } - else - { - media.pause(); - console.log('Media paused:', media); - } - }); -} - -function updateStatus(status) -{ - // Handle status updates if needed - console.log('Media status:', status) -} - -// Initialize WebSocket connection -console.log('Attempting to establish WebSocket connection to ws://127.0.0.1:3000/'); -const socket = new WebSocket('ws://127.0.0.1:3000/'); - -socket.addEventListener('open', function (event) { - console.log('WebSocket connection established in background.js'); - // You can send messages here -}); - -socket.addEventListener('error', function (event) { - console.error('WebSocket error in background.js:', event); - // Implement additional error handling if necessary -}); - -socket.addEventListener('message', function (event) { - console.log('Message from server in background.js:', event.data); - try { - const message = JSON.parse(event.data); - if (message.action === 'toggleMedia') { - console.log('Received toggleMedia command from WebSocket server'); - - // Query the active tab in the current window - chrome.tabs.query({active: true, currentWindow: true}, function(tabs) { - if (tabs.length === 0) { - console.warn('No active tabs found.'); - return; - } - - const activeTab = tabs[0]; - console.log('Active tab ID:', activeTab.id); - - // Execute the toggleMediaPlayback function in the active tab - chrome.scripting.executeScript( - { - target: {tabId: activeTab.id}, - function: toggleMediaPlayback - }, - () => { - if (chrome.runtime.lastError) { - console.error('executeScript error:', chrome.runtime.lastError); - } else { - console.log('Executed toggleMediaPlayback in active tab:', activeTab.id); - } - } - ); - }); - } else { - console.warn('Unknown action received:', message.action); - } - } catch (error) { - console.error('Error parsing message from WebSocket:', error); - } -}); - -socket.addEventListener('close', function (event) { - console.log('WebSocket connection closed in background.js:', event); - // Implement reconnection logic if necessary -}); - -// Remove the duplicate message listener to avoid conflicts -// chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { -// if (message.action === 'toggleMedia') { -// console.log('Received toggleMedia command from content.js'); -// // Forward the toggle command to the WebSocket server -// socket.send(JSON.stringify({ action: 'toggleMedia' })); -// } -// }); - -// Function to send toggle command to all connected clients (if needed) -function sendToggleCommand() { - const message = JSON.stringify({ action: 'toggleMedia' }); - console.log('Sending toggleMedia command to all connected clients'); - wss.clients.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(message); - console.log('Sent toggleMedia command to a client'); - } - }); -} \ No newline at end of file diff --git a/streamsage_extension/com.example.media_controller.json b/streamsage_extension/com.example.media_controller.json deleted file mode 100644 index 86857fa..0000000 --- a/streamsage_extension/com.example.media_controller.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "com.example.media_controller", - "description": "Native host for Media Controller extension", - "path": "path/to/nativeHost.js", - "type": "stdio", - "allowed_origins": [ - "chrome-extension://YOUR_EXTENSION_ID/" - ] -} \ No newline at end of file diff --git a/streamsage_extension/content.js b/streamsage_extension/content.js deleted file mode 100644 index 2c4c1e8..0000000 --- a/streamsage_extension/content.js +++ /dev/null @@ -1,57 +0,0 @@ -(() => { - // Remove direct WebSocket connection from content.js - - function toggleMediaPlayback() { - const mediaElements = document.querySelectorAll('video, audio'); - - if (mediaElements.length === 0) { - console.log('No media elements found on this page.'); - return; - } - - mediaElements.forEach(media => { - if (media.paused) { - media.play(); - } else { - media.pause(); - } - }); - - // Send a message to background.js to handle the toggle command - chrome.runtime.sendMessage({ action: 'toggleMedia' }, (response) => { - if (chrome.runtime.lastError) { - console.error('Error sending message to background:', chrome.runtime.lastError); - } else { - console.log('Toggle command sent to background.js'); - } - }); - - console.log('Attempting to establish WebSocket connection to ws://localhost:3000/'); - const socket = new WebSocket('ws://localhost:3000/'); - - socket.addEventListener('open', function (event) { - console.log('WebSocket connection established'); - // You can send messages here - }); - - socket.addEventListener('error', function (event) { - console.error('WebSocket error:', event); - console.log('Error details:', event.message || event); - }); - - socket.addEventListener('message', function (event) { - console.log('Message from server:', event.data); - }); - - socket.addEventListener('close', function (event) { - console.log('WebSocket connection closed:', event); - }); - } - - // Example: Add a keyboard shortcut or button to trigger toggleMediaPlayback - document.addEventListener('keydown', (e) => { - if (e.ctrlKey && e.key === 'm') { // Example: Ctrl + M - toggleMediaPlayback(); - } - }); -})(); \ No newline at end of file diff --git "a/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (1).png" "b/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (1).png" deleted file mode 100644 index 5c24361..0000000 Binary files "a/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (1).png" and /dev/null differ diff --git "a/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (2).png" "b/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (2).png" deleted file mode 100644 index 9b54940..0000000 Binary files "a/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (2).png" and /dev/null differ diff --git "a/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (3).png" "b/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (3).png" deleted file mode 100644 index a8c12f8..0000000 Binary files "a/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (3).png" and /dev/null differ diff --git "a/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (4).png" "b/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (4).png" deleted file mode 100644 index a4b639f..0000000 Binary files "a/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM (4).png" and /dev/null differ diff --git "a/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM.png" "b/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM.png" deleted file mode 100644 index 9b54940..0000000 Binary files "a/streamsage_extension/icons/Screenshot 2024-10-06 at 9.43.56\342\200\257PM.png" and /dev/null differ diff --git a/streamsage_extension/icons/icon128.png b/streamsage_extension/icons/icon128.png deleted file mode 100644 index f61e6b2..0000000 Binary files a/streamsage_extension/icons/icon128.png and /dev/null differ diff --git a/streamsage_extension/icons/icon48.png b/streamsage_extension/icons/icon48.png deleted file mode 100644 index 7b4fbbe..0000000 Binary files a/streamsage_extension/icons/icon48.png and /dev/null differ diff --git a/streamsage_extension/icons/icons.jpg b/streamsage_extension/icons/icons.jpg deleted file mode 100644 index 985659d..0000000 Binary files a/streamsage_extension/icons/icons.jpg and /dev/null differ diff --git a/streamsage_extension/index.html b/streamsage_extension/index.html deleted file mode 100644 index 21c9bbf..0000000 --- a/streamsage_extension/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - Electron Control Panel - - -

Electron Control Panel

- - - - \ No newline at end of file diff --git a/streamsage_extension/main.js b/streamsage_extension/main.js deleted file mode 100644 index 29b196b..0000000 --- a/streamsage_extension/main.js +++ /dev/null @@ -1,81 +0,0 @@ -const { app, BrowserWindow, ipcMain } = require('electron'); -const path = require('path'); -const WebSocket = require('ws'); - -let mainWindow; -let wss; - -function createWindow () { - mainWindow = new BrowserWindow({ - width: 800, - height: 600, - webPreferences: { - preload: path.join(__dirname, 'preload.js'), - nodeIntegration: false, - contextIsolation: true - } - }); - - mainWindow.loadFile('index.html'); -} - -app.whenReady().then(() => { - createWindow(); - - console.log('Initializing WebSocket server on port 3000'); - // Initialize WebSocket server on port 3000 - wss = new WebSocket.Server({ port: 3000 }, () => { - console.log('WebSocket Server is listening on ws://localhost:3000'); - }); - - wss.on('connection', (ws) => { - console.log('A new client Connected!'); - ws.send('Welcome New Client!'); - - ws.on('message', (message) => { - console.log('received: %s', message); - // Broadcast to all connected clients - wss.clients.forEach((client) => { - if (client !== ws && client.readyState === WebSocket.OPEN) { - client.send(message); - console.log('Broadcasted message to a client'); - } - }); - }); - - ws.on('error', (error) => { - console.error('WebSocket server error:', error); - }); - - ws.on('close', () => { - console.log('Chrome extension disconnected'); - }); - }); - - // Handle IPC from renderer to send commands via WebSocket - ipcMain.on('send-toggle', (event, arg) => { - console.log('Received toggle command from renderer'); - sendToggleCommand(); - }); - - app.on('activate', function () { - if (BrowserWindow.getAllWindows().length === 0) createWindow(); - }); -}); - -app.on('window-all-closed', function () { - console.log('All windows closed. Quitting app.'); - if (process.platform !== 'darwin') app.quit(); -}); - -// Function to send toggle command to all connected clients -function sendToggleCommand() { - const message = JSON.stringify({ action: 'toggleMedia' }); - console.log('Sending toggleMedia command to all connected clients'); - wss.clients.forEach(client => { - if (client.readyState === WebSocket.OPEN) { - client.send(message); - console.log('Sent toggleMedia command to a client'); - } - }); -} \ No newline at end of file diff --git a/streamsage_extension/manifest.json b/streamsage_extension/manifest.json deleted file mode 100644 index 5bc7470..0000000 --- a/streamsage_extension/manifest.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "manifest_version": 3, - "name": "Media Controller", - "version": "1.0", - "description": "Play or pause media using WebSocket commands and a browser button.", - "permissions": - [ - "activeTab", - "scripting" - ], - "host_permissions": - [ - "ws://127.0.0.1:3000/*" - ], - "background": - { - "service_worker": "background.js" - }, - "action": - { - "default_popup": "popup.html", - "default_icon": - { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - } - }, - "icons": - { - "16": "icons/icon16.png", - "48": "icons/icon48.png", - "128": "icons/icon128.png" - }, - "content_scripts": - [ - { - "matches": [""], - "js": ["content.js"] - } - ] -} \ No newline at end of file diff --git a/streamsage_extension/native/nativeHost.js b/streamsage_extension/native/nativeHost.js deleted file mode 100644 index 64514b4..0000000 --- a/streamsage_extension/native/nativeHost.js +++ /dev/null @@ -1,34 +0,0 @@ -const http = require('http') -const readline = require('readline') - -const PORT = 8080 - -// Function to send messages to the Chrome extension -function sendMessageToExtension(message) -{ - // Implement Native Messaging protocol to communicate with the extension - // This typically involves reading from stdin and writing to stdout with proper framing - // For simplicity, this is a placeholder - console.log(JSON.stringify(message)) -} - -// Create HTTP server -const server = http.createServer((req, res) => -{ - if (req.method === 'POST' && req.url === '/toggle') - { - sendMessageToExtension({action: "toggleMedia"}) - res.writeHead(200, {'Content-Type': 'text/plain'}) - res.end('Toggle command sent.\n') - } - else - { - res.writeHead(404, {'Content-Type': 'text/plain'}) - res.end('Not Found\n') - } -}) - -server.listen(PORT, () => -{ - console.log(`Native host listening on port ${PORT}`) -}) \ No newline at end of file diff --git a/streamsage_extension/package.json b/streamsage_extension/package.json deleted file mode 100644 index 63ecbdd..0000000 --- a/streamsage_extension/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "dependencies": { - "electron": "^32.1.2", - "ws": "^8.18.0" - }, - "scripts": { - "start": "node main.js" - } -} diff --git a/streamsage_extension/popup.html b/streamsage_extension/popup.html deleted file mode 100644 index af24987..0000000 --- a/streamsage_extension/popup.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - Media Controller - - - -
Connected to Electron app.
- - - - - \ No newline at end of file diff --git a/streamsage_extension/popup.js b/streamsage_extension/popup.js deleted file mode 100644 index bc414ef..0000000 --- a/streamsage_extension/popup.js +++ /dev/null @@ -1,53 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() -{ - const statusDiv = document.getElementById('status') - const toggleButton = document.getElementById('toggleButton') - - // Update status on load - updateMediaStatus() - - // Add event listener to button - toggleButton.addEventListener('click', function() - { - chrome.runtime.sendMessage({action: "toggleMedia"}) - setTimeout(updateMediaStatus, 500) // Delay to allow state change - }) - - function updateMediaStatus() - { - chrome.tabs.query({active: true, currentWindow: true}, function(tabs) - { - chrome.scripting.executeScript( - { - target: {tabId: tabs[0].id}, - function: getMediaStatus - }, - (results) => - { - if (results && results[0] && results[0].result) - { - statusDiv.textContent = results[0].result - } - else - { - statusDiv.textContent = 'No media found' - } - } - ) - }) - } -}) - -function getMediaStatus() -{ - const mediaElements = document.querySelectorAll('video, audio') - - if (mediaElements.length === 0) - { - return 'No media on this page' - } - - const isPlaying = Array.from(mediaElements).some(media => !media.paused) - - return isPlaying ? 'Playing' : 'Paused' -} \ No newline at end of file diff --git a/streamsage_extension/preload.js b/streamsage_extension/preload.js deleted file mode 100644 index 0519ecb..0000000 --- a/streamsage_extension/preload.js +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/streamsage_extension/renderer.js b/streamsage_extension/renderer.js deleted file mode 100644 index a1346d9..0000000 --- a/streamsage_extension/renderer.js +++ /dev/null @@ -1,5 +0,0 @@ -const { ipcRenderer } = require('electron'); - -document.getElementById('toggleButton').addEventListener('click', () => { - ipcRenderer.send('send-toggle', 'toggleMedia'); -}); \ No newline at end of file