@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,647 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { getProjectMetadataBackend } from '../../backends/projectMetadataBackend.js';
|
|
3
|
+
import { syncProjectSnapshot } from '../../backends/outputBackend.js';
|
|
4
|
+
import { env } from '../../config/env.js';
|
|
5
|
+
import {
|
|
6
|
+
MODEL_CATEGORIES,
|
|
7
|
+
resolveKeyframeSize,
|
|
8
|
+
resolveProjectModelOptions,
|
|
9
|
+
resolveProjectModelSelections
|
|
10
|
+
} from '../../config/models.js';
|
|
11
|
+
import { regenerateProjectAsset, runPipeline } from '../../pipeline/orchestrator.js';
|
|
12
|
+
import { createJob, findActiveJobByProject } from '../../store/jobStore.js';
|
|
13
|
+
import { JobStep, emptyStepState } from '../../types/job.js';
|
|
14
|
+
import {
|
|
15
|
+
ensureProject,
|
|
16
|
+
getProjectDir,
|
|
17
|
+
readProjectConfig,
|
|
18
|
+
readProjectRunState,
|
|
19
|
+
readProjectScriptAsset,
|
|
20
|
+
readProjectStory,
|
|
21
|
+
resolveProjectName,
|
|
22
|
+
writeProjectArtifacts,
|
|
23
|
+
writeProjectConfig,
|
|
24
|
+
writeProjectRunState,
|
|
25
|
+
writeProjectScriptAsset,
|
|
26
|
+
writeProjectStory
|
|
27
|
+
} from '../../store/projectStore.js';
|
|
28
|
+
|
|
29
|
+
function parseBoundedLimit(input, { defaultValue, maxValue }) {
|
|
30
|
+
const parsed = Number(input);
|
|
31
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
32
|
+
return defaultValue;
|
|
33
|
+
}
|
|
34
|
+
return Math.min(parsed, maxValue);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseBoolean(input, defaultValue) {
|
|
38
|
+
if (input === undefined || input === null) {
|
|
39
|
+
return defaultValue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const value = String(input).trim().toLowerCase();
|
|
43
|
+
if (value === 'true') return true;
|
|
44
|
+
if (value === 'false') return false;
|
|
45
|
+
return defaultValue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isPlainObject(value) {
|
|
49
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isStringArray(value) {
|
|
53
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function parseContentUpdatePayload(body) {
|
|
57
|
+
const payload = body || {};
|
|
58
|
+
const hasShots = payload.shots !== undefined;
|
|
59
|
+
const hasPrompts = payload.prompts !== undefined;
|
|
60
|
+
|
|
61
|
+
if (payload.story !== undefined && typeof payload.story !== 'string') {
|
|
62
|
+
throw new Error('story must be a string when provided');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (payload.script !== undefined && typeof payload.script !== 'string') {
|
|
66
|
+
throw new Error('script must be a string when provided');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (payload.tone !== undefined && typeof payload.tone !== 'string') {
|
|
70
|
+
throw new Error('tone must be a string when provided');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (hasShots && hasPrompts) {
|
|
74
|
+
throw new Error('provide only one of shots or prompts');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const nextShots = hasShots ? payload.shots : payload.prompts;
|
|
78
|
+
if (nextShots !== undefined && !isStringArray(nextShots)) {
|
|
79
|
+
throw new Error('shots/prompts must be an array of strings when provided');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
payload.story === undefined
|
|
84
|
+
&& payload.script === undefined
|
|
85
|
+
&& payload.tone === undefined
|
|
86
|
+
&& nextShots === undefined
|
|
87
|
+
) {
|
|
88
|
+
throw new Error('at least one of story, script, shots/prompts, or tone is required');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
story: payload.story,
|
|
93
|
+
script: payload.script,
|
|
94
|
+
tone: payload.tone,
|
|
95
|
+
shots: nextShots
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function cloneRunStateArtifacts(runState) {
|
|
100
|
+
const artifacts = runState?.artifacts || {};
|
|
101
|
+
return {
|
|
102
|
+
...artifacts,
|
|
103
|
+
keyframeUrls: [...(artifacts.keyframeUrls || [])],
|
|
104
|
+
keyframePaths: [...(artifacts.keyframePaths || [])],
|
|
105
|
+
segmentUrls: [...(artifacts.segmentUrls || [])],
|
|
106
|
+
segmentPaths: [...(artifacts.segmentPaths || [])],
|
|
107
|
+
shots: [...(artifacts.shots || [])],
|
|
108
|
+
timeline: [...(artifacts.timeline || [])],
|
|
109
|
+
shotHashes: [...(artifacts.shotHashes || [])]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function applyRunStateInvalidationForConfigChange({ runState, previousConfig, nextConfig }) {
|
|
114
|
+
if (!runState || typeof runState !== 'object') {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const steps = {
|
|
119
|
+
...emptyStepState(),
|
|
120
|
+
...(runState.steps || {})
|
|
121
|
+
};
|
|
122
|
+
const artifacts = cloneRunStateArtifacts(runState);
|
|
123
|
+
const previousSizeKey = resolveKeyframeSize(previousConfig || {}).key;
|
|
124
|
+
const nextSizeKey = resolveKeyframeSize(nextConfig || {}).key;
|
|
125
|
+
const durationChanged = previousConfig?.targetDurationSec !== nextConfig?.targetDurationSec;
|
|
126
|
+
const aspectChanged = previousConfig?.aspectRatio !== nextConfig?.aspectRatio;
|
|
127
|
+
const sizeChanged = previousSizeKey !== nextSizeKey;
|
|
128
|
+
const previousModels = resolveProjectModelSelections(previousConfig?.models);
|
|
129
|
+
const nextModels = resolveProjectModelSelections(nextConfig?.models);
|
|
130
|
+
const previousModelOptions = resolveProjectModelOptions(previousConfig?.modelOptions, previousModels);
|
|
131
|
+
const nextModelOptions = resolveProjectModelOptions(nextConfig?.modelOptions, nextModels);
|
|
132
|
+
const changedModelOptionCategories = Object.values(MODEL_CATEGORIES).filter(
|
|
133
|
+
(category) => JSON.stringify(previousModelOptions[category]) !== JSON.stringify(nextModelOptions[category])
|
|
134
|
+
);
|
|
135
|
+
const modelsChanged = JSON.stringify(previousModels) !== JSON.stringify(nextModels);
|
|
136
|
+
|
|
137
|
+
const resetFromScript = () => {
|
|
138
|
+
steps[JobStep.SCRIPT] = false;
|
|
139
|
+
steps[JobStep.VOICE] = false;
|
|
140
|
+
steps[JobStep.KEYFRAMES] = false;
|
|
141
|
+
steps[JobStep.SEGMENTS] = false;
|
|
142
|
+
steps[JobStep.COMPOSE] = false;
|
|
143
|
+
steps[JobStep.ALIGN] = false;
|
|
144
|
+
steps[JobStep.BURNIN] = false;
|
|
145
|
+
artifacts.script = '';
|
|
146
|
+
artifacts.tone = '';
|
|
147
|
+
artifacts.shots = [];
|
|
148
|
+
artifacts.timeline = [];
|
|
149
|
+
artifacts.voiceoverUrl = '';
|
|
150
|
+
artifacts.voiceoverPath = '';
|
|
151
|
+
artifacts.keyframeUrls = [];
|
|
152
|
+
artifacts.keyframePaths = [];
|
|
153
|
+
artifacts.segmentUrls = [];
|
|
154
|
+
artifacts.segmentPaths = [];
|
|
155
|
+
artifacts.finalBaseVideoPath = '';
|
|
156
|
+
artifacts.finalCaptionedVideoPath = '';
|
|
157
|
+
artifacts.subtitleSeedPath = '';
|
|
158
|
+
artifacts.subtitleAlignedSrtPath = '';
|
|
159
|
+
artifacts.subtitleAssPath = '';
|
|
160
|
+
artifacts.finalVideoPath = '';
|
|
161
|
+
artifacts.scriptHash = '';
|
|
162
|
+
artifacts.shotHashes = [];
|
|
163
|
+
artifacts.scriptSourceStoryHash = '';
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const resetFromVoice = () => {
|
|
167
|
+
steps[JobStep.VOICE] = false;
|
|
168
|
+
steps[JobStep.KEYFRAMES] = false;
|
|
169
|
+
steps[JobStep.SEGMENTS] = false;
|
|
170
|
+
steps[JobStep.COMPOSE] = false;
|
|
171
|
+
steps[JobStep.ALIGN] = false;
|
|
172
|
+
steps[JobStep.BURNIN] = false;
|
|
173
|
+
artifacts.timeline = [];
|
|
174
|
+
artifacts.voiceoverUrl = '';
|
|
175
|
+
artifacts.voiceoverPath = '';
|
|
176
|
+
artifacts.keyframeUrls = [];
|
|
177
|
+
artifacts.keyframePaths = [];
|
|
178
|
+
artifacts.segmentUrls = [];
|
|
179
|
+
artifacts.segmentPaths = [];
|
|
180
|
+
artifacts.finalBaseVideoPath = '';
|
|
181
|
+
artifacts.finalCaptionedVideoPath = '';
|
|
182
|
+
artifacts.subtitleSeedPath = '';
|
|
183
|
+
artifacts.subtitleAlignedSrtPath = '';
|
|
184
|
+
artifacts.subtitleAssPath = '';
|
|
185
|
+
artifacts.finalVideoPath = '';
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const resetFromKeyframes = () => {
|
|
189
|
+
steps[JobStep.KEYFRAMES] = false;
|
|
190
|
+
steps[JobStep.SEGMENTS] = false;
|
|
191
|
+
steps[JobStep.COMPOSE] = false;
|
|
192
|
+
steps[JobStep.ALIGN] = false;
|
|
193
|
+
steps[JobStep.BURNIN] = false;
|
|
194
|
+
artifacts.keyframeUrls = [];
|
|
195
|
+
artifacts.keyframePaths = [];
|
|
196
|
+
artifacts.segmentUrls = [];
|
|
197
|
+
artifacts.segmentPaths = [];
|
|
198
|
+
artifacts.finalBaseVideoPath = '';
|
|
199
|
+
artifacts.finalCaptionedVideoPath = '';
|
|
200
|
+
artifacts.subtitleSeedPath = '';
|
|
201
|
+
artifacts.subtitleAlignedSrtPath = '';
|
|
202
|
+
artifacts.subtitleAssPath = '';
|
|
203
|
+
artifacts.finalVideoPath = '';
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const resetFromSegments = () => {
|
|
207
|
+
steps[JobStep.SEGMENTS] = false;
|
|
208
|
+
steps[JobStep.COMPOSE] = false;
|
|
209
|
+
steps[JobStep.ALIGN] = false;
|
|
210
|
+
steps[JobStep.BURNIN] = false;
|
|
211
|
+
artifacts.segmentUrls = [];
|
|
212
|
+
artifacts.segmentPaths = [];
|
|
213
|
+
artifacts.finalBaseVideoPath = '';
|
|
214
|
+
artifacts.finalCaptionedVideoPath = '';
|
|
215
|
+
artifacts.subtitleSeedPath = '';
|
|
216
|
+
artifacts.subtitleAlignedSrtPath = '';
|
|
217
|
+
artifacts.subtitleAssPath = '';
|
|
218
|
+
artifacts.finalVideoPath = '';
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
if (durationChanged || modelsChanged) {
|
|
222
|
+
resetFromScript();
|
|
223
|
+
} else if (changedModelOptionCategories.length > 1) {
|
|
224
|
+
resetFromScript();
|
|
225
|
+
} else if (changedModelOptionCategories[0] === MODEL_CATEGORIES.textToText) {
|
|
226
|
+
resetFromScript();
|
|
227
|
+
} else if (changedModelOptionCategories[0] === MODEL_CATEGORIES.textToSpeech) {
|
|
228
|
+
resetFromVoice();
|
|
229
|
+
} else if (changedModelOptionCategories[0] === MODEL_CATEGORIES.textToImage) {
|
|
230
|
+
resetFromKeyframes();
|
|
231
|
+
} else if (changedModelOptionCategories[0] === MODEL_CATEGORIES.imageTextToVideo) {
|
|
232
|
+
resetFromSegments();
|
|
233
|
+
} else if (aspectChanged || sizeChanged) {
|
|
234
|
+
resetFromKeyframes();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
artifacts.modelSelections = nextModels;
|
|
238
|
+
artifacts.modelOptions = nextModelOptions;
|
|
239
|
+
artifacts.subtitleOptions = nextConfig?.subtitleOptions;
|
|
240
|
+
|
|
241
|
+
if (JSON.stringify(previousConfig?.subtitleOptions || {}) !== JSON.stringify(nextConfig?.subtitleOptions || {})) {
|
|
242
|
+
steps[JobStep.ALIGN] = false;
|
|
243
|
+
steps[JobStep.BURNIN] = false;
|
|
244
|
+
artifacts.finalCaptionedVideoPath = '';
|
|
245
|
+
artifacts.subtitleSeedPath = '';
|
|
246
|
+
artifacts.subtitleAlignedSrtPath = '';
|
|
247
|
+
artifacts.subtitleAssPath = '';
|
|
248
|
+
artifacts.finalVideoPath = artifacts.finalBaseVideoPath || artifacts.finalVideoPath;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
status: runState.status,
|
|
253
|
+
error: runState.error || null,
|
|
254
|
+
steps,
|
|
255
|
+
artifacts,
|
|
256
|
+
updatedAt: new Date().toISOString()
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function applyRunStateInvalidationForContentChange({ runState, updates }) {
|
|
261
|
+
if (!runState || typeof runState !== 'object') {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (updates.script === undefined) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const steps = {
|
|
270
|
+
...emptyStepState(),
|
|
271
|
+
...(runState.steps || {})
|
|
272
|
+
};
|
|
273
|
+
const artifacts = cloneRunStateArtifacts(runState);
|
|
274
|
+
|
|
275
|
+
artifacts.script = updates.script;
|
|
276
|
+
artifacts.scriptHash = '';
|
|
277
|
+
artifacts.scriptWordCount = null;
|
|
278
|
+
artifacts.targetWordCount = null;
|
|
279
|
+
artifacts.shots = [];
|
|
280
|
+
artifacts.shotHashes = [];
|
|
281
|
+
artifacts.timeline = [];
|
|
282
|
+
artifacts.voiceoverUrl = '';
|
|
283
|
+
artifacts.voiceoverPath = '';
|
|
284
|
+
artifacts.keyframeUrls = [];
|
|
285
|
+
artifacts.keyframePaths = [];
|
|
286
|
+
artifacts.segmentUrls = [];
|
|
287
|
+
artifacts.segmentPaths = [];
|
|
288
|
+
artifacts.finalBaseVideoPath = '';
|
|
289
|
+
artifacts.finalCaptionedVideoPath = '';
|
|
290
|
+
artifacts.subtitleSeedPath = '';
|
|
291
|
+
artifacts.subtitleAlignedSrtPath = '';
|
|
292
|
+
artifacts.subtitleAssPath = '';
|
|
293
|
+
artifacts.finalVideoPath = '';
|
|
294
|
+
|
|
295
|
+
steps[JobStep.SCRIPT] = true;
|
|
296
|
+
steps[JobStep.VOICE] = false;
|
|
297
|
+
steps[JobStep.KEYFRAMES] = false;
|
|
298
|
+
steps[JobStep.SEGMENTS] = false;
|
|
299
|
+
steps[JobStep.COMPOSE] = false;
|
|
300
|
+
steps[JobStep.ALIGN] = false;
|
|
301
|
+
steps[JobStep.BURNIN] = false;
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
status: runState.status,
|
|
305
|
+
error: runState.error || null,
|
|
306
|
+
steps,
|
|
307
|
+
artifacts,
|
|
308
|
+
updatedAt: new Date().toISOString()
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function createProjectsRouter(deps = {}) {
|
|
313
|
+
const router = express.Router();
|
|
314
|
+
const syncProjectSnapshotFn = deps.syncProjectSnapshotFn || syncProjectSnapshot;
|
|
315
|
+
const findActiveJobByProjectFn = deps.findActiveJobByProjectFn || findActiveJobByProject;
|
|
316
|
+
const createJobFn = deps.createJobFn || createJob;
|
|
317
|
+
const runPipelineFn = deps.runPipelineFn || runPipeline;
|
|
318
|
+
const regenerateProjectAssetFn = deps.regenerateProjectAssetFn || regenerateProjectAsset;
|
|
319
|
+
|
|
320
|
+
router.get('/', async (_req, res) => {
|
|
321
|
+
try {
|
|
322
|
+
const backend = getProjectMetadataBackend();
|
|
323
|
+
const projects = await backend.listProjects();
|
|
324
|
+
res.json({ projects });
|
|
325
|
+
} catch (error) {
|
|
326
|
+
res.status(500).json({ error: error.message });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
router.post('/', async (req, res) => {
|
|
331
|
+
try {
|
|
332
|
+
const { project, story, config, metadata } = req.body || {};
|
|
333
|
+
if (!project) {
|
|
334
|
+
res.status(400).json({ error: 'project is required' });
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (story !== undefined && typeof story !== 'string') {
|
|
339
|
+
res.status(400).json({ error: 'story must be a string when provided' });
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (config !== undefined && !isPlainObject(config)) {
|
|
344
|
+
res.status(400).json({ error: 'config must be an object when provided' });
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (metadata !== undefined && !isPlainObject(metadata)) {
|
|
349
|
+
res.status(400).json({ error: 'metadata must be an object when provided' });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const backend = getProjectMetadataBackend();
|
|
354
|
+
const created = await backend.createProject({ project, story, config, metadata });
|
|
355
|
+
res.status(201).json(created);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
res.status(400).json({ error: error.message });
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
router.get('/:project', async (req, res) => {
|
|
362
|
+
try {
|
|
363
|
+
const backend = getProjectMetadataBackend();
|
|
364
|
+
const details = await backend.getProject(req.params.project);
|
|
365
|
+
res.json(details);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
res.status(404).json({ error: error.message });
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
router.get('/:project/logs', async (req, res) => {
|
|
372
|
+
try {
|
|
373
|
+
const backend = getProjectMetadataBackend();
|
|
374
|
+
const includeEntries = parseBoolean(req.query.includeEntries, false);
|
|
375
|
+
const limit = parseBoundedLimit(req.query.limit, {
|
|
376
|
+
defaultValue: env.apiDefaultLogsLimit,
|
|
377
|
+
maxValue: env.apiMaxLogsLimit
|
|
378
|
+
});
|
|
379
|
+
const details = await backend.getRequestLogs(req.params.project, { limit, includeEntries });
|
|
380
|
+
res.json(details);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
res.status(400).json({ error: error.message });
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
router.get('/:project/prompts', async (req, res) => {
|
|
387
|
+
try {
|
|
388
|
+
const backend = getProjectMetadataBackend();
|
|
389
|
+
const limit = parseBoundedLimit(req.query.limit, {
|
|
390
|
+
defaultValue: env.apiDefaultLogsLimit,
|
|
391
|
+
maxValue: env.apiMaxLogsLimit
|
|
392
|
+
});
|
|
393
|
+
const details = await backend.getPromptData(req.params.project, { limit });
|
|
394
|
+
res.json(details);
|
|
395
|
+
} catch (error) {
|
|
396
|
+
res.status(400).json({ error: error.message });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
router.get('/:project/artifacts', async (req, res) => {
|
|
401
|
+
try {
|
|
402
|
+
const backend = getProjectMetadataBackend();
|
|
403
|
+
const details = await backend.getArtifacts(req.params.project);
|
|
404
|
+
res.json(details);
|
|
405
|
+
} catch (error) {
|
|
406
|
+
res.status(400).json({ error: error.message });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
router.get('/:project/sync', async (req, res) => {
|
|
411
|
+
try {
|
|
412
|
+
const backend = getProjectMetadataBackend();
|
|
413
|
+
const details = await backend.getSyncStatus(req.params.project);
|
|
414
|
+
res.json(details);
|
|
415
|
+
} catch (error) {
|
|
416
|
+
res.status(400).json({ error: error.message });
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
router.get('/:project/snapshots', async (req, res) => {
|
|
421
|
+
try {
|
|
422
|
+
const backend = getProjectMetadataBackend();
|
|
423
|
+
const details = await backend.getSnapshots(req.params.project);
|
|
424
|
+
res.json(details);
|
|
425
|
+
} catch (error) {
|
|
426
|
+
res.status(400).json({ error: error.message });
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
router.get('/:project/analytics', async (req, res) => {
|
|
431
|
+
try {
|
|
432
|
+
const backend = getProjectMetadataBackend();
|
|
433
|
+
const details = await backend.getAnalyticsSummary(req.params.project);
|
|
434
|
+
res.json(details);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
res.status(400).json({ error: error.message });
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
router.get('/:project/analytics/runs', async (req, res) => {
|
|
441
|
+
try {
|
|
442
|
+
const backend = getProjectMetadataBackend();
|
|
443
|
+
const limit = parseBoundedLimit(req.query.limit, {
|
|
444
|
+
defaultValue: env.apiDefaultLogsLimit,
|
|
445
|
+
maxValue: env.apiMaxLogsLimit
|
|
446
|
+
});
|
|
447
|
+
const details = await backend.getAnalyticsRuns(req.params.project, { limit });
|
|
448
|
+
res.json(details);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
res.status(400).json({ error: error.message });
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
router.get('/:project/analytics/runs/:runId', async (req, res) => {
|
|
455
|
+
try {
|
|
456
|
+
const backend = getProjectMetadataBackend();
|
|
457
|
+
const details = await backend.getAnalyticsRun(req.params.project, req.params.runId);
|
|
458
|
+
res.json(details);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
res.status(404).json({ error: error.message });
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
router.patch('/:project/config', async (req, res) => {
|
|
465
|
+
try {
|
|
466
|
+
const project = resolveProjectName(req.params.project);
|
|
467
|
+
const { config } = req.body || {};
|
|
468
|
+
if (!config || typeof config !== 'object') {
|
|
469
|
+
res.status(400).json({ error: 'config object is required' });
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
await ensureProject(project);
|
|
474
|
+
const previousConfig = await readProjectConfig(project);
|
|
475
|
+
const normalized = await writeProjectConfig(project, config);
|
|
476
|
+
const runState = await readProjectRunState(project);
|
|
477
|
+
const nextRunState = applyRunStateInvalidationForConfigChange({
|
|
478
|
+
runState,
|
|
479
|
+
previousConfig,
|
|
480
|
+
nextConfig: normalized
|
|
481
|
+
});
|
|
482
|
+
if (nextRunState) {
|
|
483
|
+
await writeProjectRunState(project, nextRunState);
|
|
484
|
+
await writeProjectArtifacts(project, nextRunState.artifacts || {});
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
await syncProjectSnapshotFn({
|
|
488
|
+
project,
|
|
489
|
+
projectDir: getProjectDir(project)
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const backend = getProjectMetadataBackend();
|
|
493
|
+
const details = await backend.getProject(project);
|
|
494
|
+
res.json({ project, config: normalized, details });
|
|
495
|
+
} catch (error) {
|
|
496
|
+
res.status(400).json({ error: error.message });
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
router.patch('/:project/metadata', async (req, res) => {
|
|
501
|
+
try {
|
|
502
|
+
const { metadata } = req.body || {};
|
|
503
|
+
if (!metadata || typeof metadata !== 'object') {
|
|
504
|
+
res.status(400).json({ error: 'metadata object is required' });
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const backend = getProjectMetadataBackend();
|
|
509
|
+
const updated = await backend.updateMetadata(req.params.project, metadata);
|
|
510
|
+
res.json(updated);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
res.status(400).json({ error: error.message });
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
router.patch('/:project/content', async (req, res) => {
|
|
517
|
+
try {
|
|
518
|
+
const project = resolveProjectName(req.params.project);
|
|
519
|
+
await ensureProject(project);
|
|
520
|
+
const updates = parseContentUpdatePayload(req.body);
|
|
521
|
+
|
|
522
|
+
if (updates.story !== undefined) {
|
|
523
|
+
await writeProjectStory(project, updates.story);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (updates.script !== undefined || updates.shots !== undefined || updates.tone !== undefined) {
|
|
527
|
+
const currentScriptAsset = await readProjectScriptAsset(project);
|
|
528
|
+
const nextScriptAsset = {
|
|
529
|
+
...(currentScriptAsset || {}),
|
|
530
|
+
...(updates.script !== undefined ? { script: updates.script } : {}),
|
|
531
|
+
...(updates.shots !== undefined ? { shots: updates.shots } : {}),
|
|
532
|
+
...(updates.tone !== undefined ? { tone: updates.tone } : {})
|
|
533
|
+
};
|
|
534
|
+
await writeProjectScriptAsset(project, nextScriptAsset);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const runState = await readProjectRunState(project);
|
|
538
|
+
const nextRunState = applyRunStateInvalidationForContentChange({ runState, updates });
|
|
539
|
+
if (nextRunState) {
|
|
540
|
+
await writeProjectRunState(project, nextRunState);
|
|
541
|
+
await writeProjectArtifacts(project, nextRunState.artifacts || {});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
await syncProjectSnapshotFn({
|
|
545
|
+
project,
|
|
546
|
+
projectDir: getProjectDir(project)
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const backend = getProjectMetadataBackend();
|
|
550
|
+
const details = await backend.getProject(project);
|
|
551
|
+
res.json({
|
|
552
|
+
project,
|
|
553
|
+
updated: {
|
|
554
|
+
story: updates.story !== undefined,
|
|
555
|
+
script: updates.script !== undefined,
|
|
556
|
+
shots: updates.shots !== undefined,
|
|
557
|
+
tone: updates.tone !== undefined
|
|
558
|
+
},
|
|
559
|
+
details
|
|
560
|
+
});
|
|
561
|
+
} catch (error) {
|
|
562
|
+
res.status(400).json({ error: error.message });
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
router.post('/:project/regenerate', async (req, res) => {
|
|
567
|
+
try {
|
|
568
|
+
const project = resolveProjectName(req.params.project);
|
|
569
|
+
const existingActiveJob = findActiveJobByProjectFn(project);
|
|
570
|
+
if (existingActiveJob) {
|
|
571
|
+
res.status(409).json({
|
|
572
|
+
error: `project already has an active job (${existingActiveJob.id})`,
|
|
573
|
+
jobId: existingActiveJob.id,
|
|
574
|
+
status: existingActiveJob.status
|
|
575
|
+
});
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const {
|
|
580
|
+
forceRestart,
|
|
581
|
+
targetType,
|
|
582
|
+
index
|
|
583
|
+
} = req.body || {};
|
|
584
|
+
|
|
585
|
+
const hasTargetType = targetType !== undefined;
|
|
586
|
+
const hasIndex = index !== undefined;
|
|
587
|
+
const normalizedTargetType = String(targetType || '').trim().toLowerCase();
|
|
588
|
+
|
|
589
|
+
if (!hasTargetType && hasIndex) {
|
|
590
|
+
res.status(400).json({ error: 'targetType is required when index is provided' });
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (hasTargetType) {
|
|
595
|
+
if (forceRestart !== undefined) {
|
|
596
|
+
res.status(400).json({ error: 'forceRestart is not supported for targeted regeneration' });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const targetRequiresIndex = normalizedTargetType === 'keyframe' || normalizedTargetType === 'segment';
|
|
601
|
+
if (targetRequiresIndex && !hasIndex) {
|
|
602
|
+
res.status(400).json({ error: 'index is required for keyframe/segment targeted regeneration' });
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (['voiceover', 'script', 'align', 'burnin'].includes(normalizedTargetType) && hasIndex) {
|
|
607
|
+
res.status(400).json({ error: 'index is not supported for script/voiceover/align/burnin targeted regeneration' });
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const targeted = await regenerateProjectAssetFn(project, {
|
|
612
|
+
targetType: normalizedTargetType,
|
|
613
|
+
...(hasIndex ? { index } : {})
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
res.json({
|
|
617
|
+
project,
|
|
618
|
+
targetType: targeted.targetType,
|
|
619
|
+
...(targeted.index !== undefined ? { index: targeted.index } : {}),
|
|
620
|
+
updatedAt: targeted.updatedAt,
|
|
621
|
+
steps: targeted.steps
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (forceRestart !== undefined && typeof forceRestart !== 'boolean') {
|
|
627
|
+
res.status(400).json({ error: 'forceRestart must be a boolean when provided' });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const story = await readProjectStory(project);
|
|
632
|
+
const job = createJobFn({ story, project });
|
|
633
|
+
setImmediate(() => runPipelineFn(job.id, { forceRestart: Boolean(forceRestart) }));
|
|
634
|
+
|
|
635
|
+
res.status(202).json({
|
|
636
|
+
jobId: job.id,
|
|
637
|
+
status: job.status,
|
|
638
|
+
project,
|
|
639
|
+
forceRestart: Boolean(forceRestart)
|
|
640
|
+
});
|
|
641
|
+
} catch (error) {
|
|
642
|
+
res.status(400).json({ error: error.message });
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
return router;
|
|
647
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
|
|
3
|
+
export function createWebhookRouter() {
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
|
|
6
|
+
router.post('/replicate', (req, res) => {
|
|
7
|
+
res.status(501).json({
|
|
8
|
+
error: 'Webhook handling is disabled until signature verification and durable queue reconciliation are implemented'
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return router;
|
|
13
|
+
}
|