视频保存
This commit is contained in:
47
admin/src/api/transcribe.js
Normal file
47
admin/src/api/transcribe.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 语音转写 API(Phase B)
|
||||||
|
*
|
||||||
|
* 对应后端路由:
|
||||||
|
* POST /api/v1/admin/recordings/:id/transcribe 提交转写任务(异步)
|
||||||
|
* GET /api/v1/admin/recordings/:id/transcript 查询转写结果
|
||||||
|
*
|
||||||
|
* 后端通过外部 LLM 配置中心 (MySQL t_llm_provider/t_llm_model/t_llm_key)
|
||||||
|
* 选择启用的 ASR 模型 + Key,调用 openai_compatible /audio/transcriptions。
|
||||||
|
*
|
||||||
|
* 状态机:pending → running → ready / failed
|
||||||
|
*
|
||||||
|
* 503:LLM 配置库未启用,admin 应提示「请联系管理员配置 STT」
|
||||||
|
* 202:任务已在跑,前端可继续轮询
|
||||||
|
*/
|
||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询某段录制的最新转写结果
|
||||||
|
* @param {number} recordingId
|
||||||
|
* @returns {Promise<{data: TranscriptDTO|null}>}
|
||||||
|
*/
|
||||||
|
export function getTranscript(recordingId) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/admin/recordings/${recordingId}/transcript`,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提交转写任务
|
||||||
|
* @param {number} recordingId
|
||||||
|
* @param {Object} [opts]
|
||||||
|
* @param {boolean} [opts.force=false] - true=已 ready 时强制重跑
|
||||||
|
* @param {string} [opts.language=''] - ISO-639 语言提示,"" 表示自动检测
|
||||||
|
* @returns {Promise<{data: TranscriptDTO}>}
|
||||||
|
*/
|
||||||
|
export function submitTranscribe(recordingId, opts = {}) {
|
||||||
|
return request({
|
||||||
|
url: `/api/v1/admin/recordings/${recordingId}/transcribe`,
|
||||||
|
method: 'post',
|
||||||
|
data: {
|
||||||
|
force: !!opts.force,
|
||||||
|
language: opts.language || ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/echochat/backend/app/admin/service"
|
"github.com/echochat/backend/app/admin/service"
|
||||||
"github.com/echochat/backend/app/dto"
|
"github.com/echochat/backend/app/dto"
|
||||||
|
meetingService "github.com/echochat/backend/app/meeting/service"
|
||||||
"github.com/echochat/backend/pkg/logs"
|
"github.com/echochat/backend/pkg/logs"
|
||||||
"github.com/echochat/backend/pkg/utils"
|
"github.com/echochat/backend/pkg/utils"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -76,11 +77,43 @@ func (ctl *MeetingManageController) GetMeetingStats(c *gin.Context) {
|
|||||||
utils.ResponseOK(c, resp)
|
utils.ResponseOK(c, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ForceStopRecording POST /api/v1/admin/meetings/recordings/:id/force-stop
|
||||||
|
//
|
||||||
|
// 兼底场景:host 离线/失联未点停止;host 端点击停止后 ffmpeg 异常但 webhook 失效;
|
||||||
|
// 任何长时间停留在 recording 状态的录制都可由此接口收尾。
|
||||||
|
//
|
||||||
|
// 行为:
|
||||||
|
// - 终态(ready/failed/uploading):幂等返回 200
|
||||||
|
// - recording 状态:走 media-server stop + MinIO 上传 + DB 状态机
|
||||||
|
func (ctl *MeetingManageController) ForceStopRecording(c *gin.Context) {
|
||||||
|
ctx := c.Request.Context()
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
utils.ResponseBadRequest(c, "录制 ID 非法")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctl.svc.ForceStopRecording(ctx, id); err != nil {
|
||||||
|
ctl.handleError(c, err, "强制结束录制失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
utils.ResponseOK(c, gin.H{"recording_id": id, "stopped": true})
|
||||||
|
}
|
||||||
|
|
||||||
// handleError 统一业务错误映射
|
// handleError 统一业务错误映射
|
||||||
func (ctl *MeetingManageController) handleError(c *gin.Context, err error, fallbackMsg string) {
|
func (ctl *MeetingManageController) handleError(c *gin.Context, err error, fallbackMsg string) {
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, service.ErrMeetingNotFound):
|
case errors.Is(err, service.ErrMeetingNotFound),
|
||||||
|
errors.Is(err, meetingService.ErrMeetingNotFound),
|
||||||
|
errors.Is(err, meetingService.ErrRecordingNotFound):
|
||||||
utils.ResponseNotFound(c, err.Error())
|
utils.ResponseNotFound(c, err.Error())
|
||||||
|
case errors.Is(err, meetingService.ErrRecordingNotStoppable):
|
||||||
|
utils.ResponseBadRequest(c, err.Error())
|
||||||
|
case errors.Is(err, meetingService.ErrMediaServiceUnavailable):
|
||||||
|
logs.Warn(c.Request.Context(), "controller.meeting_manage_controller.handleError",
|
||||||
|
"媒体服务不可用", zap.Error(err))
|
||||||
|
utils.ResponseError(c, "媒体服务暂时不可用,请稍后重试")
|
||||||
default:
|
default:
|
||||||
logs.Warn(c.Request.Context(), "controller.meeting_manage_controller.handleError",
|
logs.Warn(c.Request.Context(), "controller.meeting_manage_controller.handleError",
|
||||||
fallbackMsg, zap.Error(err))
|
fallbackMsg, zap.Error(err))
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
adminDAO "github.com/echochat/backend/app/admin/dao"
|
adminDAO "github.com/echochat/backend/app/admin/dao"
|
||||||
"github.com/echochat/backend/app/dto"
|
"github.com/echochat/backend/app/dto"
|
||||||
meetingModel "github.com/echochat/backend/app/meeting/model"
|
meetingModel "github.com/echochat/backend/app/meeting/model"
|
||||||
|
meetingService "github.com/echochat/backend/app/meeting/service"
|
||||||
"github.com/echochat/backend/pkg/logs"
|
"github.com/echochat/backend/pkg/logs"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -23,11 +24,28 @@ var ErrMeetingNotFound = errors.New("会议记录不存在")
|
|||||||
// 暂不在本服务里重复包装,避免与现有 RBAC/中间件冲突。
|
// 暂不在本服务里重复包装,避免与现有 RBAC/中间件冲突。
|
||||||
type MeetingManageService struct {
|
type MeetingManageService struct {
|
||||||
dao *adminDAO.MeetingManageDAO
|
dao *adminDAO.MeetingManageDAO
|
||||||
|
recSvc *meetingService.MeetingRecordingService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMeetingManageService Wire Provider
|
// NewMeetingManageService Wire Provider
|
||||||
func NewMeetingManageService(dao *adminDAO.MeetingManageDAO) *MeetingManageService {
|
func NewMeetingManageService(
|
||||||
return &MeetingManageService{dao: dao}
|
dao *adminDAO.MeetingManageDAO,
|
||||||
|
recSvc *meetingService.MeetingRecordingService,
|
||||||
|
) *MeetingManageService {
|
||||||
|
return &MeetingManageService{dao: dao, recSvc: recSvc}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForceStopRecording 管理员强制结束指定录制
|
||||||
|
//
|
||||||
|
// 委托给 meeting/service.MeetingRecordingService.ForceStopByAdmin:
|
||||||
|
// - 终态(ready/failed/uploading)幂等返回 nil
|
||||||
|
// - 否则走完整 stop + 上传 MinIO + DB 状态机
|
||||||
|
func (s *MeetingManageService) ForceStopRecording(ctx context.Context, recordingID int64) error {
|
||||||
|
funcName := "service.meeting_manage_service.ForceStopRecording"
|
||||||
|
logs.Info(ctx, funcName, "管理员请求强制结束录制", zap.Int64("recording_id", recordingID))
|
||||||
|
|
||||||
|
_, err := s.recSvc.ForceStopByAdmin(ctx, recordingID)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- public methods ----------
|
// ---------- public methods ----------
|
||||||
|
|||||||
Reference in New Issue
Block a user