207 lines
5.9 KiB
TypeScript
207 lines
5.9 KiB
TypeScript
/**
|
|
* Services Configuration Module
|
|
* Reads service configuration from the database (appConfig table)
|
|
* and provides runtime mode switching for FFmpeg, SAM 2, ControlNet, and LoRA
|
|
*
|
|
* The admin panel persists configuration under key "services_config" as JSON.
|
|
* This module reads that config and provides typed accessors for each service.
|
|
*/
|
|
|
|
import { getDb } from "./db";
|
|
|
|
export interface ServicesConfig {
|
|
ffmpegMode: "simulated" | "external";
|
|
ffmpegEndpoint: string;
|
|
sam2Mode: "simulated" | "external";
|
|
sam2Endpoint: string;
|
|
}
|
|
|
|
const DEFAULT_CONFIG: ServicesConfig = {
|
|
ffmpegMode: "simulated",
|
|
ffmpegEndpoint: "",
|
|
sam2Mode: "simulated",
|
|
sam2Endpoint: "",
|
|
};
|
|
|
|
let cachedConfig: ServicesConfig | null = null;
|
|
let cacheTimestamp = 0;
|
|
const CACHE_TTL_MS = 30_000; // 30 seconds cache
|
|
|
|
/**
|
|
* Load services configuration from the database
|
|
* Uses a 30-second cache to avoid excessive DB reads
|
|
*/
|
|
export async function getServicesConfig(): Promise<ServicesConfig> {
|
|
const now = Date.now();
|
|
if (cachedConfig && (now - cacheTimestamp) < CACHE_TTL_MS) {
|
|
return cachedConfig;
|
|
}
|
|
|
|
try {
|
|
const db = await getDb();
|
|
if (db) {
|
|
const { appConfig } = await import("../drizzle/schema");
|
|
const { eq } = await import("drizzle-orm");
|
|
const result = await db.select().from(appConfig).where(eq(appConfig.key, "services_config")).limit(1);
|
|
if (result[0]?.value) {
|
|
const parsed = JSON.parse(result[0].value);
|
|
cachedConfig = {
|
|
ffmpegMode: parsed.ffmpegMode || "simulated",
|
|
ffmpegEndpoint: parsed.ffmpegEndpoint || "",
|
|
sam2Mode: parsed.sam2Mode || "simulated",
|
|
sam2Endpoint: parsed.sam2Endpoint || "",
|
|
};
|
|
cacheTimestamp = now;
|
|
return cachedConfig;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.warn("[ServicesConfig] Failed to load config from DB:", error);
|
|
}
|
|
|
|
return DEFAULT_CONFIG;
|
|
}
|
|
|
|
/**
|
|
* Invalidate the cached config (called after admin saves new config)
|
|
*/
|
|
export function invalidateServicesConfigCache(): void {
|
|
cachedConfig = null;
|
|
cacheTimestamp = 0;
|
|
}
|
|
|
|
/**
|
|
* Call an external FFmpeg service for frame extraction
|
|
* Only used when ffmpegMode === "external"
|
|
*/
|
|
export async function callExternalFFmpeg(
|
|
endpoint: string,
|
|
action: "extract-frames" | "extract-audio" | "encode-video",
|
|
params: Record<string, unknown>
|
|
): Promise<unknown> {
|
|
const url = `${endpoint}/${action}`;
|
|
console.log(`[FFmpeg External] Calling ${url}`);
|
|
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(params),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`FFmpeg service error: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Call an external SAM 2 service for segmentation
|
|
* Only used when sam2Mode === "external"
|
|
*/
|
|
export async function callExternalSAM2(
|
|
endpoint: string,
|
|
action: "segment" | "propagate",
|
|
params: Record<string, unknown>
|
|
): Promise<unknown> {
|
|
const url = `${endpoint}/${action}`;
|
|
console.log(`[SAM2 External] Calling ${url}`);
|
|
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(params),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`SAM 2 service error: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Determine the generation strategy for a character based on their modelType
|
|
* This selects the appropriate generation approach:
|
|
* - "none": Standard generateImage with prompt engineering
|
|
* - "lora": Pass LoRA model identifier in the prompt/params for consistent style
|
|
* - "ip_adapter": Use the reference sheet as IP-Adapter input for style transfer
|
|
*/
|
|
export function getCharacterGenerationStrategy(
|
|
modelType: "lora" | "ip_adapter" | "none",
|
|
characterName: string,
|
|
referenceSheetUrl?: string | null
|
|
): {
|
|
promptPrefix: string;
|
|
referenceImages: Array<{ url: string; mimeType: "image/png" | "image/jpeg" }>;
|
|
strategy: string;
|
|
} {
|
|
switch (modelType) {
|
|
case "lora":
|
|
return {
|
|
promptPrefix: `[LoRA:${characterName}] `,
|
|
referenceImages: referenceSheetUrl
|
|
? [{ url: referenceSheetUrl, mimeType: "image/png" }]
|
|
: [],
|
|
strategy: "lora",
|
|
};
|
|
case "ip_adapter":
|
|
return {
|
|
promptPrefix: `Character "${characterName}" in exact same style as the reference image. `,
|
|
referenceImages: referenceSheetUrl
|
|
? [{ url: referenceSheetUrl, mimeType: "image/png" }]
|
|
: [],
|
|
strategy: "ip_adapter",
|
|
};
|
|
default:
|
|
return {
|
|
promptPrefix: `Character "${characterName}". `,
|
|
referenceImages: referenceSheetUrl
|
|
? [{ url: referenceSheetUrl, mimeType: "image/png" }]
|
|
: [],
|
|
strategy: "standard",
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build ControlNet-aware prompt constraints for pose preservation
|
|
* Adds explicit instructions to maintain pose, proportions, and position
|
|
*/
|
|
export function buildPoseConstraints(
|
|
basePrompt: string,
|
|
options: {
|
|
preservePose?: boolean;
|
|
preserveProportions?: boolean;
|
|
preservePosition?: boolean;
|
|
poseDescription?: string;
|
|
} = {}
|
|
): string {
|
|
const {
|
|
preservePose = true,
|
|
preserveProportions = true,
|
|
preservePosition = true,
|
|
poseDescription,
|
|
} = options;
|
|
|
|
const constraints: string[] = [basePrompt];
|
|
|
|
if (preservePose) {
|
|
constraints.push("Maintain the exact same body pose and gesture as the reference.");
|
|
}
|
|
if (preserveProportions) {
|
|
constraints.push("Keep identical proportions, scale, and aspect ratio.");
|
|
}
|
|
if (preservePosition) {
|
|
constraints.push("Position the subject at the exact same coordinates in the frame.");
|
|
}
|
|
if (poseDescription) {
|
|
constraints.push(`Pose reference: ${poseDescription}`);
|
|
}
|
|
|
|
constraints.push(
|
|
"This is for animation frame consistency - any deviation in pose or position will break continuity."
|
|
);
|
|
|
|
return constraints.join(" ");
|
|
}
|