feat: prepare deployment and latest app updates
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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: ""
|
||||
|
||||
@@ -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", ""),
|
||||
|
||||
@@ -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.`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,14 +146,11 @@ 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
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -273,32 +273,23 @@ 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;
|
||||
@@ -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),
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user