feat(realtime-canvas): migrate provider to BFL flux.2
This commit is contained in:
parent
795fcff90f
commit
b5fdbd7c10
@ -102,8 +102,8 @@ import {
|
|||||||
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
import { fetchUploadWithRetry } from "@/lib/client/upload-with-retry";
|
||||||
|
|
||||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||||
const DEFAULT_RUNWARE_REALTIME_MODEL = "runware:101@1";
|
const DEFAULT_BFL_REALTIME_MODEL = "flux-2-klein-4b";
|
||||||
const DEFAULT_RUNWARE_STYLIZE_MODEL = "runware:106@1";
|
const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai";
|
||||||
|
|
||||||
function createEmptyCanvasSkillTag(): CanvasSkillTag {
|
function createEmptyCanvasSkillTag(): CanvasSkillTag {
|
||||||
return {
|
return {
|
||||||
@ -810,11 +810,9 @@ export function AdminSettingsClient() {
|
|||||||
// 实时画布API配置状态
|
// 实时画布API配置状态
|
||||||
const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({
|
const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
falApiKey: "",
|
bflApiKey: "",
|
||||||
runwareModel: DEFAULT_RUNWARE_REALTIME_MODEL,
|
bflBaseUrl: DEFAULT_BFL_BASE_URL,
|
||||||
runwareStylizeModel: DEFAULT_RUNWARE_STYLIZE_MODEL,
|
bflModel: DEFAULT_BFL_REALTIME_MODEL,
|
||||||
runwareControlModel: "runware:25@1",
|
|
||||||
runwareControlPreprocessor: "canny" as "canny" | "lineart" | "depth",
|
|
||||||
renderMode: "balanced" as "strict-structure" | "balanced" | "stylize",
|
renderMode: "balanced" as "strict-structure" | "balanced" | "stylize",
|
||||||
throttleInterval: 100,
|
throttleInterval: 100,
|
||||||
maxBuffering: 2,
|
maxBuffering: 2,
|
||||||
@ -970,12 +968,12 @@ export function AdminSettingsClient() {
|
|||||||
if (realtimeCanvasConfigData?.setting?.value) {
|
if (realtimeCanvasConfigData?.setting?.value) {
|
||||||
const config = realtimeCanvasConfigData.setting.value as {
|
const config = realtimeCanvasConfigData.setting.value as {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
bflApiKey?: string;
|
||||||
|
bflBaseUrl?: string;
|
||||||
|
bflModel?: string;
|
||||||
runwareApiKey?: string;
|
runwareApiKey?: string;
|
||||||
falApiKey?: string;
|
falApiKey?: string;
|
||||||
runwareModel?: string;
|
runwareModel?: string;
|
||||||
runwareStylizeModel?: string;
|
|
||||||
runwareControlModel?: string;
|
|
||||||
runwareControlPreprocessor?: "canny" | "lineart" | "depth";
|
|
||||||
renderMode?: "strict-structure" | "balanced" | "stylize";
|
renderMode?: "strict-structure" | "balanced" | "stylize";
|
||||||
throttleInterval?: number;
|
throttleInterval?: number;
|
||||||
maxBuffering?: number;
|
maxBuffering?: number;
|
||||||
@ -990,15 +988,9 @@ export function AdminSettingsClient() {
|
|||||||
};
|
};
|
||||||
setRealtimeCanvasConfig({
|
setRealtimeCanvasConfig({
|
||||||
enabled: config.enabled ?? true,
|
enabled: config.enabled ?? true,
|
||||||
falApiKey: config.runwareApiKey || config.falApiKey || "",
|
bflApiKey: config.bflApiKey || config.runwareApiKey || config.falApiKey || "",
|
||||||
runwareModel: config.runwareModel || DEFAULT_RUNWARE_REALTIME_MODEL,
|
bflBaseUrl: config.bflBaseUrl || DEFAULT_BFL_BASE_URL,
|
||||||
runwareStylizeModel: config.runwareStylizeModel || DEFAULT_RUNWARE_STYLIZE_MODEL,
|
bflModel: config.bflModel || config.runwareModel || DEFAULT_BFL_REALTIME_MODEL,
|
||||||
runwareControlModel: config.runwareControlModel || "runware:25@1",
|
|
||||||
runwareControlPreprocessor:
|
|
||||||
config.runwareControlPreprocessor === "lineart" ||
|
|
||||||
config.runwareControlPreprocessor === "depth"
|
|
||||||
? config.runwareControlPreprocessor
|
|
||||||
: "canny",
|
|
||||||
renderMode:
|
renderMode:
|
||||||
config.renderMode === "strict-structure" || config.renderMode === "stylize"
|
config.renderMode === "strict-structure" || config.renderMode === "stylize"
|
||||||
? config.renderMode
|
? config.renderMode
|
||||||
@ -2031,36 +2023,29 @@ export function AdminSettingsClient() {
|
|||||||
// --- 实时画布API配置逻辑 ---
|
// --- 实时画布API配置逻辑 ---
|
||||||
const handleSaveRealtimeCanvasConfig = async () => {
|
const handleSaveRealtimeCanvasConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const runwareApiKey = realtimeCanvasConfig.falApiKey.trim();
|
const bflApiKey = realtimeCanvasConfig.bflApiKey.trim();
|
||||||
const runwareModel =
|
const bflBaseUrl = realtimeCanvasConfig.bflBaseUrl.trim() || DEFAULT_BFL_BASE_URL;
|
||||||
realtimeCanvasConfig.runwareModel.trim() || DEFAULT_RUNWARE_REALTIME_MODEL;
|
const bflModel = realtimeCanvasConfig.bflModel.trim() || DEFAULT_BFL_REALTIME_MODEL;
|
||||||
const runwareStylizeModel =
|
|
||||||
realtimeCanvasConfig.runwareStylizeModel.trim() || DEFAULT_RUNWARE_STYLIZE_MODEL;
|
|
||||||
const runwareControlModel =
|
|
||||||
realtimeCanvasConfig.runwareControlModel.trim() || "runware:25@1";
|
|
||||||
|
|
||||||
if (realtimeCanvasConfig.enabled && !runwareApiKey) {
|
if (realtimeCanvasConfig.enabled && !bflApiKey) {
|
||||||
toast.error("启用实时画布功能需要配置 Runware API Key");
|
toast.error("启用实时画布功能需要配置 BFL API Key");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (realtimeCanvasConfig.enabled) {
|
if (realtimeCanvasConfig.enabled) {
|
||||||
for (const modelId of [runwareModel, runwareStylizeModel].filter(Boolean)) {
|
const probeResponse = await fetch("/api/admin/realtime/model-capability", {
|
||||||
const probeResponse = await fetch("/api/admin/realtime/model-capability", {
|
method: "POST",
|
||||||
method: "POST",
|
headers: { "Content-Type": "application/json" },
|
||||||
headers: { "Content-Type": "application/json" },
|
body: JSON.stringify({
|
||||||
body: JSON.stringify({
|
apiKey: bflApiKey,
|
||||||
apiKey: runwareApiKey,
|
modelId: bflModel,
|
||||||
modelId,
|
baseUrl: bflBaseUrl,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const probeData = await probeResponse.json().catch(() => null);
|
const probeData = await probeResponse.json().catch(() => null);
|
||||||
if (!probeResponse.ok || probeData?.supported === false) {
|
if (!probeResponse.ok || probeData?.supported === false) {
|
||||||
toast.error(
|
toast.error(probeData?.message || "模型能力探针失败,已阻止保存");
|
||||||
probeData?.message || "模型能力探针失败:当前模型不支持 seedImage,已阻止保存"
|
return;
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2071,12 +2056,12 @@ export function AdminSettingsClient() {
|
|||||||
key: "realtime_canvas_config",
|
key: "realtime_canvas_config",
|
||||||
value: {
|
value: {
|
||||||
enabled: realtimeCanvasConfig.enabled,
|
enabled: realtimeCanvasConfig.enabled,
|
||||||
runwareApiKey,
|
bflApiKey,
|
||||||
falApiKey: runwareApiKey,
|
bflBaseUrl,
|
||||||
runwareModel,
|
bflModel,
|
||||||
runwareStylizeModel,
|
runwareApiKey: bflApiKey,
|
||||||
runwareControlModel,
|
falApiKey: bflApiKey,
|
||||||
runwareControlPreprocessor: realtimeCanvasConfig.runwareControlPreprocessor,
|
runwareModel: bflModel,
|
||||||
renderMode: realtimeCanvasConfig.renderMode,
|
renderMode: realtimeCanvasConfig.renderMode,
|
||||||
throttleInterval: Math.min(260, Math.max(60, Number(realtimeCanvasConfig.throttleInterval) || 100)),
|
throttleInterval: Math.min(260, Math.max(60, Number(realtimeCanvasConfig.throttleInterval) || 100)),
|
||||||
maxBuffering: Math.min(6, Math.max(1, Number(realtimeCanvasConfig.maxBuffering) || 2)),
|
maxBuffering: Math.min(6, Math.max(1, Number(realtimeCanvasConfig.maxBuffering) || 2)),
|
||||||
@ -8359,7 +8344,7 @@ export function AdminSettingsClient() {
|
|||||||
实时画布配置
|
实时画布配置
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
配置实时画布功能的 Runware 参数、模型与 API Key。保存时会自动探针校验模型是否支持 seedImage。
|
配置实时画布功能的 BFL(FLUX.2)参数、模型与 API Key。保存时会自动探针校验模型可用性。
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6 pt-6">
|
<CardContent className="space-y-6 pt-6">
|
||||||
@ -8384,67 +8369,50 @@ export function AdminSettingsClient() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="realtimeFalApiKey">
|
<Label htmlFor="realtimeBflApiKey">
|
||||||
Runware API Key <span className="text-destructive">*</span>
|
BFL API Key <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="realtimeFalApiKey"
|
id="realtimeBflApiKey"
|
||||||
type="password"
|
type="password"
|
||||||
value={realtimeCanvasConfig.falApiKey}
|
value={realtimeCanvasConfig.bflApiKey}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setRealtimeCanvasConfig({ ...realtimeCanvasConfig, falApiKey: e.target.value })
|
setRealtimeCanvasConfig({ ...realtimeCanvasConfig, bflApiKey: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder="rw_xxxxxxxxxxxxx"
|
placeholder="bfl_xxxxxxxxxxxxx"
|
||||||
disabled={!realtimeCanvasConfig.enabled}
|
disabled={!realtimeCanvasConfig.enabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="runwareModel">
|
<Label htmlFor="realtimeBflBaseUrl">BFL API Base URL</Label>
|
||||||
Runware 模型 ID <span className="text-destructive">*</span>
|
<Input
|
||||||
|
id="realtimeBflBaseUrl"
|
||||||
|
value={realtimeCanvasConfig.bflBaseUrl}
|
||||||
|
onChange={(e) =>
|
||||||
|
setRealtimeCanvasConfig({ ...realtimeCanvasConfig, bflBaseUrl: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder={DEFAULT_BFL_BASE_URL}
|
||||||
|
disabled={!realtimeCanvasConfig.enabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="realtimeBflModel">
|
||||||
|
FLUX.2 模型 ID <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="runwareModel"
|
id="realtimeBflModel"
|
||||||
value={realtimeCanvasConfig.runwareModel}
|
value={realtimeCanvasConfig.bflModel}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setRealtimeCanvasConfig({ ...realtimeCanvasConfig, runwareModel: e.target.value })
|
setRealtimeCanvasConfig({ ...realtimeCanvasConfig, bflModel: e.target.value })
|
||||||
}
|
}
|
||||||
placeholder={DEFAULT_RUNWARE_REALTIME_MODEL}
|
placeholder={DEFAULT_BFL_REALTIME_MODEL}
|
||||||
disabled={!realtimeCanvasConfig.enabled}
|
disabled={!realtimeCanvasConfig.enabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-1 gap-4">
|
||||||
<Label htmlFor="runwareStylizeModel">写实增强模型 ID</Label>
|
|
||||||
<Input
|
|
||||||
id="runwareStylizeModel"
|
|
||||||
value={realtimeCanvasConfig.runwareStylizeModel}
|
|
||||||
onChange={(e) =>
|
|
||||||
setRealtimeCanvasConfig({
|
|
||||||
...realtimeCanvasConfig,
|
|
||||||
runwareStylizeModel: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder={DEFAULT_RUNWARE_STYLIZE_MODEL}
|
|
||||||
disabled={!realtimeCanvasConfig.enabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="runwareControlModel">ControlNet 模型 ID</Label>
|
|
||||||
<Input
|
|
||||||
id="runwareControlModel"
|
|
||||||
value={realtimeCanvasConfig.runwareControlModel}
|
|
||||||
onChange={(e) =>
|
|
||||||
setRealtimeCanvasConfig({
|
|
||||||
...realtimeCanvasConfig,
|
|
||||||
runwareControlModel: e.target.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="runware:25@1"
|
|
||||||
disabled={!realtimeCanvasConfig.enabled}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>实时渲染模式</Label>
|
<Label>实时渲染模式</Label>
|
||||||
<Select
|
<Select
|
||||||
@ -8465,30 +8433,9 @@ export function AdminSettingsClient() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
|
||||||
<Label>ControlNet 预处理</Label>
|
|
||||||
<Select
|
|
||||||
value={realtimeCanvasConfig.runwareControlPreprocessor}
|
|
||||||
onValueChange={(value: "canny" | "lineart" | "depth") =>
|
|
||||||
setRealtimeCanvasConfig({
|
|
||||||
...realtimeCanvasConfig,
|
|
||||||
runwareControlPreprocessor: value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<SelectTrigger>
|
|
||||||
<SelectValue />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="canny">canny</SelectItem>
|
|
||||||
<SelectItem value="lineart">lineart</SelectItem>
|
|
||||||
<SelectItem value="depth">depth</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
推荐:主模型 {DEFAULT_RUNWARE_REALTIME_MODEL} + ControlNet {`runware:25@1`}。写实模型 {DEFAULT_RUNWARE_STYLIZE_MODEL} 作为回退链路。
|
推荐:实时草图模型使用 `flux-2-klein-4b`,高质量可切换 `flux-2-pro-preview`。
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { requireAdmin } from "@/lib/middleware/admin";
|
import { requireAdmin } from "@/lib/middleware/admin";
|
||||||
import { ChatSDKError } from "@/lib/errors";
|
import { ChatSDKError } from "@/lib/errors";
|
||||||
import { probeRunwareSeedImageSupport } from "@/lib/realtime/runware-capability";
|
import { probeBflModelSupport } from "@/lib/realtime/bfl-capability";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
apiKey: z.string().min(1),
|
apiKey: z.string().min(1),
|
||||||
modelId: z.string().min(1),
|
modelId: z.string().min(1),
|
||||||
|
baseUrl: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
function mapStatus(code: string) {
|
function mapStatus(code: string) {
|
||||||
@ -19,9 +20,10 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const data = schema.parse(body);
|
const data = schema.parse(body);
|
||||||
const result = await probeRunwareSeedImageSupport({
|
const result = await probeBflModelSupport({
|
||||||
apiKey: data.apiKey,
|
apiKey: data.apiKey,
|
||||||
modelId: data.modelId,
|
modelId: data.modelId,
|
||||||
|
baseUrl: data.baseUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!result.supported) {
|
if (!result.supported) {
|
||||||
|
|||||||
@ -6,16 +6,15 @@ import {
|
|||||||
upsertSystemSetting,
|
upsertSystemSetting,
|
||||||
} from "@/lib/db/queries";
|
} from "@/lib/db/queries";
|
||||||
import { ChatSDKError } from "@/lib/errors";
|
import { ChatSDKError } from "@/lib/errors";
|
||||||
import { probeRunwareSeedImageSupport } from "@/lib/realtime/runware-capability";
|
import { probeBflModelSupport } from "@/lib/realtime/bfl-capability";
|
||||||
|
|
||||||
const settingSchema = z.object({
|
const settingSchema = z.object({
|
||||||
key: z.string(),
|
key: z.string(),
|
||||||
value: z.any(),
|
value: z.any(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const DEFAULT_RUNWARE_MODEL = "runware:101@1";
|
const DEFAULT_BFL_MODEL = "flux-2-klein-4b";
|
||||||
const DEFAULT_RUNWARE_STYLIZE_MODEL = "runware:106@1";
|
const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai";
|
||||||
const DEFAULT_RUNWARE_CONTROL_MODEL = "runware:25@1";
|
|
||||||
|
|
||||||
function isObjectLike(value: unknown): value is Record<string, unknown> {
|
function isObjectLike(value: unknown): value is Record<string, unknown> {
|
||||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||||
@ -63,18 +62,11 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const enabled = value.enabled !== false;
|
const enabled = value.enabled !== false;
|
||||||
const runwareApiKey = String(value.runwareApiKey || value.falApiKey || value.apiKey || "").trim();
|
const bflApiKey = String(
|
||||||
const runwareModel = String(value.runwareModel || DEFAULT_RUNWARE_MODEL).trim() || DEFAULT_RUNWARE_MODEL;
|
value.bflApiKey || value.runwareApiKey || value.falApiKey || value.apiKey || ""
|
||||||
const runwareStylizeModel =
|
).trim();
|
||||||
String(value.runwareStylizeModel || DEFAULT_RUNWARE_STYLIZE_MODEL).trim() ||
|
const bflModel = String(value.bflModel || value.runwareModel || DEFAULT_BFL_MODEL).trim() || DEFAULT_BFL_MODEL;
|
||||||
DEFAULT_RUNWARE_STYLIZE_MODEL;
|
const bflBaseUrl = String(value.bflBaseUrl || DEFAULT_BFL_BASE_URL).trim() || DEFAULT_BFL_BASE_URL;
|
||||||
const runwareControlModel =
|
|
||||||
String(value.runwareControlModel || DEFAULT_RUNWARE_CONTROL_MODEL).trim() ||
|
|
||||||
DEFAULT_RUNWARE_CONTROL_MODEL;
|
|
||||||
const runwareControlPreprocessor =
|
|
||||||
value.runwareControlPreprocessor === "lineart" || value.runwareControlPreprocessor === "depth"
|
|
||||||
? value.runwareControlPreprocessor
|
|
||||||
: "canny";
|
|
||||||
const renderMode =
|
const renderMode =
|
||||||
value.renderMode === "strict-structure" ||
|
value.renderMode === "strict-structure" ||
|
||||||
value.renderMode === "stylize" ||
|
value.renderMode === "stylize" ||
|
||||||
@ -83,34 +75,34 @@ export async function POST(request: Request) {
|
|||||||
: "balanced";
|
: "balanced";
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
if (!runwareApiKey) {
|
if (!bflApiKey) {
|
||||||
return new ChatSDKError("bad_request:api", "启用实时画布必须配置 Runware API Key").toResponse();
|
return new ChatSDKError("bad_request:api", "启用实时画布必须配置 BFL API Key").toResponse();
|
||||||
}
|
}
|
||||||
for (const modelId of [runwareModel, runwareStylizeModel].filter(Boolean)) {
|
const probeResult = await probeBflModelSupport({
|
||||||
const probeResult = await probeRunwareSeedImageSupport({
|
apiKey: bflApiKey,
|
||||||
apiKey: runwareApiKey,
|
modelId: bflModel,
|
||||||
modelId,
|
baseUrl: bflBaseUrl,
|
||||||
});
|
});
|
||||||
if (!probeResult.supported) {
|
if (!probeResult.supported) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
code: probeResult.code,
|
code: probeResult.code,
|
||||||
message: probeResult.message,
|
message: probeResult.message,
|
||||||
},
|
},
|
||||||
{ status: mapProbeStatus(probeResult.code) }
|
{ status: mapProbeStatus(probeResult.code) }
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextValue = {
|
nextValue = {
|
||||||
...value,
|
...value,
|
||||||
runwareApiKey,
|
bflApiKey,
|
||||||
falApiKey: runwareApiKey,
|
bflModel,
|
||||||
runwareModel,
|
bflBaseUrl,
|
||||||
runwareStylizeModel,
|
runwareApiKey: bflApiKey,
|
||||||
runwareControlModel,
|
falApiKey: bflApiKey,
|
||||||
runwareControlPreprocessor,
|
apiKey: bflApiKey,
|
||||||
|
runwareModel: bflModel,
|
||||||
renderMode,
|
renderMode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import { getSystemSetting } from "@/lib/db/queries";
|
|||||||
|
|
||||||
type RealtimeCanvasConfig = {
|
type RealtimeCanvasConfig = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
bflApiKey?: string;
|
||||||
|
bflModel?: string;
|
||||||
|
bflBaseUrl?: string;
|
||||||
runwareApiKey?: string;
|
runwareApiKey?: string;
|
||||||
runwareModel?: string;
|
runwareModel?: string;
|
||||||
runwareControlModel?: string;
|
|
||||||
runwareControlPreprocessor?: "canny" | "lineart" | "depth";
|
|
||||||
renderMode?: "strict-structure" | "balanced" | "stylize";
|
renderMode?: "strict-structure" | "balanced" | "stylize";
|
||||||
falApiKey?: string;
|
falApiKey?: string;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
@ -38,15 +39,15 @@ const defaultConfig = {
|
|||||||
jpegQuality: 0.55,
|
jpegQuality: 0.55,
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
id: "runware:101@1",
|
id: "flux-2-klein-4b",
|
||||||
defaults: {
|
defaults: {
|
||||||
steps: 8,
|
steps: 3,
|
||||||
cfgScale: 7,
|
cfgScale: 7,
|
||||||
strength: 0.32,
|
strength: 0.32,
|
||||||
imageSize: "square" as const,
|
imageSize: "square" as const,
|
||||||
},
|
},
|
||||||
limits: {
|
limits: {
|
||||||
steps: { min: 1, max: 12 },
|
steps: { min: 1, max: 6 },
|
||||||
cfgScale: { min: 1, max: 14 },
|
cfgScale: { min: 1, max: 14 },
|
||||||
strength: { min: 0.05, max: 1.0 },
|
strength: { min: 0.05, max: 1.0 },
|
||||||
},
|
},
|
||||||
@ -77,20 +78,21 @@ export async function GET() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const runwareApiKey = String(
|
const bflApiKey = String(
|
||||||
config.runwareApiKey ||
|
config.bflApiKey ||
|
||||||
|
config.runwareApiKey ||
|
||||||
config.falApiKey ||
|
config.falApiKey ||
|
||||||
config.apiKey ||
|
config.apiKey ||
|
||||||
process.env.RUNWARE_API_KEY ||
|
process.env.BFL_API_KEY ||
|
||||||
process.env.RUNWARE_KEY ||
|
process.env.BFL_KEY ||
|
||||||
""
|
""
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
if (!runwareApiKey) {
|
if (!bflApiKey) {
|
||||||
return Response.json(
|
return Response.json(
|
||||||
{
|
{
|
||||||
code: "RUNWARE_KEY_MISSING",
|
code: "BFL_KEY_MISSING",
|
||||||
message: "未配置 Runware API Key(请在服务器环境变量或设置中配置)",
|
message: "未配置 BFL API Key(请在服务器环境变量或设置中配置)",
|
||||||
},
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
@ -138,13 +140,20 @@ export async function GET() {
|
|||||||
);
|
);
|
||||||
const imageSize = config.defaults?.imageSize === "square_hd" ? "square_hd" : "square";
|
const imageSize = config.defaults?.imageSize === "square_hd" ? "square_hd" : "square";
|
||||||
const modelId = String(
|
const modelId = String(
|
||||||
config.runwareModel || process.env.RUNWARE_MODEL || defaultConfig.model.id
|
config.bflModel ||
|
||||||
|
config.runwareModel ||
|
||||||
|
process.env.BFL_MODEL ||
|
||||||
|
process.env.RUNWARE_MODEL ||
|
||||||
|
defaultConfig.model.id
|
||||||
).trim();
|
).trim();
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
provider: {
|
provider: {
|
||||||
type: "runware",
|
type: "bfl",
|
||||||
|
baseUrl:
|
||||||
|
String(config.bflBaseUrl || process.env.BFL_BASE_URL || "https://api.bfl.ai").trim() ||
|
||||||
|
"https://api.bfl.ai",
|
||||||
},
|
},
|
||||||
transport: {
|
transport: {
|
||||||
throttleInterval,
|
throttleInterval,
|
||||||
@ -171,13 +180,5 @@ export async function GET() {
|
|||||||
? config.renderMode
|
? config.renderMode
|
||||||
: "balanced",
|
: "balanced",
|
||||||
},
|
},
|
||||||
control: {
|
|
||||||
model: String(config.runwareControlModel || "runware:25@1").trim() || "runware:25@1",
|
|
||||||
preprocessor:
|
|
||||||
config.runwareControlPreprocessor === "lineart" ||
|
|
||||||
config.runwareControlPreprocessor === "depth"
|
|
||||||
? config.runwareControlPreprocessor
|
|
||||||
: "canny",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -54,7 +54,7 @@ export default function CanvasRealtimePage() {
|
|||||||
status: "completed",
|
status: "completed",
|
||||||
title: "实时画布结果",
|
title: "实时画布结果",
|
||||||
prompt,
|
prompt,
|
||||||
model: "fal-ai/flux-2/klein/realtime",
|
model: "bfl/flux-2",
|
||||||
},
|
},
|
||||||
connections: [],
|
connections: [],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -96,9 +96,9 @@ const defaultRealtimeConfig: RealtimeConfig = {
|
|||||||
jpegQuality: 0.55,
|
jpegQuality: 0.55,
|
||||||
},
|
},
|
||||||
model: {
|
model: {
|
||||||
id: "runware:101@1",
|
id: "flux-2-klein-4b",
|
||||||
defaults: {
|
defaults: {
|
||||||
steps: 8,
|
steps: 3,
|
||||||
cfgScale: 7,
|
cfgScale: 7,
|
||||||
strength: 0.32,
|
strength: 0.32,
|
||||||
imageSize: "square",
|
imageSize: "square",
|
||||||
@ -1414,7 +1414,7 @@ export function RealtimeCanvasDialog({
|
|||||||
|
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
connectionRef.current = {
|
connectionRef.current = {
|
||||||
provider: "runware",
|
provider: "bfl",
|
||||||
modelId: resolvedConfig.model.id,
|
modelId: resolvedConfig.model.id,
|
||||||
reconnectToken: refreshToken,
|
reconnectToken: refreshToken,
|
||||||
};
|
};
|
||||||
|
|||||||
159
lib/realtime/bfl-capability.ts
Normal file
159
lib/realtime/bfl-capability.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai";
|
||||||
|
const DEFAULT_BFL_MODEL_ID = "flux-2-klein-4b";
|
||||||
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
||||||
|
|
||||||
|
export type BflCapabilityCode =
|
||||||
|
| "SUPPORTED"
|
||||||
|
| "AUTH_FAILED"
|
||||||
|
| "MODEL_INVALID"
|
||||||
|
| "UPSTREAM_ERROR";
|
||||||
|
|
||||||
|
export type BflModelProbeResult = {
|
||||||
|
supported: boolean;
|
||||||
|
code: BflCapabilityCode;
|
||||||
|
message: string;
|
||||||
|
status?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type BflProbeParams = {
|
||||||
|
apiKey: string;
|
||||||
|
modelId?: string;
|
||||||
|
baseUrl?: string;
|
||||||
|
timeoutMs?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function normalizeBaseUrl(raw: string | undefined) {
|
||||||
|
const next = String(raw || DEFAULT_BFL_BASE_URL).trim();
|
||||||
|
return (next || DEFAULT_BFL_BASE_URL).replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeModelPath(modelId: string) {
|
||||||
|
return modelId
|
||||||
|
.split("/")
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => encodeURIComponent(part))
|
||||||
|
.join("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
function toErrorMessage(payload: any, fallback: string) {
|
||||||
|
const direct = String(
|
||||||
|
payload?.error?.message ||
|
||||||
|
payload?.error ||
|
||||||
|
payload?.message ||
|
||||||
|
payload?.detail ||
|
||||||
|
payload?.msg ||
|
||||||
|
""
|
||||||
|
).trim();
|
||||||
|
return direct || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeModelInvalid(message: string) {
|
||||||
|
const normalized = message.toLowerCase();
|
||||||
|
return (
|
||||||
|
normalized.includes("model") &&
|
||||||
|
(normalized.includes("not found") ||
|
||||||
|
normalized.includes("invalid") ||
|
||||||
|
normalized.includes("unknown") ||
|
||||||
|
normalized.includes("does not exist"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasTaskIdentity(payload: any) {
|
||||||
|
return Boolean(
|
||||||
|
(typeof payload?.id === "string" && payload.id.trim()) ||
|
||||||
|
(typeof payload?.polling_url === "string" && payload.polling_url.trim()) ||
|
||||||
|
(typeof payload?.response?.id === "string" && payload.response.id.trim()) ||
|
||||||
|
(typeof payload?.response?.polling_url === "string" &&
|
||||||
|
payload.response.polling_url.trim())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function probeBflModelSupport(params: BflProbeParams): Promise<BflModelProbeResult> {
|
||||||
|
const apiKey = String(params.apiKey || "").trim();
|
||||||
|
const modelId = String(params.modelId || DEFAULT_BFL_MODEL_ID).trim() || DEFAULT_BFL_MODEL_ID;
|
||||||
|
const baseUrl = normalizeBaseUrl(params.baseUrl);
|
||||||
|
const timeoutMs = Math.max(3_000, Number(params.timeoutMs || DEFAULT_TIMEOUT_MS));
|
||||||
|
|
||||||
|
if (!apiKey) {
|
||||||
|
return {
|
||||||
|
supported: false,
|
||||||
|
code: "AUTH_FAILED",
|
||||||
|
message: "缺少 BFL API Key。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const endpoint = `${baseUrl}/v1/${encodeModelPath(modelId)}`;
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"x-key": apiKey,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Accept: "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
prompt: "probe image generation capability",
|
||||||
|
width: 512,
|
||||||
|
height: 512,
|
||||||
|
safety_tolerance: 2,
|
||||||
|
}),
|
||||||
|
cache: "no-store",
|
||||||
|
signal: controller.signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = await response.json().catch(() => null);
|
||||||
|
const message = toErrorMessage(payload, `BFL 探针请求失败(${response.status})`);
|
||||||
|
|
||||||
|
if (response.ok && hasTaskIdentity(payload)) {
|
||||||
|
return {
|
||||||
|
supported: true,
|
||||||
|
code: "SUPPORTED",
|
||||||
|
message: "模型可用。",
|
||||||
|
status: response.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 401 || response.status === 403) {
|
||||||
|
return {
|
||||||
|
supported: false,
|
||||||
|
code: "AUTH_FAILED",
|
||||||
|
message: "BFL API Key 无效或无权限,请检查后重试。",
|
||||||
|
status: response.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status === 404 || looksLikeModelInvalid(message)) {
|
||||||
|
return {
|
||||||
|
supported: false,
|
||||||
|
code: "MODEL_INVALID",
|
||||||
|
message: `模型 ${modelId} 不存在或不可用,请更换模型后重试。`,
|
||||||
|
status: response.status,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
supported: false,
|
||||||
|
code: "UPSTREAM_ERROR",
|
||||||
|
message,
|
||||||
|
status: response.status,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.name === "AbortError") {
|
||||||
|
return {
|
||||||
|
supported: false,
|
||||||
|
code: "UPSTREAM_ERROR",
|
||||||
|
message: "BFL 探针请求超时,请稍后重试。",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
supported: false,
|
||||||
|
code: "UPSTREAM_ERROR",
|
||||||
|
message: error instanceof Error ? error.message : "BFL 探针请求失败。",
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user