├── LICENSE ├── README.md ├── background.js ├── manifest.json └── scripts ├── content-script.js └── getYt.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 S1mon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Summize - A YouTube Video Summarizer 2 | 3 | ![example](https://user-images.githubusercontent.com/47905276/217866101-15722291-994f-4a73-be7f-4dc6e13f92e7.png) 4 | 5 | Summize is a browser extension that summarizes YouTube videos using OpenAI's GPT-3.5 language model. With it, you can quickly get a summary of a video, saving you time and making it easier to understand. 6 | 7 | ## Features 8 | 9 | - Summarizes YouTube videos in real-time when you open the video 10 | - Uses OpenAI's GPT-3.5 language model to generate summaries 11 | - Uses chatgpt's website to generate summaries instead of the api, making it completely free 12 | 13 | ## Installation 14 | 15 | To install Summize, follow these steps: 16 | 17 | 1. Download the extension from GitHub by clicking the "Download" button on the repository page. 18 | 2. In your Chrome browser, navigate to the "Extensions" page (chrome://extensions/). 19 | 3. Enable "Developer mode" by clicking the toggle switch in the top-right corner of the page. 20 | 4. Click the "Load Unpacked" button and select the downloaded folder for the Summize extension. 21 | 5. The extension should now be installed and ready to use. 22 | 23 | ## Usage 24 | 25 | Once the extension is installed, you can use it by simply visiting a YouTube video and it will summarize it. 26 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | chrome.commands.onCommand.addListener((shortcut) => { 2 | console.log('lets reload'); 3 | console.log(shortcut); 4 | if (shortcut.includes("+M")) { 5 | // reload current tab 6 | chrome.tabs.reload(); 7 | chrome.runtime.reload(); 8 | 9 | } 10 | }) 11 | 12 | function generateUUIDv4() { 13 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 14 | var r = Math.random() * 16 | 0, 15 | v = c === 'x' ? r : (r & 0x3 | 0x8); 16 | return v.toString(16); 17 | }); 18 | } 19 | 20 | 21 | async function getToken() { 22 | 23 | let accessToken 24 | let response = await fetch("https://chat.openai.com/api/auth/session") 25 | 26 | let body = await response.json() 27 | 28 | console.log(body.accessToken) 29 | 30 | accessToken = body.accessToken 31 | 32 | return accessToken 33 | } 34 | 35 | let targetTabId; // The ID of the tab where the content script is running 36 | let oldMessageId 37 | 38 | 39 | async function handleStreamedResponse(token, question, videoId, parts) { 40 | // Make a request to the server that returns a streamed response 41 | const uuid1 = generateUUIDv4(); 42 | 43 | console.log("videoId: " + videoId) 44 | 45 | console.log("question: " + question) 46 | let subs 47 | let questionParts 48 | let multipart = false 49 | 50 | // if the question is more than 15k characters, then we need to split it into multiple requests 51 | 52 | 53 | questionParts = question.match(/.{1,15000}/g); 54 | console.log(questionParts) 55 | for (let i = parts.length; i < questionParts.length; i++) { 56 | if (i == 0) { 57 | console.log("first part") 58 | if (question.length > 15000) { 59 | subs = `This is the first part of the transcript. Keep it to a super short summary, no more than 3 sentences. Dont mention that you're reading a transcript, and exlude any parts about a sponsership: ` + questionParts[i] 60 | } else { 61 | subs = "this is a youtube video's transcript. Short summary (dont mention that you're reading a transcript): " + questionParts[i] 62 | } 63 | break 64 | } else { 65 | subs = `Previous summary: ${parts}. This is the ${i + 1} part of the transcript. Rewritten, continued summary (dont mention that you're reading a transcript): ` + questionParts[i] 66 | multipart = true 67 | break 68 | } 69 | } 70 | 71 | 72 | console.log("subs ", subs) 73 | 74 | let body 75 | if (!multipart) { 76 | body = { "action": "next", "messages": [{ "id": "73954541-ff9e-4785-85e4-d45527ccca73", "author": { "role": "user" }, "content": { "content_type": "text", "parts": [subs] } }], "parent_message_id": uuid1, "model": "text-davinci-002-render-sha", "timezone_offset_min": 240 } 77 | } else { 78 | let response = await fetch("https://chat.openai.com/backend-api/conversations?offset=0&limit=20", { 79 | "headers": { 80 | "accept": "text/event-stream", 81 | "accept-language": "en-US,en;q=0.9", 82 | "authorization": `Bearer ${token}`, 83 | "content-type": "application/json", 84 | "sec-ch-ua": "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\"", 85 | "sec-ch-ua-mobile": "?0", 86 | "sec-ch-ua-platform": "\"macOS\"", 87 | "sec-fetch-dest": "empty", 88 | "sec-fetch-mode": "cors", 89 | "sec-fetch-site": "same-origin", 90 | "x-openai-assistant-app-id": "", 91 | "origin": "https://chat.openai.com" 92 | }, 93 | }) 94 | 95 | let body2 = await response.json() 96 | 97 | console.log(response, body2) 98 | 99 | let id = body2.items[0].id 100 | 101 | console.log(id) 102 | body = { "action": "next", "conversation_id": id, "messages": [{ "id": "73954541-ff9e-4785-85e4-d45527ccca73", "author": { "role": "user" }, "content": { "content_type": "text", "parts": [subs] } }], "parent_message_id": uuid1, "model": "text-davinci-002-render-sha", "timezone_offset_min": 240 } 103 | } 104 | body = JSON.stringify(body); 105 | const response = await fetch("https://chat.openai.com/backend-api/conversation", { 106 | "headers": { 107 | "accept": "text/event-stream", 108 | "accept-language": "en-US,en;q=0.9", 109 | "authorization": `Bearer ${token}`, 110 | "content-type": "application/json", 111 | "sec-ch-ua": "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\"", 112 | "sec-ch-ua-mobile": "?0", 113 | "sec-ch-ua-platform": "\"macOS\"", 114 | "sec-fetch-dest": "empty", 115 | "sec-fetch-mode": "cors", 116 | "sec-fetch-site": "same-origin", 117 | "x-openai-assistant-app-id": "", 118 | "origin": "https://chat.openai.com" 119 | }, 120 | "referrer": "https://chat.openai.com/chat", 121 | "referrerPolicy": "strict-origin-when-cross-origin", 122 | "body": body, 123 | "method": "POST", 124 | "mode": "cors", 125 | "credentials": "include" 126 | }); 127 | 128 | // Access the body property of the response, which is a ReadableStream 129 | const reader = response.body.getReader(); 130 | 131 | // Recursive function to read and process chunks of data from the stream 132 | const readChunk = async () => { 133 | try { 134 | // Read a chunk of data from the stream 135 | const { value, done } = await reader.read(); 136 | 137 | // If the stream is done, exit the recursion 138 | if (done) { 139 | console.log('Stream complete.'); 140 | await deleteConvo(token) 141 | return; 142 | } 143 | 144 | // Process the received data chunk (e.g., convert to string and log to console) 145 | const chunkText = new TextDecoder().decode(value); 146 | 147 | let chunks = chunkText.split("\n\n") 148 | 149 | chunks = chunks.map((chunk) => { 150 | return chunk.replaceAll('data: ', '') 151 | }) 152 | 153 | console.log(chunks) 154 | 155 | if (chunks[0].includes("[DONE]")) { 156 | 157 | if (parts.length != questionParts.length) { 158 | // add the last final text to the parts array 159 | parts.push(chunks[1]) 160 | console.log("parts: ", parts) 161 | return handleStreamedResponse(token, question, videoId, parts) 162 | } 163 | 164 | console.log("done") 165 | await deleteConvo(token) 166 | return; 167 | } 168 | 169 | // console.log(chunkText.replace('data: ', '')) 170 | let chunkJson 171 | 172 | for (let i = 0; i < chunks.length; i++) { 173 | if (chunks[i]) { 174 | try { 175 | chunkJson = JSON.parse(chunks[i]) 176 | break; 177 | } catch (e) { 178 | console.log(e) 179 | } 180 | } 181 | } 182 | 183 | if (!chunkJson) { 184 | return readChunk(); 185 | } 186 | 187 | let finalText = chunkJson.message.content.parts[0] 188 | console.log(finalText) 189 | 190 | 191 | // Check if the tab is still open before sending the message 192 | chrome.tabs.get(targetTabId, (tab) => { 193 | if (chrome.runtime.lastError) { 194 | console.log('Tab is closed. Stopping streaming.'); 195 | } else { 196 | // Send the message to the content script in the specified tab 197 | chrome.tabs.sendMessage(targetTabId, { action: 'streamed_text_' + videoId, text: finalText }); 198 | } 199 | }); 200 | // Continue reading the next chunk 201 | return readChunk(); 202 | } catch (error) { 203 | // Handle any errors that may occur while reading the stream 204 | console.error('Error while reading the stream:', error); 205 | } 206 | }; 207 | 208 | // Start reading the stream 209 | readChunk(); 210 | 211 | // deleteConvo(token) 212 | 213 | 214 | } 215 | 216 | async function deleteConvo(token) { 217 | // https://chat.openai.com/backend-api/conversations?offset=0&limit=20 218 | let response = await fetch("https://chat.openai.com/backend-api/conversations?offset=0&limit=20", { 219 | "headers": { 220 | "accept": "text/event-stream", 221 | "accept-language": "en-US,en;q=0.9", 222 | "authorization": `Bearer ${token}`, 223 | "content-type": "application/json", 224 | "sec-ch-ua": "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\"", 225 | "sec-ch-ua-mobile": "?0", 226 | "sec-ch-ua-platform": "\"macOS\"", 227 | "sec-fetch-dest": "empty", 228 | "sec-fetch-mode": "cors", 229 | "sec-fetch-site": "same-origin", 230 | "x-openai-assistant-app-id": "", 231 | "origin": "https://chat.openai.com" 232 | }, 233 | }) 234 | 235 | let body = await response.json() 236 | 237 | console.log(response, body) 238 | 239 | let id = body.items[0].id 240 | 241 | console.log(id) 242 | 243 | let deleteResponse = await fetch(`https://chat.openai.com/backend-api/conversation/${id}`, { 244 | "headers": { 245 | "accept": "text/event-stream", 246 | "accept-language": "en-US,en;q=0.9", 247 | "authorization": `Bearer ${token}`, 248 | "content-type": "application/json", 249 | "sec-ch-ua": "\"Not_A Brand\";v=\"99\", \"Google Chrome\";v=\"109\", \"Chromium\";v=\"109\"", 250 | "sec-ch-ua-mobile": "?0", 251 | "sec-ch-ua-platform": "\"macOS\"", 252 | "sec-fetch-dest": "empty", 253 | "sec-fetch-mode": "cors", 254 | "sec-fetch-site": "same-origin", 255 | "x-openai-assistant-app-id": "", 256 | "origin": "https://chat.openai.com" 257 | }, 258 | "method": "PATCH", 259 | "body": `{"is_visible":false}` 260 | }) 261 | 262 | let deleteBody = await deleteResponse.json() 263 | 264 | console.log(deleteResponse, deleteBody) 265 | } 266 | 267 | // Listen for messages from content script 268 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 269 | if (request.message === 'getAccessToken') { 270 | getToken().then(accessToken => { 271 | targetTabId = sender.tab.id 272 | handleStreamedResponse(accessToken, request.subs, request.videoId, []) 273 | sendResponse({ accessToken: accessToken }); 274 | }); 275 | // Indicate that the response will be sent asynchronously 276 | return true; 277 | } 278 | }); 279 | 280 | 281 | // detect when the tab url changes, and it's a youtube video 282 | chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) { 283 | if (changeInfo.url && changeInfo.url.includes('youtube.com/watch')) { 284 | // send message to content script to start the stream 285 | console.log("New Video") 286 | chrome.tabs.sendMessage(tabId, { action: 'start_stream' }); 287 | } 288 | // otherwise detect if the page is refreshed 289 | else if (changeInfo.status === 'complete') { 290 | // send message to content script to start the stream 291 | console.log("Page Refreshed") 292 | chrome.tabs.sendMessage(tabId, { action: 'start_stream' }); 293 | } 294 | }); -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "TestGPT", 4 | "version": "1.0.0", 5 | "description": "A chrome extension that summarizes youtube videos with chatgpt", 6 | "host_permissions": [ 7 | "https://chat.openai.com/chat/*", 8 | "https://chat.openai.com/chat" 9 | ], 10 | "action": { 11 | "default_title": "Summize" 12 | }, 13 | "content_scripts": [ 14 | { 15 | "matches": [ 16 | "https://chat.openai.com/chat/*", 17 | "https://chat.openai.com/chat", 18 | "https://chat.openai.com/chat?run", 19 | "https://www.youtube.com/*", 20 | "https://example.com/" 21 | ], 22 | "js": [ 23 | "scripts/content-script.js" 24 | ], 25 | "all_frames": true 26 | } 27 | ], 28 | "background": { 29 | "service_worker": "background.js", 30 | "type": "module" 31 | }, 32 | "permissions": [ 33 | "tabs", 34 | "storage", 35 | "activeTab", 36 | "webRequest", 37 | "cookies", 38 | "webNavigation" 39 | ], 40 | "commands": { 41 | "Ctrl+M": { 42 | "suggested_key": { 43 | "default": "Ctrl+M", 44 | "mac": "Command+M" 45 | }, 46 | "description": "Ctrl+M." 47 | } 48 | }, 49 | "web_accessible_resources": [ 50 | { 51 | "resources": [ 52 | "scripts/getYt.js", 53 | "scripts/getSession.js", 54 | "popup/frame.html", 55 | "scripts/eventsource-polyfill.js" 56 | ], 57 | "matches": [ 58 | "" 59 | ] 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /scripts/content-script.js: -------------------------------------------------------------------------------- 1 | let accessToken 2 | let videoId 3 | 4 | function convertYoutubeApiUrlToNormalUrl(apiUrl) { 5 | const regex = /\\u0026/g; 6 | apiUrl = apiUrl.replace(regex, '&'); 7 | return decodeURIComponent(apiUrl); 8 | } 9 | 10 | window.addEventListener('message', (event) => { 11 | if (event.source != window) { 12 | return; 13 | } 14 | 15 | if (event.data.type && (event.data.type == 'FROM_INJECTED_SCRIPT')) { 16 | console.log('Content script received: ' + event.data.payload); 17 | 18 | console.log("videoId: " + videoId) 19 | 20 | chrome.runtime.sendMessage({ message: 'getAccessToken', subs: event.data.payload, videoId: videoId }, (response) => { 21 | accessToken = response; 22 | console.log(accessToken); 23 | }); 24 | 25 | } 26 | }); 27 | 28 | chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 29 | if (request.action === 'streamed_text_' + videoId) { 30 | const streamedText = request.text; 31 | // Process the streamed text received from the background script 32 | console.log(streamedText) 33 | 34 | let h2 = document.querySelector("#sum") 35 | 36 | h2.innerText = streamedText 37 | } else if (request.action === 'start_stream') { 38 | // Start the stream 39 | console.log("start stream") 40 | 41 | const h2 = document.createElement("h2"); 42 | // get id of the video 43 | videoId = window.location.href.split("v=")[1]; 44 | 45 | h2.id = "sum" 46 | 47 | // delete any old h2s 48 | const oldH2 = document.querySelector("#sum") 49 | 50 | if (oldH2) { 51 | oldH2.remove() 52 | } 53 | 54 | 55 | const interval = setInterval(() => { 56 | if (document.querySelector("#title > h1")) { 57 | document.querySelector("#title > h1").parentNode.appendChild(h2); 58 | clearInterval(interval); 59 | } 60 | }, 1000); 61 | 62 | let response = fetch("https://www.youtube.com/watch?v=" + window.location.href.split("v=")[1]).then(res => res.text()).then(text => { 63 | console.log(text) 64 | 65 | // get the first instance of "https://www.youtube.com/api/timedtext?v=", and get the full url, then console log it 66 | const regex = /https:\/\/www.youtube.com\/api\/timedtext\?v=.*?(?=")/; 67 | const match = text.match(regex); 68 | 69 | if (match) { 70 | const apiUrl = match[0]; 71 | let subsUrl = convertYoutubeApiUrlToNormalUrl(apiUrl); 72 | 73 | 74 | if (subsUrl.slice(-6) != "lang=en") { 75 | // change last 2 characters to "en" 76 | subsUrl = subsUrl.slice(0, -2) + "en"; 77 | } 78 | 79 | console.log(subsUrl); 80 | 81 | 82 | 83 | subs = fetch(subsUrl) 84 | subs.then(response => { 85 | response.text().then(text => { 86 | console.log(text) 87 | let xml = new DOMParser().parseFromString(text, "text/xml"); 88 | let textNodes = [...xml.getElementsByTagName('text')]; 89 | let subsText = textNodes.map(x => x.textContent).join("\n").replaceAll(''', "'"); 90 | console.log(subsText); 91 | 92 | // replace \n with "\n" 93 | subsText = subsText.replaceAll("\n", "\\n") 94 | 95 | // replace any space thats more than 1 with a single space 96 | subsText = subsText.replaceAll(/\s+/g, ' '); 97 | 98 | chrome.runtime.sendMessage({ message: 'getAccessToken', subs: subsText, videoId: videoId }, (response) => { 99 | accessToken = response; 100 | console.log(accessToken); 101 | }); 102 | }); 103 | }) 104 | } 105 | }) 106 | 107 | 108 | 109 | 110 | } else if (request.action === 'test') { 111 | console.log("test") 112 | 113 | let response = fetch("https://www.youtube.com/watch?v=" + window.location.href.split("v=")[1]).then(res => res.text()).then(text => { 114 | console.log(text) 115 | 116 | // get the first instance of "https://www.youtube.com/api/timedtext?v=", and get the full url, then console log it 117 | const regex = /https:\/\/www.youtube.com\/api\/timedtext\?v=.*?(?=")/; 118 | const match = text.match(regex); 119 | 120 | if (match) { 121 | const apiUrl = match[0]; 122 | const normalUrl = convertYoutubeApiUrlToNormalUrl(apiUrl); 123 | console.log(normalUrl); 124 | } 125 | }) 126 | 127 | // log text 128 | 129 | } 130 | }); -------------------------------------------------------------------------------- /scripts/getYt.js: -------------------------------------------------------------------------------- 1 | console.log("getting subs"); 2 | console.log(ytInitialPlayerResponse); 3 | 4 | // define subsUrl if it doesnt exist 5 | if (typeof subsUrl === 'undefined') { 6 | var subsUrl = ""; 7 | } 8 | 9 | 10 | subsUrl = ytInitialPlayerResponse.captions.playerCaptionsTracklistRenderer.captionTracks[0].baseUrl; 11 | 12 | // if the end of the subsUrl is anything but "?lang=en", then change it 13 | if (subsUrl.slice(-6) != "lang=en") { 14 | // change last 2 characters to "en" 15 | subsUrl = subsUrl.slice(0, -2) + "en"; 16 | } 17 | 18 | console.log(subsUrl); 19 | 20 | subs = fetch(subsUrl) 21 | subs.then(response => { 22 | response.text().then(text => { 23 | console.log(text) 24 | let xml = new DOMParser().parseFromString(text,"text/xml"); 25 | let textNodes = [...xml.getElementsByTagName('text')]; 26 | let subsText = textNodes.map(x => x.textContent).join("\n").replaceAll(''',"'"); 27 | console.log(subsText); 28 | 29 | // replace \n with "\n" 30 | subsText = subsText.replaceAll("\n", "\\n") 31 | 32 | // replace any space thats more than 1 with a single space 33 | subsText = subsText.replaceAll(/\s+/g, ' '); 34 | 35 | window.postMessage({ type: 'FROM_INJECTED_SCRIPT', payload: subsText }, '*'); 36 | 37 | }); 38 | 39 | 40 | }) 41 | --------------------------------------------------------------------------------