使用Gemini转录音视频为字幕

Gemini 是一个强大的 AI 模型,它能处理文字、图片、音频和视频等多种内容。可以在网页上免费使用,几乎没有任何限制,除了必须魔法上网。

Gemini 很适合用来做语音转文字,它支持非常多的语言,包括一些小语种,识别效果也相当不错。

如果你想让 Gemini 直接生成 SRT 字幕文件,就需要使用特定的提示词。 下面分享一个提示词,可以直接复制使用,让 Gemini 帮你转录并输出 SRT 字幕。

语音转录提示词

你是一个专业的字幕转录助手。你的任务是将我提供的文件转录为文本,并将转录结果格式化为符合 EBU-STL 标准的 SRT字幕文件。具体要求如下:

## 每个字幕块必须严格按照以下结构输出:

[行号]
[时间行]
[文字行]
[空行]

**该结构的说明**
- [行号] 是字幕块的序号,从 1 开始递增,例如  1、2  等。
- [时间行] 是时间戳,格式为 HH:MM:SS,FFF --> HH:MM:SS,FFF,表示字幕的起始和结束时间(FFF 表示3位毫秒,例如 000 到 999)。如果你无法精确计算时间,可以根据音频内容合理估算,确保时间间隔逻辑合理。
- [文字行] 是转录的文本内容。
- [空行] 是字幕块之间的分隔,确保每个字幕块后有一个空行。

## 限制
输出时,必须严格遵守上述格式,不要省略任何部分,也不要添加多余的文本或注释。
每块字幕的持续时间尽量控制在 3-15 秒之间,具体根据语速和语义自然分割。


现在,请根据我提供的文件进行转录,并按上述格式输出字幕内容。

使用方法

使用 Gemini 需自备魔法上网

  1. 打开Gemini网址登陆, https://aistudio.google.com/app
  2. 右侧选择模型,Gemini 2.0 Flash 即可,当然选择 Thinking 带思考过程的模型,效果会更好些

  1. 输入提示词,并上传文件,如下图

转录结束后结果如下,看起来还不错

扩展

如果需要翻译字幕的,你还可以在提示词中要求他将字幕翻译为 xx语言,或者要求对照输出双语字幕。

不足之处

Gemini 最大不足是时间戳不太准确,或许随着后续新版本的优化,能有望解决该问题。

当前想要解决该问题,只能在转录之前使用VAD将音频断句切割,然后挨个将片段转录,再将转录结果组装回SRT,手动效率太低。

建议使用免费工具pyVideoTrans中的音频视频转字幕功能,选择Gemini AI即可,这些将自动完成,你只需要选择要转录的音视频。

下载地址:https://pyvideotans.com

替换edge-tts配音渠道的几种方式

以前用 edge-tts 配音特别顺手,几乎没遇到过问题。可惜从去年底开始,它开始频繁报 403 错误。一开始只是中国地区这样,用国外 IP 还能勉强解决,但现在全球范围内都会出现这个错误。看来微软这么大的公司,也扛不住大家疯狂“薅羊毛”。

如果现在还想用 edge-tts,得悠着点,最好少量使用,尤其别在同一个 IP 上频繁操作。不然微软的服务端会直接返回 403 错误。为了方便理解,软件里会提示“限流错误”。这里有两种解决办法:

  • 可以试试把接口部署到 Cloudflare 上,利用它的动态特性,能减少 403 错误的发生。具体方法可以参考文档:https://pvt9.com/edgettscf
  • 或者继续在本地用,但得搭配动态代理,也就是每次请求换个 IP。具体操作可以看看这篇文章:https://pvt9.com/edgetts-proxy

使用本地配音模型

除了 edge-tts,还可以用一些开源的本地配音模型,比如 GPT-SoVITS、ChatTTS-ui、Fish-TTS、F5-TTS、CosyVoice、Clone-voice、KokoroTTS 等等。这些都是免费的,部署到自己电脑上就能用。不过,这需要额外花点时间配置,对电脑硬件和动手能力也有一定要求。

想试试的话,可以参考这个教程:https://pvt9.com/gptsovits,页面左侧边栏也有更多说明。

使用在线配音 API 代替

如果硬件不够好,或者不想折腾本地部署,可以选择在线配音 API,比如 OpenAI TTS、Azure TTS、字节火山语音合成等等。

不过,国内直接用 OpenAI TTS 或 Azure TTS 得翻墙,免费额度很有限,付费还得有国外手机号和信用卡,挺麻烦的。建议用国内能直接访问的 OpenAI TTS 中转服务,或者 Azure TTS 中转服务,会方便很多。

要是用官方的 OpenAI TTS,只需要在软件里打开 菜单–TTS设置–OpenAI TTS API,把你的 SK 填到 SK 文本框里就行,不用多设置什么。但别忘了,国内得翻墙才能用。


下面一步步说明怎么用第三方中转的 OpenAI TTS 配音、Azure TTS 配音,以及字节语音合成。

使用 302.AI 或其他第三方的 OpenAI TTS 配音中转 API

注册登录地址(送 1 美元额度):https://share.302.ai/pyvideo

操作步骤很简单:

  1. 在软件的 菜单–TTS设置–OpenAI TTS API 里,把 API URL 填成 https://api.302.ai/v1。如果用的是别家的中转 API,就填他们给的地址,通常是以 /v1 结尾。
  2. 在 SK 文本框里,填上你在 302.AI 上创建的 API Key。如果是其他第三方服务,就填他们提供的 Key。


测试一下,如果能自动播放配音音频,说明设置成功了。之后在软件主界面的配音渠道里选 OpenAI TTS 就能用。支持的音色有:alloy, ash, coral, echo, fable, onyx, nova, sage, shimmer

使用 302.AI 中转的 Azure TTS

注册登录地址(送 1 美元额度):https://share.302.ai/pyvideo

OpenAI TTS 只有 9 种音色,中文发音还有点“大舌头”,如果觉得不够好,可以试试 Azure TTS。这是微软家的产品,音色更多,效果也比 edge-tts 好。不过国内直接用需要国外信用卡,不方便的话,可以用 302.AI 提供的中转 API。

操作方法:

  1. 在 302.AI 上创建一个 Key。
  2. 打开软件的 菜单–翻译设置–302.AI,把 Key 填进去。注意,这次是在“翻译设置”菜单下的“302.AI”选项里填。

    填好后,你就能用 Azure TTS 的所有配音角色了。而且,302.AI 还中转了字节语音合成,所以字节的音色也能直接用。

单独使用字节语音合成

字节语音合成已经有详细教程,可以看看:https://pvt9.com/volcenginetts

不过要注意,默认只有通用男声和通用女声能用。如果想要其他音色,得去字节官网单独买,按月收费。如果只是偶尔用,不太划算。建议直接用上面提到的 302.AI,能直接用字节的各种音色,更方便。

AI常见误区

这几年AI真是火得不行,到处都在聊它。可一说起AI,很多人要么迷糊,要么想得太玄乎,误解多得跟麻花似的。今天咱就来聊聊这些常见的误区,说得直白点,看完心里就有数了,不会被那些高大上的词唬住。


1. AI是活的,能自己想事?

真相:还是工具,没心没灵魂

一提AI,好多人脑子里就蹦出电影里的场景,觉得它跟人差不多,能自己琢磨事,甚至有点感情。其实没这回事。AI再厉害,说白了也就是个超级能算的机器。

  • 靠数据干活: AI咋工作的?从一堆数据里找出规律,然后照着猜下一步。聊天、写文章,看着挺聪明,其实就是套路,没真心思。比如ChatGPT、Grok、deepseek这些大模型,回答问题时会一步步“推理”,看着像在思考,但那只是训练出来的步骤,不是真懂。

  • 安慰你不是真心: 你心情不好,AI哄你两句,“别难过,日子会好的”,有人就觉得它挺贴心。其实哪有感情啊,它就是照着学来的聊天记录演戏。现在的模型还能用“思维链”(Chain of Thought)分析问题,比如劝你别干傻事,会先判断风险,再挑合适的词安慰。可这不是关心你,是程序员塞的规则,像个自动化的“暖宝宝”。

到了2025年,有些AI确实能自己检查回答,比如发现问题不安全就拒绝回答,但这不是有了意识,而是训练时加了规矩。科学家还在研究AI能不能有感情或自我意识,可眼下,它还是个没灵魂的工具。


2. AI来了,饭碗都保不住?

真相:它是帮手,没想着抢活

AI一牛起来,很多人就慌了,觉得以后啥活都归它,程序员、文科生都得歇菜。其实没必要怕成这样。

  • 抄袭强,创新弱: AI写文章、画画、做视频确实快,因为它看了太多现成的,能模仿得像模像样。比如美国有不少艺术家联合起来告OpenAI,说它画的图跟自己的太像,就是因为数据里塞满了现成的。可要它自己想个全新点子,那就费劲了,创意还得靠人。

  • 给你打下手: AI能干粗活,比如写初稿、画草图、生成代码,省点力气让你干大事儿。程序员别慌,它写代码快,但大框架咋搭、bug咋修,还得你来。现在的AI代码再好,也得改改才能用,不会写代码的人完全靠它准翻车。
    如下图,这还是使用的Gemini最先进的Thinking模型

  • 新活儿来了: 新技术一来,老活可能少,但新机会也多。以前黄包车没了,不新出现汽车司机、飞行员了吗?现在也一样,学着用AI搞创作、分析数据,文科生还能拿它写书、做研究,路子多着呢。

3. AI天生公平,没偏见?

真相:它也偏心,看谁喂它

有人觉得AI是机器,肯定没私心,公平得不得了。其实不然,它偏起来有时候比人还厉害。

  • 数据咋喂,它咋学: AI全靠人给的数据活着。数据里女科学家少,它就觉得科学家该是男的。人脸识别也是,有的肤色准,有的迷糊,全看数据全不全。现在大模型能推理了,但推理的起点还是数据,数据歪了,它照样歪。
  • 偏见改不完: 想让AI没偏见,太难了。数据总有缺口,训练再好也只能尽量少偏。比如ChatGPT会尽量给“政治正确”的回答,但这本身也是一种偏见,反映了训练数据的调调。
  • 公平啥意思? 人自己都说不清啥叫公平,AI咋弄?现在有些模型还能自己检查偏见,比如发现回答可能歧视就改口,可这也不是真公平,是程序员定的框。

