@telepat/rilo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/index.js +1 -0
- package/models/black-forest-labs__flux-2-pro.json +78 -0
- package/models/black-forest-labs__flux-schnell.json +95 -0
- package/models/bytedance__seedream-4.json +71 -0
- package/models/deepseek-ai__deepseek-v3.json +61 -0
- package/models/google__nano-banana-pro.json +92 -0
- package/models/google__veo-3.1-fast.json +93 -0
- package/models/google__veo-3.1.json +93 -0
- package/models/jaaari__kokoro-82m.json +86 -0
- package/models/kwaivgi__kling-v3-video.json +101 -0
- package/models/minimax__speech-02-turbo.json +141 -0
- package/models/pixverse__pixverse-v5.6.json +113 -0
- package/models/prunaai__z-image-turbo.json +107 -0
- package/models/resemble-ai__chatterbox-turbo.json +102 -0
- package/models/wan-video__wan-2.2-i2v-fast.json +139 -0
- package/package.json +67 -0
- package/src/api/firebaseFunction.js +46 -0
- package/src/api/middleware/auth.js +70 -0
- package/src/api/openapi/generateOpenApi.js +21 -0
- package/src/api/openapi/spec.js +831 -0
- package/src/api/routes/jobs.js +45 -0
- package/src/api/routes/projectAssets.js +63 -0
- package/src/api/routes/projects.js +647 -0
- package/src/api/routes/webhooks.js +13 -0
- package/src/api/server.js +88 -0
- package/src/backends/firebaseClient.js +57 -0
- package/src/backends/outputBackend.js +186 -0
- package/src/backends/projectMetadataBackend.js +550 -0
- package/src/cli/commands/openHome.js +70 -0
- package/src/cli/commands/settingsFlow.js +196 -0
- package/src/cli/index.js +192 -0
- package/src/config/env.js +158 -0
- package/src/config/keystore.js +175 -0
- package/src/config/models.js +281 -0
- package/src/config/settingsSchema.js +214 -0
- package/src/media/ffmpeg.js +144 -0
- package/src/media/files.js +77 -0
- package/src/media/subtitles.js +444 -0
- package/src/observability/apiTrace.js +17 -0
- package/src/observability/logger.js +7 -0
- package/src/observability/metrics.js +10 -0
- package/src/pipeline/inputSanitizer.js +6 -0
- package/src/pipeline/orchestrator.js +1669 -0
- package/src/policy/contentGuardrails.js +30 -0
- package/src/providers/predictions.js +188 -0
- package/src/providers/replicateClient.js +12 -0
- package/src/steps/alignSubtitles.js +156 -0
- package/src/steps/burnInSubtitles.js +22 -0
- package/src/steps/composeFinalVideo.js +57 -0
- package/src/steps/generateKeyframes.js +70 -0
- package/src/steps/generateVideoSegments.js +95 -0
- package/src/steps/generateVoiceover.js +128 -0
- package/src/steps/imageToVideoAdapters.js +100 -0
- package/src/steps/script.js +177 -0
- package/src/steps/textToImageAdapters.js +87 -0
- package/src/store/assetStore.js +5 -0
- package/src/store/jobStore.js +102 -0
- package/src/store/projectAnalyticsStore.js +625 -0
- package/src/store/projectStore.js +684 -0
- package/src/store/settingsStore.js +155 -0
- package/src/store/staleAssetStore.js +63 -0
- package/src/types/job.js +28 -0
- package/src/types/media.js +28 -0
- package/src/worker/processor.js +24 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
const SERVICE_NAME = 'rilo';
|
|
7
|
+
const SECRETS_FILE = '.secrets';
|
|
8
|
+
|
|
9
|
+
let keytarClientPromise;
|
|
10
|
+
|
|
11
|
+
async function getKeytarClient() {
|
|
12
|
+
if (keytarClientPromise !== undefined) {
|
|
13
|
+
return keytarClientPromise;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
keytarClientPromise = (async () => {
|
|
17
|
+
try {
|
|
18
|
+
const imported = await import('keytar');
|
|
19
|
+
const candidate = imported.default ?? imported;
|
|
20
|
+
if (
|
|
21
|
+
candidate &&
|
|
22
|
+
typeof candidate === 'object' &&
|
|
23
|
+
typeof candidate.setPassword === 'function' &&
|
|
24
|
+
typeof candidate.getPassword === 'function' &&
|
|
25
|
+
typeof candidate.deletePassword === 'function'
|
|
26
|
+
) {
|
|
27
|
+
return candidate;
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// keytar is optional; fall through to encrypted file storage
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
})();
|
|
34
|
+
|
|
35
|
+
return keytarClientPromise;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getRiloDir() {
|
|
39
|
+
return path.join(os.homedir(), '.rilo');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getFallbackPath() {
|
|
43
|
+
return path.join(getRiloDir(), SECRETS_FILE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getMachineKey() {
|
|
47
|
+
const username = process.env.USER ?? process.env.USERNAME ?? 'user';
|
|
48
|
+
return crypto
|
|
49
|
+
.createHash('sha256')
|
|
50
|
+
.update(`${process.platform}:${process.arch}:${username}`)
|
|
51
|
+
.digest();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function encrypt(value) {
|
|
55
|
+
const iv = crypto.randomBytes(16);
|
|
56
|
+
const key = getMachineKey();
|
|
57
|
+
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
|
58
|
+
const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
|
|
59
|
+
return `${iv.toString('hex')}:${encrypted.toString('hex')}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function decrypt(value) {
|
|
63
|
+
const colonIdx = value.indexOf(':');
|
|
64
|
+
if (colonIdx === -1) throw new Error('Invalid encrypted payload');
|
|
65
|
+
const ivHex = value.slice(0, colonIdx);
|
|
66
|
+
const contentHex = value.slice(colonIdx + 1);
|
|
67
|
+
if (!ivHex || !contentHex) throw new Error('Invalid encrypted payload');
|
|
68
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
69
|
+
const content = Buffer.from(contentHex, 'hex');
|
|
70
|
+
const decipher = crypto.createDecipheriv('aes-256-cbc', getMachineKey(), iv);
|
|
71
|
+
const decrypted = Buffer.concat([decipher.update(content), decipher.final()]);
|
|
72
|
+
return decrypted.toString('utf8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readFallbackSecrets() {
|
|
76
|
+
const filePath = getFallbackPath();
|
|
77
|
+
if (!fs.existsSync(filePath)) return {};
|
|
78
|
+
try {
|
|
79
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
80
|
+
const decrypted = decrypt(raw);
|
|
81
|
+
const parsed = JSON.parse(decrypted);
|
|
82
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
|
|
83
|
+
} catch {
|
|
84
|
+
// corrupt or from old format; treat as empty
|
|
85
|
+
}
|
|
86
|
+
return {};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let _warnedFallback = false;
|
|
90
|
+
|
|
91
|
+
function writeFallbackSecrets(secrets) {
|
|
92
|
+
const filtered = Object.fromEntries(
|
|
93
|
+
Object.entries(secrets).filter(([, v]) => typeof v === 'string' && v.trim() !== '')
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const filePath = getFallbackPath();
|
|
97
|
+
if (Object.keys(filtered).length === 0) {
|
|
98
|
+
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const dir = getRiloDir();
|
|
103
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
104
|
+
|
|
105
|
+
if (!_warnedFallback) {
|
|
106
|
+
_warnedFallback = true;
|
|
107
|
+
console.warn(
|
|
108
|
+
'[rilo] Native OS keystore unavailable. ' +
|
|
109
|
+
'Secrets are stored in an AES-256 encrypted file (~/.rilo/.secrets). ' +
|
|
110
|
+
'For stronger security, install a keytar-compatible libsecret on your system.'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fs.writeFileSync(filePath, encrypt(JSON.stringify(filtered)), { encoding: 'utf8', mode: 0o600 });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Store a secret. Tries the OS native keystore first; falls back to
|
|
119
|
+
* an AES-256 encrypted file under ~/.rilo/.secrets.
|
|
120
|
+
* @param {string} key - account/key name (e.g. 'replicateApiToken')
|
|
121
|
+
* @param {string} value - secret value
|
|
122
|
+
*/
|
|
123
|
+
export async function setSecret(key, value) {
|
|
124
|
+
const client = await getKeytarClient();
|
|
125
|
+
if (client) {
|
|
126
|
+
try {
|
|
127
|
+
await client.setPassword(SERVICE_NAME, key, value);
|
|
128
|
+
return;
|
|
129
|
+
} catch {
|
|
130
|
+
// fall through
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const current = readFallbackSecrets();
|
|
134
|
+
current[key] = value;
|
|
135
|
+
writeFallbackSecrets(current);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Retrieve a secret. Returns null if not stored.
|
|
140
|
+
* @param {string} key
|
|
141
|
+
* @returns {Promise<string|null>}
|
|
142
|
+
*/
|
|
143
|
+
export async function getSecret(key) {
|
|
144
|
+
const client = await getKeytarClient();
|
|
145
|
+
if (client) {
|
|
146
|
+
try {
|
|
147
|
+
const value = await client.getPassword(SERVICE_NAME, key);
|
|
148
|
+
if (value != null) return value;
|
|
149
|
+
} catch {
|
|
150
|
+
// fall through
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return readFallbackSecrets()[key] ?? null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Delete a stored secret.
|
|
158
|
+
* @param {string} key
|
|
159
|
+
*/
|
|
160
|
+
export async function deleteSecret(key) {
|
|
161
|
+
const client = await getKeytarClient();
|
|
162
|
+
if (client) {
|
|
163
|
+
try {
|
|
164
|
+
await client.deletePassword(SERVICE_NAME, key);
|
|
165
|
+
} catch {
|
|
166
|
+
// ignore keychain errors; continue to clean up file fallback
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const current = readFallbackSecrets();
|
|
170
|
+
delete current[key];
|
|
171
|
+
writeFallbackSecrets(current);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Exported for testing
|
|
175
|
+
export { readFallbackSecrets, writeFallbackSecrets, encrypt, decrypt, getMachineKey };
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const MODELS_DIR = path.resolve(CONFIG_DIR, '../../models');
|
|
7
|
+
|
|
8
|
+
export const MODELS = {
|
|
9
|
+
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
|
+
video: 'wan-video/wan-2.2-i2v-fast',
|
|
16
|
+
klingVideo3: 'kwaivgi/kling-v3-video',
|
|
17
|
+
pixverseV56: 'pixverse/pixverse-v5.6',
|
|
18
|
+
veo31: 'google/veo-3.1',
|
|
19
|
+
veo31Fast: 'google/veo-3.1-fast',
|
|
20
|
+
tts: 'minimax/speech-02-turbo',
|
|
21
|
+
chatterboxTurbo: 'resemble-ai/chatterbox-turbo',
|
|
22
|
+
kokoro82m: 'jaaari/kokoro-82m'
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const MODEL_CATEGORIES = {
|
|
26
|
+
textToText: 'textToText',
|
|
27
|
+
textToSpeech: 'textToSpeech',
|
|
28
|
+
textToImage: 'textToImage',
|
|
29
|
+
imageTextToVideo: 'imageTextToVideo'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const DEFAULT_MODEL_SELECTIONS = {
|
|
33
|
+
[MODEL_CATEGORIES.textToText]: MODELS.deepseek,
|
|
34
|
+
[MODEL_CATEGORIES.textToSpeech]: MODELS.tts,
|
|
35
|
+
[MODEL_CATEGORIES.textToImage]: MODELS.keyframe,
|
|
36
|
+
[MODEL_CATEGORIES.imageTextToVideo]: MODELS.video
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const MODEL_SELECTION_KEYS = Object.keys(DEFAULT_MODEL_SELECTIONS);
|
|
40
|
+
export const MODEL_OPTION_KEYS = [...MODEL_SELECTION_KEYS];
|
|
41
|
+
|
|
42
|
+
const MODEL_METADATA_FILES = {
|
|
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
|
+
[MODELS.video]: 'wan-video__wan-2.2-i2v-fast.json',
|
|
50
|
+
[MODELS.klingVideo3]: 'kwaivgi__kling-v3-video.json',
|
|
51
|
+
[MODELS.pixverseV56]: 'pixverse__pixverse-v5.6.json',
|
|
52
|
+
[MODELS.veo31]: 'google__veo-3.1.json',
|
|
53
|
+
[MODELS.veo31Fast]: 'google__veo-3.1-fast.json',
|
|
54
|
+
[MODELS.tts]: 'minimax__speech-02-turbo.json',
|
|
55
|
+
[MODELS.chatterboxTurbo]: 'resemble-ai__chatterbox-turbo.json',
|
|
56
|
+
[MODELS.kokoro82m]: 'jaaari__kokoro-82m.json'
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const MODEL_IDS_BY_CATEGORY = {
|
|
60
|
+
[MODEL_CATEGORIES.textToText]: [MODELS.deepseek],
|
|
61
|
+
[MODEL_CATEGORIES.textToSpeech]: [MODELS.tts, MODELS.chatterboxTurbo, MODELS.kokoro82m],
|
|
62
|
+
[MODEL_CATEGORIES.textToImage]: [MODELS.keyframe, MODELS.flux, MODELS.fluxSchnell, MODELS.nanoBananaPro, MODELS.seedream4],
|
|
63
|
+
[MODEL_CATEGORIES.imageTextToVideo]: [MODELS.video, MODELS.klingVideo3, MODELS.pixverseV56, MODELS.veo31, MODELS.veo31Fast]
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export function toNullableNumber(value) {
|
|
67
|
+
if (value === null || value === undefined || value === '') {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const parsed = Number.parseFloat(value);
|
|
71
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function normalizePricing(pricing = {}) {
|
|
75
|
+
return {
|
|
76
|
+
usdPerSecond: toNullableNumber(pricing.usdPerSecond),
|
|
77
|
+
usdPer1kInputTokens: toNullableNumber(pricing.usdPer1kInputTokens),
|
|
78
|
+
usdPer1kOutputTokens: toNullableNumber(pricing.usdPer1kOutputTokens)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function readModelMetadata(modelId) {
|
|
83
|
+
const fileName = MODEL_METADATA_FILES[modelId];
|
|
84
|
+
if (!fileName) {
|
|
85
|
+
return {
|
|
86
|
+
modelId,
|
|
87
|
+
pricing: normalizePricing({})
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const filePath = path.join(MODELS_DIR, fileName);
|
|
92
|
+
try {
|
|
93
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
return {
|
|
96
|
+
...parsed,
|
|
97
|
+
modelId: parsed.modelId || modelId,
|
|
98
|
+
pricing: normalizePricing(parsed.pricing || {})
|
|
99
|
+
};
|
|
100
|
+
} catch {
|
|
101
|
+
return {
|
|
102
|
+
modelId,
|
|
103
|
+
pricing: normalizePricing({})
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const MODEL_METADATA = Object.fromEntries(
|
|
109
|
+
Object.values(MODELS).map((modelId) => [modelId, readModelMetadata(modelId)])
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
export const SUPPORTED_MODEL_IDS = Object.keys(MODEL_METADATA);
|
|
113
|
+
|
|
114
|
+
export const MODEL_PRICING = Object.fromEntries(
|
|
115
|
+
Object.entries(MODEL_METADATA).map(([modelId, metadata]) => [modelId, normalizePricing(metadata.pricing || {})])
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
function isPlainObject(value) {
|
|
119
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getSafeInputOptions(metadata = {}) {
|
|
123
|
+
const inputOptions = isPlainObject(metadata.inputOptions) ? metadata.inputOptions : {};
|
|
124
|
+
const userConfigurable = Array.isArray(inputOptions.userConfigurable)
|
|
125
|
+
? inputOptions.userConfigurable.filter((entry) => typeof entry === 'string' && entry.trim())
|
|
126
|
+
: [];
|
|
127
|
+
const pipelineManaged = Array.isArray(inputOptions.pipelineManaged)
|
|
128
|
+
? inputOptions.pipelineManaged.filter((entry) => typeof entry === 'string' && entry.trim())
|
|
129
|
+
: [];
|
|
130
|
+
const fields = isPlainObject(inputOptions.fields) ? inputOptions.fields : {};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
userConfigurable,
|
|
134
|
+
pipelineManaged,
|
|
135
|
+
fields
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getModelInputOptions(modelId) {
|
|
140
|
+
return getSafeInputOptions(MODEL_METADATA[modelId]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function resolveModelInputOptionsForCategory(category, modelSelections = {}) {
|
|
144
|
+
const modelId = resolveModelForCategory(category, modelSelections);
|
|
145
|
+
return getModelInputOptions(modelId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function resolveDefaultModelOptionsForCategory(category, modelSelections = {}) {
|
|
149
|
+
const inputOptions = resolveModelInputOptionsForCategory(category, modelSelections);
|
|
150
|
+
const defaults = {};
|
|
151
|
+
|
|
152
|
+
for (const optionKey of inputOptions.userConfigurable) {
|
|
153
|
+
const field = inputOptions.fields[optionKey];
|
|
154
|
+
if (field && Object.prototype.hasOwnProperty.call(field, 'default')) {
|
|
155
|
+
defaults[optionKey] = field.default;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return defaults;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function getDefaultProjectModelOptions(modelSelections = {}) {
|
|
163
|
+
return Object.fromEntries(
|
|
164
|
+
MODEL_OPTION_KEYS.map((category) => [category, resolveDefaultModelOptionsForCategory(category, modelSelections)])
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function resolveProjectModelOptions(modelOptions = {}, modelSelections = {}) {
|
|
169
|
+
const defaults = getDefaultProjectModelOptions(modelSelections);
|
|
170
|
+
const normalizedInput = isPlainObject(modelOptions) ? modelOptions : {};
|
|
171
|
+
|
|
172
|
+
return Object.fromEntries(
|
|
173
|
+
MODEL_OPTION_KEYS.map((category) => {
|
|
174
|
+
const candidate = normalizedInput[category];
|
|
175
|
+
if (!isPlainObject(candidate)) {
|
|
176
|
+
return [category, defaults[category]];
|
|
177
|
+
}
|
|
178
|
+
return [category, { ...defaults[category], ...candidate }];
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function isKnownModelId(modelId) {
|
|
184
|
+
return typeof modelId === 'string' && SUPPORTED_MODEL_IDS.includes(modelId);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function resolveProjectModelSelections(modelSelections = {}) {
|
|
188
|
+
if (!modelSelections || typeof modelSelections !== 'object' || Array.isArray(modelSelections)) {
|
|
189
|
+
return {
|
|
190
|
+
...DEFAULT_MODEL_SELECTIONS
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const resolved = {
|
|
195
|
+
...DEFAULT_MODEL_SELECTIONS
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
for (const key of MODEL_SELECTION_KEYS) {
|
|
199
|
+
const candidate = modelSelections[key];
|
|
200
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
201
|
+
resolved[key] = candidate.trim();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return resolved;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function resolveModelForCategory(category, modelSelections = {}) {
|
|
209
|
+
if (!MODEL_SELECTION_KEYS.includes(category)) {
|
|
210
|
+
throw new Error(`Unknown model category: ${category}`);
|
|
211
|
+
}
|
|
212
|
+
return resolveProjectModelSelections(modelSelections)[category];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function getSupportedModelIdsForCategory(category) {
|
|
216
|
+
if (!MODEL_SELECTION_KEYS.includes(category)) {
|
|
217
|
+
throw new Error(`Unknown model category: ${category}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return [...(MODEL_IDS_BY_CATEGORY[category] || [])];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export const DEFAULT_VIDEO_CONFIG = {
|
|
224
|
+
width: 720,
|
|
225
|
+
height: 1280,
|
|
226
|
+
fps: 16,
|
|
227
|
+
durationSec: 60,
|
|
228
|
+
shots: 12,
|
|
229
|
+
segmentDurationSec: 5,
|
|
230
|
+
renderSpecVersion: 2
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export function resolveTargetDurationSec(config = {}) {
|
|
234
|
+
const value = config.targetDurationSec;
|
|
235
|
+
if (Number.isInteger(value) && value >= 5) {
|
|
236
|
+
return value;
|
|
237
|
+
}
|
|
238
|
+
return DEFAULT_VIDEO_CONFIG.durationSec;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function resolveShotCount(config = {}) {
|
|
242
|
+
const targetDurationSec = resolveTargetDurationSec(config);
|
|
243
|
+
return Math.max(1, Math.ceil(targetDurationSec / DEFAULT_VIDEO_CONFIG.segmentDurationSec));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export const ASPECT_RATIO_PRESETS = {
|
|
247
|
+
'1:1': {
|
|
248
|
+
keyframeWidth: 1024,
|
|
249
|
+
keyframeHeight: 1024,
|
|
250
|
+
videoResolution: '720p'
|
|
251
|
+
},
|
|
252
|
+
'16:9': {
|
|
253
|
+
keyframeWidth: 1024,
|
|
254
|
+
keyframeHeight: 576,
|
|
255
|
+
videoResolution: '720p'
|
|
256
|
+
},
|
|
257
|
+
'9:16': {
|
|
258
|
+
keyframeWidth: 576,
|
|
259
|
+
keyframeHeight: 1024,
|
|
260
|
+
videoResolution: '720p'
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export function resolveKeyframeSize(config = {}) {
|
|
265
|
+
const aspectRatio = config.aspectRatio || '9:16';
|
|
266
|
+
const preset = ASPECT_RATIO_PRESETS[aspectRatio] || ASPECT_RATIO_PRESETS['9:16'];
|
|
267
|
+
|
|
268
|
+
if (Number.isInteger(config.keyframeWidth) && Number.isInteger(config.keyframeHeight)) {
|
|
269
|
+
return {
|
|
270
|
+
width: config.keyframeWidth,
|
|
271
|
+
height: config.keyframeHeight,
|
|
272
|
+
key: `${config.keyframeWidth}x${config.keyframeHeight}`
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
width: preset.keyframeWidth,
|
|
278
|
+
height: preset.keyframeHeight,
|
|
279
|
+
key: `${preset.keyframeWidth}x${preset.keyframeHeight}`
|
|
280
|
+
};
|
|
281
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Describes every setting that can be managed via `rilo settings`.
|
|
3
|
+
*
|
|
4
|
+
* type: 'number' | 'string' | 'secure'
|
|
5
|
+
* - 'secure' => stored in the OS keystore / encrypted file (never in config.json)
|
|
6
|
+
* - 'number' => parsed as an integer
|
|
7
|
+
* - 'string' => stored as-is
|
|
8
|
+
*
|
|
9
|
+
* envNames: the env var names that map to this setting (first has priority).
|
|
10
|
+
* The env.js module already resolves these in order, so we only list
|
|
11
|
+
* them here for display and documentation purposes.
|
|
12
|
+
*
|
|
13
|
+
* configKey: the camelCase key written to ~/.rilo/config.json (public settings only).
|
|
14
|
+
*
|
|
15
|
+
* keystoreKey: the account name used with keytar / encrypted file (secure settings only).
|
|
16
|
+
*/
|
|
17
|
+
export const SETTINGS = [
|
|
18
|
+
// ── Secure settings (stored in keystore) ──────────────────────────────────
|
|
19
|
+
{
|
|
20
|
+
id: 'replicateApiToken',
|
|
21
|
+
label: 'Replicate API Token',
|
|
22
|
+
description: 'Your Replicate API key (replicate.com/account/api-tokens).',
|
|
23
|
+
type: 'secure',
|
|
24
|
+
keystoreKey: 'replicateApiToken',
|
|
25
|
+
envNames: ['RILO_REPLICATE_API_TOKEN', 'REPLICATE_API_TOKEN'],
|
|
26
|
+
default: ''
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'apiBearerToken',
|
|
30
|
+
label: 'API Bearer Token',
|
|
31
|
+
description: 'Bearer token for authenticating requests to the rilo HTTP API.',
|
|
32
|
+
type: 'secure',
|
|
33
|
+
keystoreKey: 'apiBearerToken',
|
|
34
|
+
envNames: ['RILO_API_BEARER_TOKEN', 'API_BEARER_TOKEN'],
|
|
35
|
+
default: ''
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// ── Public settings (stored in ~/.rilo/config.json) ───────────────────────
|
|
39
|
+
{
|
|
40
|
+
id: 'maxRetries',
|
|
41
|
+
label: 'Max Retries',
|
|
42
|
+
description: 'Number of times to retry a failed prediction before giving up.',
|
|
43
|
+
type: 'number',
|
|
44
|
+
configKey: 'maxRetries',
|
|
45
|
+
envNames: ['MAX_RETRIES'],
|
|
46
|
+
default: 2,
|
|
47
|
+
validate(v) {
|
|
48
|
+
const n = Number(v);
|
|
49
|
+
if (!Number.isInteger(n) || n < 0) return 'Must be a non-negative integer.';
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'retryDelayMs',
|
|
55
|
+
label: 'Retry Delay (ms)',
|
|
56
|
+
description: 'Milliseconds to wait between retries.',
|
|
57
|
+
type: 'number',
|
|
58
|
+
configKey: 'retryDelayMs',
|
|
59
|
+
envNames: ['RETRY_DELAY_MS'],
|
|
60
|
+
default: 2500,
|
|
61
|
+
validate(v) {
|
|
62
|
+
const n = Number(v);
|
|
63
|
+
if (!Number.isInteger(n) || n < 0) return 'Must be a non-negative integer.';
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'predictionPollIntervalMs',
|
|
69
|
+
label: 'Poll Interval (ms)',
|
|
70
|
+
description: 'How often (ms) to poll Replicate for prediction status.',
|
|
71
|
+
type: 'number',
|
|
72
|
+
configKey: 'predictionPollIntervalMs',
|
|
73
|
+
envNames: ['PREDICTION_POLL_INTERVAL_MS'],
|
|
74
|
+
default: 1500,
|
|
75
|
+
validate(v) {
|
|
76
|
+
const n = Number(v);
|
|
77
|
+
if (!Number.isInteger(n) || n < 100) return 'Must be an integer ≥ 100.';
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'predictionMaxWaitMs',
|
|
83
|
+
label: 'Max Prediction Wait (ms)',
|
|
84
|
+
description: 'Maximum time (ms) to wait for a single prediction to complete.',
|
|
85
|
+
type: 'number',
|
|
86
|
+
configKey: 'predictionMaxWaitMs',
|
|
87
|
+
envNames: ['PREDICTION_MAX_WAIT_MS'],
|
|
88
|
+
default: 600000,
|
|
89
|
+
validate(v) {
|
|
90
|
+
const n = Number(v);
|
|
91
|
+
if (!Number.isInteger(n) || n < 1000) return 'Must be an integer ≥ 1000.';
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'downloadTimeoutMs',
|
|
97
|
+
label: 'Download Timeout (ms)',
|
|
98
|
+
description: 'Timeout (ms) for downloading generated media files.',
|
|
99
|
+
type: 'number',
|
|
100
|
+
configKey: 'downloadTimeoutMs',
|
|
101
|
+
envNames: ['DOWNLOAD_TIMEOUT_MS'],
|
|
102
|
+
default: 20000,
|
|
103
|
+
validate(v) {
|
|
104
|
+
const n = Number(v);
|
|
105
|
+
if (!Number.isInteger(n) || n < 1000) return 'Must be an integer ≥ 1000.';
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'downloadMaxBytes',
|
|
111
|
+
label: 'Download Max Size (bytes)',
|
|
112
|
+
description: 'Maximum allowed size in bytes for a downloaded file.',
|
|
113
|
+
type: 'number',
|
|
114
|
+
configKey: 'downloadMaxBytes',
|
|
115
|
+
envNames: ['DOWNLOAD_MAX_BYTES'],
|
|
116
|
+
default: 104857600,
|
|
117
|
+
validate(v) {
|
|
118
|
+
const n = Number(v);
|
|
119
|
+
if (!Number.isInteger(n) || n < 1) return 'Must be a positive integer.';
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'downloadAllowedHosts',
|
|
125
|
+
label: 'Download Allowed Hosts',
|
|
126
|
+
description: 'Comma-separated list of hostnames allowed for media downloads.',
|
|
127
|
+
type: 'string',
|
|
128
|
+
configKey: 'downloadAllowedHosts',
|
|
129
|
+
envNames: ['DOWNLOAD_ALLOWED_HOSTS'],
|
|
130
|
+
default: 'replicate.delivery,replicate.com',
|
|
131
|
+
validate(v) {
|
|
132
|
+
if (!v || !v.trim()) return 'Must not be empty.';
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'apiDefaultLogsLimit',
|
|
138
|
+
label: 'Default Logs Limit',
|
|
139
|
+
description: 'Default number of log entries returned by the API.',
|
|
140
|
+
type: 'number',
|
|
141
|
+
configKey: 'apiDefaultLogsLimit',
|
|
142
|
+
envNames: ['API_DEFAULT_LOGS_LIMIT'],
|
|
143
|
+
default: 100,
|
|
144
|
+
validate(v) {
|
|
145
|
+
const n = Number(v);
|
|
146
|
+
if (!Number.isInteger(n) || n < 1) return 'Must be a positive integer.';
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'apiMaxLogsLimit',
|
|
152
|
+
label: 'Max Logs Limit',
|
|
153
|
+
description: 'Hard cap on log entries returned by the API.',
|
|
154
|
+
type: 'number',
|
|
155
|
+
configKey: 'apiMaxLogsLimit',
|
|
156
|
+
envNames: ['API_MAX_LOGS_LIMIT'],
|
|
157
|
+
default: 1000,
|
|
158
|
+
validate(v) {
|
|
159
|
+
const n = Number(v);
|
|
160
|
+
if (!Number.isInteger(n) || n < 1) return 'Must be a positive integer.';
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'ffmpegBin',
|
|
166
|
+
label: 'ffmpeg Binary',
|
|
167
|
+
description: 'Path or command name for the ffmpeg binary.',
|
|
168
|
+
type: 'string',
|
|
169
|
+
configKey: 'ffmpegBin',
|
|
170
|
+
envNames: ['FFMPEG_BIN'],
|
|
171
|
+
default: 'ffmpeg',
|
|
172
|
+
validate(v) {
|
|
173
|
+
if (!v || !v.trim()) return 'Must not be empty.';
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
id: 'ffprobeBin',
|
|
179
|
+
label: 'ffprobe Binary',
|
|
180
|
+
description: 'Path or command name for the ffprobe binary.',
|
|
181
|
+
type: 'string',
|
|
182
|
+
configKey: 'ffprobeBin',
|
|
183
|
+
envNames: ['FFPROBE_BIN'],
|
|
184
|
+
default: 'ffprobe',
|
|
185
|
+
validate(v) {
|
|
186
|
+
if (!v || !v.trim()) return 'Must not be empty.';
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
id: 'ffsubsyncBin',
|
|
192
|
+
label: 'ffsubsync Binary',
|
|
193
|
+
description: 'Path or command name for the ffsubsync binary (optional subtitle tool).',
|
|
194
|
+
type: 'string',
|
|
195
|
+
configKey: 'ffsubsyncBin',
|
|
196
|
+
envNames: ['FFSUBSYNC_BIN'],
|
|
197
|
+
default: 'ffsubsync',
|
|
198
|
+
validate(v) {
|
|
199
|
+
if (!v || !v.trim()) return 'Must not be empty.';
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
/** Lookup by id */
|
|
206
|
+
export function getSettingById(id) {
|
|
207
|
+
return SETTINGS.find((s) => s.id === id) ?? null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Settings stored in keystore */
|
|
211
|
+
export const SECURE_SETTINGS = SETTINGS.filter((s) => s.type === 'secure');
|
|
212
|
+
|
|
213
|
+
/** Settings stored in ~/.rilo/config.json */
|
|
214
|
+
export const PUBLIC_SETTINGS = SETTINGS.filter((s) => s.type !== 'secure');
|