| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139 |
- 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<BatchItemRunner>[2]): Promise<BatchJobRunResult> {
- 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, any>): 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<string, any> {
- 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';
- }
- }
|