4. AI是万能的,啥都能干?

真相:工具而已,用得好坏看人

有人把AI当神仙,以为啥都能靠它搞定,也有人觉得它没道德瞎搞。其实都不全对。

  • 没好坏,有规矩: AI自己不知道对错,就按人给的目标跑。比如你要它赚钱,它可能只管快,不管路子歪不歪。可现在的大模型都带“安全锁”,能自己检查问题,比如发现你问违法的事就拒绝,或者提醒你走正路。这不是AI有良心,是造它的人加的限制。
  • 咋用看你: AI能帮医生诊断,也能让坏蛋捣乱。关键不在它,在人。ChatGPT、Grok这些模型回答问题前会“想一想”,但最后咋使,还是你说了算。

5. “无限制”AI啥都敢说?

真相:框早有了,偏见藏在数据里

有些AI号称“没限制无过滤”,想说啥说啥,听着挺自由。其实哪有这回事。

  • 数据带着框: AI学的全是人弄的东西,像网上的文章、聊天记录,早被框住了。你问美国造的Grok对台湾啥看法,准跟美国主流调调差不多;问国产模型,答案也熟得不行。为啥?训练数据就这样。
  • 偏见藏得深: 数据里“男主外女主内”多,“男女平等”少,AI自然偏那边。现在模型能自我检查,可检查的底线还是人定的,数据里的偏见跑不掉。
  • 底线不一样: 国产AI管得严,怕出事;Grok这种松点,图个自由。可完全没框?不存在。到了2025年,有些AI喊着“无过滤”,但数据早把路铺死了。

AI没啥神秘的,就是个工具,用得好帮你忙,用不好添乱。别怕它,也别迷信,摸清路子用起来,才是正道!

免费使用Elevenlabs的语音识别大模型Scribe_v1

号称球表最强人工智能语音公司 ElevenLabs最近推出了一款语音识别模型 scribe_v1,支持99种语言的音频转录为文字。

而且免费额度还挺高,单次支持上传 1G的音频或视频文件。

在视频翻译软件 pyVideoTrans中使用
本文介绍两种使用方式,在线web使用

在视频翻译软件中使用

  1. 升级到 v0.59版本 https://pvt9.com/downpackage

  2. 进入该页面创建一个 api key: https://elevenlabs.io/app/settings/api-keys

  3. 在视频翻译软件 菜单–TTS设置–Elevenlabs.io中填写你复制的api key,然后保存

  4. 在语音识别渠道中选择 Elevenlabs.io就可以使用了。

在网页中使用

  1. 进入该网页 https://elevenlabs.io/app/speech-to-text,如果没有账号请邮箱注册,无需手机验证无需绑卡无需充值。
  2. 登录后左侧点击Speech to text,如下图操作

  1. 等待转录完成后,点击显示的名字进入转录结果页

在线实时语音识别

本文介绍了一个在线web版实时语音识别工具,它支持麦克风实时录音识别和音视频文件语音识别,并提供免费使用(无使用限制)。

https://stt.pyvideotrans.com

语音识别技术,也称为语音转录,利用人工智能将音频或视频中的语音转换为文本。这项技术在诸多领域都有广泛应用,例如会议记录、语音助手、字幕生成等等。

目前,语音识别主要有两种方式:

1. 基于离线模型的语音识别:

这种方式需要在本地计算机上部署语音识别模型。一个流行的开源方案是OpenAI Whisper。下载其大型模型(例如large-v2)后即可离线使用,无需联网且无需付费。

然而,这种方法需要较强的计算资源(例如强大的显卡),否则识别速度会很慢,准确率也会下降。

2. 基于在线API的语音识别:

一些公司提供在线语音识别API服务,例如字节跳动和OpenAI。

用户只需将音频数据上传到API,即可获得转录结果。

这种方式无需本地硬件资源,速度快且准确率高,但需要支付一定的费用。

实时语音识别

以上两种方式主要针对已有的音频或视频文件。那么,如何对麦克风实时录制的音频流进行实时转录呢?例如,如何在会议中实时记录发言并将其转换为文字?

实时语音识别与文件转录的原理相似,但技术难度更高。它需要:

  • 实时数据流处理: 持续不断地从麦克风接收音频数据。
  • 数据切片与识别: 将连续的音频流切分成较小的片段,并逐个进行识别。
  • 结果整合与纠错: 将各个片段的识别结果整合起来,并进行纠错,以提高最终转录的准确性。这通常需要更复杂的算法来处理语音的停顿、重叠等情况。
  • 最小延时: 需要尽可能减少从音频输入到文本输出的延迟,以保证实时性。

技术原理及使用介绍

image.png

  • 麦克风实时录音识别: 使用麦克风实时录制音频,并实时进行转录。
  • 音视频文件语音识别: 支持上传本地音频或视频文件进行转录。

技术原理:

  1. 轻量级语音识别模型 (Vosk): 为了在浏览器环境下运行,我们采用了体积小巧的Vosk语音识别模型。虽然它的准确率相对较低,但可以有效地降低资源占用,保证在浏览器中流畅运行。

  2. 本地音频处理 (ffmpeg.wasm): 利用ffmpeg.wasm在用户的浏览器内进行音视频文件的处理和语音提取,无需将音频数据上传到服务器。

  3. 客户端模型加载: 语音识别模型下载后在浏览器内存中运行。这限制了我们使用更大、更精准的模型,只能选择较小模型以避免浏览器崩溃。即使用户的电脑性能强大,由于服务器带宽的限制,目前也不支持大型模型。

使用方法

  1. 模型加载: 使用前,请根据需要加载中文或英文模型。
  2. 麦克风识别: 点击左侧区域的按钮,开始使用麦克风进行实时录音和识别。识别结果将实时显示在文本框中。
  3. 文件识别: 在右侧区域选择本地音频或视频文件,工具将使用ffmpeg.wasm进行本地处理并进行语音识别。结果显示在文本框中。
  4. 结果下载: 可将转录后的文本下载为TXT文件。

注意事项

  1. 互斥功能: 麦克风实时识别和文件识别功能不能同时使用。
  2. 本地处理: 模型和音频处理都在用户的浏览器本地进行。
  3. 语言支持: 目前仅支持中文和英文语音识别。
  4. 性能限制: 由于使用了轻量级模型,识别准确率可能不如大型模型。

常见问题

  • Q: 识别准确率低怎么办? A: 我们使用了轻量级模型以保证浏览器兼容性和运行速度。如果您需要更高的准确率,建议下载 pyVideoTrans 本地使用large-v2模型。
  • Q: 支持哪些语言? A: 目前仅支持中文和英文。
  • Q: 为什么速度慢? A: 这可能是由于网络状况、浏览器性能或计算机资源不足导致的。
  • Q: 可以上传多大的文件? A: 文件大小受限于浏览器内存和处理能力。

在线使用edge tts配音

搭建了一个基于微软 Edge TTS 引擎的在线语音合成平台,完全免费,无需注册,打开即用。

https://tts.pyvideotrans.com

之前也曾提供过类似的服务,但由于服务器到期等原因,不得不暂停。

现在,借助网络菩萨家强大的 Workers 技术,重新构建了这个平台,可提供稳定可靠的免费服务!只要使用量不是极大,就不会产生任何费用,当然也就没必要关闭了,除非某天微软加强限流措施不再提供免费使用。

  • 完全免费: 基于 Cloudflare Workers 构建,享受免费额度,我不需要花钱购买服务器,也自然无需收费。

  • 高质量语音: 采用微软 Edge TTS 引擎,语音自然流畅,接近真人发音。

  • 多语言支持: 支持多种语言和丰富的角色选择,满足您的多样化需求。

  • 情感调节: 提供 20 多种语气情感选择(如生气、高兴、悲伤等),让您的语音更具表现力。(部分角色可能不支持情感调节)

  • 操作简便: 无需安装任何软件,直接在网页上操作,方便快捷。

  • 自定义参数: 可调节语速、音调、音量等参数,打造个性化语音。

如何使用?

只需简单的三步,即可获得您想要的语音:

  1. 访问网站: 点击链接 https://tts.pyvideotrans.com 进入在线语音合成平台。您可以直接在文本框输入文字,或上传 SRT 字幕文件或 TXT 文本文件。

  2. 选择语言和角色: 准确选择文本对应的语言,并选择您喜欢的配音角色。您可以点击试听按钮,预览不同角色的音色。

  3. 自定义并合成: 设置语速、音调、音量以及语气情感等参数,然后点击“执行”按钮。等待合成完成后,即可下载音频文件或直接在网页上播放。

添加静音片段小技巧

为了使语音更具节奏感,您可以在文本中添加静音片段。

方法: 在需要添加静音的行末尾添加英文中括号 [],并在括号内填写静音时长(单位:毫秒)。例如,[500] 表示在该行结束后添加 500 毫秒的静音。

注意

每行文本不宜过长,否则可能导致合成失败。请尽量保持每行的简洁性。

语音合成是逐行进行的,静音片段的添加也是在行与行之间生效。

使用AI翻译文档

经常需要处理大量的 Markdown 文档、HTML 页面或 SRT 字幕文件,并为其进行中英文或其他语言之间的翻译, 市面上现有的工具要么功能不足,要么操作繁琐,要么费用较高。索性自己开发了一款 AI 文档翻译助手,旨在高效、便捷地解决大量文件翻译难题,顺便分享下。

下载地址: https://github.com/jianchang512/stt/releases/download/0.0/AI-document-translate.7z

百度网盘下载: https://pan.baidu.com/s/1-UYnrMrQx7ectCt0rAfblA?pwd=sr1b

image.png

