@telepat/rilo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/index.js +1 -0
- package/models/black-forest-labs__flux-2-pro.json +78 -0
- package/models/black-forest-labs__flux-schnell.json +95 -0
- package/models/bytedance__seedream-4.json +71 -0
- package/models/deepseek-ai__deepseek-v3.json +61 -0
- package/models/google__nano-banana-pro.json +92 -0
- package/models/google__veo-3.1-fast.json +93 -0
- package/models/google__veo-3.1.json +93 -0
- package/models/jaaari__kokoro-82m.json +86 -0
- package/models/kwaivgi__kling-v3-video.json +101 -0
- package/models/minimax__speech-02-turbo.json +141 -0
- package/models/pixverse__pixverse-v5.6.json +113 -0
- package/models/prunaai__z-image-turbo.json +107 -0
- package/models/resemble-ai__chatterbox-turbo.json +102 -0
- package/models/wan-video__wan-2.2-i2v-fast.json +139 -0
- package/package.json +67 -0
- package/src/api/firebaseFunction.js +46 -0
- package/src/api/middleware/auth.js +70 -0
- package/src/api/openapi/generateOpenApi.js +21 -0
- package/src/api/openapi/spec.js +831 -0
- package/src/api/routes/jobs.js +45 -0
- package/src/api/routes/projectAssets.js +63 -0
- package/src/api/routes/projects.js +647 -0
- package/src/api/routes/webhooks.js +13 -0
- package/src/api/server.js +88 -0
- package/src/backends/firebaseClient.js +57 -0
- package/src/backends/outputBackend.js +186 -0
- package/src/backends/projectMetadataBackend.js +550 -0
- package/src/cli/commands/openHome.js +70 -0
- package/src/cli/commands/settingsFlow.js +196 -0
- package/src/cli/index.js +192 -0
- package/src/config/env.js +158 -0
- package/src/config/keystore.js +175 -0
- package/src/config/models.js +281 -0
- package/src/config/settingsSchema.js +214 -0
- package/src/media/ffmpeg.js +144 -0
- package/src/media/files.js +77 -0
- package/src/media/subtitles.js +444 -0
- package/src/observability/apiTrace.js +17 -0
- package/src/observability/logger.js +7 -0
- package/src/observability/metrics.js +10 -0
- package/src/pipeline/inputSanitizer.js +6 -0
- package/src/pipeline/orchestrator.js +1669 -0
- package/src/policy/contentGuardrails.js +30 -0
- package/src/providers/predictions.js +188 -0
- package/src/providers/replicateClient.js +12 -0
- package/src/steps/alignSubtitles.js +156 -0
- package/src/steps/burnInSubtitles.js +22 -0
- package/src/steps/composeFinalVideo.js +57 -0
- package/src/steps/generateKeyframes.js +70 -0
- package/src/steps/generateVideoSegments.js +95 -0
- package/src/steps/generateVoiceover.js +128 -0
- package/src/steps/imageToVideoAdapters.js +100 -0
- package/src/steps/script.js +177 -0
- package/src/steps/textToImageAdapters.js +87 -0
- package/src/store/assetStore.js +5 -0
- package/src/store/jobStore.js +102 -0
- package/src/store/projectAnalyticsStore.js +625 -0
- package/src/store/projectStore.js +684 -0
- package/src/store/settingsStore.js +155 -0
- package/src/store/staleAssetStore.js +63 -0
- package/src/types/job.js +28 -0
- package/src/types/media.js +28 -0
- package/src/worker/processor.js +24 -0
|
@@ -0,0 +1,1669 @@
|
|
|
1
|
+
import { preprocessStory } from './inputSanitizer.js';
|
|
2
|
+
import { generateScript, generateShots } from '../steps/script.js';
|
|
3
|
+
import {
|
|
4
|
+
buildFixedTimeline,
|
|
5
|
+
generateVoiceover,
|
|
6
|
+
persistVoiceover,
|
|
7
|
+
resolveSegmentCountFromAudioDuration
|
|
8
|
+
} from '../steps/generateVoiceover.js';
|
|
9
|
+
import { generateKeyframe, persistKeyframe, persistKeyframes } from '../steps/generateKeyframes.js';
|
|
10
|
+
import {
|
|
11
|
+
generateVideoSegmentAtIndex,
|
|
12
|
+
persistSegment,
|
|
13
|
+
persistSegments
|
|
14
|
+
} from '../steps/generateVideoSegments.js';
|
|
15
|
+
import { composeFinalVideo } from '../steps/composeFinalVideo.js';
|
|
16
|
+
import { alignSubtitlesToVideo } from '../steps/alignSubtitles.js';
|
|
17
|
+
import { burnInSubtitles } from '../steps/burnInSubtitles.js';
|
|
18
|
+
import { JobStatus, JobStep, emptyStepState } from '../types/job.js';
|
|
19
|
+
import { logError, logInfo } from '../observability/logger.js';
|
|
20
|
+
import { persistArtifacts } from '../store/assetStore.js';
|
|
21
|
+
import {
|
|
22
|
+
acquireProjectRunLock,
|
|
23
|
+
getJob,
|
|
24
|
+
getProjectRunLockOwner,
|
|
25
|
+
releaseProjectRunLock,
|
|
26
|
+
updateJob
|
|
27
|
+
} from '../store/jobStore.js';
|
|
28
|
+
import {
|
|
29
|
+
DEFAULT_VIDEO_CONFIG,
|
|
30
|
+
MODEL_CATEGORIES,
|
|
31
|
+
resolveKeyframeSize,
|
|
32
|
+
resolveProjectModelOptions,
|
|
33
|
+
resolveProjectModelSelections,
|
|
34
|
+
resolveShotCount,
|
|
35
|
+
resolveTargetDurationSec
|
|
36
|
+
} from '../config/models.js';
|
|
37
|
+
import fs from 'node:fs/promises';
|
|
38
|
+
import crypto from 'node:crypto';
|
|
39
|
+
import path from 'node:path';
|
|
40
|
+
import { ensureDir, writeJson } from '../media/files.js';
|
|
41
|
+
import { probeMediaDurationSeconds } from '../media/ffmpeg.js';
|
|
42
|
+
import {
|
|
43
|
+
ensureProject,
|
|
44
|
+
readProjectConfig,
|
|
45
|
+
readProjectStory,
|
|
46
|
+
getProjectDir,
|
|
47
|
+
readProjectRunState,
|
|
48
|
+
resolveProjectName,
|
|
49
|
+
writeProjectRunState
|
|
50
|
+
} from '../store/projectStore.js';
|
|
51
|
+
import { archiveProjectAssets } from '../store/staleAssetStore.js';
|
|
52
|
+
import { syncProjectSnapshot } from '../backends/outputBackend.js';
|
|
53
|
+
import {
|
|
54
|
+
collectRunPredictions,
|
|
55
|
+
createRunId,
|
|
56
|
+
createRunRecord,
|
|
57
|
+
finalizeRunRecord,
|
|
58
|
+
markStageFinished,
|
|
59
|
+
markStageReused,
|
|
60
|
+
markStageStarted,
|
|
61
|
+
writeRunRecord
|
|
62
|
+
} from '../store/projectAnalyticsStore.js';
|
|
63
|
+
|
|
64
|
+
const DEFAULT_ORCHESTRATOR_DEPS = {
|
|
65
|
+
preprocessStory,
|
|
66
|
+
generateScript,
|
|
67
|
+
generateShots,
|
|
68
|
+
buildFixedTimeline,
|
|
69
|
+
generateVoiceover,
|
|
70
|
+
persistVoiceover,
|
|
71
|
+
resolveSegmentCountFromAudioDuration,
|
|
72
|
+
generateKeyframe,
|
|
73
|
+
persistKeyframe,
|
|
74
|
+
persistKeyframes,
|
|
75
|
+
generateVideoSegmentAtIndex,
|
|
76
|
+
persistSegment,
|
|
77
|
+
persistSegments,
|
|
78
|
+
composeFinalVideo,
|
|
79
|
+
alignSubtitlesToVideo,
|
|
80
|
+
burnInSubtitles,
|
|
81
|
+
persistArtifacts,
|
|
82
|
+
probeMediaDurationSeconds,
|
|
83
|
+
ensureProject,
|
|
84
|
+
readProjectConfig,
|
|
85
|
+
readProjectStory,
|
|
86
|
+
getProjectDir,
|
|
87
|
+
readProjectRunState,
|
|
88
|
+
resolveProjectName,
|
|
89
|
+
writeProjectRunState,
|
|
90
|
+
archiveProjectAssets,
|
|
91
|
+
syncProjectSnapshot,
|
|
92
|
+
collectRunPredictions,
|
|
93
|
+
createRunId,
|
|
94
|
+
createRunRecord,
|
|
95
|
+
finalizeRunRecord,
|
|
96
|
+
markStageFinished,
|
|
97
|
+
markStageReused,
|
|
98
|
+
markStageStarted,
|
|
99
|
+
writeRunRecord
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
async function persistCheckpoint(job, projectDir = null, deps = DEFAULT_ORCHESTRATOR_DEPS) {
|
|
103
|
+
const resolvedProjectDir = projectDir || getProjectDir(job.payload.project);
|
|
104
|
+
|
|
105
|
+
await deps.persistArtifacts(job.payload.project, job.artifacts);
|
|
106
|
+
await deps.writeProjectRunState(job.payload.project, {
|
|
107
|
+
status: job.status,
|
|
108
|
+
error: job.error,
|
|
109
|
+
steps: job.steps,
|
|
110
|
+
artifacts: job.artifacts,
|
|
111
|
+
updatedAt: job.updatedAt
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await deps.syncProjectSnapshot({
|
|
115
|
+
project: job.payload.project,
|
|
116
|
+
projectDir: resolvedProjectDir
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function persistTextAssets(projectDir, script, shots, tone) {
|
|
121
|
+
const textDir = path.join(projectDir, 'assets', 'text');
|
|
122
|
+
await ensureDir(textDir);
|
|
123
|
+
await writeJson(path.join(textDir, 'script.json'), {
|
|
124
|
+
script,
|
|
125
|
+
shots,
|
|
126
|
+
tone
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hashText(value) {
|
|
131
|
+
return crypto.createHash('sha256').update(String(value || '')).digest('hex');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hashShots(shots) {
|
|
135
|
+
return (shots || []).map((shot) => hashText(shot));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function findChangedShotIndexes(previousHashes, nextHashes) {
|
|
139
|
+
const changed = [];
|
|
140
|
+
const maxLength = Math.max(previousHashes.length, nextHashes.length);
|
|
141
|
+
for (let i = 0; i < maxLength; i += 1) {
|
|
142
|
+
if ((previousHashes[i] || '') !== (nextHashes[i] || '')) {
|
|
143
|
+
changed.push(i);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return changed;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function collectAffectedSegmentIndexes(changedShotIndexes, totalKeyframes) {
|
|
150
|
+
const affected = new Set();
|
|
151
|
+
for (const shotIndex of changedShotIndexes) {
|
|
152
|
+
if (shotIndex > 0) {
|
|
153
|
+
affected.add(shotIndex - 1);
|
|
154
|
+
}
|
|
155
|
+
if (shotIndex < totalKeyframes - 1) {
|
|
156
|
+
affected.add(shotIndex);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return affected;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function clearSubtitleArtifacts(artifacts) {
|
|
163
|
+
return {
|
|
164
|
+
...artifacts,
|
|
165
|
+
subtitleSeedPath: '',
|
|
166
|
+
subtitleAlignedSrtPath: '',
|
|
167
|
+
subtitleAssPath: '',
|
|
168
|
+
finalCaptionedVideoPath: '',
|
|
169
|
+
subtitlesUrl: ''
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function fileExists(targetPath) {
|
|
174
|
+
try {
|
|
175
|
+
await fs.access(targetPath);
|
|
176
|
+
return true;
|
|
177
|
+
} catch {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function readScriptAsset(projectDir) {
|
|
183
|
+
try {
|
|
184
|
+
const filePath = path.join(projectDir, 'assets', 'text', 'script.json');
|
|
185
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
186
|
+
const parsed = JSON.parse(raw);
|
|
187
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (!parsed.script || typeof parsed.script !== 'string') {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
...parsed,
|
|
195
|
+
shots: Array.isArray(parsed.shots) ? parsed.shots : []
|
|
196
|
+
};
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error.code === 'ENOENT') {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function regenerateProjectAsset(projectName, target, options = {}) {
|
|
206
|
+
const deps = {
|
|
207
|
+
...DEFAULT_ORCHESTRATOR_DEPS,
|
|
208
|
+
...(options.deps || {})
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const project = deps.resolveProjectName(projectName);
|
|
212
|
+
const targetType = String(target?.targetType || '').trim().toLowerCase();
|
|
213
|
+
const hasIndex = target?.index !== undefined;
|
|
214
|
+
const index = hasIndex ? Number(target?.index) : null;
|
|
215
|
+
|
|
216
|
+
if (!['script', 'voiceover', 'keyframe', 'segment', 'align', 'burnin'].includes(targetType)) {
|
|
217
|
+
throw new Error('targetType must be one of script, voiceover, keyframe, segment, align, burnin');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (['script', 'voiceover', 'align', 'burnin'].includes(targetType) && hasIndex) {
|
|
221
|
+
throw new Error('index is not supported for script/voiceover/align/burnin regeneration');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (['keyframe', 'segment'].includes(targetType) && (!Number.isInteger(index) || index < 0)) {
|
|
225
|
+
throw new Error('index must be a non-negative integer');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
await deps.ensureProject(project);
|
|
229
|
+
const projectConfig = await deps.readProjectConfig(project);
|
|
230
|
+
const modelSelections = resolveProjectModelSelections(projectConfig.models);
|
|
231
|
+
const modelOptions = resolveProjectModelOptions(projectConfig.modelOptions, modelSelections);
|
|
232
|
+
const projectDir = deps.getProjectDir(project);
|
|
233
|
+
const runState = await deps.readProjectRunState(project);
|
|
234
|
+
|
|
235
|
+
if (!runState?.artifacts) {
|
|
236
|
+
throw new Error('project has no prior artifacts to regenerate from');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const artifacts = {
|
|
240
|
+
...runState.artifacts,
|
|
241
|
+
modelSelections,
|
|
242
|
+
modelOptions,
|
|
243
|
+
subtitleOptions: projectConfig.subtitleOptions,
|
|
244
|
+
keyframeUrls: [...(runState.artifacts.keyframeUrls || [])],
|
|
245
|
+
keyframePaths: [...(runState.artifacts.keyframePaths || [])],
|
|
246
|
+
segmentUrls: [...(runState.artifacts.segmentUrls || [])],
|
|
247
|
+
segmentPaths: [...(runState.artifacts.segmentPaths || [])]
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
if (
|
|
251
|
+
['voiceover', 'keyframe', 'segment'].includes(targetType)
|
|
252
|
+
&& (!Array.isArray(artifacts.shots) || artifacts.shots.length === 0)
|
|
253
|
+
) {
|
|
254
|
+
const scriptAsset = await readScriptAsset(projectDir);
|
|
255
|
+
if (Array.isArray(scriptAsset?.shots) && scriptAsset.shots.length > 0) {
|
|
256
|
+
artifacts.shots = [...scriptAsset.shots];
|
|
257
|
+
artifacts.shotHashes = hashShots(artifacts.shots);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const steps = {
|
|
262
|
+
...emptyStepState(),
|
|
263
|
+
...(runState.steps || {})
|
|
264
|
+
};
|
|
265
|
+
const keyframeCount = Array.isArray(artifacts.shots) ? artifacts.shots.length : 0;
|
|
266
|
+
const segmentCount = Math.max(0, keyframeCount - 1);
|
|
267
|
+
|
|
268
|
+
if (['keyframe', 'segment'].includes(targetType)) {
|
|
269
|
+
if (!Array.isArray(artifacts.shots) || artifacts.shots.length === 0) {
|
|
270
|
+
throw new Error('project has no shots to regenerate');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (targetType === 'keyframe' && index >= keyframeCount) {
|
|
274
|
+
throw new Error(`index out of range for project keyframes (${keyframeCount})`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (targetType === 'segment' && index >= segmentCount) {
|
|
278
|
+
throw new Error(`index out of range for project segments (${segmentCount})`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const traceBase = {
|
|
283
|
+
project,
|
|
284
|
+
projectDir,
|
|
285
|
+
reason: 'targeted_asset_regeneration',
|
|
286
|
+
targetType,
|
|
287
|
+
...((targetType === 'keyframe' || targetType === 'segment') ? { index } : {})
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
if (targetType === 'script') {
|
|
291
|
+
const story = await deps.readProjectStory(project);
|
|
292
|
+
const safeStory = deps.preprocessStory(story);
|
|
293
|
+
const scriptResult = await deps.generateScript(
|
|
294
|
+
safeStory,
|
|
295
|
+
{
|
|
296
|
+
targetDurationSec: resolveTargetDurationSec(projectConfig),
|
|
297
|
+
modelId: modelSelections[MODEL_CATEGORIES.textToText],
|
|
298
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.textToText]
|
|
299
|
+
},
|
|
300
|
+
traceBase
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
artifacts.script = scriptResult.script;
|
|
304
|
+
artifacts.tone = scriptResult.tone || artifacts.tone || 'neutral';
|
|
305
|
+
artifacts.scriptWordCount = scriptResult.scriptWordCount || null;
|
|
306
|
+
artifacts.targetWordCount = scriptResult.targetWordCount || null;
|
|
307
|
+
artifacts.scriptHash = hashText(scriptResult.script);
|
|
308
|
+
artifacts.scriptSourceStoryHash = hashText(safeStory);
|
|
309
|
+
artifacts.shots = [];
|
|
310
|
+
artifacts.shotHashes = [];
|
|
311
|
+
artifacts.timeline = [];
|
|
312
|
+
artifacts.voiceoverUrl = '';
|
|
313
|
+
artifacts.voiceoverPath = '';
|
|
314
|
+
artifacts.keyframeUrls = [];
|
|
315
|
+
artifacts.keyframePaths = [];
|
|
316
|
+
artifacts.segmentUrls = [];
|
|
317
|
+
artifacts.segmentPaths = [];
|
|
318
|
+
artifacts.finalBaseVideoPath = '';
|
|
319
|
+
artifacts.finalVideoPath = '';
|
|
320
|
+
Object.assign(artifacts, clearSubtitleArtifacts(artifacts));
|
|
321
|
+
|
|
322
|
+
steps[JobStep.SCRIPT] = true;
|
|
323
|
+
steps[JobStep.VOICE] = false;
|
|
324
|
+
steps[JobStep.KEYFRAMES] = false;
|
|
325
|
+
steps[JobStep.SEGMENTS] = false;
|
|
326
|
+
steps[JobStep.COMPOSE] = false;
|
|
327
|
+
steps[JobStep.ALIGN] = false;
|
|
328
|
+
steps[JobStep.BURNIN] = false;
|
|
329
|
+
|
|
330
|
+
await persistTextAssets(projectDir, artifacts.script, artifacts.shots, artifacts.tone || 'neutral');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (targetType === 'voiceover') {
|
|
334
|
+
if (!artifacts.script) {
|
|
335
|
+
throw new Error('project has no script to regenerate voiceover from');
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const fallbackSegmentCount = Array.isArray(artifacts.timeline)
|
|
339
|
+
? artifacts.timeline.length
|
|
340
|
+
: Math.max(
|
|
341
|
+
0,
|
|
342
|
+
(Array.isArray(artifacts.keyframeUrls) ? artifacts.keyframeUrls.length : 0) - 1,
|
|
343
|
+
Array.isArray(artifacts.segmentUrls) ? artifacts.segmentUrls.length : 0
|
|
344
|
+
);
|
|
345
|
+
const estimatedShotsCount = Array.isArray(artifacts.shots) && artifacts.shots.length > 0
|
|
346
|
+
? artifacts.shots.length
|
|
347
|
+
: Math.max(2, fallbackSegmentCount + 1);
|
|
348
|
+
|
|
349
|
+
const previousAudioDurationSec = Number.isFinite(artifacts.audioDurationSec)
|
|
350
|
+
? artifacts.audioDurationSec
|
|
351
|
+
: null;
|
|
352
|
+
const previousSegmentCount = Array.isArray(artifacts.timeline)
|
|
353
|
+
? artifacts.timeline.length
|
|
354
|
+
: Math.max(0, estimatedShotsCount - 1);
|
|
355
|
+
|
|
356
|
+
const voiceResult = await deps.generateVoiceover(
|
|
357
|
+
artifacts.script,
|
|
358
|
+
{
|
|
359
|
+
shotsCount: Math.max(1, estimatedShotsCount - 1),
|
|
360
|
+
segmentDurationSec: DEFAULT_VIDEO_CONFIG.segmentDurationSec,
|
|
361
|
+
targetDurationSec: resolveTargetDurationSec(projectConfig),
|
|
362
|
+
modelId: modelSelections[MODEL_CATEGORIES.textToSpeech],
|
|
363
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.textToSpeech]
|
|
364
|
+
},
|
|
365
|
+
traceBase
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
artifacts.voiceoverUrl = voiceResult.voiceoverUrl;
|
|
369
|
+
artifacts.voiceoverPath = await deps.persistVoiceover(projectDir, voiceResult.voiceoverUrl);
|
|
370
|
+
artifacts.ttsPlan = voiceResult.ttsPlan;
|
|
371
|
+
|
|
372
|
+
const audioDurationSec = await deps.probeMediaDurationSeconds(artifacts.voiceoverPath);
|
|
373
|
+
const requiredSegments = deps.resolveSegmentCountFromAudioDuration(
|
|
374
|
+
audioDurationSec,
|
|
375
|
+
DEFAULT_VIDEO_CONFIG.segmentDurationSec
|
|
376
|
+
);
|
|
377
|
+
const requiredKeyframes = Math.max(2, requiredSegments + 1);
|
|
378
|
+
const timeline = deps.buildFixedTimeline(requiredSegments, DEFAULT_VIDEO_CONFIG.segmentDurationSec);
|
|
379
|
+
artifacts.audioDurationSec = audioDurationSec;
|
|
380
|
+
artifacts.plannedShots = requiredSegments;
|
|
381
|
+
artifacts.segmentDurationSec = DEFAULT_VIDEO_CONFIG.segmentDurationSec;
|
|
382
|
+
artifacts.timeline = timeline;
|
|
383
|
+
|
|
384
|
+
const sameAudioDuration =
|
|
385
|
+
previousAudioDurationSec !== null
|
|
386
|
+
&& Math.abs(previousAudioDurationSec - audioDurationSec) <= 0.05;
|
|
387
|
+
const sameVisualLength = requiredSegments === previousSegmentCount;
|
|
388
|
+
|
|
389
|
+
if (!Array.isArray(artifacts.shots) || artifacts.shots.length !== requiredKeyframes) {
|
|
390
|
+
const shotsResult = await deps.generateShots(
|
|
391
|
+
artifacts.script,
|
|
392
|
+
{
|
|
393
|
+
shotCount: requiredKeyframes,
|
|
394
|
+
tone: artifacts.tone || 'neutral',
|
|
395
|
+
modelId: modelSelections[MODEL_CATEGORIES.textToText],
|
|
396
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.textToText]
|
|
397
|
+
},
|
|
398
|
+
traceBase
|
|
399
|
+
);
|
|
400
|
+
artifacts.shots = shotsResult.shots;
|
|
401
|
+
artifacts.shotHashes = hashShots(shotsResult.shots);
|
|
402
|
+
artifacts.keyframeUrls = [];
|
|
403
|
+
artifacts.keyframePaths = [];
|
|
404
|
+
artifacts.segmentUrls = [];
|
|
405
|
+
artifacts.segmentPaths = [];
|
|
406
|
+
await persistTextAssets(projectDir, artifacts.script, artifacts.shots, artifacts.tone || 'neutral');
|
|
407
|
+
steps[JobStep.KEYFRAMES] = false;
|
|
408
|
+
steps[JobStep.SEGMENTS] = false;
|
|
409
|
+
steps[JobStep.COMPOSE] = false;
|
|
410
|
+
steps[JobStep.ALIGN] = false;
|
|
411
|
+
steps[JobStep.BURNIN] = false;
|
|
412
|
+
artifacts.finalBaseVideoPath = '';
|
|
413
|
+
artifacts.finalVideoPath = '';
|
|
414
|
+
Object.assign(artifacts, clearSubtitleArtifacts(artifacts));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
steps[JobStep.VOICE] = true;
|
|
418
|
+
|
|
419
|
+
const canComposeWithExistingVisuals =
|
|
420
|
+
Array.isArray(artifacts.segmentUrls)
|
|
421
|
+
&& artifacts.segmentUrls.length === requiredSegments
|
|
422
|
+
&& artifacts.segmentUrls.every((url) => Boolean(url));
|
|
423
|
+
|
|
424
|
+
if (sameAudioDuration && sameVisualLength && canComposeWithExistingVisuals) {
|
|
425
|
+
const composed = await deps.composeFinalVideo({
|
|
426
|
+
projectDir,
|
|
427
|
+
segmentUrls: artifacts.segmentUrls,
|
|
428
|
+
segmentPaths: artifacts.segmentPaths,
|
|
429
|
+
voiceoverPath: artifacts.voiceoverPath,
|
|
430
|
+
voiceoverUrl: artifacts.voiceoverUrl,
|
|
431
|
+
keyframePaths: artifacts.keyframePaths,
|
|
432
|
+
finalDurationMode: projectConfig.finalDurationMode
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
artifacts.finalVideoPath = composed.finalVideoPath;
|
|
436
|
+
artifacts.finalBaseVideoPath = composed.finalVideoPath;
|
|
437
|
+
artifacts.voiceoverPath = composed.voiceoverPath;
|
|
438
|
+
artifacts.keyframePaths = composed.keyframePaths;
|
|
439
|
+
artifacts.segmentPaths = composed.segmentPaths;
|
|
440
|
+
steps[JobStep.COMPOSE] = true;
|
|
441
|
+
steps[JobStep.ALIGN] = false;
|
|
442
|
+
steps[JobStep.BURNIN] = false;
|
|
443
|
+
Object.assign(artifacts, clearSubtitleArtifacts(artifacts));
|
|
444
|
+
} else {
|
|
445
|
+
steps[JobStep.COMPOSE] = false;
|
|
446
|
+
steps[JobStep.ALIGN] = false;
|
|
447
|
+
steps[JobStep.BURNIN] = false;
|
|
448
|
+
artifacts.finalBaseVideoPath = '';
|
|
449
|
+
artifacts.finalVideoPath = '';
|
|
450
|
+
Object.assign(artifacts, clearSubtitleArtifacts(artifacts));
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (targetType === 'keyframe') {
|
|
455
|
+
const keyframeSize = resolveKeyframeSize(projectConfig);
|
|
456
|
+
const keyframeUrl = await deps.generateKeyframe(
|
|
457
|
+
artifacts.shots[index],
|
|
458
|
+
artifacts.tone || 'neutral',
|
|
459
|
+
projectConfig.aspectRatio,
|
|
460
|
+
index,
|
|
461
|
+
traceBase,
|
|
462
|
+
keyframeSize,
|
|
463
|
+
{
|
|
464
|
+
modelId: modelSelections[MODEL_CATEGORIES.textToImage],
|
|
465
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.textToImage]
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
artifacts.keyframeUrls[index] = keyframeUrl;
|
|
469
|
+
artifacts.keyframePaths[index] = await deps.persistKeyframe(projectDir, keyframeUrl, index);
|
|
470
|
+
|
|
471
|
+
const affectedSegmentIndexes = collectAffectedSegmentIndexes([index], artifacts.shots.length);
|
|
472
|
+
const maxSegmentIndex = Math.max(0, artifacts.shots.length - 1);
|
|
473
|
+
for (const segmentIndex of affectedSegmentIndexes) {
|
|
474
|
+
if (segmentIndex < 0 || segmentIndex >= maxSegmentIndex) {
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
artifacts.segmentUrls[segmentIndex] = '';
|
|
478
|
+
artifacts.segmentPaths[segmentIndex] = '';
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
steps[JobStep.SEGMENTS] = false;
|
|
482
|
+
steps[JobStep.COMPOSE] = false;
|
|
483
|
+
steps[JobStep.ALIGN] = false;
|
|
484
|
+
steps[JobStep.BURNIN] = false;
|
|
485
|
+
artifacts.finalBaseVideoPath = '';
|
|
486
|
+
artifacts.finalVideoPath = '';
|
|
487
|
+
Object.assign(artifacts, clearSubtitleArtifacts(artifacts));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (targetType === 'segment') {
|
|
491
|
+
if (!Array.isArray(artifacts.timeline) || artifacts.timeline.length === 0) {
|
|
492
|
+
throw new Error('project has no timeline to regenerate segment from');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!artifacts.keyframeUrls[index]) {
|
|
496
|
+
throw new Error('missing keyframe URL required for segment regeneration');
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const segmentUrl = await deps.generateVideoSegmentAtIndex(
|
|
500
|
+
index,
|
|
501
|
+
artifacts.keyframeUrls,
|
|
502
|
+
artifacts.timeline,
|
|
503
|
+
artifacts.shots,
|
|
504
|
+
projectConfig.aspectRatio,
|
|
505
|
+
traceBase,
|
|
506
|
+
{
|
|
507
|
+
modelId: modelSelections[MODEL_CATEGORIES.imageTextToVideo],
|
|
508
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.imageTextToVideo]
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
artifacts.segmentUrls[index] = segmentUrl;
|
|
512
|
+
artifacts.segmentPaths[index] = await deps.persistSegment(projectDir, segmentUrl, index);
|
|
513
|
+
|
|
514
|
+
steps[JobStep.COMPOSE] = false;
|
|
515
|
+
steps[JobStep.ALIGN] = false;
|
|
516
|
+
steps[JobStep.BURNIN] = false;
|
|
517
|
+
artifacts.finalBaseVideoPath = '';
|
|
518
|
+
artifacts.finalVideoPath = '';
|
|
519
|
+
Object.assign(artifacts, clearSubtitleArtifacts(artifacts));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (targetType === 'align') {
|
|
523
|
+
const baseVideoPath = artifacts.finalBaseVideoPath || artifacts.finalVideoPath;
|
|
524
|
+
if (!baseVideoPath) {
|
|
525
|
+
throw new Error('project has no composed video to align subtitles against');
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (!artifacts.script || !String(artifacts.script).trim()) {
|
|
529
|
+
throw new Error('project has no script to align subtitles from');
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const aligned = await deps.alignSubtitlesToVideo({
|
|
533
|
+
projectDir,
|
|
534
|
+
videoPath: baseVideoPath,
|
|
535
|
+
script: artifacts.script,
|
|
536
|
+
totalDurationSec: artifacts.audioDurationSec || resolveTargetDurationSec(projectConfig),
|
|
537
|
+
subtitleOptions: projectConfig.subtitleOptions
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
artifacts.subtitleSeedPath = aligned.subtitleSeedPath;
|
|
541
|
+
artifacts.subtitleAlignedSrtPath = aligned.subtitleAlignedSrtPath;
|
|
542
|
+
artifacts.subtitleAssPath = aligned.subtitleAssPath;
|
|
543
|
+
artifacts.finalBaseVideoPath = baseVideoPath;
|
|
544
|
+
artifacts.finalCaptionedVideoPath = '';
|
|
545
|
+
artifacts.finalVideoPath = baseVideoPath;
|
|
546
|
+
steps[JobStep.ALIGN] = true;
|
|
547
|
+
steps[JobStep.BURNIN] = false;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (targetType === 'burnin') {
|
|
551
|
+
const baseVideoPath = artifacts.finalBaseVideoPath || artifacts.finalVideoPath;
|
|
552
|
+
if (!baseVideoPath) {
|
|
553
|
+
throw new Error('project has no composed video to burn subtitles into');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (!artifacts.subtitleAssPath) {
|
|
557
|
+
throw new Error('project has no aligned subtitle file; run align first');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const burned = await deps.burnInSubtitles({
|
|
561
|
+
projectDir,
|
|
562
|
+
videoPath: baseVideoPath,
|
|
563
|
+
subtitleAssPath: artifacts.subtitleAssPath
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
artifacts.finalBaseVideoPath = baseVideoPath;
|
|
567
|
+
artifacts.finalCaptionedVideoPath = burned.finalCaptionedVideoPath;
|
|
568
|
+
artifacts.finalVideoPath = burned.finalCaptionedVideoPath;
|
|
569
|
+
steps[JobStep.BURNIN] = true;
|
|
570
|
+
steps[JobStep.ALIGN] = true;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const updatedAt = new Date().toISOString();
|
|
574
|
+
await deps.persistArtifacts(project, artifacts);
|
|
575
|
+
await deps.writeProjectRunState(project, {
|
|
576
|
+
status: runState.status || JobStatus.COMPLETED,
|
|
577
|
+
error: runState.error || null,
|
|
578
|
+
steps,
|
|
579
|
+
artifacts,
|
|
580
|
+
updatedAt
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await deps.syncProjectSnapshot({ project, projectDir });
|
|
584
|
+
|
|
585
|
+
return {
|
|
586
|
+
project,
|
|
587
|
+
targetType,
|
|
588
|
+
...((targetType === 'keyframe' || targetType === 'segment') ? { index } : {}),
|
|
589
|
+
artifacts,
|
|
590
|
+
steps,
|
|
591
|
+
updatedAt
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export async function runPipeline(jobId, options = {}) {
|
|
596
|
+
const deps = {
|
|
597
|
+
...DEFAULT_ORCHESTRATOR_DEPS,
|
|
598
|
+
...(options.deps || {})
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const forceRestart = Boolean(options.forceRestart);
|
|
602
|
+
const job = getJob(jobId);
|
|
603
|
+
if (!job) {
|
|
604
|
+
throw new Error(`Job not found: ${jobId}`);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
let analyticsProject = null;
|
|
608
|
+
let analyticsProjectDir = null;
|
|
609
|
+
let analyticsRun = null;
|
|
610
|
+
let lockedProject = null;
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const project = deps.resolveProjectName(job.payload.project || `job-${jobId}`);
|
|
614
|
+
const lockAcquired = acquireProjectRunLock(project, jobId);
|
|
615
|
+
if (!lockAcquired) {
|
|
616
|
+
const ownerJobId = getProjectRunLockOwner(project);
|
|
617
|
+
const ownerSuffix = ownerJobId ? ` (owner job: ${ownerJobId})` : '';
|
|
618
|
+
const lockError = new Error(`Project ${project} is already running${ownerSuffix}`);
|
|
619
|
+
lockError.nonRetryable = true;
|
|
620
|
+
throw lockError;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
lockedProject = project;
|
|
624
|
+
analyticsProject = project;
|
|
625
|
+
await deps.ensureProject(project);
|
|
626
|
+
const projectConfig = await deps.readProjectConfig(project);
|
|
627
|
+
const modelSelections = resolveProjectModelSelections(projectConfig.models);
|
|
628
|
+
const modelOptions = resolveProjectModelOptions(projectConfig.modelOptions, modelSelections);
|
|
629
|
+
const projectDir = deps.getProjectDir(project);
|
|
630
|
+
const targetDurationSec = resolveTargetDurationSec(projectConfig);
|
|
631
|
+
const plannedShots = resolveShotCount(projectConfig);
|
|
632
|
+
const safeStory = deps.preprocessStory(job.payload.story);
|
|
633
|
+
analyticsProjectDir = projectDir;
|
|
634
|
+
const analyticsRunId = deps.createRunId();
|
|
635
|
+
analyticsRun = deps.createRunRecord({
|
|
636
|
+
runId: analyticsRunId,
|
|
637
|
+
project,
|
|
638
|
+
jobId,
|
|
639
|
+
forceRestart
|
|
640
|
+
});
|
|
641
|
+
await deps.writeRunRecord(project, analyticsRun);
|
|
642
|
+
|
|
643
|
+
const saveAnalytics = async () => {
|
|
644
|
+
/* c8 ignore next 3 */
|
|
645
|
+
if (!analyticsRun || !analyticsProject) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
await deps.writeRunRecord(analyticsProject, analyticsRun);
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
const startStageAnalytics = async (stage, details = null) => {
|
|
652
|
+
analyticsRun = deps.markStageStarted(analyticsRun, stage, details);
|
|
653
|
+
await saveAnalytics();
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
const finishStageAnalytics = async (stage, payload = {}) => {
|
|
657
|
+
analyticsRun = deps.markStageFinished(analyticsRun, stage, payload);
|
|
658
|
+
await saveAnalytics();
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const reuseStageAnalytics = async (stage, details = null) => {
|
|
662
|
+
if (analyticsRun?.stages?.[stage]?.executed) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
analyticsRun = deps.markStageReused(analyticsRun, stage, details);
|
|
666
|
+
await saveAnalytics();
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const keyframeSize = resolveKeyframeSize(projectConfig);
|
|
670
|
+
|
|
671
|
+
const previous = forceRestart ? null : await deps.readProjectRunState(project);
|
|
672
|
+
const resumeState = previous || null;
|
|
673
|
+
|
|
674
|
+
const seedArtifacts = resumeState?.artifacts || job.artifacts;
|
|
675
|
+
const seedSteps = resumeState?.steps || emptyStepState();
|
|
676
|
+
const traceBase = {
|
|
677
|
+
project,
|
|
678
|
+
projectDir,
|
|
679
|
+
jobId,
|
|
680
|
+
runId: analyticsRunId
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
const alignVisualPlanToAudioDuration = async () => {
|
|
684
|
+
let alignedJob = getJob(jobId);
|
|
685
|
+
const segmentDurationSec = DEFAULT_VIDEO_CONFIG.segmentDurationSec;
|
|
686
|
+
|
|
687
|
+
if (!alignedJob?.artifacts?.voiceoverPath) {
|
|
688
|
+
return alignedJob;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let audioDurationSec;
|
|
692
|
+
try {
|
|
693
|
+
audioDurationSec = await deps.probeMediaDurationSeconds(alignedJob.artifacts.voiceoverPath);
|
|
694
|
+
} catch (error) {
|
|
695
|
+
logInfo('voiceover_duration_probe_failed', {
|
|
696
|
+
jobId,
|
|
697
|
+
project,
|
|
698
|
+
voiceoverPath: alignedJob.artifacts.voiceoverPath,
|
|
699
|
+
error: error.message
|
|
700
|
+
});
|
|
701
|
+
return alignedJob;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const requiredSegments = deps.resolveSegmentCountFromAudioDuration(audioDurationSec, segmentDurationSec);
|
|
705
|
+
const requiredKeyframes = Math.max(2, requiredSegments + 1);
|
|
706
|
+
let alignedShots = Array.isArray(alignedJob.artifacts.shots) ? alignedJob.artifacts.shots : [];
|
|
707
|
+
const alignedTimeline = deps.buildFixedTimeline(requiredSegments, segmentDurationSec);
|
|
708
|
+
|
|
709
|
+
if (alignedShots.length !== requiredKeyframes) {
|
|
710
|
+
const shotsResult = await deps.generateShots(
|
|
711
|
+
alignedJob.artifacts.script,
|
|
712
|
+
{
|
|
713
|
+
shotCount: requiredKeyframes,
|
|
714
|
+
tone: alignedJob.artifacts.tone || 'neutral',
|
|
715
|
+
modelId: modelSelections[MODEL_CATEGORIES.textToText],
|
|
716
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.textToText]
|
|
717
|
+
},
|
|
718
|
+
traceBase
|
|
719
|
+
);
|
|
720
|
+
alignedShots = shotsResult.shots;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const shotsChanged = JSON.stringify(alignedShots) !== JSON.stringify(alignedJob.artifacts.shots || []);
|
|
724
|
+
const timelineChanged = JSON.stringify(alignedTimeline) !== JSON.stringify(alignedJob.artifacts.timeline || []);
|
|
725
|
+
const durationChanged =
|
|
726
|
+
!Number.isFinite(alignedJob.artifacts.audioDurationSec) ||
|
|
727
|
+
Math.abs(alignedJob.artifacts.audioDurationSec - audioDurationSec) > 0.01;
|
|
728
|
+
|
|
729
|
+
if (!shotsChanged && !timelineChanged && !durationChanged) {
|
|
730
|
+
return alignedJob;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const nextArtifacts = {
|
|
734
|
+
...alignedJob.artifacts,
|
|
735
|
+
audioDurationSec,
|
|
736
|
+
plannedShots: requiredSegments,
|
|
737
|
+
segmentDurationSec,
|
|
738
|
+
shots: alignedShots,
|
|
739
|
+
shotHashes: hashShots(alignedShots),
|
|
740
|
+
timeline: alignedTimeline
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const nextSteps = {
|
|
744
|
+
...alignedJob.steps
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
if (shotsChanged) {
|
|
748
|
+
nextSteps[JobStep.KEYFRAMES] = false;
|
|
749
|
+
nextSteps[JobStep.SEGMENTS] = false;
|
|
750
|
+
nextSteps[JobStep.COMPOSE] = false;
|
|
751
|
+
nextSteps[JobStep.ALIGN] = false;
|
|
752
|
+
nextSteps[JobStep.BURNIN] = false;
|
|
753
|
+
nextArtifacts.keyframeUrls = [];
|
|
754
|
+
nextArtifacts.keyframePaths = [];
|
|
755
|
+
nextArtifacts.segmentUrls = [];
|
|
756
|
+
nextArtifacts.segmentPaths = [];
|
|
757
|
+
nextArtifacts.finalBaseVideoPath = '';
|
|
758
|
+
nextArtifacts.finalVideoPath = '';
|
|
759
|
+
Object.assign(nextArtifacts, clearSubtitleArtifacts(nextArtifacts));
|
|
760
|
+
} else if (timelineChanged) {
|
|
761
|
+
nextSteps[JobStep.SEGMENTS] = false;
|
|
762
|
+
nextSteps[JobStep.COMPOSE] = false;
|
|
763
|
+
nextSteps[JobStep.ALIGN] = false;
|
|
764
|
+
nextSteps[JobStep.BURNIN] = false;
|
|
765
|
+
nextArtifacts.segmentUrls = [];
|
|
766
|
+
nextArtifacts.segmentPaths = [];
|
|
767
|
+
nextArtifacts.finalBaseVideoPath = '';
|
|
768
|
+
nextArtifacts.finalVideoPath = '';
|
|
769
|
+
Object.assign(nextArtifacts, clearSubtitleArtifacts(nextArtifacts));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (shotsChanged) {
|
|
773
|
+
await persistTextAssets(
|
|
774
|
+
projectDir,
|
|
775
|
+
nextArtifacts.script || '',
|
|
776
|
+
alignedShots,
|
|
777
|
+
nextArtifacts.tone || 'neutral'
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
updateJob(jobId, {
|
|
782
|
+
artifacts: nextArtifacts,
|
|
783
|
+
steps: nextSteps
|
|
784
|
+
});
|
|
785
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
786
|
+
alignedJob = getJob(jobId);
|
|
787
|
+
return alignedJob;
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const storyHash = hashText(safeStory);
|
|
791
|
+
const scriptAsset = await readScriptAsset(projectDir);
|
|
792
|
+
const scriptSourceStoryHash = seedArtifacts.scriptSourceStoryHash || '';
|
|
793
|
+
const hasLegacyState = !seedArtifacts.scriptSourceStoryHash && !!seedArtifacts.script;
|
|
794
|
+
|
|
795
|
+
if (hasLegacyState || (scriptSourceStoryHash && scriptSourceStoryHash !== storyHash)) {
|
|
796
|
+
seedSteps[JobStep.SCRIPT] = false;
|
|
797
|
+
seedSteps[JobStep.VOICE] = false;
|
|
798
|
+
seedSteps[JobStep.KEYFRAMES] = false;
|
|
799
|
+
seedSteps[JobStep.SEGMENTS] = false;
|
|
800
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
801
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
802
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
803
|
+
seedArtifacts.script = '';
|
|
804
|
+
seedArtifacts.tone = '';
|
|
805
|
+
seedArtifacts.shots = [];
|
|
806
|
+
seedArtifacts.timeline = [];
|
|
807
|
+
seedArtifacts.voiceoverUrl = '';
|
|
808
|
+
seedArtifacts.voiceoverPath = '';
|
|
809
|
+
seedArtifacts.keyframeUrls = [];
|
|
810
|
+
seedArtifacts.keyframePaths = [];
|
|
811
|
+
seedArtifacts.segmentUrls = [];
|
|
812
|
+
seedArtifacts.segmentPaths = [];
|
|
813
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
814
|
+
seedArtifacts.finalVideoPath = '';
|
|
815
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
816
|
+
seedArtifacts.scriptHash = '';
|
|
817
|
+
seedArtifacts.shotHashes = [];
|
|
818
|
+
seedArtifacts.scriptSourceStoryHash = '';
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (
|
|
822
|
+
scriptAsset &&
|
|
823
|
+
seedArtifacts.scriptHash &&
|
|
824
|
+
hashText(scriptAsset.script) !== seedArtifacts.scriptHash
|
|
825
|
+
) {
|
|
826
|
+
seedArtifacts.script = scriptAsset.script;
|
|
827
|
+
seedArtifacts.scriptHash = hashText(scriptAsset.script);
|
|
828
|
+
seedSteps[JobStep.VOICE] = false;
|
|
829
|
+
seedSteps[JobStep.KEYFRAMES] = false;
|
|
830
|
+
seedSteps[JobStep.SEGMENTS] = false;
|
|
831
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
832
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
833
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
834
|
+
seedArtifacts.shots = [];
|
|
835
|
+
seedArtifacts.shotHashes = [];
|
|
836
|
+
seedArtifacts.timeline = [];
|
|
837
|
+
seedArtifacts.voiceoverUrl = '';
|
|
838
|
+
seedArtifacts.voiceoverPath = '';
|
|
839
|
+
seedArtifacts.keyframeUrls = [];
|
|
840
|
+
seedArtifacts.keyframePaths = [];
|
|
841
|
+
seedArtifacts.segmentUrls = [];
|
|
842
|
+
seedArtifacts.segmentPaths = [];
|
|
843
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
844
|
+
seedArtifacts.finalVideoPath = '';
|
|
845
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const shotHashesFromAsset = scriptAsset ? hashShots(scriptAsset.shots) : [];
|
|
849
|
+
const changedShotIndexes =
|
|
850
|
+
scriptAsset && seedArtifacts.shotHashes?.length
|
|
851
|
+
? findChangedShotIndexes(seedArtifacts.shotHashes, shotHashesFromAsset)
|
|
852
|
+
: [];
|
|
853
|
+
|
|
854
|
+
if (scriptAsset && changedShotIndexes.length > 0) {
|
|
855
|
+
seedArtifacts.shots = scriptAsset.shots;
|
|
856
|
+
seedArtifacts.shotHashes = shotHashesFromAsset;
|
|
857
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
858
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
859
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
860
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
861
|
+
seedArtifacts.finalVideoPath = '';
|
|
862
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (seedArtifacts.aspectRatio && seedArtifacts.aspectRatio !== projectConfig.aspectRatio) {
|
|
866
|
+
seedSteps[JobStep.KEYFRAMES] = false;
|
|
867
|
+
seedSteps[JobStep.SEGMENTS] = false;
|
|
868
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
869
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
870
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
871
|
+
seedArtifacts.keyframeUrls = [];
|
|
872
|
+
seedArtifacts.keyframePaths = [];
|
|
873
|
+
seedArtifacts.segmentUrls = [];
|
|
874
|
+
seedArtifacts.segmentPaths = [];
|
|
875
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
876
|
+
seedArtifacts.finalVideoPath = '';
|
|
877
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if ((seedArtifacts.finalDurationMode || 'match_audio') !== projectConfig.finalDurationMode) {
|
|
881
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
882
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
883
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
884
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
885
|
+
seedArtifacts.finalVideoPath = '';
|
|
886
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const previousTargetDuration = Number.isInteger(seedArtifacts.targetDurationSec)
|
|
890
|
+
? seedArtifacts.targetDurationSec
|
|
891
|
+
: DEFAULT_VIDEO_CONFIG.durationSec;
|
|
892
|
+
const previousModelSelections = resolveProjectModelSelections(seedArtifacts.modelSelections);
|
|
893
|
+
const previousModelOptions = resolveProjectModelOptions(seedArtifacts.modelOptions, previousModelSelections);
|
|
894
|
+
const changedModelOptionCategories = Object.values(MODEL_CATEGORIES).filter(
|
|
895
|
+
(category) => JSON.stringify(previousModelOptions[category]) !== JSON.stringify(modelOptions[category])
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
const resetFromScript = () => {
|
|
899
|
+
seedSteps[JobStep.SCRIPT] = false;
|
|
900
|
+
seedSteps[JobStep.VOICE] = false;
|
|
901
|
+
seedSteps[JobStep.KEYFRAMES] = false;
|
|
902
|
+
seedSteps[JobStep.SEGMENTS] = false;
|
|
903
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
904
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
905
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
906
|
+
seedArtifacts.script = '';
|
|
907
|
+
seedArtifacts.tone = '';
|
|
908
|
+
seedArtifacts.shots = [];
|
|
909
|
+
seedArtifacts.timeline = [];
|
|
910
|
+
seedArtifacts.voiceoverUrl = '';
|
|
911
|
+
seedArtifacts.voiceoverPath = '';
|
|
912
|
+
seedArtifacts.keyframeUrls = [];
|
|
913
|
+
seedArtifacts.keyframePaths = [];
|
|
914
|
+
seedArtifacts.segmentUrls = [];
|
|
915
|
+
seedArtifacts.segmentPaths = [];
|
|
916
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
917
|
+
seedArtifacts.finalVideoPath = '';
|
|
918
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
919
|
+
seedArtifacts.scriptHash = '';
|
|
920
|
+
seedArtifacts.shotHashes = [];
|
|
921
|
+
seedArtifacts.scriptSourceStoryHash = '';
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
const resetFromVoice = () => {
|
|
925
|
+
seedSteps[JobStep.VOICE] = false;
|
|
926
|
+
seedSteps[JobStep.KEYFRAMES] = false;
|
|
927
|
+
seedSteps[JobStep.SEGMENTS] = false;
|
|
928
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
929
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
930
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
931
|
+
seedArtifacts.timeline = [];
|
|
932
|
+
seedArtifacts.voiceoverUrl = '';
|
|
933
|
+
seedArtifacts.voiceoverPath = '';
|
|
934
|
+
seedArtifacts.keyframeUrls = [];
|
|
935
|
+
seedArtifacts.keyframePaths = [];
|
|
936
|
+
seedArtifacts.segmentUrls = [];
|
|
937
|
+
seedArtifacts.segmentPaths = [];
|
|
938
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
939
|
+
seedArtifacts.finalVideoPath = '';
|
|
940
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
941
|
+
};
|
|
942
|
+
|
|
943
|
+
const resetFromKeyframes = () => {
|
|
944
|
+
seedSteps[JobStep.KEYFRAMES] = false;
|
|
945
|
+
seedSteps[JobStep.SEGMENTS] = false;
|
|
946
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
947
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
948
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
949
|
+
seedArtifacts.keyframeUrls = [];
|
|
950
|
+
seedArtifacts.keyframePaths = [];
|
|
951
|
+
seedArtifacts.segmentUrls = [];
|
|
952
|
+
seedArtifacts.segmentPaths = [];
|
|
953
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
954
|
+
seedArtifacts.finalVideoPath = '';
|
|
955
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
const resetFromSegments = () => {
|
|
959
|
+
seedSteps[JobStep.SEGMENTS] = false;
|
|
960
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
961
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
962
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
963
|
+
seedArtifacts.segmentUrls = [];
|
|
964
|
+
seedArtifacts.segmentPaths = [];
|
|
965
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
966
|
+
seedArtifacts.finalVideoPath = '';
|
|
967
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
if (previousTargetDuration !== targetDurationSec) {
|
|
971
|
+
resetFromScript();
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (JSON.stringify(previousModelSelections) !== JSON.stringify(modelSelections)) {
|
|
975
|
+
resetFromScript();
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (changedModelOptionCategories.length > 1) {
|
|
979
|
+
resetFromScript();
|
|
980
|
+
} else if (changedModelOptionCategories[0] === MODEL_CATEGORIES.textToText) {
|
|
981
|
+
resetFromScript();
|
|
982
|
+
} else if (changedModelOptionCategories[0] === MODEL_CATEGORIES.textToSpeech) {
|
|
983
|
+
resetFromVoice();
|
|
984
|
+
} else if (changedModelOptionCategories[0] === MODEL_CATEGORIES.textToImage) {
|
|
985
|
+
resetFromKeyframes();
|
|
986
|
+
} else if (changedModelOptionCategories[0] === MODEL_CATEGORIES.imageTextToVideo) {
|
|
987
|
+
resetFromSegments();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (
|
|
991
|
+
(seedArtifacts.keyframeSizeKey && seedArtifacts.keyframeSizeKey !== keyframeSize.key) ||
|
|
992
|
+
(!seedArtifacts.keyframeSizeKey && Number.isInteger(projectConfig.keyframeWidth) && Number.isInteger(projectConfig.keyframeHeight))
|
|
993
|
+
) {
|
|
994
|
+
seedSteps[JobStep.KEYFRAMES] = false;
|
|
995
|
+
seedSteps[JobStep.SEGMENTS] = false;
|
|
996
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
997
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
998
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
999
|
+
seedArtifacts.keyframeUrls = [];
|
|
1000
|
+
seedArtifacts.keyframePaths = [];
|
|
1001
|
+
seedArtifacts.segmentUrls = [];
|
|
1002
|
+
seedArtifacts.segmentPaths = [];
|
|
1003
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
1004
|
+
seedArtifacts.finalVideoPath = '';
|
|
1005
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if ((seedArtifacts.renderSpecVersion || 0) !== DEFAULT_VIDEO_CONFIG.renderSpecVersion) {
|
|
1009
|
+
seedSteps[JobStep.KEYFRAMES] = false;
|
|
1010
|
+
seedSteps[JobStep.SEGMENTS] = false;
|
|
1011
|
+
seedSteps[JobStep.COMPOSE] = false;
|
|
1012
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
1013
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
1014
|
+
seedArtifacts.keyframeUrls = [];
|
|
1015
|
+
seedArtifacts.keyframePaths = [];
|
|
1016
|
+
seedArtifacts.segmentUrls = [];
|
|
1017
|
+
seedArtifacts.segmentPaths = [];
|
|
1018
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
1019
|
+
seedArtifacts.finalVideoPath = '';
|
|
1020
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (JSON.stringify(seedArtifacts.subtitleOptions || {}) !== JSON.stringify(projectConfig.subtitleOptions || {})) {
|
|
1024
|
+
seedSteps[JobStep.ALIGN] = false;
|
|
1025
|
+
seedSteps[JobStep.BURNIN] = false;
|
|
1026
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const shouldArchiveCurrentAssets =
|
|
1030
|
+
forceRestart ||
|
|
1031
|
+
!seedSteps[JobStep.SCRIPT] ||
|
|
1032
|
+
!seedSteps[JobStep.VOICE] ||
|
|
1033
|
+
!seedSteps[JobStep.KEYFRAMES] ||
|
|
1034
|
+
!seedSteps[JobStep.SEGMENTS];
|
|
1035
|
+
|
|
1036
|
+
if (shouldArchiveCurrentAssets) {
|
|
1037
|
+
const archivedSnapshot = await deps.archiveProjectAssets(projectDir);
|
|
1038
|
+
if (archivedSnapshot) {
|
|
1039
|
+
seedArtifacts.voiceoverPath = '';
|
|
1040
|
+
seedArtifacts.keyframePaths = [];
|
|
1041
|
+
seedArtifacts.segmentPaths = [];
|
|
1042
|
+
seedArtifacts.finalBaseVideoPath = '';
|
|
1043
|
+
seedArtifacts.finalVideoPath = '';
|
|
1044
|
+
Object.assign(seedArtifacts, clearSubtitleArtifacts(seedArtifacts));
|
|
1045
|
+
logInfo('assets_archived_to_snapshots', { project, archivedSnapshot });
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
updateJob(jobId, {
|
|
1050
|
+
payload: {
|
|
1051
|
+
...job.payload,
|
|
1052
|
+
project,
|
|
1053
|
+
projectConfig,
|
|
1054
|
+
changedShotIndexes,
|
|
1055
|
+
activeSegmentIndex: null
|
|
1056
|
+
},
|
|
1057
|
+
status: JobStatus.RUNNING,
|
|
1058
|
+
error: null,
|
|
1059
|
+
artifacts: {
|
|
1060
|
+
...seedArtifacts,
|
|
1061
|
+
modelSelections,
|
|
1062
|
+
modelOptions,
|
|
1063
|
+
renderSpecVersion: DEFAULT_VIDEO_CONFIG.renderSpecVersion,
|
|
1064
|
+
keyframeSizeKey: keyframeSize.key,
|
|
1065
|
+
aspectRatio: projectConfig.aspectRatio,
|
|
1066
|
+
finalDurationMode: projectConfig.finalDurationMode,
|
|
1067
|
+
subtitleOptions: projectConfig.subtitleOptions,
|
|
1068
|
+
targetDurationSec,
|
|
1069
|
+
segmentDurationSec: DEFAULT_VIDEO_CONFIG.segmentDurationSec,
|
|
1070
|
+
plannedShots
|
|
1071
|
+
},
|
|
1072
|
+
steps: seedSteps
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1076
|
+
|
|
1077
|
+
if (resumeState) {
|
|
1078
|
+
logInfo('job_resumed', { jobId, project, steps: resumeState.steps });
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
let currentJob = getJob(jobId);
|
|
1082
|
+
|
|
1083
|
+
if (!currentJob.steps[JobStep.SCRIPT]) {
|
|
1084
|
+
await startStageAnalytics(JobStep.SCRIPT, { mode: 'execute' });
|
|
1085
|
+
const scriptResult = await deps.generateScript(
|
|
1086
|
+
safeStory,
|
|
1087
|
+
{
|
|
1088
|
+
targetDurationSec,
|
|
1089
|
+
modelId: modelSelections[MODEL_CATEGORIES.textToText],
|
|
1090
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.textToText]
|
|
1091
|
+
},
|
|
1092
|
+
traceBase
|
|
1093
|
+
);
|
|
1094
|
+
await persistTextAssets(projectDir, scriptResult.script, [], scriptResult.tone);
|
|
1095
|
+
updateJob(jobId, {
|
|
1096
|
+
artifacts: {
|
|
1097
|
+
...currentJob.artifacts,
|
|
1098
|
+
script: scriptResult.script,
|
|
1099
|
+
shots: [],
|
|
1100
|
+
tone: scriptResult.tone,
|
|
1101
|
+
scriptWordCount: scriptResult.scriptWordCount || null,
|
|
1102
|
+
targetWordCount: scriptResult.targetWordCount || null,
|
|
1103
|
+
storyHash,
|
|
1104
|
+
scriptSourceStoryHash: storyHash,
|
|
1105
|
+
targetDurationSec,
|
|
1106
|
+
segmentDurationSec: DEFAULT_VIDEO_CONFIG.segmentDurationSec,
|
|
1107
|
+
finalDurationMode: projectConfig.finalDurationMode,
|
|
1108
|
+
plannedShots,
|
|
1109
|
+
scriptHash: hashText(scriptResult.script),
|
|
1110
|
+
shotHashes: []
|
|
1111
|
+
},
|
|
1112
|
+
steps: {
|
|
1113
|
+
...currentJob.steps,
|
|
1114
|
+
[JobStep.SCRIPT]: true
|
|
1115
|
+
}
|
|
1116
|
+
});
|
|
1117
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1118
|
+
await finishStageAnalytics(JobStep.SCRIPT, { executed: true, status: 'succeeded' });
|
|
1119
|
+
currentJob = getJob(jobId);
|
|
1120
|
+
} else if (currentJob.artifacts.script) {
|
|
1121
|
+
await persistTextAssets(
|
|
1122
|
+
projectDir,
|
|
1123
|
+
currentJob.artifacts.script,
|
|
1124
|
+
currentJob.artifacts.shots,
|
|
1125
|
+
currentJob.artifacts.tone || 'neutral'
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
const scriptHash = hashText(currentJob.artifacts.script);
|
|
1129
|
+
const shotHashes = hashShots(currentJob.artifacts.shots);
|
|
1130
|
+
if (scriptHash !== currentJob.artifacts.scriptHash || JSON.stringify(shotHashes) !== JSON.stringify(currentJob.artifacts.shotHashes || [])) {
|
|
1131
|
+
updateJob(jobId, {
|
|
1132
|
+
artifacts: {
|
|
1133
|
+
...currentJob.artifacts,
|
|
1134
|
+
scriptHash,
|
|
1135
|
+
shotHashes
|
|
1136
|
+
}
|
|
1137
|
+
});
|
|
1138
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1139
|
+
currentJob = getJob(jobId);
|
|
1140
|
+
}
|
|
1141
|
+
await reuseStageAnalytics(JobStep.SCRIPT, { mode: 'reused' });
|
|
1142
|
+
} else {
|
|
1143
|
+
await reuseStageAnalytics(JobStep.SCRIPT, { mode: 'reused' });
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (!currentJob.steps[JobStep.VOICE]) {
|
|
1147
|
+
await startStageAnalytics(JobStep.VOICE, { mode: 'execute' });
|
|
1148
|
+
const voiceResult = await deps.generateVoiceover(
|
|
1149
|
+
currentJob.artifacts.script,
|
|
1150
|
+
{
|
|
1151
|
+
shotsCount: Math.max(1, (currentJob.artifacts.shots.length || plannedShots) - 1),
|
|
1152
|
+
segmentDurationSec: DEFAULT_VIDEO_CONFIG.segmentDurationSec,
|
|
1153
|
+
targetDurationSec,
|
|
1154
|
+
modelId: modelSelections[MODEL_CATEGORIES.textToSpeech],
|
|
1155
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.textToSpeech]
|
|
1156
|
+
},
|
|
1157
|
+
traceBase
|
|
1158
|
+
);
|
|
1159
|
+
const voiceoverPath = await deps.persistVoiceover(projectDir, voiceResult.voiceoverUrl);
|
|
1160
|
+
updateJob(jobId, {
|
|
1161
|
+
artifacts: {
|
|
1162
|
+
...currentJob.artifacts,
|
|
1163
|
+
timeline: voiceResult.timeline,
|
|
1164
|
+
voiceoverUrl: voiceResult.voiceoverUrl,
|
|
1165
|
+
voiceoverPath,
|
|
1166
|
+
ttsPlan: voiceResult.ttsPlan || null
|
|
1167
|
+
},
|
|
1168
|
+
steps: {
|
|
1169
|
+
...currentJob.steps,
|
|
1170
|
+
[JobStep.VOICE]: true
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1174
|
+
await finishStageAnalytics(JobStep.VOICE, { executed: true, status: 'succeeded' });
|
|
1175
|
+
currentJob = getJob(jobId);
|
|
1176
|
+
} else if (currentJob.artifacts.voiceoverUrl && !currentJob.artifacts.voiceoverPath) {
|
|
1177
|
+
const voiceoverPath = await deps.persistVoiceover(projectDir, currentJob.artifacts.voiceoverUrl);
|
|
1178
|
+
updateJob(jobId, {
|
|
1179
|
+
artifacts: {
|
|
1180
|
+
...currentJob.artifacts,
|
|
1181
|
+
voiceoverPath
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1185
|
+
currentJob = getJob(jobId);
|
|
1186
|
+
await reuseStageAnalytics(JobStep.VOICE, { mode: 'reused_with_repair' });
|
|
1187
|
+
} else {
|
|
1188
|
+
await reuseStageAnalytics(JobStep.VOICE, { mode: 'reused' });
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
currentJob = await alignVisualPlanToAudioDuration();
|
|
1192
|
+
|
|
1193
|
+
if (currentJob.payload.changedShotIndexes?.length > 0 && currentJob.artifacts.keyframeUrls.length > 0) {
|
|
1194
|
+
await startStageAnalytics(JobStep.KEYFRAMES, { mode: 'partial_regen' });
|
|
1195
|
+
await startStageAnalytics(JobStep.SEGMENTS, { mode: 'partial_regen' });
|
|
1196
|
+
const changed = currentJob.payload.changedShotIndexes;
|
|
1197
|
+
const keyframeUrls = [...currentJob.artifacts.keyframeUrls];
|
|
1198
|
+
const keyframePaths = [...(currentJob.artifacts.keyframePaths || [])];
|
|
1199
|
+
|
|
1200
|
+
updateJob(jobId, {
|
|
1201
|
+
payload: {
|
|
1202
|
+
...currentJob.payload,
|
|
1203
|
+
activeStep: JobStep.KEYFRAMES,
|
|
1204
|
+
activeSegmentIndex: null
|
|
1205
|
+
}
|
|
1206
|
+
});
|
|
1207
|
+
currentJob = getJob(jobId);
|
|
1208
|
+
|
|
1209
|
+
for (const shotIndex of changed) {
|
|
1210
|
+
if (shotIndex < 0 || shotIndex >= currentJob.artifacts.shots.length) {
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
const keyframeUrl = await deps.generateKeyframe(
|
|
1214
|
+
currentJob.artifacts.shots[shotIndex],
|
|
1215
|
+
currentJob.artifacts.tone || 'neutral',
|
|
1216
|
+
projectConfig.aspectRatio,
|
|
1217
|
+
shotIndex,
|
|
1218
|
+
traceBase,
|
|
1219
|
+
keyframeSize,
|
|
1220
|
+
{
|
|
1221
|
+
modelId: modelSelections[MODEL_CATEGORIES.textToImage],
|
|
1222
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.textToImage]
|
|
1223
|
+
}
|
|
1224
|
+
);
|
|
1225
|
+
keyframeUrls[shotIndex] = keyframeUrl;
|
|
1226
|
+
keyframePaths[shotIndex] = await deps.persistKeyframe(projectDir, keyframeUrl, shotIndex);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const totalShots = currentJob.artifacts.shots.length;
|
|
1230
|
+
const totalSegments = Math.max(0, totalShots - 1);
|
|
1231
|
+
const affectedSegmentIndexes = collectAffectedSegmentIndexes(changed, totalShots);
|
|
1232
|
+
|
|
1233
|
+
// If assets were archived before this run, unchanged slots may still have URLs but no local files.
|
|
1234
|
+
for (let shotIndex = 0; shotIndex < totalShots; shotIndex += 1) {
|
|
1235
|
+
if (!keyframeUrls[shotIndex]) {
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const hasPath = Boolean(keyframePaths[shotIndex]);
|
|
1240
|
+
if (!hasPath) {
|
|
1241
|
+
keyframePaths[shotIndex] = await deps.persistKeyframe(projectDir, keyframeUrls[shotIndex], shotIndex);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
const segmentUrls = [...(currentJob.artifacts.segmentUrls || [])];
|
|
1246
|
+
const segmentPaths = [...(currentJob.artifacts.segmentPaths || [])];
|
|
1247
|
+
|
|
1248
|
+
updateJob(jobId, {
|
|
1249
|
+
payload: {
|
|
1250
|
+
...currentJob.payload,
|
|
1251
|
+
activeStep: JobStep.SEGMENTS,
|
|
1252
|
+
activeSegmentIndex: null
|
|
1253
|
+
}
|
|
1254
|
+
});
|
|
1255
|
+
currentJob = getJob(jobId);
|
|
1256
|
+
|
|
1257
|
+
for (const segmentIndex of affectedSegmentIndexes) {
|
|
1258
|
+
if (segmentIndex < 0 || segmentIndex >= totalSegments) {
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
updateJob(jobId, {
|
|
1262
|
+
payload: {
|
|
1263
|
+
...getJob(jobId).payload,
|
|
1264
|
+
activeStep: JobStep.SEGMENTS,
|
|
1265
|
+
activeSegmentIndex: segmentIndex
|
|
1266
|
+
}
|
|
1267
|
+
});
|
|
1268
|
+
currentJob = getJob(jobId);
|
|
1269
|
+
const segmentUrl = await deps.generateVideoSegmentAtIndex(
|
|
1270
|
+
segmentIndex,
|
|
1271
|
+
keyframeUrls,
|
|
1272
|
+
currentJob.artifacts.timeline,
|
|
1273
|
+
currentJob.artifacts.shots,
|
|
1274
|
+
projectConfig.aspectRatio,
|
|
1275
|
+
traceBase,
|
|
1276
|
+
{
|
|
1277
|
+
modelId: modelSelections[MODEL_CATEGORIES.imageTextToVideo],
|
|
1278
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.imageTextToVideo]
|
|
1279
|
+
}
|
|
1280
|
+
);
|
|
1281
|
+
segmentUrls[segmentIndex] = segmentUrl;
|
|
1282
|
+
segmentPaths[segmentIndex] = await deps.persistSegment(projectDir, segmentUrl, segmentIndex);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
for (let segmentIndex = 0; segmentIndex < totalSegments; segmentIndex += 1) {
|
|
1286
|
+
if (!segmentUrls[segmentIndex]) {
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const hasPath = Boolean(segmentPaths[segmentIndex]);
|
|
1291
|
+
if (!hasPath) {
|
|
1292
|
+
segmentPaths[segmentIndex] = await deps.persistSegment(projectDir, segmentUrls[segmentIndex], segmentIndex);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
updateJob(jobId, {
|
|
1297
|
+
payload: {
|
|
1298
|
+
...currentJob.payload,
|
|
1299
|
+
changedShotIndexes: [],
|
|
1300
|
+
activeStep: null,
|
|
1301
|
+
activeSegmentIndex: null
|
|
1302
|
+
},
|
|
1303
|
+
artifacts: {
|
|
1304
|
+
...currentJob.artifacts,
|
|
1305
|
+
keyframeUrls,
|
|
1306
|
+
keyframePaths,
|
|
1307
|
+
segmentUrls,
|
|
1308
|
+
segmentPaths,
|
|
1309
|
+
shotHashes: hashShots(currentJob.artifacts.shots),
|
|
1310
|
+
finalBaseVideoPath: '',
|
|
1311
|
+
finalVideoPath: ''
|
|
1312
|
+
},
|
|
1313
|
+
steps: {
|
|
1314
|
+
...currentJob.steps,
|
|
1315
|
+
[JobStep.KEYFRAMES]: true,
|
|
1316
|
+
[JobStep.SEGMENTS]: true,
|
|
1317
|
+
[JobStep.COMPOSE]: false,
|
|
1318
|
+
[JobStep.ALIGN]: false,
|
|
1319
|
+
[JobStep.BURNIN]: false
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
updateJob(jobId, {
|
|
1323
|
+
artifacts: clearSubtitleArtifacts(getJob(jobId).artifacts)
|
|
1324
|
+
});
|
|
1325
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1326
|
+
await finishStageAnalytics(JobStep.KEYFRAMES, { executed: true, status: 'succeeded', details: { mode: 'partial_regen', changedShots: changed } });
|
|
1327
|
+
await finishStageAnalytics(JobStep.SEGMENTS, { executed: true, status: 'succeeded', details: { mode: 'partial_regen' } });
|
|
1328
|
+
currentJob = getJob(jobId);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
if (!currentJob.steps[JobStep.KEYFRAMES]) {
|
|
1332
|
+
await startStageAnalytics(JobStep.KEYFRAMES, { mode: 'execute' });
|
|
1333
|
+
const keyframeUrls = [...(currentJob.artifacts.keyframeUrls || [])];
|
|
1334
|
+
const keyframePaths = [...(currentJob.artifacts.keyframePaths || [])];
|
|
1335
|
+
for (let shotIndex = 0; shotIndex < currentJob.artifacts.shots.length; shotIndex += 1) {
|
|
1336
|
+
const hasUrl = Boolean(keyframeUrls[shotIndex]);
|
|
1337
|
+
let hasPath = Boolean(keyframePaths[shotIndex]);
|
|
1338
|
+
|
|
1339
|
+
if (hasPath && !(await fileExists(keyframePaths[shotIndex]))) {
|
|
1340
|
+
keyframePaths[shotIndex] = '';
|
|
1341
|
+
hasPath = false;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (hasUrl && hasPath) {
|
|
1345
|
+
continue;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (!hasUrl) {
|
|
1349
|
+
keyframeUrls[shotIndex] = await deps.generateKeyframe(
|
|
1350
|
+
currentJob.artifacts.shots[shotIndex],
|
|
1351
|
+
currentJob.artifacts.tone || 'neutral',
|
|
1352
|
+
projectConfig.aspectRatio,
|
|
1353
|
+
shotIndex,
|
|
1354
|
+
traceBase,
|
|
1355
|
+
keyframeSize,
|
|
1356
|
+
{
|
|
1357
|
+
modelId: modelSelections[MODEL_CATEGORIES.textToImage],
|
|
1358
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.textToImage]
|
|
1359
|
+
}
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
keyframePaths[shotIndex] = await deps.persistKeyframe(projectDir, keyframeUrls[shotIndex], shotIndex);
|
|
1364
|
+
|
|
1365
|
+
updateJob(jobId, {
|
|
1366
|
+
artifacts: {
|
|
1367
|
+
...currentJob.artifacts,
|
|
1368
|
+
keyframeUrls,
|
|
1369
|
+
keyframePaths
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1373
|
+
currentJob = getJob(jobId);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
updateJob(jobId, {
|
|
1377
|
+
artifacts: {
|
|
1378
|
+
...currentJob.artifacts,
|
|
1379
|
+
keyframeUrls,
|
|
1380
|
+
keyframePaths
|
|
1381
|
+
},
|
|
1382
|
+
steps: {
|
|
1383
|
+
...currentJob.steps,
|
|
1384
|
+
[JobStep.KEYFRAMES]: true
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1388
|
+
await finishStageAnalytics(JobStep.KEYFRAMES, { executed: true, status: 'succeeded' });
|
|
1389
|
+
currentJob = getJob(jobId);
|
|
1390
|
+
} else if (
|
|
1391
|
+
currentJob.artifacts.keyframeUrls.length > 0 &&
|
|
1392
|
+
(!currentJob.artifacts.keyframePaths || currentJob.artifacts.keyframePaths.length === 0)
|
|
1393
|
+
) {
|
|
1394
|
+
const keyframePaths = await deps.persistKeyframes(projectDir, currentJob.artifacts.keyframeUrls);
|
|
1395
|
+
updateJob(jobId, {
|
|
1396
|
+
artifacts: {
|
|
1397
|
+
...currentJob.artifacts,
|
|
1398
|
+
keyframePaths
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1402
|
+
currentJob = getJob(jobId);
|
|
1403
|
+
await reuseStageAnalytics(JobStep.KEYFRAMES, { mode: 'reused_with_repair' });
|
|
1404
|
+
} else {
|
|
1405
|
+
await reuseStageAnalytics(JobStep.KEYFRAMES, { mode: 'reused' });
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (!currentJob.steps[JobStep.SEGMENTS]) {
|
|
1409
|
+
await startStageAnalytics(JobStep.SEGMENTS, { mode: 'execute' });
|
|
1410
|
+
const segmentUrls = [...(currentJob.artifacts.segmentUrls || [])];
|
|
1411
|
+
const segmentPaths = [...(currentJob.artifacts.segmentPaths || [])];
|
|
1412
|
+
updateJob(jobId, {
|
|
1413
|
+
payload: {
|
|
1414
|
+
...currentJob.payload,
|
|
1415
|
+
activeStep: JobStep.SEGMENTS,
|
|
1416
|
+
activeSegmentIndex: null
|
|
1417
|
+
}
|
|
1418
|
+
});
|
|
1419
|
+
currentJob = getJob(jobId);
|
|
1420
|
+
for (let segmentIndex = 0; segmentIndex < Math.max(0, currentJob.artifacts.shots.length - 1); segmentIndex += 1) {
|
|
1421
|
+
const hasUrl = Boolean(segmentUrls[segmentIndex]);
|
|
1422
|
+
let hasPath = Boolean(segmentPaths[segmentIndex]);
|
|
1423
|
+
|
|
1424
|
+
if (hasPath && !(await fileExists(segmentPaths[segmentIndex]))) {
|
|
1425
|
+
segmentPaths[segmentIndex] = '';
|
|
1426
|
+
hasPath = false;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
if (hasUrl && hasPath) {
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
if (!hasUrl) {
|
|
1434
|
+
updateJob(jobId, {
|
|
1435
|
+
payload: {
|
|
1436
|
+
...getJob(jobId).payload,
|
|
1437
|
+
activeStep: JobStep.SEGMENTS,
|
|
1438
|
+
activeSegmentIndex: segmentIndex
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
currentJob = getJob(jobId);
|
|
1442
|
+
segmentUrls[segmentIndex] = await deps.generateVideoSegmentAtIndex(
|
|
1443
|
+
segmentIndex,
|
|
1444
|
+
currentJob.artifacts.keyframeUrls,
|
|
1445
|
+
currentJob.artifacts.timeline,
|
|
1446
|
+
currentJob.artifacts.shots,
|
|
1447
|
+
projectConfig.aspectRatio,
|
|
1448
|
+
traceBase,
|
|
1449
|
+
{
|
|
1450
|
+
modelId: modelSelections[MODEL_CATEGORIES.imageTextToVideo],
|
|
1451
|
+
modelOptions: modelOptions[MODEL_CATEGORIES.imageTextToVideo]
|
|
1452
|
+
}
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
segmentPaths[segmentIndex] = await deps.persistSegment(projectDir, segmentUrls[segmentIndex], segmentIndex);
|
|
1457
|
+
|
|
1458
|
+
updateJob(jobId, {
|
|
1459
|
+
artifacts: {
|
|
1460
|
+
...currentJob.artifacts,
|
|
1461
|
+
segmentUrls,
|
|
1462
|
+
segmentPaths
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1466
|
+
currentJob = getJob(jobId);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
updateJob(jobId, {
|
|
1470
|
+
payload: {
|
|
1471
|
+
...currentJob.payload,
|
|
1472
|
+
activeStep: null,
|
|
1473
|
+
activeSegmentIndex: null
|
|
1474
|
+
},
|
|
1475
|
+
artifacts: {
|
|
1476
|
+
...currentJob.artifacts,
|
|
1477
|
+
segmentUrls,
|
|
1478
|
+
segmentPaths
|
|
1479
|
+
},
|
|
1480
|
+
steps: {
|
|
1481
|
+
...currentJob.steps,
|
|
1482
|
+
[JobStep.SEGMENTS]: true
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1486
|
+
await finishStageAnalytics(JobStep.SEGMENTS, { executed: true, status: 'succeeded' });
|
|
1487
|
+
currentJob = getJob(jobId);
|
|
1488
|
+
} else if (
|
|
1489
|
+
currentJob.artifacts.segmentUrls.length > 0 &&
|
|
1490
|
+
(!currentJob.artifacts.segmentPaths || currentJob.artifacts.segmentPaths.length === 0)
|
|
1491
|
+
) {
|
|
1492
|
+
const segmentPaths = await deps.persistSegments(projectDir, currentJob.artifacts.segmentUrls);
|
|
1493
|
+
updateJob(jobId, {
|
|
1494
|
+
artifacts: {
|
|
1495
|
+
...currentJob.artifacts,
|
|
1496
|
+
segmentPaths
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1500
|
+
currentJob = getJob(jobId);
|
|
1501
|
+
await reuseStageAnalytics(JobStep.SEGMENTS, { mode: 'reused_with_repair' });
|
|
1502
|
+
} else {
|
|
1503
|
+
await reuseStageAnalytics(JobStep.SEGMENTS, { mode: 'reused' });
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
if (!currentJob.steps[JobStep.COMPOSE]) {
|
|
1507
|
+
await startStageAnalytics(JobStep.COMPOSE, { mode: 'execute' });
|
|
1508
|
+
const composed = await deps.composeFinalVideo({
|
|
1509
|
+
projectDir,
|
|
1510
|
+
segmentUrls: currentJob.artifacts.segmentUrls,
|
|
1511
|
+
segmentPaths: currentJob.artifacts.segmentPaths,
|
|
1512
|
+
voiceoverPath: currentJob.artifacts.voiceoverPath,
|
|
1513
|
+
voiceoverUrl: currentJob.artifacts.voiceoverUrl,
|
|
1514
|
+
keyframePaths: currentJob.artifacts.keyframePaths,
|
|
1515
|
+
finalDurationMode: projectConfig.finalDurationMode
|
|
1516
|
+
});
|
|
1517
|
+
|
|
1518
|
+
updateJob(jobId, {
|
|
1519
|
+
artifacts: {
|
|
1520
|
+
...currentJob.artifacts,
|
|
1521
|
+
finalBaseVideoPath: composed.finalVideoPath,
|
|
1522
|
+
finalVideoPath: composed.finalVideoPath,
|
|
1523
|
+
voiceoverPath: composed.voiceoverPath,
|
|
1524
|
+
keyframePaths: composed.keyframePaths,
|
|
1525
|
+
segmentPaths: composed.segmentPaths,
|
|
1526
|
+
scriptHash: hashText(currentJob.artifacts.script),
|
|
1527
|
+
shotHashes: hashShots(currentJob.artifacts.shots)
|
|
1528
|
+
},
|
|
1529
|
+
steps: {
|
|
1530
|
+
...currentJob.steps,
|
|
1531
|
+
[JobStep.COMPOSE]: true,
|
|
1532
|
+
[JobStep.ALIGN]: false,
|
|
1533
|
+
[JobStep.BURNIN]: false
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
updateJob(jobId, {
|
|
1537
|
+
artifacts: clearSubtitleArtifacts(getJob(jobId).artifacts)
|
|
1538
|
+
});
|
|
1539
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1540
|
+
await finishStageAnalytics(JobStep.COMPOSE, { executed: true, status: 'succeeded' });
|
|
1541
|
+
currentJob = getJob(jobId);
|
|
1542
|
+
} else {
|
|
1543
|
+
if (!currentJob.artifacts.finalBaseVideoPath && currentJob.artifacts.finalVideoPath) {
|
|
1544
|
+
updateJob(jobId, {
|
|
1545
|
+
artifacts: {
|
|
1546
|
+
...currentJob.artifacts,
|
|
1547
|
+
finalBaseVideoPath: currentJob.artifacts.finalVideoPath
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1551
|
+
currentJob = getJob(jobId);
|
|
1552
|
+
}
|
|
1553
|
+
await reuseStageAnalytics(JobStep.COMPOSE, { mode: 'reused' });
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
if (projectConfig.subtitleOptions?.enabled) {
|
|
1557
|
+
if (!currentJob.steps[JobStep.ALIGN]) {
|
|
1558
|
+
await startStageAnalytics(JobStep.ALIGN, { mode: 'execute' });
|
|
1559
|
+
const subtitleSourceVideo = currentJob.artifacts.finalBaseVideoPath || currentJob.artifacts.finalVideoPath;
|
|
1560
|
+
const durationForSubtitles = Number.isFinite(currentJob.artifacts.audioDurationSec)
|
|
1561
|
+
? currentJob.artifacts.audioDurationSec
|
|
1562
|
+
: await deps.probeMediaDurationSeconds(subtitleSourceVideo);
|
|
1563
|
+
|
|
1564
|
+
const aligned = await deps.alignSubtitlesToVideo({
|
|
1565
|
+
projectDir,
|
|
1566
|
+
videoPath: subtitleSourceVideo,
|
|
1567
|
+
script: currentJob.artifacts.script,
|
|
1568
|
+
totalDurationSec: durationForSubtitles,
|
|
1569
|
+
subtitleOptions: projectConfig.subtitleOptions
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
updateJob(jobId, {
|
|
1573
|
+
artifacts: {
|
|
1574
|
+
...currentJob.artifacts,
|
|
1575
|
+
...aligned
|
|
1576
|
+
},
|
|
1577
|
+
steps: {
|
|
1578
|
+
...currentJob.steps,
|
|
1579
|
+
[JobStep.ALIGN]: true
|
|
1580
|
+
}
|
|
1581
|
+
});
|
|
1582
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1583
|
+
await finishStageAnalytics(JobStep.ALIGN, { executed: true, status: 'succeeded' });
|
|
1584
|
+
currentJob = getJob(jobId);
|
|
1585
|
+
} else {
|
|
1586
|
+
await reuseStageAnalytics(JobStep.ALIGN, { mode: 'reused' });
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (!currentJob.steps[JobStep.BURNIN]) {
|
|
1590
|
+
await startStageAnalytics(JobStep.BURNIN, { mode: 'execute' });
|
|
1591
|
+
const burned = await deps.burnInSubtitles({
|
|
1592
|
+
projectDir,
|
|
1593
|
+
videoPath: currentJob.artifacts.finalBaseVideoPath || currentJob.artifacts.finalVideoPath,
|
|
1594
|
+
subtitleAssPath: currentJob.artifacts.subtitleAssPath
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
updateJob(jobId, {
|
|
1598
|
+
artifacts: {
|
|
1599
|
+
...currentJob.artifacts,
|
|
1600
|
+
...burned,
|
|
1601
|
+
finalVideoPath: burned.finalCaptionedVideoPath
|
|
1602
|
+
},
|
|
1603
|
+
steps: {
|
|
1604
|
+
...currentJob.steps,
|
|
1605
|
+
[JobStep.BURNIN]: true
|
|
1606
|
+
}
|
|
1607
|
+
});
|
|
1608
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1609
|
+
await finishStageAnalytics(JobStep.BURNIN, { executed: true, status: 'succeeded' });
|
|
1610
|
+
currentJob = getJob(jobId);
|
|
1611
|
+
} else {
|
|
1612
|
+
await reuseStageAnalytics(JobStep.BURNIN, { mode: 'reused' });
|
|
1613
|
+
}
|
|
1614
|
+
} else {
|
|
1615
|
+
updateJob(jobId, {
|
|
1616
|
+
artifacts: {
|
|
1617
|
+
...clearSubtitleArtifacts(currentJob.artifacts),
|
|
1618
|
+
finalVideoPath: currentJob.artifacts.finalBaseVideoPath || currentJob.artifacts.finalVideoPath
|
|
1619
|
+
},
|
|
1620
|
+
steps: {
|
|
1621
|
+
...currentJob.steps,
|
|
1622
|
+
[JobStep.ALIGN]: true,
|
|
1623
|
+
[JobStep.BURNIN]: true
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
await persistCheckpoint(getJob(jobId), null, deps);
|
|
1627
|
+
await reuseStageAnalytics(JobStep.ALIGN, { mode: 'disabled' });
|
|
1628
|
+
await reuseStageAnalytics(JobStep.BURNIN, { mode: 'disabled' });
|
|
1629
|
+
currentJob = getJob(jobId);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const completed = updateJob(jobId, {
|
|
1633
|
+
status: JobStatus.COMPLETED,
|
|
1634
|
+
artifacts: getJob(jobId).artifacts
|
|
1635
|
+
});
|
|
1636
|
+
|
|
1637
|
+
await persistCheckpoint(completed, null, deps);
|
|
1638
|
+
const completedPredictions = await deps.collectRunPredictions(projectDir, analyticsRunId);
|
|
1639
|
+
analyticsRun = deps.finalizeRunRecord(analyticsRun, completedPredictions, { status: 'completed' });
|
|
1640
|
+
await saveAnalytics();
|
|
1641
|
+
logInfo('job_completed', { jobId, finalVideoPath: completed.artifacts.finalVideoPath });
|
|
1642
|
+
|
|
1643
|
+
return completed;
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
logError('job_failed', { jobId, error: error.message });
|
|
1646
|
+
const failed = updateJob(jobId, {
|
|
1647
|
+
status: JobStatus.FAILED,
|
|
1648
|
+
error: error.message
|
|
1649
|
+
});
|
|
1650
|
+
await persistCheckpoint(failed, null, deps);
|
|
1651
|
+
|
|
1652
|
+
if (analyticsRun && analyticsProject) {
|
|
1653
|
+
const failedPredictions = analyticsProjectDir
|
|
1654
|
+
? await deps.collectRunPredictions(analyticsProjectDir, analyticsRun.runId)
|
|
1655
|
+
: [];
|
|
1656
|
+
analyticsRun = deps.finalizeRunRecord(analyticsRun, failedPredictions, {
|
|
1657
|
+
status: 'failed',
|
|
1658
|
+
error: error.message
|
|
1659
|
+
});
|
|
1660
|
+
await deps.writeRunRecord(analyticsProject, analyticsRun);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
return failed;
|
|
1664
|
+
} finally {
|
|
1665
|
+
if (lockedProject) {
|
|
1666
|
+
releaseProjectRunLock(lockedProject, jobId);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
}
|