Skip to content

Instantly share code, notes, and snippets.

@venbrinoDev
Created March 27, 2025 06:59
Show Gist options
  • Save venbrinoDev/bbfe65f6de431a15a416c7332c945230 to your computer and use it in GitHub Desktop.
Save venbrinoDev/bbfe65f6de431a15a416c7332c945230 to your computer and use it in GitHub Desktop.
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