根据并发和响应延迟,实现语音识别接口自动切换需求

根据并发和响应延迟,语音识别接口自动 切换需求

需求描述:

  • 当请求的语音识别的请求数量大于3或者请求语音识别接口3秒不可达无响应,切换备用语音识别接口

科大讯飞语音识别作为备用接口

科大讯飞的API文档:

  • 进入讯飞开放平台的网页,注册账号,然后选择语音识别和实时语音听写服务.

  • API文档

阅读文档可知,讯飞的语音识别需要传参的音频文件类型是pcm也就是纯音频不带音频头文件这些。

go代码实现

go 复制代码
package service

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"github.com/gorilla/websocket"
	"io"
	"net/url"
	"os"
	"os/exec"
	"strings"
	"time"
)

// 判断文件扩展名并返回文件类型
func getFileExtension(filePath string) string {

	// 获取文件扩展名(不区分大小写)
	ext := strings.ToLower(filePath[strings.LastIndex(filePath, ".")+1:])

	return ext
}

// 使用 ffmpeg 将音频文件转换为 pcm 格式
func convertToPCM(inputFile string) ([]byte, error) {

	// 临时输出文件路径
	outputFile := "output.pcm"

	// 使用 ffmpeg 将音频文件转换为 pcm 格式
	cmd := exec.Command("ffmpeg", "-i", inputFile, "-f", "s16le", "-ac", "1", "-ar", "16000", outputFile)
	err := cmd.Run()
	if err != nil {
		return nil, fmt.Errorf("failed to convert to pcm: %v", err)
	}

	// 读取转换后的 pcm 文件
	convertedFile, err := os.Open(outputFile)
	if err != nil {
		return nil, fmt.Errorf("failed to open converted pcm file: %v", err)
	}
	defer convertedFile.Close()

	// 读取整个 pcm 文件内容到字节数组
	audioData, err := io.ReadAll(convertedFile)
	if err != nil {
		return nil, fmt.Errorf("failed to read pcm file: %v", err)
	}

	// 删除临时的 pcm 文件
	err = os.Remove(outputFile)
	if err != nil {
		fmt.Println("Warning: failed to remove temporary pcm file")
	}

	return audioData, nil

}

