vidspotai-shared 1.0.117 → 1.0.119

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.
Files changed (72) hide show
  1. package/lib/globals/aiModels/enums.d.ts +22 -1
  2. package/lib/globals/aiModels/enums.d.ts.map +1 -1
  3. package/lib/globals/aiModels/enums.js +22 -1
  4. package/lib/globals/aiModels/index.d.ts.map +1 -1
  5. package/lib/globals/aiModels/index.js +2 -0
  6. package/lib/globals/aiModels/providers/topaz.d.ts +21 -0
  7. package/lib/globals/aiModels/providers/topaz.d.ts.map +1 -0
  8. package/lib/globals/aiModels/providers/topaz.js +57 -0
  9. package/lib/globals/aiModels/types.d.ts +7 -1
  10. package/lib/globals/aiModels/types.d.ts.map +1 -1
  11. package/lib/globals/types.d.ts +26 -1
  12. package/lib/globals/types.d.ts.map +1 -1
  13. package/lib/globals/types.js +31 -1
  14. package/lib/libs/storage/MediaStorage.d.ts +4 -3
  15. package/lib/libs/storage/MediaStorage.d.ts.map +1 -1
  16. package/lib/models/index.d.ts +2 -0
  17. package/lib/models/index.d.ts.map +1 -1
  18. package/lib/models/index.js +2 -0
  19. package/lib/models/posterProject.model.d.ts +30 -0
  20. package/lib/models/posterProject.model.d.ts.map +1 -0
  21. package/lib/models/posterProject.model.js +2 -0
  22. package/lib/models/upscale.model.d.ts +57 -0
  23. package/lib/models/upscale.model.d.ts.map +1 -0
  24. package/lib/models/upscale.model.js +2 -0
  25. package/lib/schemas/project.schema.d.ts +226 -0
  26. package/lib/schemas/project.schema.d.ts.map +1 -1
  27. package/lib/schemas/project.schema.js +45 -1
  28. package/lib/schemas/videoPlan.schema.d.ts +3 -3
  29. package/lib/services/agent/tools/composeScene.tool.d.ts +204 -0
  30. package/lib/services/agent/tools/composeScene.tool.d.ts.map +1 -1
  31. package/lib/services/agent/tools/estimateCost.tool.d.ts +68 -0
  32. package/lib/services/agent/tools/estimateCost.tool.d.ts.map +1 -1
  33. package/lib/services/agent/tools/planVideo.tool.d.ts +1 -1
  34. package/lib/services/agent/tools/render.tool.d.ts +68 -0
  35. package/lib/services/agent/tools/render.tool.d.ts.map +1 -1
  36. package/lib/services/aiGen/aiGenFactory.service.d.ts +19 -1
  37. package/lib/services/aiGen/aiGenFactory.service.d.ts.map +1 -1
  38. package/lib/services/aiGen/aiGenFactory.service.js +38 -1
  39. package/lib/services/aiGen/providers/baseAiGenProvider.service.d.ts +13 -1
  40. package/lib/services/aiGen/providers/baseAiGenProvider.service.d.ts.map +1 -1
  41. package/lib/services/aiGen/providers/baseAiGenProvider.service.js +20 -0
  42. package/lib/services/aiGen/providers/fal/falUpscale.service.d.ts +38 -0
  43. package/lib/services/aiGen/providers/fal/falUpscale.service.d.ts.map +1 -0
  44. package/lib/services/aiGen/providers/fal/falUpscale.service.js +226 -0
  45. package/lib/services/aiGen/providers/fal/index.d.ts +1 -0
  46. package/lib/services/aiGen/providers/fal/index.d.ts.map +1 -1
  47. package/lib/services/aiGen/providers/fal/index.js +1 -0
  48. package/lib/services/aiGen/providers/openai/openai.service.d.ts.map +1 -1
  49. package/lib/services/aiGen/providers/openai/openai.service.js +88 -56
  50. package/lib/services/aiGen/providers/topaz/topaz.service.d.ts +51 -0
  51. package/lib/services/aiGen/providers/topaz/topaz.service.d.ts.map +1 -0
  52. package/lib/services/aiGen/providers/topaz/topaz.service.js +321 -0
  53. package/lib/services/aiGen/providers/types.d.ts +73 -2
  54. package/lib/services/aiGen/providers/types.d.ts.map +1 -1
  55. package/lib/services/editor/designToProject.d.ts +10 -1
  56. package/lib/services/editor/designToProject.d.ts.map +1 -1
  57. package/lib/services/editor/designToProject.js +78 -8
  58. package/lib/services/editor/projectToDesign.d.ts.map +1 -1
  59. package/lib/services/editor/projectToDesign.js +38 -2
  60. package/lib/services/firestore.service.d.ts +4 -0
  61. package/lib/services/firestore.service.d.ts.map +1 -1
  62. package/lib/services/firestore.service.js +6 -0
  63. package/lib/services/index.d.ts +1 -0
  64. package/lib/services/index.d.ts.map +1 -1
  65. package/lib/services/index.js +1 -0
  66. package/lib/services/moderation/index.d.ts +3 -0
  67. package/lib/services/moderation/index.d.ts.map +1 -0
  68. package/lib/services/moderation/index.js +5 -0
  69. package/lib/services/moderation/textModeration.service.d.ts +6 -0
  70. package/lib/services/moderation/textModeration.service.d.ts.map +1 -0
  71. package/lib/services/moderation/textModeration.service.js +44 -0
  72. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"openai.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/openai/openai.service.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAclB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,MAAM,CAAS;;IAQjB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAwF3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA8B3C,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IA2ChC;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA2HjC,aAAa,CAAC,EACZ,QAAQ,EACR,UAAuB,EACvB,QAAY,EACZ,SAAiB,EACjB,SAAa,EACb,OAAO,EACP,WAAW,GACZ,EAAE,iBAAiB,GAAG,MAAM;CAqB9B"}
