Spaces:
Sleeping
Sleeping
| """ | |
| image_fetcher.py β Scrapes real images from free public sources (no API keys). | |
| Sources: | |
| - Wikipedia Commons API (millions of free labeled images) | |
| - NASA Image API (space, science β completely free) | |
| - Lorem Picsum / Unsplash Source (nature, people, tech) | |
| - Open Notify / public news RSS image tags | |
| Images are saved to ./image_data/<category>/<filename>.jpg | |
| A manifest file image_manifest.jsonl tracks everything downloaded. | |
| """ | |
| import os | |
| import json | |
| import time | |
| import hashlib | |
| import requests | |
| import threading | |
| from datetime import datetime, UTC | |
| from pathlib import Path | |
| from collections import defaultdict | |
| # ββ CONFIG ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| IMAGE_DIR = Path('image_data') | |
| MANIFEST_FILE = 'image_manifest.jsonl' | |
| MAX_PER_CAT = 500 # max images stored per category | |
| IMG_SIZE = (224, 224) # resize target for training | |
| CATEGORIES = [ | |
| 'nature', 'technology', 'science', 'people', | |
| 'animals', 'food', 'sports', 'architecture' | |
| ] | |
| HEADERS = {'User-Agent': 'LivingNeuralNetwork/1.0 (educational project)'} | |
| # ββ WIKIPEDIA COMMONS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| WIKI_SEARCH_TERMS = { | |
| 'nature': ['forest','ocean','mountain','river','sky','desert','waterfall'], | |
| 'technology': ['computer','robot','satellite','circuit','smartphone','laboratory'], | |
| 'science': ['microscope','telescope','dna','chemistry','physics experiment'], | |
| 'people': ['portrait','crowd','family','athlete','scientist','engineer'], | |
| 'animals': ['lion','eagle','dolphin','elephant','butterfly','wolf','deer'], | |
| 'food': ['fruit','vegetable','bread','cooking','meal','market food'], | |
| 'sports': ['football','basketball','swimming','running','cycling','tennis'], | |
| 'architecture': ['bridge','skyscraper','cathedral','temple','stadium','museum'], | |
| } | |
| def fetch_wikipedia_images(category: str, term: str, max_imgs: int = 5) -> list: | |
| """Fetch image URLs from Wikipedia Commons for a search term.""" | |
| results = [] | |
| try: | |
| url = 'https://en.wikipedia.org/w/api.php' | |
| params = { | |
| 'action': 'query', | |
| 'generator': 'search', | |
| 'gsrsearch': f'file:{term}', | |
| 'gsrnamespace': 6, | |
| 'prop': 'imageinfo', | |
| 'iiprop': 'url|size|mime', | |
| 'iiurlwidth': 224, | |
| 'gsrlimit': max_imgs, | |
| 'format': 'json', | |
| } | |
| r = requests.get(url, params=params, headers=HEADERS, timeout=10) | |
| r.raise_for_status() | |
| pages = r.json().get('query', {}).get('pages', {}) | |
| for page in pages.values(): | |
| info = page.get('imageinfo', [{}])[0] | |
| thumb_url = info.get('thumburl') or info.get('url', '') | |
| mime = info.get('mime', '') | |
| if thumb_url and 'image' in mime: | |
| results.append({ | |
| 'url': thumb_url, | |
| 'category': category, | |
| 'source': 'wikipedia_commons', | |
| 'term': term, | |
| }) | |
| except Exception: | |
| pass | |
| return results | |
| def fetch_nasa_images(max_imgs: int = 10) -> list: | |
| """Fetch images from NASA's free image API.""" | |
| results = [] | |
| nasa_terms = ['nebula', 'galaxy', 'astronaut', 'rocket', 'mars', 'earth from space'] | |
| try: | |
| term = nasa_terms[int(time.time()) % len(nasa_terms)] | |
| r = requests.get( | |
| 'https://images-api.nasa.gov/search', | |
| params={'q': term, 'media_type': 'image', 'page_size': max_imgs}, | |
| headers=HEADERS, timeout=10 | |
| ) | |
| r.raise_for_status() | |
| items = r.json().get('collection', {}).get('items', []) | |
| for item in items: | |
| links = item.get('links', []) | |
| for link in links: | |
| if link.get('rel') == 'preview': | |
| results.append({ | |
| 'url': link['href'], | |
| 'category': 'science', | |
| 'source': 'nasa', | |
| 'term': term, | |
| }) | |
| break | |
| except Exception: | |
| pass | |
| return results | |
| def fetch_openverse_images(category: str, term: str, max_imgs: int = 5) -> list: | |
| """ | |
| Openverse is a Creative Commons image search API β completely free, no key needed. | |
| https://api.openverse.org | |
| """ | |
| results = [] | |
| try: | |
| r = requests.get( | |
| 'https://api.openverse.org/v1/images/', | |
| params={'q': term, 'page_size': max_imgs, 'license_type': 'commercial,modification'}, | |
| headers=HEADERS, timeout=10 | |
| ) | |
| r.raise_for_status() | |
| for img in r.json().get('results', []): | |
| url = img.get('url', '') | |
| if url: | |
| results.append({ | |
| 'url': url, | |
| 'category': category, | |
| 'source': 'openverse', | |
| 'term': term, | |
| 'title': img.get('title', ''), | |
| }) | |
| except Exception: | |
| pass | |
| return results | |
| # ββ DOWNLOADER ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def download_image(url: str, save_path: Path, timeout: int = 12) -> bool: | |
| """Download and save an image. Returns True on success.""" | |
| try: | |
| r = requests.get(url, headers=HEADERS, timeout=timeout, stream=True) | |
| r.raise_for_status() | |
| content_type = r.headers.get('content-type', '') | |
| if 'image' not in content_type: | |
| return False | |
| save_path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(save_path, 'wb') as f: | |
| for chunk in r.iter_content(8192): | |
| f.write(chunk) | |
| # Verify it's a valid image using basic header check | |
| with open(save_path, 'rb') as f: | |
| header = f.read(12) | |
| # JPEG, PNG, GIF, WEBP checks | |
| valid = ( | |
| header[:2] == b'\xff\xd8' or # JPEG | |
| header[:8] == b'\x89PNG\r\n\x1a\n' or # PNG | |
| header[:6] in (b'GIF87a', b'GIF89a') or # GIF | |
| header[:4] == b'RIFF' # WEBP | |
| ) | |
| if not valid: | |
| save_path.unlink(missing_ok=True) | |
| return False | |
| return True | |
| except Exception: | |
| try: | |
| save_path.unlink(missing_ok=True) | |
| except Exception: | |
| pass | |
| return False | |
| def write_manifest(record: dict): | |
| """Append one image record to the manifest file.""" | |
| try: | |
| with open(MANIFEST_FILE, 'a', encoding='utf-8') as f: | |
| f.write(json.dumps(record, ensure_ascii=False) + '\n') | |
| except Exception: | |
| pass | |
| def get_manifest_stats() -> dict: | |
| """Count images per category from manifest.""" | |
| counts = defaultdict(int) | |
| total = 0 | |
| if not os.path.exists(MANIFEST_FILE): | |
| return {'total': 0, 'by_category': {}} | |
| try: | |
| with open(MANIFEST_FILE, 'r') as f: | |
| for line in f: | |
| line = line.strip() | |
| if line: | |
| try: | |
| rec = json.loads(line) | |
| counts[rec.get('category', 'unknown')] += 1 | |
| total += 1 | |
| except Exception: | |
| pass | |
| except Exception: | |
| pass | |
| return {'total': total, 'by_category': dict(counts)} | |
| def get_category_image_count(category: str) -> int: | |
| cat_dir = IMAGE_DIR / category | |
| if not cat_dir.exists(): | |
| return 0 | |
| return len(list(cat_dir.glob('*.jpg')) + list(cat_dir.glob('*.png')) + | |
| list(cat_dir.glob('*.webp'))) | |
| # ββ MAIN FETCHER CLASS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class ImageFetcher: | |
| def __init__(self): | |
| self.total_downloaded = 0 | |
| self.total_failed = 0 | |
| self.log = [] | |
| self.category_counts = defaultdict(int) | |
| IMAGE_DIR.mkdir(exist_ok=True) | |
| for cat in CATEGORIES: | |
| (IMAGE_DIR / cat).mkdir(exist_ok=True) | |
| # Load existing counts from manifest | |
| stats = get_manifest_stats() | |
| self.total_downloaded = stats['total'] | |
| for cat, count in stats['by_category'].items(): | |
| self.category_counts[cat] = count | |
| def _log(self, msg: str): | |
| ts = datetime.now(UTC).strftime('%H:%M:%S') | |
| entry = f"[{ts}] {msg}" | |
| self.log.append(entry) | |
| if len(self.log) > 200: | |
| self.log = self.log[-200:] | |
| return entry | |
| def fetch_round(self) -> dict: | |
| """ | |
| Fetch one round of images across all categories. | |
| Returns summary of what was downloaded. | |
| """ | |
| downloaded = 0 | |
| failed = 0 | |
| candidates = [] | |
| # Wikipedia Commons β most reliable | |
| import random | |
| for cat, terms in WIKI_SEARCH_TERMS.items(): | |
| if get_category_image_count(cat) >= MAX_PER_CAT: | |
| continue | |
| term = random.choice(terms) | |
| imgs = fetch_wikipedia_images(cat, term, max_imgs=4) | |
| candidates.extend(imgs) | |
| # NASA (science category) | |
| if get_category_image_count('science') < MAX_PER_CAT: | |
| candidates.extend(fetch_nasa_images(6)) | |
| # Openverse β random category | |
| cat = random.choice(CATEGORIES) | |
| term = random.choice(WIKI_SEARCH_TERMS.get(cat, [cat])) | |
| candidates.extend(fetch_openverse_images(cat, term, 5)) | |
| # Download each candidate | |
| for img_info in candidates: | |
| cat = img_info['category'] | |
| url = img_info['url'] | |
| # Make a unique filename from URL hash | |
| url_hash = hashlib.md5(url.encode()).hexdigest()[:12] | |
| ext = url.split('?')[0].split('.')[-1].lower() | |
| if ext not in ('jpg', 'jpeg', 'png', 'gif', 'webp'): | |
| ext = 'jpg' | |
| filename = f"{url_hash}.{ext}" | |
| save_path = IMAGE_DIR / cat / filename | |
| if save_path.exists(): | |
| continue # Already have it | |
| if download_image(url, save_path): | |
| record = { | |
| **img_info, | |
| 'filename': str(save_path), | |
| 'timestamp': datetime.now(UTC).isoformat(), | |
| } | |
| write_manifest(record) | |
| self.category_counts[cat] += 1 | |
| self.total_downloaded += 1 | |
| downloaded += 1 | |
| else: | |
| failed += 1 | |
| self.total_failed += 1 | |
| time.sleep(0.15) # polite rate limiting | |
| self._log( | |
| f"Round complete: +{downloaded} images saved, " | |
| f"{failed} failed | Total: {self.total_downloaded}" | |
| ) | |
| return {'downloaded': downloaded, 'failed': failed, | |
| 'total': self.total_downloaded} | |
| def get_stats(self) -> dict: | |
| by_cat = { | |
| cat: get_category_image_count(cat) | |
| for cat in CATEGORIES | |
| } | |
| disk_mb = 0 | |
| try: | |
| for p in IMAGE_DIR.rglob('*'): | |
| if p.is_file(): | |
| disk_mb += p.stat().st_size | |
| disk_mb = round(disk_mb / 1_000_000, 1) | |
| except Exception: | |
| pass | |
| return { | |
| 'total': self.total_downloaded, | |
| 'by_category': by_cat, | |
| 'disk_mb': disk_mb, | |
| 'recent_log': self.log[-15:], | |
| } | |
| def get_sample_paths(self, category: str, n: int = 9) -> list: | |
| """Return paths of n sample images from a category.""" | |
| cat_dir = IMAGE_DIR / category | |
| if not cat_dir.exists(): | |
| return [] | |
| files = list(cat_dir.iterdir()) | |
| import random | |
| sample = random.sample(files, min(n, len(files))) | |
| return [str(p) for p in sample] | |