Advertisement
Guest User

Untitled

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