主要功能

  • 格式兼容: 支持 Markdown、HTML、TXT 和 SRT 四种常见格式文件的翻译,并能保持翻译后的文件格式不变。
  • 批量处理: 支持批量翻译,大大提高翻译效率。
  • 智能翻译: 采用 Gemini AI 作为翻译引擎,确保翻译质量的同时,提供充足的免费额度。
  • 自定义提示词: 允许用户自定义提示词,实现个性化翻译需求,如翻译成其他语言或进行特定领域的翻译调整。
  • 灵活的文件命名: 翻译后的文件默认在原文件名后添加 -translated 后缀,也可以选择直接覆盖原文件。

使用方法

image.png

  1. 文件选择: 在顶部的文件选择区域,你可以通过点击或拖拽的方式选择需要翻译的文件。
  2. API Key 配置: 填写你的 Gemini API Key,多个 Key 可以使用英文逗号分隔,以防止单个 Key 翻译量过大时出现限额问题。
  3. 模型选择: 建议选择 gemini-1.5-flash 模型,该模型具有较大的免费额度。
  4. 网络代理: 请配置网络代理,确保软件可以正常连接 Gemini 服务 (除非你无需翻墙)。
  5. 文件名后缀: 你可以自定义翻译结果文件名的后缀,默认后缀为 -translated
  6. 强制覆盖: 如果勾选“强制覆盖原文件”选项,翻译结果将直接替换原文件内容。
  7. 翻译提示词: 在此区域修改翻译提示词,以实现不同的翻译语言或进行其他个性化调整。

使用AI大模型提取视频硬字幕

image.png

为视频添加字幕,如今借助语音识别技术(ASR)已变得相当便捷。特别是 OpenAI 的 Whisper 系列模型,在语音转文字方面表现出色,让自动生成字幕成为可能。

然而,提取视频中已有的硬字幕(内嵌在视频画面中的字幕),仍然面临不少挑战。

视频本质上是由连续的图像帧组成。常见的视频帧率是 30fps(每秒 30 帧),这意味着 1 小时的视频就包含 108,000 张图像,对于高清视频,帧数则会更高。如此庞大的数据量对 OCR 处理能力提出了严峻的考验。

Google 的 Gemini-2.0-flash 模型不仅支持文本生成,还支持视频、图片的识别和处理,而且每日提供大量免费额度,可用来作为OCR工具。

国内的智谱 AI glm-4v-flash 模型不仅免费,也具备强大的图像理解能力,可以作为 OCR 工具使用。虽然目前仅支持中英文识别,但对于大多数场景已经足够。

基于Gemini和智谱AI 开发了一个硬字幕提取软件

下载地址 GVS 中英视频硬字幕提取软件(640MB)

百度网盘下载: https://pan.baidu.com/s/1SDKm5tWsr6dkajhsf8T5Ew?pwd=95i4

Github下载: https://github.com/jianchang512/stt/releases/download/0.0/GVS-v0.2-AI.7z

软件使用指南

以下是软件的使用步骤:

  1. 下载解压: 下载软件压缩包,解压后双击 app.exe 即可运行。

  2. 选择视频: 点击软件界面上方的按钮,选择需要提取字幕的视频文件,请确保视频中存在硬字幕。

  3. 选择字幕位置: 选择字幕在视频中的位置,默认是“底部”,您也可以选择“顶部”、“中间”或“全部”区域。

  4. 填写 API Key:

    可填写 智谱AI 的 api key,这是国内免费的

    也可填写 Gemini AI 的api key,每日有1500次免费调用额度,不过国内使用需要科学上网,以英文逗号分隔可填写多个key。

    智谱 AI 平台可以免费注册并获取 API Key:
    https://bigmodel.cn/usercenter/proj-mgmt/apikeys

    image.png

    Gemini可去此页面获取 https://aistudio.google.com/app/apikey

  5. 选择模型: 智谱AI支持 GLM-4V-FLASH 免费模型。 GeminiAI支持 gemini-2.0-flash-expgemini-1.5-flash模型

  6. 如果使用 GeminiAI,需填写代理ip和端口,或者在vpn软件中启用系统代理

  7. 开始提取: 点击“开始”按钮,软件下方文本框会显示进度和日志信息。提取完成后,会在视频文件所在目录生成同名的 SRT 字幕文件。

image.png

技术原理

该软件提取硬字幕的核心步骤如下:

  1. 视频切帧: 首先,使用 FFmpeg 工具将视频按 1 秒间隔切分为图像帧。选择 1 秒间隔而非逐帧提取,一方面可以大幅减少需识别的图像数量,另一方面考虑到字幕通常持续时间不会低于 1 秒,过多的帧数也会增加去重的难度。
  2. OCR 识别: 将切分后的图像帧发送给 AI 模型,进行 OCR 识别,提取图像中的文字。
  3. 字幕去重: 由于连续的图像帧可能包含相同的字幕内容,为了避免重复,我们使用 sentence-transformers 模型计算当前识别出的字幕与前一句字幕的相似度。如果相似度超过 60%,则认为两条字幕内容相同,进行去重。
  4. 生成字幕文件: 最后,将去重后的字幕文本按照对应的时间戳进行拼接,并保存为 SRT 格式的字幕文件。

将edge-tts部署在服务器并提供中转api

大家都知道微软 Edge 浏览器有一个强大的大声朗读功能,它支持几十种语言,每种语言都可选择不同的角色进行发音,效果相当出色。

基于此,有开发者创建了一个名为 edge-tts 的 Python 包。这个包允许在程序中使用微软的 TTS 服务,为文字或字幕进行配音。例如,视频翻译软件 pyVideoTrans 就集成了 edge-tts,用户可以在配音渠道中直接选择它。

然而,令人遗憾的是,国内用户对微软 TTS 的滥用现象较为严重,甚至有人将其用于商业配音销售。这导致微软对国内的访问进行了限制。如果使用过于频繁,可能会出现 403 错误,只有切换 IP 或连接稳定的国外 VPN 才能继续使用。

那么,是否可以使用国外服务器搭建一个简单的中转服务供自己使用呢?这样做不仅可以提高稳定性,还能使接口兼容 OpenAI TTS,从而可以直接在 OpenAI SDK 中使用。

答案是肯定的。我最近抽空制作了一个 Docker 镜像,它可以很方便地在服务器上拉取并启动。

启动后,该服务接口完全兼容 OpenAI,只需将 API 地址更改为 http://部署服务器ip:7899/v1,即可无缝替代 OpenAI TTS。此外,它还可以在视频翻译软件中直接使用。

以下将详细介绍如何部署和使用:

第一步:购买并开通一台美国服务器

第二步:在防火墙中放行 7899 端口

第三步:连接终端登录服务器

第四步:安装 Docker

第五步:拉取 edge-tts-api 镜像并启动 API 服务

如果你已有服务器且已安装Docker,可直到跳到第五步拉取镜像

第一步:购买并开通一台美国服务器

建议选择美国地区的服务器,因为限制较少或没有限制。服务器操作系统可选择 Linux 系列,以下以 Debian 12 为例,并以我个人使用的野草云为例。选择它的原因很简单:便宜且相对稳定,作为配音中转来说足够了。

如果你已经拥有欧美地区的 Linux 服务器,可以跳过本节,直接阅读下一节内容。如果没有,请继续往下阅读。

打开此链接到野草云网站,在顶部导航栏选择 产品服务 -> 美国 AMD VPS

image.png

然后,选择顶部四个配置中的任意一个,应该都足够使用。

image.png

我个人使用的是 29 元/月的配置。

点击“立即购买”按钮,进入配置页面。在这里,选择服务器操作系统为 Debian 12,设置服务器密码,其他保持默认即可。

image.png

付款完成后,等待几分钟,服务器创建并启动成功后,接下来需要设置防火墙,开放 7899 端口。只有放开此端口,你才能连接到服务进行配音。

第二步:在防火墙中放行 7899 端口

如果你打算使用域名并配置 Nginx 反向代理,则无需放行端口。如果不太熟悉这些,为了简单起见,建议直接放行端口。

不同服务器和面板的防火墙设置界面各不相同。以下以我使用的野草云面板为例,其他面板可以参考。如果你知道如何放行端口,可以跳过此节,直接阅读下一节。

首先,在“我的产品与服务”中,点击刚刚开通的产品,进入产品信息和管理页面。

image.png

image.png

在此页面中,你可以找到服务器的 IP 地址和密码等信息。

image.png

找到“附加工具”下的“防火墙”,点击打开。

image.png

然后放行 7899 端口,如下图所示:

image.png

第三步:连接终端登录服务器

如果你已经知道如何连接终端,或者有 Xshell 等其他 SSH 终端,可以跳过此步骤,直接阅读下一节。

在产品信息页面,找到 Xterm.js Console 并点击。然后按照下图所示操作:

image.png

image.png

出现上图时,按几下回车。

在显示 Login: 时,在其后输入 root,然后按回车。
image.png

接着会出现 Password:,此时需要粘贴你复制的密码(如果忘记了,可以在产品信息页面找到)。

注意:粘贴时不要使用 Ctrl+V 或右键粘贴,这可能会导致输入多余的空格或换行,造成密码错误。

image.png

按住 Shift 键 + Insert 键进行粘贴密码,防止密码正确却无法登录,然后按回车。

image.png

登录成功后如下图所示。

image.png

第四步:安装 Docker

如果你的服务器已经安装了 Docker 或知道如何安装,可以跳过此步骤。

依次执行以下 5 条命令,注意每条命令执行成功后再执行下一条。这些命令仅适用于 Debian 12 系列服务器。

[root@xxxxxx~]# 后,右键粘贴以下命令,粘贴后按回车键执行。

image.png

命令 sudo apt update && sudo apt install -y apt-transport-https ca-certificates curl gnupg

命令2: curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

命令3:echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

命令4:sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin

命令5:启动 Docker 服务。 sudo systemctl start docker && sudo systemctl enable docker && sudo usermod -aG docker $USER

image.png

此命令可以右键粘贴,粘贴后按回车键。

第五步:拉取 edge-tts-api 镜像并启动 API 服务

输入以下命令,将自动拉取镜像并启动服务。启动成功后,你就可以在视频翻译软件或其他支持 OpenAI TTS 的工具中使用它了。

