[], 'total'=>0, 'oss_total'=>0, 'last_scan'=>null]; return json_decode(file_get_contents($scans_file), true) ?: ['scans'=>[], 'total'=>0, 'oss_total'=>0, 'last_scan'=>null]; } function save_scans($data) { global $scans_file; file_put_contents($scans_file, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } // STRICT OSS list — only projects that are unambiguous terms function extract_oss($text) { $hits = ['github_urls' => [], 'project_names' => [], 'stacks' => [], 'docker_images' => []]; // GitHub URLs preg_match_all('#(?:https?://)?(?:www\.)?github\.com/([a-zA-Z0-9_\-\.]+)/([a-zA-Z0-9_\-\.]+)#i', $text, $m); for ($i=0; $i 'https://github.com/'.$m[1][$i].'/'.$m[2][$i], 'owner' => $m[1][$i], 'repo' => $m[2][$i]]; } // Known OSS — unambiguous names (no common English words). Word-boundary match. $known = [ 'langchain','langgraph','crewai','autogen','n8n','rasa','haystack','llamaindex','weaviate','qdrant','chromadb','milvus','pinecone', 'ollama','vllm','litellm','openrouter','langfuse','dify','flowise','opendevin','openinterpreter','lobe-chat','anything-llm','privategpt','localgpt', 'openwebui','gradio','streamlit','fastapi','nextjs','next.js','remix','astro','sveltekit','tauri','flutter','react-native', 'prisma','drizzle','supabase','nhost','appwrite','firebase', 'postgresql','postgres','mongodb','mariadb','redis','kafka','rabbitmq','clickhouse', 'nginx','traefik','caddy','kubernetes','helm','terraform','pulumi','ansible','podman', 'grafana','prometheus','loki','tempo','jaeger','elasticsearch','meilisearch','typesense','opensearch', 'minio','gitea','gitlab','forgejo','drone','jenkins','argocd','fluxcd', 'docker','keycloak','vaultwarden','authentik','nextcloud','jellyfin','paperless','immich','photoprism', 'ubuntu','debian','alpine','arch','fedora','rocky', 'tensorflow','pytorch','jax','keras','scikit-learn','huggingface','transformers', 'bert','gpt','llama','mistral','gemma','phi-3','qwen','deepseek','claude','gpt-4','gpt-4o','gpt-5','opus','sonnet','haiku', 'zapier','make.com','activepieces','windmill','temporal','airflow','prefect','dagster', 'stripe','paddle','lemonsqueezy', 'vercel','netlify','cloudflare','cloudflare-workers', 'copilot','cursor','codeium','tabnine','continue.dev', 'obsidian','logseq','notion','affine','anytype', 'blender','gimp','inkscape','krita','davinci-resolve','audacity','obs-studio', 'sap','oracle','salesforce','odoo','netsuite','workday','d365','dynamics','hubspot','zendesk','freshdesk', 'sap s/4hana','sap b1','oracle ebs','oracle fusion','sage x3','sage 100','sage intacct', 'infor m3','ifs cloud','epicor','acumatica', 'paperclip','wevads','weval','wevia','ethica','arsenal' ]; $text_l = ' '.strtolower($text).' '; foreach ($known as $k) { $kl = strtolower($k); // Must have non-alphanum on both sides (strict word boundary) if (preg_match('/[^a-z0-9]'.preg_quote($kl,'/').'[^a-z0-9]/i', $text_l)) { $hits['project_names'][] = $kl; } } $hits['project_names'] = array_values(array_unique($hits['project_names'])); // Docker images preg_match_all('/[a-z0-9_\-]+\/[a-z0-9_\-]+:[a-z0-9_\.\-]+/', $text_l, $dm); $hits['docker_images'] = array_values(array_unique($dm[0] ?? [])); return $hits; } // Convert HEIC to JPG for Gemini function ensure_jpg($path) { $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); if (in_array($ext, ['heic','heif'])) { $jpg = preg_replace('/\.(heic|heif)$/i', '.jpg', $path); exec('convert '.escapeshellarg($path).' '.escapeshellarg($jpg).' 2>/dev/null', $_, $rc); if ($rc === 0 && file_exists($jpg)) return $jpg; } return $path; } function call_gemini_vision($local_path, $prompt) { $key = ''; foreach (@file('/etc/weval/secrets.env') ?: [] as $l) { $l = trim($l); if (!$l || $l[0]==='#') continue; $parts = explode('=', $l, 2); if (count($parts)<2) continue; if (trim($parts[0]) === 'GEMINI_KEY') { $key = trim($parts[1]); break; } } if (!$key) return '(no GEMINI_KEY)'; if (!file_exists($local_path)) return '(file not found)'; $img_b64 = base64_encode(file_get_contents($local_path)); $mime = mime_content_type($local_path) ?: 'image/jpeg'; $body = ['contents' => [['parts' => [ ['inline_data' => ['mime_type' => $mime, 'data' => $img_b64]], ['text' => $prompt] ]]]]; $ch = curl_init("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=$key"); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 45, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode($body), ]); $r = curl_exec($ch); $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code !== 200) return "(gemini http $code)"; $d = json_decode($r, true); return $d['candidates'][0]['content']['parts'][0]['text'] ?? '(no text)'; } function do_scan($target, $filename, $caption, $size) { global $uploads_dir; $id = uniqid('apple_', true); $t0 = microtime(true); $target_jpg = ensure_jpg($target); // OCR $ocr_out = []; exec('tesseract '.escapeshellarg($target_jpg).' - -l eng+fra 2>/dev/null', $ocr_out); $ocr_text = implode("\n", $ocr_out); // Vision LLM $vision_text = call_gemini_vision($target_jpg, 'Extract from this image: 1) All text visible, 2) Names of software tools, frameworks, AI agents, open-source projects, SaaS platforms mentioned (with precise names), 3) Architecture diagrams components, 4) GitHub URLs or repo names, 5) Docker images, 6) Programming languages visible. Output as structured bullet list.'); // Combine $oss = extract_oss($ocr_text . "\n\n" . $vision_text); $ms = round((microtime(true) - $t0) * 1000); return [ 'id' => $id, 'filename' => $filename, 'stored_as' => basename($target), 'size_bytes' => $size, 'scan_ms' => $ms, 'scanned_at' => gmdate('c'), 'caption' => $caption, 'ocr_text' => substr($ocr_text, 0, 2000), 'vision_text' => substr($vision_text, 0, 3000), 'oss_extracted' => $oss, 'counts' => [ 'github_urls' => count($oss['github_urls']), 'project_names' => count($oss['project_names']), 'docker_images' => count($oss['docker_images']), ], 'image_url' => 'https://weval-consulting.com/data/wevia-apple-uploads/'.basename($target) ]; } if ($action === 'status') { $scans = load_scans(); echo json_encode([ 'api' => 'WEVIA-APPLE-SCAN', 'version' => 'v2.0-batch-heic', 'generated_at' => gmdate('c'), 'scans_total' => $scans['total'], 'oss_total' => $scans['oss_total'], 'last_scan' => $scans['last_scan'], 'tools' => ['ocr' => 'tesseract 5.3.4', 'vision' => 'Gemini 2.5 Flash (inline)', 'heic' => 'ImageMagick+libheif'], 'endpoints' => ['upload' => '?action=upload', 'batch' => '?action=batch (multi)', 'list' => '?action=list', 'detail' => '?action=detail&id=', 'stats' => '?action=stats', 'shortcut' => '?action=shortcut'], 'iphone_shortcut_url' => 'https://weval-consulting.com/api/wevia-apple-scan.php?action=upload' ], JSON_PRETTY_PRINT); exit; } if ($action === 'list') { $scans = load_scans(); echo json_encode(['total' => count($scans['scans']), 'scans' => array_reverse($scans['scans'])], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); exit; } if ($action === 'detail') { $id = $_GET['id'] ?? ''; $scans = load_scans(); foreach ($scans['scans'] as $s) if ($s['id'] === $id) { echo json_encode($s, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); exit; } http_response_code(404); echo json_encode(['error'=>'not_found']); exit; } if ($action === 'stats') { $scans = load_scans(); $oss_cnt = []; $gh_cnt = 0; foreach ($scans['scans'] as $s) { foreach (($s['oss_extracted']['project_names'] ?? []) as $p) $oss_cnt[$p] = ($oss_cnt[$p] ?? 0) + 1; $gh_cnt += count($s['oss_extracted']['github_urls'] ?? []); } arsort($oss_cnt); echo json_encode(['scans_total' => $scans['total'], 'oss_total' => $scans['oss_total'], 'github_urls_total' => $gh_cnt, 'top_projects' => array_slice($oss_cnt, 0, 40, true)], JSON_PRETTY_PRINT); exit; } if ($action === 'shortcut') { // Return iPhone Shortcut install instructions as JSON echo json_encode([ 'name' => 'Scan WEVIA', 'description' => 'iPhone Shortcut that sends any photo to WEVIA Apple Scanner', 'endpoint' => 'https://weval-consulting.com/api/wevia-apple-scan.php?action=upload', 'setup_steps' => [ "1. iPhone → ouvrir app Raccourcis (Shortcuts)", "2. Appuyer sur + en haut à droite pour créer nouveau raccourci", "3. Ajouter action 'Obtenir le contenu de l'URL'", "4. URL: https://weval-consulting.com/api/wevia-apple-scan.php?action=upload", "5. Étendre 'Afficher plus': Méthode = POST, Demander = Multipart Form", "6. Ajouter champ: Clé='image', Type='Fichier', Valeur='Entrée du raccourci'", "7. (Optionnel) Ajouter champ: Clé='caption', Type='Texte', Valeur='iPhone auto-scan'", "8. Ajouter action 'Afficher le résultat' après l'URL", "9. Renommer le raccourci 'Scan WEVIA'", "10. Dans les réglages, activer 'Utiliser avec Partager'", "11. Depuis app Photos iPhone: sélectionner 1+ photos → Partager → 'Scan WEVIA'", "12. Pour auto-scan d'un album iCloud: Automations → Nouvelle automation → Quand ajout à album 'WEVIA' → exécuter 'Scan WEVIA'" ], 'batch_notes' => "Pour scanner plusieurs photos d'un coup: sélectionner plusieurs photos dans Photos iPhone, puis Partager → Scan WEVIA. Le raccourci itère automatiquement." ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); exit; } if ($action === 'upload') { if (empty($_FILES['image'])) { http_response_code(400); echo json_encode(['error' => 'no_image']); exit; } $file = $_FILES['image']; if ($file['error'] !== UPLOAD_ERR_OK) { echo json_encode(['error'=>'upload_failed', 'code'=>$file['error']]); exit; } $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($ext, ['jpg','jpeg','png','gif','webp','heic','heif'])) { echo json_encode(['error'=>'unsupported_ext', 'ext'=>$ext]); exit; } $id_pre = uniqid('apple_', true); $safe = $id_pre . '.' . $ext; $target = $uploads_dir . '/' . $safe; if (!move_uploaded_file($file['tmp_name'], $target)) { echo json_encode(['error'=>'move_failed']); exit; } $entry = do_scan($target, $file['name'], $_POST['caption'] ?? '', $file['size']); $scans = load_scans(); $scans['scans'][] = $entry; $scans['total'] = count($scans['scans']); $scans['oss_total'] = ($scans['oss_total'] ?? 0) + count($entry['oss_extracted']['project_names']) + count($entry['oss_extracted']['github_urls']); $scans['last_scan'] = gmdate('c'); save_scans($scans); echo json_encode(['ok' => true, 'scan' => $entry], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); exit; } echo json_encode(['error'=>'unknown_action', 'available'=>['status','upload','list','detail','stats','shortcut']]);