import { Injectable } from '@angular/core'; import { firstValueFrom } from 'rxjs'; import { BatchJob, BatchJobItem, BatchJobRunResult } from '../models/batch-job.model'; import { BatchItemRunner } from './batch-job.service'; import { GenerationTaskService } from './generation-task.service'; import { JimengService, JimengVideoQuality } from './jimeng.service'; import { PipelineSessionService } from './pipeline-session.service'; import { VideoDurationService } from './video-duration.service'; @Injectable({ providedIn: 'root' }) export class TopicToVideoBatchRunnerService { constructor( private generationTasks: GenerationTaskService, private jimeng: JimengService, private sessions: PipelineSessionService, private videoDuration: VideoDurationService, ) {} async run(item: BatchJobItem, job: BatchJob, context?: Parameters[2]): Promise { const input = item.input || {}; const templateConfig = this.safeObject(input['templateConfig']); const topic = String(input['topic'] || item.title || '').trim(); const quality = this.normalizeQuality(templateConfig['clipQuality']); const durationSeconds = Math.max(3, Math.min(15, Math.round(Number(templateConfig['clipDurationSeconds']) || this.videoDuration.framesToSeconds(Number(templateConfig['clipFrames'] || 121))))); const frames = this.videoDuration.secondsToFrames(durationSeconds); const aspectRatio = this.normalizeAspect(templateConfig['aspect']); const task = this.generationTasks.create({ title: topic || '批量主题生视频', pipelineId: job.pipelineId, operation: 'batch-topic-to-video-real', estimatedCredits: 0, steps: [ { id: 'prepare', label: '整理主题' }, { id: 'submit', label: '提交真实生成' }, { id: 'external', label: '等待外部结果' }, { id: 'archive', label: '归档结果' }, ], snapshot: { source: 'batch-production', batchJobId: job.id, batchItemId: item.id, topicId: item.topicId, topic, templateId: input['templateId'] || job.templateId || '', templateConfig, billing: 'disabled', }, }); this.generationTasks.markRunning(task.id, 'submit', 8); const prompt = this.buildPrompt(topic, templateConfig); const result = await firstValueFrom(this.jimeng.remixVideo(prompt, { method: '1', frames, aspectRatio, quality, }, (status, progress, meta) => { if (meta?.['workId']) { const workId = String(meta['workId']); this.generationTasks.markWaitingExternal(task.id, { workId }, 'external', progress); context?.markWaitingExternal({ workId }, 'external'); } else { this.generationTasks.markRunning(task.id, progress >= 90 ? 'archive' : 'external', progress); context?.markRunning(progress >= 90 ? 'archive' : 'external'); } })); const draft = await this.sessions.createArchivedDraft({ pipelineId: job.pipelineId, title: topic || '批量主题生视频', resultUrl: result.videoUrl, status: 'completed', artifactTitle: '批量成片', artifactExtras: { source: 'batch-production', batchJobId: job.id, batchItemId: item.id, generationTaskId: task.id, workId: result.workId, quality, frames, durationSeconds, }, snapshot: { source: 'batch-production', batchJobId: job.id, batchItemId: item.id, generationTaskId: task.id, workId: result.workId, topicId: item.topicId, topic, templateId: input['templateId'] || job.templateId || '', templateConfig, billing: 'disabled', }, }); this.generationTasks.markWaitingExternal(task.id, { workId: result.workId, draftId: draft.id }, 'archive', 96); this.generationTasks.markCompleted(task.id, result.videoUrl); return { generationTaskId: task.id, resultUrl: result.videoUrl, externalTaskIds: { workId: result.workId, draftId: draft.id }, }; } private buildPrompt(topic: string, config: Record): string { const style = String(config['style'] || 'cinematic').trim(); const scenes = Number(config['nScenes'] || 5); const mediaMode = String(config['mediaMode'] || 'video'); const bible = this.safeObject(config['visualBible']); return [ `主题:${topic}`, `生成一个完整的中文短视频画面,竖屏优先,结构约 ${scenes} 个镜头。`, `视觉风格:${style},模式:${mediaMode}。`, bible['subject'] ? `固定主体:${bible['subject']}` : '', bible['product'] ? `产品/卖点:${bible['product']}` : '', bible['location'] ? `场景:${bible['location']}` : '', bible['palette'] ? `色彩:${bible['palette']}` : '', bible['lighting'] ? `光线:${bible['lighting']}` : '', '镜头需要有明确开头、推进和收束,画面连贯,避免字幕、水印、乱码文字和多余 logo。', ].filter(Boolean).join('\n'); } private safeObject(value: any): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value : {}; } private normalizeQuality(value: any): JimengVideoQuality { return value === '1080p' ? '1080p' : value === 'pro' ? 'pro' : '720p'; } private normalizeAspect(value: any): '16:9' | '4:3' | '1:1' | '3:4' | '9:16' | '21:9' { return ['16:9', '4:3', '1:1', '3:4', '9:16', '21:9'].includes(value) ? value : '9:16'; } }