express是基于node.js的一个web框架,可以更加简洁的去创建一个后台服务,由于项目的需要,引入和typescript,经过几天的努力实现了chatgpt文字流输出+有道智云语音合成的结合(略有遗憾),下面我记载以下,以供参考
要出现chatgpt原生接口的流式效果(也就是一个字一个字往外面蹦),就得只能使用SSE(event-stream)和Websocket,其实采用轮询(短轮询和长轮询)也是可以的只是占用资源,下面我先来介绍这记得交互方法
可以看出SSE和Websocket两种协议在实时通讯中起着很大作用,下面介绍这两种协议在express的应用:
import { Stream } from 'node:stream';
import api from '../ToolClass/base.js'
async function sendTextBymodel1(req,res){
const params=res.data //获取前端传过来的数据,其中包含一个属性Stream,要设置为true
const {data}=await api.post("/v1/chat/completions",params,{
responseType:"stream"
})
res.send(readStream(data)) //这里进行对返回值的处理,可以在前端处理
}
function readStream(decoded) {
let response=""
let decodedArray = decoded.split("data: ");
let longstr = "";
decodedArray.forEach(decoded => {
try {
decoded = decoded.trim();
if ( longstr == "" ){
JSON.parse(decoded);
}else{
decoded = longstr + decoded;
longstr = "";
JSON.parse(decoded);
}
}catch ( e ){
longstr = decoded;
decoded = "";
}
if(decoded!==""){
if(decoded.trim()==="[DONE]"){
return;
}else{
response = JSON.parse(decoded).choices[0].delta.content ? JSON.parse(decoded).choices[0].delta.content : ""
return response
}
}
})
return response
}
export {sendTextBymodel1}
返回的就是一个一个字符
前端通过fetch或者EventSource来进行接收,对于普通的浏览器还是行的,不过使用在uniapp中打包成安卓就不行了,此时的解决方案就是Websocket:
下载包:
npm i express-ws
注入,使用:
import expressWs from 'express-ws'
import {sendTextBymodel1} from './Controller/ChatAI.js'
const app=express()
expressWs(app)
app.ws("/chat",sendTextBymodel1)
import { Stream } from 'node:stream';
import api from '../ToolClass/base.js'
async function char(params,ws){ /* */
try {
// speecher("有道词典API使有道词典API使有道词典API使有")
const {data}=await api.post("/v1/chat/completions",JSON.parse(params),{
responseType:"stream"
})
data.on("data", async (dat)=>{
await ws.send(dat.toString('utf8'))
})
data.on('close',async () => {
await ws.close();
});
} catch (error) {
ws.send({status:402,meaasge:"Websocket服务出现错误"})
}
}
function sendTextBymodel1(ws,res){
// 使用 ws 的 send 方法向连接另一端的客户端发送数据
// ws.send("connect to express server with WebSocket success")
let flag=false
ws.on("message",async (msg)=>{
char(msg,ws)
})
ws.on("close",(e)=>{
})
}
export {sendTextBymodel1}
前端实现:
uni.connectSocket({
url:"ws://43.155.177.34:8085/chat",
header: {
'content-type': 'application/json'
}
})
uni.onSocketOpen((res)=>{
uni.sendSocketMessage({
data: param
});
});
uni.onSocketError((res)=>{
console.log('WebSocket连接打开失败,请检查!');
});
uni.onSocketMessage((res)=>{
this.readStream(res.data,_this, currentResLocation,"chat"); //与上面SSE的后端代码方法一样
})
关于有道智云语音合成API的代码如下:
import axios from 'axios'
import { generateUUID } from './util.js'
import { config } from 'dotenv';
import crypto from 'crypto'
import id3 from 'node-id3'
import fs from 'fs'
config()
const setting={
q:"",
appKey:"",
salt:"",
sign:"",
signType:"v3",
curtime:"",
voiceName:"youxiaoqin",
format:"mp3"
}
async function speecher(q:string){
initData(q)
const response=await axios.post("https://openapi.youdao.com/ttsapi",setting,{
headers:{
'Content-Type': 'application/x-www-form-urlencoded',
},
responseType: 'arraybuffer'
})
let name=Math.floor(Date.now() / 1000)
let outputFilePath = 'public/'+name+'.mp3';
try {
fs.writeFileSync(outputFilePath,response.data,'binary');
// const tags = id3.read(outputFilePath);
// const durationInSeconds = tags && tags.duration ? tags.duration : 0;
// console.log(durationInSeconds);
} catch (error) {
console.log(error);
}
return outputFilePath
}
function calculateSHA256(input) {
const hash = crypto.createHash('sha256');
hash.update(input);
return hash.digest('hex'); // 返回十六进制表示的哈希值
}
function initData(q){
setting.q=Buffer.from(q, 'utf8').toString();
setting.appKey="应用key"
let salt=generateUUID()
setting.salt=salt
setting.voiceName="youxiaoqin"
setting.curtime=Math.floor(Date.now() / 1000).toString()
let input=getInput(q)
const hashedData = calculateSHA256("应用key"+input+salt+setting.curtime+"应用秘钥");
setting.sign=hashedData
setting.signType="v3"
}
function getInput(q){
if (q.length<=20) {
return q
}
return q.slice(0, 10)+q.length+q.slice(-10)
}
export default speecher
export function generateUUID() {
let d = new Date().getTime();
let uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
let r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid;
}
export const AI_HEAD_IMG_URL="https://th.bing.com/th?id=ODL.3e2fbff4543f0d3632d34be6d02adc93&w=100&h=100&c=12&pcl=faf9f7&o=6&dpr=1.5&pid=13.1"
其中有些变量可以不使用硬编码的形式,express可以使用环境变量,使用dotenv包
整个demo做下来,本想做成流式输出文字将文字流传给流式合成语言,然后将语言传给前端,达到实时对话,但是网上找了一遍支持流式语音的API都是国外的谷歌、微软、亚马逊,但是这些调用其API需要进行注册,注册过程中需要用到国外信用卡,悲痛,国内支持的流式传输的有百度,阿里的,只是是百度和阿里的声音比较简单,所以就没做了
本文参考了:
短轮询和长轮询_长轮询和短轮询_白鲸ld的博客-CSDN博客