视频会议保存

This commit is contained in:
duoaohui
2026-05-18 21:23:00 +08:00
parent fb965cc8c4
commit 5a5706c236
20 changed files with 1211 additions and 1 deletions

View File

@@ -0,0 +1,142 @@
// 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 模型转 DTOsegments 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
}
}