mirror of
https://github.com/Stability-AI/StableStudio.git
synced 2026-07-14 03:34:33 +08:00
635 lines
18 KiB
TypeScript
635 lines
18 KiB
TypeScript
import { GrpcWebFetchTransport } from "@protobuf-ts/grpcweb-transport";
|
|
import { RpcError } from "@protobuf-ts/runtime-rpc";
|
|
import * as StableStudio from "@stability/stablestudio-plugin";
|
|
|
|
import {
|
|
EnginesServiceClient,
|
|
Generation,
|
|
GenerationServiceClient,
|
|
Project,
|
|
ProjectServiceClient,
|
|
Struct,
|
|
} from "./Proto";
|
|
|
|
const getStableDiffusionDefaultCount = () => 4;
|
|
const getStableDiffusionDefaultInputFromPrompt = (prompt: string) => ({
|
|
prompts: [
|
|
{
|
|
text: prompt,
|
|
weight: 1,
|
|
},
|
|
|
|
{
|
|
text: "",
|
|
weight: -0.75,
|
|
},
|
|
],
|
|
|
|
model: "stable-diffusion-xl-beta-v2-2-2",
|
|
sampler: { id: "0", name: "DDIM" },
|
|
style: "enhance",
|
|
|
|
width: 512,
|
|
height: 512,
|
|
|
|
cfgScale: 7,
|
|
steps: 50,
|
|
});
|
|
|
|
export const createPlugin = StableStudio.createPlugin<{
|
|
settings: {
|
|
apiKey: StableStudio.PluginSettingString;
|
|
};
|
|
}>(({ context, set }) => {
|
|
const functionsWhichNeedAPIKey = (
|
|
apiKey?: string
|
|
): Pick<
|
|
StableStudio.Plugin,
|
|
| "createStableDiffusionImages"
|
|
| "getStableDiffusionExistingImages"
|
|
| "getStableDiffusionModels"
|
|
| "deleteStableDiffusionImages"
|
|
| "getStatus"
|
|
| "getStableDiffusionSamplers"
|
|
> => {
|
|
if (!apiKey)
|
|
return {
|
|
createStableDiffusionImages: undefined,
|
|
getStableDiffusionExistingImages: undefined,
|
|
getStableDiffusionModels: undefined,
|
|
deleteStableDiffusionImages: undefined,
|
|
|
|
getStatus: () => ({
|
|
indicator: "error",
|
|
text: "Missing API key",
|
|
}),
|
|
};
|
|
|
|
const transport = new GrpcWebFetchTransport({
|
|
baseUrl: "https://grpc.stability.ai",
|
|
meta: {
|
|
Authorization: `Bearer ${apiKey}`,
|
|
|
|
"stability-client-id": "StableStudio",
|
|
"stability-client-version": context.getGitHash(),
|
|
},
|
|
});
|
|
|
|
const generation = new GenerationServiceClient(transport);
|
|
const engines = new EnginesServiceClient(transport);
|
|
const project = new ProjectServiceClient(transport);
|
|
|
|
return {
|
|
createStableDiffusionImages: async (options) => {
|
|
const count = options?.count ?? getStableDiffusionDefaultCount();
|
|
const defaultStableDiffusionInput =
|
|
getStableDiffusionDefaultInputFromPrompt(
|
|
context.getStableDiffusionRandomPrompt()
|
|
);
|
|
|
|
const input = {
|
|
...defaultStableDiffusionInput,
|
|
...options?.input,
|
|
};
|
|
|
|
const width = input.width ?? defaultStableDiffusionInput.width;
|
|
const height = input.height ?? defaultStableDiffusionInput.height;
|
|
|
|
const prompt =
|
|
input.prompts?.map(
|
|
({ text = context.getStableDiffusionRandomPrompt(), weight }) =>
|
|
Generation.Prompt.create({
|
|
prompt: { oneofKind: "text", text },
|
|
parameters: { weight },
|
|
})
|
|
) ?? [];
|
|
|
|
// add init and mask
|
|
if (input.maskImage?.blob) {
|
|
prompt.push(
|
|
Generation.Prompt.create({
|
|
parameters: { init: false, weight: 1 },
|
|
prompt: {
|
|
oneofKind: "artifact",
|
|
artifact: Generation.Artifact.create({
|
|
type: Generation.ArtifactType.ARTIFACT_MASK,
|
|
mime: "image/png",
|
|
data: {
|
|
oneofKind: "binary",
|
|
binary: new Uint8Array(
|
|
await input.maskImage.blob.arrayBuffer()
|
|
),
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
if (input.initialImage?.blob) {
|
|
prompt.push(
|
|
Generation.Prompt.create({
|
|
parameters: { init: true, weight: input.initialImage.weight },
|
|
prompt: {
|
|
oneofKind: "artifact",
|
|
artifact: Generation.Artifact.create({
|
|
type: Generation.ArtifactType.ARTIFACT_IMAGE,
|
|
mime: "image/png",
|
|
data: {
|
|
oneofKind: "binary",
|
|
binary: new Uint8Array(
|
|
await input.initialImage.blob.arrayBuffer()
|
|
),
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
);
|
|
}
|
|
|
|
console.log(input.sampler.id ? parseInt(input.sampler.id) : 0);
|
|
|
|
const imageParams = Generation.ImageParameters.create({
|
|
width: BigInt(width),
|
|
height: BigInt(height),
|
|
|
|
steps: BigInt(input.steps),
|
|
samples: BigInt(count),
|
|
seed: Array.from({ length: count }, () => input.seed ?? 0),
|
|
|
|
transform: Generation.TransformType.create({
|
|
type: {
|
|
oneofKind: "diffusion",
|
|
diffusion: input.sampler.id ? parseInt(input.sampler.id) : 0,
|
|
},
|
|
}),
|
|
|
|
parameters: [
|
|
Generation.StepParameter.create({
|
|
sampler: Generation.SamplerParameters.create({
|
|
cfgScale: input.cfgScale,
|
|
}),
|
|
|
|
scaledStep: 0,
|
|
schedule: Generation.ScheduleParameters.create({
|
|
start: input.initialImage
|
|
? 1 - (input.initialImage.weight ?? 0)
|
|
: 1,
|
|
}),
|
|
}),
|
|
],
|
|
});
|
|
|
|
const extras = (input.style || input.width !== input.height) && {
|
|
extras: Struct.fromJson({
|
|
$IPC: {
|
|
preset: input.style,
|
|
...(input.width !== input.height && { mode: "multistage" }),
|
|
},
|
|
}),
|
|
};
|
|
|
|
const request = generation.chainGenerate({
|
|
requestId: "",
|
|
stage: [
|
|
{
|
|
id: "Main",
|
|
|
|
request: Generation.Request.create({
|
|
prompt,
|
|
|
|
engineId: input.model,
|
|
requestedType: Generation.ArtifactType.ARTIFACT_IMAGE,
|
|
params: { oneofKind: "image", image: imageParams },
|
|
|
|
...extras,
|
|
}),
|
|
|
|
onStatus: [
|
|
{
|
|
target: "Asset",
|
|
reason: [],
|
|
action: [
|
|
Generation.StageAction.PASS,
|
|
Generation.StageAction.RETURN,
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
{
|
|
id: "Asset",
|
|
|
|
request: Generation.Request.create({
|
|
engineId: "asset-service",
|
|
params: {
|
|
oneofKind: "asset",
|
|
asset: {
|
|
projectId: "",
|
|
action: Generation.AssetAction.ASSET_PUT,
|
|
use: Generation.AssetUse.OUTPUT,
|
|
},
|
|
},
|
|
}),
|
|
|
|
onStatus: [
|
|
{ action: [Generation.StageAction.RETURN], reason: [] },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
let id: string | undefined;
|
|
const images: StableStudio.StableDiffusionImage[] = [];
|
|
|
|
for await (const response of request.responses) {
|
|
for (const artifact of response.artifacts) {
|
|
if (
|
|
artifact.type === Generation.ArtifactType.ARTIFACT_TEXT &&
|
|
artifact.finishReason === Generation.FinishReason.FILTER
|
|
)
|
|
throw new RpcError("Banned word detected!", "BANNED_TERM");
|
|
|
|
if (
|
|
artifact.type === Generation.ArtifactType.ARTIFACT_IMAGE &&
|
|
artifact.data.oneofKind === "binary"
|
|
) {
|
|
id = response.requestId;
|
|
images.push({
|
|
input: {
|
|
...input,
|
|
seed: artifact.seed,
|
|
},
|
|
id: artifact.uuid,
|
|
blob: new Blob([artifact.data.binary], { type: "image/png" }),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
return id ? { id, images } : undefined;
|
|
},
|
|
|
|
getStableDiffusionExistingImages: async (options) => {
|
|
const { limit, exclusiveStartImageID } = { limit: 25, ...options };
|
|
if (limit <= 0) return [];
|
|
|
|
const { response } = await project.queryAssets({
|
|
id: "",
|
|
limit: BigInt(limit),
|
|
startKey: exclusiveStartImageID,
|
|
use: [Project.ProjectAssetUse.OUTPUT],
|
|
sortDir: Project.ProjectSortDir.DESC,
|
|
tags: {},
|
|
});
|
|
|
|
type ImageWithBlobPromise = Omit<
|
|
StableStudio.StableDiffusionImage,
|
|
"blob"
|
|
> & { blob: Promise<Blob> };
|
|
|
|
type ImagesWithBlobPromises = Record<
|
|
StableStudio.ID,
|
|
Omit<StableStudio.StableDiffusionImages, "images"> & {
|
|
images: ImageWithBlobPromise[];
|
|
}
|
|
>;
|
|
|
|
const images = response.assets.reduce((previous, response) => {
|
|
if (
|
|
!response.request ||
|
|
response.request.params.oneofKind !== "image"
|
|
)
|
|
return previous;
|
|
|
|
const prompts = response.request.prompt.reduce((prompts, prompt) => {
|
|
if (
|
|
prompt.prompt.oneofKind !== "artifact" ||
|
|
prompt.prompt.artifact.data.oneofKind !== "text"
|
|
)
|
|
return prompts;
|
|
|
|
return [
|
|
...prompts,
|
|
{
|
|
text: prompt.prompt.artifact.data.text,
|
|
weight: prompt.parameters?.weight ?? 1,
|
|
},
|
|
];
|
|
}, [] as StableStudio.StableDiffusionPrompt[]);
|
|
|
|
if (prompts.length === 0) return previous;
|
|
|
|
const id = response.request.requestId.split(":")[0];
|
|
if (!id) return previous;
|
|
|
|
const imageID = response.id;
|
|
const createdAt = new Date(Number(response.createdAt) * 1000);
|
|
|
|
const model = response.request.engineId;
|
|
const style = parseExtras(response.request.extras?.fields).$IPC
|
|
?.preset;
|
|
|
|
const width = response.request.params.image.width
|
|
? Number(response.request.params.image.width)
|
|
: undefined;
|
|
|
|
const height = response.request.params.image.height
|
|
? Number(response.request.params.image.height)
|
|
: undefined;
|
|
|
|
const cfgScale = response.request.params.image.parameters.reduce(
|
|
(_, parameters) =>
|
|
parameters.sampler?.cfgScale
|
|
? Number(parameters.sampler.cfgScale)
|
|
: undefined,
|
|
undefined as number | undefined
|
|
);
|
|
|
|
const steps = response.request.params.image.steps
|
|
? Number(response.request.params.image.steps)
|
|
: undefined;
|
|
|
|
const seed = response.request.params.image.seed[0]
|
|
? Number(response.request.params.image.seed[0])
|
|
: undefined;
|
|
|
|
const input: StableStudio.StableDiffusionInput = {
|
|
prompts,
|
|
|
|
model,
|
|
style,
|
|
|
|
width,
|
|
height,
|
|
|
|
cfgScale,
|
|
steps,
|
|
seed,
|
|
};
|
|
|
|
const src = response.uri
|
|
?.replace(
|
|
"https://object.lga1.coreweave.com/stability-staging-assets",
|
|
"https://staging-cdn.stability.ai/assets"
|
|
)
|
|
?.replace(
|
|
"https://object.lga1.coreweave.com/stability-assets",
|
|
"https://cdn.stability.ai/assets"
|
|
);
|
|
|
|
const image = {
|
|
id: imageID,
|
|
createdAt,
|
|
input,
|
|
blob: fetch(src).then((response) => response.blob()),
|
|
};
|
|
|
|
return {
|
|
...previous,
|
|
[id]: {
|
|
...previous[id],
|
|
...(exclusiveStartImageID === imageID && {
|
|
exclusiveStartImageID: imageID,
|
|
}),
|
|
|
|
images: [...(previous[id]?.images ?? []), image],
|
|
id: id,
|
|
},
|
|
};
|
|
}, {} as ImagesWithBlobPromises);
|
|
|
|
return Promise.all(
|
|
Object.values(images).map(async ({ id, images }) => ({
|
|
id,
|
|
images: await Promise.all(
|
|
images?.map(async ({ blob, ...image }) => ({
|
|
...image,
|
|
blob: await blob,
|
|
}))
|
|
),
|
|
exclusiveStartImageID: images?.[images.length - 1]?.id,
|
|
}))
|
|
);
|
|
},
|
|
|
|
getStableDiffusionModels: async () => {
|
|
const request = await engines.listEngines({});
|
|
const allEngines = await request.response.engine;
|
|
return allEngines.filter((engine) => engine.type === 1 && engine.ready);
|
|
},
|
|
|
|
deleteStableDiffusionImages: async (options) => {
|
|
const imageIDs = options?.imageIDs;
|
|
imageIDs &&
|
|
(await project.deleteAssets({ id: "", assetIds: imageIDs }));
|
|
},
|
|
|
|
getStatus: () => ({
|
|
indicator: "success",
|
|
text: "Ready",
|
|
}),
|
|
};
|
|
};
|
|
|
|
return {
|
|
...functionsWhichNeedAPIKey(
|
|
localStorage.getItem("stability-apiKey") ?? undefined
|
|
),
|
|
|
|
getStableDiffusionSamplers: () => [
|
|
{ id: "0", name: "DDIM" },
|
|
{ id: "1", name: "DDPM" },
|
|
{ id: "2", name: "K Euler" },
|
|
{ id: "3", name: "K Euler Ancestral" },
|
|
{ id: "4", name: "K Heun" },
|
|
{ id: "5", name: "K DPM 2" },
|
|
{ id: "6", name: "K DPM 2 Ancestral" },
|
|
{ id: "7", name: "K LMS" },
|
|
{ id: "8", name: "K DPM++ 2S Ancestral" },
|
|
{ id: "9", name: "K DPM++ 2M" },
|
|
{ id: "10", name: "K DPM++ SDE" },
|
|
],
|
|
|
|
getStableDiffusionStyles: () => [
|
|
{
|
|
id: "enhance",
|
|
name: "Enhance",
|
|
image: "https://dreamstudio.ai/presets/enhance.png",
|
|
},
|
|
{
|
|
id: "anime",
|
|
name: "Anime",
|
|
image: "https://dreamstudio.ai/presets/anime.png",
|
|
},
|
|
{
|
|
id: "photographic",
|
|
name: "Photographic",
|
|
image: "https://dreamstudio.ai/presets/photographic.png",
|
|
},
|
|
{
|
|
id: "digital-art",
|
|
name: "Digital art",
|
|
image: "https://dreamstudio.ai/presets/digital-art.png",
|
|
},
|
|
{
|
|
id: "comic-book",
|
|
name: "Comic book",
|
|
image: "https://dreamstudio.ai/presets/comic-book.png",
|
|
},
|
|
{
|
|
id: "fantasy-art",
|
|
name: "Fantasy art",
|
|
image: "https://dreamstudio.ai/presets/fantasy-art.png",
|
|
},
|
|
{
|
|
id: "analog-film",
|
|
name: "Analog film",
|
|
image: "https://dreamstudio.ai/presets/analog-film.png",
|
|
},
|
|
{
|
|
id: "neon-punk",
|
|
name: "Neon punk",
|
|
image: "https://dreamstudio.ai/presets/neon-punk.png",
|
|
},
|
|
{
|
|
id: "isometric",
|
|
name: "Isometric",
|
|
image: "https://dreamstudio.ai/presets/isometric.png",
|
|
},
|
|
{
|
|
id: "low-poly",
|
|
name: "Low poly",
|
|
image: "https://dreamstudio.ai/presets/low-poly.png",
|
|
},
|
|
{
|
|
id: "origami",
|
|
name: "Origami",
|
|
image: "https://dreamstudio.ai/presets/origami.png",
|
|
},
|
|
{
|
|
id: "line-art",
|
|
name: "Line art",
|
|
image: "https://dreamstudio.ai/presets/line-art.png",
|
|
},
|
|
{
|
|
id: "modeling-compound",
|
|
name: "Craft clay",
|
|
image: "https://dreamstudio.ai/presets/modeling-compound.png",
|
|
},
|
|
{
|
|
id: "cinematic",
|
|
name: "Cinematic",
|
|
image: "https://dreamstudio.ai/presets/cinematic.png",
|
|
},
|
|
{
|
|
id: "3d-model",
|
|
name: "3D model",
|
|
image: "https://dreamstudio.ai/presets/3d-model.png",
|
|
},
|
|
{
|
|
id: "pixel-art",
|
|
name: "Pixel art",
|
|
image: "https://dreamstudio.ai/presets/pixel-art.png",
|
|
},
|
|
],
|
|
|
|
getStableDiffusionDefaultCount,
|
|
getStableDiffusionDefaultInput: () =>
|
|
getStableDiffusionDefaultInputFromPrompt(
|
|
context.getStableDiffusionRandomPrompt()
|
|
),
|
|
|
|
settings: {
|
|
apiKey: {
|
|
type: "string",
|
|
|
|
title: "API key",
|
|
description:
|
|
"You can find your Stability API key at https://dreamstudio.ai/account",
|
|
|
|
placeholder: "sk-...",
|
|
required: true,
|
|
password: true,
|
|
|
|
value: localStorage.getItem("stability-apiKey") ?? "",
|
|
},
|
|
},
|
|
|
|
setSetting: (key, value) => {
|
|
set(({ settings }) => ({
|
|
settings: {
|
|
...settings,
|
|
[key]: { ...settings[key], value: value as string },
|
|
},
|
|
}));
|
|
|
|
if (key === "apiKey" && typeof value === "string") {
|
|
localStorage.setItem("stability-apiKey", value);
|
|
set((plugin) => ({ ...plugin, ...functionsWhichNeedAPIKey(value) }));
|
|
}
|
|
},
|
|
|
|
manifest: {
|
|
author: "Stability AI",
|
|
description: markdownDescription,
|
|
name: "Stability AI",
|
|
license: "MIT",
|
|
link: "https://stability.ai",
|
|
version: "0.0.0",
|
|
icon: "https://stability.ai/favicon.ico",
|
|
},
|
|
};
|
|
});
|
|
|
|
function parseExtras(extras: any): any | undefined {
|
|
if (!extras) return undefined;
|
|
|
|
if (extras.kind?.oneofKind === "structValue") {
|
|
return parseExtras(extras.kind.structValue.fields);
|
|
} else if (extras.kind?.oneofKind === "listValue") {
|
|
return extras.listValue.values.map(parseExtras);
|
|
} else if (extras.kind?.oneofKind === "numberValue") {
|
|
return Number(extras.kind.numberValue);
|
|
} else if (extras.kind?.oneofKind === "stringValue") {
|
|
return extras.kind.stringValue;
|
|
} else if (extras.kind?.oneofKind === "boolValue") {
|
|
return extras.kind.boolValue;
|
|
} else if (extras.kind?.oneofKind === undefined) {
|
|
return {
|
|
...Object.keys(extras).reduce((previous, key) => {
|
|
return { ...previous, [key]: parseExtras(extras[key]) };
|
|
}, {}),
|
|
};
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
const markdownDescription = `
|
|
# Welcome to StableStudio!
|
|
|
|
## [📖 README](https://github.com/Stability-AI/StableStudio) · [🎮 Discord](https://discord.com/channels/1002292111942635562/1108055793674227782) · [🌈 DreamStudio](https://dreamstudio.ai) · [💬 Discussion](https://github.com/Stability-AI/StableStudio/discussions)
|
|
|
|
# Setup
|
|
|
|
To get started, you'll need to sign up for a [DreamStudio](https://dreamstudio.ai) account.
|
|
|
|
Once you're logged in, head to the [account page](https://dreamstudio.ai/account).
|
|
|
|
You should see a section called \`API keys\`...
|
|

|
|
|
|
If you don't already have a key, you can create one via the plus button...
|
|

|
|
|
|
You can copy your API key by clicking the copy button...
|
|

|
|
|
|
You'll be asked to accept the terms of service.
|
|
|
|
Now, paste the key into the field below...
|
|
|
|
The plugin status should change to \`Ready\` once everything is working.
|
|
` as const;
|