返回 AiToEarn
relay-video.service.ts
根目录 / project / aitoearn-backend / apps / aitoearn-ai / src / core / ai / video / relay / relay-video.service.ts
1 import type { RelayVideoCallbackDto, RelayVideoGenerationRequest, RelayVideoSubmitResponse } from '../../libs/relay/relay.interface'
2 import type { RelayVideoAiLog } from '../video-ai-log.interface'
3 import type { UserVideoGenerationRequestDto } from '../video.dto'
4 import type { VideoTaskInput } from '../video.vo'
5 import { Injectable, Optional } from '@nestjs/common'
6 import { AppException, FileUtil, ResponseCode, UserType } from '@yikart/common'
7 import { AiLogChannel, AiLogRepository, AiLogStatus, AiLogType } from '@yikart/mongodb'
8 import { TaskStatus } from '../../../../common'
9 import { AiAvailabilityService } from '../../../ai-availability/ai-availability.service'
10 import { RelayLibService } from '../../libs/relay'
11 import { ModelsConfigService } from '../../models-config'
12 import { RelayMediaResolverService } from '../../relay-media'
13
14 @Injectable()
15 export class RelayVideoService {
16 constructor(
17 private readonly relayLibService: RelayLibService,
18 private readonly aiLogRepo: AiLogRepository,
19 private readonly modelsConfigService: ModelsConfigService,
20 private readonly aiAvailability: AiAvailabilityService,
21 @Optional() private readonly relayMediaResolver?: RelayMediaResolverService,
22 ) {}
23
24 private async resolveRelayJson<T>(value: T): Promise<T> {
25 if (!this.relayMediaResolver) {
26 return value
27 }
28 return await this.relayMediaResolver.resolveJson(value)
29 }
30
31 async createFromRequest(request: UserVideoGenerationRequestDto): Promise<{ id: string }> {
32 const modelConfig = this.modelsConfigService.config.video.generation.find(m => m.name === request.model)
33 if (!modelConfig) {
34 throw new AppException(ResponseCode.InvalidModel)
35 }
36 if (request.mode && !(modelConfig.modes as readonly string[]).includes(request.mode)) {
37 throw new AppException(ResponseCode.InvalidModel)
38 }
39 const startedAt = new Date()
40
41 const payload = { ...request } as RelayVideoGenerationRequest & { userId?: string, userType?: UserType, groupId?: string }
42 delete payload.userId
43 delete payload.userType
44 delete payload.groupId
45
46 const relayPayload = await this.resolveRelayJson(payload)
47
48 const result = await this.aiAvailability.executeAsync<RelayVideoSubmitResponse>(
49 { provider: 'relay', operation: 'videoGeneration', model: request.model },
50 () => this.relayLibService.createVideo(relayPayload),
51 response => response.id || '',
52 )
53
54 const remoteTaskId = result.id
55 if (!remoteTaskId) {
56 throw new AppException(ResponseCode.AiCallFailed, { error: 'Relay task id is missing' })
57 }
58
59 const logRequest: RelayVideoAiLog['request'] = {
60 ...payload,
61 remoteTaskId,
62 }
63 if (request.groupId) {
64 logRequest.groupId = request.groupId
65 }
66
67 const aiLog = await this.aiLogRepo.create({
68 userId: request.userId,
69 userType: request.userType,
70 taskId: remoteTaskId,
71 model: request.model,
72 channel: AiLogChannel.Relay,
73 startedAt,
74 type: AiLogType.Video,
75 request: logRequest,
76 response: {
77 ...result,
78 },
79 status: AiLogStatus.Generating,
80 })
81
82 return { id: aiLog.id }
83 }
84
85 extractInput(request: RelayVideoAiLog['request']): VideoTaskInput {
86 return {
87 prompt: request.prompt || '',
88 groupId: request.groupId,
89 image: request.image,
90 images: request.images,
91 videoUrl: request.video_url,
92 videos: request.videos,
93 audios: request.audios,
94 duration: request.duration,
95 resolution: request.resolution,
96 aspectRatio: request.ratio || (request.metadata?.['aspectRatio'] as string | undefined),
97 watermark: request.watermark,
98 }
99 }
100
101 getTaskResult(result: RelayVideoCallbackDto) {
102 const status = this.normalizeStatus(result.status)
103 const errorMessage = typeof result.error === 'string'
104 ? result.error
105 : result.error?.message
106
107 return {
108 status,
109 videoUrl: result.videoUrl ? FileUtil.buildUrl(result.videoUrl) : undefined,
110 coverUrl: result.coverUrl ? FileUtil.buildUrl(result.coverUrl) : undefined,
111 error: errorMessage ? { message: errorMessage } : undefined,
112 }
113 }
114
115 private normalizeStatus(status: string | undefined): TaskStatus {
116 if (!status) {
117 return TaskStatus.InProgress
118 }
119 const normalized = status.toLowerCase()
120 if (['success', 'succeeded', 'completed', 'succeed'].includes(normalized)) {
121 return TaskStatus.Success
122 }
123 if (['failed', 'error', 'failure'].includes(normalized)) {
124 return TaskStatus.Failure
125 }
126 return TaskStatus.InProgress
127 }
128
129 async callback(result: RelayVideoCallbackDto): Promise<RelayVideoCallbackDto> {
130 const taskId = result.id
131 if (!taskId) {
132 throw new AppException(ResponseCode.InvalidAiTaskId)
133 }
134 const aiLog = await this.aiLogRepo.getByTaskId(taskId)
135 if (!aiLog || aiLog.channel !== AiLogChannel.Relay) {
136 throw new AppException(ResponseCode.InvalidAiTaskId)
137 }
138 const relayAiLog = aiLog as RelayVideoAiLog
139
140 if (relayAiLog.status !== AiLogStatus.Generating) {
141 return relayAiLog.response!
142 }
143
144 const status = this.normalizeStatus(result.status)
145 if (status === TaskStatus.InProgress) {
146 return { ...result, id: taskId }
147 }
148
149 const elapsedMs = Date.now() - relayAiLog.startedAt.getTime()
150
151 if (status === TaskStatus.Success && result.videoUrl) {
152 const callbackData: RelayVideoCallbackDto = {
153 ...result,
154 id: taskId,
155 status: 'success',
156 }
157
158 const updatedAiLog = await this.aiLogRepo.updateByIdAndStatus(
159 relayAiLog.id,
160 AiLogStatus.Generating,
161 {
162 $set: {
163 status: AiLogStatus.Success,
164 response: callbackData,
165 duration: elapsedMs,
166 },
167 },
168 )
169
170 if (!updatedAiLog) {
171 return relayAiLog.response!
172 }
173
174 await this.aiAvailability.recordAsyncComplete(
175 taskId,
176 { provider: 'relay', operation: 'videoGeneration', model: relayAiLog.model },
177 { success: true, latencyMs: elapsedMs },
178 )
179
180 return callbackData
181 }
182
183 const errorMessage = (typeof result.error === 'string' ? result.error : result.error?.message)
184 || (status === TaskStatus.Success ? 'Relay task completed but no video URL returned' : `Relay task ${result.status}`)
185 return this.failTask(relayAiLog, taskId, { ...result, id: taskId }, errorMessage, elapsedMs)
186 }
187
188 private async failTask(aiLog: RelayVideoAiLog, taskId: string, callbackData: RelayVideoCallbackDto, errorMessage: string, elapsedMs: number): Promise<RelayVideoCallbackDto> {
189 const updatedAiLog = await this.aiLogRepo.updateByIdAndStatus(
190 aiLog.id,
191 AiLogStatus.Generating,
192 {
193 $set: {
194 status: AiLogStatus.Failed,
195 response: callbackData,
196 duration: elapsedMs,
197 errorMessage,
198 },
199 },
200 )
201
202 if (!updatedAiLog) {
203 return aiLog.response!
204 }
205
206 await this.aiAvailability.recordAsyncComplete(
207 taskId,
208 { provider: 'relay', operation: 'videoGeneration', model: aiLog.model },
209 { success: false, latencyMs: elapsedMs, errorMessage },
210 )
211
212 return callbackData
213 }
214 }
215
215 lines TYPESCRIPT