feat(platform): 创作能力整合 - 新增 Gemini Images 图片请求协议
This commit is contained in:
40
web/src/lib/gemini-image.ts
Normal file
40
web/src/lib/gemini-image.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export type GeminiImageGenerationConfig = {
|
||||
responseModalities: ["TEXT", "IMAGE"];
|
||||
imageConfig?: { aspectRatio?: string; imageSize?: "1K" | "2K" | "4K" };
|
||||
};
|
||||
|
||||
export type GeminiInlineImage = {
|
||||
mimeType: string;
|
||||
data: string;
|
||||
};
|
||||
|
||||
const GEMINI_IMAGE_SIZES: Record<string, GeminiInlineImageGenerationSize> = {
|
||||
low: "1K",
|
||||
medium: "2K",
|
||||
high: "4K",
|
||||
"1k": "1K",
|
||||
"2k": "2K",
|
||||
"4k": "4K",
|
||||
};
|
||||
|
||||
type GeminiInlineImageGenerationSize = "1K" | "2K" | "4K";
|
||||
|
||||
export function parseGeminiImageDataUrl(dataUrl: string): GeminiInlineImage {
|
||||
const match = dataUrl.trim().match(/^data:([^;,\s]+)(?:;[^,]*)?;base64,([A-Za-z0-9+/=_-]+)$/i);
|
||||
if (!match) throw new Error("参考图片读取失败:未得到有效的 image data URL");
|
||||
const mimeType = match[1].toLowerCase();
|
||||
const data = match[2].replace(/-/g, "+").replace(/_/g, "/");
|
||||
if (!mimeType.startsWith("image/")) throw new Error(`参考图片 MIME 类型无效:${mimeType}`);
|
||||
if (!data) throw new Error("参考图片读取失败:图片数据为空");
|
||||
return { mimeType, data };
|
||||
}
|
||||
|
||||
export function buildGeminiImageGenerationConfig(size?: string, quality?: string): GeminiImageGenerationConfig {
|
||||
const imageConfig: NonNullable<GeminiImageGenerationConfig["imageConfig"]> = {};
|
||||
const normalizedSize = size?.trim();
|
||||
const normalizedQuality = quality?.trim().toLowerCase();
|
||||
if (normalizedSize && normalizedSize !== "auto" && /^\d+:\d+$/.test(normalizedSize)) imageConfig.aspectRatio = normalizedSize;
|
||||
const imageSize = GEMINI_IMAGE_SIZES[normalizedQuality || ""];
|
||||
if (imageSize) imageConfig.imageSize = imageSize;
|
||||
return { responseModalities: ["TEXT", "IMAGE"], ...(Object.keys(imageConfig).length ? { imageConfig } : {}) };
|
||||
}
|
||||
@@ -77,10 +77,45 @@ export type VideoCapabilityConfig = {
|
||||
// Keep explicit pixel presets for each resolution tier so the settings panel can
|
||||
// switch between 1K, 2K and 4K without silently converting the requested ratio.
|
||||
const defaultImageSizes = [
|
||||
"auto", "1:1", "3:2", "2:3", "4:3", "3:4", "16:9", "21:9", "9:16",
|
||||
"1024x1024", "1360x1024", "1024x1360", "1536x1024", "1024x1536", "1024x1280", "1280x1024", "2048x878", "1824x1024", "1024x1824",
|
||||
"2048x2048", "2304x1728", "1728x2304", "2496x1664", "1664x2496", "1792x2240", "2240x1792", "3136x1344", "2752x1536", "1536x2752",
|
||||
"2880x2880", "3264x2448", "2448x3264", "3504x2336", "2336x3504", "2560x3200", "3200x2560", "3808x1632", "3840x2160", "2160x3840",
|
||||
"auto",
|
||||
"1:1",
|
||||
"3:2",
|
||||
"2:3",
|
||||
"4:3",
|
||||
"3:4",
|
||||
"16:9",
|
||||
"21:9",
|
||||
"9:16",
|
||||
"1024x1024",
|
||||
"1360x1024",
|
||||
"1024x1360",
|
||||
"1536x1024",
|
||||
"1024x1536",
|
||||
"1024x1280",
|
||||
"1280x1024",
|
||||
"2048x878",
|
||||
"1824x1024",
|
||||
"1024x1824",
|
||||
"2048x2048",
|
||||
"2304x1728",
|
||||
"1728x2304",
|
||||
"2496x1664",
|
||||
"1664x2496",
|
||||
"1792x2240",
|
||||
"2240x1792",
|
||||
"3136x1344",
|
||||
"2752x1536",
|
||||
"1536x2752",
|
||||
"2880x2880",
|
||||
"3264x2448",
|
||||
"2448x3264",
|
||||
"3504x2336",
|
||||
"2336x3504",
|
||||
"2560x3200",
|
||||
"3200x2560",
|
||||
"3808x1632",
|
||||
"3840x2160",
|
||||
"2160x3840",
|
||||
];
|
||||
|
||||
export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = ""): ImageCapabilityConfig {
|
||||
@@ -123,6 +158,20 @@ export function defaultImageCapabilityConfig(protocol?: ModelProtocol, model = "
|
||||
image.responseFormat.supported = false;
|
||||
image.outputFormat.supported = false;
|
||||
}
|
||||
if (protocol === "gemini-image") {
|
||||
image.references.maskSupported = false;
|
||||
// Gemini Images uses imageConfig.aspectRatio, not the OpenAI-style pixel size field.
|
||||
image.size = {
|
||||
parameter: "aspect_ratio",
|
||||
values: ["auto", "1:1", "2:3", "3:2", "3:4", "4:3", "9:16", "16:9", "21:9"],
|
||||
default: "1:1",
|
||||
allowCustom: false,
|
||||
};
|
||||
image.transparentBackground = { supported: false, default: false };
|
||||
image.responseFormat = { supported: false };
|
||||
image.outputFormat = { supported: false };
|
||||
image.maxOutputs = 4;
|
||||
}
|
||||
if (protocol !== "grok-image" && model.trim().toLowerCase().startsWith("grok-imagine-image")) {
|
||||
image.references.maxImages = 0;
|
||||
image.references.maskSupported = false;
|
||||
@@ -207,20 +256,14 @@ export function modelCapabilityConfigFor(config: { channels: Array<{ id: string;
|
||||
const cost = channel?.modelCosts?.find((item) => item.model === modelName);
|
||||
const fallback = defaultModelCapabilityConfig(cost?.protocol, modelName);
|
||||
if (!cost?.capabilityConfig) return fallback;
|
||||
const text = cost.capabilityConfig.text
|
||||
? { ...fallback.text!, ...cost.capabilityConfig.text, references: { ...fallback.text!.references, ...cost.capabilityConfig.text.references } }
|
||||
: fallback.text;
|
||||
const video = cost.capabilityConfig.video
|
||||
? { ...fallback.video!, ...cost.capabilityConfig.video, references: { ...fallback.video!.references, ...cost.capabilityConfig.video.references } }
|
||||
: fallback.video;
|
||||
const text = cost.capabilityConfig.text ? { ...fallback.text!, ...cost.capabilityConfig.text, references: { ...fallback.text!.references, ...cost.capabilityConfig.text.references } } : fallback.text;
|
||||
const video = cost.capabilityConfig.video ? { ...fallback.video!, ...cost.capabilityConfig.video, references: { ...fallback.video!.references, ...cost.capabilityConfig.video.references } } : fallback.video;
|
||||
return { ...fallback, ...cost.capabilityConfig, text, image: cost.capabilityConfig.image || fallback.image, video };
|
||||
}
|
||||
|
||||
export function normalizeImageValue(profile: ImageCapabilityConfig, value: { size?: string; quality?: string; count?: string; transparentBackground?: string }) {
|
||||
const size = normalizeImageSizeSetting(profile, value.size);
|
||||
const quality = profile.quality.supported
|
||||
? (value.quality && profile.quality.values.includes(value.quality) ? value.quality : profile.quality.default || "auto")
|
||||
: profile.quality.default || "auto";
|
||||
const quality = profile.quality.supported ? (value.quality && profile.quality.values.includes(value.quality) ? value.quality : profile.quality.default || "auto") : profile.quality.default || "auto";
|
||||
const count = String(Math.max(1, Math.min(profile.maxOutputs, Math.floor(Math.abs(Number(value.count)) || 1))));
|
||||
const transparentBackground = profile.transparentBackground.supported && value.transparentBackground === "true" ? "true" : "false";
|
||||
return { size, quality, count, transparentBackground };
|
||||
@@ -242,16 +285,16 @@ export function imageSizeRequest(profile: ImageCapabilityConfig, value?: string)
|
||||
}
|
||||
|
||||
export function normalizeVideoValue(profile: VideoCapabilityConfig, value: { seconds?: string; ratio?: string; resolution?: string }) {
|
||||
const duration = profile.duration.selection === "enum"
|
||||
? (profile.duration.values || []).includes(Number(value.seconds)) ? Number(value.seconds) : profile.duration.default
|
||||
: normalizeRangeDuration(profile, Number(value.seconds));
|
||||
const duration = profile.duration.selection === "enum" ? ((profile.duration.values || []).includes(Number(value.seconds)) ? Number(value.seconds) : profile.duration.default) : normalizeRangeDuration(profile, Number(value.seconds));
|
||||
const ratio = profile.ratios.includes(value.ratio || "") ? value.ratio! : profile.defaultRatio;
|
||||
const resolution = profile.resolutions.includes(value.resolution || "") ? value.resolution! : profile.defaultResolution;
|
||||
return { seconds: String(duration), ratio, resolution };
|
||||
}
|
||||
|
||||
export function videoResolutionRequest(profile: VideoCapabilityConfig, value: string | undefined) {
|
||||
const requested = String(value || "").trim().toLowerCase();
|
||||
const requested = String(value || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!requested || requested === "auto" || requested === "default" || requested === "medium" || requested === "high") return undefined;
|
||||
const candidates = [requested];
|
||||
if (/^\d+$/.test(requested)) candidates.push(`${requested}p`);
|
||||
|
||||
@@ -5,6 +5,7 @@ export type ModelProtocol =
|
||||
| "grok-image"
|
||||
| "volcengine-ark-image"
|
||||
| "volcengine-jimeng-image"
|
||||
| "gemini-image"
|
||||
| "openai-audio"
|
||||
| "async-audio"
|
||||
| "newapi"
|
||||
@@ -35,6 +36,7 @@ export const MODEL_PROTOCOLS: ModelProtocolDefinition[] = [
|
||||
{ value: "grok-image", label: "Grok Images", capability: "image", create: "POST /v1/images/generations / edits", contentType: "application/json", media: "文生图与单张 URL 参考图,不支持蒙版" },
|
||||
{ value: "volcengine-ark-image", label: "火山方舟图片", capability: "image", create: "POST /api/v3/images/generations", contentType: "application/json", media: "文生图与 image 参考图,不支持蒙版" },
|
||||
{ value: "volcengine-jimeng-image", label: "即梦官方图片", capability: "image", create: "POST CVSync2AsyncSubmitTask", poll: "POST CVSync2AsyncGetResult", contentType: "application/json + AK/SK 签名", media: "0-14 张参考图,模型标识填写 req_key" },
|
||||
{ value: "gemini-image", label: "Gemini Images", capability: "image", create: "POST /v1beta/models/{model}:generateContent", contentType: "application/json", media: "文生图、图片编辑与多张内嵌参考图" },
|
||||
{ value: "openai-audio", label: "OpenAI Audio", capability: "audio", create: "POST /v1/audio/speech", contentType: "application/json", media: "文本转语音" },
|
||||
{ value: "async-audio", label: "异步音频任务", capability: "audio", create: "POST /v1/audio/tasks", poll: "GET /v1/audio/tasks/{task_id}", contentType: "application/json", media: "语音、音效与音乐生成" },
|
||||
{ value: "newapi", label: "OpenAI / NewAPI Videos", capability: "video", create: "POST /v1/videos", poll: "GET /v1/videos/{task_id}", contentType: "multipart/form-data", media: "input_reference[] 参考图" },
|
||||
@@ -96,6 +98,7 @@ export function protocolForModelCatalog(endpointTypes: string[] = []): ModelProt
|
||||
const normalized = new Set(endpointTypes.map((value) => value.trim().toLowerCase()));
|
||||
if (normalized.has("openai-chat") || normalized.has("chat-completion") || normalized.has("chat")) return "chat-completion";
|
||||
if (normalized.has("openai-response") || normalized.has("responses")) return "openai-response";
|
||||
if (normalized.has("gemini-image") || normalized.has("gemini-images")) return "gemini-image";
|
||||
if (normalized.has("openai-image") || normalized.has("image")) return "openai-image";
|
||||
if (normalized.has("openai-video") || normalized.has("video")) return "newapi-channel-2";
|
||||
if (normalized.has("openai-audio") || normalized.has("audio")) return "openai-audio";
|
||||
|
||||
Reference in New Issue
Block a user