Created
March 27, 2025 06:59
-
-
Save venbrinoDev/bbfe65f6de431a15a416c7332c945230 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import express, { Request, Response } from "express"; | |
import { Queue, Worker } from "bullmq"; | |
import sharp from "sharp"; | |
import vision from "@google-cloud/vision"; | |
import { Storage } from "@google-cloud/storage"; | |
import multer from "multer"; | |
const app = express(); | |
app.use(express.json()); | |
const storage = new Storage(); | |
const client = new vision.ImageAnnotatorClient(); | |
const bucketName = "zylag-ads-bucket"; | |
// Redis connection for queues | |
const redisConnection = { connection: { host: "localhost", port: 6379 } }; | |
const imageQueue = new Queue("optimize-images", redisConnection); | |
const labelQueue = new Queue("label-images", redisConnection); | |
// Multer for handling file uploads | |
const upload = multer({ storage: multer.memoryStorage() }); | |
/** Types */ | |
interface UploadedFile { | |
raw: string; | |
filePath: string; | |
adId: string; | |
} | |
interface OptimizedImage extends UploadedFile { | |
thumbnail?: string; | |
medium?: string; | |
large?: string; | |
} | |
/** | |
* API: Upload Images & Queue for Processing | |
*/ | |
app.post("/upload", upload.array("files"), async (req: Request, res: Response) => { | |
const { adId } = req.body; | |
if (!adId || !req.files || (req.files as Express.Multer.File[]).length === 0) { | |
return res.status(400).json({ success: false, message: "Invalid request" }); | |
} | |
const uploadedFiles: UploadedFile[] = []; | |
// Step 1: Upload raw images to Google Cloud Storage | |
for (const file of req.files as Express.Multer.File[]) { | |
const filePath = `ads/${adId}-${file.originalname}`; | |
await storage.bucket(bucketName).file(filePath).save(file.buffer); | |
uploadedFiles.push({ | |
raw: `https://storage.googleapis.com/${bucketName}/${filePath}`, | |
filePath, | |
adId, | |
}); | |
} | |
// Step 2: Queue images for optimization and labeling | |
await imageQueue.add("process-image", { images: uploadedFiles }); | |
await labelQueue.add("process-labels", { images: uploadedFiles }); | |
res.json({ success: true, message: "Upload complete, processing in background.", files: uploadedFiles }); | |
}); | |
/** | |
* Worker: Optimize Images | |
*/ | |
const optimizeWorker = new Worker( | |
"optimize-images", | |
async (job) => { | |
const { images }: { images: UploadedFile[] } = job.data; | |
const sizes = { thumbnail: 150, medium: 600, large: 1200 }; | |
const optimizedImages: OptimizedImage[] = []; | |
for (const image of images) { | |
const { filePath, adId, raw } = image; | |
const rawFile = storage.bucket(bucketName).file(filePath); | |
const buffer = (await rawFile.download())[0]; | |
const imageVariants: OptimizedImage = { raw, filePath, adId }; | |
// Generate optimized images | |
for (const [sizeName, width] of Object.entries(sizes)) { | |
const optimizedBuffer = await sharp(buffer) | |
.resize(width) | |
.flatten({ background: { r: 255, g: 255, b: 255 } }) | |
.jpeg({ quality: 85 }) | |
.toBuffer(); | |
const optimizedPath = `ads/${adId}-${sizeName}.jpg`; | |
await storage.bucket(bucketName).file(optimizedPath).save(optimizedBuffer); | |
(imageVariants as any)[sizeName] = `https://storage.googleapis.com/${bucketName}/${optimizedPath}`; | |
} | |
optimizedImages.push(imageVariants); | |
} | |
console.log("✅ Optimized Images:", optimizedImages); | |
return optimizedImages; | |
}, | |
{ concurrency: 5 } | |
); | |
/** | |
* Worker: Generate Labels | |
*/ | |
const labelWorker = new Worker( | |
"label-images", | |
async (job) => { | |
const { images }: { images: UploadedFile[] } = job.data; | |
const labeledImages: { raw: string; labels: string[] }[] = []; | |
for (const image of images) { | |
const { filePath, raw } = image; | |
const [visionResult] = await client.labelDetection(`gs://${bucketName}/${filePath}`); | |
const labels = visionResult.labelAnnotations?.map((label) => label.description) || []; | |
labeledImages.push({ raw, labels }); | |
} | |
console.log("✅ Labeled Images:", labeledImages); | |
return labeledImages; | |
}, | |
{ concurrency: 5 } | |
); | |
/** | |
* Helper Function: Convert Storage URL to CDN URL | |
*/ | |
const convertToCDN = (storageUrl: string): string => { | |
return storageUrl.replace(`https://storage.googleapis.com/${bucketName}/`, "https://cdn.zylag.com/"); | |
}; | |
/** | |
* API: Retrieve Image Data | |
*/ | |
app.get("/image-data/:adId", async (req: Request, res: Response) => { | |
const { adId } = req.params; | |
// Example database response (this should come from DB in real implementation) | |
const images = [ | |
{ | |
raw: `https://storage.googleapis.com/${bucketName}/ads/${adId}-image.jpg`, | |
thumbnail: `https://storage.googleapis.com/${bucketName}/ads/${adId}-thumbnail.jpg`, | |
medium: `https://storage.googleapis.com/${bucketName}/ads/${adId}-medium.jpg`, | |
large: `https://storage.googleapis.com/${bucketName}/ads/${adId}-large.jpg` | |
} | |
]; | |
const labels = ["Car", "Vehicle", "Transportation"]; | |
// Convert storage URLs to CDN for frontend | |
const formattedImages = images.map((img) => ({ | |
raw: convertToCDN(img.raw), | |
thumbnail: convertToCDN(img.thumbnail), | |
medium: convertToCDN(img.medium), | |
large: convertToCDN(img.large), | |
})); | |
res.json({ | |
images: formattedImages, | |
labels, | |
detectedText: "For Sale: Toyota Camry 2020", | |
safeSearch: { adult: "VERY_UNLIKELY", violence: "UNLIKELY" }, | |
}); | |
}); | |
// Start server | |
app.listen(3000, () => console.log("🚀 Server running on port 3000")); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment