├── 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 | [](https://forthebadge.com)
3 |
4 | 
5 |
6 | 
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 | 
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 | - 
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 |
858 |
--------------------------------------------------------------------------------