vidpipe 1.3.13 → 1.3.15
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 +9360 -8250
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1062 -685
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -85,6 +85,7 @@ var init_types = __esm({
|
|
|
85
85
|
PipelineStage2["Chapters"] = "chapters";
|
|
86
86
|
PipelineStage2["Captions"] = "captions";
|
|
87
87
|
PipelineStage2["CaptionBurn"] = "caption-burn";
|
|
88
|
+
PipelineStage2["IntroOutro"] = "intro-outro";
|
|
88
89
|
PipelineStage2["Summary"] = "summary";
|
|
89
90
|
PipelineStage2["Shorts"] = "shorts";
|
|
90
91
|
PipelineStage2["MediumClips"] = "medium-clips";
|
|
@@ -102,15 +103,16 @@ var init_types = __esm({
|
|
|
102
103
|
{ stage: "visual-enhancement" /* VisualEnhancement */, name: "Visual Enhancement", stageNumber: 4 },
|
|
103
104
|
{ stage: "captions" /* Captions */, name: "Captions", stageNumber: 5 },
|
|
104
105
|
{ stage: "caption-burn" /* CaptionBurn */, name: "Caption Burn", stageNumber: 6 },
|
|
105
|
-
{ stage: "
|
|
106
|
-
{ stage: "
|
|
107
|
-
{ stage: "
|
|
108
|
-
{ stage: "
|
|
109
|
-
{ stage: "
|
|
110
|
-
{ stage: "
|
|
111
|
-
{ stage: "
|
|
112
|
-
{ stage: "
|
|
113
|
-
{ stage: "
|
|
106
|
+
{ stage: "intro-outro" /* IntroOutro */, name: "Intro/Outro", stageNumber: 7 },
|
|
107
|
+
{ stage: "shorts" /* Shorts */, name: "Shorts", stageNumber: 8 },
|
|
108
|
+
{ stage: "medium-clips" /* MediumClips */, name: "Medium Clips", stageNumber: 9 },
|
|
109
|
+
{ stage: "chapters" /* Chapters */, name: "Chapters", stageNumber: 10 },
|
|
110
|
+
{ stage: "summary" /* Summary */, name: "Summary", stageNumber: 11 },
|
|
111
|
+
{ stage: "social-media" /* SocialMedia */, name: "Social Media", stageNumber: 12 },
|
|
112
|
+
{ stage: "short-posts" /* ShortPosts */, name: "Short Posts", stageNumber: 13 },
|
|
113
|
+
{ stage: "medium-clip-posts" /* MediumClipPosts */, name: "Medium Clip Posts", stageNumber: 14 },
|
|
114
|
+
{ stage: "queue-build" /* QueueBuild */, name: "Queue Build", stageNumber: 15 },
|
|
115
|
+
{ stage: "blog" /* Blog */, name: "Blog", stageNumber: 16 }
|
|
114
116
|
];
|
|
115
117
|
TOTAL_STAGES = PIPELINE_STAGES.length;
|
|
116
118
|
PLATFORM_CHAR_LIMITS = {
|
|
@@ -446,6 +448,12 @@ function saveGlobalConfig(config2) {
|
|
|
446
448
|
chmodSync(configPath, 384);
|
|
447
449
|
}
|
|
448
450
|
}
|
|
451
|
+
function getGlobalConfigValue(section, key) {
|
|
452
|
+
const config2 = loadGlobalConfig();
|
|
453
|
+
const sectionValues = config2[section];
|
|
454
|
+
const value = sectionValues[key];
|
|
455
|
+
return typeof value === "string" ? value : void 0;
|
|
456
|
+
}
|
|
449
457
|
function setGlobalConfigValue(section, key, value) {
|
|
450
458
|
const config2 = loadGlobalConfig();
|
|
451
459
|
const sectionValues = config2[section];
|
|
@@ -592,6 +600,11 @@ function resolveConfig(cliOptions = {}) {
|
|
|
592
600
|
process.env.SKIP_VISUAL_ENHANCEMENT,
|
|
593
601
|
false
|
|
594
602
|
),
|
|
603
|
+
SKIP_INTRO_OUTRO: resolveBoolean(
|
|
604
|
+
cliOptions.introOutro === void 0 ? void 0 : !cliOptions.introOutro,
|
|
605
|
+
process.env.SKIP_INTRO_OUTRO,
|
|
606
|
+
false
|
|
607
|
+
),
|
|
595
608
|
LATE_API_KEY: resolveString(
|
|
596
609
|
cliOptions.lateApiKey,
|
|
597
610
|
process.env.LATE_API_KEY,
|
|
@@ -863,7 +876,7 @@ var init_githubClient = __esm({
|
|
|
863
876
|
const response = await this.octokit.rest.issues.listForRepo({
|
|
864
877
|
owner: this.owner,
|
|
865
878
|
repo: this.repo,
|
|
866
|
-
state: "
|
|
879
|
+
state: options.state ?? "all",
|
|
867
880
|
labels: options.labels && options.labels.length > 0 ? normalizeLabels(options.labels).join(",") : void 0,
|
|
868
881
|
sort: void 0,
|
|
869
882
|
direction: void 0,
|
|
@@ -4661,6 +4674,150 @@ var require_node = __commonJS({
|
|
|
4661
4674
|
}
|
|
4662
4675
|
});
|
|
4663
4676
|
|
|
4677
|
+
// src/L3-services/postStore/postStore.ts
|
|
4678
|
+
function getQueueDir() {
|
|
4679
|
+
const { OUTPUT_DIR } = getConfig();
|
|
4680
|
+
return join(OUTPUT_DIR, "publish-queue");
|
|
4681
|
+
}
|
|
4682
|
+
function getPublishedDir() {
|
|
4683
|
+
const { OUTPUT_DIR } = getConfig();
|
|
4684
|
+
return join(OUTPUT_DIR, "published");
|
|
4685
|
+
}
|
|
4686
|
+
async function readQueueItem(folderPath, id) {
|
|
4687
|
+
const metadataPath = join(folderPath, "metadata.json");
|
|
4688
|
+
const postPath = join(folderPath, "post.md");
|
|
4689
|
+
try {
|
|
4690
|
+
const metadataRaw = await readTextFile(metadataPath);
|
|
4691
|
+
const metadata = JSON.parse(metadataRaw);
|
|
4692
|
+
let postContent = "";
|
|
4693
|
+
try {
|
|
4694
|
+
postContent = await readTextFile(postPath);
|
|
4695
|
+
} catch {
|
|
4696
|
+
logger_default.debug(`No post.md found for ${String(id).replace(/[\r\n]/g, "")}`);
|
|
4697
|
+
}
|
|
4698
|
+
const videoPath = join(folderPath, "media.mp4");
|
|
4699
|
+
const imagePath = join(folderPath, "media.png");
|
|
4700
|
+
let mediaPath = null;
|
|
4701
|
+
let hasMedia = false;
|
|
4702
|
+
if (await fileExists(videoPath)) {
|
|
4703
|
+
mediaPath = videoPath;
|
|
4704
|
+
hasMedia = true;
|
|
4705
|
+
} else if (await fileExists(imagePath)) {
|
|
4706
|
+
mediaPath = imagePath;
|
|
4707
|
+
hasMedia = true;
|
|
4708
|
+
}
|
|
4709
|
+
return {
|
|
4710
|
+
id,
|
|
4711
|
+
metadata,
|
|
4712
|
+
postContent,
|
|
4713
|
+
hasMedia,
|
|
4714
|
+
mediaPath,
|
|
4715
|
+
folderPath
|
|
4716
|
+
};
|
|
4717
|
+
} catch (err) {
|
|
4718
|
+
logger_default.debug(`Failed to read queue item ${String(id).replace(/[\r\n]/g, "")}: ${String(err).replace(/[\r\n]/g, "")}`);
|
|
4719
|
+
return null;
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
async function getPendingItems() {
|
|
4723
|
+
const queueDir = getQueueDir();
|
|
4724
|
+
await ensureDirectory(queueDir);
|
|
4725
|
+
let entries;
|
|
4726
|
+
try {
|
|
4727
|
+
const dirents = await listDirectoryWithTypes(queueDir);
|
|
4728
|
+
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4729
|
+
} catch {
|
|
4730
|
+
return [];
|
|
4731
|
+
}
|
|
4732
|
+
const items = [];
|
|
4733
|
+
for (const name of entries) {
|
|
4734
|
+
const item = await readQueueItem(join(queueDir, name), name);
|
|
4735
|
+
if (item) items.push(item);
|
|
4736
|
+
}
|
|
4737
|
+
items.sort((a, b) => {
|
|
4738
|
+
if (a.hasMedia !== b.hasMedia) return a.hasMedia ? -1 : 1;
|
|
4739
|
+
return a.metadata.createdAt.localeCompare(b.metadata.createdAt);
|
|
4740
|
+
});
|
|
4741
|
+
return items;
|
|
4742
|
+
}
|
|
4743
|
+
async function createItem(id, metadata, postContent, mediaSourcePath) {
|
|
4744
|
+
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
4745
|
+
throw new Error(`Invalid ID format: ${id}`);
|
|
4746
|
+
}
|
|
4747
|
+
const folderPath = join(getQueueDir(), basename(id));
|
|
4748
|
+
await ensureDirectory(folderPath);
|
|
4749
|
+
await writeJsonFile(join(folderPath, "metadata.json"), metadata);
|
|
4750
|
+
await writeTextFile(join(folderPath, "post.md"), postContent);
|
|
4751
|
+
let hasMedia = false;
|
|
4752
|
+
const ext = mediaSourcePath ? extname(mediaSourcePath) : ".mp4";
|
|
4753
|
+
const mediaFilename = `media${ext}`;
|
|
4754
|
+
const mediaPath = join(folderPath, mediaFilename);
|
|
4755
|
+
if (mediaSourcePath) {
|
|
4756
|
+
await copyFile(mediaSourcePath, mediaPath);
|
|
4757
|
+
hasMedia = true;
|
|
4758
|
+
}
|
|
4759
|
+
logger_default.debug(`Created queue item: ${String(id).replace(/[\r\n]/g, "")}`);
|
|
4760
|
+
return {
|
|
4761
|
+
id,
|
|
4762
|
+
metadata,
|
|
4763
|
+
postContent,
|
|
4764
|
+
hasMedia,
|
|
4765
|
+
mediaPath: hasMedia ? mediaPath : null,
|
|
4766
|
+
folderPath
|
|
4767
|
+
};
|
|
4768
|
+
}
|
|
4769
|
+
async function getPublishedItems() {
|
|
4770
|
+
const publishedDir = getPublishedDir();
|
|
4771
|
+
await ensureDirectory(publishedDir);
|
|
4772
|
+
let entries;
|
|
4773
|
+
try {
|
|
4774
|
+
const dirents = await listDirectoryWithTypes(publishedDir);
|
|
4775
|
+
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
4776
|
+
} catch {
|
|
4777
|
+
return [];
|
|
4778
|
+
}
|
|
4779
|
+
const items = [];
|
|
4780
|
+
for (const name of entries) {
|
|
4781
|
+
const item = await readQueueItem(join(publishedDir, name), name);
|
|
4782
|
+
if (item) items.push(item);
|
|
4783
|
+
}
|
|
4784
|
+
items.sort((a, b) => a.metadata.createdAt.localeCompare(b.metadata.createdAt));
|
|
4785
|
+
return items;
|
|
4786
|
+
}
|
|
4787
|
+
async function getScheduledItemsByIdeaIds(ideaIds) {
|
|
4788
|
+
if (ideaIds.length === 0) return [];
|
|
4789
|
+
const ideaIdSet = new Set(ideaIds);
|
|
4790
|
+
const [pendingItems, publishedItems] = await Promise.all([
|
|
4791
|
+
getPendingItems(),
|
|
4792
|
+
getPublishedItems()
|
|
4793
|
+
]);
|
|
4794
|
+
return [...pendingItems, ...publishedItems].filter(
|
|
4795
|
+
(item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
|
|
4796
|
+
);
|
|
4797
|
+
}
|
|
4798
|
+
async function itemExists(id) {
|
|
4799
|
+
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
4800
|
+
throw new Error(`Invalid ID format: ${id}`);
|
|
4801
|
+
}
|
|
4802
|
+
if (await fileExists(join(getQueueDir(), basename(id)))) {
|
|
4803
|
+
return "pending";
|
|
4804
|
+
}
|
|
4805
|
+
if (await fileExists(join(getPublishedDir(), basename(id)))) {
|
|
4806
|
+
return "published";
|
|
4807
|
+
}
|
|
4808
|
+
return null;
|
|
4809
|
+
}
|
|
4810
|
+
var init_postStore = __esm({
|
|
4811
|
+
"src/L3-services/postStore/postStore.ts"() {
|
|
4812
|
+
"use strict";
|
|
4813
|
+
init_types();
|
|
4814
|
+
init_environment();
|
|
4815
|
+
init_configLogger();
|
|
4816
|
+
init_fileSystem();
|
|
4817
|
+
init_paths();
|
|
4818
|
+
}
|
|
4819
|
+
});
|
|
4820
|
+
|
|
4664
4821
|
// src/L7-app/sdk/VidPipeSDK.ts
|
|
4665
4822
|
init_types();
|
|
4666
4823
|
init_environment();
|
|
@@ -4712,6 +4869,17 @@ var progressEmitter = new ProgressEmitter();
|
|
|
4712
4869
|
// src/L1-infra/process/process.ts
|
|
4713
4870
|
import { execFile as nodeExecFile, execSync as nodeExecSync, spawnSync as nodeSpawnSync } from "child_process";
|
|
4714
4871
|
import { createRequire } from "module";
|
|
4872
|
+
function execCommand(cmd, args, opts) {
|
|
4873
|
+
return new Promise((resolve3, reject) => {
|
|
4874
|
+
nodeExecFile(cmd, args, { ...opts, encoding: "utf-8" }, (error, stdout, stderr) => {
|
|
4875
|
+
if (error) {
|
|
4876
|
+
reject(Object.assign(error, { stdout: String(stdout ?? ""), stderr: String(stderr ?? "") }));
|
|
4877
|
+
} else {
|
|
4878
|
+
resolve3({ stdout: String(stdout ?? ""), stderr: String(stderr ?? "") });
|
|
4879
|
+
}
|
|
4880
|
+
});
|
|
4881
|
+
});
|
|
4882
|
+
}
|
|
4715
4883
|
function execFileRaw(cmd, args, opts, callback) {
|
|
4716
4884
|
nodeExecFile(cmd, args, { ...opts, encoding: "utf-8" }, (error, stdout, stderr) => {
|
|
4717
4885
|
callback(error, String(stdout ?? ""), String(stderr ?? ""));
|
|
@@ -7403,16 +7571,10 @@ var LateApiClient = class {
|
|
|
7403
7571
|
return data.accounts ?? [];
|
|
7404
7572
|
}
|
|
7405
7573
|
async getScheduledPosts(platform) {
|
|
7406
|
-
|
|
7407
|
-
if (platform) params.set("platform", platform);
|
|
7408
|
-
const data = await this.request(`/posts?${params}`);
|
|
7409
|
-
return data.posts ?? [];
|
|
7574
|
+
return this.listPosts({ status: "scheduled", platform });
|
|
7410
7575
|
}
|
|
7411
7576
|
async getDraftPosts(platform) {
|
|
7412
|
-
|
|
7413
|
-
if (platform) params.set("platform", platform);
|
|
7414
|
-
const data = await this.request(`/posts?${params}`);
|
|
7415
|
-
return data.posts ?? [];
|
|
7577
|
+
return this.listPosts({ status: "draft", platform });
|
|
7416
7578
|
}
|
|
7417
7579
|
async createPost(params) {
|
|
7418
7580
|
const data = await this.request("/posts", {
|
|
@@ -7518,6 +7680,7 @@ function createLateApiClient(...args) {
|
|
|
7518
7680
|
// src/L2-clients/scheduleStore/scheduleStore.ts
|
|
7519
7681
|
init_fileSystem();
|
|
7520
7682
|
init_paths();
|
|
7683
|
+
init_globalConfig();
|
|
7521
7684
|
async function readScheduleFile(filePath) {
|
|
7522
7685
|
return readTextFile(filePath);
|
|
7523
7686
|
}
|
|
@@ -7529,7 +7692,10 @@ async function writeScheduleFile(filePath, content) {
|
|
|
7529
7692
|
});
|
|
7530
7693
|
}
|
|
7531
7694
|
function resolveSchedulePath(configPath) {
|
|
7532
|
-
|
|
7695
|
+
if (configPath) return configPath;
|
|
7696
|
+
const globalPath = getGlobalConfigValue("defaults", "scheduleConfig");
|
|
7697
|
+
if (globalPath) return globalPath;
|
|
7698
|
+
return join(process.cwd(), "schedule.json");
|
|
7533
7699
|
}
|
|
7534
7700
|
|
|
7535
7701
|
// src/L3-services/scheduler/scheduleConfig.ts
|
|
@@ -7794,7 +7960,7 @@ function getPlatformSchedule(platform, clipType) {
|
|
|
7794
7960
|
avoidDays: sub.avoidDays
|
|
7795
7961
|
};
|
|
7796
7962
|
}
|
|
7797
|
-
if (
|
|
7963
|
+
if (schedule.slots.length === 0 && schedule.byClipType) {
|
|
7798
7964
|
const allSlots = [];
|
|
7799
7965
|
const allAvoidDays = /* @__PURE__ */ new Set();
|
|
7800
7966
|
for (const sub of Object.values(schedule.byClipType)) {
|
|
@@ -7815,162 +7981,36 @@ function getDisplacementConfig() {
|
|
|
7815
7981
|
return cachedConfig?.displacement ?? { ...defaultDisplacement };
|
|
7816
7982
|
}
|
|
7817
7983
|
|
|
7818
|
-
// src/L3-services/
|
|
7819
|
-
|
|
7820
|
-
init_environment();
|
|
7984
|
+
// src/L3-services/scheduler/realign.ts
|
|
7985
|
+
init_postStore();
|
|
7821
7986
|
init_configLogger();
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
7830
|
-
return
|
|
7987
|
+
|
|
7988
|
+
// src/L3-services/scheduler/scheduler.ts
|
|
7989
|
+
init_configLogger();
|
|
7990
|
+
init_postStore();
|
|
7991
|
+
var MAX_LOOKAHEAD_DAYS = 730;
|
|
7992
|
+
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
7993
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
7994
|
+
function normalizeDateTime(isoString) {
|
|
7995
|
+
return new Date(isoString).getTime();
|
|
7831
7996
|
}
|
|
7832
|
-
|
|
7833
|
-
|
|
7834
|
-
const postPath = join(folderPath, "post.md");
|
|
7835
|
-
try {
|
|
7836
|
-
const metadataRaw = await readTextFile(metadataPath);
|
|
7837
|
-
const metadata = JSON.parse(metadataRaw);
|
|
7838
|
-
let postContent = "";
|
|
7839
|
-
try {
|
|
7840
|
-
postContent = await readTextFile(postPath);
|
|
7841
|
-
} catch {
|
|
7842
|
-
logger_default.debug(`No post.md found for ${String(id).replace(/[\r\n]/g, "")}`);
|
|
7843
|
-
}
|
|
7844
|
-
const videoPath = join(folderPath, "media.mp4");
|
|
7845
|
-
const imagePath = join(folderPath, "media.png");
|
|
7846
|
-
let mediaPath = null;
|
|
7847
|
-
let hasMedia = false;
|
|
7848
|
-
if (await fileExists(videoPath)) {
|
|
7849
|
-
mediaPath = videoPath;
|
|
7850
|
-
hasMedia = true;
|
|
7851
|
-
} else if (await fileExists(imagePath)) {
|
|
7852
|
-
mediaPath = imagePath;
|
|
7853
|
-
hasMedia = true;
|
|
7854
|
-
}
|
|
7855
|
-
return {
|
|
7856
|
-
id,
|
|
7857
|
-
metadata,
|
|
7858
|
-
postContent,
|
|
7859
|
-
hasMedia,
|
|
7860
|
-
mediaPath,
|
|
7861
|
-
folderPath
|
|
7862
|
-
};
|
|
7863
|
-
} catch (err) {
|
|
7864
|
-
logger_default.debug(`Failed to read queue item ${String(id).replace(/[\r\n]/g, "")}: ${String(err).replace(/[\r\n]/g, "")}`);
|
|
7865
|
-
return null;
|
|
7866
|
-
}
|
|
7997
|
+
function sanitizeLogValue(value) {
|
|
7998
|
+
return value.replace(/[\r\n]/g, "");
|
|
7867
7999
|
}
|
|
7868
|
-
|
|
7869
|
-
const
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
try {
|
|
7873
|
-
const dirents = await listDirectoryWithTypes(queueDir);
|
|
7874
|
-
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
7875
|
-
} catch {
|
|
7876
|
-
return [];
|
|
7877
|
-
}
|
|
7878
|
-
const items = [];
|
|
7879
|
-
for (const name of entries) {
|
|
7880
|
-
const item = await readQueueItem(join(queueDir, name), name);
|
|
7881
|
-
if (item) items.push(item);
|
|
7882
|
-
}
|
|
7883
|
-
items.sort((a, b) => {
|
|
7884
|
-
if (a.hasMedia !== b.hasMedia) return a.hasMedia ? -1 : 1;
|
|
7885
|
-
return a.metadata.createdAt.localeCompare(b.metadata.createdAt);
|
|
8000
|
+
function getTimezoneOffset(timezone, date) {
|
|
8001
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
8002
|
+
timeZone: timezone,
|
|
8003
|
+
timeZoneName: "longOffset"
|
|
7886
8004
|
});
|
|
7887
|
-
|
|
7888
|
-
|
|
7889
|
-
|
|
7890
|
-
if (
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
7894
|
-
|
|
7895
|
-
|
|
7896
|
-
await writeTextFile(join(folderPath, "post.md"), postContent);
|
|
7897
|
-
let hasMedia = false;
|
|
7898
|
-
const ext = mediaSourcePath ? extname(mediaSourcePath) : ".mp4";
|
|
7899
|
-
const mediaFilename = `media${ext}`;
|
|
7900
|
-
const mediaPath = join(folderPath, mediaFilename);
|
|
7901
|
-
if (mediaSourcePath) {
|
|
7902
|
-
await copyFile(mediaSourcePath, mediaPath);
|
|
7903
|
-
hasMedia = true;
|
|
7904
|
-
}
|
|
7905
|
-
logger_default.debug(`Created queue item: ${String(id).replace(/[\r\n]/g, "")}`);
|
|
7906
|
-
return {
|
|
7907
|
-
id,
|
|
7908
|
-
metadata,
|
|
7909
|
-
postContent,
|
|
7910
|
-
hasMedia,
|
|
7911
|
-
mediaPath: hasMedia ? mediaPath : null,
|
|
7912
|
-
folderPath
|
|
7913
|
-
};
|
|
7914
|
-
}
|
|
7915
|
-
async function getPublishedItems() {
|
|
7916
|
-
const publishedDir = getPublishedDir();
|
|
7917
|
-
await ensureDirectory(publishedDir);
|
|
7918
|
-
let entries;
|
|
7919
|
-
try {
|
|
7920
|
-
const dirents = await listDirectoryWithTypes(publishedDir);
|
|
7921
|
-
entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
7922
|
-
} catch {
|
|
7923
|
-
return [];
|
|
7924
|
-
}
|
|
7925
|
-
const items = [];
|
|
7926
|
-
for (const name of entries) {
|
|
7927
|
-
const item = await readQueueItem(join(publishedDir, name), name);
|
|
7928
|
-
if (item) items.push(item);
|
|
7929
|
-
}
|
|
7930
|
-
items.sort((a, b) => a.metadata.createdAt.localeCompare(b.metadata.createdAt));
|
|
7931
|
-
return items;
|
|
7932
|
-
}
|
|
7933
|
-
async function getScheduledItemsByIdeaIds(ideaIds) {
|
|
7934
|
-
if (ideaIds.length === 0) return [];
|
|
7935
|
-
const ideaIdSet = new Set(ideaIds);
|
|
7936
|
-
const [pendingItems, publishedItems] = await Promise.all([
|
|
7937
|
-
getPendingItems(),
|
|
7938
|
-
getPublishedItems()
|
|
7939
|
-
]);
|
|
7940
|
-
return [...pendingItems, ...publishedItems].filter(
|
|
7941
|
-
(item) => item.metadata.ideaIds?.some((id) => ideaIdSet.has(id)) ?? false
|
|
7942
|
-
);
|
|
7943
|
-
}
|
|
7944
|
-
async function getPublishedItemByLatePostId(latePostId) {
|
|
7945
|
-
const publishedItems = await getPublishedItems();
|
|
7946
|
-
return publishedItems.find((item) => item.metadata.latePostId === latePostId) ?? null;
|
|
7947
|
-
}
|
|
7948
|
-
async function itemExists(id) {
|
|
7949
|
-
if (!id || !/^[a-zA-Z0-9_-]+$/.test(id)) {
|
|
7950
|
-
throw new Error(`Invalid ID format: ${id}`);
|
|
7951
|
-
}
|
|
7952
|
-
if (await fileExists(join(getQueueDir(), basename(id)))) {
|
|
7953
|
-
return "pending";
|
|
7954
|
-
}
|
|
7955
|
-
if (await fileExists(join(getPublishedDir(), basename(id)))) {
|
|
7956
|
-
return "published";
|
|
7957
|
-
}
|
|
7958
|
-
return null;
|
|
7959
|
-
}
|
|
7960
|
-
|
|
7961
|
-
// src/L3-services/scheduler/realign.ts
|
|
7962
|
-
init_configLogger();
|
|
7963
|
-
function getTimezoneOffset(timezone, date) {
|
|
7964
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
7965
|
-
timeZone: timezone,
|
|
7966
|
-
timeZoneName: "longOffset"
|
|
7967
|
-
});
|
|
7968
|
-
const parts = formatter.formatToParts(date);
|
|
7969
|
-
const tzPart = parts.find((p) => p.type === "timeZoneName");
|
|
7970
|
-
const match = tzPart?.value?.match(/GMT([+-]\d{2}:\d{2})/);
|
|
7971
|
-
if (match) return match[1];
|
|
7972
|
-
if (tzPart?.value === "GMT") return "+00:00";
|
|
7973
|
-
return "+00:00";
|
|
8005
|
+
const parts = formatter.formatToParts(date);
|
|
8006
|
+
const tzPart = parts.find((part) => part.type === "timeZoneName");
|
|
8007
|
+
const match = tzPart?.value?.match(/GMT([+-]\d{2}:\d{2})/);
|
|
8008
|
+
if (match) return match[1];
|
|
8009
|
+
if (tzPart?.value === "GMT") return "+00:00";
|
|
8010
|
+
logger_default.warn(
|
|
8011
|
+
`Could not parse timezone offset for timezone "${timezone}" on date "${date.toISOString()}". Raw timeZoneName part: "${tzPart?.value ?? "undefined"}". Falling back to UTC (+00:00).`
|
|
8012
|
+
);
|
|
8013
|
+
return "+00:00";
|
|
7974
8014
|
}
|
|
7975
8015
|
function buildSlotDatetime(date, time, timezone) {
|
|
7976
8016
|
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
@@ -7980,9 +8020,12 @@ function buildSlotDatetime(date, time, timezone) {
|
|
|
7980
8020
|
day: "2-digit"
|
|
7981
8021
|
});
|
|
7982
8022
|
const parts = formatter.formatToParts(date);
|
|
7983
|
-
const
|
|
7984
|
-
const
|
|
7985
|
-
const
|
|
8023
|
+
const yearPart = parts.find((part) => part.type === "year")?.value;
|
|
8024
|
+
const monthPart = parts.find((part) => part.type === "month")?.value;
|
|
8025
|
+
const dayPart = parts.find((part) => part.type === "day")?.value;
|
|
8026
|
+
const year = yearPart ?? String(date.getFullYear());
|
|
8027
|
+
const month = (monthPart ?? String(date.getMonth() + 1)).padStart(2, "0");
|
|
8028
|
+
const day = (dayPart ?? String(date.getDate())).padStart(2, "0");
|
|
7986
8029
|
const offset = getTimezoneOffset(timezone, date);
|
|
7987
8030
|
return `${year}-${month}-${day}T${time}:00${offset}`;
|
|
7988
8031
|
}
|
|
@@ -8003,6 +8046,277 @@ function getDayOfWeekInTimezone(date, timezone) {
|
|
|
8003
8046
|
};
|
|
8004
8047
|
return map[short] ?? "mon";
|
|
8005
8048
|
}
|
|
8049
|
+
async function fetchScheduledPostsSafe(platform) {
|
|
8050
|
+
try {
|
|
8051
|
+
const client = new LateApiClient();
|
|
8052
|
+
return await client.getScheduledPosts(platform);
|
|
8053
|
+
} catch (err) {
|
|
8054
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8055
|
+
logger_default.warn(`Late API unreachable, using local data only: ${msg}`);
|
|
8056
|
+
return [];
|
|
8057
|
+
}
|
|
8058
|
+
}
|
|
8059
|
+
async function buildBookedMap(platform) {
|
|
8060
|
+
const [latePosts, publishedItems] = await Promise.all([
|
|
8061
|
+
fetchScheduledPostsSafe(platform),
|
|
8062
|
+
getPublishedItems()
|
|
8063
|
+
]);
|
|
8064
|
+
const ideaLinkedPostIds = /* @__PURE__ */ new Set();
|
|
8065
|
+
for (const item of publishedItems) {
|
|
8066
|
+
if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
|
|
8067
|
+
ideaLinkedPostIds.add(item.metadata.latePostId);
|
|
8068
|
+
}
|
|
8069
|
+
}
|
|
8070
|
+
const map = /* @__PURE__ */ new Map();
|
|
8071
|
+
for (const post of latePosts) {
|
|
8072
|
+
if (!post.scheduledFor) continue;
|
|
8073
|
+
for (const scheduledPlatform of post.platforms) {
|
|
8074
|
+
if (!platform || scheduledPlatform.platform === platform) {
|
|
8075
|
+
const ms = normalizeDateTime(post.scheduledFor);
|
|
8076
|
+
map.set(ms, {
|
|
8077
|
+
scheduledFor: post.scheduledFor,
|
|
8078
|
+
source: "late",
|
|
8079
|
+
postId: post._id,
|
|
8080
|
+
platform: scheduledPlatform.platform,
|
|
8081
|
+
status: post.status,
|
|
8082
|
+
ideaLinked: ideaLinkedPostIds.has(post._id)
|
|
8083
|
+
});
|
|
8084
|
+
}
|
|
8085
|
+
}
|
|
8086
|
+
}
|
|
8087
|
+
for (const item of publishedItems) {
|
|
8088
|
+
if (platform && item.metadata.platform !== platform) continue;
|
|
8089
|
+
if (!item.metadata.scheduledFor) continue;
|
|
8090
|
+
const ms = normalizeDateTime(item.metadata.scheduledFor);
|
|
8091
|
+
if (!map.has(ms)) {
|
|
8092
|
+
map.set(ms, {
|
|
8093
|
+
scheduledFor: item.metadata.scheduledFor,
|
|
8094
|
+
source: "local",
|
|
8095
|
+
itemId: item.id,
|
|
8096
|
+
platform: item.metadata.platform,
|
|
8097
|
+
ideaLinked: Boolean(item.metadata.ideaIds?.length)
|
|
8098
|
+
});
|
|
8099
|
+
}
|
|
8100
|
+
}
|
|
8101
|
+
return map;
|
|
8102
|
+
}
|
|
8103
|
+
async function getIdeaLinkedLatePostIds() {
|
|
8104
|
+
const publishedItems = await getPublishedItems();
|
|
8105
|
+
const ids = /* @__PURE__ */ new Set();
|
|
8106
|
+
for (const item of publishedItems) {
|
|
8107
|
+
if (item.metadata.latePostId && item.metadata.ideaIds?.length) {
|
|
8108
|
+
ids.add(item.metadata.latePostId);
|
|
8109
|
+
}
|
|
8110
|
+
}
|
|
8111
|
+
return ids;
|
|
8112
|
+
}
|
|
8113
|
+
function* generateTimeslots(platformConfig, timezone, fromMs, maxMs) {
|
|
8114
|
+
const baseDate = new Date(fromMs);
|
|
8115
|
+
const upperMs = maxMs ?? fromMs + MAX_LOOKAHEAD_DAYS * DAY_MS;
|
|
8116
|
+
for (let dayOffset = 0; dayOffset <= MAX_LOOKAHEAD_DAYS; dayOffset++) {
|
|
8117
|
+
const day = new Date(baseDate);
|
|
8118
|
+
day.setDate(day.getDate() + dayOffset);
|
|
8119
|
+
const dayOfWeek = getDayOfWeekInTimezone(day, timezone);
|
|
8120
|
+
if (platformConfig.avoidDays.includes(dayOfWeek)) continue;
|
|
8121
|
+
const dayCandidates = [];
|
|
8122
|
+
for (const slot of platformConfig.slots) {
|
|
8123
|
+
if (!slot.days.includes(dayOfWeek)) continue;
|
|
8124
|
+
const datetime = buildSlotDatetime(day, slot.time, timezone);
|
|
8125
|
+
const ms = normalizeDateTime(datetime);
|
|
8126
|
+
if (ms <= fromMs) continue;
|
|
8127
|
+
if (ms > upperMs) continue;
|
|
8128
|
+
dayCandidates.push({ datetime, ms });
|
|
8129
|
+
}
|
|
8130
|
+
dayCandidates.sort((a, b) => a.ms - b.ms);
|
|
8131
|
+
for (const candidate of dayCandidates) yield candidate;
|
|
8132
|
+
if (dayCandidates.length === 0) {
|
|
8133
|
+
const dayStartMs = normalizeDateTime(buildSlotDatetime(day, "00:00", timezone));
|
|
8134
|
+
if (dayStartMs > upperMs) break;
|
|
8135
|
+
}
|
|
8136
|
+
}
|
|
8137
|
+
}
|
|
8138
|
+
function passesIdeaSpacing(candidateMs, candidatePlatform, ideaRefs, samePlatformMs, crossPlatformMs) {
|
|
8139
|
+
for (const ref of ideaRefs) {
|
|
8140
|
+
const diff = Math.abs(candidateMs - ref.scheduledForMs);
|
|
8141
|
+
if (ref.platform === candidatePlatform && diff < samePlatformMs) return false;
|
|
8142
|
+
if (diff < crossPlatformMs) return false;
|
|
8143
|
+
}
|
|
8144
|
+
return true;
|
|
8145
|
+
}
|
|
8146
|
+
async function getIdeaReferences(ideaIds, bookedMap) {
|
|
8147
|
+
const sameIdeaPosts = await getScheduledItemsByIdeaIds(ideaIds);
|
|
8148
|
+
const lateSlotsByPostId = /* @__PURE__ */ new Map();
|
|
8149
|
+
const localSlotsByItemId = /* @__PURE__ */ new Map();
|
|
8150
|
+
for (const slot of bookedMap.values()) {
|
|
8151
|
+
if (slot.postId) {
|
|
8152
|
+
const arr = lateSlotsByPostId.get(slot.postId) ?? [];
|
|
8153
|
+
arr.push(slot);
|
|
8154
|
+
lateSlotsByPostId.set(slot.postId, arr);
|
|
8155
|
+
}
|
|
8156
|
+
if (slot.itemId) {
|
|
8157
|
+
const arr = localSlotsByItemId.get(slot.itemId) ?? [];
|
|
8158
|
+
arr.push(slot);
|
|
8159
|
+
localSlotsByItemId.set(slot.itemId, arr);
|
|
8160
|
+
}
|
|
8161
|
+
}
|
|
8162
|
+
const refs = [];
|
|
8163
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8164
|
+
const addRef = (platform, scheduledFor) => {
|
|
8165
|
+
if (!scheduledFor) return;
|
|
8166
|
+
const key = `${platform}@${scheduledFor}`;
|
|
8167
|
+
if (seen.has(key)) return;
|
|
8168
|
+
seen.add(key);
|
|
8169
|
+
refs.push({ platform, scheduledForMs: normalizeDateTime(scheduledFor) });
|
|
8170
|
+
};
|
|
8171
|
+
for (const item of sameIdeaPosts) {
|
|
8172
|
+
addRef(item.metadata.platform, item.metadata.scheduledFor);
|
|
8173
|
+
if (item.metadata.latePostId) {
|
|
8174
|
+
for (const slot of lateSlotsByPostId.get(item.metadata.latePostId) ?? []) {
|
|
8175
|
+
addRef(slot.platform, slot.scheduledFor);
|
|
8176
|
+
}
|
|
8177
|
+
}
|
|
8178
|
+
for (const slot of localSlotsByItemId.get(item.id) ?? []) {
|
|
8179
|
+
addRef(slot.platform, slot.scheduledFor);
|
|
8180
|
+
}
|
|
8181
|
+
}
|
|
8182
|
+
return refs;
|
|
8183
|
+
}
|
|
8184
|
+
async function schedulePost(platformConfig, fromMs, isIdeaPost, label, ctx) {
|
|
8185
|
+
const indent = " ".repeat(ctx.depth);
|
|
8186
|
+
let checked = 0;
|
|
8187
|
+
let skippedBooked = 0;
|
|
8188
|
+
let skippedSpacing = 0;
|
|
8189
|
+
logger_default.debug(`${indent}[schedulePost] Looking for slot for ${label} (idea=${isIdeaPost}) from ${new Date(fromMs).toISOString()}`);
|
|
8190
|
+
for (const { datetime, ms } of generateTimeslots(platformConfig, ctx.timezone, fromMs)) {
|
|
8191
|
+
checked++;
|
|
8192
|
+
const booked = ctx.bookedMap.get(ms);
|
|
8193
|
+
if (!booked) {
|
|
8194
|
+
if (isIdeaPost && ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
|
|
8195
|
+
skippedSpacing++;
|
|
8196
|
+
if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
|
|
8197
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} too close to same-idea post \u2014 skipping`);
|
|
8198
|
+
}
|
|
8199
|
+
continue;
|
|
8200
|
+
}
|
|
8201
|
+
logger_default.debug(`${indent}[schedulePost] \u2705 Found empty slot: ${datetime} (checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing)`);
|
|
8202
|
+
return datetime;
|
|
8203
|
+
}
|
|
8204
|
+
if (isIdeaPost && ctx.displacementEnabled && !booked.ideaLinked && booked.source === "late" && booked.postId) {
|
|
8205
|
+
if (ctx.ideaRefs.length > 0 && !passesIdeaSpacing(ms, ctx.platform, ctx.ideaRefs, ctx.samePlatformMs, ctx.crossPlatformMs)) {
|
|
8206
|
+
skippedSpacing++;
|
|
8207
|
+
if (skippedSpacing <= 5 || skippedSpacing % 50 === 0) {
|
|
8208
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} too close to same-idea post \u2014 skipping (even though displaceable)`);
|
|
8209
|
+
}
|
|
8210
|
+
continue;
|
|
8211
|
+
}
|
|
8212
|
+
logger_default.info(`${indent}[schedulePost] \u{1F504} Slot ${datetime} taken by non-idea post ${booked.postId} \u2014 displacing`);
|
|
8213
|
+
const newHome = await schedulePost(
|
|
8214
|
+
platformConfig,
|
|
8215
|
+
ms,
|
|
8216
|
+
false,
|
|
8217
|
+
`displaced:${booked.postId}`,
|
|
8218
|
+
{ ...ctx, depth: ctx.depth + 1 }
|
|
8219
|
+
);
|
|
8220
|
+
if (newHome) {
|
|
8221
|
+
if (!ctx.dryRun) {
|
|
8222
|
+
try {
|
|
8223
|
+
await ctx.lateClient.schedulePost(booked.postId, newHome);
|
|
8224
|
+
} catch (err) {
|
|
8225
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8226
|
+
logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Failed to displace ${booked.postId} via Late API: ${msg} \u2014 skipping slot`);
|
|
8227
|
+
continue;
|
|
8228
|
+
}
|
|
8229
|
+
}
|
|
8230
|
+
logger_default.info(`${indent}[schedulePost] \u{1F4E6} Displaced ${booked.postId}: ${datetime} \u2192 ${newHome}`);
|
|
8231
|
+
ctx.bookedMap.delete(ms);
|
|
8232
|
+
const newMs = normalizeDateTime(newHome);
|
|
8233
|
+
ctx.bookedMap.set(newMs, { ...booked, scheduledFor: newHome });
|
|
8234
|
+
logger_default.debug(`${indent}[schedulePost] \u2705 Taking slot: ${datetime} (checked ${checked} candidates)`);
|
|
8235
|
+
return datetime;
|
|
8236
|
+
}
|
|
8237
|
+
logger_default.warn(`${indent}[schedulePost] \u26A0\uFE0F Could not displace ${booked.postId} \u2014 no empty slot found after ${datetime}`);
|
|
8238
|
+
}
|
|
8239
|
+
if (booked.ideaLinked) {
|
|
8240
|
+
skippedBooked++;
|
|
8241
|
+
if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
|
|
8242
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken by idea post ${booked.postId ?? booked.itemId} \u2014 skipping`);
|
|
8243
|
+
}
|
|
8244
|
+
continue;
|
|
8245
|
+
}
|
|
8246
|
+
skippedBooked++;
|
|
8247
|
+
if (skippedBooked <= 5 || skippedBooked % 50 === 0) {
|
|
8248
|
+
logger_default.debug(`${indent}[schedulePost] \u23ED\uFE0F Slot ${datetime} taken (${booked.source}/${booked.postId ?? booked.itemId}) \u2014 skipping`);
|
|
8249
|
+
}
|
|
8250
|
+
}
|
|
8251
|
+
logger_default.warn(`[schedulePost] \u274C No slot found for ${label} \u2014 checked ${checked} candidates, skipped ${skippedBooked} booked, ${skippedSpacing} spacing`);
|
|
8252
|
+
return null;
|
|
8253
|
+
}
|
|
8254
|
+
async function findNextSlot(platform, clipType, options) {
|
|
8255
|
+
const config2 = await loadScheduleConfig();
|
|
8256
|
+
const platformConfig = getPlatformSchedule(platform, clipType);
|
|
8257
|
+
if (!platformConfig) {
|
|
8258
|
+
logger_default.warn(`No schedule config found for platform "${sanitizeLogValue(platform)}"`);
|
|
8259
|
+
return null;
|
|
8260
|
+
}
|
|
8261
|
+
const { timezone } = config2;
|
|
8262
|
+
const nowMs = Date.now();
|
|
8263
|
+
const ideaIds = options?.ideaIds?.filter(Boolean) ?? [];
|
|
8264
|
+
const isIdeaAware = ideaIds.length > 0;
|
|
8265
|
+
const bookedMap = await buildBookedMap(platform);
|
|
8266
|
+
const ideaLinkedPostIds = await getIdeaLinkedLatePostIds();
|
|
8267
|
+
const label = `${platform}/${clipType ?? "default"}`;
|
|
8268
|
+
let ideaRefs = [];
|
|
8269
|
+
let samePlatformMs = 0;
|
|
8270
|
+
let crossPlatformMs = 0;
|
|
8271
|
+
if (isIdeaAware) {
|
|
8272
|
+
const allBookedMap = await buildBookedMap();
|
|
8273
|
+
ideaRefs = await getIdeaReferences(ideaIds, allBookedMap);
|
|
8274
|
+
const spacingConfig = getIdeaSpacingConfig();
|
|
8275
|
+
samePlatformMs = spacingConfig.samePlatformHours * HOUR_MS;
|
|
8276
|
+
crossPlatformMs = spacingConfig.crossPlatformHours * HOUR_MS;
|
|
8277
|
+
}
|
|
8278
|
+
logger_default.info(`[findNextSlot] Scheduling ${label} (idea=${isIdeaAware}, booked=${bookedMap.size} slots, spacingRefs=${ideaRefs.length})`);
|
|
8279
|
+
const ctx = {
|
|
8280
|
+
timezone,
|
|
8281
|
+
bookedMap,
|
|
8282
|
+
ideaLinkedPostIds,
|
|
8283
|
+
lateClient: new LateApiClient(),
|
|
8284
|
+
displacementEnabled: getDisplacementConfig().enabled,
|
|
8285
|
+
dryRun: false,
|
|
8286
|
+
depth: 0,
|
|
8287
|
+
ideaRefs,
|
|
8288
|
+
samePlatformMs,
|
|
8289
|
+
crossPlatformMs,
|
|
8290
|
+
platform
|
|
8291
|
+
};
|
|
8292
|
+
const result = await schedulePost(platformConfig, nowMs, isIdeaAware, label, ctx);
|
|
8293
|
+
if (!result) {
|
|
8294
|
+
logger_default.warn(`[findNextSlot] No available slot for "${sanitizeLogValue(platform)}" within ${MAX_LOOKAHEAD_DAYS} days`);
|
|
8295
|
+
}
|
|
8296
|
+
return result;
|
|
8297
|
+
}
|
|
8298
|
+
async function getScheduleCalendar(startDate, endDate) {
|
|
8299
|
+
const bookedMap = await buildBookedMap();
|
|
8300
|
+
let filtered = [...bookedMap.values()].filter((slot) => slot.source === "local" || slot.status === "scheduled").map((slot) => ({
|
|
8301
|
+
platform: slot.platform,
|
|
8302
|
+
scheduledFor: slot.scheduledFor,
|
|
8303
|
+
source: slot.source,
|
|
8304
|
+
postId: slot.postId,
|
|
8305
|
+
itemId: slot.itemId
|
|
8306
|
+
}));
|
|
8307
|
+
if (startDate) {
|
|
8308
|
+
const startMs = startDate.getTime();
|
|
8309
|
+
filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) >= startMs);
|
|
8310
|
+
}
|
|
8311
|
+
if (endDate) {
|
|
8312
|
+
const endMs = endDate.getTime();
|
|
8313
|
+
filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) <= endMs);
|
|
8314
|
+
}
|
|
8315
|
+
filtered.sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
|
|
8316
|
+
return filtered;
|
|
8317
|
+
}
|
|
8318
|
+
|
|
8319
|
+
// src/L3-services/scheduler/realign.ts
|
|
8006
8320
|
var PLATFORM_ALIASES2 = { twitter: "x" };
|
|
8007
8321
|
function normalizeSchedulePlatform(platform) {
|
|
8008
8322
|
return PLATFORM_ALIASES2[platform] ?? platform;
|
|
@@ -8035,33 +8349,22 @@ async function fetchAllPosts(client, statuses, platform) {
|
|
|
8035
8349
|
}
|
|
8036
8350
|
return allPosts;
|
|
8037
8351
|
}
|
|
8038
|
-
function
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
if (!slot.days.includes(dayOfWeek)) continue;
|
|
8055
|
-
const iso = buildSlotDatetime(day, slot.time, timezone);
|
|
8056
|
-
const ms = new Date(iso).getTime();
|
|
8057
|
-
if (ms <= nowMs) continue;
|
|
8058
|
-
if (!bookedMs.has(ms)) {
|
|
8059
|
-
available.push(iso);
|
|
8060
|
-
bookedMs.add(ms);
|
|
8061
|
-
}
|
|
8062
|
-
}
|
|
8063
|
-
}
|
|
8064
|
-
return available;
|
|
8352
|
+
function isOnValidSlot(iso, schedule, timezone) {
|
|
8353
|
+
if (schedule.slots.length === 0) return false;
|
|
8354
|
+
const date = new Date(iso);
|
|
8355
|
+
const dayOfWeek = getDayOfWeekInTimezone(date, timezone);
|
|
8356
|
+
if (schedule.avoidDays.includes(dayOfWeek)) return false;
|
|
8357
|
+
const timeFormatter = new Intl.DateTimeFormat("en-US", {
|
|
8358
|
+
timeZone: timezone,
|
|
8359
|
+
hour: "2-digit",
|
|
8360
|
+
minute: "2-digit",
|
|
8361
|
+
hour12: false
|
|
8362
|
+
});
|
|
8363
|
+
const timeParts = timeFormatter.formatToParts(date);
|
|
8364
|
+
const hour = timeParts.find((p) => p.type === "hour")?.value ?? "00";
|
|
8365
|
+
const minute = timeParts.find((p) => p.type === "minute")?.value ?? "00";
|
|
8366
|
+
const timeKey = `${hour}:${minute}`;
|
|
8367
|
+
return schedule.slots.some((slot) => slot.time === timeKey && slot.days.includes(dayOfWeek));
|
|
8065
8368
|
}
|
|
8066
8369
|
async function buildRealignPlan(options = {}) {
|
|
8067
8370
|
const config2 = await loadScheduleConfig();
|
|
@@ -8073,9 +8376,8 @@ async function buildRealignPlan(options = {}) {
|
|
|
8073
8376
|
return { posts: [], toCancel: [], skipped: 0, unmatched: 0, totalFetched: 0 };
|
|
8074
8377
|
}
|
|
8075
8378
|
const { byLatePostId, byContent } = options.clipTypeMaps ?? await buildClipTypeMaps();
|
|
8076
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
8077
8379
|
let unmatched = 0;
|
|
8078
|
-
|
|
8380
|
+
const tagged = [];
|
|
8079
8381
|
for (const post of allPosts) {
|
|
8080
8382
|
const platform = post.platforms[0]?.platform;
|
|
8081
8383
|
if (!platform) continue;
|
|
@@ -8083,494 +8385,135 @@ async function buildRealignPlan(options = {}) {
|
|
|
8083
8385
|
if (!clipType && post.content) {
|
|
8084
8386
|
const contentKey = `${platform}::${normalizeContent(post.content)}`;
|
|
8085
8387
|
clipType = byContent.get(contentKey) ?? null;
|
|
8086
|
-
if (clipType) contentMatched++;
|
|
8087
8388
|
}
|
|
8088
8389
|
if (!clipType) {
|
|
8089
8390
|
clipType = "short";
|
|
8090
8391
|
unmatched++;
|
|
8091
8392
|
}
|
|
8092
|
-
|
|
8093
|
-
if (!grouped.has(key)) grouped.set(key, []);
|
|
8094
|
-
grouped.get(key).push({ post, platform, clipType });
|
|
8095
|
-
}
|
|
8096
|
-
const bookedMs = /* @__PURE__ */ new Set();
|
|
8097
|
-
if (contentMatched > 0) {
|
|
8098
|
-
logger_default.info(`${contentMatched} post(s) matched by content fallback (no latePostId)`);
|
|
8393
|
+
tagged.push({ post, platform, clipType });
|
|
8099
8394
|
}
|
|
8100
|
-
const
|
|
8101
|
-
const
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
8112
|
-
|
|
8113
|
-
|
|
8114
|
-
|
|
8115
|
-
|
|
8116
|
-
|
|
8117
|
-
}
|
|
8118
|
-
continue;
|
|
8119
|
-
}
|
|
8120
|
-
posts.sort((a, b) => {
|
|
8121
|
-
const aTime = a.post.scheduledFor ? new Date(a.post.scheduledFor).getTime() : Infinity;
|
|
8122
|
-
const bTime = b.post.scheduledFor ? new Date(b.post.scheduledFor).getTime() : Infinity;
|
|
8123
|
-
return aTime - bTime;
|
|
8124
|
-
});
|
|
8125
|
-
const slots = generateSlots(schedulePlatform, clipType, posts.length, bookedMs, timezone);
|
|
8126
|
-
for (let i = 0; i < posts.length; i++) {
|
|
8127
|
-
const { post } = posts[i];
|
|
8128
|
-
const newSlot = slots[i];
|
|
8129
|
-
if (!newSlot) {
|
|
8130
|
-
if (post.status !== "cancelled") {
|
|
8131
|
-
toCancel.push({
|
|
8132
|
-
post,
|
|
8133
|
-
platform,
|
|
8134
|
-
clipType: posts[i].clipType,
|
|
8135
|
-
reason: `No more available slots for ${schedulePlatform}/${clipType}`
|
|
8136
|
-
});
|
|
8137
|
-
}
|
|
8138
|
-
continue;
|
|
8139
|
-
}
|
|
8140
|
-
const currentMs = post.scheduledFor ? new Date(post.scheduledFor).getTime() : 0;
|
|
8141
|
-
const newMs = new Date(newSlot).getTime();
|
|
8142
|
-
if (currentMs === newMs && post.status === "scheduled") {
|
|
8143
|
-
skipped++;
|
|
8144
|
-
continue;
|
|
8145
|
-
}
|
|
8146
|
-
result.push({
|
|
8147
|
-
post,
|
|
8148
|
-
platform,
|
|
8149
|
-
clipType: posts[i].clipType,
|
|
8150
|
-
oldScheduledFor: post.scheduledFor ?? null,
|
|
8151
|
-
newScheduledFor: newSlot
|
|
8152
|
-
});
|
|
8153
|
-
}
|
|
8154
|
-
}
|
|
8155
|
-
result.sort((a, b) => new Date(a.newScheduledFor).getTime() - new Date(b.newScheduledFor).getTime());
|
|
8156
|
-
return { posts: result, toCancel, skipped, unmatched, totalFetched: allPosts.length };
|
|
8157
|
-
}
|
|
8158
|
-
async function executeRealignPlan(plan, onProgress) {
|
|
8159
|
-
const client = new LateApiClient();
|
|
8160
|
-
let updated = 0;
|
|
8161
|
-
let cancelled = 0;
|
|
8162
|
-
let failed = 0;
|
|
8163
|
-
const errors = [];
|
|
8164
|
-
const totalOps = plan.toCancel.length + plan.posts.length;
|
|
8165
|
-
let completed = 0;
|
|
8166
|
-
for (const entry of plan.toCancel) {
|
|
8167
|
-
completed++;
|
|
8168
|
-
try {
|
|
8169
|
-
await client.updatePost(entry.post._id, { status: "cancelled" });
|
|
8170
|
-
cancelled++;
|
|
8171
|
-
const preview = entry.post.content.slice(0, 40).replace(/\n/g, " ");
|
|
8172
|
-
logger_default.info(`[${completed}/${totalOps}] \u{1F6AB} Cancelled ${entry.platform}/${entry.clipType}: "${preview}..."`);
|
|
8173
|
-
onProgress?.(completed, totalOps, "cancelling");
|
|
8174
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
8175
|
-
} catch (err) {
|
|
8176
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
8177
|
-
errors.push({ postId: entry.post._id, error: msg });
|
|
8178
|
-
failed++;
|
|
8179
|
-
logger_default.error(`[${completed}/${totalOps}] \u274C Failed to cancel ${entry.post._id}: ${msg}`);
|
|
8180
|
-
}
|
|
8181
|
-
}
|
|
8182
|
-
for (const entry of plan.posts) {
|
|
8183
|
-
completed++;
|
|
8184
|
-
try {
|
|
8185
|
-
await client.schedulePost(entry.post._id, entry.newScheduledFor);
|
|
8186
|
-
updated++;
|
|
8187
|
-
const preview = entry.post.content.slice(0, 40).replace(/\n/g, " ");
|
|
8188
|
-
logger_default.info(`[${completed}/${totalOps}] \u2705 ${entry.platform}/${entry.clipType}: "${preview}..." \u2192 ${entry.newScheduledFor}`);
|
|
8189
|
-
onProgress?.(completed, totalOps, "updating");
|
|
8190
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
8191
|
-
} catch (err) {
|
|
8192
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
8193
|
-
errors.push({ postId: entry.post._id, error: msg });
|
|
8194
|
-
failed++;
|
|
8195
|
-
logger_default.error(`[${completed}/${totalOps}] \u274C Failed to update ${entry.post._id}: ${msg}`);
|
|
8196
|
-
}
|
|
8197
|
-
}
|
|
8198
|
-
return { updated, cancelled, failed, errors };
|
|
8199
|
-
}
|
|
8200
|
-
|
|
8201
|
-
// src/L3-services/scheduler/scheduler.ts
|
|
8202
|
-
init_configLogger();
|
|
8203
|
-
function normalizeDateTime(isoString) {
|
|
8204
|
-
return new Date(isoString).getTime();
|
|
8205
|
-
}
|
|
8206
|
-
var CHUNK_DAYS = 14;
|
|
8207
|
-
var MAX_LOOKAHEAD_DAYS = 730;
|
|
8208
|
-
var DEFAULT_IDEA_WINDOW_DAYS = 14;
|
|
8209
|
-
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
8210
|
-
var HOUR_MS = 60 * 60 * 1e3;
|
|
8211
|
-
function sanitizeLogValue(value) {
|
|
8212
|
-
return value.replace(/[\r\n]/g, "");
|
|
8213
|
-
}
|
|
8214
|
-
function getTimezoneOffset2(timezone, date) {
|
|
8215
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
8216
|
-
timeZone: timezone,
|
|
8217
|
-
timeZoneName: "longOffset"
|
|
8218
|
-
});
|
|
8219
|
-
const parts = formatter.formatToParts(date);
|
|
8220
|
-
const tzPart = parts.find((part) => part.type === "timeZoneName");
|
|
8221
|
-
const match = tzPart?.value?.match(/GMT([+-]\d{2}:\d{2})/);
|
|
8222
|
-
if (match) return match[1];
|
|
8223
|
-
if (tzPart?.value === "GMT") return "+00:00";
|
|
8224
|
-
logger_default.warn(
|
|
8225
|
-
`Could not parse timezone offset for timezone "${timezone}" on date "${date.toISOString()}". Raw timeZoneName part: "${tzPart?.value ?? "undefined"}". Falling back to UTC (+00:00).`
|
|
8226
|
-
);
|
|
8227
|
-
return "+00:00";
|
|
8228
|
-
}
|
|
8229
|
-
function buildSlotDatetime2(date, time, timezone) {
|
|
8230
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
8231
|
-
timeZone: timezone,
|
|
8232
|
-
year: "numeric",
|
|
8233
|
-
month: "2-digit",
|
|
8234
|
-
day: "2-digit"
|
|
8235
|
-
});
|
|
8236
|
-
const parts = formatter.formatToParts(date);
|
|
8237
|
-
const yearPart = parts.find((part) => part.type === "year")?.value;
|
|
8238
|
-
const monthPart = parts.find((part) => part.type === "month")?.value;
|
|
8239
|
-
const dayPart = parts.find((part) => part.type === "day")?.value;
|
|
8240
|
-
const year = yearPart ?? String(date.getFullYear());
|
|
8241
|
-
const month = (monthPart ?? String(date.getMonth() + 1)).padStart(2, "0");
|
|
8242
|
-
const day = (dayPart ?? String(date.getDate())).padStart(2, "0");
|
|
8243
|
-
const offset = getTimezoneOffset2(timezone, date);
|
|
8244
|
-
return `${year}-${month}-${day}T${time}:00${offset}`;
|
|
8245
|
-
}
|
|
8246
|
-
function getDayOfWeekInTimezone2(date, timezone) {
|
|
8247
|
-
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
8248
|
-
timeZone: timezone,
|
|
8249
|
-
weekday: "short"
|
|
8250
|
-
});
|
|
8251
|
-
const short = formatter.format(date).toLowerCase().slice(0, 3);
|
|
8252
|
-
const map = {
|
|
8253
|
-
sun: "sun",
|
|
8254
|
-
mon: "mon",
|
|
8255
|
-
tue: "tue",
|
|
8256
|
-
wed: "wed",
|
|
8257
|
-
thu: "thu",
|
|
8258
|
-
fri: "fri",
|
|
8259
|
-
sat: "sat"
|
|
8260
|
-
};
|
|
8261
|
-
return map[short] ?? "mon";
|
|
8262
|
-
}
|
|
8263
|
-
async function fetchScheduledPostsSafe(platform) {
|
|
8264
|
-
try {
|
|
8265
|
-
const client = new LateApiClient();
|
|
8266
|
-
return await client.getScheduledPosts(platform);
|
|
8267
|
-
} catch (err) {
|
|
8268
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
8269
|
-
logger_default.warn(`Late API unreachable, using local data only: ${msg}`);
|
|
8270
|
-
return [];
|
|
8271
|
-
}
|
|
8272
|
-
}
|
|
8273
|
-
async function buildBookedSlots(platform) {
|
|
8274
|
-
const [latePosts, publishedItems] = await Promise.all([
|
|
8275
|
-
fetchScheduledPostsSafe(platform),
|
|
8276
|
-
getPublishedItems()
|
|
8277
|
-
]);
|
|
8278
|
-
const slots = [];
|
|
8279
|
-
for (const post of latePosts) {
|
|
8280
|
-
if (!post.scheduledFor) continue;
|
|
8281
|
-
for (const scheduledPlatform of post.platforms) {
|
|
8282
|
-
if (!platform || scheduledPlatform.platform === platform) {
|
|
8283
|
-
slots.push({
|
|
8284
|
-
scheduledFor: post.scheduledFor,
|
|
8285
|
-
source: "late",
|
|
8286
|
-
postId: post._id,
|
|
8287
|
-
platform: scheduledPlatform.platform,
|
|
8288
|
-
status: post.status
|
|
8289
|
-
});
|
|
8290
|
-
}
|
|
8291
|
-
}
|
|
8292
|
-
}
|
|
8293
|
-
for (const item of publishedItems) {
|
|
8294
|
-
if (platform && item.metadata.platform !== platform) continue;
|
|
8295
|
-
if (!item.metadata.scheduledFor) continue;
|
|
8296
|
-
slots.push({
|
|
8297
|
-
scheduledFor: item.metadata.scheduledFor,
|
|
8298
|
-
source: "local",
|
|
8299
|
-
itemId: item.id,
|
|
8300
|
-
platform: item.metadata.platform
|
|
8301
|
-
});
|
|
8302
|
-
}
|
|
8303
|
-
return slots;
|
|
8304
|
-
}
|
|
8305
|
-
function buildIdeaReferences(sameIdeaPosts, allBookedSlots) {
|
|
8306
|
-
const lateSlotsByPostId = /* @__PURE__ */ new Map();
|
|
8307
|
-
const localSlotsByItemId = /* @__PURE__ */ new Map();
|
|
8308
|
-
for (const slot of allBookedSlots) {
|
|
8309
|
-
if (slot.postId) {
|
|
8310
|
-
const slots = lateSlotsByPostId.get(slot.postId) ?? [];
|
|
8311
|
-
slots.push(slot);
|
|
8312
|
-
lateSlotsByPostId.set(slot.postId, slots);
|
|
8313
|
-
}
|
|
8314
|
-
if (slot.itemId) {
|
|
8315
|
-
const slots = localSlotsByItemId.get(slot.itemId) ?? [];
|
|
8316
|
-
slots.push(slot);
|
|
8317
|
-
localSlotsByItemId.set(slot.itemId, slots);
|
|
8318
|
-
}
|
|
8319
|
-
}
|
|
8320
|
-
const references = [];
|
|
8321
|
-
const seen = /* @__PURE__ */ new Set();
|
|
8322
|
-
const addReference = (platformName, scheduledFor) => {
|
|
8323
|
-
if (!scheduledFor) return;
|
|
8324
|
-
const key = `${platformName}@${scheduledFor}`;
|
|
8325
|
-
if (seen.has(key)) return;
|
|
8326
|
-
seen.add(key);
|
|
8327
|
-
references.push({ platform: platformName, scheduledFor });
|
|
8328
|
-
};
|
|
8329
|
-
for (const item of sameIdeaPosts) {
|
|
8330
|
-
addReference(item.metadata.platform, item.metadata.scheduledFor);
|
|
8331
|
-
if (item.metadata.latePostId) {
|
|
8332
|
-
for (const slot of lateSlotsByPostId.get(item.metadata.latePostId) ?? []) {
|
|
8333
|
-
addReference(slot.platform, slot.scheduledFor);
|
|
8334
|
-
}
|
|
8335
|
-
}
|
|
8336
|
-
for (const slot of localSlotsByItemId.get(item.id) ?? []) {
|
|
8337
|
-
addReference(slot.platform, slot.scheduledFor);
|
|
8395
|
+
const bookedMap = await buildBookedMap();
|
|
8396
|
+
const ctx = {
|
|
8397
|
+
timezone,
|
|
8398
|
+
bookedMap,
|
|
8399
|
+
ideaLinkedPostIds: /* @__PURE__ */ new Set(),
|
|
8400
|
+
lateClient: client,
|
|
8401
|
+
displacementEnabled: getDisplacementConfig().enabled,
|
|
8402
|
+
dryRun: true,
|
|
8403
|
+
depth: 0,
|
|
8404
|
+
ideaRefs: [],
|
|
8405
|
+
samePlatformMs: 0,
|
|
8406
|
+
crossPlatformMs: 0,
|
|
8407
|
+
platform: ""
|
|
8408
|
+
};
|
|
8409
|
+
for (const [, slot] of bookedMap) {
|
|
8410
|
+
if (slot.ideaLinked && slot.postId) {
|
|
8411
|
+
ctx.ideaLinkedPostIds.add(slot.postId);
|
|
8338
8412
|
}
|
|
8339
8413
|
}
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
|
|
8344
|
-
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
|
|
8350
|
-
|
|
8414
|
+
const result = [];
|
|
8415
|
+
const toCancel = [];
|
|
8416
|
+
let skipped = 0;
|
|
8417
|
+
tagged.sort((a, b) => {
|
|
8418
|
+
const aIdea = ctx.ideaLinkedPostIds.has(a.post._id) ? 0 : 1;
|
|
8419
|
+
const bIdea = ctx.ideaLinkedPostIds.has(b.post._id) ? 0 : 1;
|
|
8420
|
+
return aIdea - bIdea;
|
|
8421
|
+
});
|
|
8422
|
+
const nowMs = Date.now();
|
|
8423
|
+
for (const { post, platform, clipType } of tagged) {
|
|
8424
|
+
const schedulePlatform = normalizeSchedulePlatform(platform);
|
|
8425
|
+
const platformConfig = getPlatformSchedule(schedulePlatform, clipType);
|
|
8426
|
+
if (!platformConfig || platformConfig.slots.length === 0) {
|
|
8427
|
+
if (post.status !== "cancelled") {
|
|
8428
|
+
toCancel.push({ post, platform, clipType, reason: `No schedule slots for ${schedulePlatform}/${clipType}` });
|
|
8351
8429
|
}
|
|
8352
|
-
|
|
8353
|
-
|
|
8430
|
+
continue;
|
|
8431
|
+
}
|
|
8432
|
+
if (post.scheduledFor && post.status === "scheduled" && isOnValidSlot(post.scheduledFor, platformConfig, timezone)) {
|
|
8433
|
+
skipped++;
|
|
8434
|
+
continue;
|
|
8435
|
+
}
|
|
8436
|
+
if (post.scheduledFor) {
|
|
8437
|
+
const currentMs2 = new Date(post.scheduledFor).getTime();
|
|
8438
|
+
const currentBooked = bookedMap.get(currentMs2);
|
|
8439
|
+
if (currentBooked?.postId === post._id) {
|
|
8440
|
+
bookedMap.delete(currentMs2);
|
|
8354
8441
|
}
|
|
8355
8442
|
}
|
|
8356
|
-
|
|
8357
|
-
|
|
8358
|
-
|
|
8359
|
-
|
|
8360
|
-
|
|
8361
|
-
|
|
8362
|
-
|
|
8363
|
-
return {
|
|
8364
|
-
emptyWindowEndMs: defaultWindowEndMs,
|
|
8365
|
-
displacementWindowEndMs: defaultWindowEndMs
|
|
8366
|
-
};
|
|
8367
|
-
}
|
|
8368
|
-
const publishByMs = normalizeDateTime(publishBy);
|
|
8369
|
-
if (Number.isNaN(publishByMs)) {
|
|
8370
|
-
logger_default.warn(`Invalid publishBy "${sanitizeLogValue(publishBy)}" provided; scheduling normally without urgency bias`);
|
|
8371
|
-
return {};
|
|
8372
|
-
}
|
|
8373
|
-
const daysUntilPublishBy = (publishByMs - nowMs) / DAY_MS;
|
|
8374
|
-
if (daysUntilPublishBy <= 0) {
|
|
8375
|
-
logger_default.warn(`publishBy "${sanitizeLogValue(publishBy)}" has already passed; scheduling normally without urgency bias`);
|
|
8376
|
-
return {};
|
|
8377
|
-
}
|
|
8378
|
-
if (daysUntilPublishBy < 3) {
|
|
8379
|
-
logger_default.debug(`Urgent publishBy "${sanitizeLogValue(publishBy)}"; prioritizing earliest displaceable slot`);
|
|
8380
|
-
}
|
|
8381
|
-
return {
|
|
8382
|
-
emptyWindowEndMs: publishByMs,
|
|
8383
|
-
displacementWindowEndMs: daysUntilPublishBy < 7 ? Math.min(publishByMs, nowMs + 3 * DAY_MS) : publishByMs
|
|
8384
|
-
};
|
|
8385
|
-
}
|
|
8386
|
-
function findEmptySlot({
|
|
8387
|
-
platformConfig,
|
|
8388
|
-
timezone,
|
|
8389
|
-
bookedDatetimes,
|
|
8390
|
-
platform,
|
|
8391
|
-
searchFromMs,
|
|
8392
|
-
includeSearchDay = false,
|
|
8393
|
-
maxCandidateMs,
|
|
8394
|
-
passesCandidate
|
|
8395
|
-
}) {
|
|
8396
|
-
if (maxCandidateMs !== void 0 && maxCandidateMs < searchFromMs) {
|
|
8397
|
-
return null;
|
|
8398
|
-
}
|
|
8399
|
-
const baseDate = new Date(searchFromMs);
|
|
8400
|
-
const initialOffset = includeSearchDay ? 0 : 1;
|
|
8401
|
-
let maxDayOffset = MAX_LOOKAHEAD_DAYS;
|
|
8402
|
-
if (maxCandidateMs !== void 0) {
|
|
8403
|
-
maxDayOffset = Math.min(
|
|
8404
|
-
MAX_LOOKAHEAD_DAYS,
|
|
8405
|
-
Math.max(initialOffset, Math.ceil((maxCandidateMs - searchFromMs) / DAY_MS))
|
|
8406
|
-
);
|
|
8407
|
-
}
|
|
8408
|
-
let startOffset = initialOffset;
|
|
8409
|
-
while (startOffset <= maxDayOffset) {
|
|
8410
|
-
const endOffset = Math.min(startOffset + CHUNK_DAYS - 1, maxDayOffset);
|
|
8411
|
-
const candidates = [];
|
|
8412
|
-
for (let dayOffset = startOffset; dayOffset <= endOffset; dayOffset++) {
|
|
8413
|
-
const candidateDate = new Date(baseDate);
|
|
8414
|
-
candidateDate.setDate(candidateDate.getDate() + dayOffset);
|
|
8415
|
-
const dayOfWeek = getDayOfWeekInTimezone2(candidateDate, timezone);
|
|
8416
|
-
if (platformConfig.avoidDays.includes(dayOfWeek)) continue;
|
|
8417
|
-
for (const slot of platformConfig.slots) {
|
|
8418
|
-
if (!slot.days.includes(dayOfWeek)) continue;
|
|
8419
|
-
const candidate = buildSlotDatetime2(candidateDate, slot.time, timezone);
|
|
8420
|
-
const candidateMs = normalizeDateTime(candidate);
|
|
8421
|
-
if (candidateMs <= searchFromMs) continue;
|
|
8422
|
-
if (maxCandidateMs !== void 0 && candidateMs > maxCandidateMs) continue;
|
|
8423
|
-
if (bookedDatetimes.has(candidateMs)) continue;
|
|
8424
|
-
if (passesCandidate && !passesCandidate(candidateMs, platform)) continue;
|
|
8425
|
-
candidates.push(candidate);
|
|
8426
|
-
}
|
|
8427
|
-
}
|
|
8428
|
-
candidates.sort((left, right) => normalizeDateTime(left) - normalizeDateTime(right));
|
|
8429
|
-
if (candidates.length > 0) {
|
|
8430
|
-
return candidates[0];
|
|
8431
|
-
}
|
|
8432
|
-
startOffset = endOffset + 1;
|
|
8433
|
-
}
|
|
8434
|
-
return null;
|
|
8435
|
-
}
|
|
8436
|
-
async function tryDisplacement({
|
|
8437
|
-
bookedSlots,
|
|
8438
|
-
platform,
|
|
8439
|
-
platformConfig,
|
|
8440
|
-
timezone,
|
|
8441
|
-
bookedDatetimes,
|
|
8442
|
-
options,
|
|
8443
|
-
nowMs,
|
|
8444
|
-
maxCandidateMs,
|
|
8445
|
-
passesSpacing
|
|
8446
|
-
}) {
|
|
8447
|
-
const displacementConfig = getDisplacementConfig();
|
|
8448
|
-
if (!displacementConfig.enabled || !options.ideaIds?.length) {
|
|
8449
|
-
return null;
|
|
8450
|
-
}
|
|
8451
|
-
const candidateSlots = bookedSlots.filter((slot) => {
|
|
8452
|
-
const slotMs = normalizeDateTime(slot.scheduledFor);
|
|
8453
|
-
if (slotMs <= nowMs) return false;
|
|
8454
|
-
if (maxCandidateMs !== void 0 && slotMs > maxCandidateMs) return false;
|
|
8455
|
-
return true;
|
|
8456
|
-
}).sort((left, right) => normalizeDateTime(left.scheduledFor) - normalizeDateTime(right.scheduledFor));
|
|
8457
|
-
const lateClient = new LateApiClient();
|
|
8458
|
-
const publishedItemCache = /* @__PURE__ */ new Map();
|
|
8459
|
-
for (const slot of candidateSlots) {
|
|
8460
|
-
if (slot.source !== "late" || !slot.postId) continue;
|
|
8461
|
-
const candidateMs = normalizeDateTime(slot.scheduledFor);
|
|
8462
|
-
if (passesSpacing && !passesSpacing(candidateMs, platform)) continue;
|
|
8463
|
-
let publishedItem = publishedItemCache.get(slot.postId);
|
|
8464
|
-
if (publishedItem === void 0) {
|
|
8465
|
-
publishedItem = await getPublishedItemByLatePostId(slot.postId);
|
|
8466
|
-
publishedItemCache.set(slot.postId, publishedItem);
|
|
8467
|
-
}
|
|
8468
|
-
if (!publishedItem) {
|
|
8443
|
+
const isIdea = ctx.ideaLinkedPostIds.has(post._id);
|
|
8444
|
+
const label = `${schedulePlatform}/${clipType}:${post._id.slice(-6)}`;
|
|
8445
|
+
const newSlot = await schedulePost(platformConfig, nowMs, isIdea, label, ctx);
|
|
8446
|
+
if (!newSlot) {
|
|
8447
|
+
if (post.status !== "cancelled") {
|
|
8448
|
+
toCancel.push({ post, platform, clipType, reason: `No available slot for ${schedulePlatform}/${clipType}` });
|
|
8449
|
+
}
|
|
8469
8450
|
continue;
|
|
8470
8451
|
}
|
|
8471
|
-
|
|
8452
|
+
const newMs = new Date(newSlot).getTime();
|
|
8453
|
+
ctx.bookedMap.set(newMs, {
|
|
8454
|
+
scheduledFor: newSlot,
|
|
8455
|
+
source: "late",
|
|
8456
|
+
postId: post._id,
|
|
8457
|
+
platform: schedulePlatform,
|
|
8458
|
+
ideaLinked: isIdea
|
|
8459
|
+
});
|
|
8460
|
+
const currentMs = post.scheduledFor ? new Date(post.scheduledFor).getTime() : 0;
|
|
8461
|
+
if (currentMs === newMs && post.status === "scheduled") {
|
|
8462
|
+
skipped++;
|
|
8472
8463
|
continue;
|
|
8473
8464
|
}
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
platformConfig: displacedPlatformConfig,
|
|
8477
|
-
timezone,
|
|
8478
|
-
bookedDatetimes,
|
|
8465
|
+
result.push({
|
|
8466
|
+
post,
|
|
8479
8467
|
platform,
|
|
8480
|
-
|
|
8481
|
-
|
|
8468
|
+
clipType,
|
|
8469
|
+
oldScheduledFor: post.scheduledFor ?? null,
|
|
8470
|
+
newScheduledFor: newSlot
|
|
8482
8471
|
});
|
|
8483
|
-
if (!newSlot) continue;
|
|
8484
|
-
await lateClient.schedulePost(slot.postId, newSlot);
|
|
8485
|
-
logger_default.info(
|
|
8486
|
-
`Displaced post ${sanitizeLogValue(slot.postId)} from ${sanitizeLogValue(slot.scheduledFor)} to ${sanitizeLogValue(newSlot)} for idea-linked content`
|
|
8487
|
-
);
|
|
8488
|
-
return {
|
|
8489
|
-
slot: slot.scheduledFor,
|
|
8490
|
-
displaced: {
|
|
8491
|
-
postId: slot.postId,
|
|
8492
|
-
originalSlot: slot.scheduledFor,
|
|
8493
|
-
newSlot
|
|
8494
|
-
}
|
|
8495
|
-
};
|
|
8496
8472
|
}
|
|
8497
|
-
|
|
8473
|
+
result.sort((a, b) => new Date(a.newScheduledFor).getTime() - new Date(b.newScheduledFor).getTime());
|
|
8474
|
+
return { posts: result, toCancel, skipped, unmatched, totalFetched: allPosts.length };
|
|
8498
8475
|
}
|
|
8499
|
-
async function
|
|
8500
|
-
const
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8507
|
-
const
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
) : void 0;
|
|
8522
|
-
const searchWindow = isIdeaAware ? resolveSearchWindow(nowMs, options) : {};
|
|
8523
|
-
const emptySlot = findEmptySlot({
|
|
8524
|
-
platformConfig,
|
|
8525
|
-
timezone,
|
|
8526
|
-
bookedDatetimes,
|
|
8527
|
-
platform,
|
|
8528
|
-
searchFromMs: nowMs,
|
|
8529
|
-
maxCandidateMs: searchWindow.emptyWindowEndMs,
|
|
8530
|
-
passesCandidate: spacingGuard
|
|
8531
|
-
});
|
|
8532
|
-
if (emptySlot) {
|
|
8533
|
-
logger_default.debug(`Found available slot for ${sanitizeLogValue(platform)}: ${sanitizeLogValue(emptySlot)}`);
|
|
8534
|
-
return emptySlot;
|
|
8535
|
-
}
|
|
8536
|
-
if (isIdeaAware) {
|
|
8537
|
-
const displaced = await tryDisplacement({
|
|
8538
|
-
bookedSlots,
|
|
8539
|
-
platform,
|
|
8540
|
-
platformConfig,
|
|
8541
|
-
timezone,
|
|
8542
|
-
bookedDatetimes,
|
|
8543
|
-
options: { ...options, ideaIds },
|
|
8544
|
-
nowMs,
|
|
8545
|
-
maxCandidateMs: searchWindow.displacementWindowEndMs,
|
|
8546
|
-
passesSpacing: spacingGuard
|
|
8547
|
-
});
|
|
8548
|
-
if (displaced) {
|
|
8549
|
-
return displaced.slot;
|
|
8476
|
+
async function executeRealignPlan(plan, onProgress) {
|
|
8477
|
+
const client = new LateApiClient();
|
|
8478
|
+
let updated = 0;
|
|
8479
|
+
let cancelled = 0;
|
|
8480
|
+
let failed = 0;
|
|
8481
|
+
const errors = [];
|
|
8482
|
+
const totalOps = plan.toCancel.length + plan.posts.length;
|
|
8483
|
+
let completed = 0;
|
|
8484
|
+
for (const entry of plan.toCancel) {
|
|
8485
|
+
completed++;
|
|
8486
|
+
try {
|
|
8487
|
+
await client.updatePost(entry.post._id, { status: "cancelled" });
|
|
8488
|
+
cancelled++;
|
|
8489
|
+
const preview = entry.post.content.slice(0, 40).replace(/\n/g, " ");
|
|
8490
|
+
logger_default.info(`[${completed}/${totalOps}] \u{1F6AB} Cancelled ${entry.platform}/${entry.clipType}: "${preview}..."`);
|
|
8491
|
+
onProgress?.(completed, totalOps, "cancelling");
|
|
8492
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
8493
|
+
} catch (err) {
|
|
8494
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8495
|
+
errors.push({ postId: entry.post._id, error: msg });
|
|
8496
|
+
failed++;
|
|
8497
|
+
logger_default.error(`[${completed}/${totalOps}] \u274C Failed to cancel ${entry.post._id}: ${msg}`);
|
|
8550
8498
|
}
|
|
8551
8499
|
}
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8557
|
-
|
|
8558
|
-
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
|
|
8562
|
-
|
|
8563
|
-
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
}
|
|
8568
|
-
if (endDate) {
|
|
8569
|
-
const endMs = endDate.getTime();
|
|
8570
|
-
filtered = filtered.filter((slot) => normalizeDateTime(slot.scheduledFor) <= endMs);
|
|
8500
|
+
for (const entry of plan.posts) {
|
|
8501
|
+
completed++;
|
|
8502
|
+
try {
|
|
8503
|
+
await client.schedulePost(entry.post._id, entry.newScheduledFor);
|
|
8504
|
+
updated++;
|
|
8505
|
+
const preview = entry.post.content.slice(0, 40).replace(/\n/g, " ");
|
|
8506
|
+
logger_default.info(`[${completed}/${totalOps}] \u2705 ${entry.platform}/${entry.clipType}: "${preview}..." \u2192 ${entry.newScheduledFor}`);
|
|
8507
|
+
onProgress?.(completed, totalOps, "updating");
|
|
8508
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
8509
|
+
} catch (err) {
|
|
8510
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8511
|
+
errors.push({ postId: entry.post._id, error: msg });
|
|
8512
|
+
failed++;
|
|
8513
|
+
logger_default.error(`[${completed}/${totalOps}] \u274C Failed to update ${entry.post._id}: ${msg}`);
|
|
8514
|
+
}
|
|
8571
8515
|
}
|
|
8572
|
-
|
|
8573
|
-
return filtered;
|
|
8516
|
+
return { updated, cancelled, failed, errors };
|
|
8574
8517
|
}
|
|
8575
8518
|
|
|
8576
8519
|
// src/L2-clients/ffmpeg/audioExtraction.ts
|
|
@@ -10079,6 +10022,9 @@ function platformAcceptsMedia(platform, clipType) {
|
|
|
10079
10022
|
return getMediaRule(platform, clipType) !== null;
|
|
10080
10023
|
}
|
|
10081
10024
|
|
|
10025
|
+
// src/L3-services/queueBuilder/queueBuilder.ts
|
|
10026
|
+
init_postStore();
|
|
10027
|
+
|
|
10082
10028
|
// src/L1-infra/image/image.ts
|
|
10083
10029
|
import { default as default7 } from "sharp";
|
|
10084
10030
|
|
|
@@ -10577,6 +10523,10 @@ function getWhisperPrompt() {
|
|
|
10577
10523
|
const brand = getBrandConfig();
|
|
10578
10524
|
return brand.customVocabulary.join(", ");
|
|
10579
10525
|
}
|
|
10526
|
+
function getIntroOutroConfig() {
|
|
10527
|
+
const brand = getBrandConfig();
|
|
10528
|
+
return brand.introOutro ?? { enabled: false, fadeDuration: 0 };
|
|
10529
|
+
}
|
|
10580
10530
|
|
|
10581
10531
|
// src/L4-agents/IdeationAgent.ts
|
|
10582
10532
|
init_environment();
|
|
@@ -10766,7 +10716,7 @@ function buildSystemPrompt(brand, existingIdeas, seedTopics, count, ideaRepo) {
|
|
|
10766
10716
|
}
|
|
10767
10717
|
return promptSections.join("\n");
|
|
10768
10718
|
}
|
|
10769
|
-
function buildUserMessage(count, seedTopics, hasMcpServers) {
|
|
10719
|
+
function buildUserMessage(count, seedTopics, hasMcpServers, userPrompt) {
|
|
10770
10720
|
const focusText = seedTopics.length > 0 ? `Focus areas: ${seedTopics.join(", ")}` : "Focus areas: choose the strongest timely opportunities from the creator context and current trends.";
|
|
10771
10721
|
const steps = [
|
|
10772
10722
|
"1. Call get_brand_context to load the creator profile.",
|
|
@@ -10790,13 +10740,15 @@ function buildUserMessage(count, seedTopics, hasMcpServers) {
|
|
|
10790
10740
|
"5. Call finalize_ideas when done."
|
|
10791
10741
|
);
|
|
10792
10742
|
}
|
|
10793
|
-
|
|
10743
|
+
const sections = [
|
|
10794
10744
|
`Generate ${count} new content ideas.`,
|
|
10795
|
-
focusText
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10745
|
+
focusText
|
|
10746
|
+
];
|
|
10747
|
+
if (userPrompt) {
|
|
10748
|
+
sections.push("", `## User Prompt`, userPrompt);
|
|
10749
|
+
}
|
|
10750
|
+
sections.push("", "Follow this exact workflow:", ...steps);
|
|
10751
|
+
return sections.join("\n");
|
|
10800
10752
|
}
|
|
10801
10753
|
async function loadBrandContext(brandPath) {
|
|
10802
10754
|
if (!brandPath) {
|
|
@@ -11313,7 +11265,7 @@ async function generateIdeas(options = {}) {
|
|
|
11313
11265
|
});
|
|
11314
11266
|
try {
|
|
11315
11267
|
const hasMcpServers = !!(config2.EXA_API_KEY || config2.YOUTUBE_API_KEY || config2.PERPLEXITY_API_KEY);
|
|
11316
|
-
const userMessage = buildUserMessage(count, seedTopics, hasMcpServers);
|
|
11268
|
+
const userMessage = buildUserMessage(count, seedTopics, hasMcpServers, options.prompt);
|
|
11317
11269
|
await agent.run(userMessage);
|
|
11318
11270
|
const ideas = agent.getGeneratedIdeas();
|
|
11319
11271
|
if (!agent.isFinalized()) {
|
|
@@ -11443,6 +11395,290 @@ var Asset = class {
|
|
|
11443
11395
|
init_paths();
|
|
11444
11396
|
init_fileSystem();
|
|
11445
11397
|
|
|
11398
|
+
// src/L2-clients/ffmpeg/videoConcat.ts
|
|
11399
|
+
init_fileSystem();
|
|
11400
|
+
init_paths();
|
|
11401
|
+
init_configLogger();
|
|
11402
|
+
async function concatVideos(segments, output, opts = {}) {
|
|
11403
|
+
if (segments.length === 0) {
|
|
11404
|
+
throw new Error("concatVideos: no segments provided");
|
|
11405
|
+
}
|
|
11406
|
+
if (segments.length === 1) {
|
|
11407
|
+
await ensureDirectory(dirname(output));
|
|
11408
|
+
await execCommand(getFFmpegPath(), [
|
|
11409
|
+
"-y",
|
|
11410
|
+
"-i",
|
|
11411
|
+
segments[0],
|
|
11412
|
+
"-c",
|
|
11413
|
+
"copy",
|
|
11414
|
+
output
|
|
11415
|
+
], { maxBuffer: 50 * 1024 * 1024 });
|
|
11416
|
+
return output;
|
|
11417
|
+
}
|
|
11418
|
+
const fadeDuration = opts.fadeDuration ?? 0;
|
|
11419
|
+
if (fadeDuration > 0) {
|
|
11420
|
+
return concatWithXfade(segments, output, fadeDuration);
|
|
11421
|
+
}
|
|
11422
|
+
return concatWithDemuxer(segments, output);
|
|
11423
|
+
}
|
|
11424
|
+
async function concatWithDemuxer(segments, output) {
|
|
11425
|
+
await ensureDirectory(dirname(output));
|
|
11426
|
+
const listContent = segments.map((s) => `file '${s.replace(/'/g, "'\\''")}'`).join("\n");
|
|
11427
|
+
const listPath = output + ".concat-list.txt";
|
|
11428
|
+
await writeTextFile(listPath, listContent);
|
|
11429
|
+
logger_default.info(`Concat (demuxer): ${segments.length} segments \u2192 ${output}`);
|
|
11430
|
+
await execCommand(getFFmpegPath(), [
|
|
11431
|
+
"-y",
|
|
11432
|
+
"-f",
|
|
11433
|
+
"concat",
|
|
11434
|
+
"-safe",
|
|
11435
|
+
"0",
|
|
11436
|
+
"-i",
|
|
11437
|
+
listPath,
|
|
11438
|
+
"-c",
|
|
11439
|
+
"copy",
|
|
11440
|
+
"-movflags",
|
|
11441
|
+
"+faststart",
|
|
11442
|
+
output
|
|
11443
|
+
], { maxBuffer: 50 * 1024 * 1024 });
|
|
11444
|
+
return output;
|
|
11445
|
+
}
|
|
11446
|
+
async function concatWithXfade(segments, output, fadeDuration) {
|
|
11447
|
+
await ensureDirectory(dirname(output));
|
|
11448
|
+
logger_default.info(`Concat (xfade ${fadeDuration}s): ${segments.length} segments \u2192 ${output}`);
|
|
11449
|
+
const durations = await Promise.all(segments.map((s) => getVideoDuration2(s)));
|
|
11450
|
+
const inputs = segments.flatMap((s) => ["-i", s]);
|
|
11451
|
+
const filterParts = [];
|
|
11452
|
+
for (let i = 0; i < segments.length; i++) {
|
|
11453
|
+
filterParts.push(`[${i}:v]fps=30,settb=AVTB,setpts=PTS-STARTPTS[vin${i}]`);
|
|
11454
|
+
filterParts.push(`[${i}:a]aresample=async=1,asetpts=PTS-STARTPTS[ain${i}]`);
|
|
11455
|
+
}
|
|
11456
|
+
let prevLabel = "[vin0]";
|
|
11457
|
+
let prevAudioLabel = "[ain0]";
|
|
11458
|
+
let cumulativeOffset = 0;
|
|
11459
|
+
for (let i = 1; i < segments.length; i++) {
|
|
11460
|
+
const offset = cumulativeOffset + durations[i - 1] - fadeDuration;
|
|
11461
|
+
const outLabel = i < segments.length - 1 ? `[v${i}]` : "[vout]";
|
|
11462
|
+
const outAudioLabel = i < segments.length - 1 ? `[a${i}]` : "[aout]";
|
|
11463
|
+
filterParts.push(
|
|
11464
|
+
`${prevLabel}[vin${i}]xfade=transition=fade:duration=${fadeDuration}:offset=${offset.toFixed(3)}${outLabel}`
|
|
11465
|
+
);
|
|
11466
|
+
filterParts.push(
|
|
11467
|
+
`${prevAudioLabel}[ain${i}]acrossfade=d=${fadeDuration}${outAudioLabel}`
|
|
11468
|
+
);
|
|
11469
|
+
prevLabel = outLabel;
|
|
11470
|
+
prevAudioLabel = outAudioLabel;
|
|
11471
|
+
cumulativeOffset = offset;
|
|
11472
|
+
}
|
|
11473
|
+
const filterComplex = filterParts.join(";");
|
|
11474
|
+
await execCommand(getFFmpegPath(), [
|
|
11475
|
+
"-y",
|
|
11476
|
+
...inputs,
|
|
11477
|
+
"-filter_complex",
|
|
11478
|
+
filterComplex,
|
|
11479
|
+
"-map",
|
|
11480
|
+
"[vout]",
|
|
11481
|
+
"-map",
|
|
11482
|
+
"[aout]",
|
|
11483
|
+
"-c:v",
|
|
11484
|
+
"libx264",
|
|
11485
|
+
"-pix_fmt",
|
|
11486
|
+
"yuv420p",
|
|
11487
|
+
"-preset",
|
|
11488
|
+
"ultrafast",
|
|
11489
|
+
"-crf",
|
|
11490
|
+
"23",
|
|
11491
|
+
"-c:a",
|
|
11492
|
+
"aac",
|
|
11493
|
+
"-b:a",
|
|
11494
|
+
"128k",
|
|
11495
|
+
"-movflags",
|
|
11496
|
+
"+faststart",
|
|
11497
|
+
output
|
|
11498
|
+
], { maxBuffer: 50 * 1024 * 1024 });
|
|
11499
|
+
return output;
|
|
11500
|
+
}
|
|
11501
|
+
async function normalizeForConcat(videoPath, referenceVideo, output) {
|
|
11502
|
+
await ensureDirectory(dirname(output));
|
|
11503
|
+
const refProps = await getVideoProperties(referenceVideo);
|
|
11504
|
+
logger_default.info(`Normalizing ${videoPath} to match ${referenceVideo} (${refProps.width}x${refProps.height} ${refProps.fps}fps)`);
|
|
11505
|
+
await execCommand(getFFmpegPath(), [
|
|
11506
|
+
"-y",
|
|
11507
|
+
"-i",
|
|
11508
|
+
videoPath,
|
|
11509
|
+
"-vf",
|
|
11510
|
+
`scale=${refProps.width}:${refProps.height}:force_original_aspect_ratio=decrease,pad=${refProps.width}:${refProps.height}:(ow-iw)/2:(oh-ih)/2,fps=${refProps.fps}`,
|
|
11511
|
+
"-c:v",
|
|
11512
|
+
"libx264",
|
|
11513
|
+
"-pix_fmt",
|
|
11514
|
+
"yuv420p",
|
|
11515
|
+
"-preset",
|
|
11516
|
+
"ultrafast",
|
|
11517
|
+
"-crf",
|
|
11518
|
+
"23",
|
|
11519
|
+
"-c:a",
|
|
11520
|
+
"aac",
|
|
11521
|
+
"-b:a",
|
|
11522
|
+
"128k",
|
|
11523
|
+
"-ar",
|
|
11524
|
+
"48000",
|
|
11525
|
+
"-ac",
|
|
11526
|
+
"2",
|
|
11527
|
+
"-movflags",
|
|
11528
|
+
"+faststart",
|
|
11529
|
+
output
|
|
11530
|
+
], { maxBuffer: 50 * 1024 * 1024 });
|
|
11531
|
+
return output;
|
|
11532
|
+
}
|
|
11533
|
+
async function getVideoDuration2(videoPath) {
|
|
11534
|
+
const { stdout } = await execCommand(getFFprobePath(), [
|
|
11535
|
+
"-v",
|
|
11536
|
+
"error",
|
|
11537
|
+
"-show_entries",
|
|
11538
|
+
"format=duration",
|
|
11539
|
+
"-of",
|
|
11540
|
+
"csv=p=0",
|
|
11541
|
+
videoPath
|
|
11542
|
+
], { timeout: 1e4 });
|
|
11543
|
+
const duration = parseFloat(stdout.trim());
|
|
11544
|
+
if (!isFinite(duration) || duration <= 0) {
|
|
11545
|
+
throw new Error(`Failed to get duration for ${videoPath}: ${stdout.trim()}`);
|
|
11546
|
+
}
|
|
11547
|
+
return duration;
|
|
11548
|
+
}
|
|
11549
|
+
async function getVideoProperties(videoPath) {
|
|
11550
|
+
const { stdout } = await execCommand(getFFprobePath(), [
|
|
11551
|
+
"-v",
|
|
11552
|
+
"error",
|
|
11553
|
+
"-select_streams",
|
|
11554
|
+
"v:0",
|
|
11555
|
+
"-show_entries",
|
|
11556
|
+
"stream=width,height,r_frame_rate",
|
|
11557
|
+
"-of",
|
|
11558
|
+
"json",
|
|
11559
|
+
videoPath
|
|
11560
|
+
], { timeout: 1e4 });
|
|
11561
|
+
const data = JSON.parse(stdout);
|
|
11562
|
+
const stream = data.streams?.[0];
|
|
11563
|
+
if (!stream) throw new Error(`No video stream found in ${videoPath}`);
|
|
11564
|
+
const fpsRaw = stream.r_frame_rate ?? "30/1";
|
|
11565
|
+
const fpsParts = fpsRaw.split("/");
|
|
11566
|
+
const fps = fpsParts.length === 2 ? Math.round(parseInt(fpsParts[0]) / parseInt(fpsParts[1])) : 30;
|
|
11567
|
+
return {
|
|
11568
|
+
width: stream.width,
|
|
11569
|
+
height: stream.height,
|
|
11570
|
+
fps: isFinite(fps) && fps > 0 ? fps : 30
|
|
11571
|
+
};
|
|
11572
|
+
}
|
|
11573
|
+
|
|
11574
|
+
// src/L3-services/introOutro/introOutroService.ts
|
|
11575
|
+
init_fileSystem();
|
|
11576
|
+
init_paths();
|
|
11577
|
+
init_environment();
|
|
11578
|
+
init_configLogger();
|
|
11579
|
+
|
|
11580
|
+
// src/L0-pure/introOutro/introOutroResolver.ts
|
|
11581
|
+
function resolveIntroOutroToggle(config2, videoType, platform) {
|
|
11582
|
+
const globalDefault = { intro: config2.enabled, outro: config2.enabled };
|
|
11583
|
+
const videoTypeRule = config2.rules?.[videoType];
|
|
11584
|
+
const baseToggle = videoTypeRule ? { intro: videoTypeRule.intro, outro: videoTypeRule.outro } : globalDefault;
|
|
11585
|
+
if (!platform || !config2.platformOverrides?.[platform]?.[videoType]) {
|
|
11586
|
+
return baseToggle;
|
|
11587
|
+
}
|
|
11588
|
+
const platformRule = config2.platformOverrides[platform][videoType];
|
|
11589
|
+
return {
|
|
11590
|
+
intro: platformRule.intro ?? baseToggle.intro,
|
|
11591
|
+
outro: platformRule.outro ?? baseToggle.outro
|
|
11592
|
+
};
|
|
11593
|
+
}
|
|
11594
|
+
function resolveIntroPath(config2, platform, aspectRatio) {
|
|
11595
|
+
if (!config2.intro) return null;
|
|
11596
|
+
if (aspectRatio && config2.intro.aspectRatios?.[aspectRatio]) {
|
|
11597
|
+
return config2.intro.aspectRatios[aspectRatio];
|
|
11598
|
+
}
|
|
11599
|
+
if (platform && config2.intro.platforms?.[platform]) {
|
|
11600
|
+
return config2.intro.platforms[platform];
|
|
11601
|
+
}
|
|
11602
|
+
return config2.intro.default ?? null;
|
|
11603
|
+
}
|
|
11604
|
+
function resolveOutroPath(config2, platform, aspectRatio) {
|
|
11605
|
+
if (!config2.outro) return null;
|
|
11606
|
+
if (aspectRatio && config2.outro.aspectRatios?.[aspectRatio]) {
|
|
11607
|
+
return config2.outro.aspectRatios[aspectRatio];
|
|
11608
|
+
}
|
|
11609
|
+
if (platform && config2.outro.platforms?.[platform]) {
|
|
11610
|
+
return config2.outro.platforms[platform];
|
|
11611
|
+
}
|
|
11612
|
+
return config2.outro.default ?? null;
|
|
11613
|
+
}
|
|
11614
|
+
|
|
11615
|
+
// src/L3-services/introOutro/introOutroService.ts
|
|
11616
|
+
async function applyIntroOutro(videoPath, videoType, outputPath, platform, aspectRatio) {
|
|
11617
|
+
const envConfig = getConfig();
|
|
11618
|
+
if (envConfig.SKIP_INTRO_OUTRO) {
|
|
11619
|
+
logger_default.debug("Intro/outro skipped via SKIP_INTRO_OUTRO");
|
|
11620
|
+
return videoPath;
|
|
11621
|
+
}
|
|
11622
|
+
const config2 = getIntroOutroConfig();
|
|
11623
|
+
if (!config2.enabled) {
|
|
11624
|
+
logger_default.debug("Intro/outro disabled in brand config");
|
|
11625
|
+
return videoPath;
|
|
11626
|
+
}
|
|
11627
|
+
const toggle = resolveIntroOutroToggle(config2, videoType, platform);
|
|
11628
|
+
if (!toggle.intro && !toggle.outro) {
|
|
11629
|
+
logger_default.debug(`Intro/outro both disabled for ${videoType}${platform ? ` / ${platform}` : ""}`);
|
|
11630
|
+
return videoPath;
|
|
11631
|
+
}
|
|
11632
|
+
const brandPath = envConfig.BRAND_PATH;
|
|
11633
|
+
const brandDir = dirname(brandPath);
|
|
11634
|
+
const introRelative = toggle.intro ? resolveIntroPath(config2, platform, aspectRatio) : null;
|
|
11635
|
+
const outroRelative = toggle.outro ? resolveOutroPath(config2, platform, aspectRatio) : null;
|
|
11636
|
+
const introPath = introRelative ? resolve(brandDir, introRelative) : null;
|
|
11637
|
+
const outroPath = outroRelative ? resolve(brandDir, outroRelative) : null;
|
|
11638
|
+
const introExists = introPath ? await fileExists(introPath) : false;
|
|
11639
|
+
const outroExists = outroPath ? await fileExists(outroPath) : false;
|
|
11640
|
+
if (introPath && !introExists) {
|
|
11641
|
+
logger_default.warn(`Intro video not found: ${introPath} \u2014 skipping intro`);
|
|
11642
|
+
}
|
|
11643
|
+
if (outroPath && !outroExists) {
|
|
11644
|
+
logger_default.warn(`Outro video not found: ${outroPath} \u2014 skipping outro`);
|
|
11645
|
+
}
|
|
11646
|
+
const validIntro = introPath && introExists ? introPath : null;
|
|
11647
|
+
const validOutro = outroPath && outroExists ? outroPath : null;
|
|
11648
|
+
if (!validIntro && !validOutro) {
|
|
11649
|
+
logger_default.debug("No valid intro/outro files found \u2014 skipping");
|
|
11650
|
+
return videoPath;
|
|
11651
|
+
}
|
|
11652
|
+
const videoDir = dirname(outputPath);
|
|
11653
|
+
const segments = [];
|
|
11654
|
+
const normalizedIntroPath = validIntro ? join(videoDir, ".intro-normalized.mp4") : null;
|
|
11655
|
+
const normalizedOutroPath = validOutro ? join(videoDir, ".outro-normalized.mp4") : null;
|
|
11656
|
+
try {
|
|
11657
|
+
if (validIntro && normalizedIntroPath) {
|
|
11658
|
+
await normalizeForConcat(validIntro, videoPath, normalizedIntroPath);
|
|
11659
|
+
segments.push(normalizedIntroPath);
|
|
11660
|
+
}
|
|
11661
|
+
segments.push(videoPath);
|
|
11662
|
+
if (validOutro && normalizedOutroPath) {
|
|
11663
|
+
await normalizeForConcat(validOutro, videoPath, normalizedOutroPath);
|
|
11664
|
+
segments.push(normalizedOutroPath);
|
|
11665
|
+
}
|
|
11666
|
+
logger_default.info(`Applying intro/outro (${validIntro ? "intro" : ""}${validIntro && validOutro ? "+" : ""}${validOutro ? "outro" : ""}) for ${videoType}${platform ? ` / ${platform}` : ""}: ${outputPath}`);
|
|
11667
|
+
await concatVideos(segments, outputPath, { fadeDuration: config2.fadeDuration });
|
|
11668
|
+
return outputPath;
|
|
11669
|
+
} finally {
|
|
11670
|
+
if (normalizedIntroPath) await removeFile(normalizedIntroPath).catch(() => {
|
|
11671
|
+
});
|
|
11672
|
+
if (normalizedOutroPath) await removeFile(normalizedOutroPath).catch(() => {
|
|
11673
|
+
});
|
|
11674
|
+
}
|
|
11675
|
+
}
|
|
11676
|
+
|
|
11677
|
+
// src/L4-agents/videoServiceBridge.ts
|
|
11678
|
+
function applyIntroOutro2(videoPath, videoType, outputPath, platform, aspectRatio) {
|
|
11679
|
+
return applyIntroOutro(videoPath, videoType, outputPath, platform, aspectRatio);
|
|
11680
|
+
}
|
|
11681
|
+
|
|
11446
11682
|
// src/L0-pure/captions/captionGenerator.ts
|
|
11447
11683
|
function pad(n, width) {
|
|
11448
11684
|
return String(n).padStart(width, "0");
|
|
@@ -12588,6 +12824,10 @@ var ShortVideoAsset = class extends VideoAsset {
|
|
|
12588
12824
|
get videoPath() {
|
|
12589
12825
|
return join(this.videoDir, "media.mp4");
|
|
12590
12826
|
}
|
|
12827
|
+
/** Path to the short with intro/outro applied */
|
|
12828
|
+
get introOutroVideoPath() {
|
|
12829
|
+
return join(this.videoDir, "media-intro-outro.mp4");
|
|
12830
|
+
}
|
|
12591
12831
|
/** Directory containing social posts for this short */
|
|
12592
12832
|
get postsDir() {
|
|
12593
12833
|
return join(this.videoDir, "posts");
|
|
@@ -12654,6 +12894,57 @@ var ShortVideoAsset = class extends VideoAsset {
|
|
|
12654
12894
|
await extractCompositeClip2(parentVideo, this.clip.segments, this.videoPath);
|
|
12655
12895
|
return this.videoPath;
|
|
12656
12896
|
}
|
|
12897
|
+
/**
|
|
12898
|
+
* Apply intro/outro to the short clip.
|
|
12899
|
+
* Uses brand config rules for 'shorts' video type.
|
|
12900
|
+
*
|
|
12901
|
+
* @returns Path to the intro/outro'd video, or the original path if skipped
|
|
12902
|
+
*/
|
|
12903
|
+
async getIntroOutroVideo() {
|
|
12904
|
+
if (await fileExists(this.introOutroVideoPath)) {
|
|
12905
|
+
return this.introOutroVideoPath;
|
|
12906
|
+
}
|
|
12907
|
+
const candidates = [this.clip.captionedPath, this.clip.outputPath];
|
|
12908
|
+
let clipPath;
|
|
12909
|
+
for (const candidate of candidates) {
|
|
12910
|
+
if (candidate && await fileExists(candidate)) {
|
|
12911
|
+
clipPath = candidate;
|
|
12912
|
+
break;
|
|
12913
|
+
}
|
|
12914
|
+
}
|
|
12915
|
+
if (!clipPath) {
|
|
12916
|
+
clipPath = await this.getResult();
|
|
12917
|
+
}
|
|
12918
|
+
return applyIntroOutro2(clipPath, "shorts", this.introOutroVideoPath);
|
|
12919
|
+
}
|
|
12920
|
+
/**
|
|
12921
|
+
* Apply intro/outro to all platform variants of this short.
|
|
12922
|
+
* Resolves the correct intro/outro file per aspect ratio, auto-cropping
|
|
12923
|
+
* from the default file when no ratio-specific file is configured.
|
|
12924
|
+
*
|
|
12925
|
+
* @returns Map of platform to intro/outro'd variant path
|
|
12926
|
+
*/
|
|
12927
|
+
async getIntroOutroVariants() {
|
|
12928
|
+
const results = /* @__PURE__ */ new Map();
|
|
12929
|
+
if (!this.clip.variants || this.clip.variants.length === 0) return results;
|
|
12930
|
+
for (const variant of this.clip.variants) {
|
|
12931
|
+
const outputPath = join(this.videoDir, `media-${variant.platform}-intro-outro.mp4`);
|
|
12932
|
+
if (await fileExists(outputPath)) {
|
|
12933
|
+
results.set(variant.platform, outputPath);
|
|
12934
|
+
continue;
|
|
12935
|
+
}
|
|
12936
|
+
if (!await fileExists(variant.path)) continue;
|
|
12937
|
+
const result = await applyIntroOutro2(
|
|
12938
|
+
variant.path,
|
|
12939
|
+
"shorts",
|
|
12940
|
+
outputPath,
|
|
12941
|
+
variant.platform,
|
|
12942
|
+
variant.aspectRatio
|
|
12943
|
+
);
|
|
12944
|
+
results.set(variant.platform, result);
|
|
12945
|
+
}
|
|
12946
|
+
return results;
|
|
12947
|
+
}
|
|
12657
12948
|
// ── Transcript ───────────────────────────────────────────────────────────────
|
|
12658
12949
|
/**
|
|
12659
12950
|
* Get transcript filtered to this short's time range.
|
|
@@ -12723,6 +13014,10 @@ var MediumClipAsset = class extends VideoAsset {
|
|
|
12723
13014
|
get videoPath() {
|
|
12724
13015
|
return join(this.videoDir, "media.mp4");
|
|
12725
13016
|
}
|
|
13017
|
+
/** Path to the clip with intro/outro applied */
|
|
13018
|
+
get introOutroVideoPath() {
|
|
13019
|
+
return join(this.videoDir, "media-intro-outro.mp4");
|
|
13020
|
+
}
|
|
12726
13021
|
/**
|
|
12727
13022
|
* Directory containing social media posts for this clip.
|
|
12728
13023
|
*/
|
|
@@ -12770,6 +13065,29 @@ var MediumClipAsset = class extends VideoAsset {
|
|
|
12770
13065
|
await extractCompositeClip2(parentVideo, this.clip.segments, this.videoPath);
|
|
12771
13066
|
return this.videoPath;
|
|
12772
13067
|
}
|
|
13068
|
+
/**
|
|
13069
|
+
* Apply intro/outro to the medium clip.
|
|
13070
|
+
* Uses brand config rules for 'medium-clips' video type.
|
|
13071
|
+
*
|
|
13072
|
+
* @returns Path to the intro/outro'd video, or the original path if skipped
|
|
13073
|
+
*/
|
|
13074
|
+
async getIntroOutroVideo() {
|
|
13075
|
+
if (await fileExists(this.introOutroVideoPath)) {
|
|
13076
|
+
return this.introOutroVideoPath;
|
|
13077
|
+
}
|
|
13078
|
+
const candidates = [this.clip.captionedPath, this.clip.outputPath];
|
|
13079
|
+
let clipPath;
|
|
13080
|
+
for (const candidate of candidates) {
|
|
13081
|
+
if (candidate && await fileExists(candidate)) {
|
|
13082
|
+
clipPath = candidate;
|
|
13083
|
+
break;
|
|
13084
|
+
}
|
|
13085
|
+
}
|
|
13086
|
+
if (!clipPath) {
|
|
13087
|
+
clipPath = await this.getResult();
|
|
13088
|
+
}
|
|
13089
|
+
return applyIntroOutro2(clipPath, "medium-clips", this.introOutroVideoPath);
|
|
13090
|
+
}
|
|
12773
13091
|
};
|
|
12774
13092
|
|
|
12775
13093
|
// src/L5-assets/MainVideoAsset.ts
|
|
@@ -12861,7 +13179,7 @@ var SilenceRemovalAgent = class extends BaseAgent {
|
|
|
12861
13179
|
return this.removals;
|
|
12862
13180
|
}
|
|
12863
13181
|
};
|
|
12864
|
-
async function
|
|
13182
|
+
async function getVideoDuration3(videoPath) {
|
|
12865
13183
|
try {
|
|
12866
13184
|
const metadata = await ffprobe2(videoPath);
|
|
12867
13185
|
return metadata.format.duration ?? 0;
|
|
@@ -12936,7 +13254,7 @@ async function removeDeadSilence(video, transcript, model) {
|
|
|
12936
13254
|
logger_default.info("[SilenceRemoval] All removals exceeded 20% cap \u2014 skipping edit");
|
|
12937
13255
|
return noEdit;
|
|
12938
13256
|
}
|
|
12939
|
-
const videoDuration = await
|
|
13257
|
+
const videoDuration = await getVideoDuration3(video.repoPath);
|
|
12940
13258
|
const sortedRemovals = [...removals].sort((a, b) => a.start - b.start);
|
|
12941
13259
|
const keepSegments = [];
|
|
12942
13260
|
let cursor = 0;
|
|
@@ -15417,6 +15735,10 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
|
|
|
15417
15735
|
get producedVideoPath() {
|
|
15418
15736
|
return join(this.videoDir, `${this.slug}-produced.mp4`);
|
|
15419
15737
|
}
|
|
15738
|
+
/** Path to the video with intro/outro applied: videoDir/{slug}-intro-outro.mp4 */
|
|
15739
|
+
get introOutroVideoPath() {
|
|
15740
|
+
return join(this.videoDir, `${this.slug}-intro-outro.mp4`);
|
|
15741
|
+
}
|
|
15420
15742
|
/** Path to a produced video for a specific aspect ratio: videoDir/{slug}-produced-{ar}.mp4 */
|
|
15421
15743
|
producedVideoPathFor(aspectRatio) {
|
|
15422
15744
|
const arSuffix = aspectRatio.replace(":", "x");
|
|
@@ -15694,6 +16016,21 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
|
|
|
15694
16016
|
logger_default.info(`Captions burned into video: ${this.captionedVideoPath}`);
|
|
15695
16017
|
return this.captionedVideoPath;
|
|
15696
16018
|
}
|
|
16019
|
+
/**
|
|
16020
|
+
* Get the video with intro/outro applied.
|
|
16021
|
+
* Concatenates intro (if configured) + captioned video + outro (if configured).
|
|
16022
|
+
*
|
|
16023
|
+
* @param opts - Options controlling generation
|
|
16024
|
+
* @returns Path to the intro/outro video, or captioned video if skipped
|
|
16025
|
+
*/
|
|
16026
|
+
async getIntroOutroVideo(opts) {
|
|
16027
|
+
if (!opts?.force && await fileExists(this.introOutroVideoPath)) {
|
|
16028
|
+
return this.introOutroVideoPath;
|
|
16029
|
+
}
|
|
16030
|
+
const captionedPath = await this.getCaptionedVideo(opts);
|
|
16031
|
+
const result = await applyIntroOutro2(captionedPath, "main", this.introOutroVideoPath);
|
|
16032
|
+
return result;
|
|
16033
|
+
}
|
|
15697
16034
|
/**
|
|
15698
16035
|
* Get the fully produced video.
|
|
15699
16036
|
* If not already generated, runs the ProducerAgent.
|
|
@@ -16424,16 +16761,53 @@ async function processVideo(videoPath, ideas) {
|
|
|
16424
16761
|
} else {
|
|
16425
16762
|
skipStage("caption-burn" /* CaptionBurn */, "SKIP_CAPTIONS");
|
|
16426
16763
|
}
|
|
16764
|
+
let introOutroVideoPath;
|
|
16765
|
+
if (!cfg.SKIP_INTRO_OUTRO) {
|
|
16766
|
+
introOutroVideoPath = await trackStage("intro-outro" /* IntroOutro */, () => asset.getIntroOutroVideo());
|
|
16767
|
+
} else {
|
|
16768
|
+
skipStage("intro-outro" /* IntroOutro */, "SKIP_INTRO_OUTRO");
|
|
16769
|
+
}
|
|
16427
16770
|
let shorts = [];
|
|
16428
16771
|
if (!cfg.SKIP_SHORTS) {
|
|
16429
|
-
const shortAssets = await trackStage("shorts" /* Shorts */, () =>
|
|
16772
|
+
const shortAssets = await trackStage("shorts" /* Shorts */, async () => {
|
|
16773
|
+
const assets = await asset.getShorts();
|
|
16774
|
+
if (!cfg.SKIP_INTRO_OUTRO) {
|
|
16775
|
+
for (const shortAsset of assets) {
|
|
16776
|
+
const introOutroPath = await shortAsset.getIntroOutroVideo();
|
|
16777
|
+
if (introOutroPath !== shortAsset.clip.outputPath) {
|
|
16778
|
+
shortAsset.clip.outputPath = introOutroPath;
|
|
16779
|
+
shortAsset.clip.captionedPath = introOutroPath;
|
|
16780
|
+
}
|
|
16781
|
+
const variantResults = await shortAsset.getIntroOutroVariants();
|
|
16782
|
+
if (shortAsset.clip.variants) {
|
|
16783
|
+
for (const variant of shortAsset.clip.variants) {
|
|
16784
|
+
const updated = variantResults.get(variant.platform);
|
|
16785
|
+
if (updated) variant.path = updated;
|
|
16786
|
+
}
|
|
16787
|
+
}
|
|
16788
|
+
}
|
|
16789
|
+
}
|
|
16790
|
+
return assets;
|
|
16791
|
+
}) ?? [];
|
|
16430
16792
|
shorts = shortAssets.map((s) => s.clip);
|
|
16431
16793
|
} else {
|
|
16432
16794
|
skipStage("shorts" /* Shorts */, "SKIP_SHORTS");
|
|
16433
16795
|
}
|
|
16434
16796
|
let mediumClips = [];
|
|
16435
16797
|
if (!cfg.SKIP_MEDIUM_CLIPS) {
|
|
16436
|
-
const mediumAssets = await trackStage("medium-clips" /* MediumClips */, () =>
|
|
16798
|
+
const mediumAssets = await trackStage("medium-clips" /* MediumClips */, async () => {
|
|
16799
|
+
const assets = await asset.getMediumClips();
|
|
16800
|
+
if (!cfg.SKIP_INTRO_OUTRO) {
|
|
16801
|
+
for (const clipAsset of assets) {
|
|
16802
|
+
const introOutroPath = await clipAsset.getIntroOutroVideo();
|
|
16803
|
+
if (introOutroPath !== clipAsset.clip.outputPath) {
|
|
16804
|
+
clipAsset.clip.outputPath = introOutroPath;
|
|
16805
|
+
clipAsset.clip.captionedPath = introOutroPath;
|
|
16806
|
+
}
|
|
16807
|
+
}
|
|
16808
|
+
}
|
|
16809
|
+
return assets;
|
|
16810
|
+
}) ?? [];
|
|
16437
16811
|
mediumClips = mediumAssets.map((m) => m.clip);
|
|
16438
16812
|
} else {
|
|
16439
16813
|
skipStage("medium-clips" /* MediumClips */, "SKIP_MEDIUM_CLIPS");
|
|
@@ -16470,7 +16844,7 @@ async function processVideo(videoPath, ideas) {
|
|
|
16470
16844
|
skipStage("medium-clip-posts" /* MediumClipPosts */, "SKIP_SOCIAL");
|
|
16471
16845
|
}
|
|
16472
16846
|
if (!cfg.SKIP_SOCIAL_PUBLISH && socialPosts.length > 0) {
|
|
16473
|
-
await trackStage("queue-build" /* QueueBuild */, () => asset.buildQueue(shorts, mediumClips, socialPosts, captionedVideoPath));
|
|
16847
|
+
await trackStage("queue-build" /* QueueBuild */, () => asset.buildQueue(shorts, mediumClips, socialPosts, introOutroVideoPath ?? captionedVideoPath));
|
|
16474
16848
|
} else if (cfg.SKIP_SOCIAL_PUBLISH) {
|
|
16475
16849
|
skipStage("queue-build" /* QueueBuild */, "SKIP_SOCIAL_PUBLISH");
|
|
16476
16850
|
} else {
|
|
@@ -16506,6 +16880,7 @@ async function processVideo(videoPath, ideas) {
|
|
|
16506
16880
|
enhancedVideoPath,
|
|
16507
16881
|
captions: captions ? [captions.srt, captions.vtt, captions.ass] : void 0,
|
|
16508
16882
|
captionedVideoPath,
|
|
16883
|
+
introOutroVideoPath,
|
|
16509
16884
|
summary,
|
|
16510
16885
|
chapters,
|
|
16511
16886
|
shorts,
|
|
@@ -16599,7 +16974,8 @@ var defaultKeys = [
|
|
|
16599
16974
|
"brandPath",
|
|
16600
16975
|
"ideasRepo",
|
|
16601
16976
|
"lateProfileId",
|
|
16602
|
-
"geminiModel"
|
|
16977
|
+
"geminiModel",
|
|
16978
|
+
"scheduleConfig"
|
|
16603
16979
|
];
|
|
16604
16980
|
var configKeyMap = {
|
|
16605
16981
|
"openai-key": { section: "credentials", key: "openaiApiKey" },
|
|
@@ -16617,7 +16993,8 @@ var configKeyMap = {
|
|
|
16617
16993
|
"brand-path": { section: "defaults", key: "brandPath" },
|
|
16618
16994
|
"ideas-repo": { section: "defaults", key: "ideasRepo" },
|
|
16619
16995
|
"late-profile-id": { section: "defaults", key: "lateProfileId" },
|
|
16620
|
-
"gemini-model": { section: "defaults", key: "geminiModel" }
|
|
16996
|
+
"gemini-model": { section: "defaults", key: "geminiModel" },
|
|
16997
|
+
"schedule-config": { section: "defaults", key: "scheduleConfig" }
|
|
16621
16998
|
};
|
|
16622
16999
|
var providerDefaults = {
|
|
16623
17000
|
copilot: "Claude Opus 4.6",
|