Files
Cyrene/backend/gateway/internal/handler/voice_handler.go
T
AskaEth bcf4d4e621 feat: 第五轮开发 - 14项未来路线图功能完整实现
W1-W14 全部完成:
- W1: 消息搜索 (ILIKE全文检索 + SearchModal)
- W2: 对话导出 (JSON/Markdown/TXT三格式)
- W3: 记忆时间线 DevTools 可视化
- W4: 通知推送系统 (WebSocket + Browser Notification API)
- W5: 定时提醒 (30s轮询 + 重复提醒 + WebSocket推送)
- W6: 每日简报 (08:00自动生成: 天气+新闻+提醒+AI摘要)
- W7: IoT场景自动化 (规则引擎 10s轮询 + 条件评估 + 场景执行)
- W8: 语音输入 (浏览器 Speech Recognition API)
- W9: STT服务 (voice-service + whisper.cpp)
- W10: TTS服务 (浏览器 Speech Synthesis + edge-tts三档回退)
- W11: 文件管理 (上传/下载/缩略图/纯Go bilinear缩放)
- W12: 知识库RAG (PostgreSQL tsvector + 文档分块 + 检索)
- W13: 多模态 (图片上传+分析: Vision API + 本地Go分析回退)
- W14: PWA (Service Worker + 离线页 + install prompt)

总计: 6个Go微服务 + 10+前端组件 + 10+ PostgreSQL表 + 4个后台调度器
2026-05-19 12:01:09 +08:00

180 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"bytes"
"encoding/json"
"io"
"log"
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
// VoiceHandler 语音处理器 — 代理到 voice-service
type VoiceHandler struct {
voiceServiceURL string
client *http.Client
}
// NewVoiceHandler 创建语音处理器
func NewVoiceHandler(voiceServiceURL string) *VoiceHandler {
return &VoiceHandler{
voiceServiceURL: voiceServiceURL,
client: &http.Client{},
}
}
// Transcribe POST /api/v1/voice/transcribe
// 代理 multipart/form-data 请求到 voice-service
func (h *VoiceHandler) Transcribe(c *gin.Context) {
// 限制上传大小 (10MB)
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10<<20)
// 读取原始请求体
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败或文件过大,最大支持 10MB"})
return
}
// 构建代理请求
url := strings.TrimRight(h.voiceServiceURL, "/") + "/api/v1/transcribe"
proxyReq, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "构建代理请求失败"})
return
}
proxyReq.Header.Set("Content-Type", c.GetHeader("Content-Type"))
resp, err := h.client.Do(proxyReq)
if err != nil {
log.Printf("[voice] Voice-Service 不可达 (Transcribe): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": "Voice-Service 不可达: " + err.Error(),
"errorType": "voice_service_unreachable",
"hint": "Voice-Service 服务未启动或不可达,请先在「服务管理」面板中启动 Voice-Service",
})
return
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
// 透传状态码和响应
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody)
}
// TTSSynthesize POST /api/v1/voice/tts
// 代理 JSON 请求到 voice-service TTS 合成
func (h *VoiceHandler) TTSSynthesize(c *gin.Context) {
// 读取 JSON 请求体
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "读取请求体失败"})
return
}
// 构建代理请求
url := strings.TrimRight(h.voiceServiceURL, "/") + "/api/v1/tts/synthesize"
proxyReq, err := http.NewRequest("POST", url, bytes.NewReader(bodyBytes))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "构建代理请求失败"})
return
}
proxyReq.Header.Set("Content-Type", "application/json")
resp, err := h.client.Do(proxyReq)
if err != nil {
log.Printf("[voice] Voice-Service 不可达 (TTS): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": "Voice-Service 不可达: " + err.Error(),
"errorType": "voice_service_unreachable",
"hint": "Voice-Service 服务未启动或不可达",
})
return
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "读取 Voice-Service 响应失败"})
return
}
// 透传状态码、Content-Type 和响应体
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), respBody)
}
// TTSVoices GET /api/v1/voice/tts/voices
// 代理请求到 voice-service 获取可用语音列表
func (h *VoiceHandler) TTSVoices(c *gin.Context) {
url := strings.TrimRight(h.voiceServiceURL, "/") + "/api/v1/tts/voices"
resp, err := h.client.Get(url)
if err != nil {
log.Printf("[voice] Voice-Service 不可达 (Voices): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": "Voice-Service 不可达: " + err.Error(),
"errorType": "voice_service_unreachable",
})
return
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
// 解析并透传
var data interface{}
json.Unmarshal(respBody, &data)
c.JSON(resp.StatusCode, data)
}
// TTSStatus GET /api/v1/voice/tts/status
// 代理请求到 voice-service 获取 TTS 状态
func (h *VoiceHandler) TTSStatus(c *gin.Context) {
url := strings.TrimRight(h.voiceServiceURL, "/") + "/api/v1/tts/status"
resp, err := h.client.Get(url)
if err != nil {
log.Printf("[voice] Voice-Service 不可达 (TTS Status): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": "Voice-Service 不可达: " + err.Error(),
"errorType": "voice_service_unreachable",
})
return
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var data interface{}
json.Unmarshal(respBody, &data)
c.JSON(resp.StatusCode, data)
}
// VoiceStatus GET /api/v1/voice/status
// 代理请求到 voice-service 获取完整状态(STT + TTS
func (h *VoiceHandler) VoiceStatus(c *gin.Context) {
url := strings.TrimRight(h.voiceServiceURL, "/") + "/api/v1/status"
resp, err := h.client.Get(url)
if err != nil {
log.Printf("[voice] Voice-Service 不可达 (Status): %v", err)
c.JSON(http.StatusBadGateway, gin.H{
"error": "Voice-Service 不可达: " + err.Error(),
"errorType": "voice_service_unreachable",
})
return
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
var data interface{}
json.Unmarshal(respBody, &data)
c.JSON(resp.StatusCode, data)
}