File: /home/panomity.de/vr.panomity.com/api/gpuq-vision-exec.php
<?php
/**
* GPUQ Vision-Exec für CMS4VR
*
* Wird vom GPUQ-Worker aufgerufen (job_type vr_vision_describe).
* Erzeugt eine Szenen-Beschreibung aus dem Equirect-Bild einer Szene
* und speichert sie (en → panos.ollama_context, andere Sprachen →
* pano_ai_context).
*
* POST { _gpuq_secret, pano_id, language }
*/
header('Content-Type: application/json; charset=utf-8');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['ok' => false, 'message' => 'Method not allowed']);
exit;
}
$config = require __DIR__ . '/config.gpuq.php';
$raw = file_get_contents('php://input') ?: '';
$body = json_decode($raw, true);
if (!is_array($body)) {
http_response_code(400);
echo json_encode(['ok' => false, 'message' => 'Invalid JSON payload']);
exit;
}
$secret = (string)($body['_gpuq_secret'] ?? '');
if ($secret === '' || !hash_equals((string)($config['self_exec_secret'] ?? ''), $secret)) {
http_response_code(403);
echo json_encode(['ok' => false, 'message' => 'Forbidden']);
exit;
}
$panoId = (int)($body['pano_id'] ?? 0);
$language = strtolower(trim((string)($body['language'] ?? 'en')));
if ($panoId <= 0) {
http_response_code(400);
echo json_encode(['ok' => false, 'message' => 'Invalid pano_id']);
exit;
}
$dbPath = __DIR__ . '/../admin/data/vrm.db';
$db = new SQLite3($dbPath);
$db->busyTimeout(10000);
require_once __DIR__ . '/ollama_context_helper.php';
ensure_panos_ollama_context_column($db);
ensure_pano_ai_context_table($db);
// Bestes Quellbild: Equirect (4826px) statt Preview-Streifen.
$baseDir = __DIR__ . '/../panos/p' . $panoId . '.tiles/';
$imagePath = null;
foreach (['equirect.jpg', 'preview.jpg', 'thumb.jpg'] as $cand) {
if (is_file($baseDir . $cand)) {
$imagePath = $baseDir . $cand;
break;
}
}
if ($imagePath === null) {
http_response_code(404);
echo json_encode(['ok' => false, 'message' => 'Scene image not found']);
exit;
}
// Auf handhabbare Größe für das Vision-Modell skalieren (GD).
$maxWidth = 1600;
$src = @imagecreatefromjpeg($imagePath);
if ($src === false) {
http_response_code(500);
echo json_encode(['ok' => false, 'message' => 'Failed to read scene image']);
exit;
}
$w = imagesx($src);
$h = imagesy($src);
if ($w > $maxWidth) {
$nw = $maxWidth;
$nh = (int)round($h * $maxWidth / $w);
$dst = imagecreatetruecolor($nw, $nh);
imagecopyresampled($dst, $src, 0, 0, 0, 0, $nw, $nh, $w, $h);
imagedestroy($src);
$src = $dst;
}
ob_start();
imagejpeg($src, null, 88);
$jpeg = ob_get_clean();
imagedestroy($src);
$imageBase64 = base64_encode($jpeg);
$languageName = ollama_context_language_name($language);
$prompt =
"You are a professional real-estate assistant analyzing one 360-degree panorama " .
"(equirectangular projection, so the image edges wrap around and vertical lines may look curved).\n" .
"The scene is part of a condominium tour.\n\n" .
"Describe ONLY what is actually visible. Output language: " . $languageName . ".\n\n" .
"Format: 8-13 concise bullet points, no intro sentence, covering:\n" .
"- room type and its function\n" .
"- layout and flow (open plan, adjoining rooms, doorways, stairs)\n" .
"- floors, walls, ceiling: materials and colors\n" .
"- fixtures, appliances, built-ins, furniture that is present\n" .
"- windows, daylight, lighting fixtures\n" .
"- notable selling points (views, storage, finishes, condition)\n" .
"- anything unusual or limiting that is visible\n\n" .
"Be specific and concrete. Never invent measurements, prices, brands or facts that are not visible.";
$ollamaUrl = rtrim((string)($config['ollama_url'] ?? 'http://127.0.0.1:11434'), '/');
$model = (string)($config['vision_model'] ?? 'qwen2.5vl:7b');
$try = call_ollama_vision_chat($ollamaUrl, $model, $prompt, $imageBase64, false);
if ($try['http'] !== 200 || !empty($try['err'])) {
// Fallback auf das Standardmodell mit Vision-Fähigkeit
$try = call_ollama_vision_chat($ollamaUrl, (string)($config['vision_model_fallback'] ?? 'gemma3:latest'), $prompt, $imageBase64, false);
}
if ($try['http'] !== 200 || !empty($try['err'])) {
http_response_code(502);
echo json_encode(['ok' => false, 'message' => 'Vision model error', 'http_code' => (int)$try['http'], 'details' => (string)$try['err']]);
exit;
}
$response = json_decode((string)$try['body'], true);
$context = sanitize_generated_context((string)($response['message']['content'] ?? ''));
if ($context === '') {
http_response_code(502);
echo json_encode(['ok' => false, 'message' => 'Empty response from vision model']);
exit;
}
save_pano_context($db, $panoId, $language, $context);
echo json_encode([
'ok' => true,
'pano_id' => $panoId,
'language' => $language,
'model' => $model,
'context' => $context,
], JSON_UNESCAPED_UNICODE);