返回 AiToEarn
drama-recap.service.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / ai / aideo / drama-recap.service.ts
1 import { Injectable, Logger } from '@nestjs/common'
2 import { AssetsService } from '@yikart/assets'
3 import { AppException, ResponseCode, UserType } from '@yikart/common'
4 import {
5 AideoAiLogResponse,
6 AiLog,
7 AiLogChannel,
8 AiLogRepository,
9 AiLogStatus,
10 AiLogType,
11 AssetType,
12 } from '@yikart/mongodb'
13 import { VolcengineVideoUtils } from '../../agent/mcp/volcengine/volcengine.utils'
14 import {
15 CreateDramaRecapTaskRequest,
16 CreateDramaRecapTaskResponse,
17 DramaRecapTaskStatus,
18 QueryDramaRecapTaskResponse,
19 VolcengineService,
20 } from '../libs/volcengine'
21 import { UserGetDramaRecapTaskRequest, UserSubmitDramaRecapTaskRequest } from './aideo.dto'
22
23 /**
24 * 短剧解说服务
25 * 负责短剧解说任务的提交、查询和计费
26 */
27 @Injectable()
28 export class DramaRecapService {
29 private readonly logger = new Logger(DramaRecapService.name)
30
31 constructor(
32 private readonly volcengineService: VolcengineService,
33 private readonly aiLogRepo: AiLogRepository,
34 private readonly assetsService: AssetsService,
35 ) { }
36
37 /**
38 * 提交短剧解说任务
39 */
40 async submitDramaRecapTask(
41 request: UserSubmitDramaRecapTaskRequest,
42 ): Promise<{ taskId: string, dramaScriptTaskId: string }> {
43 const {
44 userId,
45 userType,
46 vids,
47 dramaScriptTaskId,
48 recapText,
49 speakerConfig,
50 isEraseSubtitle = true,
51 fontConfig,
52 recapStyle,
53 recapTextSpeed = 1.2,
54 recapTextLength,
55 pauseTime = 120,
56 allowRepeatMatch = false,
57 } = request
58
59 this.logger.log({ userId, vids, dramaScriptTaskId }, '[DramaRecap] 提交任务')
60
61 const startedAt = new Date()
62
63 // 1. 处理输入视频(如果是 URL,先上传获取 VID)
64 const processedVids = await Promise.all(vids.map(async (videoInput) => {
65 let vid: string
66 if (videoInput.startsWith('vid://')) {
67 vid = videoInput.replace('vid://', '')
68 }
69 else if (videoInput.startsWith('http://') || videoInput.startsWith('https://')) {
70 vid = await this.volcengineService.downloadUrlAndUploadAsStream(videoInput)
71 }
72 else {
73 // 假设直接传入的就是 VID
74 vid = videoInput
75 }
76 return vid
77 }))
78
79 // 构建请求参数
80 const apiRequest: CreateDramaRecapTaskRequest = {
81 SpaceName: this.volcengineService.getSpaceName(),
82 Vids: processedVids,
83 DramaScriptTaskId: dramaScriptTaskId,
84 // 短剧解说配置
85 DramaRecapConfig: {
86 // 如果提供了自定义解说词,使用它;否则启用自动生成
87 ...(recapText ? { RecapText: recapText } : { AutoGenerateRecapText: true }),
88 ...(recapStyle && { RecapStyle: recapStyle }),
89 ...(recapTextSpeed !== undefined && { RecapTextSpeed: recapTextSpeed }),
90 ...(recapTextLength !== undefined && { RecapTextLength: recapTextLength }),
91 ...(pauseTime !== undefined && { PauseTime: pauseTime }),
92 ...(allowRepeatMatch !== undefined && { AllowRepeatMatch: allowRepeatMatch }),
93 },
94 ...(speakerConfig && {
95 SpeakerConfig: {
96 AppId: speakerConfig.appId,
97 Cluster: speakerConfig.cluster,
98 VoiceType: speakerConfig.voiceType,
99 },
100 }),
101 IsEraseSubtitle: isEraseSubtitle,
102 ...(fontConfig && {
103 FontConfig: {
104 Color: fontConfig.color,
105 Size: fontConfig.size,
106 Name: fontConfig.name,
107 },
108 }),
109 }
110
111 // 提交任务
112 const response: CreateDramaRecapTaskResponse = await this.volcengineService.createDramaRecapTask(apiRequest)
113
114 const { TaskId: volcengineTaskId, DramaScriptTaskId: responseDramaScriptTaskId } = response
115
116 this.logger.log({ volcengineTaskId, dramaScriptTaskId: responseDramaScriptTaskId }, '[DramaRecap] 任务提交成功')
117
118 const aiLog = await this.aiLogRepo.create({
119 userId,
120 userType: userType as UserType,
121 taskId: volcengineTaskId,
122 model: 'drama-recap',
123 channel: AiLogChannel.Volcengine,
124 startedAt,
125 type: AiLogType.Aideo,
126 request: {
127 vids,
128 dramaScriptTaskId,
129 recapText,
130 speakerConfig,
131 isEraseSubtitle,
132 fontConfig,
133 recapStyle,
134 recapTextSpeed,
135 recapTextLength,
136 pauseTime,
137 allowRepeatMatch,
138 },
139 response: { taskId: volcengineTaskId, dramaScriptTaskId: responseDramaScriptTaskId },
140 status: AiLogStatus.Generating,
141 })
142
143 return { taskId: aiLog.id, dramaScriptTaskId: responseDramaScriptTaskId }
144 }
145
146 /**
147 * 获取短剧解说任务结果(用户主动查询接口)
148 * 注意:定时任务会自动处理任务状态,此方法主要用于用户主动查询
149 */
150 async getDramaRecapTask(
151 request: UserGetDramaRecapTaskRequest,
152 ): Promise<{
153 taskId: string
154 status: DramaRecapTaskStatus
155 outputVid?: string
156 outputUrl?: string
157 errorMessage?: string
158 }> {
159 const { taskId } = request
160
161 this.logger.log({ taskId }, '[DramaRecap] 查询任务状态')
162
163 // 从数据库查询任务记录(taskId 是数据库记录的 id,不是火山引擎的 TaskId)
164 const log = await this.aiLogRepo.getById(taskId)
165
166 if (!log || log.model !== 'drama-recap') {
167 throw new AppException(ResponseCode.InvalidAiTaskId)
168 }
169
170 // 获取火山引擎的任务 ID
171 const volcengineTaskId = log.taskId
172 if (!volcengineTaskId) {
173 throw new AppException(ResponseCode.InvalidAiTaskId)
174 }
175
176 // 如果任务已完成或失败,直接返回数据库中的结果
177 if (log.status === AiLogStatus.Success) {
178 const response = log.response as AideoAiLogResponse | undefined
179 return {
180 taskId,
181 status: DramaRecapTaskStatus.Completed,
182 outputVid: response?.outputVid,
183 outputUrl: response?.outputUrl,
184 }
185 }
186
187 if (log.status === AiLogStatus.Failed) {
188 const response = log.response as AideoAiLogResponse | undefined
189 return {
190 taskId,
191 status: DramaRecapTaskStatus.Failed,
192 errorMessage: log.errorMessage || response?.errorMessage || '任务执行失败',
193 }
194 }
195
196 // 查询火山引擎任务状态
197 const result: QueryDramaRecapTaskResponse = await this.volcengineService.getDramaRecapTask({
198 TaskId: volcengineTaskId,
199 SpaceName: this.volcengineService.getSpaceName(),
200 })
201
202 // 如果任务已完成,触发处理逻辑
203 if (result.Status === DramaRecapTaskStatus.Completed || result.Status === DramaRecapTaskStatus.Failed) {
204 // 调用 processDramaRecapTask 处理任务(更新数据库、下载视频、扣费等)
205 await this.processDramaRecapTask(log, result)
206
207 // 重新查询数据库获取最新状态
208 const updatedLog = await this.aiLogRepo.getById(log.id)
209 if (updatedLog) {
210 const response = updatedLog.response as AideoAiLogResponse | undefined
211 return {
212 taskId,
213 status: updatedLog.status === AiLogStatus.Success ? DramaRecapTaskStatus.Completed : DramaRecapTaskStatus.Failed,
214 outputVid: response?.outputVid,
215 outputUrl: response?.outputUrl,
216 errorMessage: updatedLog.errorMessage,
217 }
218 }
219 }
220
221 return {
222 taskId,
223 status: result.Status,
224 }
225 }
226
227 /**
228 * 处理短剧解说任务结果(下载视频、上传 S3、计费)
229 */
230 async processDramaRecapTask(task: AiLog, result: QueryDramaRecapTaskResponse): Promise<void> {
231 if (result.Status === DramaRecapTaskStatus.Completed && result.Vid) {
232 this.logger.log({ Vid: result.Vid }, '[DramaRecap] 开始下载视频并上传到 S3')
233
234 // 下载视频并上传到 S3
235 const s3Url = await this.saveVideoFromVid(result.Vid, task, 'drama-recap')
236
237 // 更新响应数据
238 const response = task.response as AideoAiLogResponse | undefined
239 const updatedResponse = {
240 ...(response || {}),
241 outputVid: result.Vid,
242 outputUrl: s3Url,
243 }
244
245 await this.aiLogRepo.updateById(task.id, {
246 status: AiLogStatus.Success,
247 finishedAt: new Date(),
248 response: updatedResponse,
249 })
250
251 this.logger.log({ taskId: task.taskId, outputVid: result.Vid }, '[DramaRecap] 任务处理完成')
252 }
253 else if (result.Status === DramaRecapTaskStatus.Failed) {
254 // 更新任务状态为失败
255 await this.aiLogRepo.updateById(task.id, {
256 status: AiLogStatus.Failed,
257 finishedAt: new Date(),
258 response: { ...result, errorMessage: result.ErrorMessage },
259 errorMessage: result.ErrorMessage,
260 })
261
262 this.logger.error({ taskId: task.taskId, errorMessage: result.ErrorMessage }, '[DramaRecap] 任务失败')
263 }
264 }
265
266 /**
267 * 从 VID 获取视频并上传
268 */
269 private async saveVideoFromVid(
270 vid: string,
271 task: AiLog,
272 filenamePrefix: string,
273 ): Promise<string | undefined> {
274 return VolcengineVideoUtils.saveVideoFromVid(
275 vid,
276 task.userId,
277 `${task.id}-${filenamePrefix}`,
278 task.model || 'drama-recap',
279 this.volcengineService,
280 this.assetsService,
281 this.logger,
282 AssetType.AideoOutput,
283 )
284 }
285 }
286
286 lines TYPESCRIPT