222 lines
12 KiB
PHP
222 lines
12 KiB
PHP
<?php
|
|
// WEVIA APPLE SCAN API v2 — batch + HEIC + precise OSS
|
|
header('Content-Type: application/json; charset=utf-8');
|
|
header('Access-Control-Allow-Origin: *');
|
|
|
|
$action = $_GET['action'] ?? $_POST['action'] ?? 'status';
|
|
$scans_file = '/var/www/html/data/wevia-apple-scans.json';
|
|
$uploads_dir = '/var/www/html/data/wevia-apple-uploads';
|
|
|
|
function load_scans() {
|
|
global $scans_file;
|
|
if (!file_exists($scans_file)) return ['scans'=>[], '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<count($m[0]); $i++) {
|
|
$hits['github_urls'][] = ['url' => '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']]);
|