You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
270 lines
8.4 KiB
270 lines
8.4 KiB
import type { GenerationInput, ModelCapability } from "@/lib/providers/types";
|
|
import { modelSupportsReferenceMedia } from "@/utils/generationInputSupport";
|
|
|
|
export type PopiModelDetailForTaskPayload = {
|
|
id?: number;
|
|
name?: string;
|
|
code?: string;
|
|
aiModelCodeAlias?: string;
|
|
};
|
|
|
|
type ReferenceSubject = {
|
|
id: number;
|
|
type: "image" | "video" | "audio";
|
|
url: string;
|
|
name: string;
|
|
duration?: number;
|
|
};
|
|
|
|
const POPISERVER_LEGACY_AUDIO_KEYS = [
|
|
"audio",
|
|
"audios",
|
|
"audio_url",
|
|
"audio_urls",
|
|
"reference_audio",
|
|
"reference_audio_url",
|
|
"reference_audio_urls",
|
|
];
|
|
|
|
function asString(value: unknown): string | null {
|
|
return typeof value === "string" && value.length > 0 ? value : null;
|
|
}
|
|
|
|
function asNumber(value: unknown): number | null {
|
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
if (typeof value === "string" && value.trim().length > 0) {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getCapabilityMediaType(capabilities: ModelCapability[]): "image" | "video" | "audio" {
|
|
if (capabilities.some((capability) => capability.includes("audio"))) return "audio";
|
|
if (capabilities.some((capability) => capability.includes("video"))) return "video";
|
|
return "image";
|
|
}
|
|
|
|
function inferTaskType(input: GenerationInput): number {
|
|
const metadataType = asNumber(input.model.metadata?.type);
|
|
if (metadataType) return metadataType;
|
|
const mediaType = getCapabilityMediaType(input.model.capabilities);
|
|
if (mediaType === "video") return 2;
|
|
if (mediaType === "audio") return 3;
|
|
return 1;
|
|
}
|
|
|
|
function inferTaskSubType(input: GenerationInput, type: number): number {
|
|
const metadataSubType = asNumber(input.model.metadata?.subType);
|
|
if (metadataSubType) return metadataSubType;
|
|
const metadataSubTypes = input.model.metadata?.subTypes;
|
|
if (Array.isArray(metadataSubTypes)) {
|
|
const first = metadataSubTypes.map(asNumber).find((value): value is number => value !== null);
|
|
if (first) return first;
|
|
}
|
|
|
|
if (type === 3) return 301;
|
|
if (type === 2) return 203;
|
|
return (input.images?.length ?? 0) > 0 ? 103 : 102;
|
|
}
|
|
|
|
function resolveAspectRatio(parameters: Record<string, unknown> | undefined): string {
|
|
return asString(parameters?.ratio) ?? asString(parameters?.aspectRatio) ?? asString(parameters?.aspect_ratio) ?? "";
|
|
}
|
|
|
|
function needsPromptEmbeddedParameters(modelCode: string, modelAlias: string): boolean {
|
|
return (
|
|
modelCode === "gpt-image-2-vip" ||
|
|
modelCode === "gpt-image-2-all" ||
|
|
modelAlias === "gpt-image-2-vip" ||
|
|
modelAlias === "gpt-image-2-all"
|
|
);
|
|
}
|
|
|
|
function buildPopiChatPrompt(
|
|
prompt: string,
|
|
parameters: Record<string, unknown>,
|
|
aspectRatio: string,
|
|
modelCode: string,
|
|
modelAlias: string
|
|
): string {
|
|
if (!needsPromptEmbeddedParameters(modelCode, modelAlias)) return prompt;
|
|
|
|
const resolution = asString(parameters.resolution) ?? "1K";
|
|
return [
|
|
`比例:${aspectRatio}`,
|
|
`清晰度:${resolution}`,
|
|
`提示词:${prompt}`,
|
|
].join("\n");
|
|
}
|
|
|
|
function normalizeReferenceSubjectName(name: string): string {
|
|
return name.replace(/^@+/, "");
|
|
}
|
|
|
|
function referenceSubjectMetadata(referenceSubjectList: unknown): ReferenceSubject[] {
|
|
if (!Array.isArray(referenceSubjectList)) return [];
|
|
|
|
return referenceSubjectList.flatMap((item) => {
|
|
if (!item || typeof item !== "object" || Array.isArray(item)) return [];
|
|
const subject = item as Record<string, unknown>;
|
|
const type =
|
|
subject.type === "video"
|
|
? "video"
|
|
: subject.type === "image"
|
|
? "image"
|
|
: subject.type === "audio"
|
|
? "audio"
|
|
: null;
|
|
if (!type) return [];
|
|
const id = asNumber(subject.id);
|
|
const name = asString(subject.name);
|
|
const url = asString(subject.url);
|
|
const duration = asNumber(subject.duration);
|
|
return [{
|
|
id: id ?? 0,
|
|
type,
|
|
url: url ?? "",
|
|
name: normalizeReferenceSubjectName(name ?? ""),
|
|
...(duration !== null ? { duration } : {}),
|
|
}];
|
|
});
|
|
}
|
|
|
|
function removeLegacyAudioParameters(parameters: Record<string, unknown>): Record<string, unknown> {
|
|
return Object.fromEntries(
|
|
Object.entries(parameters).filter(
|
|
([key]) => !POPISERVER_LEGACY_AUDIO_KEYS.includes(key) && key !== "type" && key !== "subType"
|
|
)
|
|
);
|
|
}
|
|
|
|
function buildReferenceSubjectList(
|
|
images: string[],
|
|
videos: string[],
|
|
voices: string[],
|
|
referenceSubjectList: unknown
|
|
): ReferenceSubject[] {
|
|
const metadata = referenceSubjectMetadata(referenceSubjectList);
|
|
const metadataByType = {
|
|
image: metadata.filter((item) => item.type === "image"),
|
|
video: metadata.filter((item) => item.type === "video"),
|
|
audio: metadata.filter((item) => item.type === "audio"),
|
|
};
|
|
const subjects: ReferenceSubject[] = [];
|
|
|
|
images.forEach((url, index) => {
|
|
const id = subjects.length + 1;
|
|
const meta = metadataByType.image[index];
|
|
subjects.push({
|
|
id,
|
|
type: "image",
|
|
url,
|
|
name: normalizeReferenceSubjectName(meta?.name || `图${id}`),
|
|
});
|
|
});
|
|
|
|
videos.forEach((url, index) => {
|
|
const id = subjects.length + 1;
|
|
const meta = metadataByType.video[index];
|
|
subjects.push({
|
|
id,
|
|
type: "video",
|
|
url,
|
|
name: normalizeReferenceSubjectName(meta?.name || `视频${id}`),
|
|
...(meta?.duration ? { duration: meta.duration } : {}),
|
|
});
|
|
});
|
|
|
|
voices.forEach((url, index) => {
|
|
const id = subjects.length + 1;
|
|
const meta = metadataByType.audio[index];
|
|
subjects.push({
|
|
id,
|
|
type: "audio",
|
|
url,
|
|
name: normalizeReferenceSubjectName(meta?.name || `音频${id}`),
|
|
});
|
|
});
|
|
|
|
return subjects;
|
|
}
|
|
|
|
export function buildPopiTaskBody(
|
|
input: GenerationInput,
|
|
modelDetail: PopiModelDetailForTaskPayload | null
|
|
): Record<string, unknown> {
|
|
const rawParameters = removeLegacyAudioParameters(input.parameters || {});
|
|
const aspectRatio = resolveAspectRatio(rawParameters);
|
|
const { referenceSubjectList: _referenceSubjectList, ...parameters } = rawParameters;
|
|
const selectedModel = {
|
|
provider: "popiserver" as const,
|
|
modelId: input.model.id,
|
|
displayName: input.model.name,
|
|
capabilities: input.model.capabilities,
|
|
metadata: input.model.metadata,
|
|
};
|
|
const type = asNumber(input.type) ?? inferTaskType(input);
|
|
const subType = asNumber(input.subType) ?? inferTaskSubType(input, type);
|
|
const modelCode =
|
|
asString(input.model.metadata?.aiModelCode) ??
|
|
modelDetail?.code ??
|
|
input.model.id;
|
|
const modelAlias =
|
|
asString(input.model.metadata?.aiModelCodeAlias) ??
|
|
modelDetail?.aiModelCodeAlias ??
|
|
modelCode;
|
|
const aiModelId =
|
|
asNumber(input.model.metadata?.aiModelId) ??
|
|
asNumber(input.model.id) ??
|
|
modelDetail?.id;
|
|
const images = input.images || [];
|
|
const videos = modelSupportsReferenceMedia(selectedModel, "video", rawParameters)
|
|
? input.videos || []
|
|
: [];
|
|
const voices = modelSupportsReferenceMedia(selectedModel, "audio", rawParameters)
|
|
? input.voices || []
|
|
: [];
|
|
const referenceSubjectList = type === 1 || type === 2
|
|
? buildReferenceSubjectList([...new Set(images)], [...new Set(videos)], [...new Set(voices)], input.referenceSubjectList)
|
|
: [];
|
|
const voiceId =
|
|
asString(input.audioVoiceId) ??
|
|
asString(parameters.voiceId) ??
|
|
asString(parameters.voice_id) ??
|
|
"";
|
|
|
|
const commonBody = {
|
|
type,
|
|
chatPrompt: buildPopiChatPrompt(input.prompt, parameters, aspectRatio, modelCode, modelAlias),
|
|
origin: "canvas",
|
|
...(asString(parameters.model) ? { model: asString(parameters.model) } : {}),
|
|
...(asString(parameters.aiPlatform) ? { aiPlatform: asString(parameters.aiPlatform) } : {}),
|
|
aiModelName: asString(parameters.aiModelName) ?? input.model.name ?? modelDetail?.name ?? modelCode,
|
|
aiModelId,
|
|
aiModelCode: modelCode,
|
|
aiModelCodeAlias: modelAlias,
|
|
subType,
|
|
...(asNumber(input.batchSize) !== null || asNumber(parameters.batchSize) !== null
|
|
? { batchSize: asNumber(input.batchSize) ?? asNumber(parameters.batchSize) }
|
|
: {}),
|
|
};
|
|
|
|
if (type === 3) {
|
|
return {
|
|
...parameters,
|
|
...commonBody,
|
|
...(voices.length > 0 ? { voices: [...new Set(voices)] } : {}),
|
|
...(voiceId ? { voiceId } : {}),
|
|
};
|
|
}
|
|
|
|
return {
|
|
...parameters,
|
|
...commonBody,
|
|
...(images.length > 0 ? { images: [...new Set(images)] } : {}),
|
|
...(videos.length > 0 ? { videos: [...new Set(videos)] } : {}),
|
|
...(voices.length > 0 ? { voices: [...new Set(voices)] } : {}),
|
|
...(referenceSubjectList.length > 0 ? { referenceSubjectList } : {}),
|
|
};
|
|
}
|
|
|