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";
|
||||
|
||||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||||
const DEFAULT_RUNWARE_REALTIME_MODEL = "runware:101@1";
|
||||
const DEFAULT_RUNWARE_STYLIZE_MODEL = "runware:106@1";
|
||||
const DEFAULT_BFL_REALTIME_MODEL = "flux-2-klein-4b";
|
||||
const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai";
|
||||
|
||||
function createEmptyCanvasSkillTag(): CanvasSkillTag {
|
||||
return {
|
||||
@ -810,11 +810,9 @@ export function AdminSettingsClient() {
|
||||
// 实时画布API配置状态
|
||||
const [realtimeCanvasConfig, setRealtimeCanvasConfig] = useState({
|
||||
enabled: true,
|
||||
falApiKey: "",
|
||||
runwareModel: DEFAULT_RUNWARE_REALTIME_MODEL,
|
||||
runwareStylizeModel: DEFAULT_RUNWARE_STYLIZE_MODEL,
|
||||
runwareControlModel: "runware:25@1",
|
||||
runwareControlPreprocessor: "canny" as "canny" | "lineart" | "depth",
|
||||
bflApiKey: "",
|
||||
bflBaseUrl: DEFAULT_BFL_BASE_URL,
|
||||
bflModel: DEFAULT_BFL_REALTIME_MODEL,
|
||||
renderMode: "balanced" as "strict-structure" | "balanced" | "stylize",
|
||||
throttleInterval: 100,
|
||||
maxBuffering: 2,
|
||||
@ -970,12 +968,12 @@ export function AdminSettingsClient() {
|
||||
if (realtimeCanvasConfigData?.setting?.value) {
|
||||
const config = realtimeCanvasConfigData.setting.value as {
|
||||
enabled?: boolean;
|
||||
bflApiKey?: string;
|
||||
bflBaseUrl?: string;
|
||||
bflModel?: string;
|
||||
runwareApiKey?: string;
|
||||
falApiKey?: string;
|
||||
runwareModel?: string;
|
||||
runwareStylizeModel?: string;
|
||||
runwareControlModel?: string;
|
||||
runwareControlPreprocessor?: "canny" | "lineart" | "depth";
|
||||
renderMode?: "strict-structure" | "balanced" | "stylize";
|
||||
throttleInterval?: number;
|
||||
maxBuffering?: number;
|
||||
@ -990,15 +988,9 @@ export function AdminSettingsClient() {
|
||||
};
|
||||
setRealtimeCanvasConfig({
|
||||
enabled: config.enabled ?? true,
|
||||
falApiKey: config.runwareApiKey || config.falApiKey || "",
|
||||
runwareModel: config.runwareModel || DEFAULT_RUNWARE_REALTIME_MODEL,
|
||||
runwareStylizeModel: config.runwareStylizeModel || DEFAULT_RUNWARE_STYLIZE_MODEL,
|
||||
runwareControlModel: config.runwareControlModel || "runware:25@1",
|
||||
runwareControlPreprocessor:
|
||||
config.runwareControlPreprocessor === "lineart" ||
|
||||
config.runwareControlPreprocessor === "depth"
|
||||
? config.runwareControlPreprocessor
|
||||
: "canny",
|
||||
bflApiKey: config.bflApiKey || config.runwareApiKey || config.falApiKey || "",
|
||||
bflBaseUrl: config.bflBaseUrl || DEFAULT_BFL_BASE_URL,
|
||||
bflModel: config.bflModel || config.runwareModel || DEFAULT_BFL_REALTIME_MODEL,
|
||||
renderMode:
|
||||
config.renderMode === "strict-structure" || config.renderMode === "stylize"
|
||||
? config.renderMode
|
||||
@ -2031,36 +2023,29 @@ export function AdminSettingsClient() {
|
||||
// --- 实时画布API配置逻辑 ---
|
||||
const handleSaveRealtimeCanvasConfig = async () => {
|
||||
try {
|
||||
const runwareApiKey = realtimeCanvasConfig.falApiKey.trim();
|
||||
const runwareModel =
|
||||
realtimeCanvasConfig.runwareModel.trim() || DEFAULT_RUNWARE_REALTIME_MODEL;
|
||||
const runwareStylizeModel =
|
||||
realtimeCanvasConfig.runwareStylizeModel.trim() || DEFAULT_RUNWARE_STYLIZE_MODEL;
|
||||
const runwareControlModel =
|
||||
realtimeCanvasConfig.runwareControlModel.trim() || "runware:25@1";
|
||||
const bflApiKey = realtimeCanvasConfig.bflApiKey.trim();
|
||||
const bflBaseUrl = realtimeCanvasConfig.bflBaseUrl.trim() || DEFAULT_BFL_BASE_URL;
|
||||
const bflModel = realtimeCanvasConfig.bflModel.trim() || DEFAULT_BFL_REALTIME_MODEL;
|
||||
|
||||
if (realtimeCanvasConfig.enabled && !runwareApiKey) {
|
||||
toast.error("启用实时画布功能需要配置 Runware API Key");
|
||||
if (realtimeCanvasConfig.enabled && !bflApiKey) {
|
||||
toast.error("启用实时画布功能需要配置 BFL API Key");
|
||||
return;
|
||||
}
|
||||
|
||||
if (realtimeCanvasConfig.enabled) {
|
||||
for (const modelId of [runwareModel, runwareStylizeModel].filter(Boolean)) {
|
||||
const probeResponse = await fetch("/api/admin/realtime/model-capability", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
apiKey: runwareApiKey,
|
||||
modelId,
|
||||
}),
|
||||
});
|
||||
const probeData = await probeResponse.json().catch(() => null);
|
||||
if (!probeResponse.ok || probeData?.supported === false) {
|
||||
toast.error(
|
||||
probeData?.message || "模型能力探针失败:当前模型不支持 seedImage,已阻止保存"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const probeResponse = await fetch("/api/admin/realtime/model-capability", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
apiKey: bflApiKey,
|
||||
modelId: bflModel,
|
||||
baseUrl: bflBaseUrl,
|
||||
}),
|
||||
});
|
||||
const probeData = await probeResponse.json().catch(() => null);
|
||||
if (!probeResponse.ok || probeData?.supported === false) {
|
||||
toast.error(probeData?.message || "模型能力探针失败,已阻止保存");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2071,12 +2056,12 @@ export function AdminSettingsClient() {
|
||||
key: "realtime_canvas_config",
|
||||
value: {
|
||||
enabled: realtimeCanvasConfig.enabled,
|
||||
runwareApiKey,
|
||||
falApiKey: runwareApiKey,
|
||||
runwareModel,
|
||||
runwareStylizeModel,
|
||||
runwareControlModel,
|
||||
runwareControlPreprocessor: realtimeCanvasConfig.runwareControlPreprocessor,
|
||||
bflApiKey,
|
||||
bflBaseUrl,
|
||||
bflModel,
|
||||
runwareApiKey: bflApiKey,
|
||||
falApiKey: bflApiKey,
|
||||
runwareModel: bflModel,
|
||||
renderMode: realtimeCanvasConfig.renderMode,
|
||||
throttleInterval: Math.min(260, Math.max(60, Number(realtimeCanvasConfig.throttleInterval) || 100)),
|
||||
maxBuffering: Math.min(6, Math.max(1, Number(realtimeCanvasConfig.maxBuffering) || 2)),
|
||||
@ -8359,7 +8344,7 @@ export function AdminSettingsClient() {
|
||||
实时画布配置
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
配置实时画布功能的 Runware 参数、模型与 API Key。保存时会自动探针校验模型是否支持 seedImage。
|
||||
配置实时画布功能的 BFL(FLUX.2)参数、模型与 API Key。保存时会自动探针校验模型可用性。
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6 pt-6">
|
||||
@ -8384,67 +8369,50 @@ export function AdminSettingsClient() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="realtimeFalApiKey">
|
||||
Runware API Key <span className="text-destructive">*</span>
|
||||
<Label htmlFor="realtimeBflApiKey">
|
||||
BFL API Key <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="realtimeFalApiKey"
|
||||
id="realtimeBflApiKey"
|
||||
type="password"
|
||||
value={realtimeCanvasConfig.falApiKey}
|
||||
value={realtimeCanvasConfig.bflApiKey}
|
||||
onChange={(e) =>
|
||||
setRealtimeCanvasConfig({ ...realtimeCanvasConfig, falApiKey: e.target.value })
|
||||
setRealtimeCanvasConfig({ ...realtimeCanvasConfig, bflApiKey: e.target.value })
|
||||
}
|
||||
placeholder="rw_xxxxxxxxxxxxx"
|
||||
placeholder="bfl_xxxxxxxxxxxxx"
|
||||
disabled={!realtimeCanvasConfig.enabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="runwareModel">
|
||||
Runware 模型 ID <span className="text-destructive">*</span>
|
||||
<Label htmlFor="realtimeBflBaseUrl">BFL API Base URL</Label>
|
||||
<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>
|
||||
<Input
|
||||
id="runwareModel"
|
||||
value={realtimeCanvasConfig.runwareModel}
|
||||
id="realtimeBflModel"
|
||||
value={realtimeCanvasConfig.bflModel}
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<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="grid grid-cols-1 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label>实时渲染模式</Label>
|
||||
<Select
|
||||
@ -8465,30 +8433,9 @@ export function AdminSettingsClient() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import { z } from "zod";
|
||||
import { requireAdmin } from "@/lib/middleware/admin";
|
||||
import { ChatSDKError } from "@/lib/errors";
|
||||
import { probeRunwareSeedImageSupport } from "@/lib/realtime/runware-capability";
|
||||
import { probeBflModelSupport } from "@/lib/realtime/bfl-capability";
|
||||
|
||||
const schema = z.object({
|
||||
apiKey: z.string().min(1),
|
||||
modelId: z.string().min(1),
|
||||
baseUrl: z.string().optional(),
|
||||
});
|
||||
|
||||
function mapStatus(code: string) {
|
||||
@ -19,9 +20,10 @@ export async function POST(request: Request) {
|
||||
|
||||
const body = await request.json();
|
||||
const data = schema.parse(body);
|
||||
const result = await probeRunwareSeedImageSupport({
|
||||
const result = await probeBflModelSupport({
|
||||
apiKey: data.apiKey,
|
||||
modelId: data.modelId,
|
||||
baseUrl: data.baseUrl,
|
||||
});
|
||||
|
||||
if (!result.supported) {
|
||||
|
||||
@ -6,16 +6,15 @@ import {
|
||||
upsertSystemSetting,
|
||||
} from "@/lib/db/queries";
|
||||
import { ChatSDKError } from "@/lib/errors";
|
||||
import { probeRunwareSeedImageSupport } from "@/lib/realtime/runware-capability";
|
||||
import { probeBflModelSupport } from "@/lib/realtime/bfl-capability";
|
||||
|
||||
const settingSchema = z.object({
|
||||
key: z.string(),
|
||||
value: z.any(),
|
||||
});
|
||||
|
||||
const DEFAULT_RUNWARE_MODEL = "runware:101@1";
|
||||
const DEFAULT_RUNWARE_STYLIZE_MODEL = "runware:106@1";
|
||||
const DEFAULT_RUNWARE_CONTROL_MODEL = "runware:25@1";
|
||||
const DEFAULT_BFL_MODEL = "flux-2-klein-4b";
|
||||
const DEFAULT_BFL_BASE_URL = "https://api.bfl.ai";
|
||||
|
||||
function isObjectLike(value: unknown): value is Record<string, unknown> {
|
||||
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 runwareApiKey = String(value.runwareApiKey || value.falApiKey || value.apiKey || "").trim();
|
||||
const runwareModel = String(value.runwareModel || DEFAULT_RUNWARE_MODEL).trim() || DEFAULT_RUNWARE_MODEL;
|
||||
const runwareStylizeModel =
|
||||
String(value.runwareStylizeModel || DEFAULT_RUNWARE_STYLIZE_MODEL).trim() ||
|
||||
DEFAULT_RUNWARE_STYLIZE_MODEL;
|
||||
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 bflApiKey = String(
|
||||
value.bflApiKey || value.runwareApiKey || value.falApiKey || value.apiKey || ""
|
||||
).trim();
|
||||
const bflModel = String(value.bflModel || value.runwareModel || DEFAULT_BFL_MODEL).trim() || DEFAULT_BFL_MODEL;
|
||||
const bflBaseUrl = String(value.bflBaseUrl || DEFAULT_BFL_BASE_URL).trim() || DEFAULT_BFL_BASE_URL;
|
||||
const renderMode =
|
||||
value.renderMode === "strict-structure" ||
|
||||
value.renderMode === "stylize" ||
|
||||
@ -83,34 +75,34 @@ export async function POST(request: Request) {
|
||||
: "balanced";
|
||||
|
||||
if (enabled) {
|
||||
if (!runwareApiKey) {
|
||||
return new ChatSDKError("bad_request:api", "启用实时画布必须配置 Runware API Key").toResponse();
|
||||
if (!bflApiKey) {
|
||||
return new ChatSDKError("bad_request:api", "启用实时画布必须配置 BFL API Key").toResponse();
|
||||
}
|
||||
for (const modelId of [runwareModel, runwareStylizeModel].filter(Boolean)) {
|
||||
const probeResult = await probeRunwareSeedImageSupport({
|
||||
apiKey: runwareApiKey,
|
||||
modelId,
|
||||
});
|
||||
if (!probeResult.supported) {
|
||||
return Response.json(
|
||||
{
|
||||
code: probeResult.code,
|
||||
message: probeResult.message,
|
||||
},
|
||||
{ status: mapProbeStatus(probeResult.code) }
|
||||
);
|
||||
}
|
||||
const probeResult = await probeBflModelSupport({
|
||||
apiKey: bflApiKey,
|
||||
modelId: bflModel,
|
||||
baseUrl: bflBaseUrl,
|
||||
});
|
||||
if (!probeResult.supported) {
|
||||
return Response.json(
|
||||
{
|
||||
code: probeResult.code,
|
||||
message: probeResult.message,
|
||||
},
|
||||
{ status: mapProbeStatus(probeResult.code) }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
nextValue = {
|
||||
...value,
|
||||
runwareApiKey,
|
||||
falApiKey: runwareApiKey,
|
||||
runwareModel,
|
||||
runwareStylizeModel,
|
||||
runwareControlModel,
|
||||
runwareControlPreprocessor,
|
||||
bflApiKey,
|
||||
bflModel,
|
||||
bflBaseUrl,
|
||||
runwareApiKey: bflApiKey,
|
||||
falApiKey: bflApiKey,
|
||||
apiKey: bflApiKey,
|
||||
runwareModel: bflModel,
|
||||
renderMode,
|
||||
};
|
||||
}
|
||||
|
||||
@ -3,10 +3,11 @@ import { getSystemSetting } from "@/lib/db/queries";
|
||||
|
||||
type RealtimeCanvasConfig = {
|
||||
enabled?: boolean;
|
||||
bflApiKey?: string;
|
||||
bflModel?: string;
|
||||
bflBaseUrl?: string;
|
||||
runwareApiKey?: string;
|
||||
runwareModel?: string;
|
||||
runwareControlModel?: string;
|
||||
runwareControlPreprocessor?: "canny" | "lineart" | "depth";
|
||||
renderMode?: "strict-structure" | "balanced" | "stylize";
|
||||
falApiKey?: string;
|
||||
apiKey?: string;
|
||||
@ -38,15 +39,15 @@ const defaultConfig = {
|
||||
jpegQuality: 0.55,
|
||||
},
|
||||
model: {
|
||||
id: "runware:101@1",
|
||||
id: "flux-2-klein-4b",
|
||||
defaults: {
|
||||
steps: 8,
|
||||
steps: 3,
|
||||
cfgScale: 7,
|
||||
strength: 0.32,
|
||||
imageSize: "square" as const,
|
||||
},
|
||||
limits: {
|
||||
steps: { min: 1, max: 12 },
|
||||
steps: { min: 1, max: 6 },
|
||||
cfgScale: { min: 1, max: 14 },
|
||||
strength: { min: 0.05, max: 1.0 },
|
||||
},
|
||||
@ -77,20 +78,21 @@ export async function GET() {
|
||||
);
|
||||
}
|
||||
|
||||
const runwareApiKey = String(
|
||||
config.runwareApiKey ||
|
||||
const bflApiKey = String(
|
||||
config.bflApiKey ||
|
||||
config.runwareApiKey ||
|
||||
config.falApiKey ||
|
||||
config.apiKey ||
|
||||
process.env.RUNWARE_API_KEY ||
|
||||
process.env.RUNWARE_KEY ||
|
||||
process.env.BFL_API_KEY ||
|
||||
process.env.BFL_KEY ||
|
||||
""
|
||||
).trim();
|
||||
|
||||
if (!runwareApiKey) {
|
||||
if (!bflApiKey) {
|
||||
return Response.json(
|
||||
{
|
||||
code: "RUNWARE_KEY_MISSING",
|
||||
message: "未配置 Runware API Key(请在服务器环境变量或设置中配置)",
|
||||
code: "BFL_KEY_MISSING",
|
||||
message: "未配置 BFL API Key(请在服务器环境变量或设置中配置)",
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
@ -138,13 +140,20 @@ export async function GET() {
|
||||
);
|
||||
const imageSize = config.defaults?.imageSize === "square_hd" ? "square_hd" : "square";
|
||||
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();
|
||||
|
||||
return Response.json({
|
||||
...defaultConfig,
|
||||
provider: {
|
||||
type: "runware",
|
||||
type: "bfl",
|
||||
baseUrl:
|
||||
String(config.bflBaseUrl || process.env.BFL_BASE_URL || "https://api.bfl.ai").trim() ||
|
||||
"https://api.bfl.ai",
|
||||
},
|
||||
transport: {
|
||||
throttleInterval,
|
||||
@ -171,13 +180,5 @@ export async function GET() {
|
||||
? config.renderMode
|
||||
: "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",
|
||||
title: "实时画布结果",
|
||||
prompt,
|
||||
model: "fal-ai/flux-2/klein/realtime",
|
||||
model: "bfl/flux-2",
|
||||
},
|
||||
connections: [],
|
||||
});
|
||||
|
||||
@ -96,9 +96,9 @@ const defaultRealtimeConfig: RealtimeConfig = {
|
||||
jpegQuality: 0.55,
|
||||
},
|
||||
model: {
|
||||
id: "runware:101@1",
|
||||
id: "flux-2-klein-4b",
|
||||
defaults: {
|
||||
steps: 8,
|
||||
steps: 3,
|
||||
cfgScale: 7,
|
||||
strength: 0.32,
|
||||
imageSize: "square",
|
||||
@ -1414,7 +1414,7 @@ export function RealtimeCanvasDialog({
|
||||
|
||||
if (cancelled) return;
|
||||
connectionRef.current = {
|
||||
provider: "runware",
|
||||
provider: "bfl",
|
||||
modelId: resolvedConfig.model.id,
|
||||
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