// Package controller 提供 transcribe 模块的 admin HTTP 接口 package controller import ( "encoding/json" "errors" "net/http" "strconv" "github.com/echochat/backend/app/dto" "github.com/echochat/backend/app/transcribe/dao" "github.com/echochat/backend/app/transcribe/model" "github.com/echochat/backend/app/transcribe/service" "github.com/echochat/backend/pkg/utils" "github.com/gin-gonic/gin" ) // TranscribeController admin 端转写控制器 type TranscribeController struct { svc *service.TranscribeService } // NewTranscribeController 创建实例 func NewTranscribeController(svc *service.TranscribeService) *TranscribeController { return &TranscribeController{svc: svc} } // GetTranscript GET /admin/recordings/:id/transcript // // 返回值: // - 200 + transcript:存在时返回最新状态 // - 200 + null:尚未发起转写(前端据此显示"开始转写"按钮) func (ctl *TranscribeController) GetTranscript(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil || id <= 0 { utils.ResponseBadRequest(c, "无效的录制 ID") return } t, err := ctl.svc.GetByRecording(c.Request.Context(), id) if err != nil { utils.ResponseError(c, "查询转写失败") return } utils.ResponseOK(c, toDTO(t)) } // SubmitTranscribe POST /admin/recordings/:id/transcribe // // body: { force: bool, language: string } // // 行为:异步启动 STT,返回 running 状态快照 func (ctl *TranscribeController) SubmitTranscribe(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil || id <= 0 { utils.ResponseBadRequest(c, "无效的录制 ID") return } var req dto.AdminTranscribeRequest // body 可选;POST 不带 body 也允许(默认 force=false language="") _ = c.ShouldBindJSON(&req) t, err := ctl.svc.Submit(c.Request.Context(), id, req.Force, req.Language) if err != nil { ctl.handleError(c, err, t) return } utils.ResponseOK(c, toDTO(t)) } // handleError 统一业务错误映射 func (ctl *TranscribeController) handleError(c *gin.Context, err error, t *model.Transcript) { switch { case errors.Is(err, service.ErrRecordingNotFound): utils.ResponseNotFound(c, err.Error()) case errors.Is(err, service.ErrRecordingNotReady): utils.ResponseBadRequest(c, err.Error()) case errors.Is(err, service.ErrSTTNotConfigured), errors.Is(err, dao.ErrLLMSourceDisabled): // 503 表示服务暂不可用,前端可据此提示"请联系管理员配置 STT" c.JSON(http.StatusServiceUnavailable, gin.H{ "code": 503, "message": "语音转写服务未配置或不可用", "data": nil, }) case errors.Is(err, service.ErrTranscribeRunning): // 已在跑:返回当前进行中行,前端可继续轮询 c.JSON(http.StatusAccepted, gin.H{ "code": 0, "message": "已有转写任务进行中", "data": toDTO(t), }) case errors.Is(err, dao.ErrNoActiveSTT): utils.ResponseBadRequest(c, "未找到可用的 STT 模型或 API Key,请检查 LLM 配置中心") default: utils.ResponseError(c, "发起转写失败: "+err.Error()) } } // toDTO 模型转 DTO,segments JSON 解码 + 状态中文标签 func toDTO(t *model.Transcript) *dto.AdminTranscriptDTO { if t == nil { return nil } segs := []dto.AdminTranscriptSeg{} if t.Segments != "" { _ = json.Unmarshal([]byte(t.Segments), &segs) } return &dto.AdminTranscriptDTO{ ID: t.ID, RecordingID: t.RecordingID, RoomID: t.RoomID, Status: t.Status, StatusLabel: statusLabel(t.Status), Text: t.Text, Segments: segs, Language: t.Language, DurationSec: t.DurationSec, ProviderCode: t.ProviderCode, ModelCode: t.ModelCode, ErrorMsg: t.ErrorMsg, StartedAt: dto.FormatTranscriptTimePtr(t.StartedAt), FinishedAt: dto.FormatTranscriptTimePtr(t.FinishedAt), CreatedAt: dto.FormatTranscriptTime(t.CreatedAt), UpdatedAt: dto.FormatTranscriptTime(t.UpdatedAt), } } func statusLabel(s string) string { switch s { case model.TranscriptStatusPending: return "待处理" case model.TranscriptStatusRunning: return "转写中" case model.TranscriptStatusReady: return "已就绪" case model.TranscriptStatusFailed: return "失败" default: return s } }