// 识别音频流的函数
func recognizeSpeechFromAudioStream(audioFilePath string) (string, error) {

	// 判断音频文件格式
	ext := getFileExtension(audioFilePath)

	// 如果是 wav 或 mp3 格式,则转换为 pcm
	var audioStream []byte
	var err error
	if ext == "wav" || ext == "mp3" {
		audioStream, err = convertToPCM(audioFilePath)
		if err != nil {
			return "", fmt.Errorf("failed to convert audio file to pcm: %v", err)
		}
	} else if ext == "pcm" {
		// 如果已经是 pcm 格式,直接读取文件
		audioFile, err := os.Open(audioFilePath)
		if err != nil {
			return "", fmt.Errorf("failed to open pcm file: %v", err)
		}
		defer audioFile.Close()

		audioStream, err = io.ReadAll(audioFile)
		if err != nil {
			return "", fmt.Errorf("failed to read pcm file: %v", err)
		}
	} else {
		return "", fmt.Errorf("unsupported audio format: %s", ext)
	}

	hostUrl := "wss://iat-api.xfyun.cn/v2/iat"
	appid := "xxxxx"
	apiSecret := "xxxxxx" // API密钥,需替换为自己的值
	apiKey := "xxxxxx"    // API Key,需替换为自己的值

	const (
		STATUS_FIRST_FRAME    = 0 // 标识音频的第一帧
		STATUS_CONTINUE_FRAME = 1 // 标识音频的中间帧
		STATUS_LAST_FRAME     = 2 // 标识音频的最后一帧
	)

	// 打开 WebSocket 连接
	st := time.Now() // 获取当前时间用于后续计算总耗时
	d := websocket.Dialer{
		HandshakeTimeout: 30 * time.Second, // 增加握手超时时间
	}
	conn, resp, err := d.Dial(assembleAuthUrl(hostUrl, apiKey, apiSecret), nil)
	if err != nil {
		return "", fmt.Errorf("failed to connect WebSocket: %v", err)
	} else if resp.StatusCode != 101 {
		fmt.Printf("WebSocket connection failed with status code: %d", resp.StatusCode)
		return "", fmt.Errorf("WebSocket connection failed with status code: %d", resp.StatusCode)
	}
	defer conn.Close() // 关闭WebSocket连接

	// 设定音频数据处理参数
	var frameSize = 1280
	var intervel = 40 * time.Millisecond
	// 开启协程发送数据
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 取消上下文,通知相关操作停止

	// 读取字节流并发送音频数据
	go func() {
		status := STATUS_FIRST_FRAME
		// 定义一个缓存
		buffer := make([]byte, frameSize)
		for i := 0; i < len(audioStream); i += frameSize {
			// 这里加入select语句来处理上下文取消
			select {
			case <-ctx.Done():
				// 如果上下文被取消,结束循环
				fmt.Println("session end ---")
				return
			default:
			}

			// 读取当前帧数据
			end := i + frameSize
			if end > len(audioStream) {

				end = len(audioStream)     // 如果是最后一帧,修正帧的结束位置
				status = STATUS_LAST_FRAME // 设置为最后一帧

			}
			copy(buffer, audioStream[i:end]) // 将当前帧数据复制到buffer

			// 根据状态发送音频数据
			switch status {
			case STATUS_FIRST_FRAME:
				frameData := map[string]interface{}{
					"common": map[string]interface{}{
						"app_id": appid,
					},
					"business": map[string]interface{}{
						"language": "zh_cn",
						"domain":   "iat",
						"accent":   "mandarin",
					},
					"data": map[string]interface{}{
						"status":   STATUS_FIRST_FRAME,
						"format":   "audio/L16;rate=16000",
						"audio":    base64.StdEncoding.EncodeToString(buffer[:end-i]),
						"encoding": "raw",
					},
				}
				conn.WriteJSON(frameData) // 发送数据

				status = STATUS_CONTINUE_FRAME // 设置为中间帧
				time.Sleep(intervel)           // 延迟发送下一帧
			case STATUS_CONTINUE_FRAME:
				frameData := map[string]interface{}{
					"data": map[string]interface{}{
						"status":   STATUS_CONTINUE_FRAME,
						"format":   "audio/L16;rate=16000",
						"audio":    base64.StdEncoding.EncodeToString(buffer[:end-i]),
						"encoding": "raw",
					},
				}
				conn.WriteJSON(frameData)
				// 判断是否为最后一帧
				if status == STATUS_LAST_FRAME {
					fmt.Println(" send last ") // 打印发送最后一帧数据
					return
				}
			case STATUS_LAST_FRAME:
				frameData := map[string]interface{}{
					"data": map[string]interface{}{
						"status":   STATUS_LAST_FRAME,
						"format":   "audio/L16;rate=16000",
						"audio":    base64.StdEncoding.EncodeToString(buffer[:end-i]),
						"encoding": "raw",
					},
				}
				conn.WriteJSON(frameData)
				return
			}

			//time.Sleep(intervel)
		}
	}()

	// 读取 WebSocket 返回的识别结果
	var fullResult strings.Builder

	for {
		var respData RespData
		_, msg, err := conn.ReadMessage()
		if err != nil {
			return "", fmt.Errorf("failed to read message: %v", err)
		}
		err = json.Unmarshal(msg, &respData)
		if err != nil {
			return "", fmt.Errorf("failed to parse response:%v", err.Error())
		}
		if respData.Code != 0 {
			return "", fmt.Errorf("error code: %d, message: %s", respData.Code, respData.Message)
		}
		fullResult.WriteString(respData.Data.Result.String())
		if respData.Data.Status == 2 {
			fmt.Println(respData.Code, respData.Message)
			fmt.Println("Final result received in :", time.Since(st))
			//return respData.Data.Result.String(), nil
			cancel() //通知协程停止发送
			break
		}
	}

	return fullResult.String(), nil

}

