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/ollama_context_helper.php
<?php
/**
 * Gemeinsame Hilfsfunktionen für Szenen-Kontext.
 *
 * Die Erzeugung läuft über die GPUQ-Queue (job_type vr_vision_describe →
 * api/gpuq-vision-exec.php). Englisch liegt in panos.ollama_context
 * (Admin-editierbar), weitere Sprachen in pano_ai_context.
 */

require_once __DIR__ . '/gpuq_client.php';

function ensure_panos_ollama_context_column(SQLite3 $db): void
{
    $res = $db->query("PRAGMA table_info('panos')");
    $hasCtx = false;
    if ($res) {
        while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
            if (!empty($row['name']) && $row['name'] === 'ollama_context') {
                $hasCtx = true;
                break;
            }
        }
    }
    if (!$hasCtx) {
        $db->exec('ALTER TABLE `panos` ADD `ollama_context` TEXT');
    }
}

function ensure_pano_ai_context_table(SQLite3 $db): void
{
    $db->exec('CREATE TABLE IF NOT EXISTS pano_ai_context (
        pano_id INTEGER NOT NULL,
        language TEXT NOT NULL,
        context TEXT NOT NULL,
        updated_at TEXT NOT NULL,
        PRIMARY KEY (pano_id, language)
    )');
}

function ollama_context_language_name(string $language): string
{
    $lang = strtolower(trim($language));
    $map = [
        'en' => 'English',
        'de' => 'German',
        'es' => 'Spanish',
        'fr' => 'French',
        'it' => 'Italian',
        'pt' => 'Portuguese',
        'nl' => 'Dutch',
        'pl' => 'Polish',
    ];
    return $map[$lang] ?? 'English';
}

function find_pano_image_path(string $baseDir): ?string
{
    $candidates = [
        $baseDir . 'equirect.jpg',
        $baseDir . 'preview.jpg',
        $baseDir . 'fb_thumb.jpg',
        $baseDir . 'thumb.jpg',
    ];

    foreach ($candidates as $cand) {
        if (is_file($cand)) {
            return $cand;
        }
    }

    return null;
}

/** Kontext lesen: en aus panos.ollama_context, andere Sprachen aus pano_ai_context. */
function read_pano_context(SQLite3 $db, int $panoId, string $language): string
{
    $language = strtolower(trim($language));

    if ($language !== 'en') {
        $stmt = $db->prepare('SELECT context FROM pano_ai_context WHERE pano_id = :id AND language = :lang');
        $stmt->bindValue(':id', $panoId, SQLITE3_INTEGER);
        $stmt->bindValue(':lang', $language, SQLITE3_TEXT);
        $result = $stmt->execute();
        if ($row = $result->fetchArray(SQLITE3_ASSOC)) {
            $ctx = trim((string)($row['context'] ?? ''));
            if ($ctx !== '') {
                return $ctx;
            }
        }
    }

    $stmt = $db->prepare('SELECT ollama_context FROM panos WHERE id = :id');
    $stmt->bindValue(':id', $panoId, SQLITE3_INTEGER);
    $result = $stmt->execute();
    if ($row = $result->fetchArray(SQLITE3_ASSOC)) {
        return trim((string)($row['ollama_context'] ?? ''));
    }
    return '';
}

