@brotu/ai 0.4.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/CATALOG.md +298 -4
- package/README.md +47 -8
- package/dist/adapters/brotu.adapter.d.ts.map +1 -1
- package/dist/adapters/topaz.adapter.d.ts +71 -0
- package/dist/adapters/topaz.adapter.d.ts.map +1 -0
- package/dist/catalog.cjs +143 -8
- package/dist/catalog.d.ts +22 -1
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +5 -1
- package/dist/{chunk-QOWHHKSI.js → chunk-QQIHMDJC.js} +146 -9
- package/dist/client.d.ts +25 -4
- package/dist/client.d.ts.map +1 -1
- package/dist/index.cjs +805 -75
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +610 -12
- package/dist/lib/hooks.d.ts +32 -0
- package/dist/lib/hooks.d.ts.map +1 -0
- package/dist/ports/content-generator.port.d.ts +12 -0
- package/dist/ports/content-generator.port.d.ts.map +1 -1
- package/dist/providers/topaz.models.d.ts +24 -0
- package/dist/providers/topaz.models.d.ts.map +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BROTU_SUPPORTED_CATEGORIES,
|
|
2
3
|
BYTEPLUS_CATALOG,
|
|
3
4
|
BYTEPLUS_IMAGE_MODELS,
|
|
4
5
|
BYTEPLUS_MODELS,
|
|
@@ -26,6 +27,11 @@ import {
|
|
|
26
27
|
QWEN_IMAGE_PATHS,
|
|
27
28
|
QWEN_TEXT_MODELS,
|
|
28
29
|
QWEN_VIDEO_MODELS,
|
|
30
|
+
TOPAZ_CATALOG,
|
|
31
|
+
TOPAZ_IMAGE_MODELS,
|
|
32
|
+
TOPAZ_MODELS,
|
|
33
|
+
TOPAZ_VIDEO_MODELS,
|
|
34
|
+
describeModels,
|
|
29
35
|
fieldsFor,
|
|
30
36
|
getAvailableModels,
|
|
31
37
|
getModel,
|
|
@@ -36,7 +42,7 @@ import {
|
|
|
36
42
|
resetCatalog,
|
|
37
43
|
resolveProvider,
|
|
38
44
|
videoPathFor
|
|
39
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-QQIHMDJC.js";
|
|
40
46
|
|
|
41
47
|
// src/lib/jobs.ts
|
|
42
48
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -111,7 +117,7 @@ var BrotuAdapter = class {
|
|
|
111
117
|
}
|
|
112
118
|
opts;
|
|
113
119
|
providerName = "brotu";
|
|
114
|
-
supportedTypes =
|
|
120
|
+
supportedTypes = BROTU_SUPPORTED_CATEGORIES;
|
|
115
121
|
workspaceIdCache;
|
|
116
122
|
get origin() {
|
|
117
123
|
return (this.opts.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
@@ -2423,6 +2429,475 @@ var QwenAdapter = class {
|
|
|
2423
2429
|
}
|
|
2424
2430
|
};
|
|
2425
2431
|
|
|
2432
|
+
// src/adapters/topaz.adapter.ts
|
|
2433
|
+
var DEFAULT_BASE_URL7 = "https://api.topazlabs.com";
|
|
2434
|
+
var POLL_INTERVAL_MS6 = 5e3;
|
|
2435
|
+
var DEFAULT_MAX_POLL_ATTEMPTS6 = 240;
|
|
2436
|
+
var DEFAULT_FPS = 30;
|
|
2437
|
+
var PIXELS = {
|
|
2438
|
+
"720p": { width: 1280, height: 720 },
|
|
2439
|
+
"720P": { width: 1280, height: 720 },
|
|
2440
|
+
"1080p": { width: 1920, height: 1080 },
|
|
2441
|
+
"1080P": { width: 1920, height: 1080 },
|
|
2442
|
+
"4k": { width: 3840, height: 2160 },
|
|
2443
|
+
"4K": { width: 3840, height: 2160 },
|
|
2444
|
+
"1K": { width: 1024, height: 1024 },
|
|
2445
|
+
"2K": { width: 2048, height: 2048 }
|
|
2446
|
+
};
|
|
2447
|
+
var TopazAdapter = class {
|
|
2448
|
+
providerName = "topaz";
|
|
2449
|
+
supportedTypes = ["video", "image"];
|
|
2450
|
+
opts;
|
|
2451
|
+
constructor(opts) {
|
|
2452
|
+
this.opts = opts;
|
|
2453
|
+
}
|
|
2454
|
+
get baseUrl() {
|
|
2455
|
+
return (this.opts.baseUrl ?? DEFAULT_BASE_URL7).replace(/\/$/, "");
|
|
2456
|
+
}
|
|
2457
|
+
get headers() {
|
|
2458
|
+
return {
|
|
2459
|
+
"X-API-Key": this.opts.apiKey,
|
|
2460
|
+
accept: "application/json",
|
|
2461
|
+
"Content-Type": "application/json"
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
binding(modelId) {
|
|
2465
|
+
const id = modelId ?? "";
|
|
2466
|
+
const found = TOPAZ_MODELS[id];
|
|
2467
|
+
if (!found) {
|
|
2468
|
+
throw new Error(
|
|
2469
|
+
`"${id}" is not a Topaz model. Known: ${TOPAZ_CATALOG.map((model) => model.id).join(", ")}.`
|
|
2470
|
+
);
|
|
2471
|
+
}
|
|
2472
|
+
return [id, found];
|
|
2473
|
+
}
|
|
2474
|
+
/** 720p / 1080p / 4k, or an explicit "1920x1080" / "1920*1080" pair. */
|
|
2475
|
+
resolutionOf(resolution) {
|
|
2476
|
+
if (!resolution) return PIXELS["1080p"];
|
|
2477
|
+
const named = PIXELS[resolution];
|
|
2478
|
+
if (named) return named;
|
|
2479
|
+
const match = /^(\d{2,5})[x*](\d{2,5})$/i.exec(resolution.trim());
|
|
2480
|
+
if (match?.[1] && match[2]) {
|
|
2481
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
2482
|
+
}
|
|
2483
|
+
throw new Error(
|
|
2484
|
+
`Topaz wants 720p, 1080p, 4k or WxH, not "${resolution}".`
|
|
2485
|
+
);
|
|
2486
|
+
}
|
|
2487
|
+
containerOf(url) {
|
|
2488
|
+
const path = url.split("?")[0]?.toLowerCase() ?? "";
|
|
2489
|
+
if (path.endsWith(".mov")) return "mov";
|
|
2490
|
+
if (path.endsWith(".mkv")) return "mkv";
|
|
2491
|
+
return "mp4";
|
|
2492
|
+
}
|
|
2493
|
+
filterOf(binding, params) {
|
|
2494
|
+
const extras = params.providerOptions?.topaz ?? {};
|
|
2495
|
+
if (binding.kind === "upscale") {
|
|
2496
|
+
return { auto: "Auto", ...extras, model: binding.vendorModel };
|
|
2497
|
+
}
|
|
2498
|
+
const slowmo = numberish(extras.slowmo) ?? 1;
|
|
2499
|
+
const fps = numberish(extras.fps) ?? DEFAULT_FPS;
|
|
2500
|
+
if (slowmo < 1 || slowmo > 16) {
|
|
2501
|
+
throw new Error(`Topaz interpolation slowmo is 1\u201316, not ${slowmo}.`);
|
|
2502
|
+
}
|
|
2503
|
+
if (fps < 15 || fps > 240) {
|
|
2504
|
+
throw new Error(`Topaz interpolation fps is 15\u2013240, not ${fps}.`);
|
|
2505
|
+
}
|
|
2506
|
+
const filter = {
|
|
2507
|
+
model: binding.vendorModel,
|
|
2508
|
+
slowmo,
|
|
2509
|
+
fps
|
|
2510
|
+
};
|
|
2511
|
+
if (typeof extras.duplicate === "boolean") {
|
|
2512
|
+
filter.duplicate = extras.duplicate;
|
|
2513
|
+
}
|
|
2514
|
+
if (extras.duplicateThreshold !== void 0) {
|
|
2515
|
+
filter.duplicateThreshold = extras.duplicateThreshold;
|
|
2516
|
+
}
|
|
2517
|
+
return filter;
|
|
2518
|
+
}
|
|
2519
|
+
requestBody(binding, params, sourceUrl) {
|
|
2520
|
+
const extras = params.providerOptions?.topaz ?? {};
|
|
2521
|
+
const fps = numberish(extras.fps) ?? DEFAULT_FPS;
|
|
2522
|
+
return {
|
|
2523
|
+
source: { container: this.containerOf(sourceUrl) },
|
|
2524
|
+
filters: [this.filterOf(binding, params)],
|
|
2525
|
+
output: {
|
|
2526
|
+
resolution: this.resolutionOf(params.resolution),
|
|
2527
|
+
frameRate: fps,
|
|
2528
|
+
audioCodec: "AAC",
|
|
2529
|
+
audioTransfer: "Copy",
|
|
2530
|
+
dynamicCompressionLevel: "High",
|
|
2531
|
+
container: "mp4"
|
|
2532
|
+
}
|
|
2533
|
+
};
|
|
2534
|
+
}
|
|
2535
|
+
async request(path, init) {
|
|
2536
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
2537
|
+
method: init?.method ?? "GET",
|
|
2538
|
+
headers: this.headers,
|
|
2539
|
+
body: init?.body !== void 0 ? JSON.stringify(init.body) : void 0
|
|
2540
|
+
});
|
|
2541
|
+
const payload = await response.json();
|
|
2542
|
+
if (!response.ok) {
|
|
2543
|
+
throw new Error(
|
|
2544
|
+
payload.message ?? `Topaz returned ${response.status} for ${path}.`
|
|
2545
|
+
);
|
|
2546
|
+
}
|
|
2547
|
+
return payload;
|
|
2548
|
+
}
|
|
2549
|
+
async downloadSource(url) {
|
|
2550
|
+
const response = await fetch(url);
|
|
2551
|
+
if (!response.ok) {
|
|
2552
|
+
throw new Error(`Could not download the source video (${response.status}).`);
|
|
2553
|
+
}
|
|
2554
|
+
return {
|
|
2555
|
+
bytes: await response.arrayBuffer(),
|
|
2556
|
+
contentType: response.headers.get("content-type") ?? "video/mp4"
|
|
2557
|
+
};
|
|
2558
|
+
}
|
|
2559
|
+
async submitTask(params) {
|
|
2560
|
+
const [, binding] = this.binding(params.model);
|
|
2561
|
+
if (binding.surface !== "video") {
|
|
2562
|
+
throw new Error(
|
|
2563
|
+
`Topaz "${params.model}" is an image model. Use ai.image.`
|
|
2564
|
+
);
|
|
2565
|
+
}
|
|
2566
|
+
const sourceUrl = params.videoUrl ?? params.videoUrls?.[0];
|
|
2567
|
+
if (!sourceUrl) {
|
|
2568
|
+
throw new Error(
|
|
2569
|
+
`Topaz "${params.model}" needs a source video \u2014 pass videoUrl.`
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2572
|
+
const source = await this.downloadSource(sourceUrl);
|
|
2573
|
+
const created = await this.request("/video/express", {
|
|
2574
|
+
method: "POST",
|
|
2575
|
+
body: this.requestBody(binding, params, sourceUrl)
|
|
2576
|
+
});
|
|
2577
|
+
const taskId = created.requestId?.trim();
|
|
2578
|
+
const uploadUrl = created.uploadUrls?.[0];
|
|
2579
|
+
if (!taskId || !uploadUrl) {
|
|
2580
|
+
throw new Error(
|
|
2581
|
+
"Topaz accepted the request but returned no request id or upload URL."
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
const uploaded = await fetch(uploadUrl, {
|
|
2585
|
+
method: "PUT",
|
|
2586
|
+
headers: { "Content-Type": source.contentType },
|
|
2587
|
+
body: source.bytes
|
|
2588
|
+
});
|
|
2589
|
+
if (!uploaded.ok) {
|
|
2590
|
+
throw new Error(`Topaz upload failed (${uploaded.status}).`);
|
|
2591
|
+
}
|
|
2592
|
+
return { taskId, pollEndpoint: `/video/${taskId}/status` };
|
|
2593
|
+
}
|
|
2594
|
+
outputsFrom(payload, job) {
|
|
2595
|
+
const url = payload.download?.url;
|
|
2596
|
+
if (!url) return [];
|
|
2597
|
+
const expiresAtMs = payload.download?.expiresAt;
|
|
2598
|
+
return [
|
|
2599
|
+
{
|
|
2600
|
+
url,
|
|
2601
|
+
mimeType: "video/mp4",
|
|
2602
|
+
taskId: job.id,
|
|
2603
|
+
expiresAt: expiresAtMs ? new Date(expiresAtMs).toISOString() : expiresInHours(24),
|
|
2604
|
+
raw: {
|
|
2605
|
+
status: payload.status,
|
|
2606
|
+
progress: payload.progress,
|
|
2607
|
+
estimates: payload.estimates,
|
|
2608
|
+
outputSize: payload.outputSize
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
];
|
|
2612
|
+
}
|
|
2613
|
+
snapshot(payload, job) {
|
|
2614
|
+
const raw = (payload.status ?? "").toLowerCase();
|
|
2615
|
+
if (raw === "failed" || raw === "canceled") {
|
|
2616
|
+
return {
|
|
2617
|
+
status: "failed",
|
|
2618
|
+
error: payload.message ?? `Topaz request ${raw}.`
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
if (raw !== "complete") return { status: "pending" };
|
|
2622
|
+
const outputs = this.outputsFrom(payload, job);
|
|
2623
|
+
if (outputs.length === 0) return { status: "pending" };
|
|
2624
|
+
return {
|
|
2625
|
+
status: "succeeded",
|
|
2626
|
+
result: {
|
|
2627
|
+
success: true,
|
|
2628
|
+
outputs,
|
|
2629
|
+
creditsUsed: 0,
|
|
2630
|
+
provider: this.providerName,
|
|
2631
|
+
model: job.model,
|
|
2632
|
+
processingTimeMs: 0
|
|
2633
|
+
}
|
|
2634
|
+
};
|
|
2635
|
+
}
|
|
2636
|
+
async run(params) {
|
|
2637
|
+
const startedAt = Date.now();
|
|
2638
|
+
let modelId = params.model ?? "(none)";
|
|
2639
|
+
const failure = (error) => ({
|
|
2640
|
+
success: false,
|
|
2641
|
+
outputs: [],
|
|
2642
|
+
creditsUsed: 0,
|
|
2643
|
+
provider: this.providerName,
|
|
2644
|
+
model: modelId,
|
|
2645
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2646
|
+
error
|
|
2647
|
+
});
|
|
2648
|
+
let submitted;
|
|
2649
|
+
try {
|
|
2650
|
+
modelId = this.binding(params.model)[0];
|
|
2651
|
+
submitted = await this.submitTask(params);
|
|
2652
|
+
} catch (error) {
|
|
2653
|
+
if (isPendingJob(error)) throw error;
|
|
2654
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2655
|
+
}
|
|
2656
|
+
if (isSubmitMode()) {
|
|
2657
|
+
throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
|
|
2658
|
+
}
|
|
2659
|
+
const job = {
|
|
2660
|
+
id: submitted.taskId,
|
|
2661
|
+
provider: this.providerName,
|
|
2662
|
+
model: modelId,
|
|
2663
|
+
kind: "video",
|
|
2664
|
+
pollEndpoint: submitted.pollEndpoint,
|
|
2665
|
+
params,
|
|
2666
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
2667
|
+
};
|
|
2668
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
|
|
2669
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2670
|
+
const snapshot = await this.completeJob(job);
|
|
2671
|
+
if (snapshot.status === "failed") {
|
|
2672
|
+
return failure(snapshot.error ?? "Topaz request failed.");
|
|
2673
|
+
}
|
|
2674
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2675
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
2676
|
+
}
|
|
2677
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
|
|
2678
|
+
}
|
|
2679
|
+
return failure(
|
|
2680
|
+
`Topaz request ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
2683
|
+
async completeJob(job) {
|
|
2684
|
+
try {
|
|
2685
|
+
if (job.pollEndpoint?.startsWith("/image/")) {
|
|
2686
|
+
return this.imageSnapshot(job);
|
|
2687
|
+
}
|
|
2688
|
+
const payload = await this.request(
|
|
2689
|
+
job.pollEndpoint ?? `/video/${job.id}/status`
|
|
2690
|
+
);
|
|
2691
|
+
return this.snapshot(payload, job);
|
|
2692
|
+
} catch (error) {
|
|
2693
|
+
return {
|
|
2694
|
+
status: "failed",
|
|
2695
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
generateVideo(params) {
|
|
2700
|
+
return this.run(params);
|
|
2701
|
+
}
|
|
2702
|
+
imageBody(binding, params, sourceUrl) {
|
|
2703
|
+
const extras = params.providerOptions?.topaz ?? {};
|
|
2704
|
+
const pixels = params.resolution ? this.resolutionOf(params.resolution) : void 0;
|
|
2705
|
+
const fields = {
|
|
2706
|
+
source_url: sourceUrl,
|
|
2707
|
+
model: binding.vendorModel,
|
|
2708
|
+
output_format: params.outputFormat === "png" ? "png" : params.outputFormat === "jpeg" ? "jpeg" : "jpeg"
|
|
2709
|
+
};
|
|
2710
|
+
if (pixels) {
|
|
2711
|
+
fields.output_width = String(pixels.width);
|
|
2712
|
+
fields.output_height = String(pixels.height);
|
|
2713
|
+
}
|
|
2714
|
+
for (const [key, value] of Object.entries(extras)) {
|
|
2715
|
+
if (value === void 0) continue;
|
|
2716
|
+
fields[key] = typeof value === "string" ? value : String(value);
|
|
2717
|
+
}
|
|
2718
|
+
return fields;
|
|
2719
|
+
}
|
|
2720
|
+
async submitImage(params) {
|
|
2721
|
+
const [, binding] = this.binding(params.model);
|
|
2722
|
+
if (binding.surface !== "image" || !binding.imagePath) {
|
|
2723
|
+
throw new Error(
|
|
2724
|
+
`Topaz "${params.model}" is a video model. Use ai.video with videoUrl.`
|
|
2725
|
+
);
|
|
2726
|
+
}
|
|
2727
|
+
const sourceUrl = params.referenceImages?.[0];
|
|
2728
|
+
if (!sourceUrl) {
|
|
2729
|
+
throw new Error(
|
|
2730
|
+
`Topaz "${params.model}" needs a source image \u2014 pass referenceImages.`
|
|
2731
|
+
);
|
|
2732
|
+
}
|
|
2733
|
+
const form = new FormData();
|
|
2734
|
+
for (const [key, value] of Object.entries(
|
|
2735
|
+
this.imageBody(binding, params, sourceUrl)
|
|
2736
|
+
)) {
|
|
2737
|
+
form.set(key, value);
|
|
2738
|
+
}
|
|
2739
|
+
const response = await fetch(`${this.baseUrl}${binding.imagePath}`, {
|
|
2740
|
+
method: "POST",
|
|
2741
|
+
headers: {
|
|
2742
|
+
"X-API-Key": this.opts.apiKey,
|
|
2743
|
+
accept: "application/json"
|
|
2744
|
+
},
|
|
2745
|
+
body: form
|
|
2746
|
+
});
|
|
2747
|
+
const payload = await response.json();
|
|
2748
|
+
if (!response.ok) {
|
|
2749
|
+
throw new Error(
|
|
2750
|
+
payload.message ?? `Topaz returned ${response.status} enhancing the image.`
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
const taskId = payload.process_id?.trim();
|
|
2754
|
+
if (!taskId) {
|
|
2755
|
+
throw new Error(
|
|
2756
|
+
"Topaz accepted the image but returned no process id."
|
|
2757
|
+
);
|
|
2758
|
+
}
|
|
2759
|
+
return { taskId, pollEndpoint: `/image/v1/status/${taskId}` };
|
|
2760
|
+
}
|
|
2761
|
+
async imageSnapshot(job) {
|
|
2762
|
+
const payload = await this.request(
|
|
2763
|
+
job.pollEndpoint ?? `/image/v1/status/${job.id}`
|
|
2764
|
+
);
|
|
2765
|
+
const raw = (payload.status ?? "").toLowerCase();
|
|
2766
|
+
if (raw === "failed" || raw === "cancelled") {
|
|
2767
|
+
return {
|
|
2768
|
+
status: "failed",
|
|
2769
|
+
error: `Topaz image ${raw}.`
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
if (raw !== "completed") return { status: "pending" };
|
|
2773
|
+
let url = payload.download_url;
|
|
2774
|
+
let expiresAt = payload.eta ? new Date(payload.eta > 1e10 ? payload.eta : payload.eta * 1e3).toISOString() : void 0;
|
|
2775
|
+
if (!url) {
|
|
2776
|
+
const download = await this.request(
|
|
2777
|
+
`/image/v1/download/${job.id}`
|
|
2778
|
+
);
|
|
2779
|
+
url = download.download_url;
|
|
2780
|
+
if (download.expiry) {
|
|
2781
|
+
expiresAt = new Date(
|
|
2782
|
+
download.expiry > 1e10 ? download.expiry : download.expiry * 1e3
|
|
2783
|
+
).toISOString();
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
if (!url) return { status: "pending" };
|
|
2787
|
+
return {
|
|
2788
|
+
status: "succeeded",
|
|
2789
|
+
result: {
|
|
2790
|
+
success: true,
|
|
2791
|
+
outputs: [
|
|
2792
|
+
{
|
|
2793
|
+
url,
|
|
2794
|
+
mimeType: "image/jpeg",
|
|
2795
|
+
taskId: job.id,
|
|
2796
|
+
expiresAt: expiresAt ?? expiresInHours(1),
|
|
2797
|
+
raw: {
|
|
2798
|
+
status: payload.status,
|
|
2799
|
+
progress: payload.progress,
|
|
2800
|
+
model: payload.model
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
],
|
|
2804
|
+
creditsUsed: payload.credits ?? 0,
|
|
2805
|
+
provider: this.providerName,
|
|
2806
|
+
model: job.model,
|
|
2807
|
+
processingTimeMs: 0
|
|
2808
|
+
}
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
async runImage(params) {
|
|
2812
|
+
const startedAt = Date.now();
|
|
2813
|
+
let modelId = params.model ?? "(none)";
|
|
2814
|
+
const failure = (error) => ({
|
|
2815
|
+
success: false,
|
|
2816
|
+
outputs: [],
|
|
2817
|
+
creditsUsed: 0,
|
|
2818
|
+
provider: this.providerName,
|
|
2819
|
+
model: modelId,
|
|
2820
|
+
processingTimeMs: Date.now() - startedAt,
|
|
2821
|
+
error
|
|
2822
|
+
});
|
|
2823
|
+
let submitted;
|
|
2824
|
+
try {
|
|
2825
|
+
modelId = this.binding(params.model)[0];
|
|
2826
|
+
submitted = await this.submitImage(params);
|
|
2827
|
+
} catch (error) {
|
|
2828
|
+
if (isPendingJob(error)) throw error;
|
|
2829
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
2830
|
+
}
|
|
2831
|
+
if (isSubmitMode()) {
|
|
2832
|
+
throw new PendingJob(submitted.taskId, submitted.pollEndpoint);
|
|
2833
|
+
}
|
|
2834
|
+
const job = {
|
|
2835
|
+
id: submitted.taskId,
|
|
2836
|
+
provider: this.providerName,
|
|
2837
|
+
model: modelId,
|
|
2838
|
+
kind: "image",
|
|
2839
|
+
pollEndpoint: submitted.pollEndpoint,
|
|
2840
|
+
params,
|
|
2841
|
+
submittedAt: new Date(startedAt).toISOString()
|
|
2842
|
+
};
|
|
2843
|
+
const maxAttempts = this.opts.maxPollAttempts ?? DEFAULT_MAX_POLL_ATTEMPTS6;
|
|
2844
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
2845
|
+
const snapshot = await this.completeJob(job);
|
|
2846
|
+
if (snapshot.status === "failed") {
|
|
2847
|
+
return failure(snapshot.error ?? "Topaz image failed.");
|
|
2848
|
+
}
|
|
2849
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2850
|
+
return { ...snapshot.result, processingTimeMs: Date.now() - startedAt };
|
|
2851
|
+
}
|
|
2852
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS6));
|
|
2853
|
+
}
|
|
2854
|
+
return failure(
|
|
2855
|
+
`Topaz image ${submitted.taskId} did not finish after ${maxAttempts} checks.`
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
generateImage(params) {
|
|
2859
|
+
return this.runImage(params);
|
|
2860
|
+
}
|
|
2861
|
+
generateText(params) {
|
|
2862
|
+
return Promise.resolve(unsupported(this.providerName, params.model, "text"));
|
|
2863
|
+
}
|
|
2864
|
+
generateAudio(params) {
|
|
2865
|
+
return Promise.resolve(unsupported(this.providerName, params.model, "audio"));
|
|
2866
|
+
}
|
|
2867
|
+
estimateCost(type, params) {
|
|
2868
|
+
return Promise.resolve(estimateFor(this.providerName, type, params));
|
|
2869
|
+
}
|
|
2870
|
+
supportsModel(model) {
|
|
2871
|
+
return model in TOPAZ_MODELS;
|
|
2872
|
+
}
|
|
2873
|
+
getAvailableModels() {
|
|
2874
|
+
return TOPAZ_CATALOG.map((model) => ({
|
|
2875
|
+
id: model.id,
|
|
2876
|
+
name: model.name,
|
|
2877
|
+
type: model.category
|
|
2878
|
+
}));
|
|
2879
|
+
}
|
|
2880
|
+
};
|
|
2881
|
+
function numberish(value) {
|
|
2882
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2883
|
+
if (typeof value === "string" && value.length > 0) {
|
|
2884
|
+
const parsed = Number(value);
|
|
2885
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
2886
|
+
}
|
|
2887
|
+
return void 0;
|
|
2888
|
+
}
|
|
2889
|
+
function unsupported(provider, model, kind) {
|
|
2890
|
+
return {
|
|
2891
|
+
success: false,
|
|
2892
|
+
outputs: [],
|
|
2893
|
+
creditsUsed: 0,
|
|
2894
|
+
provider,
|
|
2895
|
+
model: model ?? "",
|
|
2896
|
+
processingTimeMs: 0,
|
|
2897
|
+
error: `Topaz does not generate ${kind} on this adapter.`
|
|
2898
|
+
};
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2426
2901
|
// src/helpers/result.ts
|
|
2427
2902
|
function ok(data) {
|
|
2428
2903
|
return { data, error: null };
|
|
@@ -2439,6 +2914,22 @@ function failFrom(code, cause, context) {
|
|
|
2439
2914
|
});
|
|
2440
2915
|
}
|
|
2441
2916
|
|
|
2917
|
+
// src/lib/hooks.ts
|
|
2918
|
+
function hookName(kind, stage) {
|
|
2919
|
+
return `on${kind[0].toUpperCase()}${kind.slice(1)}${stage}`;
|
|
2920
|
+
}
|
|
2921
|
+
function findHook(hooks, kind, stage) {
|
|
2922
|
+
if (!hooks || !kind) return void 0;
|
|
2923
|
+
return hooks[hookName(kind, stage)];
|
|
2924
|
+
}
|
|
2925
|
+
async function runHook(hook, event) {
|
|
2926
|
+
if (!hook) return;
|
|
2927
|
+
try {
|
|
2928
|
+
await hook(event);
|
|
2929
|
+
} catch {
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2442
2933
|
// src/lib/storage.ts
|
|
2443
2934
|
async function loadS3(config) {
|
|
2444
2935
|
let s3;
|
|
@@ -2607,9 +3098,10 @@ var NATIVE_PROVIDERS = [
|
|
|
2607
3098
|
"google",
|
|
2608
3099
|
"kling",
|
|
2609
3100
|
"openai",
|
|
2610
|
-
"qwen"
|
|
3101
|
+
"qwen",
|
|
3102
|
+
"topaz"
|
|
2611
3103
|
];
|
|
2612
|
-
function
|
|
3104
|
+
function brotu(options) {
|
|
2613
3105
|
const apiKey = options.apiKey?.trim();
|
|
2614
3106
|
if (!apiKey) {
|
|
2615
3107
|
throw new Error(
|
|
@@ -2706,6 +3198,12 @@ function brotuClient(options) {
|
|
|
2706
3198
|
baseUrl: provider.baseUrl
|
|
2707
3199
|
});
|
|
2708
3200
|
}
|
|
3201
|
+
if (provider.id === "topaz") {
|
|
3202
|
+
return new TopazAdapter({
|
|
3203
|
+
apiKey: provider.apiKey,
|
|
3204
|
+
baseUrl: provider.baseUrl
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
2709
3207
|
return void 0;
|
|
2710
3208
|
}
|
|
2711
3209
|
function generateWith(adapter, kind, params) {
|
|
@@ -2722,7 +3220,10 @@ function brotuClient(options) {
|
|
|
2722
3220
|
}
|
|
2723
3221
|
async function notifySettled(input) {
|
|
2724
3222
|
const config = webhookFor(input.params);
|
|
2725
|
-
|
|
3223
|
+
const kind = input.kind ?? input.job?.kind;
|
|
3224
|
+
const stage = input.event === "generation.succeeded" ? "Success" : "Error";
|
|
3225
|
+
const hook = findHook(options.hooks, kind, stage);
|
|
3226
|
+
if (!config && !hook) return;
|
|
2726
3227
|
const jobId = input.job?.id;
|
|
2727
3228
|
if (jobId) {
|
|
2728
3229
|
if (notifiedJobIds.has(jobId)) return;
|
|
@@ -2733,14 +3234,38 @@ function brotuClient(options) {
|
|
|
2733
3234
|
jobId,
|
|
2734
3235
|
provider: input.provider ?? input.job?.provider,
|
|
2735
3236
|
model: input.model ?? input.job?.model,
|
|
2736
|
-
kind
|
|
3237
|
+
kind,
|
|
2737
3238
|
outputs: input.outputs,
|
|
2738
3239
|
error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
|
|
2739
3240
|
metadata: input.metadata ?? input.job?.metadata,
|
|
2740
3241
|
processingTimeMs: input.processingTimeMs,
|
|
2741
3242
|
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2742
3243
|
};
|
|
2743
|
-
|
|
3244
|
+
if (hook && kind) {
|
|
3245
|
+
await runHook(hook, {
|
|
3246
|
+
kind,
|
|
3247
|
+
stage,
|
|
3248
|
+
provider: payload.provider ?? "",
|
|
3249
|
+
model: payload.model ?? "",
|
|
3250
|
+
jobId: payload.jobId,
|
|
3251
|
+
outputs: payload.outputs,
|
|
3252
|
+
error: payload.error,
|
|
3253
|
+
metadata: payload.metadata,
|
|
3254
|
+
processingTimeMs: payload.processingTimeMs,
|
|
3255
|
+
at: payload.completedAt
|
|
3256
|
+
});
|
|
3257
|
+
}
|
|
3258
|
+
if (config) await deliverWebhook(config, payload);
|
|
3259
|
+
}
|
|
3260
|
+
function notifyLoading(kind, routed, params) {
|
|
3261
|
+
return runHook(findHook(options.hooks, kind, "Loading"), {
|
|
3262
|
+
kind,
|
|
3263
|
+
stage: "Loading",
|
|
3264
|
+
provider: routed.provider,
|
|
3265
|
+
model: routed.model,
|
|
3266
|
+
metadata: params.metadata,
|
|
3267
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
3268
|
+
});
|
|
2744
3269
|
}
|
|
2745
3270
|
async function toGeneration(raw, metadata) {
|
|
2746
3271
|
if (!raw.success) {
|
|
@@ -2763,6 +3288,7 @@ function brotuClient(options) {
|
|
|
2763
3288
|
async function generate(kind, params) {
|
|
2764
3289
|
const routed = route(params.model);
|
|
2765
3290
|
if (routed.error) return fail(routed.error);
|
|
3291
|
+
await notifyLoading(kind, routed.data, params);
|
|
2766
3292
|
try {
|
|
2767
3293
|
const result = await toGeneration(
|
|
2768
3294
|
await generateWith(routed.data.adapter, kind, params),
|
|
@@ -2813,6 +3339,7 @@ function brotuClient(options) {
|
|
|
2813
3339
|
async function submit(kind, params) {
|
|
2814
3340
|
const routed = route(params.model);
|
|
2815
3341
|
if (routed.error) return fail(routed.error);
|
|
3342
|
+
await notifyLoading(kind, routed.data, params);
|
|
2816
3343
|
const base = {
|
|
2817
3344
|
provider: routed.data.provider,
|
|
2818
3345
|
model: routed.data.model,
|
|
@@ -2984,24 +3511,88 @@ function brotuClient(options) {
|
|
|
2984
3511
|
}
|
|
2985
3512
|
};
|
|
2986
3513
|
}
|
|
3514
|
+
function listFor(category) {
|
|
3515
|
+
return describeModels(optionsWithKey).filter(
|
|
3516
|
+
(entry) => entry.model.category === category
|
|
3517
|
+
);
|
|
3518
|
+
}
|
|
2987
3519
|
return {
|
|
2988
3520
|
google: googleNamespace(),
|
|
2989
3521
|
kling: klingNamespace(),
|
|
2990
3522
|
image: {
|
|
2991
3523
|
submit: (params) => submit("image", params),
|
|
2992
|
-
generate: (params) => generate("image", params)
|
|
3524
|
+
generate: (params) => generate("image", params),
|
|
3525
|
+
list: () => listFor("image"),
|
|
3526
|
+
upscale: (params) => {
|
|
3527
|
+
if (!params.imageUrl?.trim()) {
|
|
3528
|
+
return Promise.resolve(
|
|
3529
|
+
fail({
|
|
3530
|
+
code: "invalid_request",
|
|
3531
|
+
message: "image.upscale needs imageUrl.",
|
|
3532
|
+
model: params.model
|
|
3533
|
+
})
|
|
3534
|
+
);
|
|
3535
|
+
}
|
|
3536
|
+
const model = getModel(params.model);
|
|
3537
|
+
if (model && !model.nodeTypes.includes("image_upscale")) {
|
|
3538
|
+
return Promise.resolve(
|
|
3539
|
+
fail({
|
|
3540
|
+
code: "invalid_request",
|
|
3541
|
+
message: `"${params.model}" is not an image upscale model.`,
|
|
3542
|
+
model: params.model
|
|
3543
|
+
})
|
|
3544
|
+
);
|
|
3545
|
+
}
|
|
3546
|
+
return generate("image", {
|
|
3547
|
+
...params,
|
|
3548
|
+
prompt: params.prompt ?? "",
|
|
3549
|
+
referenceImages: [
|
|
3550
|
+
params.imageUrl,
|
|
3551
|
+
...params.referenceImages ?? []
|
|
3552
|
+
]
|
|
3553
|
+
});
|
|
3554
|
+
}
|
|
2993
3555
|
},
|
|
2994
3556
|
video: {
|
|
2995
3557
|
submit: (params) => submit("video", params),
|
|
2996
|
-
generate: (params) => generate("video", params)
|
|
3558
|
+
generate: (params) => generate("video", params),
|
|
3559
|
+
list: () => listFor("video"),
|
|
3560
|
+
upscale: (params) => {
|
|
3561
|
+
if (!params.videoUrl?.trim()) {
|
|
3562
|
+
return Promise.resolve(
|
|
3563
|
+
fail({
|
|
3564
|
+
code: "invalid_request",
|
|
3565
|
+
message: "video.upscale needs videoUrl.",
|
|
3566
|
+
model: params.model
|
|
3567
|
+
})
|
|
3568
|
+
);
|
|
3569
|
+
}
|
|
3570
|
+
const model = getModel(params.model);
|
|
3571
|
+
if (model && !model.nodeTypes.includes("video_upscale")) {
|
|
3572
|
+
return Promise.resolve(
|
|
3573
|
+
fail({
|
|
3574
|
+
code: "invalid_request",
|
|
3575
|
+
message: `"${params.model}" is not a video upscale model. Interpolation models use ai.video.submit.`,
|
|
3576
|
+
model: params.model
|
|
3577
|
+
})
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
3580
|
+
return submit("video", {
|
|
3581
|
+
...params,
|
|
3582
|
+
prompt: params.prompt ?? "",
|
|
3583
|
+
videoUrl: params.videoUrl
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
2997
3586
|
},
|
|
2998
3587
|
text: {
|
|
2999
3588
|
submit: (params) => submit("text", params),
|
|
3000
|
-
generate: (params) => generate("text", params)
|
|
3589
|
+
generate: (params) => generate("text", params),
|
|
3590
|
+
list: () => listFor("text")
|
|
3001
3591
|
},
|
|
3002
3592
|
audio: {
|
|
3003
3593
|
submit: (params) => submit("audio", params),
|
|
3004
|
-
generate: (params) => generate("audio", params)
|
|
3594
|
+
generate: (params) => generate("audio", params),
|
|
3595
|
+
list: () => listFor("audio")
|
|
3005
3596
|
},
|
|
3006
3597
|
webhook: {
|
|
3007
3598
|
set(value) {
|
|
@@ -3067,6 +3658,7 @@ function brotuClient(options) {
|
|
|
3067
3658
|
};
|
|
3068
3659
|
}
|
|
3069
3660
|
export {
|
|
3661
|
+
BROTU_SUPPORTED_CATEGORIES,
|
|
3070
3662
|
BYTEPLUS_CATALOG,
|
|
3071
3663
|
BYTEPLUS_MODELS,
|
|
3072
3664
|
BrotuAdapter,
|
|
@@ -3098,9 +3690,15 @@ export {
|
|
|
3098
3690
|
QWEN_TEXT_MODELS,
|
|
3099
3691
|
QWEN_VIDEO_MODELS,
|
|
3100
3692
|
QwenAdapter,
|
|
3101
|
-
|
|
3693
|
+
TOPAZ_CATALOG,
|
|
3694
|
+
TOPAZ_IMAGE_MODELS,
|
|
3695
|
+
TOPAZ_MODELS,
|
|
3696
|
+
TOPAZ_VIDEO_MODELS,
|
|
3697
|
+
TopazAdapter,
|
|
3698
|
+
brotu,
|
|
3102
3699
|
createS3Storage,
|
|
3103
3700
|
deliverWebhook,
|
|
3701
|
+
describeModels,
|
|
3104
3702
|
fail,
|
|
3105
3703
|
getAvailableModels,
|
|
3106
3704
|
getModel,
|