Infrastructure: - MinIO déployé en local pour le stockage S3 (docker-compose) - Storage proxy réécrit: sert les fichiers depuis MinIO en streaming (plus de 307 redirect vers CDN externe) - Legacy /manus-storage/ redirige vers /storage/ LLM & Image Generation: - LLM: Gemini uniquement (suppression du fallback Forge) - Image generation: Gemini Imagen direct (suppression Forge GenerateImage) - llmConfig simplifié, un seul provider Nettoyage Manus: - Modules Forge stubbés (dataApi, heartbeat, map, notification, voiceTranscription) - ENV simplifié (suppression forgeApiUrl, forgeApiKey) - Analytics Manus supprimées du HTML - systemRouter simplifié Migration données: - 750 fichiers migrés de Forge S3 vers MinIO (69.8 MB) - URLs DB mises à jour: /manus-storage/ -> /storage/ - Script de migration inclus (scripts/migrate-to-minio.mjs) Performance: - Frame load: 500ms -> 62ms (8x plus rapide) - Plus aucune dépendance réseau transatlantique pour le stockage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
96 lines
2.6 KiB
TypeScript
96 lines
2.6 KiB
TypeScript
import { storagePut } from "server/storage";
|
|
|
|
export type GenerateImageOptions = {
|
|
prompt: string;
|
|
originalImages?: Array<{
|
|
url?: string;
|
|
b64Json?: string;
|
|
mimeType?: string;
|
|
}>;
|
|
};
|
|
|
|
export type GenerateImageResponse = {
|
|
url?: string;
|
|
};
|
|
|
|
export async function generateImage(
|
|
options: GenerateImageOptions
|
|
): Promise<GenerateImageResponse> {
|
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
if (!apiKey) {
|
|
throw new Error("GEMINI_API_KEY is not configured");
|
|
}
|
|
|
|
const url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent";
|
|
|
|
const parts: any[] = [{ text: options.prompt }];
|
|
|
|
if (options.originalImages) {
|
|
for (const img of options.originalImages) {
|
|
if (img.url && img.url.startsWith("http")) {
|
|
try {
|
|
const resp = await fetch(img.url);
|
|
if (resp.ok) {
|
|
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
const mime = img.mimeType || resp.headers.get("content-type") || "image/jpeg";
|
|
parts.push({
|
|
inlineData: {
|
|
mimeType: mime,
|
|
data: buffer.toString("base64"),
|
|
},
|
|
});
|
|
}
|
|
} catch (e) {
|
|
console.warn("[ImageGen] Failed to download reference image:", e);
|
|
}
|
|
} else if (img.b64Json) {
|
|
parts.push({
|
|
inlineData: {
|
|
mimeType: img.mimeType || "image/png",
|
|
data: img.b64Json,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const payload = {
|
|
contents: [{ parts }],
|
|
generationConfig: {
|
|
responseModalities: ["TEXT", "IMAGE"],
|
|
},
|
|
};
|
|
|
|
const response = await fetch(`${url}?key=${apiKey}`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const detail = await response.text().catch(() => "");
|
|
throw new Error(
|
|
`Image generation failed (${response.status}): ${detail.slice(0, 200)}`
|
|
);
|
|
}
|
|
|
|
const result = await response.json() as any;
|
|
|
|
const candidates = result.candidates || [];
|
|
for (const candidate of candidates) {
|
|
for (const part of candidate.content?.parts || []) {
|
|
if (part.inlineData?.data) {
|
|
const buffer = Buffer.from(part.inlineData.data, "base64");
|
|
const ext = part.inlineData.mimeType?.includes("png") ? "png" : "jpg";
|
|
const { url: storageUrl } = await storagePut(
|
|
`generated/${Date.now()}.${ext}`,
|
|
buffer,
|
|
part.inlineData.mimeType || "image/png"
|
|
);
|
|
return { url: storageUrl };
|
|
}
|
|
}
|
|
}
|
|
|
|
throw new Error("No image generated in response");
|
|
}
|