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/generate-scene-metadata.php
<?php
/**
 * Batch-Generierung der Szenen-Metadaten über GPUQ (CLI only).
 *
 * Für jede sichtbare Szene:
 *  1. Vision-Beschreibung (en + de) aus dem Equirect-Bild → Kontext
 *  2. Eindeutiger Raumname + kurze sichtbare Beschreibung (en + de)
 *     → pano_phrases.name / .description
 *  3. Konversationsstarter (en + de) → ollama_conversation_starters
 *
 * Usage:
 *   php generate-scene-metadata.php [--force] [--only-contexts] [--pano=ID]
 */

if (PHP_SAPI !== 'cli') {
    die("CLI only\n");
}

require_once __DIR__ . '/gpuq_client.php';
require_once __DIR__ . '/ollama_context_helper.php';

$force = in_array('--force', $argv, true);
$onlyContexts = in_array('--only-contexts', $argv, true);
$onlyPano = null;
foreach ($argv as $arg) {
    if (preg_match('/^--pano=(\d+)$/', $arg, $m)) {
        $onlyPano = (int)$m[1];
    }
}

$dbPath = __DIR__ . '/../admin/data/vrm.db';
$db = new SQLite3($dbPath);
$db->busyTimeout(15000);
ensure_panos_ollama_context_column($db);
ensure_pano_ai_context_table($db);

$languages = [];
$res = $db->query('SELECT id, short FROM languages');
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
    $languages[$row['short']] = (int)$row['id'];
}

$panos = [];
$res = $db->query("SELECT id FROM panos WHERE visible = 1 AND (type = 'pano' OR type = '' OR type IS NULL) ORDER BY position, id");
while ($row = $res->fetchArray(SQLITE3_ASSOC)) {
    if ($onlyPano === null || (int)$row['id'] === $onlyPano) {
        $panos[] = (int)$row['id'];
    }
}

echo "Panos: " . count($panos) . ", Sprachen: " . implode(',', array_keys($languages)) . "\n";

// ---------- Phase 1: Vision-Kontexte ----------
foreach ($panos as $panoId) {
    foreach (array_keys($languages) as $lang) {
        $existing = read_pano_context($db, $panoId, $lang);
        $looksGeneric = (strpos($existing, '*   This appears') === 0) || (strpos($existing, 'Okay,') === 0);
        if (!$force && $existing !== '' && !$looksGeneric) {
            echo "p$panoId/$lang: vorhanden, übersprungen\n";
            continue;
        }
        $t0 = microtime(true);
        $result = generate_ollama_context_for_pano($db, $panoId, $lang, true);
        $dt = round(microtime(true) - $t0, 1);
        if (!empty($result['success'])) {
            echo "p$panoId/$lang: OK ({$dt}s, " . mb_strlen($result['context']) . " Zeichen)\n";
        } else {
            echo "p$panoId/$lang: FEHLER ({$dt}s): " . ($result['error'] ?? '?') . "\n";
        }
    }
}

if ($onlyContexts) {
    echo "Nur Kontexte angefordert — fertig.\n";
    exit(0);
}

// ---------- Phase 2: Namen + sichtbare Beschreibungen ----------
// Kontexte einsammeln (englisch als Basis für konsistente Benennung).
$contexts = [];
foreach ($panos as $panoId) {
    $ctx = read_pano_context($db, $panoId, 'en');
    $contexts[$panoId] = mb_substr($ctx, 0, 1200);
}

$usedNames = ['en' => [], 'de' => []];

