diff --git a/_footer_admin.php b/_footer_admin.php index 790879d..cebbfe0 100644 --- a/_footer_admin.php +++ b/_footer_admin.php @@ -45,6 +45,134 @@ setDescribeButtonsDisabled(false); } + function showDescribePromptModal(job, articleId){ + var existing = document.getElementById('mvlog-describe-prompt-modal'); + if (existing) existing.remove(); + var overlay = document.createElement('div'); + overlay.id = 'mvlog-describe-prompt-modal'; + overlay.style.cssText = 'position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;z-index:9999;'; + var box = document.createElement('div'); + box.style.cssText = 'background:white;color:#111;padding:20px;max-width:800px;width:min(800px,92vw);max-height:80vh;overflow:auto;border-radius:6px;font-family:inherit;'; + var h = document.createElement('h3'); + h.textContent = 'Additional infos/guidelines'; + var p = document.createElement('div'); + p.style.cssText = 'margin:0 0 10px 0;color:#444;'; + p.textContent = 'Optional extra guidance for the description prompt.'; + var label = document.createElement('div'); + label.style.cssText = 'font-weight:600;margin:10px 0 6px;'; + label.textContent = 'Infos/guidelines'; + var textarea = document.createElement('textarea'); + textarea.rows = 8; + textarea.style.cssText = 'width:100%;box-sizing:border-box;font-family:inherit;padding:10px;border:1px solid #ccc;border-radius:4px;resize:vertical;'; + textarea.placeholder = 'Loading saved prompt…'; + textarea.disabled = true; + var btnBar = document.createElement('div'); + btnBar.style.cssText = 'margin-top:12px;text-align:right;'; + var submitBtn = document.createElement('button'); + submitBtn.className = 'button'; + submitBtn.textContent = 'Describe'; + submitBtn.disabled = true; + var cancelBtn = document.createElement('button'); + cancelBtn.className = 'button'; + cancelBtn.textContent = 'Cancel'; + cancelBtn.style.marginLeft = '8px'; + btnBar.appendChild(submitBtn); + btnBar.appendChild(cancelBtn); + box.appendChild(h); + box.appendChild(p); + box.appendChild(label); + box.appendChild(textarea); + box.appendChild(btnBar); + overlay.appendChild(box); + document.body.appendChild(overlay); + + function closeModal(){ + if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay); + document.removeEventListener('keydown', onKeyDown); + } + function submit(){ + var promptText = textarea.value || ''; + closeModal(); + runDescribeRequest(job, articleId, promptText); + } + function onKeyDown(ev){ + if (ev.key === 'Escape') { + ev.preventDefault(); + closeModal(); + } else if ((ev.ctrlKey || ev.metaKey) && ev.key === 'Enter') { + ev.preventDefault(); + submit(); + } + } + function loadSavedPrompt(){ + var body = 'id=' + encodeURIComponent(articleId) + '&job=' + encodeURIComponent(job); + fetch('lib/describe_prompt.php', { + method: 'POST', + credentials: 'same-origin', + headers: {'Content-Type':'application/x-www-form-urlencoded'}, + body: body + }).then(function(r){ return r.json().catch(function(){ return {}; }); }) + .then(function(data){ + if (!overlay || !overlay.parentNode) return; + if (data && data.status === 'ok' && typeof data.additional_prompt === 'string') { + textarea.value = data.additional_prompt; + } + }).catch(function(){}).finally(function(){ + if (!overlay || !overlay.parentNode) return; + textarea.disabled = false; + submitBtn.disabled = false; + textarea.placeholder = 'Optional details about the images/videos/trip…'; + setTimeout(function(){ textarea.focus(); }, 0); + }); + } + + cancelBtn.addEventListener('click', function(){ closeModal(); }); + submitBtn.addEventListener('click', submit); + overlay.addEventListener('click', function(ev){ if (ev.target === overlay) closeModal(); }); + document.addEventListener('keydown', onKeyDown); + loadSavedPrompt(); + } + + function runDescribeRequest(job, articleId, additionalPrompt){ + showDescribeBusy(); + var body = 'id=' + encodeURIComponent(articleId) + '&job=' + encodeURIComponent(job) + '&max_frames=16'; + if (additionalPrompt && additionalPrompt.trim()) { + body += '&additional_prompt=' + encodeURIComponent(additionalPrompt); + } + fetch('lib/generate_data_sync.php', { + method: 'POST', + credentials: 'same-origin', + headers: {'Content-Type':'application/x-www-form-urlencoded'}, + body: body + }).then(function(r){ return r.json().catch(()=>({})); }) + .then(function(data){ + if (data && data.status === 'ok'){ + var desc = data.description || (data.raw_json && JSON.stringify(data.raw_json)) || ''; + var teaser = data.teaser || ''; + var rawFirst = (Array.isArray(data.raw_json) && data.raw_json.length) ? data.raw_json[0] : data.raw_json; + var quoteDa = data.quote_da || (rawFirst && rawFirst.quote_da) || ''; + showModal(job, articleId, desc, teaser, quoteDa, data.raw_json || null); + } else { + var msg = (data && data.message) ? data.message : 'No response'; + if (data && data.log) { + try { + var log = String(data.log); + var lines = log.split('\n').map(function(l){ return l.trim(); }).filter(function(l){ return l.length>0; }); + var keywords = ['quota','rate limit','rate-limit','429','503','overloaded','high demand','busy','unavailable','error','failed','retry']; + var found = lines.find(function(l){ var ll=l.toLowerCase(); return keywords.some(function(k){ return ll.indexOf(k) !== -1; }); }); + if (found) msg += ' — ' + found; + else if (lines.length) msg += ' — ' + lines[0].slice(0,200); + } catch(e){} + } + showToast('Error: ' + msg, false, (data && data.log) ? (typeof data.log === 'string' ? data.log : JSON.stringify(data.log)) : null); + } + }).catch(function(err){ + showToast('Request failed: ' + err, false); + }).finally(function(){ + hideDescribeBusy(); + }); + } + function showModal(job, articleId, description, teaser, quoteDa, rawJson){ var existing = document.getElementById('mvlog-gemini-modal'); if (existing) existing.remove(); @@ -127,39 +255,7 @@ var job = el.dataset.job || ''; var articleId = el.dataset.id || ''; if (!job && !articleId) return; - showDescribeBusy(); - fetch('lib/generate_data_sync.php', { - method: 'POST', - credentials: 'same-origin', - headers: {'Content-Type':'application/x-www-form-urlencoded'}, - body: 'id=' + encodeURIComponent(articleId) + '&job=' + encodeURIComponent(job) + '&max_frames=16' - }).then(function(r){ return r.json().catch(()=>({})); }) - .then(function(data){ - if (data && data.status === 'ok'){ - var desc = data.description || (data.raw_json && JSON.stringify(data.raw_json)) || ''; - var teaser = data.teaser || ''; - var rawFirst = (Array.isArray(data.raw_json) && data.raw_json.length) ? data.raw_json[0] : data.raw_json; - var quoteDa = data.quote_da || (rawFirst && rawFirst.quote_da) || ''; - showModal(job, articleId, desc, teaser, quoteDa, data.raw_json || null); - } else { - var msg = (data && data.message) ? data.message : 'No response'; - if (data && data.log) { - try { - var log = String(data.log); - var lines = log.split('\n').map(function(l){ return l.trim(); }).filter(function(l){ return l.length>0; }); - var keywords = ['quota','rate limit','rate-limit','429','503','overloaded','high demand','busy','unavailable','error','failed','retry']; - var found = lines.find(function(l){ var ll=l.toLowerCase(); return keywords.some(function(k){ return ll.indexOf(k) !== -1; }); }); - if (found) msg += ' — ' + found; - else if (lines.length) msg += ' — ' + lines[0].slice(0,200); - } catch(e){} - } - showToast('Error: ' + msg, false, (data && data.log) ? (typeof data.log === 'string' ? data.log : JSON.stringify(data.log)) : null); - } -}).catch(function(err){ - showToast('Request failed: ' + err, false); - }).finally(function(){ - hideDescribeBusy(); - }); + showDescribePromptModal(job, articleId); }); attachDescribeButtons(); diff --git a/admin.php b/admin.php index 31bb58d..86bf9cb 100644 --- a/admin.php +++ b/admin.php @@ -1291,6 +1291,7 @@ $keywordValue = $rawKeyword; $filterPreview = !empty($_GET['f_preview']); $filterRender = !empty($_GET['f_render']); $filterShown = !empty($_GET['f_shown']); +$filterPermalink = !empty($_GET['f_permalink']); $statusFilterActive = $filterPreview || $filterRender || $filterShown || $filterPermalink; $searchEmpty = ($rawKeyword === '' && !$statusFilterActive); if ($rawKeyword !== '' && preg_match('/^\s*(date|location|place)\s*=\s*(.+)\s*$/i', $rawKeyword, $m)) { @@ -1299,7 +1300,7 @@ if ($rawKeyword !== '' && preg_match('/^\s*(date|location|place)\s*=\s*(.+)\s*$/ $keywordValue = trim((string)$m[2]); } if ($keywordValue !== '' || $statusFilterActive) { - $dirs = array_values(array_filter($dirs, function($dir) use ($keywordType, $keywordValue, $config, $statusFilterActive, $filterPreview, $filterRender, $filterShown) { + $dirs = array_values(array_filter($dirs, function($dir) use ($keywordType, $keywordValue, $config, $statusFilterActive, $filterPreview, $filterRender, $filterShown, $filterPermalink) { $data = read_data($dir); $state = input_dir_state($dir); $fullOutput = basename((string)($state['output'] ?? '')); @@ -1316,6 +1317,7 @@ if ($keywordValue !== '' || $statusFilterActive) { if ($filterPreview && $preview) $statusMatch = true; if ($filterRender && $enabled) $statusMatch = true; if ($filterShown && $visible) $statusMatch = true; + if ($filterPermalink && input_dir_permalink($dir)) $statusMatch = true; if (!$statusMatch) return false; } @@ -1349,7 +1351,7 @@ $runningJobs = active_worker_jobs($config);

Videos without input dir

No orphan videos.


MB
-

No input directories yet.

(string)($data['title'] ?? ''),'teaser'=>(string)($data['teaser'] ?? ''),'date'=>(string)($data['date'] ?? ''),'location'=>(string)($data['place'] ?? ''),'quote_da'=>(string)($data['quote_da'] ?? ''),'description'=>(string)($data['description'] ?? '')]; $currentSignature=$info['signature'] ?? ''; $fullFingerprint=is_array($state)?(string)($state['fingerprint'] ?? ''):''; $previewFingerprint=is_array($state)?(string)($state['preview_fingerprint'] ?? ''):''; $editedSinceRender=$hasFullVideo && $currentSignature !== '' && $fullFingerprint !== '' && $currentSignature !== $fullFingerprint; $editedSincePreview=!$hasFullVideo && $previewVideo && $currentSignature !== '' && $previewFingerprint !== '' && $currentSignature !== $previewFingerprint; $staleLabel=$editedSinceRender ? 'Edited since render' : ($editedSincePreview ? 'Edited since preview' : ''); ?>
Map
No video yet
Map

Thumb source:

+

No input directories yet.

(string)($data['title'] ?? ''),'teaser'=>(string)($data['teaser'] ?? ''),'date'=>(string)($data['date'] ?? ''),'location'=>(string)($data['place'] ?? ''),'quote_da'=>(string)($data['quote_da'] ?? ''),'description'=>(string)($data['description'] ?? '')]; $currentSignature=$info['signature'] ?? ''; $fullFingerprint=is_array($state)?(string)($state['fingerprint'] ?? ''):''; $previewFingerprint=is_array($state)?(string)($state['preview_fingerprint'] ?? ''):''; $editedSinceRender=$hasFullVideo && $currentSignature !== '' && $fullFingerprint !== '' && $currentSignature !== $fullFingerprint; $editedSincePreview=!$hasFullVideo && $previewVideo && $currentSignature !== '' && $previewFingerprint !== '' && $currentSignature !== $previewFingerprint; $staleLabel=$editedSinceRender ? 'Edited since render' : ($editedSincePreview ? 'Edited since preview' : ''); ?>
Map
No video yet
Map

Thumb source:

diff --git a/bin/generate_data.sh b/bin/generate_data.sh index 74ed5f8..959412f 100755 --- a/bin/generate_data.sh +++ b/bin/generate_data.sh @@ -4,10 +4,14 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" STATE_FILE="${SCRIPT_DIR}/.personality_index" -IN_DIR="${1:?Usage: $0 [\"Title\"] [\"Location\"] [max_frames]}" +IN_DIR="${1:?Usage: $0 [\"Title\"] [\"Location\"] [max_frames] [additional_prompt]}" TITLE="${2:-}" LOCATION="${3:-}" MAX_FRAMES="${4:-12}" +ADDITIONAL_PROMPT="${5:-}" +if [ "$ADDITIONAL_PROMPT" = "no-append" ]; then + ADDITIONAL_PROMPT="" +fi # Source system-wide Gemini API key if present so the webserver can run this as www-data. if [ -f /etc/mvlog/gemini.env ]; then @@ -117,6 +121,15 @@ if [ -z "$TITLE" ]; then TITLE="$(basename "$IN_DIR")" fi +ADDITIONAL_PROMPT_BLOCK="" +if [ -n "$ADDITIONAL_PROMPT" ]; then + ADDITIONAL_PROMPT_BLOCK=$(cat < 'error', 'message' => 'job not found']); + exit; +} + +$path = $jobdir . '/.mvlog-describe-prompt.txt'; +$prompt = is_file($path) ? trim((string)@file_get_contents($path)) : ''; + +echo json_encode(['status' => 'ok', 'additional_prompt' => $prompt], JSON_UNESCAPED_UNICODE); diff --git a/lib/generate_data.php b/lib/generate_data.php index a4d7024..840c11e 100644 --- a/lib/generate_data.php +++ b/lib/generate_data.php @@ -9,6 +9,7 @@ header('Content-Type: application/json; charset=utf-8'); $job = $_POST['job'] ?? ''; $max_frames = isset($_POST['max_frames']) ? intval($_POST['max_frames']) : 16; +$additional_prompt = trim((string)($_POST['additional_prompt'] ?? '')); if (!preg_match('/^[0-9]{8}[-_][A-Za-z0-9._-]+$/', $job)) { http_response_code(400); @@ -23,6 +24,14 @@ if (!is_dir($jobdir)) { exit; } +$describe_prompt_file = $jobdir . '/.mvlog-describe-prompt.txt'; +if ($additional_prompt !== '') { + @file_put_contents($describe_prompt_file, $additional_prompt . PHP_EOL); + @chmod($describe_prompt_file, 0664); + @chown($describe_prompt_file, 'www-data'); + @chgrp($describe_prompt_file, 'www-data'); +} + // Read existing data.txt for Title and Location (case-insensitive aliases) $data_file = $jobdir . '/data.txt'; $title = ''; @@ -135,7 +144,11 @@ if (!is_file($script)) { $log = $jobdir . '/generate_data.log'; $bash = '/bin/bash'; -$cmd = escapeshellcmd($bash) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($jobdir) . ' ' . escapeshellarg($title) . ' ' . escapeshellarg($location) . ' ' . escapeshellarg((string)$max_frames) . ' >> ' . escapeshellarg($log) . ' 2>&1 & echo $!'; +if ($additional_prompt !== '') { + $cmd = escapeshellcmd($bash) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($jobdir) . ' ' . escapeshellarg($title) . ' ' . escapeshellarg($location) . ' ' . escapeshellarg((string)$max_frames) . ' ' . escapeshellarg($additional_prompt) . ' >> ' . escapeshellarg($log) . ' 2>&1 & echo $!'; +} else { + $cmd = escapeshellcmd($bash) . ' ' . escapeshellarg($script) . ' ' . escapeshellarg($jobdir) . ' ' . escapeshellarg($title) . ' ' . escapeshellarg($location) . ' ' . escapeshellarg((string)$max_frames) . ' ' . escapeshellarg('no-append') . ' >> ' . escapeshellarg($log) . ' 2>&1 & echo $!'; +} $pid = trim(@shell_exec($cmd)); diff --git a/lib/generate_data_sync.php b/lib/generate_data_sync.php index 8f03a4d..9ed3f64 100644 --- a/lib/generate_data_sync.php +++ b/lib/generate_data_sync.php @@ -10,6 +10,7 @@ header('Content-Type: application/json; charset=utf-8'); $id = strtolower(trim((string)($_POST['id'] ?? ''))); $job = trim((string)($_POST['job'] ?? '')); $max_frames = isset($_POST['max_frames']) ? intval($_POST['max_frames']) : 16; +$additional_prompt = trim((string)($_POST['additional_prompt'] ?? '')); function mvlog_article_id_is_valid(string $id): bool { return (bool)preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $id); @@ -63,6 +64,14 @@ if ($jobdir === null || !is_dir($jobdir)) { exit; } +$describe_prompt_file = $jobdir . '/.mvlog-describe-prompt.txt'; +if ($additional_prompt !== '') { + @file_put_contents($describe_prompt_file, $additional_prompt . PHP_EOL); + @chmod($describe_prompt_file, 0664); + @chown($describe_prompt_file, 'www-data'); + @chgrp($describe_prompt_file, 'www-data'); +} + // Read existing data.txt for Title and Location (case-insensitive aliases) $data_file = $jobdir . '/data.txt'; $title = ''; @@ -101,8 +110,12 @@ if (!is_file($script)) { exit; } -// Build command and execute. Use no-append flag as last argument. -$cmd = escapeshellcmd($script) . ' ' . escapeshellarg($jobdir) . ' ' . escapeshellarg($title) . ' ' . escapeshellarg($location) . ' ' . escapeshellarg((string)$max_frames) . ' ' . escapeshellarg('no-append') . ' 2>&1'; +// Build command and execute. Pass optional additional prompt as the last argument. +if ($additional_prompt !== '') { + $cmd = escapeshellcmd($script) . ' ' . escapeshellarg($jobdir) . ' ' . escapeshellarg($title) . ' ' . escapeshellarg($location) . ' ' . escapeshellarg((string)$max_frames) . ' ' . escapeshellarg($additional_prompt) . ' 2>&1'; +} else { + $cmd = escapeshellcmd($script) . ' ' . escapeshellarg($jobdir) . ' ' . escapeshellarg($title) . ' ' . escapeshellarg($location) . ' ' . escapeshellarg((string)$max_frames) . ' ' . escapeshellarg('no-append') . ' 2>&1'; +} // Allow long-running @set_time_limit(0);