Advertisement
Guest User

Untitled

a guest
May 31st, 2025
218
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 10.17 KB | None | 0 0
  1. // ==UserScript==
  2. // @name ChatGPT Read-Aloud+ v3 - Standalone
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.6
  5. // @description Adds a play audio button and voice selector dropdown to ChatGPT assistant messages.
  6. // @match https://chatgpt.com/*
  7. // @grant none
  8. // @run-at document-start
  9. // ==/UserScript==
  10.  
  11. (function () {
  12. 'use strict';
  13.  
  14. // --- Token Capture Logic ---
  15. let bearerToken = null;
  16. let tokenCapturedThisSession = false;
  17.  
  18. let tokenPromiseResolve;
  19. const tokenPromise = new Promise((resolve) => {
  20. tokenPromiseResolve = resolve;
  21. });
  22.  
  23. const originalFetch = window.fetch;
  24. window.fetch = async function(resource, config = {}) {
  25. let authHeader = null;
  26. let source = null;
  27.  
  28.  
  29. try {
  30.  
  31. // Check resource.headers if resource is a Request object
  32. if (resource instanceof Request && resource.headers) {
  33. const headerValue = resource.headers.get("Authorization");
  34. if (headerValue && headerValue.startsWith("Bearer ")) {
  35. authHeader = headerValue;
  36. source = "resource.headers";
  37. }
  38. }
  39.  
  40. // Check config.headers as well
  41. const headers = config.headers;
  42. if (!authHeader && headers) {
  43. if (headers instanceof Headers) {
  44. const headerValue = headers.get("Authorization");
  45. if (headerValue && headerValue.startsWith("Bearer ")) {
  46. authHeader = headerValue;
  47. source = "config.headers (Headers)";
  48. }
  49. } else if (typeof headers === "object") {
  50. const headerValue = headers["Authorization"] || headers["authorization"];
  51. if (headerValue && headerValue.startsWith("Bearer ")) {
  52. authHeader = headerValue;
  53. source = "config.headers (object)";
  54. }
  55. }
  56. }
  57.  
  58. if (authHeader) {
  59. if (authHeader !== bearerToken) {
  60. bearerToken = authHeader;
  61. if (!tokenCapturedThisSession) {
  62. tokenCapturedThisSession = true;
  63. console.log(`[TokenCapture] Token captured from ${source}`);
  64. tokenPromiseResolve(bearerToken);
  65. }
  66. }
  67. } else {
  68. }
  69. } catch (err) {
  70. console.warn("[TokenCapture] Error during fetch inspection:", err);
  71. }
  72.  
  73. return originalFetch(resource, config);
  74. };
  75.  
  76. console.log("[TokenCapture] Waiting for token...");
  77.  
  78. tokenPromise.then((token) => {
  79. console.log("[TokenCapture] Token captured!");
  80. window.fetch = originalFetch;
  81. });
  82. // --- End Token Capture Logic ---
  83.  
  84. const VOICE_STORAGE_KEY = 'tm-selected-voice';
  85. let availableVoices = [];
  86. let selectedVoice = null;
  87. const dropdowns = new Set();
  88.  
  89. const PLAY_ICON_PATH = "M8 5v14l11-7z";
  90. const DROPDOWN_ICON_PATH = "M7 10l5 5 5-5z";
  91. const SPINNER_SVG = `<svg width="24" height="24" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
  92. <circle cx="50" cy="50" r="32" stroke-width="8" stroke="#999" stroke-dasharray="50.2655 50.2655" fill="none" stroke-linecap="round">
  93. <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"></animateTransform>
  94. </circle>
  95. </svg>`;
  96.  
  97.  
  98. async function fetchVoices() {
  99. try {
  100. const res = await fetch("https://chatgpt.com/backend-api/settings/voices", {
  101. method: "GET",
  102. credentials: "include",
  103. headers: {
  104. "authorization": bearerToken,
  105. },
  106. });
  107.  
  108. const json = await res.json();
  109. availableVoices = json.voices;
  110. selectedVoice = localStorage.getItem(VOICE_STORAGE_KEY) || json.selected;
  111. } catch (e) {
  112. console.error("[Voice Fetch Error]", e);
  113. }
  114. }
  115.  
  116. function createAudioButton(messageId) {
  117. const button = document.createElement("button");
  118. const playIcon = document.createElement("div");
  119. playIcon.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="${PLAY_ICON_PATH}" fill="currentColor"/></svg>`;
  120. button.appendChild(playIcon);
  121. button.className = "tm-play-button text-token-text-secondary hover:bg-token-bg-secondary rounded-lg";
  122. button.title = "Play Audio";
  123. button.style.marginLeft = "4px";
  124.  
  125. button.onclick = async () => {
  126. const originalHTML = playIcon.innerHTML;
  127. playIcon.innerHTML = SPINNER_SVG;
  128. try {
  129. const conversationId = window.location.pathname.split('/').pop();
  130. const voice = localStorage.getItem(VOICE_STORAGE_KEY) || selectedVoice || 'glimmer';
  131. const url = `https://chatgpt.com/backend-api/synthesize?message_id=${messageId}&conversation_id=${conversationId}&voice=${voice}&format=aac`;
  132.  
  133. const response = await fetch(url, {
  134. method: "GET",
  135. headers: {
  136. "accept": "*/*",
  137. "authorization": bearerToken,
  138. },
  139. credentials: "include",
  140. });
  141.  
  142. const blob = await response.blob();
  143. const audioURL = URL.createObjectURL(blob);
  144. const audio = new Audio(audioURL);
  145. audio.play();
  146.  
  147. playIcon.innerHTML = originalHTML;
  148. } catch (err) {
  149. console.error("Audio fetch/play failed:", err);
  150. playIcon.innerHTML = originalHTML;
  151. }
  152. };
  153.  
  154. return button;
  155. }
  156.  
  157. function updateAllDropdowns() {
  158. dropdowns.forEach(dropdown => {
  159. Array.from(dropdown.children).forEach(option => {
  160. const isSelected = option.dataset.voice === selectedVoice;
  161. option.style.fontWeight = isSelected ? "bold" : "normal";
  162. option.style.background = isSelected ? "#2a2a2a" : "transparent";
  163. });
  164. });
  165. }
  166.  
  167. function createVoiceDropdown(triggerButton) {
  168. const dropdown = document.createElement("div");
  169. dropdown.style.position = "fixed";
  170. dropdown.style.background = "var(--token-bg-primary, #1e1e1e)";
  171. dropdown.style.border = "1px solid #333";
  172. dropdown.style.color = "#fff";
  173. dropdown.style.zIndex = "9999";
  174. dropdown.style.display = "none";
  175. dropdown.style.minWidth = "120px";
  176. dropdown.style.boxShadow = "0px 4px 10px rgba(0,0,0,0.5)";
  177. dropdown.style.borderRadius = "6px";
  178. dropdown.style.padding = "4px 0";
  179.  
  180. availableVoices.forEach(voice => {
  181. const option = document.createElement("div");
  182. option.textContent = voice.name;
  183. option.dataset.voice = voice.voice;
  184. option.style.padding = "6px 12px";
  185. option.style.cursor = "pointer";
  186. option.style.fontSize = "14px";
  187. option.style.color = "#eee";
  188. option.style.transition = "background 0.2s";
  189. if (voice.voice === selectedVoice) {
  190. option.style.fontWeight = "bold";
  191. option.style.background = "#2a2a2a";
  192. }
  193. option.onmouseenter = () => option.style.background = "#333";
  194. option.onmouseleave = () => option.style.background = voice.voice === selectedVoice ? "#2a2a2a" : "transparent";
  195. option.onclick = () => {
  196. localStorage.setItem(VOICE_STORAGE_KEY, voice.voice);
  197. selectedVoice = voice.voice;
  198. dropdown.style.display = "none";
  199. updateAllDropdowns();
  200. console.log("Voice changed to:", voice.voice);
  201. };
  202. dropdown.appendChild(option);
  203. });
  204.  
  205. triggerButton.onclick = (e) => {
  206. e.stopPropagation();
  207. const rect = triggerButton.getBoundingClientRect();
  208. dropdown.style.left = `${rect.left}px`;
  209. dropdown.style.top = `${rect.bottom + 4}px`;
  210. dropdown.style.display = dropdown.style.display === "none" ? "block" : "none";
  211. };
  212.  
  213. document.body.appendChild(dropdown);
  214. dropdowns.add(dropdown);
  215. document.body.addEventListener("click", () => dropdown.style.display = "none");
  216. }
  217.  
  218. function addButtonsToMessages() {
  219. document.querySelectorAll("[data-message-id]").forEach(messageEl => {
  220. const messageId = messageEl.getAttribute("data-message-id");
  221. const role = messageEl.getAttribute("data-message-author-role");
  222. if (role !== "assistant") return;
  223.  
  224. const container = messageEl.parentElement?.nextElementSibling?.querySelector("div.flex.items-center");
  225. if (container && !container.querySelector(".tm-play-button")) {
  226. container.appendChild(createAudioButton(messageId));
  227.  
  228. const voiceBtn = document.createElement("button");
  229. voiceBtn.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="${DROPDOWN_ICON_PATH}" fill="currentColor"/></svg>`;
  230. voiceBtn.className = "tm-voice-button text-token-text-secondary hover:bg-token-bg-secondary rounded-lg";
  231. voiceBtn.title = "Select Voice";
  232. voiceBtn.style.marginLeft = "4px";
  233.  
  234. container.appendChild(voiceBtn);
  235. createVoiceDropdown(voiceBtn);
  236. }
  237. });
  238. }
  239.  
  240. async function init() {
  241. await fetchVoices();
  242. addButtonsToMessages();
  243. const observer = new MutationObserver(() => addButtonsToMessages());
  244. observer.observe(document.body, { childList: true, subtree: true });
  245. }
  246.  
  247. // Wait for token before running main logic
  248. tokenPromise.then(() => {
  249. init();
  250. });
  251. })();
  252.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement