@telepat/rilo 0.1.7 → 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 ADDED
@@ -0,0 +1,101 @@
1
+ <p align="center"><img src="./assets/avatar/rilo-logo.webp" width="128" alt="Rilo"></p>
2
+ <h1 align="center">Rilo</h1>
3
+ <p align="center"><em>Verwandle eine Geschichte in ein fertiges Video — KI-generiertes Skript, Voiceover, Keyframes und Komposition, alles mit einem Befehl.</em></p>
4
+
5
+ <p align="center">
6
+ <a href="https://docs.telepat.io/rilo">📖 Docs</a>
7
+ · <a href="./README.md">🇺🇸 English</a>
8
+ · <a href="./README.zh-CN.md">🇨🇳 简体中文</a>
9
+ · <a href="./README.de.md">🇩🇪 Deutsch</a>
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://github.com/telepat-io/rilo/actions/workflows/ci.yml"><img src="https://github.com/telepat-io/rilo/actions/workflows/ci.yml/badge.svg?branch=main" alt="Build"></a>
14
+ <a href="https://codecov.io/gh/telepat-io/rilo"><img src="https://codecov.io/gh/telepat-io/rilo/graph/badge.svg" alt="Codecov"></a>
15
+ <a href="https://www.npmjs.com/package/@telepat/rilo"><img src="https://img.shields.io/npm/v/@telepat/rilo" alt="npm"></a>
16
+ <a href="https://github.com/telepat-io/rilo/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-yellow.svg" alt="License"></a>
17
+ </p>
18
+
19
+ Rilo verwandelt eine Geschichte in ein fertiges Video — KI-generiertes Skript, Voiceover, Keyframes und Komposition, alles mit einem Befehl.
20
+
21
+ Schreiben Sie Ihre Geschichte in einfachem Text. Rilo übernimmt den Rest: Skripterstellung, Erzählung, visuelle Keyframes, Videosegmente und die finale Komposition – mit optionaler Untertitel-Synchronisation und Einbrennung.
22
+
23
+ Entwickelt für Kreative und Teams, die reproduzierbare, hochwertige Videos in großem Umfang ohne manuellen Schnitt benötigen.
24
+
25
+ ## Features
26
+
27
+ - **Vollständige Pipeline, ein Befehl** — Story → Skript → Voiceover → Keyframes → Segmente → finales Video. `rilo --project demo --story-file ./story.txt`
28
+ - **Checkpoint-gestützte Durchläufe** — Jede Stufe speichert Artefakte. Setzen Sie beliebige Stufen fort oder generieren Sie sie selektiv neu. `rilo --project demo --force`
29
+ - **Ihre Modelle, Ihre Kontrolle** — Wählen Sie T2I- und I2V-Modelle. Überschreiben Sie modellspezifische Optionen. Wechseln Sie Modelle jederzeit.
30
+ - **Code-gesteuerte Pipeline** — Deterministische Orchestrierung, Checkpointing und Artefaktverwaltung. Tokens werden für die Generierung ausgegeben, nicht für Infrastruktur.
31
+ - **Untertitel-Synchronisation & Einbrennung** — Untertitel automatisch an Voiceover-Timing ausrichten. Ins finale Video einbrennen.
32
+ - **Vorschau-Dashboard** — Web-UI für Projektverwaltung, Neugenerierung und Asset-Vorschau. `rilo preview`
33
+ - **HTTP-API & Webhooks** — Bearer-Token-Authentifizierung, OpenAPI-3.1-Spezifikation, Webhook-Abonnements. Firebase oder lokal.
34
+ - **Plattformübergreifend** — macOS, Linux, Windows. Node.js 22+ und ffmpeg.
35
+
36
+ ## Quick Start
37
+
38
+ Voraussetzungen: Node.js 22+, ffmpeg im PATH und ein Replicate API-Token.
39
+
40
+ ```bash
41
+ npm install -g @telepat/rilo
42
+ rilo settings
43
+ rilo --project demo --story-file ./story.txt
44
+ ```
45
+
46
+ Erwartetes Ergebnis:
47
+
48
+ - Ein Projektordner wird unter `projects/demo/` erstellt.
49
+ - Die vollständige Pipeline durchläuft Skript, Voiceover, Keyframes, Segmente und Komposition.
50
+ - Das finale Video wird unter `projects/demo/final.mp4` abgelegt.
51
+ - Dashboard-Vorschau verfügbar via `rilo preview`.
52
+
53
+ ## Requirements
54
+
55
+ - Node.js 22+
56
+ - ffmpeg im PATH
57
+ - Replicate API-Token
58
+ - macOS, Linux oder Windows
59
+
60
+ ## How It Works
61
+
62
+ Rilo durchläuft eine gestufte Pipeline: Skripterstellung, Voiceover-Synthese, Shot-Prompt-Generierung, Keyframe-Rendering, Segmentgenerierung und finale Videokomposition. Jede Stufe schreibt Checkpoint-Artefakte, sodass Sie selektiv fortsetzen oder neugenerieren können.
63
+
64
+ Die Konfiguration fasst CLI-Flags, Umgebungsvariablen und `~/.rilo/config.json` mit Schema-Defaults zusammen. Das Vorschau-Dashboard (`rilo preview`) startet eine lokale API, einen Worker und ein Vite-React-Frontend für Monitoring und Bearbeitung.
65
+
66
+ ## Using With AI Agents
67
+
68
+ Rilo bietet mehrere Schnittstellen für agentische und automatisierte Workflows:
69
+
70
+ - **CLI-Automatisierung** — Die gesamte Generierung wird durch CLI-Flags und Umgebungsvariablen gesteuert. Nach der initialen Einrichtung sind keine interaktiven Prompts erforderlich.
71
+ - **HTTP-API** — `rilo preview` startet eine Express-API mit vollständigem Job- und Projekt-CRUD, Asset-Serving und Webhook-Endpunkten. Bearer-Token-Authentifizierung via `Authorization: Bearer <API_BEARER_TOKEN>`.
72
+ - **OpenAPI-Spezifikation** — Automatisch generierte OpenAPI-3.1-Spezifikation für schema-gesteuerte Agent-Integration.
73
+ - **Webhooks** — Abonnieren Sie Job-Lifecycle-Events für externe Orchestrierung.
74
+ - **Firebase Functions** — Stellen Sie `src/api/firebaseFunction.js` für serverloses API-Hosting bereit.
75
+ - **Agent-Dokumentation** — [API Reference](https://docs.telepat.io/rilo/reference/api-reference) behandelt Endpunkte, Authentifizierung und Webhooks.
76
+
77
+ ## Security And Trust
78
+
79
+ - API-Tokens und Replicate-Zugangsdaten werden, wenn verfügbar, im OS-Keystore gespeichert (macOS Keychain, Windows Credential Manager, Linux Secret Service).
80
+ - Fallback auf eine AES-256-verschlüsselte Datei unter `~/.rilo/.secrets`, falls kein nativer Keystore verfügbar ist.
81
+ - Umgebungsvariablen (`TELEPAT_REPLICATE_TOKEN`, `RILO_API_BEARER_TOKEN`) haben höchste Priorität und überschreiben gespeicherte Werte.
82
+ - Der Preview-Modus `--expose` sollte nur in vertrauenswürdigen Netzwerken oder isolierten Umgebungen verwendet werden.
83
+
84
+ ## Documentation And Support
85
+
86
+ - [Documentation site](https://docs.telepat.io/rilo)
87
+ - [Quickstart](https://docs.telepat.io/rilo/getting-started/quickstart)
88
+ - [CLI Reference](https://docs.telepat.io/rilo/reference/cli-reference)
89
+ - [Configuration Guide](https://docs.telepat.io/rilo/guides/configuration)
90
+ - [API Reference](https://docs.telepat.io/rilo/reference/api-reference)
91
+ - [Troubleshooting](https://docs.telepat.io/rilo/guides/troubleshooting)
92
+ - [Repository](https://github.com/telepat-io/rilo)
93
+ - [npm package](https://www.npmjs.com/package/@telepat/rilo)
94
+
95
+ ## Contributing
96
+
97
+ Beiträge sind willkommen. Siehe [Development](https://docs.telepat.io/rilo/contributing/development) für lokale Einrichtung, Build-Befehle und Test-Workflows.
98
+
99
+ ## License
100
+
101
+ MIT. Siehe [LICENSE](./LICENSE).
package/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  <a href="https://docs.telepat.io/rilo">📖 Docs</a>
7
7
  · <a href="./README.md">🇺🇸 English</a>
8
8
  · <a href="./README.zh-CN.md">🇨🇳 简体中文</a>
9
+ · <a href="./README.de.md">🇩🇪 Deutsch</a>
9
10
  </p>
10
11
 
11
12
  <p align="center">
@@ -77,7 +78,7 @@ Rilo provides multiple surfaces for agentic and automated workflows:
77
78
 
78
79
  - API tokens and Replicate credentials are stored in the OS keystore (macOS Keychain, Windows Credential Manager, Linux Secret Service) when available.
79
80
  - Falls back to an AES-256 encrypted file at `~/.rilo/.secrets` if no native keystore is available.
80
- - Environment variables (`RILO_REPLICATE_API_TOKEN`, `RILO_API_BEARER_TOKEN`) take highest precedence and override stored values.
81
+ - Environment variables (`TELEPAT_REPLICATE_TOKEN`, `RILO_API_BEARER_TOKEN`) take highest precedence and override stored values.
81
82
  - Preview `--expose` mode should only be used on trusted networks or isolated environments.
82
83
 
83
84
  ## Documentation And Support
package/README.zh-CN.md CHANGED
@@ -6,6 +6,7 @@
6
6
  <a href="https://docs.telepat.io/rilo">📖 文档</a>
7
7
  · <a href="./README.md">🇺🇸 English</a>
8
8
  · <a href="./README.zh-CN.md">🇨🇳 简体中文</a>
9
+ · <a href="./README.de.md">🇩🇪 Deutsch</a>
9
10
  </p>
10
11
 
11
12
  <p align="center">
@@ -77,7 +78,7 @@ Rilo 为智能体和自动化工作流提供多种接口:
77
78
 
78
79
  - API token 和 Replicate 凭证在 OS 密钥库可用时保存(macOS Keychain、Windows Credential Manager、Linux Secret Service)。
79
80
  - 如果无原生密钥库可用,则回退到 `~/.rilo/.secrets` 的 AES-256 加密文件。
80
- - 环境变量(`RILO_REPLICATE_API_TOKEN`、`RILO_API_BEARER_TOKEN`)优先级最高,会覆盖已存储的值。
81
+ - 环境变量(`TELEPAT_REPLICATE_TOKEN`、`RILO_API_BEARER_TOKEN`)优先级最高,会覆盖已存储的值。
81
82
  - Preview `--expose` 模式应仅在可信网络或隔离环境中使用。
82
83
 
83
84
  ## 文档与支持
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telepat/rilo",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "rilo": "src/cli/index.js"
@@ -53,6 +53,7 @@
53
53
  "express": "^4.21.2",
54
54
  "firebase-admin": "^13.4.0",
55
55
  "firebase-functions": "^6.4.0",
56
+ "@telepat/limn": "^0.1.7",
56
57
  "replicate": "^1.0.1",
57
58
  "uuid": "^11.1.0"
58
59
  },
@@ -21,7 +21,7 @@ const runtimeOptions = {
21
21
  invoker: 'public',
22
22
  secrets: [
23
23
  'SECRET_API_BEARER_TOKEN',
24
- 'SECRET_REPLICATE_API_TOKEN',
24
+ 'TELEPAT_REPLICATE_TOKEN',
25
25
  'SECRET_OUTPUT_BACKEND',
26
26
  'SECRET_FIREBASE_PROJECT_ID',
27
27
  'SECRET_FIREBASE_STORAGE_BUCKET'
package/src/cli/index.js CHANGED
@@ -138,7 +138,7 @@ SETTINGS
138
138
  npm install -g @telepat/rilo
139
139
 
140
140
  Or with environment variables:
141
- export RILO_REPLICATE_API_TOKEN=r8_xxxxx
141
+ export TELEPAT_REPLICATE_TOKEN=r8_xxxxx
142
142
  export RILO_MAX_RETRIES=5
143
143
  export PREDICTION_MAX_WAIT_MS=900000
144
144
  rilo --project my-project --story-file ./story.txt
package/src/config/env.js CHANGED
@@ -41,9 +41,11 @@ export function parseAllowedHosts(value, fallback = 'replicate.delivery,replicat
41
41
 
42
42
  export const env = {
43
43
  replicateApiToken: parseEnvString(
44
- process.env.SECRET_REPLICATE_API_TOKEN
45
- || process.env.RILO_REPLICATE_API_TOKEN
46
- || process.env.REPLICATE_API_TOKEN,
44
+ process.env.TELEPAT_REPLICATE_TOKEN,
45
+ ''
46
+ ),
47
+ openRouterApiKey: parseEnvString(
48
+ process.env.TELEPAT_OPENROUTER_KEY,
47
49
  ''
48
50
  ),
49
51
  apiBearerToken: parseEnvString(
@@ -147,7 +149,7 @@ export async function applyStoredSettings() {
147
149
 
148
150
  export function assertRequiredEnv() {
149
151
  if (!env.replicateApiToken) {
150
- throw new Error('Missing REPLICATE_API_TOKEN in environment');
152
+ throw new Error('Missing TELEPAT_REPLICATE_TOKEN in environment');
151
153
  }
152
154
  }
153
155
 
@@ -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]: MODELS.keyframe,
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 = Object.keys(MODEL_METADATA);
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
- resolved[key] = candidate.trim();
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: ['RILO_REPLICATE_API_TOKEN', 'REPLICATE_API_TOKEN'],
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 keyframeSize = resolveKeyframeSize(projectConfig);
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
- modelId: modelSelections[MODEL_CATEGORIES.textToImage],
475
+ limn,
476
+ family: limnFamily,
477
+ replicateModel: t2iReplicateModel,
465
478
  modelOptions: modelOptions[MODEL_CATEGORIES.textToImage]
466
479
  }
467
480
  );
468
- artifacts.keyframeUrls[index] = keyframeUrl;
469
- artifacts.keyframePaths[index] = await deps.persistKeyframe(projectDir, keyframeUrl, index);
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 (JSON.stringify(previousModelSelections) !== JSON.stringify(modelSelections)) {
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 keyframeUrl = await deps.generateKeyframe(
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
- modelId: modelSelections[MODEL_CATEGORIES.textToImage],
1250
+ limn,
1251
+ family: limnFamily,
1252
+ replicateModel: t2iReplicateModel,
1226
1253
  modelOptions: modelOptions[MODEL_CATEGORIES.textToImage]
1227
1254
  }
1228
1255
  );
1229
- keyframeUrls[shotIndex] = keyframeUrl;
1230
- keyframePaths[shotIndex] = await deps.persistKeyframe(projectDir, keyframeUrl, shotIndex);
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
- keyframeUrls[shotIndex] = await deps.generateKeyframe(
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
- modelId: modelSelections[MODEL_CATEGORIES.textToImage],
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 { downloadToFile, ensureDir } from '../media/files.js';
5
- import { resolveTextToImageAdapter } from './textToImageAdapters.js';
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
- trace = null,
13
- sizeOverride = null,
10
+ _trace,
14
11
  options = {}
15
12
  ) {
16
13
  const deps = options.deps || {};
17
- const runModelFn = deps.runModel || runModel;
18
- const extractOutputUriFn = deps.extractOutputUri || extractOutputUri;
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
- const preset = ASPECT_RATIO_PRESETS[aspectRatio] || ASPECT_RATIO_PRESETS['9:16'];
21
- const width = sizeOverride?.width || preset.keyframeWidth || ASPECT_RATIO_PRESETS['9:16'].keyframeWidth;
22
- const height = sizeOverride?.height || preset.keyframeHeight || ASPECT_RATIO_PRESETS['9:16'].keyframeHeight;
23
- const modelId = options.modelId || resolveModelForCategory(MODEL_CATEGORIES.textToImage);
24
- const modelOptions = options.modelOptions;
25
- const adapter = resolveTextToImageAdapter(modelId);
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 prediction = await runModelFn({
28
- model: modelId,
29
- input: adapter.buildInput({ promptText, tone, index, aspectRatio, width, height, modelOptions }),
30
- trace: trace ? { ...trace, step: 'keyframe', index } : null
27
+ const result = await limn.generate(promptText, family, {
28
+ aspectRatio,
29
+ ...(replicateModel ? { replicateModel } : {}),
30
+ options: modelOptions
31
31
  });
32
32
 
33
- const imageUrl = extractOutputUriFn(prediction.output);
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 imageUrl;
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', trace = null, options = {}) {
42
- const urls = [];
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 imageUrl = await generateKeyframe(shots[i], tone, aspectRatio, i, trace, null, options);
45
- urls.push(imageUrl);
48
+ const result = await generateKeyframe(shots[i], tone, aspectRatio, i, _trace, options);
49
+ results.push(result);
46
50
  }
47
- return urls;
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, keyframeUrl, index, options = {}) {
60
+ export async function persistKeyframe(projectDir, keyframeResult, index, options = {}) {
51
61
  const deps = options.deps || {};
52
62
  const ensureDirFn = deps.ensureDir || ensureDir;
53
- const downloadToFileFn = deps.downloadToFile || downloadToFile;
63
+ const writeFileFn = deps.writeFile || writeFile;
54
64
 
55
65
  const keyframesDir = path.join(projectDir, 'assets', 'keyframes');
56
66
  await ensureDirFn(keyframesDir);
57
- const keyframePath = path.join(keyframesDir, `keyframe_${String(index + 1).padStart(2, '0')}.png`);
58
- await downloadToFileFn(keyframeUrl, keyframePath);
59
- return keyframePath;
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, keyframeUrls, options = {}) {
88
+ export async function persistKeyframes(projectDir, keyframeResults, options = {}) {
63
89
  const keyframePaths = [];
64
- for (let i = 0; i < keyframeUrls.length; i += 1) {
65
- const keyframePath = await persistKeyframe(projectDir, keyframeUrls[i], i, options);
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,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
- }
@@ -1,95 +0,0 @@
1
- {
2
- "modelId": "black-forest-labs/flux-schnell",
3
- "provider": "replicate",
4
- "displayName": "FLUX Schnell",
5
- "category": "image-generation",
6
- "pricingSourceUrl": "https://replicate.com/black-forest-labs/flux-schnell",
7
- "pricingNotes": "Replicate pricing: $3 per 1000 output images.",
8
- "pricing": {
9
- "usdPerSecond": null,
10
- "usdPer1kInputTokens": null,
11
- "usdPer1kOutputTokens": null
12
- },
13
- "pricingRules": {
14
- "basis": "output_image_count",
15
- "usdPerImage": 0.003
16
- },
17
- "inputOptions": {
18
- "userConfigurable": [
19
- "num_outputs",
20
- "num_inference_steps",
21
- "seed",
22
- "output_format",
23
- "output_quality",
24
- "disable_safety_checker",
25
- "go_fast",
26
- "megapixels"
27
- ],
28
- "pipelineManaged": [
29
- "prompt",
30
- "aspect_ratio"
31
- ],
32
- "fields": {
33
- "num_outputs": {
34
- "type": "integer",
35
- "default": 1,
36
- "minimum": 1,
37
- "maximum": 4
38
- },
39
- "num_inference_steps": {
40
- "type": "integer",
41
- "default": 4,
42
- "minimum": 1,
43
- "maximum": 4
44
- },
45
- "seed": {
46
- "type": "integer",
47
- "nullable": true
48
- },
49
- "output_format": {
50
- "type": "string",
51
- "default": "webp",
52
- "enum": [
53
- "webp",
54
- "jpg",
55
- "png"
56
- ]
57
- },
58
- "output_quality": {
59
- "type": "integer",
60
- "default": 80,
61
- "minimum": 0,
62
- "maximum": 100
63
- },
64
- "disable_safety_checker": {
65
- "type": "boolean",
66
- "default": false
67
- },
68
- "go_fast": {
69
- "type": "boolean",
70
- "default": true
71
- },
72
- "megapixels": {
73
- "type": "string",
74
- "default": "1",
75
- "allowAnyString": true,
76
- "recommendedValues": [
77
- "1"
78
- ]
79
- },
80
- "prompt": {
81
- "type": "string",
82
- "required": true
83
- },
84
- "aspect_ratio": {
85
- "type": "string",
86
- "required": true,
87
- "enum": [
88
- "1:1",
89
- "16:9",
90
- "9:16"
91
- ]
92
- }
93
- }
94
- }
95
- }
@@ -1,71 +0,0 @@
1
- {
2
- "modelId": "bytedance/seedream-4",
3
- "provider": "replicate",
4
- "displayName": "Seedream 4",
5
- "category": "image-generation",
6
- "pricingSourceUrl": "https://replicate.com/bytedance/seedream-4",
7
- "pricingNotes": "Replicate pricing: $0.03 per output image.",
8
- "pricing": {
9
- "usdPerSecond": null,
10
- "usdPer1kInputTokens": null,
11
- "usdPer1kOutputTokens": null
12
- },
13
- "pricingRules": {
14
- "basis": "output_image_count",
15
- "usdPerImage": 0.03
16
- },
17
- "inputOptions": {
18
- "userConfigurable": [
19
- "size",
20
- "sequential_image_generation",
21
- "max_images",
22
- "enhance_prompt"
23
- ],
24
- "pipelineManaged": [
25
- "prompt",
26
- "aspect_ratio"
27
- ],
28
- "fields": {
29
- "size": {
30
- "type": "string",
31
- "default": "2K",
32
- "enum": [
33
- "1K",
34
- "2K",
35
- "4K"
36
- ]
37
- },
38
- "sequential_image_generation": {
39
- "type": "string",
40
- "default": "disabled",
41
- "enum": [
42
- "disabled",
43
- "auto"
44
- ]
45
- },
46
- "max_images": {
47
- "type": "integer",
48
- "default": 1,
49
- "minimum": 1,
50
- "maximum": 15
51
- },
52
- "enhance_prompt": {
53
- "type": "boolean",
54
- "default": true
55
- },
56
- "prompt": {
57
- "type": "string",
58
- "required": true
59
- },
60
- "aspect_ratio": {
61
- "type": "string",
62
- "required": true,
63
- "enum": [
64
- "1:1",
65
- "16:9",
66
- "9:16"
67
- ]
68
- }
69
- }
70
- }
71
- }
@@ -1,92 +0,0 @@
1
- {
2
- "modelId": "google/nano-banana-pro",
3
- "provider": "replicate",
4
- "displayName": "Nano Banana Pro",
5
- "category": "image-generation",
6
- "pricingSourceUrl": "https://replicate.com/google/nano-banana-pro",
7
- "pricingNotes": "Replicate pricing by target resolution: 1K/2K=$0.15 per output image, 4K=$0.30 per output image, fallback tier=$0.035 per output image.",
8
- "pricing": {
9
- "usdPerSecond": null,
10
- "usdPer1kInputTokens": null,
11
- "usdPer1kOutputTokens": null
12
- },
13
- "pricingRules": {
14
- "basis": "output_image_resolution",
15
- "tiers": [
16
- {
17
- "resolution": "1K",
18
- "usdPerImage": 0.15
19
- },
20
- {
21
- "resolution": "2K",
22
- "usdPerImage": 0.15
23
- },
24
- {
25
- "resolution": "4K",
26
- "usdPerImage": 0.3
27
- },
28
- {
29
- "resolution": "fallback",
30
- "usdPerImage": 0.035
31
- }
32
- ]
33
- },
34
- "inputOptions": {
35
- "userConfigurable": [
36
- "resolution",
37
- "output_format",
38
- "safety_filter_level",
39
- "allow_fallback_model"
40
- ],
41
- "pipelineManaged": [
42
- "prompt",
43
- "aspect_ratio"
44
- ],
45
- "fields": {
46
- "resolution": {
47
- "type": "string",
48
- "default": "2K",
49
- "enum": [
50
- "1K",
51
- "2K",
52
- "4K"
53
- ]
54
- },
55
- "output_format": {
56
- "type": "string",
57
- "default": "jpg",
58
- "enum": [
59
- "jpg",
60
- "png",
61
- "webp"
62
- ]
63
- },
64
- "safety_filter_level": {
65
- "type": "string",
66
- "default": "block_only_high",
67
- "enum": [
68
- "block_low_and_above",
69
- "block_medium_and_above",
70
- "block_only_high"
71
- ]
72
- },
73
- "allow_fallback_model": {
74
- "type": "boolean",
75
- "default": false
76
- },
77
- "prompt": {
78
- "type": "string",
79
- "required": true
80
- },
81
- "aspect_ratio": {
82
- "type": "string",
83
- "required": true,
84
- "enum": [
85
- "1:1",
86
- "16:9",
87
- "9:16"
88
- ]
89
- }
90
- }
91
- }
92
- }
@@ -1,107 +0,0 @@
1
- {
2
- "modelId": "prunaai/z-image-turbo",
3
- "provider": "replicate",
4
- "displayName": "Z Image Turbo",
5
- "category": "image-generation",
6
- "pricingSourceUrl": "https://replicate.com/prunaai/z-image-turbo",
7
- "pricingNotes": "Replicate pricing is tiered by output image resolution (megapixels).",
8
- "pricing": {
9
- "usdPerSecond": null,
10
- "usdPer1kInputTokens": null,
11
- "usdPer1kOutputTokens": null
12
- },
13
- "pricingRules": {
14
- "basis": "output_image_megapixels",
15
- "tiers": [
16
- {
17
- "maxMegapixels": 0.5,
18
- "usdPerImage": 0.0025
19
- },
20
- {
21
- "maxMegapixels": 1,
22
- "usdPerImage": 0.005
23
- },
24
- {
25
- "maxMegapixels": 2,
26
- "usdPerImage": 0.01
27
- },
28
- {
29
- "maxMegapixels": 3,
30
- "usdPerImage": 0.015
31
- },
32
- {
33
- "maxMegapixels": 4,
34
- "usdPerImage": 0.02
35
- }
36
- ]
37
- },
38
- "inputOptions": {
39
- "userConfigurable": [
40
- "num_inference_steps",
41
- "guidance_scale",
42
- "seed",
43
- "go_fast",
44
- "output_format",
45
- "output_quality"
46
- ],
47
- "pipelineManaged": [
48
- "prompt",
49
- "width",
50
- "height"
51
- ],
52
- "fields": {
53
- "num_inference_steps": {
54
- "type": "integer",
55
- "default": 8,
56
- "minimum": 1,
57
- "maximum": 50
58
- },
59
- "guidance_scale": {
60
- "type": "number",
61
- "default": 0,
62
- "minimum": 0,
63
- "maximum": 20
64
- },
65
- "seed": {
66
- "type": "integer",
67
- "nullable": true
68
- },
69
- "go_fast": {
70
- "type": "boolean",
71
- "default": false
72
- },
73
- "output_format": {
74
- "type": "string",
75
- "default": "jpg",
76
- "enum": [
77
- "jpg",
78
- "jpeg",
79
- "png",
80
- "webp"
81
- ]
82
- },
83
- "output_quality": {
84
- "type": "integer",
85
- "default": 80,
86
- "minimum": 0,
87
- "maximum": 100
88
- },
89
- "prompt": {
90
- "type": "string",
91
- "required": true
92
- },
93
- "width": {
94
- "type": "integer",
95
- "required": true,
96
- "minimum": 64,
97
- "maximum": 2048
98
- },
99
- "height": {
100
- "type": "integer",
101
- "required": true,
102
- "minimum": 64,
103
- "maximum": 2048
104
- }
105
- }
106
- }
107
- }
@@ -1,87 +0,0 @@
1
- import { MODELS } from '../config/models.js';
2
-
3
- function normalizeModelOptions(candidate) {
4
- if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
5
- return {};
6
- }
7
- return candidate;
8
- }
9
-
10
- function buildCommonPrompt(promptText, tone, index) {
11
- return `Cinematic documentary style, coherent character continuity, shot ${index + 1}, tone ${tone}. ${promptText}`;
12
- }
13
-
14
- function buildPrunaInput({ promptText, tone, index, width, height, modelOptions }) {
15
- return {
16
- ...normalizeModelOptions(modelOptions),
17
- prompt: buildCommonPrompt(promptText, tone, index),
18
- width,
19
- height
20
- };
21
- }
22
-
23
- function buildFluxInput({ promptText, tone, index, width, height, modelOptions }) {
24
- return {
25
- ...normalizeModelOptions(modelOptions),
26
- prompt: buildCommonPrompt(promptText, tone, index),
27
- aspect_ratio: 'custom',
28
- width,
29
- height
30
- };
31
- }
32
-
33
- function buildFluxSchnellInput({ promptText, tone, index, aspectRatio, modelOptions }) {
34
- return {
35
- ...normalizeModelOptions(modelOptions),
36
- prompt: buildCommonPrompt(promptText, tone, index),
37
- aspect_ratio: aspectRatio
38
- };
39
- }
40
-
41
- function buildNanoBananaProInput({ promptText, tone, index, aspectRatio, modelOptions }) {
42
- return {
43
- ...normalizeModelOptions(modelOptions),
44
- prompt: buildCommonPrompt(promptText, tone, index),
45
- aspect_ratio: aspectRatio
46
- };
47
- }
48
-
49
- function buildSeedream4Input({ promptText, tone, index, aspectRatio, modelOptions }) {
50
- return {
51
- ...normalizeModelOptions(modelOptions),
52
- prompt: buildCommonPrompt(promptText, tone, index),
53
- aspect_ratio: aspectRatio
54
- };
55
- }
56
-
57
- const TEXT_TO_IMAGE_ADAPTERS = {
58
- [MODELS.keyframe]: {
59
- modelId: MODELS.keyframe,
60
- buildInput: buildPrunaInput
61
- },
62
- [MODELS.flux]: {
63
- modelId: MODELS.flux,
64
- buildInput: buildFluxInput
65
- },
66
- [MODELS.fluxSchnell]: {
67
- modelId: MODELS.fluxSchnell,
68
- buildInput: buildFluxSchnellInput
69
- },
70
- [MODELS.nanoBananaPro]: {
71
- modelId: MODELS.nanoBananaPro,
72
- buildInput: buildNanoBananaProInput
73
- },
74
- [MODELS.seedream4]: {
75
- modelId: MODELS.seedream4,
76
- buildInput: buildSeedream4Input
77
- }
78
- };
79
-
80
- const DEFAULT_TEXT_TO_IMAGE_ADAPTER = {
81
- modelId: null,
82
- buildInput: buildPrunaInput
83
- };
84
-
85
- export function resolveTextToImageAdapter(modelId) {
86
- return TEXT_TO_IMAGE_ADAPTERS[modelId] || DEFAULT_TEXT_TO_IMAGE_ADAPTER;
87
- }