vidspotai-shared 1.0.115 → 1.0.117

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.
@@ -4,4 +4,15 @@ export declare const firestore: admin.firestore.Firestore;
4
4
  export declare const FieldValue: typeof FirebaseFirestore.FieldValue;
5
5
  export declare const storage: import("firebase-admin/lib/storage/storage").Storage;
6
6
  export declare function getBucket(): import("@google-cloud/storage").Bucket;
7
+ /**
8
+ * OAuth2 access token for the service account backing this Admin app.
9
+ * Scoped by firebase-admin to include cloud-platform + identitytoolkit
10
+ * (see firebase-admin's credential-internal.js), so it's valid against any
11
+ * Google API the service account has IAM permission for — not just the
12
+ * Admin SDK's own wrapped calls. Use this instead of minting a token by hand
13
+ * when a check needs a raw REST call the Admin SDK doesn't expose (e.g. the
14
+ * Identity Toolkit admin/v2 config endpoint, which the Node SDK's
15
+ * ProjectConfig doesn't surface `authorizedDomains` on).
16
+ */
17
+ export declare function getGoogleAccessToken(): Promise<string>;
7
18
  //# sourceMappingURL=firebase.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"firebase.d.ts","sourceRoot":"","sources":["../../src/libs/firebase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AAwBxC,eAAO,MAAM,IAAI,6CAAe,CAAC;AACjC,eAAO,MAAM,SAAS,2BAAoB,CAAC;AAE3C,eAAO,MAAM,UAAU,qCAA6B,CAAC;AACrD,eAAO,MAAM,OAAO,sDAAkB,CAAC;AAEvC,wBAAgB,SAAS,2CAExB"}