// RespData 用于解析返回的响应数据
type RespData struct {
	Sid     string `json:"sid"`
	Code    int    `json:"code"`
	Message string `json:"message"`
	Data    Data   `json:"data"`
}

// Data 用于封装识别数据
type Data struct {
	Result Result `json:"result"`
	Status int    `json:"status"`
}

// Result 用于封装识别结果
type Result struct {
	Ls  bool   `json:"ls"`
	Rg  []int  `json:"rg"`
	Sn  int    `json:"sn"`
	Pgs string `json:"pgs"`
	Ws  []Ws   `json:"ws"`
}

// String 返回识别结果的字符串
func (t *Result) String() string {
	var wss string
	for _, v := range t.Ws {
		wss += v.String()
	}
	return wss
}

// Ws 用于封装识别单词的位置信息
type Ws struct {
	Bg int  `json:"bg"`
	Cw []Cw `json:"cw"`
}

// String 返回单词的字符串
func (w *Ws) String() string {
	var wss string
	for _, v := range w.Cw {
		wss += v.W
	}
	return wss
}

// Cw 用于封装单词信息
type Cw struct {
	Sc int    `json:"sc"`
	W  string `json:"w"`
}

// 创建鉴权URL
func assembleAuthUrl(hosturl string, apiKey, apiSecret string) string {

	ul, err := url.Parse(hosturl)
	if err != nil {
		fmt.Println(err)
	}
	// 获取当前时间,用于签名
	date := time.Now().UTC().Format(time.RFC1123)

	// 拼接签名字符串
	signString := []string{"host: " + ul.Host, "date: " + date, "GET " + ul.Path + " HTTP/1.1"}
	sgin := strings.Join(signString, "\n")
	fmt.Println(sgin)

	// 计算HMAC签名
	sha := HmacWithShaTobase64("hmac-sha256", sgin, apiSecret)
	fmt.Println(sha)

	// 构建请求参数
	authUrl := fmt.Sprintf("hmac username=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey,
		"hmac-sha256", "host date request-line", sha)

	// base64编码后加入URL
	authorization := base64.StdEncoding.EncodeToString([]byte(authUrl))

	v := url.Values{}
	v.Add("host", ul.Host)
	v.Add("date", date)
	v.Add("authorization", authorization)

	// 返回完整的请求URL
	callurl := hosturl + "?" + v.Encode()
	return callurl
}

// HmacWithShaTobase64 计算HMAC SHA256签名并返回base64编码结果
func HmacWithShaTobase64(algorithm, data, key string) string {
	mac := hmac.New(sha256.New, []byte(key))
	mac.Write([]byte(data))
	encodeData := mac.Sum(nil)
	return base64.StdEncoding.EncodeToString(encodeData)
}

Whisper语音识别作为主接口使用

  • 使用的语音识别模型是:BELLE-2/Belle-whisper-large-v3-zh-punct,它对标点符号识别比较好。
使用Python封装接口
python 复制代码
from flask import Flask, request, jsonify
from transformers import pipeline
from opencc import OpenCC
from pydub import AudioSegment   # ✅ 新增导入
import numpy as np
import io

app = Flask(__name__)

# 初始化 Whisper 模型
transcriber = pipeline(
    "automatic-speech-recognition",
    model="BELLE-2/Belle-whisper-large-v3-zh-punct",
    device="cuda:0"
)

# 设置强制中文转录
transcriber.model.config.forced_decoder_ids = (
    transcriber.tokenizer.get_decoder_prompt_ids(
        language="zh",
        task="transcribe"
    )
)
# 繁体 -> 简体
cc = OpenCC('t2s')

