2026-01-20 09:02:26 -06:00

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;
}