1
+ {"version":3,"file":"firebase.d.ts","sourceRoot":"","sources":["../../src/libs/firebase.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AAwBxC,eAAO,MAAM,IAAI,6CAAe,CAAC;AACjC,eAAO,MAAM,SAAS,2BAAoB,CAAC;AAE3C,eAAO,MAAM,UAAU,qCAA6B,CAAC;AACrD,eAAO,MAAM,OAAO,sDAAkB,CAAC;AAEvC,wBAAgB,SAAS,2CAExB;AAED;;;;;;;;;GASG;AACH,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,MAAM,CAAC,CAM5D"}
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.storage = exports.FieldValue = exports.firestore = exports.auth = void 0;
37
37
  exports.getBucket = getBucket;
38
+ exports.getGoogleAccessToken = getGoogleAccessToken;
38
39
  const admin = __importStar(require("firebase-admin"));
39
40
  const dotenv = __importStar(require("dotenv"));
40
41
  dotenv.config();
@@ -64,3 +65,20 @@ exports.storage = admin.storage();
64
65
  function getBucket() {
65
66
  return exports.storage.bucket();
66
67
  }
68
+ /**
69
+ * OAuth2 access token for the service account backing this Admin app.
70
+ * Scoped by firebase-admin to include cloud-platform + identitytoolkit
71
+ * (see firebase-admin's credential-internal.js), so it's valid against any
72
+ * Google API the service account has IAM permission for — not just the
73
+ * Admin SDK's own wrapped calls. Use this instead of minting a token by hand
74
+ * when a check needs a raw REST call the Admin SDK doesn't expose (e.g. the
75
+ * Identity Toolkit admin/v2 config endpoint, which the Node SDK's
76
+ * ProjectConfig doesn't surface `authorizedDomains` on).
77
+ */
78
+ async function getGoogleAccessToken() {
79
+ const token = await admin.app().options.credential?.getAccessToken();
80
+ if (!token?.access_token) {
81
+ throw new Error("Failed to obtain a Google access token from the Firebase Admin credential");
82
+ }
83
+ return token.access_token;
84
+ }
@@ -35,6 +35,20 @@
35
35
  * `mirrorFromReadable` below for a best-effort R2 copy. Those still write
36
36
  * real bytes to GCS today — converting them is a separate, larger effort
37
37
  * (see the notes doc §9).
38
+ *
39
+ * ⚠️ RE-DOWNLOADING a URL this returned, anywhere else in the backend (a
40
+ * provider re-fetching a bible reference image, a pipeline step re-fetching a
41
+ * scene clip to post-process it, ASR re-fetching an audio URL, handing a URL
42
+ * to an external API that dereferences it server-side, etc): NEVER call raw
43
+ * `fetch()`/`axios.get()` on it. Since the flip above, it's GCS-shaped but
44
+ * R2-backed, so a direct fetch against storage.googleapis.com 404s — it only
45
+ * resolves through the CDN Worker's R2 read-through. Use `fetchOwnMedia()`
46
+ * (drop-in `fetch()` replacement) when you're doing the fetching yourself, or
47
+ * `toCdnUrl(url, { visibility: "private" })` when you're just embedding the
48
+ * URL string into a payload for someone else to fetch — both in ./cdnUrl,
49
+ * both safe no-ops on a genuinely external URL. This is not hypothetical:
50
+ * ~10 independent call sites had each rolled their own raw download and
51
+ * broke in production the same day this flipped (see notes doc §9).
38
52
  */
39
53
  import type { Readable } from "stream";
40
54
  import type { IStorageBackend } from "./StorageBackend";
@@ -1 +1 @@
1
- {"version":3,"file":"MediaStorage.d.ts","sourceRoot":"","sources":["../../../src/libs/storage/MediaStorage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAGvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED,qBAAa,YAAY;IACvB,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;gBAEhC,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,eAAe;IAK9D;;;;;;OAMG;IACG,GAAG,CACP,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC;IAKlB;;;;;OAKG;IACG,WAAW,CACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAMzC;;;;;;;OAOG;IACG,WAAW,CACf,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IAQhB;;;;;OAKG;IACG,cAAc,CAClB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IAQhB;;;;;;OAMG;IACG,kBAAkB,CACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,QAAQ,EAChB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC;IAShB,iDAAiD;IACjD,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIlD;;;;;;;;OAQG;IACG,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C;;;;;OAKG;YACW,gBAAgB;CAa/B;AAED,8EAA8E;AAC9E,eAAO,MAAM,YAAY,cAAsD,CAAC"}
1
+ {"version":3,"file":"MediaStorage.d.ts","sourceRoot":"","sources":["../../../src/libs/storage/MediaStorage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAGvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAGxD;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED,qBAAa,YAAY;IACvB,iEAAiE;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,0EAA0E;IAC1E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;gBAEhC,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,eAAe;IAK9D;;;;;;OAMG;IACG,GAAG,CACP,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,CAAC;IAKlB;;;;;OAKG;IACG,WAAW,CACf,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAMzC;;;;;;;OAOG;IACG,WAAW,CACf,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IAQhB;;;;;OAKG;IACG,cAAc,CAClB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IAQhB;;;;;;OAMG;IACG,kBAAkB,CACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,QAAQ,EAChB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC;IAShB,iDAAiD;IACjD,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAIlD;;;;;;;;OAQG;IACG,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI/C;;;;;OAKG;YACW,gBAAgB;CAa/B;AAED,8EAA8E;AAC9E,eAAO,MAAM,YAAY,cAAsD,CAAC"}
@@ -24,4 +24,30 @@ export type MediaVisibility = "public" | "private";
24
24
  export declare function toCdnUrl(url: string, opts?: {
25
25
  visibility?: MediaVisibility;
26
26
  }): string;
27
+ /**
28
+ * The sanctioned way to RE-DOWNLOAD a URL this backend itself minted via
29
+ * `mediaStorage.put()`/`putFromFile()`, from anywhere in the backend (a
30
+ * provider re-fetching a bible reference image, a pipeline step re-fetching a
31
+ * scene clip to post-process it, etc).
32
+ *
33
+ * Why this exists: since the Stage 2 R2-primary flip (see MediaStorage.ts's
34
+ * class doc), those URLs are GCS-signed-SHAPED but R2-BACKED — the object no
35
+ * longer exists at the literal storage.googleapis.com address, only under the
36
+ * CDN Worker's R2 read-through. A plain `fetch(url)` on one 404s. This
37
+ * wrapper is the fix, applied once, here — use it instead of a raw `fetch()`
38
+ * so a future call site can't reintroduce the same bug by forgetting to
39
+ * rewrite the URL first (which is exactly how this broke: ~10 independent
40
+ * fetch/axios call sites had each rolled their own download, see
41
+ * notes/R2_MEDIA_MIGRATION_PLAN.md §9).
42
+ *
43
+ * Safe no-op passthrough for a genuinely external URL (AI provider output,
44
+ * stock API, etc) — `toCdnUrl` only rewrites our own host prefixes.
45
+ *
46
+ * Not applicable when WE aren't the one fetching — e.g. handing a URL to an
47
+ * external API that dereferences it server-side (AssemblyAI's `audio_url`,
48
+ * Veo's `video.uri`, PixVerse's `image_url`). Those need the same rewrite,
49
+ * but via `toCdnUrl()` directly on the string embedded in the request, since
50
+ * there's no local `fetch()` call to wrap.
51
+ */
52
+ export declare function fetchOwnMedia(url: string, init?: RequestInit): Promise<Response>;
27
53
  //# sourceMappingURL=cdnUrl.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cdnUrl.d.ts","sourceRoot":"","sources":["../../../src/libs/storage/cdnUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAeH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEnD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,eAAe,CAAA;CAAE,GACtC,MAAM,CAeR"}
1
+ {"version":3,"file":"cdnUrl.d.ts","sourceRoot":"","sources":["../../../src/libs/storage/cdnUrl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAeH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,CAAC;AAEnD;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,eAAe,CAAA;CAAE,GACtC,MAAM,CAeR;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEhF"}
@@ -14,6 +14,7 @@
14
14
  */
15
15
  Object.defineProperty(exports, "__esModule", { value: true });
16
16
  exports.toCdnUrl = toCdnUrl;
17
+ exports.fetchOwnMedia = fetchOwnMedia;
17
18
  const FIREBASE_STORAGE_HOST = "https://firebasestorage.googleapis.com";
18
19
  const GCS_HOST = "https://storage.googleapis.com";
19
20
  const BUCKET = "vidspot-firebase.firebasestorage.app";
@@ -46,3 +47,31 @@ function toCdnUrl(url, opts) {
46
47
  }
47
48
  return url;
48
49
  }
50
+ /**
51
+ * The sanctioned way to RE-DOWNLOAD a URL this backend itself minted via
52
+ * `mediaStorage.put()`/`putFromFile()`, from anywhere in the backend (a
53
+ * provider re-fetching a bible reference image, a pipeline step re-fetching a
54
+ * scene clip to post-process it, etc).
55
+ *
56
+ * Why this exists: since the Stage 2 R2-primary flip (see MediaStorage.ts's
57
+ * class doc), those URLs are GCS-signed-SHAPED but R2-BACKED — the object no
58
+ * longer exists at the literal storage.googleapis.com address, only under the
59
+ * CDN Worker's R2 read-through. A plain `fetch(url)` on one 404s. This
60
+ * wrapper is the fix, applied once, here — use it instead of a raw `fetch()`
61
+ * so a future call site can't reintroduce the same bug by forgetting to
62
+ * rewrite the URL first (which is exactly how this broke: ~10 independent
63
+ * fetch/axios call sites had each rolled their own download, see
64
+ * notes/R2_MEDIA_MIGRATION_PLAN.md §9).
65
+ *
66
+ * Safe no-op passthrough for a genuinely external URL (AI provider output,
67
+ * stock API, etc) — `toCdnUrl` only rewrites our own host prefixes.
68
+ *
69
+ * Not applicable when WE aren't the one fetching — e.g. handing a URL to an
70
+ * external API that dereferences it server-side (AssemblyAI's `audio_url`,
71
+ * Veo's `video.uri`, PixVerse's `image_url`). Those need the same rewrite,
72
+ * but via `toCdnUrl()` directly on the string embedded in the request, since
73
+ * there's no local `fetch()` call to wrap.
74
+ */
75
+ function fetchOwnMedia(url, init) {
76
+ return fetch(toCdnUrl(url, { visibility: "private" }), init);
77
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"bibleImageVision.d.ts","sourceRoot":"","sources":["../../../src/services/agent/bibleImageVision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAIH,OAAO,KAAK,EACV,gBAAgB,EAEjB,MAAM,gCAAgC,CAAC;AAExC,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4BD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,gBAAgB,CAAC,CA2C3B"}
1
+ {"version":3,"file":"bibleImageVision.d.ts","sourceRoot":"","sources":["../../../src/services/agent/bibleImageVision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH,OAAO,KAAK,EACV,gBAAgB,EAEjB,MAAM,gCAAgC,CAAC;AAExC,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA4BD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,gBAAgB,EACvB,MAAM,EAAE,sBAAsB,GAC7B,OAAO,CAAC,gBAAgB,CAAC,CA2C3B"}
@@ -32,6 +32,7 @@
32
32
  Object.defineProperty(exports, "__esModule", { value: true });
33
33
  exports.runBibleImageVision = runBibleImageVision;
34
34
  const genai_1 = require("@google/genai");
35
+ const cdnUrl_1 = require("../../libs/storage/cdnUrl");
35
36
  const logger_1 = require("../../utils/logger");
36
37
  const DEFAULT_MODEL = "gemini-2.5-flash";
37
38
  /**
@@ -122,7 +123,7 @@ async function describeImage(ai, model, imageUrl, prompt, timeoutMs) {
122
123
  let bytes;
123
124
  let mimeType;
124
125
  try {
125
- const r = await fetch(imageUrl, { signal: controller.signal });
126
+ const r = await (0, cdnUrl_1.fetchOwnMedia)(imageUrl, { signal: controller.signal });
126
127
  if (!r.ok) {
127
128
  throw new Error(`fetch ${r.status} ${r.statusText}`);
128
129
  }
@@ -1 +1 @@
1
- {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA+ElB,qBAAa,aAAc,SAAQ,wBAAwB;IAKzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAQ/C;;;;;;;;;OASG;IACG,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IAoChC;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAoCvB;;;;OAIG;YACW,kBAAkB;IAsC1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA2T3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAmK3C,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAiBnB,cAAc;IAqK5B;;;;;;OAMG;IACH;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAIjC,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,SAAa,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,iBAAiB,GAAG,MAAM;CA8BpJ"}
1
+ {"version":3,"file":"google.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/google.service.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AA+ElB,qBAAa,aAAc,SAAQ,wBAAwB;IAKzD,OAAO,CAAC,EAAE,CAAc;IACxB,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAK;;IAQ/C;;;;;;;;;OASG;IACG,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IAoChC;;;;;OAKG;IACH,OAAO,CAAC,eAAe;IAoCvB;;;;OAIG;YACW,kBAAkB;IAsC1B,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAgU3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAmK3C,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;YAiBnB,cAAc;IAqK5B;;;;;;OAMG;IACH;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAIjC,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAY,EAAE,UAAmB,EAAE,SAAiB,EAAE,SAAa,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,iBAAiB,GAAG,MAAM;CA8BpJ"}
@@ -318,7 +318,12 @@ class GoogleService extends baseAiGenProvider_service_1.BaseAiGenProviderService
318
318
  };
319
319
  // Video extension takes precedence over image (mutually exclusive in Veo API).
320
320
  if (params.inputVideoUrl) {
321
- request.video = { uri: params.inputVideoUrl };
321
+ // Veo's own servers dereference this URI directly — unlike
322
+ // fetchAsImage() below, WE never fetch it ourselves, so the URL string
323
+ // itself must already resolve publicly. Our own mediaStorage URLs are
324
+ // GCS-shaped but R2-backed post Stage-2-flip, so route through the CDN
325
+ // Worker's R2 read-through (no-op for a genuinely external URL).
326
+ request.video = { uri: (0, storage_1.toCdnUrl)(params.inputVideoUrl, { visibility: "private" }) };
322
327
  }
323
328
  else if (params.inputImageUrl) {
324
329
  request.image = await this.withTransientRetry("input-image fetch", () => (0, googleFetch_1.fetchAsImage)(params.inputImageUrl));
@@ -1 +1 @@
1
- {"version":3,"file":"googleFetch.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/googleFetch.ts"],"names":[],"mappings":"AAkCA,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAiB1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAOvD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAyBjG"}
1
+ {"version":3,"file":"googleFetch.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/google/googleFetch.ts"],"names":[],"mappings":"AAmCA,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAiB1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAOvD;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKlD;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC,CAyBjG"}
@@ -5,6 +5,7 @@ exports.isTransientFetchError = isTransientFetchError;
5
5
  exports.inferImageMime = inferImageMime;
6
6
  exports.fetchAsImage = fetchAsImage;
7
7
  const errors_1 = require("../../../../utils/errors");
8
+ const cdnUrl_1 = require("../../../../libs/storage/cdnUrl");
8
9
  /**
9
10
  * Network + image helpers for the Google provider: transient-error detection
10
11
  * (for withTransientRetry) and downloading an input image as Veo-compatible
@@ -73,7 +74,7 @@ function inferImageMime(url) {
73
74
  return "image/png";
74
75
  }
75
76
  async function fetchAsImage(url) {
76
- const resp = await fetch(url);
77
+ const resp = await (0, cdnUrl_1.fetchOwnMedia)(url);
77
78
  // fetch() does NOT throw on 4xx/5xx — surface the HTTP error so we don't
78
79
  // encode an HTML error page as image bytes (Veo silently rejects them).
79
80
  if (!resp.ok) {
@@ -237,7 +237,7 @@ class OpenaiService extends baseAiGenProvider_service_1.BaseAiGenProviderService
237
237
  let resp;
238
238
  if (isEdit) {
239
239
  const images = await Promise.all(refs.map(async (url) => {
240
- const r = await fetch(url);
240
+ const r = await (0, storage_1.fetchOwnMedia)(url);
241
241
  if (!r.ok) {
242
242
  logger_1.logger.error("openai: ref fetch failed", {
243
243
  url,
@@ -251,7 +251,7 @@ class OpenaiService extends baseAiGenProvider_service_1.BaseAiGenProviderService
251
251
  }));
252
252
  const mask = params.maskImageUrl
253
253
  ? await (async () => {
254
- const r = await fetch(params.maskImageUrl);
254
+ const r = await (0, storage_1.fetchOwnMedia)(params.maskImageUrl);
255
255
  if (!r.ok) {
256
256
  logger_1.logger.error("openai: mask fetch failed", {
257
257
  url: params.maskImageUrl,
@@ -1 +1 @@
1
- {"version":3,"file":"pixverse.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/pixverse/pixverse.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAgGlB,qBAAa,eAAgB,SAAQ,wBAAwB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkD;IAK1E;;;;;;OAMG;YACW,WAAW;IA6CnB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyL3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAoHjD,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAkDjD"}
1
+ {"version":3,"file":"pixverse.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/pixverse/pixverse.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAgGlB,qBAAa,eAAgB,SAAQ,wBAAwB;IAC3D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkD;IAK1E;;;;;;OAMG;YACW,WAAW;IAkDnB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAyL3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAoHjD,aAAa,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM;CAkDjD"}
@@ -95,7 +95,12 @@ class PixVerseService extends baseAiGenProvider_service_1.BaseAiGenProviderServi
95
95
  */
96
96
  async uploadImage(imageUrl, traceId) {
97
97
  const form = new FormData();
98
- form.append("image_url", imageUrl);
98
+ // PixVerse's own servers dereference this URL directly — we never fetch
99
+ // it ourselves, so the URL string itself must already resolve publicly.
100
+ // Our own mediaStorage URLs are GCS-shaped but R2-backed post
101
+ // Stage-2-flip, so route through the CDN Worker (no-op for a genuinely
102
+ // external/user-uploaded URL).
103
+ form.append("image_url", (0, storage_1.toCdnUrl)(imageUrl, { visibility: "private" }));
99
104
  return (0, transientRetry_1.withTransientRetry)("pixverse:uploadImage", async () => {
100
105
  const resp = await fetch(`https://app-api.pixverse.ai/openapi/v2/image/upload`, {
101
106
  method: "POST",
@@ -1 +1 @@
1
- {"version":3,"file":"assemblyai.service.d.ts","sourceRoot":"","sources":["../../../src/services/asr/assemblyai.service.ts"],"names":[],"mappings":"AAIA;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uCAAuC;IACvC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClG,sDAAsD;IACtD,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;YASlB,OAAO;IAyBrB,yEAAyE;IACnE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAWzD,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAaxD;;;;OAIG;IACG,UAAU,CACd,MAAM,EAAE,gBAAgB,EACxB,EAAE,SAAmB,EAAE,UAAiB,EAAE,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3F,OAAO,CAAC,gBAAgB,CAAC;IAc5B;;;;;OAKG;IACH,aAAa,CAAC,gBAAgB,EAAE,MAAM,EAAE,aAAa,UAAQ,GAAG,MAAM;CAMvE"}
1
+ {"version":3,"file":"assemblyai.service.d.ts","sourceRoot":"","sources":["../../../src/services/asr/assemblyai.service.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iDAAiD;IACjD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uCAAuC;IACvC,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,0DAA0D;IAC1D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0CAA0C;IAC1C,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClG,sDAAsD;IACtD,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACtG,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;YASlB,OAAO;IAyBrB,yEAAyE;IACnE,MAAM,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAgBzD,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAaxD;;;;OAIG;IACG,UAAU,CACd,MAAM,EAAE,gBAAgB,EACxB,EAAE,SAAmB,EAAE,UAAiB,EAAE,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAO,GAC3F,OAAO,CAAC,gBAAgB,CAAC;IAc5B;;;;;OAKG;IACH,aAAa,CAAC,gBAAgB,EAAE,MAAM,EAAE,aAAa,UAAQ,GAAG,MAAM;CAMvE"}
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.AssemblyAIService = void 0;
7
7
  const axios_1 = __importDefault(require("axios"));
8
+ const cdnUrl_1 = require("../../libs/storage/cdnUrl");
8
9
  const logger_1 = require("../../utils/logger");
9
10
  const helpers_1 = require("../../utils/helpers");
10
11
  class AssemblyAIService {
@@ -36,7 +37,12 @@ class AssemblyAIService {
36
37
  /** Submit a transcription job. Returns the transcript id immediately. */
37
38
  async submit(params) {
38
39
  const body = {
39
- audio_url: params.audioUrl,
40
+ // AssemblyAI's own servers dereference this URL directly — we never
41
+ // fetch it ourselves, so the URL string itself must already resolve
42
+ // publicly. Our own mediaStorage URLs are GCS-shaped but R2-backed
43
+ // post Stage-2-flip, so route through the CDN Worker (no-op for a
44
+ // genuinely external/user-uploaded URL).
45
+ audio_url: (0, cdnUrl_1.toCdnUrl)(params.audioUrl, { visibility: "private" }),
40
46
  ...(params.languageCode ? { language_code: params.languageCode } : { language_detection: true }),
41
47
  ...(params.speakerLabels ? { speaker_labels: true } : {}),
42
48
  ...(params.redactPii ? { redact_pii: true, redact_pii_policies: ["us_social_security_number", "credit_card_number", "email_address", "phone_number"] } : {}),
@@ -1 +1 @@
1
- {"version":3,"file":"whisper.service.d.ts","sourceRoot":"","sources":["../../../src/services/asr/whisper.service.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AA2B/E,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;IAShC;;;;OAIG;IACG,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAwDrE;;;;OAIG;IACH,aAAa,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM;CAIhD"}
1
+ {"version":3,"file":"whisper.service.d.ts","sourceRoot":"","sources":["../../../src/services/asr/whisper.service.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AA2B/E,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;IAShC;;;;OAIG;IACG,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA8DrE;;;;OAIG;IACH,aAAa,CAAC,gBAAgB,EAAE,MAAM,GAAG,MAAM;CAIhD"}
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.WhisperAsrService = void 0;
40
40
  const axios_1 = __importDefault(require("axios"));
41
41
  const openai_1 = __importStar(require("openai"));
42
+ const cdnUrl_1 = require("../../libs/storage/cdnUrl");
42
43
  const logger_1 = require("../../utils/logger");
43
44
  const helpers_1 = require("../../utils/helpers");
44
45
  /**
@@ -83,7 +84,10 @@ class WhisperAsrService {
83
84
  if (params.redactPii) {
84
85
  logger_1.logger.warn("Whisper fallback requested with redactPii=true — Whisper has no native PII redaction, returning unredacted transcript");
85
86
  }
86
- const { data: audioBuf, headers } = await axios_1.default.get(params.audioUrl, {
87
+ // Our own mediaStorage URLs are GCS-shaped but R2-backed post
88
+ // Stage-2-flip — must go through the CDN Worker's R2 read-through
89
+ // rather than being fetched directly against storage.googleapis.com.
90
+ const { data: audioBuf, headers } = await axios_1.default.get((0, cdnUrl_1.toCdnUrl)(params.audioUrl, { visibility: "private" }), {
87
91
  responseType: "arraybuffer",
88
92
  timeout: DOWNLOAD_TIMEOUT_MS,
89
93
  maxContentLength: WHISPER_MAX_BYTES,
@@ -1 +1 @@
1
- {"version":3,"file":"translation.service.d.ts","sourceRoot":"","sources":["../../../src/services/translation/translation.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAqGvD;;;;GAIG;AACH,iBAAe,SAAS,CACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,EACpB,EAAE,GAAE,cAAkC,EACtC,OAAO,GAAE,IAAI,GAAG,IAAW,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAiDD;;;;;;;;;;;;;;GAcG;AACH,iBAAe,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8ChE;AAOD,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC;AAGF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,kBAAY,CAAC;AAC/C,0DAA0D;AAC1D,eAAO,MAAM,UAAU,uBAAiB,CAAC"}
1
+ {"version":3,"file":"translation.service.d.ts","sourceRoot":"","sources":["../../../src/services/translation/translation.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGlE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,eAAO,MAAM,kBAAkB,uBAAuB,CAAC;AAwIvD;;;;GAIG;AACH,iBAAe,SAAS,CACtB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,EACpB,EAAE,GAAE,cAAkC,EACtC,OAAO,GAAE,IAAI,GAAG,IAAW,GAC1B,OAAO,CAAC,MAAM,CAAC,CAsCjB;AAuDD;;;;;;;;;;;;;;GAcG;AACH,iBAAe,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CA8ChE;AAOD,eAAO,MAAM,kBAAkB;;;CAG9B,CAAC;AAGF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,kBAAY,CAAC;AAC/C,0DAA0D;AAC1D,eAAO,MAAM,UAAU,uBAAiB,CAAC"}
@@ -78,9 +78,18 @@ const PRIMARY_MAX_ATTEMPTS = 2;
78
78
  // two layers compounded into an unbounded stall — a Vietnamese request was
79
79
  // measured at 48s in prod (2026-07-26): ~12s burning both primary attempts,
80
80
  // then ~36s waiting on a slow generateText that nothing was bounding. Cap the
81
- // fallback so the worst case is primary (~12.2s) + fallback (15s) ≈ 27s and the
82
- // caller gets a definite answer instead of holding the request open.
83
- const FALLBACK_TIMEOUT_MS = 15000;
81
+ // fallback so the worst case is primary (~12.2s) + fallback (~16s) ≈ 28s and
82
+ // the caller gets a definite answer instead of holding the request open.
83
+ //
84
+ // 2026-09-22: a single un-retried 15s shot then ALSO started losing — both
85
+ // primary AND fallback failed in prod for `id`→`en` (Slack: "translation
86
+ // failed on BOTH primary and LLM fallback"), at ~27.2s total, matching this
87
+ // budget almost exactly. One bad connection with zero recovery chance was
88
+ // enough to take the whole fallback down. Split the same total budget into
89
+ // two shorter attempts so a transient hiccup (dropped connection, slow TLS
90
+ // handshake) gets a second, fresh try instead of being fatal.
91
+ const FALLBACK_ATTEMPT_TIMEOUT_MS = 8000;
92
+ const FALLBACK_MAX_ATTEMPTS = 2;
84
93
  // gpt-4o-mini: cheap (~$0.15/1M in, $0.60/1M out → fractions of a cent per
85
94
  // prompt), reliable, and independent of the Google video quota. Swap here if a
86
95
  // cheaper effective model becomes available.
@@ -104,12 +113,27 @@ async function callPrimaryTranslate(text, from, to, version) {
104
113
  }
105
114
  return translated;
106
115
  }
107
- /**
108
- * FALLBACK: translate via a cheap LLM. Lazy-imports the provider factory so this
109
- * module stays light (utils/helpers re-exports it) and to avoid an import cycle
110
- * with the aiGen layer.
111
- */
112
- async function callLlmTranslate(text, from, to) {
116
+ /** Races `promise` against a timeout; the loser is abandoned (no AbortSignal
117
+ * on this path), but the caller is unblocked — which is the point. */
118
+ async function withTimeout(promise, ms, label) {
119
+ let timer;
120
+ try {
121
+ return await Promise.race([
122
+ promise,
123
+ new Promise((_, reject) => {
124
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
125
+ }),
126
+ ]);
127
+ }
128
+ finally {
129
+ if (timer)
130
+ clearTimeout(timer);
131
+ }
132
+ }
133
+ /** One un-retried LLM translate attempt. Lazy-imports the provider factory so
134
+ * this module stays light (utils/helpers re-exports it) and to avoid an
135
+ * import cycle with the aiGen layer. */
136
+ async function callLlmTranslateOnce(text, from, to) {
113
137
  const { getAiGenProviderService } = await Promise.resolve().then(() => __importStar(require("../aiGen/aiGenFactory.service")));
114
138
  const service = getAiGenProviderService(FALLBACK_MODEL);
115
139
  const target = languageLabel(to);
@@ -117,25 +141,13 @@ async function callLlmTranslate(text, from, to) {
117
141
  `Preserve meaning, tone, names, numbers, hashtags, emojis and line breaks. ` +
118
142
  `Output ONLY the translated text — no quotes, no language labels, no commentary. ` +
119
143
  `If the text is already in ${target}, return it unchanged.`;
120
- // Bounded: generateText itself takes no timeout, so race it. The loser is
121
- // abandoned rather than cancelled (there is no AbortSignal on this path), but
122
- // the caller is unblocked — which is the point.
123
- let timer;
124
- const { text: out } = await Promise.race([
125
- service.generateText({
126
- input: [
127
- { role: "system", content: system },
128
- { role: "user", content: text },
129
- ],
130
- modelKey: FALLBACK_MODEL,
131
- options: { temperature: 0 },
132
- }),
133
- new Promise((_, reject) => {
134
- timer = setTimeout(() => reject(new Error(`LLM fallback timed out after ${FALLBACK_TIMEOUT_MS}ms`)), FALLBACK_TIMEOUT_MS);
135
- }),
136
- ]).finally(() => {
137
- if (timer)
138
- clearTimeout(timer);
144
+ const { text: out } = await service.generateText({
145
+ input: [
146
+ { role: "system", content: system },
147
+ { role: "user", content: text },
148
+ ],
149
+ modelKey: FALLBACK_MODEL,
150
+ options: { temperature: 0 },
139
151
  });
140
152
  const trimmed = (out ?? "").trim();
141
153
  if (!trimmed || trimmed === "No response") {
@@ -143,6 +155,20 @@ async function callLlmTranslate(text, from, to) {
143
155
  }
144
156
  return trimmed;
145
157
  }
158
+ /** FALLBACK: translate via a cheap LLM, with a real retry — see the
159
+ * FALLBACK_ATTEMPT_TIMEOUT_MS comment above for why one shot wasn't enough. */
160
+ async function callLlmTranslate(text, from, to) {
161
+ let lastErr;
162
+ for (let attempt = 1; attempt <= FALLBACK_MAX_ATTEMPTS; attempt++) {
163
+ try {
164
+ return await withTimeout(callLlmTranslateOnce(text, from, to), FALLBACK_ATTEMPT_TIMEOUT_MS, "LLM fallback");
165
+ }
166
+ catch (err) {
167
+ lastErr = err;
168
+ }
169
+ }
170
+ throw lastErr;
171
+ }
146
172
  /**
147
173
  * Translate `text` from → to (default English). Primary CF first (with a couple
148
174
  * of quick retries), then a cheap LLM. Throws Error(TRANSLATION_FAILED) only if
@@ -188,40 +214,23 @@ async function translate(text, from, to = types_1.ELANGUAGE_CODE.en, version = "
188
214
  throw e;
189
215
  }
190
216
  }
191
- /**
192
- * FALLBACK: detect via the same cheap LLM `translate` falls back to. Returns
193
- * the bare ISO 639-1 code. Throws if the reply doesn't look like a code.
194
- */
195
- async function callLlmDetect(text) {
217
+ /** One un-retried LLM detect attempt. */
218
+ async function callLlmDetectOnce(text) {
196
219
  const { getAiGenProviderService } = await Promise.resolve().then(() => __importStar(require("../aiGen/aiGenFactory.service")));
197
220
  const service = getAiGenProviderService(FALLBACK_MODEL);
198
221
  const system = "You are a language identification engine. Reply with ONLY the ISO 639-1 " +
199
222
  "code of the language the user's text is written in — two lowercase " +
200
223
  'letters, nothing else (e.g. "es", "ja", "ar"). Use "zh-cn" or "zh-tw" ' +
201
224
  "for Chinese. No punctuation, no explanation.";
202
- // Bounded the same way as callLlmTranslate (see comment above it): this is
203
- // called synchronously from POST /v1/video/generate before the BullMQ
204
- // enqueue, so an unbounded generateText here stalls the HTTP request all
205
- // the way to the Cloud Run 300s ceiling instead of failing over to the
206
- // "default to en" path below.
207
- let timer;
208
- const { text: out } = await Promise.race([
209
- service.generateText({
210
- // Only the opening slice matters for identification, and capping it
211
- // keeps this cheap when a long-form brief triggers the fallback.
212
- input: [
213
- { role: "system", content: system },
214
- { role: "user", content: text.slice(0, 1000) },
215
- ],
216
- modelKey: FALLBACK_MODEL,
217
- options: { temperature: 0 },
218
- }),
219
- new Promise((_, reject) => {
220
- timer = setTimeout(() => reject(new Error(`LLM detect timed out after ${FALLBACK_TIMEOUT_MS}ms`)), FALLBACK_TIMEOUT_MS);
221
- }),
222
- ]).finally(() => {
223
- if (timer)
224
- clearTimeout(timer);
225
+ const { text: out } = await service.generateText({
226
+ // Only the opening slice matters for identification, and capping it
227
+ // keeps this cheap when a long-form brief triggers the fallback.
228
+ input: [
229
+ { role: "system", content: system },
230
+ { role: "user", content: text.slice(0, 1000) },
231
+ ],
232
+ modelKey: FALLBACK_MODEL,
233
+ options: { temperature: 0 },
225
234
  });
226
235
  const code = (out ?? "").trim().toLowerCase().replace(/[^a-z-]/g, "");
227
236
  if (!/^[a-z]{2}(-[a-z]{2})?$/.test(code)) {
@@ -229,6 +238,27 @@ async function callLlmDetect(text) {
229
238
  }
230
239
  return code;
231
240
  }
241
+ /**
242
+ * FALLBACK: detect via the same cheap LLM `translate` falls back to, with the
243
+ * same retry treatment as callLlmTranslate. Returns the bare ISO 639-1 code.
244
+ *
245
+ * This is called synchronously from POST /v1/video/generate before the
246
+ * BullMQ enqueue, so an unbounded generateText here stalls the HTTP request
247
+ * all the way to the Cloud Run 300s ceiling instead of failing over to the
248
+ * "default to en" path below — hence the same per-attempt bound as translate.
249
+ */
250
+ async function callLlmDetect(text) {
251
+ let lastErr;
252
+ for (let attempt = 1; attempt <= FALLBACK_MAX_ATTEMPTS; attempt++) {
253
+ try {
254
+ return await withTimeout(callLlmDetectOnce(text), FALLBACK_ATTEMPT_TIMEOUT_MS, "LLM detect");
255
+ }
256
+ catch (err) {
257
+ lastErr = err;
258
+ }
259
+ }
260
+ throw lastErr;
261
+ }
232
262
  /**
233
263
  * Detect the language of `text`. Primary CF → cheap LLM → fail OPEN to English.
234
264
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vidspotai-shared",
3
- "version": "1.0.115",
3
+ "version": "1.0.117",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "exports": {