feat: add Bailian image generation support

This commit is contained in:
inman
2026-07-14 12:58:50 +08:00
parent 9a3070c749
commit f642b5e71f
18 changed files with 464 additions and 74 deletions

View File

@@ -470,6 +470,7 @@ function providerLabel(provider: GenerationJob["provider"]) {
if (provider === "volcengine-visual") return "火山视觉";
if (provider === "evolink") return "EvoLink";
if (provider === "seedance") return "Seedance";
if (provider === "bailian") return "阿里云百炼";
return "Mock";
}

View File

@@ -21,7 +21,8 @@ import { extractMaterialPlaceholders } from "@/lib/prompt/material-placeholders"
type GenerateMode = "image" | "video";
type StudioMode = GenerateMode | ImageEditMode;
type MaterialKind = PromptMaterial["type"];
type ImageGenerateEngine = "jimeng" | "evolink";
type ImageGenerateEngine = "jimeng" | "evolink" | "bailian";
type VideoGenerateEngine = "seedance" | "bailian";
type MentionState = {
start: number;
@@ -154,6 +155,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: StudioMo
const [imageEngine, setImageEngine] = useState<ImageGenerateEngine>("jimeng");
const [jimengInfluence, setJimengInfluence] = useState(jimengInfluenceOptions[1].id);
const [evolinkQuality, setEvolinkQuality] = useState(evolinkQualityOptions[1].id);
const [videoEngine, setVideoEngine] = useState<VideoGenerateEngine>("seedance");
const [videoRatio, setVideoRatio] = useState("9:16");
const [videoDuration, setVideoDuration] = useState(VIDEO_DURATION_DEFAULT);
const [videoResolution, setVideoResolution] = useState("720p");
@@ -247,7 +249,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: StudioMo
.then((response) => response.ok ? response.json() : null)
.then((payload: { capabilities?: HealthCapability[] } | null) => {
const engine = payload?.capabilities?.find((capability) => capability.id === "image.generate")?.engine;
if (active && (engine === "evolink" || engine === "jimeng")) setImageEngine(engine);
if (active && (engine === "evolink" || engine === "jimeng" || engine === "bailian")) setImageEngine(engine);
})
.catch(() => undefined);
return () => {
@@ -743,6 +745,12 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: StudioMo
setNotice(null);
try {
const endpoint = generateMode === "image" ? "/api/generations/image" : "/api/generations/video";
if (generateMode === "video" && videoEngine === "bailian") {
const imageCount = materials.filter((material) => material.type === "image").length;
if (imageCount < 1 || imageCount > 2 || materials.some((material) => material.type !== "image")) {
throw new Error("百炼图生视频请上传 1 张首帧图,或按顺序上传 2 张首尾帧图。");
}
}
const body = generateMode === "image"
? {
capability: "image.generate",
@@ -757,12 +765,13 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: StudioMo
force_single: true
}
: {
engine: videoEngine,
prompt,
materials,
settings: {
ratio: videoRatio,
...(videoEngine === "seedance" ? { ratio: videoRatio } : {}),
duration: videoDuration,
resolution: videoResolution
resolution: videoEngine === "bailian" ? videoResolution.toUpperCase() : videoResolution
}
};
const response = await fetch(endpoint, {
@@ -1056,6 +1065,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: StudioMo
<select id="imageEngine" value={imageEngine} onChange={(event) => setImageEngine(normalizeImageEngine(event.target.value) || "jimeng")}>
<option value="jimeng"></option>
<option value="evolink">Image2</option>
<option value="bailian"></option>
</select>
</div>
<div className="field inline-field">
@@ -1069,11 +1079,13 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: StudioMo
</select>
</div>
<div className="field inline-field">
<label htmlFor="imageEngineTuning">{imageEngine === "evolink" ? "生成质量" : "文本影响"}</label>
<label htmlFor="imageEngineTuning">{imageEngine === "evolink" ? "生成质量" : imageEngine === "bailian" ? "生成模式" : "文本影响"}</label>
{imageEngine === "evolink" ? (
<select id="imageEngineTuning" value={evolinkQuality} onChange={(event) => setEvolinkQuality(event.target.value)}>
{evolinkQualityOptions.map((option) => <option key={option.id} value={option.id}>{option.label}</option>)}
</select>
) : imageEngine === "bailian" ? (
<select id="imageEngineTuning" value="thinking" disabled><option value="thinking"></option></select>
) : (
<select id="imageEngineTuning" value={jimengInfluence} onChange={(event) => setJimengInfluence(event.target.value)}>
{jimengInfluenceOptions.map((option) => <option key={option.id} value={option.id}>{option.label}</option>)}
@@ -1083,6 +1095,14 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: StudioMo
</>
) : (
<>
<div className="field inline-field">
<label htmlFor="videoEngine"></label>
<select id="videoEngine" value={videoEngine} onChange={(event) => setVideoEngine(event.target.value === "bailian" ? "bailian" : "seedance")}>
<option value="seedance">Seedance</option>
<option value="bailian"></option>
</select>
</div>
{videoEngine === "seedance" ? (
<div className="field inline-field">
<label htmlFor="videoRatio"></label>
<select id="videoRatio" value={videoRatio} onChange={(event) => setVideoRatio(event.target.value)}>
@@ -1091,6 +1111,7 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: StudioMo
))}
</select>
</div>
) : null}
<div className="field inline-field">
<label htmlFor="videoDuration"></label>
<select
@@ -1098,13 +1119,13 @@ export function CreateStudio({ initialMode = "image" }: { initialMode?: StudioMo
value={videoDuration}
onChange={(event) => setVideoDuration(clampVideoDuration(event.target.value, videoDuration, { allowAuto: false }))}
>
{VIDEO_DURATION_OPTIONS.map((seconds) => <option key={seconds} value={seconds}>{seconds} </option>)}
{(videoEngine === "bailian" ? Array.from({ length: 14 }, (_, index) => index + 2) : VIDEO_DURATION_OPTIONS).map((seconds) => <option key={seconds} value={seconds}>{seconds} </option>)}
</select>
</div>
<div className="field inline-field">
<label htmlFor="videoResolution"></label>
<select id="videoResolution" value={videoResolution} onChange={(event) => setVideoResolution(event.target.value)}>
{VIDEO_RESOLUTIONS.map((resolution) => <option key={resolution} value={resolution}>{resolution}</option>)}
{(videoEngine === "bailian" ? ["720p", "1080p"] : VIDEO_RESOLUTIONS).map((resolution) => <option key={resolution} value={resolution}>{resolution}</option>)}
</select>
</div>
</>
@@ -1537,12 +1558,12 @@ function sizeLabelFromSettings(width?: number, height?: number) {
}
function normalizeImageEngine(value?: string): ImageGenerateEngine | undefined {
if (value === "jimeng" || value === "evolink") return value;
if (value === "jimeng" || value === "evolink" || value === "bailian") return value;
return undefined;
}
function imageEngineLabel(engine: ImageGenerateEngine) {
return engine === "evolink" ? "Image2" : "即梦";
return engine === "evolink" ? "Image2" : engine === "bailian" ? "百炼" : "即梦";
}
function sortTemplates(a: ImageTemplate, b: ImageTemplate) {

View File

@@ -28,6 +28,7 @@ type SettingsPayload = {
visual: string;
evolink: string;
seedance: string;
bailian: string;
auth: string;
organization: string;
data: string;
@@ -226,6 +227,7 @@ export function SettingsPanel() {
<ServiceBadge label="即梦" value={payload?.modes.visual === "real" ? "真实接口" : "Mock"} ready={payload?.modes.visual === "real"} />
<ServiceBadge label="EvoLink" value={payload?.modes.evolink === "real" ? "真实接口" : "Mock"} ready={payload?.modes.evolink === "real"} />
<ServiceBadge label="Seedance" value={payload?.modes.seedance === "real" ? "真实接口" : "Mock"} ready={payload?.modes.seedance === "real"} />
<ServiceBadge label="百炼" value={payload?.modes.bailian === "real" ? "真实接口" : payload?.modes.bailian === "mock" ? "Mock" : "未配置"} ready={payload?.modes.bailian === "real"} />
<ServiceBadge label="账户" value={authModeLabel(payload?.modes.auth)} ready={payload?.modes.auth === "configured"} />
<ServiceBadge label="组织" value={payload?.modes.organization === "configured" ? "已配置" : "待配置"} ready={payload?.modes.organization === "configured"} />
<ServiceBadge label="数据层" value={payload?.modes.data === "supabase" ? "Supabase" : "本地 JSON"} ready={false} />