foreach ($panos as $panoId) {
    $avoidEn = $usedNames['en'] ? ("Already used English names (do NOT reuse): " . implode('; ', $usedNames['en']) . "\n") : '';
    $avoidDe = $usedNames['de'] ? ("Bereits vergebene deutsche Namen (NICHT wiederverwenden): " . implode('; ', $usedNames['de']) . "\n") : '';

    $prompt =
        "You are naming one scene of a condominium virtual tour based on what is visible.\n" .
        "Output ONLY one JSON object, no markdown, no commentary:\n" .
        '{"name_en":"...","name_de":"...","desc_en":"...","desc_de":"..."}' . "\n\n" .
        "Rules:\n" .
        "- name_en/name_de: short distinctive room name (2-4 words), e.g. 'Living Room', 'Kitchen & Dining', 'Primary Bedroom', 'Balcony View'.\n" .
        "- All names must be unique across the tour; disambiguate naturally if needed ('Living Room — Fireplace').\n" .
        $avoidEn . $avoidDe .
        "- desc_en/desc_de: one appealing but factual sentence (max 22 words) for tour visitors.\n" .
        "- German must be idiomatic, not a literal translation.\n\n" .
        "SCENE p$panoId:\n" . ($contexts[$panoId] ?: '(no description)');

    echo "Benenne p$panoId...\n";
    $chat = gpuq_chat([['role' => 'user', 'content' => $prompt]], ['priority' => 'normal', 'timeout' => 240, 'max_wait' => 280]);
    if (empty($chat['success'])) {
        echo "  FEHLER: " . ($chat['error'] ?? '?') . "\n";
        continue;
    }

    if (!preg_match('/\{[^{}]*\}/s', $chat['content'], $jm)) {
        echo "  Kein JSON gefunden\n";
        continue;
    }
    $obj = json_decode($jm[0], true);
    if (!is_array($obj)) {
        echo "  JSON unlesbar\n";
        continue;
    }
    {
        foreach ([['en', 'name_en', 'desc_en'], ['de', 'name_de', 'desc_de']] as [$lang, $nameKey, $descKey]) {
            $name = trim((string)($obj[$nameKey] ?? ''));
            $desc = trim((string)($obj[$descKey] ?? ''));
            if ($name === '' || !isset($languages[$lang])) {
                continue;
            }
            $usedNames[$lang][] = $name;
            $stmt = $db->prepare('UPDATE pano_phrases SET name = :name, description = :desc
                WHERE panos_id = :pid AND languages_id = :lid');
            $stmt->bindValue(':name', $name, SQLITE3_TEXT);
            $stmt->bindValue(':desc', $desc, SQLITE3_TEXT);
            $stmt->bindValue(':pid', $panoId, SQLITE3_INTEGER);
            $stmt->bindValue(':lid', $languages[$lang], SQLITE3_INTEGER);
            $stmt->execute();
            if ($db->changes() === 0) {
                $stmt = $db->prepare('INSERT INTO pano_phrases (panos_id, languages_id, name, description)
                    VALUES (:pid, :lid, :name, :desc)');
                $stmt->bindValue(':pid', $panoId, SQLITE3_INTEGER);
                $stmt->bindValue(':lid', $languages[$lang], SQLITE3_INTEGER);
                $stmt->bindValue(':name', $name, SQLITE3_TEXT);
                $stmt->bindValue(':desc', $desc, SQLITE3_TEXT);
                $stmt->execute();
            }
            echo "  p$panoId/$lang: $name\n";
        }
    }
}

// ---------- Phase 3: Konversationsstarter ----------
$db->exec('CREATE TABLE IF NOT EXISTS ollama_conversation_starters (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    pano_id INTEGER,
    category_id INTEGER,
    language TEXT NOT NULL,
    starter_text TEXT NOT NULL,
    position INTEGER DEFAULT 0,
    active INTEGER DEFAULT 1
)');

foreach ($panos as $panoId) {
    foreach (array_keys($languages) as $lang) {
        $cnt = $db->querySingle('SELECT COUNT(*) FROM ollama_conversation_starters WHERE pano_id = ' . $panoId . ' AND language = \'' . $lang . '\' AND active = 1');
        if (!$force && (int)$cnt >= 2) {
            continue;
        }
        $ctx = read_pano_context($db, $panoId, $lang);
        if ($ctx === '') {
            continue;
        }
        $langName = ollama_context_language_name($lang);
        $prompt =
            "Language: $langName.\n" .
            "Create exactly 3 short conversation starters a VISITOR of a virtual condo tour would tap. " .
            "First-person visitor perspective, questions or short requests, max 9 words each. " .
            "One per line, no numbering, no bullets, nothing else.\n\nScene context:\n" . $ctx;
        $chat = gpuq_chat([['role' => 'user', 'content' => $prompt]], ['priority' => 'normal', 'timeout' => 240, 'max_wait' => 280]);
        if (empty($chat['success'])) {
            echo "starter p$panoId/$lang: FEHLER " . ($chat['error'] ?? '') . "\n";
            continue;
        }
        $db->exec('DELETE FROM ollama_conversation_starters WHERE pano_id = ' . $panoId . ' AND language = \'' . $lang . '\'');
        $pos = 0;
        foreach (explode("\n", $chat['content']) as $line) {
            $starter = trim(preg_replace('/^[\d\.\-\*\•"]+\s*/u', '', trim($line)));
            $starter = trim($starter, '"');
            if ($starter === '' || mb_strlen($starter) < 6 || $pos >= 3) {
                continue;
            }
            $stmt = $db->prepare('INSERT INTO ollama_conversation_starters (pano_id, category_id, language, starter_text, position, active)
                VALUES (:pid, NULL, :lang, :text, :pos, 1)');
            $stmt->bindValue(':pid', $panoId, SQLITE3_INTEGER);
            $stmt->bindValue(':lang', $lang, SQLITE3_TEXT);
            $stmt->bindValue(':text', $starter, SQLITE3_TEXT);
            $stmt->bindValue(':pos', $pos, SQLITE3_INTEGER);
            $stmt->execute();
            $pos++;
        }
        echo "starter p$panoId/$lang: $pos gespeichert\n";
    }
}

$db->close();
echo "Fertig. Tour-XML neu erzeugen mit: php " . __DIR__ . "/regenerate-xml.php\n";