@brotu/ai 0.1.0 → 0.3.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 +154 -2
- package/LICENSE +1 -1
- package/README.md +242 -58
- package/dist/adapters/google.adapter.d.ts +4 -2
- package/dist/adapters/google.adapter.d.ts.map +1 -1
- package/dist/adapters/openai.adapter.d.ts +2 -1
- package/dist/adapters/openai.adapter.d.ts.map +1 -1
- package/dist/catalog.cjs +114 -17
- package/dist/catalog.js +1 -1
- package/dist/{chunk-GKLTQ55S.js → chunk-PL534BFN.js} +118 -17
- package/dist/client.d.ts +10 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/index.cjs +554 -43
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +438 -26
- package/dist/lib/webhook.d.ts +28 -0
- package/dist/lib/webhook.d.ts.map +1 -0
- package/dist/ports/content-generator.port.d.ts +6 -0
- package/dist/ports/content-generator.port.d.ts.map +1 -1
- package/dist/providers/google.models.d.ts +23 -0
- package/dist/providers/google.models.d.ts.map +1 -1
- package/dist/providers/openai.models.d.ts +7 -0
- package/dist/providers/openai.models.d.ts.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +6 -2
package/dist/index.d.ts
CHANGED
|
@@ -17,13 +17,14 @@ export type * from "./constants/model.types";
|
|
|
17
17
|
export { type AIError, type AIErrorCode, fail, type Generation, ok, type Result, } from "./helpers/result";
|
|
18
18
|
export { isPendingJob, type Job, type JobSnapshot, type JobStatus, PendingJob, } from "./lib/jobs";
|
|
19
19
|
export { createS3Storage, persistOutputs, type S3StorageConfig, type Storage, } from "./lib/storage";
|
|
20
|
+
export { deliverWebhook, resolveWebhook, type WebhookConfig, type WebhookEvent, type WebhookEventName, } from "./lib/webhook";
|
|
20
21
|
export type * from "./ports/content-generator.port";
|
|
21
22
|
export { BYTEPLUS_CATALOG, BYTEPLUS_MODELS, type BytePlusModelBinding, } from "./providers/byteplus.models";
|
|
22
23
|
export { ELEVENLABS_CATALOG, ELEVENLABS_MODELS, ELEVENLABS_OUTPUT_FORMATS, } from "./providers/elevenlabs.models";
|
|
23
|
-
export { GOOGLE_CATALOG, GOOGLE_IMAGE_MODELS, GOOGLE_VIDEO_MODELS, } from "./providers/google.models";
|
|
24
|
+
export { GOOGLE_AUDIO_MODELS, GOOGLE_CATALOG, GOOGLE_IMAGE_MODELS, GOOGLE_TEXT_MODELS, GOOGLE_TTS_VOICES, GOOGLE_VIDEO_MODELS, } from "./providers/google.models";
|
|
24
25
|
export type { KlingModelBinding } from "./providers/kling.models";
|
|
25
26
|
export { type AvatarInput, type ImageOmniInput, KLING_CAPABILITIES, KLING_CATALOG, KLING_MODELS, type KlingCapability, type KlingRequestOptions, type KlingResultField, type MotionControlInput, type OmniVideoInput, type OutpaintingInput, } from "./providers/kling.models";
|
|
26
|
-
export { OPENAI_CATALOG, OPENAI_IMAGE_MODELS, } from "./providers/openai.models";
|
|
27
|
+
export { OPENAI_CATALOG, OPENAI_IMAGE_MODELS, OPENAI_TEXT_MODELS, } from "./providers/openai.models";
|
|
27
28
|
export { QWEN_AUDIO_MODELS, QWEN_CATALOG, QWEN_IMAGE_MODELS, QWEN_TEXT_MODELS, QWEN_VIDEO_MODELS, type QwenImageBinding, type QwenVideoBinding, } from "./providers/qwen.models";
|
|
28
29
|
export type { BrotuAIOptions, ProviderConfig, ReferenceVideoInfo, ResolvedProvider, } from "./types";
|
|
29
30
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,YAAY,EACX,wBAAwB,EACxB,eAAe,GACf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EACN,kBAAkB,EAClB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,eAAe,GACf,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,mBAAmB,yBAAyB,CAAC;AAC7C,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,IAAI,EACJ,KAAK,UAAU,EACf,EAAE,EACF,KAAK,MAAM,GACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACN,YAAY,EACZ,KAAK,GAAG,EACR,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,UAAU,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,eAAe,EACf,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,OAAO,GACZ,MAAM,eAAe,CAAC;AACvB,mBAAmB,gCAAgC,CAAC;AACpD,OAAO,EACN,gBAAgB,EAChB,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACN,kBAAkB,EAClB,iBAAiB,EACjB,yBAAyB,GACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACN,cAAc,EACd,mBAAmB,EACnB,mBAAmB,GACnB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,cAAc,EACd,mBAAmB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAC9D,YAAY,EACX,wBAAwB,EACxB,eAAe,GACf,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAClE,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AACpE,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EACN,kBAAkB,EAClB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,eAAe,GACf,MAAM,WAAW,CAAC;AACnB,YAAY,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,mBAAmB,yBAAyB,CAAC;AAC7C,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,IAAI,EACJ,KAAK,UAAU,EACf,EAAE,EACF,KAAK,MAAM,GACX,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACN,YAAY,EACZ,KAAK,GAAG,EACR,KAAK,WAAW,EAChB,KAAK,SAAS,EACd,UAAU,GACV,MAAM,YAAY,CAAC;AACpB,OAAO,EACN,eAAe,EACf,cAAc,EACd,KAAK,eAAe,EACpB,KAAK,OAAO,GACZ,MAAM,eAAe,CAAC;AACvB,OAAO,EACN,cAAc,EACd,cAAc,EACd,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,gBAAgB,GACrB,MAAM,eAAe,CAAC;AACvB,mBAAmB,gCAAgC,CAAC;AACpD,OAAO,EACN,gBAAgB,EAChB,eAAe,EACf,KAAK,oBAAoB,GACzB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACN,kBAAkB,EAClB,iBAAiB,EACjB,yBAAyB,GACzB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACN,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,GACnB,MAAM,2BAA2B,CAAC;AACnC,YAAY,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAClE,OAAO,EACN,KAAK,WAAW,EAChB,KAAK,cAAc,EACnB,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,GACrB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACN,cAAc,EACd,mBAAmB,EACnB,kBAAkB,GAClB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACN,iBAAiB,EACjB,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,GACrB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EACX,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,gBAAgB,GAChB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,11 @@ import {
|
|
|
5
5
|
ELEVENLABS_CATALOG,
|
|
6
6
|
ELEVENLABS_MODELS,
|
|
7
7
|
ELEVENLABS_OUTPUT_FORMATS,
|
|
8
|
+
GOOGLE_AUDIO_MODELS,
|
|
8
9
|
GOOGLE_CATALOG,
|
|
9
10
|
GOOGLE_IMAGE_MODELS,
|
|
11
|
+
GOOGLE_TEXT_MODELS,
|
|
12
|
+
GOOGLE_TTS_VOICES,
|
|
10
13
|
GOOGLE_VIDEO_MODELS,
|
|
11
14
|
KLING_AUDIO_MODEL,
|
|
12
15
|
KLING_CAPABILITIES,
|
|
@@ -15,6 +18,7 @@ import {
|
|
|
15
18
|
KLING_VOICES,
|
|
16
19
|
OPENAI_CATALOG,
|
|
17
20
|
OPENAI_IMAGE_MODELS,
|
|
21
|
+
OPENAI_TEXT_MODELS,
|
|
18
22
|
QWEN_AUDIO_MODELS,
|
|
19
23
|
QWEN_CATALOG,
|
|
20
24
|
QWEN_IMAGE_MODELS,
|
|
@@ -31,7 +35,7 @@ import {
|
|
|
31
35
|
resetCatalog,
|
|
32
36
|
resolveProvider,
|
|
33
37
|
videoPathFor
|
|
34
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-PL534BFN.js";
|
|
35
39
|
|
|
36
40
|
// src/lib/jobs.ts
|
|
37
41
|
import { AsyncLocalStorage } from "async_hooks";
|
|
@@ -582,7 +586,12 @@ var POLL_INTERVAL_MS2 = 1e4;
|
|
|
582
586
|
var DEFAULT_MAX_POLL_ATTEMPTS2 = 120;
|
|
583
587
|
var GoogleAdapter = class {
|
|
584
588
|
providerName = "google";
|
|
585
|
-
supportedTypes = [
|
|
589
|
+
supportedTypes = [
|
|
590
|
+
"image",
|
|
591
|
+
"video",
|
|
592
|
+
"text",
|
|
593
|
+
"audio"
|
|
594
|
+
];
|
|
586
595
|
opts;
|
|
587
596
|
constructor(opts) {
|
|
588
597
|
this.opts = opts;
|
|
@@ -904,17 +913,143 @@ var GoogleAdapter = class {
|
|
|
904
913
|
generateVideo(params) {
|
|
905
914
|
return this.runVideo(params);
|
|
906
915
|
}
|
|
907
|
-
|
|
908
|
-
|
|
916
|
+
textBody(modelId, params) {
|
|
917
|
+
const input = [
|
|
918
|
+
{ type: "text", text: params.prompt }
|
|
919
|
+
];
|
|
920
|
+
for (const image of params.referenceImages ?? []) {
|
|
921
|
+
const [meta, data] = image.split(",");
|
|
922
|
+
input.push({
|
|
923
|
+
type: "image",
|
|
924
|
+
mime_type: meta?.match(/data:([^;]+)/)?.[1] ?? "image/png",
|
|
925
|
+
data: data ?? image
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
return {
|
|
929
|
+
model: modelId,
|
|
930
|
+
input,
|
|
931
|
+
...params.systemPrompt ? { instructions: params.systemPrompt } : {},
|
|
932
|
+
generation_config: {
|
|
933
|
+
...params.maxTokens ? { max_output_tokens: params.maxTokens } : {},
|
|
934
|
+
...params.temperature !== void 0 ? { temperature: params.temperature } : {},
|
|
935
|
+
...params.topP !== void 0 ? { top_p: params.topP } : {}
|
|
936
|
+
},
|
|
937
|
+
...params.providerOptions?.google ?? {}
|
|
938
|
+
};
|
|
909
939
|
}
|
|
910
|
-
|
|
911
|
-
|
|
940
|
+
speechBody(modelId, params, voice) {
|
|
941
|
+
return {
|
|
942
|
+
model: modelId,
|
|
943
|
+
input: params.prompt,
|
|
944
|
+
response_format: { type: "audio" },
|
|
945
|
+
generation_config: {
|
|
946
|
+
speech_config: [{ voice }]
|
|
947
|
+
},
|
|
948
|
+
...params.providerOptions?.google ?? {}
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
async generateText(params) {
|
|
952
|
+
const startedAt = Date.now();
|
|
953
|
+
const modelId = params.model ?? "";
|
|
954
|
+
const failure = (error) => ({
|
|
955
|
+
success: false,
|
|
956
|
+
outputs: [],
|
|
957
|
+
creditsUsed: 0,
|
|
958
|
+
provider: this.providerName,
|
|
959
|
+
model: modelId,
|
|
960
|
+
processingTimeMs: Date.now() - startedAt,
|
|
961
|
+
error
|
|
962
|
+
});
|
|
963
|
+
if (!(modelId in GOOGLE_TEXT_MODELS)) {
|
|
964
|
+
return failure(
|
|
965
|
+
`"${modelId}" is not a Gemini text model. Known: ${Object.keys(GOOGLE_TEXT_MODELS).join(", ")}.`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const payload = await this.request(
|
|
970
|
+
INTERACTIONS_PATH,
|
|
971
|
+
{
|
|
972
|
+
method: "POST",
|
|
973
|
+
body: this.textBody(modelId, params)
|
|
974
|
+
}
|
|
975
|
+
);
|
|
976
|
+
const text = payload.output_text ?? payload.steps?.flatMap((step) => step.content ?? []).find((part) => part.type === "text" || part.text)?.text;
|
|
977
|
+
if (!text) return failure("Google returned no text.");
|
|
978
|
+
return {
|
|
979
|
+
success: true,
|
|
980
|
+
outputs: [
|
|
981
|
+
{
|
|
982
|
+
url: `data:text/plain;base64,${Buffer.from(text).toString("base64")}`,
|
|
983
|
+
mimeType: "text/plain",
|
|
984
|
+
raw: { text }
|
|
985
|
+
}
|
|
986
|
+
],
|
|
987
|
+
creditsUsed: 0,
|
|
988
|
+
provider: this.providerName,
|
|
989
|
+
model: modelId,
|
|
990
|
+
processingTimeMs: Date.now() - startedAt
|
|
991
|
+
};
|
|
992
|
+
} catch (error) {
|
|
993
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
async generateAudio(params) {
|
|
997
|
+
const startedAt = Date.now();
|
|
998
|
+
const modelId = params.model ?? "";
|
|
999
|
+
const failure = (error) => ({
|
|
1000
|
+
success: false,
|
|
1001
|
+
outputs: [],
|
|
1002
|
+
creditsUsed: 0,
|
|
1003
|
+
provider: this.providerName,
|
|
1004
|
+
model: modelId,
|
|
1005
|
+
processingTimeMs: Date.now() - startedAt,
|
|
1006
|
+
error
|
|
1007
|
+
});
|
|
1008
|
+
const binding = GOOGLE_AUDIO_MODELS[modelId];
|
|
1009
|
+
if (!binding) {
|
|
1010
|
+
return failure(
|
|
1011
|
+
`"${modelId}" is not a Gemini speech model. Known: ${Object.keys(GOOGLE_AUDIO_MODELS).join(", ")}.`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
const voice = params.voice ?? "Kore";
|
|
1015
|
+
if (!binding.voices.includes(voice)) {
|
|
1016
|
+
return failure(
|
|
1017
|
+
`Gemini TTS offers ${GOOGLE_TTS_VOICES.join(", ")}, not "${voice}".`
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
try {
|
|
1021
|
+
const payload = await this.request(
|
|
1022
|
+
INTERACTIONS_PATH,
|
|
1023
|
+
{
|
|
1024
|
+
method: "POST",
|
|
1025
|
+
body: this.speechBody(modelId, params, voice)
|
|
1026
|
+
}
|
|
1027
|
+
);
|
|
1028
|
+
const data = payload.output_audio?.data;
|
|
1029
|
+
if (!data) return failure("Google returned no audio.");
|
|
1030
|
+
return {
|
|
1031
|
+
success: true,
|
|
1032
|
+
outputs: [
|
|
1033
|
+
{
|
|
1034
|
+
url: `data:audio/wav;base64,${data}`,
|
|
1035
|
+
mimeType: "audio/wav",
|
|
1036
|
+
raw: { inline: true, voice }
|
|
1037
|
+
}
|
|
1038
|
+
],
|
|
1039
|
+
creditsUsed: 0,
|
|
1040
|
+
provider: this.providerName,
|
|
1041
|
+
model: modelId,
|
|
1042
|
+
processingTimeMs: Date.now() - startedAt
|
|
1043
|
+
};
|
|
1044
|
+
} catch (error) {
|
|
1045
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1046
|
+
}
|
|
912
1047
|
}
|
|
913
1048
|
async estimateCost(type, params) {
|
|
914
1049
|
return estimateFor(this.providerName, type, params);
|
|
915
1050
|
}
|
|
916
1051
|
supportsModel(model) {
|
|
917
|
-
return model in GOOGLE_IMAGE_MODELS || model in GOOGLE_VIDEO_MODELS;
|
|
1052
|
+
return model in GOOGLE_IMAGE_MODELS || model in GOOGLE_VIDEO_MODELS || model in GOOGLE_TEXT_MODELS || model in GOOGLE_AUDIO_MODELS;
|
|
918
1053
|
}
|
|
919
1054
|
getAvailableModels() {
|
|
920
1055
|
return [
|
|
@@ -927,6 +1062,16 @@ var GoogleAdapter = class {
|
|
|
927
1062
|
id,
|
|
928
1063
|
name: id,
|
|
929
1064
|
type: "video"
|
|
1065
|
+
})),
|
|
1066
|
+
...Object.keys(GOOGLE_TEXT_MODELS).map((id) => ({
|
|
1067
|
+
id,
|
|
1068
|
+
name: id,
|
|
1069
|
+
type: "text"
|
|
1070
|
+
})),
|
|
1071
|
+
...Object.keys(GOOGLE_AUDIO_MODELS).map((id) => ({
|
|
1072
|
+
id,
|
|
1073
|
+
name: id,
|
|
1074
|
+
type: "audio"
|
|
930
1075
|
}))
|
|
931
1076
|
];
|
|
932
1077
|
}
|
|
@@ -1283,7 +1428,7 @@ var IMAGES_PATH2 = "/v1/images/generations";
|
|
|
1283
1428
|
var OpenAIAdapter = class {
|
|
1284
1429
|
providerName = "openai";
|
|
1285
1430
|
// No video: the Videos API shuts down on 24 September 2026.
|
|
1286
|
-
supportedTypes = ["image"];
|
|
1431
|
+
supportedTypes = ["image", "text"];
|
|
1287
1432
|
opts;
|
|
1288
1433
|
constructor(opts) {
|
|
1289
1434
|
this.opts = opts;
|
|
@@ -1398,8 +1543,79 @@ var OpenAIAdapter = class {
|
|
|
1398
1543
|
generateAudio(_params) {
|
|
1399
1544
|
throw new Error("OpenAI has no speech synthesis wired up here.");
|
|
1400
1545
|
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1546
|
+
textBody(params) {
|
|
1547
|
+
const content = params.referenceImages?.length ? [
|
|
1548
|
+
{ type: "input_text", text: params.prompt },
|
|
1549
|
+
...params.referenceImages.map((url) => ({
|
|
1550
|
+
type: "input_image",
|
|
1551
|
+
image_url: url
|
|
1552
|
+
}))
|
|
1553
|
+
] : params.prompt;
|
|
1554
|
+
const input = [];
|
|
1555
|
+
if (params.systemPrompt) {
|
|
1556
|
+
input.push({ role: "system", content: params.systemPrompt });
|
|
1557
|
+
}
|
|
1558
|
+
input.push({ role: "user", content });
|
|
1559
|
+
return {
|
|
1560
|
+
model: params.model,
|
|
1561
|
+
input,
|
|
1562
|
+
...params.maxTokens ? { max_output_tokens: params.maxTokens } : {},
|
|
1563
|
+
...params.temperature !== void 0 ? { temperature: params.temperature } : {},
|
|
1564
|
+
...params.topP !== void 0 ? { top_p: params.topP } : {},
|
|
1565
|
+
...params.providerOptions?.openai ?? {}
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
async generateText(params) {
|
|
1569
|
+
const startedAt = Date.now();
|
|
1570
|
+
const modelId = params.model ?? "";
|
|
1571
|
+
const failure = (error) => ({
|
|
1572
|
+
success: false,
|
|
1573
|
+
outputs: [],
|
|
1574
|
+
creditsUsed: 0,
|
|
1575
|
+
provider: this.providerName,
|
|
1576
|
+
model: modelId,
|
|
1577
|
+
processingTimeMs: Date.now() - startedAt,
|
|
1578
|
+
error
|
|
1579
|
+
});
|
|
1580
|
+
if (!(modelId in OPENAI_TEXT_MODELS)) {
|
|
1581
|
+
return failure(
|
|
1582
|
+
`"${modelId}" is not an OpenAI text model. Known: ${Object.keys(OPENAI_TEXT_MODELS).join(", ")}.`
|
|
1583
|
+
);
|
|
1584
|
+
}
|
|
1585
|
+
try {
|
|
1586
|
+
const response = await fetch(`${this.baseUrl}/v1/responses`, {
|
|
1587
|
+
method: "POST",
|
|
1588
|
+
headers: {
|
|
1589
|
+
Authorization: `Bearer ${this.opts.apiKey}`,
|
|
1590
|
+
"Content-Type": "application/json",
|
|
1591
|
+
...this.opts.organization ? { "OpenAI-Organization": this.opts.organization } : {}
|
|
1592
|
+
},
|
|
1593
|
+
body: JSON.stringify(this.textBody(params))
|
|
1594
|
+
});
|
|
1595
|
+
const payload = await response.json();
|
|
1596
|
+
if (payload.error) throw new Error(payload.error.message);
|
|
1597
|
+
if (!response.ok) {
|
|
1598
|
+
throw new Error(`OpenAI returned ${response.status}.`);
|
|
1599
|
+
}
|
|
1600
|
+
const text = payload.output_text ?? payload.output?.flatMap((item) => item.content ?? []).find((part) => part.type === "output_text" || part.text)?.text;
|
|
1601
|
+
if (!text) return failure("OpenAI returned no text.");
|
|
1602
|
+
return {
|
|
1603
|
+
success: true,
|
|
1604
|
+
outputs: [
|
|
1605
|
+
{
|
|
1606
|
+
url: `data:text/plain;base64,${Buffer.from(text).toString("base64")}`,
|
|
1607
|
+
mimeType: "text/plain",
|
|
1608
|
+
raw: { text, tokens: payload.usage?.output_tokens }
|
|
1609
|
+
}
|
|
1610
|
+
],
|
|
1611
|
+
creditsUsed: 0,
|
|
1612
|
+
provider: this.providerName,
|
|
1613
|
+
model: modelId,
|
|
1614
|
+
processingTimeMs: Date.now() - startedAt
|
|
1615
|
+
};
|
|
1616
|
+
} catch (error) {
|
|
1617
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1618
|
+
}
|
|
1403
1619
|
}
|
|
1404
1620
|
/** Synchronous provider: there is never a queued job to come back to. */
|
|
1405
1621
|
async completeJob(job) {
|
|
@@ -1420,14 +1636,21 @@ var OpenAIAdapter = class {
|
|
|
1420
1636
|
};
|
|
1421
1637
|
}
|
|
1422
1638
|
supportsModel(model) {
|
|
1423
|
-
return model in OPENAI_IMAGE_MODELS;
|
|
1639
|
+
return model in OPENAI_IMAGE_MODELS || model in OPENAI_TEXT_MODELS;
|
|
1424
1640
|
}
|
|
1425
1641
|
getAvailableModels() {
|
|
1426
|
-
return
|
|
1427
|
-
id
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1642
|
+
return [
|
|
1643
|
+
...Object.keys(OPENAI_IMAGE_MODELS).map((id) => ({
|
|
1644
|
+
id,
|
|
1645
|
+
name: id,
|
|
1646
|
+
type: "image"
|
|
1647
|
+
})),
|
|
1648
|
+
...Object.keys(OPENAI_TEXT_MODELS).map((id) => ({
|
|
1649
|
+
id,
|
|
1650
|
+
name: id,
|
|
1651
|
+
type: "text"
|
|
1652
|
+
}))
|
|
1653
|
+
];
|
|
1431
1654
|
}
|
|
1432
1655
|
};
|
|
1433
1656
|
|
|
@@ -2033,6 +2256,45 @@ async function persistOutputs(storage, outputs) {
|
|
|
2033
2256
|
);
|
|
2034
2257
|
}
|
|
2035
2258
|
|
|
2259
|
+
// src/lib/webhook.ts
|
|
2260
|
+
function resolveWebhook(value) {
|
|
2261
|
+
if (!value) return void 0;
|
|
2262
|
+
if (typeof value === "string") {
|
|
2263
|
+
return isHttpUrl(value) ? { url: value } : void 0;
|
|
2264
|
+
}
|
|
2265
|
+
if (!isHttpUrl(value.url)) return void 0;
|
|
2266
|
+
return {
|
|
2267
|
+
url: value.url,
|
|
2268
|
+
secret: value.secret,
|
|
2269
|
+
headers: value.headers
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
function isHttpUrl(value) {
|
|
2273
|
+
try {
|
|
2274
|
+
const parsed = new URL(value);
|
|
2275
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
2276
|
+
} catch {
|
|
2277
|
+
return false;
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
async function deliverWebhook(config, payload) {
|
|
2281
|
+
try {
|
|
2282
|
+
await fetch(config.url, {
|
|
2283
|
+
method: "POST",
|
|
2284
|
+
headers: {
|
|
2285
|
+
"content-type": "application/json",
|
|
2286
|
+
"user-agent": "@brotu/ai",
|
|
2287
|
+
"x-brotu-event": payload.event,
|
|
2288
|
+
...config.secret ? { "x-brotu-webhook-secret": config.secret } : {},
|
|
2289
|
+
...config.headers
|
|
2290
|
+
},
|
|
2291
|
+
body: JSON.stringify(payload),
|
|
2292
|
+
signal: AbortSignal.timeout(1e4)
|
|
2293
|
+
});
|
|
2294
|
+
} catch {
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2036
2298
|
// src/client.ts
|
|
2037
2299
|
var NATIVE_PROVIDERS = [
|
|
2038
2300
|
"byteplus",
|
|
@@ -2047,6 +2309,8 @@ function brotuClient(options) {
|
|
|
2047
2309
|
throw new Error("brotuClient requires at least one provider API key.");
|
|
2048
2310
|
}
|
|
2049
2311
|
registerModels(options.models);
|
|
2312
|
+
let registeredWebhook = resolveWebhook(options.webhook);
|
|
2313
|
+
const notifiedJobIds = /* @__PURE__ */ new Set();
|
|
2050
2314
|
const storage = options.storage ? createS3Storage(options.storage) : void 0;
|
|
2051
2315
|
const shouldPersist = storage && options.storage?.persistOutputs !== false;
|
|
2052
2316
|
const adapters = /* @__PURE__ */ new Map();
|
|
@@ -2136,6 +2400,31 @@ function brotuClient(options) {
|
|
|
2136
2400
|
return adapter.generateAudio(params);
|
|
2137
2401
|
return adapter.generateText(params);
|
|
2138
2402
|
}
|
|
2403
|
+
function webhookFor(params) {
|
|
2404
|
+
return resolveWebhook(params?.webhook) ?? registeredWebhook;
|
|
2405
|
+
}
|
|
2406
|
+
async function notifySettled(input) {
|
|
2407
|
+
const config = webhookFor(input.params);
|
|
2408
|
+
if (!config) return;
|
|
2409
|
+
const jobId = input.job?.id;
|
|
2410
|
+
if (jobId) {
|
|
2411
|
+
if (notifiedJobIds.has(jobId)) return;
|
|
2412
|
+
notifiedJobIds.add(jobId);
|
|
2413
|
+
}
|
|
2414
|
+
const payload = {
|
|
2415
|
+
event: input.event,
|
|
2416
|
+
jobId,
|
|
2417
|
+
provider: input.provider ?? input.job?.provider,
|
|
2418
|
+
model: input.model ?? input.job?.model,
|
|
2419
|
+
kind: input.kind ?? input.job?.kind,
|
|
2420
|
+
outputs: input.outputs,
|
|
2421
|
+
error: input.error ? { code: input.error.code, message: input.error.message } : void 0,
|
|
2422
|
+
metadata: input.metadata ?? input.job?.metadata,
|
|
2423
|
+
processingTimeMs: input.processingTimeMs,
|
|
2424
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2425
|
+
};
|
|
2426
|
+
await deliverWebhook(config, payload);
|
|
2427
|
+
}
|
|
2139
2428
|
async function toGeneration(raw, metadata) {
|
|
2140
2429
|
if (!raw.success) {
|
|
2141
2430
|
return fail({
|
|
@@ -2158,15 +2447,50 @@ function brotuClient(options) {
|
|
|
2158
2447
|
const routed = route(params.model);
|
|
2159
2448
|
if (routed.error) return fail(routed.error);
|
|
2160
2449
|
try {
|
|
2161
|
-
|
|
2450
|
+
const result = await toGeneration(
|
|
2162
2451
|
await generateWith(routed.data.adapter, kind, params),
|
|
2163
2452
|
params.metadata
|
|
2164
2453
|
);
|
|
2454
|
+
if (result.error) {
|
|
2455
|
+
await notifySettled({
|
|
2456
|
+
event: "generation.failed",
|
|
2457
|
+
params,
|
|
2458
|
+
kind,
|
|
2459
|
+
provider: routed.data.provider,
|
|
2460
|
+
model: routed.data.model,
|
|
2461
|
+
error: result.error,
|
|
2462
|
+
metadata: params.metadata
|
|
2463
|
+
});
|
|
2464
|
+
return result;
|
|
2465
|
+
}
|
|
2466
|
+
await notifySettled({
|
|
2467
|
+
event: "generation.succeeded",
|
|
2468
|
+
params,
|
|
2469
|
+
kind,
|
|
2470
|
+
provider: result.data.provider,
|
|
2471
|
+
model: result.data.model,
|
|
2472
|
+
outputs: result.data.outputs,
|
|
2473
|
+
metadata: result.data.metadata,
|
|
2474
|
+
processingTimeMs: result.data.processingTimeMs
|
|
2475
|
+
});
|
|
2476
|
+
return result;
|
|
2165
2477
|
} catch (error) {
|
|
2166
|
-
|
|
2478
|
+
const failed = failFrom("provider_error", error, {
|
|
2167
2479
|
provider: routed.data.provider,
|
|
2168
2480
|
model: routed.data.model
|
|
2169
2481
|
});
|
|
2482
|
+
if (failed.error) {
|
|
2483
|
+
await notifySettled({
|
|
2484
|
+
event: "generation.failed",
|
|
2485
|
+
params,
|
|
2486
|
+
kind,
|
|
2487
|
+
provider: routed.data.provider,
|
|
2488
|
+
model: routed.data.model,
|
|
2489
|
+
error: failed.error,
|
|
2490
|
+
metadata: params.metadata
|
|
2491
|
+
});
|
|
2492
|
+
}
|
|
2493
|
+
return failed;
|
|
2170
2494
|
}
|
|
2171
2495
|
}
|
|
2172
2496
|
async function submit(kind, params) {
|
|
@@ -2185,18 +2509,40 @@ function brotuClient(options) {
|
|
|
2185
2509
|
() => generateWith(routed.data.adapter, kind, params)
|
|
2186
2510
|
);
|
|
2187
2511
|
if (!raw.success) {
|
|
2188
|
-
|
|
2512
|
+
const error = {
|
|
2189
2513
|
code: "provider_error",
|
|
2190
2514
|
message: raw.error ?? "The provider rejected the request.",
|
|
2191
2515
|
provider: raw.provider,
|
|
2192
2516
|
model: raw.model
|
|
2517
|
+
};
|
|
2518
|
+
await notifySettled({
|
|
2519
|
+
event: "generation.failed",
|
|
2520
|
+
params,
|
|
2521
|
+
kind,
|
|
2522
|
+
provider: raw.provider,
|
|
2523
|
+
model: raw.model,
|
|
2524
|
+
error,
|
|
2525
|
+
metadata: params.metadata
|
|
2193
2526
|
});
|
|
2527
|
+
return fail(error);
|
|
2194
2528
|
}
|
|
2195
|
-
|
|
2529
|
+
const job = {
|
|
2196
2530
|
...base,
|
|
2197
2531
|
id: `inline-${base.model}-${base.submittedAt}`,
|
|
2198
2532
|
result: raw
|
|
2533
|
+
};
|
|
2534
|
+
await notifySettled({
|
|
2535
|
+
event: "generation.succeeded",
|
|
2536
|
+
params,
|
|
2537
|
+
job,
|
|
2538
|
+
kind,
|
|
2539
|
+
provider: raw.provider,
|
|
2540
|
+
model: raw.model,
|
|
2541
|
+
outputs: raw.outputs,
|
|
2542
|
+
metadata: params.metadata,
|
|
2543
|
+
processingTimeMs: raw.processingTimeMs
|
|
2199
2544
|
});
|
|
2545
|
+
return ok(job);
|
|
2200
2546
|
} catch (error) {
|
|
2201
2547
|
if (!isPendingJob(error)) {
|
|
2202
2548
|
return failFrom("provider_error", error, {
|
|
@@ -2211,11 +2557,54 @@ function brotuClient(options) {
|
|
|
2211
2557
|
});
|
|
2212
2558
|
}
|
|
2213
2559
|
}
|
|
2560
|
+
async function notifyFromSnapshot(job, snapshot) {
|
|
2561
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2562
|
+
await notifySettled({
|
|
2563
|
+
event: "generation.succeeded",
|
|
2564
|
+
params: job.params,
|
|
2565
|
+
job,
|
|
2566
|
+
outputs: snapshot.result.outputs,
|
|
2567
|
+
provider: snapshot.result.provider,
|
|
2568
|
+
model: snapshot.result.model,
|
|
2569
|
+
processingTimeMs: snapshot.result.processingTimeMs,
|
|
2570
|
+
metadata: job.metadata
|
|
2571
|
+
});
|
|
2572
|
+
return;
|
|
2573
|
+
}
|
|
2574
|
+
if (snapshot.status === "failed") {
|
|
2575
|
+
await notifySettled({
|
|
2576
|
+
event: "generation.failed",
|
|
2577
|
+
params: job.params,
|
|
2578
|
+
job,
|
|
2579
|
+
error: {
|
|
2580
|
+
code: "provider_error",
|
|
2581
|
+
message: snapshot.error ?? "The job failed.",
|
|
2582
|
+
provider: job.provider,
|
|
2583
|
+
model: job.model
|
|
2584
|
+
},
|
|
2585
|
+
metadata: job.metadata
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
async function finalizeSnapshot(job, snapshot) {
|
|
2590
|
+
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2591
|
+
const persisted = await toGeneration(snapshot.result, job.metadata);
|
|
2592
|
+
if (!persisted.error) {
|
|
2593
|
+
snapshot = {
|
|
2594
|
+
status: "succeeded",
|
|
2595
|
+
result: {
|
|
2596
|
+
...snapshot.result,
|
|
2597
|
+
outputs: persisted.data.outputs
|
|
2598
|
+
}
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
await notifyFromSnapshot(job, snapshot);
|
|
2603
|
+
return snapshot;
|
|
2604
|
+
}
|
|
2214
2605
|
async function pollJob(job) {
|
|
2215
2606
|
if (job.result) {
|
|
2216
|
-
return ok(
|
|
2217
|
-
job.result.success ? { status: "succeeded", result: job.result } : { status: "failed", error: job.result.error }
|
|
2218
|
-
);
|
|
2607
|
+
return ok(await finalizeSnapshot(job, job.result.success ? { status: "succeeded", result: job.result } : { status: "failed", error: job.result.error }));
|
|
2219
2608
|
}
|
|
2220
2609
|
const routed = route(job.model);
|
|
2221
2610
|
if (routed.error) return fail(routed.error);
|
|
@@ -2228,7 +2617,7 @@ function brotuClient(options) {
|
|
|
2228
2617
|
});
|
|
2229
2618
|
}
|
|
2230
2619
|
try {
|
|
2231
|
-
return ok(await adapter.completeJob(job));
|
|
2620
|
+
return ok(await finalizeSnapshot(job, await adapter.completeJob(job)));
|
|
2232
2621
|
} catch (error) {
|
|
2233
2622
|
return failFrom("provider_error", error, {
|
|
2234
2623
|
provider: job.provider,
|
|
@@ -2297,6 +2686,17 @@ function brotuClient(options) {
|
|
|
2297
2686
|
submit: (params) => submit("audio", params),
|
|
2298
2687
|
generate: (params) => generate("audio", params)
|
|
2299
2688
|
},
|
|
2689
|
+
webhook: {
|
|
2690
|
+
set(value) {
|
|
2691
|
+
registeredWebhook = resolveWebhook(value);
|
|
2692
|
+
},
|
|
2693
|
+
clear() {
|
|
2694
|
+
registeredWebhook = void 0;
|
|
2695
|
+
},
|
|
2696
|
+
get() {
|
|
2697
|
+
return registeredWebhook;
|
|
2698
|
+
}
|
|
2699
|
+
},
|
|
2300
2700
|
jobs: {
|
|
2301
2701
|
poll: pollJob,
|
|
2302
2702
|
wait: async (job, waitOptions) => {
|
|
@@ -2306,7 +2706,13 @@ function brotuClient(options) {
|
|
|
2306
2706
|
if (polled.error) return fail(polled.error);
|
|
2307
2707
|
const snapshot = polled.data;
|
|
2308
2708
|
if (snapshot.status === "succeeded" && snapshot.result) {
|
|
2309
|
-
return
|
|
2709
|
+
return ok({
|
|
2710
|
+
outputs: snapshot.result.outputs,
|
|
2711
|
+
provider: snapshot.result.provider,
|
|
2712
|
+
model: snapshot.result.model,
|
|
2713
|
+
processingTimeMs: snapshot.result.processingTimeMs,
|
|
2714
|
+
metadata: job.metadata
|
|
2715
|
+
});
|
|
2310
2716
|
}
|
|
2311
2717
|
if (snapshot.status === "failed") {
|
|
2312
2718
|
return fail({
|
|
@@ -2351,8 +2757,11 @@ export {
|
|
|
2351
2757
|
ELEVENLABS_MODELS,
|
|
2352
2758
|
ELEVENLABS_OUTPUT_FORMATS,
|
|
2353
2759
|
ElevenLabsAdapter,
|
|
2760
|
+
GOOGLE_AUDIO_MODELS,
|
|
2354
2761
|
GOOGLE_CATALOG,
|
|
2355
2762
|
GOOGLE_IMAGE_MODELS,
|
|
2763
|
+
GOOGLE_TEXT_MODELS,
|
|
2764
|
+
GOOGLE_TTS_VOICES,
|
|
2356
2765
|
GOOGLE_VIDEO_MODELS,
|
|
2357
2766
|
GoogleAdapter,
|
|
2358
2767
|
KLING_CAPABILITIES,
|
|
@@ -2361,6 +2770,7 @@ export {
|
|
|
2361
2770
|
KlingAdapter,
|
|
2362
2771
|
OPENAI_CATALOG,
|
|
2363
2772
|
OPENAI_IMAGE_MODELS,
|
|
2773
|
+
OPENAI_TEXT_MODELS,
|
|
2364
2774
|
OpenAIAdapter,
|
|
2365
2775
|
PendingJob,
|
|
2366
2776
|
QWEN_AUDIO_MODELS,
|
|
@@ -2371,6 +2781,7 @@ export {
|
|
|
2371
2781
|
QwenAdapter,
|
|
2372
2782
|
brotuClient,
|
|
2373
2783
|
createS3Storage,
|
|
2784
|
+
deliverWebhook,
|
|
2374
2785
|
fail,
|
|
2375
2786
|
getAvailableModels,
|
|
2376
2787
|
getModel,
|
|
@@ -2382,5 +2793,6 @@ export {
|
|
|
2382
2793
|
persistOutputs,
|
|
2383
2794
|
registerModels,
|
|
2384
2795
|
resetCatalog,
|
|
2385
|
-
resolveProvider
|
|
2796
|
+
resolveProvider,
|
|
2797
|
+
resolveWebhook
|
|
2386
2798
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AIError } from "../helpers/result";
|
|
2
|
+
import type { GenerationOutput, GenerationType } from "../ports/content-generator.port";
|
|
3
|
+
export type WebhookEventName = "generation.succeeded" | "generation.failed";
|
|
4
|
+
export interface WebhookConfig {
|
|
5
|
+
url: string;
|
|
6
|
+
/** Sent as `x-brotu-webhook-secret` so your endpoint can reject strangers. */
|
|
7
|
+
secret?: string;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
export interface WebhookEvent {
|
|
11
|
+
event: WebhookEventName;
|
|
12
|
+
jobId?: string;
|
|
13
|
+
provider?: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
kind?: GenerationType;
|
|
16
|
+
outputs?: GenerationOutput[];
|
|
17
|
+
error?: Pick<AIError, "code" | "message">;
|
|
18
|
+
metadata?: Record<string, string>;
|
|
19
|
+
processingTimeMs?: number;
|
|
20
|
+
completedAt: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function resolveWebhook(value: string | WebhookConfig | undefined): WebhookConfig | undefined;
|
|
23
|
+
/**
|
|
24
|
+
* POST the settled generation at the registered URL. Failures are swallowed:
|
|
25
|
+
* a down hook must not fail the call the user is waiting on.
|
|
26
|
+
*/
|
|
27
|
+
export declare function deliverWebhook(config: WebhookConfig, payload: WebhookEvent): Promise<void>;
|
|
28
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/lib/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,KAAK,EACX,gBAAgB,EAChB,cAAc,EACd,MAAM,iCAAiC,CAAC;AAEzC,MAAM,MAAM,gBAAgB,GAAG,sBAAsB,GAAG,mBAAmB,CAAC;AAE5E,MAAM,WAAW,aAAa;IAC7B,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,YAAY;IAC5B,KAAK,EAAE,gBAAgB,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC1C,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,cAAc,CAC7B,KAAK,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,GACvC,aAAa,GAAG,SAAS,CAW3B;AAWD;;;GAGG;AACH,wBAAsB,cAAc,CACnC,MAAM,EAAE,aAAa,EACrB,OAAO,EAAE,YAAY,GACnB,OAAO,CAAC,IAAI,CAAC,CAmBf"}
|