| 1 | import type { OpenApiResponse } from '@volcengine/openapi/lib/base/types' |
| 2 | import type { VodService } from '@volcengine/openapi/lib/services/vod' |
| 3 | import type { |
| 4 | VodCommitUploadInfoResult, |
| 5 | VodGetMediaInfosRequest, |
| 6 | VodGetMediaInfosResult, |
| 7 | VodGetPlayInfoRequest, |
| 8 | VodGetPlayInfoResult, |
| 9 | VodQueryUploadTaskInfoRequest, |
| 10 | VodQueryUploadTaskInfoResult, |
| 11 | VodUploadMaterialRequest, |
| 12 | VodUploadMediaByUrlRequest, |
| 13 | VodUploadMediaByUrlResult, |
| 14 | } from '@volcengine/openapi/lib/services/vod/types' |
| 15 | import type { |
| 16 | AsyncVCreativeTaskRequest, |
| 17 | AsyncVCreativeTaskResponse, |
| 18 | CreateDramaRecapTaskRequest, |
| 19 | CreateDramaRecapTaskResponse, |
| 20 | CreateVideoGenerationTaskRequest, |
| 21 | GetAideoTaskResultRequest, |
| 22 | GetAideoTaskResultResponse, |
| 23 | GetDirectEditResultItem, |
| 24 | GetDirectEditResultRequest, |
| 25 | GetVCreativeTaskResultRequest, |
| 26 | GetVCreativeTaskResultResponse, |
| 27 | GetVideoGenerationTaskResponse, |
| 28 | MultiInput, |
| 29 | QueryDramaRecapTaskRequest, |
| 30 | QueryDramaRecapTaskResponse, |
| 31 | SkillParams, |
| 32 | SubmitAideoTaskAsyncResponse, |
| 33 | SubmitDirectEditTaskAsyncRequest, |
| 34 | SubmitDirectEditTaskAsyncResponse, |
| 35 | VideoInput, |
| 36 | VideoUrlInput, |
| 37 | } from './volcengine.interface' |
| 38 | import { Injectable, Logger } from '@nestjs/common' |
| 39 | import { vodOpenapi } from '@volcengine/openapi' |
| 40 | import { AppException, ResponseCode } from '@yikart/common' |
| 41 | import { AiAvailabilityService } from '../../../ai-availability' |
| 42 | import { AideoService } from './services/aideo.service' |
| 43 | import { DirectEditService } from './services/direct-edit.service' |
| 44 | import { DramaRecapService } from './services/drama-recap.service' |
| 45 | import { MediaService } from './services/media.service' |
| 46 | import { UploadService } from './services/upload.service' |
| 47 | import { VCreativeService } from './services/vcreative.service' |
| 48 | import { VideoGenService } from './services/video-gen.service' |
| 49 | import { VolcengineConfig } from './volcengine.config' |
| 50 | import { SkillType } from './volcengine.interface' |
| 51 | |
| 52 | /** |
| 53 | * Volcengine 服务 Facade |
| 54 | * 提供统一的 API 访问入口,委托到各专门服务 |
| 55 | * 保持向后兼容:所有原有方法签名保持不变 |
| 56 | */ |
| 57 | @Injectable() |
| 58 | export class VolcengineService { |
| 59 | private readonly logger = new Logger(VolcengineService.name) |
| 60 | private readonly vodService: VodService |
| 61 | |
| 62 | constructor( |
| 63 | private readonly config: VolcengineConfig, |
| 64 | private readonly uploadService: UploadService, |
| 65 | private readonly mediaService: MediaService, |
| 66 | private readonly videoGenService: VideoGenService, |
| 67 | private readonly aideoService: AideoService, |
| 68 | private readonly vCreativeService: VCreativeService, |
| 69 | private readonly directEditService: DirectEditService, |
| 70 | private readonly dramaRecapService: DramaRecapService, |
| 71 | private readonly aiAvailability: AiAvailabilityService, |
| 72 | ) { |
| 73 | this.vodService = this.createVodService() |
| 74 | } |
| 75 | |
| 76 | private async withAvailability<T>(operation: string, fn: () => Promise<T>, model?: string): Promise<T> { |
| 77 | return this.aiAvailability.execute( |
| 78 | { provider: 'volcengine', operation, model }, |
| 79 | fn, |
| 80 | ) |
| 81 | } |
| 82 | |
| 83 | // ========== 配置方法 ========== |
| 84 | |
| 85 | /** |
| 86 | * 获取播放基础 URL |
| 87 | */ |
| 88 | getPlaybackBaseUrl(): string { |
| 89 | return this.config.playbackBaseUrl |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * 获取空间名称 |
| 94 | */ |
| 95 | getSpaceName(): string { |
| 96 | return this.config.spaceName |
| 97 | } |
| 98 | |
| 99 | // ========== 上传服务 (委托到 VolcengineUploadService) ========== |
| 100 | |
| 101 | /** |
| 102 | * URL 批量拉取上传 |
| 103 | */ |
| 104 | async uploadMediaByUrl( |
| 105 | request: VodUploadMediaByUrlRequest, |
| 106 | ): Promise<VodUploadMediaByUrlResult> { |
| 107 | return this.withAvailability('uploadMediaByUrl', async () => this.uploadService.uploadMediaByUrl(request)) |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * 流式上传 |
| 112 | */ |
| 113 | async uploadMaterial( |
| 114 | request: VodUploadMaterialRequest, |
| 115 | ): Promise<VodCommitUploadInfoResult> { |
| 116 | return this.withAvailability('uploadMaterial', async () => this.uploadService.uploadMaterial(request)) |
| 117 | } |
| 118 | |
| 119 | /** |
| 120 | * 查询上传任务状态 |
| 121 | * @deprecated 使用 getUploadTaskInfo 代替 |
| 122 | */ |
| 123 | async queryUploadTaskInfo( |
| 124 | request: VodQueryUploadTaskInfoRequest, |
| 125 | ): Promise<VodQueryUploadTaskInfoResult> { |
| 126 | return this.withAvailability('queryUploadTaskInfo', async () => this.uploadService.getUploadTaskInfo(request)) |
| 127 | } |
| 128 | |
| 129 | /** |
| 130 | * 获取上传任务状态 |
| 131 | */ |
| 132 | async getUploadTaskInfo( |
| 133 | request: VodQueryUploadTaskInfoRequest, |
| 134 | ): Promise<VodQueryUploadTaskInfoResult> { |
| 135 | return this.withAvailability('getUploadTaskInfo', async () => this.uploadService.getUploadTaskInfo(request)) |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * 下载 URL 并使用 stream 方式上传 |
| 140 | */ |
| 141 | async downloadUrlAndUploadAsStream( |
| 142 | url: string, |
| 143 | options?: VideoUrlInput, |
| 144 | ): Promise<string> { |
| 145 | return this.withAvailability('downloadUrlAndUploadAsStream', async () => this.uploadService.downloadUrlAndUploadAsStream(url, options)) |
| 146 | } |
| 147 | |
| 148 | // ========== 媒资服务 (委托到 VolcengineMediaService) ========== |
| 149 | |
| 150 | /** |
| 151 | * 获取媒资信息 |
| 152 | */ |
| 153 | async getMediaInfos( |
| 154 | request: VodGetMediaInfosRequest, |
| 155 | ): Promise<VodGetMediaInfosResult> { |
| 156 | return this.withAvailability('getMediaInfos', async () => this.mediaService.getMediaInfos(request)) |
| 157 | } |
| 158 | |
| 159 | /** |
| 160 | * 获取播放信息 |
| 161 | */ |
| 162 | async getPlayInfo( |
| 163 | request: VodGetPlayInfoRequest, |
| 164 | ): Promise<VodGetPlayInfoResult> { |
| 165 | return this.withAvailability('getPlayInfo', async () => this.mediaService.getPlayInfo(request)) |
| 166 | } |
| 167 | |
| 168 | /** |
| 169 | * 构建带鉴权的播放URL |
| 170 | */ |
| 171 | async buildAuthenticatedPlayUrl(uri: string): Promise<string> { |
| 172 | return this.withAvailability('buildAuthenticatedPlayUrl', async () => this.mediaService.buildAuthenticatedPlayUrl(uri)) |
| 173 | } |
| 174 | |
| 175 | // ========== 视频生成服务 (委托到 VolcengineVideoGenService) ========== |
| 176 | |
| 177 | /** |
| 178 | * 创建视频生成任务 |
| 179 | */ |
| 180 | async createVideoGenerationTask( |
| 181 | request: CreateVideoGenerationTaskRequest, |
| 182 | ) { |
| 183 | return this.withAvailability('createVideoGenerationTask', async () => this.videoGenService.createVideoGenerationTask(request)) |
| 184 | } |
| 185 | |
| 186 | /** |
| 187 | * 查询视频生成任务 |
| 188 | */ |
| 189 | async getVideoGenerationTask( |
| 190 | taskId: string, |
| 191 | ): Promise<GetVideoGenerationTaskResponse> { |
| 192 | return this.withAvailability('getVideoGenerationTask', async () => this.videoGenService.getVideoGenerationTask(taskId)) |
| 193 | } |
| 194 | |
| 195 | /** |
| 196 | * 取消或删除视频生成任务 |
| 197 | */ |
| 198 | async deleteVideoGenerationTask( |
| 199 | taskId: string, |
| 200 | ) { |
| 201 | return this.withAvailability('deleteVideoGenerationTask', async () => this.videoGenService.deleteVideoGenerationTask(taskId)) |
| 202 | } |
| 203 | |
| 204 | // ========== Aideo 服务 (委托到 VolcengineAideoService) ========== |
| 205 | |
| 206 | /** |
| 207 | * 提交异步智能视频处理任务 |
| 208 | */ |
| 209 | async submitAideoTaskAsync( |
| 210 | request: |
| 211 | | { |
| 212 | SpaceName?: string |
| 213 | MultiInputs: MultiInput[] |
| 214 | Prompt: string |
| 215 | } |
| 216 | | { |
| 217 | SpaceName?: string |
| 218 | MultiInputs: MultiInput[] |
| 219 | SkillType: SkillType |
| 220 | SkillParams?: SkillParams |
| 221 | }, |
| 222 | ): Promise<SubmitAideoTaskAsyncResponse> { |
| 223 | return this.withAvailability('submitAideoTaskAsync', async () => this.aideoService.submitAideoTaskAsync(request)) |
| 224 | } |
| 225 | |
| 226 | /** |
| 227 | * 获取异步智能视频处理任务结果 |
| 228 | */ |
| 229 | async getAideoTaskResult( |
| 230 | request: GetAideoTaskResultRequest, |
| 231 | ): Promise<GetAideoTaskResultResponse> { |
| 232 | return this.withAvailability('getAideoTaskResult', async () => this.aideoService.getAideoTaskResult(request)) |
| 233 | } |
| 234 | |
| 235 | /** |
| 236 | * 智能视频上传并提交异步智能视频处理任务 |
| 237 | */ |
| 238 | async submitAideoTaskAsyncWithUpload( |
| 239 | request: |
| 240 | | { |
| 241 | SpaceName?: string |
| 242 | MultiInputs: VideoInput[] |
| 243 | Prompt: string |
| 244 | } |
| 245 | | { |
| 246 | SpaceName?: string |
| 247 | MultiInputs: VideoInput[] |
| 248 | SkillType: SkillType |
| 249 | SkillParams?: SkillParams |
| 250 | }, |
| 251 | ): Promise<SubmitAideoTaskAsyncResponse & { vids: string[] }> { |
| 252 | return this.withAvailability('submitAideoTaskAsyncWithUpload', async () => this.aideoService.submitAideoTaskAsyncWithUpload(request)) |
| 253 | } |
| 254 | |
| 255 | // ========== VCreative 服务 (委托到 VolcengineVCreativeService) ========== |
| 256 | |
| 257 | /** |
| 258 | * 提交 AI 漫剧转绘任务 |
| 259 | */ |
| 260 | async asyncVCreativeTask( |
| 261 | request: AsyncVCreativeTaskRequest, |
| 262 | ): Promise<AsyncVCreativeTaskResponse> { |
| 263 | return this.withAvailability('asyncVCreativeTask', async () => this.vCreativeService.asyncVCreativeTask(request)) |
| 264 | } |
| 265 | |
| 266 | /** |
| 267 | * 获取 AI 漫剧转绘任务结果 |
| 268 | */ |
| 269 | async getVCreativeTaskResult( |
| 270 | request: GetVCreativeTaskResultRequest, |
| 271 | ): Promise<GetVCreativeTaskResultResponse> { |
| 272 | return this.withAvailability('getVCreativeTaskResult', async () => this.vCreativeService.getVCreativeTaskResult(request)) |
| 273 | } |
| 274 | |
| 275 | // ========== DirectEdit 服务 (委托到 VolcengineDirectEditService) ========== |
| 276 | |
| 277 | /** |
| 278 | * 提交异步剪辑任务 |
| 279 | */ |
| 280 | async submitDirectEditTaskAsync( |
| 281 | request: SubmitDirectEditTaskAsyncRequest, |
| 282 | ): Promise<SubmitDirectEditTaskAsyncResponse> { |
| 283 | return this.withAvailability('submitDirectEditTaskAsync', async () => this.directEditService.submitDirectEditTaskAsync(request)) |
| 284 | } |
| 285 | |
| 286 | /** |
| 287 | * 获取剪辑任务结果 |
| 288 | */ |
| 289 | async getDirectEditResult( |
| 290 | request: GetDirectEditResultRequest, |
| 291 | ): Promise<GetDirectEditResultItem> { |
| 292 | return this.withAvailability('getDirectEditResult', async () => this.directEditService.getDirectEditResult(request)) |
| 293 | } |
| 294 | |
| 295 | // ========== DramaRecap 服务 (委托到 VolcengineDramaRecapService) ========== |
| 296 | |
| 297 | /** |
| 298 | * 创建短剧解说任务 |
| 299 | */ |
| 300 | async createDramaRecapTask( |
| 301 | request: CreateDramaRecapTaskRequest, |
| 302 | ): Promise<CreateDramaRecapTaskResponse> { |
| 303 | return this.withAvailability('createDramaRecapTask', async () => this.dramaRecapService.createDramaRecapTask(request)) |
| 304 | } |
| 305 | |
| 306 | /** |
| 307 | * 查询短剧解说任务结果 |
| 308 | * @deprecated 使用 getDramaRecapTask 代替 |
| 309 | */ |
| 310 | async queryDramaRecapTask( |
| 311 | request: QueryDramaRecapTaskRequest, |
| 312 | ): Promise<QueryDramaRecapTaskResponse> { |
| 313 | return this.withAvailability('queryDramaRecapTask', async () => this.dramaRecapService.getDramaRecapTask(request)) |
| 314 | } |
| 315 | |
| 316 | /** |
| 317 | * 获取短剧解说任务结果 |
| 318 | */ |
| 319 | async getDramaRecapTask( |
| 320 | request: QueryDramaRecapTaskRequest, |
| 321 | ): Promise<QueryDramaRecapTaskResponse> { |
| 322 | return this.withAvailability('getDramaRecapTask', async () => this.dramaRecapService.getDramaRecapTask(request)) |
| 323 | } |
| 324 | |
| 325 | // ========== 内部辅助方法 ========== |
| 326 | |
| 327 | /** |
| 328 | * 创建视频点播服务实例 |
| 329 | */ |
| 330 | private createVodService(): VodService { |
| 331 | return new vodOpenapi.VodService({ |
| 332 | accessKeyId: this.config.accessKeyId, |
| 333 | secretKey: this.config.secretAccessKey, |
| 334 | serviceName: 'vod', |
| 335 | }) |
| 336 | } |
| 337 | |
| 338 | /** |
| 339 | * 检查 API 响应中的错误并抛出异常(内部方法) |
| 340 | */ |
| 341 | private checkApiResponseError<T>( |
| 342 | response: OpenApiResponse<T>, |
| 343 | operation: string, |
| 344 | requestData?: unknown, |
| 345 | ): asserts response is OpenApiResponse<T> & { Result: T } { |
| 346 | if (response.ResponseMetadata?.Error) { |
| 347 | const error = response.ResponseMetadata.Error |
| 348 | this.logger.error( |
| 349 | { error, requestData }, |
| 350 | `${operation} failed: ${error.Code || 'Unknown'} - ${error.Message}`, |
| 351 | ) |
| 352 | throw new AppException(ResponseCode.AiCallFailed, { |
| 353 | code: error.Code || 'Unknown', |
| 354 | message: error.Message, |
| 355 | }) |
| 356 | } |
| 357 | |
| 358 | if (!response.Result) { |
| 359 | this.logger.error({ requestData }, `${operation} returned no result`) |
| 360 | throw new AppException(ResponseCode.AiCallFailed, { |
| 361 | message: typeof response === 'string' ? response : 'No result returned', |
| 362 | }) |
| 363 | } |
| 364 | } |
| 365 | } |
| 366 |