feat: prepare deployment and latest app updates

This commit is contained in:
eric
2026-03-22 03:18:03 +01:00
parent 74042182ed
commit e0ba54f2c3
8 changed files with 28 additions and 64 deletions

View File

@@ -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

View File

@@ -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 |

View File

@@ -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: ""

View File

@@ -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", ""),

View File

@@ -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.`;
}

View File

@@ -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<string, { width: number; height: number }> = {
"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,40 +146,41 @@ export class ImageGenService {
async generate(options: ImageGenOptions & { style?: string }): Promise<ImageGenResult> {
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
? await enhancePromptWithAI(prompt, style)
: 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,
});

View File

@@ -273,33 +273,24 @@ const toolHandlers: Record<string, ToolHandler> = {
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;
const isNsfwRequest = nsfwKeywords.test(prompt) || style === "hentai";
@@ -310,7 +301,6 @@ const toolHandlers: Record<string, ToolHandler> = {
const result = await imageGen.generate({
prompt,
model: modelChoice,
aspectRatio,
numImages: 1,
nsfw: isNsfwRequest && Boolean(context.nsfwImageEnabled),

View File

@@ -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"],