vidpipe 1.3.17 → 1.3.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1340 -260
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.js +1452 -483
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -253,10 +253,10 @@ function closeFileDescriptor(fd) {
|
|
|
253
253
|
closeSync(fd);
|
|
254
254
|
}
|
|
255
255
|
async function makeTempDir(prefix) {
|
|
256
|
-
return new Promise((
|
|
256
|
+
return new Promise((resolve4, reject) => {
|
|
257
257
|
tmp.dir({ prefix, mode: 448 }, (err, path) => {
|
|
258
258
|
if (err) reject(err);
|
|
259
|
-
else
|
|
259
|
+
else resolve4(path);
|
|
260
260
|
});
|
|
261
261
|
});
|
|
262
262
|
}
|
|
@@ -752,11 +752,12 @@ var init_types = __esm({
|
|
|
752
752
|
{ stage: "medium-clips" /* MediumClips */, name: "Medium Clips", stageNumber: 9 },
|
|
753
753
|
{ stage: "chapters" /* Chapters */, name: "Chapters", stageNumber: 10 },
|
|
754
754
|
{ stage: "summary" /* Summary */, name: "Summary", stageNumber: 11 },
|
|
755
|
-
{ stage: "
|
|
756
|
-
{ stage: "
|
|
757
|
-
{ stage: "
|
|
758
|
-
{ stage: "
|
|
759
|
-
{ stage: "
|
|
755
|
+
{ stage: "idea-discovery" /* IdeaDiscovery */, name: "Idea Discovery", stageNumber: 12 },
|
|
756
|
+
{ stage: "social-media" /* SocialMedia */, name: "Social Media", stageNumber: 13 },
|
|
757
|
+
{ stage: "short-posts" /* ShortPosts */, name: "Short Posts", stageNumber: 14 },
|
|
758
|
+
{ stage: "medium-clip-posts" /* MediumClipPosts */, name: "Medium Clip Posts", stageNumber: 15 },
|
|
759
|
+
{ stage: "queue-build" /* QueueBuild */, name: "Queue Build", stageNumber: 16 },
|
|
760
|
+
{ stage: "blog" /* Blog */, name: "Blog", stageNumber: 17 }
|
|
760
761
|
];
|
|
761
762
|
TOTAL_STAGES = PIPELINE_STAGES.length;
|
|
762
763
|
PLATFORM_CHAR_LIMITS = {
|
|
@@ -782,12 +783,12 @@ var init_ffmpeg = __esm({
|
|
|
782
783
|
import { execFile as nodeExecFile, execSync as nodeExecSync, spawnSync as nodeSpawnSync } from "child_process";
|
|
783
784
|
import { createRequire } from "module";
|
|
784
785
|
function execCommand(cmd, args, opts) {
|
|
785
|
-
return new Promise((
|
|
786
|
+
return new Promise((resolve4, reject) => {
|
|
786
787
|
nodeExecFile(cmd, args, { ...opts, encoding: "utf-8" }, (error, stdout, stderr) => {
|
|
787
788
|
if (error) {
|
|
788
789
|
reject(Object.assign(error, { stdout: String(stdout ?? ""), stderr: String(stderr ?? "") }));
|
|
789
790
|
} else {
|
|
790
|
-
|
|
791
|
+
resolve4({ stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
791
792
|
}
|
|
792
793
|
});
|
|
793
794
|
});
|
|
@@ -851,11 +852,11 @@ function createFFmpeg(input) {
|
|
|
851
852
|
return cmd;
|
|
852
853
|
}
|
|
853
854
|
function ffprobe(filePath) {
|
|
854
|
-
return new Promise((
|
|
855
|
+
return new Promise((resolve4, reject) => {
|
|
855
856
|
default3.setFfprobePath(getFFprobePath());
|
|
856
857
|
default3.ffprobe(filePath, (err, data) => {
|
|
857
858
|
if (err) reject(err);
|
|
858
|
-
else
|
|
859
|
+
else resolve4(data);
|
|
859
860
|
});
|
|
860
861
|
});
|
|
861
862
|
}
|
|
@@ -878,7 +879,7 @@ async function extractAudio(videoPath, outputPath, options = {}) {
|
|
|
878
879
|
const outputDir = dirname(outputPath);
|
|
879
880
|
await ensureDirectory(outputDir);
|
|
880
881
|
logger_default.info(`Extracting audio (${format}): ${videoPath} \u2192 ${outputPath}`);
|
|
881
|
-
return new Promise((
|
|
882
|
+
return new Promise((resolve4, reject) => {
|
|
882
883
|
const command = createFFmpeg(videoPath).noVideo().audioChannels(1);
|
|
883
884
|
if (format === "mp3") {
|
|
884
885
|
command.audioCodec("libmp3lame").audioBitrate("64k").audioFrequency(16e3);
|
|
@@ -887,7 +888,7 @@ async function extractAudio(videoPath, outputPath, options = {}) {
|
|
|
887
888
|
}
|
|
888
889
|
command.output(outputPath).on("end", () => {
|
|
889
890
|
logger_default.info(`Audio extraction complete: ${outputPath}`);
|
|
890
|
-
|
|
891
|
+
resolve4(outputPath);
|
|
891
892
|
}).on("error", (err) => {
|
|
892
893
|
logger_default.error(`Audio extraction failed: ${err.message}`);
|
|
893
894
|
reject(new Error(`Audio extraction failed: ${err.message}`));
|
|
@@ -913,8 +914,8 @@ async function splitAudioIntoChunks(audioPath, maxChunkSizeMB = 24) {
|
|
|
913
914
|
const startTime = i * chunkDuration;
|
|
914
915
|
const chunkPath = `${base}_chunk${i}${ext}`;
|
|
915
916
|
chunkPaths.push(chunkPath);
|
|
916
|
-
await new Promise((
|
|
917
|
-
const cmd = createFFmpeg(audioPath).setStartTime(startTime).setDuration(chunkDuration).audioCodec("copy").output(chunkPath).on("end", () =>
|
|
917
|
+
await new Promise((resolve4, reject) => {
|
|
918
|
+
const cmd = createFFmpeg(audioPath).setStartTime(startTime).setDuration(chunkDuration).audioCodec("copy").output(chunkPath).on("end", () => resolve4()).on("error", (err) => reject(new Error(`Chunk split failed: ${err.message}`)));
|
|
918
919
|
cmd.run();
|
|
919
920
|
});
|
|
920
921
|
logger_default.info(`Created chunk ${i + 1}/${numChunks}: ${chunkPath}`);
|
|
@@ -941,19 +942,19 @@ var init_audioExtraction = __esm({
|
|
|
941
942
|
|
|
942
943
|
// src/L2-clients/ffmpeg/clipExtraction.ts
|
|
943
944
|
async function getVideoFps(videoPath) {
|
|
944
|
-
return new Promise((
|
|
945
|
+
return new Promise((resolve4) => {
|
|
945
946
|
execFileRaw(
|
|
946
947
|
ffprobePath,
|
|
947
948
|
["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate", "-of", "csv=p=0", videoPath],
|
|
948
949
|
{ timeout: 5e3 },
|
|
949
950
|
(error, stdout) => {
|
|
950
951
|
if (error || !stdout.trim()) {
|
|
951
|
-
|
|
952
|
+
resolve4(DEFAULT_FPS);
|
|
952
953
|
return;
|
|
953
954
|
}
|
|
954
955
|
const parts = stdout.trim().split("/");
|
|
955
956
|
const fps = parts.length === 2 ? parseInt(parts[0]) / parseInt(parts[1]) : parseFloat(stdout.trim());
|
|
956
|
-
|
|
957
|
+
resolve4(isFinite(fps) && fps > 0 ? Math.round(fps) : DEFAULT_FPS);
|
|
957
958
|
}
|
|
958
959
|
);
|
|
959
960
|
});
|
|
@@ -965,10 +966,10 @@ async function extractClip(videoPath, start, end, outputPath, buffer = 1) {
|
|
|
965
966
|
const bufferedEnd = end + buffer;
|
|
966
967
|
const duration = bufferedEnd - bufferedStart;
|
|
967
968
|
logger_default.info(`Extracting clip [${start}s\u2013${end}s] (buffered: ${bufferedStart.toFixed(2)}s\u2013${bufferedEnd.toFixed(2)}s) \u2192 ${outputPath}`);
|
|
968
|
-
return new Promise((
|
|
969
|
+
return new Promise((resolve4, reject) => {
|
|
969
970
|
createFFmpeg(videoPath).setStartTime(bufferedStart).setDuration(duration).outputOptions(["-c:v", "libx264", "-preset", "ultrafast", "-crf", "23", "-threads", "4", "-c:a", "aac", "-b:a", "128k"]).output(outputPath).on("end", () => {
|
|
970
971
|
logger_default.info(`Clip extraction complete: ${outputPath}`);
|
|
971
|
-
|
|
972
|
+
resolve4(outputPath);
|
|
972
973
|
}).on("error", (err) => {
|
|
973
974
|
logger_default.error(`Clip extraction failed: ${err.message}`);
|
|
974
975
|
reject(new Error(`Clip extraction failed: ${err.message}`));
|
|
@@ -996,8 +997,8 @@ async function extractCompositeClip(videoPath, segments, outputPath, buffer = 1)
|
|
|
996
997
|
const bufferedStart = Math.max(0, seg.start - buffer);
|
|
997
998
|
const bufferedEnd = seg.end + buffer;
|
|
998
999
|
logger_default.info(`Extracting segment ${i + 1}/${segments.length} [${seg.start}s\u2013${seg.end}s] (buffered: ${bufferedStart.toFixed(2)}s\u2013${bufferedEnd.toFixed(2)}s)`);
|
|
999
|
-
await new Promise((
|
|
1000
|
-
createFFmpeg(videoPath).setStartTime(bufferedStart).setDuration(bufferedEnd - bufferedStart).outputOptions(["-threads", "4", "-preset", "ultrafast"]).output(tempPath).on("end", () =>
|
|
1000
|
+
await new Promise((resolve4, reject) => {
|
|
1001
|
+
createFFmpeg(videoPath).setStartTime(bufferedStart).setDuration(bufferedEnd - bufferedStart).outputOptions(["-threads", "4", "-preset", "ultrafast"]).output(tempPath).on("end", () => resolve4()).on("error", (err) => reject(new Error(`Segment ${i} extraction failed: ${err.message}`))).run();
|
|
1001
1002
|
});
|
|
1002
1003
|
}
|
|
1003
1004
|
concatListFile = default2.fileSync({ dir: tempDir, postfix: ".txt", prefix: "concat-" });
|
|
@@ -1006,8 +1007,8 @@ async function extractCompositeClip(videoPath, segments, outputPath, buffer = 1)
|
|
|
1006
1007
|
await writeTextFile(concatListPath, listContent);
|
|
1007
1008
|
closeFileDescriptor(concatListFile.fd);
|
|
1008
1009
|
logger_default.info(`Concatenating ${segments.length} segments \u2192 ${outputPath}`);
|
|
1009
|
-
await new Promise((
|
|
1010
|
-
createFFmpeg().input(concatListPath).inputOptions(["-f", "concat", "-safe", "0"]).outputOptions(["-c:v", "libx264", "-preset", "ultrafast", "-crf", "23", "-threads", "4", "-c:a", "aac"]).output(outputPath).on("end", () =>
|
|
1010
|
+
await new Promise((resolve4, reject) => {
|
|
1011
|
+
createFFmpeg().input(concatListPath).inputOptions(["-f", "concat", "-safe", "0"]).outputOptions(["-c:v", "libx264", "-preset", "ultrafast", "-crf", "23", "-threads", "4", "-c:a", "aac"]).output(outputPath).on("end", () => resolve4()).on("error", (err) => reject(new Error(`Concat failed: ${err.message}`))).run();
|
|
1011
1012
|
});
|
|
1012
1013
|
logger_default.info(`Composite clip complete: ${outputPath}`);
|
|
1013
1014
|
return outputPath;
|
|
@@ -1097,7 +1098,7 @@ async function extractCompositeClipWithTransitions(videoPath, segments, outputPa
|
|
|
1097
1098
|
outputPath
|
|
1098
1099
|
];
|
|
1099
1100
|
logger_default.info(`[ClipExtraction] Compositing ${segments.length} segments with xfade transitions \u2192 ${outputPath}`);
|
|
1100
|
-
return new Promise((
|
|
1101
|
+
return new Promise((resolve4, reject) => {
|
|
1101
1102
|
execFileRaw(ffmpegPath, args, { maxBuffer: 50 * 1024 * 1024 }, (error, _stdout, stderr) => {
|
|
1102
1103
|
if (error) {
|
|
1103
1104
|
logger_default.error(`[ClipExtraction] xfade composite failed: ${stderr}`);
|
|
@@ -1105,7 +1106,7 @@ async function extractCompositeClipWithTransitions(videoPath, segments, outputPa
|
|
|
1105
1106
|
return;
|
|
1106
1107
|
}
|
|
1107
1108
|
logger_default.info(`[ClipExtraction] xfade composite complete: ${outputPath}`);
|
|
1108
|
-
|
|
1109
|
+
resolve4(outputPath);
|
|
1109
1110
|
});
|
|
1110
1111
|
});
|
|
1111
1112
|
}
|
|
@@ -1180,7 +1181,7 @@ async function singlePassEdit(inputPath, keepSegments, outputPath) {
|
|
|
1180
1181
|
outputPath
|
|
1181
1182
|
];
|
|
1182
1183
|
logger_default.info(`[SinglePassEdit] Editing ${keepSegments.length} segments \u2192 ${outputPath}`);
|
|
1183
|
-
return new Promise((
|
|
1184
|
+
return new Promise((resolve4, reject) => {
|
|
1184
1185
|
execFileRaw(ffmpegPath2, args, { maxBuffer: 50 * 1024 * 1024 }, (error, _stdout, stderr) => {
|
|
1185
1186
|
if (error) {
|
|
1186
1187
|
logger_default.error(`[SinglePassEdit] FFmpeg failed: ${stderr}`);
|
|
@@ -1188,7 +1189,7 @@ async function singlePassEdit(inputPath, keepSegments, outputPath) {
|
|
|
1188
1189
|
return;
|
|
1189
1190
|
}
|
|
1190
1191
|
logger_default.info(`[SinglePassEdit] Complete: ${outputPath}`);
|
|
1191
|
-
|
|
1192
|
+
resolve4(outputPath);
|
|
1192
1193
|
});
|
|
1193
1194
|
});
|
|
1194
1195
|
}
|
|
@@ -1247,7 +1248,7 @@ async function burnCaptions(videoPath, assPath, outputPath) {
|
|
|
1247
1248
|
"4",
|
|
1248
1249
|
tempOutput
|
|
1249
1250
|
];
|
|
1250
|
-
return new Promise((
|
|
1251
|
+
return new Promise((resolve4, reject) => {
|
|
1251
1252
|
execFileRaw(ffmpegPath3, args, { cwd: workDir, maxBuffer: 10 * 1024 * 1024 }, async (error, _stdout, stderr) => {
|
|
1252
1253
|
const cleanup = async () => {
|
|
1253
1254
|
const files = await listDirectory(workDir).catch(() => []);
|
|
@@ -1271,7 +1272,7 @@ async function burnCaptions(videoPath, assPath, outputPath) {
|
|
|
1271
1272
|
}
|
|
1272
1273
|
await cleanup();
|
|
1273
1274
|
logger_default.info(`Captions burned: ${outputPath}`);
|
|
1274
|
-
|
|
1275
|
+
resolve4(outputPath);
|
|
1275
1276
|
});
|
|
1276
1277
|
});
|
|
1277
1278
|
}
|
|
@@ -1292,7 +1293,7 @@ var init_captionBurning = __esm({
|
|
|
1292
1293
|
// src/L2-clients/ffmpeg/silenceDetection.ts
|
|
1293
1294
|
async function detectSilence(audioPath, minDuration = 1, noiseThreshold = "-30dB") {
|
|
1294
1295
|
logger_default.info(`Detecting silence in: ${audioPath} (min=${minDuration}s, threshold=${noiseThreshold})`);
|
|
1295
|
-
return new Promise((
|
|
1296
|
+
return new Promise((resolve4, reject) => {
|
|
1296
1297
|
const regions = [];
|
|
1297
1298
|
let stderr = "";
|
|
1298
1299
|
createFFmpeg(audioPath).audioFilters(`silencedetect=noise=${noiseThreshold}:d=${minDuration}`).format("null").output("-").on("stderr", (line) => {
|
|
@@ -1322,7 +1323,7 @@ async function detectSilence(audioPath, minDuration = 1, noiseThreshold = "-30dB
|
|
|
1322
1323
|
logger_default.info(`Sample silence regions: ${validRegions.slice(0, 3).map((r) => `${r.start.toFixed(1)}s-${r.end.toFixed(1)}s (${r.duration.toFixed(2)}s)`).join(", ")}`);
|
|
1323
1324
|
}
|
|
1324
1325
|
logger_default.info(`Detected ${validRegions.length} silence regions`);
|
|
1325
|
-
|
|
1326
|
+
resolve4(validRegions);
|
|
1326
1327
|
}).on("error", (err) => {
|
|
1327
1328
|
logger_default.error(`Silence detection failed: ${err.message}`);
|
|
1328
1329
|
reject(new Error(`Silence detection failed: ${err.message}`));
|
|
@@ -1342,10 +1343,10 @@ async function captureFrame(videoPath, timestamp, outputPath) {
|
|
|
1342
1343
|
const outputDir = dirname(outputPath);
|
|
1343
1344
|
await ensureDirectory(outputDir);
|
|
1344
1345
|
logger_default.info(`Capturing frame at ${timestamp}s \u2192 ${outputPath}`);
|
|
1345
|
-
return new Promise((
|
|
1346
|
+
return new Promise((resolve4, reject) => {
|
|
1346
1347
|
createFFmpeg(videoPath).seekInput(timestamp).frames(1).output(outputPath).on("end", () => {
|
|
1347
1348
|
logger_default.info(`Frame captured: ${outputPath}`);
|
|
1348
|
-
|
|
1349
|
+
resolve4(outputPath);
|
|
1349
1350
|
}).on("error", (err) => {
|
|
1350
1351
|
logger_default.error(`Frame capture failed: ${err.message}`);
|
|
1351
1352
|
reject(new Error(`Frame capture failed: ${err.message}`));
|
|
@@ -1373,7 +1374,7 @@ var init_media = __esm({
|
|
|
1373
1374
|
|
|
1374
1375
|
// src/L2-clients/ffmpeg/faceDetection.ts
|
|
1375
1376
|
async function getVideoDuration(videoPath) {
|
|
1376
|
-
return new Promise((
|
|
1377
|
+
return new Promise((resolve4, reject) => {
|
|
1377
1378
|
execFileRaw(
|
|
1378
1379
|
ffprobePath2,
|
|
1379
1380
|
["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", videoPath],
|
|
@@ -1383,13 +1384,13 @@ async function getVideoDuration(videoPath) {
|
|
|
1383
1384
|
reject(new Error(`ffprobe failed: ${error.message}`));
|
|
1384
1385
|
return;
|
|
1385
1386
|
}
|
|
1386
|
-
|
|
1387
|
+
resolve4(parseFloat(stdout.trim()));
|
|
1387
1388
|
}
|
|
1388
1389
|
);
|
|
1389
1390
|
});
|
|
1390
1391
|
}
|
|
1391
1392
|
async function getVideoResolution(videoPath) {
|
|
1392
|
-
return new Promise((
|
|
1393
|
+
return new Promise((resolve4, reject) => {
|
|
1393
1394
|
execFileRaw(
|
|
1394
1395
|
ffprobePath2,
|
|
1395
1396
|
[
|
|
@@ -1410,7 +1411,7 @@ async function getVideoResolution(videoPath) {
|
|
|
1410
1411
|
return;
|
|
1411
1412
|
}
|
|
1412
1413
|
const [w, h] = stdout.trim().split(",").map(Number);
|
|
1413
|
-
|
|
1414
|
+
resolve4({ width: w, height: h });
|
|
1414
1415
|
}
|
|
1415
1416
|
);
|
|
1416
1417
|
});
|
|
@@ -1428,7 +1429,7 @@ async function extractSampleFrames(videoPath, tempDir) {
|
|
|
1428
1429
|
for (let i = 0; i < timestamps.length; i++) {
|
|
1429
1430
|
const framePath = join(tempDir, `frame_${i}.png`);
|
|
1430
1431
|
framePaths.push(framePath);
|
|
1431
|
-
await new Promise((
|
|
1432
|
+
await new Promise((resolve4, reject) => {
|
|
1432
1433
|
execFileRaw(
|
|
1433
1434
|
ffmpegPath4,
|
|
1434
1435
|
[
|
|
@@ -1451,7 +1452,7 @@ async function extractSampleFrames(videoPath, tempDir) {
|
|
|
1451
1452
|
reject(new Error(`Frame extraction failed at ${timestamps[i]}s: ${error.message}`));
|
|
1452
1453
|
return;
|
|
1453
1454
|
}
|
|
1454
|
-
|
|
1455
|
+
resolve4();
|
|
1455
1456
|
}
|
|
1456
1457
|
);
|
|
1457
1458
|
});
|
|
@@ -1816,7 +1817,7 @@ async function convertAspectRatio(inputPath, outputPath, targetRatio, options =
|
|
|
1816
1817
|
"4",
|
|
1817
1818
|
outputPath
|
|
1818
1819
|
];
|
|
1819
|
-
return new Promise((
|
|
1820
|
+
return new Promise((resolve4, reject) => {
|
|
1820
1821
|
execFileRaw(ffmpegPath5, args, { maxBuffer: 10 * 1024 * 1024 }, (error, _stdout, stderr) => {
|
|
1821
1822
|
if (error) {
|
|
1822
1823
|
logger_default.error(`Aspect ratio conversion failed: ${stderr || error.message}`);
|
|
@@ -1824,7 +1825,7 @@ async function convertAspectRatio(inputPath, outputPath, targetRatio, options =
|
|
|
1824
1825
|
return;
|
|
1825
1826
|
}
|
|
1826
1827
|
logger_default.info(`Aspect ratio conversion complete: ${outputPath}`);
|
|
1827
|
-
|
|
1828
|
+
resolve4(outputPath);
|
|
1828
1829
|
});
|
|
1829
1830
|
});
|
|
1830
1831
|
}
|
|
@@ -1892,7 +1893,7 @@ async function convertWithSmartLayout(inputPath, outputPath, config2, webcamOver
|
|
|
1892
1893
|
"4",
|
|
1893
1894
|
outputPath
|
|
1894
1895
|
];
|
|
1895
|
-
return new Promise((
|
|
1896
|
+
return new Promise((resolve4, reject) => {
|
|
1896
1897
|
execFileRaw(ffmpegPath5, args, { maxBuffer: 10 * 1024 * 1024 }, (error, _stdout, stderr) => {
|
|
1897
1898
|
if (error) {
|
|
1898
1899
|
logger_default.error(`[${label}] FFmpeg failed: ${stderr || error.message}`);
|
|
@@ -1900,7 +1901,7 @@ async function convertWithSmartLayout(inputPath, outputPath, config2, webcamOver
|
|
|
1900
1901
|
return;
|
|
1901
1902
|
}
|
|
1902
1903
|
logger_default.info(`[${label}] Complete: ${outputPath}`);
|
|
1903
|
-
|
|
1904
|
+
resolve4(outputPath);
|
|
1904
1905
|
});
|
|
1905
1906
|
});
|
|
1906
1907
|
}
|
|
@@ -2068,7 +2069,7 @@ async function compositeOverlays(videoPath, overlays, outputPath, videoWidth, vi
|
|
|
2068
2069
|
outputPath
|
|
2069
2070
|
);
|
|
2070
2071
|
logger_default.info(`[OverlayCompositing] Compositing ${overlays.length} overlays \u2192 ${outputPath}`);
|
|
2071
|
-
return new Promise((
|
|
2072
|
+
return new Promise((resolve4, reject) => {
|
|
2072
2073
|
execFileRaw(ffmpegPath6, args, { maxBuffer: 50 * 1024 * 1024 }, (error, _stdout, stderr) => {
|
|
2073
2074
|
if (error) {
|
|
2074
2075
|
logger_default.error(`[OverlayCompositing] FFmpeg failed: ${stderr}`);
|
|
@@ -2076,7 +2077,7 @@ async function compositeOverlays(videoPath, overlays, outputPath, videoWidth, vi
|
|
|
2076
2077
|
return;
|
|
2077
2078
|
}
|
|
2078
2079
|
logger_default.info(`[OverlayCompositing] Complete: ${outputPath}`);
|
|
2079
|
-
|
|
2080
|
+
resolve4(outputPath);
|
|
2080
2081
|
});
|
|
2081
2082
|
});
|
|
2082
2083
|
}
|
|
@@ -2092,7 +2093,7 @@ var init_overlayCompositing = __esm({
|
|
|
2092
2093
|
// src/L2-clients/ffmpeg/transcoding.ts
|
|
2093
2094
|
function transcodeToMp4(inputPath, outputPath) {
|
|
2094
2095
|
const outputDir = dirname(outputPath);
|
|
2095
|
-
return new Promise((
|
|
2096
|
+
return new Promise((resolve4, reject) => {
|
|
2096
2097
|
ensureDirectory(outputDir).then(() => {
|
|
2097
2098
|
logger_default.info(`Transcoding to MP4: ${inputPath} \u2192 ${outputPath}`);
|
|
2098
2099
|
createFFmpeg(inputPath).outputOptions([
|
|
@@ -2114,7 +2115,7 @@ function transcodeToMp4(inputPath, outputPath) {
|
|
|
2114
2115
|
"+faststart"
|
|
2115
2116
|
]).output(outputPath).on("end", () => {
|
|
2116
2117
|
logger_default.info(`Transcoding complete: ${outputPath}`);
|
|
2117
|
-
|
|
2118
|
+
resolve4(outputPath);
|
|
2118
2119
|
}).on("error", (err) => {
|
|
2119
2120
|
logger_default.error(`Transcoding failed: ${err.message}`);
|
|
2120
2121
|
reject(new Error(`Transcoding to MP4 failed: ${err.message}`));
|
|
@@ -2190,6 +2191,13 @@ var init_videoOperations = __esm({
|
|
|
2190
2191
|
});
|
|
2191
2192
|
|
|
2192
2193
|
// src/L1-infra/config/brand.ts
|
|
2194
|
+
var brand_exports = {};
|
|
2195
|
+
__export(brand_exports, {
|
|
2196
|
+
getBrandConfig: () => getBrandConfig,
|
|
2197
|
+
getIntroOutroConfig: () => getIntroOutroConfig,
|
|
2198
|
+
getThumbnailConfig: () => getThumbnailConfig,
|
|
2199
|
+
getWhisperPrompt: () => getWhisperPrompt
|
|
2200
|
+
});
|
|
2193
2201
|
function validateBrandConfig(brand) {
|
|
2194
2202
|
const requiredStrings = ["name", "handle", "tagline"];
|
|
2195
2203
|
for (const field of requiredStrings) {
|
|
@@ -3539,8 +3547,8 @@ var require_semaphore = __commonJS({
|
|
|
3539
3547
|
this._waiting = [];
|
|
3540
3548
|
}
|
|
3541
3549
|
lock(thunk) {
|
|
3542
|
-
return new Promise((
|
|
3543
|
-
this._waiting.push({ thunk, resolve:
|
|
3550
|
+
return new Promise((resolve4, reject) => {
|
|
3551
|
+
this._waiting.push({ thunk, resolve: resolve4, reject });
|
|
3544
3552
|
this.runNext();
|
|
3545
3553
|
});
|
|
3546
3554
|
}
|
|
@@ -5030,9 +5038,9 @@ ${JSON.stringify(message, null, 4)}`);
|
|
|
5030
5038
|
if (typeof cancellationStrategy.sender.enableCancellation === "function") {
|
|
5031
5039
|
cancellationStrategy.sender.enableCancellation(requestMessage);
|
|
5032
5040
|
}
|
|
5033
|
-
return new Promise(async (
|
|
5041
|
+
return new Promise(async (resolve4, reject) => {
|
|
5034
5042
|
const resolveWithCleanup = (r) => {
|
|
5035
|
-
|
|
5043
|
+
resolve4(r);
|
|
5036
5044
|
cancellationStrategy.sender.cleanup(id);
|
|
5037
5045
|
disposable?.dispose();
|
|
5038
5046
|
};
|
|
@@ -5444,10 +5452,10 @@ var require_ril = __commonJS({
|
|
|
5444
5452
|
return api_1.Disposable.create(() => this.stream.off("end", listener));
|
|
5445
5453
|
}
|
|
5446
5454
|
write(data, encoding) {
|
|
5447
|
-
return new Promise((
|
|
5455
|
+
return new Promise((resolve4, reject) => {
|
|
5448
5456
|
const callback = (error) => {
|
|
5449
5457
|
if (error === void 0 || error === null) {
|
|
5450
|
-
|
|
5458
|
+
resolve4();
|
|
5451
5459
|
} else {
|
|
5452
5460
|
reject(error);
|
|
5453
5461
|
}
|
|
@@ -5699,10 +5707,10 @@ var require_main = __commonJS({
|
|
|
5699
5707
|
exports.generateRandomPipeName = generateRandomPipeName;
|
|
5700
5708
|
function createClientPipeTransport(pipeName, encoding = "utf-8") {
|
|
5701
5709
|
let connectResolve;
|
|
5702
|
-
const connected = new Promise((
|
|
5703
|
-
connectResolve =
|
|
5710
|
+
const connected = new Promise((resolve4, _reject) => {
|
|
5711
|
+
connectResolve = resolve4;
|
|
5704
5712
|
});
|
|
5705
|
-
return new Promise((
|
|
5713
|
+
return new Promise((resolve4, reject) => {
|
|
5706
5714
|
let server = (0, net_1.createServer)((socket) => {
|
|
5707
5715
|
server.close();
|
|
5708
5716
|
connectResolve([
|
|
@@ -5713,7 +5721,7 @@ var require_main = __commonJS({
|
|
|
5713
5721
|
server.on("error", reject);
|
|
5714
5722
|
server.listen(pipeName, () => {
|
|
5715
5723
|
server.removeListener("error", reject);
|
|
5716
|
-
|
|
5724
|
+
resolve4({
|
|
5717
5725
|
onConnected: () => {
|
|
5718
5726
|
return connected;
|
|
5719
5727
|
}
|
|
@@ -5732,10 +5740,10 @@ var require_main = __commonJS({
|
|
|
5732
5740
|
exports.createServerPipeTransport = createServerPipeTransport;
|
|
5733
5741
|
function createClientSocketTransport(port, encoding = "utf-8") {
|
|
5734
5742
|
let connectResolve;
|
|
5735
|
-
const connected = new Promise((
|
|
5736
|
-
connectResolve =
|
|
5743
|
+
const connected = new Promise((resolve4, _reject) => {
|
|
5744
|
+
connectResolve = resolve4;
|
|
5737
5745
|
});
|
|
5738
|
-
return new Promise((
|
|
5746
|
+
return new Promise((resolve4, reject) => {
|
|
5739
5747
|
const server = (0, net_1.createServer)((socket) => {
|
|
5740
5748
|
server.close();
|
|
5741
5749
|
connectResolve([
|
|
@@ -5746,7 +5754,7 @@ var require_main = __commonJS({
|
|
|
5746
5754
|
server.on("error", reject);
|
|
5747
5755
|
server.listen(port, "127.0.0.1", () => {
|
|
5748
5756
|
server.removeListener("error", reject);
|
|
5749
|
-
|
|
5757
|
+
resolve4({
|
|
5750
5758
|
onConnected: () => {
|
|
5751
5759
|
return connected;
|
|
5752
5760
|
}
|
|
@@ -5966,8 +5974,8 @@ var init_session = __esm({
|
|
|
5966
5974
|
const effectiveTimeout = timeout ?? 6e4;
|
|
5967
5975
|
let resolveIdle;
|
|
5968
5976
|
let rejectWithError;
|
|
5969
|
-
const idlePromise = new Promise((
|
|
5970
|
-
resolveIdle =
|
|
5977
|
+
const idlePromise = new Promise((resolve4, reject) => {
|
|
5978
|
+
resolveIdle = resolve4;
|
|
5971
5979
|
rejectWithError = reject;
|
|
5972
5980
|
});
|
|
5973
5981
|
let lastAssistantMessage;
|
|
@@ -6605,7 +6613,7 @@ var init_client = __esm({
|
|
|
6605
6613
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
6606
6614
|
if (attempt < 3) {
|
|
6607
6615
|
const delay = 100 * Math.pow(2, attempt - 1);
|
|
6608
|
-
await new Promise((
|
|
6616
|
+
await new Promise((resolve4) => setTimeout(resolve4, delay));
|
|
6609
6617
|
}
|
|
6610
6618
|
}
|
|
6611
6619
|
}
|
|
@@ -6949,8 +6957,8 @@ var init_client = __esm({
|
|
|
6949
6957
|
}
|
|
6950
6958
|
await this.modelsCacheLock;
|
|
6951
6959
|
let resolveLock;
|
|
6952
|
-
this.modelsCacheLock = new Promise((
|
|
6953
|
-
resolveLock =
|
|
6960
|
+
this.modelsCacheLock = new Promise((resolve4) => {
|
|
6961
|
+
resolveLock = resolve4;
|
|
6954
6962
|
});
|
|
6955
6963
|
try {
|
|
6956
6964
|
if (this.modelsCache !== null) {
|
|
@@ -7147,7 +7155,7 @@ var init_client = __esm({
|
|
|
7147
7155
|
* Start the CLI server process
|
|
7148
7156
|
*/
|
|
7149
7157
|
async startCLIServer() {
|
|
7150
|
-
return new Promise((
|
|
7158
|
+
return new Promise((resolve4, reject) => {
|
|
7151
7159
|
this.stderrBuffer = "";
|
|
7152
7160
|
const args = [
|
|
7153
7161
|
...this.options.cliArgs,
|
|
@@ -7198,7 +7206,7 @@ var init_client = __esm({
|
|
|
7198
7206
|
let resolved = false;
|
|
7199
7207
|
if (this.options.useStdio) {
|
|
7200
7208
|
resolved = true;
|
|
7201
|
-
|
|
7209
|
+
resolve4();
|
|
7202
7210
|
} else {
|
|
7203
7211
|
this.cliProcess.stdout?.on("data", (data) => {
|
|
7204
7212
|
stdout += data.toString();
|
|
@@ -7206,7 +7214,7 @@ var init_client = __esm({
|
|
|
7206
7214
|
if (match && !resolved) {
|
|
7207
7215
|
this.actualPort = parseInt(match[1], 10);
|
|
7208
7216
|
resolved = true;
|
|
7209
|
-
|
|
7217
|
+
resolve4();
|
|
7210
7218
|
}
|
|
7211
7219
|
});
|
|
7212
7220
|
}
|
|
@@ -7335,7 +7343,7 @@ stderr: ${stderrOutput}`
|
|
|
7335
7343
|
if (!this.actualPort) {
|
|
7336
7344
|
throw new Error("Server port not available");
|
|
7337
7345
|
}
|
|
7338
|
-
return new Promise((
|
|
7346
|
+
return new Promise((resolve4, reject) => {
|
|
7339
7347
|
this.socket = new Socket();
|
|
7340
7348
|
this.socket.connect(this.actualPort, this.actualHost, () => {
|
|
7341
7349
|
this.connection = (0, import_node2.createMessageConnection)(
|
|
@@ -7344,7 +7352,7 @@ stderr: ${stderrOutput}`
|
|
|
7344
7352
|
);
|
|
7345
7353
|
this.attachConnectionHandlers();
|
|
7346
7354
|
this.connection.listen();
|
|
7347
|
-
|
|
7355
|
+
resolve4();
|
|
7348
7356
|
});
|
|
7349
7357
|
this.socket.on("error", (error) => {
|
|
7350
7358
|
reject(new Error(`Failed to connect to CLI server: ${error.message}`));
|
|
@@ -7828,7 +7836,7 @@ var init_CopilotProvider = __esm({
|
|
|
7828
7836
|
logger_default.info("[CopilotProvider] Creating session\u2026");
|
|
7829
7837
|
let copilotSession;
|
|
7830
7838
|
try {
|
|
7831
|
-
copilotSession = await new Promise((
|
|
7839
|
+
copilotSession = await new Promise((resolve4, reject) => {
|
|
7832
7840
|
const timeoutId = setTimeout(
|
|
7833
7841
|
() => reject(new Error(
|
|
7834
7842
|
`[CopilotProvider] createSession timed out after ${SESSION_CREATE_TIMEOUT_MS / 1e3}s \u2014 the Copilot SDK language server may not be reachable. Check GitHub authentication and network connectivity.`
|
|
@@ -7851,7 +7859,7 @@ var init_CopilotProvider = __esm({
|
|
|
7851
7859
|
}).then(
|
|
7852
7860
|
(session) => {
|
|
7853
7861
|
clearTimeout(timeoutId);
|
|
7854
|
-
|
|
7862
|
+
resolve4(session);
|
|
7855
7863
|
},
|
|
7856
7864
|
(err) => {
|
|
7857
7865
|
clearTimeout(timeoutId);
|
|
@@ -7942,7 +7950,7 @@ var init_CopilotProvider = __esm({
|
|
|
7942
7950
|
* would fire while the agent is legitimately waiting for the user.
|
|
7943
7951
|
*/
|
|
7944
7952
|
sendAndWaitForIdle(message) {
|
|
7945
|
-
return new Promise((
|
|
7953
|
+
return new Promise((resolve4, reject) => {
|
|
7946
7954
|
let lastAssistantMessage;
|
|
7947
7955
|
const unsubMessage = this.session.on("assistant.message", (event) => {
|
|
7948
7956
|
lastAssistantMessage = event;
|
|
@@ -7951,7 +7959,7 @@ var init_CopilotProvider = __esm({
|
|
|
7951
7959
|
unsubMessage();
|
|
7952
7960
|
unsubIdle();
|
|
7953
7961
|
unsubError();
|
|
7954
|
-
|
|
7962
|
+
resolve4(lastAssistantMessage);
|
|
7955
7963
|
});
|
|
7956
7964
|
const unsubError = this.session.on("session.error", (event) => {
|
|
7957
7965
|
unsubMessage();
|
|
@@ -8658,7 +8666,7 @@ var init_BaseAgent = __esm({
|
|
|
8658
8666
|
this.resetForRetry();
|
|
8659
8667
|
const delayMs = 2e3 * Math.pow(2, attempt - 1);
|
|
8660
8668
|
logger_default.warn(`[${this.agentName}] Transient error (attempt ${attempt}/${_BaseAgent.MAX_RETRIES}), retrying in ${delayMs / 1e3}s: ${message}`);
|
|
8661
|
-
await new Promise((
|
|
8669
|
+
await new Promise((resolve4) => setTimeout(resolve4, delayMs));
|
|
8662
8670
|
}
|
|
8663
8671
|
}
|
|
8664
8672
|
throw lastError;
|
|
@@ -9811,22 +9819,6 @@ var init_ideaService = __esm({
|
|
|
9811
9819
|
});
|
|
9812
9820
|
|
|
9813
9821
|
// src/L3-services/postStore/postStore.ts
|
|
9814
|
-
var postStore_exports = {};
|
|
9815
|
-
__export(postStore_exports, {
|
|
9816
|
-
approveBulk: () => approveBulk,
|
|
9817
|
-
approveItem: () => approveItem,
|
|
9818
|
-
createItem: () => createItem,
|
|
9819
|
-
getGroupedPendingItems: () => getGroupedPendingItems,
|
|
9820
|
-
getItem: () => getItem,
|
|
9821
|
-
getPendingItems: () => getPendingItems,
|
|
9822
|
-
getPublishedItemByLatePostId: () => getPublishedItemByLatePostId,
|
|
9823
|
-
getPublishedItems: () => getPublishedItems,
|
|
9824
|
-
getScheduledItemsByIdeaIds: () => getScheduledItemsByIdeaIds,
|
|
9825
|
-
itemExists: () => itemExists,
|
|
9826
|
-
rejectItem: () => rejectItem,
|
|
9827
|
-
updateItem: () => updateItem,
|
|
9828
|
-
updatePublishedItemSchedule: () => updatePublishedItemSchedule
|
|
9829
|
-
});
|
|
9830
9822
|
function getQueueDir() {
|
|
9831
9823
|
const { OUTPUT_DIR } = getConfig();
|
|
9832
9824
|
return join(OUTPUT_DIR, "publish-queue");
|
|
@@ -10207,10 +10199,6 @@ async function getScheduledItemsByIdeaIds(ideaIds) {
|
|
|
10207
10199
|
(item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
|
|
10208
10200
|
);
|
|
10209
10201
|
}
|
|
10210
|
-
async function getPublishedItemByLatePostId(latePostId) {
|
|
10211
|
-
const publishedItems = await getPublishedItems();
|
|
10212
|
-
return publishedItems.find((item) => item.metadata.latePostId === latePostId) ?? null;
|
|
10213
|
-
}
|
|
10214
10202
|
async function updatePublishedItemSchedule(id, scheduledFor) {
|
|
10215
10203
|
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
10216
10204
|
throw new Error(`Invalid ID format: ${id}`);
|
|
@@ -10572,8 +10560,8 @@ function validateByClipType(byClipType, platformName) {
|
|
|
10572
10560
|
return validated;
|
|
10573
10561
|
}
|
|
10574
10562
|
function validatePositiveNumber(value, fieldName) {
|
|
10575
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value
|
|
10576
|
-
throw new Error(`${fieldName} must be a
|
|
10563
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
10564
|
+
throw new Error(`${fieldName} must be a non-negative number`);
|
|
10577
10565
|
}
|
|
10578
10566
|
return value;
|
|
10579
10567
|
}
|
|
@@ -10717,9 +10705,6 @@ function getPlatformSchedule(platform, clipType) {
|
|
|
10717
10705
|
function getIdeaSpacingConfig() {
|
|
10718
10706
|
return cachedConfig?.ideaSpacing ?? { ...defaultIdeaSpacing };
|
|
10719
10707
|
}
|
|
10720
|
-
function getDisplacementConfig() {
|
|
10721
|
-
return cachedConfig?.displacement ?? { ...defaultDisplacement };
|
|
10722
|
-
}
|
|
10723
10708
|
var VALID_DAYS, TIME_REGEX, defaultIdeaSpacing, defaultDisplacement, cachedConfig, PLATFORM_ALIASES;
|
|
10724
10709
|
var init_scheduleConfig = __esm({
|
|
10725
10710
|
"src/L3-services/scheduler/scheduleConfig.ts"() {
|
|
@@ -10813,9 +10798,11 @@ async function buildBookedMap(platform) {
|
|
|
10813
10798
|
getPublishedItems()
|
|
10814
10799
|
]);
|
|
10815
10800
|
const ideaLinkedPostIds = /* @__PURE__ */ new Set();
|
|
10801
|
+
const latePostIdToIdeaIds = /* @__PURE__ */ new Map();
|
|
10816
10802
|
for (const item of publishedItems) {
|
|
10817
10803
|
if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
|
|
10818
10804
|
ideaLinkedPostIds.add(item.metadata.latePostId);
|
|
10805
|
+
latePostIdToIdeaIds.set(item.metadata.latePostId, item.metadata.ideaIds);
|
|
10819
10806
|
}
|
|
10820
10807
|
}
|
|
10821
10808
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -10830,7 +10817,8 @@ async function buildBookedMap(platform) {
|
|
|
10830
10817
|
postId: post._id,
|
|
10831
10818
|
platform: scheduledPlatform.platform,
|
|
10832
10819
|
status: post.status,
|
|
10833
|
-
ideaLinked: ideaLinkedPostIds.has(post._id)
|
|
10820
|
+
ideaLinked: ideaLinkedPostIds.has(post._id),
|
|
10821
|
+
ideaIds: latePostIdToIdeaIds.get(post._id)
|
|
10834
10822
|
});
|
|
10835
10823
|
}
|
|
10836
10824
|
}
|
|
@@ -10839,28 +10827,20 @@ async function buildBookedMap(platform) {
|
|
|
10839
10827
|
if (platform && item.metadata.platform !== platform) continue;
|
|
10840
10828
|
if (!item.metadata.scheduledFor) continue;
|
|
10841
10829
|
const ms = normalizeDateTime(item.metadata.scheduledFor);
|
|
10830
|
+
if (ms < Date.now()) continue;
|
|
10842
10831
|
if (!map.has(ms)) {
|
|
10843
10832
|
map.set(ms, {
|
|
10844
10833
|
scheduledFor: item.metadata.scheduledFor,
|
|
10845
10834
|
source: "local",
|
|
10846
10835
|
itemId: item.id,
|
|
10847
10836
|
platform: item.metadata.platform,
|
|
10848
|
-
ideaLinked: Boolean(item.metadata.ideaIds?.length)
|
|
10837
|
+
ideaLinked: Boolean(item.metadata.ideaIds?.length),
|
|
10838
|
+
ideaIds: item.metadata.ideaIds
|
|
10849
10839
|
});
|
|
10850
10840
|
}
|
|
10851
10841
|
}
|
|
10852
10842
|
return map;
|
|
10853
10843
|
}
|
|
10854
|
-
async function getIdeaLinkedLatePostIds() {
|
|
10855
|
-
const publishedItems = await getPublishedItems();
|
|
10856
|
-
const ids = /* @__PURE__ */ new Set();
|
|
10857
|
-
for (const item of publishedItems) {
|
|
10858
|
-
if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
|
|
10859
|
-
ids.add(item.metadata.latePostId);
|
|
10860
|
-
}
|
|
10861
|
-
}
|
|
10862
|
-
return ids;
|
|
10863
|
-
}
|
|
10864
10844
|
function* generateTimeslots(platformConfig, timezone, fromMs, maxMs) {
|
|
10865
10845
|
const baseDate = new Date(fromMs);
|
|
10866
10846
|
const upperMs = maxMs ?? fromMs + MAX_LOOKAHEAD_DAYS * DAY_MS;
|
|
@@ -10932,13 +10912,55 @@ async function getIdeaReferences(ideaIds, bookedMap) {
|
|
|
10932
10912
|
}
|
|
10933
10913
|
return refs;
|
|
10934
10914
|
}
|
|
10935
|
-
|
|
10915
|
+
function getBookedSlotPublishByMs(booked, ideaPublishByMap) {
|
|
10916
|
+
if (!booked.ideaIds?.length) return void 0;
|
|
10917
|
+
let earliest;
|
|
10918
|
+
for (const ideaId of booked.ideaIds) {
|
|
10919
|
+
const ms = ideaPublishByMap.get(ideaId);
|
|
10920
|
+
if (ms !== void 0 && (earliest === void 0 || ms < earliest)) {
|
|
10921
|
+
earliest = ms;
|
|
10922
|
+
}
|
|
10923
|
+
}
|
|
10924
|
+
return earliest;
|
|
10925
|
+
}
|
|
10926
|
+
async function buildIdeaPublishByMap(bookedMap, lookupIdeaPublishBy) {
|
|
10927
|
+
const allIdeaIds = /* @__PURE__ */ new Set();
|
|
10928
|
+
for (const slot of bookedMap.values()) {
|
|
10929
|
+
if (slot.ideaIds) {
|
|
10930
|
+
for (const id of slot.ideaIds) allIdeaIds.add(id);
|
|
10931
|
+
}
|
|
10932
|
+
}
|
|
10933
|
+
const map = /* @__PURE__ */ new Map();
|
|
10934
|
+
if (allIdeaIds.size === 0) return map;
|
|
10935
|
+
for (const ideaId of allIdeaIds) {
|
|
10936
|
+
const ms = await lookupIdeaPublishBy(ideaId);
|
|
10937
|
+
if (ms !== void 0) map.set(ideaId, ms);
|
|
10938
|
+
}
|
|
10939
|
+
return map;
|
|
10940
|
+
}
|
|
10941
|
+
async function createIdeaPublishByLookup() {
|
|
10942
|
+
try {
|
|
10943
|
+
const { getIdea: getIdea2 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
|
|
10944
|
+
return async (ideaId) => {
|
|
10945
|
+
try {
|
|
10946
|
+
const idea = await getIdea2(parseInt(ideaId, 10));
|
|
10947
|
+
if (idea?.publishBy) return new Date(idea.publishBy).getTime();
|
|
10948
|
+
} catch {
|
|
10949
|
+
}
|
|
10950
|
+
return void 0;
|
|
10951
|
+
};
|
|
10952
|
+
} catch {
|
|
10953
|
+
return async () => void 0;
|
|
10954
|
+
}
|
|
10955
|
+
}
|
|
10956
|
+
async function findSlot(platformConfig, fromMs, isIdeaPost, ownPostId, label, ctx) {
|
|
10936
10957
|
const indent = " ".repeat(ctx.depth);
|
|
10937
10958
|
let checked = 0;
|
|
10938
10959
|
let skippedBooked = 0;
|
|
10939
10960
|
let skippedSpacing = 0;
|
|
10940
|
-
|
|
10941
|
-
for
|
|
10961
|
+
const maxMs = ctx.depth === 0 ? ctx.publishByMs : void 0;
|
|
10962
|
+
logger_default.debug(`${indent}[schedulePost] Looking for slot for ${label} (idea=${isIdeaPost}) from ${new Date(fromMs).toISOString()}${maxMs ? ` until ${new Date(maxMs).toISOString()}` : ""}`);
|
|
10963
|
+
for (const { datetime, ms } of generateTimeslots(platformConfig, ctx.timezone, fromMs, maxMs)) {
|
|
10942
10964
|
checked++;
|
|
10943
10965
|
const booked = ctx.bookedMap.get(ms);
|
|
10944
10966
|
if (!booked) {
|
|
@@ -10949,10 +10971,14 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
|
|
|
10949
10971
|
}
|
|
10950
10972
|
continue;
|
|
10951
10973
|
}
|
|
10952
|
-
logger_default.
|
|
10974
|
+
logger_default.info(`${indent}[schedulePost] \u2705 Found empty slot: ${datetime} (checked ${checked}, skipped ${skippedBooked} booked, ${skippedSpacing} spacing)`);
|
|
10975
|
+
return datetime;
|
|
10976
|
+
}
|
|
10977
|
+
if (ownPostId && booked.postId === ownPostId) {
|
|
10978
|
+
logger_default.info(`${indent}[schedulePost] \u2705 Keeping own slot: ${datetime} (checked ${checked})`);
|
|
10953
10979
|
return datetime;
|
|
10954
10980
|
}
|
|
10955
|
-
if (isIdeaPost &&
|
|
10981
|
+
if (isIdeaPost && !booked.ideaLinked && booked.source === "late" && booked.postId) {
|
|
10956
10982
|
if (ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
|
|
10957
10983
|
skippedSpacing++;
|
|
10958
10984
|
if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
|
|
@@ -10961,12 +10987,13 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
|
|
|
10961
10987
|
continue;
|
|
10962
10988
|
}
|
|
10963
10989
|
logger_default.info(`${indent}[schedulePost] \u{1F504} Slot ${datetime} taken by non-idea post ${booked.postId} \u2014 displacing`);
|
|
10964
|
-
const newHome = await
|
|
10990
|
+
const newHome = await findSlot(
|
|
10965
10991
|
platformConfig,
|
|
10966
10992
|
ms,
|
|
10967
10993
|
false,
|
|
10994
|
+
booked.postId,
|
|
10968
10995
|
`displaced:${booked.postId}`,
|
|
10969
|
-
{ ...ctx, depth: ctx.depth + 1 }
|
|
10996
|
+
{ ...ctx, depth: ctx.depth + 1, publishByMs: void 0 }
|
|
10970
10997
|
);
|
|
10971
10998
|
if (newHome) {
|
|
10972
10999
|
if (!ctx.dryRun) {
|
|
@@ -10982,12 +11009,51 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
|
|
|
10982
11009
|
ctx.bookedMap.delete(ms);
|
|
10983
11010
|
const newMs = normalizeDateTime(newHome);
|
|
10984
11011
|
ctx.bookedMap.set(newMs, { ...booked, scheduledFor: newHome });
|
|
10985
|
-
logger_default.
|
|
11012
|
+
logger_default.info(`${indent}[schedulePost] \u2705 Taking slot: ${datetime} (checked ${checked}, displaced ${booked.postId})`);
|
|
10986
11013
|
return datetime;
|
|
10987
11014
|
}
|
|
10988
11015
|
logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Could not displace ${booked.postId} \u2014 no empty slot found after ${datetime}`);
|
|
10989
11016
|
}
|
|
10990
|
-
if (booked.ideaLinked) {
|
|
11017
|
+
if (isIdeaPost && booked.ideaLinked && booked.source === "late" && booked.postId) {
|
|
11018
|
+
if (ctx.publishByMs) {
|
|
11019
|
+
const incumbentPublishByMs = getBookedSlotPublishByMs(booked, ctx.ideaPublishByMap);
|
|
11020
|
+
if (!incumbentPublishByMs || ctx.publishByMs < incumbentPublishByMs) {
|
|
11021
|
+
if (ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
|
|
11022
|
+
skippedSpacing++;
|
|
11023
|
+
if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
|
|
11024
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} too close to same-idea post \u2014 skipping (even though idea-displaceable)`);
|
|
11025
|
+
}
|
|
11026
|
+
continue;
|
|
11027
|
+
}
|
|
11028
|
+
logger_default.info(`${indent}[schedulePost] \u{1F504} Slot ${datetime} taken by idea post ${booked.postId} with later deadline \u2014 displacing`);
|
|
11029
|
+
const newHome = await findSlot(
|
|
11030
|
+
platformConfig,
|
|
11031
|
+
ms,
|
|
11032
|
+
true,
|
|
11033
|
+
booked.postId,
|
|
11034
|
+
`displaced-idea:${booked.postId}`,
|
|
11035
|
+
{ ...ctx, depth: ctx.depth + 1, publishByMs: incumbentPublishByMs }
|
|
11036
|
+
);
|
|
11037
|
+
if (newHome) {
|
|
11038
|
+
if (!ctx.dryRun) {
|
|
11039
|
+
try {
|
|
11040
|
+
await ctx.lateClient.schedulePost(booked.postId, newHome);
|
|
11041
|
+
} catch (err) {
|
|
11042
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
11043
|
+
logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Failed to displace idea ${booked.postId} via Late API: ${msg} \u2014 skipping slot`);
|
|
11044
|
+
continue;
|
|
11045
|
+
}
|
|
11046
|
+
}
|
|
11047
|
+
logger_default.info(`${indent}[schedulePost] \u{1F4E6} Displaced idea ${booked.postId}: ${datetime} \u2192 ${newHome}`);
|
|
11048
|
+
ctx.bookedMap.delete(ms);
|
|
11049
|
+
const newMs = normalizeDateTime(newHome);
|
|
11050
|
+
ctx.bookedMap.set(newMs, { ...booked, scheduledFor: newHome });
|
|
11051
|
+
logger_default.info(`${indent}[schedulePost] \u2705 Taking slot: ${datetime} (checked ${checked}, displaced idea ${booked.postId})`);
|
|
11052
|
+
return datetime;
|
|
11053
|
+
}
|
|
11054
|
+
logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Could not displace idea ${booked.postId} \u2014 no slot found after ${datetime}`);
|
|
11055
|
+
}
|
|
11056
|
+
}
|
|
10991
11057
|
skippedBooked++;
|
|
10992
11058
|
if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
|
|
10993
11059
|
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken by idea post ${booked.postId ?? booked.itemId} \u2014 skipping`);
|
|
@@ -11002,135 +11068,187 @@ async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
|
|
|
11002
11068
|
logger_default.warn(`[schedulePost] \u274C No slot found for ${label} \u2014 checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing`);
|
|
11003
11069
|
return null;
|
|
11004
11070
|
}
|
|
11005
|
-
async function
|
|
11071
|
+
async function schedulePost(platform, clipType, options) {
|
|
11006
11072
|
const config2 = await loadScheduleConfig();
|
|
11007
11073
|
const platformConfig = getPlatformSchedule(platform, clipType);
|
|
11008
11074
|
if (!platformConfig) {
|
|
11009
|
-
logger_default.warn(`No schedule config found for platform "${sanitizeLogValue(platform)}"`);
|
|
11075
|
+
logger_default.warn(`[schedulePost] No schedule config found for platform "${sanitizeLogValue(platform)}"`);
|
|
11010
11076
|
return null;
|
|
11011
11077
|
}
|
|
11012
11078
|
const { timezone } = config2;
|
|
11013
11079
|
const nowMs = Date.now();
|
|
11014
11080
|
const ideaIds = options?.ideaIds?.filter(Boolean) ?? [];
|
|
11015
|
-
const
|
|
11016
|
-
const
|
|
11017
|
-
const
|
|
11081
|
+
const isIdeaPost = ideaIds.length > 0;
|
|
11082
|
+
const publishBy = options?.publishBy;
|
|
11083
|
+
const publishByMs = publishBy ? new Date(publishBy).getTime() : void 0;
|
|
11084
|
+
const postId = options?.postId;
|
|
11085
|
+
const dryRun = options?.dryRun ?? false;
|
|
11018
11086
|
const label = `${platform}/${clipType ?? "default"}`;
|
|
11087
|
+
const bookedMap = options?._bookedMap ?? await buildBookedMap(platform);
|
|
11019
11088
|
let ideaRefs = [];
|
|
11020
11089
|
let samePlatformMs = 0;
|
|
11021
11090
|
let crossPlatformMs = 0;
|
|
11022
|
-
if (
|
|
11023
|
-
const allBookedMap = await buildBookedMap();
|
|
11091
|
+
if (isIdeaPost) {
|
|
11092
|
+
const allBookedMap = options?._bookedMap ?? await buildBookedMap();
|
|
11024
11093
|
ideaRefs = await getIdeaReferences(ideaIds, allBookedMap);
|
|
11025
11094
|
const spacingConfig = getIdeaSpacingConfig();
|
|
11026
11095
|
samePlatformMs = spacingConfig.samePlatformHours * HOUR_MS;
|
|
11027
11096
|
crossPlatformMs = spacingConfig.crossPlatformHours * HOUR_MS;
|
|
11028
11097
|
}
|
|
11029
|
-
|
|
11098
|
+
let ideaPublishByMap;
|
|
11099
|
+
if (options?._ideaPublishByMap) {
|
|
11100
|
+
ideaPublishByMap = options._ideaPublishByMap;
|
|
11101
|
+
} else if (publishByMs && Number.isFinite(publishByMs) && publishByMs > nowMs) {
|
|
11102
|
+
const lookup = await createIdeaPublishByLookup();
|
|
11103
|
+
ideaPublishByMap = await buildIdeaPublishByMap(bookedMap, lookup);
|
|
11104
|
+
} else {
|
|
11105
|
+
ideaPublishByMap = /* @__PURE__ */ new Map();
|
|
11106
|
+
}
|
|
11107
|
+
logger_default.info(`[schedulePost] Scheduling ${label} (idea=${isIdeaPost}, booked=${bookedMap.size} slots, spacingRefs=${ideaRefs.length}${publishBy ? `, publishBy=${publishBy}` : ""}${postId ? `, postId=${postId}` : ""})`);
|
|
11030
11108
|
const ctx = {
|
|
11031
11109
|
timezone,
|
|
11032
11110
|
bookedMap,
|
|
11033
|
-
ideaLinkedPostIds,
|
|
11034
11111
|
lateClient: new LateApiClient(),
|
|
11035
|
-
|
|
11036
|
-
dryRun: false,
|
|
11112
|
+
dryRun,
|
|
11037
11113
|
depth: 0,
|
|
11038
11114
|
ideaRefs,
|
|
11039
11115
|
samePlatformMs,
|
|
11040
11116
|
crossPlatformMs,
|
|
11041
|
-
platform
|
|
11117
|
+
platform,
|
|
11118
|
+
ideaPublishByMap
|
|
11042
11119
|
};
|
|
11043
|
-
|
|
11120
|
+
let result = null;
|
|
11121
|
+
if (publishByMs && Number.isFinite(publishByMs) && publishByMs > nowMs) {
|
|
11122
|
+
result = await findSlot(platformConfig, nowMs, isIdeaPost, postId, label, { ...ctx, publishByMs });
|
|
11123
|
+
if (!result) {
|
|
11124
|
+
logger_default.warn(`[schedulePost] \u26A0\uFE0F No slot for ${label} before publishBy ${publishBy} \u2014 searching past deadline`);
|
|
11125
|
+
result = await findSlot(platformConfig, nowMs, isIdeaPost, postId, label, { ...ctx, publishByMs: void 0 });
|
|
11126
|
+
}
|
|
11127
|
+
} else {
|
|
11128
|
+
result = await findSlot(platformConfig, nowMs, isIdeaPost, postId, label, ctx);
|
|
11129
|
+
}
|
|
11044
11130
|
if (!result) {
|
|
11045
|
-
logger_default.warn(`[
|
|
11131
|
+
logger_default.warn(`[schedulePost] \u274C No available slot for "${sanitizeLogValue(platform)}" within ${MAX_LOOKAHEAD_DAYS} days`);
|
|
11132
|
+
} else if (publishByMs && Number.isFinite(publishByMs)) {
|
|
11133
|
+
const slotMs = new Date(result).getTime();
|
|
11134
|
+
if (slotMs > publishByMs) {
|
|
11135
|
+
const daysLate = Math.ceil((slotMs - publishByMs) / (24 * 60 * 60 * 1e3));
|
|
11136
|
+
logger_default.warn(`[schedulePost] \u26A0\uFE0F ${label} scheduled for ${result} \u2014 ${daysLate} day(s) AFTER publishBy deadline ${publishBy}`);
|
|
11137
|
+
} else {
|
|
11138
|
+
logger_default.info(`[schedulePost] \u2705 ${label} \u2192 ${result} (within publishBy ${publishBy})`);
|
|
11139
|
+
}
|
|
11140
|
+
} else {
|
|
11141
|
+
logger_default.info(`[schedulePost] \u2705 ${label} \u2192 ${result}`);
|
|
11046
11142
|
}
|
|
11047
11143
|
return result;
|
|
11048
11144
|
}
|
|
11049
|
-
async function
|
|
11145
|
+
async function findNextSlot(platform, clipType, options) {
|
|
11146
|
+
return schedulePost(platform, clipType, options);
|
|
11147
|
+
}
|
|
11148
|
+
async function rescheduleAllPosts(options) {
|
|
11050
11149
|
const dryRun = options?.dryRun ?? false;
|
|
11051
|
-
const { updatePublishedItemSchedule: updatePublishedItemSchedule2 } = await Promise.resolve().then(() => (init_postStore(), postStore_exports));
|
|
11052
11150
|
const config2 = await loadScheduleConfig();
|
|
11053
11151
|
const { timezone } = config2;
|
|
11054
11152
|
const publishedItems = await getPublishedItems();
|
|
11055
|
-
const
|
|
11056
|
-
|
|
11057
|
-
|
|
11058
|
-
if (ideaPosts.length === 0) {
|
|
11059
|
-
logger_default.info("No idea-linked posts to reschedule");
|
|
11153
|
+
const postsWithLateId = publishedItems.filter((item) => item.metadata.latePostId);
|
|
11154
|
+
if (postsWithLateId.length === 0) {
|
|
11155
|
+
logger_default.info("No posts to reschedule");
|
|
11060
11156
|
return { rescheduled: 0, unchanged: 0, failed: 0, details: [] };
|
|
11061
11157
|
}
|
|
11062
|
-
|
|
11063
|
-
const
|
|
11064
|
-
const
|
|
11065
|
-
|
|
11066
|
-
if (slot.postId && ideaLatePostIds.has(slot.postId)) {
|
|
11067
|
-
fullBookedMap.delete(ms);
|
|
11068
|
-
}
|
|
11069
|
-
}
|
|
11070
|
-
const { getIdea: getIdea2 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
|
|
11071
|
-
const ideaPublishByMap = /* @__PURE__ */ new Map();
|
|
11158
|
+
const ideaPosts = postsWithLateId.filter((item) => item.metadata.ideaIds?.length);
|
|
11159
|
+
const nonIdeaPosts = postsWithLateId.filter((item) => !item.metadata.ideaIds?.length);
|
|
11160
|
+
const lookup = await createIdeaPublishByLookup();
|
|
11161
|
+
const ideaPublishByStringMap = /* @__PURE__ */ new Map();
|
|
11072
11162
|
for (const item of ideaPosts) {
|
|
11073
11163
|
const ideaId = item.metadata.ideaIds?.[0];
|
|
11074
|
-
if (ideaId && !
|
|
11075
|
-
|
|
11076
|
-
|
|
11077
|
-
|
|
11078
|
-
} catch {
|
|
11164
|
+
if (ideaId && !ideaPublishByStringMap.has(ideaId)) {
|
|
11165
|
+
const publishByMs = await lookup(ideaId);
|
|
11166
|
+
if (publishByMs !== void 0) {
|
|
11167
|
+
ideaPublishByStringMap.set(ideaId, new Date(publishByMs).toISOString());
|
|
11079
11168
|
}
|
|
11080
11169
|
}
|
|
11081
11170
|
}
|
|
11082
11171
|
ideaPosts.sort((a, b) => {
|
|
11083
11172
|
const aId = a.metadata.ideaIds?.[0];
|
|
11084
11173
|
const bId = b.metadata.ideaIds?.[0];
|
|
11085
|
-
const aDate = aId ?
|
|
11086
|
-
const bDate = bId ?
|
|
11174
|
+
const aDate = aId ? ideaPublishByStringMap.get(aId) ?? "9999" : "9999";
|
|
11175
|
+
const bDate = bId ? ideaPublishByStringMap.get(bId) ?? "9999" : "9999";
|
|
11087
11176
|
return aDate.localeCompare(bDate);
|
|
11088
11177
|
});
|
|
11178
|
+
const allPosts = [...ideaPosts, ...nonIdeaPosts];
|
|
11179
|
+
logger_default.info(`Rescheduling ${allPosts.length} posts (${ideaPosts.length} idea, ${nonIdeaPosts.length} non-idea)`);
|
|
11180
|
+
const bookedMap = await buildBookedMap();
|
|
11181
|
+
const ideaPublishByMsMap = /* @__PURE__ */ new Map();
|
|
11182
|
+
for (const [ideaId, dateStr] of ideaPublishByStringMap) {
|
|
11183
|
+
ideaPublishByMsMap.set(ideaId, new Date(dateStr).getTime());
|
|
11184
|
+
}
|
|
11089
11185
|
const lateClient = new LateApiClient();
|
|
11090
11186
|
const result = { rescheduled: 0, unchanged: 0, failed: 0, details: [] };
|
|
11091
11187
|
const nowMs = Date.now();
|
|
11092
|
-
const
|
|
11093
|
-
|
|
11094
|
-
bookedMap: fullBookedMap,
|
|
11095
|
-
ideaLinkedPostIds: /* @__PURE__ */ new Set(),
|
|
11096
|
-
lateClient,
|
|
11097
|
-
displacementEnabled: getDisplacementConfig().enabled,
|
|
11098
|
-
dryRun,
|
|
11099
|
-
depth: 0,
|
|
11100
|
-
ideaRefs: [],
|
|
11101
|
-
samePlatformMs: 0,
|
|
11102
|
-
crossPlatformMs: 0,
|
|
11103
|
-
platform: ""
|
|
11104
|
-
};
|
|
11105
|
-
for (const item of ideaPosts) {
|
|
11106
|
-
const platform = item.metadata.platform;
|
|
11188
|
+
for (const item of allPosts) {
|
|
11189
|
+
const itemPlatform = item.metadata.platform;
|
|
11107
11190
|
const clipType = item.metadata.clipType;
|
|
11108
11191
|
const latePostId = item.metadata.latePostId;
|
|
11109
11192
|
const oldSlot = item.metadata.scheduledFor;
|
|
11110
|
-
const
|
|
11193
|
+
const isIdea = Boolean(item.metadata.ideaIds?.length);
|
|
11194
|
+
const label = `${item.id} (${itemPlatform}/${clipType})`;
|
|
11111
11195
|
try {
|
|
11112
|
-
const platformConfig = getPlatformSchedule(
|
|
11196
|
+
const platformConfig = getPlatformSchedule(itemPlatform, clipType);
|
|
11113
11197
|
if (!platformConfig) {
|
|
11114
|
-
result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: null, error: "No schedule config" });
|
|
11198
|
+
result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: "No schedule config" });
|
|
11115
11199
|
result.failed++;
|
|
11116
11200
|
continue;
|
|
11117
11201
|
}
|
|
11118
|
-
|
|
11202
|
+
let ideaRefs = [];
|
|
11203
|
+
let samePlatformMs = 0;
|
|
11204
|
+
let crossPlatformMs = 0;
|
|
11205
|
+
if (isIdea) {
|
|
11206
|
+
ideaRefs = await getIdeaReferences(item.metadata.ideaIds, bookedMap);
|
|
11207
|
+
const spacingConfig = getIdeaSpacingConfig();
|
|
11208
|
+
samePlatformMs = spacingConfig.samePlatformHours * HOUR_MS;
|
|
11209
|
+
crossPlatformMs = spacingConfig.crossPlatformHours * HOUR_MS;
|
|
11210
|
+
}
|
|
11211
|
+
const publishBy = isIdea && item.metadata.ideaIds?.[0] ? ideaPublishByStringMap.get(item.metadata.ideaIds[0]) : void 0;
|
|
11212
|
+
const publishByMs = publishBy ? new Date(publishBy).getTime() : void 0;
|
|
11213
|
+
const ctx = {
|
|
11214
|
+
timezone,
|
|
11215
|
+
bookedMap,
|
|
11216
|
+
lateClient,
|
|
11217
|
+
dryRun,
|
|
11218
|
+
depth: 0,
|
|
11219
|
+
ideaRefs,
|
|
11220
|
+
samePlatformMs,
|
|
11221
|
+
crossPlatformMs,
|
|
11222
|
+
platform: itemPlatform,
|
|
11223
|
+
publishByMs: void 0,
|
|
11224
|
+
ideaPublishByMap: ideaPublishByMsMap
|
|
11225
|
+
};
|
|
11226
|
+
let newSlotDatetime = null;
|
|
11227
|
+
if (publishByMs && Number.isFinite(publishByMs) && publishByMs > nowMs) {
|
|
11228
|
+
newSlotDatetime = await findSlot(platformConfig, nowMs, isIdea, latePostId, label, { ...ctx, publishByMs });
|
|
11229
|
+
if (!newSlotDatetime) {
|
|
11230
|
+
logger_default.warn(`[reschedule] \u26A0\uFE0F No slot for ${label} before publishBy \u2014 searching past deadline`);
|
|
11231
|
+
newSlotDatetime = await findSlot(platformConfig, nowMs, isIdea, latePostId, label, ctx);
|
|
11232
|
+
}
|
|
11233
|
+
} else {
|
|
11234
|
+
newSlotDatetime = await findSlot(platformConfig, nowMs, isIdea, latePostId, label, ctx);
|
|
11235
|
+
}
|
|
11119
11236
|
if (!newSlotDatetime) {
|
|
11120
|
-
result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: null, error: "No slot found" });
|
|
11237
|
+
result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: "No slot found" });
|
|
11121
11238
|
result.failed++;
|
|
11122
11239
|
continue;
|
|
11123
11240
|
}
|
|
11124
11241
|
const newSlotMs = normalizeDateTime(newSlotDatetime);
|
|
11125
11242
|
if (oldSlot && normalizeDateTime(oldSlot) === newSlotMs) {
|
|
11126
|
-
result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: newSlotDatetime });
|
|
11243
|
+
result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: newSlotDatetime });
|
|
11127
11244
|
result.unchanged++;
|
|
11128
|
-
|
|
11245
|
+
bookedMap.set(newSlotMs, {
|
|
11129
11246
|
scheduledFor: newSlotDatetime,
|
|
11130
11247
|
source: "late",
|
|
11131
11248
|
postId: latePostId,
|
|
11132
|
-
platform,
|
|
11133
|
-
ideaLinked:
|
|
11249
|
+
platform: itemPlatform,
|
|
11250
|
+
ideaLinked: isIdea,
|
|
11251
|
+
ideaIds: item.metadata.ideaIds
|
|
11134
11252
|
});
|
|
11135
11253
|
continue;
|
|
11136
11254
|
}
|
|
@@ -11141,34 +11259,45 @@ async function rescheduleIdeaPosts(options) {
|
|
|
11141
11259
|
const errMsg = scheduleErr instanceof Error ? scheduleErr.message : String(scheduleErr);
|
|
11142
11260
|
if (errMsg.includes("Published posts can only have their recycling config updated")) {
|
|
11143
11261
|
logger_default.info(`Skipping ${label}: post already published on platform`);
|
|
11144
|
-
result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: null, error: "Already published \u2014 skipped" });
|
|
11262
|
+
result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: "Already published \u2014 skipped" });
|
|
11145
11263
|
result.unchanged++;
|
|
11146
11264
|
continue;
|
|
11147
11265
|
}
|
|
11148
11266
|
throw scheduleErr;
|
|
11149
11267
|
}
|
|
11150
|
-
await
|
|
11268
|
+
await updatePublishedItemSchedule(item.id, newSlotDatetime);
|
|
11151
11269
|
}
|
|
11152
|
-
|
|
11270
|
+
if (oldSlot) {
|
|
11271
|
+
const oldMs = normalizeDateTime(oldSlot);
|
|
11272
|
+
const oldBooked = bookedMap.get(oldMs);
|
|
11273
|
+
if (oldBooked?.postId === latePostId) {
|
|
11274
|
+
bookedMap.delete(oldMs);
|
|
11275
|
+
}
|
|
11276
|
+
}
|
|
11277
|
+
bookedMap.set(newSlotMs, {
|
|
11153
11278
|
scheduledFor: newSlotDatetime,
|
|
11154
11279
|
source: "late",
|
|
11155
11280
|
postId: latePostId,
|
|
11156
|
-
platform,
|
|
11157
|
-
ideaLinked:
|
|
11281
|
+
platform: itemPlatform,
|
|
11282
|
+
ideaLinked: isIdea,
|
|
11283
|
+
ideaIds: item.metadata.ideaIds
|
|
11158
11284
|
});
|
|
11159
11285
|
logger_default.info(`Rescheduled ${label}: ${oldSlot ?? "unscheduled"} \u2192 ${newSlotDatetime}`);
|
|
11160
|
-
result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: newSlotDatetime });
|
|
11286
|
+
result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: newSlotDatetime });
|
|
11161
11287
|
result.rescheduled++;
|
|
11162
11288
|
} catch (err) {
|
|
11163
11289
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11164
11290
|
logger_default.error(`Failed to reschedule ${label}: ${msg}`);
|
|
11165
|
-
result.details.push({ itemId: item.id, platform, latePostId, oldSlot, newSlot: null, error: msg });
|
|
11291
|
+
result.details.push({ itemId: item.id, platform: itemPlatform, latePostId, oldSlot, newSlot: null, error: msg });
|
|
11166
11292
|
result.failed++;
|
|
11167
11293
|
}
|
|
11168
11294
|
}
|
|
11169
11295
|
logger_default.info(`Reschedule complete: ${result.rescheduled} moved, ${result.unchanged} unchanged, ${result.failed} failed`);
|
|
11170
11296
|
return result;
|
|
11171
11297
|
}
|
|
11298
|
+
async function rescheduleIdeaPosts(options) {
|
|
11299
|
+
return rescheduleAllPosts(options);
|
|
11300
|
+
}
|
|
11172
11301
|
async function getScheduleCalendar(startDate, endDate) {
|
|
11173
11302
|
const bookedMap = await buildBookedMap();
|
|
11174
11303
|
let filtered = [...bookedMap.values()].filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
|
|
@@ -11470,7 +11599,7 @@ var FileWatcher = class _FileWatcher extends EventEmitter {
|
|
|
11470
11599
|
async isFileStable(filePath) {
|
|
11471
11600
|
try {
|
|
11472
11601
|
const sizeBefore = getFileStatsSync(filePath).size;
|
|
11473
|
-
await new Promise((
|
|
11602
|
+
await new Promise((resolve4) => setTimeout(resolve4, _FileWatcher.EXTRA_STABILITY_DELAY));
|
|
11474
11603
|
const sizeAfter = getFileStatsSync(filePath).size;
|
|
11475
11604
|
return sizeBefore === sizeAfter;
|
|
11476
11605
|
} catch {
|
|
@@ -12314,7 +12443,7 @@ async function analyzeVideoEditorial(videoPath, durationSeconds, model) {
|
|
|
12314
12443
|
logger_default.info(`[Gemini] Waiting for file processing to complete...`);
|
|
12315
12444
|
let fileState = file.state;
|
|
12316
12445
|
while (fileState === "PROCESSING") {
|
|
12317
|
-
await new Promise((
|
|
12446
|
+
await new Promise((resolve4) => setTimeout(resolve4, 2e3));
|
|
12318
12447
|
const updated = await ai.files.get({ name: file.name });
|
|
12319
12448
|
fileState = updated.state;
|
|
12320
12449
|
logger_default.debug(`[Gemini] File state: ${fileState}`);
|
|
@@ -12358,7 +12487,7 @@ async function analyzeVideoClipDirection(videoPath, durationSeconds, model) {
|
|
|
12358
12487
|
logger_default.info(`[Gemini] Waiting for file processing to complete...`);
|
|
12359
12488
|
let fileState = file.state;
|
|
12360
12489
|
while (fileState === "PROCESSING") {
|
|
12361
|
-
await new Promise((
|
|
12490
|
+
await new Promise((resolve4) => setTimeout(resolve4, 2e3));
|
|
12362
12491
|
const updated = await ai.files.get({ name: file.name });
|
|
12363
12492
|
fileState = updated.state;
|
|
12364
12493
|
logger_default.debug(`[Gemini] File state: ${fileState}`);
|
|
@@ -12431,7 +12560,7 @@ async function analyzeVideoForEnhancements(videoPath, durationSeconds, transcrip
|
|
|
12431
12560
|
logger_default.info(`[Gemini] Waiting for file processing to complete...`);
|
|
12432
12561
|
let fileState = file.state;
|
|
12433
12562
|
while (fileState === "PROCESSING") {
|
|
12434
|
-
await new Promise((
|
|
12563
|
+
await new Promise((resolve4) => setTimeout(resolve4, 2e3));
|
|
12435
12564
|
const updated = await ai.files.get({ name: file.name });
|
|
12436
12565
|
fileState = updated.state;
|
|
12437
12566
|
logger_default.debug(`[Gemini] File state: ${fileState}`);
|
|
@@ -12542,7 +12671,7 @@ async function transcribeAudio(audioPath) {
|
|
|
12542
12671
|
if (attempt === MAX_RETRIES) throw retryError;
|
|
12543
12672
|
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
12544
12673
|
logger_default.warn(`Whisper attempt ${attempt}/${MAX_RETRIES} failed: ${msg} \u2014 retrying in ${RETRY_DELAY_MS / 1e3}s`);
|
|
12545
|
-
await new Promise((
|
|
12674
|
+
await new Promise((resolve4) => setTimeout(resolve4, RETRY_DELAY_MS));
|
|
12546
12675
|
}
|
|
12547
12676
|
}
|
|
12548
12677
|
if (!response) throw new Error("Whisper transcription failed after all retries");
|
|
@@ -16101,6 +16230,7 @@ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captio
|
|
|
16101
16230
|
let mediaPath = null;
|
|
16102
16231
|
let sourceClip = null;
|
|
16103
16232
|
let thumbnailPath = null;
|
|
16233
|
+
let clipIdeaIssueNumber;
|
|
16104
16234
|
if (frontmatter.shortSlug) {
|
|
16105
16235
|
const short = shorts.find((s) => s.slug === frontmatter.shortSlug);
|
|
16106
16236
|
const medium = mediumClips.find((m) => m.slug === frontmatter.shortSlug);
|
|
@@ -16110,12 +16240,14 @@ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captio
|
|
|
16110
16240
|
sourceClip = dirname(short.outputPath);
|
|
16111
16241
|
mediaPath = resolveShortMedia(short, post.platform);
|
|
16112
16242
|
thumbnailPath = short.thumbnailPath ?? null;
|
|
16243
|
+
clipIdeaIssueNumber = short.ideaIssueNumber;
|
|
16113
16244
|
} else if (medium) {
|
|
16114
16245
|
clipSlug = medium.slug;
|
|
16115
16246
|
clipType = "medium-clip";
|
|
16116
16247
|
sourceClip = dirname(medium.outputPath);
|
|
16117
16248
|
mediaPath = resolveMediumMedia(medium, post.platform);
|
|
16118
16249
|
thumbnailPath = medium.thumbnailPath ?? null;
|
|
16250
|
+
clipIdeaIssueNumber = medium.ideaIssueNumber;
|
|
16119
16251
|
} else {
|
|
16120
16252
|
clipSlug = frontmatter.shortSlug;
|
|
16121
16253
|
clipType = "short";
|
|
@@ -16172,7 +16304,7 @@ async function buildPublishQueue(video, shorts, mediumClips, socialPosts, captio
|
|
|
16172
16304
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16173
16305
|
reviewedAt: null,
|
|
16174
16306
|
publishedAt: null,
|
|
16175
|
-
ideaIds: ideaIds && ideaIds.length > 0 ? ideaIds : void 0,
|
|
16307
|
+
ideaIds: clipIdeaIssueNumber ? [String(clipIdeaIssueNumber)] : ideaIds && ideaIds.length > 0 ? ideaIds : void 0,
|
|
16176
16308
|
thumbnailPath
|
|
16177
16309
|
};
|
|
16178
16310
|
const stripped = stripFrontmatter(post.content);
|
|
@@ -16232,13 +16364,393 @@ function buildPublishQueue2(...args) {
|
|
|
16232
16364
|
return buildPublishQueue(...args);
|
|
16233
16365
|
}
|
|
16234
16366
|
|
|
16367
|
+
// src/L4-agents/IdeaDiscoveryAgent.ts
|
|
16368
|
+
init_environment();
|
|
16369
|
+
init_configLogger();
|
|
16370
|
+
init_ideaService();
|
|
16371
|
+
init_BaseAgent();
|
|
16372
|
+
var SYSTEM_PROMPT6 = `You are an Idea Discovery agent for a video content pipeline. Your job is to analyze video clips and match them to existing content ideas \u2014 or create new ideas when no match exists.
|
|
16373
|
+
|
|
16374
|
+
## Two-Phase Workflow
|
|
16375
|
+
|
|
16376
|
+
### Phase A \u2014 Match Existing Ideas
|
|
16377
|
+
For each clip (short and medium), determine if it covers a topic that DIRECTLY matches an existing idea.
|
|
16378
|
+
- A match requires the clip's core topic to be the SAME topic as the idea's topic and talking points.
|
|
16379
|
+
- Loose thematic connections are NOT matches (e.g., both about "coding" is too vague).
|
|
16380
|
+
- When in doubt, DO NOT match \u2014 create a new idea instead.
|
|
16381
|
+
|
|
16382
|
+
### Phase B \u2014 Create New Ideas
|
|
16383
|
+
For unmatched clips, create a new idea derived from the video content:
|
|
16384
|
+
- The topic, hook, key takeaway, and talking points come from what the creator ACTUALLY SAID in the transcript.
|
|
16385
|
+
- Do NOT invent content the creator didn't discuss.
|
|
16386
|
+
- Use web research (if available) to augment with trend context \u2014 why this topic matters right now, related articles, supporting data.
|
|
16387
|
+
- Set the publishBy date to the configured deadline.
|
|
16388
|
+
|
|
16389
|
+
## Quality Rules
|
|
16390
|
+
- Every clip MUST end up with an idea assignment (either matched or newly created).
|
|
16391
|
+
- Each new idea must have a clear, specific hook (not generic like "Learn about AI").
|
|
16392
|
+
- New idea talking points must come from the actual transcript content.
|
|
16393
|
+
- Use get_clip_transcript to read what the creator said in each clip's time range before matching or creating.
|
|
16394
|
+
- Call finalize_assignments exactly once when all clips are assigned.
|
|
16395
|
+
|
|
16396
|
+
## Platform Targeting
|
|
16397
|
+
- Short-form clips (TikTok, YouTube Shorts, Instagram Reels): Hook-first, single concept
|
|
16398
|
+
- Medium clips (YouTube, LinkedIn): Deep dives, tutorials, story arcs`;
|
|
16399
|
+
function computeDuration(segments) {
|
|
16400
|
+
return segments.reduce((sum, s) => sum + (s.end - s.start), 0);
|
|
16401
|
+
}
|
|
16402
|
+
function clipsToInfo(shorts, mediumClips) {
|
|
16403
|
+
const clips = [];
|
|
16404
|
+
for (let i = 0; i < shorts.length; i++) {
|
|
16405
|
+
const short = shorts[i];
|
|
16406
|
+
const segments = (short.segments ?? []).map((s) => ({ start: s.start, end: s.end, description: s.description ?? "" }));
|
|
16407
|
+
clips.push({
|
|
16408
|
+
id: short.id ?? `short-${i + 1}`,
|
|
16409
|
+
type: "short",
|
|
16410
|
+
title: short.title ?? "",
|
|
16411
|
+
description: short.description ?? "",
|
|
16412
|
+
tags: short.tags ?? [],
|
|
16413
|
+
segments,
|
|
16414
|
+
totalDuration: short.totalDuration ?? computeDuration(segments)
|
|
16415
|
+
});
|
|
16416
|
+
}
|
|
16417
|
+
for (let i = 0; i < mediumClips.length; i++) {
|
|
16418
|
+
const medium = mediumClips[i];
|
|
16419
|
+
const segments = (medium.segments ?? []).map((s) => ({ start: s.start, end: s.end, description: s.description ?? "" }));
|
|
16420
|
+
clips.push({
|
|
16421
|
+
id: medium.id ?? `medium-${i + 1}`,
|
|
16422
|
+
type: "medium-clip",
|
|
16423
|
+
title: medium.title ?? "",
|
|
16424
|
+
description: medium.description ?? "",
|
|
16425
|
+
tags: medium.tags ?? [],
|
|
16426
|
+
topic: medium.topic,
|
|
16427
|
+
segments,
|
|
16428
|
+
totalDuration: medium.totalDuration ?? computeDuration(segments)
|
|
16429
|
+
});
|
|
16430
|
+
}
|
|
16431
|
+
return clips;
|
|
16432
|
+
}
|
|
16433
|
+
function getTranscriptForTimeRange(transcript, start, end) {
|
|
16434
|
+
return transcript.filter((seg) => seg.end > start && seg.start < end).map((seg) => seg.text).join(" ").trim();
|
|
16435
|
+
}
|
|
16436
|
+
function summarizeIdeas(ideas) {
|
|
16437
|
+
if (ideas.length === 0) return "No existing ideas found.";
|
|
16438
|
+
return ideas.map((idea) => [
|
|
16439
|
+
`- #${idea.issueNumber}: "${idea.topic}"`,
|
|
16440
|
+
` Hook: ${idea.hook}`,
|
|
16441
|
+
` Tags: ${idea.tags.join(", ")}`,
|
|
16442
|
+
` Talking points: ${idea.talkingPoints.join("; ")}`,
|
|
16443
|
+
` Status: ${idea.status}`
|
|
16444
|
+
].join("\n")).join("\n");
|
|
16445
|
+
}
|
|
16446
|
+
function summarizeClips(clips) {
|
|
16447
|
+
return clips.map((clip) => [
|
|
16448
|
+
`- ${clip.id} (${clip.type}, ${clip.totalDuration.toFixed(0)}s): "${clip.title}"`,
|
|
16449
|
+
` Description: ${clip.description}`,
|
|
16450
|
+
` Tags: ${clip.tags.join(", ")}`,
|
|
16451
|
+
clip.topic ? ` Topic: ${clip.topic}` : "",
|
|
16452
|
+
` Segments: ${clip.segments.map((s) => `${s.start.toFixed(1)}-${s.end.toFixed(1)}s`).join(", ")}`
|
|
16453
|
+
].filter(Boolean).join("\n")).join("\n");
|
|
16454
|
+
}
|
|
16455
|
+
function buildUserMessage(clips, ideas, summary, publishBy, hasMcpServers) {
|
|
16456
|
+
const sections = [
|
|
16457
|
+
`## Video Summary
|
|
16458
|
+
${summary.substring(0, 2e3)}`,
|
|
16459
|
+
`
|
|
16460
|
+
## Clips to Assign (${clips.length} total)
|
|
16461
|
+
${summarizeClips(clips)}`,
|
|
16462
|
+
`
|
|
16463
|
+
## Existing Ideas (${ideas.length} total)
|
|
16464
|
+
${summarizeIdeas(ideas)}`,
|
|
16465
|
+
`
|
|
16466
|
+
## Default publishBy for new ideas: ${publishBy}`
|
|
16467
|
+
];
|
|
16468
|
+
const steps = [
|
|
16469
|
+
"\n## Your Steps:",
|
|
16470
|
+
"1. For each clip, call get_clip_transcript to read what the creator said.",
|
|
16471
|
+
"2. Compare each clip's content against the existing ideas above.",
|
|
16472
|
+
"3. For strong matches, call assign_idea_to_clip with the existing idea's issue number."
|
|
16473
|
+
];
|
|
16474
|
+
if (hasMcpServers) {
|
|
16475
|
+
steps.push(
|
|
16476
|
+
"4. For unmatched clips, use web search tools to research trending context for the topic.",
|
|
16477
|
+
"5. Call create_idea_for_clip with the topic derived from transcript + trend context from research."
|
|
16478
|
+
);
|
|
16479
|
+
} else {
|
|
16480
|
+
steps.push(
|
|
16481
|
+
"4. For unmatched clips, call create_idea_for_clip with the topic derived from the transcript."
|
|
16482
|
+
);
|
|
16483
|
+
}
|
|
16484
|
+
steps.push(
|
|
16485
|
+
`${hasMcpServers ? "6" : "5"}. Once ALL clips have assignments, call finalize_assignments.`
|
|
16486
|
+
);
|
|
16487
|
+
sections.push(steps.join("\n"));
|
|
16488
|
+
return sections.join("\n");
|
|
16489
|
+
}
|
|
16490
|
+
var IdeaDiscoveryAgent = class extends BaseAgent {
|
|
16491
|
+
constructor(input) {
|
|
16492
|
+
super("IdeaDiscoveryAgent", SYSTEM_PROMPT6);
|
|
16493
|
+
this.input = input;
|
|
16494
|
+
this.clips = clipsToInfo(input.shorts, input.mediumClips);
|
|
16495
|
+
this.transcript = input.transcript;
|
|
16496
|
+
this.publishBy = input.publishBy;
|
|
16497
|
+
this.defaultPlatforms = input.defaultPlatforms;
|
|
16498
|
+
}
|
|
16499
|
+
assignments = [];
|
|
16500
|
+
newIdeas = [];
|
|
16501
|
+
finalized = false;
|
|
16502
|
+
clips;
|
|
16503
|
+
transcript;
|
|
16504
|
+
publishBy;
|
|
16505
|
+
defaultPlatforms;
|
|
16506
|
+
getTimeoutMs() {
|
|
16507
|
+
return 0;
|
|
16508
|
+
}
|
|
16509
|
+
async discover() {
|
|
16510
|
+
if (this.clips.length === 0) {
|
|
16511
|
+
return { assignments: [], newIdeas: [], matchedCount: 0, createdCount: 0 };
|
|
16512
|
+
}
|
|
16513
|
+
const allIdeas = await this.loadIdeas();
|
|
16514
|
+
const hasMcp = this.getMcpServers() !== void 0;
|
|
16515
|
+
const userMessage = buildUserMessage(
|
|
16516
|
+
this.clips,
|
|
16517
|
+
allIdeas,
|
|
16518
|
+
this.input.summary,
|
|
16519
|
+
this.publishBy,
|
|
16520
|
+
hasMcp
|
|
16521
|
+
);
|
|
16522
|
+
await this.run(userMessage);
|
|
16523
|
+
return {
|
|
16524
|
+
assignments: [...this.assignments],
|
|
16525
|
+
newIdeas: [...this.newIdeas],
|
|
16526
|
+
matchedCount: this.assignments.filter(
|
|
16527
|
+
(a) => !this.newIdeas.some((idea) => idea.issueNumber === a.ideaIssueNumber)
|
|
16528
|
+
).length,
|
|
16529
|
+
createdCount: this.newIdeas.length
|
|
16530
|
+
};
|
|
16531
|
+
}
|
|
16532
|
+
async loadIdeas() {
|
|
16533
|
+
if (this.input.providedIdeas && this.input.providedIdeas.length > 0) {
|
|
16534
|
+
return [...this.input.providedIdeas];
|
|
16535
|
+
}
|
|
16536
|
+
try {
|
|
16537
|
+
const readyIdeas = await listIdeas({ status: "ready" });
|
|
16538
|
+
const draftIdeas = await listIdeas({ status: "draft" });
|
|
16539
|
+
return [...readyIdeas, ...draftIdeas];
|
|
16540
|
+
} catch (err) {
|
|
16541
|
+
logger_default.warn(`[IdeaDiscoveryAgent] Failed to fetch ideas: ${err instanceof Error ? err.message : String(err)}`);
|
|
16542
|
+
return this.input.existingIdeas ? [...this.input.existingIdeas] : [];
|
|
16543
|
+
}
|
|
16544
|
+
}
|
|
16545
|
+
resetForRetry() {
|
|
16546
|
+
this.assignments = [];
|
|
16547
|
+
this.newIdeas = [];
|
|
16548
|
+
this.finalized = false;
|
|
16549
|
+
}
|
|
16550
|
+
getMcpServers() {
|
|
16551
|
+
const config2 = getConfig();
|
|
16552
|
+
const servers = {};
|
|
16553
|
+
if (config2.EXA_API_KEY) {
|
|
16554
|
+
servers.exa = {
|
|
16555
|
+
type: "http",
|
|
16556
|
+
url: `${config2.EXA_MCP_URL}?exaApiKey=${config2.EXA_API_KEY}&tools=web_search_exa`,
|
|
16557
|
+
headers: {},
|
|
16558
|
+
tools: ["*"]
|
|
16559
|
+
};
|
|
16560
|
+
}
|
|
16561
|
+
if (config2.PERPLEXITY_API_KEY) {
|
|
16562
|
+
servers.perplexity = {
|
|
16563
|
+
type: "local",
|
|
16564
|
+
command: "npx",
|
|
16565
|
+
args: ["-y", "perplexity-mcp"],
|
|
16566
|
+
env: { PERPLEXITY_API_KEY: config2.PERPLEXITY_API_KEY },
|
|
16567
|
+
tools: ["*"]
|
|
16568
|
+
};
|
|
16569
|
+
}
|
|
16570
|
+
return Object.keys(servers).length > 0 ? servers : void 0;
|
|
16571
|
+
}
|
|
16572
|
+
getTools() {
|
|
16573
|
+
return [
|
|
16574
|
+
{
|
|
16575
|
+
name: "get_clip_transcript",
|
|
16576
|
+
description: "Get the transcript text for a specific clip by its ID. Returns the text spoken in that time range.",
|
|
16577
|
+
parameters: {
|
|
16578
|
+
type: "object",
|
|
16579
|
+
properties: {
|
|
16580
|
+
clipId: { type: "string", description: 'The clip ID (e.g., "short-1" or "medium-1")' }
|
|
16581
|
+
},
|
|
16582
|
+
required: ["clipId"]
|
|
16583
|
+
},
|
|
16584
|
+
handler: async (args) => this.handleToolCall("get_clip_transcript", args)
|
|
16585
|
+
},
|
|
16586
|
+
{
|
|
16587
|
+
name: "assign_idea_to_clip",
|
|
16588
|
+
description: "Assign an existing idea to a clip. Only use when there is a STRONG topical match between the clip content and the idea.",
|
|
16589
|
+
parameters: {
|
|
16590
|
+
type: "object",
|
|
16591
|
+
properties: {
|
|
16592
|
+
clipId: { type: "string", description: "The clip ID to assign" },
|
|
16593
|
+
ideaIssueNumber: { type: "number", description: "The GitHub Issue number of the matching idea" },
|
|
16594
|
+
reason: { type: "string", description: "Brief explanation of why this is a strong match" }
|
|
16595
|
+
},
|
|
16596
|
+
required: ["clipId", "ideaIssueNumber", "reason"]
|
|
16597
|
+
},
|
|
16598
|
+
handler: async (args) => this.handleToolCall("assign_idea_to_clip", args)
|
|
16599
|
+
},
|
|
16600
|
+
{
|
|
16601
|
+
name: "create_idea_for_clip",
|
|
16602
|
+
description: "Create a new idea from the clip content and assign it. Use when no existing idea matches the clip topic.",
|
|
16603
|
+
parameters: {
|
|
16604
|
+
type: "object",
|
|
16605
|
+
properties: {
|
|
16606
|
+
clipId: { type: "string", description: "The clip ID to create an idea for" },
|
|
16607
|
+
topic: { type: "string", description: "Main topic (derived from transcript content)" },
|
|
16608
|
+
hook: { type: "string", description: "Attention-grabbing angle (\u226480 chars, from what the creator actually said)" },
|
|
16609
|
+
audience: { type: "string", description: "Target audience for this content" },
|
|
16610
|
+
keyTakeaway: { type: "string", description: "The one thing viewers should remember (from transcript)" },
|
|
16611
|
+
talkingPoints: {
|
|
16612
|
+
type: "array",
|
|
16613
|
+
items: { type: "string" },
|
|
16614
|
+
description: "Key points covered (from actual transcript content)"
|
|
16615
|
+
},
|
|
16616
|
+
tags: {
|
|
16617
|
+
type: "array",
|
|
16618
|
+
items: { type: "string" },
|
|
16619
|
+
description: "Categorization tags (lowercase)"
|
|
16620
|
+
},
|
|
16621
|
+
trendContext: {
|
|
16622
|
+
type: "string",
|
|
16623
|
+
description: "Optional: why this topic is timely NOW (from web research if available)"
|
|
16624
|
+
}
|
|
16625
|
+
},
|
|
16626
|
+
required: ["clipId", "topic", "hook", "audience", "keyTakeaway", "talkingPoints", "tags"]
|
|
16627
|
+
},
|
|
16628
|
+
handler: async (args) => this.handleToolCall("create_idea_for_clip", args)
|
|
16629
|
+
},
|
|
16630
|
+
{
|
|
16631
|
+
name: "finalize_assignments",
|
|
16632
|
+
description: "Confirm all clip-to-idea assignments are complete. Call exactly once when every clip has been assigned.",
|
|
16633
|
+
parameters: {
|
|
16634
|
+
type: "object",
|
|
16635
|
+
properties: {
|
|
16636
|
+
summary: { type: "string", description: "Brief summary of assignments made" }
|
|
16637
|
+
},
|
|
16638
|
+
required: ["summary"]
|
|
16639
|
+
},
|
|
16640
|
+
handler: async (args) => this.handleToolCall("finalize_assignments", args)
|
|
16641
|
+
}
|
|
16642
|
+
];
|
|
16643
|
+
}
|
|
16644
|
+
async handleToolCall(toolName, args) {
|
|
16645
|
+
switch (toolName) {
|
|
16646
|
+
case "get_clip_transcript":
|
|
16647
|
+
return this.handleGetClipTranscript(args);
|
|
16648
|
+
case "assign_idea_to_clip":
|
|
16649
|
+
return this.handleAssignIdea(args);
|
|
16650
|
+
case "create_idea_for_clip":
|
|
16651
|
+
return this.handleCreateIdeaForClip(args);
|
|
16652
|
+
case "finalize_assignments":
|
|
16653
|
+
return this.handleFinalize(args);
|
|
16654
|
+
default:
|
|
16655
|
+
throw new Error(`Unknown tool: ${toolName}`);
|
|
16656
|
+
}
|
|
16657
|
+
}
|
|
16658
|
+
handleGetClipTranscript(args) {
|
|
16659
|
+
const clipId = String(args.clipId ?? "");
|
|
16660
|
+
const clip = this.clips.find((c) => c.id === clipId);
|
|
16661
|
+
if (!clip) {
|
|
16662
|
+
throw new Error(`Clip not found: ${clipId}. Available: ${this.clips.map((c) => c.id).join(", ")}`);
|
|
16663
|
+
}
|
|
16664
|
+
const texts = [];
|
|
16665
|
+
for (const seg of clip.segments) {
|
|
16666
|
+
const text = getTranscriptForTimeRange(this.transcript, seg.start, seg.end);
|
|
16667
|
+
if (text) texts.push(text);
|
|
16668
|
+
}
|
|
16669
|
+
return { clipId, transcript: texts.join("\n\n") || "(No transcript found for this time range)" };
|
|
16670
|
+
}
|
|
16671
|
+
handleAssignIdea(args) {
|
|
16672
|
+
const clipId = String(args.clipId ?? "");
|
|
16673
|
+
const ideaIssueNumber = Number(args.ideaIssueNumber);
|
|
16674
|
+
const reason = String(args.reason ?? "");
|
|
16675
|
+
if (!clipId || !this.clips.find((c) => c.id === clipId)) {
|
|
16676
|
+
throw new Error(`Invalid clipId: ${clipId}`);
|
|
16677
|
+
}
|
|
16678
|
+
if (!Number.isInteger(ideaIssueNumber) || ideaIssueNumber <= 0) {
|
|
16679
|
+
throw new Error(`Invalid ideaIssueNumber: ${ideaIssueNumber}`);
|
|
16680
|
+
}
|
|
16681
|
+
if (this.assignments.some((a) => a.clipId === clipId)) {
|
|
16682
|
+
throw new Error(`Clip ${clipId} already has an assignment`);
|
|
16683
|
+
}
|
|
16684
|
+
this.assignments.push({ clipId, ideaIssueNumber });
|
|
16685
|
+
logger_default.info(`[IdeaDiscoveryAgent] Matched ${clipId} \u2192 idea #${ideaIssueNumber}: ${reason}`);
|
|
16686
|
+
return { clipId, ideaIssueNumber, status: "assigned" };
|
|
16687
|
+
}
|
|
16688
|
+
async handleCreateIdeaForClip(args) {
|
|
16689
|
+
const clipId = String(args.clipId ?? "");
|
|
16690
|
+
if (!clipId || !this.clips.find((c) => c.id === clipId)) {
|
|
16691
|
+
throw new Error(`Invalid clipId: ${clipId}`);
|
|
16692
|
+
}
|
|
16693
|
+
if (this.assignments.some((a) => a.clipId === clipId)) {
|
|
16694
|
+
throw new Error(`Clip ${clipId} already has an assignment`);
|
|
16695
|
+
}
|
|
16696
|
+
const hook = String(args.hook ?? "").trim();
|
|
16697
|
+
if (hook.length > 80) {
|
|
16698
|
+
throw new Error(`Hook must be 80 characters or fewer: ${hook}`);
|
|
16699
|
+
}
|
|
16700
|
+
const talkingPoints = Array.isArray(args.talkingPoints) ? args.talkingPoints.map((tp) => String(tp).trim()).filter((tp) => tp.length > 0) : [];
|
|
16701
|
+
if (talkingPoints.length === 0) {
|
|
16702
|
+
throw new Error("talkingPoints must be a non-empty array of strings");
|
|
16703
|
+
}
|
|
16704
|
+
const tags = Array.isArray(args.tags) ? args.tags.map((t) => String(t).trim().toLowerCase()).filter((t) => t.length > 0) : [];
|
|
16705
|
+
const input = {
|
|
16706
|
+
topic: String(args.topic ?? "").trim(),
|
|
16707
|
+
hook,
|
|
16708
|
+
audience: String(args.audience ?? "").trim(),
|
|
16709
|
+
keyTakeaway: String(args.keyTakeaway ?? "").trim(),
|
|
16710
|
+
talkingPoints,
|
|
16711
|
+
platforms: [...this.defaultPlatforms],
|
|
16712
|
+
tags,
|
|
16713
|
+
publishBy: this.publishBy,
|
|
16714
|
+
trendContext: typeof args.trendContext === "string" ? args.trendContext.trim() || void 0 : void 0
|
|
16715
|
+
};
|
|
16716
|
+
const idea = await createIdea(input);
|
|
16717
|
+
this.newIdeas.push(idea);
|
|
16718
|
+
this.assignments.push({ clipId, ideaIssueNumber: idea.issueNumber });
|
|
16719
|
+
logger_default.info(`[IdeaDiscoveryAgent] Created idea #${idea.issueNumber} ("${idea.topic}") for ${clipId}`);
|
|
16720
|
+
return { clipId, ideaIssueNumber: idea.issueNumber, status: "created" };
|
|
16721
|
+
}
|
|
16722
|
+
handleFinalize(args) {
|
|
16723
|
+
this.finalized = true;
|
|
16724
|
+
const summary = String(args.summary ?? "");
|
|
16725
|
+
const assignedIds = new Set(this.assignments.map((a) => a.clipId));
|
|
16726
|
+
const unassigned = this.clips.filter((c) => !assignedIds.has(c.id)).map((c) => c.id);
|
|
16727
|
+
const matchedCount = this.assignments.filter(
|
|
16728
|
+
(a) => !this.newIdeas.some((idea) => idea.issueNumber === a.ideaIssueNumber)
|
|
16729
|
+
).length;
|
|
16730
|
+
logger_default.info(`[IdeaDiscoveryAgent] Finalized: ${this.assignments.length}/${this.clips.length} clips assigned (${matchedCount} matched, ${this.newIdeas.length} created). ${summary}`);
|
|
16731
|
+
if (unassigned.length > 0) {
|
|
16732
|
+
logger_default.warn(`[IdeaDiscoveryAgent] ${unassigned.length} clips unassigned: ${unassigned.join(", ")}`);
|
|
16733
|
+
}
|
|
16734
|
+
return {
|
|
16735
|
+
totalClips: this.clips.length,
|
|
16736
|
+
assigned: this.assignments.length,
|
|
16737
|
+
matched: matchedCount,
|
|
16738
|
+
created: this.newIdeas.length,
|
|
16739
|
+
unassigned
|
|
16740
|
+
};
|
|
16741
|
+
}
|
|
16742
|
+
async destroy() {
|
|
16743
|
+
await super.destroy();
|
|
16744
|
+
}
|
|
16745
|
+
};
|
|
16746
|
+
|
|
16235
16747
|
// src/L4-agents/GraphicsAgent.ts
|
|
16236
16748
|
init_BaseAgent();
|
|
16237
16749
|
init_paths();
|
|
16238
16750
|
init_fileSystem();
|
|
16239
16751
|
init_configLogger();
|
|
16240
16752
|
import sharp from "sharp";
|
|
16241
|
-
var
|
|
16753
|
+
var SYSTEM_PROMPT7 = `You are a visual content designer and editorial director for educational video content. You are given an editorial report from a video analyst describing moments in a video where AI-generated image overlays could enhance viewer comprehension.
|
|
16242
16754
|
|
|
16243
16755
|
Your job is to make the FINAL editorial decision for each opportunity:
|
|
16244
16756
|
1. Decide whether to generate an image or skip the opportunity
|
|
@@ -16321,7 +16833,7 @@ var GraphicsAgent = class extends BaseAgent {
|
|
|
16321
16833
|
enhancementsDir = "";
|
|
16322
16834
|
imageIndex = 0;
|
|
16323
16835
|
constructor(model) {
|
|
16324
|
-
super("GraphicsAgent",
|
|
16836
|
+
super("GraphicsAgent", SYSTEM_PROMPT7, void 0, model);
|
|
16325
16837
|
}
|
|
16326
16838
|
resetForRetry() {
|
|
16327
16839
|
this.overlays = [];
|
|
@@ -16486,6 +16998,8 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
|
|
|
16486
16998
|
slug;
|
|
16487
16999
|
/** Content ideas linked to this video for editorial direction */
|
|
16488
17000
|
_ideas = [];
|
|
17001
|
+
/** Per-clip idea assignments from idea discovery (clipId → ideaIssueNumber) */
|
|
17002
|
+
_clipIdeaMap = /* @__PURE__ */ new Map();
|
|
16489
17003
|
/** Set ideas for editorial direction */
|
|
16490
17004
|
setIdeas(ideas) {
|
|
16491
17005
|
this._ideas = ideas;
|
|
@@ -16494,6 +17008,10 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
|
|
|
16494
17008
|
get ideas() {
|
|
16495
17009
|
return this._ideas;
|
|
16496
17010
|
}
|
|
17011
|
+
/** Get per-clip idea assignments */
|
|
17012
|
+
get clipIdeaMap() {
|
|
17013
|
+
return this._clipIdeaMap;
|
|
17014
|
+
}
|
|
16497
17015
|
constructor(sourcePath, videoDir, slug) {
|
|
16498
17016
|
super();
|
|
16499
17017
|
this.sourcePath = sourcePath;
|
|
@@ -16654,12 +17172,12 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
|
|
|
16654
17172
|
await transcodeToMp42(sourcePath, destPath);
|
|
16655
17173
|
logger_default.info(`Transcoded video to ${destPath}`);
|
|
16656
17174
|
} else {
|
|
16657
|
-
await new Promise((
|
|
17175
|
+
await new Promise((resolve4, reject) => {
|
|
16658
17176
|
const readStream = openReadStream(sourcePath);
|
|
16659
17177
|
const writeStream = openWriteStream(destPath);
|
|
16660
17178
|
readStream.on("error", reject);
|
|
16661
17179
|
writeStream.on("error", reject);
|
|
16662
|
-
writeStream.on("finish",
|
|
17180
|
+
writeStream.on("finish", resolve4);
|
|
16663
17181
|
readStream.pipe(writeStream);
|
|
16664
17182
|
});
|
|
16665
17183
|
logger_default.info(`Copied video to ${destPath}`);
|
|
@@ -17413,6 +17931,47 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
|
|
|
17413
17931
|
}
|
|
17414
17932
|
return posts;
|
|
17415
17933
|
}
|
|
17934
|
+
/**
|
|
17935
|
+
* Run idea discovery: match clips to existing ideas, create new ones for unmatched.
|
|
17936
|
+
* Updates clip objects with ideaIssueNumber and adds newly created ideas to _ideas.
|
|
17937
|
+
*/
|
|
17938
|
+
async discoverIdeas(shorts, mediumClips, publishBy) {
|
|
17939
|
+
const transcript = await this.getTranscript();
|
|
17940
|
+
const summary = await this.getSummary();
|
|
17941
|
+
const brand = (await Promise.resolve().then(() => (init_brand(), brand_exports))).getBrandConfig();
|
|
17942
|
+
const defaultPlatforms = brand.hashtags?.platforms ? Object.keys(brand.hashtags.platforms).map((p) => p) : ["youtube" /* YouTube */, "tiktok" /* TikTok */, "instagram" /* Instagram */, "linkedin" /* LinkedIn */, "x" /* X */];
|
|
17943
|
+
const agent = new IdeaDiscoveryAgent({
|
|
17944
|
+
shorts,
|
|
17945
|
+
mediumClips,
|
|
17946
|
+
transcript: transcript.segments,
|
|
17947
|
+
summary: typeof summary === "string" ? summary : summary?.overview ?? "",
|
|
17948
|
+
providedIdeas: this._ideas.length > 0 ? this._ideas : void 0,
|
|
17949
|
+
publishBy,
|
|
17950
|
+
defaultPlatforms
|
|
17951
|
+
});
|
|
17952
|
+
const result = await agent.discover();
|
|
17953
|
+
for (const assignment of result.assignments) {
|
|
17954
|
+
this._clipIdeaMap.set(assignment.clipId, assignment.ideaIssueNumber);
|
|
17955
|
+
const short = shorts.find((s) => s.id === assignment.clipId);
|
|
17956
|
+
if (short) {
|
|
17957
|
+
short.ideaIssueNumber = assignment.ideaIssueNumber;
|
|
17958
|
+
continue;
|
|
17959
|
+
}
|
|
17960
|
+
const medium = mediumClips.find((m) => m.id === assignment.clipId);
|
|
17961
|
+
if (medium) {
|
|
17962
|
+
medium.ideaIssueNumber = assignment.ideaIssueNumber;
|
|
17963
|
+
}
|
|
17964
|
+
}
|
|
17965
|
+
if (result.newIdeas.length > 0) {
|
|
17966
|
+
const existingIds = new Set(this._ideas.map((i) => i.issueNumber));
|
|
17967
|
+
for (const idea of result.newIdeas) {
|
|
17968
|
+
if (!existingIds.has(idea.issueNumber)) {
|
|
17969
|
+
this._ideas.push(idea);
|
|
17970
|
+
}
|
|
17971
|
+
}
|
|
17972
|
+
}
|
|
17973
|
+
return result;
|
|
17974
|
+
}
|
|
17416
17975
|
/**
|
|
17417
17976
|
* Build the publish queue via the queue builder service.
|
|
17418
17977
|
*/
|
|
@@ -17525,33 +18084,20 @@ async function buildRealignPlan(options = {}) {
|
|
|
17525
18084
|
tagged.push({ post, platform, clipType });
|
|
17526
18085
|
}
|
|
17527
18086
|
const bookedMap = await buildBookedMap();
|
|
17528
|
-
const
|
|
17529
|
-
timezone,
|
|
17530
|
-
bookedMap,
|
|
17531
|
-
ideaLinkedPostIds: /* @__PURE__ */ new Set(),
|
|
17532
|
-
lateClient: client,
|
|
17533
|
-
displacementEnabled: getDisplacementConfig().enabled,
|
|
17534
|
-
dryRun: true,
|
|
17535
|
-
depth: 0,
|
|
17536
|
-
ideaRefs: [],
|
|
17537
|
-
samePlatformMs: 0,
|
|
17538
|
-
crossPlatformMs: 0,
|
|
17539
|
-
platform: ""
|
|
17540
|
-
};
|
|
18087
|
+
const ideaLinkedPostIds = /* @__PURE__ */ new Set();
|
|
17541
18088
|
for (const [, slot] of bookedMap) {
|
|
17542
18089
|
if (slot.ideaLinked && slot.postId) {
|
|
17543
|
-
|
|
18090
|
+
ideaLinkedPostIds.add(slot.postId);
|
|
17544
18091
|
}
|
|
17545
18092
|
}
|
|
17546
18093
|
const result = [];
|
|
17547
18094
|
const toCancel = [];
|
|
17548
18095
|
let skipped = 0;
|
|
17549
18096
|
tagged.sort((a, b) => {
|
|
17550
|
-
const aIdea =
|
|
17551
|
-
const bIdea =
|
|
18097
|
+
const aIdea = ideaLinkedPostIds.has(a.post._id) ? 0 : 1;
|
|
18098
|
+
const bIdea = ideaLinkedPostIds.has(b.post._id) ? 0 : 1;
|
|
17552
18099
|
return aIdea - bIdea;
|
|
17553
18100
|
});
|
|
17554
|
-
const nowMs = Date.now();
|
|
17555
18101
|
for (const { post, platform, clipType } of tagged) {
|
|
17556
18102
|
const schedulePlatform = normalizeSchedulePlatform(platform);
|
|
17557
18103
|
const platformConfig = getPlatformSchedule(schedulePlatform, clipType);
|
|
@@ -17572,9 +18118,13 @@ async function buildRealignPlan(options = {}) {
|
|
|
17572
18118
|
bookedMap.delete(currentMs2);
|
|
17573
18119
|
}
|
|
17574
18120
|
}
|
|
17575
|
-
const isIdea =
|
|
17576
|
-
const
|
|
17577
|
-
|
|
18121
|
+
const isIdea = ideaLinkedPostIds.has(post._id);
|
|
18122
|
+
const newSlot = await schedulePost(schedulePlatform, clipType, {
|
|
18123
|
+
postId: post._id,
|
|
18124
|
+
ideaIds: isIdea ? bookedMap.get(new Date(post.scheduledFor ?? 0).getTime())?.ideaIds ?? [] : void 0,
|
|
18125
|
+
dryRun: true,
|
|
18126
|
+
_bookedMap: bookedMap
|
|
18127
|
+
});
|
|
17578
18128
|
if (!newSlot) {
|
|
17579
18129
|
if (post.status !== "cancelled") {
|
|
17580
18130
|
toCancel.push({ post, platform, clipType, reason: `No available slot for ${schedulePlatform}/${clipType}` });
|
|
@@ -17582,7 +18132,7 @@ async function buildRealignPlan(options = {}) {
|
|
|
17582
18132
|
continue;
|
|
17583
18133
|
}
|
|
17584
18134
|
const newMs = new Date(newSlot).getTime();
|
|
17585
|
-
|
|
18135
|
+
bookedMap.set(newMs, {
|
|
17586
18136
|
scheduledFor: newSlot,
|
|
17587
18137
|
source: "late",
|
|
17588
18138
|
postId: post._id,
|
|
@@ -17662,7 +18212,7 @@ var TOOL_LABELS = {
|
|
|
17662
18212
|
check_realign_status: "\u{1F4CA} Checking realignment progress",
|
|
17663
18213
|
ask_user: "\u{1F4AC} Asking for your input"
|
|
17664
18214
|
};
|
|
17665
|
-
var
|
|
18215
|
+
var SYSTEM_PROMPT8 = `You are a schedule management assistant for Late.co social media posts.
|
|
17666
18216
|
|
|
17667
18217
|
You help the user view, analyze, and reprioritize their posting schedule across platforms.
|
|
17668
18218
|
|
|
@@ -17685,7 +18235,7 @@ var ScheduleAgent = class extends BaseAgent {
|
|
|
17685
18235
|
chatOutput;
|
|
17686
18236
|
realignJobs = /* @__PURE__ */ new Map();
|
|
17687
18237
|
constructor(userInputHandler, model) {
|
|
17688
|
-
super("ScheduleAgent",
|
|
18238
|
+
super("ScheduleAgent", SYSTEM_PROMPT8, void 0, model);
|
|
17689
18239
|
this.userInputHandler = userInputHandler;
|
|
17690
18240
|
}
|
|
17691
18241
|
/** Set a callback for chat-friendly status messages (tool starts, progress). */
|
|
@@ -18286,7 +18836,7 @@ function buildSystemPrompt4(brand, existingIdeas, seedTopics, count, ideaRepo) {
|
|
|
18286
18836
|
}
|
|
18287
18837
|
return promptSections.join("\n");
|
|
18288
18838
|
}
|
|
18289
|
-
function
|
|
18839
|
+
function buildUserMessage2(count, seedTopics, hasMcpServers, userPrompt) {
|
|
18290
18840
|
const focusText = seedTopics.length > 0 ? `Focus areas: ${seedTopics.join(", ")}` : "Focus areas: choose the strongest timely opportunities from the creator context and current trends.";
|
|
18291
18841
|
const steps = [
|
|
18292
18842
|
"1. Call get_brand_context to load the creator profile.",
|
|
@@ -18835,7 +19385,7 @@ async function generateIdeas(options = {}) {
|
|
|
18835
19385
|
});
|
|
18836
19386
|
try {
|
|
18837
19387
|
const hasMcpServers = !!(config2.EXA_API_KEY || config2.YOUTUBE_API_KEY || config2.PERPLEXITY_API_KEY);
|
|
18838
|
-
const userMessage =
|
|
19388
|
+
const userMessage = buildUserMessage2(count, seedTopics, hasMcpServers, options.prompt);
|
|
18839
19389
|
await agent.run(userMessage);
|
|
18840
19390
|
const ideas = agent.getGeneratedIdeas();
|
|
18841
19391
|
if (!agent.isFinalized()) {
|
|
@@ -18894,7 +19444,7 @@ var interviewEmitter = new InterviewEmitter();
|
|
|
18894
19444
|
|
|
18895
19445
|
// src/L4-agents/InterviewAgent.ts
|
|
18896
19446
|
init_configLogger();
|
|
18897
|
-
var
|
|
19447
|
+
var SYSTEM_PROMPT9 = `You are a Socratic interview coach helping a content creator sharpen their video idea. Ask ONE short question at a time (1 sentence max).
|
|
18898
19448
|
|
|
18899
19449
|
## Rules
|
|
18900
19450
|
- Every question must be a SINGLE sentence. No multi-part questions. No preamble. No encouragement filler.
|
|
@@ -18923,7 +19473,7 @@ var InterviewAgent = class extends BaseAgent {
|
|
|
18923
19473
|
ended = false;
|
|
18924
19474
|
idea = null;
|
|
18925
19475
|
constructor(model) {
|
|
18926
|
-
super("InterviewAgent",
|
|
19476
|
+
super("InterviewAgent", SYSTEM_PROMPT9, void 0, model);
|
|
18927
19477
|
}
|
|
18928
19478
|
getTimeoutMs() {
|
|
18929
19479
|
return 0;
|
|
@@ -19194,6 +19744,296 @@ var InterviewAgent = class extends BaseAgent {
|
|
|
19194
19744
|
}
|
|
19195
19745
|
};
|
|
19196
19746
|
|
|
19747
|
+
// src/L4-agents/AgendaAgent.ts
|
|
19748
|
+
init_brand();
|
|
19749
|
+
init_configLogger();
|
|
19750
|
+
init_BaseAgent();
|
|
19751
|
+
var SYSTEM_PROMPT10 = `You are a recording agenda planner for a content creator. You take multiple video ideas and structure them into a single cohesive recording agenda with natural transitions, time estimates, and recording notes.
|
|
19752
|
+
|
|
19753
|
+
## Rules
|
|
19754
|
+
- You MUST use tools for ALL output. Never respond with plain text.
|
|
19755
|
+
- Structure ideas into a logical recording flow that feels natural, not disjointed.
|
|
19756
|
+
- Estimate ~2 minutes per talking point as a baseline, then adjust for complexity.
|
|
19757
|
+
- Write an engaging intro hook that previews what the video will cover.
|
|
19758
|
+
- Write a CTA outro (subscribe, like, follow).
|
|
19759
|
+
- Include recording notes for each section: energy cues, visual references, key phrases to emphasize.
|
|
19760
|
+
- Transitions between sections should feel conversational, not abrupt.
|
|
19761
|
+
|
|
19762
|
+
## Process
|
|
19763
|
+
1. Call get_brand_context to load the creator's voice, style, and content pillars.
|
|
19764
|
+
2. Call get_idea_details for each idea to inspect the full content.
|
|
19765
|
+
3. Call add_section for each idea in the order you want them recorded. Set the order field sequentially starting at 1.
|
|
19766
|
+
4. Call set_intro with an engaging opening hook.
|
|
19767
|
+
5. Call set_outro with a closing CTA.
|
|
19768
|
+
6. Call finalize_agenda with a brief summary of the agenda.
|
|
19769
|
+
|
|
19770
|
+
## Ordering Strategy
|
|
19771
|
+
- Lead with the most attention-grabbing idea (strongest hook).
|
|
19772
|
+
- Group related topics so transitions feel natural.
|
|
19773
|
+
- End with the most forward-looking or actionable topic (leaves viewers motivated).
|
|
19774
|
+
- Alternate energy levels: high-energy topic \u2192 reflective topic \u2192 high-energy.`;
|
|
19775
|
+
var AgendaAgent = class extends BaseAgent {
|
|
19776
|
+
ideas = [];
|
|
19777
|
+
sections = [];
|
|
19778
|
+
intro = "";
|
|
19779
|
+
outro = "";
|
|
19780
|
+
finalized = false;
|
|
19781
|
+
constructor(ideas, model) {
|
|
19782
|
+
super("AgendaAgent", SYSTEM_PROMPT10, void 0, model);
|
|
19783
|
+
this.ideas = ideas;
|
|
19784
|
+
}
|
|
19785
|
+
resetForRetry() {
|
|
19786
|
+
this.sections = [];
|
|
19787
|
+
this.intro = "";
|
|
19788
|
+
this.outro = "";
|
|
19789
|
+
this.finalized = false;
|
|
19790
|
+
}
|
|
19791
|
+
getTools() {
|
|
19792
|
+
return [
|
|
19793
|
+
{
|
|
19794
|
+
name: "get_brand_context",
|
|
19795
|
+
description: "Return the creator brand context including voice, style, and content pillars.",
|
|
19796
|
+
parameters: {
|
|
19797
|
+
type: "object",
|
|
19798
|
+
properties: {}
|
|
19799
|
+
},
|
|
19800
|
+
handler: async (args) => this.handleToolCall("get_brand_context", args)
|
|
19801
|
+
},
|
|
19802
|
+
{
|
|
19803
|
+
name: "get_idea_details",
|
|
19804
|
+
description: "Return the full details of an idea by its index in the provided array.",
|
|
19805
|
+
parameters: {
|
|
19806
|
+
type: "object",
|
|
19807
|
+
properties: {
|
|
19808
|
+
ideaIndex: {
|
|
19809
|
+
type: "number",
|
|
19810
|
+
description: "Zero-based index of the idea in the provided array."
|
|
19811
|
+
}
|
|
19812
|
+
},
|
|
19813
|
+
required: ["ideaIndex"],
|
|
19814
|
+
additionalProperties: false
|
|
19815
|
+
},
|
|
19816
|
+
handler: async (args) => this.handleToolCall("get_idea_details", args)
|
|
19817
|
+
},
|
|
19818
|
+
{
|
|
19819
|
+
name: "add_section",
|
|
19820
|
+
description: "Add a recording section to the agenda. Call once per idea, in the order they should be recorded.",
|
|
19821
|
+
parameters: {
|
|
19822
|
+
type: "object",
|
|
19823
|
+
properties: {
|
|
19824
|
+
title: {
|
|
19825
|
+
type: "string",
|
|
19826
|
+
description: "Section title for the recording outline."
|
|
19827
|
+
},
|
|
19828
|
+
ideaIssueNumber: {
|
|
19829
|
+
type: "number",
|
|
19830
|
+
description: "GitHub Issue number of the idea this section covers."
|
|
19831
|
+
},
|
|
19832
|
+
estimatedMinutes: {
|
|
19833
|
+
type: "number",
|
|
19834
|
+
description: "Estimated recording time in minutes for this section."
|
|
19835
|
+
},
|
|
19836
|
+
talkingPoints: {
|
|
19837
|
+
type: "array",
|
|
19838
|
+
items: { type: "string" },
|
|
19839
|
+
description: "Talking points to cover in this section."
|
|
19840
|
+
},
|
|
19841
|
+
transition: {
|
|
19842
|
+
type: "string",
|
|
19843
|
+
description: "Transition phrase to lead into the next section. Empty string for the last section."
|
|
19844
|
+
},
|
|
19845
|
+
notes: {
|
|
19846
|
+
type: "string",
|
|
19847
|
+
description: "Recording notes: energy cues, visual references, key phrases to emphasize."
|
|
19848
|
+
}
|
|
19849
|
+
},
|
|
19850
|
+
required: ["title", "ideaIssueNumber", "estimatedMinutes", "talkingPoints", "transition", "notes"],
|
|
19851
|
+
additionalProperties: false
|
|
19852
|
+
},
|
|
19853
|
+
handler: async (args) => this.handleToolCall("add_section", args)
|
|
19854
|
+
},
|
|
19855
|
+
{
|
|
19856
|
+
name: "set_intro",
|
|
19857
|
+
description: "Set the opening hook text for the recording.",
|
|
19858
|
+
parameters: {
|
|
19859
|
+
type: "object",
|
|
19860
|
+
properties: {
|
|
19861
|
+
text: {
|
|
19862
|
+
type: "string",
|
|
19863
|
+
description: "Opening hook text that previews what the video will cover."
|
|
19864
|
+
}
|
|
19865
|
+
},
|
|
19866
|
+
required: ["text"],
|
|
19867
|
+
additionalProperties: false
|
|
19868
|
+
},
|
|
19869
|
+
handler: async (args) => this.handleToolCall("set_intro", args)
|
|
19870
|
+
},
|
|
19871
|
+
{
|
|
19872
|
+
name: "set_outro",
|
|
19873
|
+
description: "Set the closing CTA text for the recording.",
|
|
19874
|
+
parameters: {
|
|
19875
|
+
type: "object",
|
|
19876
|
+
properties: {
|
|
19877
|
+
text: {
|
|
19878
|
+
type: "string",
|
|
19879
|
+
description: "Closing call-to-action text (subscribe, like, follow)."
|
|
19880
|
+
}
|
|
19881
|
+
},
|
|
19882
|
+
required: ["text"],
|
|
19883
|
+
additionalProperties: false
|
|
19884
|
+
},
|
|
19885
|
+
handler: async (args) => this.handleToolCall("set_outro", args)
|
|
19886
|
+
},
|
|
19887
|
+
{
|
|
19888
|
+
name: "finalize_agenda",
|
|
19889
|
+
description: "Confirm that the agenda is complete. Call this after all sections, intro, and outro are set.",
|
|
19890
|
+
parameters: {
|
|
19891
|
+
type: "object",
|
|
19892
|
+
properties: {
|
|
19893
|
+
summary: {
|
|
19894
|
+
type: "string",
|
|
19895
|
+
description: "Brief summary of the agenda structure and reasoning."
|
|
19896
|
+
}
|
|
19897
|
+
},
|
|
19898
|
+
required: ["summary"],
|
|
19899
|
+
additionalProperties: false
|
|
19900
|
+
},
|
|
19901
|
+
handler: async (args) => this.handleToolCall("finalize_agenda", args)
|
|
19902
|
+
}
|
|
19903
|
+
];
|
|
19904
|
+
}
|
|
19905
|
+
async handleToolCall(toolName, args) {
|
|
19906
|
+
switch (toolName) {
|
|
19907
|
+
case "get_brand_context":
|
|
19908
|
+
return getBrandConfig();
|
|
19909
|
+
case "get_idea_details":
|
|
19910
|
+
return this.handleGetIdeaDetails(args);
|
|
19911
|
+
case "add_section":
|
|
19912
|
+
return this.handleAddSection(args);
|
|
19913
|
+
case "set_intro":
|
|
19914
|
+
return this.handleSetIntro(args);
|
|
19915
|
+
case "set_outro":
|
|
19916
|
+
return this.handleSetOutro(args);
|
|
19917
|
+
case "finalize_agenda":
|
|
19918
|
+
return this.handleFinalizeAgenda(args);
|
|
19919
|
+
default:
|
|
19920
|
+
return { error: `Unknown tool: ${toolName}` };
|
|
19921
|
+
}
|
|
19922
|
+
}
|
|
19923
|
+
handleGetIdeaDetails(args) {
|
|
19924
|
+
const ideaIndex = Number(args.ideaIndex ?? -1);
|
|
19925
|
+
if (ideaIndex < 0 || ideaIndex >= this.ideas.length) {
|
|
19926
|
+
return { error: `Invalid ideaIndex ${ideaIndex}. Must be 0\u2013${this.ideas.length - 1}.` };
|
|
19927
|
+
}
|
|
19928
|
+
return this.ideas[ideaIndex];
|
|
19929
|
+
}
|
|
19930
|
+
handleAddSection(args) {
|
|
19931
|
+
const section = {
|
|
19932
|
+
order: this.sections.length + 1,
|
|
19933
|
+
title: String(args.title ?? ""),
|
|
19934
|
+
ideaIssueNumber: Number(args.ideaIssueNumber ?? 0),
|
|
19935
|
+
estimatedMinutes: Number(args.estimatedMinutes ?? 2),
|
|
19936
|
+
talkingPoints: args.talkingPoints ?? [],
|
|
19937
|
+
transition: String(args.transition ?? ""),
|
|
19938
|
+
notes: String(args.notes ?? "")
|
|
19939
|
+
};
|
|
19940
|
+
this.sections.push(section);
|
|
19941
|
+
logger_default.info(`[AgendaAgent] Added section ${section.order}: ${section.title}`);
|
|
19942
|
+
return { added: true, order: section.order };
|
|
19943
|
+
}
|
|
19944
|
+
handleSetIntro(args) {
|
|
19945
|
+
this.intro = String(args.text ?? "");
|
|
19946
|
+
logger_default.info(`[AgendaAgent] Set intro (${this.intro.length} chars)`);
|
|
19947
|
+
return { set: true, field: "intro" };
|
|
19948
|
+
}
|
|
19949
|
+
handleSetOutro(args) {
|
|
19950
|
+
this.outro = String(args.text ?? "");
|
|
19951
|
+
logger_default.info(`[AgendaAgent] Set outro (${this.outro.length} chars)`);
|
|
19952
|
+
return { set: true, field: "outro" };
|
|
19953
|
+
}
|
|
19954
|
+
handleFinalizeAgenda(args) {
|
|
19955
|
+
this.finalized = true;
|
|
19956
|
+
const summary = String(args.summary ?? "");
|
|
19957
|
+
logger_default.info(`[AgendaAgent] Agenda finalized: ${summary}`);
|
|
19958
|
+
return { finalized: true, summary };
|
|
19959
|
+
}
|
|
19960
|
+
async generateAgenda(ideas) {
|
|
19961
|
+
this.ideas = ideas;
|
|
19962
|
+
this.resetForRetry();
|
|
19963
|
+
const startTime = Date.now();
|
|
19964
|
+
const ideaList = ideas.map(
|
|
19965
|
+
(idea, i) => `${i}. **#${idea.issueNumber} \u2014 ${idea.topic}**
|
|
19966
|
+
Hook: ${idea.hook}
|
|
19967
|
+
Talking points: ${idea.talkingPoints.join(", ")}`
|
|
19968
|
+
).join("\n");
|
|
19969
|
+
const userMessage = [
|
|
19970
|
+
`Create a recording agenda from these ${ideas.length} ideas:`,
|
|
19971
|
+
"",
|
|
19972
|
+
ideaList,
|
|
19973
|
+
"",
|
|
19974
|
+
"Structure them into a cohesive recording flow. Call get_brand_context first, then get_idea_details for each, then add_section for each in recording order, then set_intro, set_outro, and finalize_agenda."
|
|
19975
|
+
].join("\n");
|
|
19976
|
+
await this.run(userMessage);
|
|
19977
|
+
const estimatedDuration = this.sections.reduce((sum, s) => sum + s.estimatedMinutes, 0);
|
|
19978
|
+
const markdown = this.buildMarkdown(estimatedDuration);
|
|
19979
|
+
return {
|
|
19980
|
+
sections: this.sections,
|
|
19981
|
+
intro: this.intro,
|
|
19982
|
+
outro: this.outro,
|
|
19983
|
+
estimatedDuration,
|
|
19984
|
+
markdown,
|
|
19985
|
+
durationMs: Date.now() - startTime
|
|
19986
|
+
};
|
|
19987
|
+
}
|
|
19988
|
+
buildMarkdown(estimatedDuration) {
|
|
19989
|
+
const lines = [
|
|
19990
|
+
"# Recording Agenda",
|
|
19991
|
+
"",
|
|
19992
|
+
`**Estimated Duration:** ${estimatedDuration} minutes`,
|
|
19993
|
+
`**Ideas Covered:** ${this.sections.length}`,
|
|
19994
|
+
"",
|
|
19995
|
+
"## Opening",
|
|
19996
|
+
"",
|
|
19997
|
+
this.intro
|
|
19998
|
+
];
|
|
19999
|
+
for (const section of this.sections) {
|
|
20000
|
+
lines.push(
|
|
20001
|
+
"",
|
|
20002
|
+
"---",
|
|
20003
|
+
"",
|
|
20004
|
+
`## Section ${section.order}: ${section.title}`,
|
|
20005
|
+
`**Idea:** #${section.ideaIssueNumber} | **Time:** ~${section.estimatedMinutes} min`,
|
|
20006
|
+
"",
|
|
20007
|
+
"### Talking Points",
|
|
20008
|
+
...section.talkingPoints.map((p) => `- ${p}`),
|
|
20009
|
+
"",
|
|
20010
|
+
"### Notes",
|
|
20011
|
+
section.notes
|
|
20012
|
+
);
|
|
20013
|
+
if (section.transition) {
|
|
20014
|
+
lines.push(
|
|
20015
|
+
"",
|
|
20016
|
+
"### Transition",
|
|
20017
|
+
`> ${section.transition}`
|
|
20018
|
+
);
|
|
20019
|
+
}
|
|
20020
|
+
}
|
|
20021
|
+
lines.push(
|
|
20022
|
+
"",
|
|
20023
|
+
"---",
|
|
20024
|
+
"",
|
|
20025
|
+
"## Closing",
|
|
20026
|
+
"",
|
|
20027
|
+
this.outro,
|
|
20028
|
+
""
|
|
20029
|
+
);
|
|
20030
|
+
return lines.join("\n");
|
|
20031
|
+
}
|
|
20032
|
+
async destroy() {
|
|
20033
|
+
await super.destroy();
|
|
20034
|
+
}
|
|
20035
|
+
};
|
|
20036
|
+
|
|
19197
20037
|
// src/L5-assets/pipelineServices.ts
|
|
19198
20038
|
var costTracker3 = {
|
|
19199
20039
|
reset: (...args) => costTracker2.reset(...args),
|
|
@@ -19223,6 +20063,12 @@ function createInterviewAgent(...args) {
|
|
|
19223
20063
|
function createScheduleAgent(...args) {
|
|
19224
20064
|
return new ScheduleAgent(...args);
|
|
19225
20065
|
}
|
|
20066
|
+
function createAgendaAgent(...args) {
|
|
20067
|
+
return new AgendaAgent(...args);
|
|
20068
|
+
}
|
|
20069
|
+
function createIdeaDiscoveryAgent(...args) {
|
|
20070
|
+
return new IdeaDiscoveryAgent(...args);
|
|
20071
|
+
}
|
|
19226
20072
|
|
|
19227
20073
|
// src/L6-pipeline/pipeline.ts
|
|
19228
20074
|
init_types();
|
|
@@ -19279,7 +20125,7 @@ async function runStage(stageName, fn, stageResults) {
|
|
|
19279
20125
|
return void 0;
|
|
19280
20126
|
}
|
|
19281
20127
|
}
|
|
19282
|
-
async function processVideo(videoPath, ideas) {
|
|
20128
|
+
async function processVideo(videoPath, ideas, publishBy) {
|
|
19283
20129
|
const pipelineStart = Date.now();
|
|
19284
20130
|
const stageResults = [];
|
|
19285
20131
|
const cfg = getConfig();
|
|
@@ -19446,6 +20292,15 @@ async function processVideo(videoPath, ideas) {
|
|
|
19446
20292
|
} catch (err) {
|
|
19447
20293
|
logger_default.warn(`[Pipeline] Failed to generate main video thumbnail: ${err instanceof Error ? err.message : String(err)}`);
|
|
19448
20294
|
}
|
|
20295
|
+
const defaultPublishBy2 = publishBy ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
|
|
20296
|
+
const hasClips = shorts.length > 0 || mediumClips.length > 0;
|
|
20297
|
+
if (hasClips) {
|
|
20298
|
+
await trackStage("idea-discovery" /* IdeaDiscovery */, async () => {
|
|
20299
|
+
await asset.discoverIdeas(shorts, mediumClips, defaultPublishBy2);
|
|
20300
|
+
});
|
|
20301
|
+
} else {
|
|
20302
|
+
skipStage("idea-discovery" /* IdeaDiscovery */, "NO_CLIPS");
|
|
20303
|
+
}
|
|
19449
20304
|
let socialPosts = [];
|
|
19450
20305
|
if (!cfg.SKIP_SOCIAL) {
|
|
19451
20306
|
const mainPosts = await trackStage("social-media" /* SocialMedia */, () => asset.getSocialPosts()) ?? [];
|
|
@@ -19570,13 +20425,13 @@ function generateCostMarkdown(report) {
|
|
|
19570
20425
|
}
|
|
19571
20426
|
return md;
|
|
19572
20427
|
}
|
|
19573
|
-
async function processVideoSafe(videoPath, ideas) {
|
|
20428
|
+
async function processVideoSafe(videoPath, ideas, publishBy) {
|
|
19574
20429
|
const filename = basename(videoPath);
|
|
19575
20430
|
const slug = filename.replace(/\.(mp4|mov|webm|avi|mkv)$/i, "");
|
|
19576
20431
|
await markPending3(slug, videoPath);
|
|
19577
20432
|
await markProcessing3(slug);
|
|
19578
20433
|
try {
|
|
19579
|
-
const result = await processVideo(videoPath, ideas);
|
|
20434
|
+
const result = await processVideo(videoPath, ideas, publishBy);
|
|
19580
20435
|
await markCompleted3(slug);
|
|
19581
20436
|
return result;
|
|
19582
20437
|
} catch (err) {
|
|
@@ -19882,8 +20737,8 @@ function getFFprobePath3(...args) {
|
|
|
19882
20737
|
init_scheduleConfig();
|
|
19883
20738
|
var rl = createReadlineInterface({ input: process.stdin, output: process.stdout });
|
|
19884
20739
|
function ask(question) {
|
|
19885
|
-
return new Promise((
|
|
19886
|
-
rl.question(question, (answer) =>
|
|
20740
|
+
return new Promise((resolve4) => {
|
|
20741
|
+
rl.question(question, (answer) => resolve4(answer));
|
|
19887
20742
|
});
|
|
19888
20743
|
}
|
|
19889
20744
|
async function runInit() {
|
|
@@ -20264,18 +21119,18 @@ function ChatApp({ controller }) {
|
|
|
20264
21119
|
useInput((_input, key) => {
|
|
20265
21120
|
if (key.ctrl && _input === "c") {
|
|
20266
21121
|
controller.interrupted = true;
|
|
20267
|
-
const
|
|
21122
|
+
const resolve4 = controller._pendingResolve;
|
|
20268
21123
|
controller._pendingResolve = null;
|
|
20269
|
-
if (
|
|
21124
|
+
if (resolve4) resolve4("");
|
|
20270
21125
|
exit();
|
|
20271
21126
|
}
|
|
20272
21127
|
});
|
|
20273
21128
|
const handleSubmit = useCallback((value) => {
|
|
20274
21129
|
setInputValue("");
|
|
20275
21130
|
setInputActive(false);
|
|
20276
|
-
const
|
|
21131
|
+
const resolve4 = controller._pendingResolve;
|
|
20277
21132
|
controller._pendingResolve = null;
|
|
20278
|
-
if (
|
|
21133
|
+
if (resolve4) resolve4(value.trim());
|
|
20279
21134
|
}, [controller]);
|
|
20280
21135
|
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", height: "100%", children: [
|
|
20281
21136
|
/* @__PURE__ */ jsx(
|
|
@@ -20386,8 +21241,8 @@ var AltScreenChat = class {
|
|
|
20386
21241
|
}
|
|
20387
21242
|
/** Prompt for user input. Returns their trimmed text. */
|
|
20388
21243
|
promptInput(_prompt) {
|
|
20389
|
-
return new Promise((
|
|
20390
|
-
this._pendingResolve =
|
|
21244
|
+
return new Promise((resolve4) => {
|
|
21245
|
+
this._pendingResolve = resolve4;
|
|
20391
21246
|
this.bridge?.setInputActive(true);
|
|
20392
21247
|
this.bridge?.forceRender();
|
|
20393
21248
|
});
|
|
@@ -20414,18 +21269,18 @@ async function runChat() {
|
|
|
20414
21269
|
const choiceText = request.choices.map((c, i) => ` ${i + 1}. ${c}`).join("\n");
|
|
20415
21270
|
chat.addMessage("system", choiceText + (request.allowFreeform !== false ? "\n (or type a custom answer)" : ""));
|
|
20416
21271
|
}
|
|
20417
|
-
return new Promise((
|
|
21272
|
+
return new Promise((resolve4) => {
|
|
20418
21273
|
chat.promptInput("> ").then((answer) => {
|
|
20419
21274
|
const trimmed = answer.trim();
|
|
20420
21275
|
chat.addMessage("user", trimmed);
|
|
20421
21276
|
if (request.choices && request.choices.length > 0) {
|
|
20422
21277
|
const num = parseInt(trimmed, 10);
|
|
20423
21278
|
if (num >= 1 && num <= request.choices.length) {
|
|
20424
|
-
|
|
21279
|
+
resolve4({ answer: request.choices[num - 1], wasFreeform: false });
|
|
20425
21280
|
return;
|
|
20426
21281
|
}
|
|
20427
21282
|
}
|
|
20428
|
-
|
|
21283
|
+
resolve4({ answer: trimmed, wasFreeform: true });
|
|
20429
21284
|
});
|
|
20430
21285
|
});
|
|
20431
21286
|
};
|
|
@@ -20483,6 +21338,22 @@ async function startInterview(idea, answerProvider, onEvent) {
|
|
|
20483
21338
|
if (onEvent) interviewEmitter.removeListener(onEvent);
|
|
20484
21339
|
}
|
|
20485
21340
|
}
|
|
21341
|
+
async function generateAgenda(ideas) {
|
|
21342
|
+
const agent = createAgendaAgent(ideas);
|
|
21343
|
+
try {
|
|
21344
|
+
return await agent.generateAgenda(ideas);
|
|
21345
|
+
} finally {
|
|
21346
|
+
await agent.destroy();
|
|
21347
|
+
}
|
|
21348
|
+
}
|
|
21349
|
+
async function discoverIdeas(input) {
|
|
21350
|
+
const agent = createIdeaDiscoveryAgent(input);
|
|
21351
|
+
try {
|
|
21352
|
+
return await agent.discover();
|
|
21353
|
+
} finally {
|
|
21354
|
+
await agent.destroy();
|
|
21355
|
+
}
|
|
21356
|
+
}
|
|
20486
21357
|
|
|
20487
21358
|
// src/L7-app/commands/ideate.ts
|
|
20488
21359
|
init_types();
|
|
@@ -20838,8 +21709,160 @@ async function saveResults(result, chat, issueNumber) {
|
|
|
20838
21709
|
if (response.toLowerCase().startsWith("y")) {
|
|
20839
21710
|
await updateIdea(issueNumber, { status: "ready" });
|
|
20840
21711
|
chat.showInsight(`\u2705 Idea #${issueNumber} marked as ready`);
|
|
20841
|
-
await new Promise((
|
|
21712
|
+
await new Promise((resolve4) => setTimeout(resolve4, 1500));
|
|
21713
|
+
}
|
|
21714
|
+
}
|
|
21715
|
+
|
|
21716
|
+
// src/L7-app/commands/agenda.ts
|
|
21717
|
+
init_ideaService2();
|
|
21718
|
+
import { resolve as resolve3 } from "path";
|
|
21719
|
+
init_fileSystem();
|
|
21720
|
+
init_configLogger();
|
|
21721
|
+
async function runAgenda(issueNumbers, options) {
|
|
21722
|
+
if (issueNumbers.length === 0) {
|
|
21723
|
+
logger_default.error("At least one idea issue number is required.");
|
|
21724
|
+
process.exit(1);
|
|
21725
|
+
}
|
|
21726
|
+
const ids = issueNumbers.flatMap((n) => n.split(",")).map((s) => s.trim()).filter(Boolean);
|
|
21727
|
+
if (ids.length === 0) {
|
|
21728
|
+
logger_default.error("No valid idea issue numbers provided.");
|
|
21729
|
+
process.exit(1);
|
|
21730
|
+
}
|
|
21731
|
+
let ideas;
|
|
21732
|
+
try {
|
|
21733
|
+
ideas = await getIdeasByIds(ids);
|
|
21734
|
+
} catch (err) {
|
|
21735
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
21736
|
+
logger_default.error(`Failed to resolve ideas: ${msg}`);
|
|
21737
|
+
process.exit(1);
|
|
21738
|
+
}
|
|
21739
|
+
if (ideas.length === 0) {
|
|
21740
|
+
logger_default.error("No ideas found for the provided issue numbers.");
|
|
21741
|
+
process.exit(1);
|
|
21742
|
+
}
|
|
21743
|
+
logger_default.info(`Generating agenda for ${ideas.length} idea(s): ${ideas.map((i) => `#${i.issueNumber} "${i.topic}"`).join(", ")}`);
|
|
21744
|
+
const result = await generateAgenda(ideas);
|
|
21745
|
+
const outputPath = options.output ? resolve3(options.output) : resolve3(`agenda-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.md`);
|
|
21746
|
+
await writeTextFile(outputPath, result.markdown);
|
|
21747
|
+
logger_default.info(`Agenda saved to ${outputPath}`);
|
|
21748
|
+
console.log(`
|
|
21749
|
+
\u2705 Agenda generated (${result.estimatedDuration} min, ${result.sections.length} sections)`);
|
|
21750
|
+
console.log(` Saved to: ${outputPath}
|
|
21751
|
+
`);
|
|
21752
|
+
for (const section of result.sections) {
|
|
21753
|
+
console.log(` ${section.order}. ${section.title} (~${section.estimatedMinutes} min) \u2014 idea #${section.ideaIssueNumber}`);
|
|
21754
|
+
}
|
|
21755
|
+
console.log();
|
|
21756
|
+
}
|
|
21757
|
+
|
|
21758
|
+
// src/L7-app/commands/discoverIdeas.ts
|
|
21759
|
+
init_postStore();
|
|
21760
|
+
init_fileSystem();
|
|
21761
|
+
init_brand();
|
|
21762
|
+
init_paths();
|
|
21763
|
+
init_types();
|
|
21764
|
+
init_configLogger();
|
|
21765
|
+
async function runDiscoverIdeas(options) {
|
|
21766
|
+
const pendingItems = await getPendingItems();
|
|
21767
|
+
if (pendingItems.length === 0) {
|
|
21768
|
+
console.log("No pending items in the publish queue.");
|
|
21769
|
+
return;
|
|
21770
|
+
}
|
|
21771
|
+
const untagged = pendingItems.filter((item) => !item.metadata.ideaIds?.length);
|
|
21772
|
+
if (untagged.length === 0) {
|
|
21773
|
+
console.log(`All ${pendingItems.length} pending items already have ideas assigned.`);
|
|
21774
|
+
return;
|
|
21775
|
+
}
|
|
21776
|
+
console.log(`Found ${untagged.length} untagged items (of ${pendingItems.length} total pending).
|
|
21777
|
+
`);
|
|
21778
|
+
const byVideo = /* @__PURE__ */ new Map();
|
|
21779
|
+
for (const item of untagged) {
|
|
21780
|
+
const key = item.metadata.sourceVideo;
|
|
21781
|
+
const group = byVideo.get(key) ?? [];
|
|
21782
|
+
group.push(item);
|
|
21783
|
+
byVideo.set(key, group);
|
|
21784
|
+
}
|
|
21785
|
+
const defaultPublishBy2 = options.publishBy ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
|
|
21786
|
+
let totalUpdated = 0;
|
|
21787
|
+
let totalFailed = 0;
|
|
21788
|
+
for (const [videoDir, items] of byVideo) {
|
|
21789
|
+
console.log(`
|
|
21790
|
+
\u{1F4F9} ${videoDir} (${items.length} untagged items)`);
|
|
21791
|
+
const transcriptPath = join(videoDir, "transcript.json");
|
|
21792
|
+
const shortsPlanPath = join(videoDir, "shorts-plan.json");
|
|
21793
|
+
const mediumPlanPath = join(videoDir, "medium-clips-plan.json");
|
|
21794
|
+
const summaryPath = join(videoDir, "summary.json");
|
|
21795
|
+
if (!await fileExists(transcriptPath)) {
|
|
21796
|
+
logger_default.warn(`No transcript.json in ${videoDir} \u2014 skipping`);
|
|
21797
|
+
totalFailed += items.length;
|
|
21798
|
+
continue;
|
|
21799
|
+
}
|
|
21800
|
+
const transcript = await readJsonFile(transcriptPath);
|
|
21801
|
+
const shorts = await fileExists(shortsPlanPath) ? await readJsonFile(shortsPlanPath) : [];
|
|
21802
|
+
const mediumClips = await fileExists(mediumPlanPath) ? await readJsonFile(mediumPlanPath) : [];
|
|
21803
|
+
let summaryText = "";
|
|
21804
|
+
if (await fileExists(summaryPath)) {
|
|
21805
|
+
const summary = await readJsonFile(summaryPath);
|
|
21806
|
+
summaryText = summary.overview ?? "";
|
|
21807
|
+
}
|
|
21808
|
+
if (shorts.length === 0 && mediumClips.length === 0) {
|
|
21809
|
+
logger_default.warn(`No shorts or medium clips found in ${videoDir} \u2014 skipping`);
|
|
21810
|
+
totalFailed += items.length;
|
|
21811
|
+
continue;
|
|
21812
|
+
}
|
|
21813
|
+
const brand = getBrandConfig();
|
|
21814
|
+
const defaultPlatforms = ["youtube" /* YouTube */, "tiktok" /* TikTok */, "instagram" /* Instagram */, "linkedin" /* LinkedIn */, "x" /* X */];
|
|
21815
|
+
console.log(` Running idea discovery on ${shorts.length} shorts + ${mediumClips.length} medium clips...`);
|
|
21816
|
+
try {
|
|
21817
|
+
const result = await discoverIdeas({
|
|
21818
|
+
shorts,
|
|
21819
|
+
mediumClips,
|
|
21820
|
+
transcript: transcript.segments,
|
|
21821
|
+
summary: summaryText,
|
|
21822
|
+
publishBy: defaultPublishBy2,
|
|
21823
|
+
defaultPlatforms
|
|
21824
|
+
});
|
|
21825
|
+
console.log(` \u2705 ${result.matchedCount} matched, ${result.createdCount} created
|
|
21826
|
+
`);
|
|
21827
|
+
const clipIdeaMap = /* @__PURE__ */ new Map();
|
|
21828
|
+
for (const assignment of result.assignments) {
|
|
21829
|
+
clipIdeaMap.set(assignment.clipId, assignment.ideaIssueNumber);
|
|
21830
|
+
}
|
|
21831
|
+
const allIdeaIds = [...new Set(result.assignments.map((a) => String(a.ideaIssueNumber)))];
|
|
21832
|
+
for (const item of items) {
|
|
21833
|
+
if (options.dryRun) {
|
|
21834
|
+
console.log(` [dry-run] Would update ${item.metadata.id}`);
|
|
21835
|
+
continue;
|
|
21836
|
+
}
|
|
21837
|
+
let ideaIds;
|
|
21838
|
+
if (item.metadata.sourceClip) {
|
|
21839
|
+
const matchedShort = shorts.find((s) => s.slug === item.metadata.sourceClip);
|
|
21840
|
+
const matchedMedium = mediumClips.find((m) => m.slug === item.metadata.sourceClip);
|
|
21841
|
+
const clipId = matchedShort?.id ?? matchedMedium?.id;
|
|
21842
|
+
if (clipId && clipIdeaMap.has(clipId)) {
|
|
21843
|
+
ideaIds = [String(clipIdeaMap.get(clipId))];
|
|
21844
|
+
}
|
|
21845
|
+
}
|
|
21846
|
+
if (!ideaIds && allIdeaIds.length > 0) {
|
|
21847
|
+
ideaIds = allIdeaIds;
|
|
21848
|
+
}
|
|
21849
|
+
if (ideaIds) {
|
|
21850
|
+
await updateItem(item.metadata.id, { metadata: { ideaIds } });
|
|
21851
|
+
console.log(` \u{1F4CC} ${item.metadata.id} \u2192 idea(s) ${ideaIds.join(", ")}`);
|
|
21852
|
+
totalUpdated++;
|
|
21853
|
+
} else {
|
|
21854
|
+
console.log(` \u26A0\uFE0F ${item.metadata.id} \u2014 no idea match found`);
|
|
21855
|
+
totalFailed++;
|
|
21856
|
+
}
|
|
21857
|
+
}
|
|
21858
|
+
} catch (err) {
|
|
21859
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
21860
|
+
logger_default.error(`Idea discovery failed for ${videoDir}: ${msg}`);
|
|
21861
|
+
totalFailed += items.length;
|
|
21862
|
+
}
|
|
20842
21863
|
}
|
|
21864
|
+
console.log(`
|
|
21865
|
+
\u{1F3C1} Done: ${totalUpdated} items updated, ${totalFailed} skipped/failed`);
|
|
20843
21866
|
}
|
|
20844
21867
|
|
|
20845
21868
|
// src/L1-infra/readline/readlinePromises.ts
|
|
@@ -21985,8 +23008,8 @@ init_configLogger();
|
|
|
21985
23008
|
var queue = [];
|
|
21986
23009
|
var processing = false;
|
|
21987
23010
|
function enqueueApproval(itemIds) {
|
|
21988
|
-
return new Promise((
|
|
21989
|
-
queue.push({ itemIds, resolve:
|
|
23011
|
+
return new Promise((resolve4) => {
|
|
23012
|
+
queue.push({ itemIds, resolve: resolve4 });
|
|
21990
23013
|
if (!processing) drain();
|
|
21991
23014
|
});
|
|
21992
23015
|
}
|
|
@@ -22040,23 +23063,26 @@ async function processApprovalBatch(itemIds) {
|
|
|
22040
23063
|
}
|
|
22041
23064
|
}
|
|
22042
23065
|
const enriched = loadedItems.map(({ id, item }) => {
|
|
23066
|
+
const createdAt = item?.metadata.createdAt ?? null;
|
|
22043
23067
|
if (!item?.metadata.ideaIds?.length) {
|
|
22044
|
-
return { id, publishBy: null, hasIdeas: false };
|
|
23068
|
+
return { id, publishBy: null, hasIdeas: false, createdAt };
|
|
22045
23069
|
}
|
|
22046
23070
|
const dates = item.metadata.ideaIds.map((ideaId) => ideaMap.get(ideaId)?.publishBy).filter((publishBy) => Boolean(publishBy)).sort();
|
|
22047
|
-
return { id, publishBy: dates[0] ?? null, hasIdeas: true };
|
|
23071
|
+
return { id, publishBy: dates[0] ?? null, hasIdeas: true, createdAt };
|
|
22048
23072
|
});
|
|
22049
|
-
const now = Date.now();
|
|
22050
|
-
const sevenDays = 7 * 24 * 60 * 60 * 1e3;
|
|
22051
23073
|
enriched.sort((a, b) => {
|
|
22052
|
-
const aPublishByTime = a.publishBy ? new Date(a.publishBy).getTime() : Number.NaN;
|
|
22053
|
-
const bPublishByTime = b.publishBy ? new Date(b.publishBy).getTime() : Number.NaN;
|
|
22054
|
-
const aUrgent = a.hasIdeas && Number.isFinite(aPublishByTime) && aPublishByTime - now < sevenDays;
|
|
22055
|
-
const bUrgent = b.hasIdeas && Number.isFinite(bPublishByTime) && bPublishByTime - now < sevenDays;
|
|
22056
|
-
if (aUrgent && !bUrgent) return -1;
|
|
22057
|
-
if (!aUrgent && bUrgent) return 1;
|
|
22058
23074
|
if (a.hasIdeas && !b.hasIdeas) return -1;
|
|
22059
23075
|
if (!a.hasIdeas && b.hasIdeas) return 1;
|
|
23076
|
+
if (a.hasIdeas && b.hasIdeas) {
|
|
23077
|
+
const aTime = a.publishBy ? new Date(a.publishBy).getTime() : Infinity;
|
|
23078
|
+
const bTime = b.publishBy ? new Date(b.publishBy).getTime() : Infinity;
|
|
23079
|
+
if (aTime !== bTime) return aTime - bTime;
|
|
23080
|
+
if (a.createdAt && b.createdAt) {
|
|
23081
|
+
const aCreated = new Date(a.createdAt).getTime();
|
|
23082
|
+
const bCreated = new Date(b.createdAt).getTime();
|
|
23083
|
+
if (aCreated !== bCreated) return aCreated - bCreated;
|
|
23084
|
+
}
|
|
23085
|
+
}
|
|
22060
23086
|
return 0;
|
|
22061
23087
|
});
|
|
22062
23088
|
const sortedIds = enriched.map((entry) => entry.id);
|
|
@@ -22404,7 +23430,7 @@ async function startReviewServer(options = {}) {
|
|
|
22404
23430
|
res.sendFile(join(publicDir, "index.html"));
|
|
22405
23431
|
}
|
|
22406
23432
|
});
|
|
22407
|
-
return new Promise((
|
|
23433
|
+
return new Promise((resolve4, reject) => {
|
|
22408
23434
|
const tryPort = (p, attempts) => {
|
|
22409
23435
|
const server = app.listen(p, "127.0.0.1", () => {
|
|
22410
23436
|
logger_default.info(`Review server running at http://localhost:${p}`);
|
|
@@ -22413,7 +23439,7 @@ async function startReviewServer(options = {}) {
|
|
|
22413
23439
|
connections.add(conn);
|
|
22414
23440
|
conn.on("close", () => connections.delete(conn));
|
|
22415
23441
|
});
|
|
22416
|
-
|
|
23442
|
+
resolve4({
|
|
22417
23443
|
port: p,
|
|
22418
23444
|
close: () => new Promise((res) => {
|
|
22419
23445
|
let done = false;
|
|
@@ -22448,6 +23474,28 @@ async function startReviewServer(options = {}) {
|
|
|
22448
23474
|
});
|
|
22449
23475
|
}
|
|
22450
23476
|
|
|
23477
|
+
// src/L7-app/parsePublishBy.ts
|
|
23478
|
+
function parsePublishBy(raw) {
|
|
23479
|
+
const trimmed = raw.trim();
|
|
23480
|
+
const relativeMatch = trimmed.match(/^\+(\d+)d$/i);
|
|
23481
|
+
if (relativeMatch) {
|
|
23482
|
+
return new Date(Date.now() + parseInt(relativeMatch[1], 10) * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
|
|
23483
|
+
}
|
|
23484
|
+
const isoDatePattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
23485
|
+
if (!isoDatePattern.test(trimmed)) {
|
|
23486
|
+
throw new Error(
|
|
23487
|
+
`Invalid --publish-by value "${trimmed}". Expected "+Nd" (e.g., +7d) or ISO date "YYYY-MM-DD".`
|
|
23488
|
+
);
|
|
23489
|
+
}
|
|
23490
|
+
const parsed = new Date(trimmed);
|
|
23491
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
23492
|
+
throw new Error(
|
|
23493
|
+
`Invalid --publish-by date "${trimmed}". Provide a valid calendar date in "YYYY-MM-DD" format or use "+Nd".`
|
|
23494
|
+
);
|
|
23495
|
+
}
|
|
23496
|
+
return parsed.toISOString().split("T")[0];
|
|
23497
|
+
}
|
|
23498
|
+
|
|
22451
23499
|
// src/L7-app/cli.ts
|
|
22452
23500
|
init_fileSystem();
|
|
22453
23501
|
init_paths();
|
|
@@ -22517,6 +23565,24 @@ program.command("ideate-start <issue-number>").description("Start an interactive
|
|
|
22517
23565
|
await runIdeateStart(issueNumber, opts);
|
|
22518
23566
|
process.exit(0);
|
|
22519
23567
|
});
|
|
23568
|
+
program.command("agenda <issue-numbers...>").description("Generate a structured recording agenda from multiple ideas").option("--output <path>", "Output file path for the agenda markdown").action(async (issueNumbers, opts) => {
|
|
23569
|
+
initConfig({});
|
|
23570
|
+
await runAgenda(issueNumbers, opts);
|
|
23571
|
+
process.exit(0);
|
|
23572
|
+
});
|
|
23573
|
+
program.command("discover-ideas").description("Retroactively run idea discovery on pending publish queue items that have no ideas assigned").option("--publish-by <date>", "Publish-by deadline for new ideas (ISO date or +Nd, default: +7d)").option("--dry-run", "Preview what would be updated without making changes").action(async (opts) => {
|
|
23574
|
+
initConfig({});
|
|
23575
|
+
if (opts.publishBy) {
|
|
23576
|
+
try {
|
|
23577
|
+
opts.publishBy = parsePublishBy(String(opts.publishBy));
|
|
23578
|
+
} catch (err) {
|
|
23579
|
+
logger_default.error(err.message);
|
|
23580
|
+
process.exit(1);
|
|
23581
|
+
}
|
|
23582
|
+
}
|
|
23583
|
+
await runDiscoverIdeas(opts);
|
|
23584
|
+
process.exit(0);
|
|
23585
|
+
});
|
|
22520
23586
|
program.command("ideate").description("Generate AI-powered content ideas using trend research").option("--topics <topics>", "Comma-separated seed topics").option("--count <n>", "Number of ideas to generate (default: 5)", "5").option("--output <dir>", "Ideas directory (default: ./ideas)").option("--brand <path>", "Brand config path (default: ./brand.json)").option("--list", "List existing ideas instead of generating").option("--status <status>", "Filter by status when listing (draft|ready|recorded|published)").option("--format <format>", "Output format: table (default) or json").option("--add", "Add a single idea (AI-researched by default, or --no-ai for direct)").option("--topic <topic>", "Idea topic/title (required with --add)").option("--hook <hook>", "Attention-grabbing hook (default: topic, --no-ai only)").option("--audience <audience>", "Target audience (default: developers, --no-ai only)").option("--platforms <platforms>", "Comma-separated platforms: tiktok,youtube,instagram,linkedin,x (--no-ai only)").option("--key-takeaway <takeaway>", "Core message the viewer should remember (--no-ai only)").option("--talking-points <points>", "Comma-separated talking points (--no-ai only)").option("--tags <tags>", "Comma-separated categorization tags (--no-ai only)").option("--publish-by <date>", "Publish deadline (ISO 8601 date, default: 14 days from now, --no-ai only)").option("--trend-context <context>", "Why this topic is timely (--no-ai only)").option("--no-ai", "Skip AI research agent \u2014 create directly from CLI flags + defaults").option("-p, --prompt <prompt>", 'Free-form prompt to guide idea generation (e.g., "Cover this article: https://...")').action(async (opts) => {
|
|
22521
23587
|
initConfig();
|
|
22522
23588
|
await runIdeate(opts);
|
|
@@ -22561,7 +23627,7 @@ program.command("thumbnail").description("Generate a thumbnail for a recording f
|
|
|
22561
23627
|
});
|
|
22562
23628
|
process.exit(0);
|
|
22563
23629
|
});
|
|
22564
|
-
var defaultCmd = program.command("process", { isDefault: true }).argument("[video-path]", "Path to a video file to process (implies --once)").option("--watch-dir <path>", "Folder to watch for new recordings (default: env WATCH_FOLDER)").option("--output-dir <path>", "Output directory for processed videos (default: ./recordings)").option("--openai-key <key>", "OpenAI API key (default: env OPENAI_API_KEY)").option("--exa-key <key>", "Exa AI API key for web search (default: env EXA_API_KEY)").option("--youtube-key <key>", "YouTube API key (default: env YOUTUBE_API_KEY)").option("--perplexity-key <key>", "Perplexity API key (default: env PERPLEXITY_API_KEY)").option("--once", "Process a single video and exit (no watching)").option("--brand <path>", "Path to brand.json config (default: ./brand.json)").option("--no-silence-removal", "Skip silence removal stage").option("--no-shorts", "Skip shorts generation").option("--no-medium-clips", "Skip medium clip generation").option("--no-social", "Skip social media post generation").option("--no-captions", "Skip caption generation/burning").option("--no-visual-enhancement", "Skip visual enhancement (AI image overlays)").option("--no-intro-outro", "Skip intro/outro concatenation").option("--no-social-publish", "Skip social media publishing/queue-build stage").option("--late-api-key <key>", "Late API key (default: env LATE_API_KEY)").option("--late-profile-id <id>", "Late profile ID (default: env LATE_PROFILE_ID)").option("--ideas <ids>", "Comma-separated idea IDs to link to this video").option("-v, --verbose", "Verbose logging").option("--progress", "Emit structured JSON progress events to stderr").option("--doctor", "Check all prerequisites and exit").action(async (videoPath) => {
|
|
23630
|
+
var defaultCmd = program.command("process", { isDefault: true }).argument("[video-path]", "Path to a video file to process (implies --once)").option("--watch-dir <path>", "Folder to watch for new recordings (default: env WATCH_FOLDER)").option("--output-dir <path>", "Output directory for processed videos (default: ./recordings)").option("--openai-key <key>", "OpenAI API key (default: env OPENAI_API_KEY)").option("--exa-key <key>", "Exa AI API key for web search (default: env EXA_API_KEY)").option("--youtube-key <key>", "YouTube API key (default: env YOUTUBE_API_KEY)").option("--perplexity-key <key>", "Perplexity API key (default: env PERPLEXITY_API_KEY)").option("--once", "Process a single video and exit (no watching)").option("--brand <path>", "Path to brand.json config (default: ./brand.json)").option("--no-silence-removal", "Skip silence removal stage").option("--no-shorts", "Skip shorts generation").option("--no-medium-clips", "Skip medium clip generation").option("--no-social", "Skip social media post generation").option("--no-captions", "Skip caption generation/burning").option("--no-visual-enhancement", "Skip visual enhancement (AI image overlays)").option("--no-intro-outro", "Skip intro/outro concatenation").option("--no-social-publish", "Skip social media publishing/queue-build stage").option("--late-api-key <key>", "Late API key (default: env LATE_API_KEY)").option("--late-profile-id <id>", "Late profile ID (default: env LATE_PROFILE_ID)").option("--ideas <ids>", "Comma-separated idea IDs to link to this video").option("--publish-by <date>", "Publish-by deadline for auto-created ideas (ISO date or +Nd for relative, default: +7d)").option("-v, --verbose", "Verbose logging").option("--progress", "Emit structured JSON progress events to stderr").option("--doctor", "Check all prerequisites and exit").action(async (videoPath) => {
|
|
22565
23631
|
const opts = defaultCmd.opts();
|
|
22566
23632
|
if (opts.doctor) {
|
|
22567
23633
|
await runDoctor();
|
|
@@ -22612,7 +23678,21 @@ var defaultCmd = program.command("process", { isDefault: true }).argument("[vide
|
|
|
22612
23678
|
if (videoPath) {
|
|
22613
23679
|
const resolvedPath = resolve(videoPath);
|
|
22614
23680
|
logger_default.info(`Processing single video: ${resolvedPath}`);
|
|
22615
|
-
|
|
23681
|
+
let publishBy;
|
|
23682
|
+
if (opts.publishBy) {
|
|
23683
|
+
const raw = String(opts.publishBy).trim();
|
|
23684
|
+
const relativeMatch = raw.match(/^\+(\d+)d$/i);
|
|
23685
|
+
if (relativeMatch) {
|
|
23686
|
+
const days = parseInt(relativeMatch[1], 10);
|
|
23687
|
+
publishBy = new Date(Date.now() + days * 24 * 60 * 60 * 1e3).toISOString().split("T")[0];
|
|
23688
|
+
} else if (!Number.isNaN(new Date(raw).getTime())) {
|
|
23689
|
+
publishBy = raw;
|
|
23690
|
+
} else {
|
|
23691
|
+
logger_default.error(`Invalid --publish-by format: "${raw}". Use ISO date (2026-04-01) or relative (+7d).`);
|
|
23692
|
+
process.exit(1);
|
|
23693
|
+
}
|
|
23694
|
+
}
|
|
23695
|
+
await processVideoSafe(resolvedPath, ideas, publishBy);
|
|
22616
23696
|
if (ideas && ideas.length > 0) {
|
|
22617
23697
|
try {
|
|
22618
23698
|
const { markRecorded: markRecorded3 } = await Promise.resolve().then(() => (init_ideaService(), ideaService_exports));
|