@app.route("/transcribe", methods=["POST"])
def transcribe_audio():
    if "file" not in request.files:
        return jsonify({"error": "请求中没有文件"}), 400
    file = request.files["file"]
    if file.filename == "":
        return jsonify({"error": "没有选择文件"}), 400

    # 读取二进制数据
    audio_bytes = file.read()
    audio_stream = io.BytesIO(audio_bytes)

    # 用 pydub 解码(支持 wav/mp3/m4a/ogg 等)
    audio = AudioSegment.from_file(audio_stream)
    audio = audio.set_channels(1).set_frame_rate(16000)  # 单声道、16kHz

    # 转为 float32 numpy
    waveform = np.array(audio.get_array_of_samples()).astype(np.float32) / 32768.0
    samplerate = audio.frame_rate

    # 送给 Whisper
    
    result = transcriber({"array": waveform, "sampling_rate": samplerate})

    # 转简体
    text = result.get("text", "")
    simplified_text = cc.convert(text)
    segments = [{"text": simplified_text}]

    print("transcription =>", segments)
    return jsonify({"transcription": segments})


if __name__ == "__main__":
    app.run(debug=True, port=13667, host="0.0.0.0")
goland调用Python接口
go 复制代码
package service

import (
	"WorldEpcho/src/config"
	"WorldEpcho/src/config/e"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"io"
	"io/ioutil"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"sync/atomic"
	"time"
)

// ---------------- 新增:全局并发计数 ----------------
var inFlightASR int32 // 统计"尚未回复的语音识别"数量

// TranscriptionSegment 定义一个结构体来匹配JSON中transcription的数组元素
type TranscriptionSegment struct {
	//Start float64 `json:"start"`
	//End   float64 `json:"end"`
	Text string `json:"text"`
}

// TranscriptionResponse 是服务器响应的结构体
type TranscriptionResponse struct {
	Transcription []TranscriptionSegment `json:"transcription"`
}

// TranscribeAudio 用于处理上传的音频文件并调用语音识别接口
func TranscribeAudio(c *gin.Context) {
	/*
	   判断用户是否登录
	*/
	_, isLogin := IsUserLoggedIn(c)
	if !isLogin {
		log.Println("用户未登录")
		c.JSON(http.StatusOK, gin.H{"code": 0, "message": "用户未登录"})
		return
	}

	// 从请求中获取文件
	file, _, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"code": 0, "message": "读取请求文件失败"})
		return
	}
	defer file.Close()

	// 准备发送请求
	url := config.Conf.TranscribeUrl
	fmt.Println(config.ColorBlue, "request url: ", url, config.ColorReset)
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile("file", "filename")
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"code": 0, "message": "创建表单语音文件失败"})
		return
	}
	// 将文件复制到请求体
	_, err = io.Copy(part, file)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"code": 0, "message": "将文件赋值到请求体失败"})
		return
	}

	writer.Close()

	// 发送请求
	fmt.Println(config.ColorCyan, "发送请求...", config.ColorReset)
	request, err := http.NewRequest("POST", url, body)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"code": 0, "message": "发送请求数据失败"})
		return
	}
	request.Header.Set("Content-Type", writer.FormDataContentType())

	client := &http.Client{}
	resp, err := client.Do(request)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"code": 0, "message": "请求失败"})
		return
	}
	defer resp.Body.Close()

	// 读取响应
	respBody, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"code": 0, "message": "获取响应数据失败"})
		return
	}

	// 解析 JSON 到结构体
	var transResp TranscriptionResponse
	err = json.Unmarshal(respBody, &transResp)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"code": 0, "message": "解析json结构体错误"})
		return
	}
	// 输出结果
	c.JSON(http.StatusOK, gin.H{"code": 1, "message": transResp})
	fmt.Println(config.ColorPurple, "语音转换后响应结果:", config.ColorReset)
	fmt.Println(config.ColorGreen, transResp, config.ColorReset)

}

