将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