feedeas 0.1.0-alpha.17 → 0.1.0-alpha.19
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 +301 -251
- package/package.json +2 -1
package/dist/cli/index.js
CHANGED
|
@@ -93,75 +93,141 @@ import { Command } from "commander";
|
|
|
93
93
|
import { chromium } from "playwright-core";
|
|
94
94
|
import { spawn as spawn4, spawnSync } from "child_process";
|
|
95
95
|
import fs8 from "fs";
|
|
96
|
-
import
|
|
96
|
+
import path9 from "path";
|
|
97
97
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
98
98
|
|
|
99
99
|
// src/cli/services/ffprobe.ts
|
|
100
|
-
import { spawn } from "child_process";
|
|
101
100
|
import fs from "fs";
|
|
101
|
+
import path from "path";
|
|
102
|
+
function isNodeRuntime() {
|
|
103
|
+
return typeof process !== "undefined" && !!process.versions?.node;
|
|
104
|
+
}
|
|
105
|
+
function isImageExt(ext) {
|
|
106
|
+
return [".jpg", ".jpeg", ".png", ".webp", ".gif"].includes(ext);
|
|
107
|
+
}
|
|
108
|
+
function isAudioExt(ext) {
|
|
109
|
+
return [".mp3", ".wav", ".m4a", ".ogg"].includes(ext);
|
|
110
|
+
}
|
|
111
|
+
async function getAudioMetadataFallback(filePath) {
|
|
112
|
+
const musicMetadata = await import("music-metadata");
|
|
113
|
+
const metadata = await musicMetadata.parseFile(filePath);
|
|
114
|
+
return {
|
|
115
|
+
duration: metadata.format.duration,
|
|
116
|
+
sampleRate: metadata.format.sampleRate,
|
|
117
|
+
channels: metadata.format.numberOfChannels,
|
|
118
|
+
codec: metadata.format.codec,
|
|
119
|
+
bitrate: metadata.format.bitrate ?? void 0
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
async function getImageDimensionsFallback(filePath) {
|
|
123
|
+
const imageSize = await import("image-size");
|
|
124
|
+
const buffer = fs.readFileSync(filePath);
|
|
125
|
+
const dimensions = imageSize.default(buffer);
|
|
126
|
+
return { width: dimensions.width ?? void 0, height: dimensions.height ?? void 0 };
|
|
127
|
+
}
|
|
102
128
|
var FFprobeService = class {
|
|
103
129
|
static async getMetadata(filePath) {
|
|
104
130
|
if (!fs.existsSync(filePath)) {
|
|
105
131
|
throw new Error(`File not found: ${filePath}`);
|
|
106
132
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
133
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
134
|
+
const stats = fs.statSync(filePath);
|
|
135
|
+
if (isNodeRuntime()) {
|
|
136
|
+
try {
|
|
137
|
+
const { spawn: spawn5 } = await import("node:child_process");
|
|
138
|
+
return await new Promise((resolve, reject) => {
|
|
139
|
+
const ffprobe = spawn5("ffprobe", [
|
|
140
|
+
"-v",
|
|
141
|
+
"quiet",
|
|
142
|
+
"-print_format",
|
|
143
|
+
"json",
|
|
144
|
+
"-show_format",
|
|
145
|
+
"-show_streams",
|
|
146
|
+
filePath
|
|
147
|
+
]);
|
|
148
|
+
let stdout = "";
|
|
149
|
+
let stderr = "";
|
|
150
|
+
ffprobe.stdout.on("data", (data) => {
|
|
151
|
+
stdout += data;
|
|
152
|
+
});
|
|
153
|
+
ffprobe.stderr.on("data", (data) => {
|
|
154
|
+
stderr += data;
|
|
155
|
+
});
|
|
156
|
+
ffprobe.on("close", (code) => {
|
|
157
|
+
if (code !== 0) {
|
|
158
|
+
reject(new Error(`ffprobe exited with code ${code}: ${stderr}`));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const data = JSON.parse(stdout);
|
|
163
|
+
const format = data.format || {};
|
|
164
|
+
const streams = data.streams || [];
|
|
165
|
+
const videoStream = streams.find((s) => s.codec_type === "video");
|
|
166
|
+
const audioStream = streams.find((s) => s.codec_type === "audio");
|
|
167
|
+
const metadata = {
|
|
168
|
+
path: filePath,
|
|
169
|
+
format: format.format_name || "unknown",
|
|
170
|
+
duration: parseFloat(format.duration || "0"),
|
|
171
|
+
size: parseInt(format.size || "0")
|
|
172
|
+
};
|
|
173
|
+
if (videoStream) {
|
|
174
|
+
metadata.width = videoStream.width;
|
|
175
|
+
metadata.height = videoStream.height;
|
|
176
|
+
}
|
|
177
|
+
if (audioStream) {
|
|
178
|
+
metadata.audioChannels = audioStream.channels;
|
|
179
|
+
metadata.sampleRate = parseInt(audioStream.sample_rate || "0");
|
|
180
|
+
metadata.audioCodec = audioStream.codec_name || void 0;
|
|
181
|
+
}
|
|
182
|
+
if (format?.bit_rate) {
|
|
183
|
+
metadata.bitrate = parseInt(format.bit_rate || "0");
|
|
184
|
+
}
|
|
185
|
+
resolve(metadata);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
reject(new Error(`Failed to parse ffprobe output: ${err}`));
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
ffprobe.on("error", (err) => {
|
|
191
|
+
reject(new Error(`Failed to spawn ffprobe: ${err.message}`));
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (err?.code !== "ENOENT") {
|
|
196
|
+
throw err;
|
|
153
197
|
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (isImageExt(ext)) {
|
|
201
|
+
const dims = await getImageDimensionsFallback(filePath);
|
|
202
|
+
return {
|
|
203
|
+
path: filePath,
|
|
204
|
+
format: ext.replace(".", "") || "image",
|
|
205
|
+
duration: 0,
|
|
206
|
+
size: stats.size,
|
|
207
|
+
width: dims.width,
|
|
208
|
+
height: dims.height
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (isAudioExt(ext)) {
|
|
212
|
+
const audio = await getAudioMetadataFallback(filePath);
|
|
213
|
+
return {
|
|
214
|
+
path: filePath,
|
|
215
|
+
format: ext.replace(".", "") || "audio",
|
|
216
|
+
duration: audio.duration || 0,
|
|
217
|
+
size: stats.size,
|
|
218
|
+
audioChannels: audio.channels,
|
|
219
|
+
sampleRate: audio.sampleRate,
|
|
220
|
+
audioCodec: audio.codec,
|
|
221
|
+
bitrate: audio.bitrate
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
throw new Error("ffprobe is required to inspect this file type.");
|
|
159
225
|
}
|
|
160
226
|
};
|
|
161
227
|
|
|
162
228
|
// src/cli/services/assets/resolver.ts
|
|
163
229
|
import fs2 from "node:fs";
|
|
164
|
-
import
|
|
230
|
+
import path2 from "node:path";
|
|
165
231
|
function looksLikeScheme(uri) {
|
|
166
232
|
return /^[a-z][a-z0-9+.-]*:/.test(uri);
|
|
167
233
|
}
|
|
@@ -177,7 +243,7 @@ var RelativeFsResolver = class {
|
|
|
177
243
|
const localPath2 = decodeURIComponent(new URL(uri).pathname);
|
|
178
244
|
return { type: "local", uri, localPath: localPath2 };
|
|
179
245
|
}
|
|
180
|
-
const localPath =
|
|
246
|
+
const localPath = path2.isAbsolute(uri) ? uri : path2.resolve(rootPath, uri);
|
|
181
247
|
return { type: "local", uri, localPath };
|
|
182
248
|
}
|
|
183
249
|
};
|
|
@@ -337,24 +403,24 @@ import { existsSync } from "node:fs";
|
|
|
337
403
|
|
|
338
404
|
// src/cli/server/cli-runner.ts
|
|
339
405
|
import { spawn as spawn2 } from "child_process";
|
|
340
|
-
import
|
|
406
|
+
import path3 from "path";
|
|
341
407
|
import fs3 from "fs";
|
|
342
408
|
import { fileURLToPath } from "url";
|
|
343
409
|
var __filename = fileURLToPath(import.meta.url);
|
|
344
|
-
var __dirname =
|
|
410
|
+
var __dirname = path3.dirname(__filename);
|
|
345
411
|
async function runCliCommand(args, cwd) {
|
|
346
412
|
const isCompiled = __filename.endsWith(".js");
|
|
347
413
|
let command;
|
|
348
414
|
let spawnArgs;
|
|
349
415
|
if (isCompiled) {
|
|
350
|
-
const binPath =
|
|
416
|
+
const binPath = path3.resolve(__dirname, "../../../bin/feedeas.js");
|
|
351
417
|
if (!fs3.existsSync(binPath)) {
|
|
352
418
|
throw new Error(`Could not find compiled CLI binary at ${binPath}`);
|
|
353
419
|
}
|
|
354
420
|
command = "node";
|
|
355
421
|
spawnArgs = [binPath, ...args];
|
|
356
422
|
} else {
|
|
357
|
-
const srcPath =
|
|
423
|
+
const srcPath = path3.resolve(__dirname, "../index.ts");
|
|
358
424
|
command = "bun";
|
|
359
425
|
spawnArgs = ["run", srcPath, ...args];
|
|
360
426
|
}
|
|
@@ -420,7 +486,7 @@ ${stderrData || stdoutData}`));
|
|
|
420
486
|
}
|
|
421
487
|
|
|
422
488
|
// src/cli/services/taste.ts
|
|
423
|
-
import
|
|
489
|
+
import path4 from "node:path";
|
|
424
490
|
var SAMPLE_SCENE_HEADER = "## Sample Scene (Starter)";
|
|
425
491
|
var DEFAULT_SAMPLE_SCENE_JSON = {
|
|
426
492
|
meta: {
|
|
@@ -550,12 +616,12 @@ var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
|
550
616
|
]);
|
|
551
617
|
function safeResolveFromCwd(relativePath) {
|
|
552
618
|
const cwd = process.cwd();
|
|
553
|
-
const normalized =
|
|
619
|
+
const normalized = path4.normalize(relativePath);
|
|
554
620
|
if (normalized.includes("..")) {
|
|
555
621
|
throw new Error("Invalid path");
|
|
556
622
|
}
|
|
557
|
-
const fullPath =
|
|
558
|
-
if (fullPath !== cwd && !fullPath.startsWith(`${cwd}${
|
|
623
|
+
const fullPath = path4.resolve(cwd, normalized);
|
|
624
|
+
if (fullPath !== cwd && !fullPath.startsWith(`${cwd}${path4.sep}`)) {
|
|
559
625
|
throw new Error("Invalid path");
|
|
560
626
|
}
|
|
561
627
|
return fullPath;
|
|
@@ -1366,7 +1432,7 @@ async function simulateIdeas(params) {
|
|
|
1366
1432
|
|
|
1367
1433
|
// src/cli/services/taste-store.ts
|
|
1368
1434
|
import fs4 from "node:fs";
|
|
1369
|
-
import
|
|
1435
|
+
import path5 from "node:path";
|
|
1370
1436
|
import { createHash } from "node:crypto";
|
|
1371
1437
|
var DEFAULT_SUGGESTIONS_FILE_CONTENT = "# Taste Suggestions v1\n";
|
|
1372
1438
|
function hashContent(content) {
|
|
@@ -1489,9 +1555,9 @@ var MarkdownTasteStore = class {
|
|
|
1489
1555
|
}
|
|
1490
1556
|
async ensureWorkspace() {
|
|
1491
1557
|
const { tastePath, memoryPath, suggestionsPath } = this.resolveAll();
|
|
1492
|
-
fs4.mkdirSync(
|
|
1493
|
-
fs4.mkdirSync(
|
|
1494
|
-
fs4.mkdirSync(
|
|
1558
|
+
fs4.mkdirSync(path5.dirname(tastePath), { recursive: true });
|
|
1559
|
+
fs4.mkdirSync(path5.dirname(memoryPath), { recursive: true });
|
|
1560
|
+
fs4.mkdirSync(path5.dirname(suggestionsPath), { recursive: true });
|
|
1495
1561
|
if (!fs4.existsSync(tastePath)) {
|
|
1496
1562
|
fs4.writeFileSync(tastePath, DEFAULT_TASTE_FILE_CONTENT, "utf-8");
|
|
1497
1563
|
}
|
|
@@ -1648,7 +1714,7 @@ function createTasteStore(params) {
|
|
|
1648
1714
|
|
|
1649
1715
|
// src/cli/services/scenes/file-store.ts
|
|
1650
1716
|
import fs5 from "node:fs";
|
|
1651
|
-
import
|
|
1717
|
+
import path6 from "node:path";
|
|
1652
1718
|
import { randomUUID } from "node:crypto";
|
|
1653
1719
|
|
|
1654
1720
|
// src/cli/services/scenes/types.ts
|
|
@@ -1670,10 +1736,10 @@ function nowIso() {
|
|
|
1670
1736
|
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1671
1737
|
}
|
|
1672
1738
|
function getStorePath(cwd) {
|
|
1673
|
-
return
|
|
1739
|
+
return path6.resolve(cwd, ".feedeas/store.json");
|
|
1674
1740
|
}
|
|
1675
1741
|
function ensureStore(pathname) {
|
|
1676
|
-
const dir =
|
|
1742
|
+
const dir = path6.dirname(pathname);
|
|
1677
1743
|
if (!fs5.existsSync(dir)) {
|
|
1678
1744
|
fs5.mkdirSync(dir, { recursive: true });
|
|
1679
1745
|
}
|
|
@@ -1900,10 +1966,10 @@ api.get("/v1/files/read", async (c) => {
|
|
|
1900
1966
|
});
|
|
1901
1967
|
api.post("/v1/files/write", async (c) => {
|
|
1902
1968
|
const body = await c.req.json();
|
|
1903
|
-
const { path:
|
|
1904
|
-
if (!
|
|
1969
|
+
const { path: path23, content } = body;
|
|
1970
|
+
if (!path23 || content === void 0) return c.json({ error: "Path and content required" }, 400);
|
|
1905
1971
|
try {
|
|
1906
|
-
const fullPath = resolveSafePath(
|
|
1972
|
+
const fullPath = resolveSafePath(path23);
|
|
1907
1973
|
const dir = dirname(fullPath);
|
|
1908
1974
|
if (!existsSync(dir)) {
|
|
1909
1975
|
await mkdir(dir, { recursive: true });
|
|
@@ -2406,14 +2472,14 @@ api.post("/cli/run", async (c) => {
|
|
|
2406
2472
|
var api_default = api;
|
|
2407
2473
|
|
|
2408
2474
|
// src/cli/server/index.ts
|
|
2409
|
-
import
|
|
2475
|
+
import path7 from "path";
|
|
2410
2476
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2411
2477
|
import fs6 from "fs";
|
|
2412
2478
|
var app = new Hono2();
|
|
2413
2479
|
app.use("/*", cors());
|
|
2414
2480
|
app.route("/api", api_default);
|
|
2415
2481
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
2416
|
-
var __dirname2 =
|
|
2482
|
+
var __dirname2 = path7.dirname(__filename2);
|
|
2417
2483
|
var createServer = (staticRoot) => {
|
|
2418
2484
|
const app2 = new Hono2();
|
|
2419
2485
|
app2.use("/*", cors());
|
|
@@ -2444,7 +2510,7 @@ var createServer = (staticRoot) => {
|
|
|
2444
2510
|
};
|
|
2445
2511
|
app2.get("/*", async (c) => {
|
|
2446
2512
|
const requestPath = c.req.path === "/" ? "/index.html" : c.req.path;
|
|
2447
|
-
const filePath =
|
|
2513
|
+
const filePath = path7.join(staticRoot, requestPath);
|
|
2448
2514
|
try {
|
|
2449
2515
|
if (fs6.existsSync(filePath)) {
|
|
2450
2516
|
const file = await fs6.promises.readFile(filePath);
|
|
@@ -2459,7 +2525,7 @@ var createServer = (staticRoot) => {
|
|
|
2459
2525
|
}
|
|
2460
2526
|
if (!requestPath.includes(".")) {
|
|
2461
2527
|
try {
|
|
2462
|
-
const indexPath =
|
|
2528
|
+
const indexPath = path7.join(staticRoot, "index.html");
|
|
2463
2529
|
if (fs6.existsSync(indexPath)) {
|
|
2464
2530
|
const indexFile = await fs6.promises.readFile(indexPath);
|
|
2465
2531
|
return new Response(indexFile, {
|
|
@@ -2582,12 +2648,12 @@ var SceneApiClient = class {
|
|
|
2582
2648
|
|
|
2583
2649
|
// src/cli/services/scenes/flow-state.ts
|
|
2584
2650
|
import fs7 from "node:fs";
|
|
2585
|
-
import
|
|
2651
|
+
import path8 from "node:path";
|
|
2586
2652
|
function sessionPath(cwd) {
|
|
2587
|
-
return
|
|
2653
|
+
return path8.resolve(cwd, ".feedeas/session.json");
|
|
2588
2654
|
}
|
|
2589
2655
|
function ensureDir(filePath) {
|
|
2590
|
-
const dir =
|
|
2656
|
+
const dir = path8.dirname(filePath);
|
|
2591
2657
|
if (!fs7.existsSync(dir)) fs7.mkdirSync(dir, { recursive: true });
|
|
2592
2658
|
}
|
|
2593
2659
|
function readFlowState(cwd = process.cwd()) {
|
|
@@ -2631,7 +2697,7 @@ function resolveApiUrl(explicit) {
|
|
|
2631
2697
|
|
|
2632
2698
|
// src/cli/commands/record.ts
|
|
2633
2699
|
var __filename3 = fileURLToPath3(import.meta.url);
|
|
2634
|
-
var __dirname3 =
|
|
2700
|
+
var __dirname3 = path9.dirname(__filename3);
|
|
2635
2701
|
function formatBytes(bytes) {
|
|
2636
2702
|
if (bytes === 0) return "0 B";
|
|
2637
2703
|
const k = 1024;
|
|
@@ -2640,7 +2706,7 @@ function formatBytes(bytes) {
|
|
|
2640
2706
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
2641
2707
|
}
|
|
2642
2708
|
function getMissingUiAssets(staticRoot) {
|
|
2643
|
-
const indexPath =
|
|
2709
|
+
const indexPath = path9.join(staticRoot, "index.html");
|
|
2644
2710
|
if (!fs8.existsSync(indexPath)) {
|
|
2645
2711
|
return {
|
|
2646
2712
|
missingFiles: [indexPath],
|
|
@@ -2653,7 +2719,7 @@ function getMissingUiAssets(staticRoot) {
|
|
|
2653
2719
|
const missingFiles = [];
|
|
2654
2720
|
const expectedUrls = [];
|
|
2655
2721
|
for (const assetPath of assetPaths) {
|
|
2656
|
-
const localPath =
|
|
2722
|
+
const localPath = path9.join(staticRoot, assetPath.replace(/^\//, ""));
|
|
2657
2723
|
if (!fs8.existsSync(localPath)) {
|
|
2658
2724
|
missingFiles.push(localPath);
|
|
2659
2725
|
expectedUrls.push(assetPath);
|
|
@@ -2861,9 +2927,9 @@ Ready to record. Run without --dry-run to start rendering.`);
|
|
|
2861
2927
|
ensureFfmpegAvailableOrExit();
|
|
2862
2928
|
await ensurePlaywrightReadyOrExit();
|
|
2863
2929
|
console.log("Starting temporary server...");
|
|
2864
|
-
let staticRoot =
|
|
2930
|
+
let staticRoot = path9.resolve(__dirname3, "../../dist/ui");
|
|
2865
2931
|
if (!fs8.existsSync(staticRoot)) {
|
|
2866
|
-
staticRoot =
|
|
2932
|
+
staticRoot = path9.resolve(__dirname3, "../ui");
|
|
2867
2933
|
}
|
|
2868
2934
|
if (!fs8.existsSync(staticRoot)) {
|
|
2869
2935
|
console.error(`Error: UI assets not found at ${staticRoot}. Please run 'bun run build' first.`);
|
|
@@ -3081,10 +3147,10 @@ Ready to record. Run without --dry-run to start rendering.`);
|
|
|
3081
3147
|
// src/cli/commands/inspect.ts
|
|
3082
3148
|
import { Command as Command2 } from "commander";
|
|
3083
3149
|
import fs9 from "fs";
|
|
3084
|
-
import
|
|
3150
|
+
import path10 from "path";
|
|
3085
3151
|
var inspectCommand = new Command2("inspect");
|
|
3086
3152
|
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) => {
|
|
3087
|
-
const filePath =
|
|
3153
|
+
const filePath = path10.resolve(process.cwd(), file);
|
|
3088
3154
|
if (!fs9.existsSync(filePath)) {
|
|
3089
3155
|
console.error(`File not found: ${filePath}`);
|
|
3090
3156
|
process.exit(1);
|
|
@@ -3248,14 +3314,14 @@ validateCommand.description("Validate a scene by project and scene ID").option("
|
|
|
3248
3314
|
// src/cli/commands/set-scene.ts
|
|
3249
3315
|
import { Command as Command4 } from "commander";
|
|
3250
3316
|
import fs11 from "fs";
|
|
3251
|
-
import
|
|
3317
|
+
import path11 from "path";
|
|
3252
3318
|
var setSceneCommand = new Command4("set-scene");
|
|
3253
3319
|
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) => {
|
|
3254
3320
|
let contentStr = "";
|
|
3255
3321
|
if (options.content) {
|
|
3256
3322
|
contentStr = options.content;
|
|
3257
3323
|
} else if (options.input) {
|
|
3258
|
-
const inputPath =
|
|
3324
|
+
const inputPath = path11.resolve(process.cwd(), options.input);
|
|
3259
3325
|
if (!fs11.existsSync(inputPath)) {
|
|
3260
3326
|
console.error(`Input file not found: ${inputPath}`);
|
|
3261
3327
|
process.exit(1);
|
|
@@ -3302,29 +3368,29 @@ setSceneCommand.description("Update an existing scene by project/scene ID").opti
|
|
|
3302
3368
|
// src/cli/commands/imagine.ts
|
|
3303
3369
|
import { Command as Command5 } from "commander";
|
|
3304
3370
|
import fs13 from "fs";
|
|
3305
|
-
import
|
|
3371
|
+
import path13 from "path";
|
|
3306
3372
|
|
|
3307
3373
|
// src/cli/utils/path.ts
|
|
3308
|
-
import
|
|
3374
|
+
import path12 from "path";
|
|
3309
3375
|
import fs12 from "fs";
|
|
3310
3376
|
function resolveAssetOutputPath(outputOption, cwd = process.cwd()) {
|
|
3311
|
-
if (
|
|
3377
|
+
if (path12.isAbsolute(outputOption)) {
|
|
3312
3378
|
return outputOption;
|
|
3313
3379
|
}
|
|
3314
|
-
const assetsDir =
|
|
3380
|
+
const assetsDir = path12.resolve(cwd, "assets");
|
|
3315
3381
|
if (!fs12.existsSync(assetsDir)) {
|
|
3316
3382
|
fs12.mkdirSync(assetsDir, { recursive: true });
|
|
3317
3383
|
}
|
|
3318
|
-
const normalizedInput =
|
|
3384
|
+
const normalizedInput = path12.normalize(outputOption);
|
|
3319
3385
|
let cleanRelativePath = normalizedInput;
|
|
3320
|
-
const segments = normalizedInput.split(
|
|
3386
|
+
const segments = normalizedInput.split(path12.sep);
|
|
3321
3387
|
if (segments[0] === "assets") {
|
|
3322
|
-
cleanRelativePath = segments.slice(1).join(
|
|
3388
|
+
cleanRelativePath = segments.slice(1).join(path12.sep);
|
|
3323
3389
|
}
|
|
3324
3390
|
if (!cleanRelativePath) {
|
|
3325
3391
|
cleanRelativePath = `unnamed-asset-${Date.now()}`;
|
|
3326
3392
|
}
|
|
3327
|
-
return
|
|
3393
|
+
return path12.join(assetsDir, cleanRelativePath);
|
|
3328
3394
|
}
|
|
3329
3395
|
|
|
3330
3396
|
// src/cli/commands/imagine.ts
|
|
@@ -3421,15 +3487,15 @@ Note:
|
|
|
3421
3487
|
imageSize
|
|
3422
3488
|
});
|
|
3423
3489
|
const outputPath = resolveAssetOutputPath(options.output);
|
|
3424
|
-
const outputDir =
|
|
3425
|
-
const outputExt =
|
|
3426
|
-
const outputBase =
|
|
3490
|
+
const outputDir = path13.dirname(outputPath);
|
|
3491
|
+
const outputExt = path13.extname(outputPath) || ".png";
|
|
3492
|
+
const outputBase = path13.basename(outputPath, outputExt);
|
|
3427
3493
|
if (!fs13.existsSync(outputDir)) {
|
|
3428
3494
|
fs13.mkdirSync(outputDir, { recursive: true });
|
|
3429
3495
|
}
|
|
3430
3496
|
const savedFiles = [];
|
|
3431
3497
|
for (let i = 0; i < images.length; i++) {
|
|
3432
|
-
const filename = images.length > 1 ?
|
|
3498
|
+
const filename = images.length > 1 ? path13.join(outputDir, `${outputBase}_${i + 1}${outputExt}`) : outputPath;
|
|
3433
3499
|
fs13.writeFileSync(filename, images[i]);
|
|
3434
3500
|
if (!options.json) console.log(`\u2705 Image saved: ${filename}`);
|
|
3435
3501
|
savedFiles.push(filename);
|
|
@@ -3541,7 +3607,7 @@ function resolveAspectRatio(input, projectFile) {
|
|
|
3541
3607
|
return normalizeAspectRatio(input);
|
|
3542
3608
|
}
|
|
3543
3609
|
function inferAspectRatioFromProject(projectFile) {
|
|
3544
|
-
const filePath =
|
|
3610
|
+
const filePath = path13.resolve(process.cwd(), projectFile);
|
|
3545
3611
|
if (!fs13.existsSync(filePath)) return null;
|
|
3546
3612
|
try {
|
|
3547
3613
|
const scene = JSON.parse(fs13.readFileSync(filePath, "utf-8"));
|
|
@@ -3585,43 +3651,42 @@ function gcd(a, b) {
|
|
|
3585
3651
|
// src/cli/commands/audio.ts
|
|
3586
3652
|
import { Command as Command6 } from "commander";
|
|
3587
3653
|
import fs16 from "fs";
|
|
3588
|
-
import
|
|
3654
|
+
import path16 from "path";
|
|
3589
3655
|
|
|
3590
3656
|
// src/cli/services/whisper.ts
|
|
3591
3657
|
import fs14 from "fs";
|
|
3592
|
-
import
|
|
3658
|
+
import path14 from "path";
|
|
3593
3659
|
import os from "os";
|
|
3594
|
-
import { spawn as spawn5 } from "child_process";
|
|
3595
3660
|
import https from "https";
|
|
3596
3661
|
var WhisperService = class {
|
|
3597
3662
|
static getHomeDir() {
|
|
3598
3663
|
return os.homedir();
|
|
3599
3664
|
}
|
|
3600
3665
|
static getBaseDir() {
|
|
3601
|
-
return
|
|
3666
|
+
return path14.join(this.getHomeDir(), ".feedeas");
|
|
3602
3667
|
}
|
|
3603
3668
|
static getBinDir() {
|
|
3604
|
-
return
|
|
3669
|
+
return path14.join(this.getBaseDir(), "bin");
|
|
3605
3670
|
}
|
|
3606
3671
|
static getModelsDir() {
|
|
3607
|
-
return
|
|
3672
|
+
return path14.join(this.getBaseDir(), "models");
|
|
3608
3673
|
}
|
|
3609
3674
|
static getExecutablePath() {
|
|
3610
|
-
const repoDir =
|
|
3675
|
+
const repoDir = path14.join(this.getBaseDir(), "whisper.cpp-repo");
|
|
3611
3676
|
const possiblePaths = [
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3677
|
+
path14.join(repoDir, "build", "bin", "whisper-cli"),
|
|
3678
|
+
path14.join(repoDir, "build", "bin", "main"),
|
|
3679
|
+
path14.join(repoDir, "main"),
|
|
3680
|
+
path14.join(this.getBinDir(), "whisper-main")
|
|
3616
3681
|
// fallback to copied/downloaded
|
|
3617
3682
|
];
|
|
3618
3683
|
for (const p of possiblePaths) {
|
|
3619
3684
|
if (fs14.existsSync(p)) return p;
|
|
3620
3685
|
}
|
|
3621
|
-
return
|
|
3686
|
+
return path14.join(this.getBinDir(), "whisper-main");
|
|
3622
3687
|
}
|
|
3623
3688
|
static getModelPath(modelName = "base.en") {
|
|
3624
|
-
return
|
|
3689
|
+
return path14.join(this.getModelsDir(), `ggml-${modelName}.bin`);
|
|
3625
3690
|
}
|
|
3626
3691
|
/**
|
|
3627
3692
|
* Download a file from a URL to a destination path
|
|
@@ -3663,7 +3728,7 @@ var WhisperService = class {
|
|
|
3663
3728
|
if (!fs14.existsSync(execPath)) {
|
|
3664
3729
|
console.log("\u2B07\uFE0F Whisper binary not found. Attempting to build...");
|
|
3665
3730
|
try {
|
|
3666
|
-
const repoDir =
|
|
3731
|
+
const repoDir = path14.join(this.getBaseDir(), "whisper.cpp-repo");
|
|
3667
3732
|
if (!fs14.existsSync(repoDir)) {
|
|
3668
3733
|
console.log("\u{1F4E6} Cloning whisper.cpp...");
|
|
3669
3734
|
await runCommand("git", ["clone", "https://github.com/ggerganov/whisper.cpp.git", repoDir]);
|
|
@@ -3696,12 +3761,12 @@ Please install manually: 'brew install whisper-cpp'`);
|
|
|
3696
3761
|
static async transcribe(audioPath) {
|
|
3697
3762
|
const execPath = this.getExecutablePath();
|
|
3698
3763
|
const modelPath = this.getModelPath();
|
|
3699
|
-
const ext =
|
|
3764
|
+
const ext = path14.extname(audioPath).toLowerCase();
|
|
3700
3765
|
let inputToWhisper = audioPath;
|
|
3701
3766
|
let isTempFile = false;
|
|
3702
3767
|
if (ext !== ".wav") {
|
|
3703
3768
|
console.log("\u{1F504} Converting audio to 16kHz WAV for Whisper...");
|
|
3704
|
-
const tempWav =
|
|
3769
|
+
const tempWav = path14.join(path14.dirname(audioPath), `temp_${Date.now()}.wav`);
|
|
3705
3770
|
await runCommand("ffmpeg", [
|
|
3706
3771
|
"-i",
|
|
3707
3772
|
audioPath,
|
|
@@ -3718,7 +3783,7 @@ Please install manually: 'brew install whisper-cpp'`);
|
|
|
3718
3783
|
isTempFile = true;
|
|
3719
3784
|
}
|
|
3720
3785
|
try {
|
|
3721
|
-
const outputBase =
|
|
3786
|
+
const outputBase = path14.join(path14.dirname(audioPath), path14.basename(audioPath, path14.extname(audioPath)));
|
|
3722
3787
|
const baseArgs = [
|
|
3723
3788
|
"-m",
|
|
3724
3789
|
modelPath,
|
|
@@ -3761,19 +3826,30 @@ Please install manually: 'brew install whisper-cpp'`);
|
|
|
3761
3826
|
}
|
|
3762
3827
|
}
|
|
3763
3828
|
};
|
|
3829
|
+
function isNodeRuntime2() {
|
|
3830
|
+
return typeof process !== "undefined" && !!process.versions?.node;
|
|
3831
|
+
}
|
|
3764
3832
|
function runCommand(command, args, cwd) {
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3833
|
+
if (!isNodeRuntime2()) {
|
|
3834
|
+
return Promise.reject(new Error(`"${command}" requires a Node runtime with child_process support.`));
|
|
3835
|
+
}
|
|
3836
|
+
return new Promise(async (resolve, reject) => {
|
|
3837
|
+
try {
|
|
3838
|
+
const { spawn: spawn5 } = await import("node:child_process");
|
|
3839
|
+
const proc = spawn5(command, args, { cwd, stdio: "inherit" });
|
|
3840
|
+
proc.on("close", (code) => {
|
|
3841
|
+
if (code === 0) resolve();
|
|
3842
|
+
else reject(new Error(`${command} exited with code ${code}`));
|
|
3843
|
+
});
|
|
3844
|
+
proc.on("error", reject);
|
|
3845
|
+
} catch (err) {
|
|
3846
|
+
reject(err);
|
|
3847
|
+
}
|
|
3772
3848
|
});
|
|
3773
3849
|
}
|
|
3774
3850
|
function runWhisper(execPath, args) {
|
|
3775
3851
|
return new Promise((resolve, reject) => {
|
|
3776
|
-
const proc =
|
|
3852
|
+
const proc = spawn(execPath, args, { stdio: "inherit" });
|
|
3777
3853
|
proc.on("close", (code) => {
|
|
3778
3854
|
if (code !== 0) {
|
|
3779
3855
|
reject(new Error(`Whisper process exited with code ${code}`));
|
|
@@ -3837,24 +3913,37 @@ function roundTo3(value) {
|
|
|
3837
3913
|
|
|
3838
3914
|
// src/cli/services/audio-convert.ts
|
|
3839
3915
|
import fs15 from "fs";
|
|
3840
|
-
import
|
|
3841
|
-
import { spawn as spawn6 } from "node:child_process";
|
|
3916
|
+
import path15 from "path";
|
|
3842
3917
|
import { pathToFileURL } from "node:url";
|
|
3843
3918
|
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
|
3844
|
-
function
|
|
3919
|
+
function isNodeRuntime3() {
|
|
3845
3920
|
return typeof process !== "undefined" && !!process.versions?.node;
|
|
3846
3921
|
}
|
|
3847
|
-
|
|
3922
|
+
function buildCodecArgs(options) {
|
|
3923
|
+
if (options.format === "wav") {
|
|
3924
|
+
return ["-c:a", "pcm_s16le"];
|
|
3925
|
+
}
|
|
3926
|
+
if (typeof options.mp3Quality === "number") {
|
|
3927
|
+
return ["-c:a", "libmp3lame", "-q:a", String(options.mp3Quality)];
|
|
3928
|
+
}
|
|
3929
|
+
return ["-c:a", "libmp3lame", "-b:a", options.mp3Bitrate || "128k"];
|
|
3930
|
+
}
|
|
3931
|
+
async function convertWithNodeFfmpeg(options) {
|
|
3932
|
+
const { pcmPath, outputPath, sampleRate, channels, durationSec } = options;
|
|
3933
|
+
const codecArgs = buildCodecArgs(options);
|
|
3934
|
+
const { spawn: spawn5 } = await import("node:child_process");
|
|
3848
3935
|
await new Promise((resolve, reject) => {
|
|
3849
|
-
const ffmpeg =
|
|
3936
|
+
const ffmpeg = spawn5("ffmpeg", [
|
|
3850
3937
|
"-f",
|
|
3851
3938
|
"s16le",
|
|
3852
3939
|
"-ar",
|
|
3853
|
-
|
|
3940
|
+
String(sampleRate),
|
|
3854
3941
|
"-ac",
|
|
3855
|
-
|
|
3942
|
+
String(channels),
|
|
3856
3943
|
"-i",
|
|
3857
3944
|
pcmPath,
|
|
3945
|
+
...typeof durationSec === "number" ? ["-t", String(durationSec)] : [],
|
|
3946
|
+
...codecArgs,
|
|
3858
3947
|
"-y",
|
|
3859
3948
|
outputPath
|
|
3860
3949
|
], { stdio: "inherit" });
|
|
@@ -3865,41 +3954,42 @@ async function convertWithNodeFfmpeg(pcmPath, outputPath) {
|
|
|
3865
3954
|
ffmpeg.on("error", (err) => reject(err));
|
|
3866
3955
|
});
|
|
3867
3956
|
}
|
|
3868
|
-
async function convertWithWasm(
|
|
3957
|
+
async function convertWithWasm(options) {
|
|
3958
|
+
const { pcmPath, outputPath, sampleRate, channels, durationSec } = options;
|
|
3869
3959
|
const ffmpeg = new FFmpeg();
|
|
3870
3960
|
const baseDir = typeof process !== "undefined" && typeof process.cwd === "function" ? process.cwd() : ".";
|
|
3871
|
-
const coreDir =
|
|
3872
|
-
const coreURL = pathToFileURL(
|
|
3873
|
-
const wasmURL = pathToFileURL(
|
|
3874
|
-
const workerURL = pathToFileURL(
|
|
3961
|
+
const coreDir = path15.resolve(baseDir, "node_modules/@ffmpeg/core/dist");
|
|
3962
|
+
const coreURL = pathToFileURL(path15.join(coreDir, "ffmpeg-core.js")).toString();
|
|
3963
|
+
const wasmURL = pathToFileURL(path15.join(coreDir, "ffmpeg-core.wasm")).toString();
|
|
3964
|
+
const workerURL = pathToFileURL(path15.join(coreDir, "ffmpeg-core.worker.js")).toString();
|
|
3875
3965
|
await ffmpeg.load({ coreURL, wasmURL, workerURL });
|
|
3876
3966
|
const pcmBytes = await fs15.promises.readFile(pcmPath);
|
|
3877
3967
|
await ffmpeg.writeFile("input.pcm", pcmBytes);
|
|
3968
|
+
const codecArgs = buildCodecArgs(options);
|
|
3969
|
+
const outputName = options.format === "wav" ? "output.wav" : "output.mp3";
|
|
3878
3970
|
await ffmpeg.exec([
|
|
3879
3971
|
"-f",
|
|
3880
3972
|
"s16le",
|
|
3881
3973
|
"-ar",
|
|
3882
|
-
|
|
3974
|
+
String(sampleRate),
|
|
3883
3975
|
"-ac",
|
|
3884
|
-
|
|
3976
|
+
String(channels),
|
|
3885
3977
|
"-i",
|
|
3886
3978
|
"input.pcm",
|
|
3887
|
-
"-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
"128k",
|
|
3891
|
-
"output.mp3"
|
|
3979
|
+
...typeof durationSec === "number" ? ["-t", String(durationSec)] : [],
|
|
3980
|
+
...codecArgs,
|
|
3981
|
+
outputName
|
|
3892
3982
|
]);
|
|
3893
|
-
const output = await ffmpeg.readFile(
|
|
3983
|
+
const output = await ffmpeg.readFile(outputName);
|
|
3894
3984
|
await fs15.promises.writeFile(outputPath, output);
|
|
3895
3985
|
}
|
|
3896
|
-
async function
|
|
3897
|
-
const { pcmPath
|
|
3898
|
-
let useWasm = !
|
|
3986
|
+
async function convertPcmToAudio(options) {
|
|
3987
|
+
const { pcmPath } = options;
|
|
3988
|
+
let useWasm = !isNodeRuntime3();
|
|
3899
3989
|
try {
|
|
3900
3990
|
try {
|
|
3901
3991
|
if (!useWasm) {
|
|
3902
|
-
await convertWithNodeFfmpeg(
|
|
3992
|
+
await convertWithNodeFfmpeg(options);
|
|
3903
3993
|
return;
|
|
3904
3994
|
}
|
|
3905
3995
|
} catch (err) {
|
|
@@ -3910,7 +4000,7 @@ async function convertPcmToMp3(options) {
|
|
|
3910
4000
|
}
|
|
3911
4001
|
if (useWasm) {
|
|
3912
4002
|
try {
|
|
3913
|
-
await convertWithWasm(
|
|
4003
|
+
await convertWithWasm(options);
|
|
3914
4004
|
return;
|
|
3915
4005
|
} catch (err) {
|
|
3916
4006
|
throw new Error(
|
|
@@ -3930,6 +4020,15 @@ async function convertPcmToMp3(options) {
|
|
|
3930
4020
|
}
|
|
3931
4021
|
}
|
|
3932
4022
|
}
|
|
4023
|
+
async function convertPcmToMp3(options) {
|
|
4024
|
+
await convertPcmToAudio({
|
|
4025
|
+
...options,
|
|
4026
|
+
format: "mp3",
|
|
4027
|
+
sampleRate: 24e3,
|
|
4028
|
+
channels: 1,
|
|
4029
|
+
mp3Bitrate: "128k"
|
|
4030
|
+
});
|
|
4031
|
+
}
|
|
3933
4032
|
|
|
3934
4033
|
// src/cli/commands/audio.ts
|
|
3935
4034
|
var audioCommand = new Command6("generate:audio");
|
|
@@ -3954,9 +4053,9 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
|
|
|
3954
4053
|
console.log("\u{1F5E3}\uFE0F Generating speech with Gemini...");
|
|
3955
4054
|
const audioBuffer = await generateGeminiAudio(text, apiKey, options.voice);
|
|
3956
4055
|
const outputPath = resolveAssetOutputPath(options.output);
|
|
3957
|
-
const tempPcmPath =
|
|
4056
|
+
const tempPcmPath = path16.join(path16.dirname(outputPath), `temp_${Date.now()}.pcm`);
|
|
3958
4057
|
fs16.writeFileSync(tempPcmPath, audioBuffer);
|
|
3959
|
-
console.log(`\u{1F504} Converting raw PCM to ${
|
|
4058
|
+
console.log(`\u{1F504} Converting raw PCM to ${path16.extname(outputPath)}...`);
|
|
3960
4059
|
await convertPcmToMp3({
|
|
3961
4060
|
pcmPath: tempPcmPath,
|
|
3962
4061
|
outputPath
|
|
@@ -3967,7 +4066,7 @@ audioCommand.alias("audio").description("Generate audio from text using Gemini A
|
|
|
3967
4066
|
console.log("\u{1F50D} Aligning audio with Whisper...");
|
|
3968
4067
|
await WhisperService.ensureReady();
|
|
3969
4068
|
const metadata = await WhisperService.transcribe(outputPath);
|
|
3970
|
-
const metaPath = outputPath.replace(
|
|
4069
|
+
const metaPath = outputPath.replace(path16.extname(outputPath), ".json");
|
|
3971
4070
|
fs16.writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
|
|
3972
4071
|
console.log(`\u2705 Metadata saved: ${metaPath}`);
|
|
3973
4072
|
} catch (wErr) {
|
|
@@ -4019,8 +4118,7 @@ async function generateGeminiAudio(text, apiKey, voiceName) {
|
|
|
4019
4118
|
// src/cli/commands/bgm.ts
|
|
4020
4119
|
import { Command as Command7 } from "commander";
|
|
4021
4120
|
import fs17 from "fs";
|
|
4022
|
-
import
|
|
4023
|
-
import { spawn as spawn7 } from "child_process";
|
|
4121
|
+
import path17 from "path";
|
|
4024
4122
|
var bgmCommand = new Command7("generate:bgm");
|
|
4025
4123
|
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) => {
|
|
4026
4124
|
try {
|
|
@@ -4093,7 +4191,7 @@ function parseSeed(input) {
|
|
|
4093
4191
|
return value;
|
|
4094
4192
|
}
|
|
4095
4193
|
function inferOutputFormat(outputPath) {
|
|
4096
|
-
const ext =
|
|
4194
|
+
const ext = path17.extname(outputPath).toLowerCase();
|
|
4097
4195
|
if (ext === ".wav") return "wav";
|
|
4098
4196
|
if (ext === ".mp3" || !ext) return "mp3";
|
|
4099
4197
|
throw new Error("Invalid output format. Use .mp3 or .wav.");
|
|
@@ -4181,54 +4279,35 @@ async function generateGeminiMusicPcm(request) {
|
|
|
4181
4279
|
});
|
|
4182
4280
|
}
|
|
4183
4281
|
async function convertPcmToOutput(pcmBuffer, outputPath, format, durationSec) {
|
|
4184
|
-
const outputDir =
|
|
4282
|
+
const outputDir = path17.dirname(outputPath);
|
|
4185
4283
|
if (!fs17.existsSync(outputDir)) fs17.mkdirSync(outputDir, { recursive: true });
|
|
4186
|
-
const tempPcmPath =
|
|
4284
|
+
const tempPcmPath = path17.join(outputDir, `temp_bgm_${Date.now()}.pcm`);
|
|
4187
4285
|
fs17.writeFileSync(tempPcmPath, pcmBuffer);
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
"2",
|
|
4198
|
-
"-i",
|
|
4199
|
-
tempPcmPath,
|
|
4200
|
-
"-t",
|
|
4201
|
-
String(durationSec),
|
|
4202
|
-
...codecArgs,
|
|
4203
|
-
"-y",
|
|
4204
|
-
outputPath
|
|
4205
|
-
], { stdio: "inherit" });
|
|
4206
|
-
ffmpeg.on("close", (code) => {
|
|
4207
|
-
if (code === 0) resolve();
|
|
4208
|
-
else reject(new Error(`FFmpeg exited with code ${code}`));
|
|
4209
|
-
});
|
|
4210
|
-
ffmpeg.on("error", reject);
|
|
4211
|
-
});
|
|
4212
|
-
} finally {
|
|
4213
|
-
if (fs17.existsSync(tempPcmPath)) fs17.unlinkSync(tempPcmPath);
|
|
4214
|
-
}
|
|
4286
|
+
await convertPcmToAudio({
|
|
4287
|
+
pcmPath: tempPcmPath,
|
|
4288
|
+
outputPath,
|
|
4289
|
+
format,
|
|
4290
|
+
sampleRate: 48e3,
|
|
4291
|
+
channels: 2,
|
|
4292
|
+
durationSec,
|
|
4293
|
+
mp3Quality: format === "mp3" ? 2 : void 0
|
|
4294
|
+
});
|
|
4215
4295
|
}
|
|
4216
4296
|
|
|
4217
4297
|
// src/cli/commands/asset.ts
|
|
4218
4298
|
import { Command as Command8 } from "commander";
|
|
4219
4299
|
import fs18 from "fs";
|
|
4220
|
-
import
|
|
4221
|
-
import { spawn as spawn8 } from "child_process";
|
|
4300
|
+
import path18 from "path";
|
|
4222
4301
|
var assetCommand = new Command8("asset").description("Asset information and management");
|
|
4223
4302
|
assetCommand.command("info <file>").description("Show detailed information about an asset").action(async (file) => {
|
|
4224
|
-
const assetPath =
|
|
4303
|
+
const assetPath = path18.resolve(process.cwd(), "assets", file);
|
|
4225
4304
|
if (!fs18.existsSync(assetPath)) {
|
|
4226
4305
|
console.error(`Asset not found: ${file}`);
|
|
4227
4306
|
console.error(`Looked in: ${assetPath}`);
|
|
4228
4307
|
process.exit(1);
|
|
4229
4308
|
}
|
|
4230
4309
|
const stats = fs18.statSync(assetPath);
|
|
4231
|
-
const ext =
|
|
4310
|
+
const ext = path18.extname(file).toLowerCase();
|
|
4232
4311
|
console.log(`
|
|
4233
4312
|
Asset: ${file}`);
|
|
4234
4313
|
console.log(`Size: ${formatBytes2(stats.size)}`);
|
|
@@ -4251,7 +4330,7 @@ Asset: ${file}`);
|
|
|
4251
4330
|
console.log(`Channels: ${audioInfo.channels}`);
|
|
4252
4331
|
console.log(`Bitrate: ${audioInfo.bitrate}`);
|
|
4253
4332
|
const metadataFile = file.replace(ext, ".json");
|
|
4254
|
-
const metadataPath =
|
|
4333
|
+
const metadataPath = path18.resolve(process.cwd(), "assets", metadataFile);
|
|
4255
4334
|
if (fs18.existsSync(metadataPath)) {
|
|
4256
4335
|
console.log(`
|
|
4257
4336
|
Metadata: ${metadataFile}`);
|
|
@@ -4300,47 +4379,18 @@ async function getImageDimensions(filePath) {
|
|
|
4300
4379
|
}
|
|
4301
4380
|
}
|
|
4302
4381
|
async function getAudioInfo(filePath) {
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
"json",
|
|
4311
|
-
filePath
|
|
4312
|
-
]);
|
|
4313
|
-
let output = "";
|
|
4314
|
-
ffprobe.stdout.on("data", (data) => {
|
|
4315
|
-
output += data.toString();
|
|
4316
|
-
});
|
|
4317
|
-
ffprobe.on("close", (code) => {
|
|
4318
|
-
if (code !== 0) {
|
|
4319
|
-
reject(new Error("ffprobe failed"));
|
|
4320
|
-
return;
|
|
4321
|
-
}
|
|
4322
|
-
try {
|
|
4323
|
-
const data = JSON.parse(output);
|
|
4324
|
-
const stream = data.streams?.[0] || {};
|
|
4325
|
-
const format = data.format || {};
|
|
4326
|
-
resolve({
|
|
4327
|
-
duration: format.duration ? parseFloat(format.duration).toFixed(2) : "unknown",
|
|
4328
|
-
codec: stream.codec_name || "unknown",
|
|
4329
|
-
sampleRate: stream.sample_rate ? `${stream.sample_rate} Hz` : "unknown",
|
|
4330
|
-
channels: stream.channels ? stream.channels === 1 ? "mono" : stream.channels === 2 ? "stereo" : `${stream.channels}ch` : "unknown",
|
|
4331
|
-
bitrate: format.bit_rate ? `${Math.round(parseInt(format.bit_rate) / 1e3)} kbps` : "unknown"
|
|
4332
|
-
});
|
|
4333
|
-
} catch (err) {
|
|
4334
|
-
reject(err);
|
|
4335
|
-
}
|
|
4336
|
-
});
|
|
4337
|
-
ffprobe.on("error", reject);
|
|
4338
|
-
});
|
|
4382
|
+
const metadata = await FFprobeService.getMetadata(filePath);
|
|
4383
|
+
const duration = metadata.duration ? metadata.duration.toFixed(2) : "unknown";
|
|
4384
|
+
const codec = metadata.audioCodec || metadata.format || "unknown";
|
|
4385
|
+
const sampleRate = metadata.sampleRate ? `${metadata.sampleRate} Hz` : "unknown";
|
|
4386
|
+
const channels = metadata.audioChannels ? metadata.audioChannels === 1 ? "mono" : metadata.audioChannels === 2 ? "stereo" : `${metadata.audioChannels}ch` : "unknown";
|
|
4387
|
+
const bitrate = metadata.bitrate ? `${Math.round(metadata.bitrate / 1e3)} kbps` : "unknown";
|
|
4388
|
+
return { duration, codec, sampleRate, channels, bitrate };
|
|
4339
4389
|
}
|
|
4340
4390
|
|
|
4341
4391
|
// src/cli/index.ts
|
|
4342
4392
|
import open2 from "open";
|
|
4343
|
-
import
|
|
4393
|
+
import path22 from "path";
|
|
4344
4394
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
4345
4395
|
import fs21 from "fs";
|
|
4346
4396
|
|
|
@@ -4759,7 +4809,7 @@ addEntityCommand.description("Add an entity to an existing scene").option("--pro
|
|
|
4759
4809
|
|
|
4760
4810
|
// src/cli/services/telemetry.ts
|
|
4761
4811
|
import os2 from "os";
|
|
4762
|
-
import
|
|
4812
|
+
import path19 from "path";
|
|
4763
4813
|
import { createHash as createHash2 } from "crypto";
|
|
4764
4814
|
var POSTHOG_CAPTURE_URL = "https://us.i.posthog.com/capture/";
|
|
4765
4815
|
var TELEMETRY_DISABLED_VALUES = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
@@ -4771,7 +4821,7 @@ var PostHogTelemetryService = class {
|
|
|
4771
4821
|
if (!this.enabled) return;
|
|
4772
4822
|
this.capture("cli_feedback", {
|
|
4773
4823
|
details: String(details).slice(0, 2e3),
|
|
4774
|
-
cwd_basename:
|
|
4824
|
+
cwd_basename: path19.basename(process.cwd())
|
|
4775
4825
|
});
|
|
4776
4826
|
}
|
|
4777
4827
|
apiKey;
|
|
@@ -4794,7 +4844,7 @@ var PostHogTelemetryService = class {
|
|
|
4794
4844
|
command_name: this.getCommandPath(actionCommand),
|
|
4795
4845
|
command_aliases: actionCommand.aliases(),
|
|
4796
4846
|
options_used: this.getUsedOptionNames(actionCommand),
|
|
4797
|
-
cwd_basename:
|
|
4847
|
+
cwd_basename: path19.basename(process.cwd()),
|
|
4798
4848
|
node_version: process.version,
|
|
4799
4849
|
platform: process.platform,
|
|
4800
4850
|
arch: process.arch
|
|
@@ -4807,7 +4857,7 @@ var PostHogTelemetryService = class {
|
|
|
4807
4857
|
status,
|
|
4808
4858
|
duration_ms: Math.max(0, Math.round(durationMs)),
|
|
4809
4859
|
error_message: errorMessage ? String(errorMessage).slice(0, 300) : void 0,
|
|
4810
|
-
cwd_basename:
|
|
4860
|
+
cwd_basename: path19.basename(process.cwd())
|
|
4811
4861
|
});
|
|
4812
4862
|
}
|
|
4813
4863
|
getCommandPath(command) {
|
|
@@ -4861,25 +4911,25 @@ var PostHogTelemetryService = class {
|
|
|
4861
4911
|
// src/cli/commands/taste.ts
|
|
4862
4912
|
import { Command as Command12 } from "commander";
|
|
4863
4913
|
import fs19 from "node:fs";
|
|
4864
|
-
import
|
|
4914
|
+
import path20 from "node:path";
|
|
4865
4915
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4866
4916
|
import open from "open";
|
|
4867
4917
|
var __filename4 = fileURLToPath4(import.meta.url);
|
|
4868
|
-
var __dirname4 =
|
|
4918
|
+
var __dirname4 = path20.dirname(__filename4);
|
|
4869
4919
|
function resolveStaticRoot() {
|
|
4870
|
-
let staticRoot =
|
|
4920
|
+
let staticRoot = path20.resolve(__dirname4, "../../../dist/ui");
|
|
4871
4921
|
if (!fs19.existsSync(staticRoot)) {
|
|
4872
|
-
staticRoot =
|
|
4922
|
+
staticRoot = path20.resolve(__dirname4, "../../ui");
|
|
4873
4923
|
}
|
|
4874
4924
|
return staticRoot;
|
|
4875
4925
|
}
|
|
4876
4926
|
function prepareWorkingDirectory(pathArg) {
|
|
4877
4927
|
if (!pathArg) return;
|
|
4878
|
-
const targetPath =
|
|
4928
|
+
const targetPath = path20.resolve(process.cwd(), pathArg);
|
|
4879
4929
|
if (fs19.existsSync(targetPath)) {
|
|
4880
4930
|
const stats = fs19.statSync(targetPath);
|
|
4881
4931
|
if (stats.isFile()) {
|
|
4882
|
-
process.chdir(
|
|
4932
|
+
process.chdir(path20.dirname(targetPath));
|
|
4883
4933
|
return;
|
|
4884
4934
|
}
|
|
4885
4935
|
if (stats.isDirectory()) {
|
|
@@ -4887,8 +4937,8 @@ function prepareWorkingDirectory(pathArg) {
|
|
|
4887
4937
|
return;
|
|
4888
4938
|
}
|
|
4889
4939
|
}
|
|
4890
|
-
if (
|
|
4891
|
-
const dir =
|
|
4940
|
+
if (path20.extname(pathArg)) {
|
|
4941
|
+
const dir = path20.dirname(targetPath);
|
|
4892
4942
|
fs19.mkdirSync(dir, { recursive: true });
|
|
4893
4943
|
process.chdir(dir);
|
|
4894
4944
|
return;
|
|
@@ -5181,7 +5231,7 @@ projectCommand.command("list").description("List projects").option("--api-url <u
|
|
|
5181
5231
|
|
|
5182
5232
|
// src/cli/commands/scene.ts
|
|
5183
5233
|
import fs20 from "node:fs";
|
|
5184
|
-
import
|
|
5234
|
+
import path21 from "node:path";
|
|
5185
5235
|
import { Command as Command15 } from "commander";
|
|
5186
5236
|
var sceneCommand = new Command15("scene").description("Manage scenes");
|
|
5187
5237
|
sceneCommand.command("list").option("--project-id <id>", "Project ID").option("--api-url <url>", "API base URL (e.g. http://localhost:3331)").action(async (options) => {
|
|
@@ -5213,7 +5263,7 @@ sceneCommand.command("create").option("--project-id <id>", "Project ID").option(
|
|
|
5213
5263
|
});
|
|
5214
5264
|
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
5265
|
try {
|
|
5216
|
-
const filePath =
|
|
5266
|
+
const filePath = path21.resolve(process.cwd(), String(options.file));
|
|
5217
5267
|
if (!fs20.existsSync(filePath)) {
|
|
5218
5268
|
throw new Error(`File not found: ${filePath}`);
|
|
5219
5269
|
}
|
|
@@ -5224,10 +5274,10 @@ sceneCommand.command("import").option("--project-id <id>", "Project ID").require
|
|
|
5224
5274
|
const projectId = resolveProjectId(options.projectId);
|
|
5225
5275
|
const apiUrl = resolveApiUrl(options.apiUrl);
|
|
5226
5276
|
const scene = apiUrl ? await new SceneApiClient(apiUrl).createScene(projectId, {
|
|
5227
|
-
name: options.name ? String(options.name) :
|
|
5277
|
+
name: options.name ? String(options.name) : path21.basename(filePath, path21.extname(filePath)),
|
|
5228
5278
|
scene: { meta: parsed.meta, entities: parsed.entities }
|
|
5229
5279
|
}) : await createSceneStoreBundle("file").scenes.createScene(projectId, {
|
|
5230
|
-
name: options.name ? String(options.name) :
|
|
5280
|
+
name: options.name ? String(options.name) : path21.basename(filePath, path21.extname(filePath)),
|
|
5231
5281
|
scene: { meta: parsed.meta, entities: parsed.entities }
|
|
5232
5282
|
});
|
|
5233
5283
|
writeFlowState({ projectId, sceneId: scene.id, apiUrl });
|
|
@@ -5246,8 +5296,8 @@ sceneCommand.command("export").option("--project-id <id>", "Project ID").option(
|
|
|
5246
5296
|
if (!scene) {
|
|
5247
5297
|
throw new Error(`Scene not found: ${sceneId}`);
|
|
5248
5298
|
}
|
|
5249
|
-
const out =
|
|
5250
|
-
fs20.mkdirSync(
|
|
5299
|
+
const out = path21.resolve(process.cwd(), String(options.file));
|
|
5300
|
+
fs20.mkdirSync(path21.dirname(out), { recursive: true });
|
|
5251
5301
|
fs20.writeFileSync(out, JSON.stringify({ meta: scene.meta, entities: scene.entities }, null, 2), "utf-8");
|
|
5252
5302
|
writeFlowState({ projectId, sceneId, apiUrl });
|
|
5253
5303
|
console.log(`Scene exported to ${out}`);
|
|
@@ -5267,11 +5317,11 @@ sceneCommand.command("use").description("Set current flow state for project/scen
|
|
|
5267
5317
|
|
|
5268
5318
|
// src/cli/index.ts
|
|
5269
5319
|
var __filename5 = fileURLToPath5(import.meta.url);
|
|
5270
|
-
var __dirname5 =
|
|
5320
|
+
var __dirname5 = path22.dirname(__filename5);
|
|
5271
5321
|
var program = new Command16();
|
|
5272
5322
|
var telemetry = new PostHogTelemetryService();
|
|
5273
5323
|
var commandStartTimes = /* @__PURE__ */ new WeakMap();
|
|
5274
|
-
program.name("feedeas").description("CLI for Feedeas - AI-native video creation tool").version("0.1.0-alpha.
|
|
5324
|
+
program.name("feedeas").description("CLI for Feedeas - AI-native video creation tool").version("0.1.0-alpha.19");
|
|
5275
5325
|
program.hook("preAction", (_thisCommand, actionCommand) => {
|
|
5276
5326
|
commandStartTimes.set(actionCommand, Date.now());
|
|
5277
5327
|
telemetry.trackCommandStarted(actionCommand);
|
|
@@ -5298,9 +5348,9 @@ program.command("edit").alias("start").alias("init").description("Start the Feed
|
|
|
5298
5348
|
})();
|
|
5299
5349
|
const cwd = process.cwd();
|
|
5300
5350
|
console.log(`Starting Feedeas in ${cwd}...`);
|
|
5301
|
-
let staticRoot =
|
|
5351
|
+
let staticRoot = path22.resolve(__dirname5, "../../dist/ui");
|
|
5302
5352
|
if (!fs21.existsSync(staticRoot)) {
|
|
5303
|
-
staticRoot =
|
|
5353
|
+
staticRoot = path22.resolve(__dirname5, "../ui");
|
|
5304
5354
|
}
|
|
5305
5355
|
if (!fs21.existsSync(staticRoot)) {
|
|
5306
5356
|
console.warn(`Warning: UI assets not found at ${staticRoot}. Did you run 'bun run build'?`);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "feedeas",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.1.0-alpha.
|
|
5
|
+
"version": "0.1.0-alpha.19",
|
|
6
6
|
"devDependencies": {
|
|
7
7
|
"@tailwindcss/vite": "^4.1.18",
|
|
8
8
|
"@types/bun": "latest",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"hono": "^4.11.4",
|
|
29
29
|
"image-size": "^2.0.2",
|
|
30
30
|
"lucide-react": "^0.562.0",
|
|
31
|
+
"music-metadata": "^8.3.0",
|
|
31
32
|
"open": "^11.0.0",
|
|
32
33
|
"playwright-core": "^1.58.2",
|
|
33
34
|
"react": "^19.2.3",
|