├── src ├── js │ ├── main.js │ ├── autoscroll.js │ └── app.js └── css │ └── main.css ├── assets ├── screenshot1.png └── psy-dis.svg ├── package.json ├── LICENSE ├── .gitignore ├── index.html └── README.md /src/js/main.js: -------------------------------------------------------------------------------- 1 | import '../css/main.css'; 2 | import './app.js'; 3 | import './autoscroll.js'; -------------------------------------------------------------------------------- /assets/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/us/ai-interview-assistant/HEAD/assets/screenshot1.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speech-to-text-app", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite --port=3000", 8 | "build": "vite build", 9 | "preview": "vite preview" 10 | }, 11 | "devDependencies": { 12 | "vite": "^5.0.0" 13 | }, 14 | "dependencies": { 15 | "yarn": "^1.22.22" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/js/autoscroll.js: -------------------------------------------------------------------------------- 1 | // autoScroll.js 2 | document.addEventListener('DOMContentLoaded', () => { 3 | const resultElement = document.getElementById('result'); 4 | const chatgptResultElement = document.getElementById('chatgptResponse'); 5 | // Function to scroll to the bottom 6 | function scrollToBottom() { 7 | resultElement.scrollTop = resultElement.scrollHeight; 8 | chatgptResultElement.scrollTop = chatgptResultElement.scrollHeight 9 | } 10 | 11 | // Assuming content is appended dynamically, call scrollToBottom() after updates 12 | // For demonstration, assume the content is updated every second 13 | setInterval(() => { 14 | scrollToBottom(); 15 | // chatgptResultElement(); 16 | }, 1000); 17 | }); 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GLWT(Good Luck With That) Public License 2 | Copyright (c) Everyone, except Author 3 | 4 | Everyone is permitted to copy, distribute, modify, merge, sell, publish, 5 | sublicense or whatever they want with this software but at their OWN RISK. 6 | 7 | Preamble 8 | 9 | The author has absolutely no clue what the code in this project does. 10 | It might just work or not, there is no third option. 11 | 12 | 13 | GOOD LUCK WITH THAT PUBLIC LICENSE 14 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION, AND MODIFICATION 15 | 16 | 0. You just DO WHATEVER YOU WANT TO as long as you NEVER LEAVE A 17 | TRACE TO TRACK THE AUTHOR of the original product to blame for or hold 18 | responsible. 19 | 20 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | 25 | Good luck and Godspeed. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | # Node modules directory 27 | node_modules/ 28 | 29 | # Logs 30 | *.log 31 | logs/ 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Dependency directories 37 | package-lock.json 38 | yarn.lock 39 | 40 | # Compiled source # 41 | ################### 42 | dist/ 43 | build/ 44 | *.o 45 | *.so 46 | *.dylib 47 | *.dll 48 | *.exe 49 | *.out 50 | 51 | # Environment files 52 | .env 53 | .env.local 54 | .env.*.local 55 | 56 | # IDE and editor directories 57 | .vscode/ 58 | .idea/ 59 | *.suo 60 | *.ntvs* 61 | *.njsproj 62 | *.sln 63 | *.sw? 64 | *.swo 65 | *.swn 66 | 67 | # macOS 68 | .DS_Store 69 | 70 | # Windows 71 | Thumbs.db 72 | Desktop.ini 73 | 74 | # Linux 75 | *~ 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # Miscellaneous 81 | *.tgz 82 | *.gz 83 | *.zip 84 | *.rar 85 | 86 | # Coverage directory used by tools like istanbul 87 | coverage/ 88 | 89 | # npm package and Yarn cache 90 | *.tgz 91 | .yarn/ 92 | .pnp.* 93 | 94 | # Temporary files 95 | *.tmp 96 | *.temp 97 | *.swp 98 | *.swo 99 | 100 | # Public assets and build artifacts 101 | public/ 102 | *.css.map 103 | *.js.map 104 | *.map 105 | 106 | # Shell scripts 107 | *.sh 108 | 109 | # Ignore editor-specific files 110 | *.sublime-workspace 111 | *.sublime-project 112 | -------------------------------------------------------------------------------- /src/css/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | font-synthesis: none; 6 | text-rendering: optimizeLegibility; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | * { 12 | margin: 0; 13 | padding: 0; 14 | box-sizing: border-box; 15 | } 16 | 17 | body { 18 | background: radial-gradient( 19 | circle at 100%, 20 | rgba(3, 6, 21, 0.9) 15%, 21 | rgba(189, 205, 226, 0.5) 5%, 22 | rgba(7, 9, 22, 0.9) 15% 23 | ), 24 | url('../assets/psy-dis.svg') center/cover; 25 | height: 100vh; 26 | padding: 40px 0; 27 | overflow-y: auto; 28 | } 29 | 30 | .container { 31 | max-width: 1100px; 32 | margin: 0 auto; 33 | display: flex; 34 | flex-direction: column; 35 | align-items: center; 36 | padding: 0 15px; 37 | } 38 | 39 | h1 { 40 | color: #fff; 41 | font-size: 1.5rem; 42 | text-transform: uppercase; 43 | } 44 | 45 | .btn-wrapper { 46 | margin-top: 20px; 47 | display: flex; 48 | flex-wrap: wrap; 49 | justify-content: center; 50 | align-items: center; 51 | gap: 10px; 52 | } 53 | 54 | button { 55 | display: flex; 56 | align-items: center; 57 | column-gap: 5px; 58 | border: none; 59 | cursor: pointer; 60 | padding: 12px 24px; 61 | border-radius: 3px; 62 | font-weight: 600; 63 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); 64 | transition: opacity 400ms ease-in-out; 65 | } 66 | 67 | button:disabled { 68 | opacity: 0.47; 69 | cursor: default; 70 | } 71 | 72 | button:hover:not(:disabled) { 73 | opacity: 0.9; 74 | } 75 | 76 | button > svg { 77 | height: 1rem; 78 | } 79 | 80 | .result { 81 | background-color: #fff; 82 | width: 100%; 83 | max-height: 400px; /* Set maximum height */ 84 | min-height: 200px; 85 | padding: 10px; 86 | border-radius: 3px; 87 | margin-top: 20px; 88 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); 89 | text-transform: capitalize; 90 | overflow-y: auto; /* Enable vertical scrolling */ 91 | scroll-behavior: smooth; /* Smooth scrolling */ 92 | } 93 | 94 | .result:empty { 95 | display: none; 96 | } 97 | 98 | .hidden { 99 | display: none !important; 100 | } 101 | 102 | @media screen and (min-width: 768px) { 103 | h1 { 104 | font-size: 3.125rem; 105 | text-transform: capitalize; 106 | } 107 | 108 | .container { 109 | padding: 0 30px; 110 | } 111 | 112 | .result { 113 | padding: 15px; 114 | } 115 | } 116 | 117 | .d-flex { 118 | display: flex; 119 | align-items: stretch; 120 | } 121 | 122 | .flex-row { 123 | flex-direction: row; 124 | } 125 | 126 | .flex-fill { 127 | flex: 1; 128 | } 129 | 130 | .p-2 { 131 | padding: 0.5rem; /* Example padding */ 132 | } 133 | 134 | .flex-item { 135 | flex-grow: 1; 136 | flex-shrink: 1; 137 | flex-basis: 0; 138 | max-width: 50%; /* Ensures it does not grow beyond 50% of the container's width */ 139 | } 140 | 141 | .hidden { 142 | display: none !important; 143 | } 144 | 145 | .form-label { 146 | /* display: block; 147 | margin-bottom: 5px; 148 | font-size: 0.875rem; */ 149 | color: #ffffff; 150 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | HARK 11 | 12 | 13 | 14 |
15 |

HARK

16 |
17 |
18 |
19 | 20 | 21 |
22 | 23 |
24 | 34 | 35 | 36 |
37 | 38 |
39 | 40 | 45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 | 57 | 64 |
65 | 66 | 67 |
68 | 69 | 70 |
71 |
72 | 73 |
74 | 75 |
76 |
77 |
78 |
79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hark: The Ultimate Real-Time Speech-to-Text-to-LLM* 🚀 2 | [![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com) 3 | 4 | ![screenshot of app](https://github.com/us/Hark/blob/main/assets/screenshot1.png) 5 | 6 | ![chatgpt cheat](https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHBkcW11OTBvYjZvZnlpY3libTI1eWJ5a3V3dTR3ZWdoNXNvZjVneSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ehgxL0CGDqOtO8MYjv/giphy-downsized-medium.gif) 7 | 8 | Hark is your new favorite gadget for turning live audio into text, all while mingling with OpenAI’s GPT-4 for some extra brainpower! Whether you're capturing epic meetings or casual chats, Hark’s got you covered with its slick features and nerdy charm. 9 | 10 | # 🌟 Key Features 11 | 12 | - **Real-Time Speech-to-Text-to-LLM**: Watch in awe as live audio transforms into text instantaneously thanks to cutting-edge speech recognition. 13 | - **Multi-Language Support**: Speak in your language of choice! Hark supports a ton of languages for flawless transcriptions. 14 | - **Interactive GPT-4 Integration**: Chat with OpenAI’s GPT-4 for smart answers and insights that go beyond mere transcription. 15 | - **Meeting Summarization**: Get concise summaries of your meetings that highlight all the important bits without the fluff. 16 | - **User-Friendly Interface**: Big, friendly buttons for starting, stopping, and clearing recordings—perfect for all levels of tech wizardry. 17 | 18 | # 🚀 Getting Started 19 | 20 | Gear up and get ready to roll! Make sure you have [Node.js](https://nodejs.org/) and [Yarn](https://yarnpkg.com/) installed on your machine. 21 | 22 | ## Installation 23 | 24 | 1. **Clone the repo:** 25 | 26 | ```bash 27 | git clone https://github.com/us/hark.git 28 | cd hark 29 | ``` 30 | 31 | 2. **Install dependencies:** 32 | 33 | ```bash 34 | npm install yarn 35 | yarn 36 | ``` 37 | 38 | # 🎧 Usage Guide 39 | 40 | ## Audio Input Setup 41 | 42 | ### macOS (OS X) 43 | 44 | To capture system audio on macOS, grab BlackHole—a nifty virtual audio driver. 45 | 46 | 1. **Install BlackHole**: Download and install BlackHole. 47 | 48 | 2. **Create a Multi-Output Device**: 49 | - Open Audio MIDI Setup. 50 | - Hit the + button and choose "Create Multi-Output Device." 51 | - Add your speakers and BlackHole to this device. 52 | - Set it as your system audio output. 53 | 54 | 3. **Set BlackHole as Input**: 55 | - In Hark, select BlackHole from the audio input device dropdown. 56 | 57 | ### Windows 58 | 59 | To achieve a similar setup on Windows, use **Voicemeeter**. 60 | 61 | 1. **Install Voicemeeter**: Download and install [Voicemeeter](https://vb-audio.com/Voicemeeter/). 62 | 63 | 2. **Configure Voicemeeter**: 64 | - Open Voicemeeter. 65 | - Set **Hardware Input 1** as your default microphone and send it only to `B`. 66 | - Also, send the virtual input to both `A` and `B` (with `A` for hearing through your default speakers and `B` for virtual output). 67 | - Set **Hardware Out A1** as your default output, typically your system speakers. 68 | - Double-check the Windows sound settings in the system tray to ensure Voicemeeter hasn’t changed your default speaker output. (Keep your sound output as your default device, not voicemeeter!) 69 | 70 | 3. **Configure Audio in Google Meet and Hark**: 71 | - In Google Meet, set the input as your default mic and output as Voicemeeter Input. 72 | - In Hark, choose **B1** as the input device. 73 | 74 | ## Run the Application 75 | 76 | Fire up your local server with: 77 | 78 | ```bash 79 | yarn dev 80 | ``` 81 | 82 | Then check out your app at [http://localhost:3000](http://localhost:3000). 83 | 84 | # 🔧 Configuration 85 | 86 | # 📜 How It Works 87 | 88 | 1. **Select Audio Device**: Choose BlackHole to capture system sound. 89 | 2. **Start Recording**: Hit "Start Recording" to capture and transcribe audio in real-time. 90 | 3. **Language Selection**: Pick your preferred language from the dropdown. 91 | 4. **Ask GPT-4**: Use "Answer the Latest Question" to get smart responses from GPT-4. 92 | 5. **Summarize Meeting**: Click "What’s This Meeting About?" for a quick summary of your discussion. 93 | 6. **Stop Recording**: End the session with "Stop Recording." 94 | 7. **Clear Results**: Hit "Clear" to reset and prep for the next session. 95 | 96 | ![Gollum Image](https://media.giphy.com/media/V4uGHRgz0zi6Y/giphy-downsized-large.gif) 97 | 98 | # 🔮 Future Features 99 | 100 | - **Whisper Integration**: Planning to add Whisper API for even more accurate transcriptions. Note: It's heavy and slow, so our current system is still quicker. 101 | - **More Languages**: Expanding language options to cover even more tongues. 102 | - **React UI Overhaul**: A fresh, React-based UI to make the interface even more user-friendly. 103 | - **Local Speech-to-Text Models**: Offline capabilities so you’re never left hanging. 104 | - **Expanded Model Support**: Additional AI models for broader interaction possibilities. 105 | 106 | # 🔍 Final Checklist Before Using Hark 107 | 108 | Before you dive into using Hark, make sure you've completed these steps for a seamless experience: 109 | 110 | 1. **Audio Routing**: Ensure that audio routing is correctly set up with BlackHole (or a similar virtual audio driver). BlackHole captures system audio, allowing Hark to process sound from other applications. 111 | 2. **Input Device Configuration**: Verify that BlackHole is selected as the input device within Hark. This ensures the app captures all system sounds accurately. 112 | 3. **API Key Setup**: Enter your OpenAI API key in `app.js` to enable GPT-4 interactions. 113 | 4. **Model Selection**: Choose the appropriate GPT-4 model for your needs. 114 | 5. **Application Testing**: Start listening with Hark, and test by asking questions to ensure everything works as expected. 115 | 116 | By following these steps, you ensure that Hark is fully functional and ready to provide a smooth, real-time transcription and interaction experience. 117 | 118 | # 🤝 Contributing 119 | 120 | Got ideas or want to help out? We’re all ears! Submit a pull request or open an issue to join the fun. 121 | 122 | ### How this will help you: 123 | - More feedback to fix and improve your project. 124 | - New ideas about your project. 125 | - Greater fame. 126 | - ![SungerBob Image](https://media.giphy.com/media/3o7absbD7PbTFQa0c8/source.gif) 127 | 128 | --- 129 | 130 | _“Sharing knowledge is the most fundamental act of friendship. Because it is a way you can give something without losing something.”_ 131 | 132 | _**— Richard Stallman**_ 133 | 134 | ## 📜 License 135 | This project is licensed under [GLWTPL](https://github.com/us/hark/blob/master/LICENSE) (GOOD LUCK WITH THAT PUBLIC LICENSE) 136 | 137 | ## ⚠️ Disclaimer 138 | It is built for educational purposes only. If you choose to use it otherwise, the developers will not be held responsible. Please, do not use it with evil intent. 139 | 140 | # 📬 Contact 141 | 142 | Questions, suggestions, or just want to chat? Hit us up at [rahmetsaritekin@gmail.com](mailto:rahmetsaritekin@gmail.com). 143 | -------------------------------------------------------------------------------- /src/js/app.js: -------------------------------------------------------------------------------- 1 | const audioDeviceSelect = document.getElementById('audioDeviceSelect'); 2 | const resultElement = document.getElementById('result'); 3 | const startBtn = document.getElementById('startBtn'); 4 | const animatedSvg = startBtn.querySelector('svg'); 5 | const stopBtn = document.getElementById('stopBtn'); 6 | const clearBtn = document.getElementById('clearBtn'); 7 | const languageSelect = document.getElementById('languageSelect'); 8 | const askChatGPTBtn = document.getElementById('askChatGPTBtn'); 9 | const summarizeBtn = document.getElementById('summarizeBtn'); 10 | const wordCountInput = document.getElementById('wordCountInput'); 11 | const chatgptResponse = document.getElementById('chatgptResponse'); 12 | const apiKeyInput = document.getElementById('apiKeyInput'); 13 | const modelSelect = document.getElementById('modelSelect'); 14 | 15 | let recognition; 16 | let fullTranscript = ''; // Store full transcript 17 | let recognizing = false; // Flag to track recognition state 18 | let restartTimeout; // Timeout for debouncing recognition restarts 19 | 20 | // Load saved state from localStorage 21 | loadState(); 22 | 23 | // List available audio input devices and populate the dropdown 24 | navigator.mediaDevices.enumerateDevices() 25 | .then(devices => { 26 | devices.forEach(device => { 27 | if (device.kind === 'audioinput') { 28 | const option = document.createElement('option'); 29 | option.value = device.deviceId; 30 | option.text = `${device.label || 'Unknown Device'} (${device.kind})`; 31 | audioDeviceSelect.appendChild(option); 32 | } 33 | }); 34 | 35 | // Restore previously selected device 36 | const savedDeviceId = localStorage.getItem('audioDeviceId'); 37 | if (savedDeviceId) { 38 | audioDeviceSelect.value = savedDeviceId; 39 | setAudioInputDevice(savedDeviceId); 40 | } 41 | }) 42 | .catch(err => console.error('Error accessing audio devices:', err)); 43 | 44 | // Set the selected audio input device and start the speech recognition 45 | audioDeviceSelect.addEventListener('change', () => { 46 | const selectedDeviceId = audioDeviceSelect.value; 47 | localStorage.setItem('audioDeviceId', selectedDeviceId); 48 | setAudioInputDevice(selectedDeviceId); 49 | }); 50 | 51 | // Configure the audio input device 52 | function setAudioInputDevice(deviceId) { 53 | const constraints = { 54 | audio: { 55 | deviceId: deviceId ? { exact: deviceId } : undefined, 56 | echoCancellation: true, // Yankı iptali 57 | noiseSuppression: true, // Gürültü azaltma 58 | autoGainControl: true // Otomatik kazanç kontrolü, ses seviyesini dengelemek için 59 | } 60 | }; 61 | 62 | navigator.mediaDevices.getUserMedia(constraints) 63 | .then(stream => { 64 | const audioTracks = stream.getAudioTracks(); 65 | console.log('Using audio input device:', audioTracks[0].label); 66 | 67 | if (recognition && typeof recognition.stop === 'function') { 68 | recognition.stop(); 69 | } 70 | 71 | recognition = new (window.SpeechRecognition || window.webkitSpeechRecognition)(); 72 | recognition.lang = languageSelect.value; // Keep the same language setting 73 | recognition.continuous = true; 74 | recognition.interimResults = true; 75 | 76 | recognition.onstart = () => { 77 | startBtn.disabled = true; 78 | stopBtn.disabled = false; 79 | animatedSvg.classList.remove('hidden'); 80 | recognizing = true; 81 | console.log('Recording started'); 82 | }; 83 | 84 | recognition.onresult = function (event) { 85 | let interimTranscript = ''; 86 | for (let i = event.resultIndex; i < event.results.length; i++) { 87 | if (event.results[i].isFinal) { 88 | const finalTranscript = event.results[i][0].transcript.trim(); 89 | fullTranscript += finalTranscript + ' '; // Append to full transcript 90 | } else { 91 | interimTranscript += event.results[i][0].transcript; // Capture interim results 92 | } 93 | } 94 | resultElement.innerText = fullTranscript + interimTranscript; // Display interim and final transcripts 95 | saveState(); // Save state after each result 96 | }; 97 | 98 | recognition.onerror = function (event) { 99 | console.error('Speech recognition error:', event.error); 100 | if (recognizing && event.error !== 'aborted') { 101 | restartRecognition(); 102 | } 103 | }; 104 | 105 | recognition.onend = function () { 106 | if (recognizing) { 107 | console.log('Speech recognition ended, restarting...'); 108 | restartRecognition(); 109 | } else { 110 | startBtn.disabled = false; 111 | stopBtn.disabled = true; 112 | animatedSvg.classList.add('hidden'); 113 | console.log('Speech recognition stopped'); 114 | } 115 | }; 116 | 117 | recognition.start(); 118 | }) 119 | .catch(err => console.error('Error accessing selected audio input:', err)); 120 | } 121 | 122 | // Restart the recognition process with a delay to handle pauses 123 | function restartRecognition() { 124 | clearTimeout(restartTimeout); 125 | restartTimeout = setTimeout(() => { 126 | if (recognizing) { 127 | console.log('Restarting recognition after pause.'); 128 | recognition.start(); 129 | } 130 | }, 1000); // 1 second delay before restarting 131 | } 132 | 133 | // Start recording 134 | function startRecording() { 135 | recognizing = true; 136 | const selectedDeviceId = audioDeviceSelect.value; 137 | setAudioInputDevice(selectedDeviceId); 138 | } 139 | 140 | // Stop recording 141 | function stopRecording() { 142 | recognizing = false; 143 | clearTimeout(restartTimeout); // Clear any pending restarts 144 | if (recognition && typeof recognition.stop === 'function') { 145 | recognition.stop(); 146 | } 147 | } 148 | 149 | // Update the language for speech recognition 150 | function updateLanguage() { 151 | if (recognition) { 152 | recognition.lang = languageSelect.value; 153 | console.log(`Language changed to: ${recognition.lang}`); 154 | saveState(); // Save state when language is changed 155 | } 156 | } 157 | 158 | // Handle ChatGPT interactions 159 | async function askChatGPT(type) { 160 | const wordCount = parseInt(wordCountInput.value, 10) || 100; // Default to 100 words if not a valid number 161 | const lastWords = getLastWords(wordCount); 162 | 163 | // Save API key and model selection whenever ChatGPT is called 164 | localStorage.setItem('apiKey', apiKeyInput.value); 165 | localStorage.setItem('model', modelSelect.value); 166 | 167 | const apiKey = apiKeyInput.value; // Get the current API key from input 168 | const model = modelSelect.value; // Get the current model from selection 169 | 170 | let prompt; 171 | if (type === 'latestQuestion') { 172 | prompt = 173 | `You are attending a meeting, and the following transcription has been captured by a speech-to-text system. Some words may be incorrect. Please answer the latest question asked by the interviewer, providing a brief response first, followed by a more detailed explanation if necessary. Here is the text: 174 | 175 | "${lastWords}" 176 | 177 | What is the answer to the latest question?`; 178 | } else if (type === 'summary') { 179 | prompt = 180 | `You are reviewing a transcription of a meeting. The transcription was captured by a speech-to-text system, so some words may be incorrect. Please summarize what the meeting is about, providing a brief overview first, followed by more details if necessary. Here is the text: 181 | 182 | "${lastWords}" 183 | 184 | What is this meeting about?`; 185 | } 186 | 187 | try { 188 | const response = await fetch('https://api.openai.com/v1/chat/completions', { 189 | method: 'POST', 190 | headers: { 191 | 'Content-Type': 'application/json', 192 | 'Authorization': `Bearer ${apiKey}` // Use the stored API key 193 | }, 194 | body: JSON.stringify({ 195 | model: model, // Use the stored model 196 | messages: [ 197 | { "role": "system", "content": "You are a helpful assistant." }, 198 | { "role": "user", "content": prompt } 199 | ], 200 | stream: true // Enable streaming 201 | }) 202 | }); 203 | 204 | if (!response.ok) { 205 | throw new Error(`Error with OpenAI API: ${response.statusText}`); 206 | } 207 | 208 | const reader = response.body.getReader(); 209 | const decoder = new TextDecoder('utf-8'); 210 | let partialData = ''; 211 | chatgptResponse.innerText = ''; // Clear previous response 212 | 213 | while (true) { 214 | const { done, value } = await reader.read(); 215 | if (done) break; 216 | partialData += decoder.decode(value, { stream: true }); 217 | 218 | // Process chunks as they arrive 219 | const segments = partialData.split('\n'); 220 | partialData = segments.pop(); // Keep incomplete data for next chunk 221 | 222 | for (const segment of segments) { 223 | if (segment.trim() === '') continue; 224 | 225 | // Check for [DONE] message 226 | if (segment === 'data: [DONE]') { 227 | return; // End of stream, exit the loop 228 | } 229 | 230 | // Parse JSON message 231 | const jsonSegment = segment.slice(6); // Remove 'data: ' prefix 232 | try { 233 | const message = JSON.parse(jsonSegment); 234 | if (message.choices) { 235 | const text = message.choices[0].delta?.content || ''; 236 | chatgptResponse.innerText += text; // Append to response 237 | } 238 | } catch (err) { 239 | console.error('Error parsing JSON:', err); 240 | } 241 | } 242 | } 243 | } catch (error) { 244 | console.error('Error with OpenAI API:', error); 245 | chatgptResponse.innerText = 'Failed to get a response from ChatGPT.'; 246 | } 247 | } 248 | 249 | // Get the last few words of the transcript 250 | function getLastWords(wordCount) { 251 | const words = fullTranscript.split(/\s+/); 252 | return words.slice(-wordCount).join(' '); 253 | } 254 | 255 | // Clear results and reset the transcript 256 | function clearResults() { 257 | fullTranscript = ''; 258 | resultElement.innerText = ''; 259 | chatgptResponse.innerText = ''; 260 | localStorage.removeItem('fullTranscript'); 261 | console.log('Results cleared'); 262 | } 263 | 264 | // Save the current state to localStorage 265 | function saveState() { 266 | localStorage.setItem('fullTranscript', fullTranscript); 267 | localStorage.setItem('language', languageSelect.value); 268 | localStorage.setItem('wordCount', wordCountInput.value); 269 | const selectedDeviceId = audioDeviceSelect.value; 270 | localStorage.setItem('audioDeviceId', selectedDeviceId); 271 | } 272 | 273 | // Load the saved state from localStorage 274 | function loadState() { 275 | const savedTranscript = localStorage.getItem('fullTranscript'); 276 | const savedLanguage = localStorage.getItem('language'); 277 | const savedWordCount = localStorage.getItem('wordCount'); 278 | const savedDeviceId = localStorage.getItem('audioDeviceId'); 279 | const savedApiKey = localStorage.getItem('apiKey'); // Load API key 280 | const savedModel = localStorage.getItem('model'); // Load model 281 | 282 | if (savedTranscript) { 283 | fullTranscript = savedTranscript; 284 | resultElement.innerText = fullTranscript; 285 | } 286 | if (savedLanguage) { 287 | languageSelect.value = savedLanguage; 288 | } 289 | if (savedWordCount) { 290 | wordCountInput.value = savedWordCount; 291 | } 292 | if (savedDeviceId) { 293 | audioDeviceSelect.value = savedDeviceId; 294 | } 295 | if (savedApiKey) { 296 | apiKeyInput.value = savedApiKey; // Set API key input 297 | } 298 | if (savedModel) { 299 | modelSelect.value = savedModel; // Set model select 300 | } 301 | } 302 | 303 | // Initialize event listeners 304 | startBtn.addEventListener('click', startRecording); 305 | stopBtn.addEventListener('click', stopRecording); 306 | clearBtn.addEventListener('click', clearResults); 307 | languageSelect.addEventListener('change', updateLanguage); 308 | askChatGPTBtn.addEventListener('click', () => askChatGPT('latestQuestion')); 309 | summarizeBtn.addEventListener('click', () => askChatGPT('summary')); 310 | -------------------------------------------------------------------------------- /assets/psy-dis.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 13 | 16 | 18 | 21 | 24 | 28 | 31 | 35 | 39 | 44 | 48 | 52 | 57 | 71 | 84 | 99 | 102 | 118 | 133 | 150 | 165 | 180 | 182 | 198 | 214 | 231 | 249 | 266 | 283 | 300 | 317 | 333 | 349 | 365 | 380 | 395 | 410 | 424 | 438 | 452 | 465 | 478 | 778 | 786 | 793 | 802 | 811 | 818 | 825 | 832 | 838 | 844 | 848 | 851 | 854 | 856 | 857 | 858 | --------------------------------------------------------------------------------