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