feedeas 0.1.0-alpha.15 → 0.1.0-alpha.18
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/dist/cli/index.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
4
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
5
|
-
}) : x)(function(x) {
|
|
6
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
7
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
8
|
-
});
|
|
9
3
|
var __esm = (fn, res) => function __init() {
|
|
10
4
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
5
|
};
|
|
@@ -92,14 +86,14 @@ var init_playwright_installer = __esm({
|
|
|
92
86
|
});
|
|
93
87
|
|
|
94
88
|
// src/cli/index.ts
|
|
95
|
-
import { Command as
|
|
89
|
+
import { Command as Command16 } from "commander";
|
|
96
90
|
|
|
97
91
|
// src/cli/commands/record.ts
|
|
98
92
|
import { Command } from "commander";
|
|
99
93
|
import { chromium } from "playwright-core";
|
|
100
94
|
import { spawn as spawn4, spawnSync } from "child_process";
|
|
101
|
-
import
|
|
102
|
-
import
|
|
95
|
+
import fs8 from "fs";
|
|
96
|
+
import path8 from "path";
|
|
103
97
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
104
98
|
|
|
105
99
|
// src/cli/services/ffprobe.ts
|
|
@@ -165,10 +159,91 @@ var FFprobeService = class {
|
|
|
165
159
|
}
|
|
166
160
|
};
|
|
167
161
|
|
|
162
|
+
// src/cli/services/assets/resolver.ts
|
|
163
|
+
import fs2 from "node:fs";
|
|
164
|
+
import path from "node:path";
|
|
165
|
+
function looksLikeScheme(uri) {
|
|
166
|
+
return /^[a-z][a-z0-9+.-]*:/.test(uri);
|
|
167
|
+
}
|
|
168
|
+
var RelativeFsResolver = class {
|
|
169
|
+
supports(uri) {
|
|
170
|
+
if (!uri) return false;
|
|
171
|
+
if (uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("data:")) return false;
|
|
172
|
+
if (uri.startsWith("gcp://")) return false;
|
|
173
|
+
return !looksLikeScheme(uri) || uri.startsWith("file://") || uri.startsWith("/") || uri.startsWith("./") || uri.startsWith("../");
|
|
174
|
+
}
|
|
175
|
+
resolve(uri, rootPath) {
|
|
176
|
+
if (uri.startsWith("file://")) {
|
|
177
|
+
const localPath2 = decodeURIComponent(new URL(uri).pathname);
|
|
178
|
+
return { type: "local", uri, localPath: localPath2 };
|
|
179
|
+
}
|
|
180
|
+
const localPath = path.isAbsolute(uri) ? uri : path.resolve(rootPath, uri);
|
|
181
|
+
return { type: "local", uri, localPath };
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
var HttpResolver = class {
|
|
185
|
+
supports(uri) {
|
|
186
|
+
return uri.startsWith("http://") || uri.startsWith("https://");
|
|
187
|
+
}
|
|
188
|
+
resolve(uri) {
|
|
189
|
+
return { type: "remote", uri, remoteUrl: uri };
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
var GcpResolver = class {
|
|
193
|
+
supports(uri) {
|
|
194
|
+
return uri.startsWith("gcp://");
|
|
195
|
+
}
|
|
196
|
+
resolve(uri) {
|
|
197
|
+
return {
|
|
198
|
+
type: "unsupported",
|
|
199
|
+
uri,
|
|
200
|
+
reason: "gcp:// resolver is configured as a scaffold. Add a concrete GCP resolver integration to enable this scheme."
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
var AssetResolverRegistry = class {
|
|
205
|
+
constructor(resolvers) {
|
|
206
|
+
this.resolvers = resolvers;
|
|
207
|
+
}
|
|
208
|
+
resolve(uri, rootPath) {
|
|
209
|
+
const resolver = this.resolvers.find((entry) => entry.supports(uri));
|
|
210
|
+
if (!resolver) {
|
|
211
|
+
return {
|
|
212
|
+
type: "unsupported",
|
|
213
|
+
uri,
|
|
214
|
+
reason: `Unsupported URI scheme for asset: ${uri}`
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return resolver.resolve(uri, rootPath);
|
|
218
|
+
}
|
|
219
|
+
assertUsable(uri, rootPath) {
|
|
220
|
+
const resolved = this.resolve(uri, rootPath);
|
|
221
|
+
if (resolved.type === "unsupported") {
|
|
222
|
+
return { ok: false, reason: resolved.reason };
|
|
223
|
+
}
|
|
224
|
+
if (resolved.type === "local" && !fs2.existsSync(resolved.localPath)) {
|
|
225
|
+
return { ok: false, reason: `Asset not found at ${resolved.localPath}` };
|
|
226
|
+
}
|
|
227
|
+
return { ok: true };
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function createDefaultAssetResolverRegistry() {
|
|
231
|
+
return new AssetResolverRegistry([
|
|
232
|
+
new HttpResolver(),
|
|
233
|
+
new GcpResolver(),
|
|
234
|
+
new RelativeFsResolver()
|
|
235
|
+
]);
|
|
236
|
+
}
|
|
237
|
+
function toBrowserAssetUrl(uri) {
|
|
238
|
+
if (!uri) return "";
|
|
239
|
+
if (uri.startsWith("http://") || uri.startsWith("https://") || uri.startsWith("data:")) return uri;
|
|
240
|
+
return `/api/v1/assets/content?uri=${encodeURIComponent(uri)}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
168
243
|
// src/cli/services/scene-resolver.ts
|
|
169
|
-
import path from "path";
|
|
170
244
|
var SceneResolver = class {
|
|
171
245
|
static async resolve(scene, projectRoot) {
|
|
246
|
+
const assetRegistry2 = createDefaultAssetResolverRegistry();
|
|
172
247
|
const resolvedScene = JSON.parse(JSON.stringify(scene));
|
|
173
248
|
const entities = resolvedScene.entities;
|
|
174
249
|
for (const entity of entities) {
|
|
@@ -176,9 +251,13 @@ var SceneResolver = class {
|
|
|
176
251
|
if (entity.type === "audio" || entity.type === "video") {
|
|
177
252
|
if (entity.src) {
|
|
178
253
|
try {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
254
|
+
const resolved = assetRegistry2.resolve(entity.src, projectRoot);
|
|
255
|
+
if (resolved.type === "local") {
|
|
256
|
+
const metadata = await FFprobeService.getMetadata(resolved.localPath);
|
|
257
|
+
entity.duration = metadata.duration;
|
|
258
|
+
} else {
|
|
259
|
+
entity.duration = 5;
|
|
260
|
+
}
|
|
182
261
|
} catch (e) {
|
|
183
262
|
console.warn(`\u26A0\uFE0F Could not resolve auto duration for ${entity.src}: ${e}`);
|
|
184
263
|
entity.duration = 5;
|
|
@@ -253,13 +332,13 @@ import { cors } from "hono/cors";
|
|
|
253
332
|
// src/cli/server/api.ts
|
|
254
333
|
import { Hono } from "hono";
|
|
255
334
|
import { readdir, readFile, writeFile, mkdir } from "node:fs/promises";
|
|
256
|
-
import { join, dirname } from "node:path";
|
|
335
|
+
import { join, dirname, relative } from "node:path";
|
|
257
336
|
import { existsSync } from "node:fs";
|
|
258
337
|
|
|
259
338
|
// src/cli/server/cli-runner.ts
|
|
260
339
|
import { spawn as spawn2 } from "child_process";
|
|
261
340
|
import path2 from "path";
|
|
262
|
-
import
|
|
341
|
+
import fs3 from "fs";
|
|
263
342
|
import { fileURLToPath } from "url";
|
|
264
343
|
var __filename = fileURLToPath(import.meta.url);
|
|
265
344
|
var __dirname = path2.dirname(__filename);
|
|
@@ -269,7 +348,7 @@ async function runCliCommand(args, cwd) {
|
|
|
269
348
|
let spawnArgs;
|
|
270
349
|
if (isCompiled) {
|
|
271
350
|
const binPath = path2.resolve(__dirname, "../../../bin/feedeas.js");
|
|
272
|
-
if (!
|
|
351
|
+
if (!fs3.existsSync(binPath)) {
|
|
273
352
|
throw new Error(`Could not find compiled CLI binary at ${binPath}`);
|
|
274
353
|
}
|
|
275
354
|
command = "node";
|
|
@@ -342,6 +421,60 @@ ${stderrData || stdoutData}`));
|
|
|
342
421
|
|
|
343
422
|
// src/cli/services/taste.ts
|
|
344
423
|
import path3 from "node:path";
|
|
424
|
+
var SAMPLE_SCENE_HEADER = "## Sample Scene (Starter)";
|
|
425
|
+
var DEFAULT_SAMPLE_SCENE_JSON = {
|
|
426
|
+
meta: {
|
|
427
|
+
width: 1080,
|
|
428
|
+
height: 1350,
|
|
429
|
+
duration: 5
|
|
430
|
+
},
|
|
431
|
+
entities: [
|
|
432
|
+
{
|
|
433
|
+
id: "text-1",
|
|
434
|
+
type: "text",
|
|
435
|
+
name: "Hook",
|
|
436
|
+
text: "Your Hook Goes Here",
|
|
437
|
+
startTime: 0,
|
|
438
|
+
duration: 5,
|
|
439
|
+
visible: true,
|
|
440
|
+
x: 540,
|
|
441
|
+
y: 520,
|
|
442
|
+
fontSize: 72,
|
|
443
|
+
fontFamily: "Inter, system-ui",
|
|
444
|
+
fontWeight: "bold",
|
|
445
|
+
color: "#ffffff",
|
|
446
|
+
bgColor: "transparent",
|
|
447
|
+
maxWidth: 880,
|
|
448
|
+
lineHeight: 1.2,
|
|
449
|
+
padding: 0,
|
|
450
|
+
textAlign: "center",
|
|
451
|
+
enter: { type: "scale", duration: 0.4 }
|
|
452
|
+
}
|
|
453
|
+
]
|
|
454
|
+
};
|
|
455
|
+
function isObject(value) {
|
|
456
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
457
|
+
}
|
|
458
|
+
function isValidSampleScene(value) {
|
|
459
|
+
if (!isObject(value)) return false;
|
|
460
|
+
if (!isObject(value.meta)) return false;
|
|
461
|
+
if (!Array.isArray(value.entities)) return false;
|
|
462
|
+
return value.entities.length > 0;
|
|
463
|
+
}
|
|
464
|
+
function formatSampleSceneSection(scene) {
|
|
465
|
+
const normalized = isValidSampleScene(scene) ? scene : DEFAULT_SAMPLE_SCENE_JSON;
|
|
466
|
+
const json = JSON.stringify(normalized, null, 2);
|
|
467
|
+
return `${SAMPLE_SCENE_HEADER}
|
|
468
|
+
Use this as a copy-ready baseline and adapt per concept.
|
|
469
|
+
|
|
470
|
+
\`\`\`json
|
|
471
|
+
${json}
|
|
472
|
+
\`\`\``;
|
|
473
|
+
}
|
|
474
|
+
var SAMPLE_SCENE_SECTION = formatSampleSceneSection(DEFAULT_SAMPLE_SCENE_JSON);
|
|
475
|
+
function escapeRegExp(value) {
|
|
476
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
477
|
+
}
|
|
345
478
|
var DEFAULT_TASTE_FILE_CONTENT = `# Taste File v1
|
|
346
479
|
## Brand Identity
|
|
347
480
|
Describe brand voice, tone, and core promise.
|
|
@@ -367,6 +500,8 @@ Describe visual and narrative constraints.
|
|
|
367
500
|
## Freshness Rules
|
|
368
501
|
State how each new idea should stay fresh.
|
|
369
502
|
|
|
503
|
+
${SAMPLE_SCENE_SECTION}
|
|
504
|
+
|
|
370
505
|
## Generation Instructions
|
|
371
506
|
Give concrete instructions for generating content ideas.
|
|
372
507
|
`;
|
|
@@ -425,6 +560,28 @@ function safeResolveFromCwd(relativePath) {
|
|
|
425
560
|
}
|
|
426
561
|
return fullPath;
|
|
427
562
|
}
|
|
563
|
+
function ensureSampleSceneSection(content, sampleScene) {
|
|
564
|
+
const current = (content || "").trim();
|
|
565
|
+
if (!current) return DEFAULT_TASTE_FILE_CONTENT;
|
|
566
|
+
const section = formatSampleSceneSection(sampleScene);
|
|
567
|
+
if (current.includes(SAMPLE_SCENE_HEADER)) {
|
|
568
|
+
const escapedHeader = escapeRegExp(SAMPLE_SCENE_HEADER);
|
|
569
|
+
return current.replace(
|
|
570
|
+
new RegExp(`${escapedHeader}[\\s\\S]*?(?=\\n##\\s|$)`),
|
|
571
|
+
section
|
|
572
|
+
).trimEnd() + "\n";
|
|
573
|
+
}
|
|
574
|
+
const generationHeader = "## Generation Instructions";
|
|
575
|
+
if (current.includes(generationHeader)) {
|
|
576
|
+
return current.replace(generationHeader, `${section}
|
|
577
|
+
|
|
578
|
+
${generationHeader}`).trimEnd() + "\n";
|
|
579
|
+
}
|
|
580
|
+
return `${current}
|
|
581
|
+
|
|
582
|
+
${section}
|
|
583
|
+
`;
|
|
584
|
+
}
|
|
428
585
|
function parseListValue(value) {
|
|
429
586
|
const trimmed = value.trim();
|
|
430
587
|
if (!trimmed) return [];
|
|
@@ -641,6 +798,115 @@ function extractJson(text) {
|
|
|
641
798
|
}
|
|
642
799
|
throw new Error("Could not parse JSON from model response");
|
|
643
800
|
}
|
|
801
|
+
function tryExtractJson(text) {
|
|
802
|
+
try {
|
|
803
|
+
return extractJson(text);
|
|
804
|
+
} catch {
|
|
805
|
+
return null;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function tryExtractLooseJson(text) {
|
|
809
|
+
const cleaned = (text || "").trim();
|
|
810
|
+
if (!cleaned) return null;
|
|
811
|
+
const withoutPrefix = cleaned.startsWith("json") ? cleaned.replace(/^json\s*/i, "").trim() : cleaned;
|
|
812
|
+
const candidate = tryExtractJson(withoutPrefix);
|
|
813
|
+
if (candidate) return candidate;
|
|
814
|
+
const firstBrace = withoutPrefix.indexOf("{");
|
|
815
|
+
const lastBrace = withoutPrefix.lastIndexOf("}");
|
|
816
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
817
|
+
const slice = withoutPrefix.slice(firstBrace, lastBrace + 1);
|
|
818
|
+
try {
|
|
819
|
+
return JSON.parse(slice);
|
|
820
|
+
} catch {
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
if (withoutPrefix.startsWith('"') && withoutPrefix.endsWith('"')) {
|
|
825
|
+
try {
|
|
826
|
+
const parsed = JSON.parse(withoutPrefix);
|
|
827
|
+
if (typeof parsed === "string") {
|
|
828
|
+
return tryExtractLooseJson(parsed);
|
|
829
|
+
}
|
|
830
|
+
return parsed;
|
|
831
|
+
} catch {
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
function extractReplyFromRaw(raw) {
|
|
838
|
+
const match = raw.match(/"reply"\s*:\s*([\s\S]*?)(?:,"proposedTasteContent"|,"sampleScene"|,"citationIds"|,"askUser"|}\s*$)/i);
|
|
839
|
+
if (!match?.[1]) return void 0;
|
|
840
|
+
let value = match[1].trim();
|
|
841
|
+
if (value.startsWith('"')) {
|
|
842
|
+
const endQuote = value.lastIndexOf('"');
|
|
843
|
+
if (endQuote > 0) {
|
|
844
|
+
value = value.slice(1, endQuote);
|
|
845
|
+
}
|
|
846
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"');
|
|
847
|
+
}
|
|
848
|
+
return value.trim() || void 0;
|
|
849
|
+
}
|
|
850
|
+
function extractProposedTasteContentFromRaw(raw) {
|
|
851
|
+
const fenced = raw.match(/```(?:md|markdown)?\s*([\s\S]*?)```/i);
|
|
852
|
+
if (fenced?.[1]?.includes("# Taste File")) {
|
|
853
|
+
return fenced[1].trim();
|
|
854
|
+
}
|
|
855
|
+
const jsonBlock = raw.match(/\{[\s\S]*\}/);
|
|
856
|
+
if (jsonBlock?.[0]) {
|
|
857
|
+
try {
|
|
858
|
+
const parsed = JSON.parse(jsonBlock[0]);
|
|
859
|
+
if (parsed && typeof parsed.proposedTasteContent === "string") {
|
|
860
|
+
return parsed.proposedTasteContent.trim();
|
|
861
|
+
}
|
|
862
|
+
} catch {
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const fieldMatch = raw.match(/"proposedTasteContent"\s*:\s*([\s\S]*?)(?:,"citationIds"|,"sampleScene"|,"askUser"|}\s*$)/i);
|
|
866
|
+
if (fieldMatch?.[1]) {
|
|
867
|
+
let value = fieldMatch[1].trim();
|
|
868
|
+
if (value.startsWith("```")) {
|
|
869
|
+
const fencedValue = value.match(/```(?:md|markdown|json)?\s*([\s\S]*?)```/i);
|
|
870
|
+
if (fencedValue?.[1]) {
|
|
871
|
+
return fencedValue[1].trim();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
if (value.startsWith('"')) {
|
|
875
|
+
const endQuote = value.lastIndexOf('"');
|
|
876
|
+
if (endQuote > 0) {
|
|
877
|
+
value = value.slice(1, endQuote);
|
|
878
|
+
}
|
|
879
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, " ").replace(/\\"/g, '"');
|
|
880
|
+
}
|
|
881
|
+
if (value.includes("# Taste File")) {
|
|
882
|
+
const markerIndex = value.indexOf("# Taste File");
|
|
883
|
+
return value.slice(markerIndex).trim();
|
|
884
|
+
}
|
|
885
|
+
return value.trim() || void 0;
|
|
886
|
+
}
|
|
887
|
+
const marker = "# Taste File";
|
|
888
|
+
const idx = raw.indexOf(marker);
|
|
889
|
+
if (idx === -1) return void 0;
|
|
890
|
+
const candidate = raw.slice(idx).trim();
|
|
891
|
+
return candidate.length > marker.length ? candidate : void 0;
|
|
892
|
+
}
|
|
893
|
+
function normalizeProposedTasteContent(content) {
|
|
894
|
+
const trimmed = (content || "").trim();
|
|
895
|
+
if (!trimmed) return "";
|
|
896
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
897
|
+
try {
|
|
898
|
+
const parsed = JSON.parse(trimmed);
|
|
899
|
+
if (typeof parsed === "string") {
|
|
900
|
+
return parsed.trim();
|
|
901
|
+
}
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (!trimmed.includes("\n") && trimmed.includes("\\n")) {
|
|
906
|
+
return trimmed.replace(/\\n/g, "\n").replace(/\\t/g, " ").replace(/\\"/g, '"').trim();
|
|
907
|
+
}
|
|
908
|
+
return trimmed;
|
|
909
|
+
}
|
|
644
910
|
function coerceString(value) {
|
|
645
911
|
if (value == null) return "";
|
|
646
912
|
if (typeof value === "string") return value.trim();
|
|
@@ -667,6 +933,140 @@ function coerceStringArray(value) {
|
|
|
667
933
|
}
|
|
668
934
|
return coerceString(value).split(/[,\n]/g).map((item) => item.trim()).filter(Boolean);
|
|
669
935
|
}
|
|
936
|
+
function normalizedId(prefix, idx) {
|
|
937
|
+
return `${prefix}_${Date.now()}_${idx + 1}`;
|
|
938
|
+
}
|
|
939
|
+
function normalizeBullets(value) {
|
|
940
|
+
const arr = coerceStringArray(value).map((item) => item.replace(/^[-*]\s*/, "").trim()).filter(Boolean);
|
|
941
|
+
if (arr.length >= 3) return arr.slice(0, 5);
|
|
942
|
+
const fallback = [...arr];
|
|
943
|
+
const defaults = [
|
|
944
|
+
"Opening hook scene and visual direction",
|
|
945
|
+
"Main beat that demonstrates the core value",
|
|
946
|
+
"Closing beat with CTA or payoff"
|
|
947
|
+
];
|
|
948
|
+
for (const item of defaults) {
|
|
949
|
+
if (fallback.length >= 3) break;
|
|
950
|
+
fallback.push(item);
|
|
951
|
+
}
|
|
952
|
+
return fallback.slice(0, 5);
|
|
953
|
+
}
|
|
954
|
+
function extractPlanCard(value, idx) {
|
|
955
|
+
const objectValue = typeof value === "object" && value !== null ? value : {};
|
|
956
|
+
const title = coerceString(
|
|
957
|
+
objectValue.title || objectValue.name || objectValue.idea || objectValue.heading || `Plan ${idx + 1}`
|
|
958
|
+
) || `Plan ${idx + 1}`;
|
|
959
|
+
const bullets = normalizeBullets(
|
|
960
|
+
objectValue.bullets || objectValue.plannedScenes || objectValue.scenes || objectValue.points || objectValue.items
|
|
961
|
+
).map((text, bulletIdx) => ({
|
|
962
|
+
id: `${normalizedId("bullet", idx)}_${bulletIdx + 1}`,
|
|
963
|
+
text
|
|
964
|
+
}));
|
|
965
|
+
return {
|
|
966
|
+
id: normalizedId("card", idx),
|
|
967
|
+
title,
|
|
968
|
+
bullets,
|
|
969
|
+
freshnessScore: void 0
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function parsePlanCardsFromModel(raw, count) {
|
|
973
|
+
let parsed = null;
|
|
974
|
+
try {
|
|
975
|
+
parsed = extractJson(raw);
|
|
976
|
+
} catch {
|
|
977
|
+
}
|
|
978
|
+
const source = parsed?.cards || parsed?.ideas || parsed?.plans || parsed;
|
|
979
|
+
const list = Array.isArray(source) ? source : [source].filter(Boolean);
|
|
980
|
+
const cards = list.map((item, idx) => extractPlanCard(item, idx)).filter((item) => item.title && item.bullets.length >= 3);
|
|
981
|
+
if (cards.length >= count) return cards.slice(0, count);
|
|
982
|
+
const padded = [...cards];
|
|
983
|
+
while (padded.length < count) {
|
|
984
|
+
const idx = padded.length;
|
|
985
|
+
padded.push(extractPlanCard({ title: `Plan ${idx + 1}`, bullets: [] }, idx));
|
|
986
|
+
}
|
|
987
|
+
return padded;
|
|
988
|
+
}
|
|
989
|
+
function parseFollowupPromptsFromModel(raw, bulletId, defaultCount = 4) {
|
|
990
|
+
let parsed = null;
|
|
991
|
+
try {
|
|
992
|
+
parsed = extractJson(raw);
|
|
993
|
+
} catch {
|
|
994
|
+
}
|
|
995
|
+
const source = parsed?.prompts || parsed?.images || parsed?.variants || parsed;
|
|
996
|
+
const values = Array.isArray(source) ? source : source ? [source] : [];
|
|
997
|
+
const prompts = values.map((value) => {
|
|
998
|
+
const item = typeof value === "object" && value !== null ? value : {};
|
|
999
|
+
return {
|
|
1000
|
+
prompt: coerceString(item.prompt || item.text || value),
|
|
1001
|
+
style: coerceString(item.style || item.look),
|
|
1002
|
+
shotType: coerceString(item.shotType || item.shot_type || item.shot),
|
|
1003
|
+
negativePrompt: coerceString(item.negativePrompt || item.negative_prompt || item.negative)
|
|
1004
|
+
};
|
|
1005
|
+
}).filter((item) => item.prompt);
|
|
1006
|
+
const normalized = prompts.length > 0 ? prompts.slice(0, 6) : [{
|
|
1007
|
+
prompt: coerceString(raw) || "Cinematic portrait scene matching the selected bullet with clear subject and composition.",
|
|
1008
|
+
style: "",
|
|
1009
|
+
shotType: "medium",
|
|
1010
|
+
negativePrompt: "blurry, overexposed, cluttered frame"
|
|
1011
|
+
}];
|
|
1012
|
+
while (normalized.length < Math.min(defaultCount, 6)) {
|
|
1013
|
+
normalized.push({
|
|
1014
|
+
prompt: `${normalized[0].prompt} (variant ${normalized.length + 1})`,
|
|
1015
|
+
style: normalized[0].style,
|
|
1016
|
+
shotType: normalized[0].shotType,
|
|
1017
|
+
negativePrompt: normalized[0].negativePrompt
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
return {
|
|
1021
|
+
bulletId,
|
|
1022
|
+
prompts: normalized.slice(0, 6)
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function normalizeAskUser(source, maxPrompts = 3) {
|
|
1026
|
+
if (!source) return void 0;
|
|
1027
|
+
const entries = Array.isArray(source) ? source : [source];
|
|
1028
|
+
const prompts = entries.map((entry, promptIndex) => {
|
|
1029
|
+
const record = typeof entry === "object" && entry !== null ? entry : {};
|
|
1030
|
+
const prompt = coerceString(record.prompt || record.question || record.title);
|
|
1031
|
+
if (!prompt) return null;
|
|
1032
|
+
const id = coerceString(record.id) || `ask_${Date.now()}_${promptIndex}`;
|
|
1033
|
+
const helper = coerceString(record.helper);
|
|
1034
|
+
const allowFreeText = Boolean(record.allowFreeText ?? record.allow_free_text);
|
|
1035
|
+
const maxSelectionsRaw = Number(record.maxSelections ?? record.max_selections);
|
|
1036
|
+
const maxSelections = Number.isFinite(maxSelectionsRaw) && maxSelectionsRaw > 0 ? maxSelectionsRaw : void 0;
|
|
1037
|
+
const optionSource = Array.isArray(record.options) ? record.options : Array.isArray(record.choices) ? record.choices : [];
|
|
1038
|
+
const options = optionSource.map((opt, optionIndex) => {
|
|
1039
|
+
const optRecord = typeof opt === "object" && opt !== null ? opt : {};
|
|
1040
|
+
const label = coerceString(optRecord.label || optRecord.title || optRecord.text || optRecord.value);
|
|
1041
|
+
const value = coerceString(optRecord.value || optRecord.label || optRecord.text || label);
|
|
1042
|
+
if (!label && !value) return null;
|
|
1043
|
+
const imagePrompt = coerceString(optRecord.imagePrompt || optRecord.image_prompt);
|
|
1044
|
+
const imageUrl = coerceString(optRecord.imageUrl || optRecord.image_url);
|
|
1045
|
+
const imageUri = coerceString(optRecord.imageUri || optRecord.image_uri);
|
|
1046
|
+
const type = coerceString(optRecord.type) === "image" || imagePrompt || imageUrl || imageUri ? "image" : "text";
|
|
1047
|
+
return {
|
|
1048
|
+
id: coerceString(optRecord.id) || `${id}_opt_${optionIndex}`,
|
|
1049
|
+
label: label || value,
|
|
1050
|
+
value: value || label,
|
|
1051
|
+
type,
|
|
1052
|
+
imagePrompt: imagePrompt || void 0,
|
|
1053
|
+
imageUrl: imageUrl || void 0,
|
|
1054
|
+
imageUri: imageUri || void 0
|
|
1055
|
+
};
|
|
1056
|
+
}).filter(Boolean);
|
|
1057
|
+
if (options.length === 0) return null;
|
|
1058
|
+
return {
|
|
1059
|
+
id,
|
|
1060
|
+
prompt,
|
|
1061
|
+
helper: helper || void 0,
|
|
1062
|
+
options: options.slice(0, 6),
|
|
1063
|
+
allowFreeText,
|
|
1064
|
+
maxSelections
|
|
1065
|
+
};
|
|
1066
|
+
}).filter(Boolean);
|
|
1067
|
+
if (prompts.length === 0) return void 0;
|
|
1068
|
+
return prompts.slice(0, Math.min(maxPrompts, 3));
|
|
1069
|
+
}
|
|
670
1070
|
async function callGeminiText(prompt, apiKey, model = "gemini-2.5-flash") {
|
|
671
1071
|
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent`;
|
|
672
1072
|
const response = await fetch(`${endpoint}?key=${apiKey}`, {
|
|
@@ -691,11 +1091,31 @@ async function callGeminiText(prompt, apiKey, model = "gemini-2.5-flash") {
|
|
|
691
1091
|
}
|
|
692
1092
|
async function runTasteChat(params) {
|
|
693
1093
|
const sampleMemories = params.memories.slice(0, 10).map((m) => `${m.id}: ${m.title}`).join("\n");
|
|
1094
|
+
const history = (params.chatHistory || []).slice(-8).map((entry) => `${entry.role === "user" ? "User" : "Assistant"}: ${entry.text}`).join("\n");
|
|
1095
|
+
const askUserCount = Number(params.askUserCount || 0);
|
|
1096
|
+
const askUserMax = Number(params.askUserMax || 3);
|
|
1097
|
+
const interactionCount = Number(params.interactionCount || 0);
|
|
1098
|
+
const interactionMin = Number(params.interactionMin || 3);
|
|
1099
|
+
const interactionMax = Number(params.interactionMax || 6);
|
|
694
1100
|
const prompt = [
|
|
695
1101
|
"You are a taste-file assistant for social content strategy.",
|
|
696
1102
|
"Given user message and taste file, respond with practical guidance.",
|
|
1103
|
+
"When offering choices, include AskUser prompts with clickable options; otherwise ask open questions in reply.",
|
|
1104
|
+
"Prefer text choices. Use image choices only when visual style or theme selection is best.",
|
|
1105
|
+
`You have used ${askUserCount}/${askUserMax} AskUser interactions. If count >= max, draft proposedTasteContent now.`,
|
|
1106
|
+
`You have used ${interactionCount}/${interactionMax} total interactions. Do not draft before ${interactionMin} unless user explicitly asks.`,
|
|
1107
|
+
"Do NOT draft proposedTasteContent before the limit unless the user explicitly asks for a rewrite or a draft.",
|
|
1108
|
+
"If you are NOT drafting, do NOT include proposedTasteContent and do NOT include raw JSON in reply.",
|
|
697
1109
|
"If rewrite is needed, return full updated taste markdown in proposedTasteContent.",
|
|
698
|
-
|
|
1110
|
+
"Taste files are reusable generators, NOT one-off idea drafts or simulations.",
|
|
1111
|
+
"When drafting proposedTasteContent, do NOT include specific content ideas, scripts, or single-topic writeups.",
|
|
1112
|
+
"Instead, define reusable guidance: brand voice, audience, focus areas, negatives, content formats, style constraints, freshness rules, and explicit generation instructions.",
|
|
1113
|
+
"Generation Instructions must be procedural and reusable: define step-by-step idea generation, required output format, and hook/delivery constraints.",
|
|
1114
|
+
"If rewrite is needed, also return sampleScene as a valid feedeas scene JSON object (with meta + entities).",
|
|
1115
|
+
'Return strict JSON: {"reply": "...", "proposedTasteContent": "... optional ...", "sampleScene": {... optional ...}, "citationIds": ["mem_id"], "askUser": [{"id":"...","prompt":"...","helper":"... optional","allowFreeText":true,"maxSelections":1,"options":[{"id":"...","label":"...","value":"...","type":"text|image","imagePrompt":"... optional"}]}]}',
|
|
1116
|
+
"",
|
|
1117
|
+
`Chat history:
|
|
1118
|
+
${history || "(none)"}`,
|
|
699
1119
|
"",
|
|
700
1120
|
`User message:
|
|
701
1121
|
${params.message}`,
|
|
@@ -707,14 +1127,41 @@ ${params.tasteContent}`,
|
|
|
707
1127
|
${sampleMemories || "(none)"}`
|
|
708
1128
|
].join("\n");
|
|
709
1129
|
const raw = await callGeminiText(prompt, params.apiKey, params.model);
|
|
710
|
-
const
|
|
1130
|
+
const userRequestedRewrite = /\b(rewrite|draft|update|apply|revise|refresh|create\s+the\s+taste|generate\s+the\s+taste)\b/i.test(params.message);
|
|
1131
|
+
const allowDraft = userRequestedRewrite || interactionCount >= interactionMax || askUserCount >= askUserMax && interactionCount >= interactionMin;
|
|
1132
|
+
const parsed = tryExtractJson(raw) ?? tryExtractLooseJson(raw);
|
|
1133
|
+
if (!parsed) {
|
|
1134
|
+
const recovered = allowDraft ? extractProposedTasteContentFromRaw(raw) : void 0;
|
|
1135
|
+
const normalized = recovered ? normalizeProposedTasteContent(recovered) : void 0;
|
|
1136
|
+
const extractedReply = extractReplyFromRaw(raw);
|
|
1137
|
+
const fallbackReply = extractedReply || coerceString(raw);
|
|
1138
|
+
const reply = normalized ? "Drafted a rewrite. Review and apply if it looks right." : fallbackReply.trim().startsWith("{") || fallbackReply.includes('"proposedTasteContent"') ? "Got it. To narrow this down, what specific angle or constraint should I prioritize next?" : fallbackReply;
|
|
1139
|
+
return {
|
|
1140
|
+
reply,
|
|
1141
|
+
proposedTasteContent: normalized ? ensureSampleSceneSection(normalized) : void 0,
|
|
1142
|
+
citations: []
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
711
1145
|
const citationIds = Array.isArray(parsed.citationIds) ? parsed.citationIds : [];
|
|
712
1146
|
const citationMap = new Map(params.memories.map((m) => [m.id, m.title]));
|
|
713
1147
|
const citations = citationIds.filter((id) => citationMap.has(id)).map((id) => ({ memoryId: id, title: citationMap.get(id) || id }));
|
|
1148
|
+
const sampleScene = parsed.sampleScene;
|
|
1149
|
+
const askUser = askUserCount >= askUserMax ? void 0 : normalizeAskUser(parsed.askUser || parsed.ask_user, askUserMax);
|
|
1150
|
+
const proposedTasteContent = allowDraft && parsed.proposedTasteContent ? ensureSampleSceneSection(normalizeProposedTasteContent(coerceString(parsed.proposedTasteContent)), sampleScene) : (() => {
|
|
1151
|
+
if (!allowDraft) return void 0;
|
|
1152
|
+
const recovered = extractProposedTasteContentFromRaw(raw);
|
|
1153
|
+
const normalized = recovered ? normalizeProposedTasteContent(recovered) : void 0;
|
|
1154
|
+
return normalized ? ensureSampleSceneSection(normalized, sampleScene) : void 0;
|
|
1155
|
+
})();
|
|
1156
|
+
const sanitizedProposed = proposedTasteContent?.trim().startsWith("{") && proposedTasteContent.includes('"reply"') ? (() => {
|
|
1157
|
+
const recovered = extractProposedTasteContentFromRaw(proposedTasteContent);
|
|
1158
|
+
return recovered ? normalizeProposedTasteContent(recovered) : proposedTasteContent;
|
|
1159
|
+
})() : proposedTasteContent;
|
|
714
1160
|
return {
|
|
715
1161
|
reply: coerceString(parsed.reply || raw),
|
|
716
|
-
proposedTasteContent:
|
|
717
|
-
citations
|
|
1162
|
+
proposedTasteContent: sanitizedProposed,
|
|
1163
|
+
citations,
|
|
1164
|
+
askUser
|
|
718
1165
|
};
|
|
719
1166
|
}
|
|
720
1167
|
async function generateIdea(tasteContent, memories, apiKey, model) {
|
|
@@ -732,16 +1179,133 @@ ${tasteContent}`,
|
|
|
732
1179
|
${recent || "(none)"}`
|
|
733
1180
|
].join("\n");
|
|
734
1181
|
const raw = await callGeminiText(prompt, apiKey, model);
|
|
735
|
-
const parsed =
|
|
1182
|
+
const parsed = tryExtractJson(raw) || {};
|
|
736
1183
|
return {
|
|
737
1184
|
title: coerceString(parsed.title || "Untitled idea"),
|
|
738
1185
|
format: coerceString(parsed.format || "daily-reel") || "daily-reel",
|
|
739
1186
|
tags: coerceStringArray(parsed.tags),
|
|
740
1187
|
freshnessTerms: coerceStringArray(parsed.freshnessTerms || parsed.freshness_terms),
|
|
741
|
-
summary: coerceString(parsed.summary),
|
|
742
|
-
content: coerceString(parsed.content)
|
|
1188
|
+
summary: coerceString(parsed.summary || raw.split("\n").slice(0, 2).join(" ").slice(0, 220)),
|
|
1189
|
+
content: coerceString(parsed.content || raw)
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
async function generatePlanCard(tasteContent, memories, apiKey, model) {
|
|
1193
|
+
const recent = memories.slice(0, 20).map((m) => `- ${m.title}: ${m.summary}`).join("\n");
|
|
1194
|
+
const prompt = [
|
|
1195
|
+
"Generate one simple simulation card from this taste file.",
|
|
1196
|
+
"Keep it coach-like and steerable.",
|
|
1197
|
+
"Return strict JSON with keys: title, bullets (array of 3 to 5 concise planned scenes).",
|
|
1198
|
+
"",
|
|
1199
|
+
`Taste file:
|
|
1200
|
+
${tasteContent}`,
|
|
1201
|
+
"",
|
|
1202
|
+
`Recent ideas:
|
|
1203
|
+
${recent || "(none)"}`
|
|
1204
|
+
].join("\n");
|
|
1205
|
+
const raw = await callGeminiText(prompt, apiKey, model);
|
|
1206
|
+
return parsePlanCardsFromModel(raw, 1)[0];
|
|
1207
|
+
}
|
|
1208
|
+
function computePlanCardOverlap(candidate, existing) {
|
|
1209
|
+
const candidateText = `${candidate.title} ${candidate.bullets.map((b) => b.text).join(" ")}`;
|
|
1210
|
+
const candidateTokens = tokenize(candidateText);
|
|
1211
|
+
let maxScore = 0;
|
|
1212
|
+
for (const prev of existing) {
|
|
1213
|
+
const prevText = `${prev.title} ${prev.bullets.map((b) => b.text).join(" ")}`;
|
|
1214
|
+
const prevTokens = tokenize(prevText);
|
|
1215
|
+
maxScore = Math.max(maxScore, jaccard(candidateTokens, prevTokens));
|
|
1216
|
+
}
|
|
1217
|
+
return maxScore;
|
|
1218
|
+
}
|
|
1219
|
+
async function simulatePlanCards(params) {
|
|
1220
|
+
const model = params.model || "gemini-2.5-flash";
|
|
1221
|
+
const existingCards = params.memories.slice(0, 40).map((memory, idx) => ({
|
|
1222
|
+
id: `mem_card_${idx + 1}`,
|
|
1223
|
+
title: memory.title,
|
|
1224
|
+
bullets: normalizeBullets(memory.summary || memory.content).map((text, bulletIdx) => ({
|
|
1225
|
+
id: `mem_bullet_${idx + 1}_${bulletIdx + 1}`,
|
|
1226
|
+
text
|
|
1227
|
+
}))
|
|
1228
|
+
}));
|
|
1229
|
+
const cards = [];
|
|
1230
|
+
const rejected = [];
|
|
1231
|
+
const targetCount = Math.max(1, params.count);
|
|
1232
|
+
const threshold = 0.6;
|
|
1233
|
+
const generateWithChecks = async () => {
|
|
1234
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1235
|
+
const candidate = await generatePlanCard(params.tasteContent, params.memories, params.apiKey, model);
|
|
1236
|
+
const overlap = computePlanCardOverlap(candidate, [...existingCards, ...cards]);
|
|
1237
|
+
if (overlap <= threshold) {
|
|
1238
|
+
candidate.freshnessScore = Number((1 - overlap).toFixed(3));
|
|
1239
|
+
return candidate;
|
|
1240
|
+
}
|
|
1241
|
+
rejected.push({
|
|
1242
|
+
title: candidate.title,
|
|
1243
|
+
reason: "overlap too high",
|
|
1244
|
+
overlapScore: Number(overlap.toFixed(3))
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
return null;
|
|
1248
|
+
};
|
|
1249
|
+
if (params.mode === "parallel") {
|
|
1250
|
+
const generated = await Promise.all(
|
|
1251
|
+
Array.from({ length: targetCount }, () => generatePlanCard(params.tasteContent, params.memories, params.apiKey, model))
|
|
1252
|
+
);
|
|
1253
|
+
for (const candidate of generated) {
|
|
1254
|
+
const overlap = computePlanCardOverlap(candidate, [...existingCards, ...cards]);
|
|
1255
|
+
if (overlap <= threshold) {
|
|
1256
|
+
candidate.freshnessScore = Number((1 - overlap).toFixed(3));
|
|
1257
|
+
cards.push(candidate);
|
|
1258
|
+
} else {
|
|
1259
|
+
rejected.push({
|
|
1260
|
+
title: candidate.title,
|
|
1261
|
+
reason: "parallel overlap filter",
|
|
1262
|
+
overlapScore: Number(overlap.toFixed(3))
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
while (cards.length < targetCount) {
|
|
1268
|
+
const card = await generateWithChecks();
|
|
1269
|
+
if (!card) break;
|
|
1270
|
+
cards.push(card);
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
ideas: [],
|
|
1274
|
+
planCards: cards.slice(0, targetCount),
|
|
1275
|
+
rejected,
|
|
1276
|
+
mode: params.mode,
|
|
1277
|
+
stats: {
|
|
1278
|
+
requested: targetCount,
|
|
1279
|
+
accepted: cards.length,
|
|
1280
|
+
rejected: rejected.length
|
|
1281
|
+
}
|
|
743
1282
|
};
|
|
744
1283
|
}
|
|
1284
|
+
async function generateBulletFollowupPrompts(params) {
|
|
1285
|
+
const model = params.model || "gemini-2.5-flash";
|
|
1286
|
+
const target = Math.max(3, Math.min(6, params.count || 4));
|
|
1287
|
+
const memoryHints = params.memories.slice(0, 10).map((m) => `- ${m.title}: ${m.summary}`).join("\n");
|
|
1288
|
+
const prompt = [
|
|
1289
|
+
"Generate image prompt variants for a selected planned scene bullet.",
|
|
1290
|
+
"Return strict JSON with key prompts (array of objects).",
|
|
1291
|
+
"Each object keys: prompt, style, shotType, negativePrompt.",
|
|
1292
|
+
`Return ${target} to 6 variants.`,
|
|
1293
|
+
"",
|
|
1294
|
+
`Taste file:
|
|
1295
|
+
${params.tasteContent}`,
|
|
1296
|
+
"",
|
|
1297
|
+
`Card title:
|
|
1298
|
+
${params.cardTitle}`,
|
|
1299
|
+
"",
|
|
1300
|
+
`Selected bullet:
|
|
1301
|
+
${params.bulletText}`,
|
|
1302
|
+
"",
|
|
1303
|
+
`Memory hints:
|
|
1304
|
+
${memoryHints || "(none)"}`
|
|
1305
|
+
].join("\n");
|
|
1306
|
+
const raw = await callGeminiText(prompt, params.apiKey, model);
|
|
1307
|
+
return parseFollowupPromptsFromModel(raw, params.bulletId, target);
|
|
1308
|
+
}
|
|
745
1309
|
async function simulateIdeas(params) {
|
|
746
1310
|
const model = params.model || "gemini-2.5-flash";
|
|
747
1311
|
const existing = [...params.memories];
|
|
@@ -801,7 +1365,7 @@ async function simulateIdeas(params) {
|
|
|
801
1365
|
}
|
|
802
1366
|
|
|
803
1367
|
// src/cli/services/taste-store.ts
|
|
804
|
-
import
|
|
1368
|
+
import fs4 from "node:fs";
|
|
805
1369
|
import path4 from "node:path";
|
|
806
1370
|
import { createHash } from "node:crypto";
|
|
807
1371
|
var DEFAULT_SUGGESTIONS_FILE_CONTENT = "# Taste Suggestions v1\n";
|
|
@@ -925,45 +1489,46 @@ var MarkdownTasteStore = class {
|
|
|
925
1489
|
}
|
|
926
1490
|
async ensureWorkspace() {
|
|
927
1491
|
const { tastePath, memoryPath, suggestionsPath } = this.resolveAll();
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
if (!
|
|
932
|
-
|
|
1492
|
+
fs4.mkdirSync(path4.dirname(tastePath), { recursive: true });
|
|
1493
|
+
fs4.mkdirSync(path4.dirname(memoryPath), { recursive: true });
|
|
1494
|
+
fs4.mkdirSync(path4.dirname(suggestionsPath), { recursive: true });
|
|
1495
|
+
if (!fs4.existsSync(tastePath)) {
|
|
1496
|
+
fs4.writeFileSync(tastePath, DEFAULT_TASTE_FILE_CONTENT, "utf-8");
|
|
933
1497
|
}
|
|
934
|
-
if (!
|
|
935
|
-
|
|
1498
|
+
if (!fs4.existsSync(memoryPath)) {
|
|
1499
|
+
fs4.writeFileSync(memoryPath, DEFAULT_MEMORY_FILE_CONTENT, "utf-8");
|
|
936
1500
|
}
|
|
937
|
-
if (!
|
|
938
|
-
|
|
1501
|
+
if (!fs4.existsSync(suggestionsPath)) {
|
|
1502
|
+
fs4.writeFileSync(suggestionsPath, DEFAULT_SUGGESTIONS_FILE_CONTENT, "utf-8");
|
|
939
1503
|
}
|
|
940
1504
|
}
|
|
941
1505
|
async readTaste() {
|
|
942
1506
|
await this.ensureWorkspace();
|
|
943
1507
|
const { tastePath } = this.resolveAll();
|
|
944
|
-
const content =
|
|
1508
|
+
const content = fs4.readFileSync(tastePath, "utf-8");
|
|
945
1509
|
return { content, version: hashContent(content) };
|
|
946
1510
|
}
|
|
947
1511
|
async writeTaste(content, expectedVersion) {
|
|
948
1512
|
await this.ensureWorkspace();
|
|
949
1513
|
const { tastePath } = this.resolveAll();
|
|
950
|
-
const current =
|
|
1514
|
+
const current = fs4.existsSync(tastePath) ? fs4.readFileSync(tastePath, "utf-8") : "";
|
|
951
1515
|
const currentVersion = hashContent(current);
|
|
952
1516
|
if (expectedVersion && expectedVersion !== currentVersion) {
|
|
953
1517
|
throw new Error("Taste file changed since proposal creation. Re-run suggest before apply.");
|
|
954
1518
|
}
|
|
955
|
-
|
|
956
|
-
|
|
1519
|
+
const normalized = ensureSampleSceneSection(content);
|
|
1520
|
+
fs4.writeFileSync(tastePath, normalized, "utf-8");
|
|
1521
|
+
return { version: hashContent(normalized) };
|
|
957
1522
|
}
|
|
958
1523
|
async appendMemory(entry) {
|
|
959
1524
|
await this.ensureWorkspace();
|
|
960
1525
|
const { memoryPath } = this.resolveAll();
|
|
961
|
-
const current =
|
|
1526
|
+
const current = fs4.existsSync(memoryPath) ? fs4.readFileSync(memoryPath, "utf-8") : "";
|
|
962
1527
|
const id = buildId("mem");
|
|
963
1528
|
const markdown = `${(current || DEFAULT_MEMORY_FILE_CONTENT).trimEnd()}
|
|
964
1529
|
|
|
965
1530
|
${formatMemoryEntry(entry, id)}`;
|
|
966
|
-
|
|
1531
|
+
fs4.writeFileSync(memoryPath, markdown, "utf-8");
|
|
967
1532
|
const parsed = parseTasteMemoryMarkdown(markdown).find((item) => item.id === id);
|
|
968
1533
|
if (!parsed) {
|
|
969
1534
|
throw new Error("Failed to append memory entry");
|
|
@@ -973,14 +1538,14 @@ ${formatMemoryEntry(entry, id)}`;
|
|
|
973
1538
|
async queryMemory(options) {
|
|
974
1539
|
await this.ensureWorkspace();
|
|
975
1540
|
const { memoryPath } = this.resolveAll();
|
|
976
|
-
const content =
|
|
1541
|
+
const content = fs4.existsSync(memoryPath) ? fs4.readFileSync(memoryPath, "utf-8") : "";
|
|
977
1542
|
const items = parseTasteMemoryMarkdown(content);
|
|
978
1543
|
return queryMemoryItems(items, options);
|
|
979
1544
|
}
|
|
980
1545
|
async appendSuggestion(suggestion) {
|
|
981
1546
|
await this.ensureWorkspace();
|
|
982
1547
|
const { suggestionsPath } = this.resolveAll();
|
|
983
|
-
const current =
|
|
1548
|
+
const current = fs4.existsSync(suggestionsPath) ? fs4.readFileSync(suggestionsPath, "utf-8") : "";
|
|
984
1549
|
const entry = {
|
|
985
1550
|
id: buildId("sug"),
|
|
986
1551
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -997,13 +1562,13 @@ ${formatMemoryEntry(entry, id)}`;
|
|
|
997
1562
|
};
|
|
998
1563
|
const existing = parseSuggestionMarkdown(current);
|
|
999
1564
|
const markdown = formatSuggestionsMarkdown([...existing, entry]);
|
|
1000
|
-
|
|
1565
|
+
fs4.writeFileSync(suggestionsPath, markdown, "utf-8");
|
|
1001
1566
|
return entry;
|
|
1002
1567
|
}
|
|
1003
1568
|
async listSuggestions(status) {
|
|
1004
1569
|
await this.ensureWorkspace();
|
|
1005
1570
|
const { suggestionsPath } = this.resolveAll();
|
|
1006
|
-
const current =
|
|
1571
|
+
const current = fs4.existsSync(suggestionsPath) ? fs4.readFileSync(suggestionsPath, "utf-8") : "";
|
|
1007
1572
|
const entries = parseSuggestionMarkdown(current).sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
1008
1573
|
if (!status) {
|
|
1009
1574
|
return entries;
|
|
@@ -1013,7 +1578,7 @@ ${formatMemoryEntry(entry, id)}`;
|
|
|
1013
1578
|
async updateSuggestionStatus(id, status, metadata) {
|
|
1014
1579
|
await this.ensureWorkspace();
|
|
1015
1580
|
const { suggestionsPath } = this.resolveAll();
|
|
1016
|
-
const current =
|
|
1581
|
+
const current = fs4.existsSync(suggestionsPath) ? fs4.readFileSync(suggestionsPath, "utf-8") : "";
|
|
1017
1582
|
const entries = parseSuggestionMarkdown(current);
|
|
1018
1583
|
const idx = entries.findIndex((entry) => entry.id === id);
|
|
1019
1584
|
if (idx === -1) {
|
|
@@ -1028,7 +1593,7 @@ ${formatMemoryEntry(entry, id)}`;
|
|
|
1028
1593
|
reviewNote: metadata?.reviewNote
|
|
1029
1594
|
};
|
|
1030
1595
|
entries[idx] = updated;
|
|
1031
|
-
|
|
1596
|
+
fs4.writeFileSync(suggestionsPath, formatSuggestionsMarkdown(entries), "utf-8");
|
|
1032
1597
|
return updated;
|
|
1033
1598
|
}
|
|
1034
1599
|
};
|
|
@@ -1081,9 +1646,216 @@ function createTasteStore(params) {
|
|
|
1081
1646
|
return new MarkdownTasteStore(paths);
|
|
1082
1647
|
}
|
|
1083
1648
|
|
|
1649
|
+
// src/cli/services/scenes/file-store.ts
|
|
1650
|
+
import fs5 from "node:fs";
|
|
1651
|
+
import path5 from "node:path";
|
|
1652
|
+
import { randomUUID } from "node:crypto";
|
|
1653
|
+
|
|
1654
|
+
// src/cli/services/scenes/types.ts
|
|
1655
|
+
var NotFoundError = class extends Error {
|
|
1656
|
+
};
|
|
1657
|
+
var ConflictError = class extends Error {
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
// src/cli/services/scenes/file-store.ts
|
|
1661
|
+
var DEFAULT_SCENE = {
|
|
1662
|
+
meta: {
|
|
1663
|
+
width: 1080,
|
|
1664
|
+
height: 1920,
|
|
1665
|
+
duration: 10
|
|
1666
|
+
},
|
|
1667
|
+
entities: []
|
|
1668
|
+
};
|
|
1669
|
+
function nowIso() {
|
|
1670
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1671
|
+
}
|
|
1672
|
+
function getStorePath(cwd) {
|
|
1673
|
+
return path5.resolve(cwd, ".feedeas/store.json");
|
|
1674
|
+
}
|
|
1675
|
+
function ensureStore(pathname) {
|
|
1676
|
+
const dir = path5.dirname(pathname);
|
|
1677
|
+
if (!fs5.existsSync(dir)) {
|
|
1678
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1679
|
+
}
|
|
1680
|
+
if (!fs5.existsSync(pathname)) {
|
|
1681
|
+
const initial = { projects: [], scenes: [] };
|
|
1682
|
+
fs5.writeFileSync(pathname, JSON.stringify(initial, null, 2), "utf-8");
|
|
1683
|
+
return initial;
|
|
1684
|
+
}
|
|
1685
|
+
const parsed = JSON.parse(fs5.readFileSync(pathname, "utf-8"));
|
|
1686
|
+
return {
|
|
1687
|
+
projects: parsed.projects || [],
|
|
1688
|
+
scenes: parsed.scenes || []
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
function saveStore(pathname, data) {
|
|
1692
|
+
fs5.writeFileSync(pathname, JSON.stringify(data, null, 2), "utf-8");
|
|
1693
|
+
}
|
|
1694
|
+
var FileProjectStore = class {
|
|
1695
|
+
constructor(cwd) {
|
|
1696
|
+
this.cwd = cwd;
|
|
1697
|
+
}
|
|
1698
|
+
load() {
|
|
1699
|
+
return ensureStore(getStorePath(this.cwd));
|
|
1700
|
+
}
|
|
1701
|
+
save(data) {
|
|
1702
|
+
saveStore(getStorePath(this.cwd), data);
|
|
1703
|
+
}
|
|
1704
|
+
async createProject(input) {
|
|
1705
|
+
const data = this.load();
|
|
1706
|
+
const now = nowIso();
|
|
1707
|
+
const project = {
|
|
1708
|
+
id: randomUUID(),
|
|
1709
|
+
name: input.name,
|
|
1710
|
+
defaultSceneId: null,
|
|
1711
|
+
rootPath: input.rootPath || this.cwd,
|
|
1712
|
+
ownerId: input.ownerId || null,
|
|
1713
|
+
createdAt: now,
|
|
1714
|
+
updatedAt: now
|
|
1715
|
+
};
|
|
1716
|
+
data.projects.push(project);
|
|
1717
|
+
this.save(data);
|
|
1718
|
+
return project;
|
|
1719
|
+
}
|
|
1720
|
+
async listProjects() {
|
|
1721
|
+
const data = this.load();
|
|
1722
|
+
return [...data.projects].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
1723
|
+
}
|
|
1724
|
+
async getProject(projectId) {
|
|
1725
|
+
const data = this.load();
|
|
1726
|
+
return data.projects.find((p) => p.id === projectId) || null;
|
|
1727
|
+
}
|
|
1728
|
+
async updateProject(projectId, updates) {
|
|
1729
|
+
const data = this.load();
|
|
1730
|
+
const idx = data.projects.findIndex((p) => p.id === projectId);
|
|
1731
|
+
if (idx === -1) {
|
|
1732
|
+
throw new NotFoundError(`Project not found: ${projectId}`);
|
|
1733
|
+
}
|
|
1734
|
+
const next = {
|
|
1735
|
+
...data.projects[idx],
|
|
1736
|
+
...updates,
|
|
1737
|
+
updatedAt: nowIso()
|
|
1738
|
+
};
|
|
1739
|
+
data.projects[idx] = next;
|
|
1740
|
+
this.save(data);
|
|
1741
|
+
return next;
|
|
1742
|
+
}
|
|
1743
|
+
};
|
|
1744
|
+
var FileSceneStore = class {
|
|
1745
|
+
constructor(cwd) {
|
|
1746
|
+
this.cwd = cwd;
|
|
1747
|
+
}
|
|
1748
|
+
load() {
|
|
1749
|
+
return ensureStore(getStorePath(this.cwd));
|
|
1750
|
+
}
|
|
1751
|
+
save(data) {
|
|
1752
|
+
saveStore(getStorePath(this.cwd), data);
|
|
1753
|
+
}
|
|
1754
|
+
ensureProject(data, projectId) {
|
|
1755
|
+
const found = data.projects.some((p) => p.id === projectId);
|
|
1756
|
+
if (!found) {
|
|
1757
|
+
throw new NotFoundError(`Project not found: ${projectId}`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
async listScenes(projectId) {
|
|
1761
|
+
const data = this.load();
|
|
1762
|
+
this.ensureProject(data, projectId);
|
|
1763
|
+
return data.scenes.filter((s) => s.projectId === projectId).sort((a, b) => a.orderIndex - b.orderIndex || a.createdAt.localeCompare(b.createdAt));
|
|
1764
|
+
}
|
|
1765
|
+
async getScene(projectId, sceneId) {
|
|
1766
|
+
const data = this.load();
|
|
1767
|
+
this.ensureProject(data, projectId);
|
|
1768
|
+
return data.scenes.find((s) => s.projectId === projectId && s.id === sceneId) || null;
|
|
1769
|
+
}
|
|
1770
|
+
async createScene(projectId, input) {
|
|
1771
|
+
const data = this.load();
|
|
1772
|
+
this.ensureProject(data, projectId);
|
|
1773
|
+
const projectScenes = data.scenes.filter((s) => s.projectId === projectId);
|
|
1774
|
+
const nextOrder = input.orderIndex ?? projectScenes.length;
|
|
1775
|
+
const now = nowIso();
|
|
1776
|
+
const record = {
|
|
1777
|
+
id: randomUUID(),
|
|
1778
|
+
projectId,
|
|
1779
|
+
name: input.name || `Scene ${projectScenes.length + 1}`,
|
|
1780
|
+
orderIndex: nextOrder,
|
|
1781
|
+
version: 1,
|
|
1782
|
+
meta: input.scene?.meta || DEFAULT_SCENE.meta,
|
|
1783
|
+
entities: input.scene?.entities || DEFAULT_SCENE.entities,
|
|
1784
|
+
createdAt: now,
|
|
1785
|
+
updatedAt: now
|
|
1786
|
+
};
|
|
1787
|
+
data.scenes.push(record);
|
|
1788
|
+
const projectIdx = data.projects.findIndex((p) => p.id === projectId);
|
|
1789
|
+
if (projectIdx >= 0 && !data.projects[projectIdx].defaultSceneId) {
|
|
1790
|
+
data.projects[projectIdx] = {
|
|
1791
|
+
...data.projects[projectIdx],
|
|
1792
|
+
defaultSceneId: record.id,
|
|
1793
|
+
updatedAt: now
|
|
1794
|
+
};
|
|
1795
|
+
}
|
|
1796
|
+
this.save(data);
|
|
1797
|
+
return record;
|
|
1798
|
+
}
|
|
1799
|
+
async updateScene(projectId, sceneId, updates, expectedVersion) {
|
|
1800
|
+
const data = this.load();
|
|
1801
|
+
this.ensureProject(data, projectId);
|
|
1802
|
+
const idx = data.scenes.findIndex((s) => s.projectId === projectId && s.id === sceneId);
|
|
1803
|
+
if (idx === -1) {
|
|
1804
|
+
throw new NotFoundError(`Scene not found: ${sceneId}`);
|
|
1805
|
+
}
|
|
1806
|
+
const current = data.scenes[idx];
|
|
1807
|
+
if (typeof expectedVersion === "number" && current.version !== expectedVersion) {
|
|
1808
|
+
throw new ConflictError(`Version conflict for scene ${sceneId}`);
|
|
1809
|
+
}
|
|
1810
|
+
const next = {
|
|
1811
|
+
...current,
|
|
1812
|
+
name: updates.name ?? current.name,
|
|
1813
|
+
meta: updates.meta ?? current.meta,
|
|
1814
|
+
entities: updates.entities ?? current.entities,
|
|
1815
|
+
version: current.version + 1,
|
|
1816
|
+
updatedAt: nowIso()
|
|
1817
|
+
};
|
|
1818
|
+
data.scenes[idx] = next;
|
|
1819
|
+
this.save(data);
|
|
1820
|
+
return next;
|
|
1821
|
+
}
|
|
1822
|
+
async deleteScene(projectId, sceneId) {
|
|
1823
|
+
const data = this.load();
|
|
1824
|
+
this.ensureProject(data, projectId);
|
|
1825
|
+
const before = data.scenes.length;
|
|
1826
|
+
data.scenes = data.scenes.filter((s) => !(s.projectId === projectId && s.id === sceneId));
|
|
1827
|
+
if (data.scenes.length === before) {
|
|
1828
|
+
throw new NotFoundError(`Scene not found: ${sceneId}`);
|
|
1829
|
+
}
|
|
1830
|
+
const projectIdx = data.projects.findIndex((p) => p.id === projectId);
|
|
1831
|
+
if (projectIdx >= 0 && data.projects[projectIdx].defaultSceneId === sceneId) {
|
|
1832
|
+
const replacement = data.scenes.find((s) => s.projectId === projectId)?.id || null;
|
|
1833
|
+
data.projects[projectIdx] = {
|
|
1834
|
+
...data.projects[projectIdx],
|
|
1835
|
+
defaultSceneId: replacement,
|
|
1836
|
+
updatedAt: nowIso()
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
this.save(data);
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1843
|
+
// src/cli/services/scenes/store-factory.ts
|
|
1844
|
+
function normalizeSceneStorageBackend(value) {
|
|
1845
|
+
return "file";
|
|
1846
|
+
}
|
|
1847
|
+
function createSceneStoreBundle(backendInput) {
|
|
1848
|
+
normalizeSceneStorageBackend(backendInput);
|
|
1849
|
+
const cwd = process.cwd();
|
|
1850
|
+
const projects = new FileProjectStore(cwd);
|
|
1851
|
+
const scenes = new FileSceneStore(cwd);
|
|
1852
|
+
return { projects, scenes };
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1084
1855
|
// src/cli/server/api.ts
|
|
1085
1856
|
var api = new Hono();
|
|
1086
1857
|
var resolveSafePath = (relativePath) => safeResolveFromCwd(relativePath);
|
|
1858
|
+
var assetRegistry = createDefaultAssetResolverRegistry();
|
|
1087
1859
|
function createStoreFromBody(body) {
|
|
1088
1860
|
return createTasteStore({
|
|
1089
1861
|
backend: body.storage ? String(body.storage) : void 0,
|
|
@@ -1092,7 +1864,16 @@ function createStoreFromBody(body) {
|
|
|
1092
1864
|
suggestionsFilePath: body.suggestionsFilePath ? String(body.suggestionsFilePath) : "taste/suggestions.md"
|
|
1093
1865
|
});
|
|
1094
1866
|
}
|
|
1095
|
-
|
|
1867
|
+
async function getProjectRoot(projectId) {
|
|
1868
|
+
if (!projectId) return process.cwd();
|
|
1869
|
+
const bundle = createSceneStoreBundle();
|
|
1870
|
+
const project = await bundle.projects.getProject(projectId);
|
|
1871
|
+
if (!project) {
|
|
1872
|
+
throw new NotFoundError(`Project not found: ${projectId}`);
|
|
1873
|
+
}
|
|
1874
|
+
return project.rootPath;
|
|
1875
|
+
}
|
|
1876
|
+
api.get("/v1/files/list", async (c) => {
|
|
1096
1877
|
const cwd = process.cwd();
|
|
1097
1878
|
try {
|
|
1098
1879
|
const files = await readdir(cwd, { withFileTypes: true });
|
|
@@ -1106,36 +1887,23 @@ api.get("/fs/list", async (c) => {
|
|
|
1106
1887
|
return c.json({ error: e.message }, 500);
|
|
1107
1888
|
}
|
|
1108
1889
|
});
|
|
1109
|
-
api.get("/
|
|
1110
|
-
const
|
|
1111
|
-
if (!
|
|
1890
|
+
api.get("/v1/files/read", async (c) => {
|
|
1891
|
+
const filePath = c.req.query("path");
|
|
1892
|
+
if (!filePath) return c.json({ error: "Path required" }, 400);
|
|
1112
1893
|
try {
|
|
1113
|
-
const fullPath = resolveSafePath(
|
|
1894
|
+
const fullPath = resolveSafePath(filePath);
|
|
1114
1895
|
const content = await readFile(fullPath, "utf-8");
|
|
1115
|
-
if (path19.endsWith(".json")) {
|
|
1116
|
-
try {
|
|
1117
|
-
const json = JSON.parse(content);
|
|
1118
|
-
if (json.meta && Array.isArray(json.entities)) {
|
|
1119
|
-
console.debug(`[API] Resolving scene: ${path19}`);
|
|
1120
|
-
const resolved = await SceneResolver.resolve(json, process.cwd());
|
|
1121
|
-
return c.json({ content: JSON.stringify(resolved, null, 2) });
|
|
1122
|
-
}
|
|
1123
|
-
} catch (e) {
|
|
1124
|
-
console.error(`[API] Scene resolution failed for ${path19}:`, e.message);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
1896
|
return c.json({ content });
|
|
1128
1897
|
} catch (e) {
|
|
1129
|
-
console.error(`[API] Error reading file ${path19}:`, e.message, e.stack);
|
|
1130
1898
|
return c.json({ error: e.message }, 500);
|
|
1131
1899
|
}
|
|
1132
1900
|
});
|
|
1133
|
-
api.post("/
|
|
1901
|
+
api.post("/v1/files/write", async (c) => {
|
|
1134
1902
|
const body = await c.req.json();
|
|
1135
|
-
const { path:
|
|
1136
|
-
if (!
|
|
1903
|
+
const { path: path22, content } = body;
|
|
1904
|
+
if (!path22 || content === void 0) return c.json({ error: "Path and content required" }, 400);
|
|
1137
1905
|
try {
|
|
1138
|
-
const fullPath = resolveSafePath(
|
|
1906
|
+
const fullPath = resolveSafePath(path22);
|
|
1139
1907
|
const dir = dirname(fullPath);
|
|
1140
1908
|
if (!existsSync(dir)) {
|
|
1141
1909
|
await mkdir(dir, { recursive: true });
|
|
@@ -1146,14 +1914,139 @@ api.post("/fs/write", async (c) => {
|
|
|
1146
1914
|
return c.json({ error: e.message }, 500);
|
|
1147
1915
|
}
|
|
1148
1916
|
});
|
|
1149
|
-
api.post("/
|
|
1917
|
+
api.post("/v1/projects", async (c) => {
|
|
1918
|
+
try {
|
|
1919
|
+
const body = await c.req.json();
|
|
1920
|
+
const bundle = createSceneStoreBundle(body.storage ? String(body.storage) : void 0);
|
|
1921
|
+
const project = await bundle.projects.createProject({
|
|
1922
|
+
name: body.name ? String(body.name) : "Untitled Project",
|
|
1923
|
+
rootPath: body.rootPath ? String(body.rootPath) : process.cwd(),
|
|
1924
|
+
ownerId: body.ownerId ? String(body.ownerId) : null
|
|
1925
|
+
});
|
|
1926
|
+
const defaultScene = await bundle.scenes.createScene(project.id, {
|
|
1927
|
+
name: body.defaultSceneName ? String(body.defaultSceneName) : "Scene 1"
|
|
1928
|
+
});
|
|
1929
|
+
const updated = await bundle.projects.updateProject(project.id, { defaultSceneId: defaultScene.id });
|
|
1930
|
+
return c.json({ project: updated, defaultScene }, 201);
|
|
1931
|
+
} catch (e) {
|
|
1932
|
+
return c.json({ error: e.message }, 500);
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
api.get("/v1/projects", async (c) => {
|
|
1936
|
+
try {
|
|
1937
|
+
const bundle = createSceneStoreBundle();
|
|
1938
|
+
const projects = await bundle.projects.listProjects();
|
|
1939
|
+
return c.json({ projects });
|
|
1940
|
+
} catch (e) {
|
|
1941
|
+
return c.json({ error: e.message }, 500);
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
api.get("/v1/projects/:projectId", async (c) => {
|
|
1945
|
+
try {
|
|
1946
|
+
const projectId = c.req.param("projectId");
|
|
1947
|
+
const bundle = createSceneStoreBundle();
|
|
1948
|
+
const project = await bundle.projects.getProject(projectId);
|
|
1949
|
+
if (!project) return c.json({ error: "Project not found" }, 404);
|
|
1950
|
+
return c.json({ project });
|
|
1951
|
+
} catch (e) {
|
|
1952
|
+
return c.json({ error: e.message }, 500);
|
|
1953
|
+
}
|
|
1954
|
+
});
|
|
1955
|
+
api.get("/v1/projects/:projectId/scenes", async (c) => {
|
|
1956
|
+
try {
|
|
1957
|
+
const projectId = c.req.param("projectId");
|
|
1958
|
+
const bundle = createSceneStoreBundle();
|
|
1959
|
+
const scenes = await bundle.scenes.listScenes(projectId);
|
|
1960
|
+
return c.json({ scenes });
|
|
1961
|
+
} catch (e) {
|
|
1962
|
+
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
1963
|
+
return c.json({ error: e.message }, 500);
|
|
1964
|
+
}
|
|
1965
|
+
});
|
|
1966
|
+
api.post("/v1/projects/:projectId/scenes", async (c) => {
|
|
1967
|
+
try {
|
|
1968
|
+
const projectId = c.req.param("projectId");
|
|
1969
|
+
const body = await c.req.json();
|
|
1970
|
+
const bundle = createSceneStoreBundle(body.storage ? String(body.storage) : void 0);
|
|
1971
|
+
const scene = await bundle.scenes.createScene(projectId, {
|
|
1972
|
+
name: body.name ? String(body.name) : void 0,
|
|
1973
|
+
orderIndex: body.orderIndex !== void 0 ? Number(body.orderIndex) : void 0,
|
|
1974
|
+
scene: body.scene
|
|
1975
|
+
});
|
|
1976
|
+
return c.json({ scene }, 201);
|
|
1977
|
+
} catch (e) {
|
|
1978
|
+
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
1979
|
+
return c.json({ error: e.message }, 500);
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
api.get("/v1/projects/:projectId/scenes/:sceneId", async (c) => {
|
|
1983
|
+
try {
|
|
1984
|
+
const { projectId, sceneId } = c.req.param();
|
|
1985
|
+
const bundle = createSceneStoreBundle();
|
|
1986
|
+
const scene = await bundle.scenes.getScene(projectId, sceneId);
|
|
1987
|
+
if (!scene) return c.json({ error: "Scene not found" }, 404);
|
|
1988
|
+
const resolved = await SceneResolver.resolve(
|
|
1989
|
+
{
|
|
1990
|
+
meta: scene.meta,
|
|
1991
|
+
entities: scene.entities
|
|
1992
|
+
},
|
|
1993
|
+
(await bundle.projects.getProject(projectId))?.rootPath || process.cwd()
|
|
1994
|
+
);
|
|
1995
|
+
return c.json({
|
|
1996
|
+
scene: {
|
|
1997
|
+
...scene,
|
|
1998
|
+
meta: resolved.meta,
|
|
1999
|
+
entities: resolved.entities
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
} catch (e) {
|
|
2003
|
+
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
2004
|
+
return c.json({ error: e.message }, 500);
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
api.put("/v1/projects/:projectId/scenes/:sceneId", async (c) => {
|
|
2008
|
+
try {
|
|
2009
|
+
const { projectId, sceneId } = c.req.param();
|
|
2010
|
+
const body = await c.req.json();
|
|
2011
|
+
const bundle = createSceneStoreBundle(body.storage ? String(body.storage) : void 0);
|
|
2012
|
+
const scene = await bundle.scenes.updateScene(
|
|
2013
|
+
projectId,
|
|
2014
|
+
sceneId,
|
|
2015
|
+
{
|
|
2016
|
+
name: body.name ? String(body.name) : void 0,
|
|
2017
|
+
meta: body.meta,
|
|
2018
|
+
entities: body.entities
|
|
2019
|
+
},
|
|
2020
|
+
body.version !== void 0 ? Number(body.version) : void 0
|
|
2021
|
+
);
|
|
2022
|
+
return c.json({ scene });
|
|
2023
|
+
} catch (e) {
|
|
2024
|
+
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
2025
|
+
if (e instanceof ConflictError || e?.name === "ConflictError") return c.json({ error: e.message }, 409);
|
|
2026
|
+
return c.json({ error: e.message }, 500);
|
|
2027
|
+
}
|
|
2028
|
+
});
|
|
2029
|
+
api.delete("/v1/projects/:projectId/scenes/:sceneId", async (c) => {
|
|
2030
|
+
try {
|
|
2031
|
+
const { projectId, sceneId } = c.req.param();
|
|
2032
|
+
const bundle = createSceneStoreBundle();
|
|
2033
|
+
await bundle.scenes.deleteScene(projectId, sceneId);
|
|
2034
|
+
return c.body(null, 204);
|
|
2035
|
+
} catch (e) {
|
|
2036
|
+
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
2037
|
+
return c.json({ error: e.message }, 500);
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
api.post("/v1/assets/upload", async (c) => {
|
|
1150
2041
|
try {
|
|
1151
2042
|
const body = await c.req.parseBody();
|
|
1152
2043
|
const file = body.file;
|
|
1153
2044
|
if (!file || !(file instanceof File)) {
|
|
1154
2045
|
return c.json({ error: "File required" }, 400);
|
|
1155
2046
|
}
|
|
1156
|
-
const
|
|
2047
|
+
const projectId = body.projectId ? String(body.projectId) : null;
|
|
2048
|
+
const rootPath = await getProjectRoot(projectId);
|
|
2049
|
+
const assetsDir = join(rootPath, "assets");
|
|
1157
2050
|
if (!existsSync(assetsDir)) {
|
|
1158
2051
|
await mkdir(assetsDir, { recursive: true });
|
|
1159
2052
|
}
|
|
@@ -1161,57 +2054,58 @@ api.post("/fs/upload", async (c) => {
|
|
|
1161
2054
|
const filePath = join(assetsDir, fileName);
|
|
1162
2055
|
const bytes = Buffer.from(await file.arrayBuffer());
|
|
1163
2056
|
await writeFile(filePath, bytes);
|
|
2057
|
+
const uri = `assets/${fileName}`;
|
|
1164
2058
|
return c.json({
|
|
1165
2059
|
success: true,
|
|
1166
|
-
|
|
1167
|
-
url: `/api/
|
|
2060
|
+
uri,
|
|
2061
|
+
url: `/api/v1/assets/content?uri=${encodeURIComponent(uri)}${projectId ? `&projectId=${encodeURIComponent(projectId)}` : ""}`
|
|
1168
2062
|
});
|
|
1169
2063
|
} catch (e) {
|
|
1170
|
-
console.error(e);
|
|
1171
2064
|
return c.json({ error: e.message }, 500);
|
|
1172
2065
|
}
|
|
1173
2066
|
});
|
|
1174
|
-
api.get("/
|
|
1175
|
-
const reqPath = c.req.path;
|
|
1176
|
-
const marker = "/fs/assets/";
|
|
1177
|
-
const index = reqPath.lastIndexOf(marker);
|
|
1178
|
-
const relativePath = reqPath.substring(index + marker.length);
|
|
1179
|
-
if (relativePath.includes("..")) {
|
|
1180
|
-
console.error(`[Asset Server] Blocked directory traversal attempt: ${relativePath}`);
|
|
1181
|
-
return c.json({ error: "Invalid path" }, 403);
|
|
1182
|
-
}
|
|
1183
|
-
let fullPath = "";
|
|
2067
|
+
api.get("/v1/assets/content", async (c) => {
|
|
1184
2068
|
try {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
return c.json({ error: "
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
}
|
|
1193
|
-
|
|
2069
|
+
const uri = c.req.query("uri");
|
|
2070
|
+
const projectId = c.req.query("projectId");
|
|
2071
|
+
if (!uri) return c.json({ error: "uri is required" }, 400);
|
|
2072
|
+
const rootPath = await getProjectRoot(projectId || null);
|
|
2073
|
+
const resolved = assetRegistry.resolve(uri, rootPath);
|
|
2074
|
+
if (resolved.type === "unsupported") {
|
|
2075
|
+
return c.json({ error: resolved.reason }, 400);
|
|
2076
|
+
}
|
|
2077
|
+
if (resolved.type === "remote") {
|
|
2078
|
+
const response = await fetch(resolved.remoteUrl);
|
|
2079
|
+
if (!response.ok) {
|
|
2080
|
+
return c.json({ error: `Failed to fetch remote asset: ${response.status}` }, 502);
|
|
2081
|
+
}
|
|
2082
|
+
const bytes = await response.arrayBuffer();
|
|
2083
|
+
const contentType = response.headers.get("content-type") || "application/octet-stream";
|
|
2084
|
+
return new Response(bytes, {
|
|
2085
|
+
headers: {
|
|
2086
|
+
"Content-Type": contentType,
|
|
2087
|
+
"Cache-Control": "public, max-age=300"
|
|
2088
|
+
}
|
|
2089
|
+
});
|
|
1194
2090
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
try {
|
|
1214
|
-
const file = await readFile(fullPath);
|
|
2091
|
+
if (!existsSync(resolved.localPath)) {
|
|
2092
|
+
return c.json({ error: `Asset not found at ${resolved.localPath}` }, 404);
|
|
2093
|
+
}
|
|
2094
|
+
const ext = resolved.localPath.split(".").pop()?.toLowerCase();
|
|
2095
|
+
const mimeTypes = {
|
|
2096
|
+
png: "image/png",
|
|
2097
|
+
jpg: "image/jpeg",
|
|
2098
|
+
jpeg: "image/jpeg",
|
|
2099
|
+
gif: "image/gif",
|
|
2100
|
+
webp: "image/webp",
|
|
2101
|
+
svg: "image/svg+xml",
|
|
2102
|
+
mp3: "audio/mpeg",
|
|
2103
|
+
mp4: "video/mp4",
|
|
2104
|
+
webm: "video/webm",
|
|
2105
|
+
wav: "audio/wav"
|
|
2106
|
+
};
|
|
2107
|
+
const mimeType = mimeTypes[ext || ""] || "application/octet-stream";
|
|
2108
|
+
const file = await readFile(resolved.localPath);
|
|
1215
2109
|
return new Response(file, {
|
|
1216
2110
|
headers: {
|
|
1217
2111
|
"Content-Type": mimeType,
|
|
@@ -1219,8 +2113,22 @@ api.get("/fs/assets/*", async (c) => {
|
|
|
1219
2113
|
}
|
|
1220
2114
|
});
|
|
1221
2115
|
} catch (e) {
|
|
1222
|
-
|
|
1223
|
-
return c.
|
|
2116
|
+
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
2117
|
+
return c.json({ error: e.message }, 500);
|
|
2118
|
+
}
|
|
2119
|
+
});
|
|
2120
|
+
api.post("/v1/assets/resolve", async (c) => {
|
|
2121
|
+
try {
|
|
2122
|
+
const body = await c.req.json();
|
|
2123
|
+
const uri = String(body.uri || "").trim();
|
|
2124
|
+
const projectId = body.projectId ? String(body.projectId) : null;
|
|
2125
|
+
if (!uri) return c.json({ error: "uri is required" }, 400);
|
|
2126
|
+
const rootPath = await getProjectRoot(projectId);
|
|
2127
|
+
const resolved = assetRegistry.resolve(uri, rootPath);
|
|
2128
|
+
return c.json({ resolved });
|
|
2129
|
+
} catch (e) {
|
|
2130
|
+
if (e instanceof NotFoundError) return c.json({ error: e.message }, 404);
|
|
2131
|
+
return c.json({ error: e.message }, 500);
|
|
1224
2132
|
}
|
|
1225
2133
|
});
|
|
1226
2134
|
api.post("/taste/chat", async (c) => {
|
|
@@ -1229,6 +2137,15 @@ api.post("/taste/chat", async (c) => {
|
|
|
1229
2137
|
const message = String(body.message || "").trim();
|
|
1230
2138
|
const model = body.model ? String(body.model) : void 0;
|
|
1231
2139
|
const apiKey = body.apiKey || process.env.GEMINI_API_KEY;
|
|
2140
|
+
const chatHistory = Array.isArray(body.chatHistory) ? body.chatHistory.map((entry) => ({
|
|
2141
|
+
role: entry.role === "assistant" ? "assistant" : "user",
|
|
2142
|
+
text: String(entry.text || "")
|
|
2143
|
+
})) : void 0;
|
|
2144
|
+
const askUserCount = body.askUserCount ? Number(body.askUserCount) : void 0;
|
|
2145
|
+
const askUserMax = body.askUserMax ? Number(body.askUserMax) : void 0;
|
|
2146
|
+
const interactionCount = body.interactionCount ? Number(body.interactionCount) : void 0;
|
|
2147
|
+
const interactionMin = body.interactionMin ? Number(body.interactionMin) : void 0;
|
|
2148
|
+
const interactionMax = body.interactionMax ? Number(body.interactionMax) : void 0;
|
|
1232
2149
|
if (!message) return c.json({ error: "message is required" }, 400);
|
|
1233
2150
|
if (!apiKey) return c.json({ error: "Missing GEMINI_API_KEY" }, 400);
|
|
1234
2151
|
const store = createStoreFromBody(body);
|
|
@@ -1240,13 +2157,47 @@ api.post("/taste/chat", async (c) => {
|
|
|
1240
2157
|
tasteContent: body.tasteContent ? String(body.tasteContent) : taste.content,
|
|
1241
2158
|
memories,
|
|
1242
2159
|
apiKey: String(apiKey),
|
|
1243
|
-
model
|
|
2160
|
+
model,
|
|
2161
|
+
chatHistory,
|
|
2162
|
+
askUserCount,
|
|
2163
|
+
askUserMax,
|
|
2164
|
+
interactionCount,
|
|
2165
|
+
interactionMin,
|
|
2166
|
+
interactionMax
|
|
1244
2167
|
});
|
|
1245
2168
|
return c.json(result);
|
|
1246
2169
|
} catch (e) {
|
|
1247
2170
|
return c.json({ error: e.message }, 500);
|
|
1248
2171
|
}
|
|
1249
2172
|
});
|
|
2173
|
+
api.post("/taste/askuser/imagine", async (c) => {
|
|
2174
|
+
try {
|
|
2175
|
+
const body = await c.req.json();
|
|
2176
|
+
const options = Array.isArray(body.options) ? body.options : [];
|
|
2177
|
+
if (options.length === 0) return c.json({ error: "options array is required" }, 400);
|
|
2178
|
+
const results = [];
|
|
2179
|
+
const now = Date.now();
|
|
2180
|
+
for (let i = 0; i < options.length; i++) {
|
|
2181
|
+
const entry = options[i] || {};
|
|
2182
|
+
const optionId = String(entry.optionId || "").trim();
|
|
2183
|
+
const prompt = String(entry.prompt || "").trim();
|
|
2184
|
+
const aspectRatio = String(entry.aspectRatio || "9:16");
|
|
2185
|
+
if (!optionId || !prompt) continue;
|
|
2186
|
+
const safeId = optionId.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
2187
|
+
const fileName = `askuser/${now}_${safeId}.png`;
|
|
2188
|
+
const args = ["imagine", prompt, "--aspect-ratio", aspectRatio, "-o", fileName, "--json"];
|
|
2189
|
+
const output = await runCliCommand(args, process.cwd());
|
|
2190
|
+
const files = Array.isArray(output.files) ? output.files : [];
|
|
2191
|
+
const first = files[0] ? String(files[0]) : "";
|
|
2192
|
+
if (!first) continue;
|
|
2193
|
+
const uri = relative(process.cwd(), first).replace(/\\/g, "/");
|
|
2194
|
+
results.push({ optionId, uri, url: toBrowserAssetUrl(uri) });
|
|
2195
|
+
}
|
|
2196
|
+
return c.json({ results });
|
|
2197
|
+
} catch (e) {
|
|
2198
|
+
return c.json({ error: e.message }, 500);
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
1250
2201
|
api.post("/taste/simulate", async (c) => {
|
|
1251
2202
|
try {
|
|
1252
2203
|
const body = await c.req.json();
|
|
@@ -1268,14 +2219,53 @@ api.post("/taste/simulate", async (c) => {
|
|
|
1268
2219
|
apiKey: String(apiKey),
|
|
1269
2220
|
model
|
|
1270
2221
|
});
|
|
1271
|
-
|
|
2222
|
+
const planResult = await simulatePlanCards({
|
|
2223
|
+
tasteContent: body.tasteContent ? String(body.tasteContent) : taste.content,
|
|
2224
|
+
memories,
|
|
2225
|
+
count,
|
|
2226
|
+
mode,
|
|
2227
|
+
apiKey: String(apiKey),
|
|
2228
|
+
model
|
|
2229
|
+
});
|
|
2230
|
+
const appendedIds = [];
|
|
1272
2231
|
if (saveToMemory && result.ideas.length > 0) {
|
|
1273
2232
|
for (const idea of result.ideas) {
|
|
1274
2233
|
const saved = await store.appendMemory({ ...idea, status: "generated" });
|
|
1275
2234
|
appendedIds.push(saved.id);
|
|
1276
2235
|
}
|
|
1277
2236
|
}
|
|
1278
|
-
return c.json({ ...result, appendedIds });
|
|
2237
|
+
return c.json({ ...result, planCards: planResult.planCards || [], appendedIds });
|
|
2238
|
+
} catch (e) {
|
|
2239
|
+
return c.json({ error: e.message }, 500);
|
|
2240
|
+
}
|
|
2241
|
+
});
|
|
2242
|
+
api.post("/taste/simulate/followup", async (c) => {
|
|
2243
|
+
try {
|
|
2244
|
+
const body = await c.req.json();
|
|
2245
|
+
const cardTitle = String(body.cardTitle || "").trim();
|
|
2246
|
+
const bulletText = String(body.bulletText || "").trim();
|
|
2247
|
+
const bulletId = String(body.bulletId || "").trim() || `bullet_${Date.now()}`;
|
|
2248
|
+
const model = body.model ? String(body.model) : void 0;
|
|
2249
|
+
const apiKey = body.apiKey || process.env.GEMINI_API_KEY;
|
|
2250
|
+
const count = Math.max(3, Math.min(6, Number(body.count || 4)));
|
|
2251
|
+
if (!cardTitle) return c.json({ error: "cardTitle is required" }, 400);
|
|
2252
|
+
if (!bulletText) return c.json({ error: "bulletText is required" }, 400);
|
|
2253
|
+
if (!apiKey) return c.json({ error: "Missing GEMINI_API_KEY" }, 400);
|
|
2254
|
+
const store = createStoreFromBody(body);
|
|
2255
|
+
await store.ensureWorkspace();
|
|
2256
|
+
const taste = await store.readTaste();
|
|
2257
|
+
const memories = body.memoryContent ? parseTasteMemoryMarkdown(String(body.memoryContent)).slice(0, 40) : await store.queryMemory({ limit: 40 });
|
|
2258
|
+
const result = await generateBulletFollowupPrompts({
|
|
2259
|
+
tasteContent: body.tasteContent ? String(body.tasteContent) : taste.content,
|
|
2260
|
+
memories,
|
|
2261
|
+
cardTitle,
|
|
2262
|
+
bulletText,
|
|
2263
|
+
bulletId,
|
|
2264
|
+
apiKey: String(apiKey),
|
|
2265
|
+
model,
|
|
2266
|
+
count
|
|
2267
|
+
});
|
|
2268
|
+
return c.json(result);
|
|
1279
2269
|
} catch (e) {
|
|
1280
2270
|
return c.json({ error: e.message }, 500);
|
|
1281
2271
|
}
|
|
@@ -1410,21 +2400,20 @@ api.post("/cli/run", async (c) => {
|
|
|
1410
2400
|
const result = await runCliCommand(args, cwd);
|
|
1411
2401
|
return c.json(result);
|
|
1412
2402
|
} catch (e) {
|
|
1413
|
-
console.error("[API] /cli/run error:", e.message);
|
|
1414
2403
|
return c.json({ error: e.message }, 500);
|
|
1415
2404
|
}
|
|
1416
2405
|
});
|
|
1417
2406
|
var api_default = api;
|
|
1418
2407
|
|
|
1419
2408
|
// src/cli/server/index.ts
|
|
1420
|
-
import
|
|
2409
|
+
import path6 from "path";
|
|
1421
2410
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1422
|
-
import
|
|
2411
|
+
import fs6 from "fs";
|
|
1423
2412
|
var app = new Hono2();
|
|
1424
2413
|
app.use("/*", cors());
|
|
1425
2414
|
app.route("/api", api_default);
|
|
1426
2415
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
1427
|
-
var __dirname2 =
|
|
2416
|
+
var __dirname2 = path6.dirname(__filename2);
|
|
1428
2417
|
var createServer = (staticRoot) => {
|
|
1429
2418
|
const app2 = new Hono2();
|
|
1430
2419
|
app2.use("/*", cors());
|
|
@@ -1455,10 +2444,10 @@ var createServer = (staticRoot) => {
|
|
|
1455
2444
|
};
|
|
1456
2445
|
app2.get("/*", async (c) => {
|
|
1457
2446
|
const requestPath = c.req.path === "/" ? "/index.html" : c.req.path;
|
|
1458
|
-
const filePath =
|
|
2447
|
+
const filePath = path6.join(staticRoot, requestPath);
|
|
1459
2448
|
try {
|
|
1460
|
-
if (
|
|
1461
|
-
const file = await
|
|
2449
|
+
if (fs6.existsSync(filePath)) {
|
|
2450
|
+
const file = await fs6.promises.readFile(filePath);
|
|
1462
2451
|
const mimeType = getMimeType(filePath);
|
|
1463
2452
|
return new Response(file, {
|
|
1464
2453
|
headers: {
|
|
@@ -1470,9 +2459,9 @@ var createServer = (staticRoot) => {
|
|
|
1470
2459
|
}
|
|
1471
2460
|
if (!requestPath.includes(".")) {
|
|
1472
2461
|
try {
|
|
1473
|
-
const indexPath =
|
|
1474
|
-
if (
|
|
1475
|
-
const indexFile = await
|
|
2462
|
+
const indexPath = path6.join(staticRoot, "index.html");
|
|
2463
|
+
if (fs6.existsSync(indexPath)) {
|
|
2464
|
+
const indexFile = await fs6.promises.readFile(indexPath);
|
|
1476
2465
|
return new Response(indexFile, {
|
|
1477
2466
|
headers: {
|
|
1478
2467
|
"Content-Type": "text/html"
|
|
@@ -1499,9 +2488,150 @@ function startServer(app2, port) {
|
|
|
1499
2488
|
};
|
|
1500
2489
|
}
|
|
1501
2490
|
|
|
2491
|
+
// src/cli/services/scenes/api-client.ts
|
|
2492
|
+
function normalizeApiBaseUrl(input) {
|
|
2493
|
+
const base = (input || process.env.FEEDEAS_API_URL || "http://localhost:3331").replace(/\/$/, "");
|
|
2494
|
+
if (base.endsWith("/api/v1")) return base;
|
|
2495
|
+
if (base.endsWith("/api")) return `${base}/v1`;
|
|
2496
|
+
return `${base}/api/v1`;
|
|
2497
|
+
}
|
|
2498
|
+
async function readJson(res) {
|
|
2499
|
+
const data = await res.json().catch(() => ({}));
|
|
2500
|
+
if (!res.ok) {
|
|
2501
|
+
throw new Error(data.error || `API request failed (${res.status})`);
|
|
2502
|
+
}
|
|
2503
|
+
return data;
|
|
2504
|
+
}
|
|
2505
|
+
var SceneApiClient = class {
|
|
2506
|
+
baseUrl;
|
|
2507
|
+
constructor(apiBaseUrl) {
|
|
2508
|
+
this.baseUrl = normalizeApiBaseUrl(apiBaseUrl);
|
|
2509
|
+
}
|
|
2510
|
+
async createProject(input) {
|
|
2511
|
+
const res = await fetch(`${this.baseUrl}/projects`, {
|
|
2512
|
+
method: "POST",
|
|
2513
|
+
headers: { "Content-Type": "application/json" },
|
|
2514
|
+
body: JSON.stringify(input)
|
|
2515
|
+
});
|
|
2516
|
+
return readJson(res);
|
|
2517
|
+
}
|
|
2518
|
+
async listProjects() {
|
|
2519
|
+
const res = await fetch(`${this.baseUrl}/projects`);
|
|
2520
|
+
const data = await readJson(res);
|
|
2521
|
+
return data.projects || [];
|
|
2522
|
+
}
|
|
2523
|
+
async getProject(projectId) {
|
|
2524
|
+
const res = await fetch(`${this.baseUrl}/projects/${encodeURIComponent(projectId)}`);
|
|
2525
|
+
if (res.status === 404) return null;
|
|
2526
|
+
const data = await readJson(res);
|
|
2527
|
+
return data.project || null;
|
|
2528
|
+
}
|
|
2529
|
+
async listScenes(projectId) {
|
|
2530
|
+
const res = await fetch(`${this.baseUrl}/projects/${encodeURIComponent(projectId)}/scenes`);
|
|
2531
|
+
const data = await readJson(res);
|
|
2532
|
+
return data.scenes || [];
|
|
2533
|
+
}
|
|
2534
|
+
async createScene(projectId, input) {
|
|
2535
|
+
const res = await fetch(`${this.baseUrl}/projects/${encodeURIComponent(projectId)}/scenes`, {
|
|
2536
|
+
method: "POST",
|
|
2537
|
+
headers: { "Content-Type": "application/json" },
|
|
2538
|
+
body: JSON.stringify(input)
|
|
2539
|
+
});
|
|
2540
|
+
const data = await readJson(res);
|
|
2541
|
+
return data.scene;
|
|
2542
|
+
}
|
|
2543
|
+
async getScene(projectId, sceneId) {
|
|
2544
|
+
const res = await fetch(`${this.baseUrl}/projects/${encodeURIComponent(projectId)}/scenes/${encodeURIComponent(sceneId)}`);
|
|
2545
|
+
if (res.status === 404) return null;
|
|
2546
|
+
const data = await readJson(res);
|
|
2547
|
+
return data.scene || null;
|
|
2548
|
+
}
|
|
2549
|
+
async updateScene(projectId, sceneId, input) {
|
|
2550
|
+
const res = await fetch(`${this.baseUrl}/projects/${encodeURIComponent(projectId)}/scenes/${encodeURIComponent(sceneId)}`, {
|
|
2551
|
+
method: "PUT",
|
|
2552
|
+
headers: { "Content-Type": "application/json" },
|
|
2553
|
+
body: JSON.stringify(input)
|
|
2554
|
+
});
|
|
2555
|
+
const data = await readJson(res);
|
|
2556
|
+
return data.scene;
|
|
2557
|
+
}
|
|
2558
|
+
async deleteScene(projectId, sceneId) {
|
|
2559
|
+
const res = await fetch(`${this.baseUrl}/projects/${encodeURIComponent(projectId)}/scenes/${encodeURIComponent(sceneId)}`, {
|
|
2560
|
+
method: "DELETE"
|
|
2561
|
+
});
|
|
2562
|
+
if (!res.ok && res.status !== 204) {
|
|
2563
|
+
const data = await res.json().catch(() => ({}));
|
|
2564
|
+
throw new Error(data.error || `Delete scene failed (${res.status})`);
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
async resolveAsset(uri, projectId) {
|
|
2568
|
+
const res = await fetch(`${this.baseUrl}/assets/resolve`, {
|
|
2569
|
+
method: "POST",
|
|
2570
|
+
headers: { "Content-Type": "application/json" },
|
|
2571
|
+
body: JSON.stringify({ uri, projectId })
|
|
2572
|
+
});
|
|
2573
|
+
const data = await readJson(res);
|
|
2574
|
+
return data.resolved;
|
|
2575
|
+
}
|
|
2576
|
+
assetContentUrl(uri, projectId) {
|
|
2577
|
+
const q = new URLSearchParams({ uri });
|
|
2578
|
+
if (projectId) q.set("projectId", projectId);
|
|
2579
|
+
return `${this.baseUrl}/assets/content?${q.toString()}`;
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
|
|
2583
|
+
// src/cli/services/scenes/flow-state.ts
|
|
2584
|
+
import fs7 from "node:fs";
|
|
2585
|
+
import path7 from "node:path";
|
|
2586
|
+
function sessionPath(cwd) {
|
|
2587
|
+
return path7.resolve(cwd, ".feedeas/session.json");
|
|
2588
|
+
}
|
|
2589
|
+
function ensureDir(filePath) {
|
|
2590
|
+
const dir = path7.dirname(filePath);
|
|
2591
|
+
if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
|
|
2592
|
+
}
|
|
2593
|
+
function readFlowState(cwd = process.cwd()) {
|
|
2594
|
+
const file = sessionPath(cwd);
|
|
2595
|
+
if (!fs7.existsSync(file)) return null;
|
|
2596
|
+
try {
|
|
2597
|
+
return JSON.parse(fs7.readFileSync(file, "utf-8"));
|
|
2598
|
+
} catch {
|
|
2599
|
+
return null;
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
function writeFlowState(updates, cwd = process.cwd()) {
|
|
2603
|
+
const existing = readFlowState(cwd) || { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
2604
|
+
const next = {
|
|
2605
|
+
...existing,
|
|
2606
|
+
...updates,
|
|
2607
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2608
|
+
};
|
|
2609
|
+
const file = sessionPath(cwd);
|
|
2610
|
+
ensureDir(file);
|
|
2611
|
+
fs7.writeFileSync(file, JSON.stringify(next, null, 2), "utf-8");
|
|
2612
|
+
return next;
|
|
2613
|
+
}
|
|
2614
|
+
function resolveProjectId(explicit) {
|
|
2615
|
+
if (explicit) return explicit;
|
|
2616
|
+
const state = readFlowState();
|
|
2617
|
+
if (state?.projectId) return state.projectId;
|
|
2618
|
+
throw new Error("Missing project ID. Pass --project-id or set flow state by running project create / scene use.");
|
|
2619
|
+
}
|
|
2620
|
+
function resolveSceneId(explicit) {
|
|
2621
|
+
if (explicit) return explicit;
|
|
2622
|
+
const state = readFlowState();
|
|
2623
|
+
if (state?.sceneId) return state.sceneId;
|
|
2624
|
+
throw new Error("Missing scene ID. Pass --scene-id or set flow state by running scene create/import/use.");
|
|
2625
|
+
}
|
|
2626
|
+
function resolveApiUrl(explicit) {
|
|
2627
|
+
if (explicit) return explicit;
|
|
2628
|
+
const state = readFlowState();
|
|
2629
|
+
return state?.apiUrl;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
1502
2632
|
// src/cli/commands/record.ts
|
|
1503
2633
|
var __filename3 = fileURLToPath3(import.meta.url);
|
|
1504
|
-
var __dirname3 =
|
|
2634
|
+
var __dirname3 = path8.dirname(__filename3);
|
|
1505
2635
|
function formatBytes(bytes) {
|
|
1506
2636
|
if (bytes === 0) return "0 B";
|
|
1507
2637
|
const k = 1024;
|
|
@@ -1510,21 +2640,21 @@ function formatBytes(bytes) {
|
|
|
1510
2640
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
1511
2641
|
}
|
|
1512
2642
|
function getMissingUiAssets(staticRoot) {
|
|
1513
|
-
const indexPath =
|
|
1514
|
-
if (!
|
|
2643
|
+
const indexPath = path8.join(staticRoot, "index.html");
|
|
2644
|
+
if (!fs8.existsSync(indexPath)) {
|
|
1515
2645
|
return {
|
|
1516
2646
|
missingFiles: [indexPath],
|
|
1517
2647
|
expectedUrls: []
|
|
1518
2648
|
};
|
|
1519
2649
|
}
|
|
1520
|
-
const indexHtml =
|
|
2650
|
+
const indexHtml = fs8.readFileSync(indexPath, "utf-8");
|
|
1521
2651
|
const assetMatches = Array.from(indexHtml.matchAll(/(?:src|href)=["'](\/assets\/[^"']+)["']/g));
|
|
1522
2652
|
const assetPaths = [...new Set(assetMatches.map((match) => match[1]))];
|
|
1523
2653
|
const missingFiles = [];
|
|
1524
2654
|
const expectedUrls = [];
|
|
1525
2655
|
for (const assetPath of assetPaths) {
|
|
1526
|
-
const localPath =
|
|
1527
|
-
if (!
|
|
2656
|
+
const localPath = path8.join(staticRoot, assetPath.replace(/^\//, ""));
|
|
2657
|
+
if (!fs8.existsSync(localPath)) {
|
|
1528
2658
|
missingFiles.push(localPath);
|
|
1529
2659
|
expectedUrls.push(assetPath);
|
|
1530
2660
|
}
|
|
@@ -1612,21 +2742,28 @@ async function ensurePlaywrightReadyOrExit() {
|
|
|
1612
2742
|
process.exit(1);
|
|
1613
2743
|
}
|
|
1614
2744
|
}
|
|
1615
|
-
var recordCommand = new Command("record").description("Record the project to a video file with audio").option("-o, --output <path>", "Output file path", "output.mp4").option("-u, --url <url>", "URL of the running server", "http://localhost:3331").option("-w, --width <number>", "Viewport width", "1920").option("-h, --height <number>", "Viewport height", "1080").option("-f, --fps <number>", "Frames per second", "30").option("--project <
|
|
1616
|
-
const { output, url, width, height, fps,
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
2745
|
+
var recordCommand = new Command("record").description("Record the project to a video file with audio").option("-o, --output <path>", "Output file path", "output.mp4").option("-u, --url <url>", "URL of the running server", "http://localhost:3331").option("-w, --width <number>", "Viewport width", "1920").option("-h, --height <number>", "Viewport height", "1080").option("-f, --fps <number>", "Frames per second", "30").option("--project-id <id>", "Project ID").option("--scene-id <id>", "Scene ID").option("--api-url <url>", "API base URL for scene/project metadata").option("--dry-run", "Validate scene and show estimates without rendering").option("--debug", "Enable verbose logging (FFmpeg, browser console)").action(async (options) => {
|
|
2746
|
+
const { output, url, width, height, fps, debug, dryRun } = options;
|
|
2747
|
+
const projectId = resolveProjectId(options.projectId);
|
|
2748
|
+
const sceneId = resolveSceneId(options.sceneId);
|
|
2749
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
2750
|
+
const apiClient = apiUrl ? new SceneApiClient(apiUrl) : null;
|
|
2751
|
+
const localBundle = createSceneStoreBundle("file");
|
|
2752
|
+
const assetRegistry2 = createDefaultAssetResolverRegistry();
|
|
2753
|
+
const projectRecord = apiClient ? await apiClient.getProject(projectId) : await localBundle.projects.getProject(projectId);
|
|
2754
|
+
if (!projectRecord) {
|
|
2755
|
+
console.error(`Project not found: ${projectId}`);
|
|
1620
2756
|
process.exit(1);
|
|
1621
2757
|
}
|
|
1622
|
-
const projectDir = path6.dirname(projectPath);
|
|
1623
|
-
process.chdir(projectDir);
|
|
1624
|
-
console.log(`Working in project directory: ${projectDir}`);
|
|
1625
2758
|
let project;
|
|
1626
2759
|
try {
|
|
1627
|
-
const
|
|
1628
|
-
|
|
1629
|
-
|
|
2760
|
+
const sceneRecord = apiClient ? await apiClient.getScene(projectId, sceneId) : await localBundle.scenes.getScene(projectId, sceneId);
|
|
2761
|
+
if (!sceneRecord) {
|
|
2762
|
+
console.error(`Scene not found: ${sceneId}`);
|
|
2763
|
+
process.exit(1);
|
|
2764
|
+
}
|
|
2765
|
+
const rawProject = { meta: sceneRecord.meta, entities: sceneRecord.entities };
|
|
2766
|
+
project = await SceneResolver.resolve(rawProject, projectRecord.rootPath || process.cwd());
|
|
1630
2767
|
} catch (e) {
|
|
1631
2768
|
console.error(`Error loading project: ${e.message}`);
|
|
1632
2769
|
process.exit(1);
|
|
@@ -1639,18 +2776,24 @@ var recordCommand = new Command("record").description("Record the project to a v
|
|
|
1639
2776
|
const imageEntities = state.entities.filter((e) => e.type === "image" && e.src);
|
|
1640
2777
|
const allAssets = [...audioEntities, ...imageEntities];
|
|
1641
2778
|
const missingAssets = [];
|
|
1642
|
-
|
|
1643
|
-
if (!entity.src)
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
2779
|
+
for (const entity of allAssets) {
|
|
2780
|
+
if (!entity.src) continue;
|
|
2781
|
+
if (apiClient) {
|
|
2782
|
+
try {
|
|
2783
|
+
const resolved = await apiClient.resolveAsset(entity.src, projectId);
|
|
2784
|
+
if (resolved.type === "unsupported") {
|
|
2785
|
+
missingAssets.push(entity.src);
|
|
2786
|
+
}
|
|
2787
|
+
} catch {
|
|
2788
|
+
missingAssets.push(entity.src);
|
|
2789
|
+
}
|
|
1647
2790
|
} else {
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
2791
|
+
const validity = assetRegistry2.assertUsable(entity.src, projectRecord.rootPath || process.cwd());
|
|
2792
|
+
if (!validity.ok) {
|
|
2793
|
+
missingAssets.push(entity.src);
|
|
2794
|
+
}
|
|
1652
2795
|
}
|
|
1653
|
-
}
|
|
2796
|
+
}
|
|
1654
2797
|
if (missingAssets.length > 0) {
|
|
1655
2798
|
console.error("\nMissing assets:");
|
|
1656
2799
|
missingAssets.forEach((asset) => console.error(` \u2717 ${asset}`));
|
|
@@ -1658,7 +2801,7 @@ var recordCommand = new Command("record").description("Record the project to a v
|
|
|
1658
2801
|
}
|
|
1659
2802
|
if (dryRun) {
|
|
1660
2803
|
console.log("\n=== Validation Results ===");
|
|
1661
|
-
console.log(`\u2713 Scene: ${
|
|
2804
|
+
console.log(`\u2713 Scene: ${sceneId}`);
|
|
1662
2805
|
console.log(`\u2713 Duration: ${duration}s @${state.meta.width}\xD7${state.meta.height}`);
|
|
1663
2806
|
const counts = {};
|
|
1664
2807
|
state.entities.forEach((e) => {
|
|
@@ -1671,12 +2814,21 @@ var recordCommand = new Command("record").description("Record the project to a v
|
|
|
1671
2814
|
Assets(${allAssets.length}):`);
|
|
1672
2815
|
allAssets.forEach((entity) => {
|
|
1673
2816
|
if (!entity.src) return;
|
|
1674
|
-
|
|
1675
|
-
if (
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
2817
|
+
const src = entity.src;
|
|
2818
|
+
if (apiClient) {
|
|
2819
|
+
const looksRemote = src.startsWith("http://") || src.startsWith("https://") || src.startsWith("data:");
|
|
2820
|
+
console.log(` \u2713 ${src} (${looksRemote ? "remote" : "resolved via API"})`);
|
|
2821
|
+
} else {
|
|
2822
|
+
const resolved = assetRegistry2.resolve(src, projectRecord.rootPath || process.cwd());
|
|
2823
|
+
if (resolved.type === "local" && fs8.existsSync(resolved.localPath)) {
|
|
2824
|
+
const stats = fs8.statSync(resolved.localPath);
|
|
2825
|
+
const sizeStr = formatBytes(stats.size);
|
|
2826
|
+
console.log(` \u2713 ${src} (${sizeStr})`);
|
|
2827
|
+
} else if (resolved.type === "remote") {
|
|
2828
|
+
console.log(` \u2713 ${src} (remote)`);
|
|
2829
|
+
} else {
|
|
2830
|
+
console.log(` \u2713 ${src}`);
|
|
2831
|
+
}
|
|
1680
2832
|
}
|
|
1681
2833
|
});
|
|
1682
2834
|
}
|
|
@@ -1702,17 +2854,18 @@ Warnings:`);
|
|
|
1702
2854
|
Estimate: ${totalFrames} frames, ~${estimatedMinutes}m ${remainingSeconds}s @${estimatedFps} fps`);
|
|
1703
2855
|
console.log(`
|
|
1704
2856
|
Ready to record. Run without --dry-run to start rendering.`);
|
|
2857
|
+
writeFlowState({ projectId, sceneId, apiUrl });
|
|
1705
2858
|
process.exit(0);
|
|
1706
2859
|
}
|
|
1707
2860
|
console.log("Running runtime preflight checks...");
|
|
1708
2861
|
ensureFfmpegAvailableOrExit();
|
|
1709
2862
|
await ensurePlaywrightReadyOrExit();
|
|
1710
2863
|
console.log("Starting temporary server...");
|
|
1711
|
-
let staticRoot =
|
|
1712
|
-
if (!
|
|
1713
|
-
staticRoot =
|
|
2864
|
+
let staticRoot = path8.resolve(__dirname3, "../../dist/ui");
|
|
2865
|
+
if (!fs8.existsSync(staticRoot)) {
|
|
2866
|
+
staticRoot = path8.resolve(__dirname3, "../ui");
|
|
1714
2867
|
}
|
|
1715
|
-
if (!
|
|
2868
|
+
if (!fs8.existsSync(staticRoot)) {
|
|
1716
2869
|
console.error(`Error: UI assets not found at ${staticRoot}. Please run 'bun run build' first.`);
|
|
1717
2870
|
process.exit(1);
|
|
1718
2871
|
}
|
|
@@ -1761,27 +2914,31 @@ Ready to record. Run without --dry-run to start rendering.`);
|
|
|
1761
2914
|
const widthNum = parseInt(width);
|
|
1762
2915
|
const heightNum = parseInt(height);
|
|
1763
2916
|
console.log("Starting recording...");
|
|
1764
|
-
console.log(`Project: ${
|
|
2917
|
+
console.log(`Project: ${projectId}`);
|
|
2918
|
+
console.log(`Scene: ${sceneId}`);
|
|
1765
2919
|
console.log(`Duration: ${duration}s, Frames: ${totalFrames}`);
|
|
1766
2920
|
const validAudio = audioEntities.filter((e) => {
|
|
1767
2921
|
if (!e.src) return false;
|
|
1768
|
-
|
|
1769
|
-
if (
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
} else {
|
|
1773
|
-
assetPath = e.src;
|
|
1774
|
-
}
|
|
1775
|
-
} else {
|
|
1776
|
-
assetPath = path6.resolve(process.cwd(), e.src);
|
|
2922
|
+
const src = e.src;
|
|
2923
|
+
if (src.startsWith("http://") || src.startsWith("https://") || src.startsWith("data:")) {
|
|
2924
|
+
e.src = src;
|
|
2925
|
+
return true;
|
|
1777
2926
|
}
|
|
1778
|
-
if (
|
|
1779
|
-
e.src =
|
|
2927
|
+
if (apiClient) {
|
|
2928
|
+
e.src = apiClient.assetContentUrl(src, projectId);
|
|
2929
|
+
return true;
|
|
2930
|
+
}
|
|
2931
|
+
const resolved = assetRegistry2.resolve(src, projectRecord.rootPath || process.cwd());
|
|
2932
|
+
if (resolved.type === "local" && fs8.existsSync(resolved.localPath)) {
|
|
2933
|
+
e.src = resolved.localPath;
|
|
1780
2934
|
return true;
|
|
1781
|
-
} else {
|
|
1782
|
-
console.warn(`Asset not found: ${assetPath} (orig: ${e.src})`);
|
|
1783
|
-
return false;
|
|
1784
2935
|
}
|
|
2936
|
+
if (resolved.type === "remote") {
|
|
2937
|
+
e.src = resolved.remoteUrl;
|
|
2938
|
+
return true;
|
|
2939
|
+
}
|
|
2940
|
+
console.warn(`Asset not found or unsupported: ${e.src}`);
|
|
2941
|
+
return false;
|
|
1785
2942
|
});
|
|
1786
2943
|
const ffmpegArgs = [
|
|
1787
2944
|
"-y",
|
|
@@ -1885,8 +3042,7 @@ Ready to record. Run without --dry-run to start rendering.`);
|
|
|
1885
3042
|
try {
|
|
1886
3043
|
for (let i = 0; i < totalFrames; i++) {
|
|
1887
3044
|
const time = i / fpsNum;
|
|
1888
|
-
const
|
|
1889
|
-
const targetUrl = `${url}?mode=render&time=${time}&project=${encodeURIComponent(projectRelativePath)}`;
|
|
3045
|
+
const targetUrl = `${url}?mode=render&time=${time}&projectId=${encodeURIComponent(projectId)}&sceneId=${encodeURIComponent(sceneId)}`;
|
|
1890
3046
|
await page.goto(targetUrl, { waitUntil: "load" });
|
|
1891
3047
|
await page.waitForSelector('canvas[data-ready="true"]', { timeout: 3e4 });
|
|
1892
3048
|
const buffer = await page.locator("canvas").screenshot({ type: "png" });
|
|
@@ -1919,16 +3075,17 @@ Ready to record. Run without --dry-run to start rendering.`);
|
|
|
1919
3075
|
console.log("Stopping temporary server...");
|
|
1920
3076
|
server.stop();
|
|
1921
3077
|
}
|
|
3078
|
+
writeFlowState({ projectId, sceneId, apiUrl });
|
|
1922
3079
|
});
|
|
1923
3080
|
|
|
1924
3081
|
// src/cli/commands/inspect.ts
|
|
1925
3082
|
import { Command as Command2 } from "commander";
|
|
1926
|
-
import
|
|
1927
|
-
import
|
|
3083
|
+
import fs9 from "fs";
|
|
3084
|
+
import path9 from "path";
|
|
1928
3085
|
var inspectCommand = new Command2("inspect");
|
|
1929
3086
|
inspectCommand.description("Inspect an asset file to get metadata (duration, format, etc.)").argument("<file>", "Path to the asset file").option("--json", "Output in JSON format").action(async (file, options) => {
|
|
1930
|
-
const filePath =
|
|
1931
|
-
if (!
|
|
3087
|
+
const filePath = path9.resolve(process.cwd(), file);
|
|
3088
|
+
if (!fs9.existsSync(filePath)) {
|
|
1932
3089
|
console.error(`File not found: ${filePath}`);
|
|
1933
3090
|
process.exit(1);
|
|
1934
3091
|
}
|
|
@@ -1957,23 +3114,33 @@ inspectCommand.description("Inspect an asset file to get metadata (duration, for
|
|
|
1957
3114
|
|
|
1958
3115
|
// src/cli/commands/validate.ts
|
|
1959
3116
|
import { Command as Command3 } from "commander";
|
|
1960
|
-
import
|
|
1961
|
-
import path8 from "path";
|
|
3117
|
+
import fs10 from "fs";
|
|
1962
3118
|
var validateCommand = new Command3("validate");
|
|
1963
3119
|
function resolveRuntimeFit(fit, imageRatio, canvasRatio) {
|
|
1964
3120
|
if (fit === "cover" || fit === "contain") return fit;
|
|
1965
3121
|
const ratioDelta = Math.abs(imageRatio / canvasRatio - 1);
|
|
1966
3122
|
return ratioDelta > 0.2 ? "contain" : "cover";
|
|
1967
3123
|
}
|
|
1968
|
-
validateCommand.description("Validate a scene
|
|
1969
|
-
const filePath = path8.resolve(process.cwd(), file);
|
|
1970
|
-
if (!fs7.existsSync(filePath)) {
|
|
1971
|
-
console.error(`File not found: ${filePath}`);
|
|
1972
|
-
process.exit(1);
|
|
1973
|
-
}
|
|
3124
|
+
validateCommand.description("Validate a scene by project and scene ID").option("--project-id <id>", "Project ID").option("--scene-id <id>", "Scene ID").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
1974
3125
|
try {
|
|
1975
|
-
const
|
|
1976
|
-
const
|
|
3126
|
+
const projectId = resolveProjectId(options.projectId);
|
|
3127
|
+
const sceneId = resolveSceneId(options.sceneId);
|
|
3128
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
3129
|
+
const apiClient = apiUrl ? new SceneApiClient(apiUrl) : null;
|
|
3130
|
+
const bundle = createSceneStoreBundle("file");
|
|
3131
|
+
const project = apiClient ? await apiClient.getProject(projectId) : await bundle.projects.getProject(projectId);
|
|
3132
|
+
if (!project) {
|
|
3133
|
+
console.error(JSON.stringify({ valid: false, errors: [`Project not found: ${projectId}`] }, null, 2));
|
|
3134
|
+
process.exit(1);
|
|
3135
|
+
}
|
|
3136
|
+
const sceneRecord = apiClient ? await apiClient.getScene(projectId, sceneId) : await bundle.scenes.getScene(projectId, sceneId);
|
|
3137
|
+
if (!sceneRecord) {
|
|
3138
|
+
console.error(JSON.stringify({ valid: false, errors: [`Scene not found: ${sceneId}`] }, null, 2));
|
|
3139
|
+
process.exit(1);
|
|
3140
|
+
}
|
|
3141
|
+
const registry = createDefaultAssetResolverRegistry();
|
|
3142
|
+
const rawScene = { meta: sceneRecord.meta, entities: sceneRecord.entities };
|
|
3143
|
+
const data = await SceneResolver.resolve(rawScene, project.rootPath || process.cwd());
|
|
1977
3144
|
const errors = [];
|
|
1978
3145
|
const warnings = [];
|
|
1979
3146
|
if (!data.meta) {
|
|
@@ -2009,36 +3176,32 @@ validateCommand.description("Validate a scene file").argument("<file>", "Path to
|
|
|
2009
3176
|
if (typeof entity.src !== "string") {
|
|
2010
3177
|
errors.push(`${prefix} "src" must be a string`);
|
|
2011
3178
|
} else {
|
|
2012
|
-
|
|
2013
|
-
if (
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
if (cropPct > 15) {
|
|
2034
|
-
warnings.push(
|
|
2035
|
-
`${prefix} may crop ~${cropPct.toFixed(1)}% of image area at render time (image ${metadata.width}x${metadata.height}, canvas ${data.meta.width}x${data.meta.height}). Consider "fit": "contain" or generate matching aspect ratio.`
|
|
2036
|
-
);
|
|
3179
|
+
const resolved = registry.resolve(entity.src, project.rootPath || process.cwd());
|
|
3180
|
+
if (resolved.type === "unsupported") {
|
|
3181
|
+
errors.push(`${prefix} unsupported asset URI: ${entity.src} (${resolved.reason})`);
|
|
3182
|
+
} else if (resolved.type === "local") {
|
|
3183
|
+
if (!fs10.existsSync(resolved.localPath)) {
|
|
3184
|
+
errors.push(`${prefix} asset not found at ${resolved.localPath} (src: "${entity.src}")`);
|
|
3185
|
+
} else {
|
|
3186
|
+
try {
|
|
3187
|
+
const metadata = await FFprobeService.getMetadata(resolved.localPath);
|
|
3188
|
+
if (metadata.width && metadata.height && data.meta?.width && data.meta?.height) {
|
|
3189
|
+
const imageRatio = metadata.width / metadata.height;
|
|
3190
|
+
const canvasRatio = data.meta.width / data.meta.height;
|
|
3191
|
+
const fit = resolveRuntimeFit(entity.fit, imageRatio, canvasRatio);
|
|
3192
|
+
if (fit === "cover") {
|
|
3193
|
+
const visibleFraction = Math.min(canvasRatio / imageRatio, imageRatio / canvasRatio);
|
|
3194
|
+
const cropPct = (1 - visibleFraction) * 100;
|
|
3195
|
+
if (cropPct > 15) {
|
|
3196
|
+
warnings.push(
|
|
3197
|
+
`${prefix} may crop ~${cropPct.toFixed(1)}% of image area at render time (image ${metadata.width}x${metadata.height}, canvas ${data.meta.width}x${data.meta.height}). Consider "fit": "contain" or generate matching aspect ratio.`
|
|
3198
|
+
);
|
|
3199
|
+
}
|
|
2037
3200
|
}
|
|
2038
3201
|
}
|
|
3202
|
+
} catch (metaError) {
|
|
3203
|
+
warnings.push(`${prefix} could not inspect image dimensions: ${metaError.message}`);
|
|
2039
3204
|
}
|
|
2040
|
-
} catch (metaError) {
|
|
2041
|
-
warnings.push(`${prefix} could not inspect image dimensions: ${metaError.message}`);
|
|
2042
3205
|
}
|
|
2043
3206
|
}
|
|
2044
3207
|
}
|
|
@@ -2049,17 +3212,11 @@ validateCommand.description("Validate a scene file").argument("<file>", "Path to
|
|
|
2049
3212
|
} else if (typeof entity.src !== "string") {
|
|
2050
3213
|
errors.push(`${prefix} "src" must be a string`);
|
|
2051
3214
|
} else {
|
|
2052
|
-
|
|
2053
|
-
if (
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
fullPath = entity.src;
|
|
2058
|
-
} else {
|
|
2059
|
-
fullPath = path8.resolve(process.cwd(), entity.src);
|
|
2060
|
-
}
|
|
2061
|
-
if (!fs7.existsSync(fullPath)) {
|
|
2062
|
-
errors.push(`${prefix} asset not found at ${fullPath} (derived from src: "${entity.src}")`);
|
|
3215
|
+
const resolved = registry.resolve(entity.src, project.rootPath || process.cwd());
|
|
3216
|
+
if (resolved.type === "unsupported") {
|
|
3217
|
+
errors.push(`${prefix} unsupported asset URI: ${entity.src} (${resolved.reason})`);
|
|
3218
|
+
} else if (resolved.type === "local" && !fs10.existsSync(resolved.localPath)) {
|
|
3219
|
+
errors.push(`${prefix} asset not found at ${resolved.localPath} (src: "${entity.src}")`);
|
|
2063
3220
|
}
|
|
2064
3221
|
}
|
|
2065
3222
|
if (entity.sourceStart !== void 0 && typeof entity.sourceStart !== "number") {
|
|
@@ -2079,9 +3236,9 @@ validateCommand.description("Validate a scene file").argument("<file>", "Path to
|
|
|
2079
3236
|
if (errors.length > 0) {
|
|
2080
3237
|
console.log(JSON.stringify({ valid: false, errors, warnings }, null, 2));
|
|
2081
3238
|
process.exit(1);
|
|
2082
|
-
} else {
|
|
2083
|
-
console.log(JSON.stringify({ valid: true, warnings }, null, 2));
|
|
2084
3239
|
}
|
|
3240
|
+
writeFlowState({ projectId, sceneId, apiUrl });
|
|
3241
|
+
console.log(JSON.stringify({ valid: true, warnings }, null, 2));
|
|
2085
3242
|
} catch (e) {
|
|
2086
3243
|
console.error(JSON.stringify({ valid: false, errors: ["Error validating scene: " + e.message] }, null, 2));
|
|
2087
3244
|
process.exit(1);
|
|
@@ -2090,43 +3247,87 @@ validateCommand.description("Validate a scene file").argument("<file>", "Path to
|
|
|
2090
3247
|
|
|
2091
3248
|
// src/cli/commands/set-scene.ts
|
|
2092
3249
|
import { Command as Command4 } from "commander";
|
|
2093
|
-
import
|
|
2094
|
-
import
|
|
3250
|
+
import fs11 from "fs";
|
|
3251
|
+
import path10 from "path";
|
|
2095
3252
|
var setSceneCommand = new Command4("set-scene");
|
|
2096
|
-
setSceneCommand.description("Update
|
|
2097
|
-
const targetPath = path9.resolve(process.cwd(), file);
|
|
3253
|
+
setSceneCommand.description("Update an existing scene by project/scene ID").option("--project-id <id>", "Project ID").option("--scene-id <id>", "Scene ID").option("-c, --content <json>", "JSON content string").option("-i, --input <file>", "Input JSON file to copy from").option("--version <number>", "Expected scene version").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
2098
3254
|
let contentStr = "";
|
|
2099
3255
|
if (options.content) {
|
|
2100
3256
|
contentStr = options.content;
|
|
2101
3257
|
} else if (options.input) {
|
|
2102
|
-
const inputPath =
|
|
2103
|
-
if (!
|
|
3258
|
+
const inputPath = path10.resolve(process.cwd(), options.input);
|
|
3259
|
+
if (!fs11.existsSync(inputPath)) {
|
|
2104
3260
|
console.error(`Input file not found: ${inputPath}`);
|
|
2105
3261
|
process.exit(1);
|
|
2106
3262
|
}
|
|
2107
|
-
contentStr =
|
|
3263
|
+
contentStr = fs11.readFileSync(inputPath, "utf-8");
|
|
2108
3264
|
} else {
|
|
2109
3265
|
console.error("Error: Please provide --content <json> or --input <file>");
|
|
2110
3266
|
process.exit(1);
|
|
2111
3267
|
}
|
|
2112
3268
|
try {
|
|
2113
3269
|
const data = JSON.parse(contentStr);
|
|
2114
|
-
if (!data.meta || !data.entities) {
|
|
3270
|
+
if (!data.meta || !Array.isArray(data.entities)) {
|
|
2115
3271
|
console.error('Error: Invalid scene format. Missing "meta" or "entities".');
|
|
2116
3272
|
process.exit(1);
|
|
2117
3273
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
3274
|
+
const projectId = resolveProjectId(options.projectId);
|
|
3275
|
+
const sceneId = resolveSceneId(options.sceneId);
|
|
3276
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
3277
|
+
const scene = apiUrl ? await new SceneApiClient(apiUrl).updateScene(
|
|
3278
|
+
projectId,
|
|
3279
|
+
sceneId,
|
|
3280
|
+
{
|
|
3281
|
+
meta: data.meta,
|
|
3282
|
+
entities: data.entities,
|
|
3283
|
+
version: options.version !== void 0 ? Number(options.version) : void 0
|
|
3284
|
+
}
|
|
3285
|
+
) : await createSceneStoreBundle("file").scenes.updateScene(
|
|
3286
|
+
projectId,
|
|
3287
|
+
sceneId,
|
|
3288
|
+
{
|
|
3289
|
+
meta: data.meta,
|
|
3290
|
+
entities: data.entities
|
|
3291
|
+
},
|
|
3292
|
+
options.version !== void 0 ? Number(options.version) : void 0
|
|
3293
|
+
);
|
|
3294
|
+
writeFlowState({ projectId, sceneId, apiUrl });
|
|
3295
|
+
console.log(`Scene updated: project=${scene.projectId} scene=${scene.id} version=${scene.version}`);
|
|
2120
3296
|
} catch (e) {
|
|
2121
|
-
console.error("Error: Content is not valid JSON.", e.message);
|
|
3297
|
+
console.error("Error: Content is not valid JSON or update failed.", e.message);
|
|
2122
3298
|
process.exit(1);
|
|
2123
3299
|
}
|
|
2124
3300
|
});
|
|
2125
3301
|
|
|
2126
3302
|
// src/cli/commands/imagine.ts
|
|
2127
3303
|
import { Command as Command5 } from "commander";
|
|
2128
|
-
import
|
|
2129
|
-
import
|
|
3304
|
+
import fs13 from "fs";
|
|
3305
|
+
import path12 from "path";
|
|
3306
|
+
|
|
3307
|
+
// src/cli/utils/path.ts
|
|
3308
|
+
import path11 from "path";
|
|
3309
|
+
import fs12 from "fs";
|
|
3310
|
+
function resolveAssetOutputPath(outputOption, cwd = process.cwd()) {
|
|
3311
|
+
if (path11.isAbsolute(outputOption)) {
|
|
3312
|
+
return outputOption;
|
|
3313
|
+
}
|
|
3314
|
+
const assetsDir = path11.resolve(cwd, "assets");
|
|
3315
|
+
if (!fs12.existsSync(assetsDir)) {
|
|
3316
|
+
fs12.mkdirSync(assetsDir, { recursive: true });
|
|
3317
|
+
}
|
|
3318
|
+
const normalizedInput = path11.normalize(outputOption);
|
|
3319
|
+
let cleanRelativePath = normalizedInput;
|
|
3320
|
+
const segments = normalizedInput.split(path11.sep);
|
|
3321
|
+
if (segments[0] === "assets") {
|
|
3322
|
+
cleanRelativePath = segments.slice(1).join(path11.sep);
|
|
3323
|
+
}
|
|
3324
|
+
if (!cleanRelativePath) {
|
|
3325
|
+
cleanRelativePath = `unnamed-asset-${Date.now()}`;
|
|
3326
|
+
}
|
|
3327
|
+
return path11.join(assetsDir, cleanRelativePath);
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
// src/cli/commands/imagine.ts
|
|
2130
3331
|
var imagineCommand = new Command5("imagine");
|
|
2131
3332
|
imagineCommand.description("Generate images using Google Gemini AI (Imagen)").argument("<prompt-or-file>", "Text prompt or path to a file containing the prompt").option("-o, --output <filename>", "Output filename (relative to assets/ or absolute path)", "generated.png").option("-k, --api-key <key>", "Gemini API key (or set GEMINI_API_KEY env var)").option("-n, --number <count>", "Number of images to generate", "1").option("--aspect-ratio <ratio>", "Aspect ratio (e.g., 9:16, 1:1, 16:9, or auto)", "auto").option("--image-size <size>", "Image size hint: 1K, 2K, or 4K", "2K").option("--project <path>", "Scene file path to infer aspect ratio from (reads meta.width/meta.height)", "scene_1.json").option("--json", "Emit machine-readable JSON output").addHelpText("after", `
|
|
2132
3333
|
|
|
@@ -2176,9 +3377,9 @@ Note:
|
|
|
2176
3377
|
`).action(async (promptOrFile, options) => {
|
|
2177
3378
|
try {
|
|
2178
3379
|
let prompt;
|
|
2179
|
-
if (
|
|
3380
|
+
if (fs13.existsSync(promptOrFile)) {
|
|
2180
3381
|
console.log(`Reading prompt from file: ${promptOrFile}`);
|
|
2181
|
-
prompt =
|
|
3382
|
+
prompt = fs13.readFileSync(promptOrFile, "utf-8").trim();
|
|
2182
3383
|
} else {
|
|
2183
3384
|
prompt = promptOrFile;
|
|
2184
3385
|
}
|
|
@@ -2219,27 +3420,17 @@ Note:
|
|
|
2219
3420
|
aspectRatio,
|
|
2220
3421
|
imageSize
|
|
2221
3422
|
});
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
console.log(`\u{1F4C1} Creating assets directory: ${assetsDir}`);
|
|
2229
|
-
fs9.mkdirSync(assetsDir, { recursive: true });
|
|
2230
|
-
}
|
|
2231
|
-
outputPath = path10.join(assetsDir, options.output);
|
|
2232
|
-
}
|
|
2233
|
-
const outputDir = path10.dirname(outputPath);
|
|
2234
|
-
const outputExt = path10.extname(outputPath) || ".png";
|
|
2235
|
-
const outputBase = path10.basename(outputPath, outputExt);
|
|
2236
|
-
if (!fs9.existsSync(outputDir)) {
|
|
2237
|
-
fs9.mkdirSync(outputDir, { recursive: true });
|
|
3423
|
+
const outputPath = resolveAssetOutputPath(options.output);
|
|
3424
|
+
const outputDir = path12.dirname(outputPath);
|
|
3425
|
+
const outputExt = path12.extname(outputPath) || ".png";
|
|
3426
|
+
const outputBase = path12.basename(outputPath, outputExt);
|
|
3427
|
+
if (!fs13.existsSync(outputDir)) {
|
|
3428
|
+
fs13.mkdirSync(outputDir, { recursive: true });
|
|
2238
3429
|
}
|
|
2239
3430
|
const savedFiles = [];
|
|
2240
3431
|
for (let i = 0; i < images.length; i++) {
|
|
2241
|
-
const filename = images.length > 1 ?
|
|
2242
|
-
|
|
3432
|
+
const filename = images.length > 1 ? path12.join(outputDir, `${outputBase}_${i + 1}${outputExt}`) : outputPath;
|
|
3433
|
+
fs13.writeFileSync(filename, images[i]);
|
|
2243
3434
|
if (!options.json) console.log(`\u2705 Image saved: ${filename}`);
|
|
2244
3435
|
savedFiles.push(filename);
|
|
2245
3436
|
}
|
|
@@ -2263,6 +3454,9 @@ Note:
|
|
|
2263
3454
|
if (error.cause) {
|
|
2264
3455
|
console.error("Details:", error.cause);
|
|
2265
3456
|
}
|
|
3457
|
+
if (error.message.includes("Responsible AI practices")) {
|
|
3458
|
+
console.error("\n\u26A0\uFE0F Google GenAI Filter violation. Please try standardizing the prompt to be more descriptive and avoid vague words.");
|
|
3459
|
+
}
|
|
2266
3460
|
process.exit(1);
|
|
2267
3461
|
}
|
|
2268
3462
|
});
|
|
@@ -2347,10 +3541,10 @@ function resolveAspectRatio(input, projectFile) {
|
|
|
2347
3541
|
return normalizeAspectRatio(input);
|
|
2348
3542
|
}
|
|
2349
3543
|
function inferAspectRatioFromProject(projectFile) {
|
|
2350
|
-
const filePath =
|
|
2351
|
-
if (!
|
|
3544
|
+
const filePath = path12.resolve(process.cwd(), projectFile);
|
|
3545
|
+
if (!fs13.existsSync(filePath)) return null;
|
|
2352
3546
|
try {
|
|
2353
|
-
const scene = JSON.parse(
|
|
3547
|
+
const scene = JSON.parse(fs13.readFileSync(filePath, "utf-8"));
|
|
2354
3548
|
const width = scene?.meta?.width;
|
|
2355
3549
|
const height = scene?.meta?.height;
|
|
2356
3550
|
if (typeof width !== "number" || typeof height !== "number" || width <= 0 || height <= 0) {
|
|
@@ -2390,12 +3584,12 @@ function gcd(a, b) {
|
|
|
2390
3584
|
|
|
2391
3585
|
// src/cli/commands/audio.ts
|
|
2392
3586
|
import { Command as Command6 } from "commander";
|
|
2393
|
-
import
|
|
2394
|
-
import
|
|
3587
|
+
import fs16 from "fs";
|
|
3588
|
+
import path15 from "path";
|
|
2395
3589
|
|
|
2396
3590
|
// src/cli/services/whisper.ts
|
|
2397
|
-
import
|
|
2398
|
-
import
|
|
3591
|
+
import fs14 from "fs";
|
|
3592
|
+
import path13 from "path";
|
|
2399
3593
|
import os from "os";
|
|
2400
3594
|
import { spawn as spawn5 } from "child_process";
|
|
2401
3595
|
import https from "https";
|
|
@@ -2404,37 +3598,37 @@ var WhisperService = class {
|
|
|
2404
3598
|
return os.homedir();
|
|
2405
3599
|
}
|
|
2406
3600
|
static getBaseDir() {
|
|
2407
|
-
return
|
|
3601
|
+
return path13.join(this.getHomeDir(), ".feedeas");
|
|
2408
3602
|
}
|
|
2409
3603
|
static getBinDir() {
|
|
2410
|
-
return
|
|
3604
|
+
return path13.join(this.getBaseDir(), "bin");
|
|
2411
3605
|
}
|
|
2412
3606
|
static getModelsDir() {
|
|
2413
|
-
return
|
|
3607
|
+
return path13.join(this.getBaseDir(), "models");
|
|
2414
3608
|
}
|
|
2415
3609
|
static getExecutablePath() {
|
|
2416
|
-
const repoDir =
|
|
3610
|
+
const repoDir = path13.join(this.getBaseDir(), "whisper.cpp-repo");
|
|
2417
3611
|
const possiblePaths = [
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
3612
|
+
path13.join(repoDir, "build", "bin", "whisper-cli"),
|
|
3613
|
+
path13.join(repoDir, "build", "bin", "main"),
|
|
3614
|
+
path13.join(repoDir, "main"),
|
|
3615
|
+
path13.join(this.getBinDir(), "whisper-main")
|
|
2422
3616
|
// fallback to copied/downloaded
|
|
2423
3617
|
];
|
|
2424
3618
|
for (const p of possiblePaths) {
|
|
2425
|
-
if (
|
|
3619
|
+
if (fs14.existsSync(p)) return p;
|
|
2426
3620
|
}
|
|
2427
|
-
return
|
|
3621
|
+
return path13.join(this.getBinDir(), "whisper-main");
|
|
2428
3622
|
}
|
|
2429
3623
|
static getModelPath(modelName = "base.en") {
|
|
2430
|
-
return
|
|
3624
|
+
return path13.join(this.getModelsDir(), `ggml-${modelName}.bin`);
|
|
2431
3625
|
}
|
|
2432
3626
|
/**
|
|
2433
3627
|
* Download a file from a URL to a destination path
|
|
2434
3628
|
*/
|
|
2435
3629
|
static async downloadFile(url, destPath) {
|
|
2436
3630
|
return new Promise((resolve, reject) => {
|
|
2437
|
-
const file =
|
|
3631
|
+
const file = fs14.createWriteStream(destPath);
|
|
2438
3632
|
https.get(url, (response) => {
|
|
2439
3633
|
if (response.statusCode === 302 || response.statusCode === 301) {
|
|
2440
3634
|
this.downloadFile(response.headers.location, destPath).then(resolve).catch(reject);
|
|
@@ -2450,7 +3644,7 @@ var WhisperService = class {
|
|
|
2450
3644
|
resolve();
|
|
2451
3645
|
});
|
|
2452
3646
|
}).on("error", (err) => {
|
|
2453
|
-
|
|
3647
|
+
fs14.unlink(destPath, () => {
|
|
2454
3648
|
});
|
|
2455
3649
|
reject(err);
|
|
2456
3650
|
});
|
|
@@ -2462,15 +3656,15 @@ var WhisperService = class {
|
|
|
2462
3656
|
static async ensureReady() {
|
|
2463
3657
|
const binDir = this.getBinDir();
|
|
2464
3658
|
const modelsDir = this.getModelsDir();
|
|
2465
|
-
if (!
|
|
2466
|
-
if (!
|
|
3659
|
+
if (!fs14.existsSync(binDir)) fs14.mkdirSync(binDir, { recursive: true });
|
|
3660
|
+
if (!fs14.existsSync(modelsDir)) fs14.mkdirSync(modelsDir, { recursive: true });
|
|
2467
3661
|
const execPath = this.getExecutablePath();
|
|
2468
3662
|
const modelPath = this.getModelPath();
|
|
2469
|
-
if (!
|
|
3663
|
+
if (!fs14.existsSync(execPath)) {
|
|
2470
3664
|
console.log("\u2B07\uFE0F Whisper binary not found. Attempting to build...");
|
|
2471
3665
|
try {
|
|
2472
|
-
const repoDir =
|
|
2473
|
-
if (!
|
|
3666
|
+
const repoDir = path13.join(this.getBaseDir(), "whisper.cpp-repo");
|
|
3667
|
+
if (!fs14.existsSync(repoDir)) {
|
|
2474
3668
|
console.log("\u{1F4E6} Cloning whisper.cpp...");
|
|
2475
3669
|
await runCommand("git", ["clone", "https://github.com/ggerganov/whisper.cpp.git", repoDir]);
|
|
2476
3670
|
} else {
|
|
@@ -2478,7 +3672,7 @@ var WhisperService = class {
|
|
|
2478
3672
|
console.log("\u{1F528} Building whisper.cpp (this may take a minute)...");
|
|
2479
3673
|
await runCommand("make", [], repoDir);
|
|
2480
3674
|
const newPath = this.getExecutablePath();
|
|
2481
|
-
if (
|
|
3675
|
+
if (fs14.existsSync(newPath)) {
|
|
2482
3676
|
console.log(`\u2705 Whisper binary built at: ${newPath}`);
|
|
2483
3677
|
} else {
|
|
2484
3678
|
throw new Error("Build command finished but binary not found in expected paths.");
|
|
@@ -2489,7 +3683,7 @@ var WhisperService = class {
|
|
|
2489
3683
|
Please install manually: 'brew install whisper-cpp'`);
|
|
2490
3684
|
}
|
|
2491
3685
|
}
|
|
2492
|
-
if (!
|
|
3686
|
+
if (!fs14.existsSync(modelPath)) {
|
|
2493
3687
|
console.log("\u2B07\uFE0F Downloading Whisper model (base.en)...");
|
|
2494
3688
|
const modelUrl = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.en.bin";
|
|
2495
3689
|
await this.downloadFile(modelUrl, modelPath);
|
|
@@ -2502,12 +3696,12 @@ Please install manually: 'brew install whisper-cpp'`);
|
|
|
2502
3696
|
static async transcribe(audioPath) {
|
|
2503
3697
|
const execPath = this.getExecutablePath();
|
|
2504
3698
|
const modelPath = this.getModelPath();
|
|
2505
|
-
const ext =
|
|
3699
|
+
const ext = path13.extname(audioPath).toLowerCase();
|
|
2506
3700
|
let inputToWhisper = audioPath;
|
|
2507
3701
|
let isTempFile = false;
|
|
2508
3702
|
if (ext !== ".wav") {
|
|
2509
3703
|
console.log("\u{1F504} Converting audio to 16kHz WAV for Whisper...");
|
|
2510
|
-
const tempWav =
|
|
3704
|
+
const tempWav = path13.join(path13.dirname(audioPath), `temp_${Date.now()}.wav`);
|
|
2511
3705
|
await runCommand("ffmpeg", [
|
|
2512
3706
|
"-i",
|
|
2513
3707
|
audioPath,
|
|
@@ -2524,7 +3718,7 @@ Please install manually: 'brew install whisper-cpp'`);
|
|
|
2524
3718
|
isTempFile = true;
|
|
2525
3719
|
}
|
|
2526
3720
|
try {
|
|
2527
|
-
const outputBase =
|
|
3721
|
+
const outputBase = path13.join(path13.dirname(audioPath), path13.basename(audioPath, path13.extname(audioPath)));
|
|
2528
3722
|
const baseArgs = [
|
|
2529
3723
|
"-m",
|
|
2530
3724
|
modelPath,
|
|
@@ -2561,8 +3755,8 @@ Please install manually: 'brew install whisper-cpp'`);
|
|
|
2561
3755
|
words
|
|
2562
3756
|
};
|
|
2563
3757
|
} finally {
|
|
2564
|
-
if (isTempFile &&
|
|
2565
|
-
|
|
3758
|
+
if (isTempFile && fs14.existsSync(inputToWhisper)) {
|
|
3759
|
+
fs14.unlinkSync(inputToWhisper);
|
|
2566
3760
|
}
|
|
2567
3761
|
}
|
|
2568
3762
|
}
|
|
@@ -2588,12 +3782,12 @@ function runWhisper(execPath, args) {
|
|
|
2588
3782
|
const outputFileIndex = args.indexOf("-of");
|
|
2589
3783
|
const outputBase = outputFileIndex >= 0 ? args[outputFileIndex + 1] : void 0;
|
|
2590
3784
|
const jsonPath = outputBase ? `${outputBase}.json` : "";
|
|
2591
|
-
if (!jsonPath || !
|
|
3785
|
+
if (!jsonPath || !fs14.existsSync(jsonPath)) {
|
|
2592
3786
|
reject(new Error("Whisper output JSON not found"));
|
|
2593
3787
|
return;
|
|
2594
3788
|
}
|
|
2595
3789
|
try {
|
|
2596
|
-
const data = JSON.parse(
|
|
3790
|
+
const data = JSON.parse(fs14.readFileSync(jsonPath, "utf-8"));
|
|
2597
3791
|
resolve(data);
|
|
2598
3792
|
} catch (err) {
|
|
2599
3793
|
reject(err);
|
|
@@ -2641,14 +3835,133 @@ function roundTo3(value) {
|
|
|
2641
3835
|
return Math.round(value * 1e3) / 1e3;
|
|
2642
3836
|
}
|
|
2643
3837
|
|
|
3838
|
+
// src/cli/services/audio-convert.ts
|
|
3839
|
+
import fs15 from "fs";
|
|
3840
|
+
import path14 from "path";
|
|
3841
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
3842
|
+
import { pathToFileURL } from "node:url";
|
|
3843
|
+
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
|
3844
|
+
function isNodeRuntime() {
|
|
3845
|
+
return typeof process !== "undefined" && !!process.versions?.node;
|
|
3846
|
+
}
|
|
3847
|
+
function buildCodecArgs(options) {
|
|
3848
|
+
if (options.format === "wav") {
|
|
3849
|
+
return ["-c:a", "pcm_s16le"];
|
|
3850
|
+
}
|
|
3851
|
+
if (typeof options.mp3Quality === "number") {
|
|
3852
|
+
return ["-c:a", "libmp3lame", "-q:a", String(options.mp3Quality)];
|
|
3853
|
+
}
|
|
3854
|
+
return ["-c:a", "libmp3lame", "-b:a", options.mp3Bitrate || "128k"];
|
|
3855
|
+
}
|
|
3856
|
+
async function convertWithNodeFfmpeg(options) {
|
|
3857
|
+
const { pcmPath, outputPath, sampleRate, channels, durationSec } = options;
|
|
3858
|
+
const codecArgs = buildCodecArgs(options);
|
|
3859
|
+
await new Promise((resolve, reject) => {
|
|
3860
|
+
const ffmpeg = spawn6("ffmpeg", [
|
|
3861
|
+
"-f",
|
|
3862
|
+
"s16le",
|
|
3863
|
+
"-ar",
|
|
3864
|
+
String(sampleRate),
|
|
3865
|
+
"-ac",
|
|
3866
|
+
String(channels),
|
|
3867
|
+
"-i",
|
|
3868
|
+
pcmPath,
|
|
3869
|
+
...typeof durationSec === "number" ? ["-t", String(durationSec)] : [],
|
|
3870
|
+
...codecArgs,
|
|
3871
|
+
"-y",
|
|
3872
|
+
outputPath
|
|
3873
|
+
], { stdio: "inherit" });
|
|
3874
|
+
ffmpeg.on("close", (code) => {
|
|
3875
|
+
if (code === 0) resolve();
|
|
3876
|
+
else reject(new Error(`FFmpeg exited with code ${code}`));
|
|
3877
|
+
});
|
|
3878
|
+
ffmpeg.on("error", (err) => reject(err));
|
|
3879
|
+
});
|
|
3880
|
+
}
|
|
3881
|
+
async function convertWithWasm(options) {
|
|
3882
|
+
const { pcmPath, outputPath, sampleRate, channels, durationSec } = options;
|
|
3883
|
+
const ffmpeg = new FFmpeg();
|
|
3884
|
+
const baseDir = typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : ".";
|
|
3885
|
+
const coreDir = path14.resolve(baseDir, "node_modules/@ffmpeg/core/dist");
|
|
3886
|
+
const coreURL = pathToFileURL(path14.join(coreDir, "ffmpeg-core.js")).toString();
|
|
3887
|
+
const wasmURL = pathToFileURL(path14.join(coreDir, "ffmpeg-core.wasm")).toString();
|
|
3888
|
+
const workerURL = pathToFileURL(path14.join(coreDir, "ffmpeg-core.worker.js")).toString();
|
|
3889
|
+
await ffmpeg.load({ coreURL, wasmURL, workerURL });
|
|
3890
|
+
const pcmBytes = await fs15.promises.readFile(pcmPath);
|
|
3891
|
+
await ffmpeg.writeFile("input.pcm", pcmBytes);
|
|
3892
|
+
const codecArgs = buildCodecArgs(options);
|
|
3893
|
+
const outputName = options.format === "wav" ? "output.wav" : "output.mp3";
|
|
3894
|
+
await ffmpeg.exec([
|
|
3895
|
+
"-f",
|
|
3896
|
+
"s16le",
|
|
3897
|
+
"-ar",
|
|
3898
|
+
String(sampleRate),
|
|
3899
|
+
"-ac",
|
|
3900
|
+
String(channels),
|
|
3901
|
+
"-i",
|
|
3902
|
+
"input.pcm",
|
|
3903
|
+
...typeof durationSec === "number" ? ["-t", String(durationSec)] : [],
|
|
3904
|
+
...codecArgs,
|
|
3905
|
+
outputName
|
|
3906
|
+
]);
|
|
3907
|
+
const output = await ffmpeg.readFile(outputName);
|
|
3908
|
+
await fs15.promises.writeFile(outputPath, output);
|
|
3909
|
+
}
|
|
3910
|
+
async function convertPcmToAudio(options) {
|
|
3911
|
+
const { pcmPath } = options;
|
|
3912
|
+
let useWasm = !isNodeRuntime();
|
|
3913
|
+
try {
|
|
3914
|
+
try {
|
|
3915
|
+
if (!useWasm) {
|
|
3916
|
+
await convertWithNodeFfmpeg(options);
|
|
3917
|
+
return;
|
|
3918
|
+
}
|
|
3919
|
+
} catch (err) {
|
|
3920
|
+
if (err?.code !== "ENOENT") {
|
|
3921
|
+
throw err;
|
|
3922
|
+
}
|
|
3923
|
+
useWasm = true;
|
|
3924
|
+
}
|
|
3925
|
+
if (useWasm) {
|
|
3926
|
+
try {
|
|
3927
|
+
await convertWithWasm(options);
|
|
3928
|
+
return;
|
|
3929
|
+
} catch (err) {
|
|
3930
|
+
throw new Error(
|
|
3931
|
+
`Audio conversion failed. Install ffmpeg or ensure @ffmpeg/core assets are present for wasm conversion. (${err?.message || err})`
|
|
3932
|
+
);
|
|
3933
|
+
}
|
|
3934
|
+
}
|
|
3935
|
+
throw new Error(
|
|
3936
|
+
"Audio conversion failed. Install ffmpeg or ensure @ffmpeg/core assets are present for wasm conversion."
|
|
3937
|
+
);
|
|
3938
|
+
} finally {
|
|
3939
|
+
if (fs15.existsSync(pcmPath)) {
|
|
3940
|
+
try {
|
|
3941
|
+
await fs15.promises.unlink(pcmPath);
|
|
3942
|
+
} catch {
|
|
3943
|
+
}
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
async function convertPcmToMp3(options) {
|
|
3948
|
+
await convertPcmToAudio({
|
|
3949
|
+
...options,
|
|
3950
|
+
format: "mp3",
|
|
3951
|
+
sampleRate: 24e3,
|
|
3952
|
+
channels: 1,
|
|
3953
|
+
mp3Bitrate: "128k"
|
|
3954
|
+
});
|
|
3955
|
+
}
|
|
3956
|
+
|
|
2644
3957
|
// src/cli/commands/audio.ts
|
|
2645
3958
|
var audioCommand = new Command6("generate:audio");
|
|
2646
3959
|
audioCommand.alias("audio").description("Generate audio from text using Gemini API and extract metadata").argument("<text-or-file>", "Input text or path to text file").option("-o, --output <filename>", "Output filename (relative to assets/ or absolute path)", "speech.mp3").option("-k, --api-key <key>", "Gemini API key (or set GEMINI_API_KEY env var)").option("--voice <name>", "Voice name (optional)").option("--no-transcribe", "Skip Whisper transcription/metadata generation").action(async (textOrFile, options) => {
|
|
2647
3960
|
try {
|
|
2648
3961
|
let text;
|
|
2649
|
-
if (
|
|
3962
|
+
if (fs16.existsSync(textOrFile)) {
|
|
2650
3963
|
console.log(`\u{1F4D6} Reading text from file: ${textOrFile}`);
|
|
2651
|
-
text =
|
|
3964
|
+
text = fs16.readFileSync(textOrFile, "utf-8").trim();
|
|
2652
3965
|
} else {
|
|
2653
3966
|
text = textOrFile;
|
|
2654
3967
|
}
|
|
@@ -2663,41 +3976,22 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
|
|
|
2663
3976
|
}
|
|
2664
3977
|
console.log("\u{1F5E3}\uFE0F Generating speech with Gemini...");
|
|
2665
3978
|
const audioBuffer = await generateGeminiAudio(text, apiKey, options.voice);
|
|
2666
|
-
const outputPath =
|
|
2667
|
-
const tempPcmPath =
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
"-ar",
|
|
2676
|
-
"24000",
|
|
2677
|
-
"-ac",
|
|
2678
|
-
"1",
|
|
2679
|
-
"-i",
|
|
2680
|
-
tempPcmPath,
|
|
2681
|
-
"-y",
|
|
2682
|
-
outputPath
|
|
2683
|
-
], { stdio: "inherit" });
|
|
2684
|
-
ffmpeg.on("close", (code) => {
|
|
2685
|
-
if (code === 0) resolve();
|
|
2686
|
-
else reject(new Error(`FFmpeg exited with code ${code}`));
|
|
2687
|
-
});
|
|
2688
|
-
ffmpeg.on("error", (err) => reject(err));
|
|
2689
|
-
});
|
|
2690
|
-
console.log(`\u2705 Audio saved: ${outputPath}`);
|
|
2691
|
-
} finally {
|
|
2692
|
-
if (fs11.existsSync(tempPcmPath)) fs11.unlinkSync(tempPcmPath);
|
|
2693
|
-
}
|
|
3979
|
+
const outputPath = resolveAssetOutputPath(options.output);
|
|
3980
|
+
const tempPcmPath = path15.join(path15.dirname(outputPath), `temp_${Date.now()}.pcm`);
|
|
3981
|
+
fs16.writeFileSync(tempPcmPath, audioBuffer);
|
|
3982
|
+
console.log(`\u{1F504} Converting raw PCM to ${path15.extname(outputPath)}...`);
|
|
3983
|
+
await convertPcmToMp3({
|
|
3984
|
+
pcmPath: tempPcmPath,
|
|
3985
|
+
outputPath
|
|
3986
|
+
});
|
|
3987
|
+
console.log(`\u2705 Audio saved: ${outputPath}`);
|
|
2694
3988
|
if (options.transcribe) {
|
|
2695
3989
|
try {
|
|
2696
3990
|
console.log("\u{1F50D} Aligning audio with Whisper...");
|
|
2697
3991
|
await WhisperService.ensureReady();
|
|
2698
3992
|
const metadata = await WhisperService.transcribe(outputPath);
|
|
2699
|
-
const metaPath = outputPath.replace(
|
|
2700
|
-
|
|
3993
|
+
const metaPath = outputPath.replace(path15.extname(outputPath), ".json");
|
|
3994
|
+
fs16.writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
|
|
2701
3995
|
console.log(`\u2705 Metadata saved: ${metaPath}`);
|
|
2702
3996
|
} catch (wErr) {
|
|
2703
3997
|
console.warn("\u26A0\uFE0F Whisper alignment failed:", wErr.message);
|
|
@@ -2708,12 +4002,6 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
|
|
|
2708
4002
|
process.exit(1);
|
|
2709
4003
|
}
|
|
2710
4004
|
});
|
|
2711
|
-
function resolveOutputPath(outputOption) {
|
|
2712
|
-
if (path12.isAbsolute(outputOption)) return outputOption;
|
|
2713
|
-
const assetsDir = path12.resolve(process.cwd(), "assets");
|
|
2714
|
-
if (!fs11.existsSync(assetsDir)) fs11.mkdirSync(assetsDir, { recursive: true });
|
|
2715
|
-
return path12.join(assetsDir, outputOption);
|
|
2716
|
-
}
|
|
2717
4005
|
async function generateGeminiAudio(text, apiKey, voiceName) {
|
|
2718
4006
|
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent`;
|
|
2719
4007
|
const requestBody = {
|
|
@@ -2753,9 +4041,8 @@ async function generateGeminiAudio(text, apiKey, voiceName) {
|
|
|
2753
4041
|
|
|
2754
4042
|
// src/cli/commands/bgm.ts
|
|
2755
4043
|
import { Command as Command7 } from "commander";
|
|
2756
|
-
import
|
|
2757
|
-
import
|
|
2758
|
-
import { spawn as spawn6 } from "child_process";
|
|
4044
|
+
import fs17 from "fs";
|
|
4045
|
+
import path16 from "path";
|
|
2759
4046
|
var bgmCommand = new Command7("generate:bgm");
|
|
2760
4047
|
bgmCommand.alias("bgm").alias("music").description("Generate background music with Gemini Lyria (Live Music API)").argument("<prompt-or-file>", "Music prompt text or path to a text file").option("-o, --output <filename>", "Output filename (relative to assets/ or absolute path)", "bgm.mp3").option("-k, --api-key <key>", "Gemini API key (or set GEMINI_API_KEY env var)").option("-d, --duration <seconds>", "Target duration in seconds (5-300)", "30").option("--seed <number>", "Optional seed for reproducible output").option("--json", "Emit machine-readable JSON output").action(async (promptOrFile, options) => {
|
|
2761
4048
|
try {
|
|
@@ -2766,7 +4053,7 @@ bgmCommand.alias("bgm").alias("music").description("Generate background music wi
|
|
|
2766
4053
|
}
|
|
2767
4054
|
const durationSec = parseDuration(options.duration);
|
|
2768
4055
|
const seed = parseSeed(options.seed);
|
|
2769
|
-
const outputPath =
|
|
4056
|
+
const outputPath = resolveAssetOutputPath(options.output);
|
|
2770
4057
|
const format = inferOutputFormat(outputPath);
|
|
2771
4058
|
const request = {
|
|
2772
4059
|
provider: "gemini",
|
|
@@ -2806,7 +4093,7 @@ bgmCommand.alias("bgm").alias("music").description("Generate background music wi
|
|
|
2806
4093
|
}
|
|
2807
4094
|
});
|
|
2808
4095
|
function resolvePrompt(promptOrFile) {
|
|
2809
|
-
const prompt =
|
|
4096
|
+
const prompt = fs17.existsSync(promptOrFile) ? fs17.readFileSync(promptOrFile, "utf-8").trim() : String(promptOrFile).trim();
|
|
2810
4097
|
if (!prompt) {
|
|
2811
4098
|
throw new Error("Prompt cannot be empty");
|
|
2812
4099
|
}
|
|
@@ -2827,14 +4114,8 @@ function parseSeed(input) {
|
|
|
2827
4114
|
}
|
|
2828
4115
|
return value;
|
|
2829
4116
|
}
|
|
2830
|
-
function resolveOutputPath2(outputOption) {
|
|
2831
|
-
if (path13.isAbsolute(outputOption)) return outputOption;
|
|
2832
|
-
const assetsDir = path13.resolve(process.cwd(), "assets");
|
|
2833
|
-
if (!fs12.existsSync(assetsDir)) fs12.mkdirSync(assetsDir, { recursive: true });
|
|
2834
|
-
return path13.join(assetsDir, outputOption);
|
|
2835
|
-
}
|
|
2836
4117
|
function inferOutputFormat(outputPath) {
|
|
2837
|
-
const ext =
|
|
4118
|
+
const ext = path16.extname(outputPath).toLowerCase();
|
|
2838
4119
|
if (ext === ".wav") return "wav";
|
|
2839
4120
|
if (ext === ".mp3" || !ext) return "mp3";
|
|
2840
4121
|
throw new Error("Invalid output format. Use .mp3 or .wav.");
|
|
@@ -2922,54 +4203,36 @@ async function generateGeminiMusicPcm(request) {
|
|
|
2922
4203
|
});
|
|
2923
4204
|
}
|
|
2924
4205
|
async function convertPcmToOutput(pcmBuffer, outputPath, format, durationSec) {
|
|
2925
|
-
const outputDir =
|
|
2926
|
-
if (!
|
|
2927
|
-
const tempPcmPath =
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
"2",
|
|
2939
|
-
"-i",
|
|
2940
|
-
tempPcmPath,
|
|
2941
|
-
"-t",
|
|
2942
|
-
String(durationSec),
|
|
2943
|
-
...codecArgs,
|
|
2944
|
-
"-y",
|
|
2945
|
-
outputPath
|
|
2946
|
-
], { stdio: "inherit" });
|
|
2947
|
-
ffmpeg.on("close", (code) => {
|
|
2948
|
-
if (code === 0) resolve();
|
|
2949
|
-
else reject(new Error(`FFmpeg exited with code ${code}`));
|
|
2950
|
-
});
|
|
2951
|
-
ffmpeg.on("error", reject);
|
|
2952
|
-
});
|
|
2953
|
-
} finally {
|
|
2954
|
-
if (fs12.existsSync(tempPcmPath)) fs12.unlinkSync(tempPcmPath);
|
|
2955
|
-
}
|
|
4206
|
+
const outputDir = path16.dirname(outputPath);
|
|
4207
|
+
if (!fs17.existsSync(outputDir)) fs17.mkdirSync(outputDir, { recursive: true });
|
|
4208
|
+
const tempPcmPath = path16.join(outputDir, `temp_bgm_${Date.now()}.pcm`);
|
|
4209
|
+
fs17.writeFileSync(tempPcmPath, pcmBuffer);
|
|
4210
|
+
await convertPcmToAudio({
|
|
4211
|
+
pcmPath: tempPcmPath,
|
|
4212
|
+
outputPath,
|
|
4213
|
+
format,
|
|
4214
|
+
sampleRate: 48e3,
|
|
4215
|
+
channels: 2,
|
|
4216
|
+
durationSec,
|
|
4217
|
+
mp3Quality: format === "mp3" ? 2 : void 0
|
|
4218
|
+
});
|
|
2956
4219
|
}
|
|
2957
4220
|
|
|
2958
4221
|
// src/cli/commands/asset.ts
|
|
2959
4222
|
import { Command as Command8 } from "commander";
|
|
2960
|
-
import
|
|
2961
|
-
import
|
|
4223
|
+
import fs18 from "fs";
|
|
4224
|
+
import path17 from "path";
|
|
2962
4225
|
import { spawn as spawn7 } from "child_process";
|
|
2963
4226
|
var assetCommand = new Command8("asset").description("Asset information and management");
|
|
2964
4227
|
assetCommand.command("info <file>").description("Show detailed information about an asset").action(async (file) => {
|
|
2965
|
-
const assetPath =
|
|
2966
|
-
if (!
|
|
4228
|
+
const assetPath = path17.resolve(process.cwd(), "assets", file);
|
|
4229
|
+
if (!fs18.existsSync(assetPath)) {
|
|
2967
4230
|
console.error(`Asset not found: ${file}`);
|
|
2968
4231
|
console.error(`Looked in: ${assetPath}`);
|
|
2969
4232
|
process.exit(1);
|
|
2970
4233
|
}
|
|
2971
|
-
const stats =
|
|
2972
|
-
const ext =
|
|
4234
|
+
const stats = fs18.statSync(assetPath);
|
|
4235
|
+
const ext = path17.extname(file).toLowerCase();
|
|
2973
4236
|
console.log(`
|
|
2974
4237
|
Asset: ${file}`);
|
|
2975
4238
|
console.log(`Size: ${formatBytes2(stats.size)}`);
|
|
@@ -2992,11 +4255,11 @@ Asset: ${file}`);
|
|
|
2992
4255
|
console.log(`Channels: ${audioInfo.channels}`);
|
|
2993
4256
|
console.log(`Bitrate: ${audioInfo.bitrate}`);
|
|
2994
4257
|
const metadataFile = file.replace(ext, ".json");
|
|
2995
|
-
const metadataPath =
|
|
2996
|
-
if (
|
|
4258
|
+
const metadataPath = path17.resolve(process.cwd(), "assets", metadataFile);
|
|
4259
|
+
if (fs18.existsSync(metadataPath)) {
|
|
2997
4260
|
console.log(`
|
|
2998
4261
|
Metadata: ${metadataFile}`);
|
|
2999
|
-
const metadata = JSON.parse(
|
|
4262
|
+
const metadata = JSON.parse(fs18.readFileSync(metadataPath, "utf-8"));
|
|
3000
4263
|
if (Array.isArray(metadata.words) && metadata.words.length > 0) {
|
|
3001
4264
|
console.log(`Word timings: ${metadata.words.length} words`);
|
|
3002
4265
|
const firstWord = metadata.words[0];
|
|
@@ -3033,7 +4296,8 @@ function getAssetType(ext) {
|
|
|
3033
4296
|
async function getImageDimensions(filePath) {
|
|
3034
4297
|
try {
|
|
3035
4298
|
const imageSize = await import("image-size");
|
|
3036
|
-
const
|
|
4299
|
+
const buffer = fs18.readFileSync(filePath);
|
|
4300
|
+
const dimensions = imageSize.default(buffer);
|
|
3037
4301
|
return { width: dimensions.width || 0, height: dimensions.height || 0 };
|
|
3038
4302
|
} catch (err) {
|
|
3039
4303
|
throw new Error("image-size package not available");
|
|
@@ -3080,9 +4344,9 @@ async function getAudioInfo(filePath) {
|
|
|
3080
4344
|
|
|
3081
4345
|
// src/cli/index.ts
|
|
3082
4346
|
import open2 from "open";
|
|
3083
|
-
import
|
|
4347
|
+
import path21 from "path";
|
|
3084
4348
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
3085
|
-
import
|
|
4349
|
+
import fs21 from "fs";
|
|
3086
4350
|
|
|
3087
4351
|
// src/cli/commands/example.ts
|
|
3088
4352
|
import { Command as Command9 } from "commander";
|
|
@@ -3375,72 +4639,42 @@ function printSchema(name, schema) {
|
|
|
3375
4639
|
|
|
3376
4640
|
// src/cli/commands/create-scene.ts
|
|
3377
4641
|
import { Command as Command11 } from "commander";
|
|
3378
|
-
import path15 from "path";
|
|
3379
|
-
|
|
3380
|
-
// src/cli/services/scene-builder.ts
|
|
3381
|
-
import fs14 from "fs";
|
|
3382
|
-
function loadScene(filePath) {
|
|
3383
|
-
if (fs14.existsSync(filePath)) {
|
|
3384
|
-
try {
|
|
3385
|
-
return JSON.parse(fs14.readFileSync(filePath, "utf-8"));
|
|
3386
|
-
} catch (e) {
|
|
3387
|
-
throw new Error(`Failed to parse existing scene file: ${e}`);
|
|
3388
|
-
}
|
|
3389
|
-
} else {
|
|
3390
|
-
return {
|
|
3391
|
-
meta: {
|
|
3392
|
-
width: 1080,
|
|
3393
|
-
height: 1920,
|
|
3394
|
-
duration: 10
|
|
3395
|
-
},
|
|
3396
|
-
entities: []
|
|
3397
|
-
};
|
|
3398
|
-
}
|
|
3399
|
-
}
|
|
3400
|
-
function saveScene(filePath, scene) {
|
|
3401
|
-
fs14.writeFileSync(filePath, JSON.stringify(scene, null, 2));
|
|
3402
|
-
}
|
|
3403
|
-
function addEntityToScene(scene, entity) {
|
|
3404
|
-
if (!entity.id) {
|
|
3405
|
-
const typeCount = scene.entities.filter((e) => e.type === entity.type).length + 1;
|
|
3406
|
-
entity.id = `${entity.type}-${typeCount}`;
|
|
3407
|
-
}
|
|
3408
|
-
if (scene.entities.some((e) => e.id === entity.id)) {
|
|
3409
|
-
throw new Error(`Entity with ID "${entity.id}" already exists.`);
|
|
3410
|
-
}
|
|
3411
|
-
scene.entities.push(entity);
|
|
3412
|
-
if (typeof entity.startTime === "number" && typeof entity.duration === "number") {
|
|
3413
|
-
const entityEnd = entity.startTime + entity.duration;
|
|
3414
|
-
if (entityEnd > scene.meta.duration) {
|
|
3415
|
-
scene.meta.duration = Math.ceil(entityEnd);
|
|
3416
|
-
}
|
|
3417
|
-
}
|
|
3418
|
-
}
|
|
3419
|
-
|
|
3420
|
-
// src/cli/commands/create-scene.ts
|
|
3421
|
-
import fs15 from "fs";
|
|
3422
4642
|
var createSceneCommand = new Command11("create-scene");
|
|
3423
|
-
createSceneCommand.description("Create
|
|
3424
|
-
const filePath = path15.resolve(process.cwd(), file);
|
|
4643
|
+
createSceneCommand.description("Create a new scene in a project").option("--project-id <id>", "Project ID").option("--name <name>", "Scene name").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
3425
4644
|
try {
|
|
3426
|
-
const
|
|
3427
|
-
|
|
3428
|
-
|
|
4645
|
+
const projectId = resolveProjectId(options.projectId);
|
|
4646
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
4647
|
+
const scene = apiUrl ? await new SceneApiClient(apiUrl).createScene(projectId, {
|
|
4648
|
+
name: options.name ? String(options.name) : void 0
|
|
4649
|
+
}) : await createSceneStoreBundle("file").scenes.createScene(projectId, {
|
|
4650
|
+
name: options.name ? String(options.name) : void 0
|
|
4651
|
+
});
|
|
4652
|
+
writeFlowState({ projectId, sceneId: scene.id, apiUrl });
|
|
4653
|
+
console.log(`\u2705 Scene created: ${scene.id} (project ${scene.projectId})`);
|
|
3429
4654
|
} catch (error) {
|
|
3430
4655
|
console.error(`\u274C Error creating scene: ${error.message}`);
|
|
3431
4656
|
process.exit(1);
|
|
3432
4657
|
}
|
|
3433
4658
|
});
|
|
3434
4659
|
var addEntityCommand = new Command11("add-entity");
|
|
3435
|
-
addEntityCommand.description("Add an entity to an existing scene
|
|
3436
|
-
const filePath = path15.resolve(process.cwd(), file);
|
|
3437
|
-
if (!fs15.existsSync(filePath)) {
|
|
3438
|
-
console.error(`\u274C Scene file not found: ${filePath}`);
|
|
3439
|
-
process.exit(1);
|
|
3440
|
-
}
|
|
4660
|
+
addEntityCommand.description("Add an entity to an existing scene").option("--project-id <id>", "Project ID").option("--scene-id <id>", "Scene ID").requiredOption("-t, --type <type>", "Entity type (image, audio, text)").option("--src <path>", "Asset URI/path").option("--text <content>", "Text content (for text)").option("--fit <mode>", "Image fit mode: smart | contain | cover", "smart").option("--start <number>", "Start time in seconds").option("--duration <string>", 'Duration in seconds or "auto"', "5").option("--at <position>", 'Position ("end" to append after last entity)', "end").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
3441
4661
|
try {
|
|
3442
|
-
const
|
|
4662
|
+
const bundle = createSceneStoreBundle("file");
|
|
4663
|
+
const projectId = resolveProjectId(options.projectId);
|
|
4664
|
+
const sceneId = resolveSceneId(options.sceneId);
|
|
4665
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
4666
|
+
const apiClient = apiUrl ? new SceneApiClient(apiUrl) : null;
|
|
4667
|
+
const project = apiClient ? await apiClient.getProject(projectId) : await bundle.projects.getProject(projectId);
|
|
4668
|
+
if (!project) {
|
|
4669
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
4670
|
+
}
|
|
4671
|
+
const scene = apiClient ? await apiClient.getScene(projectId, sceneId) : await bundle.scenes.getScene(projectId, sceneId);
|
|
4672
|
+
if (!scene) {
|
|
4673
|
+
throw new Error(`Scene not found: ${sceneId}`);
|
|
4674
|
+
}
|
|
3443
4675
|
const type = options.type;
|
|
4676
|
+
const entities = [...scene.entities];
|
|
4677
|
+
const registry = createDefaultAssetResolverRegistry();
|
|
3444
4678
|
let entity = {
|
|
3445
4679
|
type,
|
|
3446
4680
|
visible: true
|
|
@@ -3448,30 +4682,27 @@ addEntityCommand.description("Add an entity to an existing scene file").argument
|
|
|
3448
4682
|
let startTime = parseFloat(options.start);
|
|
3449
4683
|
let duration = parseFloat(options.duration);
|
|
3450
4684
|
if (isNaN(startTime) && options.at === "end") {
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
if (
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
});
|
|
3460
|
-
startTime = maxEnd;
|
|
3461
|
-
} else {
|
|
3462
|
-
startTime = 0;
|
|
3463
|
-
}
|
|
4685
|
+
let maxEnd = 0;
|
|
4686
|
+
entities.forEach((e) => {
|
|
4687
|
+
if (typeof e.startTime === "number" && typeof e.duration === "number") {
|
|
4688
|
+
const end = e.startTime + e.duration;
|
|
4689
|
+
if (end > maxEnd) maxEnd = end;
|
|
4690
|
+
}
|
|
4691
|
+
});
|
|
4692
|
+
startTime = maxEnd;
|
|
3464
4693
|
} else if (isNaN(startTime)) {
|
|
3465
4694
|
startTime = 0;
|
|
3466
4695
|
}
|
|
3467
4696
|
if (options.duration === "auto" && options.src) {
|
|
3468
|
-
const
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
4697
|
+
const resolved = registry.resolve(options.src, project.rootPath || process.cwd());
|
|
4698
|
+
if (resolved.type === "local") {
|
|
4699
|
+
try {
|
|
4700
|
+
const metadata = await FFprobeService.getMetadata(resolved.localPath);
|
|
4701
|
+
duration = metadata.duration;
|
|
4702
|
+
} catch {
|
|
4703
|
+
duration = 5;
|
|
4704
|
+
}
|
|
4705
|
+
} else {
|
|
3475
4706
|
duration = 5;
|
|
3476
4707
|
}
|
|
3477
4708
|
} else if (options.duration === "auto") {
|
|
@@ -3498,12 +4729,32 @@ addEntityCommand.description("Add an entity to an existing scene file").argument
|
|
|
3498
4729
|
entity.color = "#ffffff";
|
|
3499
4730
|
entity.x = 540;
|
|
3500
4731
|
entity.y = 960;
|
|
4732
|
+
entity.fontFamily = "Arial";
|
|
4733
|
+
entity.fontWeight = "700";
|
|
4734
|
+
entity.bgColor = "transparent";
|
|
4735
|
+
entity.maxWidth = 900;
|
|
4736
|
+
entity.lineHeight = 1.2;
|
|
4737
|
+
entity.padding = 8;
|
|
4738
|
+
entity.textAlign = "left";
|
|
3501
4739
|
} else {
|
|
3502
4740
|
throw new Error(`Unknown type: ${type}`);
|
|
3503
4741
|
}
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
4742
|
+
if (!entity.id) {
|
|
4743
|
+
const typeCount = entities.filter((e) => e.type === entity.type).length + 1;
|
|
4744
|
+
entity.id = `${entity.type}-${typeCount}`;
|
|
4745
|
+
}
|
|
4746
|
+
entity.name = entity.name || entity.id;
|
|
4747
|
+
entities.push(entity);
|
|
4748
|
+
const updated = apiClient ? await apiClient.updateScene(projectId, sceneId, {
|
|
4749
|
+
entities,
|
|
4750
|
+
meta: scene.meta,
|
|
4751
|
+
version: scene.version
|
|
4752
|
+
}) : await bundle.scenes.updateScene(projectId, sceneId, {
|
|
4753
|
+
entities,
|
|
4754
|
+
meta: scene.meta
|
|
4755
|
+
}, scene.version);
|
|
4756
|
+
writeFlowState({ projectId, sceneId, apiUrl });
|
|
4757
|
+
console.log(`\u2705 Added ${type} entity to scene ${updated.id} at ${startTime}s`);
|
|
3507
4758
|
} catch (error) {
|
|
3508
4759
|
console.error(`\u274C Error adding entity: ${error.message}`);
|
|
3509
4760
|
process.exit(1);
|
|
@@ -3512,7 +4763,7 @@ addEntityCommand.description("Add an entity to an existing scene file").argument
|
|
|
3512
4763
|
|
|
3513
4764
|
// src/cli/services/telemetry.ts
|
|
3514
4765
|
import os2 from "os";
|
|
3515
|
-
import
|
|
4766
|
+
import path18 from "path";
|
|
3516
4767
|
import { createHash as createHash2 } from "crypto";
|
|
3517
4768
|
var POSTHOG_CAPTURE_URL = "https://us.i.posthog.com/capture/";
|
|
3518
4769
|
var TELEMETRY_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
@@ -3524,7 +4775,7 @@ var PostHogTelemetryService = class {
|
|
|
3524
4775
|
if (!this.enabled) return;
|
|
3525
4776
|
this.capture("cli_feedback", {
|
|
3526
4777
|
details: String(details).slice(0, 2e3),
|
|
3527
|
-
cwd_basename:
|
|
4778
|
+
cwd_basename: path18.basename(process.cwd())
|
|
3528
4779
|
});
|
|
3529
4780
|
}
|
|
3530
4781
|
apiKey;
|
|
@@ -3547,7 +4798,7 @@ var PostHogTelemetryService = class {
|
|
|
3547
4798
|
command_name: this.getCommandPath(actionCommand),
|
|
3548
4799
|
command_aliases: actionCommand.aliases(),
|
|
3549
4800
|
options_used: this.getUsedOptionNames(actionCommand),
|
|
3550
|
-
cwd_basename:
|
|
4801
|
+
cwd_basename: path18.basename(process.cwd()),
|
|
3551
4802
|
node_version: process.version,
|
|
3552
4803
|
platform: process.platform,
|
|
3553
4804
|
arch: process.arch
|
|
@@ -3560,7 +4811,7 @@ var PostHogTelemetryService = class {
|
|
|
3560
4811
|
status,
|
|
3561
4812
|
duration_ms: Math.max(0, Math.round(durationMs)),
|
|
3562
4813
|
error_message: errorMessage ? String(errorMessage).slice(0, 300) : void 0,
|
|
3563
|
-
cwd_basename:
|
|
4814
|
+
cwd_basename: path18.basename(process.cwd())
|
|
3564
4815
|
});
|
|
3565
4816
|
}
|
|
3566
4817
|
getCommandPath(command) {
|
|
@@ -3613,26 +4864,26 @@ var PostHogTelemetryService = class {
|
|
|
3613
4864
|
|
|
3614
4865
|
// src/cli/commands/taste.ts
|
|
3615
4866
|
import { Command as Command12 } from "commander";
|
|
3616
|
-
import
|
|
3617
|
-
import
|
|
4867
|
+
import fs19 from "node:fs";
|
|
4868
|
+
import path19 from "node:path";
|
|
3618
4869
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3619
4870
|
import open from "open";
|
|
3620
4871
|
var __filename4 = fileURLToPath4(import.meta.url);
|
|
3621
|
-
var __dirname4 =
|
|
4872
|
+
var __dirname4 = path19.dirname(__filename4);
|
|
3622
4873
|
function resolveStaticRoot() {
|
|
3623
|
-
let staticRoot =
|
|
3624
|
-
if (!
|
|
3625
|
-
staticRoot =
|
|
4874
|
+
let staticRoot = path19.resolve(__dirname4, "../../../dist/ui");
|
|
4875
|
+
if (!fs19.existsSync(staticRoot)) {
|
|
4876
|
+
staticRoot = path19.resolve(__dirname4, "../../ui");
|
|
3626
4877
|
}
|
|
3627
4878
|
return staticRoot;
|
|
3628
4879
|
}
|
|
3629
4880
|
function prepareWorkingDirectory(pathArg) {
|
|
3630
4881
|
if (!pathArg) return;
|
|
3631
|
-
const targetPath =
|
|
3632
|
-
if (
|
|
3633
|
-
const stats =
|
|
4882
|
+
const targetPath = path19.resolve(process.cwd(), pathArg);
|
|
4883
|
+
if (fs19.existsSync(targetPath)) {
|
|
4884
|
+
const stats = fs19.statSync(targetPath);
|
|
3634
4885
|
if (stats.isFile()) {
|
|
3635
|
-
process.chdir(
|
|
4886
|
+
process.chdir(path19.dirname(targetPath));
|
|
3636
4887
|
return;
|
|
3637
4888
|
}
|
|
3638
4889
|
if (stats.isDirectory()) {
|
|
@@ -3640,13 +4891,13 @@ function prepareWorkingDirectory(pathArg) {
|
|
|
3640
4891
|
return;
|
|
3641
4892
|
}
|
|
3642
4893
|
}
|
|
3643
|
-
if (
|
|
3644
|
-
const dir =
|
|
3645
|
-
|
|
4894
|
+
if (path19.extname(pathArg)) {
|
|
4895
|
+
const dir = path19.dirname(targetPath);
|
|
4896
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
3646
4897
|
process.chdir(dir);
|
|
3647
4898
|
return;
|
|
3648
4899
|
}
|
|
3649
|
-
|
|
4900
|
+
fs19.mkdirSync(targetPath, { recursive: true });
|
|
3650
4901
|
process.chdir(targetPath);
|
|
3651
4902
|
}
|
|
3652
4903
|
function createStoreFromOptions(options) {
|
|
@@ -3666,7 +4917,7 @@ var tasteCommand = new Command12("taste").description("Launch and automate the T
|
|
|
3666
4917
|
const store = createStoreFromOptions(options);
|
|
3667
4918
|
await store.ensureWorkspace();
|
|
3668
4919
|
const staticRoot = resolveStaticRoot();
|
|
3669
|
-
if (!
|
|
4920
|
+
if (!fs19.existsSync(staticRoot)) {
|
|
3670
4921
|
console.warn(`Warning: UI assets not found at ${staticRoot}. Did you run 'bun run build'?`);
|
|
3671
4922
|
}
|
|
3672
4923
|
const port = parseInt(options.port, 10);
|
|
@@ -3891,13 +5142,140 @@ function createFeedbackCommand(telemetry2) {
|
|
|
3891
5142
|
});
|
|
3892
5143
|
}
|
|
3893
5144
|
|
|
5145
|
+
// src/cli/commands/project.ts
|
|
5146
|
+
import { Command as Command14 } from "commander";
|
|
5147
|
+
var projectCommand = new Command14("project").description("Manage projects");
|
|
5148
|
+
projectCommand.command("create").description("Create a project with a default scene").requiredOption("--name <name>", "Project name").option("--root-path <path>", "Project root path", process.cwd()).option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
5149
|
+
try {
|
|
5150
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5151
|
+
if (apiUrl) {
|
|
5152
|
+
const client = new SceneApiClient(apiUrl);
|
|
5153
|
+
const created = await client.createProject({
|
|
5154
|
+
name: String(options.name),
|
|
5155
|
+
rootPath: String(options.rootPath || process.cwd())
|
|
5156
|
+
});
|
|
5157
|
+
writeFlowState({ projectId: created.project.id, sceneId: created.defaultScene.id, apiUrl });
|
|
5158
|
+
console.log(JSON.stringify(created, null, 2));
|
|
5159
|
+
} else {
|
|
5160
|
+
const bundle = createSceneStoreBundle("file");
|
|
5161
|
+
const project = await bundle.projects.createProject({
|
|
5162
|
+
name: String(options.name),
|
|
5163
|
+
rootPath: String(options.rootPath || process.cwd())
|
|
5164
|
+
});
|
|
5165
|
+
const defaultScene = await bundle.scenes.createScene(project.id, { name: "Scene 1" });
|
|
5166
|
+
const updated = await bundle.projects.updateProject(project.id, { defaultSceneId: defaultScene.id });
|
|
5167
|
+
writeFlowState({ projectId: updated.id, sceneId: defaultScene.id });
|
|
5168
|
+
console.log(JSON.stringify({ project: updated, defaultScene }, null, 2));
|
|
5169
|
+
}
|
|
5170
|
+
} catch (error) {
|
|
5171
|
+
console.error(`Error: ${error.message}`);
|
|
5172
|
+
process.exit(1);
|
|
5173
|
+
}
|
|
5174
|
+
});
|
|
5175
|
+
projectCommand.command("list").description("List projects").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
5176
|
+
try {
|
|
5177
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5178
|
+
const projects = apiUrl ? await new SceneApiClient(apiUrl).listProjects() : await createSceneStoreBundle("file").projects.listProjects();
|
|
5179
|
+
console.log(JSON.stringify({ projects }, null, 2));
|
|
5180
|
+
} catch (error) {
|
|
5181
|
+
console.error(`Error: ${error.message}`);
|
|
5182
|
+
process.exit(1);
|
|
5183
|
+
}
|
|
5184
|
+
});
|
|
5185
|
+
|
|
5186
|
+
// src/cli/commands/scene.ts
|
|
5187
|
+
import fs20 from "node:fs";
|
|
5188
|
+
import path20 from "node:path";
|
|
5189
|
+
import { Command as Command15 } from "commander";
|
|
5190
|
+
var sceneCommand = new Command15("scene").description("Manage scenes");
|
|
5191
|
+
sceneCommand.command("list").option("--project-id <id>", "Project ID").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
5192
|
+
try {
|
|
5193
|
+
const projectId = resolveProjectId(options.projectId);
|
|
5194
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5195
|
+
const scenes = apiUrl ? await new SceneApiClient(apiUrl).listScenes(projectId) : await createSceneStoreBundle("file").scenes.listScenes(projectId);
|
|
5196
|
+
console.log(JSON.stringify({ scenes }, null, 2));
|
|
5197
|
+
} catch (error) {
|
|
5198
|
+
console.error(`Error: ${error.message}`);
|
|
5199
|
+
process.exit(1);
|
|
5200
|
+
}
|
|
5201
|
+
});
|
|
5202
|
+
sceneCommand.command("create").option("--project-id <id>", "Project ID").option("--name <name>", "Scene name").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
5203
|
+
try {
|
|
5204
|
+
const projectId = resolveProjectId(options.projectId);
|
|
5205
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5206
|
+
const scene = apiUrl ? await new SceneApiClient(apiUrl).createScene(projectId, {
|
|
5207
|
+
name: options.name ? String(options.name) : void 0
|
|
5208
|
+
}) : await createSceneStoreBundle("file").scenes.createScene(projectId, {
|
|
5209
|
+
name: options.name ? String(options.name) : void 0
|
|
5210
|
+
});
|
|
5211
|
+
writeFlowState({ projectId, sceneId: scene.id, apiUrl });
|
|
5212
|
+
console.log(JSON.stringify({ scene }, null, 2));
|
|
5213
|
+
} catch (error) {
|
|
5214
|
+
console.error(`Error: ${error.message}`);
|
|
5215
|
+
process.exit(1);
|
|
5216
|
+
}
|
|
5217
|
+
});
|
|
5218
|
+
sceneCommand.command("import").option("--project-id <id>", "Project ID").requiredOption("--file <path>", "Scene JSON file path").option("--name <name>", "Scene name").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
5219
|
+
try {
|
|
5220
|
+
const filePath = path20.resolve(process.cwd(), String(options.file));
|
|
5221
|
+
if (!fs20.existsSync(filePath)) {
|
|
5222
|
+
throw new Error(`File not found: ${filePath}`);
|
|
5223
|
+
}
|
|
5224
|
+
const parsed = JSON.parse(fs20.readFileSync(filePath, "utf-8"));
|
|
5225
|
+
if (!parsed?.meta || !Array.isArray(parsed?.entities)) {
|
|
5226
|
+
throw new Error("Invalid scene file. Expected { meta, entities }.");
|
|
5227
|
+
}
|
|
5228
|
+
const projectId = resolveProjectId(options.projectId);
|
|
5229
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5230
|
+
const scene = apiUrl ? await new SceneApiClient(apiUrl).createScene(projectId, {
|
|
5231
|
+
name: options.name ? String(options.name) : path20.basename(filePath, path20.extname(filePath)),
|
|
5232
|
+
scene: { meta: parsed.meta, entities: parsed.entities }
|
|
5233
|
+
}) : await createSceneStoreBundle("file").scenes.createScene(projectId, {
|
|
5234
|
+
name: options.name ? String(options.name) : path20.basename(filePath, path20.extname(filePath)),
|
|
5235
|
+
scene: { meta: parsed.meta, entities: parsed.entities }
|
|
5236
|
+
});
|
|
5237
|
+
writeFlowState({ projectId, sceneId: scene.id, apiUrl });
|
|
5238
|
+
console.log(JSON.stringify({ scene }, null, 2));
|
|
5239
|
+
} catch (error) {
|
|
5240
|
+
console.error(`Error: ${error.message}`);
|
|
5241
|
+
process.exit(1);
|
|
5242
|
+
}
|
|
5243
|
+
});
|
|
5244
|
+
sceneCommand.command("export").option("--project-id <id>", "Project ID").option("--scene-id <id>", "Scene ID").requiredOption("--file <path>", "Output file path").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
5245
|
+
try {
|
|
5246
|
+
const projectId = resolveProjectId(options.projectId);
|
|
5247
|
+
const sceneId = resolveSceneId(options.sceneId);
|
|
5248
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5249
|
+
const scene = apiUrl ? await new SceneApiClient(apiUrl).getScene(projectId, sceneId) : await createSceneStoreBundle("file").scenes.getScene(projectId, sceneId);
|
|
5250
|
+
if (!scene) {
|
|
5251
|
+
throw new Error(`Scene not found: ${sceneId}`);
|
|
5252
|
+
}
|
|
5253
|
+
const out = path20.resolve(process.cwd(), String(options.file));
|
|
5254
|
+
fs20.mkdirSync(path20.dirname(out), { recursive: true });
|
|
5255
|
+
fs20.writeFileSync(out, JSON.stringify({ meta: scene.meta, entities: scene.entities }, null, 2), "utf-8");
|
|
5256
|
+
writeFlowState({ projectId, sceneId, apiUrl });
|
|
5257
|
+
console.log(`Scene exported to ${out}`);
|
|
5258
|
+
} catch (error) {
|
|
5259
|
+
console.error(`Error: ${error.message}`);
|
|
5260
|
+
process.exit(1);
|
|
5261
|
+
}
|
|
5262
|
+
});
|
|
5263
|
+
sceneCommand.command("use").description("Set current flow state for project/scene").requiredOption("--project-id <id>", "Project ID").option("--scene-id <id>", "Scene ID").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action((options) => {
|
|
5264
|
+
const next = writeFlowState({
|
|
5265
|
+
projectId: String(options.projectId),
|
|
5266
|
+
sceneId: options.sceneId ? String(options.sceneId) : void 0,
|
|
5267
|
+
apiUrl: options.apiUrl ? String(options.apiUrl) : void 0
|
|
5268
|
+
});
|
|
5269
|
+
console.log(JSON.stringify({ session: next }, null, 2));
|
|
5270
|
+
});
|
|
5271
|
+
|
|
3894
5272
|
// src/cli/index.ts
|
|
3895
5273
|
var __filename5 = fileURLToPath5(import.meta.url);
|
|
3896
|
-
var __dirname5 =
|
|
3897
|
-
var program = new
|
|
5274
|
+
var __dirname5 = path21.dirname(__filename5);
|
|
5275
|
+
var program = new Command16();
|
|
3898
5276
|
var telemetry = new PostHogTelemetryService();
|
|
3899
5277
|
var commandStartTimes = /* @__PURE__ */ new WeakMap();
|
|
3900
|
-
program.name("feedeas").description("CLI for Feedeas - AI-native video creation tool").version("0.1.0-alpha.
|
|
5278
|
+
program.name("feedeas").description("CLI for Feedeas - AI-native video creation tool").version("0.1.0-alpha.18");
|
|
3901
5279
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
3902
5280
|
commandStartTimes.set(actionCommand, Date.now());
|
|
3903
5281
|
telemetry.trackCommandStarted(actionCommand);
|
|
@@ -3906,57 +5284,49 @@ program.hook("postAction", (_thisCommand, actionCommand) => {
|
|
|
3906
5284
|
const startedAt = commandStartTimes.get(actionCommand) || Date.now();
|
|
3907
5285
|
telemetry.trackCommandFinished(actionCommand, "success", Date.now() - startedAt);
|
|
3908
5286
|
});
|
|
3909
|
-
program.command("edit
|
|
5287
|
+
program.command("edit").alias("start").alias("init").description("Start the Feedeas editor server for a project/scene.").option("-p, --port <number>", "Port to run on", "3331").option("--project-id <id>", "Project ID to open").option("--scene-id <id>", "Scene ID to open").option("--no-open", "Do not open browser").action(async (options) => {
|
|
3910
5288
|
const port = parseInt(options.port);
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
if (stats.isFile()) {
|
|
3917
|
-
sceneFile = path18.basename(targetPath);
|
|
3918
|
-
const dir = path18.dirname(targetPath);
|
|
3919
|
-
process.chdir(dir);
|
|
3920
|
-
console.log(`Opening scene file: ${sceneFile}`);
|
|
3921
|
-
} else if (stats.isDirectory()) {
|
|
3922
|
-
process.chdir(targetPath);
|
|
3923
|
-
}
|
|
3924
|
-
} else {
|
|
3925
|
-
if (pathArg.endsWith(".json")) {
|
|
3926
|
-
sceneFile = path18.basename(pathArg);
|
|
3927
|
-
const dir = path18.dirname(path18.resolve(process.cwd(), pathArg));
|
|
3928
|
-
if (!fs17.existsSync(dir)) {
|
|
3929
|
-
console.log(`Creating directory ${dir}...`);
|
|
3930
|
-
fs17.mkdirSync(dir, { recursive: true });
|
|
3931
|
-
}
|
|
3932
|
-
process.chdir(dir);
|
|
3933
|
-
console.log(`Will create new scene file: ${sceneFile}`);
|
|
3934
|
-
} else {
|
|
3935
|
-
console.log(`Creating directory ${targetPath}...`);
|
|
3936
|
-
fs17.mkdirSync(targetPath, { recursive: true });
|
|
3937
|
-
process.chdir(targetPath);
|
|
3938
|
-
}
|
|
5289
|
+
const projectId = options.projectId ? resolveProjectId(options.projectId) : (() => {
|
|
5290
|
+
try {
|
|
5291
|
+
return resolveProjectId();
|
|
5292
|
+
} catch {
|
|
5293
|
+
return void 0;
|
|
3939
5294
|
}
|
|
3940
|
-
}
|
|
5295
|
+
})();
|
|
5296
|
+
const sceneId = options.sceneId ? resolveSceneId(options.sceneId) : (() => {
|
|
5297
|
+
try {
|
|
5298
|
+
return resolveSceneId();
|
|
5299
|
+
} catch {
|
|
5300
|
+
return void 0;
|
|
5301
|
+
}
|
|
5302
|
+
})();
|
|
3941
5303
|
const cwd = process.cwd();
|
|
3942
5304
|
console.log(`Starting Feedeas in ${cwd}...`);
|
|
3943
|
-
let staticRoot =
|
|
3944
|
-
if (!
|
|
3945
|
-
staticRoot =
|
|
5305
|
+
let staticRoot = path21.resolve(__dirname5, "../../dist/ui");
|
|
5306
|
+
if (!fs21.existsSync(staticRoot)) {
|
|
5307
|
+
staticRoot = path21.resolve(__dirname5, "../ui");
|
|
3946
5308
|
}
|
|
3947
|
-
if (!
|
|
5309
|
+
if (!fs21.existsSync(staticRoot)) {
|
|
3948
5310
|
console.warn(`Warning: UI assets not found at ${staticRoot}. Did you run 'bun run build'?`);
|
|
3949
5311
|
}
|
|
3950
5312
|
const app2 = createServer(staticRoot);
|
|
3951
5313
|
startServer(app2, port);
|
|
3952
|
-
const
|
|
5314
|
+
const params = new URLSearchParams();
|
|
5315
|
+
if (projectId) params.set("projectId", String(projectId));
|
|
5316
|
+
if (sceneId) params.set("sceneId", String(sceneId));
|
|
5317
|
+
const url = `http://localhost:${port}/editor${params.toString() ? `?${params.toString()}` : ""}`;
|
|
3953
5318
|
console.log(`Server running at ${url}`);
|
|
3954
5319
|
if (options.open) {
|
|
3955
5320
|
await open2(url);
|
|
3956
5321
|
}
|
|
5322
|
+
if (projectId || sceneId) {
|
|
5323
|
+
writeFlowState({ projectId, sceneId });
|
|
5324
|
+
}
|
|
3957
5325
|
});
|
|
3958
|
-
program.command("snap").alias("screenshot").description("Take a snapshot of the canvas at a specific time").argument("<time>", "Time in seconds").option("-o, --output <path>", "Output file path", "snapshot.png").option("-u, --url <url>", "URL of the running server", "http://localhost:3331").option("-W, --width <number>", "Viewport width", "1080").option("-H, --height <number>", "Viewport height", "1350").action(async (time, options) => {
|
|
5326
|
+
program.command("snap").alias("screenshot").description("Take a snapshot of the canvas at a specific time").argument("<time>", "Time in seconds").option("-o, --output <path>", "Output file path", "snapshot.png").option("-u, --url <url>", "URL of the running server", "http://localhost:3331").option("--project-id <id>", "Project ID").option("--scene-id <id>", "Scene ID").option("-W, --width <number>", "Viewport width", "1080").option("-H, --height <number>", "Viewport height", "1350").action(async (time, options) => {
|
|
3959
5327
|
console.log(`Taking snapshot at ${time}s...`);
|
|
5328
|
+
const projectId = resolveProjectId(options.projectId);
|
|
5329
|
+
const sceneId = resolveSceneId(options.sceneId);
|
|
3960
5330
|
try {
|
|
3961
5331
|
const { chromium: chromium2 } = await import("playwright-core");
|
|
3962
5332
|
let browser;
|
|
@@ -3985,7 +5355,7 @@ program.command("snap").alias("screenshot").description("Take a snapshot of the
|
|
|
3985
5355
|
const width = parseInt(options.width);
|
|
3986
5356
|
const height = parseInt(options.height);
|
|
3987
5357
|
await page.setViewportSize({ width, height });
|
|
3988
|
-
const targetUrl = `${options.url}?time=${time}&mode=render`;
|
|
5358
|
+
const targetUrl = `${options.url}?time=${time}&mode=render&projectId=${encodeURIComponent(projectId)}&sceneId=${encodeURIComponent(sceneId)}`;
|
|
3989
5359
|
console.log(`Navigating to ${targetUrl}...`);
|
|
3990
5360
|
await page.goto(targetUrl);
|
|
3991
5361
|
try {
|
|
@@ -3999,6 +5369,7 @@ program.command("snap").alias("screenshot").description("Take a snapshot of the
|
|
|
3999
5369
|
const file = options.output;
|
|
4000
5370
|
await page.locator("canvas").first().screenshot({ path: file });
|
|
4001
5371
|
console.log(`Snapshot saved to ${file}`);
|
|
5372
|
+
writeFlowState({ projectId, sceneId });
|
|
4002
5373
|
await browser.close();
|
|
4003
5374
|
} catch (e) {
|
|
4004
5375
|
console.error("Failed to take snapshot:", e.message);
|
|
@@ -4019,16 +5390,18 @@ program.addCommand(createSceneCommand);
|
|
|
4019
5390
|
program.addCommand(addEntityCommand);
|
|
4020
5391
|
program.addCommand(tasteCommand);
|
|
4021
5392
|
program.addCommand(createFeedbackCommand(telemetry));
|
|
5393
|
+
program.addCommand(projectCommand);
|
|
5394
|
+
program.addCommand(sceneCommand);
|
|
4022
5395
|
program.addHelpText("after", `
|
|
4023
5396
|
|
|
4024
5397
|
Agent tip:
|
|
4025
5398
|
feedeas feedback "<what to improve>"
|
|
4026
5399
|
|
|
4027
5400
|
Quick Start (30 seconds):
|
|
4028
|
-
1. feedeas
|
|
4029
|
-
2. feedeas
|
|
4030
|
-
3. feedeas edit
|
|
4031
|
-
4. feedeas record --project scene
|
|
5401
|
+
1. feedeas project create --name "My Project" > project.json
|
|
5402
|
+
2. feedeas scene list --project-id <projectId>
|
|
5403
|
+
3. feedeas edit --project-id <projectId> --scene-id <sceneId>
|
|
5404
|
+
4. feedeas record --project-id <projectId> --scene-id <sceneId> # Render video directly
|
|
4032
5405
|
|
|
4033
5406
|
Guided Flow (Agent-Friendly, End-to-End):
|
|
4034
5407
|
1. mkdir -p my-reel && cd my-reel
|
|
@@ -4053,8 +5426,8 @@ Agent Conversation Example:
|
|
|
4053
5426
|
Agent (tool): feedeas audio "Life changes in small daily choices. A morning ritual of mindfulness can set the tone for your day..." -o narration.mp3 --no-transcribe
|
|
4054
5427
|
Agent: Now I'll generate some visuals to match the narration.
|
|
4055
5428
|
Agent (tool): feedeas example documentary > scene.json
|
|
4056
|
-
Agent (tool): feedeas imagine "
|
|
4057
|
-
Agent (tool): feedeas imagine "A person meditating in a park
|
|
5429
|
+
Agent (tool): feedeas imagine "A peaceful sunrise over a mountain lake, cinematic lighting" --aspect-ratio 9:16 -o scene1.png
|
|
5430
|
+
Agent (tool): feedeas imagine "A person meditating in a park, soft morning light" --aspect-ratio 9:16 -o scene2.png
|
|
4058
5431
|
Agent (tool): feedeas audio "Life changes in small daily choices..." -o narration.mp3 --no-transcribe
|
|
4059
5432
|
Agent (tool): feedeas bgm "soft reflective ambient, no vocals..." -d 25 -o bgm.mp3
|
|
4060
5433
|
Agent: Now I\u2019ll wire assets into scene.json with src values under assets/.
|
|
@@ -4113,7 +5486,7 @@ Transition Types:
|
|
|
4113
5486
|
|
|
4114
5487
|
Project Structure:
|
|
4115
5488
|
your-project/
|
|
4116
|
-
\u251C\u2500\u2500
|
|
5489
|
+
\u251C\u2500\u2500 .feedeas/ # Project + scene resource storage (default file backend)
|
|
4117
5490
|
\u251C\u2500\u2500 assets/ # Asset folder (images, audio)
|
|
4118
5491
|
\u2502 \u251C\u2500\u2500 image.jpg
|
|
4119
5492
|
\u2502 \u251C\u2500\u2500 music.mp3
|
|
@@ -4126,14 +5499,14 @@ Programmatic Workflow (for Agents):
|
|
|
4126
5499
|
$ feedeas audio "narration text" -o assets/voice.mp3 # Generate audio
|
|
4127
5500
|
$ feedeas bgm "cinematic bgm, no vocals" -o assets/bgm.mp3
|
|
4128
5501
|
$ feedeas example documentary # See a complete example
|
|
4129
|
-
$ feedeas set-scene
|
|
4130
|
-
$ feedeas validate
|
|
5502
|
+
$ feedeas set-scene --project-id <pid> --scene-id <sid> --content '{...}'
|
|
5503
|
+
$ feedeas validate --project-id <pid> --scene-id <sid> # Validate before rendering
|
|
4131
5504
|
$ feedeas edit --no-open & # Start server in background
|
|
4132
|
-
$ feedeas record --project
|
|
5505
|
+
$ feedeas record --project-id <pid> --scene-id <sid> # Render to video
|
|
4133
5506
|
|
|
4134
5507
|
Interactive Workflow (for Humans):
|
|
4135
|
-
$ feedeas edit
|
|
4136
|
-
$ feedeas snap 2.5 -
|
|
5508
|
+
$ feedeas edit --project-id <pid> --scene-id <sid> # Start editor
|
|
5509
|
+
$ feedeas snap 2.5 --project-id <pid> --scene-id <sid> -o frame.png
|
|
4137
5510
|
|
|
4138
5511
|
Taste Workflow:
|
|
4139
5512
|
$ feedeas taste # Start UI Workspace
|