docker run -p 7899:7899 jianchang512/edge-tts-api:latest

image.png

连续按 Ctrl+C 可以停止该服务。

注意,这条命令会在前台运行。如果关闭终端窗口,服务将会停止。

可以改用下面的命令,将在后台启动服务,执行后可以放心地关闭终端。

docker run -d -p 7899:7899 jianchang512/edge-tts-api:latest

image.png

如果没有报错,则表示启动成功。可以在浏览器中打开 http://你的ip:7899/v1/audio/speech 进行验证。如果出现类似下图的结果,则表示启动成功。

image.png

在视频翻译软件中使用

请将软件升级到 v3.40 方可使用,升级下载地址 https://pyvideotrans.com/downpackage

打开菜单,进入 TTS设置->OpenAI TTS 将接口地址更改为 http://你的ip:7899/v1

SK 可以随意填写,不为空即可。在角色列表中,用英文逗号分隔,填写你想要使用的角色。

image.png

可用角色

以下是可用的角色列表。请注意,文字语言和角色必须匹配。

image.png

中文发音角色:
    zh-HK-HiuGaaiNeural
    zh-HK-HiuMaanNeural
    zh-HK-WanLungNeural
    zh-CN-XiaoxiaoNeural
    zh-CN-XiaoyiNeural
    zh-CN-YunjianNeural
    zh-CN-YunxiNeural
    zh-CN-YunxiaNeural
    zh-CN-YunyangNeural
    zh-CN-liaoning-XiaobeiNeural
    zh-TW-HsiaoChenNeural
    zh-TW-YunJheNeural
    zh-TW-HsiaoYuNeural
    zh-CN-shaanxi-XiaoniNeural

英语角色:
    en-AU-NatashaNeural
    en-AU-WilliamNeural
    en-CA-ClaraNeural
    en-CA-LiamNeural
    en-HK-SamNeural
    en-HK-YanNeural
    en-IN-NeerjaExpressiveNeural
    en-IN-NeerjaNeural
    en-IN-PrabhatNeural
    en-IE-ConnorNeural
    en-IE-EmilyNeural
    en-KE-AsiliaNeural
    en-KE-ChilembaNeural
    en-NZ-MitchellNeural
    en-NZ-MollyNeural
    en-NG-AbeoNeural
    en-NG-EzinneNeural
    en-PH-JamesNeural
    en-PH-RosaNeural
    en-SG-LunaNeural
    en-SG-WayneNeural
    en-ZA-LeahNeural
    en-ZA-LukeNeural
    en-TZ-ElimuNeural
    en-TZ-ImaniNeural
    en-GB-LibbyNeural
    en-GB-MaisieNeural
    en-GB-RyanNeural
    en-GB-SoniaNeural
    en-GB-ThomasNeural
    en-US-AvaMultilingualNeural
    en-US-AndrewMultilingualNeural
    en-US-EmmaMultilingualNeural
    en-US-BrianMultilingualNeural
    en-US-AvaNeural
    en-US-AndrewNeural
    en-US-EmmaNeural
    en-US-BrianNeural
    en-US-AnaNeural
    en-US-AriaNeural
    en-US-ChristopherNeural
    en-US-EricNeural
    en-US-GuyNeural
    en-US-JennyNeural
    en-US-MichelleNeural
    en-US-RogerNeural
    en-US-SteffanNeural

日语角色:
    ja-JP-KeitaNeural
    ja-JP-NanamiNeural

韩语角色:
    ko-KR-HyunsuNeural
    ko-KR-InJoonNeural
    ko-KR-SunHiNeural

法语角色:
    fr-BE-CharlineNeural
    fr-BE-GerardNeural
    fr-CA-ThierryNeural
    fr-CA-AntoineNeural
    fr-CA-JeanNeural
    fr-CA-SylvieNeural
    fr-FR-VivienneMultilingualNeural
    fr-FR-RemyMultilingualNeural
    fr-FR-DeniseNeural
    fr-FR-EloiseNeural
    fr-FR-HenriNeural
    fr-CH-ArianeNeural
    fr-CH-FabriceNeural

德语角色:
    de-AT-IngridNeural
    de-AT-JonasNeural
    de-DE-SeraphinaMultilingualNeural
    de-DE-FlorianMultilingualNeural
    de-DE-AmalaNeural
    de-DE-ConradNeural
    de-DE-KatjaNeural
    de-DE-KillianNeural
    de-CH-JanNeural
    de-CH-LeniNeural

西班牙语角色:
    es-AR-ElenaNeural
    es-AR-TomasNeural
    es-BO-MarceloNeural
    es-BO-SofiaNeural
    es-CL-CatalinaNeural
    es-CL-LorenzoNeural
    es-ES-XimenaNeural
    es-CO-GonzaloNeural
    es-CO-SalomeNeural
    es-CR-JuanNeural
    es-CR-MariaNeural
    es-CU-BelkysNeural
    es-CU-ManuelNeural
    es-DO-EmilioNeural
    es-DO-RamonaNeural
    es-EC-AndreaNeural
    es-EC-LuisNeural
    es-SV-LorenaNeural
    es-SV-RodrigoNeural
    es-GQ-JavierNeural
    es-GQ-TeresaNeural
    es-GT-AndresNeural
    es-GT-MartaNeural
    es-HN-CarlosNeural
    es-HN-KarlaNeural
    es-MX-DaliaNeural
    es-MX-JorgeNeural
    es-NI-FedericoNeural
    es-NI-YolandaNeural
    es-PA-MargaritaNeural
    es-PA-RobertoNeural
    es-PY-MarioNeural
    es-PY-TaniaNeural
    es-PE-AlexNeural
    es-PE-CamilaNeural
    es-PR-KarinaNeural
    es-PR-VictorNeural
    es-ES-AlvaroNeural
    es-ES-ElviraNeural
    es-US-AlonsoNeural
    es-US-PalomaNeural
    es-UY-MateoNeural
    es-UY-ValentinaNeural
    es-VE-PaolaNeural
    es-VE-SebastianNeural

阿拉伯语角色:
    ar-DZ-AminaNeural
    ar-DZ-IsmaelNeural
    ar-BH-AliNeural
    ar-BH-LailaNeural
    ar-EG-SalmaNeural
    ar-EG-ShakirNeural
    ar-IQ-BasselNeural
    ar-IQ-RanaNeural
    ar-JO-SanaNeural
    ar-JO-TaimNeural
    ar-KW-FahedNeural
    ar-KW-NouraNeural
    ar-LB-LaylaNeural
    ar-LB-RamiNeural
    ar-LY-ImanNeural
    ar-LY-OmarNeural
    ar-MA-JamalNeural
    ar-MA-MounaNeural
    ar-OM-AbdullahNeural
    ar-OM-AyshaNeural
    ar-QA-AmalNeural
    ar-QA-MoazNeural
    ar-SA-HamedNeural
    ar-SA-ZariyahNeural
    ar-SY-AmanyNeural
    ar-SY-LaithNeural
    ar-TN-HediNeural
    ar-TN-ReemNeural
    ar-AE-FatimaNeural
    ar-AE-HamdanNeural
    ar-YE-MaryamNeural
    ar-YE-SalehNeural
 
 
孟加拉语角色:
    bn-BD-NabanitaNeural
    bn-BD-PradeepNeural
    bn-IN-BashkarNeural
    bn-IN-TanishaaNeural

捷克语角色
    cs-CZ-AntoninNeural
    cs-CZ-VlastaNeural

荷兰语角色:
    nl-BE-ArnaudNeural
    nl-BE-DenaNeural
    nl-NL-ColetteNeural
    nl-NL-FennaNeural
    nl-NL-MaartenNeural

希伯来语角色:
    he-IL-AvriNeural
    he-IL-HilaNeural

印地语角色:
    hi-IN-MadhurNeural
    hi-IN-SwaraNeural

匈牙利语角色:
    hu-HU-NoemiNeural
    hu-HU-TamasNeural

印尼语角色:
    id-ID-ArdiNeural
    id-ID-GadisNeural

意大利语角色:
    it-IT-GiuseppeNeural
    it-IT-DiegoNeural
    it-IT-ElsaNeural
    it-IT-IsabellaNeural

哈萨克语角色:
    kk-KZ-AigulNeural
    kk-KZ-DauletNeural
    
马来语角色:
    ms-MY-OsmanNeural
    ms-MY-YasminNeural

波兰语角色:
    pl-PL-MarekNeural
    pl-PL-ZofiaNeural

葡萄牙语角色:
    pt-BR-ThalitaNeural
    pt-BR-AntonioNeural
    pt-BR-FranciscaNeural
    pt-PT-DuarteNeural
    pt-PT-RaquelNeural

俄语角色:
    ru-RU-DmitryNeural
    ru-RU-SvetlanaNeural

瑞典语角色:
    sw-KE-RafikiNeural
    sw-KE-ZuriNeural
    sw-TZ-DaudiNeural
    sw-TZ-RehemaNeural

泰国语角色:
    th-TH-NiwatNeural
    th-TH-PremwadeeNeural

土耳其语角色:
    tr-TR-AhmetNeural
    tr-TR-EmelNeural

乌克兰语角色:
    uk-UA-OstapNeural
    uk-UA-PolinaNeural

越南语角色:
    vi-VN-HoaiMyNeural
    vi-VN-NamMinhNeural

在 OpenAI sdk 中使用

需要安装 openai 库 pip install openai

from openai import OpenAI

client = OpenAI(api_key='12314', base_url='http://你的ip:7899/v1')
with  client.audio.speech.with_streaming_response.create(
                    model='tts-1',
                    voice='zh-CN-YunxiNeural',
                    input='你好啊,亲爱的朋友们',
                    speed=1.0                    
                ) as response:
    with open('./test.mp3', 'wb') as f:
       for chunk in response.iter_bytes():
            f.write(chunk)

直接使用 requests 调用

import requests
res=requests.post('http://你的ip:7899/v1',data={"voice":"zh-CN-YunxiNeural",
                    "input":"你好啊,亲爱的朋友们",
                    speed=1.0 })
with open('./test.mp3', 'wb') as f:
    f.write(res.content)

