【直播切片工作台】第31章:任务接口与任务服务

第31章:任务接口与任务服务

本章覆盖异步任务的"入口层"------handler/v1/task.go 与 service/task_service.go。它们是三组任务流水线(ai_slice / draft / ai_slice_draft)的编排起点:把 HTTP 请求翻译为带快照字段的 Task 落库,并即时唤醒对应 Worker。

流程

任务创建遵循"双路径"模型。推荐路径:前端只传 live_id + clips0(时间轴选区),由后端完成排序合并、按有效 ASR 拆分为多个 video_project、逐个发布任务;兼容路径:只传 video_project_id,复用已有项目。两条路径最终都汇聚到同一段"快照组装"代码。

flowchart TD A[POST /v1/tasks/ai-slice] --> B{live_id 还是 video_project_id?} B -- live_id + clips0 --> C[校验 clips0 与 ASR 完成态] C --> D[sortAndMerge 合并选区] D --> E[splitClips0IntoProjects<br/>按有效 ASR 约每 30 分钟拆分] E --> F[逐组创建 VideoProject] B -- video_project_id --> G[读取既有项目校验 clips0] F --> H[组装 Task 快照字段<br/>live_url/live_name/width/height] G --> H H --> I[task_repo.Create 落库] I --> J[Worker.Enqueue 唤醒] J --> K[返回 list/total 立即响应]

CreateDraft 是最简的特例:只接受 video_project_id,直播 URL 与切片由 Worker 从关联表读取------创建时即校验 ResolveClipRanges + ValidateClipRanges,把无效任务挡在队列之外。

实现

服务层最值得品味的是三层防御的创建期校验。第一层是素材状态:requireASRCompletedMaterial 检查 ASRStatus == completed,未完成直接返回 ErrTaskASRNotReady,避免 LLM 拿不到分句空跑。第二层是提示词:resolveSysPrompt 允许 prompt_id == 0 回落到 DefaultVideoProjectPromptID,但内容为空则拒绝建任务。第三层是选区与 ASR 的重叠校验:validateAISliceGroups 确保每段 clips0 与 live_asr 有重叠分句,否则抛 ErrTaskClips0NoASR。

快照策略贯穿始终:CreateDraft 中画布尺寸由 draft.ResolveCanvasSize(请求值, 项目值) 解析后写入 task.width/height;live_url/live_name 从素材实体复制。列表页因此永远不需要 JOIN------这是第 12 章 Task 模型设计的兑现处。

TaskExt 的纪律同样严格:clips 不写入 ext(由 Worker 从 video_project 读取),marshalTaskExt 超过 1024 字节即报错。ext 只承载 ID 引用与少量元数据,大对象一律走关联表。

错误到 HTTP 状态码的映射集中在 handler 的 writeTaskCreateError:NotFound 类(素材/项目/提示词不存在)→ 404;ASR 未就绪、重名 → 400。GetTask 轮询接口则直接读库返回全字段,created_by 通过 createdByResolver 批量解析为展示名(nickname 优先)。

📌 设计决策

  • 双路径创建:新路径(live_id + clips0)让前端无需先建项目,把"拆分粒度"决策权收回后端;兼容路径保留 API 早期形态,避免破坏既有调用方。
  • 批量任务原子性妥协 :createAISliceJobs 在循环中逐个建 project + task,中途失败会留下部分任务但返回错误。选择"不回滚"而非事务包裹,因为已成功的任务本身有效,且回滚需要跨 project/task 两表清理。
  • Worker 可为 nil :NewTaskService 注入三个 Worker 均可空,单测场景只验证落库逻辑不真正调度------依赖倒置让调度成为可插拔能力。
  • 列表返回 TaskListItem 而非 Task:列表投影复用快照字段 + ext 原文,避免为分页列表加载完整实体,也与仓储层 SELECT 列裁剪对齐。

代码示例

片段 1 展示推荐路径的核心编排(摘自 task_service.go,节选):

