HEX
Server: LiteSpeed
System: Linux houston.panomity.com 6.8.0-100-generic #100-Ubuntu SMP PREEMPT_DYNAMIC Tue Jan 13 16:40:06 UTC 2026 x86_64
User: nudepix (1011)
PHP: 7.4.33
Disabled: pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare,
Upload Files
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);