mirror of
https://github.com/placeholder-soft/storytime.git
synced 2026-01-12 22:30:22 +08:00
feat: api for generating images
This commit is contained in:
8
functions/.envrc
Normal file
8
functions/.envrc
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PATH_add ./node_modules/.bin
|
||||
|
||||
if [ -f .envrc.override ]; then
|
||||
source_env .envrc.override
|
||||
fi
|
||||
1
functions/.gitignore
vendored
1
functions/.gitignore
vendored
@@ -7,3 +7,4 @@ typings/
|
||||
|
||||
# Node.js dependency directory
|
||||
node_modules/
|
||||
lib/
|
||||
|
||||
1
functions/.tool-versions
Normal file
1
functions/.tool-versions
Normal file
@@ -0,0 +1 @@
|
||||
nodejs 18.19.0
|
||||
@@ -14,13 +14,19 @@
|
||||
},
|
||||
"main": "lib/index.js",
|
||||
"dependencies": {
|
||||
"@google-cloud/functions-framework": "^3.3.0",
|
||||
"@types/express": "^4.17.21",
|
||||
"express": "^4.18.2",
|
||||
"firebase-admin": "^11.11.1",
|
||||
"firebase-functions": "^4.3.1",
|
||||
"got-cjs": "^12.5.4",
|
||||
"model": "../model",
|
||||
"firebase-admin": "^11.8.0",
|
||||
"firebase-functions": "^4.3.1"
|
||||
"nanoid": "3",
|
||||
"openai": "^4.20.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.3.3",
|
||||
"firebase-functions-test": "^3.1.0"
|
||||
"firebase-functions-test": "^3.1.0",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
563
functions/pnpm-lock.yaml
generated
563
functions/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
13
functions/src/credentials/admin-key.json
Normal file
13
functions/src/credentials/admin-key.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"type": "service_account",
|
||||
"project_id": "storytime-web3",
|
||||
"private_key_id": "c948532af7977cea6405e645825b2bdfd2c3e3d6",
|
||||
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDClnYzEADTVtoI\nKEt2gnpEpEKQLVOVWao7Z2QNalKtuEtZb3GqL5tFDWi5d0CzejoqADQTq30bIV/m\narYk5JnCDG8NlJ7prcJc2t5KfrKOX/ReVdy7Jn1pCy3wv7S5qChEYNOojzDufEmb\nBFclFUUGlFkZO5lgQHKQfXUr0j7khL0Z0d6QxhT8D4tEqLjCYz+ZRlPvHdEvP9kf\naBc3e/amQNXwwyhXidtOPwYsVkIBTmHBtUYXx4gcTJrOwumHwhJYQcRpgfjtt2yO\ncARYRlADtArJY+SXY6laSD9KNZfJ3CWKZtGcbtdMCOcRdMlD5hIOS7HH02Iy7wcv\nHuBckoW/AgMBAAECggEAGLTkUXZyNw3w2sFMV5dDMVej20zjWclhO8VUZv79fBuJ\ndv5snF21ZW6e7JGPha+la/N2U/BduaX4Aw3Ajm2iIjPUSTv6HhSBuFV9N6XfmQPm\nR1tijowEDLmD00IcDx337ZACaoVdLU1a8Sj7bEoc517K81EQbTAG+9aaG9QdnS5k\nD0HyHbcNneT6lQrjv+3iMh7zGwzmSWSxD81fKhrGalAB97OZwdiwZSQKp2icySRU\nsnhk0FFsggQ2KHaX26zrp0s9VSUGMvD82xDymSU6YWwrIlaC2P1lDy72bo2EaeRS\nh9qZKld7e1vFNUuKaZhiHPNF8CgEhq9ztVvOVgQW3QKBgQDmJqhDyoWSg7Qr1rba\nOs4UOigQnyRrd8vwarxyvm97tUTbzAeyaKvfxSNlxxRnGowh2Gm92sX7togJ+5w1\nnTBjxQjvvFLaTEsUdRQUPH7UcEbG71+YJpS82Izznc4UNYP3GLlsljQ20eQ+2PM/\nrHc4zm+qZb1MVoXiduCjKvU5SwKBgQDYcUh7J0TiKTDTWyyHagDVIImz0yfY3FXf\noqov8kJemApHgXSkH4NjSQV7bDpBWytt+kZEjIlLMXCx9/gUNRQIdAzX4n5qeOZr\nohRP6Fh7atycnlQQ+ifrjPcb+3OEuccxog+6nTqbTaJMig0Pa3yJqR1psIHlOxSF\nzeFzgSYw3QKBgQDRQmzT1pxEb0OuaHexTK57bbVoB4rRvAbN/f6CReyPim2le4m0\nCb/coh5hN/WxGU44p9DdMsG48GoYyZFqnhWVYV2SvKSIn73UR++NxsN63Os9jgBZ\nExtB6ZOfHeh2L4JhdTWDKb8n6QeirRfe2S09lVWqlP7dHf51vqjZMwHsqQKBgQC/\no9YETOm9wbDcgs6ze1UPYAbstJqEddqG73T2jO7c6Iu5clL/enOP9jinZlVSRtH2\nR3HuAe0SKc1ZlnAOHE0HixFQGNfLmA6U4GZRtiyZ4i1BcyKGAahU9HRbT1GiBAft\n10tL9SjOF3gLgvQ7YfVbweQsDz+D4sKyEm97IK/a9QKBgDNz3sj+qkbgMn0sW2Fw\n3o5h9oWvEk2Zvvpo8+g/w3xEgoS2RbboqsCAN+JNw/fKecFrlZA97xVgd6fQjZNb\nfnqPRx3uHFKQwaOGaEAc5yTA5JFDSBdeRO5qXrHnPTyqNbwk4hzIAz+LcSn0WJgW\neG0OLLjO1ZadmECODGEHlp8U\n-----END PRIVATE KEY-----\n",
|
||||
"client_email": "firebase-adminsdk-i9jvg@storytime-web3.iam.gserviceaccount.com",
|
||||
"client_id": "112791080690462614850",
|
||||
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
"token_uri": "https://oauth2.googleapis.com/token",
|
||||
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-i9jvg%40storytime-web3.iam.gserviceaccount.com",
|
||||
"universe_domain": "googleapis.com"
|
||||
}
|
||||
@@ -7,15 +7,115 @@
|
||||
* See a full list of supported triggers at https://firebase.google.com/docs/functions
|
||||
*/
|
||||
|
||||
import { onCall } from "firebase-functions/v2/https";
|
||||
import { onCall, onRequest, Request } from "firebase-functions/v2/https";
|
||||
import * as logger from "firebase-functions/logger";
|
||||
import { OpenAI } from "openai";
|
||||
import * as express from "express";
|
||||
|
||||
import admin from "firebase-admin";
|
||||
import { getStorage } from "firebase-admin/storage";
|
||||
import got from "got-cjs";
|
||||
import { nanoid } from "nanoid";
|
||||
|
||||
import adminKey from "./credentials/admin-key.json";
|
||||
admin.initializeApp({
|
||||
credential: admin.credential.cert(adminKey as any),
|
||||
});
|
||||
|
||||
// Start writing functions
|
||||
// https://firebase.google.com/docs/functions/typescript
|
||||
|
||||
export const execute = onCall((request) => {
|
||||
export const execute = onCall({ region: "asia-east1" }, (request) => {
|
||||
logger.info("Hello logs!", { structuredData: true });
|
||||
return {
|
||||
data: "Hello from Firebase!",
|
||||
};
|
||||
});
|
||||
const openai = new OpenAI({
|
||||
apiKey: "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",
|
||||
});
|
||||
const generateImage = async (req: Request, resp: express.Response) => {
|
||||
const prompt = req.body.prompt;
|
||||
if (prompt == null || prompt.length === 0) {
|
||||
throw new Error("prompt is required");
|
||||
}
|
||||
|
||||
const generateImageResponse = await openai.images.generate({
|
||||
model: "dall-e-3",
|
||||
prompt: prompt,
|
||||
n: 1,
|
||||
size: "1792x1024",
|
||||
});
|
||||
|
||||
const imageUrl = generateImageResponse.data[0].url;
|
||||
const revised_prompt = generateImageResponse.data[0].revised_prompt;
|
||||
|
||||
const bucket = getStorage().bucket("storytime-ai-images");
|
||||
// response.send(generateImageResponse);
|
||||
|
||||
// Validate imageUrl
|
||||
if (!imageUrl) {
|
||||
resp.status(400).send("No image URL provided.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(imageUrl);
|
||||
|
||||
// Check if image fetch was successful
|
||||
if (!response.ok)
|
||||
throw new Error(`Unable to fetch image: ${response.statusText}`);
|
||||
// Create a reference in Firebase Storage
|
||||
const fileName = `images/${Date.now()}-${nanoid()}.png`;
|
||||
const file = bucket.file(fileName);
|
||||
|
||||
const writeStream = file.createWriteStream();
|
||||
|
||||
got
|
||||
.stream(imageUrl)
|
||||
.on("error", (error) => {
|
||||
resp.status(500).send(`Error downloading image: ${error.message}`);
|
||||
})
|
||||
.pipe(writeStream)
|
||||
.on("finish", () => {
|
||||
file
|
||||
.getSignedUrl({
|
||||
action: "read",
|
||||
expires: "03-09-2491", // You can provide an expiry date for the URL
|
||||
})
|
||||
.then((signedUrls) => {
|
||||
// signedUrls[0] contains the file's public URL
|
||||
resp.status(200).send({ image_url: signedUrls[0], revised_prompt });
|
||||
})
|
||||
.catch((error) => {
|
||||
resp.status(500).send(`Error getting signed URL: ${error.message}`);
|
||||
});
|
||||
})
|
||||
.on("error", (error) => {
|
||||
resp
|
||||
.status(500)
|
||||
.send(`Error uploading image to Firebase Storage: ${error.message}`);
|
||||
});
|
||||
} catch (error) {
|
||||
resp.status(500).send(`Error downloading image: ${error}`);
|
||||
}
|
||||
};
|
||||
|
||||
export const request = onRequest(
|
||||
{
|
||||
memory: "4GiB",
|
||||
region: "asia-east1",
|
||||
invoker: "public",
|
||||
minInstances: 1,
|
||||
timeoutSeconds: 3600,
|
||||
},
|
||||
(request, response) => {
|
||||
if (request.path === "/generate-image") {
|
||||
generateImage(request, response);
|
||||
} else {
|
||||
response.status(200).send({
|
||||
routes: ["/generate-image"],
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedLocals": false,
|
||||
"outDir": "lib",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"target": "es2017"
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "ES2022"
|
||||
},
|
||||
"compileOnSave": true,
|
||||
"include": [
|
||||
|
||||
@@ -37,7 +37,7 @@ current:
|
||||
Generate image base on current scene while ensuring the options are in the scene. Ensure the character is accurate to the description and use the descriptions defined in the style, and setting section
|
||||
`,
|
||||
n: 1,
|
||||
size: "1024x1024",
|
||||
size: "1792x1024",
|
||||
});
|
||||
|
||||
console.log(`response: ${JSON.stringify(response, null, 2)}`);
|
||||
|
||||
Reference in New Issue
Block a user