| """Upload a generated dataset directly to an Edge Impulse project. |
| |
| Uses the Edge Impulse ingestion REST API with a project API key |
| (Project → Dashboard → Keys). WAV filenames use the ``label.<id>.wav`` |
| convention, so Edge Impulse auto-assigns labels from the filename prefix. |
| |
| Ingestion endpoints: |
| POST https://ingestion.edgeimpulse.com/api/training/files |
| POST https://ingestion.edgeimpulse.com/api/testing/files |
| """ |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass, field |
| from pathlib import Path |
| from typing import Callable, List, Optional |
|
|
| import requests |
|
|
| _INGESTION_URL = "https://ingestion.edgeimpulse.com/api/{category}/files" |
| _TIMEOUT = 60 |
| _BATCH_SIZE = 25 |
|
|
| ProgressFn = Callable[[str], None] |
|
|
|
|
| @dataclass |
| class UploadResult: |
| uploaded: int = 0 |
| failed: int = 0 |
| errors: List[str] = field(default_factory=list) |
|
|
|
|
| def verify_api_key(api_key: str) -> bool: |
| """Return True if the ingestion API accepts the key. |
| |
| A key with no files still returns a non-auth error, so we treat any |
| non-401/403 response as "key looks usable". |
| """ |
| if not api_key or not api_key.strip(): |
| return False |
| try: |
| response = requests.post( |
| _INGESTION_URL.format(category="training"), |
| headers={"x-api-key": api_key.strip()}, |
| timeout=_TIMEOUT, |
| ) |
| except requests.RequestException: |
| return False |
| return response.status_code not in (401, 403) |
|
|
|
|
| def _upload_batch( |
| category: str, |
| api_key: str, |
| wav_paths: List[Path], |
| allow_duplicates: bool, |
| ) -> tuple[int, Optional[str]]: |
| headers = {"x-api-key": api_key} |
| if not allow_duplicates: |
| headers["x-disallow-duplicates"] = "1" |
|
|
| files = [] |
| handles = [] |
| try: |
| for path in wav_paths: |
| handle = path.open("rb") |
| handles.append(handle) |
| files.append(("data", (path.name, handle, "audio/wav"))) |
| response = requests.post( |
| _INGESTION_URL.format(category=category), |
| headers=headers, |
| files=files, |
| timeout=_TIMEOUT, |
| ) |
| finally: |
| for handle in handles: |
| handle.close() |
|
|
| if response.status_code == 200: |
| return len(wav_paths), None |
| return 0, f"HTTP {response.status_code}: {response.text[:200]}" |
|
|
|
|
| def upload_dataset( |
| dataset_dir: str, |
| api_key: str, |
| allow_duplicates: bool = False, |
| progress: Optional[ProgressFn] = None, |
| ) -> UploadResult: |
| """Upload the ``edge_impulse_upload/{training,testing}`` WAVs to a project.""" |
| def log(message: str) -> None: |
| if progress: |
| progress(message) |
| else: |
| print(message) |
|
|
| api_key = (api_key or "").strip() |
| if not api_key: |
| raise ValueError("An Edge Impulse API key is required to upload.") |
|
|
| base = Path(dataset_dir) / "edge_impulse_upload" |
| result = UploadResult() |
|
|
| for src_split, category in (("training", "training"), ("testing", "testing")): |
| split_dir = base / src_split |
| if not split_dir.exists(): |
| continue |
| wavs = sorted(split_dir.glob("*.wav")) |
| if not wavs: |
| continue |
|
|
| log(f"Uploading {len(wavs)} {category} file(s) to Edge Impulse...") |
| for start in range(0, len(wavs), _BATCH_SIZE): |
| batch = wavs[start:start + _BATCH_SIZE] |
| uploaded, error = _upload_batch(category, api_key, batch, allow_duplicates) |
| if error is None: |
| result.uploaded += uploaded |
| log(f" {category}: {result.uploaded} uploaded") |
| else: |
| result.failed += len(batch) |
| result.errors.append(f"{category} batch @ {start}: {error}") |
| log(f" WARNING: {category} batch failed: {error}") |
|
|
| log(f"Edge Impulse upload complete: {result.uploaded} uploaded, {result.failed} failed.") |
| return result |
|
|