// 语音识别接口
func MGTranscribeAudio(c *gin.Context) {

	// 1) 校验 Token(原逻辑不变)
	tokenString := c.GetHeader("Token")
	if tokenString == "" {
		c.JSON(http.StatusOK, gin.H{"code": e.UnauthorizedStatus, "data": nil, "message": "请求头中无token,或未授权的token访问"})
		fmt.Println("请求头中无token,或未授权的token访问")
		return
	}
	isValid, err := IsValidMiGuToken(tokenString)
	if err != nil || !isValid {
		c.JSON(http.StatusOK, gin.H{"code": e.InvalidToken, "data": nil, "message": "无效或已过期的令牌"})
		fmt.Println("无效或已过期的令牌")
		return
	}

	// 2) 读取上传文件(保持参数与字段名不变:file)
	srcFile, fileHeader, err := c.Request.FormFile("file")
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"code": e.InvalidParams, "data": nil, "message": "读取请求音频文件失败"})
		fmt.Println("读取请求音频文件失败")
		return
	}
	defer srcFile.Close()

	// 把文件读入内存,便于两用(咪咕请求 + 讯飞降级)
	fileBytes, err := io.ReadAll(srcFile)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"code": e.InternalError, "data": nil, "message": "读取音频数据失败"})
		fmt.Println("读取音频数据失败")
		return
	}

	// 为讯飞接口准备一个临时文件路径(降级时使用)
	tmp, err := os.CreateTemp("", "asr-*"+uuid.New().String()+fileHeader.Filename)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"code": e.InternalError, "data": nil, "message": "创建临时文件失败"})
		return
	}
	tmpPath := tmp.Name()
	if _, err = tmp.Write(fileBytes); err != nil {
		tmp.Close()
		os.Remove(tmpPath)
		c.JSON(http.StatusInternalServerError, gin.H{"code": e.InternalError, "data": nil, "message": "写入临时文件失败"})
		return
	}
	tmp.Close()
	defer os.Remove(tmpPath)

	// 3) 准备咪咕请求体(multipart)
	url := config.Conf.TranscribeUrl
	fmt.Println(config.ColorBlue, "request url: ", url, config.ColorReset)
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	part, err := writer.CreateFormFile("file", fileHeader.Filename)
	if err != nil {
		fmt.Printf("创建表单语音文件失败: %v", err)
		c.JSON(http.StatusInternalServerError, gin.H{"code": e.InternalError, "data": nil, "message": "创建表单语音文件失败"})
		return
	}
	if _, err = io.Copy(part, bytes.NewReader(fileBytes)); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"code": e.InternalError, "data": nil, "message": "将文件赋值到请求体失败"})
		fmt.Println("将文件赋值到请求体失败")
		return
	}
	writer.Close()
	contentType := writer.FormDataContentType()

	// 打印请求参数(原样保留)
	fmt.Println("请求参数:")
	fmt.Println("URL:", url)
	fmt.Println("Headers: Content-Type:", contentType)
	fmt.Println("Token:", tokenString)
	fmt.Println("File Name:", fileHeader.Filename)

	// 4) 并发与超时控制
	// 4.1 先判断并发是否过高(>3)------过高则直接走讯飞降级
	if atomic.AddInt32(&inFlightASR, 1) > 3 {

		atomic.AddInt32(&inFlightASR, -1) // 撤销这次占位
		fmt.Println("当前尚未回复的语音识别请求数 > 3,切换到科大讯飞接口")
		xfResult, err := recognizeSpeechFromAudioStream(tmpPath)
		if err != nil {
			c.JSON(http.StatusOK, gin.H{"code": e.InternalError, "data": nil, "message": "科大讯飞识别失败: " + err.Error()})
			return
		}
		response := gin.H{
			"code":    e.SUCCESS,
			"data":    gin.H{"fullSpeechParse": xfResult},
			"message": "语音解析成功(已使用科大讯飞负载均衡)",
		}
		// 序列化响应数据为 JSON 字符串
		jsonData, err := json.Marshal(response)
		if err != nil {
			// 处理序列化错误
			fmt.Println("序列化 JSON 数据出错:", err)
			return
		}
		fmt.Println(config.ColorCyan, "语音识别响应数据 ==> ", string(jsonData), config.ColorReset)

		c.JSON(http.StatusOK, response)
		return
	}
	// 能走到这里,说明并发未超阈值,我们开启咪咕请求协程
	// 注意:这里不再增加计数(上面已经 +1),协程结束时 -1
	defer func() {
		atomic.AddInt32(&inFlightASR, -1)
	}()

	// 4.2 建立通道与上下文,用于 3s 超时兜底切换
	type miGuResp struct {
		body []byte
		err  error
	}
	resultCh := make(chan miGuResp, 1)

	// 使用可取消的 context 以便在主 goroutine 决定超时后终止请求
	reqCtx, cancel := context.WithCancel(context.Background())

	// 5) 发送咪咕请求的协程
	go func(b []byte, ct string) {
		client := &http.Client{}
		req, err := http.NewRequestWithContext(reqCtx, "POST", url, bytes.NewReader(b))
		if err != nil {
			resultCh <- miGuResp{nil, fmt.Errorf("发送请求数据失败: %w", err)}
			return
		}
		req.Header.Set("Content-Type", ct)
		// 如果服务端需要 Token 放 Header,可在此处设置;原代码打印 Token,但未设置到 Header
		// 如有需要:req.Header.Set("Token", tokenString)

		resp, err := client.Do(req)
		if err != nil {
			resultCh <- miGuResp{nil, fmt.Errorf("请求失败: %w", err)}
			return
		}
		defer resp.Body.Close()

		respBody, err := io.ReadAll(resp.Body)
		if err != nil {
			resultCh <- miGuResp{nil, fmt.Errorf("获取响应数据失败: %w", err)}
			return
		}
		resultCh <- miGuResp{respBody, nil}
	}(body.Bytes(), contentType)

	// 6) 3 秒等待咪咕结果,否则切到讯飞
	select {
	case r := <-resultCh:
		if r.err != nil {
			// 咪咕直接失败,降级到讯飞
			fmt.Println("咪咕请求报错,切换到科大讯飞:", r.err)
			xfResult, err := recognizeSpeechFromAudioStream(tmpPath)
			if err != nil {
				fmt.Println("讯飞语音识别失败: " + err.Error())
				c.JSON(http.StatusOK, gin.H{"code": e.InternalError, "data": nil, "message": "语音识别失败: " + err.Error()})
				return
			}
			fmt.Println("Whisper语音识别失败,已使用科大讯飞)")

			response := gin.H{
				"code":    e.SUCCESS,
				"data":    gin.H{"fullSpeechParse": xfResult},
				"message": "语音解析成功",
			}

			// 序列化响应数据为 JSON 字符串
			jsonData, err := json.Marshal(response)
			if err != nil {
				// 处理序列化错误
				fmt.Println("序列化 JSON 数据出错:", err)
				return
			}
			fmt.Println(config.ColorCyan, "语音识别响应数据 ==> ", string(jsonData), config.ColorReset)

			c.JSON(http.StatusOK, response)
			return
		}

		// 咪咕返回成功,解析 JSON
		var transResp TranscriptionResponse
		if err := json.Unmarshal(r.body, &transResp); err != nil {
			// 解析失败也走降级
			fmt.Println("解析咪咕JSON失败,切换到科大讯飞: ", err.Error())
			xfResult, derr := recognizeSpeechFromAudioStream(tmpPath)
			if derr != nil {
				fmt.Println("科大讯飞识别失败: " + derr.Error())
				c.JSON(http.StatusOK, gin.H{"code": e.InternalError, "data": nil, "message": "科大讯飞识别失败: " + derr.Error()})
				return
			}
			fmt.Println("语音解析成功(Whisper语音识别 JSON异常,已使用科大讯飞)")

			response := gin.H{
				"code":    e.SUCCESS,
				"data":    gin.H{"fullSpeechParse": xfResult},
				"message": "语音解析成功",
			}
			// 序列化响应数据为 JSON 字符串
			jsonData, err := json.Marshal(response)
			if err != nil {
				// 处理序列化错误
				fmt.Println("序列化 JSON 数据出错:", err)
				return
			}
			fmt.Println(config.ColorCyan, "语音识别响应数据 ==> ", string(jsonData), config.ColorReset)
			c.JSON(http.StatusOK, response)
			return
		}

		// 汇总咪咕转写文本
		var fullTranscript string
		for _, segment := range transResp.Transcription {
			fullTranscript += segment.Text
		}

		response := gin.H{
			"code":    e.SUCCESS,
			"data":    gin.H{"fullSpeechParse": fullTranscript},
			"message": "语音解析成功",
		}

		// 序列化响应数据为 JSON 字符串
		jsonData, err := json.Marshal(response)
		if err != nil {
			// 处理序列化错误
			fmt.Println("序列化 JSON 数据出错:", err)
			return
		}
		fmt.Println(config.ColorCyan, "语音识别响应数据 ==> ", string(jsonData), config.ColorReset)
		c.JSON(http.StatusOK, response)

		return

	case <-time.After(3 * time.Second):
		// 3 秒不可达,切换讯飞
		fmt.Println("Whisper 3 秒不可达,切换到科大讯飞")
		cancel() // 取消咪咕请求(让协程尽快退出)
		xfResult, err := recognizeSpeechFromAudioStream(tmpPath)
		if err != nil {
			fmt.Println("科大讯飞识别失败: " + err.Error())
			c.JSON(http.StatusOK, gin.H{"code": e.InternalError, "data": nil, "message": "科大讯飞识别失败: " + err.Error()})
			return
		}
		fmt.Println("语音解析成功(咪咕超时,已使用科大讯飞)")
		response := gin.H{
			"code":    e.SUCCESS,
			"data":    gin.H{"fullSpeechParse": xfResult},
			"message": "语音解析成功",
		}

		// 序列化响应数据为 JSON 字符串
		jsonData, err := json.Marshal(response)
		if err != nil {
			// 处理序列化错误
			fmt.Println("序列化 JSON 数据出错:", err)
			return
		}
		fmt.Println(config.ColorCyan, "语音识别响应数据 ==> ", string(jsonData), config.ColorReset)
		c.JSON(http.StatusOK, response)
		return
	}

}

