vidpipe 1.3.23 → 1.3.24
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 +2216 -902
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +51 -2
- package/dist/index.js +1421 -488
- package/dist/index.js.map +1 -1
- package/dist/public/index.html +30 -14
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -206,6 +206,7 @@ var init_types2 = __esm({
|
|
|
206
206
|
PipelineStage2["MediumClipPosts"] = "medium-clip-posts";
|
|
207
207
|
PipelineStage2["Blog"] = "blog";
|
|
208
208
|
PipelineStage2["QueueBuild"] = "queue-build";
|
|
209
|
+
PipelineStage2["CloudUpload"] = "cloud-upload";
|
|
209
210
|
return PipelineStage2;
|
|
210
211
|
})(PipelineStage || {});
|
|
211
212
|
PIPELINE_STAGES = [
|
|
@@ -225,7 +226,8 @@ var init_types2 = __esm({
|
|
|
225
226
|
{ stage: "short-posts" /* ShortPosts */, name: "Short Posts", stageNumber: 14 },
|
|
226
227
|
{ stage: "medium-clip-posts" /* MediumClipPosts */, name: "Medium Clip Posts", stageNumber: 15 },
|
|
227
228
|
{ stage: "queue-build" /* QueueBuild */, name: "Queue Build", stageNumber: 16 },
|
|
228
|
-
{ stage: "blog" /* Blog */, name: "Blog", stageNumber: 17 }
|
|
229
|
+
{ stage: "blog" /* Blog */, name: "Blog", stageNumber: 17 },
|
|
230
|
+
{ stage: "cloud-upload" /* CloudUpload */, name: "Cloud Upload", stageNumber: 18 }
|
|
229
231
|
];
|
|
230
232
|
TOTAL_STAGES = PIPELINE_STAGES.length;
|
|
231
233
|
PLATFORM_CHAR_LIMITS = {
|
|
@@ -755,7 +757,22 @@ function resolveConfig(cliOptions = {}) {
|
|
|
755
757
|
process.env.GITHUB_TOKEN,
|
|
756
758
|
globalConfig.credentials.githubToken
|
|
757
759
|
),
|
|
758
|
-
MODEL_OVERRIDES: resolveModelOverrides()
|
|
760
|
+
MODEL_OVERRIDES: resolveModelOverrides(),
|
|
761
|
+
AZURE_STORAGE_ACCOUNT_NAME: resolveString(
|
|
762
|
+
cliOptions.azureStorageAccountName,
|
|
763
|
+
process.env.AZURE_STORAGE_ACCOUNT_NAME,
|
|
764
|
+
globalConfig.credentials.azureStorageAccountName
|
|
765
|
+
),
|
|
766
|
+
AZURE_STORAGE_ACCOUNT_KEY: resolveString(
|
|
767
|
+
cliOptions.azureStorageAccountKey,
|
|
768
|
+
process.env.AZURE_STORAGE_ACCOUNT_KEY,
|
|
769
|
+
globalConfig.credentials.azureStorageAccountKey
|
|
770
|
+
),
|
|
771
|
+
AZURE_CONTAINER_NAME: resolveString(
|
|
772
|
+
cliOptions.azureContainerName,
|
|
773
|
+
process.env.AZURE_CONTAINER_NAME,
|
|
774
|
+
"vidpipe"
|
|
775
|
+
)
|
|
759
776
|
};
|
|
760
777
|
}
|
|
761
778
|
function resolveModelOverrides() {
|
|
@@ -969,9 +986,7 @@ function getErrorStatus(error) {
|
|
|
969
986
|
return void 0;
|
|
970
987
|
}
|
|
971
988
|
function getErrorMessage(error) {
|
|
972
|
-
if (error instanceof Error)
|
|
973
|
-
return error.message;
|
|
974
|
-
}
|
|
989
|
+
if (error instanceof Error) return error.message;
|
|
975
990
|
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
|
|
976
991
|
return error.message ?? "Unknown GitHub API error";
|
|
977
992
|
}
|
|
@@ -980,8 +995,8 @@ function getErrorMessage(error) {
|
|
|
980
995
|
function normalizeLabels(labels) {
|
|
981
996
|
return Array.from(new Set(labels.map((label) => label.trim()).filter((label) => label.length > 0)));
|
|
982
997
|
}
|
|
983
|
-
function
|
|
984
|
-
return
|
|
998
|
+
function sleep(ms) {
|
|
999
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
985
1000
|
}
|
|
986
1001
|
function getGitHubClient() {
|
|
987
1002
|
const config2 = getConfig();
|
|
@@ -992,13 +1007,32 @@ function getGitHubClient() {
|
|
|
992
1007
|
}
|
|
993
1008
|
return clientInstance;
|
|
994
1009
|
}
|
|
995
|
-
var
|
|
1010
|
+
var CACHE_TTL_MS, MAX_CONCURRENT, THROTTLE_WARN_REMAINING, THROTTLE_SLOW_REMAINING, THROTTLE_CRITICAL_REMAINING, MAX_RETRIES, ISSUE_WITH_COMMENTS_FRAGMENT, GitHubClientError, Semaphore, GitHubClient, clientInstance, clientKey;
|
|
996
1011
|
var init_githubClient = __esm({
|
|
997
1012
|
"src/L2-clients/github/githubClient.ts"() {
|
|
998
1013
|
"use strict";
|
|
999
1014
|
init_environment();
|
|
1000
1015
|
init_configLogger();
|
|
1001
|
-
|
|
1016
|
+
CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1017
|
+
MAX_CONCURRENT = 4;
|
|
1018
|
+
THROTTLE_WARN_REMAINING = 200;
|
|
1019
|
+
THROTTLE_SLOW_REMAINING = 100;
|
|
1020
|
+
THROTTLE_CRITICAL_REMAINING = 50;
|
|
1021
|
+
MAX_RETRIES = 3;
|
|
1022
|
+
ISSUE_WITH_COMMENTS_FRAGMENT = `
|
|
1023
|
+
number
|
|
1024
|
+
title
|
|
1025
|
+
body
|
|
1026
|
+
state
|
|
1027
|
+
labels(first: 50) { nodes { name } }
|
|
1028
|
+
comments(first: 100) {
|
|
1029
|
+
nodes { databaseId body createdAt updatedAt url }
|
|
1030
|
+
pageInfo { hasNextPage endCursor }
|
|
1031
|
+
}
|
|
1032
|
+
createdAt
|
|
1033
|
+
updatedAt
|
|
1034
|
+
url
|
|
1035
|
+
`;
|
|
1002
1036
|
GitHubClientError = class extends Error {
|
|
1003
1037
|
constructor(message, status) {
|
|
1004
1038
|
super(message);
|
|
@@ -1006,10 +1040,40 @@ var init_githubClient = __esm({
|
|
|
1006
1040
|
this.name = "GitHubClientError";
|
|
1007
1041
|
}
|
|
1008
1042
|
};
|
|
1043
|
+
Semaphore = class {
|
|
1044
|
+
constructor(limit) {
|
|
1045
|
+
this.limit = limit;
|
|
1046
|
+
}
|
|
1047
|
+
queue = [];
|
|
1048
|
+
running = 0;
|
|
1049
|
+
async acquire() {
|
|
1050
|
+
if (this.running < this.limit) {
|
|
1051
|
+
this.running++;
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
return new Promise((resolve3) => {
|
|
1055
|
+
this.queue.push(() => {
|
|
1056
|
+
this.running++;
|
|
1057
|
+
resolve3();
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
release() {
|
|
1062
|
+
this.running--;
|
|
1063
|
+
const next = this.queue.shift();
|
|
1064
|
+
if (next) next();
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1009
1067
|
GitHubClient = class {
|
|
1010
1068
|
octokit;
|
|
1011
1069
|
owner;
|
|
1012
1070
|
repo;
|
|
1071
|
+
// Rate limiting
|
|
1072
|
+
rateLimitRemaining = 5e3;
|
|
1073
|
+
rateLimitReset = 0;
|
|
1074
|
+
semaphore = new Semaphore(MAX_CONCURRENT);
|
|
1075
|
+
// Response cache — keyed by "type:identifier"
|
|
1076
|
+
cache = /* @__PURE__ */ new Map();
|
|
1013
1077
|
constructor(token, repoFullName) {
|
|
1014
1078
|
const config2 = getConfig();
|
|
1015
1079
|
const authToken = token || config2.GITHUB_TOKEN;
|
|
@@ -1025,87 +1089,184 @@ var init_githubClient = __esm({
|
|
|
1025
1089
|
this.repo = repo;
|
|
1026
1090
|
this.octokit = new Octokit({ auth: authToken });
|
|
1027
1091
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
body: input.body,
|
|
1036
|
-
labels: input.labels ? normalizeLabels(input.labels) : void 0
|
|
1037
|
-
});
|
|
1038
|
-
const issue = this.mapIssue(response.data);
|
|
1039
|
-
logger_default.info(`[GitHubClient] Created issue #${issue.number}: ${input.title}`);
|
|
1040
|
-
return issue;
|
|
1041
|
-
} catch (error) {
|
|
1042
|
-
this.logError("create issue", error);
|
|
1043
|
-
throw new GitHubClientError(`Failed to create GitHub issue: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
1092
|
+
// ── Cache helpers ────────────────────────────────────────────────────
|
|
1093
|
+
getCached(key) {
|
|
1094
|
+
const entry = this.cache.get(key);
|
|
1095
|
+
if (!entry) return void 0;
|
|
1096
|
+
if (Date.now() - entry.cachedAt > CACHE_TTL_MS) {
|
|
1097
|
+
this.cache.delete(key);
|
|
1098
|
+
return void 0;
|
|
1044
1099
|
}
|
|
1100
|
+
return entry.data;
|
|
1045
1101
|
}
|
|
1046
|
-
|
|
1047
|
-
|
|
1102
|
+
setCache(key, data) {
|
|
1103
|
+
this.cache.set(key, { data, cachedAt: Date.now() });
|
|
1104
|
+
}
|
|
1105
|
+
invalidateIssue(issueNumber) {
|
|
1106
|
+
this.cache.delete(`issue:${issueNumber}`);
|
|
1107
|
+
this.cache.delete(`comments:${issueNumber}`);
|
|
1108
|
+
for (const key of this.cache.keys()) {
|
|
1109
|
+
if (key.startsWith("issues:") || key.startsWith("search:")) {
|
|
1110
|
+
this.cache.delete(key);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
/** Clear all cached data. Useful after bulk writes. */
|
|
1115
|
+
clearCache() {
|
|
1116
|
+
this.cache.clear();
|
|
1117
|
+
}
|
|
1118
|
+
// ── Throttle / rate limit ────────────────────────────────────────────
|
|
1119
|
+
async throttle() {
|
|
1120
|
+
if (this.rateLimitRemaining < THROTTLE_CRITICAL_REMAINING) {
|
|
1121
|
+
const waitMs = Math.max(0, this.rateLimitReset * 1e3 - Date.now()) + 1e3;
|
|
1122
|
+
logger_default.warn(`[GitHubClient] Rate limit critical (${this.rateLimitRemaining} remaining) \u2014 waiting ${Math.round(waitMs / 1e3)}s`);
|
|
1123
|
+
await sleep(Math.min(waitMs, 6e4));
|
|
1124
|
+
} else if (this.rateLimitRemaining < THROTTLE_SLOW_REMAINING) {
|
|
1125
|
+
await sleep(500);
|
|
1126
|
+
} else if (this.rateLimitRemaining < THROTTLE_WARN_REMAINING) {
|
|
1127
|
+
await sleep(100);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
updateRateLimit(headers) {
|
|
1131
|
+
const remaining = headers["x-ratelimit-remaining"];
|
|
1132
|
+
const reset = headers["x-ratelimit-reset"];
|
|
1133
|
+
if (remaining !== void 0) this.rateLimitRemaining = parseInt(remaining, 10) || 0;
|
|
1134
|
+
if (reset !== void 0) this.rateLimitReset = parseInt(reset, 10) || 0;
|
|
1135
|
+
}
|
|
1136
|
+
// ── GraphQL transport ────────────────────────────────────────────────
|
|
1137
|
+
async graphql(query, variables = {}) {
|
|
1138
|
+
await this.semaphore.acquire();
|
|
1048
1139
|
try {
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1140
|
+
await this.throttle();
|
|
1141
|
+
let lastError;
|
|
1142
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
1143
|
+
try {
|
|
1144
|
+
const response = await this.octokit.request("POST /graphql", { query, variables });
|
|
1145
|
+
this.updateRateLimit(response.headers);
|
|
1146
|
+
const body = response.data;
|
|
1147
|
+
if (body.errors?.length) {
|
|
1148
|
+
const rateLimited = body.errors.some((e) => e.type === "RATE_LIMITED");
|
|
1149
|
+
if (rateLimited && attempt < MAX_RETRIES - 1) {
|
|
1150
|
+
const waitMs = Math.max(0, this.rateLimitReset * 1e3 - Date.now()) + 1e3;
|
|
1151
|
+
logger_default.warn(`[GitHubClient] GraphQL rate limited \u2014 retrying in ${Math.round(waitMs / 1e3)}s`);
|
|
1152
|
+
await sleep(Math.min(waitMs, 6e4));
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
throw new GitHubClientError(`GraphQL error: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
1156
|
+
}
|
|
1157
|
+
if (!body.data) throw new GitHubClientError("GraphQL returned no data");
|
|
1158
|
+
return body.data;
|
|
1159
|
+
} catch (error) {
|
|
1160
|
+
lastError = error;
|
|
1161
|
+
if (error instanceof GitHubClientError) throw error;
|
|
1162
|
+
const status = getErrorStatus(error);
|
|
1163
|
+
if (status === 403 && attempt < MAX_RETRIES - 1) {
|
|
1164
|
+
const backoff = Math.pow(2, attempt) * 1e3;
|
|
1165
|
+
logger_default.warn(`[GitHubClient] 403 \u2014 retrying in ${backoff}ms (attempt ${attempt + 1}/${MAX_RETRIES})`);
|
|
1166
|
+
await sleep(backoff);
|
|
1167
|
+
continue;
|
|
1168
|
+
}
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
throw lastError;
|
|
1173
|
+
} finally {
|
|
1174
|
+
this.semaphore.release();
|
|
1065
1175
|
}
|
|
1066
1176
|
}
|
|
1177
|
+
// ── GraphQL mappers ──────────────────────────────────────────────────
|
|
1178
|
+
mapGqlIssue(node) {
|
|
1179
|
+
return {
|
|
1180
|
+
number: node.number,
|
|
1181
|
+
title: node.title,
|
|
1182
|
+
body: node.body ?? "",
|
|
1183
|
+
state: node.state === "OPEN" ? "open" : "closed",
|
|
1184
|
+
labels: node.labels.nodes.map((l) => l.name).filter(Boolean),
|
|
1185
|
+
created_at: node.createdAt,
|
|
1186
|
+
updated_at: node.updatedAt,
|
|
1187
|
+
html_url: node.url
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
mapGqlComment(node) {
|
|
1191
|
+
return {
|
|
1192
|
+
id: node.databaseId,
|
|
1193
|
+
body: node.body ?? "",
|
|
1194
|
+
created_at: node.createdAt,
|
|
1195
|
+
updated_at: node.updatedAt,
|
|
1196
|
+
html_url: node.url
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
mapGqlComments(node) {
|
|
1200
|
+
return node.comments.nodes.map((c) => this.mapGqlComment(c));
|
|
1201
|
+
}
|
|
1202
|
+
// ── Read operations (GraphQL) ────────────────────────────────────────
|
|
1067
1203
|
async getIssue(issueNumber) {
|
|
1204
|
+
const cached = this.getCached(`issue:${issueNumber}`);
|
|
1205
|
+
if (cached) return cached;
|
|
1068
1206
|
logger_default.debug(`[GitHubClient] Fetching issue #${issueNumber} from ${this.owner}/${this.repo}`);
|
|
1069
1207
|
try {
|
|
1070
|
-
const
|
|
1071
|
-
owner:
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1208
|
+
const data = await this.graphql(
|
|
1209
|
+
`query($owner: String!, $repo: String!, $number: Int!) {
|
|
1210
|
+
repository(owner: $owner, name: $repo) {
|
|
1211
|
+
issue(number: $number) { ${ISSUE_WITH_COMMENTS_FRAGMENT} }
|
|
1212
|
+
}
|
|
1213
|
+
}`,
|
|
1214
|
+
{ owner: this.owner, repo: this.repo, number: issueNumber }
|
|
1215
|
+
);
|
|
1216
|
+
if (!data.repository.issue) {
|
|
1217
|
+
throw new GitHubClientError(`Issue #${issueNumber} not found`, 404);
|
|
1218
|
+
}
|
|
1219
|
+
const issue = this.mapGqlIssue(data.repository.issue);
|
|
1220
|
+
const comments = this.mapGqlComments(data.repository.issue);
|
|
1221
|
+
this.setCache(`issue:${issueNumber}`, issue);
|
|
1222
|
+
this.setCache(`comments:${issueNumber}`, comments);
|
|
1223
|
+
return issue;
|
|
1076
1224
|
} catch (error) {
|
|
1077
1225
|
this.logError(`get issue #${issueNumber}`, error);
|
|
1078
|
-
throw new GitHubClientError(
|
|
1079
|
-
`Failed to fetch GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
|
|
1080
|
-
getErrorStatus(error)
|
|
1081
|
-
);
|
|
1226
|
+
throw error instanceof GitHubClientError ? error : new GitHubClientError(`Failed to fetch GitHub issue #${issueNumber}: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
1082
1227
|
}
|
|
1083
1228
|
}
|
|
1084
1229
|
async listIssues(options = {}) {
|
|
1230
|
+
const labels = options.labels && options.labels.length > 0 ? normalizeLabels(options.labels) : [];
|
|
1231
|
+
const stateFilter = options.state ?? "all";
|
|
1232
|
+
const cacheKey = `issues:${labels.sort().join(",")}:${stateFilter}`;
|
|
1233
|
+
const cached = this.getCached(cacheKey);
|
|
1234
|
+
if (cached) return options.maxResults ? cached.slice(0, options.maxResults) : cached;
|
|
1085
1235
|
logger_default.debug(`[GitHubClient] Listing issues for ${this.owner}/${this.repo}`);
|
|
1086
|
-
const issues = [];
|
|
1087
|
-
let page;
|
|
1088
1236
|
const maxResults = options.maxResults ?? Number.POSITIVE_INFINITY;
|
|
1237
|
+
const gqlStates = stateFilter === "all" ? "[OPEN, CLOSED]" : stateFilter === "open" ? "[OPEN]" : "[CLOSED]";
|
|
1238
|
+
const labelsArg = labels.length > 0 ? `, labels: ${JSON.stringify(labels)}` : "";
|
|
1089
1239
|
try {
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
issues.push(...pageItems);
|
|
1103
|
-
if (pageItems.length < DEFAULT_PER_PAGE) {
|
|
1104
|
-
break;
|
|
1240
|
+
const allIssues = [];
|
|
1241
|
+
let cursor = null;
|
|
1242
|
+
let hasNext = true;
|
|
1243
|
+
while (hasNext && allIssues.length < maxResults) {
|
|
1244
|
+
const afterArg = cursor ? `, after: "${cursor}"` : "";
|
|
1245
|
+
const data = await this.graphql(
|
|
1246
|
+
`query($owner: String!, $repo: String!) {
|
|
1247
|
+
repository(owner: $owner, name: $repo) {
|
|
1248
|
+
issues(first: 100, states: ${gqlStates}${labelsArg}${afterArg}, orderBy: {field: UPDATED_AT, direction: DESC}) {
|
|
1249
|
+
nodes { ${ISSUE_WITH_COMMENTS_FRAGMENT} }
|
|
1250
|
+
pageInfo { hasNextPage endCursor }
|
|
1251
|
+
}
|
|
1105
1252
|
}
|
|
1106
|
-
|
|
1253
|
+
}`,
|
|
1254
|
+
{ owner: this.owner, repo: this.repo }
|
|
1255
|
+
);
|
|
1256
|
+
const pageData = data.repository.issues;
|
|
1257
|
+
for (const node of pageData.nodes) {
|
|
1258
|
+
const issue = this.mapGqlIssue(node);
|
|
1259
|
+
const comments = this.mapGqlComments(node);
|
|
1260
|
+
allIssues.push(issue);
|
|
1261
|
+
this.setCache(`issue:${issue.number}`, issue);
|
|
1262
|
+
this.setCache(`comments:${issue.number}`, comments);
|
|
1263
|
+
}
|
|
1264
|
+
hasNext = pageData.pageInfo.hasNextPage;
|
|
1265
|
+
cursor = pageData.pageInfo.endCursor;
|
|
1107
1266
|
}
|
|
1108
|
-
|
|
1267
|
+
const result = allIssues.slice(0, maxResults);
|
|
1268
|
+
this.setCache(cacheKey, result);
|
|
1269
|
+
return result;
|
|
1109
1270
|
} catch (error) {
|
|
1110
1271
|
this.logError("list issues", error);
|
|
1111
1272
|
throw new GitHubClientError(`Failed to list GitHub issues: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
@@ -1113,22 +1274,89 @@ var init_githubClient = __esm({
|
|
|
1113
1274
|
}
|
|
1114
1275
|
async searchIssues(query, options = {}) {
|
|
1115
1276
|
const searchQuery = `repo:${this.owner}/${this.repo} is:issue ${query}`.trim();
|
|
1277
|
+
const cacheKey = `search:${searchQuery}`;
|
|
1278
|
+
const cached = this.getCached(cacheKey);
|
|
1279
|
+
if (cached) return options.maxResults ? cached.slice(0, options.maxResults) : cached;
|
|
1116
1280
|
logger_default.debug(`[GitHubClient] Searching issues in ${this.owner}/${this.repo}: ${query}`);
|
|
1117
1281
|
try {
|
|
1118
|
-
const
|
|
1119
|
-
q:
|
|
1120
|
-
|
|
1282
|
+
const data = await this.graphql(
|
|
1283
|
+
`query($q: String!) {
|
|
1284
|
+
search(query: $q, type: ISSUE, first: 100) {
|
|
1285
|
+
nodes {
|
|
1286
|
+
... on Issue { __typename ${ISSUE_WITH_COMMENTS_FRAGMENT} }
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}`,
|
|
1290
|
+
{ q: searchQuery }
|
|
1291
|
+
);
|
|
1292
|
+
const issues = data.search.nodes.filter((n) => n.__typename === "Issue").map((node) => {
|
|
1293
|
+
const issue = this.mapGqlIssue(node);
|
|
1294
|
+
const comments = this.mapGqlComments(node);
|
|
1295
|
+
this.setCache(`issue:${issue.number}`, issue);
|
|
1296
|
+
this.setCache(`comments:${issue.number}`, comments);
|
|
1297
|
+
return issue;
|
|
1121
1298
|
});
|
|
1122
|
-
|
|
1299
|
+
const result = issues.slice(0, options.maxResults ?? Number.POSITIVE_INFINITY);
|
|
1300
|
+
this.setCache(cacheKey, result);
|
|
1301
|
+
return result;
|
|
1123
1302
|
} catch (error) {
|
|
1124
1303
|
this.logError("search issues", error);
|
|
1125
1304
|
throw new GitHubClientError(`Failed to search GitHub issues: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
1126
1305
|
}
|
|
1127
1306
|
}
|
|
1128
|
-
async
|
|
1129
|
-
|
|
1130
|
-
|
|
1307
|
+
async listComments(issueNumber) {
|
|
1308
|
+
const cached = this.getCached(`comments:${issueNumber}`);
|
|
1309
|
+
if (cached) return cached;
|
|
1310
|
+
logger_default.debug(`[GitHubClient] Listing comments for issue #${issueNumber} in ${this.owner}/${this.repo}`);
|
|
1311
|
+
try {
|
|
1312
|
+
await this.getIssue(issueNumber);
|
|
1313
|
+
return this.getCached(`comments:${issueNumber}`) ?? [];
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
this.logError(`list comments for issue #${issueNumber}`, error);
|
|
1316
|
+
throw error instanceof GitHubClientError ? error : new GitHubClientError(`Failed to list comments for GitHub issue #${issueNumber}: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
// ── Write operations (REST — infrequent, keep on separate quota) ─────
|
|
1320
|
+
async createIssue(input) {
|
|
1321
|
+
logger_default.debug(`[GitHubClient] Creating issue in ${this.owner}/${this.repo}: ${input.title}`);
|
|
1322
|
+
try {
|
|
1323
|
+
const response = await this.octokit.rest.issues.create({
|
|
1324
|
+
owner: this.owner,
|
|
1325
|
+
repo: this.repo,
|
|
1326
|
+
title: input.title,
|
|
1327
|
+
body: input.body,
|
|
1328
|
+
labels: input.labels ? normalizeLabels(input.labels) : void 0
|
|
1329
|
+
});
|
|
1330
|
+
const issue = this.mapRestIssue(response.data);
|
|
1331
|
+
logger_default.info(`[GitHubClient] Created issue #${issue.number}: ${input.title}`);
|
|
1332
|
+
return issue;
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
this.logError("create issue", error);
|
|
1335
|
+
throw new GitHubClientError(`Failed to create GitHub issue: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
async updateIssue(issueNumber, input) {
|
|
1339
|
+
logger_default.debug(`[GitHubClient] Updating issue #${issueNumber} in ${this.owner}/${this.repo}`);
|
|
1340
|
+
try {
|
|
1341
|
+
const response = await this.octokit.rest.issues.update({
|
|
1342
|
+
owner: this.owner,
|
|
1343
|
+
repo: this.repo,
|
|
1344
|
+
issue_number: issueNumber,
|
|
1345
|
+
title: input.title,
|
|
1346
|
+
body: input.body,
|
|
1347
|
+
state: input.state,
|
|
1348
|
+
labels: input.labels ? normalizeLabels(input.labels) : void 0
|
|
1349
|
+
});
|
|
1350
|
+
const issue = this.mapRestIssue(response.data);
|
|
1351
|
+
this.invalidateIssue(issueNumber);
|
|
1352
|
+
return issue;
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
this.logError(`update issue #${issueNumber}`, error);
|
|
1355
|
+
throw new GitHubClientError(`Failed to update GitHub issue #${issueNumber}: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
1131
1356
|
}
|
|
1357
|
+
}
|
|
1358
|
+
async addLabels(issueNumber, labels) {
|
|
1359
|
+
if (labels.length === 0) return;
|
|
1132
1360
|
logger_default.debug(`[GitHubClient] Adding labels to issue #${issueNumber} in ${this.owner}/${this.repo}`);
|
|
1133
1361
|
try {
|
|
1134
1362
|
await this.octokit.rest.issues.addLabels({
|
|
@@ -1137,12 +1365,10 @@ var init_githubClient = __esm({
|
|
|
1137
1365
|
issue_number: issueNumber,
|
|
1138
1366
|
labels
|
|
1139
1367
|
});
|
|
1368
|
+
this.invalidateIssue(issueNumber);
|
|
1140
1369
|
} catch (error) {
|
|
1141
1370
|
this.logError(`add labels to issue #${issueNumber}`, error);
|
|
1142
|
-
throw new GitHubClientError(
|
|
1143
|
-
`Failed to add labels to GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
|
|
1144
|
-
getErrorStatus(error)
|
|
1145
|
-
);
|
|
1371
|
+
throw new GitHubClientError(`Failed to add labels to GitHub issue #${issueNumber}: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
1146
1372
|
}
|
|
1147
1373
|
}
|
|
1148
1374
|
async removeLabel(issueNumber, label) {
|
|
@@ -1154,15 +1380,11 @@ var init_githubClient = __esm({
|
|
|
1154
1380
|
issue_number: issueNumber,
|
|
1155
1381
|
name: label
|
|
1156
1382
|
});
|
|
1383
|
+
this.invalidateIssue(issueNumber);
|
|
1157
1384
|
} catch (error) {
|
|
1158
|
-
if (getErrorStatus(error) === 404)
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1385
|
+
if (getErrorStatus(error) === 404) return;
|
|
1161
1386
|
this.logError(`remove label from issue #${issueNumber}`, error);
|
|
1162
|
-
throw new GitHubClientError(
|
|
1163
|
-
`Failed to remove label from GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
|
|
1164
|
-
getErrorStatus(error)
|
|
1165
|
-
);
|
|
1387
|
+
throw new GitHubClientError(`Failed to remove label from GitHub issue #${issueNumber}: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
1166
1388
|
}
|
|
1167
1389
|
}
|
|
1168
1390
|
async setLabels(issueNumber, labels) {
|
|
@@ -1174,12 +1396,10 @@ var init_githubClient = __esm({
|
|
|
1174
1396
|
issue_number: issueNumber,
|
|
1175
1397
|
labels
|
|
1176
1398
|
});
|
|
1399
|
+
this.invalidateIssue(issueNumber);
|
|
1177
1400
|
} catch (error) {
|
|
1178
1401
|
this.logError(`set labels on issue #${issueNumber}`, error);
|
|
1179
|
-
throw new GitHubClientError(
|
|
1180
|
-
`Failed to set labels on GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
|
|
1181
|
-
getErrorStatus(error)
|
|
1182
|
-
);
|
|
1402
|
+
throw new GitHubClientError(`Failed to set labels on GitHub issue #${issueNumber}: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
1183
1403
|
}
|
|
1184
1404
|
}
|
|
1185
1405
|
async addComment(issueNumber, body) {
|
|
@@ -1191,52 +1411,34 @@ var init_githubClient = __esm({
|
|
|
1191
1411
|
issue_number: issueNumber,
|
|
1192
1412
|
body
|
|
1193
1413
|
});
|
|
1194
|
-
|
|
1414
|
+
this.invalidateIssue(issueNumber);
|
|
1415
|
+
return this.mapRestComment(response.data);
|
|
1195
1416
|
} catch (error) {
|
|
1196
1417
|
this.logError(`add comment to issue #${issueNumber}`, error);
|
|
1197
|
-
throw new GitHubClientError(
|
|
1198
|
-
`Failed to add comment to GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
|
|
1199
|
-
getErrorStatus(error)
|
|
1200
|
-
);
|
|
1418
|
+
throw new GitHubClientError(`Failed to add comment to GitHub issue #${issueNumber}: ${getErrorMessage(error)}`, getErrorStatus(error));
|
|
1201
1419
|
}
|
|
1202
1420
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const comments = await this.octokit.paginate(this.octokit.rest.issues.listComments, {
|
|
1207
|
-
owner: this.owner,
|
|
1208
|
-
repo: this.repo,
|
|
1209
|
-
issue_number: issueNumber,
|
|
1210
|
-
per_page: DEFAULT_PER_PAGE
|
|
1211
|
-
});
|
|
1212
|
-
return comments.map((comment) => this.mapComment(comment));
|
|
1213
|
-
} catch (error) {
|
|
1214
|
-
this.logError(`list comments for issue #${issueNumber}`, error);
|
|
1215
|
-
throw new GitHubClientError(
|
|
1216
|
-
`Failed to list comments for GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
|
|
1217
|
-
getErrorStatus(error)
|
|
1218
|
-
);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
mapIssue(issue) {
|
|
1421
|
+
// ── REST mappers (for write operations) ──────────────────────────────
|
|
1422
|
+
mapRestIssue(data) {
|
|
1423
|
+
const labels = Array.isArray(data.labels) ? data.labels : [];
|
|
1222
1424
|
return {
|
|
1223
|
-
number:
|
|
1224
|
-
title:
|
|
1225
|
-
body:
|
|
1226
|
-
state:
|
|
1227
|
-
labels:
|
|
1228
|
-
created_at:
|
|
1229
|
-
updated_at:
|
|
1230
|
-
html_url:
|
|
1425
|
+
number: data.number,
|
|
1426
|
+
title: data.title,
|
|
1427
|
+
body: data.body ?? "",
|
|
1428
|
+
state: data.state,
|
|
1429
|
+
labels: labels.map((l) => typeof l === "string" ? l : l?.name ?? "").map((l) => l.trim()).filter((l) => l.length > 0),
|
|
1430
|
+
created_at: data.created_at,
|
|
1431
|
+
updated_at: data.updated_at,
|
|
1432
|
+
html_url: data.html_url
|
|
1231
1433
|
};
|
|
1232
1434
|
}
|
|
1233
|
-
|
|
1435
|
+
mapRestComment(data) {
|
|
1234
1436
|
return {
|
|
1235
|
-
id:
|
|
1236
|
-
body:
|
|
1237
|
-
created_at:
|
|
1238
|
-
updated_at:
|
|
1239
|
-
html_url:
|
|
1437
|
+
id: data.id,
|
|
1438
|
+
body: data.body ?? "",
|
|
1439
|
+
created_at: data.created_at,
|
|
1440
|
+
updated_at: data.updated_at,
|
|
1441
|
+
html_url: data.html_url
|
|
1240
1442
|
};
|
|
1241
1443
|
}
|
|
1242
1444
|
logError(action, error) {
|
|
@@ -2798,7 +3000,7 @@ var require_semaphore = __commonJS({
|
|
|
2798
3000
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
2799
3001
|
exports.Semaphore = void 0;
|
|
2800
3002
|
var ral_1 = require_ral();
|
|
2801
|
-
var
|
|
3003
|
+
var Semaphore2 = class {
|
|
2802
3004
|
constructor(capacity = 1) {
|
|
2803
3005
|
if (capacity <= 0) {
|
|
2804
3006
|
throw new Error("Capacity must be greater than 0");
|
|
@@ -2855,7 +3057,7 @@ var require_semaphore = __commonJS({
|
|
|
2855
3057
|
}
|
|
2856
3058
|
}
|
|
2857
3059
|
};
|
|
2858
|
-
exports.Semaphore =
|
|
3060
|
+
exports.Semaphore = Semaphore2;
|
|
2859
3061
|
}
|
|
2860
3062
|
});
|
|
2861
3063
|
|
|
@@ -7949,6 +8151,14 @@ var init_providerFactory = __esm({
|
|
|
7949
8151
|
}
|
|
7950
8152
|
});
|
|
7951
8153
|
|
|
8154
|
+
// src/L1-infra/http/network.ts
|
|
8155
|
+
import { Readable } from "stream";
|
|
8156
|
+
var init_network = __esm({
|
|
8157
|
+
"src/L1-infra/http/network.ts"() {
|
|
8158
|
+
"use strict";
|
|
8159
|
+
}
|
|
8160
|
+
});
|
|
8161
|
+
|
|
7952
8162
|
// src/L1-infra/http/httpClient.ts
|
|
7953
8163
|
async function fetchRaw(url, options) {
|
|
7954
8164
|
return fetch(url, options);
|
|
@@ -7959,6 +8169,330 @@ var init_httpClient = __esm({
|
|
|
7959
8169
|
}
|
|
7960
8170
|
});
|
|
7961
8171
|
|
|
8172
|
+
// src/L2-clients/late/lateApi.ts
|
|
8173
|
+
var LateApiClient;
|
|
8174
|
+
var init_lateApi = __esm({
|
|
8175
|
+
"src/L2-clients/late/lateApi.ts"() {
|
|
8176
|
+
"use strict";
|
|
8177
|
+
init_environment();
|
|
8178
|
+
init_configLogger();
|
|
8179
|
+
init_fileSystem();
|
|
8180
|
+
init_network();
|
|
8181
|
+
init_paths();
|
|
8182
|
+
init_httpClient();
|
|
8183
|
+
LateApiClient = class {
|
|
8184
|
+
baseUrl = "https://getlate.dev/api/v1";
|
|
8185
|
+
apiKey;
|
|
8186
|
+
constructor(apiKey) {
|
|
8187
|
+
this.apiKey = apiKey ?? getConfig().LATE_API_KEY;
|
|
8188
|
+
if (!this.apiKey) {
|
|
8189
|
+
throw new Error("LATE_API_KEY is required \u2014 set it in environment or pass to constructor");
|
|
8190
|
+
}
|
|
8191
|
+
}
|
|
8192
|
+
// ── Private request helper ───────────────────────────────────────────
|
|
8193
|
+
async request(endpoint, options = {}, retries = 3) {
|
|
8194
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
8195
|
+
const headers = {
|
|
8196
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
8197
|
+
...options.headers
|
|
8198
|
+
};
|
|
8199
|
+
if (!(options.body instanceof FormData)) {
|
|
8200
|
+
headers["Content-Type"] = "application/json";
|
|
8201
|
+
}
|
|
8202
|
+
logger_default.debug(`Late API ${options.method ?? "GET"} ${endpoint}`);
|
|
8203
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
8204
|
+
const response = await fetchRaw(url, { ...options, headers });
|
|
8205
|
+
if (response.ok) {
|
|
8206
|
+
if (response.status === 204) return void 0;
|
|
8207
|
+
return await response.json();
|
|
8208
|
+
}
|
|
8209
|
+
if (response.status === 429 && attempt < retries) {
|
|
8210
|
+
const retryAfter = Number(response.headers.get("Retry-After")) || 2;
|
|
8211
|
+
logger_default.warn(`Late API rate limited, retrying in ${retryAfter}s (attempt ${attempt}/${retries})`);
|
|
8212
|
+
await new Promise((r) => setTimeout(r, retryAfter * 1e3));
|
|
8213
|
+
continue;
|
|
8214
|
+
}
|
|
8215
|
+
if (response.status === 401) {
|
|
8216
|
+
throw new Error(
|
|
8217
|
+
"Late API authentication failed (401). Check that LATE_API_KEY is valid."
|
|
8218
|
+
);
|
|
8219
|
+
}
|
|
8220
|
+
const body = await response.text().catch(() => "<no body>");
|
|
8221
|
+
throw new Error(
|
|
8222
|
+
`Late API error ${response.status} ${options.method ?? "GET"} ${endpoint}: ${body}`
|
|
8223
|
+
);
|
|
8224
|
+
}
|
|
8225
|
+
throw new Error(`Late API request failed after ${retries} retries`);
|
|
8226
|
+
}
|
|
8227
|
+
// ── Core methods ─────────────────────────────────────────────────────
|
|
8228
|
+
async listProfiles() {
|
|
8229
|
+
const data = await this.request("/profiles");
|
|
8230
|
+
return data.profiles ?? [];
|
|
8231
|
+
}
|
|
8232
|
+
async listAccounts() {
|
|
8233
|
+
const data = await this.request("/accounts");
|
|
8234
|
+
return data.accounts ?? [];
|
|
8235
|
+
}
|
|
8236
|
+
async getScheduledPosts(platform) {
|
|
8237
|
+
return this.listPosts({ status: "scheduled", platform });
|
|
8238
|
+
}
|
|
8239
|
+
async getDraftPosts(platform) {
|
|
8240
|
+
return this.listPosts({ status: "draft", platform });
|
|
8241
|
+
}
|
|
8242
|
+
async createPost(params) {
|
|
8243
|
+
const data = await this.request("/posts", {
|
|
8244
|
+
method: "POST",
|
|
8245
|
+
body: JSON.stringify(params)
|
|
8246
|
+
});
|
|
8247
|
+
return data.post;
|
|
8248
|
+
}
|
|
8249
|
+
async deletePost(postId) {
|
|
8250
|
+
await this.request(`/posts/${encodeURIComponent(postId)}`, {
|
|
8251
|
+
method: "DELETE"
|
|
8252
|
+
});
|
|
8253
|
+
}
|
|
8254
|
+
async updatePost(postId, updates) {
|
|
8255
|
+
const data = await this.request(`/posts/${encodeURIComponent(postId)}`, {
|
|
8256
|
+
method: "PUT",
|
|
8257
|
+
body: JSON.stringify(updates)
|
|
8258
|
+
});
|
|
8259
|
+
return data.post;
|
|
8260
|
+
}
|
|
8261
|
+
/** Reschedule a post and ensure it transitions out of draft status. */
|
|
8262
|
+
async schedulePost(postId, scheduledFor) {
|
|
8263
|
+
return this.updatePost(postId, { scheduledFor, isDraft: false });
|
|
8264
|
+
}
|
|
8265
|
+
async uploadMedia(filePath) {
|
|
8266
|
+
const fileStats = await getFileStats(filePath);
|
|
8267
|
+
const fileName = basename(filePath);
|
|
8268
|
+
const ext = extname(fileName).toLowerCase();
|
|
8269
|
+
const contentType = ext === ".mp4" ? "video/mp4" : ext === ".webm" ? "video/webm" : ext === ".mov" ? "video/quicktime" : "video/mp4";
|
|
8270
|
+
logger_default.info(`Late API uploading ${String(fileName).replace(/[\r\n]/g, "")} (${(fileStats.size / 1024 / 1024).toFixed(1)} MB)`);
|
|
8271
|
+
const presign = await this.request("/media/presign", {
|
|
8272
|
+
method: "POST",
|
|
8273
|
+
body: JSON.stringify({ filename: fileName, contentType })
|
|
8274
|
+
});
|
|
8275
|
+
logger_default.debug(`Late API presigned URL obtained for ${String(fileName).replace(/[\r\n]/g, "")} (expires in ${presign.expiresIn}s)`);
|
|
8276
|
+
const nodeStream = openReadStream(filePath);
|
|
8277
|
+
try {
|
|
8278
|
+
const webStream = Readable.toWeb(nodeStream);
|
|
8279
|
+
const uploadResp = await fetchRaw(presign.uploadUrl, {
|
|
8280
|
+
method: "PUT",
|
|
8281
|
+
headers: {
|
|
8282
|
+
"Content-Type": contentType,
|
|
8283
|
+
"Content-Length": String(fileStats.size)
|
|
8284
|
+
},
|
|
8285
|
+
body: webStream,
|
|
8286
|
+
// Node.js-specific property for streaming request bodies (not in standard RequestInit type)
|
|
8287
|
+
duplex: "half"
|
|
8288
|
+
});
|
|
8289
|
+
if (!uploadResp.ok) {
|
|
8290
|
+
throw new Error(`Late media upload failed: ${uploadResp.status} ${uploadResp.statusText}`);
|
|
8291
|
+
}
|
|
8292
|
+
} finally {
|
|
8293
|
+
nodeStream.destroy();
|
|
8294
|
+
}
|
|
8295
|
+
logger_default.debug(`Late API media uploaded \u2192 ${presign.publicUrl}`);
|
|
8296
|
+
const type = contentType.startsWith("image/") ? "image" : "video";
|
|
8297
|
+
return { url: presign.publicUrl, type };
|
|
8298
|
+
}
|
|
8299
|
+
/**
|
|
8300
|
+
* Fetch posts with pagination, iterating pages until all results are collected.
|
|
8301
|
+
* Supports filtering by status and platform.
|
|
8302
|
+
*/
|
|
8303
|
+
async listPosts(options = {}) {
|
|
8304
|
+
const limit = options.limit ?? 100;
|
|
8305
|
+
const allPosts = [];
|
|
8306
|
+
let page = 1;
|
|
8307
|
+
while (true) {
|
|
8308
|
+
const params = new URLSearchParams();
|
|
8309
|
+
if (options.status) params.set("status", options.status);
|
|
8310
|
+
if (options.platform) params.set("platform", options.platform);
|
|
8311
|
+
params.set("limit", String(limit));
|
|
8312
|
+
params.set("page", String(page));
|
|
8313
|
+
const data = await this.request(
|
|
8314
|
+
`/posts?${params}`
|
|
8315
|
+
);
|
|
8316
|
+
const posts = data.posts ?? data.data ?? [];
|
|
8317
|
+
allPosts.push(...posts);
|
|
8318
|
+
if (posts.length < limit) break;
|
|
8319
|
+
page++;
|
|
8320
|
+
}
|
|
8321
|
+
return allPosts;
|
|
8322
|
+
}
|
|
8323
|
+
// ── Queue management ─────────────────────────────────────────────────
|
|
8324
|
+
async listQueues(profileId, all = false) {
|
|
8325
|
+
const params = new URLSearchParams({ profileId });
|
|
8326
|
+
if (all) params.set("all", "true");
|
|
8327
|
+
return this.request(`/queue/slots?${params}`);
|
|
8328
|
+
}
|
|
8329
|
+
async createQueue(params) {
|
|
8330
|
+
return this.request("/queue/slots", { method: "POST", body: JSON.stringify(params) });
|
|
8331
|
+
}
|
|
8332
|
+
async updateQueue(params) {
|
|
8333
|
+
return this.request("/queue/slots", { method: "PUT", body: JSON.stringify(params) });
|
|
8334
|
+
}
|
|
8335
|
+
async deleteQueue(profileId, queueId) {
|
|
8336
|
+
return this.request(`/queue/slots?profileId=${profileId}&queueId=${queueId}`, { method: "DELETE" });
|
|
8337
|
+
}
|
|
8338
|
+
async previewQueue(profileId, queueId, count = 20) {
|
|
8339
|
+
const params = new URLSearchParams({ profileId, count: String(count) });
|
|
8340
|
+
if (queueId) params.set("queueId", queueId);
|
|
8341
|
+
return this.request(`/queue/preview?${params}`);
|
|
8342
|
+
}
|
|
8343
|
+
async getNextQueueSlot(profileId, queueId) {
|
|
8344
|
+
const params = new URLSearchParams({ profileId });
|
|
8345
|
+
if (queueId) params.set("queueId", queueId);
|
|
8346
|
+
return this.request(`/queue/next-slot?${params}`);
|
|
8347
|
+
}
|
|
8348
|
+
// ── Helper ───────────────────────────────────────────────────────────
|
|
8349
|
+
async validateConnection() {
|
|
8350
|
+
try {
|
|
8351
|
+
const profiles = await this.listProfiles();
|
|
8352
|
+
const name = profiles[0]?.name;
|
|
8353
|
+
logger_default.info(`Late API connection valid \u2014 profile: ${name ?? "unknown"}`);
|
|
8354
|
+
return { valid: true, profileName: name };
|
|
8355
|
+
} catch (err) {
|
|
8356
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
8357
|
+
logger_default.error(`Late API connection failed: ${message}`);
|
|
8358
|
+
return { valid: false, error: message };
|
|
8359
|
+
}
|
|
8360
|
+
}
|
|
8361
|
+
};
|
|
8362
|
+
}
|
|
8363
|
+
});
|
|
8364
|
+
|
|
8365
|
+
// src/L3-services/queueMapping/queueMapping.ts
|
|
8366
|
+
function cachePath() {
|
|
8367
|
+
return join(process.cwd(), CACHE_FILE);
|
|
8368
|
+
}
|
|
8369
|
+
function isCacheValid(cache) {
|
|
8370
|
+
const fetchedAtTime = new Date(cache.fetchedAt).getTime();
|
|
8371
|
+
if (Number.isNaN(fetchedAtTime)) {
|
|
8372
|
+
logger_default.warn("Invalid fetchedAt in queue cache; treating as stale", {
|
|
8373
|
+
fetchedAt: cache.fetchedAt
|
|
8374
|
+
});
|
|
8375
|
+
return false;
|
|
8376
|
+
}
|
|
8377
|
+
const age = Date.now() - fetchedAtTime;
|
|
8378
|
+
return age < CACHE_TTL_MS2;
|
|
8379
|
+
}
|
|
8380
|
+
async function readFileCache() {
|
|
8381
|
+
try {
|
|
8382
|
+
const raw = await readTextFile(cachePath());
|
|
8383
|
+
const cache = JSON.parse(raw);
|
|
8384
|
+
if (cache.mappings && cache.profileId && cache.fetchedAt && isCacheValid(cache)) {
|
|
8385
|
+
return cache;
|
|
8386
|
+
}
|
|
8387
|
+
return null;
|
|
8388
|
+
} catch {
|
|
8389
|
+
return null;
|
|
8390
|
+
}
|
|
8391
|
+
}
|
|
8392
|
+
async function writeFileCache(cache) {
|
|
8393
|
+
try {
|
|
8394
|
+
if (!cache || typeof cache !== "object" || !cache.mappings || !cache.profileId || !cache.fetchedAt) {
|
|
8395
|
+
logger_default.warn("Invalid queue cache structure, skipping write");
|
|
8396
|
+
return;
|
|
8397
|
+
}
|
|
8398
|
+
const sanitized = {
|
|
8399
|
+
mappings: typeof cache.mappings === "object" ? { ...cache.mappings } : {},
|
|
8400
|
+
profileId: String(cache.profileId),
|
|
8401
|
+
fetchedAt: String(cache.fetchedAt)
|
|
8402
|
+
};
|
|
8403
|
+
for (const [name, id] of Object.entries(sanitized.mappings)) {
|
|
8404
|
+
if (typeof name !== "string" || typeof id !== "string" || /[\x00-\x1f]/.test(name) || /[\x00-\x1f]/.test(id)) {
|
|
8405
|
+
logger_default.warn("Invalid queue mapping data from API, skipping cache write");
|
|
8406
|
+
return;
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8409
|
+
const resolvedCachePath = resolve(cachePath());
|
|
8410
|
+
if (!resolvedCachePath.startsWith(resolve(process.cwd()) + sep)) {
|
|
8411
|
+
throw new Error("Cache path outside working directory");
|
|
8412
|
+
}
|
|
8413
|
+
await writeTextFile(resolvedCachePath, JSON.stringify(sanitized, null, 2));
|
|
8414
|
+
} catch (err) {
|
|
8415
|
+
logger_default.warn("Failed to write queue cache file", { error: err });
|
|
8416
|
+
}
|
|
8417
|
+
}
|
|
8418
|
+
async function fetchAndCache() {
|
|
8419
|
+
const client = new LateApiClient();
|
|
8420
|
+
const profiles = await client.listProfiles();
|
|
8421
|
+
if (profiles.length === 0) {
|
|
8422
|
+
logger_default.warn("No Late API profiles found \u2014 queue mappings will be empty");
|
|
8423
|
+
const emptyCache = {
|
|
8424
|
+
mappings: {},
|
|
8425
|
+
profileId: "",
|
|
8426
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8427
|
+
};
|
|
8428
|
+
memoryCache = emptyCache;
|
|
8429
|
+
return emptyCache;
|
|
8430
|
+
}
|
|
8431
|
+
const profileId = profiles[0]._id;
|
|
8432
|
+
const { queues } = await client.listQueues(profileId, true);
|
|
8433
|
+
if (queues.length === 0) {
|
|
8434
|
+
logger_default.warn(
|
|
8435
|
+
"No queues found in Late API \u2014 run `vidpipe sync-queues` to create platform queues"
|
|
8436
|
+
);
|
|
8437
|
+
}
|
|
8438
|
+
const mappings = {};
|
|
8439
|
+
for (const queue of queues) {
|
|
8440
|
+
mappings[queue.name] = queue._id;
|
|
8441
|
+
}
|
|
8442
|
+
const cache = {
|
|
8443
|
+
mappings,
|
|
8444
|
+
profileId,
|
|
8445
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
8446
|
+
};
|
|
8447
|
+
memoryCache = cache;
|
|
8448
|
+
await writeFileCache(cache);
|
|
8449
|
+
logger_default.info("Refreshed Late queue mappings", {
|
|
8450
|
+
queueCount: queues.length,
|
|
8451
|
+
queues: Object.keys(mappings)
|
|
8452
|
+
});
|
|
8453
|
+
return cache;
|
|
8454
|
+
}
|
|
8455
|
+
async function ensureMappings() {
|
|
8456
|
+
if (memoryCache && isCacheValid(memoryCache)) {
|
|
8457
|
+
return memoryCache;
|
|
8458
|
+
}
|
|
8459
|
+
const fileCache = await readFileCache();
|
|
8460
|
+
if (fileCache) {
|
|
8461
|
+
memoryCache = fileCache;
|
|
8462
|
+
return fileCache;
|
|
8463
|
+
}
|
|
8464
|
+
try {
|
|
8465
|
+
return await fetchAndCache();
|
|
8466
|
+
} catch (err) {
|
|
8467
|
+
logger_default.error("Failed to fetch Late queue mappings", { error: err });
|
|
8468
|
+
return { mappings: {}, profileId: "", fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
8469
|
+
}
|
|
8470
|
+
}
|
|
8471
|
+
async function getQueueId(platform, clipType) {
|
|
8472
|
+
const cache = await ensureMappings();
|
|
8473
|
+
const normalizedPlatform = platform === "twitter" ? "x" : platform;
|
|
8474
|
+
const normalizedClipType = clipType === "medium-clip" ? "medium" : clipType;
|
|
8475
|
+
const queueName = `${normalizedPlatform}-${normalizedClipType}`;
|
|
8476
|
+
return cache.mappings[queueName] ?? null;
|
|
8477
|
+
}
|
|
8478
|
+
async function getProfileId() {
|
|
8479
|
+
const cache = await ensureMappings();
|
|
8480
|
+
return cache.profileId;
|
|
8481
|
+
}
|
|
8482
|
+
var CACHE_FILE, CACHE_TTL_MS2, memoryCache;
|
|
8483
|
+
var init_queueMapping = __esm({
|
|
8484
|
+
"src/L3-services/queueMapping/queueMapping.ts"() {
|
|
8485
|
+
"use strict";
|
|
8486
|
+
init_lateApi();
|
|
8487
|
+
init_configLogger();
|
|
8488
|
+
init_fileSystem();
|
|
8489
|
+
init_paths();
|
|
8490
|
+
CACHE_FILE = ".vidpipe-queue-cache.json";
|
|
8491
|
+
CACHE_TTL_MS2 = 24 * 60 * 60 * 1e3;
|
|
8492
|
+
memoryCache = null;
|
|
8493
|
+
}
|
|
8494
|
+
});
|
|
8495
|
+
|
|
7962
8496
|
// src/L2-clients/ffmpeg/audioExtraction.ts
|
|
7963
8497
|
async function extractAudio(videoPath, outputPath, options = {}) {
|
|
7964
8498
|
const { format = "mp3" } = options;
|
|
@@ -8922,7 +9456,8 @@ async function convertWithSmartLayout(inputPath, outputPath, config2, webcamOver
|
|
|
8922
9456
|
const webcam = webcamOverride !== void 0 ? webcamOverride : await detectWebcamRegion(inputPath);
|
|
8923
9457
|
if (!webcam) {
|
|
8924
9458
|
logger_default.info(`[${label}] No webcam found, falling back to center-crop`);
|
|
8925
|
-
|
|
9459
|
+
const path = await convertAspectRatio(inputPath, outputPath, fallbackRatio);
|
|
9460
|
+
return { path, isSplitScreen: false };
|
|
8926
9461
|
}
|
|
8927
9462
|
const resolution = await getVideoResolution(inputPath);
|
|
8928
9463
|
const margin = Math.round(resolution.width * 0.02);
|
|
@@ -8987,7 +9522,7 @@ async function convertWithSmartLayout(inputPath, outputPath, config2, webcamOver
|
|
|
8987
9522
|
return;
|
|
8988
9523
|
}
|
|
8989
9524
|
logger_default.info(`[${label}] Complete: ${outputPath}`);
|
|
8990
|
-
resolve3(outputPath);
|
|
9525
|
+
resolve3({ path: outputPath, isSplitScreen: true });
|
|
8991
9526
|
});
|
|
8992
9527
|
});
|
|
8993
9528
|
}
|
|
@@ -9033,21 +9568,25 @@ async function generatePlatformVariants(inputPath, outputDir, slug, platforms =
|
|
|
9033
9568
|
const suffix = ratio === "9:16" ? "portrait" : ratio === "4:5" ? "feed" : "square";
|
|
9034
9569
|
const outPath = join(outputDir, `${slug}-${suffix}.mp4`);
|
|
9035
9570
|
try {
|
|
9571
|
+
let isSplitScreen = false;
|
|
9036
9572
|
if (ratio === "9:16") {
|
|
9037
9573
|
if (options.useAgent) {
|
|
9038
9574
|
logger_default.warn(`[generatePlatformVariants] LayoutAgent is disabled, falling back to ONNX pipeline`);
|
|
9039
9575
|
}
|
|
9040
|
-
await convertToPortraitSmart(inputPath, outPath, options.webcamOverride);
|
|
9576
|
+
const result = await convertToPortraitSmart(inputPath, outPath, options.webcamOverride);
|
|
9577
|
+
isSplitScreen = result.isSplitScreen;
|
|
9041
9578
|
} else if (ratio === "1:1") {
|
|
9042
|
-
await convertToSquareSmart(inputPath, outPath, options.webcamOverride);
|
|
9579
|
+
const result = await convertToSquareSmart(inputPath, outPath, options.webcamOverride);
|
|
9580
|
+
isSplitScreen = result.isSplitScreen;
|
|
9043
9581
|
} else if (ratio === "4:5") {
|
|
9044
|
-
await convertToFeedSmart(inputPath, outPath, options.webcamOverride);
|
|
9582
|
+
const result = await convertToFeedSmart(inputPath, outPath, options.webcamOverride);
|
|
9583
|
+
isSplitScreen = result.isSplitScreen;
|
|
9045
9584
|
} else {
|
|
9046
9585
|
await convertAspectRatio(inputPath, outPath, ratio);
|
|
9047
9586
|
}
|
|
9048
9587
|
const dims = DIMENSIONS[ratio];
|
|
9049
9588
|
for (const p of associatedPlatforms) {
|
|
9050
|
-
variants.push({ platform: p, aspectRatio: ratio, path: outPath, width: dims.width, height: dims.height });
|
|
9589
|
+
variants.push({ platform: p, aspectRatio: ratio, path: outPath, width: dims.width, height: dims.height, isSplitScreen });
|
|
9051
9590
|
}
|
|
9052
9591
|
} catch (err) {
|
|
9053
9592
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -9635,9 +10174,9 @@ var init_BaseAgent = __esm({
|
|
|
9635
10174
|
getUserInputHandler() {
|
|
9636
10175
|
return void 0;
|
|
9637
10176
|
}
|
|
9638
|
-
/** Timeout for sendAndWait calls.
|
|
10177
|
+
/** Timeout for sendAndWait calls. 0 = no timeout. Override in subclasses if needed. */
|
|
9639
10178
|
getTimeoutMs() {
|
|
9640
|
-
return
|
|
10179
|
+
return 0;
|
|
9641
10180
|
}
|
|
9642
10181
|
/**
|
|
9643
10182
|
* Reset agent-specific state before a retry attempt.
|
|
@@ -10159,15 +10698,564 @@ var init_ThumbnailAgent = __esm({
|
|
|
10159
10698
|
- **Title:** ${context.title}
|
|
10160
10699
|
- **Description:** ${context.description}${hookStr}${topicsStr}${platformStr}
|
|
10161
10700
|
|
|
10162
|
-
Call the generate_thumbnail tool with a detailed, vivid prompt that will create a click-worthy thumbnail. Remember to include text overlay in your prompt (3-5 words max).`;
|
|
10163
|
-
await this.run(userMessage);
|
|
10164
|
-
return this.generatedThumbnails;
|
|
10165
|
-
}
|
|
10166
|
-
/** Clean up the LLM session. */
|
|
10167
|
-
async destroy() {
|
|
10168
|
-
await super.destroy();
|
|
10169
|
-
}
|
|
10170
|
-
};
|
|
10701
|
+
Call the generate_thumbnail tool with a detailed, vivid prompt that will create a click-worthy thumbnail. Remember to include text overlay in your prompt (3-5 words max).`;
|
|
10702
|
+
await this.run(userMessage);
|
|
10703
|
+
return this.generatedThumbnails;
|
|
10704
|
+
}
|
|
10705
|
+
/** Clean up the LLM session. */
|
|
10706
|
+
async destroy() {
|
|
10707
|
+
await super.destroy();
|
|
10708
|
+
}
|
|
10709
|
+
};
|
|
10710
|
+
}
|
|
10711
|
+
});
|
|
10712
|
+
|
|
10713
|
+
// src/L2-clients/azure/blobClient.ts
|
|
10714
|
+
import { BlobServiceClient, StorageSharedKeyCredential } from "@azure/storage-blob";
|
|
10715
|
+
import { Readable as Readable2 } from "stream";
|
|
10716
|
+
import { createReadStream as createReadStream2 } from "fs";
|
|
10717
|
+
import { stat } from "fs/promises";
|
|
10718
|
+
function getClient() {
|
|
10719
|
+
const config2 = getConfig();
|
|
10720
|
+
const accountName = config2.AZURE_STORAGE_ACCOUNT_NAME;
|
|
10721
|
+
const accountKey = config2.AZURE_STORAGE_ACCOUNT_KEY;
|
|
10722
|
+
const containerName = config2.AZURE_CONTAINER_NAME;
|
|
10723
|
+
if (!accountName || !accountKey) {
|
|
10724
|
+
throw new Error("Azure Storage credentials not configured. Set AZURE_STORAGE_ACCOUNT_NAME and AZURE_STORAGE_ACCOUNT_KEY.");
|
|
10725
|
+
}
|
|
10726
|
+
const credential = new StorageSharedKeyCredential(accountName, accountKey);
|
|
10727
|
+
const blobService = new BlobServiceClient(`https://${accountName}.blob.core.windows.net`, credential);
|
|
10728
|
+
const container = blobService.getContainerClient(containerName);
|
|
10729
|
+
return { blobService, container };
|
|
10730
|
+
}
|
|
10731
|
+
async function uploadFile(blobPath, localFilePath, contentType) {
|
|
10732
|
+
const { container } = getClient();
|
|
10733
|
+
const blockBlob = container.getBlockBlobClient(blobPath);
|
|
10734
|
+
const fileStat = await stat(localFilePath);
|
|
10735
|
+
await blockBlob.uploadStream(
|
|
10736
|
+
createReadStream2(localFilePath),
|
|
10737
|
+
4 * 1024 * 1024,
|
|
10738
|
+
// 4MB buffer size
|
|
10739
|
+
5,
|
|
10740
|
+
// max concurrency
|
|
10741
|
+
{
|
|
10742
|
+
blobHTTPHeaders: contentType ? { blobContentType: contentType } : void 0
|
|
10743
|
+
}
|
|
10744
|
+
);
|
|
10745
|
+
logger_default.debug(`Uploaded file to blob: ${blobPath} (${fileStat.size} bytes)`);
|
|
10746
|
+
return blockBlob.url;
|
|
10747
|
+
}
|
|
10748
|
+
async function downloadToFile(blobPath, localPath) {
|
|
10749
|
+
const { container } = getClient();
|
|
10750
|
+
const blockBlob = container.getBlockBlobClient(blobPath);
|
|
10751
|
+
await blockBlob.downloadToFile(localPath);
|
|
10752
|
+
logger_default.debug(`Downloaded blob to file: ${blobPath} \u2192 ${localPath}`);
|
|
10753
|
+
}
|
|
10754
|
+
async function downloadStream(blobPath) {
|
|
10755
|
+
const { container } = getClient();
|
|
10756
|
+
const blockBlob = container.getBlockBlobClient(blobPath);
|
|
10757
|
+
const response = await blockBlob.download(0);
|
|
10758
|
+
if (!response.readableStreamBody) {
|
|
10759
|
+
throw new Error(`Failed to get readable stream for blob: ${blobPath}`);
|
|
10760
|
+
}
|
|
10761
|
+
return Readable2.from(response.readableStreamBody);
|
|
10762
|
+
}
|
|
10763
|
+
async function listBlobs(prefix) {
|
|
10764
|
+
const { container } = getClient();
|
|
10765
|
+
const blobs = [];
|
|
10766
|
+
for await (const blob of container.listBlobsFlat({ prefix })) {
|
|
10767
|
+
blobs.push(blob.name);
|
|
10768
|
+
}
|
|
10769
|
+
return blobs;
|
|
10770
|
+
}
|
|
10771
|
+
function isAzureConfigured() {
|
|
10772
|
+
const config2 = getConfig();
|
|
10773
|
+
return Boolean(config2.AZURE_STORAGE_ACCOUNT_NAME && config2.AZURE_STORAGE_ACCOUNT_KEY);
|
|
10774
|
+
}
|
|
10775
|
+
var init_blobClient = __esm({
|
|
10776
|
+
"src/L2-clients/azure/blobClient.ts"() {
|
|
10777
|
+
"use strict";
|
|
10778
|
+
init_configLogger();
|
|
10779
|
+
init_environment();
|
|
10780
|
+
}
|
|
10781
|
+
});
|
|
10782
|
+
|
|
10783
|
+
// src/L2-clients/azure/tableClient.ts
|
|
10784
|
+
import { TableClient as AzureTableClient, AzureNamedKeyCredential } from "@azure/data-tables";
|
|
10785
|
+
function getTableClient(tableName) {
|
|
10786
|
+
const config2 = getConfig();
|
|
10787
|
+
const accountName = config2.AZURE_STORAGE_ACCOUNT_NAME;
|
|
10788
|
+
const accountKey = config2.AZURE_STORAGE_ACCOUNT_KEY;
|
|
10789
|
+
if (!accountName || !accountKey) {
|
|
10790
|
+
throw new Error("Azure Storage credentials not configured. Set AZURE_STORAGE_ACCOUNT_NAME and AZURE_STORAGE_ACCOUNT_KEY.");
|
|
10791
|
+
}
|
|
10792
|
+
const credential = new AzureNamedKeyCredential(accountName, accountKey);
|
|
10793
|
+
const url = `https://${accountName}.table.core.windows.net`;
|
|
10794
|
+
return new AzureTableClient(url, tableName, credential);
|
|
10795
|
+
}
|
|
10796
|
+
async function upsertEntity(tableName, partitionKey, rowKey, properties) {
|
|
10797
|
+
const client = getTableClient(tableName);
|
|
10798
|
+
const entity = { partitionKey, rowKey, ...properties };
|
|
10799
|
+
await client.upsertEntity(entity, "Merge");
|
|
10800
|
+
logger_default.debug(`Upserted entity: ${tableName}/${partitionKey}/${rowKey}`);
|
|
10801
|
+
}
|
|
10802
|
+
async function getEntity(tableName, partitionKey, rowKey) {
|
|
10803
|
+
const client = getTableClient(tableName);
|
|
10804
|
+
try {
|
|
10805
|
+
return await client.getEntity(partitionKey, rowKey);
|
|
10806
|
+
} catch (error) {
|
|
10807
|
+
if (error instanceof Error && "statusCode" in error && error.statusCode === 404) {
|
|
10808
|
+
return null;
|
|
10809
|
+
}
|
|
10810
|
+
throw error;
|
|
10811
|
+
}
|
|
10812
|
+
}
|
|
10813
|
+
async function queryEntities(tableName, filter) {
|
|
10814
|
+
const client = getTableClient(tableName);
|
|
10815
|
+
const entities = [];
|
|
10816
|
+
for await (const entity of client.listEntities({ queryOptions: { filter } })) {
|
|
10817
|
+
entities.push(entity);
|
|
10818
|
+
}
|
|
10819
|
+
return entities;
|
|
10820
|
+
}
|
|
10821
|
+
async function updateEntity(tableName, partitionKey, rowKey, properties) {
|
|
10822
|
+
const client = getTableClient(tableName);
|
|
10823
|
+
const entity = { partitionKey, rowKey, ...properties };
|
|
10824
|
+
await client.updateEntity(entity, "Merge");
|
|
10825
|
+
logger_default.debug(`Updated entity: ${tableName}/${partitionKey}/${rowKey}`);
|
|
10826
|
+
}
|
|
10827
|
+
var init_tableClient = __esm({
|
|
10828
|
+
"src/L2-clients/azure/tableClient.ts"() {
|
|
10829
|
+
"use strict";
|
|
10830
|
+
init_configLogger();
|
|
10831
|
+
init_environment();
|
|
10832
|
+
}
|
|
10833
|
+
});
|
|
10834
|
+
|
|
10835
|
+
// src/L3-services/azureStorage/azureStorageService.ts
|
|
10836
|
+
var azureStorageService_exports = {};
|
|
10837
|
+
__export(azureStorageService_exports, {
|
|
10838
|
+
downloadBlobToFile: () => downloadBlobToFile,
|
|
10839
|
+
downloadContentMedia: () => downloadContentMedia,
|
|
10840
|
+
findContentItemByRowKey: () => findContentItemByRowKey,
|
|
10841
|
+
getContentItem: () => getContentItem,
|
|
10842
|
+
getContentItems: () => getContentItems,
|
|
10843
|
+
getRunId: () => getRunId,
|
|
10844
|
+
getVideoRecord: () => getVideoRecord,
|
|
10845
|
+
isAzureConfigured: () => isAzureConfigured2,
|
|
10846
|
+
listVideos: () => listVideos,
|
|
10847
|
+
migrateLocalContent: () => migrateLocalContent,
|
|
10848
|
+
updateContentStatus: () => updateContentStatus,
|
|
10849
|
+
uploadContentItem: () => uploadContentItem,
|
|
10850
|
+
uploadPublishQueue: () => uploadPublishQueue,
|
|
10851
|
+
uploadRawVideo: () => uploadRawVideo,
|
|
10852
|
+
uploadVideoFile: () => uploadVideoFile
|
|
10853
|
+
});
|
|
10854
|
+
import { readdir, readFile } from "fs/promises";
|
|
10855
|
+
import { join as join8 } from "path";
|
|
10856
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
10857
|
+
async function uploadVideoFile(localPath, blobPath) {
|
|
10858
|
+
logger_default.info(`Uploading video to Azure blob: ${blobPath}`);
|
|
10859
|
+
return uploadFile(blobPath, localPath, "video/mp4");
|
|
10860
|
+
}
|
|
10861
|
+
async function downloadBlobToFile(blobPath, localPath) {
|
|
10862
|
+
return downloadToFile(blobPath, localPath);
|
|
10863
|
+
}
|
|
10864
|
+
function isAzureConfigured2() {
|
|
10865
|
+
return isAzureConfigured();
|
|
10866
|
+
}
|
|
10867
|
+
function getRunId() {
|
|
10868
|
+
return process.env.GITHUB_RUN_ID || randomUUID2();
|
|
10869
|
+
}
|
|
10870
|
+
async function uploadRawVideo(localPath, runId, metadata) {
|
|
10871
|
+
const blobPath = `raw/${runId}-${metadata.originalFilename}`;
|
|
10872
|
+
logger_default.info(`Uploading raw video to Azure: ${blobPath}`);
|
|
10873
|
+
await uploadFile(blobPath, localPath, "video/mp4");
|
|
10874
|
+
await upsertEntity(VIDEOS_TABLE, "video", runId, {
|
|
10875
|
+
originalFilename: metadata.originalFilename,
|
|
10876
|
+
slug: metadata.slug,
|
|
10877
|
+
blobPath,
|
|
10878
|
+
sourceUrl: metadata.sourceUrl || "",
|
|
10879
|
+
duration: metadata.duration || 0,
|
|
10880
|
+
size: metadata.size,
|
|
10881
|
+
status: "completed",
|
|
10882
|
+
contentCount: 0,
|
|
10883
|
+
processedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10884
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10885
|
+
});
|
|
10886
|
+
logger_default.info(`Created video record: ${runId}`);
|
|
10887
|
+
return blobPath;
|
|
10888
|
+
}
|
|
10889
|
+
async function uploadContentItem(localItemDir, itemId, videoSlug, runId, metadata) {
|
|
10890
|
+
const blobBasePath = `content/${itemId}/`;
|
|
10891
|
+
const files = await readdir(localItemDir);
|
|
10892
|
+
for (const file of files) {
|
|
10893
|
+
const localFilePath = join8(localItemDir, file);
|
|
10894
|
+
const blobPath = `${blobBasePath}${file}`;
|
|
10895
|
+
const contentType = getContentType(file);
|
|
10896
|
+
await uploadFile(blobPath, localFilePath, contentType);
|
|
10897
|
+
}
|
|
10898
|
+
let itemMetadata = {};
|
|
10899
|
+
const metadataPath = join8(localItemDir, "metadata.json");
|
|
10900
|
+
try {
|
|
10901
|
+
const metadataContent = await readFile(metadataPath, "utf8");
|
|
10902
|
+
itemMetadata = JSON.parse(metadataContent);
|
|
10903
|
+
} catch {
|
|
10904
|
+
}
|
|
10905
|
+
let postContent = "";
|
|
10906
|
+
const postPath = join8(localItemDir, "post.md");
|
|
10907
|
+
try {
|
|
10908
|
+
postContent = await readFile(postPath, "utf8");
|
|
10909
|
+
} catch {
|
|
10910
|
+
}
|
|
10911
|
+
const mediaFilename = files.find((f) => f.startsWith("media.")) || "";
|
|
10912
|
+
const thumbnailFilename = files.find((f) => f.startsWith("thumbnail.")) || "";
|
|
10913
|
+
const record = {
|
|
10914
|
+
platform: String(itemMetadata.platform || metadata?.platform || ""),
|
|
10915
|
+
clipType: String(itemMetadata.clipType || metadata?.clipType || ""),
|
|
10916
|
+
status: metadata?.status || "pending_review",
|
|
10917
|
+
blobBasePath,
|
|
10918
|
+
mediaType: String(itemMetadata.mediaType || metadata?.mediaType || "video"),
|
|
10919
|
+
mediaFilename,
|
|
10920
|
+
postContent,
|
|
10921
|
+
hashtags: Array.isArray(itemMetadata.hashtags) ? itemMetadata.hashtags.join(",") : metadata?.hashtags || "",
|
|
10922
|
+
characterCount: Number(itemMetadata.characterCount || metadata?.characterCount || postContent.length),
|
|
10923
|
+
scheduledFor: String(itemMetadata.scheduledFor || metadata?.scheduledFor || ""),
|
|
10924
|
+
latePostId: String(itemMetadata.latePostId || metadata?.latePostId || ""),
|
|
10925
|
+
publishedUrl: String(itemMetadata.publishedUrl || metadata?.publishedUrl || ""),
|
|
10926
|
+
sourceVideoRunId: runId,
|
|
10927
|
+
thumbnailFilename,
|
|
10928
|
+
ideaIds: Array.isArray(itemMetadata.ideaIds) ? itemMetadata.ideaIds.join(",") : metadata?.ideaIds || "",
|
|
10929
|
+
createdAt: String(itemMetadata.createdAt || (/* @__PURE__ */ new Date()).toISOString()),
|
|
10930
|
+
reviewedAt: String(itemMetadata.reviewedAt || metadata?.reviewedAt || ""),
|
|
10931
|
+
publishedAt: String(itemMetadata.publishedAt || metadata?.publishedAt || "")
|
|
10932
|
+
};
|
|
10933
|
+
await upsertEntity(CONTENT_TABLE, videoSlug, itemId, record);
|
|
10934
|
+
logger_default.info(`Uploaded content item: ${itemId} (${record.platform}/${record.clipType}) \u2014 blob + table record created`);
|
|
10935
|
+
return blobBasePath;
|
|
10936
|
+
}
|
|
10937
|
+
async function uploadPublishQueue(publishQueueDir, videoSlug, runId) {
|
|
10938
|
+
const errors = [];
|
|
10939
|
+
let uploaded = 0;
|
|
10940
|
+
let items;
|
|
10941
|
+
try {
|
|
10942
|
+
items = await readdir(publishQueueDir);
|
|
10943
|
+
} catch {
|
|
10944
|
+
logger_default.warn(`Publish queue directory not found: ${publishQueueDir}`);
|
|
10945
|
+
return { uploaded: 0, errors: ["Publish queue directory not found"] };
|
|
10946
|
+
}
|
|
10947
|
+
for (const itemId of items) {
|
|
10948
|
+
const itemDir = join8(publishQueueDir, itemId);
|
|
10949
|
+
try {
|
|
10950
|
+
const metaPath = join8(itemDir, "metadata.json");
|
|
10951
|
+
const metaContent = await readFile(metaPath, "utf8");
|
|
10952
|
+
const meta = JSON.parse(metaContent);
|
|
10953
|
+
const sourceVideo = String(meta.sourceVideo || "");
|
|
10954
|
+
if (sourceVideo && !sourceVideo.endsWith(videoSlug)) {
|
|
10955
|
+
continue;
|
|
10956
|
+
}
|
|
10957
|
+
} catch {
|
|
10958
|
+
continue;
|
|
10959
|
+
}
|
|
10960
|
+
try {
|
|
10961
|
+
await uploadContentItem(itemDir, itemId, videoSlug, runId);
|
|
10962
|
+
uploaded++;
|
|
10963
|
+
} catch (error) {
|
|
10964
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
10965
|
+
errors.push(`${itemId}: ${msg}`);
|
|
10966
|
+
logger_default.error(`Failed to upload content item ${itemId}: ${msg}`);
|
|
10967
|
+
}
|
|
10968
|
+
}
|
|
10969
|
+
await updateEntity(VIDEOS_TABLE, "video", runId, {
|
|
10970
|
+
contentCount: uploaded
|
|
10971
|
+
});
|
|
10972
|
+
logger_default.info(`Uploaded ${uploaded} content items to Azure (${errors.length} errors)`);
|
|
10973
|
+
return { uploaded, errors };
|
|
10974
|
+
}
|
|
10975
|
+
async function migrateLocalContent(outputDir) {
|
|
10976
|
+
const errors = [];
|
|
10977
|
+
let uploaded = 0;
|
|
10978
|
+
const runId = `migration-${Date.now()}`;
|
|
10979
|
+
const publishQueueDir = join8(outputDir, "publish-queue");
|
|
10980
|
+
try {
|
|
10981
|
+
const items = await readdir(publishQueueDir);
|
|
10982
|
+
for (const itemId of items) {
|
|
10983
|
+
try {
|
|
10984
|
+
const videoSlug = extractVideoSlug(itemId);
|
|
10985
|
+
await uploadContentItem(
|
|
10986
|
+
join8(publishQueueDir, itemId),
|
|
10987
|
+
itemId,
|
|
10988
|
+
videoSlug,
|
|
10989
|
+
runId,
|
|
10990
|
+
{ status: "pending_review" }
|
|
10991
|
+
);
|
|
10992
|
+
uploaded++;
|
|
10993
|
+
} catch (error) {
|
|
10994
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
10995
|
+
errors.push(`publish-queue/${itemId}: ${msg}`);
|
|
10996
|
+
}
|
|
10997
|
+
}
|
|
10998
|
+
} catch {
|
|
10999
|
+
logger_default.info("No publish-queue directory found for migration");
|
|
11000
|
+
}
|
|
11001
|
+
const publishedDir = join8(outputDir, "published");
|
|
11002
|
+
try {
|
|
11003
|
+
const items = await readdir(publishedDir);
|
|
11004
|
+
for (const itemId of items) {
|
|
11005
|
+
try {
|
|
11006
|
+
const videoSlug = extractVideoSlug(itemId);
|
|
11007
|
+
await uploadContentItem(
|
|
11008
|
+
join8(publishedDir, itemId),
|
|
11009
|
+
itemId,
|
|
11010
|
+
videoSlug,
|
|
11011
|
+
runId,
|
|
11012
|
+
{ status: "published" }
|
|
11013
|
+
);
|
|
11014
|
+
uploaded++;
|
|
11015
|
+
} catch (error) {
|
|
11016
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
11017
|
+
errors.push(`published/${itemId}: ${msg}`);
|
|
11018
|
+
}
|
|
11019
|
+
}
|
|
11020
|
+
} catch {
|
|
11021
|
+
logger_default.info("No published directory found for migration");
|
|
11022
|
+
}
|
|
11023
|
+
logger_default.info(`Migration complete: ${uploaded} items uploaded, ${errors.length} errors`);
|
|
11024
|
+
return { uploaded, errors };
|
|
11025
|
+
}
|
|
11026
|
+
async function getContentItems(filters) {
|
|
11027
|
+
const parts = [];
|
|
11028
|
+
if (filters?.videoSlug) {
|
|
11029
|
+
parts.push(`PartitionKey eq '${filters.videoSlug}'`);
|
|
11030
|
+
}
|
|
11031
|
+
if (filters?.status) {
|
|
11032
|
+
parts.push(`status eq '${filters.status}'`);
|
|
11033
|
+
}
|
|
11034
|
+
const filter = parts.length > 0 ? parts.join(" and ") : "";
|
|
11035
|
+
const entities = await queryEntities(
|
|
11036
|
+
CONTENT_TABLE,
|
|
11037
|
+
filter
|
|
11038
|
+
);
|
|
11039
|
+
return entities;
|
|
11040
|
+
}
|
|
11041
|
+
async function getContentItem(videoSlug, itemId) {
|
|
11042
|
+
const entity = await getEntity(
|
|
11043
|
+
CONTENT_TABLE,
|
|
11044
|
+
videoSlug,
|
|
11045
|
+
itemId
|
|
11046
|
+
);
|
|
11047
|
+
return entity;
|
|
11048
|
+
}
|
|
11049
|
+
async function findContentItemByRowKey(itemId) {
|
|
11050
|
+
const results = await queryEntities(
|
|
11051
|
+
CONTENT_TABLE,
|
|
11052
|
+
`RowKey eq '${itemId}'`
|
|
11053
|
+
);
|
|
11054
|
+
return results[0] ?? null;
|
|
11055
|
+
}
|
|
11056
|
+
async function updateContentStatus(videoSlug, itemId, status, extraFields) {
|
|
11057
|
+
await updateEntity(CONTENT_TABLE, videoSlug, itemId, {
|
|
11058
|
+
status,
|
|
11059
|
+
...extraFields
|
|
11060
|
+
});
|
|
11061
|
+
logger_default.info(`Updated content status: ${itemId} \u2192 ${status}`);
|
|
11062
|
+
}
|
|
11063
|
+
async function getVideoRecord(runId) {
|
|
11064
|
+
return getEntity(
|
|
11065
|
+
VIDEOS_TABLE,
|
|
11066
|
+
"video",
|
|
11067
|
+
runId
|
|
11068
|
+
);
|
|
11069
|
+
}
|
|
11070
|
+
async function listVideos(status) {
|
|
11071
|
+
const filter = status ? `PartitionKey eq 'video' and status eq '${status}'` : "PartitionKey eq 'video'";
|
|
11072
|
+
return queryEntities(
|
|
11073
|
+
VIDEOS_TABLE,
|
|
11074
|
+
filter
|
|
11075
|
+
);
|
|
11076
|
+
}
|
|
11077
|
+
async function downloadContentMedia(blobPath) {
|
|
11078
|
+
return downloadStream(blobPath);
|
|
11079
|
+
}
|
|
11080
|
+
function getContentType(filename) {
|
|
11081
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
11082
|
+
switch (ext) {
|
|
11083
|
+
case "mp4":
|
|
11084
|
+
return "video/mp4";
|
|
11085
|
+
case "png":
|
|
11086
|
+
return "image/png";
|
|
11087
|
+
case "jpg":
|
|
11088
|
+
case "jpeg":
|
|
11089
|
+
return "image/jpeg";
|
|
11090
|
+
case "json":
|
|
11091
|
+
return "application/json";
|
|
11092
|
+
case "md":
|
|
11093
|
+
return "text/markdown";
|
|
11094
|
+
case "srt":
|
|
11095
|
+
case "vtt":
|
|
11096
|
+
case "ass":
|
|
11097
|
+
return "text/plain";
|
|
11098
|
+
default:
|
|
11099
|
+
return "application/octet-stream";
|
|
11100
|
+
}
|
|
11101
|
+
}
|
|
11102
|
+
function extractVideoSlug(itemId) {
|
|
11103
|
+
const platforms = ["youtube-shorts", "instagram-reels", "instagram-feed", "twitter", "youtube", "tiktok", "instagram", "linkedin", "x"];
|
|
11104
|
+
for (const platform of platforms) {
|
|
11105
|
+
if (itemId.endsWith(`-${platform}`)) {
|
|
11106
|
+
return itemId.slice(0, -(platform.length + 1));
|
|
11107
|
+
}
|
|
11108
|
+
}
|
|
11109
|
+
return itemId;
|
|
11110
|
+
}
|
|
11111
|
+
var VIDEOS_TABLE, CONTENT_TABLE;
|
|
11112
|
+
var init_azureStorageService = __esm({
|
|
11113
|
+
"src/L3-services/azureStorage/azureStorageService.ts"() {
|
|
11114
|
+
"use strict";
|
|
11115
|
+
init_configLogger();
|
|
11116
|
+
init_blobClient();
|
|
11117
|
+
init_tableClient();
|
|
11118
|
+
VIDEOS_TABLE = "Videos";
|
|
11119
|
+
CONTENT_TABLE = "Content";
|
|
11120
|
+
}
|
|
11121
|
+
});
|
|
11122
|
+
|
|
11123
|
+
// src/L3-services/azureStorage/azureConfigService.ts
|
|
11124
|
+
var azureConfigService_exports = {};
|
|
11125
|
+
__export(azureConfigService_exports, {
|
|
11126
|
+
listConfigFiles: () => listConfigFiles,
|
|
11127
|
+
pullConfig: () => pullConfig,
|
|
11128
|
+
pushConfig: () => pushConfig
|
|
11129
|
+
});
|
|
11130
|
+
import { readdir as readdir2, stat as stat2, mkdir } from "fs/promises";
|
|
11131
|
+
import { join as join9 } from "path";
|
|
11132
|
+
async function pushConfig(vidpipeDir) {
|
|
11133
|
+
let uploaded = 0;
|
|
11134
|
+
for (const file of CONFIG_FILES) {
|
|
11135
|
+
const fullPath = join9(vidpipeDir, file);
|
|
11136
|
+
try {
|
|
11137
|
+
await stat2(fullPath);
|
|
11138
|
+
const blobPath = `${CONFIG_PREFIX}${file}`;
|
|
11139
|
+
logger_default.info(`Uploading ${file}...`);
|
|
11140
|
+
await uploadFile(blobPath, fullPath);
|
|
11141
|
+
uploaded++;
|
|
11142
|
+
logger_default.info(` \u2705 ${blobPath}`);
|
|
11143
|
+
} catch {
|
|
11144
|
+
logger_default.debug(`Config file not found, skipping: ${file}`);
|
|
11145
|
+
}
|
|
11146
|
+
}
|
|
11147
|
+
for (const dir of CONFIG_DIRS) {
|
|
11148
|
+
const fullPath = join9(vidpipeDir, dir);
|
|
11149
|
+
try {
|
|
11150
|
+
await stat2(fullPath);
|
|
11151
|
+
logger_default.info(`Uploading ${dir}/...`);
|
|
11152
|
+
const count = await uploadDirectory(fullPath, `${CONFIG_PREFIX}${dir}`);
|
|
11153
|
+
uploaded += count;
|
|
11154
|
+
logger_default.info(` \u2705 ${dir}/ (${count} files)`);
|
|
11155
|
+
} catch {
|
|
11156
|
+
logger_default.debug(`Config directory not found, skipping: ${dir}/`);
|
|
11157
|
+
}
|
|
11158
|
+
}
|
|
11159
|
+
logger_default.info(`Pushed ${uploaded} config files to Azure`);
|
|
11160
|
+
return { uploaded };
|
|
11161
|
+
}
|
|
11162
|
+
async function uploadDirectory(localDir, blobPrefix) {
|
|
11163
|
+
let count = 0;
|
|
11164
|
+
const entries = await readdir2(localDir);
|
|
11165
|
+
for (const entry of entries) {
|
|
11166
|
+
const fullPath = join9(localDir, entry);
|
|
11167
|
+
const entryStat = await stat2(fullPath);
|
|
11168
|
+
if (entryStat.isDirectory()) {
|
|
11169
|
+
count += await uploadDirectory(fullPath, `${blobPrefix}/${entry}`);
|
|
11170
|
+
} else if (entryStat.isFile()) {
|
|
11171
|
+
const blobPath = `${blobPrefix}/${entry}`;
|
|
11172
|
+
await uploadFile(blobPath, fullPath);
|
|
11173
|
+
count++;
|
|
11174
|
+
}
|
|
11175
|
+
}
|
|
11176
|
+
return count;
|
|
11177
|
+
}
|
|
11178
|
+
async function pullConfig(targetDir) {
|
|
11179
|
+
let downloaded = 0;
|
|
11180
|
+
const blobs = await listBlobs(CONFIG_PREFIX);
|
|
11181
|
+
for (const blobPath of blobs) {
|
|
11182
|
+
const relativePath = blobPath.slice(CONFIG_PREFIX.length);
|
|
11183
|
+
const localPath = join9(targetDir, relativePath);
|
|
11184
|
+
const parentDir = join9(localPath, "..");
|
|
11185
|
+
await mkdir(parentDir, { recursive: true });
|
|
11186
|
+
await downloadToFile(blobPath, localPath);
|
|
11187
|
+
downloaded++;
|
|
11188
|
+
logger_default.debug(`Downloaded config: ${blobPath} \u2192 ${localPath}`);
|
|
11189
|
+
}
|
|
11190
|
+
logger_default.info(`Pulled ${downloaded} config files from Azure`);
|
|
11191
|
+
return { downloaded };
|
|
11192
|
+
}
|
|
11193
|
+
async function listConfigFiles() {
|
|
11194
|
+
const blobs = await listBlobs(CONFIG_PREFIX);
|
|
11195
|
+
return blobs.map((b) => b.slice(CONFIG_PREFIX.length));
|
|
11196
|
+
}
|
|
11197
|
+
var CONFIG_PREFIX, CONFIG_FILES, CONFIG_DIRS;
|
|
11198
|
+
var init_azureConfigService = __esm({
|
|
11199
|
+
"src/L3-services/azureStorage/azureConfigService.ts"() {
|
|
11200
|
+
"use strict";
|
|
11201
|
+
init_configLogger();
|
|
11202
|
+
init_blobClient();
|
|
11203
|
+
CONFIG_PREFIX = "config/";
|
|
11204
|
+
CONFIG_FILES = ["schedule.json", "brand.json"];
|
|
11205
|
+
CONFIG_DIRS = ["assets"];
|
|
11206
|
+
}
|
|
11207
|
+
});
|
|
11208
|
+
|
|
11209
|
+
// src/L4-agents/cloudStorage/cloudStorageOperations.ts
|
|
11210
|
+
var cloudStorageOperations_exports = {};
|
|
11211
|
+
__export(cloudStorageOperations_exports, {
|
|
11212
|
+
isCloudEnabled: () => isCloudEnabled,
|
|
11213
|
+
migrateLocalContent: () => migrateLocalContent2,
|
|
11214
|
+
pullConfig: () => pullConfig2,
|
|
11215
|
+
pushConfig: () => pushConfig2,
|
|
11216
|
+
uploadPipelineResults: () => uploadPipelineResults
|
|
11217
|
+
});
|
|
11218
|
+
function isCloudEnabled() {
|
|
11219
|
+
return isAzureConfigured2();
|
|
11220
|
+
}
|
|
11221
|
+
async function uploadPipelineResults(inputVideoPath, publishQueueDir, videoSlug, metadata) {
|
|
11222
|
+
const runId = getRunId();
|
|
11223
|
+
logger_default.info(`Cloud upload starting (runId: ${runId})`);
|
|
11224
|
+
let videoUploaded = false;
|
|
11225
|
+
try {
|
|
11226
|
+
await uploadRawVideo(inputVideoPath, runId, {
|
|
11227
|
+
...metadata,
|
|
11228
|
+
slug: videoSlug
|
|
11229
|
+
});
|
|
11230
|
+
videoUploaded = true;
|
|
11231
|
+
} catch (error) {
|
|
11232
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
11233
|
+
logger_default.error(`Failed to upload raw video: ${msg}`);
|
|
11234
|
+
}
|
|
11235
|
+
const result = await uploadPublishQueue(publishQueueDir, videoSlug, runId);
|
|
11236
|
+
logger_default.info(`Cloud upload complete: video=${videoUploaded}, content=${result.uploaded}, errors=${result.errors.length}`);
|
|
11237
|
+
return {
|
|
11238
|
+
runId,
|
|
11239
|
+
videoUploaded,
|
|
11240
|
+
contentUploaded: result.uploaded,
|
|
11241
|
+
errors: result.errors
|
|
11242
|
+
};
|
|
11243
|
+
}
|
|
11244
|
+
async function pullConfig2(targetDir) {
|
|
11245
|
+
return pullConfig(targetDir);
|
|
11246
|
+
}
|
|
11247
|
+
async function pushConfig2(sourceDir) {
|
|
11248
|
+
return pushConfig(sourceDir);
|
|
11249
|
+
}
|
|
11250
|
+
async function migrateLocalContent2(outputDir) {
|
|
11251
|
+
return migrateLocalContent(outputDir);
|
|
11252
|
+
}
|
|
11253
|
+
var init_cloudStorageOperations = __esm({
|
|
11254
|
+
"src/L4-agents/cloudStorage/cloudStorageOperations.ts"() {
|
|
11255
|
+
"use strict";
|
|
11256
|
+
init_configLogger();
|
|
11257
|
+
init_azureStorageService();
|
|
11258
|
+
init_azureConfigService();
|
|
10171
11259
|
}
|
|
10172
11260
|
});
|
|
10173
11261
|
|
|
@@ -10280,324 +11368,19 @@ async function loadAndValidateIdea(issueNumber) {
|
|
|
10280
11368
|
return idea;
|
|
10281
11369
|
}
|
|
10282
11370
|
|
|
10283
|
-
// src/L2-clients/late/lateApi.ts
|
|
10284
|
-
init_environment();
|
|
10285
|
-
init_configLogger();
|
|
10286
|
-
init_fileSystem();
|
|
10287
|
-
|
|
10288
|
-
// src/L1-infra/http/network.ts
|
|
10289
|
-
import { Readable } from "stream";
|
|
10290
|
-
|
|
10291
|
-
// src/L2-clients/late/lateApi.ts
|
|
10292
|
-
init_paths();
|
|
10293
|
-
init_httpClient();
|
|
10294
|
-
var LateApiClient = class {
|
|
10295
|
-
baseUrl = "https://getlate.dev/api/v1";
|
|
10296
|
-
apiKey;
|
|
10297
|
-
constructor(apiKey) {
|
|
10298
|
-
this.apiKey = apiKey ?? getConfig().LATE_API_KEY;
|
|
10299
|
-
if (!this.apiKey) {
|
|
10300
|
-
throw new Error("LATE_API_KEY is required \u2014 set it in environment or pass to constructor");
|
|
10301
|
-
}
|
|
10302
|
-
}
|
|
10303
|
-
// ── Private request helper ───────────────────────────────────────────
|
|
10304
|
-
async request(endpoint, options = {}, retries = 3) {
|
|
10305
|
-
const url = `${this.baseUrl}${endpoint}`;
|
|
10306
|
-
const headers = {
|
|
10307
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
10308
|
-
...options.headers
|
|
10309
|
-
};
|
|
10310
|
-
if (!(options.body instanceof FormData)) {
|
|
10311
|
-
headers["Content-Type"] = "application/json";
|
|
10312
|
-
}
|
|
10313
|
-
logger_default.debug(`Late API ${options.method ?? "GET"} ${endpoint}`);
|
|
10314
|
-
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
10315
|
-
const response = await fetchRaw(url, { ...options, headers });
|
|
10316
|
-
if (response.ok) {
|
|
10317
|
-
if (response.status === 204) return void 0;
|
|
10318
|
-
return await response.json();
|
|
10319
|
-
}
|
|
10320
|
-
if (response.status === 429 && attempt < retries) {
|
|
10321
|
-
const retryAfter = Number(response.headers.get("Retry-After")) || 2;
|
|
10322
|
-
logger_default.warn(`Late API rate limited, retrying in ${retryAfter}s (attempt ${attempt}/${retries})`);
|
|
10323
|
-
await new Promise((r) => setTimeout(r, retryAfter * 1e3));
|
|
10324
|
-
continue;
|
|
10325
|
-
}
|
|
10326
|
-
if (response.status === 401) {
|
|
10327
|
-
throw new Error(
|
|
10328
|
-
"Late API authentication failed (401). Check that LATE_API_KEY is valid."
|
|
10329
|
-
);
|
|
10330
|
-
}
|
|
10331
|
-
const body = await response.text().catch(() => "<no body>");
|
|
10332
|
-
throw new Error(
|
|
10333
|
-
`Late API error ${response.status} ${options.method ?? "GET"} ${endpoint}: ${body}`
|
|
10334
|
-
);
|
|
10335
|
-
}
|
|
10336
|
-
throw new Error(`Late API request failed after ${retries} retries`);
|
|
10337
|
-
}
|
|
10338
|
-
// ── Core methods ─────────────────────────────────────────────────────
|
|
10339
|
-
async listProfiles() {
|
|
10340
|
-
const data = await this.request("/profiles");
|
|
10341
|
-
return data.profiles ?? [];
|
|
10342
|
-
}
|
|
10343
|
-
async listAccounts() {
|
|
10344
|
-
const data = await this.request("/accounts");
|
|
10345
|
-
return data.accounts ?? [];
|
|
10346
|
-
}
|
|
10347
|
-
async getScheduledPosts(platform) {
|
|
10348
|
-
return this.listPosts({ status: "scheduled", platform });
|
|
10349
|
-
}
|
|
10350
|
-
async getDraftPosts(platform) {
|
|
10351
|
-
return this.listPosts({ status: "draft", platform });
|
|
10352
|
-
}
|
|
10353
|
-
async createPost(params) {
|
|
10354
|
-
const data = await this.request("/posts", {
|
|
10355
|
-
method: "POST",
|
|
10356
|
-
body: JSON.stringify(params)
|
|
10357
|
-
});
|
|
10358
|
-
return data.post;
|
|
10359
|
-
}
|
|
10360
|
-
async deletePost(postId) {
|
|
10361
|
-
await this.request(`/posts/${encodeURIComponent(postId)}`, {
|
|
10362
|
-
method: "DELETE"
|
|
10363
|
-
});
|
|
10364
|
-
}
|
|
10365
|
-
async updatePost(postId, updates) {
|
|
10366
|
-
const data = await this.request(`/posts/${encodeURIComponent(postId)}`, {
|
|
10367
|
-
method: "PUT",
|
|
10368
|
-
body: JSON.stringify(updates)
|
|
10369
|
-
});
|
|
10370
|
-
return data.post;
|
|
10371
|
-
}
|
|
10372
|
-
/** Reschedule a post and ensure it transitions out of draft status. */
|
|
10373
|
-
async schedulePost(postId, scheduledFor) {
|
|
10374
|
-
return this.updatePost(postId, { scheduledFor, isDraft: false });
|
|
10375
|
-
}
|
|
10376
|
-
async uploadMedia(filePath) {
|
|
10377
|
-
const fileStats = await getFileStats(filePath);
|
|
10378
|
-
const fileName = basename(filePath);
|
|
10379
|
-
const ext = extname(fileName).toLowerCase();
|
|
10380
|
-
const contentType = ext === ".mp4" ? "video/mp4" : ext === ".webm" ? "video/webm" : ext === ".mov" ? "video/quicktime" : "video/mp4";
|
|
10381
|
-
logger_default.info(`Late API uploading ${String(fileName).replace(/[\r\n]/g, "")} (${(fileStats.size / 1024 / 1024).toFixed(1)} MB)`);
|
|
10382
|
-
const presign = await this.request("/media/presign", {
|
|
10383
|
-
method: "POST",
|
|
10384
|
-
body: JSON.stringify({ filename: fileName, contentType })
|
|
10385
|
-
});
|
|
10386
|
-
logger_default.debug(`Late API presigned URL obtained for ${String(fileName).replace(/[\r\n]/g, "")} (expires in ${presign.expiresIn}s)`);
|
|
10387
|
-
const nodeStream = openReadStream(filePath);
|
|
10388
|
-
try {
|
|
10389
|
-
const webStream = Readable.toWeb(nodeStream);
|
|
10390
|
-
const uploadResp = await fetchRaw(presign.uploadUrl, {
|
|
10391
|
-
method: "PUT",
|
|
10392
|
-
headers: {
|
|
10393
|
-
"Content-Type": contentType,
|
|
10394
|
-
"Content-Length": String(fileStats.size)
|
|
10395
|
-
},
|
|
10396
|
-
body: webStream,
|
|
10397
|
-
// Node.js-specific property for streaming request bodies (not in standard RequestInit type)
|
|
10398
|
-
duplex: "half"
|
|
10399
|
-
});
|
|
10400
|
-
if (!uploadResp.ok) {
|
|
10401
|
-
throw new Error(`Late media upload failed: ${uploadResp.status} ${uploadResp.statusText}`);
|
|
10402
|
-
}
|
|
10403
|
-
} finally {
|
|
10404
|
-
nodeStream.destroy();
|
|
10405
|
-
}
|
|
10406
|
-
logger_default.debug(`Late API media uploaded \u2192 ${presign.publicUrl}`);
|
|
10407
|
-
const type = contentType.startsWith("image/") ? "image" : "video";
|
|
10408
|
-
return { url: presign.publicUrl, type };
|
|
10409
|
-
}
|
|
10410
|
-
/**
|
|
10411
|
-
* Fetch posts with pagination, iterating pages until all results are collected.
|
|
10412
|
-
* Supports filtering by status and platform.
|
|
10413
|
-
*/
|
|
10414
|
-
async listPosts(options = {}) {
|
|
10415
|
-
const limit = options.limit ?? 100;
|
|
10416
|
-
const allPosts = [];
|
|
10417
|
-
let page = 1;
|
|
10418
|
-
while (true) {
|
|
10419
|
-
const params = new URLSearchParams();
|
|
10420
|
-
if (options.status) params.set("status", options.status);
|
|
10421
|
-
if (options.platform) params.set("platform", options.platform);
|
|
10422
|
-
params.set("limit", String(limit));
|
|
10423
|
-
params.set("page", String(page));
|
|
10424
|
-
const data = await this.request(
|
|
10425
|
-
`/posts?${params}`
|
|
10426
|
-
);
|
|
10427
|
-
const posts = data.posts ?? data.data ?? [];
|
|
10428
|
-
allPosts.push(...posts);
|
|
10429
|
-
if (posts.length < limit) break;
|
|
10430
|
-
page++;
|
|
10431
|
-
}
|
|
10432
|
-
return allPosts;
|
|
10433
|
-
}
|
|
10434
|
-
// ── Queue management ─────────────────────────────────────────────────
|
|
10435
|
-
async listQueues(profileId, all = false) {
|
|
10436
|
-
const params = new URLSearchParams({ profileId });
|
|
10437
|
-
if (all) params.set("all", "true");
|
|
10438
|
-
return this.request(`/queue/slots?${params}`);
|
|
10439
|
-
}
|
|
10440
|
-
async createQueue(params) {
|
|
10441
|
-
return this.request("/queue/slots", { method: "POST", body: JSON.stringify(params) });
|
|
10442
|
-
}
|
|
10443
|
-
async updateQueue(params) {
|
|
10444
|
-
return this.request("/queue/slots", { method: "PUT", body: JSON.stringify(params) });
|
|
10445
|
-
}
|
|
10446
|
-
async deleteQueue(profileId, queueId) {
|
|
10447
|
-
return this.request(`/queue/slots?profileId=${profileId}&queueId=${queueId}`, { method: "DELETE" });
|
|
10448
|
-
}
|
|
10449
|
-
async previewQueue(profileId, queueId, count = 20) {
|
|
10450
|
-
const params = new URLSearchParams({ profileId, count: String(count) });
|
|
10451
|
-
if (queueId) params.set("queueId", queueId);
|
|
10452
|
-
return this.request(`/queue/preview?${params}`);
|
|
10453
|
-
}
|
|
10454
|
-
async getNextQueueSlot(profileId, queueId) {
|
|
10455
|
-
const params = new URLSearchParams({ profileId });
|
|
10456
|
-
if (queueId) params.set("queueId", queueId);
|
|
10457
|
-
return this.request(`/queue/next-slot?${params}`);
|
|
10458
|
-
}
|
|
10459
|
-
// ── Helper ───────────────────────────────────────────────────────────
|
|
10460
|
-
async validateConnection() {
|
|
10461
|
-
try {
|
|
10462
|
-
const profiles = await this.listProfiles();
|
|
10463
|
-
const name = profiles[0]?.name;
|
|
10464
|
-
logger_default.info(`Late API connection valid \u2014 profile: ${name ?? "unknown"}`);
|
|
10465
|
-
return { valid: true, profileName: name };
|
|
10466
|
-
} catch (err) {
|
|
10467
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
10468
|
-
logger_default.error(`Late API connection failed: ${message}`);
|
|
10469
|
-
return { valid: false, error: message };
|
|
10470
|
-
}
|
|
10471
|
-
}
|
|
10472
|
-
};
|
|
10473
|
-
|
|
10474
11371
|
// src/L3-services/lateApi/lateApiService.ts
|
|
11372
|
+
init_lateApi();
|
|
11373
|
+
init_configLogger();
|
|
11374
|
+
init_queueMapping();
|
|
10475
11375
|
function createLateApiClient(...args) {
|
|
10476
11376
|
return new LateApiClient(...args);
|
|
10477
11377
|
}
|
|
10478
11378
|
|
|
10479
|
-
// src/
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
10485
|
-
var memoryCache = null;
|
|
10486
|
-
function cachePath() {
|
|
10487
|
-
return join(process.cwd(), CACHE_FILE);
|
|
10488
|
-
}
|
|
10489
|
-
function isCacheValid(cache) {
|
|
10490
|
-
const fetchedAtTime = new Date(cache.fetchedAt).getTime();
|
|
10491
|
-
if (Number.isNaN(fetchedAtTime)) {
|
|
10492
|
-
logger_default.warn("Invalid fetchedAt in queue cache; treating as stale", {
|
|
10493
|
-
fetchedAt: cache.fetchedAt
|
|
10494
|
-
});
|
|
10495
|
-
return false;
|
|
10496
|
-
}
|
|
10497
|
-
const age = Date.now() - fetchedAtTime;
|
|
10498
|
-
return age < CACHE_TTL_MS;
|
|
10499
|
-
}
|
|
10500
|
-
async function readFileCache() {
|
|
10501
|
-
try {
|
|
10502
|
-
const raw = await readTextFile(cachePath());
|
|
10503
|
-
const cache = JSON.parse(raw);
|
|
10504
|
-
if (cache.mappings && cache.profileId && cache.fetchedAt && isCacheValid(cache)) {
|
|
10505
|
-
return cache;
|
|
10506
|
-
}
|
|
10507
|
-
return null;
|
|
10508
|
-
} catch {
|
|
10509
|
-
return null;
|
|
10510
|
-
}
|
|
10511
|
-
}
|
|
10512
|
-
async function writeFileCache(cache) {
|
|
10513
|
-
try {
|
|
10514
|
-
if (!cache || typeof cache !== "object" || !cache.mappings || !cache.profileId || !cache.fetchedAt) {
|
|
10515
|
-
logger_default.warn("Invalid queue cache structure, skipping write");
|
|
10516
|
-
return;
|
|
10517
|
-
}
|
|
10518
|
-
const sanitized = {
|
|
10519
|
-
mappings: typeof cache.mappings === "object" ? { ...cache.mappings } : {},
|
|
10520
|
-
profileId: String(cache.profileId),
|
|
10521
|
-
fetchedAt: String(cache.fetchedAt)
|
|
10522
|
-
};
|
|
10523
|
-
for (const [name, id] of Object.entries(sanitized.mappings)) {
|
|
10524
|
-
if (typeof name !== "string" || typeof id !== "string" || /[\x00-\x1f]/.test(name) || /[\x00-\x1f]/.test(id)) {
|
|
10525
|
-
logger_default.warn("Invalid queue mapping data from API, skipping cache write");
|
|
10526
|
-
return;
|
|
10527
|
-
}
|
|
10528
|
-
}
|
|
10529
|
-
const resolvedCachePath = resolve(cachePath());
|
|
10530
|
-
if (!resolvedCachePath.startsWith(resolve(process.cwd()) + sep)) {
|
|
10531
|
-
throw new Error("Cache path outside working directory");
|
|
10532
|
-
}
|
|
10533
|
-
await writeTextFile(resolvedCachePath, JSON.stringify(sanitized, null, 2));
|
|
10534
|
-
} catch (err) {
|
|
10535
|
-
logger_default.warn("Failed to write queue cache file", { error: err });
|
|
10536
|
-
}
|
|
10537
|
-
}
|
|
10538
|
-
async function fetchAndCache() {
|
|
10539
|
-
const client = new LateApiClient();
|
|
10540
|
-
const profiles = await client.listProfiles();
|
|
10541
|
-
if (profiles.length === 0) {
|
|
10542
|
-
logger_default.warn("No Late API profiles found \u2014 queue mappings will be empty");
|
|
10543
|
-
const emptyCache = {
|
|
10544
|
-
mappings: {},
|
|
10545
|
-
profileId: "",
|
|
10546
|
-
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10547
|
-
};
|
|
10548
|
-
memoryCache = emptyCache;
|
|
10549
|
-
return emptyCache;
|
|
10550
|
-
}
|
|
10551
|
-
const profileId = profiles[0]._id;
|
|
10552
|
-
const { queues } = await client.listQueues(profileId, true);
|
|
10553
|
-
if (queues.length === 0) {
|
|
10554
|
-
logger_default.warn(
|
|
10555
|
-
"No queues found in Late API \u2014 run `vidpipe sync-queues` to create platform queues"
|
|
10556
|
-
);
|
|
10557
|
-
}
|
|
10558
|
-
const mappings = {};
|
|
10559
|
-
for (const queue of queues) {
|
|
10560
|
-
mappings[queue.name] = queue._id;
|
|
10561
|
-
}
|
|
10562
|
-
const cache = {
|
|
10563
|
-
mappings,
|
|
10564
|
-
profileId,
|
|
10565
|
-
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10566
|
-
};
|
|
10567
|
-
memoryCache = cache;
|
|
10568
|
-
await writeFileCache(cache);
|
|
10569
|
-
logger_default.info("Refreshed Late queue mappings", {
|
|
10570
|
-
queueCount: queues.length,
|
|
10571
|
-
queues: Object.keys(mappings)
|
|
10572
|
-
});
|
|
10573
|
-
return cache;
|
|
10574
|
-
}
|
|
10575
|
-
async function ensureMappings() {
|
|
10576
|
-
if (memoryCache && isCacheValid(memoryCache)) {
|
|
10577
|
-
return memoryCache;
|
|
10578
|
-
}
|
|
10579
|
-
const fileCache = await readFileCache();
|
|
10580
|
-
if (fileCache) {
|
|
10581
|
-
memoryCache = fileCache;
|
|
10582
|
-
return fileCache;
|
|
10583
|
-
}
|
|
10584
|
-
try {
|
|
10585
|
-
return await fetchAndCache();
|
|
10586
|
-
} catch (err) {
|
|
10587
|
-
logger_default.error("Failed to fetch Late queue mappings", { error: err });
|
|
10588
|
-
return { mappings: {}, profileId: "", fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
10589
|
-
}
|
|
10590
|
-
}
|
|
10591
|
-
async function getQueueId(platform, clipType) {
|
|
10592
|
-
const cache = await ensureMappings();
|
|
10593
|
-
const normalizedPlatform = platform === "twitter" ? "x" : platform;
|
|
10594
|
-
const queueName = `${normalizedPlatform}-${clipType}`;
|
|
10595
|
-
return cache.mappings[queueName] ?? null;
|
|
10596
|
-
}
|
|
10597
|
-
async function getProfileId() {
|
|
10598
|
-
const cache = await ensureMappings();
|
|
10599
|
-
return cache.profileId;
|
|
10600
|
-
}
|
|
11379
|
+
// src/L7-app/sdk/VidPipeSDK.ts
|
|
11380
|
+
init_queueMapping();
|
|
11381
|
+
|
|
11382
|
+
// src/L3-services/scheduler/realign.ts
|
|
11383
|
+
init_lateApi();
|
|
10601
11384
|
|
|
10602
11385
|
// src/L2-clients/scheduleStore/scheduleStore.ts
|
|
10603
11386
|
init_fileSystem();
|
|
@@ -11053,6 +11836,7 @@ async function itemExists(id) {
|
|
|
11053
11836
|
init_configLogger();
|
|
11054
11837
|
|
|
11055
11838
|
// src/L3-services/scheduler/scheduler.ts
|
|
11839
|
+
init_lateApi();
|
|
11056
11840
|
init_configLogger();
|
|
11057
11841
|
var MAX_LOOKAHEAD_DAYS = 730;
|
|
11058
11842
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -12023,6 +12807,7 @@ function buildPublishQueue2(...args) {
|
|
|
12023
12807
|
|
|
12024
12808
|
// src/L4-agents/ScheduleAgent.ts
|
|
12025
12809
|
init_BaseAgent();
|
|
12810
|
+
init_queueMapping();
|
|
12026
12811
|
init_configLogger();
|
|
12027
12812
|
|
|
12028
12813
|
// src/L4-agents/IdeationAgent.ts
|
|
@@ -14311,6 +15096,21 @@ Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour,
|
|
|
14311
15096
|
Style: Default,Montserrat,120,&H00FFFFFF,&H0000FFFF,&H00000000,&H80000000,1,0,0,0,100,100,0,0,1,3,1,2,30,30,770,1
|
|
14312
15097
|
Style: Hook,Montserrat,56,&H00333333,&H00333333,&H60D0D0D0,&H60E0E0E0,1,0,0,0,100,100,2,0,3,18,2,8,80,80,250,1
|
|
14313
15098
|
|
|
15099
|
+
[Events]
|
|
15100
|
+
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
15101
|
+
`;
|
|
15102
|
+
var ASS_HEADER_PORTRAIT_LOWER = `[Script Info]
|
|
15103
|
+
Title: Auto-generated captions
|
|
15104
|
+
ScriptType: v4.00+
|
|
15105
|
+
PlayResX: 1080
|
|
15106
|
+
PlayResY: 1920
|
|
15107
|
+
WrapStyle: 0
|
|
15108
|
+
|
|
15109
|
+
[V4+ Styles]
|
|
15110
|
+
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
|
15111
|
+
Style: Default,Montserrat,120,&H00FFFFFF,&H0000FFFF,&H00000000,&H80000000,1,0,0,0,100,100,0,0,1,3,1,2,30,30,280,1
|
|
15112
|
+
Style: Hook,Montserrat,56,&H00333333,&H00333333,&H60D0D0D0,&H60E0E0E0,1,0,0,0,100,100,2,0,3,18,2,8,80,80,250,1
|
|
15113
|
+
|
|
14314
15114
|
[Events]
|
|
14315
15115
|
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
|
|
14316
15116
|
`;
|
|
@@ -14387,14 +15187,26 @@ function buildPremiumDialogueLines(words, style = "shorts") {
|
|
|
14387
15187
|
}
|
|
14388
15188
|
return dialogues;
|
|
14389
15189
|
}
|
|
15190
|
+
function getASSHeader(style) {
|
|
15191
|
+
switch (style) {
|
|
15192
|
+
case "portrait":
|
|
15193
|
+
return ASS_HEADER_PORTRAIT;
|
|
15194
|
+
case "portrait-lower":
|
|
15195
|
+
return ASS_HEADER_PORTRAIT_LOWER;
|
|
15196
|
+
case "medium":
|
|
15197
|
+
return ASS_HEADER_MEDIUM;
|
|
15198
|
+
default:
|
|
15199
|
+
return ASS_HEADER;
|
|
15200
|
+
}
|
|
15201
|
+
}
|
|
14390
15202
|
function generateStyledASS(transcript, style = "shorts") {
|
|
14391
|
-
const header = style
|
|
15203
|
+
const header = getASSHeader(style);
|
|
14392
15204
|
const allWords = transcript.words;
|
|
14393
15205
|
if (allWords.length === 0) return header;
|
|
14394
15206
|
return header + buildPremiumDialogueLines(allWords, style).join("\n") + "\n";
|
|
14395
15207
|
}
|
|
14396
15208
|
function generateStyledASSForSegment(transcript, startTime, endTime, buffer = 1, style = "shorts") {
|
|
14397
|
-
const header = style
|
|
15209
|
+
const header = getASSHeader(style);
|
|
14398
15210
|
const bufferedStart = Math.max(0, startTime - buffer);
|
|
14399
15211
|
const bufferedEnd = endTime + buffer;
|
|
14400
15212
|
const words = transcript.words.filter(
|
|
@@ -14409,7 +15221,7 @@ function generateStyledASSForSegment(transcript, startTime, endTime, buffer = 1,
|
|
|
14409
15221
|
return header + buildPremiumDialogueLines(adjusted, style).join("\n") + "\n";
|
|
14410
15222
|
}
|
|
14411
15223
|
function generateStyledASSForComposite(transcript, segments, buffer = 1, style = "shorts") {
|
|
14412
|
-
const header = style
|
|
15224
|
+
const header = getASSHeader(style);
|
|
14413
15225
|
const allAdjusted = [];
|
|
14414
15226
|
let runningOffset = 0;
|
|
14415
15227
|
for (const seg of segments) {
|
|
@@ -14436,14 +15248,16 @@ function generateHookOverlay(hookText, displayDuration = 4, _style = "portrait")
|
|
|
14436
15248
|
const text = hookText.length > HOOK_TEXT_MAX_LENGTH ? hookText.slice(0, HOOK_TEXT_MAX_LENGTH - 3) + "..." : hookText;
|
|
14437
15249
|
return `Dialogue: 1,${toASS(0)},${toASS(displayDuration)},Hook,,0,0,0,,{\\fad(300,500)}${text}`;
|
|
14438
15250
|
}
|
|
14439
|
-
function generatePortraitASSWithHook(transcript, hookText, startTime, endTime, buffer) {
|
|
14440
|
-
const
|
|
14441
|
-
const
|
|
15251
|
+
function generatePortraitASSWithHook(transcript, hookText, startTime, endTime, buffer, isSplitScreen = true) {
|
|
15252
|
+
const style = isSplitScreen ? "portrait" : "portrait-lower";
|
|
15253
|
+
const baseASS = generateStyledASSForSegment(transcript, startTime, endTime, buffer, style);
|
|
15254
|
+
const hookLine = generateHookOverlay(hookText, 4, style);
|
|
14442
15255
|
return baseASS + hookLine + "\n";
|
|
14443
15256
|
}
|
|
14444
|
-
function generatePortraitASSWithHookComposite(transcript, segments, hookText, buffer) {
|
|
14445
|
-
const
|
|
14446
|
-
const
|
|
15257
|
+
function generatePortraitASSWithHookComposite(transcript, segments, hookText, buffer, isSplitScreen = true) {
|
|
15258
|
+
const style = isSplitScreen ? "portrait" : "portrait-lower";
|
|
15259
|
+
const baseASS = generateStyledASSForComposite(transcript, segments, buffer, style);
|
|
15260
|
+
const hookLine = generateHookOverlay(hookText, 4, style);
|
|
14447
15261
|
return baseASS + hookLine + "\n";
|
|
14448
15262
|
}
|
|
14449
15263
|
|
|
@@ -14736,7 +15550,7 @@ init_configLogger();
|
|
|
14736
15550
|
init_brand();
|
|
14737
15551
|
var MAX_FILE_SIZE_MB = 25;
|
|
14738
15552
|
var WARN_FILE_SIZE_MB = 20;
|
|
14739
|
-
var
|
|
15553
|
+
var MAX_RETRIES2 = 3;
|
|
14740
15554
|
var RETRY_DELAY_MS = 5e3;
|
|
14741
15555
|
async function transcribeAudio(audioPath) {
|
|
14742
15556
|
logger_default.info(`Starting Whisper transcription: ${audioPath}`);
|
|
@@ -14758,7 +15572,7 @@ async function transcribeAudio(audioPath) {
|
|
|
14758
15572
|
try {
|
|
14759
15573
|
const prompt = getWhisperPrompt();
|
|
14760
15574
|
let response;
|
|
14761
|
-
for (let attempt = 1; attempt <=
|
|
15575
|
+
for (let attempt = 1; attempt <= MAX_RETRIES2; attempt++) {
|
|
14762
15576
|
try {
|
|
14763
15577
|
response = await openai.audio.transcriptions.create({
|
|
14764
15578
|
model: "whisper-1",
|
|
@@ -14771,9 +15585,9 @@ async function transcribeAudio(audioPath) {
|
|
|
14771
15585
|
} catch (retryError) {
|
|
14772
15586
|
const status = typeof retryError === "object" && retryError !== null && "status" in retryError ? retryError.status : void 0;
|
|
14773
15587
|
if (status === 401 || status === 400 || status === 429) throw retryError;
|
|
14774
|
-
if (attempt ===
|
|
15588
|
+
if (attempt === MAX_RETRIES2) throw retryError;
|
|
14775
15589
|
const msg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
14776
|
-
logger_default.warn(`Whisper attempt ${attempt}/${
|
|
15590
|
+
logger_default.warn(`Whisper attempt ${attempt}/${MAX_RETRIES2} failed: ${msg} \u2014 retrying in ${RETRY_DELAY_MS / 1e3}s`);
|
|
14777
15591
|
await new Promise((resolve3) => setTimeout(resolve3, RETRY_DELAY_MS));
|
|
14778
15592
|
}
|
|
14779
15593
|
}
|
|
@@ -16018,6 +16832,19 @@ init_videoOperations();
|
|
|
16018
16832
|
init_fileSystem();
|
|
16019
16833
|
init_paths();
|
|
16020
16834
|
init_configLogger();
|
|
16835
|
+
function mapVariantResults(results) {
|
|
16836
|
+
return results.map((v) => ({
|
|
16837
|
+
path: v.path,
|
|
16838
|
+
aspectRatio: v.aspectRatio,
|
|
16839
|
+
platform: v.platform,
|
|
16840
|
+
width: v.width,
|
|
16841
|
+
height: v.height,
|
|
16842
|
+
isSplitScreen: v.isSplitScreen
|
|
16843
|
+
}));
|
|
16844
|
+
}
|
|
16845
|
+
function buildPortraitCaptionASS(transcript, segments, hookText, isSplitScreen) {
|
|
16846
|
+
return segments.length === 1 ? generatePortraitASSWithHook(transcript, hookText, segments[0].start, segments[0].end, void 0, isSplitScreen) : generatePortraitASSWithHookComposite(transcript, segments, hookText, void 0, isSplitScreen);
|
|
16847
|
+
}
|
|
16021
16848
|
function buildShortsSystemPrompt(clipConfig) {
|
|
16022
16849
|
const minDuration = clipConfig?.duration?.min ?? 15;
|
|
16023
16850
|
const maxDuration = clipConfig?.duration?.max ?? 60;
|
|
@@ -16415,13 +17242,7 @@ Words: ${words}`;
|
|
|
16415
17242
|
const defaultPlatforms = ["tiktok", "youtube-shorts", "instagram-reels", "instagram-feed", "linkedin"];
|
|
16416
17243
|
const results = await generatePlatformVariants2(outputPath, shortsDir, shortSlug, defaultPlatforms, { webcamOverride });
|
|
16417
17244
|
if (results.length > 0) {
|
|
16418
|
-
clipVariants = results
|
|
16419
|
-
path: v.path,
|
|
16420
|
-
aspectRatio: v.aspectRatio,
|
|
16421
|
-
platform: v.platform,
|
|
16422
|
-
width: v.width,
|
|
16423
|
-
height: v.height
|
|
16424
|
-
}));
|
|
17245
|
+
clipVariants = mapVariantResults(results);
|
|
16425
17246
|
logger_default.info(`[ShortsAgent] Generated ${clipVariants.length} platform variants for: ${plan.title}`);
|
|
16426
17247
|
}
|
|
16427
17248
|
} catch (err) {
|
|
@@ -16449,7 +17270,8 @@ Words: ${words}`;
|
|
|
16449
17270
|
if (portraitVariants.length > 0) {
|
|
16450
17271
|
try {
|
|
16451
17272
|
const hookText = plan.hook ?? plan.title;
|
|
16452
|
-
const
|
|
17273
|
+
const isSplitScreen = portraitVariants[0].isSplitScreen ?? false;
|
|
17274
|
+
const portraitAssContent = buildPortraitCaptionASS(transcript, segments, hookText, isSplitScreen);
|
|
16453
17275
|
const portraitAssPath = join(shortsDir, `${shortSlug}-portrait.ass`);
|
|
16454
17276
|
await writeTextFile(portraitAssPath, portraitAssContent);
|
|
16455
17277
|
const portraitCaptionedPath = portraitVariants[0].path.replace(".mp4", "-captioned.mp4");
|
|
@@ -19455,6 +20277,16 @@ var MainVideoAsset = class _MainVideoAsset extends VideoAsset {
|
|
|
19455
20277
|
}
|
|
19456
20278
|
};
|
|
19457
20279
|
|
|
20280
|
+
// src/L5-assets/bridges/cloudStorageBridge.ts
|
|
20281
|
+
async function uploadToCloud(inputVideoPath, publishQueueDir, videoSlug, metadata) {
|
|
20282
|
+
const { uploadPipelineResults: uploadPipelineResults2 } = await Promise.resolve().then(() => (init_cloudStorageOperations(), cloudStorageOperations_exports));
|
|
20283
|
+
return uploadPipelineResults2(inputVideoPath, publishQueueDir, videoSlug, metadata);
|
|
20284
|
+
}
|
|
20285
|
+
async function isCloudEnabled2() {
|
|
20286
|
+
const { isCloudEnabled: check } = await Promise.resolve().then(() => (init_cloudStorageOperations(), cloudStorageOperations_exports));
|
|
20287
|
+
return check();
|
|
20288
|
+
}
|
|
20289
|
+
|
|
19458
20290
|
// src/L6-pipeline/pipeline.ts
|
|
19459
20291
|
init_types2();
|
|
19460
20292
|
async function runStage(stageName, fn, stageResults) {
|
|
@@ -19735,6 +20567,23 @@ async function processVideo(videoPath, ideas, publishBy, spec) {
|
|
|
19735
20567
|
skipStage("queue-build" /* QueueBuild */, "NO_SOCIAL_POSTS");
|
|
19736
20568
|
}
|
|
19737
20569
|
const blogPost = await trackStage("blog" /* Blog */, () => asset.getBlog());
|
|
20570
|
+
await trackStage("cloud-upload" /* CloudUpload */, async () => {
|
|
20571
|
+
const cloudEnabled = await isCloudEnabled2();
|
|
20572
|
+
if (!cloudEnabled) {
|
|
20573
|
+
logger_default.info("Cloud upload skipped \u2014 Azure storage not configured");
|
|
20574
|
+
return;
|
|
20575
|
+
}
|
|
20576
|
+
const publishQueueDir = join(cfg.OUTPUT_DIR, "publish-queue");
|
|
20577
|
+
const result = await uploadToCloud(videoPath, publishQueueDir, video.slug, {
|
|
20578
|
+
originalFilename: video.filename,
|
|
20579
|
+
duration: video.duration,
|
|
20580
|
+
size: video.size
|
|
20581
|
+
});
|
|
20582
|
+
logger_default.info(`Cloud upload complete \u2014 runId: ${result.runId}, items uploaded: ${result.contentUploaded}, video: ${result.videoUploaded}`);
|
|
20583
|
+
if (result.errors.length > 0) {
|
|
20584
|
+
logger_default.warn(`Cloud upload had ${result.errors.length} error(s): ${result.errors.join("; ")}`);
|
|
20585
|
+
}
|
|
20586
|
+
});
|
|
19738
20587
|
const totalDuration = Date.now() - pipelineStart;
|
|
19739
20588
|
const report = costTracker3.getReport();
|
|
19740
20589
|
if (report.records.length > 0) {
|
|
@@ -19848,7 +20697,9 @@ var credentialKeys = [
|
|
|
19848
20697
|
"perplexityApiKey",
|
|
19849
20698
|
"lateApiKey",
|
|
19850
20699
|
"githubToken",
|
|
19851
|
-
"geminiApiKey"
|
|
20700
|
+
"geminiApiKey",
|
|
20701
|
+
"azureStorageAccountName",
|
|
20702
|
+
"azureStorageAccountKey"
|
|
19852
20703
|
];
|
|
19853
20704
|
var defaultKeys = [
|
|
19854
20705
|
"llmProvider",
|
|
@@ -20179,8 +21030,8 @@ function mapVariantPlatforms(platforms) {
|
|
|
20179
21030
|
});
|
|
20180
21031
|
}
|
|
20181
21032
|
function getVariantSlug(videoPath) {
|
|
20182
|
-
const
|
|
20183
|
-
return
|
|
21033
|
+
const basename3 = videoPath.split(/[\\/]/).pop() ?? videoPath;
|
|
21034
|
+
return basename3.replace(/\.[^.]+$/, "");
|
|
20184
21035
|
}
|
|
20185
21036
|
function buildDiagnosticStatus(required, passed) {
|
|
20186
21037
|
if (passed) {
|
|
@@ -20548,6 +21399,88 @@ function createVidPipe(sdkConfig) {
|
|
|
20548
21399
|
path() {
|
|
20549
21400
|
return getConfigPath();
|
|
20550
21401
|
}
|
|
21402
|
+
},
|
|
21403
|
+
cloud: {
|
|
21404
|
+
async process(videoPath, options) {
|
|
21405
|
+
const { uploadVideoFile: uploadVideoFile2, isAzureConfigured: isAzureConfigured3, getRunId: getRunId2 } = await Promise.resolve().then(() => (init_azureStorageService(), azureStorageService_exports));
|
|
21406
|
+
if (!isAzureConfigured3()) {
|
|
21407
|
+
throw new Error("Azure Storage not configured");
|
|
21408
|
+
}
|
|
21409
|
+
const { basename: getBasename } = await import("path");
|
|
21410
|
+
const { stat: getFileStat } = await import("fs/promises");
|
|
21411
|
+
const filename = getBasename(videoPath);
|
|
21412
|
+
const runId = getRunId2();
|
|
21413
|
+
const blobPath = `raw/${runId}-${filename}`;
|
|
21414
|
+
await getFileStat(videoPath);
|
|
21415
|
+
await uploadVideoFile2(videoPath, blobPath);
|
|
21416
|
+
let workflowTriggered = false;
|
|
21417
|
+
try {
|
|
21418
|
+
const repo = options?.repo || "htekdev/vidpipe";
|
|
21419
|
+
const args = ["workflow", "run", "process-video.yml", "--repo", repo, "-f", `video_url=blob://${blobPath}`];
|
|
21420
|
+
if (options?.spec) args.push("-f", `spec=${options.spec}`);
|
|
21421
|
+
if (options?.ideas) args.push("-f", `ideas=${options.ideas}`);
|
|
21422
|
+
if (options?.publishBy) args.push("-f", `publish_by=${options.publishBy}`);
|
|
21423
|
+
const triggerResult = await spawnCommand("gh", args);
|
|
21424
|
+
if (triggerResult.error || triggerResult.status !== 0) {
|
|
21425
|
+
throw triggerResult.error ?? new Error(`gh workflow run failed with status ${String(triggerResult.status)}`);
|
|
21426
|
+
}
|
|
21427
|
+
workflowTriggered = true;
|
|
21428
|
+
} catch {
|
|
21429
|
+
}
|
|
21430
|
+
return { runId, blobPath, workflowTriggered };
|
|
21431
|
+
},
|
|
21432
|
+
async pushConfig() {
|
|
21433
|
+
const { dirname: getDirname } = await import("path");
|
|
21434
|
+
const config2 = getConfig();
|
|
21435
|
+
const vidpipeDir = getDirname(config2.OUTPUT_DIR);
|
|
21436
|
+
const { pushConfig: push } = await Promise.resolve().then(() => (init_azureConfigService(), azureConfigService_exports));
|
|
21437
|
+
return push(vidpipeDir);
|
|
21438
|
+
},
|
|
21439
|
+
async pullConfig() {
|
|
21440
|
+
const { dirname: getDirname } = await import("path");
|
|
21441
|
+
const config2 = getConfig();
|
|
21442
|
+
const vidpipeDir = getDirname(config2.OUTPUT_DIR);
|
|
21443
|
+
const { pullConfig: pull } = await Promise.resolve().then(() => (init_azureConfigService(), azureConfigService_exports));
|
|
21444
|
+
return pull(vidpipeDir);
|
|
21445
|
+
},
|
|
21446
|
+
async migrate() {
|
|
21447
|
+
const config2 = getConfig();
|
|
21448
|
+
const { migrateLocalContent: migrateLocalContent3 } = await Promise.resolve().then(() => (init_azureStorageService(), azureStorageService_exports));
|
|
21449
|
+
return migrateLocalContent3(config2.OUTPUT_DIR);
|
|
21450
|
+
},
|
|
21451
|
+
async download(videoUrl, outputPath) {
|
|
21452
|
+
if (videoUrl.startsWith("blob://")) {
|
|
21453
|
+
const blobPath = videoUrl.slice("blob://".length);
|
|
21454
|
+
const { downloadBlobToFile: downloadBlobToFile2 } = await Promise.resolve().then(() => (init_azureStorageService(), azureStorageService_exports));
|
|
21455
|
+
await downloadBlobToFile2(blobPath, outputPath);
|
|
21456
|
+
} else {
|
|
21457
|
+
const result = await spawnCommand("curl", ["-L", "--fail", "-o", outputPath, videoUrl]);
|
|
21458
|
+
if (result.status !== 0) {
|
|
21459
|
+
const stderr = typeof result.stderr === "string" ? result.stderr.trim() : "";
|
|
21460
|
+
throw new Error(
|
|
21461
|
+
stderr.length > 0 ? `curl download failed with exit code ${result.status}: ${stderr}` : `curl download failed with exit code ${result.status}`
|
|
21462
|
+
);
|
|
21463
|
+
}
|
|
21464
|
+
}
|
|
21465
|
+
},
|
|
21466
|
+
async status() {
|
|
21467
|
+
const { isAzureConfigured: isAzureConfigured3, getContentItems: getContentItems2, listVideos: listVideos2 } = await Promise.resolve().then(() => (init_azureStorageService(), azureStorageService_exports));
|
|
21468
|
+
const { listConfigFiles: listConfigFiles2 } = await Promise.resolve().then(() => (init_azureConfigService(), azureConfigService_exports));
|
|
21469
|
+
const configured = isAzureConfigured3();
|
|
21470
|
+
if (!configured) {
|
|
21471
|
+
return { configured, configFiles: 0, contentItems: 0, videos: 0 };
|
|
21472
|
+
}
|
|
21473
|
+
const [configFiles, contentItems, videos] = await Promise.all([
|
|
21474
|
+
listConfigFiles2(),
|
|
21475
|
+
getContentItems2(),
|
|
21476
|
+
listVideos2()
|
|
21477
|
+
]);
|
|
21478
|
+
return { configured, configFiles: configFiles.length, contentItems: contentItems.length, videos: videos.length };
|
|
21479
|
+
},
|
|
21480
|
+
isConfigured() {
|
|
21481
|
+
const config2 = getConfig();
|
|
21482
|
+
return Boolean(config2.AZURE_STORAGE_ACCOUNT_NAME && config2.AZURE_STORAGE_ACCOUNT_KEY);
|
|
21483
|
+
}
|
|
20551
21484
|
}
|
|
20552
21485
|
};
|
|
20553
21486
|
}
|