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];
}