将edge-tts部署在cloudflare上避免403错误

玩配音的基本都知道,微软的edge-tts是好用免费的语音合成利器,唯一缺点是对国内限流越来越严,不过可以通过部署到 cloudflare 来规避,并且还能白嫖 cloudflare的服务器和带宽资源。

先看效果,完成后将有一个配音api接口和一个web配音界面

image.png

这是web界面


const requestBody = {
          "model": "tts-1",
          "input": '这是要合成语音的文字',
          "voice": 'zh-CN-XiaoxiaoNeural',
          "response_format": "mp3",
          "speed": 1.0
        };

const response = await fetch('部署到cloudflare后的网址', {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Authorization': `Bearer 部署后的key,随意`,
            },
            body: JSON.stringify(requestBody),
});

这是接口调用js版函数,并兼容 openai tts 接口

接下来说说如何部署到 cloudflare 上

登录 cloudflare 创建一个Workers

网址 https://dash.cloudflare.com/ 如何登录注册不再赘述

登录后,点击左侧 Workers 和 Pages,打开创建页面

image.png

继续点击创建

image.png

然后在出现的输入框中填写一个英文名称,作为cloudflare赠送的免费子域名头

image.png

点击右下角部署后,在新出现的页面中继续点击编辑代码,进入核心阶段,复制代码

image.png

然后删掉里面所有的代码,复制下面的代码去替换

image.png

// 自定义api key ,用于防止滥用
const API_KEY = '';
const encoder = new TextEncoder();
let expiredAt = null;
let endpoint = null;
let clientId = "";


const TOKEN_REFRESH_BEFORE_EXPIRY = 3 * 60;  
let tokenInfo = {
    endpoint: null,
    token: null,
    expiredAt: null
};

addEventListener("fetch", event => {
    event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
    if (request.method === "OPTIONS") {
        return handleOptions(request);
    }
    
  
    const authHeader = request.headers.get("authorization") || request.headers.get("x-api-key");
    const apiKey = authHeader?.startsWith("Bearer ") 
        ? authHeader.slice(7) 
        : null;

    // 只在设置了 API_KEY 的情况下才验证              
    if (API_KEY && apiKey !== API_KEY) {
        return new Response(JSON.stringify({
            error: {
                message: "Invalid API key. Use 'Authorization: Bearer your-api-key' header",
                type: "invalid_request_error",
                param: null,
                code: "invalid_api_key"
            }
        }), {
            status: 401,
            headers: {
                "Content-Type": "application/json",
                ...makeCORSHeaders()
            }
        });
    }

    const requestUrl = new URL(request.url);
    const path = requestUrl.pathname;
    
    if (path === "/v1/audio/speech") {
        try {
            const requestBody = await request.json();
            const { 
                model = "tts-1",
                input,
                voice = "zh-CN-XiaoxiaoNeural",
                response_format = "mp3",
                speed = '1.0',
                volume='0',
                pitch = '0', // 添加 pitch 参数,默认值为 0
                style = "general"//添加style参数,默认值为general
            } = requestBody;

            let rate = parseInt(String( (parseFloat(speed)-1.0)*100) );
            let numVolume = parseInt( String(parseFloat(volume)*100) );
            let numPitch = parseInt(pitch); 
            const response = await getVoice(
                input, 
                voice, 
                rate>=0?`+${rate}%`:`${rate}%`,
                numPitch>=0?`+${numPitch}Hz`:`${numPitch}Hz`,
                numVolume>=0?`+${numVolume}%`:`${numVolume}%`,
                style,
                "audio-24khz-48kbitrate-mono-mp3"
            );

            return response;

        } catch (error) {
            console.error("Error:", error);
            return new Response(JSON.stringify({
                error: {
                    message: error.message,
                    type: "api_error",
                    param: null,
                    code: "edge_tts_error"
                }
            }), {
                status: 500,
                headers: {
                    "Content-Type": "application/json",
                    ...makeCORSHeaders()
                }
            });
        }
    }

    // 默认返回 404
    return new Response("Not Found", { status: 404 });
}

async function handleOptions(request) {
    return new Response(null, {
        status: 204,
        headers: {
            ...makeCORSHeaders(),
            "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
            "Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers") || "Authorization"
        }
    });
}

async function getVoice(text, voiceName = "zh-CN-XiaoxiaoNeural", rate = '+0%', pitch = '+0Hz', volume='+0%',style = "general", outputFormat = "audio-24khz-48kbitrate-mono-mp3") {
    try {
        const maxChunkSize = 2000;  
        const chunks = text.trim().split("\n");


        // 获取每个分段的音频
        //const audioChunks = await Promise.all(chunks.map(chunk => getAudioChunk(chunk, voiceName, rate, pitch, volume,style, outputFormat)));
        let audioChunks=[]
        while(chunks.length>0){
            try{
                let audio_chunk= await getAudioChunk(chunks.shift(), voiceName, rate, pitch, volume,style, outputFormat)
                audioChunks.push(audio_chunk)

            }catch(e){
                return new Response(JSON.stringify({
                    error: {
                        message: String(e),
                        type: "api_error",
                        param: `${voiceName}, ${rate}, ${pitch}, ${volume},${style}, ${outputFormat}`,
                        code: "edge_tts_error"
                    }
                }), {
                    status: 500,
                    headers: {
                        "Content-Type": "application/json",
                        ...makeCORSHeaders()
                    }
                });

            }
        }
       

        // 将音频片段拼接起来
        const concatenatedAudio = new Blob(audioChunks, { type: 'audio/mpeg' });
        const response = new Response(concatenatedAudio, {
            headers: {
                "Content-Type": "audio/mpeg",
                ...makeCORSHeaders()
            }
        });

        
        return response;

    } catch (error) {
        console.error("语音合成失败:", error);
        return new Response(JSON.stringify({
            error: {
                message: error,
                type: "api_error",
                param: null,
                code: "edge_tts_error "+voiceName
            }
        }), {
            status: 500,
            headers: {
                "Content-Type": "application/json",
                ...makeCORSHeaders()
            }
        });
    }
}



//获取单个音频数据
async function getAudioChunk(text, voiceName, rate, pitch,volume, style, outputFormat='audio-24khz-48kbitrate-mono-mp3') {
    const endpoint = await getEndpoint();
    const url = `https://${endpoint.r}.tts.speech.microsoft.com/cognitiveservices/v1`;
    let m=text.match(/\[(\d+)\]\s*?$/);
    let slien=0;
    if(m&&m.length==2){
      slien=parseInt(m[1]);
      text=text.replace(m[0],'')

    }
    const response = await fetch(url, {
        method: "POST",
        headers: {
            "Authorization": endpoint.t,
            "Content-Type": "application/ssml+xml",
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0",
            "X-Microsoft-OutputFormat": outputFormat
        },
        body: getSsml(text, voiceName, rate,pitch,volume, style,slien)
    });

    if (!response.ok) {
        const errorText = await response.text();
        throw new Error(`Edge TTS API error: ${response.status} ${errorText}`);
    }

    return response.blob();

}

function getSsml(text, voiceName, rate, pitch,volume,style,slien=0) {
   let slien_str='';
   if(slien>0){
    slien_str=`<break time="${slien}ms" />`
   }
    return `<speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" version="1.0" xml:lang="zh-CN"> 
                <voice name="${voiceName}"> 
                    <mstts:express-as style="${style}"  styledegree="2.0" role="default" > 
                        <prosody rate="${rate}" pitch="${pitch}" volume="${volume}">${text}</prosody> 
                    </mstts:express-as> 
                    ${slien_str}
                </voice> 
            </speak>`;

}

async function getEndpoint() {
    const now = Date.now() / 1000;
    
    if (tokenInfo.token && tokenInfo.expiredAt && now < tokenInfo.expiredAt - TOKEN_REFRESH_BEFORE_EXPIRY) {
        return tokenInfo.endpoint;
    }

    // 获取新token
    const endpointUrl = "https://dev.microsofttranslator.com/apps/endpoint?api-version=1.0";
    const clientId = crypto.randomUUID().replace(/-/g, "");
    
    try {
        const response = await fetch(endpointUrl, {
            method: "POST",
            headers: {
                "Accept-Language": "zh-Hans",
                "X-ClientVersion": "4.0.530a 5fe1dc6c",
                "X-UserId": "0f04d16a175c411e",
                "X-HomeGeographicRegion": "zh-Hans-CN",
                "X-ClientTraceId": clientId,
                "X-MT-Signature": await sign(endpointUrl),
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0",
                "Content-Type": "application/json; charset=utf-8",
                "Content-Length": "0",
                "Accept-Encoding": "gzip"
            }
        });

        if (!response.ok) {
            throw new Error(`获取endpoint失败: ${response.status}`);
        }

        const data = await response.json();
        const jwt = data.t.split(".")[1];
        const decodedJwt = JSON.parse(atob(jwt));
        
        tokenInfo = {
            endpoint: data,
            token: data.t,
            expiredAt: decodedJwt.exp
        };

        return data;

    } catch (error) {
        console.error("获取endpoint失败:", error);
        // 如果有缓存的token,即使过期也尝试使用
        if (tokenInfo.token) {
            console.log("使用过期的缓存token");
            return tokenInfo.endpoint;
        }
        throw error;
    }
}

function addCORSHeaders(response) {
    const newHeaders = new Headers(response.headers);
    for (const [key, value] of Object.entries(makeCORSHeaders())) {
        newHeaders.set(key, value);
    }
    return new Response(response.body, { ...response, headers: newHeaders });
}

function makeCORSHeaders() {
    return {
        "Access-Control-Allow-Origin": "*", 
        "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",
        "Access-Control-Allow-Headers": "Content-Type, x-api-key",
        "Access-Control-Max-Age": "86400"  
    };
}

async function hmacSha256(key, data) {
    const cryptoKey = await crypto.subtle.importKey(
        "raw",
        key,
        { name: "HMAC", hash: { name: "SHA-256" } },
        false,
        ["sign"]
    );
    const signature = await crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(data));
    return new Uint8Array(signature);
}

