|
|
|
@ -39,6 +39,22 @@ function dataUrlToBlob(dataUrl: string): Blob { |
|
|
|
return new Blob([bytes], { type: mimeType }); |
|
|
|
} |
|
|
|
|
|
|
|
async function localMediaToBlob(media: string): Promise<Blob> { |
|
|
|
if (media.startsWith("data:")) { |
|
|
|
return dataUrlToBlob(media); |
|
|
|
} |
|
|
|
|
|
|
|
if (media.startsWith("blob:")) { |
|
|
|
const response = await fetch(media); |
|
|
|
if (!response.ok) { |
|
|
|
throw new Error(`Failed to read local media blob: HTTP ${response.status}`); |
|
|
|
} |
|
|
|
return response.blob(); |
|
|
|
} |
|
|
|
|
|
|
|
throw new Error("Unsupported local media URL"); |
|
|
|
} |
|
|
|
|
|
|
|
function shouldUploadTemporaryMedia(media: string): boolean { |
|
|
|
return ( |
|
|
|
(media.startsWith("data:image/") || media.startsWith("data:video/")) && |
|
|
|
@ -46,6 +62,15 @@ function shouldUploadTemporaryMedia(media: string): boolean { |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
function shouldUploadPopiserverMedia(media: string): boolean { |
|
|
|
return ( |
|
|
|
media.startsWith("data:image/") || |
|
|
|
media.startsWith("data:video/") || |
|
|
|
media.startsWith("data:audio/") || |
|
|
|
media.startsWith("blob:") |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
function extensionFromMime(mimeType: string | null): string { |
|
|
|
if (mimeType === "image/jpeg") return "jpg"; |
|
|
|
if (mimeType === "image/webp") return "webp"; |
|
|
|
@ -54,6 +79,12 @@ function extensionFromMime(mimeType: string | null): string { |
|
|
|
if (mimeType === "video/webm") return "webm"; |
|
|
|
if (mimeType === "video/quicktime") return "mov"; |
|
|
|
if (mimeType?.startsWith("video/")) return "mp4"; |
|
|
|
if (mimeType === "audio/mpeg" || mimeType === "audio/mp3") return "mp3"; |
|
|
|
if (mimeType === "audio/wav" || mimeType === "audio/x-wav") return "wav"; |
|
|
|
if (mimeType === "audio/ogg") return "ogg"; |
|
|
|
if (mimeType === "audio/flac") return "flac"; |
|
|
|
if (mimeType === "audio/aac") return "aac"; |
|
|
|
if (mimeType?.startsWith("audio/")) return "mp3"; |
|
|
|
return "png"; |
|
|
|
} |
|
|
|
|
|
|
|
@ -84,6 +115,28 @@ async function uploadTemporaryMedia(media: string): Promise<{ url: string; id: s |
|
|
|
return { url: sameOriginUrl, id: result.id }; |
|
|
|
} |
|
|
|
|
|
|
|
async function uploadPopiserverMedia(media: string): Promise<string> { |
|
|
|
const blob = await localMediaToBlob(media); |
|
|
|
const mimeType = dataUrlMimeType(media) || blob.type || null; |
|
|
|
const formData = new FormData(); |
|
|
|
formData.append("file", blob, `reference.${extensionFromMime(mimeType)}`); |
|
|
|
|
|
|
|
const response = await fetch("/api/popi/media/upload", { |
|
|
|
method: "POST", |
|
|
|
body: formData, |
|
|
|
}); |
|
|
|
|
|
|
|
const result = await response.json().catch(() => null) as |
|
|
|
| { success?: boolean; url?: string; error?: string } |
|
|
|
| null; |
|
|
|
|
|
|
|
if (!response.ok || !result?.success || !result.url) { |
|
|
|
throw new Error(result?.error || "Failed to upload media to PopiServer"); |
|
|
|
} |
|
|
|
|
|
|
|
return result.url; |
|
|
|
} |
|
|
|
|
|
|
|
export async function uploadTemporaryMediaForGeneration( |
|
|
|
mediaItems: string[] |
|
|
|
): Promise<UploadedTemporaryMedia> { |
|
|
|
@ -108,6 +161,27 @@ export async function uploadTemporaryImagesForGeneration( |
|
|
|
return { images: prepared.media, cleanupIds: prepared.cleanupIds }; |
|
|
|
} |
|
|
|
|
|
|
|
export async function uploadPopiserverMediaForGeneration( |
|
|
|
mediaItems: string[] |
|
|
|
): Promise<UploadedTemporaryMedia> { |
|
|
|
const preparedMedia = await Promise.all( |
|
|
|
mediaItems.map(async (media) => { |
|
|
|
if (!shouldUploadPopiserverMedia(media)) return media; |
|
|
|
return uploadPopiserverMedia(media); |
|
|
|
}) |
|
|
|
); |
|
|
|
|
|
|
|
return { media: preparedMedia, cleanupIds: [] }; |
|
|
|
} |
|
|
|
|
|
|
|
export async function uploadPopiserverImagesForGeneration( |
|
|
|
images: string[] |
|
|
|
): Promise<UploadedTemporaryImages> { |
|
|
|
const prepared = await uploadPopiserverMediaForGeneration(images); |
|
|
|
|
|
|
|
return { images: prepared.media, cleanupIds: [] }; |
|
|
|
} |
|
|
|
|
|
|
|
export async function uploadTemporaryDynamicInputsForGeneration( |
|
|
|
dynamicInputs: Record<string, string | string[]> |
|
|
|
): Promise<UploadedTemporaryDynamicInputs> { |
|
|
|
@ -141,6 +215,33 @@ export async function uploadTemporaryDynamicInputsForGeneration( |
|
|
|
return { dynamicInputs: Object.fromEntries(entries), cleanupIds: [...new Set(cleanupIds)] }; |
|
|
|
} |
|
|
|
|
|
|
|
export async function uploadPopiserverDynamicInputsForGeneration( |
|
|
|
dynamicInputs: Record<string, string | string[]> |
|
|
|
): Promise<UploadedTemporaryDynamicInputs> { |
|
|
|
const uploadCache = new Map<string, Promise<string>>(); |
|
|
|
const prepareMedia = async (media: string): Promise<string> => { |
|
|
|
if (!shouldUploadPopiserverMedia(media)) return media; |
|
|
|
let upload = uploadCache.get(media); |
|
|
|
if (!upload) { |
|
|
|
upload = uploadPopiserverMedia(media); |
|
|
|
uploadCache.set(media, upload); |
|
|
|
} |
|
|
|
return upload; |
|
|
|
}; |
|
|
|
|
|
|
|
const entries = await Promise.all( |
|
|
|
Object.entries(dynamicInputs).map(async ([key, value]) => { |
|
|
|
if (typeof value === "string") { |
|
|
|
return [key, await prepareMedia(value)] as const; |
|
|
|
} |
|
|
|
|
|
|
|
return [key, await Promise.all(value.map(prepareMedia))] as const; |
|
|
|
}) |
|
|
|
); |
|
|
|
|
|
|
|
return { dynamicInputs: Object.fromEntries(entries), cleanupIds: [] }; |
|
|
|
} |
|
|
|
|
|
|
|
export async function cleanupTemporaryImages(ids: string[]): Promise<void> { |
|
|
|
if (ids.length === 0) return; |
|
|
|
|
|
|
|
|