1
+ {"version":3,"file":"openai.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/openai/openai.service.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,qBAAqB,EACrB,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,UAAU,CAAC;AAclB,qBAAa,aAAc,SAAQ,wBAAwB;IACzD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAa;IACxC,OAAO,CAAC,MAAM,CAAS;;IAQjB,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IAwF3B,gBAAgB,CAAC,EACrB,IAAI,EACJ,cAAc,EACd,cAAyB,GAC1B,EAAE,iBAAiB,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA8B3C,YAAY,CAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,oBAAoB,CAAC;IA2ChC;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC;IA6JjC,aAAa,CAAC,EACZ,QAAQ,EACR,UAAuB,EACvB,QAAY,EACZ,SAAiB,EACjB,SAAa,EACb,OAAO,EACP,WAAW,GACZ,EAAE,iBAAiB,GAAG,MAAM;CAqB9B"}
@@ -235,70 +235,102 @@ class OpenaiService extends baseAiGenProvider_service_1.BaseAiGenProviderService
235
235
  // non-streaming overload always applies; TS can't narrow the union
236
236
  // automatically from the absence of a field.
237
237
  let resp;
238
- if (isEdit) {
239
- const images = await Promise.all(refs.map(async (url) => {
240
- const r = await (0, storage_1.fetchOwnMedia)(url);
241
- if (!r.ok) {
242
- logger_1.logger.error("openai: ref fetch failed", {
243
- url,
244
- status: r.status,
245
- statusText: r.statusText,
246
- });
247
- throw new Error(`openai ref fetch ${r.status}`);
248
- }
249
- const name = (url.split("?")[0] ?? "ref").split("/").pop() || "ref.png";
250
- return (0, openai_1.toFile)(r, name, { type: imageMimeFromUrl(url) });
251
- }));
252
- const mask = params.maskImageUrl
253
- ? await (async () => {
254
- const r = await (0, storage_1.fetchOwnMedia)(params.maskImageUrl);
238
+ try {
239
+ if (isEdit) {
240
+ const images = await Promise.all(refs.map(async (url) => {
241
+ const r = await (0, storage_1.fetchOwnMedia)(url);
255
242
  if (!r.ok) {
256
- logger_1.logger.error("openai: mask fetch failed", {
257
- url: params.maskImageUrl,
243
+ logger_1.logger.error("openai: ref fetch failed", {
244
+ url,
258
245
  status: r.status,
259
246
  statusText: r.statusText,
260
247
  });
261
- throw new Error(`openai mask fetch ${r.status}`);
248
+ throw new Error(`openai ref fetch ${r.status}`);
262
249
  }
263
- return (0, openai_1.toFile)(r, "mask.png", { type: imageMimeFromUrl(params.maskImageUrl) });
264
- })()
265
- : undefined;
266
- resp = (await this.client.images.edit({
267
- model: modelId,
268
- prompt: params.prompt,
269
- image: images.length === 1 ? images[0] : images,
270
- ...(mask ? { mask } : {}),
271
- ...(params.numImages ? { n: params.numImages } : {}),
272
- ...(size ? { size: size } : {}),
273
- ...(params.quality ? { quality: params.quality } : {}),
274
- ...(params.background ? { background: params.background } : {}),
275
- ...(params.outputFormat ? { output_format: params.outputFormat } : {}),
276
- }));
277
- if (!resp?.data?.length) {
278
- logger_1.logger.error("openai: images endpoint returned no data", {
279
- endpoint: "edit",
280
- responseKeys: resp ? Object.keys(resp) : null,
281
- });
282
- throw new Error("openai images: empty data[]");
250
+ const name = (url.split("?")[0] ?? "ref").split("/").pop() || "ref.png";
251
+ return (0, openai_1.toFile)(r, name, { type: imageMimeFromUrl(url) });
252
+ }));
253
+ const mask = params.maskImageUrl
254
+ ? await (async () => {
255
+ const r = await (0, storage_1.fetchOwnMedia)(params.maskImageUrl);
256
+ if (!r.ok) {
257
+ logger_1.logger.error("openai: mask fetch failed", {
258
+ url: params.maskImageUrl,
259
+ status: r.status,
260
+ statusText: r.statusText,
261
+ });
262
+ throw new Error(`openai mask fetch ${r.status}`);
263
+ }
264
+ return (0, openai_1.toFile)(r, "mask.png", { type: imageMimeFromUrl(params.maskImageUrl) });
265
+ })()
266
+ : undefined;
267
+ resp = (await this.client.images.edit({
268
+ model: modelId,
269
+ prompt: params.prompt,
270
+ image: images.length === 1 ? images[0] : images,
271
+ ...(mask ? { mask } : {}),
272
+ ...(params.numImages ? { n: params.numImages } : {}),
273
+ ...(size ? { size: size } : {}),
274
+ ...(params.quality ? { quality: params.quality } : {}),
275
+ ...(params.background ? { background: params.background } : {}),
276
+ ...(params.outputFormat ? { output_format: params.outputFormat } : {}),
277
+ }));
278
+ if (!resp?.data?.length) {
279
+ logger_1.logger.error("openai: images endpoint returned no data", {
280
+ endpoint: "edit",
281
+ responseKeys: resp ? Object.keys(resp) : null,
282
+ });
283
+ throw new Error("openai images: empty data[]");
284
+ }
285
+ }
286
+ else {
287
+ resp = (await this.client.images.generate({
288
+ model: modelId,
289
+ prompt: params.prompt,
290
+ ...(params.numImages ? { n: params.numImages } : {}),
291
+ ...(size ? { size: size } : {}),
292
+ ...(params.quality ? { quality: params.quality } : {}),
293
+ ...(params.background ? { background: params.background } : {}),
294
+ ...(params.outputFormat ? { output_format: params.outputFormat } : {}),
295
+ }));
296
+ if (!resp?.data?.length) {
297
+ logger_1.logger.error("openai: images endpoint returned no data", {
298
+ endpoint: "generate",
299
+ responseKeys: resp ? Object.keys(resp) : null,
300
+ });
301
+ throw new Error("openai images: empty data[]");
302
+ }
283
303
  }
284
304
  }
285
- else {
286
- resp = (await this.client.images.generate({
287
- model: modelId,
288
- prompt: params.prompt,
289
- ...(params.numImages ? { n: params.numImages } : {}),
290
- ...(size ? { size: size } : {}),
291
- ...(params.quality ? { quality: params.quality } : {}),
292
- ...(params.background ? { background: params.background } : {}),
293
- ...(params.outputFormat ? { output_format: params.outputFormat } : {}),
294
- }));
295
- if (!resp?.data?.length) {
296
- logger_1.logger.error("openai: images endpoint returned no data", {
297
- endpoint: "generate",
298
- responseKeys: resp ? Object.keys(resp) : null,
299
- });
300
- throw new Error("openai images: empty data[]");
305
+ catch (err) {
306
+ // Only remap genuine OpenAI APIErrors (they carry `.status`). The
307
+ // ref/mask-fetch and empty-data[] errors thrown above are our own plain
308
+ // Errors with no `.status`, so they fall through unchanged and keep
309
+ // paging — those ARE ops-actionable (our storage/CDN, not user input).
310
+ const status = err?.status ?? err?.response?.status;
311
+ if (status === undefined)
312
+ throw err;
313
+ const message = err?.message ?? String(err);
314
+ if (status === 400) {
315
+ // The images.edit endpoint's most common 400 is a rejected reference
316
+ // image (unsupported mode/corrupt file/size mismatch) — user-fixable,
317
+ // not an ops incident. Fall back to the generic capability-mismatch
318
+ // code for other 400s (e.g. bad size/quality combination).
319
+ const code = isEdit && /image/i.test(message)
320
+ ? errors_1.USER_FACING_ERROR_CODES.INPUT_IMAGE_REJECTED
321
+ : errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH;
322
+ throw new errors_1.UserFacingError(message, code);
323
+ }
324
+ if (status === 401 || status === 403) {
325
+ throw new errors_1.UserFacingError(message, errors_1.USER_FACING_ERROR_CODES.PROVIDER_AUTH_ERROR);
301
326
  }
327
+ if (status === 429) {
328
+ const oaCode = err?.code ?? err?.error?.code ?? err?.type ?? err?.error?.type ?? "";
329
+ const isQuota = /insufficient_quota|billing|exceeded_current_quota/i.test(oaCode) ||
330
+ /quota|billing|insufficient/i.test(message);
331
+ throw new errors_1.UserFacingError(message, isQuota ? errors_1.USER_FACING_ERROR_CODES.ACCOUNT_QUOTA_EXCEEDED : errors_1.USER_FACING_ERROR_CODES.CAPABILITY_MISMATCH);
332
+ }
333
+ throw err;
302
334
  }
303
335
  const data = resp.data ?? [];
304
336
  const ts = Date.now();
@@ -0,0 +1,51 @@
1
+ import { BaseAiGenProviderService } from "../baseAiGenProvider.service";
2
+ import { CreditUsageParams, UpscaleImageParams, UpscaleImageStatusResult, UpscaleStatusParams, UpscaleSubmitResult, UpscaleVideoParams, UpscaleVideoStatusResult } from "../types";
3
+ /**
4
+ * Topaz Labs upscale provider (image + video). Both APIs are async — submit
5
+ * returns a provider request id, the caller (queue processor) polls the
6
+ * matching `checkUpscale*Status` method until a terminal state. On success
7
+ * the status method has ALREADY rehosted Topaz's time-limited result URL into
8
+ * our own storage (mediaStorage) — never the provider's own temp URL, same
9
+ * "always control our own copy" convention as every other provider
10
+ * (see runway.service.ts checkVideoStatus).
11
+ */
12
+ export declare class TopazService extends BaseAiGenProviderService {
13
+ private readonly apiKey;
14
+ constructor();
15
+ /**
16
+ * POST /enhance/async (multipart). Topaz needs the actual image bytes, so
17
+ * we download the caller's `inputImageUrl` first rather than passing the
18
+ * URL through — the input is already our own stored asset (from
19
+ * /v1/media/single-upload) by the time this runs.
20
+ */
21
+ upscaleImage(params: UpscaleImageParams): Promise<UpscaleSubmitResult>;
22
+ /**
23
+ * GET /status/{id} then, on completion, GET /download/{id} for a temporary
24
+ * URL — which we immediately rehost into mediaStorage. Never return
25
+ * Topaz's own URL as the job's final `outputUrl`.
26
+ */
27
+ checkUpscaleImageStatus({ providerRequestId, outputFilename, outputFilePath, }: UpscaleStatusParams): Promise<UpscaleImageStatusResult>;
28
+ /**
29
+ * Drives Topaz's full submit -> accept -> upload -> complete-upload
30
+ * sequence in one call so the queue processor only has to poll afterwards.
31
+ * Splitting this across separate BullMQ steps would multiply the ways a job
32
+ * can get stuck between retries with no clean resume point — Topaz's own
33
+ * `status` polling is the one resumable point post-submit.
34
+ */
35
+ upscaleVideo(params: UpscaleVideoParams): Promise<UpscaleSubmitResult>;
36
+ /**
37
+ * GET /video/{id}/status. On completion, downloads the returned link and
38
+ * rehosts it into mediaStorage — same "never persist the provider's own
39
+ * temp URL" rule as the image path above.
40
+ */
41
+ checkUpscaleVideoStatus({ providerRequestId, outputFilename, outputFilePath, }: UpscaleStatusParams): Promise<UpscaleVideoStatusResult>;
42
+ /**
43
+ * Image: priced per output megapixel (`cost.perMegapixel`), floored at
44
+ * MIN_CREDITS_PER_IMAGE_OP. Video: `cost.perSecond` is an UNVERIFIED
45
+ * placeholder (Topaz doesn't publish a video pricing formula — see
46
+ * topaz.ts module docblock) floored at the standard paid-op floor until the
47
+ * real rate is confirmed.
48
+ */
49
+ getCreditUsed({ modelKey, outputMegapixels, duration, creditFloor }: CreditUsageParams): number;
50
+ }
51
+ //# sourceMappingURL=topaz.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"topaz.service.d.ts","sourceRoot":"","sources":["../../../../../src/services/aiGen/providers/topaz/topaz.service.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AACxE,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,wBAAwB,EACxB,mBAAmB,EACnB,mBAAmB,EACnB,kBAAkB,EAClB,wBAAwB,EACzB,MAAM,UAAU,CAAC;AA4BlB;;;;;;;;GAQG;AACH,qBAAa,YAAa,SAAQ,wBAAwB;IACxD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;IAchC;;;;;OAKG;IACG,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAkD5E;;;;OAIG;IACG,uBAAuB,CAAC,EAC5B,iBAAiB,EACjB,cAAc,EACd,cAAiC,GAClC,EAAE,mBAAmB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IA2C1D;;;;;;OAMG;IACG,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IA6I5E;;;;OAIG;IACG,uBAAuB,CAAC,EAC5B,iBAAiB,EACjB,cAAc,EACd,cAAiC,GAClC,EAAE,mBAAmB,GAAG,OAAO,CAAC,wBAAwB,CAAC;IAyC1D;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,QAAQ,EAAE,gBAAgB,EAAE,QAAY,EAAE,WAAW,EAAE,EAAE,iBAAiB,GAAG,MAAM;CAmBpG"}
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TopazService = void 0;
4
+ const aiModels_1 = require("../../../../globals/aiModels");
5
+ const creditUnit_1 = require("../../../../globals/creditUnit");
6
+ const storage_1 = require("../../../../libs/storage");
7
+ const errors_1 = require("../../../../utils/errors");
8
+ const helpers_1 = require("../../../../utils/helpers");
9
+ const logger_1 = require("../../../../utils/logger");
10
+ const transientRetry_1 = require("../../transientRetry");
11
+ const baseAiGenProvider_service_1 = require("../baseAiGenProvider.service");
12
+ const IMAGE_BASE_URL = "https://api.topazlabs.com/image/v1";
13
+ const VIDEO_BASE_URL = "https://api.topazlabs.com/video";
14
+ /**
15
+ * Classify a Topaz HTTP failure (non-2xx). Same shape as every other
16
+ * provider's classifier (see pixverse.service.ts's classifyPixVerseHttpError)
17
+ * — 401/403 is a deploy/config bug (fail fast, no Slack noise per job), 5xx/429
18
+ * is transient and retried by withTransientRetry, everything else is a real
19
+ * bug worth alerting on.
20
+ */
21
+ function classifyTopazHttpError(status, body, op) {
22
+ if (status === 401 || status === 403) {
23
+ return new errors_1.UserFacingError(`Topaz rejected the API key (HTTP ${status}). Ask an operator to verify TOPAZ_API_KEY.`, errors_1.USER_FACING_ERROR_CODES.PROVIDER_AUTH_ERROR);
24
+ }
25
+ if (status >= 500 || status === 429) {
26
+ return new transientRetry_1.TransientHttpError(status, `Topaz ${op} transient HTTP ${status}: ${body.slice(0, 200)}`);
27
+ }
28
+ return new Error(`Topaz ${op} failed (${status}): ${body}`);
29
+ }
30
+ const IMAGE_TERMINAL_COMPLETED = ["completed", "complete", "succeeded", "success"];
31
+ const VIDEO_TERMINAL_COMPLETED = ["completed", "complete", "succeeded", "success"];
32
+ /**
33
+ * Topaz Labs upscale provider (image + video). Both APIs are async — submit
34
+ * returns a provider request id, the caller (queue processor) polls the
35
+ * matching `checkUpscale*Status` method until a terminal state. On success
36
+ * the status method has ALREADY rehosted Topaz's time-limited result URL into
37
+ * our own storage (mediaStorage) — never the provider's own temp URL, same
38
+ * "always control our own copy" convention as every other provider
39
+ * (see runway.service.ts checkVideoStatus).
40
+ */
41
+ class TopazService extends baseAiGenProvider_service_1.BaseAiGenProviderService {
42
+ constructor() {
43
+ super();
44
+ if (!process.env.TOPAZ_API_KEY) {
45
+ throw new Error("Missing TOPAZ_API_KEY in environment variables");
46
+ }
47
+ this.apiKey = process.env.TOPAZ_API_KEY;
48
+ }
49
+ // ============================================================
50
+ // IMAGE UPSCALE
51
+ // ============================================================
52
+ /**
53
+ * POST /enhance/async (multipart). Topaz needs the actual image bytes, so
54
+ * we download the caller's `inputImageUrl` first rather than passing the
55
+ * URL through — the input is already our own stored asset (from
56
+ * /v1/media/single-upload) by the time this runs.
57
+ */
58
+ async upscaleImage(params) {
59
+ const modelId = aiModels_1.aiModelConfigs[params.modelKey]?.modelId;
60
+ if (!modelId) {
61
+ throw new Error(`Unknown Topaz image modelKey: ${params.modelKey}`);
62
+ }
63
+ const srcResp = await fetch(params.inputImageUrl);
64
+ if (!srcResp.ok) {
65
+ throw new errors_1.UserFacingError("Your input image could not be read for upscaling. Please try uploading it again.", errors_1.USER_FACING_ERROR_CODES.INPUT_IMAGE_REJECTED);
66
+ }
67
+ const contentType = srcResp.headers.get("content-type") || "image/jpeg";
68
+ const imageBuffer = Buffer.from(await srcResp.arrayBuffer());
69
+ const form = new FormData();
70
+ form.append("model", modelId);
71
+ form.append("output_format", params.outputFormat ?? "jpeg");
72
+ form.append("image", new Blob([imageBuffer], { type: contentType }), "input");
73
+ return (0, transientRetry_1.withTransientRetry)("topaz:upscaleImage", async () => {
74
+ const resp = await fetch(`${IMAGE_BASE_URL}/enhance/async`, {
75
+ method: "POST",
76
+ headers: { "X-API-KEY": this.apiKey },
77
+ body: form,
78
+ });
79
+ if (!resp.ok) {
80
+ throw classifyTopazHttpError(resp.status, await resp.text(), "upscaleImage");
81
+ }
82
+ const data = (await resp.json());
83
+ if (!data.process_id) {
84
+ throw new Error("Topaz image upscale did not return a process_id");
85
+ }
86
+ return { providerRequestId: data.process_id };
87
+ }, {
88
+ onRetry: ({ attempt, maxAttempts, backoffMs, err }) => logger_1.logger.warn("Topaz upscaleImage transient error — retrying", {
89
+ attempt,
90
+ maxAttempts,
91
+ backoffMs,
92
+ message: err.message,
93
+ }),
94
+ });
95
+ }
96
+ /**
97
+ * GET /status/{id} then, on completion, GET /download/{id} for a temporary
98
+ * URL — which we immediately rehost into mediaStorage. Never return
99
+ * Topaz's own URL as the job's final `outputUrl`.
100
+ */
101
+ async checkUpscaleImageStatus({ providerRequestId, outputFilename, outputFilePath = "upscale_images", }) {
102
+ const statusResp = await fetch(`${IMAGE_BASE_URL}/status/${providerRequestId}`, {
103
+ headers: { "X-API-KEY": this.apiKey },
104
+ });
105
+ if (!statusResp.ok) {
106
+ throw classifyTopazHttpError(statusResp.status, await statusResp.text(), "checkUpscaleImageStatus");
107
+ }
108
+ const statusData = (await statusResp.json());
109
+ const normalized = (statusData.status ?? "").toLowerCase();
110
+ if (normalized === "failed" || normalized === "error") {
111
+ return { status: "failed", errorMessage: statusData.error ?? "Topaz image upscale failed" };
112
+ }
113
+ if (!IMAGE_TERMINAL_COMPLETED.includes(normalized)) {
114
+ return { status: "processing" };
115
+ }
116
+ const downloadResp = await fetch(`${IMAGE_BASE_URL}/download/${providerRequestId}`, {
117
+ headers: { "X-API-KEY": this.apiKey },
118
+ });
119
+ if (!downloadResp.ok) {
120
+ throw classifyTopazHttpError(downloadResp.status, await downloadResp.text(), "checkUpscaleImageStatus:download");
121
+ }
122
+ const { url } = (await downloadResp.json());
123
+ if (!url)
124
+ throw new Error("Topaz image download endpoint did not return a url");
125
+ const assetResp = await fetch(url);
126
+ if (!assetResp.ok) {
127
+ throw new Error(`Failed to download Topaz upscaled image: ${assetResp.status}`);
128
+ }
129
+ const contentType = assetResp.headers.get("content-type") || "image/jpeg";
130
+ const ext = (contentType.split("/")[1] || "jpeg").toLowerCase();
131
+ const buffer = Buffer.from(await assetResp.arrayBuffer());
132
+ // mediaStorage: GCS write + best-effort R2 mirror (zero-egress delivery).
133
+ const signedUrl = await storage_1.mediaStorage.put(`${outputFilePath}/${outputFilename}.${ext}`, buffer, contentType);
134
+ return { status: "completed", outputUrl: signedUrl };
135
+ }
136
+ // ============================================================
137
+ // VIDEO UPSCALE
138
+ // ============================================================
139
+ /**
140
+ * Drives Topaz's full submit -> accept -> upload -> complete-upload
141
+ * sequence in one call so the queue processor only has to poll afterwards.
142
+ * Splitting this across separate BullMQ steps would multiply the ways a job
143
+ * can get stuck between retries with no clean resume point — Topaz's own
144
+ * `status` polling is the one resumable point post-submit.
145
+ */
146
+ async upscaleVideo(params) {
147
+ const modelId = aiModels_1.aiModelConfigs[params.modelKey]?.modelId;
148
+ if (!modelId) {
149
+ throw new Error(`Unknown Topaz video modelKey: ${params.modelKey}`);
150
+ }
151
+ const { source } = params;
152
+ const requestId = await (0, transientRetry_1.withTransientRetry)("topaz:upscaleVideo:submit", async () => {
153
+ const resp = await fetch(`${VIDEO_BASE_URL}/`, {
154
+ method: "POST",
155
+ headers: { "X-API-Key": this.apiKey, "Content-Type": "application/json" },
156
+ body: JSON.stringify({
157
+ source: {
158
+ container: source.container,
159
+ size: source.size,
160
+ resolution: source.resolution,
161
+ frameRate: source.frameRate,
162
+ frameCount: source.frameCount,
163
+ duration: source.duration,
164
+ },
165
+ output: {
166
+ resolution: {
167
+ width: source.resolution.width * params.scale,
168
+ height: source.resolution.height * params.scale,
169
+ },
170
+ frameRate: source.frameRate,
171
+ container: source.container,
172
+ },
173
+ filters: [{ model: modelId, type: "upscale" }],
174
+ }),
175
+ });
176
+ if (!resp.ok) {
177
+ throw classifyTopazHttpError(resp.status, await resp.text(), "upscaleVideo:submit");
178
+ }
179
+ const data = (await resp.json());
180
+ const id = data.id ?? data.requestId;
181
+ if (!id)
182
+ throw new Error("Topaz video submit did not return a request id");
183
+ return id;
184
+ }, {
185
+ onRetry: ({ attempt, maxAttempts, backoffMs, err }) => logger_1.logger.warn("Topaz upscaleVideo submit transient error — retrying", {
186
+ attempt,
187
+ maxAttempts,
188
+ backoffMs,
189
+ message: err.message,
190
+ }),
191
+ });
192
+ const uploadUrl = await (0, transientRetry_1.withTransientRetry)("topaz:upscaleVideo:accept", async () => {
193
+ const resp = await fetch(`${VIDEO_BASE_URL}/${requestId}/accept`, {
194
+ method: "PATCH",
195
+ headers: { "X-API-Key": this.apiKey },
196
+ });
197
+ if (!resp.ok) {
198
+ throw classifyTopazHttpError(resp.status, await resp.text(), "upscaleVideo:accept");
199
+ }
200
+ const data = (await resp.json());
201
+ const url = data.url ?? data.urls?.[0];
202
+ if (!url)
203
+ throw new Error("Topaz video accept did not return an upload url");
204
+ return url;
205
+ }, {
206
+ onRetry: ({ attempt, maxAttempts, backoffMs, err }) => logger_1.logger.warn("Topaz upscaleVideo accept transient error — retrying", {
207
+ attempt,
208
+ maxAttempts,
209
+ backoffMs,
210
+ message: err.message,
211
+ }),
212
+ });
213
+ // Our own already-persisted source (from /v1/media/single-upload) — never
214
+ // a browser upload stream directly.
215
+ const srcResp = await fetch(params.inputVideoUrl);
216
+ if (!srcResp.ok) {
217
+ throw new errors_1.UserFacingError("Your input video could not be read for upscaling. Please try uploading it again.", errors_1.USER_FACING_ERROR_CODES.INPUT_IMAGE_REJECTED);
218
+ }
219
+ const videoBuffer = Buffer.from(await srcResp.arrayBuffer());
220
+ const eTag = await (0, transientRetry_1.withTransientRetry)("topaz:upscaleVideo:upload", async () => {
221
+ const putResp = await fetch(uploadUrl, {
222
+ method: "PUT",
223
+ headers: { "Content-Type": `video/${source.container}` },
224
+ body: videoBuffer,
225
+ });
226
+ if (!putResp.ok) {
227
+ throw classifyTopazHttpError(putResp.status, await putResp.text(), "upscaleVideo:upload");
228
+ }
229
+ const tag = putResp.headers.get("etag");
230
+ if (!tag)
231
+ throw new Error("Topaz video upload did not return an ETag");
232
+ return tag;
233
+ }, {
234
+ onRetry: ({ attempt, maxAttempts, backoffMs, err }) => logger_1.logger.warn("Topaz upscaleVideo upload transient error — retrying", {
235
+ attempt,
236
+ maxAttempts,
237
+ backoffMs,
238
+ message: err.message,
239
+ }),
240
+ });
241
+ await (0, transientRetry_1.withTransientRetry)("topaz:upscaleVideo:complete", async () => {
242
+ const resp = await fetch(`${VIDEO_BASE_URL}/${requestId}/complete-upload`, {
243
+ method: "PATCH",
244
+ headers: { "X-API-Key": this.apiKey, "Content-Type": "application/json" },
245
+ body: JSON.stringify({ eTag }),
246
+ });
247
+ if (!resp.ok) {
248
+ throw classifyTopazHttpError(resp.status, await resp.text(), "upscaleVideo:complete");
249
+ }
250
+ }, {
251
+ onRetry: ({ attempt, maxAttempts, backoffMs, err }) => logger_1.logger.warn("Topaz upscaleVideo complete-upload transient error — retrying", {
252
+ attempt,
253
+ maxAttempts,
254
+ backoffMs,
255
+ message: err.message,
256
+ }),
257
+ });
258
+ return { providerRequestId: requestId };
259
+ }
260
+ /**
261
+ * GET /video/{id}/status. On completion, downloads the returned link and
262
+ * rehosts it into mediaStorage — same "never persist the provider's own
263
+ * temp URL" rule as the image path above.
264
+ */
265
+ async checkUpscaleVideoStatus({ providerRequestId, outputFilename, outputFilePath = "upscale_videos", }) {
266
+ const resp = await fetch(`${VIDEO_BASE_URL}/${providerRequestId}/status`, {
267
+ headers: { "X-API-Key": this.apiKey },
268
+ });
269
+ if (!resp.ok) {
270
+ throw classifyTopazHttpError(resp.status, await resp.text(), "checkUpscaleVideoStatus");
271
+ }
272
+ const data = (await resp.json());
273
+ const normalized = (data.status ?? "").toLowerCase();
274
+ if (normalized === "failed" || normalized === "error") {
275
+ return { status: "failed", errorMessage: data.error ?? "Topaz video upscale failed" };
276
+ }
277
+ if (normalized === "cancelled" || normalized === "canceled") {
278
+ return { status: "cancelled", errorMessage: data.error ?? "Topaz video upscale was cancelled" };
279
+ }
280
+ const downloadUrl = data.downloadUrl ?? data.download_url;
281
+ if (!VIDEO_TERMINAL_COMPLETED.includes(normalized) || !downloadUrl) {
282
+ return { status: "processing" };
283
+ }
284
+ const assetResp = await fetch(downloadUrl);
285
+ if (!assetResp.ok) {
286
+ throw new Error(`Failed to download Topaz upscaled video: ${assetResp.status}`);
287
+ }
288
+ const buffer = Buffer.from(await assetResp.arrayBuffer());
289
+ // mediaStorage: GCS write + best-effort R2 mirror (zero-egress delivery).
290
+ const signedUrl = await storage_1.mediaStorage.put(`${outputFilePath}/${outputFilename}.mp4`, buffer, "video/mp4");
291
+ return { status: "completed", outputUrl: signedUrl };
292
+ }
293
+ // ============================================================
294
+ // CREDITS
295
+ // ============================================================
296
+ /**
297
+ * Image: priced per output megapixel (`cost.perMegapixel`), floored at
298
+ * MIN_CREDITS_PER_IMAGE_OP. Video: `cost.perSecond` is an UNVERIFIED
299
+ * placeholder (Topaz doesn't publish a video pricing formula — see
300
+ * topaz.ts module docblock) floored at the standard paid-op floor until the
301
+ * real rate is confirmed.
302
+ */
303
+ getCreditUsed({ modelKey, outputMegapixels, duration = 5, creditFloor }) {
304
+ const modelConfig = aiModels_1.aiModelConfigs[modelKey];
305
+ const cost = modelConfig?.cost;
306
+ if (cost?.perMegapixel !== undefined) {
307
+ // Fall back to a conservative 24MP (Topaz's 1-credit tier boundary) if
308
+ // the caller didn't compute an exact output size.
309
+ const megapixels = outputMegapixels ?? 24;
310
+ return (0, helpers_1.getCreditsFromCost)(cost.perMegapixel * megapixels, true, creditFloor ?? creditUnit_1.MIN_CREDITS_PER_IMAGE_OP);
311
+ }
312
+ if (cost?.perSecond !== undefined) {
313
+ return (0, helpers_1.getCreditsFromCost)(cost.perSecond * duration, true, creditFloor);
314
+ }
315
+ logger_1.logger.warn(`Topaz getCreditUsed: no cost config for modelKey="${modelKey}" — returning fallback`, {
316
+ modelKey,
317
+ });
318
+ return (0, helpers_1.getCreditsFromCost)(1.0, true, creditFloor);
319
+ }
320
+ }
321
+ exports.TopazService = TopazService;
@@ -1,4 +1,4 @@
1
- import { EImageGenModelKey, EMusicGenModels, ESfxGenModels, ETextGenModels, EVideoGenModels } from "../../../globals/aiModels";
1
+ import { EImageGenModelKey, EMusicGenModels, ESfxGenModels, ETextGenModels, EUpscaleImageModelKey, EUpscaleVideoModelKey, EVideoGenModels } from "../../../globals/aiModels";
2
2
  import { EVideoSceneStatus, TVideoJobTask } from "../../../globals/types";
3
3
  import { EVideoMode } from "../types";
4
4
  import type { CameraControl } from "../canonicalAdapters/cameraControl.types";
@@ -304,8 +304,73 @@ export interface SfxGenerationResult {
304
304
  mimeType: string;
305
305
  extension: string;
306
306
  }
307
+ /**
308
+ * Image upscale (Topaz). Async — the provider returns a `providerRequestId`
309
+ * immediately; the caller polls `checkUpscaleImageStatus()` until the status
310
+ * is terminal. On success the result's `outputUrl` is already persisted into
311
+ * OUR storage (mediaStorage) — never the provider's own temporary URL, same
312
+ * convention as `VideoStatusResult.videoUrl`.
313
+ */
314
+ export interface UpscaleImageParams {
315
+ modelKey: EUpscaleImageModelKey;
316
+ inputImageUrl: string;
317
+ /** 2 or 4 — see topaz.ts model config `fields.scale`. */
318
+ scale: number;
319
+ outputFormat?: "jpeg" | "png";
320
+ }
321
+ export interface UpscaleSubmitResult {
322
+ providerRequestId: string;
323
+ }
324
+ export type TUpscaleProviderStatus = "processing" | "completed" | "failed" | "cancelled";
325
+ export interface UpscaleStatusParams {
326
+ providerRequestId: string;
327
+ /** Object-storage filename (no extension) the result is persisted under on completion. */
328
+ outputFilename: string;
329
+ /** Object-storage folder. Defaults to "upscale_images" / "upscale_videos" per provider. */
330
+ outputFilePath?: string;
331
+ }
332
+ export interface UpscaleImageStatusResult {
333
+ status: TUpscaleProviderStatus;
334
+ /** Present once status is "completed" — already a mediaStorage-signed URL. */
335
+ outputUrl?: string;
336
+ width?: number;
337
+ height?: number;
338
+ errorMessage?: string;
339
+ }
340
+ /**
341
+ * Video upscale (Topaz). Multi-step: submit → accept (upload URL) → upload
342
+ * source bytes → complete-upload → poll. `upscaleVideo()` drives the whole
343
+ * submit-through-complete-upload sequence and returns the request id;
344
+ * `checkUpscaleVideoStatus()` polls from there.
345
+ */
346
+ export interface UpscaleVideoParams {
347
+ modelKey: EUpscaleVideoModelKey;
348
+ inputVideoUrl: string;
349
+ scale: number;
350
+ source: {
351
+ resolution: {
352
+ width: number;
353
+ height: number;
354
+ };
355
+ container: string;
356
+ /** Bytes. */
357
+ size: number;
358
+ /** Seconds. */
359
+ duration: number;
360
+ frameRate: number;
361
+ frameCount: number;
362
+ };
363
+ }
364
+ export interface UpscaleVideoStatusResult {
365
+ status: TUpscaleProviderStatus;
366
+ outputUrl?: string;
367
+ width?: number;
368
+ height?: number;
369
+ durationSec?: number;
370
+ errorMessage?: string;
371
+ }
307
372
  export interface CreditUsageParams {
308
- modelKey: EVideoGenModels | ETextGenModels | EImageGenModelKey | EMusicGenModels | ESfxGenModels;
373
+ modelKey: EVideoGenModels | ETextGenModels | EImageGenModelKey | EMusicGenModels | ESfxGenModels | EUpscaleImageModelKey | EUpscaleVideoModelKey;
309
374
  mode?: EVideoMode;
310
375
  duration?: number;
311
376
  resolution?: string;
@@ -338,5 +403,11 @@ export interface CreditUsageParams {
338
403
  * MIN_CREDITS_PER_PAID_OP. See globals/creditUnit.ts.
339
404
  */
340
405
  creditFloor?: number;
406
+ /**
407
+ * Topaz image upscale — output megapixels (post-scale width*height/1e6),
408
+ * priced per-megapixel (see topaz.ts `cost.perMegapixel`). Left undefined
409
+ * for every other modality.
410
+ */
411
+ outputMegapixels?: number;
341
412
  }
342
413
  //# sourceMappingURL=types.d.ts.map