添加路由

go 复制代码
	//语音识别
	router.POST("/SpeechConvertor", service.MGTranscribeAudio)
相关推荐
不老刘23 分钟前
《生成式AI消费级应用Top 100——第五版》| a16z
人工智能·ai·chatgpt·a16z
此处不留情2 小时前
基于pytorch的垃圾分类识别项目实战
人工智能·pytorch
mit6.8242 小时前
[AI人脸替换] docs | 环境部署指南 | 用户界面解析
人工智能·python
fantasy_arch2 小时前
Pytorch超分辨率模型实现与详细解释
人工智能·pytorch·python
爱读源码的大都督4 小时前
小白LLM教程:不训练模型,如何进行微调?
java·人工智能·后端
大千AI助手4 小时前
接吻数问题:从球体堆叠到高维空间的数学奥秘
人工智能·agi·deepmind·接吻数·kissingnumber·牛顿·alphaevolve
程序猿小D4 小时前
【完整源码+数据集+部署教程】硬币分类与识别系统源码和数据集:改进yolo11-SWC
人工智能·yolo·计算机视觉·数据挖掘·数据集·yolo11·硬币分类与识别系统
西猫雷婶5 小时前
神经网络|(十六)概率论基础知识-伽马函数·中
人工智能·深度学习·神经网络·学习·机器学习·概率论
Y|6 小时前
XGBoost(eXtreme Gradient Boosting,优化分布式梯度提升库)总结梳理
人工智能·算法·机器学习
空白到白6 小时前
机器学习-线性回归
人工智能·机器学习·线性回归