vidpipe 1.3.22 → 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/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 isIssueResponse(value) {
984
- return !("pull_request" in value);
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 DEFAULT_PER_PAGE, GitHubClientError, GitHubClient, clientInstance, clientKey;
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
- DEFAULT_PER_PAGE = 100;
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
- async createIssue(input) {
1029
- logger_default.debug(`[GitHubClient] Creating issue in ${this.owner}/${this.repo}: ${input.title}`);
1030
- try {
1031
- const response = await this.octokit.rest.issues.create({
1032
- owner: this.owner,
1033
- repo: this.repo,
1034
- title: input.title,
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
- async updateIssue(issueNumber, input) {
1047
- logger_default.debug(`[GitHubClient] Updating issue #${issueNumber} in ${this.owner}/${this.repo}`);
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
- const response = await this.octokit.rest.issues.update({
1050
- owner: this.owner,
1051
- repo: this.repo,
1052
- issue_number: issueNumber,
1053
- title: input.title,
1054
- body: input.body,
1055
- state: input.state,
1056
- labels: input.labels ? normalizeLabels(input.labels) : void 0
1057
- });
1058
- return this.mapIssue(response.data);
1059
- } catch (error) {
1060
- this.logError(`update issue #${issueNumber}`, error);
1061
- throw new GitHubClientError(
1062
- `Failed to update GitHub issue #${issueNumber}: ${getErrorMessage(error)}`,
1063
- getErrorStatus(error)
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 response = await this.octokit.rest.issues.get({
1071
- owner: this.owner,
1072
- repo: this.repo,
1073
- issue_number: issueNumber
1074
- });
1075
- return this.mapIssue(response.data);
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
- while (issues.length < maxResults) {
1091
- const response = await this.octokit.rest.issues.listForRepo({
1092
- owner: this.owner,
1093
- repo: this.repo,
1094
- state: options.state ?? "all",
1095
- labels: options.labels && options.labels.length > 0 ? normalizeLabels(options.labels).join(",") : void 0,
1096
- sort: void 0,
1097
- direction: void 0,
1098
- per_page: DEFAULT_PER_PAGE,
1099
- page
1100
- });
1101
- const pageItems = response.data.filter(isIssueResponse).map((issue) => this.mapIssue(issue));
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
- page = (page ?? 1) + 1;
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
- return issues.slice(0, maxResults);
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 items = await this.octokit.paginate(this.octokit.rest.search.issuesAndPullRequests, {
1119
- q: searchQuery,
1120
- per_page: DEFAULT_PER_PAGE
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
- return items.filter(isIssueResponse).map((issue) => this.mapIssue(issue)).slice(0, options.maxResults ?? Number.POSITIVE_INFINITY);
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 addLabels(issueNumber, labels) {
1129
- if (labels.length === 0) {
1130
- return;
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
- return this.mapComment(response.data);
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
- async listComments(issueNumber) {
1204
- logger_default.debug(`[GitHubClient] Listing comments for issue #${issueNumber} in ${this.owner}/${this.repo}`);
1205
- try {
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: issue.number,
1224
- title: issue.title,
1225
- body: issue.body ?? "",
1226
- state: issue.state,
1227
- labels: issue.labels.map((label) => typeof label === "string" ? label : label.name ?? "").map((label) => label.trim()).filter((label) => label.length > 0),
1228
- created_at: issue.created_at,
1229
- updated_at: issue.updated_at,
1230
- html_url: issue.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
- mapComment(comment) {
1435
+ mapRestComment(data) {
1234
1436
  return {
1235
- id: comment.id,
1236
- body: comment.body ?? "",
1237
- created_at: comment.created_at,
1238
- updated_at: comment.updated_at,
1239
- html_url: comment.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 Semaphore = class {
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 = 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
- return convertAspectRatio(inputPath, outputPath, fallbackRatio);
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. Override in interactive agents that need longer timeouts. */
10177
+ /** Timeout for sendAndWait calls. 0 = no timeout. Override in subclasses if needed. */
9639
10178
  getTimeoutMs() {
9640
- return 3e5;
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/L3-services/queueMapping/queueMapping.ts
10480
- init_configLogger();
10481
- init_fileSystem();
10482
- init_paths();
10483
- var CACHE_FILE = ".vidpipe-queue-cache.json";
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 === "portrait" ? ASS_HEADER_PORTRAIT : style === "medium" ? ASS_HEADER_MEDIUM : ASS_HEADER;
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 === "portrait" ? ASS_HEADER_PORTRAIT : style === "medium" ? ASS_HEADER_MEDIUM : ASS_HEADER;
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 === "portrait" ? ASS_HEADER_PORTRAIT : style === "medium" ? ASS_HEADER_MEDIUM : ASS_HEADER;
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 baseASS = generateStyledASSForSegment(transcript, startTime, endTime, buffer, "portrait");
14441
- const hookLine = generateHookOverlay(hookText, 4, "portrait");
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 baseASS = generateStyledASSForComposite(transcript, segments, buffer, "portrait");
14446
- const hookLine = generateHookOverlay(hookText, 4, "portrait");
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 MAX_RETRIES = 3;
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 <= MAX_RETRIES; 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 === MAX_RETRIES) throw retryError;
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}/${MAX_RETRIES} failed: ${msg} \u2014 retrying in ${RETRY_DELAY_MS / 1e3}s`);
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.map((v) => ({
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 portraitAssContent = segments.length === 1 ? generatePortraitASSWithHook(transcript, hookText, segments[0].start, segments[0].end) : generatePortraitASSWithHookComposite(transcript, segments, hookText);
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 basename2 = videoPath.split(/[\\/]/).pop() ?? videoPath;
20183
- return basename2.replace(/\.[^.]+$/, "");
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
  }