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 # Build stage
FROM oven/bun:1 AS builder FROM oven/bun:1.2.15 AS builder
WORKDIR /app WORKDIR /app
@@ -11,8 +11,8 @@ RUN apt-get update \
# Copy package files # Copy package files
COPY package.json bun.lockb ./ COPY package.json bun.lockb ./
# Install dependencies # Install dependencies. Bun 1.2.x is pinned here for @discordjs/opus ABI compatibility.
RUN bun install --frozen-lockfile RUN bun install
# Copy source code # Copy source code
COPY src ./src COPY src ./src
@@ -21,7 +21,7 @@ COPY tsconfig.json drizzle.config.ts ./
RUN bun run css:build RUN bun run css:build
# Production stage # Production stage
FROM oven/bun:1-slim FROM oven/bun:1.2.15-slim
WORKDIR /app WORKDIR /app

View File

@@ -42,6 +42,7 @@ src/
| `DISCORD_CLIENT_ID` | Discord application client ID | | `DISCORD_CLIENT_ID` | Discord application client ID |
| `DISCORD_CLIENT_SECRET` | Discord application client secret | | `DISCORD_CLIENT_SECRET` | Discord application client secret |
| `OPENROUTER_API_KEY` | OpenRouter API key for AI | | `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 | | `AI_CLASSIFICATION_FALLBACK_MODELS` | Comma-separated fallback model IDs for classification requests |
| `KLIPY_API_KEY` | Klipy API key for GIF search (optional) | | `KLIPY_API_KEY` | Klipy API key for GIF search (optional) |
| `ELEVENLABS_API_KEY` | ElevenLabs API key for voiceover | | `ELEVENLABS_API_KEY` | ElevenLabs API key for voiceover |

View File

@@ -12,7 +12,7 @@ stringData:
OPENAI_API_KEY: "" OPENAI_API_KEY: ""
HF_TOKEN: "" HF_TOKEN: ""
REPLICATE_API_KEY: "" REPLICATE_API_KEY: ""
FAL_KEY: "" FAL_API_KEY: ""
KLIPY_API_KEY: "" KLIPY_API_KEY: ""
ELEVENLABS_API_KEY: "" ELEVENLABS_API_KEY: ""
ELEVENLABS_VOICE_ID: "" ELEVENLABS_VOICE_ID: ""

View File

@@ -126,7 +126,7 @@ export const config: BotConfig = {
apiKey: getFirstEnvOrDefault(["REPLICATE_API_KEY", "REPLICATE_API_TOKEN"], ""), apiKey: getFirstEnvOrDefault(["REPLICATE_API_KEY", "REPLICATE_API_TOKEN"], ""),
}, },
fal: { fal: {
apiKey: getEnvOrDefault("FAL_KEY", ""), apiKey: getFirstEnvOrDefault(["FAL_API_KEY", "FAL_KEY"], ""),
}, },
klipy: { klipy: {
apiKey: getEnvOrDefault("KLIPY_API_KEY", ""), 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."} : "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. 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.`; 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 * Image Generation service using Fal.ai
* Supports NSFW content generation
*/ */
import { fal } from "@fal-ai/client"; import { fal } from "@fal-ai/client";
@@ -14,23 +13,13 @@ fal.config({
credentials: config.fal.apiKey, credentials: config.fal.apiKey,
}); });
/** const FAL_IMAGE_MODEL = "fal-ai/flux-pro/v1.1-ultra" as const;
* 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;
/** /**
* Image generation options * Image generation options
*/ */
export interface ImageGenOptions { export interface ImageGenOptions {
prompt: string; prompt: string;
model?: ModelType;
aspectRatio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4"; aspectRatio?: "1:1" | "16:9" | "9:16" | "4:3" | "3:4";
numImages?: number; numImages?: number;
nsfw?: boolean; nsfw?: boolean;
@@ -128,7 +117,7 @@ Output ONLY the enhanced prompt, nothing else.`;
/** /**
* Enhance a prompt for better image generation * 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 qualityBoosts = ["highly detailed", "high quality", "sharp focus", "professional"];
const hasQuality = qualityBoosts.some((q) => prompt.toLowerCase().includes(q)); 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; 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 * Image Generation Service using Fal.ai
*/ */
@@ -175,14 +146,11 @@ export class ImageGenService {
async generate(options: ImageGenOptions & { style?: string }): Promise<ImageGenResult> { async generate(options: ImageGenOptions & { style?: string }): Promise<ImageGenResult> {
const { const {
prompt, prompt,
model = "fast",
aspectRatio = "1:1", aspectRatio = "1:1",
numImages = 1, numImages = 1,
nsfw = false, nsfw = false,
style, style,
} = options; } = options;
const modelId = MODELS[model];
// First, use AI to enhance vague NSFW prompts into detailed ones // First, use AI to enhance vague NSFW prompts into detailed ones
const aiEnhancedPrompt = nsfw const aiEnhancedPrompt = nsfw
@@ -190,25 +158,29 @@ export class ImageGenService {
: prompt; : prompt;
// Then apply standard quality enhancements // Then apply standard quality enhancements
const finalPrompt = enhancePrompt(aiEnhancedPrompt, model, nsfw); const finalPrompt = enhancePrompt(aiEnhancedPrompt, nsfw);
const size = getImageSize(aspectRatio); const safetyTolerance = nsfw ? "5" : "2";
logger.debug("Generating image with Fal.ai", { logger.debug("Generating image with Fal.ai", {
model: modelId, model: FAL_IMAGE_MODEL,
size, aspectRatio,
numImages, numImages,
originalPromptLength: prompt.length, originalPromptLength: prompt.length,
finalPromptLength: finalPrompt.length, finalPromptLength: finalPrompt.length,
nsfw, nsfw,
safetyTolerance,
}); });
try { try {
const result = await fal.subscribe(modelId, { const result = await fal.subscribe(FAL_IMAGE_MODEL, {
input: { input: {
prompt: finalPrompt, prompt: finalPrompt,
image_size: size, aspect_ratio: aspectRatio,
num_images: Math.min(numImages, 4), 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, logs: false,
}); });
@@ -229,20 +201,20 @@ export class ImageGenService {
} }
logger.info("Image generated successfully", { logger.info("Image generated successfully", {
model: modelId, model: FAL_IMAGE_MODEL,
numImages: urls.length, numImages: urls.length,
}); });
return { return {
urls, urls,
model: modelId, model: FAL_IMAGE_MODEL,
prompt: finalPrompt, prompt: finalPrompt,
}; };
} catch (error) { } catch (error) {
logger.error("Image generation failed", { logger.error("Image generation failed", {
model: modelId, model: FAL_IMAGE_MODEL,
promptLength: finalPrompt.length, promptLength: finalPrompt.length,
size, aspectRatio,
nsfw, nsfw,
numImages, numImages,
}); });

View File

@@ -273,32 +273,23 @@ const toolHandlers: Record<string, ToolHandler> = {
const prompt = args.prompt as string; const prompt = args.prompt as string;
const style = args.style as string | undefined; 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 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) { if (!prompt || prompt.trim().length === 0) {
return "Error: No prompt provided for image generation."; return "Error: No prompt provided for image generation.";
} }
if (!config.fal.apiKey) { 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", { logger.info("Generating image", {
promptLength: prompt.length, promptLength: prompt.length,
style, style,
aspectRatio, aspectRatio,
quality,
userId: context.userId userId: context.userId
}); });
try { try {
const imageGen = getImageGenService(); 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 // 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 nsfwKeywords = /\b(naked|nude|nsfw|porn|xxx|hentai|sex|fuck|cock|pussy|tits)\b/i;
@@ -310,7 +301,6 @@ const toolHandlers: Record<string, ToolHandler> = {
const result = await imageGen.generate({ const result = await imageGen.generate({
prompt, prompt,
model: modelChoice,
aspectRatio, aspectRatio,
numImages: 1, numImages: 1,
nsfw: isNsfwRequest && Boolean(context.nsfwImageEnabled), nsfw: isNsfwRequest && Boolean(context.nsfwImageEnabled),

View File

@@ -220,12 +220,12 @@ export const IMAGE_GEN_TOOL: ChatCompletionTool = {
aspect_ratio: { aspect_ratio: {
type: "string", type: "string",
enum: ["1:1", "16:9", "9:16", "4:3", "3:4"], 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: { quality: {
type: "string", type: "string",
enum: ["fast", "quality", "anime"], 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"], required: ["prompt"],