go 复制代码
func (s *taskService) createAISliceJobs(ctx context.Context, createdBy uint, in createAISliceJobsInput) ([]*model.Task, error) {
	if len(in.Clips0) == 0 {
		return nil, errors.New("clips0 不能为空,请先设置待分析时间段")
	}
	if err := prepare.ValidateClipRanges(in.Clips0); err != nil {
		return nil, err
	}
	// 防御一:素材必须 ASR 完成,否则 LLM 没有分句可分析。
	material, err := s.requireASRCompletedMaterial(ctx, in.LiveID)
	if err != nil {
		return nil, err
	}
	// 防御二:系统提示词必须非空,ID 为 0 时回落默认。
	sysPrompt, err := s.resolveSysPrompt(ctx, promptID)
	if err != nil {
		return nil, err
	}
	// 排序合并重叠选区,再按有效 ASR 拆成多个项目(约每 30 分钟一个)。
	merged := sortAndMergeOverlappingClipRanges(in.Clips0)
	utterances := asr.FormatUtterancesForAPI(material.LiveASR)
	groups := splitClips0IntoProjects(merged, utterances, material.ASRParagraphs)
	groups = finalizeAISliceGroups(groups, utterances)
	if err := validateAISliceGroups(groups, utterances); err != nil {
		return nil, err
	}

	tasks := make([]*model.Task, 0, len(groups))
	for i, clips := range groups {
		project := &model.VideoProject{
			Name: names[i], LiveID: in.LiveID, PromptID: promptID,
			Clips0: clips, ProjectSource: in.ProjectSource,
			EnableCaptions: model.EnableCaptionsOn, CreatedBy: createdBy,
		}
		if err := s.videoProjectRepo.Create(ctx, project); err != nil {
			return nil, mapVideoProjectUniqueError(err)
		}
		task := &model.Task{
			Type: in.TaskType, Status: model.TaskStatusPending,
			VideoProjectID: model.NewUintPtr(project.ID),
			VideoProjectName: project.Name,
			LiveURL: material.LiveURL, LiveName: material.Name, // 快照字段
			SysPrompt: sysPrompt, CreatedBy: createdBy, Ext: ext,
		}
		if err := s.taskRepo.Create(ctx, task); err != nil {
			return nil, err
		}
		tasks = append(tasks, task)
	}
	return tasks, nil
}

片段 2 展示 ext 容量护栏与 Worker 唤醒(摘自 task_service.go):

go 复制代码
// TaskExt 写入 task.ext 的结构化元数据。
// 画布尺寸与源视频链接/名称已冗余落在 task.width/height/live_url/live_name,不再写入 ext。
type TaskExt struct {
	LiveID           uint              `json:"live_id,omitempty"`
	VideoProjectID   uint              `json:"video_project_id,omitempty"`
	SysPromptID      uint              `json:"sys_prompt_id,omitempty"`
	TargetDurationMs int64             `json:"target_duration_ms,omitempty"`
	Clips0           []model.ClipRange `json:"clips0,omitempty"`
}

func marshalTaskExt(ext TaskExt) (string, error) {
	raw, err := json.Marshal(ext)
	if err != nil {
		return "", err
	}
	if len(raw) > 1024 {
		return "", errors.New("任务扩展参数过长")
	}
	return string(raw), nil
}

// CreateDraft 尾部:落库后立即唤醒草稿 Worker,轮询兜底。
	if s.draftWorker != nil {
		s.draftWorker.Enqueue()
	}
	return task, nil

小结

任务服务是"薄入口、厚校验"的典型:入口逻辑只做参数翻译与快照组装,真正的业务展开在 Worker 侧。三层创建期校验把失败前移到请求时点,队列里只剩有希望成功的任务。

思考题

  1. createAISliceJobs 循环建 project/task 无事务回滚,若第 3 组失败,前 2 组任务已入队执行------这种"部分成功"语义对前端轮询展示有什么影响?如何向用户呈现?
  2. 若把 SysPrompt 全文写入 task(当前做法)改为只存 SysPromptID 由 Worker 再查库,会引入哪些一致性问题?当前的快照取舍解决了什么?

项目信息

  • GitHub仓库:Chyona/live-mixer
相关推荐
用户2991422196805 小时前
GitHub 热榜项目:日榜(2026-11-10)
人工智能·开源·github
miofly5 小时前
Meta发布小模型Muse Ember,智能体基准多项超GPT-6与Claude Haiku 5.5
开源·github
wflynn6 小时前
GitHub 日榜趋势速报 | 2026-11-09
开源·github
miofly8 小时前
GitHub 日榜趋势速报 | 2026-11-10
开源·github
Yingyingyingyyy8 小时前
GitHub 开源热榜项目-周榜(2026-11-09)
开源·github
孟郎郎20 小时前
打造一切(Craft Anything)——克隆构造世间万物
人工智能·ai·github·软件构建·开源软件
海洋之恒21 小时前
GitHub Actions 自动化:5 个真实运维提效案例
运维·自动化·github
小Z先生1 天前
我用 Go + 系统 WebView 写了个纯本地 Postman:不装 Electron、不连云端
后端·github
fthux1 天前
待办清单写满了,别忘了给休息留个位置:试试 Tick Rest
开源·github·mac·提醒·open source·消息提醒·tick rest·tick rest for mac·消息提醒app