@telepat/rilo 0.1.6 → 0.1.8
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/README.de.md +101 -0
- package/README.md +68 -191
- package/README.zh-CN.md +101 -0
- package/package.json +3 -2
- package/src/api/firebaseFunction.js +1 -1
- package/src/cli/index.js +2 -2
- package/src/config/env.js +6 -4
- package/src/config/models.js +38 -14
- package/src/config/settingsSchema.js +10 -1
- package/src/images/limnModelCatalog.js +39 -0
- package/src/pipeline/orchestrator.js +44 -15
- package/src/steps/generateKeyframes.js +60 -34
- package/src/store/projectStore.js +18 -5
- package/frontend/dist/talefire-logo-dark.svg +0 -34
- package/frontend/dist/talefire-logo-light.svg +0 -36
- package/models/black-forest-labs__flux-2-pro.json +0 -78
- package/models/black-forest-labs__flux-schnell.json +0 -95
- package/models/bytedance__seedream-4.json +0 -71
- package/models/google__nano-banana-pro.json +0 -92
- package/models/prunaai__z-image-turbo.json +0 -107
- package/src/steps/textToImageAdapters.js +0 -87
package/src/config/models.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import {
|
|
5
|
+
getLimnGenerationModels,
|
|
6
|
+
isKnownLimnFamily,
|
|
7
|
+
resolveFamilyFromReplicateModelId
|
|
8
|
+
} from '../images/limnModelCatalog.js';
|
|
4
9
|
|
|
5
10
|
const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
6
11
|
const MODELS_DIR = path.resolve(CONFIG_DIR, '../../models');
|
|
7
12
|
|
|
8
13
|
export const MODELS = {
|
|
9
14
|
deepseek: 'deepseek-ai/deepseek-v3',
|
|
10
|
-
keyframe: 'prunaai/z-image-turbo',
|
|
11
|
-
flux: 'black-forest-labs/flux-2-pro',
|
|
12
|
-
fluxSchnell: 'black-forest-labs/flux-schnell',
|
|
13
|
-
nanoBananaPro: 'google/nano-banana-pro',
|
|
14
|
-
seedream4: 'bytedance/seedream-4',
|
|
15
15
|
video: 'wan-video/wan-2.2-i2v-fast',
|
|
16
16
|
klingVideo3: 'kwaivgi/kling-v3-video',
|
|
17
17
|
pixverseV56: 'pixverse/pixverse-v5.6',
|
|
@@ -32,7 +32,7 @@ export const MODEL_CATEGORIES = {
|
|
|
32
32
|
export const DEFAULT_MODEL_SELECTIONS = {
|
|
33
33
|
[MODEL_CATEGORIES.textToText]: MODELS.deepseek,
|
|
34
34
|
[MODEL_CATEGORIES.textToSpeech]: MODELS.tts,
|
|
35
|
-
[MODEL_CATEGORIES.textToImage]:
|
|
35
|
+
[MODEL_CATEGORIES.textToImage]: 'z-image',
|
|
36
36
|
[MODEL_CATEGORIES.imageTextToVideo]: MODELS.video
|
|
37
37
|
};
|
|
38
38
|
|
|
@@ -41,11 +41,6 @@ export const MODEL_OPTION_KEYS = [...MODEL_SELECTION_KEYS];
|
|
|
41
41
|
|
|
42
42
|
const MODEL_METADATA_FILES = {
|
|
43
43
|
[MODELS.deepseek]: 'deepseek-ai__deepseek-v3.json',
|
|
44
|
-
[MODELS.keyframe]: 'prunaai__z-image-turbo.json',
|
|
45
|
-
[MODELS.flux]: 'black-forest-labs__flux-2-pro.json',
|
|
46
|
-
[MODELS.fluxSchnell]: 'black-forest-labs__flux-schnell.json',
|
|
47
|
-
[MODELS.nanoBananaPro]: 'google__nano-banana-pro.json',
|
|
48
|
-
[MODELS.seedream4]: 'bytedance__seedream-4.json',
|
|
49
44
|
[MODELS.video]: 'wan-video__wan-2.2-i2v-fast.json',
|
|
50
45
|
[MODELS.klingVideo3]: 'kwaivgi__kling-v3-video.json',
|
|
51
46
|
[MODELS.pixverseV56]: 'pixverse__pixverse-v5.6.json',
|
|
@@ -59,10 +54,13 @@ const MODEL_METADATA_FILES = {
|
|
|
59
54
|
export const MODEL_IDS_BY_CATEGORY = {
|
|
60
55
|
[MODEL_CATEGORIES.textToText]: [MODELS.deepseek],
|
|
61
56
|
[MODEL_CATEGORIES.textToSpeech]: [MODELS.tts, MODELS.chatterboxTurbo, MODELS.kokoro82m],
|
|
62
|
-
[MODEL_CATEGORIES.textToImage]: [MODELS.keyframe, MODELS.flux, MODELS.fluxSchnell, MODELS.nanoBananaPro, MODELS.seedream4],
|
|
63
57
|
[MODEL_CATEGORIES.imageTextToVideo]: [MODELS.video, MODELS.klingVideo3, MODELS.pixverseV56, MODELS.veo31, MODELS.veo31Fast]
|
|
64
58
|
};
|
|
65
59
|
|
|
60
|
+
export function getTextToImageModelIds() {
|
|
61
|
+
return getLimnGenerationModels().map((model) => model.family);
|
|
62
|
+
}
|
|
63
|
+
|
|
66
64
|
export function toNullableNumber(value) {
|
|
67
65
|
if (value === null || value === undefined || value === '') {
|
|
68
66
|
return null;
|
|
@@ -109,7 +107,14 @@ export const MODEL_METADATA = Object.fromEntries(
|
|
|
109
107
|
Object.values(MODELS).map((modelId) => [modelId, readModelMetadata(modelId)])
|
|
110
108
|
);
|
|
111
109
|
|
|
112
|
-
export const SUPPORTED_MODEL_IDS =
|
|
110
|
+
export const SUPPORTED_MODEL_IDS = [
|
|
111
|
+
...Object.keys(MODEL_METADATA),
|
|
112
|
+
...getTextToImageModelIds(),
|
|
113
|
+
...getTextToImageModelIds().flatMap((family) => {
|
|
114
|
+
const match = getLimnGenerationModels().find((m) => m.family === family);
|
|
115
|
+
return match ? match.replicateModelIds : [];
|
|
116
|
+
})
|
|
117
|
+
];
|
|
113
118
|
|
|
114
119
|
export const MODEL_PRICING = Object.fromEntries(
|
|
115
120
|
Object.entries(MODEL_METADATA).map(([modelId, metadata]) => [modelId, normalizePricing(metadata.pricing || {})])
|
|
@@ -181,6 +186,9 @@ export function resolveProjectModelOptions(modelOptions = {}, modelSelections =
|
|
|
181
186
|
}
|
|
182
187
|
|
|
183
188
|
export function isKnownModelId(modelId) {
|
|
189
|
+
if (typeof modelId === 'string' && isKnownLimnFamily(modelId)) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
184
192
|
return typeof modelId === 'string' && SUPPORTED_MODEL_IDS.includes(modelId);
|
|
185
193
|
}
|
|
186
194
|
|
|
@@ -198,10 +206,22 @@ export function resolveProjectModelSelections(modelSelections = {}) {
|
|
|
198
206
|
for (const key of MODEL_SELECTION_KEYS) {
|
|
199
207
|
const candidate = modelSelections[key];
|
|
200
208
|
if (typeof candidate === 'string' && candidate.trim()) {
|
|
201
|
-
|
|
209
|
+
const trimmed = candidate.trim();
|
|
210
|
+
// Normalize old Replicate T2I IDs to Limn family names
|
|
211
|
+
if (key === MODEL_CATEGORIES.textToImage) {
|
|
212
|
+
const family = resolveFamilyFromReplicateModelId(trimmed);
|
|
213
|
+
resolved[key] = family ?? trimmed;
|
|
214
|
+
} else {
|
|
215
|
+
resolved[key] = trimmed;
|
|
216
|
+
}
|
|
202
217
|
}
|
|
203
218
|
}
|
|
204
219
|
|
|
220
|
+
// Preserve textToImageReplicateModel if present
|
|
221
|
+
if (typeof modelSelections.textToImageReplicateModel === 'string' && modelSelections.textToImageReplicateModel.trim()) {
|
|
222
|
+
resolved.textToImageReplicateModel = modelSelections.textToImageReplicateModel.trim();
|
|
223
|
+
}
|
|
224
|
+
|
|
205
225
|
return resolved;
|
|
206
226
|
}
|
|
207
227
|
|
|
@@ -217,6 +237,10 @@ export function getSupportedModelIdsForCategory(category) {
|
|
|
217
237
|
throw new Error(`Unknown model category: ${category}`);
|
|
218
238
|
}
|
|
219
239
|
|
|
240
|
+
if (category === MODEL_CATEGORIES.textToImage) {
|
|
241
|
+
return getTextToImageModelIds();
|
|
242
|
+
}
|
|
243
|
+
|
|
220
244
|
return [...(MODEL_IDS_BY_CATEGORY[category] || [])];
|
|
221
245
|
}
|
|
222
246
|
|
|
@@ -22,7 +22,16 @@ export const SETTINGS = [
|
|
|
22
22
|
description: 'Your Replicate API key (replicate.com/account/api-tokens).',
|
|
23
23
|
type: 'secure',
|
|
24
24
|
keystoreKey: 'replicateApiToken',
|
|
25
|
-
envNames: ['
|
|
25
|
+
envNames: ['TELEPAT_REPLICATE_TOKEN'],
|
|
26
|
+
default: ''
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'openRouterApiKey',
|
|
30
|
+
label: 'OpenRouter API Key',
|
|
31
|
+
description: 'Your OpenRouter API key for LLM-driven prompt transformation (openrouter.ai/keys).',
|
|
32
|
+
type: 'secure',
|
|
33
|
+
keystoreKey: 'openRouterApiKey',
|
|
34
|
+
envNames: ['TELEPAT_OPENROUTER_KEY'],
|
|
26
35
|
default: ''
|
|
27
36
|
},
|
|
28
37
|
{
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { getSupportedModelCatalog } from '@telepat/limn';
|
|
2
|
+
|
|
3
|
+
export function getLimnGenerationModels() {
|
|
4
|
+
return getSupportedModelCatalog().filter((entry) => entry.generationEnabled);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_LIMN_MODEL_ID = 'z-image';
|
|
8
|
+
|
|
9
|
+
export function resolveFamilyFromReplicateModelId(replicateModelId) {
|
|
10
|
+
const match = getLimnGenerationModels().find((model) =>
|
|
11
|
+
model.replicateModelIds.includes(replicateModelId)
|
|
12
|
+
);
|
|
13
|
+
return match?.family ?? null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isKnownLimnFamily(family) {
|
|
17
|
+
return getLimnGenerationModels().some((model) => model.family === family);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isReplicateModelIdForFamily(family, replicateModelId) {
|
|
21
|
+
const match = getLimnGenerationModels().find((model) => model.family === family);
|
|
22
|
+
if (!match) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return match.replicateModelIds.includes(replicateModelId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getLimnReplicateModelsForFamily(family) {
|
|
29
|
+
const match = getLimnGenerationModels().find((model) => model.family === family);
|
|
30
|
+
if (!match) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
return [...match.replicateModelIds];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getLimnDefaultReplicateModel(family) {
|
|
37
|
+
const match = getLimnGenerationModels().find((model) => model.family === family);
|
|
38
|
+
return match?.defaultReplicateModelId ?? null;
|
|
39
|
+
}
|
|
@@ -28,17 +28,21 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
DEFAULT_VIDEO_CONFIG,
|
|
30
30
|
MODEL_CATEGORIES,
|
|
31
|
+
MODEL_SELECTION_KEYS,
|
|
31
32
|
resolveKeyframeSize,
|
|
32
33
|
resolveProjectModelOptions,
|
|
33
34
|
resolveProjectModelSelections,
|
|
34
35
|
resolveShotCount,
|
|
35
36
|
resolveTargetDurationSec
|
|
36
37
|
} from '../config/models.js';
|
|
38
|
+
import { Limn } from '@telepat/limn';
|
|
39
|
+
import { resolveFamilyFromReplicateModelId } from '../images/limnModelCatalog.js';
|
|
37
40
|
import fs from 'node:fs/promises';
|
|
38
41
|
import crypto from 'node:crypto';
|
|
39
42
|
import path from 'node:path';
|
|
40
43
|
import { ensureDir, writeJson } from '../media/files.js';
|
|
41
44
|
import { probeMediaDurationSeconds } from '../media/ffmpeg.js';
|
|
45
|
+
import { env } from '../config/env.js';
|
|
42
46
|
import {
|
|
43
47
|
ensureProject,
|
|
44
48
|
readProjectConfig,
|
|
@@ -229,6 +233,15 @@ export async function regenerateProjectAsset(projectName, target, options = {})
|
|
|
229
233
|
const projectConfig = await deps.readProjectConfig(project);
|
|
230
234
|
const modelSelections = resolveProjectModelSelections(projectConfig.models);
|
|
231
235
|
const modelOptions = resolveProjectModelOptions(projectConfig.modelOptions, modelSelections);
|
|
236
|
+
const rawT2IModel = modelSelections[MODEL_CATEGORIES.textToImage];
|
|
237
|
+
const limnFamily = resolveFamilyFromReplicateModelId(rawT2IModel) ?? rawT2IModel;
|
|
238
|
+
const limnImageDryRun = !env.replicateApiToken;
|
|
239
|
+
const limn = limnImageDryRun ? null : new Limn({
|
|
240
|
+
openrouterApiKey: env.openRouterApiKey || undefined,
|
|
241
|
+
replicateApiKey: env.replicateApiToken,
|
|
242
|
+
openrouterModel: modelSelections[MODEL_CATEGORIES.textToText]
|
|
243
|
+
});
|
|
244
|
+
const t2iReplicateModel = modelSelections.textToImageReplicateModel || undefined;
|
|
232
245
|
const projectDir = deps.getProjectDir(project);
|
|
233
246
|
const runState = await deps.readProjectRunState(project);
|
|
234
247
|
|
|
@@ -452,21 +465,21 @@ export async function regenerateProjectAsset(projectName, target, options = {})
|
|
|
452
465
|
}
|
|
453
466
|
|
|
454
467
|
if (targetType === 'keyframe') {
|
|
455
|
-
const
|
|
456
|
-
const keyframeUrl = await deps.generateKeyframe(
|
|
468
|
+
const keyframeResult = await deps.generateKeyframe(
|
|
457
469
|
artifacts.shots[index],
|
|
458
470
|
artifacts.tone || 'neutral',
|
|
459
471
|
projectConfig.aspectRatio,
|
|
460
472
|
index,
|
|
461
473
|
traceBase,
|
|
462
|
-
keyframeSize,
|
|
463
474
|
{
|
|
464
|
-
|
|
475
|
+
limn,
|
|
476
|
+
family: limnFamily,
|
|
477
|
+
replicateModel: t2iReplicateModel,
|
|
465
478
|
modelOptions: modelOptions[MODEL_CATEGORIES.textToImage]
|
|
466
479
|
}
|
|
467
480
|
);
|
|
468
|
-
artifacts.keyframeUrls[index] =
|
|
469
|
-
artifacts.keyframePaths[index] = await deps.persistKeyframe(projectDir,
|
|
481
|
+
artifacts.keyframeUrls[index] = keyframeResult.outputUrl || keyframeResult.modelSlug;
|
|
482
|
+
artifacts.keyframePaths[index] = await deps.persistKeyframe(projectDir, keyframeResult, index);
|
|
470
483
|
|
|
471
484
|
const affectedSegmentIndexes = collectAffectedSegmentIndexes([index], artifacts.shots.length);
|
|
472
485
|
const maxSegmentIndex = Math.max(0, artifacts.shots.length - 1);
|
|
@@ -627,6 +640,15 @@ export async function runPipeline(jobId, options = {}) {
|
|
|
627
640
|
const projectConfig = await deps.readProjectConfig(project);
|
|
628
641
|
const modelSelections = resolveProjectModelSelections(projectConfig.models);
|
|
629
642
|
const modelOptions = resolveProjectModelOptions(projectConfig.modelOptions, modelSelections);
|
|
643
|
+
const rawT2IModel = modelSelections[MODEL_CATEGORIES.textToImage];
|
|
644
|
+
const limnFamily = resolveFamilyFromReplicateModelId(rawT2IModel) ?? rawT2IModel;
|
|
645
|
+
const limnImageDryRun = !env.replicateApiToken;
|
|
646
|
+
const limn = limnImageDryRun ? null : new Limn({
|
|
647
|
+
openrouterApiKey: env.openRouterApiKey || undefined,
|
|
648
|
+
replicateApiKey: env.replicateApiToken,
|
|
649
|
+
openrouterModel: modelSelections[MODEL_CATEGORIES.textToText]
|
|
650
|
+
});
|
|
651
|
+
const t2iReplicateModel = modelSelections.textToImageReplicateModel || undefined;
|
|
630
652
|
const projectDir = deps.getProjectDir(project);
|
|
631
653
|
const targetDurationSec = resolveTargetDurationSec(projectConfig);
|
|
632
654
|
const plannedShots = resolveShotCount(projectConfig);
|
|
@@ -899,6 +921,10 @@ export async function runPipeline(jobId, options = {}) {
|
|
|
899
921
|
(category) => JSON.stringify(previousModelOptions[category]) !== JSON.stringify(modelOptions[category])
|
|
900
922
|
);
|
|
901
923
|
|
|
924
|
+
const modelSelectionsChanged = MODEL_SELECTION_KEYS.some(
|
|
925
|
+
(key) => modelSelections[key] !== previousModelSelections[key]
|
|
926
|
+
);
|
|
927
|
+
|
|
902
928
|
const resetFromScript = () => {
|
|
903
929
|
seedSteps[JobStep.SCRIPT] = false;
|
|
904
930
|
seedSteps[JobStep.VOICE] = false;
|
|
@@ -975,7 +1001,7 @@ export async function runPipeline(jobId, options = {}) {
|
|
|
975
1001
|
resetFromScript();
|
|
976
1002
|
}
|
|
977
1003
|
|
|
978
|
-
if (
|
|
1004
|
+
if (modelSelectionsChanged) {
|
|
979
1005
|
resetFromScript();
|
|
980
1006
|
}
|
|
981
1007
|
|
|
@@ -1214,20 +1240,21 @@ export async function runPipeline(jobId, options = {}) {
|
|
|
1214
1240
|
if (shotIndex < 0 || shotIndex >= currentJob.artifacts.shots.length) {
|
|
1215
1241
|
continue;
|
|
1216
1242
|
}
|
|
1217
|
-
const
|
|
1243
|
+
const keyframeResult = await deps.generateKeyframe(
|
|
1218
1244
|
currentJob.artifacts.shots[shotIndex],
|
|
1219
1245
|
currentJob.artifacts.tone || 'neutral',
|
|
1220
1246
|
projectConfig.aspectRatio,
|
|
1221
1247
|
shotIndex,
|
|
1222
1248
|
traceBase,
|
|
1223
|
-
keyframeSize,
|
|
1224
1249
|
{
|
|
1225
|
-
|
|
1250
|
+
limn,
|
|
1251
|
+
family: limnFamily,
|
|
1252
|
+
replicateModel: t2iReplicateModel,
|
|
1226
1253
|
modelOptions: modelOptions[MODEL_CATEGORIES.textToImage]
|
|
1227
1254
|
}
|
|
1228
1255
|
);
|
|
1229
|
-
keyframeUrls[shotIndex] =
|
|
1230
|
-
keyframePaths[shotIndex] = await deps.persistKeyframe(projectDir,
|
|
1256
|
+
keyframeUrls[shotIndex] = keyframeResult.outputUrl || keyframeResult.modelSlug;
|
|
1257
|
+
keyframePaths[shotIndex] = await deps.persistKeyframe(projectDir, keyframeResult, shotIndex);
|
|
1231
1258
|
}
|
|
1232
1259
|
|
|
1233
1260
|
const totalShots = currentJob.artifacts.shots.length;
|
|
@@ -1356,18 +1383,20 @@ export async function runPipeline(jobId, options = {}) {
|
|
|
1356
1383
|
}
|
|
1357
1384
|
|
|
1358
1385
|
if (!hasUrl) {
|
|
1359
|
-
|
|
1386
|
+
const keyframeResult = await deps.generateKeyframe(
|
|
1360
1387
|
currentJob.artifacts.shots[shotIndex],
|
|
1361
1388
|
currentJob.artifacts.tone || 'neutral',
|
|
1362
1389
|
projectConfig.aspectRatio,
|
|
1363
1390
|
shotIndex,
|
|
1364
1391
|
traceBase,
|
|
1365
|
-
keyframeSize,
|
|
1366
1392
|
{
|
|
1367
|
-
|
|
1393
|
+
limn,
|
|
1394
|
+
family: limnFamily,
|
|
1395
|
+
replicateModel: t2iReplicateModel,
|
|
1368
1396
|
modelOptions: modelOptions[MODEL_CATEGORIES.textToImage]
|
|
1369
1397
|
}
|
|
1370
1398
|
);
|
|
1399
|
+
keyframeUrls[shotIndex] = keyframeResult.outputUrl || keyframeResult.modelSlug;
|
|
1371
1400
|
}
|
|
1372
1401
|
|
|
1373
1402
|
keyframePaths[shotIndex] = await deps.persistKeyframe(projectDir, keyframeUrls[shotIndex], shotIndex);
|
|
@@ -1,68 +1,94 @@
|
|
|
1
|
-
import { ASPECT_RATIO_PRESETS, MODEL_CATEGORIES, resolveModelForCategory } from '../config/models.js';
|
|
2
|
-
import { runModel, extractOutputUri } from '../providers/predictions.js';
|
|
3
1
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
2
|
+
import { writeFile } from 'node:fs/promises';
|
|
3
|
+
import { ensureDir } from '../media/files.js';
|
|
6
4
|
|
|
7
5
|
export async function generateKeyframe(
|
|
8
6
|
promptText,
|
|
9
7
|
tone,
|
|
10
8
|
aspectRatio = '9:16',
|
|
11
9
|
index = 0,
|
|
12
|
-
|
|
13
|
-
sizeOverride = null,
|
|
10
|
+
_trace,
|
|
14
11
|
options = {}
|
|
15
12
|
) {
|
|
16
13
|
const deps = options.deps || {};
|
|
17
|
-
const
|
|
18
|
-
const
|
|
14
|
+
const limn = options.limn || deps.limn || null;
|
|
15
|
+
const family = options.family;
|
|
16
|
+
const replicateModel = options.replicateModel;
|
|
17
|
+
const modelOptions = options.modelOptions ?? {};
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
19
|
+
if (!family) {
|
|
20
|
+
throw new Error('Limn model family is required for keyframe generation');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!limn) {
|
|
24
|
+
throw new Error('Limn instance is required for keyframe generation');
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const result = await limn.generate(promptText, family, {
|
|
28
|
+
aspectRatio,
|
|
29
|
+
...(replicateModel ? { replicateModel } : {}),
|
|
30
|
+
options: modelOptions
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
if (!imageUrl) {
|
|
33
|
+
if (!result || !result.image) {
|
|
35
34
|
throw new Error(`Missing keyframe output for shot ${index + 1}`);
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
return
|
|
37
|
+
return {
|
|
38
|
+
buffer: result.image,
|
|
39
|
+
outputUrl: result.outputUrl || '',
|
|
40
|
+
mimeType: result.mimeType || 'image/png',
|
|
41
|
+
modelSlug: result.modelSlug || family
|
|
42
|
+
};
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
export async function generateKeyframes(shots, tone, aspectRatio = '9:16',
|
|
42
|
-
const
|
|
45
|
+
export async function generateKeyframes(shots, tone, aspectRatio = '9:16', _trace, options = {}) {
|
|
46
|
+
const results = [];
|
|
43
47
|
for (let i = 0; i < shots.length; i += 1) {
|
|
44
|
-
const
|
|
45
|
-
|
|
48
|
+
const result = await generateKeyframe(shots[i], tone, aspectRatio, i, _trace, options);
|
|
49
|
+
results.push(result);
|
|
46
50
|
}
|
|
47
|
-
return
|
|
51
|
+
return results;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mimeTypeToExtension(mimeType) {
|
|
55
|
+
if (mimeType === 'image/jpeg') return 'jpg';
|
|
56
|
+
if (mimeType === 'image/webp') return 'webp';
|
|
57
|
+
return 'png';
|
|
48
58
|
}
|
|
49
59
|
|
|
50
|
-
export async function persistKeyframe(projectDir,
|
|
60
|
+
export async function persistKeyframe(projectDir, keyframeResult, index, options = {}) {
|
|
51
61
|
const deps = options.deps || {};
|
|
52
62
|
const ensureDirFn = deps.ensureDir || ensureDir;
|
|
53
|
-
const
|
|
63
|
+
const writeFileFn = deps.writeFile || writeFile;
|
|
54
64
|
|
|
55
65
|
const keyframesDir = path.join(projectDir, 'assets', 'keyframes');
|
|
56
66
|
await ensureDirFn(keyframesDir);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
67
|
+
|
|
68
|
+
// Handle Limn buffer result
|
|
69
|
+
if (keyframeResult && typeof keyframeResult === 'object' && keyframeResult.buffer) {
|
|
70
|
+
const ext = mimeTypeToExtension(keyframeResult.mimeType || 'image/png');
|
|
71
|
+
const keyframePath = path.join(keyframesDir, `keyframe_${String(index + 1).padStart(2, '0')}.${ext}`);
|
|
72
|
+
await writeFileFn(keyframePath, keyframeResult.buffer);
|
|
73
|
+
return keyframePath;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Handle legacy URL string
|
|
77
|
+
if (typeof keyframeResult === 'string') {
|
|
78
|
+
const depsDownload = deps.downloadToFile;
|
|
79
|
+
const downloadToFileFn = depsDownload || (await import('../media/files.js')).downloadToFile;
|
|
80
|
+
const keyframePath = path.join(keyframesDir, `keyframe_${String(index + 1).padStart(2, '0')}.png`);
|
|
81
|
+
await downloadToFileFn(keyframeResult, keyframePath);
|
|
82
|
+
return keyframePath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
throw new Error('Invalid keyframe result: expected buffer object or URL string');
|
|
60
86
|
}
|
|
61
87
|
|
|
62
|
-
export async function persistKeyframes(projectDir,
|
|
88
|
+
export async function persistKeyframes(projectDir, keyframeResults, options = {}) {
|
|
63
89
|
const keyframePaths = [];
|
|
64
|
-
for (let i = 0; i <
|
|
65
|
-
const keyframePath = await persistKeyframe(projectDir,
|
|
90
|
+
for (let i = 0; i < keyframeResults.length; i += 1) {
|
|
91
|
+
const keyframePath = await persistKeyframe(projectDir, keyframeResults[i], i, options);
|
|
66
92
|
keyframePaths.push(keyframePath);
|
|
67
93
|
}
|
|
68
94
|
|
|
@@ -5,11 +5,13 @@ import { ensureDir, writeJson } from '../media/files.js';
|
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_MODEL_SELECTIONS,
|
|
7
7
|
DEFAULT_VIDEO_CONFIG,
|
|
8
|
+
MODEL_CATEGORIES,
|
|
8
9
|
MODEL_OPTION_KEYS,
|
|
9
10
|
MODEL_SELECTION_KEYS,
|
|
10
11
|
SUPPORTED_MODEL_IDS,
|
|
11
12
|
resolveModelInputOptionsForCategory,
|
|
12
|
-
resolveProjectModelOptions
|
|
13
|
+
resolveProjectModelOptions,
|
|
14
|
+
resolveProjectModelSelections
|
|
13
15
|
} from '../config/models.js';
|
|
14
16
|
|
|
15
17
|
export const SUPPORTED_ASPECT_RATIOS = ['1:1', '16:9', '9:16'];
|
|
@@ -358,6 +360,11 @@ function validateProjectModelOptions(modelOptions, modelSelections) {
|
|
|
358
360
|
throw new Error(`Invalid project config: modelOptions.${category} must be an object`);
|
|
359
361
|
}
|
|
360
362
|
|
|
363
|
+
// T2I model options are validated by Limn at generation time
|
|
364
|
+
if (category === MODEL_CATEGORIES.textToImage) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
361
368
|
const inputOptions = resolveModelInputOptionsForCategory(category, modelSelections);
|
|
362
369
|
const allowedOptions = new Set(inputOptions.userConfigurable);
|
|
363
370
|
const fields = inputOptions.fields || {};
|
|
@@ -400,12 +407,18 @@ export function normalizeProjectConfig(config) {
|
|
|
400
407
|
}
|
|
401
408
|
}
|
|
402
409
|
|
|
410
|
+
// Reject unknown model selection keys before normalization
|
|
411
|
+
if (nextConfig.models && typeof nextConfig.models === 'object' && !Array.isArray(nextConfig.models)) {
|
|
412
|
+
for (const key of Object.keys(nextConfig.models)) {
|
|
413
|
+
if (!MODEL_SELECTION_KEYS.includes(key) && key !== 'textToImageReplicateModel') {
|
|
414
|
+
throw new Error(`Invalid project config: models.${key} is not a supported model category`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
403
419
|
const mergedModels = nextConfig.models === undefined
|
|
404
420
|
? { ...DEFAULT_MODEL_SELECTIONS }
|
|
405
|
-
:
|
|
406
|
-
...DEFAULT_MODEL_SELECTIONS,
|
|
407
|
-
...(nextConfig.models || {})
|
|
408
|
-
};
|
|
421
|
+
: resolveProjectModelSelections(nextConfig.models);
|
|
409
422
|
const mergedModelOptions = mergeProjectModelOptions(nextConfig.modelOptions, mergedModels);
|
|
410
423
|
|
|
411
424
|
return {
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="18 26 197 48" width="394" height="96">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="flameGrad" x1="0%" y1="100%" x2="0%" y2="0%">
|
|
4
|
-
<stop offset="0%" stop-color="#ff3d00"/>
|
|
5
|
-
<stop offset="60%" stop-color="#ff7a00"/>
|
|
6
|
-
<stop offset="100%" stop-color="#ffcc00"/>
|
|
7
|
-
</linearGradient>
|
|
8
|
-
|
|
9
|
-
<filter id="flameGlow" x="-40%" y="-40%" width="180%" height="180%">
|
|
10
|
-
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
|
11
|
-
<feMerge>
|
|
12
|
-
<feMergeNode in="blur"/>
|
|
13
|
-
<feMergeNode in="SourceGraphic"/>
|
|
14
|
-
</feMerge>
|
|
15
|
-
</filter>
|
|
16
|
-
</defs>
|
|
17
|
-
|
|
18
|
-
<!-- Transparent background -->
|
|
19
|
-
|
|
20
|
-
<!-- Minimal flame — two triangles -->
|
|
21
|
-
<polygon points="44,31 58,68 30,68" fill="#ff4e00"/>
|
|
22
|
-
<polygon points="44,44 53,68 35,68" fill="#ff7a00"/>
|
|
23
|
-
|
|
24
|
-
<!-- Wordmark — white text, orange "fire" -->
|
|
25
|
-
<text
|
|
26
|
-
x="66" y="68"
|
|
27
|
-
font-family="'Helvetica Neue', Helvetica, Arial, sans-serif"
|
|
28
|
-
font-size="52"
|
|
29
|
-
font-weight="200"
|
|
30
|
-
letter-spacing="-1"
|
|
31
|
-
fill="#ffffff"
|
|
32
|
-
>tale<tspan font-weight="600" fill="#ff4e00">fire</tspan></text>
|
|
33
|
-
|
|
34
|
-
</svg>
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="18 26 197 48" width="394" height="96">
|
|
2
|
-
<defs>
|
|
3
|
-
<linearGradient id="flameGrad" x1="0%" y1="100%" x2="0%" y2="0%">
|
|
4
|
-
<stop offset="0%" stop-color="#ff3d00"/>
|
|
5
|
-
<stop offset="60%" stop-color="#ff7a00"/>
|
|
6
|
-
<stop offset="100%" stop-color="#ffcc00"/>
|
|
7
|
-
</linearGradient>
|
|
8
|
-
|
|
9
|
-
<filter id="flameGlow" x="-40%" y="-40%" width="180%" height="180%">
|
|
10
|
-
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
|
11
|
-
<feMerge>
|
|
12
|
-
<feMergeNode in="blur"/>
|
|
13
|
-
<feMergeNode in="SourceGraphic"/>
|
|
14
|
-
</feMerge>
|
|
15
|
-
</filter>
|
|
16
|
-
</defs>
|
|
17
|
-
|
|
18
|
-
<!-- Transparent background -->
|
|
19
|
-
|
|
20
|
-
<!-- Minimal flame — two triangles, clean and simple -->
|
|
21
|
-
<!-- Main triangle -->
|
|
22
|
-
<polygon points="44,31 58,68 30,68" fill="#ff4e00"/>
|
|
23
|
-
<!-- Small inner triangle cutout to suggest flame shape -->
|
|
24
|
-
<polygon points="44,44 53,68 35,68" fill="#ff7a00"/>
|
|
25
|
-
|
|
26
|
-
<!-- Wordmark — original letter spacing -->
|
|
27
|
-
<text
|
|
28
|
-
x="66" y="68"
|
|
29
|
-
font-family="'Helvetica Neue', Helvetica, Arial, sans-serif"
|
|
30
|
-
font-size="52"
|
|
31
|
-
font-weight="200"
|
|
32
|
-
letter-spacing="-1"
|
|
33
|
-
fill="#1a1a1a"
|
|
34
|
-
>tale<tspan font-weight="600" fill="#ff4e00">fire</tspan></text>
|
|
35
|
-
|
|
36
|
-
</svg>
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"modelId": "black-forest-labs/flux-2-pro",
|
|
3
|
-
"provider": "replicate",
|
|
4
|
-
"displayName": "FLUX 2 Pro",
|
|
5
|
-
"category": "image-generation",
|
|
6
|
-
"pricingSourceUrl": "https://replicate.com/black-forest-labs/flux-2-pro",
|
|
7
|
-
"pricingNotes": "Replicate lists multi-property pricing for run and input/output megapixels; this model currently uses best-effort cost fallback when exact rule mapping is unavailable.",
|
|
8
|
-
"pricing": {
|
|
9
|
-
"usdPerSecond": null,
|
|
10
|
-
"usdPer1kInputTokens": null,
|
|
11
|
-
"usdPer1kOutputTokens": null
|
|
12
|
-
},
|
|
13
|
-
"inputOptions": {
|
|
14
|
-
"userConfigurable": [
|
|
15
|
-
"safety_tolerance",
|
|
16
|
-
"seed",
|
|
17
|
-
"output_format",
|
|
18
|
-
"output_quality"
|
|
19
|
-
],
|
|
20
|
-
"pipelineManaged": [
|
|
21
|
-
"prompt",
|
|
22
|
-
"aspect_ratio",
|
|
23
|
-
"width",
|
|
24
|
-
"height"
|
|
25
|
-
],
|
|
26
|
-
"fields": {
|
|
27
|
-
"safety_tolerance": {
|
|
28
|
-
"type": "integer",
|
|
29
|
-
"default": 2,
|
|
30
|
-
"minimum": 1,
|
|
31
|
-
"maximum": 5
|
|
32
|
-
},
|
|
33
|
-
"seed": {
|
|
34
|
-
"type": "integer",
|
|
35
|
-
"nullable": true
|
|
36
|
-
},
|
|
37
|
-
"output_format": {
|
|
38
|
-
"type": "string",
|
|
39
|
-
"default": "webp",
|
|
40
|
-
"enum": [
|
|
41
|
-
"webp",
|
|
42
|
-
"png",
|
|
43
|
-
"jpg",
|
|
44
|
-
"jpeg"
|
|
45
|
-
]
|
|
46
|
-
},
|
|
47
|
-
"output_quality": {
|
|
48
|
-
"type": "integer",
|
|
49
|
-
"default": 80,
|
|
50
|
-
"minimum": 0,
|
|
51
|
-
"maximum": 100
|
|
52
|
-
},
|
|
53
|
-
"prompt": {
|
|
54
|
-
"type": "string",
|
|
55
|
-
"required": true
|
|
56
|
-
},
|
|
57
|
-
"aspect_ratio": {
|
|
58
|
-
"type": "string",
|
|
59
|
-
"required": true,
|
|
60
|
-
"enum": [
|
|
61
|
-
"custom"
|
|
62
|
-
]
|
|
63
|
-
},
|
|
64
|
-
"width": {
|
|
65
|
-
"type": "integer",
|
|
66
|
-
"required": true,
|
|
67
|
-
"minimum": 256,
|
|
68
|
-
"maximum": 2048
|
|
69
|
-
},
|
|
70
|
-
"height": {
|
|
71
|
-
"type": "integer",
|
|
72
|
-
"required": true,
|
|
73
|
-
"minimum": 256,
|
|
74
|
-
"maximum": 2048
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|