/** Kontext speichern (en → Legacy-Spalte für das Admin-UI, sonst Tabelle). */
function save_pano_context(SQLite3 $db, int $panoId, string $language, string $context): void
{
    $language = strtolower(trim($language));

    if ($language === 'en') {
        $stmt = $db->prepare('UPDATE panos SET ollama_context = :ctx WHERE id = :id');
        $stmt->bindValue(':ctx', $context, SQLITE3_TEXT);
        $stmt->bindValue(':id', $panoId, SQLITE3_INTEGER);
        $stmt->execute();
        return;
    }

    ensure_pano_ai_context_table($db);
    $stmt = $db->prepare('INSERT INTO pano_ai_context (pano_id, language, context, updated_at)
        VALUES (:id, :lang, :ctx, :now)
        ON CONFLICT(pano_id, language) DO UPDATE SET context = :ctx, updated_at = :now');
    $stmt->bindValue(':id', $panoId, SQLITE3_INTEGER);
    $stmt->bindValue(':lang', $language, SQLITE3_TEXT);
    $stmt->bindValue(':ctx', $context, SQLITE3_TEXT);
    $stmt->bindValue(':now', gmdate('c'), SQLITE3_TEXT);
    $stmt->execute();
}

/** Direkter Ollama-Vision-Call. Wird nur vom GPUQ-Exec-Endpoint benutzt. */
function call_ollama_vision_chat(string $url, string $model, string $prompt, string $imageBase64, bool $useDataUri = false, array $options = []): array
{
    $imagePayload = $useDataUri ? ('data:image/jpeg;base64,' . $imageBase64) : $imageBase64;
    $payload = [
        'model' => $model,
        'messages' => [
            [
                'role' => 'user',
                'content' => $prompt,
                'images' => [$imagePayload],
            ],
        ],
        'stream' => false,
        // Vision-Modell schnell wieder entladen, damit VRAM für
        // Splat-Jobs (SHARP/3DGS) frei bleibt.
        'keep_alive' => '45s',
    ];
    if (!empty($options)) {
        $payload['options'] = $options;
    }

    $ch = curl_init($url . '/api/chat');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => json_encode($payload),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_TIMEOUT => 180,
    ]);
    $response = curl_exec($ch);
    $error = curl_error($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    return [
        'http' => $httpCode,
        'err' => $error,
        'body' => $response,
    ];
}

function sanitize_generated_context(string $context): string
{
    $text = trim($context);
    // Häufige Intro-Phrasen entfernen, damit der Kontext direkt nutzbar ist.
    $text = preg_replace('/^(okay[,!]?|sure[,!]?|here(?:\'s| is).{0,120}\n+)/i', '', $text);
    return trim((string)$text);
}

/**
 * Erzeugt (oder liefert) Kontext für eine Szene über die GPUQ-Queue.
 *
 * @return array{success:bool,context:string,skipped?:bool,error?:string,job_id?:string}
 */
function generate_ollama_context_for_pano(SQLite3 $db, int $panoId, string $language = 'en', bool $forceRegenerate = false): array
{
    ensure_panos_ollama_context_column($db);
    ensure_pano_ai_context_table($db);

    if ($panoId <= 0) {
        return ['success' => false, 'context' => '', 'error' => 'Invalid pano_id'];
    }

    if (!$forceRegenerate) {
        $existing = read_pano_context($db, $panoId, $language);
        if ($existing !== '') {
            return ['success' => true, 'context' => $existing, 'skipped' => true];
        }
    }

    $cfg = gpuq_config();
    $payload = [
        '_gpuq_secret' => (string)($cfg['self_exec_secret'] ?? ''),
        'pano_id' => $panoId,
        'language' => strtolower(trim($language)),
    ];

    $jobId = gpuq_submit('vr_vision_describe', (string)($cfg['vision_exec_url'] ?? ''), $payload, 'normal', 300);
    if ($jobId === null) {
        return ['success' => false, 'context' => '', 'error' => 'GPUQ submit failed'];
    }

    $job = gpuq_wait($jobId, 240, 1.5);
    if (($job['status'] ?? '') !== 'completed') {
        return [
            'success' => false,
            'context' => '',
            'error' => 'GPUQ job ' . (string)($job['status'] ?? 'unknown') . ': ' . (string)($job['message'] ?? ''),
            'job_id' => $jobId,
        ];
    }

    $context = trim((string)($job['result']['context'] ?? ''));
    if ($context === '') {
        // Exec-Endpoint hat gespeichert, aber nichts zurückgegeben? Aus DB lesen.
        $context = read_pano_context($db, $panoId, $language);
    }
    if ($context === '') {
        return ['success' => false, 'context' => '', 'error' => 'Empty response from vision job', 'job_id' => $jobId];
    }

    return ['success' => true, 'context' => $context, 'job_id' => $jobId];
}