Gemini JSON exporter

Export messages from Gemini share links into tavern/ooba format

  1. // ==UserScript==
  2. // @name Gemini JSON exporter
  3. // @namespace lugia19.com
  4. // @version 0.4
  5. // @description Export messages from Gemini share links into tavern/ooba format
  6. // @author lugia19
  7. // @match https://gemini.google.com/share/*
  8. // @grant none
  9. // @license MIT
  10. // ==/UserScript==
  11.  
  12. //Data conversion stuff
  13. function extractConversationGemini() {
  14. const messages = [];
  15. // Collect all user and assistant messages
  16. const userQueries = document.querySelectorAll('user-query .query-text');
  17. const assistantResponses = document.querySelectorAll('response-container .message-content');
  18.  
  19. for (let i = 0; i < userQueries.length || i < assistantResponses.length; i++) {
  20. if (userQueries[i]) {
  21. messages.push({
  22. parent: "",
  23. message: {
  24. author: {
  25. role: "user"
  26. },
  27. create_time: getCurrentUnixTimestamp(),
  28. content: {
  29. parts: [userQueries[i].textContent.trim()]
  30. }
  31. }
  32. });
  33. }
  34. if (assistantResponses[i]) {
  35. messages.push({
  36. parent: "",
  37. message: {
  38. author: {
  39. role: "assistant"
  40. },
  41. create_time: getCurrentUnixTimestamp(),
  42. content: {
  43. parts: [assistantResponses[i].textContent.trim()]
  44. }
  45. }
  46. });
  47. }
  48. }
  49.  
  50. return messages;
  51. }
  52.  
  53. function convertMessageToTavern(messageData) {
  54. if (!messageData.message) {
  55. return null;
  56. }
  57.  
  58. const senderRole = messageData.message.author.role;
  59. if (senderRole === 'system') {
  60. return null;
  61. }
  62.  
  63. const isAssistant = senderRole === 'assistant';
  64. const createTime = messageData.message.create_time;
  65. const text = messageData.message.content.parts[0];
  66.  
  67. return {
  68. name: isAssistant ? 'Assistant' : 'You',
  69. is_user: !isAssistant,
  70. is_name: isAssistant,
  71. send_date: createTime,
  72. mes: text,
  73. swipes: [text],
  74. swipe_id: 0,
  75. };
  76. }
  77.  
  78. function jsonlStringify(messageArray) {
  79. return messageArray.map(msg => JSON.stringify(msg)).join('\n');
  80. }
  81.  
  82. function getTavernString() {
  83. const conversation = extractConversationGemini();
  84.  
  85. const convertedConvo = [{
  86. user_name: 'You',
  87. character_name: 'Assistant',
  88. }];
  89.  
  90. conversation.forEach((message) => {
  91. const convertedMsg = convertMessageToTavern(message);
  92. if (convertedMsg !== null) {
  93. convertedConvo.push(convertedMsg);
  94. }
  95. });
  96.  
  97. return jsonlStringify(convertedConvo);
  98. }
  99.  
  100. function getOobaString() {
  101. const messages = extractConversationGemini();
  102. const pairs = [];
  103. let idx = 0;
  104.  
  105. while (idx < messages.length - 1) {
  106. const message = messages[idx];
  107. const nextMessage = messages[idx + 1];
  108. let role, text, nextRole, nextText;
  109.  
  110. if (!message.message || !nextMessage.message) {
  111. idx += 1;
  112. continue;
  113. }
  114.  
  115. try {
  116. role = message.message.author.role;
  117. text = message.message.content.parts[0];
  118. nextRole = nextMessage.message.author.role;
  119. nextText = nextMessage.message.content.parts[0];
  120. } catch (error) {
  121. idx += 1;
  122. continue;
  123. }
  124.  
  125. if (role === 'system') {
  126. if (text !== '') {
  127. pairs.push(['<|BEGIN-VISIBLE-CHAT|>', text]);
  128. }
  129. idx += 1;
  130. continue;
  131. }
  132.  
  133. if (role === 'user') {
  134. if (nextRole === 'assistant') {
  135. pairs.push([text, nextText]);
  136. idx += 2;
  137. continue;
  138. } else if (nextRole === 'user') {
  139. pairs.push([text, '']);
  140. idx += 1;
  141. continue;
  142. }
  143. }
  144.  
  145. if (role === 'assistant') {
  146. pairs.push(['', text]);
  147. idx += 1;
  148. }
  149. }
  150. const oobaData = {
  151. internal: pairs,
  152. visible: JSON.parse(JSON.stringify(pairs)),
  153. };
  154.  
  155. if (oobaData.visible[0] && oobaData.visible[0][0] === '<|BEGIN-VISIBLE-CHAT|>') {
  156. oobaData.visible[0][0] = '';
  157. }
  158.  
  159. return JSON.stringify(oobaData, null, 2);
  160. }
  161.  
  162.  
  163. // Function to get current Unix timestamp in milliseconds
  164. function getCurrentUnixTimestamp() {
  165. return Math.floor(Date.now());
  166. }
  167.  
  168.  
  169. // Function to download data as JSON (Shamelessly copied from ChatGPT-Exporter)
  170. function downloadFile(filename, type, content) {
  171. const blob = content instanceof Blob ? content : new Blob([content], { type })
  172. const url = URL.createObjectURL(blob)
  173. const a = document.createElement('a')
  174. a.href = url
  175. a.download = filename
  176. document.body.appendChild(a)
  177. a.click()
  178. document.body.removeChild(a)
  179. }
  180.  
  181. // Create and style button
  182. const menuButton = document.createElement('button');
  183. menuButton.textContent = 'Export Chatlog';
  184. Object.assign(menuButton.style, {
  185. position: 'fixed',
  186. bottom: '20px',
  187. right: '20px',
  188. zIndex: 1000,
  189. backgroundColor: '#131314',
  190. color: 'white',
  191. border: '1px solid white',
  192. padding: '10px 20px',
  193. cursor: 'pointer',
  194. borderRadius: '5px',
  195. boxSizing: 'border-box', // Include padding and border in the width
  196. textAlign: 'center', // Center the text
  197. });
  198.  
  199. // Create menu container
  200. const menuContainer = document.createElement('div');
  201. Object.assign(menuContainer.style, {
  202. position: 'fixed',
  203. bottom: '70px', // Adjusted to not overlap the main button
  204. right: '20px',
  205. zIndex: 1001,
  206. display: 'none', // Initially hidden
  207. backgroundColor: '#131314',
  208. borderRadius: '0px',
  209. boxShadow: '0 4px 8px rgba(0,0,0,0.1)',
  210. });
  211.  
  212. // Function to toggle menu display
  213. function toggleMenu() {
  214. menuContainer.style.display = menuContainer.style.display === 'none' ? 'block' : 'none';
  215. }
  216.  
  217. // Add click event listener to menu button
  218. menuButton.addEventListener('click', toggleMenu);
  219.  
  220. // Add buttons to the menu
  221. ['Tavern', 'Ooba'].forEach(type => {
  222. const button = document.createElement('button');
  223. button.textContent = `${type} format`;
  224. Object.assign(button.style, {
  225. backgroundColor: '#131314',
  226. color: 'white',
  227. border: '1px solid white',
  228. padding: '10px 20px',
  229. cursor: 'pointer',
  230. borderRadius: '5px',
  231. display: 'block', // Ensure each button is on its own line
  232. marginTop: '10px', // Space out the buttons
  233. width: '100%', // Ensure consistent width
  234. boxSizing: 'border-box', // Include padding and border in the width
  235. textAlign: 'center', // Center the text
  236. });
  237. button.onclick = function () {
  238. let title = document.title.replace(/^\W+/, '').trim().replace(/\s+/g, '_');
  239. if (type === 'Tavern') {
  240. const resultString = getTavernString();
  241. downloadFile(`${title}_tavern.jsonl`, 'application/json', resultString);
  242. } else if (type === 'Ooba') {
  243. const resultString = getOobaString();
  244. downloadFile(`${title}_ooba.json`, 'application/json', resultString);
  245. } else { // JSON
  246. const resultString = JSON.stringify(extractConversationGemini(), null, 2);
  247. downloadFile(`${title}.json`, 'application/json', resultString);
  248. }
  249. // Optionally close the menu after download
  250. toggleMenu();
  251. };
  252. menuContainer.appendChild(button);
  253. });
  254.  
  255. // Add menu and menu button to the page
  256. document.body.appendChild(menuButton);
  257. document.body.appendChild(menuContainer);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址