Greasy Fork

Greasy Fork is available in English.

ChatGPT Voice On LiveKit Meet

跳转 ChatGPT 镜像站的语音功能到 LiveKit Meet 而不是镜像站的 LiveKit Meet。始皇的镜像站甚至支持选择高级语音模式和选择模型

当前为 2024-10-30 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         ChatGPT Voice On LiveKit Meet
// @namespace    github.com/hmjz100
// @version      1.0.1
// @description  跳转 ChatGPT 镜像站的语音功能到 LiveKit Meet 而不是镜像站的 LiveKit Meet。始皇的镜像站甚至支持选择高级语音模式和选择模型
// @license      MIT
// @author       hmjz100
// @match        https://new.oaifree.com/*
// @match        https://chat.rawchat.top/*
// @match        https://chat.sharedchat.cn/*
// @match        https://gpt.github.cn.com/*
// @match        https://free.xyhelper.cn/*
// @icon         data:image/webp;base64,UklGRnICAABXRUJQVlA4WAoAAAAQAAAAHwAAHwAAQUxQSI8AAAABd6A2kg06/T5EcqGIzkREILWfPrI5RTitte1pXprvniGwKHzbgMMgjMAkHAaB9HhUevJ9U+T/6T6i/wzctm0kde/27hVA1LJgHQAoiCX2Hvw3S62UnAc0rkOEtyHir7C+QPMbSg48lz0vKU1fl0ZUNyHRH8Pf4Tt0/Wb4Ow5/53QI3XzKKRVk8v/IxeB/BgBWUDggvAEAABALAJ0BKiAAIAA+aSqQRaQioZv6rABABoS0gAnAHJxnoP3rMm/kBkKFPL/yvok/1/878zXyp/g/cC/jv9A/x35fdjN7Hf7ANdIJ5yn1lcylRWD9msH+g4lCVG3DwlB27AAA/v/b7x/k//Og4rBakFx39Od09+fJ1x4C+DLOrn57IHc1Tex5U/5cB8l2f3fZBiD+5/7/IowL/9h145ee+E8Tn9Mx//ya2jMrsF0MWumdxn7zRJ7f7BEicBZVfKYEwPy/e9nUZPOkR9QyW1vCwpKLT4HeJVe7ayZ4dcQRfzMR0TeGbvw8U5I+jHciOCqPs43iwvLybOhARP2A/54Jx8B2XKJ3NgHNzVi3W2HkTpisnFaSV2NcPAhURRCHCp9wAwR12+PxPlPateeyuKpswYl1DEzNt/SkXzDPkSq/LyuOSsHuqlcmY8ch6wMlzG7JlrkvSIL3kuB6BS6sByZZXCNqJoa/4vqVhG8NdvpsjyX6hpz5DqxYMDrvqnl1//oedvi9KU2f/8I6YzFjiLYx5bH31D5eTMZBT8kqCpoNVnHwfQz5KqJRnU9pk4JVJRwRO0gt0GsehqDyZAAAAA==
// @require      https://unpkg.com/[email protected]/dist/jquery.min.js
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_openInTab
// @grant        unsafeWindow
// ==/UserScript==

