import { D as getEnvBool, o as logger } from "./logger-CfDSdx1q.js";
import { l as cloudConfig } from "./fetch-CHx36OdV.js";
import { l as isLoggedIntoCloud } from "./accounts-Dz-Gsg_B.js";
import { n as sha256 } from "./createHash-BWWdtTvT.js";
import { a as storeBlob, c as BLOB_SCHEME, i as recordBlobReference, s as BLOB_MIN_SIZE } from "./blobs-0KeBpJTF.js";
import { XMLParser, XMLValidator } from "fast-xml-parser";
//#region src/blobs/blobRefs.ts
const BLOB_HASH_REGEX = /^[a-f0-9]{64}$/i;
const BLOB_URI_REGEX = /promptfoo:\/\/blob\/([a-f0-9]{64})/gi;
function normalizeBlobHash(hash) {
	return hash.toLowerCase();
}
function shouldScanString(value, maxStringLength) {
	if (!value.includes("promptfoo://blob/")) return false;
	return maxStringLength === void 0 || value.startsWith("promptfoo://blob/") || value.length <= maxStringLength;
}
function extractBlobHashesFromString(value, maxStringLength) {
	if (!shouldScanString(value, maxStringLength)) return [];
	return Array.from(value.matchAll(BLOB_URI_REGEX), (match) => normalizeBlobHash(match[1]));
}
function extractBlobHashesFromValue(value, maxStringLength) {
	if (typeof value === "string") return extractBlobHashesFromString(value, maxStringLength);
	if (!value || typeof value !== "object") return [];
	const candidate = value;
	if (typeof candidate.hash === "string" && BLOB_HASH_REGEX.test(candidate.hash)) return [normalizeBlobHash(candidate.hash)];
	if (typeof candidate.uri === "string") return extractBlobHashesFromString(candidate.uri, maxStringLength);
	return [];
}
function collectBlobHashes(value, options = {}, hashes = /* @__PURE__ */ new Set(), visited = /* @__PURE__ */ new WeakSet(), depth = 0) {
	if (options.maxDepth !== void 0 && depth > options.maxDepth) return hashes;
	for (const hash of extractBlobHashesFromValue(value, options.maxStringLength)) hashes.add(hash);
	if (typeof value === "string" || !value || typeof value !== "object") return hashes;
	if (visited.has(value)) return hashes;
	visited.add(value);
	const children = Array.isArray(value) ? value : Object.values(value);
	for (const child of children) collectBlobHashes(child, options, hashes, visited, depth + 1);
	return hashes;
}
//#endregion
//#region src/blobs/remoteUpload.ts
function buildRemoteUploadTarget() {
	if (getEnvBool("PROMPTFOO_DISABLE_SHARING")) return null;
	const baseUrl = cloudConfig.getApiHost();
	const apiKey = cloudConfig.getApiKey();
	if (!baseUrl || !apiKey || !isLoggedIntoCloud()) return null;
	try {
		return {
			url: new URL("/api/blobs", baseUrl).toString(),
			apiKey
		};
	} catch (error) {
		logger.debug("[RemoteBlob] Invalid remote blob URL", {
			error: error instanceof Error ? error.message : String(error),
			baseUrl
		});
		return null;
	}
}
function shouldAttemptRemoteBlobUpload() {
	return buildRemoteUploadTarget() !== null;
}
async function uploadBlobRemote(buffer, mimeType, context) {
	const target = buildRemoteUploadTarget();
	if (!target) return null;
	try {
		const { fetchWithProxy } = await import("./fetch-CHx36OdV.js").then((n) => n.i);
		const response = await fetchWithProxy(target.url, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				Authorization: `Bearer ${target.apiKey}`
			},
			body: JSON.stringify({
				data: buffer.toString("base64"),
				mimeType,
				context
			})
		});
		if (response.status === 404 || response.status === 400) {
			logger.debug("[RemoteBlob] Remote blob upload unavailable", { status: response.status });
			return null;
		}
		if (!response.ok) {
			const text = await response.text();
			logger.debug("[RemoteBlob] Failed to upload blob", {
				status: response.status,
				statusText: response.statusText,
				body: text
			});
			return null;
		}
		const data = await response.json();
		if (!data?.ref?.hash) {
			logger.debug("[RemoteBlob] Remote upload returned malformed response");
			return null;
		}
		return data;
	} catch (error) {
		logger.debug("[RemoteBlob] Error uploading blob", { error: error instanceof Error ? error.message : String(error) });
		return null;
	}
}
//#endregion
//#region src/blobs/extractor.ts
function isDataUrl(value) {
	return /^data:(audio|image)\/[^;]+;base64,/.test(value);
}
function extractBase64(value) {
	const match = value.match(/^data:([^;]+);base64,(.+)$/);
	if (!match) return null;
	const mimeType = match[1];
	try {
		return {
			buffer: Buffer.from(match[2], "base64"),
			mimeType
		};
	} catch (error) {
		logger.warn("[BlobExtractor] Failed to parse base64 data URL", { error });
		return null;
	}
}
function shouldExternalize(buffer, minSizeBytes = BLOB_MIN_SIZE) {
	const size = buffer.length;
	return size >= minSizeBytes && size <= 52428800;
}
function getKindFromMimeType(mimeType) {
	return mimeType.startsWith("audio/") ? "audio" : "image";
}
/**
* Normalize audio format to proper MIME type.
* Some providers return just 'wav' instead of 'audio/wav'.
* @internal Exported for testing
*/
function normalizeAudioMimeType(format) {
	if (!format) return "audio/wav";
	const trimmedFormat = format.trim();
	if (/^audio\/[a-z0-9_+-]+$/i.test(trimmedFormat)) return trimmedFormat;
	const formatLower = trimmedFormat.toLowerCase();
	const mimeMap = {
		wav: "audio/wav",
		mp3: "audio/mpeg",
		ogg: "audio/ogg",
		flac: "audio/flac",
		aac: "audio/aac",
		m4a: "audio/mp4",
		webm: "audio/webm"
	};
	if (mimeMap[formatLower]) return mimeMap[formatLower];
	if (!/^[a-z0-9_-]+$/i.test(formatLower)) {
		logger.warn("[BlobExtractor] Invalid audio format, using default", { format });
		return "audio/wav";
	}
	return `audio/${formatLower}`;
}
function parseBinary(base64OrDataUrl, defaultMimeType) {
	if (isDataUrl(base64OrDataUrl)) {
		const parsed = extractBase64(base64OrDataUrl);
		if (!parsed) return null;
		return parsed;
	}
	try {
		return {
			buffer: Buffer.from(base64OrDataUrl, "base64"),
			mimeType: defaultMimeType
		};
	} catch (error) {
		logger.warn("[BlobExtractor] Failed to parse base64 data", { error });
		return null;
	}
}
async function maybeStore(base64OrDataUrl, defaultMimeType, context, location, kind, minSizeBytes = BLOB_MIN_SIZE) {
	const parsed = parseBinary(base64OrDataUrl, defaultMimeType);
	if (!parsed || !shouldExternalize(parsed.buffer, minSizeBytes)) return null;
	if (!isBlobStorageEnabled()) return null;
	const mimeType = parsed.mimeType || "application/octet-stream";
	const { ref } = await storeBlob(parsed.buffer, mimeType, {
		...context,
		location,
		kind
	});
	if (shouldAttemptRemoteBlobUpload()) uploadBlobRemote(parsed.buffer, mimeType, {
		evalId: context.evalId,
		testIdx: context.testIdx,
		promptIdx: context.promptIdx,
		location,
		kind
	}).catch((error) => {
		logger.debug("[BlobExtractor] Cloud upload failed (non-fatal)", {
			error: error instanceof Error ? error.message : String(error),
			hash: ref.hash
		});
	});
	return ref;
}
function createStoreOnce(blobContext) {
	const cache = /* @__PURE__ */ new Map();
	return async (base64OrDataUrl, defaultMimeType, location, kind, minSizeBytes) => {
		const parsed = parseBinary(base64OrDataUrl, defaultMimeType);
		if (!parsed || !shouldExternalize(parsed.buffer, minSizeBytes)) return null;
		const cacheKey = `${kind}:${parsed.buffer.toString("base64")}`;
		const existing = cache.get(cacheKey);
		if (existing) return existing;
		const pendingStore = maybeStore(base64OrDataUrl, defaultMimeType, blobContext, location, kind, minSizeBytes);
		cache.set(cacheKey, pendingStore);
		try {
			const stored = await pendingStore;
			if (!stored) cache.delete(cacheKey);
			return stored;
		} catch (error) {
			cache.delete(cacheKey);
			throw error;
		}
	};
}
function getRawSvgOutputPreview(output) {
	const trimmed = output.trim();
	if (!trimmed.startsWith("<")) return null;
	if (XMLValidator.validate(trimmed) !== true) return null;
	try {
		const parsed = new XMLParser({ ignoreAttributes: false }).parse(trimmed);
		if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || !("svg" in parsed)) return null;
	} catch {
		return null;
	}
	const buffer = Buffer.from(trimmed, "utf8");
	return {
		dataUrl: `data:image/svg+xml;base64,${buffer.toString("base64")}`,
		uri: `${BLOB_SCHEME}${sha256(buffer)}`
	};
}
function appendMetadataBlobUri(metadata, uri) {
	const existingBlobUris = Array.isArray(metadata?.blobUris) ? metadata.blobUris.filter((value) => typeof value === "string") : [];
	return {
		...metadata || {},
		blobUris: [...new Set([...existingBlobUris, uri])]
	};
}
async function storeRawSvgOutputPreview(output, metadata, storeOnce, context) {
	if (typeof output !== "string") return {
		metadata,
		mutated: false
	};
	const preview = getRawSvgOutputPreview(output);
	if (!preview) return {
		metadata,
		mutated: false
	};
	if ((Array.isArray(metadata?.blobUris) ? metadata.blobUris.filter((value) => typeof value === "string") : []).includes(preview.uri)) return {
		metadata,
		mutated: false
	};
	const stored = await storeOnce(preview.dataUrl, "image/svg+xml", "response.output", "image", 0);
	if (!stored) return {
		metadata,
		mutated: false
	};
	logger.debug("[BlobExtractor] Stored raw SVG output blob", {
		...context,
		hash: stored.hash
	});
	return {
		metadata: appendMetadataBlobUri(metadata, stored.uri),
		mutated: true
	};
}
async function externalizeDataUrls(value, storeOnce, location) {
	if (typeof value === "string") {
		if (!isDataUrl(value)) return {
			value,
			mutated: false
		};
		const parsed = extractBase64(value);
		if (!parsed) return {
			value,
			mutated: false
		};
		const storedRef = await storeOnce(value, parsed.mimeType, location, getKindFromMimeType(parsed.mimeType));
		if (!storedRef) return {
			value,
			mutated: false
		};
		return {
			value: storedRef.uri,
			mutated: true
		};
	}
	if (Array.isArray(value)) {
		let mutated = false;
		const nextValues = await Promise.all(value.map(async (item, idx) => {
			const { value: nextValue, mutated: childMutated } = await externalizeDataUrls(item, storeOnce, `${location}[${idx}]`);
			mutated ||= childMutated;
			return nextValue;
		}));
		return mutated ? {
			value: nextValues,
			mutated
		} : {
			value,
			mutated: false
		};
	}
	if (value && typeof value === "object") {
		let mutated = false;
		const nextObject = { ...value };
		for (const [key, child] of Object.entries(value)) {
			const { value: nextValue, mutated: childMutated } = await externalizeDataUrls(child, storeOnce, location ? `${location}.${key}` : key);
			if (childMutated) {
				nextObject[key] = nextValue;
				mutated = true;
			}
		}
		return mutated ? {
			value: nextObject,
			mutated: true
		} : {
			value,
			mutated: false
		};
	}
	return {
		value,
		mutated: false
	};
}
async function externalizeMetadataAudio(metadata, storeOnce) {
	if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return {
		value: metadata,
		mutated: false
	};
	const audio = metadata.audio;
	if (!audio || typeof audio !== "object" || Array.isArray(audio)) return {
		value: metadata,
		mutated: false
	};
	const audioRecord = audio;
	if (typeof audioRecord.data !== "string") return {
		value: metadata,
		mutated: false
	};
	const stored = await storeOnce(audioRecord.data, normalizeAudioMimeType(typeof audioRecord.format === "string" ? audioRecord.format : void 0), "response.metadata.audio.data", "audio");
	if (!stored) return {
		value: metadata,
		mutated: false
	};
	return {
		value: {
			...metadata,
			audio: {
				...audioRecord,
				data: void 0,
				blobRef: stored
			}
		},
		mutated: true
	};
}
/**
* Best-effort extraction of binary data from provider responses.
* Currently focuses on audio.data fields and data URL outputs.
*/
async function extractAndStoreBinaryData(response, context) {
	if (!response) return response;
	let mutated = false;
	const next = { ...response };
	const blobContext = context || {};
	const storeOnce = createStoreOnce(blobContext);
	if (response.audio?.data && typeof response.audio.data === "string") {
		const stored = await storeOnce(response.audio.data, normalizeAudioMimeType(response.audio.format), "response.audio.data", "audio");
		if (stored) {
			next.audio = {
				...response.audio,
				data: void 0,
				blobRef: stored
			};
			mutated = true;
			logger.debug("[BlobExtractor] Stored audio blob", {
				...context,
				hash: stored.hash
			});
		}
	}
	if (response.images?.length) next.images = await Promise.all(response.images.map(async (img, idx) => {
		if (!img.data || typeof img.data !== "string" || !isDataUrl(img.data)) return img;
		const stored = await storeOnce(img.data, img.mimeType || "image/png", `response.images[${idx}].data`, "image");
		if (stored) {
			mutated = true;
			logger.debug("[BlobExtractor] Stored image blob", {
				...context,
				hash: stored.hash
			});
			return {
				...img,
				data: void 0,
				blobRef: stored
			};
		}
		return img;
	}));
	const rawSvgPreview = await storeRawSvgOutputPreview(response.output, next.metadata || response.metadata, storeOnce, context);
	if (rawSvgPreview.mutated) {
		next.metadata = rawSvgPreview.metadata;
		mutated = true;
	}
	const turns = response.turns;
	if (Array.isArray(turns)) next.turns = await Promise.all(turns.map(async (turn, idx) => {
		if (turn?.audio?.data && typeof turn.audio.data === "string") {
			const stored = await storeOnce(turn.audio.data, normalizeAudioMimeType(turn.audio.format), `response.turns[${idx}].audio.data`, "audio");
			if (stored) {
				mutated = true;
				return {
					...turn,
					audio: {
						...turn.audio,
						data: void 0,
						blobRef: stored
					}
				};
			}
		}
		return turn;
	}));
	if (typeof response.output === "string" && isDataUrl(response.output)) {
		const parsed = extractBase64(response.output);
		if (parsed && shouldExternalize(parsed.buffer)) {
			const stored = await storeOnce(response.output, parsed.mimeType, "response.output", getKindFromMimeType(parsed.mimeType));
			if (stored) {
				next.output = stored.uri;
				mutated = true;
				logger.debug("[BlobExtractor] Stored output blob", {
					...context,
					hash: stored.hash
				});
			}
		}
	}
	if (typeof response.output === "string" && response.output.trim().startsWith("{") && (response.isBase64 && response.format === "json" || response.output.includes("\"b64_json\"") || response.output.includes("b64_json"))) try {
		const parsed = JSON.parse(response.output);
		if (Array.isArray(parsed.data)) {
			let jsonMutated = false;
			const storedUris = [];
			for (const item of parsed.data) if (item?.b64_json && typeof item.b64_json === "string") {
				const stored = await storeOnce(item.b64_json, "image/png", "response.output.data[].b64_json", "image");
				if (stored) {
					item.b64_json = stored.uri;
					storedUris.push(stored.uri);
					jsonMutated = true;
					mutated = true;
					logger.debug("[BlobExtractor] Stored image blob from b64_json", {
						...context,
						hash: stored.hash
					});
				}
			}
			if (jsonMutated) {
				if (storedUris.length === 1) next.output = storedUris[0];
				else if (storedUris.length > 1) next.output = JSON.stringify(storedUris);
				else next.output = JSON.stringify(parsed);
				next.metadata = {
					...response.metadata || {},
					blobUris: storedUris,
					originalFormat: response.format
				};
			}
		}
	} catch (err) {
		logger.debug("[BlobExtractor] Failed to parse base64 JSON output", {
			error: err instanceof Error ? err.message : String(err),
			location: "response.output"
		});
	}
	const metadata = next.metadata || response.metadata;
	if (metadata) {
		const { value: audioValue, mutated: audioMetadataMutated } = await externalizeMetadataAudio(metadata, storeOnce);
		const { value, mutated: dataUrlMetadataMutated } = await externalizeDataUrls(audioValue, storeOnce, "response.metadata");
		if (audioMetadataMutated || dataUrlMetadataMutated) {
			next.metadata = value;
			mutated = true;
		}
	}
	const finalResponse = mutated ? next : response;
	if (blobContext.evalId) await recordExistingBlobReferences(finalResponse, blobContext, "response");
	return finalResponse;
}
function isBlobStorageEnabled() {
	return !getEnvBool("PROMPTFOO_INLINE_MEDIA", false);
}
async function recordExistingBlobReferences(value, context, location) {
	const hashes = [...new Set(extractBlobHashesFromValue(value))];
	if (hashes.length > 0) {
		await Promise.all(hashes.map((hash) => recordBlobReference(hash, {
			...context,
			location
		})));
		return;
	}
	if (Array.isArray(value)) {
		await Promise.all(value.map((child, idx) => recordExistingBlobReferences(child, context, `${location}[${idx}]`)));
		return;
	}
	if (value && typeof value === "object") for (const [key, child] of Object.entries(value)) await recordExistingBlobReferences(child, context, location ? `${location}.${key}` : key);
}
//#endregion
export { collectBlobHashes as a, BLOB_URI_REGEX as i, isBlobStorageEnabled as n, extractBlobHashesFromValue as o, shouldAttemptRemoteBlobUpload as r, normalizeBlobHash as s, extractAndStoreBinaryData as t };

//# sourceMappingURL=extractor-BvCnG-pp.js.map