feedeas 0.1.0-alpha.15 → 0.1.0-alpha.17
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,15 +1179,132 @@ ${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;
|
|
743
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
|
+
}
|
|
1282
|
+
};
|
|
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);
|
|
744
1308
|
}
|
|
745
1309
|
async function simulateIdeas(params) {
|
|
746
1310
|
const model = params.model || "gemini-2.5-flash";
|
|
@@ -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"
|
|
@@ -1482,26 +2471,167 @@ var createServer = (staticRoot) => {
|
|
|
1482
2471
|
} catch (e) {
|
|
1483
2472
|
}
|
|
1484
2473
|
}
|
|
1485
|
-
return c.notFound();
|
|
1486
|
-
});
|
|
1487
|
-
return app2;
|
|
2474
|
+
return c.notFound();
|
|
2475
|
+
});
|
|
2476
|
+
return app2;
|
|
2477
|
+
};
|
|
2478
|
+
|
|
2479
|
+
// src/cli/server/runtime.ts
|
|
2480
|
+
import { serve } from "@hono/node-server";
|
|
2481
|
+
function startServer(app2, port) {
|
|
2482
|
+
const server = serve({
|
|
2483
|
+
fetch: app2.fetch,
|
|
2484
|
+
port
|
|
2485
|
+
});
|
|
2486
|
+
return {
|
|
2487
|
+
stop: () => server.close()
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
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
|
+
}
|
|
1488
2581
|
};
|
|
1489
2582
|
|
|
1490
|
-
// src/cli/
|
|
1491
|
-
import
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
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()
|
|
1499
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;
|
|
1500
2630
|
}
|
|
1501
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);
|
|
1780
2929
|
return true;
|
|
1781
|
-
} else {
|
|
1782
|
-
console.warn(`Asset not found: ${assetPath} (orig: ${e.src})`);
|
|
1783
|
-
return false;
|
|
1784
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;
|
|
2934
|
+
return true;
|
|
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,110 @@ 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
|
+
async function convertWithNodeFfmpeg(pcmPath, outputPath) {
|
|
3848
|
+
await new Promise((resolve, reject) => {
|
|
3849
|
+
const ffmpeg = spawn6("ffmpeg", [
|
|
3850
|
+
"-f",
|
|
3851
|
+
"s16le",
|
|
3852
|
+
"-ar",
|
|
3853
|
+
"24000",
|
|
3854
|
+
"-ac",
|
|
3855
|
+
"1",
|
|
3856
|
+
"-i",
|
|
3857
|
+
pcmPath,
|
|
3858
|
+
"-y",
|
|
3859
|
+
outputPath
|
|
3860
|
+
], { stdio: "inherit" });
|
|
3861
|
+
ffmpeg.on("close", (code) => {
|
|
3862
|
+
if (code === 0) resolve();
|
|
3863
|
+
else reject(new Error(`FFmpeg exited with code ${code}`));
|
|
3864
|
+
});
|
|
3865
|
+
ffmpeg.on("error", (err) => reject(err));
|
|
3866
|
+
});
|
|
3867
|
+
}
|
|
3868
|
+
async function convertWithWasm(pcmPath, outputPath) {
|
|
3869
|
+
const ffmpeg = new FFmpeg();
|
|
3870
|
+
const baseDir = typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : ".";
|
|
3871
|
+
const coreDir = path14.resolve(baseDir, "node_modules/@ffmpeg/core/dist");
|
|
3872
|
+
const coreURL = pathToFileURL(path14.join(coreDir, "ffmpeg-core.js")).toString();
|
|
3873
|
+
const wasmURL = pathToFileURL(path14.join(coreDir, "ffmpeg-core.wasm")).toString();
|
|
3874
|
+
const workerURL = pathToFileURL(path14.join(coreDir, "ffmpeg-core.worker.js")).toString();
|
|
3875
|
+
await ffmpeg.load({ coreURL, wasmURL, workerURL });
|
|
3876
|
+
const pcmBytes = await fs15.promises.readFile(pcmPath);
|
|
3877
|
+
await ffmpeg.writeFile("input.pcm", pcmBytes);
|
|
3878
|
+
await ffmpeg.exec([
|
|
3879
|
+
"-f",
|
|
3880
|
+
"s16le",
|
|
3881
|
+
"-ar",
|
|
3882
|
+
"24000",
|
|
3883
|
+
"-ac",
|
|
3884
|
+
"1",
|
|
3885
|
+
"-i",
|
|
3886
|
+
"input.pcm",
|
|
3887
|
+
"-codec:a",
|
|
3888
|
+
"libmp3lame",
|
|
3889
|
+
"-b:a",
|
|
3890
|
+
"128k",
|
|
3891
|
+
"output.mp3"
|
|
3892
|
+
]);
|
|
3893
|
+
const output = await ffmpeg.readFile("output.mp3");
|
|
3894
|
+
await fs15.promises.writeFile(outputPath, output);
|
|
3895
|
+
}
|
|
3896
|
+
async function convertPcmToMp3(options) {
|
|
3897
|
+
const { pcmPath, outputPath } = options;
|
|
3898
|
+
let useWasm = !isNodeRuntime();
|
|
3899
|
+
try {
|
|
3900
|
+
try {
|
|
3901
|
+
if (!useWasm) {
|
|
3902
|
+
await convertWithNodeFfmpeg(pcmPath, outputPath);
|
|
3903
|
+
return;
|
|
3904
|
+
}
|
|
3905
|
+
} catch (err) {
|
|
3906
|
+
if (err?.code !== "ENOENT") {
|
|
3907
|
+
throw err;
|
|
3908
|
+
}
|
|
3909
|
+
useWasm = true;
|
|
3910
|
+
}
|
|
3911
|
+
if (useWasm) {
|
|
3912
|
+
try {
|
|
3913
|
+
await convertWithWasm(pcmPath, outputPath);
|
|
3914
|
+
return;
|
|
3915
|
+
} catch (err) {
|
|
3916
|
+
throw new Error(
|
|
3917
|
+
`Audio conversion failed. Install ffmpeg or ensure @ffmpeg/core assets are present for wasm conversion. (${err?.message || err})`
|
|
3918
|
+
);
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
throw new Error(
|
|
3922
|
+
"Audio conversion failed. Install ffmpeg or ensure @ffmpeg/core assets are present for wasm conversion."
|
|
3923
|
+
);
|
|
3924
|
+
} finally {
|
|
3925
|
+
if (fs15.existsSync(pcmPath)) {
|
|
3926
|
+
try {
|
|
3927
|
+
await fs15.promises.unlink(pcmPath);
|
|
3928
|
+
} catch {
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
|
|
2644
3934
|
// src/cli/commands/audio.ts
|
|
2645
3935
|
var audioCommand = new Command6("generate:audio");
|
|
2646
3936
|
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
3937
|
try {
|
|
2648
3938
|
let text;
|
|
2649
|
-
if (
|
|
3939
|
+
if (fs16.existsSync(textOrFile)) {
|
|
2650
3940
|
console.log(`\u{1F4D6} Reading text from file: ${textOrFile}`);
|
|
2651
|
-
text =
|
|
3941
|
+
text = fs16.readFileSync(textOrFile, "utf-8").trim();
|
|
2652
3942
|
} else {
|
|
2653
3943
|
text = textOrFile;
|
|
2654
3944
|
}
|
|
@@ -2663,41 +3953,22 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
|
|
|
2663
3953
|
}
|
|
2664
3954
|
console.log("\u{1F5E3}\uFE0F Generating speech with Gemini...");
|
|
2665
3955
|
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
|
-
}
|
|
3956
|
+
const outputPath = resolveAssetOutputPath(options.output);
|
|
3957
|
+
const tempPcmPath = path15.join(path15.dirname(outputPath), `temp_${Date.now()}.pcm`);
|
|
3958
|
+
fs16.writeFileSync(tempPcmPath, audioBuffer);
|
|
3959
|
+
console.log(`\u{1F504} Converting raw PCM to ${path15.extname(outputPath)}...`);
|
|
3960
|
+
await convertPcmToMp3({
|
|
3961
|
+
pcmPath: tempPcmPath,
|
|
3962
|
+
outputPath
|
|
3963
|
+
});
|
|
3964
|
+
console.log(`\u2705 Audio saved: ${outputPath}`);
|
|
2694
3965
|
if (options.transcribe) {
|
|
2695
3966
|
try {
|
|
2696
3967
|
console.log("\u{1F50D} Aligning audio with Whisper...");
|
|
2697
3968
|
await WhisperService.ensureReady();
|
|
2698
3969
|
const metadata = await WhisperService.transcribe(outputPath);
|
|
2699
|
-
const metaPath = outputPath.replace(
|
|
2700
|
-
|
|
3970
|
+
const metaPath = outputPath.replace(path15.extname(outputPath), ".json");
|
|
3971
|
+
fs16.writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
|
|
2701
3972
|
console.log(`\u2705 Metadata saved: ${metaPath}`);
|
|
2702
3973
|
} catch (wErr) {
|
|
2703
3974
|
console.warn("\u26A0\uFE0F Whisper alignment failed:", wErr.message);
|
|
@@ -2708,12 +3979,6 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
|
|
|
2708
3979
|
process.exit(1);
|
|
2709
3980
|
}
|
|
2710
3981
|
});
|
|
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
3982
|
async function generateGeminiAudio(text, apiKey, voiceName) {
|
|
2718
3983
|
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent`;
|
|
2719
3984
|
const requestBody = {
|
|
@@ -2753,9 +4018,9 @@ async function generateGeminiAudio(text, apiKey, voiceName) {
|
|
|
2753
4018
|
|
|
2754
4019
|
// src/cli/commands/bgm.ts
|
|
2755
4020
|
import { Command as Command7 } from "commander";
|
|
2756
|
-
import
|
|
2757
|
-
import
|
|
2758
|
-
import { spawn as
|
|
4021
|
+
import fs17 from "fs";
|
|
4022
|
+
import path16 from "path";
|
|
4023
|
+
import { spawn as spawn7 } from "child_process";
|
|
2759
4024
|
var bgmCommand = new Command7("generate:bgm");
|
|
2760
4025
|
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
4026
|
try {
|
|
@@ -2766,7 +4031,7 @@ bgmCommand.alias("bgm").alias("music").description("Generate background music wi
|
|
|
2766
4031
|
}
|
|
2767
4032
|
const durationSec = parseDuration(options.duration);
|
|
2768
4033
|
const seed = parseSeed(options.seed);
|
|
2769
|
-
const outputPath =
|
|
4034
|
+
const outputPath = resolveAssetOutputPath(options.output);
|
|
2770
4035
|
const format = inferOutputFormat(outputPath);
|
|
2771
4036
|
const request = {
|
|
2772
4037
|
provider: "gemini",
|
|
@@ -2806,7 +4071,7 @@ bgmCommand.alias("bgm").alias("music").description("Generate background music wi
|
|
|
2806
4071
|
}
|
|
2807
4072
|
});
|
|
2808
4073
|
function resolvePrompt(promptOrFile) {
|
|
2809
|
-
const prompt =
|
|
4074
|
+
const prompt = fs17.existsSync(promptOrFile) ? fs17.readFileSync(promptOrFile, "utf-8").trim() : String(promptOrFile).trim();
|
|
2810
4075
|
if (!prompt) {
|
|
2811
4076
|
throw new Error("Prompt cannot be empty");
|
|
2812
4077
|
}
|
|
@@ -2827,14 +4092,8 @@ function parseSeed(input) {
|
|
|
2827
4092
|
}
|
|
2828
4093
|
return value;
|
|
2829
4094
|
}
|
|
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
4095
|
function inferOutputFormat(outputPath) {
|
|
2837
|
-
const ext =
|
|
4096
|
+
const ext = path16.extname(outputPath).toLowerCase();
|
|
2838
4097
|
if (ext === ".wav") return "wav";
|
|
2839
4098
|
if (ext === ".mp3" || !ext) return "mp3";
|
|
2840
4099
|
throw new Error("Invalid output format. Use .mp3 or .wav.");
|
|
@@ -2922,14 +4181,14 @@ async function generateGeminiMusicPcm(request) {
|
|
|
2922
4181
|
});
|
|
2923
4182
|
}
|
|
2924
4183
|
async function convertPcmToOutput(pcmBuffer, outputPath, format, durationSec) {
|
|
2925
|
-
const outputDir =
|
|
2926
|
-
if (!
|
|
2927
|
-
const tempPcmPath =
|
|
2928
|
-
|
|
4184
|
+
const outputDir = path16.dirname(outputPath);
|
|
4185
|
+
if (!fs17.existsSync(outputDir)) fs17.mkdirSync(outputDir, { recursive: true });
|
|
4186
|
+
const tempPcmPath = path16.join(outputDir, `temp_bgm_${Date.now()}.pcm`);
|
|
4187
|
+
fs17.writeFileSync(tempPcmPath, pcmBuffer);
|
|
2929
4188
|
const codecArgs = format === "wav" ? ["-c:a", "pcm_s16le"] : ["-c:a", "libmp3lame", "-q:a", "2"];
|
|
2930
4189
|
try {
|
|
2931
4190
|
await new Promise((resolve, reject) => {
|
|
2932
|
-
const ffmpeg =
|
|
4191
|
+
const ffmpeg = spawn7("ffmpeg", [
|
|
2933
4192
|
"-f",
|
|
2934
4193
|
"s16le",
|
|
2935
4194
|
"-ar",
|
|
@@ -2951,25 +4210,25 @@ async function convertPcmToOutput(pcmBuffer, outputPath, format, durationSec) {
|
|
|
2951
4210
|
ffmpeg.on("error", reject);
|
|
2952
4211
|
});
|
|
2953
4212
|
} finally {
|
|
2954
|
-
if (
|
|
4213
|
+
if (fs17.existsSync(tempPcmPath)) fs17.unlinkSync(tempPcmPath);
|
|
2955
4214
|
}
|
|
2956
4215
|
}
|
|
2957
4216
|
|
|
2958
4217
|
// src/cli/commands/asset.ts
|
|
2959
4218
|
import { Command as Command8 } from "commander";
|
|
2960
|
-
import
|
|
2961
|
-
import
|
|
2962
|
-
import { spawn as
|
|
4219
|
+
import fs18 from "fs";
|
|
4220
|
+
import path17 from "path";
|
|
4221
|
+
import { spawn as spawn8 } from "child_process";
|
|
2963
4222
|
var assetCommand = new Command8("asset").description("Asset information and management");
|
|
2964
4223
|
assetCommand.command("info <file>").description("Show detailed information about an asset").action(async (file) => {
|
|
2965
|
-
const assetPath =
|
|
2966
|
-
if (!
|
|
4224
|
+
const assetPath = path17.resolve(process.cwd(), "assets", file);
|
|
4225
|
+
if (!fs18.existsSync(assetPath)) {
|
|
2967
4226
|
console.error(`Asset not found: ${file}`);
|
|
2968
4227
|
console.error(`Looked in: ${assetPath}`);
|
|
2969
4228
|
process.exit(1);
|
|
2970
4229
|
}
|
|
2971
|
-
const stats =
|
|
2972
|
-
const ext =
|
|
4230
|
+
const stats = fs18.statSync(assetPath);
|
|
4231
|
+
const ext = path17.extname(file).toLowerCase();
|
|
2973
4232
|
console.log(`
|
|
2974
4233
|
Asset: ${file}`);
|
|
2975
4234
|
console.log(`Size: ${formatBytes2(stats.size)}`);
|
|
@@ -2992,11 +4251,11 @@ Asset: ${file}`);
|
|
|
2992
4251
|
console.log(`Channels: ${audioInfo.channels}`);
|
|
2993
4252
|
console.log(`Bitrate: ${audioInfo.bitrate}`);
|
|
2994
4253
|
const metadataFile = file.replace(ext, ".json");
|
|
2995
|
-
const metadataPath =
|
|
2996
|
-
if (
|
|
4254
|
+
const metadataPath = path17.resolve(process.cwd(), "assets", metadataFile);
|
|
4255
|
+
if (fs18.existsSync(metadataPath)) {
|
|
2997
4256
|
console.log(`
|
|
2998
4257
|
Metadata: ${metadataFile}`);
|
|
2999
|
-
const metadata = JSON.parse(
|
|
4258
|
+
const metadata = JSON.parse(fs18.readFileSync(metadataPath, "utf-8"));
|
|
3000
4259
|
if (Array.isArray(metadata.words) && metadata.words.length > 0) {
|
|
3001
4260
|
console.log(`Word timings: ${metadata.words.length} words`);
|
|
3002
4261
|
const firstWord = metadata.words[0];
|
|
@@ -3033,7 +4292,8 @@ function getAssetType(ext) {
|
|
|
3033
4292
|
async function getImageDimensions(filePath) {
|
|
3034
4293
|
try {
|
|
3035
4294
|
const imageSize = await import("image-size");
|
|
3036
|
-
const
|
|
4295
|
+
const buffer = fs18.readFileSync(filePath);
|
|
4296
|
+
const dimensions = imageSize.default(buffer);
|
|
3037
4297
|
return { width: dimensions.width || 0, height: dimensions.height || 0 };
|
|
3038
4298
|
} catch (err) {
|
|
3039
4299
|
throw new Error("image-size package not available");
|
|
@@ -3041,7 +4301,7 @@ async function getImageDimensions(filePath) {
|
|
|
3041
4301
|
}
|
|
3042
4302
|
async function getAudioInfo(filePath) {
|
|
3043
4303
|
return new Promise((resolve, reject) => {
|
|
3044
|
-
const ffprobe =
|
|
4304
|
+
const ffprobe = spawn8("ffprobe", [
|
|
3045
4305
|
"-v",
|
|
3046
4306
|
"error",
|
|
3047
4307
|
"-show_entries",
|
|
@@ -3080,9 +4340,9 @@ async function getAudioInfo(filePath) {
|
|
|
3080
4340
|
|
|
3081
4341
|
// src/cli/index.ts
|
|
3082
4342
|
import open2 from "open";
|
|
3083
|
-
import
|
|
4343
|
+
import path21 from "path";
|
|
3084
4344
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
3085
|
-
import
|
|
4345
|
+
import fs21 from "fs";
|
|
3086
4346
|
|
|
3087
4347
|
// src/cli/commands/example.ts
|
|
3088
4348
|
import { Command as Command9 } from "commander";
|
|
@@ -3375,72 +4635,42 @@ function printSchema(name, schema) {
|
|
|
3375
4635
|
|
|
3376
4636
|
// src/cli/commands/create-scene.ts
|
|
3377
4637
|
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
4638
|
var createSceneCommand = new Command11("create-scene");
|
|
3423
|
-
createSceneCommand.description("Create
|
|
3424
|
-
const filePath = path15.resolve(process.cwd(), file);
|
|
4639
|
+
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
4640
|
try {
|
|
3426
|
-
const
|
|
3427
|
-
|
|
3428
|
-
|
|
4641
|
+
const projectId = resolveProjectId(options.projectId);
|
|
4642
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
4643
|
+
const scene = apiUrl ? await new SceneApiClient(apiUrl).createScene(projectId, {
|
|
4644
|
+
name: options.name ? String(options.name) : void 0
|
|
4645
|
+
}) : await createSceneStoreBundle("file").scenes.createScene(projectId, {
|
|
4646
|
+
name: options.name ? String(options.name) : void 0
|
|
4647
|
+
});
|
|
4648
|
+
writeFlowState({ projectId, sceneId: scene.id, apiUrl });
|
|
4649
|
+
console.log(`\u2705 Scene created: ${scene.id} (project ${scene.projectId})`);
|
|
3429
4650
|
} catch (error) {
|
|
3430
4651
|
console.error(`\u274C Error creating scene: ${error.message}`);
|
|
3431
4652
|
process.exit(1);
|
|
3432
4653
|
}
|
|
3433
4654
|
});
|
|
3434
4655
|
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
|
-
}
|
|
4656
|
+
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
4657
|
try {
|
|
3442
|
-
const
|
|
4658
|
+
const bundle = createSceneStoreBundle("file");
|
|
4659
|
+
const projectId = resolveProjectId(options.projectId);
|
|
4660
|
+
const sceneId = resolveSceneId(options.sceneId);
|
|
4661
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
4662
|
+
const apiClient = apiUrl ? new SceneApiClient(apiUrl) : null;
|
|
4663
|
+
const project = apiClient ? await apiClient.getProject(projectId) : await bundle.projects.getProject(projectId);
|
|
4664
|
+
if (!project) {
|
|
4665
|
+
throw new Error(`Project not found: ${projectId}`);
|
|
4666
|
+
}
|
|
4667
|
+
const scene = apiClient ? await apiClient.getScene(projectId, sceneId) : await bundle.scenes.getScene(projectId, sceneId);
|
|
4668
|
+
if (!scene) {
|
|
4669
|
+
throw new Error(`Scene not found: ${sceneId}`);
|
|
4670
|
+
}
|
|
3443
4671
|
const type = options.type;
|
|
4672
|
+
const entities = [...scene.entities];
|
|
4673
|
+
const registry = createDefaultAssetResolverRegistry();
|
|
3444
4674
|
let entity = {
|
|
3445
4675
|
type,
|
|
3446
4676
|
visible: true
|
|
@@ -3448,30 +4678,27 @@ addEntityCommand.description("Add an entity to an existing scene file").argument
|
|
|
3448
4678
|
let startTime = parseFloat(options.start);
|
|
3449
4679
|
let duration = parseFloat(options.duration);
|
|
3450
4680
|
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
|
-
}
|
|
4681
|
+
let maxEnd = 0;
|
|
4682
|
+
entities.forEach((e) => {
|
|
4683
|
+
if (typeof e.startTime === "number" && typeof e.duration === "number") {
|
|
4684
|
+
const end = e.startTime + e.duration;
|
|
4685
|
+
if (end > maxEnd) maxEnd = end;
|
|
4686
|
+
}
|
|
4687
|
+
});
|
|
4688
|
+
startTime = maxEnd;
|
|
3464
4689
|
} else if (isNaN(startTime)) {
|
|
3465
4690
|
startTime = 0;
|
|
3466
4691
|
}
|
|
3467
4692
|
if (options.duration === "auto" && options.src) {
|
|
3468
|
-
const
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
4693
|
+
const resolved = registry.resolve(options.src, project.rootPath || process.cwd());
|
|
4694
|
+
if (resolved.type === "local") {
|
|
4695
|
+
try {
|
|
4696
|
+
const metadata = await FFprobeService.getMetadata(resolved.localPath);
|
|
4697
|
+
duration = metadata.duration;
|
|
4698
|
+
} catch {
|
|
4699
|
+
duration = 5;
|
|
4700
|
+
}
|
|
4701
|
+
} else {
|
|
3475
4702
|
duration = 5;
|
|
3476
4703
|
}
|
|
3477
4704
|
} else if (options.duration === "auto") {
|
|
@@ -3498,12 +4725,32 @@ addEntityCommand.description("Add an entity to an existing scene file").argument
|
|
|
3498
4725
|
entity.color = "#ffffff";
|
|
3499
4726
|
entity.x = 540;
|
|
3500
4727
|
entity.y = 960;
|
|
4728
|
+
entity.fontFamily = "Arial";
|
|
4729
|
+
entity.fontWeight = "700";
|
|
4730
|
+
entity.bgColor = "transparent";
|
|
4731
|
+
entity.maxWidth = 900;
|
|
4732
|
+
entity.lineHeight = 1.2;
|
|
4733
|
+
entity.padding = 8;
|
|
4734
|
+
entity.textAlign = "left";
|
|
3501
4735
|
} else {
|
|
3502
4736
|
throw new Error(`Unknown type: ${type}`);
|
|
3503
4737
|
}
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
4738
|
+
if (!entity.id) {
|
|
4739
|
+
const typeCount = entities.filter((e) => e.type === entity.type).length + 1;
|
|
4740
|
+
entity.id = `${entity.type}-${typeCount}`;
|
|
4741
|
+
}
|
|
4742
|
+
entity.name = entity.name || entity.id;
|
|
4743
|
+
entities.push(entity);
|
|
4744
|
+
const updated = apiClient ? await apiClient.updateScene(projectId, sceneId, {
|
|
4745
|
+
entities,
|
|
4746
|
+
meta: scene.meta,
|
|
4747
|
+
version: scene.version
|
|
4748
|
+
}) : await bundle.scenes.updateScene(projectId, sceneId, {
|
|
4749
|
+
entities,
|
|
4750
|
+
meta: scene.meta
|
|
4751
|
+
}, scene.version);
|
|
4752
|
+
writeFlowState({ projectId, sceneId, apiUrl });
|
|
4753
|
+
console.log(`\u2705 Added ${type} entity to scene ${updated.id} at ${startTime}s`);
|
|
3507
4754
|
} catch (error) {
|
|
3508
4755
|
console.error(`\u274C Error adding entity: ${error.message}`);
|
|
3509
4756
|
process.exit(1);
|
|
@@ -3512,7 +4759,7 @@ addEntityCommand.description("Add an entity to an existing scene file").argument
|
|
|
3512
4759
|
|
|
3513
4760
|
// src/cli/services/telemetry.ts
|
|
3514
4761
|
import os2 from "os";
|
|
3515
|
-
import
|
|
4762
|
+
import path18 from "path";
|
|
3516
4763
|
import { createHash as createHash2 } from "crypto";
|
|
3517
4764
|
var POSTHOG_CAPTURE_URL = "https://us.i.posthog.com/capture/";
|
|
3518
4765
|
var TELEMETRY_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
@@ -3524,7 +4771,7 @@ var PostHogTelemetryService = class {
|
|
|
3524
4771
|
if (!this.enabled) return;
|
|
3525
4772
|
this.capture("cli_feedback", {
|
|
3526
4773
|
details: String(details).slice(0, 2e3),
|
|
3527
|
-
cwd_basename:
|
|
4774
|
+
cwd_basename: path18.basename(process.cwd())
|
|
3528
4775
|
});
|
|
3529
4776
|
}
|
|
3530
4777
|
apiKey;
|
|
@@ -3547,7 +4794,7 @@ var PostHogTelemetryService = class {
|
|
|
3547
4794
|
command_name: this.getCommandPath(actionCommand),
|
|
3548
4795
|
command_aliases: actionCommand.aliases(),
|
|
3549
4796
|
options_used: this.getUsedOptionNames(actionCommand),
|
|
3550
|
-
cwd_basename:
|
|
4797
|
+
cwd_basename: path18.basename(process.cwd()),
|
|
3551
4798
|
node_version: process.version,
|
|
3552
4799
|
platform: process.platform,
|
|
3553
4800
|
arch: process.arch
|
|
@@ -3560,7 +4807,7 @@ var PostHogTelemetryService = class {
|
|
|
3560
4807
|
status,
|
|
3561
4808
|
duration_ms: Math.max(0, Math.round(durationMs)),
|
|
3562
4809
|
error_message: errorMessage ? String(errorMessage).slice(0, 300) : void 0,
|
|
3563
|
-
cwd_basename:
|
|
4810
|
+
cwd_basename: path18.basename(process.cwd())
|
|
3564
4811
|
});
|
|
3565
4812
|
}
|
|
3566
4813
|
getCommandPath(command) {
|
|
@@ -3613,26 +4860,26 @@ var PostHogTelemetryService = class {
|
|
|
3613
4860
|
|
|
3614
4861
|
// src/cli/commands/taste.ts
|
|
3615
4862
|
import { Command as Command12 } from "commander";
|
|
3616
|
-
import
|
|
3617
|
-
import
|
|
4863
|
+
import fs19 from "node:fs";
|
|
4864
|
+
import path19 from "node:path";
|
|
3618
4865
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3619
4866
|
import open from "open";
|
|
3620
4867
|
var __filename4 = fileURLToPath4(import.meta.url);
|
|
3621
|
-
var __dirname4 =
|
|
4868
|
+
var __dirname4 = path19.dirname(__filename4);
|
|
3622
4869
|
function resolveStaticRoot() {
|
|
3623
|
-
let staticRoot =
|
|
3624
|
-
if (!
|
|
3625
|
-
staticRoot =
|
|
4870
|
+
let staticRoot = path19.resolve(__dirname4, "../../../dist/ui");
|
|
4871
|
+
if (!fs19.existsSync(staticRoot)) {
|
|
4872
|
+
staticRoot = path19.resolve(__dirname4, "../../ui");
|
|
3626
4873
|
}
|
|
3627
4874
|
return staticRoot;
|
|
3628
4875
|
}
|
|
3629
4876
|
function prepareWorkingDirectory(pathArg) {
|
|
3630
4877
|
if (!pathArg) return;
|
|
3631
|
-
const targetPath =
|
|
3632
|
-
if (
|
|
3633
|
-
const stats =
|
|
4878
|
+
const targetPath = path19.resolve(process.cwd(), pathArg);
|
|
4879
|
+
if (fs19.existsSync(targetPath)) {
|
|
4880
|
+
const stats = fs19.statSync(targetPath);
|
|
3634
4881
|
if (stats.isFile()) {
|
|
3635
|
-
process.chdir(
|
|
4882
|
+
process.chdir(path19.dirname(targetPath));
|
|
3636
4883
|
return;
|
|
3637
4884
|
}
|
|
3638
4885
|
if (stats.isDirectory()) {
|
|
@@ -3640,13 +4887,13 @@ function prepareWorkingDirectory(pathArg) {
|
|
|
3640
4887
|
return;
|
|
3641
4888
|
}
|
|
3642
4889
|
}
|
|
3643
|
-
if (
|
|
3644
|
-
const dir =
|
|
3645
|
-
|
|
4890
|
+
if (path19.extname(pathArg)) {
|
|
4891
|
+
const dir = path19.dirname(targetPath);
|
|
4892
|
+
fs19.mkdirSync(dir, { recursive: true });
|
|
3646
4893
|
process.chdir(dir);
|
|
3647
4894
|
return;
|
|
3648
4895
|
}
|
|
3649
|
-
|
|
4896
|
+
fs19.mkdirSync(targetPath, { recursive: true });
|
|
3650
4897
|
process.chdir(targetPath);
|
|
3651
4898
|
}
|
|
3652
4899
|
function createStoreFromOptions(options) {
|
|
@@ -3666,7 +4913,7 @@ var tasteCommand = new Command12("taste").description("Launch and automate the T
|
|
|
3666
4913
|
const store = createStoreFromOptions(options);
|
|
3667
4914
|
await store.ensureWorkspace();
|
|
3668
4915
|
const staticRoot = resolveStaticRoot();
|
|
3669
|
-
if (!
|
|
4916
|
+
if (!fs19.existsSync(staticRoot)) {
|
|
3670
4917
|
console.warn(`Warning: UI assets not found at ${staticRoot}. Did you run 'bun run build'?`);
|
|
3671
4918
|
}
|
|
3672
4919
|
const port = parseInt(options.port, 10);
|
|
@@ -3891,13 +5138,140 @@ function createFeedbackCommand(telemetry2) {
|
|
|
3891
5138
|
});
|
|
3892
5139
|
}
|
|
3893
5140
|
|
|
5141
|
+
// src/cli/commands/project.ts
|
|
5142
|
+
import { Command as Command14 } from "commander";
|
|
5143
|
+
var projectCommand = new Command14("project").description("Manage projects");
|
|
5144
|
+
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) => {
|
|
5145
|
+
try {
|
|
5146
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5147
|
+
if (apiUrl) {
|
|
5148
|
+
const client = new SceneApiClient(apiUrl);
|
|
5149
|
+
const created = await client.createProject({
|
|
5150
|
+
name: String(options.name),
|
|
5151
|
+
rootPath: String(options.rootPath || process.cwd())
|
|
5152
|
+
});
|
|
5153
|
+
writeFlowState({ projectId: created.project.id, sceneId: created.defaultScene.id, apiUrl });
|
|
5154
|
+
console.log(JSON.stringify(created, null, 2));
|
|
5155
|
+
} else {
|
|
5156
|
+
const bundle = createSceneStoreBundle("file");
|
|
5157
|
+
const project = await bundle.projects.createProject({
|
|
5158
|
+
name: String(options.name),
|
|
5159
|
+
rootPath: String(options.rootPath || process.cwd())
|
|
5160
|
+
});
|
|
5161
|
+
const defaultScene = await bundle.scenes.createScene(project.id, { name: "Scene 1" });
|
|
5162
|
+
const updated = await bundle.projects.updateProject(project.id, { defaultSceneId: defaultScene.id });
|
|
5163
|
+
writeFlowState({ projectId: updated.id, sceneId: defaultScene.id });
|
|
5164
|
+
console.log(JSON.stringify({ project: updated, defaultScene }, null, 2));
|
|
5165
|
+
}
|
|
5166
|
+
} catch (error) {
|
|
5167
|
+
console.error(`Error: ${error.message}`);
|
|
5168
|
+
process.exit(1);
|
|
5169
|
+
}
|
|
5170
|
+
});
|
|
5171
|
+
projectCommand.command("list").description("List projects").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
5172
|
+
try {
|
|
5173
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5174
|
+
const projects = apiUrl ? await new SceneApiClient(apiUrl).listProjects() : await createSceneStoreBundle("file").projects.listProjects();
|
|
5175
|
+
console.log(JSON.stringify({ projects }, null, 2));
|
|
5176
|
+
} catch (error) {
|
|
5177
|
+
console.error(`Error: ${error.message}`);
|
|
5178
|
+
process.exit(1);
|
|
5179
|
+
}
|
|
5180
|
+
});
|
|
5181
|
+
|
|
5182
|
+
// src/cli/commands/scene.ts
|
|
5183
|
+
import fs20 from "node:fs";
|
|
5184
|
+
import path20 from "node:path";
|
|
5185
|
+
import { Command as Command15 } from "commander";
|
|
5186
|
+
var sceneCommand = new Command15("scene").description("Manage scenes");
|
|
5187
|
+
sceneCommand.command("list").option("--project-id <id>", "Project ID").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
5188
|
+
try {
|
|
5189
|
+
const projectId = resolveProjectId(options.projectId);
|
|
5190
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5191
|
+
const scenes = apiUrl ? await new SceneApiClient(apiUrl).listScenes(projectId) : await createSceneStoreBundle("file").scenes.listScenes(projectId);
|
|
5192
|
+
console.log(JSON.stringify({ scenes }, null, 2));
|
|
5193
|
+
} catch (error) {
|
|
5194
|
+
console.error(`Error: ${error.message}`);
|
|
5195
|
+
process.exit(1);
|
|
5196
|
+
}
|
|
5197
|
+
});
|
|
5198
|
+
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) => {
|
|
5199
|
+
try {
|
|
5200
|
+
const projectId = resolveProjectId(options.projectId);
|
|
5201
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5202
|
+
const scene = apiUrl ? await new SceneApiClient(apiUrl).createScene(projectId, {
|
|
5203
|
+
name: options.name ? String(options.name) : void 0
|
|
5204
|
+
}) : await createSceneStoreBundle("file").scenes.createScene(projectId, {
|
|
5205
|
+
name: options.name ? String(options.name) : void 0
|
|
5206
|
+
});
|
|
5207
|
+
writeFlowState({ projectId, sceneId: scene.id, apiUrl });
|
|
5208
|
+
console.log(JSON.stringify({ scene }, null, 2));
|
|
5209
|
+
} catch (error) {
|
|
5210
|
+
console.error(`Error: ${error.message}`);
|
|
5211
|
+
process.exit(1);
|
|
5212
|
+
}
|
|
5213
|
+
});
|
|
5214
|
+
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) => {
|
|
5215
|
+
try {
|
|
5216
|
+
const filePath = path20.resolve(process.cwd(), String(options.file));
|
|
5217
|
+
if (!fs20.existsSync(filePath)) {
|
|
5218
|
+
throw new Error(`File not found: ${filePath}`);
|
|
5219
|
+
}
|
|
5220
|
+
const parsed = JSON.parse(fs20.readFileSync(filePath, "utf-8"));
|
|
5221
|
+
if (!parsed?.meta || !Array.isArray(parsed?.entities)) {
|
|
5222
|
+
throw new Error("Invalid scene file. Expected { meta, entities }.");
|
|
5223
|
+
}
|
|
5224
|
+
const projectId = resolveProjectId(options.projectId);
|
|
5225
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5226
|
+
const scene = apiUrl ? await new SceneApiClient(apiUrl).createScene(projectId, {
|
|
5227
|
+
name: options.name ? String(options.name) : path20.basename(filePath, path20.extname(filePath)),
|
|
5228
|
+
scene: { meta: parsed.meta, entities: parsed.entities }
|
|
5229
|
+
}) : await createSceneStoreBundle("file").scenes.createScene(projectId, {
|
|
5230
|
+
name: options.name ? String(options.name) : path20.basename(filePath, path20.extname(filePath)),
|
|
5231
|
+
scene: { meta: parsed.meta, entities: parsed.entities }
|
|
5232
|
+
});
|
|
5233
|
+
writeFlowState({ projectId, sceneId: scene.id, apiUrl });
|
|
5234
|
+
console.log(JSON.stringify({ scene }, null, 2));
|
|
5235
|
+
} catch (error) {
|
|
5236
|
+
console.error(`Error: ${error.message}`);
|
|
5237
|
+
process.exit(1);
|
|
5238
|
+
}
|
|
5239
|
+
});
|
|
5240
|
+
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) => {
|
|
5241
|
+
try {
|
|
5242
|
+
const projectId = resolveProjectId(options.projectId);
|
|
5243
|
+
const sceneId = resolveSceneId(options.sceneId);
|
|
5244
|
+
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5245
|
+
const scene = apiUrl ? await new SceneApiClient(apiUrl).getScene(projectId, sceneId) : await createSceneStoreBundle("file").scenes.getScene(projectId, sceneId);
|
|
5246
|
+
if (!scene) {
|
|
5247
|
+
throw new Error(`Scene not found: ${sceneId}`);
|
|
5248
|
+
}
|
|
5249
|
+
const out = path20.resolve(process.cwd(), String(options.file));
|
|
5250
|
+
fs20.mkdirSync(path20.dirname(out), { recursive: true });
|
|
5251
|
+
fs20.writeFileSync(out, JSON.stringify({ meta: scene.meta, entities: scene.entities }, null, 2), "utf-8");
|
|
5252
|
+
writeFlowState({ projectId, sceneId, apiUrl });
|
|
5253
|
+
console.log(`Scene exported to ${out}`);
|
|
5254
|
+
} catch (error) {
|
|
5255
|
+
console.error(`Error: ${error.message}`);
|
|
5256
|
+
process.exit(1);
|
|
5257
|
+
}
|
|
5258
|
+
});
|
|
5259
|
+
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) => {
|
|
5260
|
+
const next = writeFlowState({
|
|
5261
|
+
projectId: String(options.projectId),
|
|
5262
|
+
sceneId: options.sceneId ? String(options.sceneId) : void 0,
|
|
5263
|
+
apiUrl: options.apiUrl ? String(options.apiUrl) : void 0
|
|
5264
|
+
});
|
|
5265
|
+
console.log(JSON.stringify({ session: next }, null, 2));
|
|
5266
|
+
});
|
|
5267
|
+
|
|
3894
5268
|
// src/cli/index.ts
|
|
3895
5269
|
var __filename5 = fileURLToPath5(import.meta.url);
|
|
3896
|
-
var __dirname5 =
|
|
3897
|
-
var program = new
|
|
5270
|
+
var __dirname5 = path21.dirname(__filename5);
|
|
5271
|
+
var program = new Command16();
|
|
3898
5272
|
var telemetry = new PostHogTelemetryService();
|
|
3899
5273
|
var commandStartTimes = /* @__PURE__ */ new WeakMap();
|
|
3900
|
-
program.name("feedeas").description("CLI for Feedeas - AI-native video creation tool").version("0.1.0-alpha.
|
|
5274
|
+
program.name("feedeas").description("CLI for Feedeas - AI-native video creation tool").version("0.1.0-alpha.17");
|
|
3901
5275
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
3902
5276
|
commandStartTimes.set(actionCommand, Date.now());
|
|
3903
5277
|
telemetry.trackCommandStarted(actionCommand);
|
|
@@ -3906,57 +5280,49 @@ program.hook("postAction", (_thisCommand, actionCommand) => {
|
|
|
3906
5280
|
const startedAt = commandStartTimes.get(actionCommand) || Date.now();
|
|
3907
5281
|
telemetry.trackCommandFinished(actionCommand, "success", Date.now() - startedAt);
|
|
3908
5282
|
});
|
|
3909
|
-
program.command("edit
|
|
5283
|
+
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
5284
|
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
|
-
}
|
|
5285
|
+
const projectId = options.projectId ? resolveProjectId(options.projectId) : (() => {
|
|
5286
|
+
try {
|
|
5287
|
+
return resolveProjectId();
|
|
5288
|
+
} catch {
|
|
5289
|
+
return void 0;
|
|
3939
5290
|
}
|
|
3940
|
-
}
|
|
5291
|
+
})();
|
|
5292
|
+
const sceneId = options.sceneId ? resolveSceneId(options.sceneId) : (() => {
|
|
5293
|
+
try {
|
|
5294
|
+
return resolveSceneId();
|
|
5295
|
+
} catch {
|
|
5296
|
+
return void 0;
|
|
5297
|
+
}
|
|
5298
|
+
})();
|
|
3941
5299
|
const cwd = process.cwd();
|
|
3942
5300
|
console.log(`Starting Feedeas in ${cwd}...`);
|
|
3943
|
-
let staticRoot =
|
|
3944
|
-
if (!
|
|
3945
|
-
staticRoot =
|
|
5301
|
+
let staticRoot = path21.resolve(__dirname5, "../../dist/ui");
|
|
5302
|
+
if (!fs21.existsSync(staticRoot)) {
|
|
5303
|
+
staticRoot = path21.resolve(__dirname5, "../ui");
|
|
3946
5304
|
}
|
|
3947
|
-
if (!
|
|
5305
|
+
if (!fs21.existsSync(staticRoot)) {
|
|
3948
5306
|
console.warn(`Warning: UI assets not found at ${staticRoot}. Did you run 'bun run build'?`);
|
|
3949
5307
|
}
|
|
3950
5308
|
const app2 = createServer(staticRoot);
|
|
3951
5309
|
startServer(app2, port);
|
|
3952
|
-
const
|
|
5310
|
+
const params = new URLSearchParams();
|
|
5311
|
+
if (projectId) params.set("projectId", String(projectId));
|
|
5312
|
+
if (sceneId) params.set("sceneId", String(sceneId));
|
|
5313
|
+
const url = `http://localhost:${port}/editor${params.toString() ? `?${params.toString()}` : ""}`;
|
|
3953
5314
|
console.log(`Server running at ${url}`);
|
|
3954
5315
|
if (options.open) {
|
|
3955
5316
|
await open2(url);
|
|
3956
5317
|
}
|
|
5318
|
+
if (projectId || sceneId) {
|
|
5319
|
+
writeFlowState({ projectId, sceneId });
|
|
5320
|
+
}
|
|
3957
5321
|
});
|
|
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) => {
|
|
5322
|
+
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
5323
|
console.log(`Taking snapshot at ${time}s...`);
|
|
5324
|
+
const projectId = resolveProjectId(options.projectId);
|
|
5325
|
+
const sceneId = resolveSceneId(options.sceneId);
|
|
3960
5326
|
try {
|
|
3961
5327
|
const { chromium: chromium2 } = await import("playwright-core");
|
|
3962
5328
|
let browser;
|
|
@@ -3985,7 +5351,7 @@ program.command("snap").alias("screenshot").description("Take a snapshot of the
|
|
|
3985
5351
|
const width = parseInt(options.width);
|
|
3986
5352
|
const height = parseInt(options.height);
|
|
3987
5353
|
await page.setViewportSize({ width, height });
|
|
3988
|
-
const targetUrl = `${options.url}?time=${time}&mode=render`;
|
|
5354
|
+
const targetUrl = `${options.url}?time=${time}&mode=render&projectId=${encodeURIComponent(projectId)}&sceneId=${encodeURIComponent(sceneId)}`;
|
|
3989
5355
|
console.log(`Navigating to ${targetUrl}...`);
|
|
3990
5356
|
await page.goto(targetUrl);
|
|
3991
5357
|
try {
|
|
@@ -3999,6 +5365,7 @@ program.command("snap").alias("screenshot").description("Take a snapshot of the
|
|
|
3999
5365
|
const file = options.output;
|
|
4000
5366
|
await page.locator("canvas").first().screenshot({ path: file });
|
|
4001
5367
|
console.log(`Snapshot saved to ${file}`);
|
|
5368
|
+
writeFlowState({ projectId, sceneId });
|
|
4002
5369
|
await browser.close();
|
|
4003
5370
|
} catch (e) {
|
|
4004
5371
|
console.error("Failed to take snapshot:", e.message);
|
|
@@ -4019,16 +5386,18 @@ program.addCommand(createSceneCommand);
|
|
|
4019
5386
|
program.addCommand(addEntityCommand);
|
|
4020
5387
|
program.addCommand(tasteCommand);
|
|
4021
5388
|
program.addCommand(createFeedbackCommand(telemetry));
|
|
5389
|
+
program.addCommand(projectCommand);
|
|
5390
|
+
program.addCommand(sceneCommand);
|
|
4022
5391
|
program.addHelpText("after", `
|
|
4023
5392
|
|
|
4024
5393
|
Agent tip:
|
|
4025
5394
|
feedeas feedback "<what to improve>"
|
|
4026
5395
|
|
|
4027
5396
|
Quick Start (30 seconds):
|
|
4028
|
-
1. feedeas
|
|
4029
|
-
2. feedeas
|
|
4030
|
-
3. feedeas edit
|
|
4031
|
-
4. feedeas record --project scene
|
|
5397
|
+
1. feedeas project create --name "My Project" > project.json
|
|
5398
|
+
2. feedeas scene list --project-id <projectId>
|
|
5399
|
+
3. feedeas edit --project-id <projectId> --scene-id <sceneId>
|
|
5400
|
+
4. feedeas record --project-id <projectId> --scene-id <sceneId> # Render video directly
|
|
4032
5401
|
|
|
4033
5402
|
Guided Flow (Agent-Friendly, End-to-End):
|
|
4034
5403
|
1. mkdir -p my-reel && cd my-reel
|
|
@@ -4053,8 +5422,8 @@ Agent Conversation Example:
|
|
|
4053
5422
|
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
5423
|
Agent: Now I'll generate some visuals to match the narration.
|
|
4055
5424
|
Agent (tool): feedeas example documentary > scene.json
|
|
4056
|
-
Agent (tool): feedeas imagine "
|
|
4057
|
-
Agent (tool): feedeas imagine "A person meditating in a park
|
|
5425
|
+
Agent (tool): feedeas imagine "A peaceful sunrise over a mountain lake, cinematic lighting" --aspect-ratio 9:16 -o scene1.png
|
|
5426
|
+
Agent (tool): feedeas imagine "A person meditating in a park, soft morning light" --aspect-ratio 9:16 -o scene2.png
|
|
4058
5427
|
Agent (tool): feedeas audio "Life changes in small daily choices..." -o narration.mp3 --no-transcribe
|
|
4059
5428
|
Agent (tool): feedeas bgm "soft reflective ambient, no vocals..." -d 25 -o bgm.mp3
|
|
4060
5429
|
Agent: Now I\u2019ll wire assets into scene.json with src values under assets/.
|
|
@@ -4113,7 +5482,7 @@ Transition Types:
|
|
|
4113
5482
|
|
|
4114
5483
|
Project Structure:
|
|
4115
5484
|
your-project/
|
|
4116
|
-
\u251C\u2500\u2500
|
|
5485
|
+
\u251C\u2500\u2500 .feedeas/ # Project + scene resource storage (default file backend)
|
|
4117
5486
|
\u251C\u2500\u2500 assets/ # Asset folder (images, audio)
|
|
4118
5487
|
\u2502 \u251C\u2500\u2500 image.jpg
|
|
4119
5488
|
\u2502 \u251C\u2500\u2500 music.mp3
|
|
@@ -4126,14 +5495,14 @@ Programmatic Workflow (for Agents):
|
|
|
4126
5495
|
$ feedeas audio "narration text" -o assets/voice.mp3 # Generate audio
|
|
4127
5496
|
$ feedeas bgm "cinematic bgm, no vocals" -o assets/bgm.mp3
|
|
4128
5497
|
$ feedeas example documentary # See a complete example
|
|
4129
|
-
$ feedeas set-scene
|
|
4130
|
-
$ feedeas validate
|
|
5498
|
+
$ feedeas set-scene --project-id <pid> --scene-id <sid> --content '{...}'
|
|
5499
|
+
$ feedeas validate --project-id <pid> --scene-id <sid> # Validate before rendering
|
|
4131
5500
|
$ feedeas edit --no-open & # Start server in background
|
|
4132
|
-
$ feedeas record --project
|
|
5501
|
+
$ feedeas record --project-id <pid> --scene-id <sid> # Render to video
|
|
4133
5502
|
|
|
4134
5503
|
Interactive Workflow (for Humans):
|
|
4135
|
-
$ feedeas edit
|
|
4136
|
-
$ feedeas snap 2.5 -
|
|
5504
|
+
$ feedeas edit --project-id <pid> --scene-id <sid> # Start editor
|
|
5505
|
+
$ feedeas snap 2.5 --project-id <pid> --scene-id <sid> -o frame.png
|
|
4137
5506
|
|
|
4138
5507
|
Taste Workflow:
|
|
4139
5508
|
$ feedeas taste # Start UI Workspace
|