@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 +101 -0
- package/README.md +2 -1
- package/README.zh-CN.md +2 -1
- package/package.json +2 -1
- package/src/api/firebaseFunction.js +1 -1
- package/src/cli/index.js +1 -1
- 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/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/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 (`
|
|
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
|
-
- 环境变量(`
|
|
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.
|
|
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
|
},
|
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
|
|
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.
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
152
|
+
throw new Error('Missing TELEPAT_REPLICATE_TOKEN in environment');
|
|
151
153
|
}
|
|
152
154
|
}
|
|
153
155
|
|
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,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
|
-
}
|