@brotu/ai 0.5.0 → 0.6.0

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.cjs CHANGED
@@ -62,6 +62,11 @@ __export(index_exports, {
62
62
  QWEN_TEXT_MODELS: () => QWEN_TEXT_MODELS,
63
63
  QWEN_VIDEO_MODELS: () => QWEN_VIDEO_MODELS,
64
64
  QwenAdapter: () => QwenAdapter,
65
+ TOPAZ_CATALOG: () => TOPAZ_CATALOG,
66
+ TOPAZ_IMAGE_MODELS: () => TOPAZ_IMAGE_MODELS,
67
+ TOPAZ_MODELS: () => TOPAZ_MODELS,
68
+ TOPAZ_VIDEO_MODELS: () => TOPAZ_VIDEO_MODELS,
69
+ TopazAdapter: () => TopazAdapter,
65
70
  brotu: () => brotu,
66
71
  createS3Storage: () => createS3Storage,
67
72
  deliverWebhook: () => deliverWebhook,
@@ -834,7 +839,7 @@ var imageOmni = {
834
839
  prompt: input.prompt
835
840
  };
836
841
  if (images.length > 0) {
837
- body.image_list = images.map((image) => ({ image }));
842
+ body.image_list = images.map((image2) => ({ image: image2 }));
838
843
  }
839
844
  if (elements.length > 0) {
840
845
  body.element_list = elements.map((id) => ({ element_id: id }));
@@ -1236,6 +1241,105 @@ var QWEN_CATALOG = [
1236
1241
  )
1237
1242
  ];
1238
1243
 
1244
+ // src/providers/topaz.models.ts
1245
+ var VIDEO_RES = ["720p", "1080p", "4k"];
1246
+ function video(id, name, kind, description) {
1247
+ return [id, { name, vendorModel: id, surface: "video", kind, description }];
1248
+ }
1249
+ function image(id, name, vendorModel, imagePath, description) {
1250
+ return [
1251
+ id,
1252
+ { name, vendorModel, surface: "image", imagePath, description }
1253
+ ];
1254
+ }
1255
+ var ENTRIES = [
1256
+ // Precision video upscale (Proteus family)
1257
+ video("prob-4", "Proteus", "upscale", "Default video upscale. Preserves the source."),
1258
+ video("pnat-1", "Proteus Natural", "upscale", "Softer Proteus. 2\xD7 only."),
1259
+ video("rhea-1", "Rhea", "upscale", "High-fidelity 4\xD7 upscale for fine detail."),
1260
+ video("thd-3", "Theia Fine Tune Detail", "upscale", "More aggressive detail."),
1261
+ video("thf-4", "Theia Fine Tune Fidelity", "upscale", "Lighter touch, closer to the source."),
1262
+ video("thm-2", "Themis 2", "upscale", "Motion deblur."),
1263
+ video("aaa-9", "Artemis Aliasing / Moire", "upscale", "Aliasing and moire cleanup."),
1264
+ video("ahq-12", "Artemis High Quality", "upscale", "Artemis for clean footage."),
1265
+ video("amq-13", "Artemis Medium Quality", "upscale", "Artemis for typical compression."),
1266
+ video("amqs-2", "Artemis Medium Halo", "upscale", "Medium halo reduction."),
1267
+ video("alq-13", "Artemis Low Quality", "upscale", "Artemis for degraded sources."),
1268
+ video("alqs-2", "Artemis Strong Halo", "upscale", "Strong halo reduction."),
1269
+ video("iris-3", "Iris Medium Quality", "upscale", "Face recovery, medium quality."),
1270
+ video("iris-2", "Iris Low Quality", "upscale", "Face recovery, low quality."),
1271
+ video("dtv-4", "Dione TV", "upscale", "Deinterlace broadcast footage."),
1272
+ video("ddv-3", "Dione DV", "upscale", "Deinterlace camcorder footage."),
1273
+ video("dtvs-2", "Dione Robust", "upscale", "Deinterlace degraded interlaced sources."),
1274
+ video("dtd-4", "Dione Dehalo", "upscale", "Deinterlace with halo cleanup."),
1275
+ video("dtds-2", "Dione Robust Dehalo", "upscale", "Deinterlace, degraded, with halo cleanup."),
1276
+ video("ghq-5", "Gaia High Quality", "upscale", "CGI and animation, high quality."),
1277
+ video("gcg-5", "Gaia CG", "upscale", "General CGI and renders."),
1278
+ video("nyx-3", "Nyx", "upscale", "Default video denoise."),
1279
+ video("nxl-1", "Nyx XL", "upscale", "Heavy video denoise."),
1280
+ video("nxhf-1", "Nyx High Fidelity", "upscale", "Light denoise, keep texture."),
1281
+ video("nxf-1", "Nyx Fast", "upscale", "Fast video denoise."),
1282
+ video("color-1", "Video Colorization", "upscale", "Black-and-white to color."),
1283
+ // Generative / creative video
1284
+ video("slp-2.6", "Starlight Precise 2.6", "upscale", "Generative upscale to 4K. Faces, texture, text."),
1285
+ video("slhq-1", "Starlight HQ", "upscale", "Maximum quality generative enhance."),
1286
+ video("slf-2", "Starlight Fast 2", "upscale", "Faster generative upscale."),
1287
+ video("slm-1", "Starlight Mini", "upscale", "Archival and interlaced restoration."),
1288
+ video("wonder-1", "Starlight Sharp", "upscale", "Local-first sharpening restore."),
1289
+ video("ast-2", "Astra 2", "upscale", "Creative GenAI video upscale."),
1290
+ // Frame interpolation — not upscalers
1291
+ video("aion-1", "Aion", "interpolation", "Extreme slow motion on complex, high-res motion."),
1292
+ video("apo-8", "Apollo", "interpolation", "Default slow-mo and frame-rate conversion."),
1293
+ video("apf-2", "Apollo Fast", "interpolation", "Faster Apollo."),
1294
+ video("chr-2", "Chronos", "interpolation", "Interpolation for harder motion."),
1295
+ video("chf-3", "Chronos Fast", "interpolation", "Faster Chronos."),
1296
+ // Image — Gigapixel / enhance
1297
+ image("standard-v2", "Standard 2", "Standard V2", "/image/v1/enhance/async", "Default image upscale."),
1298
+ image("high-fidelity-v2", "High Fidelity 2", "High Fidelity V2", "/image/v1/enhance/async", "Maximum source preservation."),
1299
+ image("low-resolution-v2", "Low Resolution 2", "Low Resolution V2", "/image/v1/enhance/async", "Tiny or heavily compressed sources."),
1300
+ image("cgi", "Art & CGI", "CGI", "/image/v1/enhance/async", "Illustrations, renders, game art."),
1301
+ image("text-refine", "Text & Shapes", "Text Refine", "/image/v1/enhance/async", "Documents, screenshots, type."),
1302
+ // Image — generative enhance
1303
+ image("standard-max", "Standard Max", "Standard MAX", "/image/v1/enhance-gen/async", "Default generative image upscale."),
1304
+ image("wonder", "Wonder", "Wonder", "/image/v1/enhance-gen/async", "One-click generative restore."),
1305
+ image("wonder-3.5", "Wonder 3.5", "Wonder 3.5", "/image/v1/enhance-gen/async", "Highest-quality generative image upscale."),
1306
+ image("redefine", "Redefine", "Redefine", "/image/v1/enhance-gen/async", "Prompt-guided generative enhance."),
1307
+ image("recovery-v2", "Recover 3", "Recovery V2", "/image/v1/enhance-gen/async", "Heavily damaged sources."),
1308
+ image("bloom-2", "Bloom 2", "Bloom 2", "/image/v1/enhance-gen/async", "Creative upscale for GenAI images."),
1309
+ image("denoise-max", "Denoise Max", "Denoise Max", "/image/v1/denoise-gen/async", "Highest-quality image denoise.")
1310
+ ];
1311
+ var TOPAZ_MODELS = Object.fromEntries(
1312
+ ENTRIES.flatMap(([id, binding]) => {
1313
+ const rows = [[id, binding]];
1314
+ if (binding.surface === "image" && binding.vendorModel !== id) {
1315
+ rows.push([binding.vendorModel, binding]);
1316
+ }
1317
+ return rows;
1318
+ })
1319
+ );
1320
+ function nodeTypes(binding) {
1321
+ if (binding.surface === "image") return ["image_upscale"];
1322
+ return binding.kind === "interpolation" ? ["video_gen"] : ["video_upscale"];
1323
+ }
1324
+ var TOPAZ_CATALOG = ENTRIES.map(([id, binding]) => ({
1325
+ id,
1326
+ name: binding.name,
1327
+ category: binding.surface,
1328
+ inputType: binding.surface === "video" ? "video_required" : "image_required",
1329
+ nodeTypes: nodeTypes(binding),
1330
+ creditsPerUnit: 0,
1331
+ creditUnit: binding.surface === "video" ? "second" : "image",
1332
+ supportedResolutions: binding.surface === "video" ? [...VIDEO_RES] : void 0,
1333
+ provider: "topaz",
1334
+ description: binding.description
1335
+ }));
1336
+ var TOPAZ_VIDEO_MODELS = Object.fromEntries(
1337
+ ENTRIES.filter(([, binding]) => binding.surface === "video")
1338
+ );
1339
+ var TOPAZ_IMAGE_MODELS = Object.fromEntries(
1340
+ ENTRIES.filter(([, binding]) => binding.surface === "image")
1341
+ );
1342
+
1239
1343
  // src/catalog.ts
1240
1344
  var PROVIDER_BASE_URLS = {
1241
1345
  byteplus: "https://ark.ap-southeast.bytepluses.com",
@@ -1244,6 +1348,7 @@ var PROVIDER_BASE_URLS = {
1244
1348
  google: "https://generativelanguage.googleapis.com",
1245
1349
  openai: "https://api.openai.com",
1246
1350
  qwen: "https://dashscope-intl.aliyuncs.com",
1351
+ topaz: "https://api.topazlabs.com",
1247
1352
  brotu: "https://api.brotu.app"
1248
1353
  };
1249
1354
  var DEFAULT_BROTU_API_URL = PROVIDER_BASE_URLS.brotu;
@@ -1261,7 +1366,8 @@ var BUILT_IN = [
1261
1366
  ...QWEN_CATALOG,
1262
1367
  ...OPENAI_CATALOG,
1263
1368
  ...GOOGLE_CATALOG,
1264
- ...ELEVENLABS_CATALOG
1369
+ ...ELEVENLABS_CATALOG,
1370
+ ...TOPAZ_CATALOG
1265
1371
  ];
1266
1372
  var catalog = BUILT_IN;
1267
1373
  var catalogMap = byId(BUILT_IN);
@@ -1403,7 +1509,7 @@ function expiresInHours(hours) {
1403
1509
  function estimateFor(provider, type, params, defaults) {
1404
1510
  const modelId = params.model ?? "";
1405
1511
  const model = getModel(modelId);
1406
- const video = params;
1512
+ const video2 = params;
1407
1513
  const unit = model?.pricing?.unit ?? (type === "video" ? "second" : type === "audio" ? "character" : type === "text" ? "token" : "image");
1408
1514
  let units = 1;
1409
1515
  if (unit === "character") {
@@ -1411,7 +1517,7 @@ function estimateFor(provider, type, params, defaults) {
1411
1517
  } else if (unit === "token") {
1412
1518
  units = 0;
1413
1519
  } else if (unit === "second") {
1414
- units = video.duration ?? defaults?.durationSeconds ?? model?.minDuration ?? 5;
1520
+ units = video2.duration ?? defaults?.durationSeconds ?? model?.minDuration ?? 5;
1415
1521
  }
1416
1522
  const resolution = params.resolution;
1417
1523
  const rate = (resolution ? model?.pricing?.byResolution?.[resolution] : void 0) ?? model?.pricing?.usdPerUnit;
@@ -1667,23 +1773,23 @@ var BrotuAdapter = class {
1667
1773
  }
1668
1774
  try {
1669
1775
  const workspaceId = await this.workspaceId();
1670
- const video = params;
1671
- const image = params;
1776
+ const video2 = params;
1777
+ const image2 = params;
1672
1778
  const payload = await this.request(
1673
1779
  this.studioPath("/api/v1/studio/estimate", workspaceId),
1674
1780
  {
1675
1781
  method: "POST",
1676
1782
  body: JSON.stringify({
1677
1783
  model: this.platformModel(params.model ?? ""),
1678
- duration: video.duration,
1679
- resolution: video.resolution ?? image.resolution,
1680
- aspectRatio: video.aspectRatio ?? image.aspectRatio,
1681
- withAudio: video.withAudio,
1784
+ duration: video2.duration,
1785
+ resolution: video2.resolution ?? image2.resolution,
1786
+ aspectRatio: video2.aspectRatio ?? image2.aspectRatio,
1787
+ withAudio: video2.withAudio,
1682
1788
  hasReferenceImage: Boolean(
1683
- video.imageUrl || video.imageUrls?.length || video.referenceImages?.length || image.referenceImages?.length
1789
+ video2.imageUrl || video2.imageUrls?.length || video2.referenceImages?.length || image2.referenceImages?.length
1684
1790
  ),
1685
- hasReferenceVideo: Boolean(video.videoUrl || video.videoUrls?.length),
1686
- quality: image.quality
1791
+ hasReferenceVideo: Boolean(video2.videoUrl || video2.videoUrls?.length),
1792
+ quality: image2.quality
1687
1793
  })
1688
1794
  }
1689
1795
  );
@@ -2286,12 +2392,12 @@ var GoogleAdapter = class {
2286
2392
  const input = [
2287
2393
  { type: "text", text: params.prompt }
2288
2394
  ];
2289
- for (const image of params.referenceImages ?? []) {
2290
- const [meta, data] = image.split(",");
2395
+ for (const image2 of params.referenceImages ?? []) {
2396
+ const [meta, data] = image2.split(",");
2291
2397
  input.push({
2292
2398
  type: "image",
2293
2399
  mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
2294
- data: data ?? image
2400
+ data: data ?? image2
2295
2401
  });
2296
2402
  }
2297
2403
  const payload = await this.request(
@@ -2383,10 +2489,10 @@ var GoogleAdapter = class {
2383
2489
  };
2384
2490
  const first = params.imageUrl ?? params.referenceImages?.[0];
2385
2491
  if (first) attach("image", first);
2386
- for (const image of params.imageUrls ?? []) attach("image", image);
2387
- const video = params.videoUrl ?? params.videoUrls?.[0];
2388
- if (video) attach("video", video);
2389
- const task = video ? "edit" : params.imageUrls?.length ? "reference_to_video" : first ? "image_to_video" : "text_to_video";
2492
+ for (const image2 of params.imageUrls ?? []) attach("image", image2);
2493
+ const video2 = params.videoUrl ?? params.videoUrls?.[0];
2494
+ if (video2) attach("video", video2);
2495
+ const task = video2 ? "edit" : params.imageUrls?.length ? "reference_to_video" : first ? "image_to_video" : "text_to_video";
2390
2496
  return {
2391
2497
  model: modelId,
2392
2498
  input,
@@ -2555,12 +2661,12 @@ var GoogleAdapter = class {
2555
2661
  const input = [
2556
2662
  { type: "text", text: params.prompt }
2557
2663
  ];
2558
- for (const image of params.referenceImages ?? []) {
2559
- const [meta, data] = image.split(",");
2664
+ for (const image2 of params.referenceImages ?? []) {
2665
+ const [meta, data] = image2.split(",");
2560
2666
  input.push({
2561
2667
  type: "image",
2562
2668
  mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
2563
- data: data ?? image
2669
+ data: data ?? image2
2564
2670
  });
2565
2671
  }
2566
2672
  return {
@@ -3370,11 +3476,11 @@ var QwenAdapter = class {
3370
3476
  case "media": {
3371
3477
  const media = [];
3372
3478
  if (binding.mode === "video-edit") {
3373
- const video = params.videoUrl ?? params.videoUrls?.[0];
3374
- if (!video) {
3479
+ const video2 = params.videoUrl ?? params.videoUrls?.[0];
3480
+ if (!video2) {
3375
3481
  throw new Error(`Qwen "${modelId}" needs a video \u2014 pass videoUrl.`);
3376
3482
  }
3377
- media.push({ type: "video", url: video });
3483
+ media.push({ type: "video", url: video2 });
3378
3484
  } else if (binding.mode === "r2v") {
3379
3485
  for (const url of references) {
3380
3486
  media.push({ type: "reference_image", url });
@@ -3414,7 +3520,7 @@ var QwenAdapter = class {
3414
3520
  ...output.video_url ? [output.video_url] : [],
3415
3521
  ...(output.results ?? []).map((result) => result.url).filter((url) => Boolean(url)),
3416
3522
  ...(output.choices ?? []).flatMap(
3417
- (choice) => (choice.message?.content ?? []).map((part) => part.image).filter((image) => Boolean(image))
3523
+ (choice) => (choice.message?.content ?? []).map((part) => part.image).filter((image2) => Boolean(image2))
3418
3524
  )
3419
3525
  ];
3420
3526
  return urls.map((url) => ({
@@ -3562,8 +3668,8 @@ var QwenAdapter = class {
3562
3668
  }
3563
3669
  try {
3564
3670
  const content = [];
3565
- for (const image of params.referenceImages ?? []) {
3566
- content.push({ image });
3671
+ for (const image2 of params.referenceImages ?? []) {
3672
+ content.push({ image: image2 });
3567
3673
  }
3568
3674
  content.push({ text: params.prompt });
3569
3675
  const parameters = {
@@ -3756,6 +3862,475 @@ var QwenAdapter = class {
3756
3862
  }
3757
3863
  };
3758
3864
 
3865
+ // src/adapters/topaz.adapter.ts
3866
+ var DEFAULT_BASE_URL7 = "https://api.topazlabs.com";
3867
+ var POLL_INTERVAL_MS6 = 5e3;
3868
+ var DEFAULT_MAX_POLL_ATTEMPTS6 = 240;
3869
+ var DEFAULT_FPS = 30;
3870
+ var PIXELS2 = {
3871
+ "720p": { width: 1280, height: 720 },
3872
+ "720P": { width: 1280, height: 720 },
3873
+ "1080p": { width: 1920, height: 1080 },
3874
+ "1080P": { width: 1920, height: 1080 },
3875
+ "4k": { width: 3840, height: 2160 },
3876
+ "4K": { width: 3840, height: 2160 },
3877
+ "1K": { width: 1024, height: 1024 },
3878
+ "2K": { width: 2048, height: 2048 }
3879
+ };
3880
+ var TopazAdapter = class {
3881
+ providerName = "topaz";
3882
+ supportedTypes = ["video", "image"];
3883
+ opts;
3884
+ constructor(opts) {
3885
+ this.opts = opts;
3886
+ }
3887
+ get baseUrl() {
3888
+ return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
3889
+ }
3890
+ get headers() {
3891
+ return {
3892
+ "X-API-Key": this.opts.apiKey,
3893
+ accept: "application/json",
3894
+ "Content-Type": "application/json"
3895
+ };
3896
+ }
3897
+ binding(modelId) {
3898
+ const id = modelId ?? "";
3899
+ const found = TOPAZ_MODELS[id];
3900
+ if (!found) {
3901
+ throw new Error(
3902
+ `"${id}" is not a Topaz model. Known: ${TOPAZ_CATALOG.map((model) => model.id).join(", ")}.`
3903
+ );
3904
+ }
3905
+ return [id, found];
3906
+ }
3907
+ /** 720p / 1080p / 4k, or an explicit "1920x1080" / "1920*1080" pair. */
3908
+ resolutionOf(resolution) {
3909
+ if (!resolution) return PIXELS2["1080p"];
3910
+ const named = PIXELS2[resolution];
3911
+ if (named) return named;
3912
+ const match = /^(\d{2,5})[x*](\d{2,5})$/i.exec(resolution.trim());
3913
+ if (match?.[1] && match[2]) {
3914
+ return { width: Number(match[1]), height: Number(match[2]) };
3915
+ }
3916
+ throw new Error(
3917
+ `Topaz wants 720p, 1080p, 4k or WxH, not "${resolution}".`
3918
+ );
3919
+ }
3920
+ containerOf(url) {
3921
+ const path = url.split("?")[0]?.toLowerCase() ?? "";
3922
+ if (path.endsWith(".mov")) return "mov";
3923
+ if (path.endsWith(".mkv")) return "mkv";
3924
+ return "mp4";
3925
+ }
3926
+ filterOf(binding, params) {
3927
+ const extras = params.providerOptions?.topaz ?? {};
3928
+ if (binding.kind === "upscale") {
3929
+ return { auto: "Auto", ...extras, model: binding.vendorModel };
3930
+ }
3931
+ const slowmo = numberish(extras.slowmo) ?? 1;
3932
+ const fps = numberish(extras.fps) ?? DEFAULT_FPS;
3933
+ if (slowmo < 1 || slowmo > 16) {
3934
+ throw new Error(`Topaz interpolation slowmo is 1\u201316, not ${slowmo}.`);
3935
+ }
3936
+ if (fps < 15 || fps > 240) {
3937
+ throw new Error(`Topaz interpolation fps is 15\u2013240, not ${fps}.`);
3938
+ }
3939
+ const filter = {
3940
+ model: binding.vendorModel,
3941
+ slowmo,
3942
+ fps
3943
+ };
3944
+ if (typeof extras.duplicate === "boolean") {
3945
+ filter.duplicate = extras.duplicate;
3946
+ }
3947
+ if (extras.duplicateThreshold !== void 0) {
3948
+ filter.duplicateThreshold = extras.duplicateThreshold;
3949
+ }
3950
+ return filter;
3951
+ }
3952
+ requestBody(binding, params, sourceUrl) {
3953
+ const extras = params.providerOptions?.topaz ?? {};
3954
+ const fps = numberish(extras.fps) ?? DEFAULT_FPS;
3955
+ return {
3956
+ source: { container: this.containerOf(sourceUrl) },
3957
+ filters: [this.filterOf(binding, params)],
3958
+ output: {
3959
+ resolution: this.resolutionOf(params.resolution),
3960
+ frameRate: fps,
3961
+ audioCodec: "AAC",
3962
+ audioTransfer: "Copy",
3963
+ dynamicCompressionLevel: "High",
3964
+ container: "mp4"
3965
+ }
3966
+ };
3967
+ }
3968
+ async request(path, init) {
3969
+ const response = await fetch(`${this.baseUrl}${path}`, {
3970
+ method: init?.method ?? "GET",
3971
+ headers: this.headers,
3972
+ body: init?.body !== void 0 ? JSON.stringify(init.body) : void 0
3973
+ });
3974
+ const payload = await response.json();
3975
+ if (!response.ok) {
3976
+ throw new Error(
3977
+ payload.message ?? `Topaz returned ${response.status} for ${path}.`
3978
+ );
3979
+ }
3980
+ return payload;
3981
+ }
3982
+ async downloadSource(url) {
3983
+ const response = await fetch(url);
3984
+ if (!response.ok) {
3985
+ throw new Error(`Could not download the source video (${response.status}).`);
3986
+ }
3987
+ return {
3988
+ bytes: await response.arrayBuffer(),
3989
+ contentType: response.headers.get("content-type") ?? "video/mp4"
3990
+ };
3991
+ }
3992
+ async submitTask(params) {
3993
+ const [, binding] = this.binding(params.model);
3994
+ if (binding.surface !== "video") {
3995
+ throw new Error(
3996
+ `Topaz "${params.model}" is an image model. Use ai.image.`
3997
+ );
3998
+ }
3999
+ const sourceUrl = params.videoUrl ?? params.videoUrls?.[0];
4000
+ if (!sourceUrl) {
4001
+ throw new Error(
4002
+ `Topaz "${params.model}" needs a source video \u2014 pass videoUrl.`
4003
+ );
4004
+ }
4005
+ const source = await this.downloadSource(sourceUrl);
4006
+ const created = await this.request("/video/express", {
4007
+ method: "POST",
4008
+ body: this.requestBody(binding, params, sourceUrl)
4009
+ });
4010
+ const taskId = created.requestId?.trim();
4011
+ const uploadUrl = created.uploadUrls?.[0];
4012
+ if (!taskId || !uploadUrl) {
4013
+ throw new Error(
4014
+ "Topaz accepted the request but returned no request id or upload URL."
4015
+ );
4016
+ }
4017
+ const uploaded = await fetch(uploadUrl, {
4018
+ method: "PUT",
4019
+ headers: { "Content-Type": source.contentType },
4020
+ body: source.bytes
4021
+ });
4022
+ if (!uploaded.ok) {
4023
+ throw new Error(`Topaz upload failed (${uploaded.status}).`);
4024
+ }
4025
+ return { taskId, pollEndpoint: `/video/${taskId}/status` };
4026
+ }
4027
+ outputsFrom(payload, job) {
4028
+ const url = payload.download?.url;
4029
+ if (!url) return [];
4030
+ const expiresAtMs = payload.download?.expiresAt;
4031
+ return [
4032
+ {
4033
+ url,
4034
+ mimeType: "video/mp4",
4035
+ taskId: job.id,
4036
+ expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : expiresInHours(24),
4037
+ raw: {
4038
+ status: payload.status,
4039
+ progress: payload.progress,
4040
+ estimates: payload.estimates,
4041
+ outputSize: payload.outputSize
4042
+ }
4043
+ }
4044
+ ];
4045
+ }
4046
+ snapshot(payload, job) {
4047
+ const raw = (payload.status ?? "").toLowerCase();
4048
+ if (raw === "failed" || raw === "canceled") {
4049
+ return {
4050
+ status: "failed",
4051
+ error: payload.message ?? `Topaz request ${raw}.`
4052
+ };
4053
+ }
4054
+ if (raw !== "complete") return { status: "pending" };
4055
+ const outputs = this.outputsFrom(payload, job);
4056
+ if (outputs.length === 0) return { status: "pending" };
4057
+ return {
4058
+ status: "succeeded",
4059
+ result: {
4060
+ success: true,
4061
+ outputs,
4062
+ creditsUsed: 0,
4063
+ provider: this.providerName,
4064
+ model: job.model,
4065
+ processingTimeMs: 0
4066
+ }
4067
+ };
4068
+ }
4069
+ async run(params) {
4070
+ const startedAt = Date.now();
4071
+ let modelId = params.model ?? "(none)";
4072
+ const failure = (error) => ({
4073
+ success: false,
4074
+ outputs: [],
4075
+ creditsUsed: 0,
4076
+ provider: this.providerName,
4077
+ model: modelId,
4078
+ processingTimeMs: Date.now() - startedAt,
4079
+ error
4080
+ });
4081
+ let submitted;
4082
+ try {
4083
+ modelId = this.binding(params.model)[0];
4084
+ submitted = await this.submitTask(params);
4085
+ } catch (error) {
4086
+ if (isPendingJob(error)) throw error;
4087
+ return failure(error instanceof Error ? error.message : String(error));
4088
+ }
4089
+ if (isSubmitMode()) {
4090
+ throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
4091
+ }
4092
+ const job = {
4093
+ id: submitted.taskId,
4094
+ provider: this.providerName,
4095
+ model: modelId,
4096
+ kind: "video",
4097
+ pollEndpoint: submitted.pollEndpoint,
4098
+ params,
4099
+ submittedAt: new Date(startedAt).toISOString()
4100
+ };
4101
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
4102
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4103
+ const snapshot = await this.completeJob(job);
4104
+ if (snapshot.status === "failed") {
4105
+ return failure(snapshot.error ?? "Topaz request failed.");
4106
+ }
4107
+ if (snapshot.status === "succeeded" && snapshot.result) {
4108
+ return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
4109
+ }
4110
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
4111
+ }
4112
+ return failure(
4113
+ `Topaz request ${submitted.taskId} did not finish after ${maxAttempts} checks.`
4114
+ );
4115
+ }
4116
+ async completeJob(job) {
4117
+ try {
4118
+ if (job.pollEndpoint?.startsWith("/image/")) {
4119
+ return this.imageSnapshot(job);
4120
+ }
4121
+ const payload = await this.request(
4122
+ job.pollEndpoint ?? `/video/${job.id}/status`
4123
+ );
4124
+ return this.snapshot(payload, job);
4125
+ } catch (error) {
4126
+ return {
4127
+ status: "failed",
4128
+ error: error instanceof Error ? error.message : String(error)
4129
+ };
4130
+ }
4131
+ }
4132
+ generateVideo(params) {
4133
+ return this.run(params);
4134
+ }
4135
+ imageBody(binding, params, sourceUrl) {
4136
+ const extras = params.providerOptions?.topaz ?? {};
4137
+ const pixels = params.resolution ? this.resolutionOf(params.resolution) : void 0;
4138
+ const fields = {
4139
+ source_url: sourceUrl,
4140
+ model: binding.vendorModel,
4141
+ output_format: params.outputFormat === "png" ? "png" : params.outputFormat === "jpeg" ? "jpeg" : "jpeg"
4142
+ };
4143
+ if (pixels) {
4144
+ fields.output_width = String(pixels.width);
4145
+ fields.output_height = String(pixels.height);
4146
+ }
4147
+ for (const [key, value] of Object.entries(extras)) {
4148
+ if (value === void 0) continue;
4149
+ fields[key] = typeof value === "string" ? value : String(value);
4150
+ }
4151
+ return fields;
4152
+ }
4153
+ async submitImage(params) {
4154
+ const [, binding] = this.binding(params.model);
4155
+ if (binding.surface !== "image" || !binding.imagePath) {
4156
+ throw new Error(
4157
+ `Topaz "${params.model}" is a video model. Use ai.video with videoUrl.`
4158
+ );
4159
+ }
4160
+ const sourceUrl = params.referenceImages?.[0];
4161
+ if (!sourceUrl) {
4162
+ throw new Error(
4163
+ `Topaz "${params.model}" needs a source image \u2014 pass referenceImages.`
4164
+ );
4165
+ }
4166
+ const form = new FormData();
4167
+ for (const [key, value] of Object.entries(
4168
+ this.imageBody(binding, params, sourceUrl)
4169
+ )) {
4170
+ form.set(key, value);
4171
+ }
4172
+ const response = await fetch(`${this.baseUrl}${binding.imagePath}`, {
4173
+ method: "POST",
4174
+ headers: {
4175
+ "X-API-Key": this.opts.apiKey,
4176
+ accept: "application/json"
4177
+ },
4178
+ body: form
4179
+ });
4180
+ const payload = await response.json();
4181
+ if (!response.ok) {
4182
+ throw new Error(
4183
+ payload.message ?? `Topaz returned ${response.status} enhancing the image.`
4184
+ );
4185
+ }
4186
+ const taskId = payload.process_id?.trim();
4187
+ if (!taskId) {
4188
+ throw new Error(
4189
+ "Topaz accepted the image but returned no process id."
4190
+ );
4191
+ }
4192
+ return { taskId, pollEndpoint: `/image/v1/status/${taskId}` };
4193
+ }
4194
+ async imageSnapshot(job) {
4195
+ const payload = await this.request(
4196
+ job.pollEndpoint ?? `/image/v1/status/${job.id}`
4197
+ );
4198
+ const raw = (payload.status ?? "").toLowerCase();
4199
+ if (raw === "failed" || raw === "cancelled") {
4200
+ return {
4201
+ status: "failed",
4202
+ error: `Topaz image ${raw}.`
4203
+ };
4204
+ }
4205
+ if (raw !== "completed") return { status: "pending" };
4206
+ let url = payload.download_url;
4207
+ let expiresAt = payload.eta ? new Date(payload.eta > 1e10 ? payload.eta : payload.eta * 1e3).toISOString() : void 0;
4208
+ if (!url) {
4209
+ const download = await this.request(
4210
+ `/image/v1/download/${job.id}`
4211
+ );
4212
+ url = download.download_url;
4213
+ if (download.expiry) {
4214
+ expiresAt = new Date(
4215
+ download.expiry > 1e10 ? download.expiry : download.expiry * 1e3
4216
+ ).toISOString();
4217
+ }
4218
+ }
4219
+ if (!url) return { status: "pending" };
4220
+ return {
4221
+ status: "succeeded",
4222
+ result: {
4223
+ success: true,
4224
+ outputs: [
4225
+ {
4226
+ url,
4227
+ mimeType: "image/jpeg",
4228
+ taskId: job.id,
4229
+ expiresAt: expiresAt ?? expiresInHours(1),
4230
+ raw: {
4231
+ status: payload.status,
4232
+ progress: payload.progress,
4233
+ model: payload.model
4234
+ }
4235
+ }
4236
+ ],
4237
+ creditsUsed: payload.credits ?? 0,
4238
+ provider: this.providerName,
4239
+ model: job.model,
4240
+ processingTimeMs: 0
4241
+ }
4242
+ };
4243
+ }
4244
+ async runImage(params) {
4245
+ const startedAt = Date.now();
4246
+ let modelId = params.model ?? "(none)";
4247
+ const failure = (error) => ({
4248
+ success: false,
4249
+ outputs: [],
4250
+ creditsUsed: 0,
4251
+ provider: this.providerName,
4252
+ model: modelId,
4253
+ processingTimeMs: Date.now() - startedAt,
4254
+ error
4255
+ });
4256
+ let submitted;
4257
+ try {
4258
+ modelId = this.binding(params.model)[0];
4259
+ submitted = await this.submitImage(params);
4260
+ } catch (error) {
4261
+ if (isPendingJob(error)) throw error;
4262
+ return failure(error instanceof Error ? error.message : String(error));
4263
+ }
4264
+ if (isSubmitMode()) {
4265
+ throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
4266
+ }
4267
+ const job = {
4268
+ id: submitted.taskId,
4269
+ provider: this.providerName,
4270
+ model: modelId,
4271
+ kind: "image",
4272
+ pollEndpoint: submitted.pollEndpoint,
4273
+ params,
4274
+ submittedAt: new Date(startedAt).toISOString()
4275
+ };
4276
+ const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
4277
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
4278
+ const snapshot = await this.completeJob(job);
4279
+ if (snapshot.status === "failed") {
4280
+ return failure(snapshot.error ?? "Topaz image failed.");
4281
+ }
4282
+ if (snapshot.status === "succeeded" && snapshot.result) {
4283
+ return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
4284
+ }
4285
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
4286
+ }
4287
+ return failure(
4288
+ `Topaz image ${submitted.taskId} did not finish after ${maxAttempts} checks.`
4289
+ );
4290
+ }
4291
+ generateImage(params) {
4292
+ return this.runImage(params);
4293
+ }
4294
+ generateText(params) {
4295
+ return Promise.resolve(unsupported(this.providerName, params.model, "text"));
4296
+ }
4297
+ generateAudio(params) {
4298
+ return Promise.resolve(unsupported(this.providerName, params.model, "audio"));
4299
+ }
4300
+ estimateCost(type, params) {
4301
+ return Promise.resolve(estimateFor(this.providerName, type, params));
4302
+ }
4303
+ supportsModel(model) {
4304
+ return model in TOPAZ_MODELS;
4305
+ }
4306
+ getAvailableModels() {
4307
+ return TOPAZ_CATALOG.map((model) => ({
4308
+ id: model.id,
4309
+ name: model.name,
4310
+ type: model.category
4311
+ }));
4312
+ }
4313
+ };
4314
+ function numberish(value) {
4315
+ if (typeof value === "number" && Number.isFinite(value)) return value;
4316
+ if (typeof value === "string" && value.length > 0) {
4317
+ const parsed = Number(value);
4318
+ return Number.isFinite(parsed) ? parsed : void 0;
4319
+ }
4320
+ return void 0;
4321
+ }
4322
+ function unsupported(provider, model, kind) {
4323
+ return {
4324
+ success: false,
4325
+ outputs: [],
4326
+ creditsUsed: 0,
4327
+ provider,
4328
+ model: model ?? "",
4329
+ processingTimeMs: 0,
4330
+ error: `Topaz does not generate ${kind} on this adapter.`
4331
+ };
4332
+ }
4333
+
3759
4334
  // src/helpers/result.ts
3760
4335
  function ok(data) {
3761
4336
  return { data, error: null };
@@ -3956,7 +4531,8 @@ var NATIVE_PROVIDERS = [
3956
4531
  "google",
3957
4532
  "kling",
3958
4533
  "openai",
3959
- "qwen"
4534
+ "qwen",
4535
+ "topaz"
3960
4536
  ];
3961
4537
  function brotu(options) {
3962
4538
  const apiKey = options.apiKey?.trim();
@@ -4055,6 +4631,12 @@ function brotu(options) {
4055
4631
  baseUrl: provider.baseUrl
4056
4632
  });
4057
4633
  }
4634
+ if (provider.id === "topaz") {
4635
+ return new TopazAdapter({
4636
+ apiKey: provider.apiKey,
4637
+ baseUrl: provider.baseUrl
4638
+ });
4639
+ }
4058
4640
  return void 0;
4059
4641
  }
4060
4642
  function generateWith(adapter, kind, params) {
@@ -4373,12 +4955,67 @@ function brotu(options) {
4373
4955
  image: {
4374
4956
  submit: (params) => submit("image", params),
4375
4957
  generate: (params) => generate("image", params),
4376
- list: () => listFor("image")
4958
+ list: () => listFor("image"),
4959
+ upscale: (params) => {
4960
+ if (!params.imageUrl?.trim()) {
4961
+ return Promise.resolve(
4962
+ fail({
4963
+ code: "invalid_request",
4964
+ message: "image.upscale needs imageUrl.",
4965
+ model: params.model
4966
+ })
4967
+ );
4968
+ }
4969
+ const model = getModel(params.model);
4970
+ if (model && !model.nodeTypes.includes("image_upscale")) {
4971
+ return Promise.resolve(
4972
+ fail({
4973
+ code: "invalid_request",
4974
+ message: `"${params.model}" is not an image upscale model.`,
4975
+ model: params.model
4976
+ })
4977
+ );
4978
+ }
4979
+ return generate("image", {
4980
+ ...params,
4981
+ prompt: params.prompt ?? "",
4982
+ referenceImages: [
4983
+ params.imageUrl,
4984
+ ...params.referenceImages ?? []
4985
+ ]
4986
+ });
4987
+ }
4377
4988
  },
4378
4989
  video: {
4379
4990
  submit: (params) => submit("video", params),
4380
4991
  generate: (params) => generate("video", params),
4381
- list: () => listFor("video")
4992
+ list: () => listFor("video"),
4993
+ upscale: (params) => {
4994
+ if (!params.videoUrl?.trim()) {
4995
+ return Promise.resolve(
4996
+ fail({
4997
+ code: "invalid_request",
4998
+ message: "video.upscale needs videoUrl.",
4999
+ model: params.model
5000
+ })
5001
+ );
5002
+ }
5003
+ const model = getModel(params.model);
5004
+ if (model && !model.nodeTypes.includes("video_upscale")) {
5005
+ return Promise.resolve(
5006
+ fail({
5007
+ code: "invalid_request",
5008
+ message: `"${params.model}" is not a video upscale model. Interpolation models use ai.video.submit.`,
5009
+ model: params.model
5010
+ })
5011
+ );
5012
+ }
5013
+ return submit("video", {
5014
+ ...params,
5015
+ prompt: params.prompt ?? "",
5016
+ videoUrl: params.videoUrl
5017
+ });
5018
+ }
4382
5019
  },
4383
5020
  text: {
4384
5021
  submit: (params) => submit("text", params),
@@ -4487,6 +5124,11 @@ function brotu(options) {
4487
5124
  QWEN_TEXT_MODELS,
4488
5125
  QWEN_VIDEO_MODELS,
4489
5126
  QwenAdapter,
5127
+ TOPAZ_CATALOG,
5128
+ TOPAZ_IMAGE_MODELS,
5129
+ TOPAZ_MODELS,
5130
+ TOPAZ_VIDEO_MODELS,
5131
+ TopazAdapter,
4490
5132
  brotu,
4491
5133
  createS3Storage,
4492
5134
  deliverWebhook,