Skip to content

Commit

Permalink
Merge pull request #39 from kevinreber/add-hugging-face
Browse files Browse the repository at this point in the history
Added support for Hugging Faces API and Black Forest Labs(flux) API
  • Loading branch information
kevinreber authored Dec 1, 2024
2 parents 7e35c66 + 1de4b49 commit cdbe30e
Show file tree
Hide file tree
Showing 13 changed files with 501 additions and 57 deletions.
24 changes: 11 additions & 13 deletions app/components/CreatePageForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,20 @@ import {
DialogTitle,
} from "@/components/ui/dialog";
import { ChevronDown, Check, Loader2 } from "lucide-react";
import { CreatePageLoader } from "~/routes/create";
import {
CreatePageLoader,
STYLE_OPTIONS,
MODEL_OPTIONS,
} from "~/routes/create";

const MOBILE_WIDTH = 768;
const MAX_TEXT_AREA_CHAR_COUNT = 500;
const DEFAULT_SELECTED_MODEL = {
name: "Stable Diffusion 1.6",
value: "stable-diffusion-v1-6",
image: "/assets/model-thumbs/sd-1-5.jpg",
description: "The most popular first-generation stable diffusion model.",
};

const DEFAULT_SELECTED_STYLE = {
name: "Anime",
value: "anime",
image: "/assets/preset-text-styles/anime-v2.jpg",
};
const DEFAULT_SELECTED_MODEL = MODEL_OPTIONS.find(
(model) => model.value === "stable-diffusion-v1-6"
);
const DEFAULT_SELECTED_STYLE = STYLE_OPTIONS.find(
(style) => style.value === "none"
);