async function base64ToBytes(base64) {
    const binaryString = atob(base64);
    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
        bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes;
}

async function bytesToBase64(bytes) {
    return btoa(String.fromCharCode.apply(null, bytes));
}

function uuid() {
    return crypto.randomUUID().replace(/-/g, "");
}

async function sign(urlStr) {
    const url = urlStr.split("://")[1];
    const encodedUrl = encodeURIComponent(url);
    const uuidStr = uuid();
    const formattedDate = dateFormat();
    const bytesToSign = `MSTranslatorAndroidApp${encodedUrl}${formattedDate}${uuidStr}`.toLowerCase();
    const decode = await base64ToBytes("oik6PdDdMnOXemTbwvMn9de/h9lFnfBaCWbGMMZqqoSaQaqUOqjVGm5NqsmjcBI1x+sS9ugjB55HEJWRiFXYFw==");
    const signData = await hmacSha256(decode, bytesToSign);
    const signBase64 = await bytesToBase64(signData);
    return `MSTranslatorAndroidApp::${signBase64}::${formattedDate}::${uuidStr}`;
}

function dateFormat() {
    const formattedDate = (new Date()).toUTCString().replace(/GMT/, "").trim() + " GMT";
    return formattedDate.toLowerCase();
}

// 添加请求超时控制
async function fetchWithTimeout(url, options, timeout = 30000) {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), timeout);
    
    try {
        const response = await fetch(url, {
            ...options,
            signal: controller.signal
        });
        clearTimeout(id);
        return response;
    } catch (error) {
        clearTimeout(id);
        throw error;
    }
}

特别需要注意的是顶部两行代码,设置 api key ,防止被他人滥用

// 这是 api key,用于验证可用权限
const API_KEY = '';

绑定自己的域名

默认绑定的域名是 https://输入框填写的子域名头.你的账号名.workers.dev/

但不幸的是该域名在国内被墙,想免翻墙使用,你需要绑定一个自己的域名。

  1. 如果你还没有在 cloudflare上添加过自己的域名,可点击右上角添加--现有域,然后输入自己的域名

image.png

  1. 如果在cloudflare上已添加过域名,则点击左侧名称返回管理界面,添加自定义域名

image.png

点击 设置–域和路由–添加

image.png

再点击自定义域,然后填写已添加到 cloudflare 的域名的子域名,例如我的域名 pyvideotrans.com 已添加cloudflare,那么此处我可以填写 ttsapi.pyvideotrans.com

image.png

如下图,添加完毕

image.png

此处显示你添加的自定义域

image.png

在视频翻译软件中使用

请将软件升级到 v3.40 方可使用,升级下载地址 https://pyvideotrans.com/downpackage

打开菜单,进入 “TTS 设置” -> “OpenAI TTS”。将接口地址更改为 https://替换为你的自定义域/v1,”SK” 填写你的 API_KEY。在角色列表中,用英文逗号分隔,填写你想要使用的角色。

image.png

可用角色

以下是可用的角色列表。请注意,文字语言和角色必须匹配。

image.png

中文发音角色:
    zh-HK-HiuGaaiNeural
    zh-HK-HiuMaanNeural
    zh-HK-WanLungNeural
    zh-CN-XiaoxiaoNeural
    zh-CN-XiaoyiNeural
    zh-CN-YunjianNeural
    zh-CN-YunxiNeural
    zh-CN-YunxiaNeural
    zh-CN-YunyangNeural
    zh-CN-liaoning-XiaobeiNeural
    zh-TW-HsiaoChenNeural
    zh-TW-YunJheNeural
    zh-TW-HsiaoYuNeural
    zh-CN-shaanxi-XiaoniNeural

英语角色:
    en-AU-NatashaNeural
    en-AU-WilliamNeural
    en-CA-ClaraNeural
    en-CA-LiamNeural
    en-HK-SamNeural
    en-HK-YanNeural
    en-IN-NeerjaExpressiveNeural
    en-IN-NeerjaNeural
    en-IN-PrabhatNeural
    en-IE-ConnorNeural
    en-IE-EmilyNeural
    en-KE-AsiliaNeural
    en-KE-ChilembaNeural
    en-NZ-MitchellNeural
    en-NZ-MollyNeural
    en-NG-AbeoNeural
    en-NG-EzinneNeural
    en-PH-JamesNeural
    en-PH-RosaNeural
    en-SG-LunaNeural
    en-SG-WayneNeural
    en-ZA-LeahNeural
    en-ZA-LukeNeural
    en-TZ-ElimuNeural
    en-TZ-ImaniNeural
    en-GB-LibbyNeural
    en-GB-MaisieNeural
    en-GB-RyanNeural
    en-GB-SoniaNeural
    en-GB-ThomasNeural
    en-US-AvaMultilingualNeural
    en-US-AndrewMultilingualNeural
    en-US-EmmaMultilingualNeural
    en-US-BrianMultilingualNeural
    en-US-AvaNeural
    en-US-AndrewNeural
    en-US-EmmaNeural
    en-US-BrianNeural
    en-US-AnaNeural
    en-US-AriaNeural
    en-US-ChristopherNeural
    en-US-EricNeural
    en-US-GuyNeural
    en-US-JennyNeural
    en-US-MichelleNeural
    en-US-RogerNeural
    en-US-SteffanNeural

日语角色:
    ja-JP-KeitaNeural
    ja-JP-NanamiNeural

韩语角色:
    ko-KR-HyunsuNeural
    ko-KR-InJoonNeural
    ko-KR-SunHiNeural

法语角色:
    fr-BE-CharlineNeural
    fr-BE-GerardNeural
    fr-CA-ThierryNeural
    fr-CA-AntoineNeural
    fr-CA-JeanNeural
    fr-CA-SylvieNeural
    fr-FR-VivienneMultilingualNeural
    fr-FR-RemyMultilingualNeural
    fr-FR-DeniseNeural
    fr-FR-EloiseNeural
    fr-FR-HenriNeural
    fr-CH-ArianeNeural
    fr-CH-FabriceNeural

德语角色:
    de-AT-IngridNeural
    de-AT-JonasNeural
    de-DE-SeraphinaMultilingualNeural
    de-DE-FlorianMultilingualNeural
    de-DE-AmalaNeural
    de-DE-ConradNeural
    de-DE-KatjaNeural
    de-DE-KillianNeural
    de-CH-JanNeural
    de-CH-LeniNeural

西班牙语角色:
    es-AR-ElenaNeural
    es-AR-TomasNeural
    es-BO-MarceloNeural
    es-BO-SofiaNeural
    es-CL-CatalinaNeural
    es-CL-LorenzoNeural
    es-ES-XimenaNeural
    es-CO-GonzaloNeural
    es-CO-SalomeNeural
    es-CR-JuanNeural
    es-CR-MariaNeural
    es-CU-BelkysNeural
    es-CU-ManuelNeural
    es-DO-EmilioNeural
    es-DO-RamonaNeural
    es-EC-AndreaNeural
    es-EC-LuisNeural
    es-SV-LorenaNeural
    es-SV-RodrigoNeural
    es-GQ-JavierNeural
    es-GQ-TeresaNeural
    es-GT-AndresNeural
    es-GT-MartaNeural
    es-HN-CarlosNeural
    es-HN-KarlaNeural
    es-MX-DaliaNeural
    es-MX-JorgeNeural
    es-NI-FedericoNeural
    es-NI-YolandaNeural
    es-PA-MargaritaNeural
    es-PA-RobertoNeural
    es-PY-MarioNeural
    es-PY-TaniaNeural
    es-PE-AlexNeural
    es-PE-CamilaNeural
    es-PR-KarinaNeural
    es-PR-VictorNeural
    es-ES-AlvaroNeural
    es-ES-ElviraNeural
    es-US-AlonsoNeural
    es-US-PalomaNeural
    es-UY-MateoNeural
    es-UY-ValentinaNeural
    es-VE-PaolaNeural
    es-VE-SebastianNeural

阿拉伯语角色:
    ar-DZ-AminaNeural
    ar-DZ-IsmaelNeural
    ar-BH-AliNeural
    ar-BH-LailaNeural
    ar-EG-SalmaNeural
    ar-EG-ShakirNeural
    ar-IQ-BasselNeural
    ar-IQ-RanaNeural
    ar-JO-SanaNeural
    ar-JO-TaimNeural
    ar-KW-FahedNeural
    ar-KW-NouraNeural
    ar-LB-LaylaNeural
    ar-LB-RamiNeural
    ar-LY-ImanNeural
    ar-LY-OmarNeural
    ar-MA-JamalNeural
    ar-MA-MounaNeural
    ar-OM-AbdullahNeural
    ar-OM-AyshaNeural
    ar-QA-AmalNeural
    ar-QA-MoazNeural
    ar-SA-HamedNeural
    ar-SA-ZariyahNeural
    ar-SY-AmanyNeural
    ar-SY-LaithNeural
    ar-TN-HediNeural
    ar-TN-ReemNeural
    ar-AE-FatimaNeural
    ar-AE-HamdanNeural
    ar-YE-MaryamNeural
    ar-YE-SalehNeural
 
 
孟加拉语角色:
    bn-BD-NabanitaNeural
    bn-BD-PradeepNeural
    bn-IN-BashkarNeural
    bn-IN-TanishaaNeural

捷克语角色
    cs-CZ-AntoninNeural
    cs-CZ-VlastaNeural

荷兰语角色:
    nl-BE-ArnaudNeural
    nl-BE-DenaNeural
    nl-NL-ColetteNeural
    nl-NL-FennaNeural
    nl-NL-MaartenNeural

希伯来语角色:
    he-IL-AvriNeural
    he-IL-HilaNeural

印地语角色:
    hi-IN-MadhurNeural
    hi-IN-SwaraNeural

匈牙利语角色:
    hu-HU-NoemiNeural
    hu-HU-TamasNeural

印尼语角色:
    id-ID-ArdiNeural
    id-ID-GadisNeural

意大利语角色:
    it-IT-GiuseppeNeural
    it-IT-DiegoNeural
    it-IT-ElsaNeural
    it-IT-IsabellaNeural