(function () {
	'use strict';

	// 隐藏原来的按钮
	waitForKeyElements('div:not(#ChatGPTVoice-On-LiveKitMeet-Button, #immersive-translate-popup) svg.icon[width="25"][height="25"], div#voiceButton svg, #of-custom-floating-ball svg', function (element) {
		element.parent().hide();
	});

	waitForKeyElements("body main div.mb-7.text-center, div.btn-voice, body main div.flex-shrink-0 svg", function (element) {
		if (element.hasClass("voice")) return;

		let checkOpacity = function () {
			if (element.hasClass("mb-7")) {
				if (element.find("h1.result-streaming").css('opacity') == '0') {
					let clone = element.clone(true);
					clone.addClass("voice")
					element.replaceWith(clone);

					clone.css({
						'cursor': 'pointer',
						'user-select': 'none',
						'-webkit-user-select': 'none',
						'-ms-user-select': 'none',
						'-moz-user-select': 'none'
					});

					clone.on('click touchend', handleVoiceClick);
				} else {
					setTimeout(checkOpacity, 100);
				}
			} else {
				element.addClass("voice")

				element.css({
					'cursor': 'pointer',
					'user-select': 'none',
					'-webkit-user-select': 'none',
					'-ms-user-select': 'none',
					'-moz-user-select': 'none'
				});
				element.on('click touchend', handleVoiceClick);
			}
		};

		// 初次调用检查函数
		checkOpacity();
	});

	let html = $(`<div id="ChatGPTVoice-On-LiveKitMeet">
		<div id="ChatGPTVoice-On-LiveKitMeet-Button">
			<svg width="25" height="25" class="icon" fill="none" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg">
				<path d="M18 12C18 15.3137 15.3137 18 12 18M12 18C8.68629 18 6 15.3137 6 12M12 18V21M12 21H15M12 21H9M15 6V12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12V6C9 4.34315 10.3431 3 12 3C13.6569 3 15 4.34315 15 6Z" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
			</svg>
			<span>语音</span>
		</div>
		<style id="ChatGPTVoice-On-LiveKitMeet-Style">
			div#ChatGPTVoice-On-LiveKitMeet-Button {
				border-top-left-radius: 34px;
				border-bottom-left-radius: 34px;
				background: linear-gradient(140.91deg, #7367F0 12.61%, #574AB8 76.89%);
				height: 34px;
				width: 80px;
				margin: 1px;
				display: flex !important;
				align-items: center;
				position: fixed;
				right: -35px;
				top: calc(30% - 34px);
				cursor: pointer;
				padding-left: 7px;
				z-index: 114514;
				opacity: 0.75;
				transition: right 0.3s, opacity 0.3s !important;
			}
			div#ChatGPTVoice-On-LiveKitMeet-Button:hover, 
			div#ChatGPTVoice-On-LiveKitMeet-Button.is-dragging {
				right: -5px;
				opacity: 1;
			}
			div#ChatGPTVoice-On-LiveKitMeet-Button span {
				color:#ffffff;
				font-size:15px;
				margin-left:3px;
				white-space: nowrap;
			}
		</style>
	</div>`)

	let button = html.find('#ChatGPTVoice-On-LiveKitMeet-Button');
	let isDragging = false;
	let offsetY = 0;
	let dragStartTime;

	// 从 GM 获取按钮位置
	if (GM_getValue('buttonTop')) {
		button.css('top', GM_getValue('buttonTop') + 'px');
	}

	// 点击事件处理
	button.on('click touchend', handleVoiceClick);

	// 鼠标按下事件
	button.on('mousedown touchstart', function (e) {
		e.preventDefault();
		dragStartTime = Date.now(); // 记录拖动开始时间
		offsetY = e.clientY - button.offset().top;
	});

	// 鼠标移动事件
	$(document).on('mousemove touchmove', function (e) {
		if (offsetY !== undefined) {
			let newTop = e.clientY - offsetY;
			const buttonHeight = button.outerHeight();
			const windowHeight = $(window).height();

			// 限制按钮位置
			if (newTop < 0) newTop = 0;
			if (newTop + buttonHeight > windowHeight) newTop = windowHeight - buttonHeight;

			// 判断是否拖动
			if (isDragging || (Date.now() - dragStartTime > 100)) { // 如果已经拖动或拖动时间超过100ms
				isDragging = true;
				button.addClass('is-dragging');
				button.css('top', newTop + 'px');
				GM_setValue('buttonTop', newTop);
			}
		}
	});

	// 鼠标抬起事件
	$(document).on('mouseup touchend', function () {
		if (isDragging) {
			setTimeout(function () {
				isDragging = false;
				button.removeClass('is-dragging');
			}, 100)
		}
		offsetY = undefined; // 重置 offsetY
	});

	setInterval(function () {
		if (!$('#ChatGPTVoice-On-LiveKitMeet-Button').length || !$('#ChatGPTVoice-On-LiveKitMeet-Style').length) {
			$('#ChatGPTVoice-On-LiveKitMeet').remove()
			$('body').append(html);
		}
	}, 500)

	// 绑定点击事件到新创建的按钮
	async function handleVoiceClick(event) {
		if (!event?.currentTarget || isDragging) return;
		let element = $(event.currentTarget);
		if (element.attr('data-clicked') === 'true') return;
		element.attr('data-clicked', 'true');

		// 异步获取语音链接
		await goVoice(element).catch(function (error) {
			alert('获取语音对话(会议)链接错误: \n' + error.message);
			console.error(error);
			element.removeAttr('data-clicked');
		});
	};

	async function goVoice(element) {
		// 定义不同服务器的配置
		let servers = {
			"new.oaifree.com": {
				apiPath: "/api/voice/link",
				apiType: "POST",
				url: "wss://webrtc.oaifree.com",
				model: new URL(location.href).searchParams.get('model'),
				mode: [['标准语音', '高级语音'], ['std', 'adv']],
				getToken: data => new URL(data.url).searchParams.get('token'),
				getHash: data => new URL(data.url).hash
			},
			"chat.rawchat.top": {
				apiPath: "/backend-api/voice_token",
				apiType: "GET",
				url: data => data.url,
				getToken: data => data.token,
				getHash: data => data.e2ee_key
			},
			"chat.sharedchat.cn": {
				apiPath: "/backend-api/voice_token",
				apiType: "GET",
				url: data => data.url,
				getToken: data => data.token,
				getHash: data => data.e2ee_key
			},
			"gpt.github.cn.com": {
				apiPath: "/backend-api/voice_token",
				apiType: "GET",
				url: data => data.url,
				getToken: data => data.token,
				getHash: data => data.e2ee_key
			},
			"free.xyhelper.cn": {
				apiPath: "/backend-api/voice_token",
				apiType: "GET",
				url: data => data.url,
				getToken: data => data.token,
				getHash: data => data.e2ee_key
			}
		};

		// 获取当前服务器的域名
		let host = location.hostname;

		// 获取服务器配置
		let config = servers[host];
		if (!config) {
			throw new Error(`未知服务器: ${host}`);
		}

		let extra = {
			method: config.apiType,
			headers: { 'Content-Type': 'application/json' }
		}

		if (config.model !== undefined && config.mode !== undefined && config.apiType === 'POST') {
			let model = config.model;
			let mode = config.mode;

			let modeChoice;
			if (mode && mode.length) {
				let modeOptions = mode[0]
					.map((name, index) => `(${index + 1}) ${name}`)
					.join(" ");
				let userChoice = prompt(`请选择语音模式: (不输入则使用${mode[0][0]})\n${modeOptions}`);

				let choiceIndex = parseInt(userChoice) - 1;
				if (choiceIndex >= 0 && choiceIndex < mode[1].length) {
					modeChoice = mode[1][choiceIndex];
				} else if (userChoice === null) {
					return element.removeAttr('data-clicked');
				} else {
					modeChoice = mode[1][0];
				}
			}

			if (!model) {
				let userInput = prompt("请输入模型名称: (不输入则使用默认模型)");
				if (userInput === null) {
					return element.removeAttr('data-clicked');
				}
				model = userInput;
			}

			extra.body = JSON.stringify({ model, mode: modeChoice });
		}

		// 发送请求到语音API
		let response = await unsafeWindow.fetch(config.apiPath, extra);

		// 解析返回的JSON数据
		let data = await response.json();
		console.log('服务数据: \n', data);

		// 检查返回的模式,如果是高级模式,修改颜色
		if (data.mode === "advanced") {
			element.css('color', '#f00');
		}

		// 检查是否有url或者token,否则抛出错误
		if (!data.url) {
			throw new Error(data.detail || 'No Data provided by server');
		}

		// 获取url、token、hash
		let url = typeof config.url === 'function' ? config.url(data) : config.url;
		let token = config.getToken ? config.getToken(data) : null;
		let hash = config.getHash ? config.getHash(data) : null;

		// 打印日志方便调试
		console.log('会议数据: \n', { token, hash, url });

		// 检查是否有url或者token,否则抛出错误
		if (!url || !token || !hash) throw new Error(data.detail || '语音服务未返回数据');

		// 构建 meetUrl
		let meetUrl = new URL('https://meet.livekit.io/custom');
		if (url) meetUrl.searchParams.set('liveKitUrl', url);
		if (token) meetUrl.searchParams.set('token', token);
		if (hash) meetUrl.hash = hash;

		// 打开新页面
		GM_openInTab(meetUrl.href, { active: true, insert: true, setParent: true })
		element.removeAttr('data-clicked');
	}

	function waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector) {
		function findInShadowRoots(root, selector) {
			let elements = $(root).find(selector).toArray();
			$(root).find('*').each(function () {
				let shadowRoot = this.shadowRoot;
				if (shadowRoot) {
					elements = elements.concat(findInShadowRoots(shadowRoot, selector));
				}
			});
			return elements;
		}
		var targetElements;
		if (iframeSelector) {
			targetElements = $(iframeSelector).contents();
		} else {
			targetElements = $(document);
		}
		let allElements = findInShadowRoots(targetElements, selectorTxt);
		if (allElements.length > 0) {
			allElements.forEach(function (element) {
				var jThis = $(element);
				var uniqueIdentifier = 'alreadyFound';
				var alreadyFound = jThis.data(uniqueIdentifier) || false;
				if (!alreadyFound) {
					var cancelFound = actionFunction(jThis);
					if (cancelFound) {
						return false;
					} else {
						jThis.data(uniqueIdentifier, true);
					}
				}
			});
		}
		var controlObj = waitForKeyElements.controlObj || {};
		var controlKey = selectorTxt.replace(/[^\w]/g, "_");
		var timeControl = controlObj[controlKey];
		if (allElements.length > 0 && bWaitOnce && timeControl) {
			clearInterval(timeControl);
			delete controlObj[controlKey];
		} else {
			if (!timeControl) {
				timeControl = setInterval(function () {
					waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector);
				}, 1000);
				controlObj[controlKey] = timeControl;
			}
		}
		waitForKeyElements.controlObj = controlObj;
	}
})();