feat: content engine
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 2s
Monorepo Pipeline / 🧹 Lint (push) Successful in 1m12s
Monorepo Pipeline / 🧪 Test (push) Successful in 2m59s
Monorepo Pipeline / 🏗️ Build (push) Successful in 6m52s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Directus (Base) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 4s

This commit is contained in:
2026-02-22 02:39:27 +01:00
parent a9adb2eff7
commit 3a1a88db89
11 changed files with 942 additions and 172 deletions

View File

@@ -0,0 +1,84 @@
import Replicate from "replicate";
import * as fs from "node:fs/promises";
import * as path from "node:path";
export interface ThumbnailGeneratorConfig {
replicateApiKey: string;
}
export class ThumbnailGenerator {
private replicate: Replicate;
constructor(config: ThumbnailGeneratorConfig) {
this.replicate = new Replicate({
auth: config.replicateApiKey,
});
}
public async generateImage(
topic: string,
outputPath: string,
): Promise<string> {
const systemPrompt = `Technical blueprint / architectural illustration — clean lines, monochrome base with one highlighter accent color (yellow, pink, or green). Abstract, geometric, or diagrammatic illustrations only. 'Engineering notebook sketch' — precise, minimal, professional. No text in images. No people or realistic photos.`;
const prompt = `${systemPrompt}\n\nTopic to illustrate abstractly: ${topic}`;
console.log(`🎨 Generating thumbnail for topic: "${topic}"...`);
const output = await this.replicate.run("black-forest-labs/flux-1.1-pro", {
input: {
prompt,
aspect_ratio: "16:9",
output_format: "png",
output_quality: 90,
prompt_upsampling: false,
},
});
// Replicate returns a ReadableStream for the output of flux-1.1-pro in newer Node SDKs
// Or a string URL in older ones. We handle both.
let buffer: Buffer;
if (output instanceof ReadableStream) {
console.log(`⬇️ Downloading generated stream from Replicate...`);
const chunks: Uint8Array[] = [];
const reader = output.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) chunks.push(value);
}
buffer = Buffer.concat(chunks);
} else if (
typeof output === "string" ||
(Array.isArray(output) && typeof output[0] === "string")
) {
const imageUrl = Array.isArray(output) ? output[0] : output;
console.log(
`⬇️ Downloading generated image from URL: ${imageUrl.substring(0, 50)}...`,
);
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`Failed to download image: ${response.statusText}`);
}
const arrayBuffer = await response.arrayBuffer();
buffer = Buffer.from(arrayBuffer);
} else if (Buffer.isBuffer(output)) {
buffer = output;
} else if (typeof output === "object") {
console.log("Raw output object:", output);
throw new Error("Unexpected output format from Replicate.");
} else {
throw new Error("Unknown output format from Replicate.");
}
const absPath = path.isAbsolute(outputPath)
? outputPath
: path.resolve(process.cwd(), outputPath);
await fs.mkdir(path.dirname(absPath), { recursive: true });
await fs.writeFile(absPath, buffer);
console.log(`✅ Saved thumbnail to: ${absPath}`);
return absPath;
}
}

View File

@@ -0,0 +1 @@
export * from "./generator";