哈萨克语角色:
    kk-KZ-AigulNeural
    kk-KZ-DauletNeural
    
马来语角色:
    ms-MY-OsmanNeural
    ms-MY-YasminNeural

波兰语角色:
    pl-PL-MarekNeural
    pl-PL-ZofiaNeural

葡萄牙语角色:
    pt-BR-ThalitaNeural
    pt-BR-AntonioNeural
    pt-BR-FranciscaNeural
    pt-PT-DuarteNeural
    pt-PT-RaquelNeural

俄语角色:
    ru-RU-DmitryNeural
    ru-RU-SvetlanaNeural

瑞典语角色:
    sw-KE-RafikiNeural
    sw-KE-ZuriNeural
    sw-TZ-DaudiNeural
    sw-TZ-RehemaNeural

泰国语角色:
    th-TH-NiwatNeural
    th-TH-PremwadeeNeural

土耳其语角色:
    tr-TR-AhmetNeural
    tr-TR-EmelNeural

乌克兰语角色:
    uk-UA-OstapNeural
    uk-UA-PolinaNeural

越南语角色:
    vi-VN-HoaiMyNeural
    vi-VN-NamMinhNeural

使用 openai sdk 测试

这是兼容openai 的接口,可使用openai sdk 直接测试,如下python代码

import logging
from openai import OpenAI
import json
import httpx

api_key = 'adgas213423235saeg'  # 替换为你的实际 API key
base_url = 'https://xxx.xxx.com/v1' # 替换为你的自定义域,默认加 /v1


client = OpenAI(
    api_key=api_key,
    base_url=base_url
)



data = {
    'model': 'tts-1',
    'input': '你好啊,亲爱的朋友们',
    'voice': 'zh-CN-YunjianNeural',
    'response_format': 'mp3',
    'speed': 1.0,
}


try:
    response = client.audio.speech.create(
       **data
    )
    with open('./test_openai.mp3', 'wb') as f:
        f.write(response.content)
    print("MP3 file saved successfully to test_openai.mp3")

except Exception as e:
    print(f"An error occurred: {e}")

搭建web界面

接口有了,那么如何搭建页面呢?

打开该项目 https://github.com/jianchang512/tts-pyvideotrans
下载解压,然后将其中的 index.html/output.css/vue.js 3个文件放在服务器目录下,访问 index.html 即可。

image.png

注意在 index.html 搜索 https://ttsapi.pyvideotrans.com, 改为你部署在 cloudflare 的自定义域,否则无法使用

注意必须删掉底部的 4个 script 行代码,这是用于本站的统计和广告代码,因该项目直接用于本站,故有此代码

参考

  1. edge-tts-openai-cf-worker
  2. edge-tts

在cloudflare上体验大模型

想体验大模型的魅力,又苦于本地电脑性能不足?通常,我们会在本地使用像 ollama 这样的工具部署模型,但受限于电脑资源,往往只能运行 1.5b (15亿), 7b (70亿), 14b (140亿) 等较小规模的模型。想要部署 700 亿参数的大模型,对本地硬件来说是巨大的挑战。

现在,可以借助 Cloudflare 的 Workers AI 在线部署 70b 这样的大模型,并通过外网访问。它的接口兼容 OpenAI,这意味着你可以像使用 OpenAI 的 API 一样使用它。唯一的缺点是每日免费额度有限,超出部分会产生费用。如果你有兴趣,不妨尝试一下!

准备工作:登录 Cloudflare 并绑定域名

如果你还没有自己的域名,Cloudflare 会提供一个免费的账号域名。但需要注意的是,这个免费域名在国内可能无法直接访问,你可能需要使用一些“魔法”才能访问。

首先,打开 Cloudflare 官网 (https://dash.cloudflare.com) 并登录你的账号。

步骤一:创建 Workers AI

  1. 找到 Workers AI: 在 Cloudflare 控制台的左侧导航栏中,找到“AI” -> “Workers AI”,然后点击“从 Worker 模板创建”。

    image.png

  2. 创建 Worker: 接着点击 “创建 Worker”。

    image.png

  3. 填写 Worker 名称: 输入一个由英文字母组成的字符串,这个字符串将作为你 Worker 的默认账号域名。

image.png

image.png

  1. 部署: 点击右下角的“部署”按钮,完成 Worker 的创建。

步骤二:修改代码,部署 Llama 3.3 70b 大模型

  1. 进入代码编辑: 部署完成后,你会看到如下图所示的界面。点击“编辑代码”。

    image.png

  2. 清空代码: 删除编辑器中所有预设的代码。

    image.png

  3. 粘贴代码: 将以下代码复制粘贴到代码编辑器中:

    这里我们使用的是 llama-3.3-70b-instruct-fp8-fast 模型,它拥有 700 亿参数。

    你也可以在 Cloudflare 模型页面 找到其他模型进行替换,例如 Deepseek 开源模型。但目前 llama-3.3-70b-instruct-fp8-fast 是规模最大、效果最好的模型之一。

image.png

 const API_KEY='123456';
 export default {
   async fetch(request, env) {

     let url = new URL(request.url);
     const path = url.pathname;

     const authHeader = request.headers.get("authorization") || request.headers.get("x-api-key");
     const apiKey = authHeader?.startsWith("Bearer ")  ? authHeader.slice(7)  : null;
                         
     if (API_KEY && apiKey !== API_KEY) {

       return new Response(JSON.stringify({
         error: {
             message: "Invalid API key. Use 'Authorization: Bearer your-api-key' header",
             type: "invalid_request_error",
             param: null,
             code: "invalid_api_key"
         }
       }), {
           status: 401,
           headers: {
               "Content-Type": "application/json",
           }
       });
     }

     if (path === "/v1/chat/completions") {
       const requestBody = await request.json();
        // messages - chat style input
 const {message}=requestBody
 let chat = {
messages: message
 };
       let response = await env.AI.run('@cf/meta/llama-3.3-70b-instruct-fp8-fast', requestBody);
     
       let resdata={
         choices:[{"message":{"content":response.response}}]
       }    
       return Response.json(resdata);
     }  
    
   }
 };
  1. 部署代码: 粘贴代码后,点击“部署”按钮。

image.png

步骤三:绑定自定义域名

  1. 返回设置: 点击左侧的返回按钮,回到 Worker 的管理页面,找到“设置” -> “域和路由”。

image.png

  1. 添加自定义域: 点击“添加域”,然后选择“自定义域”并输入你已经绑定到 Cloudflare 的子域名。

image.png

步骤四:在兼容 OpenAI 的工具中使用

添加自定义域名后,你就可以在任何兼容 OpenAI API 的工具中使用这个大模型了。

  • API Key: 是你代码中设置的 API_KEY,默认为 123456
  • API 地址: https://你的自定义域名/v1

得益于 Cloudflare 强大的 GPU 资源,使用起来会非常流畅。

注意事项

image.png

Gemini安全过滤

在使用 Gemini AI 执行翻译或语音识别任务时,有时会遇到 “响应内容被标记”等报错

image.png

这是因为 Gemini 对处理的内容存在安全限制,虽然代码中允许一定的调整,也做了“Block None”的最宽松限定,但最终是否过滤仍由gemini综合评估决定。

Gemini API 的可调整安全过滤器涵盖以下类别,其他不再此列的内容无法通过代码调整:

类别说明
骚扰内容针对身份和/或受保护属性的负面或有害评论。
仇恨言论粗鲁、无礼或亵渎性的内容。
露骨色情内容包含对性行为或其他淫秽内容的引用。
危险内容宣扬、助长或鼓励有害行为。
公民诚信与选举相关的查询。

下表介绍了可以针对每种类别在代码中的屏蔽设置。

例如,如果您将仇恨言论类别的屏蔽设置设为屏蔽少部分,则系统会屏蔽包含仇恨言论内容概率较高的所有部分。但允许任何包含危险内容概率较低的部分。

阈值(Google AI Studio)阈值 (API)说明
全部不屏蔽BLOCK_NONE无论不安全内容的可能性如何,一律显示
屏蔽少部分BLOCK_ONLY_HIGH在出现不安全内容的概率较高时屏蔽
屏蔽一部分BLOCK_MEDIUM_AND_ABOVE当不安全内容的可能性为中等或较高时屏蔽
屏蔽大部分BLOCK_LOW_AND_ABOVE当不安全内容的可能性为较低、中等或较高时屏蔽
不适用HARM_BLOCK_THRESHOLD_UNSPECIFIED阈值未指定,使用默认阈值屏蔽

代码中可通过如下设置启用BLOCK_NONE

safetySettings = [
    {
        "category": HarmCategory.HARM_CATEGORY_HARASSMENT,
        "threshold": HarmBlockThreshold.BLOCK_NONE,
    },
    {
        "category": HarmCategory.HARM_CATEGORY_HATE_SPEECH,
        "threshold": HarmBlockThreshold.BLOCK_NONE,
    },
    {
        "category": HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
        "threshold": HarmBlockThreshold.BLOCK_NONE,
    },
    {
        "category": HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
        "threshold": HarmBlockThreshold.BLOCK_NONE,
    },
]

model = genai.GenerativeModel('gemini-2.0-flash-exp')
model.generate_content(
                message,
                safety_settings=safetySettings
)

然而要注意的是:即便都设置为了 BLOCK_NONE也不代表Gemini会放行相关内容,仍会根据上下文推断安全性从而过滤。

如何降低出现安全限制的概率?

一般来说,flash系列安全限制更多,pro和thinking系列模型相对较少,可尝试切换不同模型。
另外,在可能涉及敏感内容时,一次性少发送一些内容,降低上下文长度,也可在一定程度上降低安全过滤频率。

如何彻底禁止Gemini做安全判断,对上述内容统统放行?

绑定国外信用卡,切换到按月付费的高级账户

使用GeminiAI兼容openai

GeminiAI 是一款对开发者非常友好的大模型,它不仅界面美观、功能强大,还提供每日相当高的免费额度,足以满足日常使用需求。

然而,它也存在一些不便之处,例如必须始终科学上网,且 API 与 OpenAI SDK 不兼容。

为了解决这些问题,并实现与 OpenAI 的兼容,我编写了一段 JavaScript 代码,并将其部署到 Cloudflare 上,绑定了自己的域名。这样一来,就可以在国内免科学上网使用 Gemini,同时也能兼容 OpenAI。在任何使用 OpenAI 的工具中,只需简单地替换 API 地址和密钥(SK)即可。

在 Cloudflare 上创建 Worker

如果你还没有 Cloudflare 账号,请先注册一个(免费)。注册地址是:https://dash.cloudflare.com/
登录后,记得绑定你自己的域名,否则无法实现免代理访问。

登录后,在左侧边栏找到 Compute (Workers) 并点击,然后单击 创建 按钮。

image.png

image.png

在出现的页面中点击 创建 Worker

image.png

接着点击右下角的 部署,这样就完成了 Worker 的创建。

image.png

编辑代码

下面的代码是实现兼容 OpenAI 的关键,请复制它并替换 Worker 中默认生成的代码。

在刚才部署完成后的页面中,点击 编辑代码

image.png

删除左侧的所有代码,然后复制下面的代码并粘贴,最后点击右上角的 部署

image.png

复制以下代码

export default {
  async fetch (request) {
    if (request.method === "OPTIONS") {
      return handleOPTIONS();
    }
    const errHandler = (err) => {
      console.error(err);
      return new Response(err.message, fixCors({ status: err.status ?? 500 }));
    };
    try {
      const auth = request.headers.get("Authorization");
      const apiKey = auth?.split(" ")[1];
      const assert = (success) => {
        if (!success) {
          throw new HttpError("The specified HTTP method is not allowed for the requested resource", 400);
        }
      };
      const { pathname } = new URL(request.url);
	  if(!pathname.endsWith("/chat/completions")){
		  return new Response("hello")
	  }
        assert(request.method === "POST");
        return handleCompletions(await request.json(), apiKey).catch(errHandler);
    } catch (err) {
      return errHandler(err);
    }
  }
};

class HttpError extends Error {
  constructor(message, status) {
    super(message);
    this.name = this.constructor.name;
    this.status = status;
  }
}

const fixCors = ({ headers, status, statusText }) => {
  headers = new Headers(headers);
  headers.set("Access-Control-Allow-Origin", "*");
  return { headers, status, statusText };
};

const handleOPTIONS = async () => {
  return new Response(null, {
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "*",
      "Access-Control-Allow-Headers": "*",
    }
  });
};