const Image = ({
src,
Expand Down
76 changes: 59 additions & 17 deletions app/routes/create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const meta: MetaFunction = () => {
return [{ title: "Create AI Generated Images" }];
};

const MODEL_OPTIONS = [
export const MODEL_OPTIONS = [
{
name: "Stable Diffusion 1.6",
value: "stable-diffusion-v1-6",
Expand All @@ -28,6 +28,30 @@ const MODEL_OPTIONS = [
image: "/assets/model-thumbs/sdxlv1.jpg",
description: "The state-of-the-art in open-source image generation.",
},
{
name: "Flux Schnell",
value: "flux-pro",
// value: "black-forest-labs/FLUX.1-schnell",
image: "/assets/model-thumbs/flux-schnell.jpg",
description:
"Fastest open-source text-to-image model to date, by Black Forest Labs.",
},
{
name: "Flux Pro 1.1",
value: "flux-pro-1.1",
// value: "black-forest-labs/FLUX.1-schnell",
image: "/assets/model-thumbs/flux-pro-1-1.jpg",
description:
"Professional grade image generation with excellent prompt following and visual quality, by Black Forest Labs.",
},
{
name: "Flux Dev",
value: "flux-dev",
// value: "black-forest-labs/FLUX.1-dev",
image: "/assets/model-thumbs/flux-dev-thumb-2.jpg",
description:
"Development version offering cost-effective image generation while maintaining good quality, by Black Forest Labs.",
},
{
name: "DALL-E 3",
value: "dall-e-3",
Expand All @@ -40,18 +64,26 @@ const MODEL_OPTIONS = [
image: "/assets/model-thumbs/dalle2.jpg",
description: "State-of-the-art image generator from OpenAI's DALL-E 2.",
},
// ! TODO: RunDiffusion/Juggernaut-XL-v9 is not accessible off Hugging Face for some reason
// {
// name: "Juggernaut XL v9",
// value: "RunDiffusion/Juggernaut-XL-v9",
// image: "/assets/model-thumbs/juggernaut-v9-rundiffusion-lightning.jpg",
// description:
// "A model by RunDiffusion that is great at creating endless images.",
// },
// {
// name: "NeverEnding Dream",
// value: "Lykon/NeverEnding-Dream",
// image: "/assets/model-thumbs/neverending-dream-1-2-2.jpg",
// description: "A model by Lykon that is great at creating endless images.",
// },
// {
// name: "Dreamshaper XL Lightning",
// image: "/assets/model-thumbs/ds-xl-lightning.jpg",
// description: "Dreamshaper XL, accelerated. High quality, fast and cheap.",
// },
// {
// name: "Flux",
// image: "/assets/model-thumbs/flux-dev-thumb-2.jpg",
// description:
// "The largest open-source text-to-image model to date, by Black Forest Labs.",
// },
// {
// name: "Ideogram 2.0",
// image: "/assets/model-thumbs/ideogram-v1.jpg",
// description: "A model by Ideogram that is amazing at Typography.",
Expand All @@ -62,9 +94,9 @@ const MODEL_OPTIONS = [
// description:
// "A model by Google DeepMind that is great at typography & prompt adherence.",
// },
];
] as const;

const STYLE_OPTIONS = [
export const STYLE_OPTIONS = [
{
name: "3d Model",
value: "3d-model",
Expand Down Expand Up @@ -128,7 +160,7 @@ const STYLE_OPTIONS = [
// name: "Tile Texture",
// image: "/assets/preset-text-styles/.jpg",
// },
];
] as const;

const MAX_PROMPT_CHARACTERS = 3500;
const MIN_NUMBER_OF_IMAGES = 1;
Expand Down Expand Up @@ -186,6 +218,13 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
};

export type CreatePageLoader = typeof loader;
export type CreateImagesFormData = {
prompt: string;
numberOfImages: number;
model: string;
stylePreset?: string;
private?: boolean;
};

export const action = async ({ request }: ActionFunctionArgs) => {
const user = await requireUserLogin(request);
Expand Down Expand Up @@ -234,15 +273,18 @@ export const action = async ({ request }: ActionFunctionArgs) => {
);
}

const response = await createNewImages(validateFormData.data, user.id);
try {
const response = await createNewImages(validateFormData.data, user.id);

if (response.setId) {
// delay to allow time for all images to be created
await new Promise((resolve) => setTimeout(resolve, 300));
return redirect(`/sets/${response.setId}`);
if (response.setId) {
// delay to allow time for all images to be created
await new Promise((resolve) => setTimeout(resolve, 300));
return redirect(`/sets/${response.setId}`);
}
} catch (error) {
console.error(`Error creating new images: ${error}`);
}

return json({ error: "Failed to create set" }, { status: 500 });
return json({ error: "Failed to create images" }, { status: 500 });
};

export default function Index() {
Expand Down
230 changes: 230 additions & 0 deletions app/server/createBlackForestImages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { invariantResponse } from "~/utils";
import {
createNewImage,
FormattedImageData,
getFormattedImageData,
} from "./createNewImage";
import { addBase64EncodedImageToAWS } from "./addBase64EncodedImageToAWS";
import { createNewSet } from "./createNewSet";
import { deleteSet } from "./deleteSet";
import { delay } from "~/utils/delay";
import { convertImageUrlToBase64 } from "~/utils/convertImageUrlToBase64";
import { Logger } from "~/utils/logger.server";
import { CreateImagesFormData } from "~/routes/create";

type BlackForestResponse = {
id: string;
status: string;
result?: {
sample: string;
};
};

const DEFAULT_WIDTH = 1024;
const DEFAULT_HEIGHT = 1024;
const MAX_POLLING_ATTEMPTS = 60;
const POLLING_INTERVAL = 500; // ms

/**
* Create an image using Black Forest Labs API
* @param formData - The form data payload
* @returns The ID of the request
*
* @docs https://docs.bfl.ml/
*
* @example
* "1c8a8479-0bf8-47b7-a9e6-7a7de7c55e16"
*/
const createBlackForestImage = async (
formData: CreateImagesFormData
): Promise<string> => {
Logger.info({
message: `[createBlackForestImages.ts]: Creating image using Black Forest Labs model: ${formData.model}`,
metadata: { formData },
});
const response = await fetch(
`${process.env.BLACK_FOREST_LABS_API_URL}/v1/${formData.model}`,
{
method: "POST",
headers: {
"x-key": process.env.BLACK_FOREST_LABS_API_KEY!,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: formData.prompt,
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
}),
}
);

invariantResponse(
response.ok,
`Failed to generate image from Black Forest Labs: ${response.statusText}`
);

const responseData = await response.json();
return responseData.id;
};

/**
* Get the status and result of an image generation request from Black Forest Labs
* @param requestId - The ID of the request to get the status and result for
* @returns The status and result of the request
*
* @example
* {
* "id": "1c8a8479-0bf8-47b7-a9e6-7a7de7c55e16",
* "status": "Ready",
* "result": {
* "sample": "https://bfldeliverysc.blob.core.windows.net/results/9bdd22c375ee4de2a4a526781485e1a3/sample.jpeg?se=2024-12-01T02%3A02%3A04Z&sp=r&sv=2024-11-04&sr=b&rsct=image/jpeg&sig=QlsyWuVVkbD7Uo6FYD1Am0TEI2zb4j4Lt7B2XFInkbs%3D",
* "prompt": "a pirate ship floating through the galaxy among the stars",
* "seed": 566575719,
* "start_time": 1733017914.4215941,
* "end_time": 1733017924.3717163,
* "duration": 9.950122117996216
* }
* }
*/
const getBlackForestImageStatus = async (
requestId: string
): Promise<BlackForestResponse> => {
Logger.info({
message: `[createBlackForestImages.ts]: Getting result from Black Forest Labs: ${requestId}`,
metadata: { requestId },
});
const resultResponse = await fetch(
`${process.env.BLACK_FOREST_LABS_API_URL}/v1/get_result?id=${requestId}`,
{
headers: {
"x-key": process.env.BLACK_FOREST_LABS_API_KEY!,
accept: "application/json",
},
}
);

invariantResponse(
resultResponse.ok,
`Failed to get result from Black Forest Labs: ${resultResponse.statusText}`
);

return resultResponse.json();
};

// Update the pollForBlackForestImageResult function
const pollForBlackForestImageResult = async (
requestId: string
): Promise<string> => {
Logger.info({
message: `[createBlackForestImages.ts]: Polling for result from Black Forest Labs: ${requestId}`,
metadata: { requestId },
});
let attempts = 0;

while (attempts < MAX_POLLING_ATTEMPTS) {
Logger.info({
message: `[createBlackForestImages.ts]: Polling attempt #${
attempts + 1
} for result from Black Forest Labs: ${requestId}`,
metadata: { requestId },
});
const resultData = await getBlackForestImageStatus(requestId);
Logger.info({
message: `[createBlackForestImages.ts]: Get Black Forest Image result for requestId: ${requestId}`,
metadata: { requestId, resultData },
});

if (resultData.status === "Ready" && resultData.result) {
Logger.info({
message: `[createBlackForestImages.ts]: Converting image URL to base64 for requestId: ${requestId}`,
metadata: { requestId },
});
// Convert the image URL to base64
const base64Image = await convertImageUrlToBase64(
resultData.result.sample
);
return base64Image;
} else if (resultData.status === "Failed") {
Logger.error({
message: `[createBlackForestImages.ts]: Image generation failed for requestId: ${requestId}`,
metadata: { requestId, resultData },
});
throw new Error("Image generation failed");
}

await delay(POLLING_INTERVAL);
attempts++;
}

throw new Error("Timeout waiting for image generation");
};

export const createBlackForestImages = async (
formData: CreateImagesFormData,
userId: string
) => {
Logger.info({
message: `[createBlackForestImages.ts]: Creating images using Black Forest Labs model: ${formData.model}`,
metadata: { formData },
});
let setId = "";
const formattedImages: FormattedImageData[] = [];
try {
// Step 1: Create a new set
const set = await createNewSet({
prompt: formData.prompt,
userId,
});

setId = set.id;

for (let i = 0; i < formData.numberOfImages; i++) {
// Step 2: Submit the generation request
const requestId = await createBlackForestImage(formData);
Logger.info({
message: `[createBlackForestImages.ts]: Successfully stored Black Forest Image data #${
i + 1
} for requestId: ${requestId}`,
});

// Step 3: Poll for results and get base64 image
const base64Image = await pollForBlackForestImageResult(requestId);

// Step 4: Create a new image in DB
const imageData = await createNewImage({
prompt: formData.prompt,
userId,
model: formData.model,
preset: undefined,
isImagePrivate: false,
setId,
});
Logger.info({
message: `[createBlackForestImages.ts]: Successfully stored Black Forest Image Data in DB: ${imageData.id}`,
});

// Step 5: Upload to S3
await addBase64EncodedImageToAWS(base64Image, imageData.id);
Logger.info({
message: `[createBlackForestImages.ts]: Successfully stored Black Forest Image data #${
i + 1
} in S3: ${imageData.id}`,
});

// Step 6: Format image data
const formattedImageData = getFormattedImageData(imageData);
formattedImages.push(formattedImageData);
}

return { images: formattedImages, setId };
} catch (error) {
Logger.error({
message: `[createBlackForestImages.ts]: Error generating image from Black Forest Labs`,
metadata: { error },
});
if (setId) {
await deleteSet({ setId });
}
return { images: [], setId: "" };
}
};
Loading

0 comments on commit cdbe30e

Please sign in to comment.