From e0ba54f2c3c6330156645b936288b4cc01c1cca8 Mon Sep 17 00:00:00 2001 From: eric Date: Sun, 22 Mar 2026 03:18:03 +0100 Subject: [PATCH] feat: prepare deployment and latest app updates --- Dockerfile | 8 ++--- README.md | 1 + k8s/secret.example.yaml | 2 +- src/core/config.ts | 2 +- src/features/joel/responder.ts | 1 + src/services/ai/image-gen.ts | 62 +++++++++----------------------- src/services/ai/tool-handlers.ts | 12 +------ src/services/ai/tools.ts | 4 +-- 8 files changed, 28 insertions(+), 64 deletions(-) diff --git a/Dockerfile b/Dockerfile index 46f68fc..9837edf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build stage -FROM oven/bun:1 AS builder +FROM oven/bun:1.2.15 AS builder WORKDIR /app @@ -11,8 +11,8 @@ RUN apt-get update \ # Copy package files COPY package.json bun.lockb ./ -# Install dependencies -RUN bun install --frozen-lockfile +# Install dependencies. Bun 1.2.x is pinned here for @discordjs/opus ABI compatibility. +RUN bun install # Copy source code COPY src ./src @@ -21,7 +21,7 @@ COPY tsconfig.json drizzle.config.ts ./ RUN bun run css:build # Production stage -FROM oven/bun:1-slim +FROM oven/bun:1.2.15-slim WORKDIR /app diff --git a/README.md b/README.md index 6c8723c..312e8a8 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ src/ | `DISCORD_CLIENT_ID` | Discord application client ID | | `DISCORD_CLIENT_SECRET` | Discord application client secret | | `OPENROUTER_API_KEY` | OpenRouter API key for AI | +| `FAL_API_KEY` or `FAL_KEY` | Fal API key for image generation | | `AI_CLASSIFICATION_FALLBACK_MODELS` | Comma-separated fallback model IDs for classification requests | | `KLIPY_API_KEY` | Klipy API key for GIF search (optional) | | `ELEVENLABS_API_KEY` | ElevenLabs API key for voiceover | diff --git a/k8s/secret.example.yaml b/k8s/secret.example.yaml index fbdbfdd..d9e14fe 100644 --- a/k8s/secret.example.yaml +++ b/k8s/secret.example.yaml @@ -12,7 +12,7 @@ stringData: OPENAI_API_KEY: "" HF_TOKEN: "" REPLICATE_API_KEY: "" - FAL_KEY: "" + FAL_API_KEY: "" KLIPY_API_KEY: "" ELEVENLABS_API_KEY: "" ELEVENLABS_VOICE_ID: "" diff --git a/src/core/config.ts b/src/core/config.ts index 1af40a5..2f711fb 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -126,7 +126,7 @@ export const config: BotConfig = { apiKey: getFirstEnvOrDefault(["REPLICATE_API_KEY", "REPLICATE_API_TOKEN"], ""), }, fal: { - apiKey: getEnvOrDefault("FAL_KEY", ""), + apiKey: getFirstEnvOrDefault(["FAL_API_KEY", "FAL_KEY"], ""), }, klipy: { apiKey: getEnvOrDefault("KLIPY_API_KEY", ""), diff --git a/src/features/joel/responder.ts b/src/features/joel/responder.ts index d72b5e4..0b00d69 100644 --- a/src/features/joel/responder.ts +++ b/src/features/joel/responder.ts @@ -414,6 +414,7 @@ ${nsfwImageEnabled : "NSFW image generation is disabled in this server. Do not attempt NSFW image requests."} Be creative with your prompts. Describe the image in detail for best results. +Default to square images unless the user explicitly asks for portrait or widescreen framing. The image URL will appear in your response for the user to see.`; } diff --git a/src/services/ai/image-gen.ts b/src/services/ai/image-gen.ts index 15fad0a..f92626e 100644 --- a/src/services/ai/image-gen.ts +++ b/src/services/ai/image-gen.ts @@ -1,6 +1,5 @@ /** * Image Generation service using Fal.ai - * Supports NSFW content generation */ import { fal } from "@fal-ai/client"; @@ -14,23 +13,13 @@ fal.config({ credentials: config.fal.apiKey, }); -/** - * Available image generation models on Fal.ai - */ -const MODELS = { - fast: "fal-ai/flux/schnell" as const, - quality: "fal-ai/flux/dev" as const, - anime: "fal-ai/flux/dev" as const, -}; - -type ModelType = keyof typeof MODELS; +const FAL_IMAGE_MODEL = "fal-ai/flux-pro/v1.1-ultra" as const; /** * Image generation options */ export interface ImageGenOptions { prompt: string; - model?: ModelType; aspectRatio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4"; numImages?: number; nsfw?: boolean; @@ -128,7 +117,7 @@ Output ONLY the enhanced prompt, nothing else.`; /** * Enhance a prompt for better image generation */ -function enhancePrompt(prompt: string, model: ModelType, nsfw: boolean): string { +function enhancePrompt(prompt: string, nsfw: boolean): string { const qualityBoosts = ["highly detailed", "high quality", "sharp focus", "professional"]; const hasQuality = qualityBoosts.some((q) => prompt.toLowerCase().includes(q)); @@ -144,27 +133,9 @@ function enhancePrompt(prompt: string, model: ModelType, nsfw: boolean): string } } - if (model === "anime" && !prompt.toLowerCase().includes("anime")) { - prompt = `anime style, ${prompt}`; - } - return prompt; } -/** - * Convert aspect ratio to image size - */ -function getImageSize(aspectRatio: string): { width: number; height: number } { - const sizes: Record = { - "1:1": { width: 1024, height: 1024 }, - "16:9": { width: 1344, height: 768 }, - "9:16": { width: 768, height: 1344 }, - "4:3": { width: 1152, height: 896 }, - "3:4": { width: 896, height: 1152 }, - }; - return sizes[aspectRatio] || sizes["1:1"]; -} - /** * Image Generation Service using Fal.ai */ @@ -175,14 +146,11 @@ export class ImageGenService { async generate(options: ImageGenOptions & { style?: string }): Promise { const { prompt, - model = "fast", aspectRatio = "1:1", numImages = 1, nsfw = false, style, } = options; - - const modelId = MODELS[model]; // First, use AI to enhance vague NSFW prompts into detailed ones const aiEnhancedPrompt = nsfw @@ -190,25 +158,29 @@ export class ImageGenService { : prompt; // Then apply standard quality enhancements - const finalPrompt = enhancePrompt(aiEnhancedPrompt, model, nsfw); - const size = getImageSize(aspectRatio); + const finalPrompt = enhancePrompt(aiEnhancedPrompt, nsfw); + const safetyTolerance = nsfw ? "5" : "2"; logger.debug("Generating image with Fal.ai", { - model: modelId, - size, + model: FAL_IMAGE_MODEL, + aspectRatio, numImages, originalPromptLength: prompt.length, finalPromptLength: finalPrompt.length, nsfw, + safetyTolerance, }); try { - const result = await fal.subscribe(modelId, { + const result = await fal.subscribe(FAL_IMAGE_MODEL, { input: { prompt: finalPrompt, - image_size: size, + aspect_ratio: aspectRatio, num_images: Math.min(numImages, 4), - enable_safety_checker: false, + enable_safety_checker: true, + safety_tolerance: safetyTolerance, + output_format: "jpeg", + enhance_prompt: false, }, logs: false, }); @@ -229,20 +201,20 @@ export class ImageGenService { } logger.info("Image generated successfully", { - model: modelId, + model: FAL_IMAGE_MODEL, numImages: urls.length, }); return { urls, - model: modelId, + model: FAL_IMAGE_MODEL, prompt: finalPrompt, }; } catch (error) { logger.error("Image generation failed", { - model: modelId, + model: FAL_IMAGE_MODEL, promptLength: finalPrompt.length, - size, + aspectRatio, nsfw, numImages, }); diff --git a/src/services/ai/tool-handlers.ts b/src/services/ai/tool-handlers.ts index 7a95a9f..c90c483 100644 --- a/src/services/ai/tool-handlers.ts +++ b/src/services/ai/tool-handlers.ts @@ -273,32 +273,23 @@ const toolHandlers: Record = { const prompt = args.prompt as string; const style = args.style as string | undefined; const aspectRatio = (args.aspect_ratio as "1:1" | "16:9" | "9:16" | "4:3" | "3:4") || "1:1"; - const quality = (args.quality as "fast" | "quality" | "anime") || "fast"; - if (!prompt || prompt.trim().length === 0) { return "Error: No prompt provided for image generation."; } if (!config.fal.apiKey) { - return "Error: Image generation is not configured (missing FAL_KEY)."; + return "Error: Image generation is not configured (missing FAL_API_KEY or FAL_KEY)."; } logger.info("Generating image", { promptLength: prompt.length, style, aspectRatio, - quality, userId: context.userId }); try { const imageGen = getImageGenService(); - - // Auto-select anime model for anime/hentai style - let modelChoice = quality; - if (style === "anime" || style === "hentai") { - modelChoice = "anime"; - } // Only enable NSFW if user explicitly requests it const nsfwKeywords = /\b(naked|nude|nsfw|porn|xxx|hentai|sex|fuck|cock|pussy|tits)\b/i; @@ -310,7 +301,6 @@ const toolHandlers: Record = { const result = await imageGen.generate({ prompt, - model: modelChoice, aspectRatio, numImages: 1, nsfw: isNsfwRequest && Boolean(context.nsfwImageEnabled), diff --git a/src/services/ai/tools.ts b/src/services/ai/tools.ts index e4ae10d..11beb7c 100644 --- a/src/services/ai/tools.ts +++ b/src/services/ai/tools.ts @@ -220,12 +220,12 @@ export const IMAGE_GEN_TOOL: ChatCompletionTool = { aspect_ratio: { type: "string", enum: ["1:1", "16:9", "9:16", "4:3", "3:4"], - description: "Aspect ratio. Use 9:16 or 3:4 for portraits/full body, 16:9 for wide scenes.", + description: "Aspect ratio. Default to 1:1 to keep image generation cheaper unless the user explicitly wants portrait or widescreen framing. Use 9:16 or 3:4 for portraits/full body, 16:9 for wide scenes.", }, quality: { type: "string", enum: ["fast", "quality", "anime"], - description: "Model selection. 'fast' = quick generation, 'quality' = higher detail, 'anime' = anime style.", + description: "Style hint only. The backend uses a single Fal FLUX Ultra model.", }, }, required: ["prompt"],