211 lines
6.0 KiB
TypeScript
211 lines
6.0 KiB
TypeScript
// routes/api/ai-images/[id]
|
|
import { define } from "../../../utils.ts";
|
|
import { ImageFile } from "../../../utils/types.ts";
|
|
import { OpenAI } from "@openai";
|
|
import {
|
|
getLocalBlobUrl,
|
|
uploadToLocalBlobStorage,
|
|
} from "../../../utils/blobStorage.ts";
|
|
import {
|
|
consumeCreateToken,
|
|
isCreateTokenValid,
|
|
} from "../../../utils/tokenEvaluator.ts";
|
|
import {
|
|
createCorsPreflightResponse,
|
|
getIslandCorsHeaders,
|
|
isIslandFetchRequestAllowed,
|
|
withCors,
|
|
} from "../../../utils/cors.ts";
|
|
|
|
// Initialize OpenAI client
|
|
const openai = new OpenAI({
|
|
apiKey: "sk-proj-i17SN2KSk10UAORJz3zfT3BlbkFJVxk8D561NlXMN4SZ4IUj",
|
|
});
|
|
|
|
export const handler = define.handlers({
|
|
async OPTIONS(ctx) {
|
|
const url = new URL(ctx.req.url);
|
|
if (!isIslandFetchRequestAllowed(url)) {
|
|
return new Response("Not Found", { status: 404 });
|
|
}
|
|
|
|
const corsHeaders = getIslandCorsHeaders(
|
|
ctx.req.headers.get("Origin"),
|
|
ctx.req.headers.get("Access-Control-Request-Headers"),
|
|
);
|
|
return createCorsPreflightResponse(ctx.req, corsHeaders);
|
|
},
|
|
async GET(ctx) {
|
|
const url = new URL(ctx.req.url);
|
|
if (!isIslandFetchRequestAllowed(url)) {
|
|
return new Response("Not Found", { status: 404 });
|
|
}
|
|
|
|
const corsHeaders = getIslandCorsHeaders(
|
|
ctx.req.headers.get("Origin"),
|
|
ctx.req.headers.get("Access-Control-Request-Headers"),
|
|
);
|
|
const respond = (body: BodyInit | null, init: ResponseInit) =>
|
|
withCors(new Response(body, init), corsHeaders);
|
|
|
|
const tokenIdParam = url.searchParams.get("create_token_id");
|
|
const stateIdParam = url.searchParams.get("state_id");
|
|
|
|
if (!tokenIdParam || !stateIdParam) {
|
|
return respond(
|
|
JSON.stringify({ error: "Authentication or create token required" }),
|
|
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
const token = await isCreateTokenValid(
|
|
tokenIdParam,
|
|
stateIdParam,
|
|
);
|
|
|
|
if (!token) {
|
|
return respond(
|
|
JSON.stringify({ error: "Invalid or expired create token" }),
|
|
{ status: 401, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
const id = decodeURIComponent(ctx.params.id);
|
|
|
|
if (!id || id.length === 0) {
|
|
return respond("ID is required", { status: 400 });
|
|
}
|
|
|
|
try {
|
|
const response = await openai.images.generate({
|
|
model: "gpt-image-1",
|
|
prompt:
|
|
'Not in writing. Generate the photo that is a person or thing with action of: "' +
|
|
id + '"',
|
|
n: 1,
|
|
quality: "low",
|
|
size: "1024x1024",
|
|
} as any);
|
|
//console.log("response",response.data);
|
|
const b64 = response?.data?.[0]?.b64_json ?? "";
|
|
if (b64 && typeof b64 === "string" && b64.length > 0) {
|
|
//const imageResponse = await fetch(imageUrl);
|
|
//const blob = await imageResponse.blob();
|
|
|
|
const baseName = sanitizeImageFileName(id);
|
|
const imageBlob = base64ToBlob(b64, "image/png");
|
|
const uploadResult = await uploadToLocalBlobStorage(
|
|
baseName,
|
|
imageBlob,
|
|
"image",
|
|
{
|
|
useTimestamp: true,
|
|
defaultExtension: ".png",
|
|
},
|
|
);
|
|
|
|
if (!uploadResult.success) {
|
|
throw new Error(`Failed to save AI image: ${uploadResult.error}`);
|
|
}
|
|
|
|
const finalFileName = uploadResult.fileName || baseName;
|
|
const signedUrl = await getLocalBlobUrl(
|
|
finalFileName,
|
|
"image",
|
|
token.user_id,
|
|
token.state_id,
|
|
);
|
|
|
|
const imageData: ImageFile = {
|
|
image_text: id,
|
|
image_file: id,
|
|
image_data: signedUrl,
|
|
};
|
|
|
|
if (tokenIdParam) {
|
|
await consumeCreateToken(tokenIdParam);
|
|
}
|
|
|
|
return respond(JSON.stringify(imageData), {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Cache-Control": "no-cache, no-store, must-revalidate",
|
|
"Pragma": "no-cache",
|
|
"Expires": "0",
|
|
},
|
|
});
|
|
} else {
|
|
return respond(
|
|
JSON.stringify({
|
|
error: "No image data returned from image generation",
|
|
}),
|
|
{
|
|
status: 502,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
},
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error generating image:", error);
|
|
}
|
|
|
|
//console.error("Error fetching quiz:", error);
|
|
return respond(
|
|
JSON.stringify({ error: "Failed to fetch image data" }),
|
|
{
|
|
status: 500,
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
},
|
|
);
|
|
},
|
|
});
|
|
|
|
function base64ToBlob(base64: string, mimeType: string): Blob {
|
|
const binaryString = atob(base64);
|
|
const bytes = new Uint8Array(binaryString.length);
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
}
|
|
return new Blob([bytes], { type: mimeType });
|
|
}
|
|
|
|
function sanitizeImageFileName(fileName: string) {
|
|
if (!fileName) {
|
|
return "image";
|
|
}
|
|
|
|
// // Step 1: Ensure the file name ends with .png
|
|
// if (!fileName.toLowerCase().endsWith('.png')) {
|
|
// fileName += '.png';
|
|
// }
|
|
|
|
// // Step 2: Remove the path if present (keep only the file name)
|
|
// fileName = fileName.split('/').pop().split('\\').pop();
|
|
|
|
// Step 3: Replace spaces and other problematic characters
|
|
fileName = fileName
|
|
.replace(/[^a-zA-Z0-9_.-]/g, "_") // Replace invalid chars with underscore
|
|
.replace(/_{2,}/g, "_") // Replace multiple underscores with single
|
|
.replace(/^[._-]+|[._-]+$/g, "") // Remove leading/trailing ._-
|
|
.toLowerCase(); // Convert to lowercase
|
|
|
|
// Step 4: Ensure the file name is not empty
|
|
// if (fileName === '.png') {
|
|
// fileName = 'image.png';
|
|
// }
|
|
|
|
// Step 5: Truncate if the file name is too long (optional)
|
|
const maxLength = 100; // Maximum file name length
|
|
if (fileName.length > maxLength) {
|
|
const extension = ".png";
|
|
fileName = fileName.slice(0, maxLength - extension.length) + extension;
|
|
}
|
|
|
|
return fileName;
|
|
}
|