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

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 } : {}),
};
}