Spaces:
Sleeping
Sleeping
File size: 9,508 Bytes
0269f70 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 | import type { Dataset, DataItem, Modality, SingleComparisonResult, DatasetMetadata } from '../types';
// Define the base URL for your backend API
// For local development, it might be 'http://localhost:8000'
// When deployed on Hugging Face Spaces, it will be a relative path '/'.
const API_BASE_URL = '/';
/**
* A helper function to handle API errors.
*/
const handleApiError = async (response: Response) => {
if (!response.ok) {
let errorMessage = `HTTP error! status: ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData.detail || JSON.stringify(errorData);
} catch (e) {
// The response was not JSON
errorMessage = await response.text();
}
throw new Error(errorMessage);
}
return response.json();
};
// Helper to correctly encode unicode strings to base64, which is required by the backend.
const unicodeToBase64 = (str: string) => {
return btoa(
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, (match, p1) => {
return String.fromCharCode(parseInt(p1, 16));
})
);
};
const contentToBase64 = (content: string | ArrayBuffer, modality: Modality): Promise<string> => {
return new Promise((resolve, reject) => {
if (modality === 'text') {
try {
// Use helper for proper unicode support
resolve(unicodeToBase64(content as string));
} catch (error) {
console.error("Failed to Base64 encode text content:", error);
reject(new Error("Failed to encode text. Ensure it doesn't contain unsupported characters."));
}
} else if (typeof content === 'string') {
// For images, content is a data URL
const parts = content.split(',');
resolve(parts.length > 1 ? parts[1] : content);
} else if (content instanceof ArrayBuffer) {
// For meshes
const bytes = new Uint8Array(content);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
resolve(btoa(binary));
} else {
reject(new Error('Unsupported content type for base64 conversion.'));
}
});
};
/**
* Post-processes data received from the backend to ensure correct frontend rendering.
* - Converts raw Base64 image strings to Data URLs.
* - Converts raw Base64 mesh strings to ArrayBuffers.
*/
const postProcessApiData = <T extends { content: string | ArrayBuffer }>(item: T, modality: 'images' | 'texts' | 'meshes'): T => {
if (modality === 'images' && typeof item.content === 'string' && !item.content.startsWith('data:')) {
item.content = `data:image/png;base64,${item.content}`;
}
if (modality === 'meshes' && typeof item.content === 'string') {
const binaryString = atob(item.content);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
item.content = bytes.buffer;
}
return item;
}
/**
* Starts the dataset processing on the backend by uploading a .zip file.
* @param file The .zip file to upload.
* @returns A promise that resolves with a job ID for polling the status.
*/
export const startDatasetProcessing = async (file: File): Promise<string> => {
const formData = new FormData();
formData.append('file', file);
const response = await fetch(`${API_BASE_URL}api/process-dataset`, {
method: 'POST',
body: formData,
});
const { job_id } = await handleApiError(response);
if (!job_id) {
throw new Error("API did not return a job ID.");
}
return job_id;
};
interface ProcessingStatus {
status: 'starting' | 'processing' | 'complete' | 'error';
stage?: string;
progress?: number;
message?: string;
result?: Dataset;
}
/**
* Polls the backend for the status of a dataset processing job.
* @param jobId The ID of the job to check.
* @returns A promise that resolves with the current status.
*/
export const getProcessingStatus = async (jobId: string): Promise<ProcessingStatus> => {
const response = await fetch(`${API_BASE_URL}api/processing-status/${jobId}`);
const status: ProcessingStatus = await handleApiError(response);
// If the job is complete, post-process the resulting dataset data
if (status.status === 'complete' && status.result) {
const processedDataset = status.result;
// The backend returns a string for the date, convert it to a Date object.
if (processedDataset.uploadDate && typeof processedDataset.uploadDate === 'string') {
processedDataset.uploadDate = new Date(processedDataset.uploadDate);
}
// Ensure all data items have the correct format for frontend rendering.
if (processedDataset.data) {
if (processedDataset.data.images) {
processedDataset.data.images = processedDataset.data.images.map((item: DataItem) => postProcessApiData(item, 'images'));
}
if (processedDataset.data.meshes) {
processedDataset.data.meshes = processedDataset.data.meshes.map((item: DataItem) => postProcessApiData(item, 'meshes'));
}
}
status.result = processedDataset;
}
return status;
}
/**
* Sends a local dataset to the backend to populate its in-memory cache.
* This is crucial for making comparisons after a page reload.
* @param dataset The full local dataset object from IndexedDB.
*/
export const ensureDatasetInCache = async (dataset: Dataset): Promise<void> => {
// The backend expects content as base64 or raw text, but our Mesh content is an ArrayBuffer.
// We need to convert it before sending. Images are already data URLs (string).
const payload = {
...dataset,
data: {
...dataset.data,
meshes: await Promise.all(dataset.data.meshes.map(async (mesh) => {
if (mesh.content instanceof ArrayBuffer) {
return { ...mesh, content: await contentToBase64(mesh.content, 'mesh') };
}
return mesh;
})),
}
};
const response = await fetch(`${API_BASE_URL}api/cache-local-dataset`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
await handleApiError(response);
};
/**
* Finds the top matches for a single item by querying the backend.
* @param sourceItem The item to find matches for.
* @param sourceModality The modality of the source item.
* @param datasetId The ID of the dataset to search within.
* @returns A promise that resolves with the comparison results.
*/
export const findTopMatches = async (
sourceItem: DataItem,
sourceModality: Modality,
datasetId: string
): Promise<SingleComparisonResult> => {
const contentAsBase64 = await contentToBase64(sourceItem.content, sourceModality);
const requestBody = {
modality: sourceModality,
content: contentAsBase64,
dataset_id: datasetId,
};
const response = await fetch(`${API_BASE_URL}api/find-matches`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestBody),
});
const result: SingleComparisonResult = await handleApiError(response);
// The API returns a representation of the source item with raw base64.
// We replace it with our original source item which has the correct format for rendering.
result.sourceItem = sourceItem;
// Post-process all returned match items to ensure they render correctly.
for (const key of Object.keys(result.results)) {
const modalityKey = key as 'images' | 'texts' | 'meshes';
const matches = result.results[modalityKey];
if (matches) {
matches.forEach(match => {
postProcessApiData(match.item, modalityKey);
});
}
}
return result;
};
// --- Service functions for SHARED datasets ---
/**
* Returns the metadata for all available shared datasets by querying the backend.
*/
export const getSharedDatasetMetadata = async (): Promise<DatasetMetadata[]> => {
try {
const response = await fetch('/api/shared-dataset-metadata');
const metadataList = await handleApiError(response);
// The backend returns strings for dates, convert them to Date objects.
return metadataList.map((meta: any) => ({
...meta,
uploadDate: new Date(meta.uploadDate),
}));
} catch (error) {
console.error("Failed to fetch shared dataset metadata:", error);
// Re-throw the error so the UI layer can handle it.
throw error;
}
};
/**
* Returns the full data structure for a specific shared dataset from the backend.
* The content for each item remains null, only the URLs are provided.
*/
export const getSharedDataset = async (id: string): Promise<Dataset | null> => {
try {
const response = await fetch(`/api/shared-dataset?id=${id}`);
const dataset = await handleApiError(response);
// Convert date string from API to Date object
dataset.uploadDate = new Date(dataset.uploadDate);
return dataset;
} catch (error) {
console.error(`Failed to fetch shared dataset with id ${id}:`, error);
// Re-throw the error so the UI layer can handle it.
throw error;
}
};
|