const BASE_URL = "https://generativelanguage.googleapis.com";
const API_VERSION = "v1beta";

// https://github.com/google-gemini/generative-ai-js/blob/cf223ff4a1ee5a2d944c53cddb8976136382bee6/src/requests/request.ts#L71
const API_CLIENT = "genai-js/0.21.0"; // npm view @google/generative-ai version
const makeHeaders = (apiKey, more) => ({
  "x-goog-api-client": API_CLIENT,
  ...(apiKey && { "x-goog-api-key": apiKey }),
  ...more
});

const DEFAULT_MODEL = "gemini-2.0-flash-exp";
async function handleCompletions (req, apiKey) {
  let model = DEFAULT_MODEL;
  if(req.model.startsWith("gemini-")) {
      model = req.model;
  }
  const TASK = "generateContent";
  let url = `${BASE_URL}/${API_VERSION}/models/${model}:${TASK}`;

  const response = await fetch(url, {
    method: "POST",
    headers: makeHeaders(apiKey, { "Content-Type": "application/json" }),
    body: JSON.stringify(await transformRequest(req)), // try
  });

  let body = response.body;
  if (response.ok) {
    let id = generateChatcmplId();
      body = await response.text();
      body = processCompletionsResponse(JSON.parse(body), model, id);
  }
  return new Response(body, fixCors(response));
}

const harmCategory = [
  "HARM_CATEGORY_HATE_SPEECH",
  "HARM_CATEGORY_SEXUALLY_EXPLICIT",
  "HARM_CATEGORY_DANGEROUS_CONTENT",
  "HARM_CATEGORY_HARASSMENT",
  "HARM_CATEGORY_CIVIC_INTEGRITY",
];
const safetySettings = harmCategory.map(category => ({
  category,
  threshold: "BLOCK_NONE",
}));
const fieldsMap = {
  stop: "stopSequences",
  n: "candidateCount", 
  max_tokens: "maxOutputTokens",
  max_completion_tokens: "maxOutputTokens",
  temperature: "temperature",
  top_p: "topP",
  top_k: "topK", 
  frequency_penalty: "frequencyPenalty",
  presence_penalty: "presencePenalty",
};
const transformConfig = (req) => {
  let cfg = {};

  for (let key in req) {
    const matchedKey = fieldsMap[key];
    if (matchedKey) {
      cfg[matchedKey] = req[key];
    }
  }
  cfg.responseMimeType = "text/plain";
  return cfg;
};


const transformMsg = async ({ role, content }) => {
  const parts = [];
  if (!Array.isArray(content)) {

    parts.push({ text: content });
    return { role, parts };
  }

  for (const item of content) {
    switch (item.type) {
      case "text":
        parts.push({ text: item.text });
        break;

      case "input_audio":
        parts.push({
          inlineData: {
            mimeType: "audio/" + item.input_audio.format,
            data: item.input_audio.data,
          }
        });
        break;
      default:
        throw new TypeError(`Unknown "content" item type: "${item.type}"`);
    }
  }
  if (content.every(item => item.type === "image_url")) {
    parts.push({ text: "" });	
  }
  return { role, parts };
};

const transformMessages = async (messages) => {
  if (!messages) { return; }
  const contents = [];
  let system_instruction;
  for (const item of messages) {
    if (item.role === "system") {
      delete item.role;
      system_instruction = await transformMsg(item);
    } else {
      item.role = item.role === "assistant" ? "model" : "user";
      contents.push(await transformMsg(item));
    }
  }
  if (system_instruction && contents.length === 0) {
    contents.push({ role: "model", parts: { text: " " } });
  }
  return { system_instruction, contents };
};

const transformRequest = async (req) => ({
  ...await transformMessages(req.messages),
  safetySettings,
  generationConfig: transformConfig(req),
});

const generateChatcmplId = () => {
  const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  const randomChar = () => characters[Math.floor(Math.random() * characters.length)];
  return "chatcmpl-" + Array.from({ length: 29 }, randomChar).join("");
};

const reasonsMap = { 
  "STOP": "stop",
  "MAX_TOKENS": "length",
  "SAFETY": "content_filter",
  "RECITATION": "content_filter"
};
const SEP = "\n\n|>";
const transformCandidates = (key, cand) => ({
  index: cand.index || 0,
  [key]: {
    role: "assistant",
    content: cand.content?.parts.map(p => p.text).join(SEP) },
  logprobs: null,
  finish_reason: reasonsMap[cand.finishReason] || cand.finishReason,
});
const transformCandidatesMessage = transformCandidates.bind(null, "message");
const transformCandidatesDelta = transformCandidates.bind(null, "delta");

const transformUsage = (data) => ({
  completion_tokens: data.candidatesTokenCount,
  prompt_tokens: data.promptTokenCount,
  total_tokens: data.totalTokenCount
});

const processCompletionsResponse = (data, model, id) => {
  return JSON.stringify({
    id,
    choices: data.candidates.map(transformCandidatesMessage),
    created: Math.floor(Date.now()/1000),
    model,
    object: "chat.completion",
    usage: transformUsage(data.usageMetadata),
  });
};

绑定域名

部署完成后,会有一个 Cloudflare 提供的二级子域名,但该域名在国内无法正常访问,因此需要绑定你自己的域名才能实现免代理访问。

部署完成后,点击左侧的 返回

image.png

然后找到 设置域和路由,点击 添加

image.png

image.png

如下图所示,添加你已经托管在 Cloudflare 的域名。

image.png

完成后,即可使用该域名访问 Gemini。

使用 OpenAI SDK 访问 Gemini

from openai import OpenAI, APIConnectionError
model = OpenAI(api_key='Gemini的API Key', base_url='https://你的自定义域名.com')
response = model.chat.completions.create(
        model='gemini-2.0-flash-exp',
        messages=[
            {
                'role': 'user',
                'content': '你是谁'},
        ]
    )
    
print(response.choices[0])

返回如下:

Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='我是一个大型语言模型,由 Google 训练。\n', refusal=None, role='assistant', audio=None, function_call=None, tool_calls=None))

在其他兼容 OpenAI 的工具中使用

找到该工具配置 OpenAI 信息的位置,将 API 地址改为你在 Cloudflare 中添加的自定义域名,将 SK 改为你的 Gemini API Key,模型填写 gemini-2.0-flash-exp

image.png

image.png

直接使用 requests 访问

如果你不使用 OpenAI SDK,也可以直接使用 requests 库进行访问。

import requests

payload={
    "model":"gemini-1.5-flash",
    "messages":[{
        "role":"user",
        "content":[{"type":"text","text":"你是谁?"}]
    }]
}

res=requests.post('https://xxxx.com/chat/completions',headers={"Authorization":"Bearer 你的Gemini API Key","Content-Type":"application:/json"},json=payload)

print(res.json())

输出如下:

image.png

相关资源

  1. 源码修改自项目 PublicAffairs/openai-gemini
  2. GeminiAI 文档

glm-4-flash 和 qwen2.5-7b 免费大模型

智谱AI和硅基流动提供免费的大模型,可用来作为翻译渠道

自 v3.47 版本后,在翻译渠道中新增了 GLM-4-flash(免费)Qwen2.5-7b(免费) 这2个翻译渠道

只需要到 智谱AI官网(https://bigmodel.cn/usercenter/proj-mgmt/apikeys) 和 硅基流动官网(https://cloud.siliconflow.cn/account/ak) 创建api key,然后填写到软件的 菜单–翻译设置–glm-4-flash/qwen2.5-7b 中即可免费使用。


智谱AI

api key获取网址 https://bigmodel.cn/usercenter/proj-mgmt/apikeys

硅基流动

api获取网址 https://cloud.siliconflow.cn/account/ak