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