Compare commits

..

2 commits

Author SHA1 Message Date
hbrain
e1a54259df Frames for describe increased to 32 2026-06-21 10:36:35 +02:00
hbrain
2ef84cf496 Render process check and stale job cleanup on ajax req 2026-06-21 09:13:10 +02:00
4 changed files with 92 additions and 25 deletions

View file

@ -2,6 +2,8 @@
<script>document.querySelectorAll('textarea[data-counter]').forEach(function(t){var c=document.getElementById(t.dataset.counter);function u(){c.textContent=t.value.length+' / '+t.maxLength+' characters';}t.addEventListener('input',u);u();});</script>
<script>
(function(){
const DESCRIBE_MAX_FRAMES = 32;
function attachDescribeButtons(){
document.querySelectorAll('.admin-video-row').forEach(function(row){
if (row.querySelector('.describe-button')) return;
@ -159,7 +161,7 @@
function runDescribeRequest(job, articleId, additionalPrompt, selectedPersonality){
showDescribeBusy();
var body = 'id=' + encodeURIComponent(articleId) + '&job=' + encodeURIComponent(job) + '&max_frames=16';
var body = 'id=' + encodeURIComponent(articleId) + '&job=' + encodeURIComponent(job) + '&max_frames=' + encodeURIComponent(DESCRIBE_MAX_FRAMES);
body += '&personality=' + encodeURIComponent(selectedPersonality || 'auto');
if (additionalPrompt && additionalPrompt.trim()) {
body += '&additional_prompt=' + encodeURIComponent(additionalPrompt);

View file

@ -7,7 +7,7 @@ STATE_FILE="${SCRIPT_DIR}/.personality_index"
IN_DIR="${1:?Usage: $0 <input-dir> [\"Title\"] [\"Location\"] [max_frames] [additional_prompt]}"
TITLE="${2:-}"
LOCATION="${3:-}"
MAX_FRAMES="${4:-12}"
MAX_FRAMES="${4:-16}"
ADDITIONAL_PROMPT="${5:-}"
SELECTED_PERSONALITY="${6:-}"
if [ "$ADDITIONAL_PROMPT" = "no-append" ]; then
@ -202,14 +202,6 @@ PROMPT=$(cat <<EOF
Look at these captured frames from a motorcycle trip.
You are writing as the main character of a motorcycle/travel blog.
Style intensity:
Use the selected personality at maximum intensity.
Do not be subtle.
Do not merely sprinkle the style on top.
Every sentence should feel shaped by that personality.
The voice should be provocative, opinionated, funny, and slightly over the top.
Roast the situation, the road, the weather, the narrator, or visible absurdities.
Avoid generic travel-blog language.
Use the following personality style, but never mention the character name:
"$PERSONALITY"
@ -221,25 +213,22 @@ ${ADDITIONAL_PROMPT_BLOCK}
Create valid JSON only.
Required fields:
- personality: exactly "$PERSONALITY_KEY"
- quote_da: exactly one natural Danish sentence
- quote_da: Write exactly one sentence in Danish in the style of Anders Matthesen. The sentence should sound like a sharp, self-deprecating observation from a middle-aged motorcycle traveler who often ends up in absurd or unexpected situations.
- personality: exactly "$PERSONALITY_KEY", used for teaser and description generation.
- teaser: English, max 32 words
- description: English, up to 448 words
- tags: 5 to 8 lowercase short tags
Content rules:
- Do not simply summarize the trip.
- Try to be positive, unless the personality is inherently negative, in which case embrace that negativity fully.
- Use the selected personality at maximum intensity.
- Do not be subtle.
- The voice MUST BE provocative, opinionated, funny, and slightly over the top.
- Highlight the funniest, strangest, most beautiful, or most ridiculous visible aspect.
- The description must have a beginning, middle, and end.
- Mention the overall experience, mood, road, weather, landscape, and travel feeling if visible.
- Include at least one memorable or amusing observation.
- End with a concluding thought.
- Be challenging, provocative, witty, and playful.
- Roasting is allowed.
- Profanity is allowed only if it fits the selected personality.
- Do not invent specific facts that are not visible or provided.
- Description or teaser MUST contain at least one of the following words (preferably more): subscribe, contact, journal. It's ok to use the same word mutliple times. Don't use them in teaser, only in description.
- Description or teaser MUST contain at least one of the following words (preferably more): subscribe, contact, journal. Don't use them in teaser, only in description.
JSON rules:
- Output JSON only.

View file

@ -4,8 +4,73 @@ mvlog_require_login();
$config = require __DIR__ . '/config.php';
header('Content-Type: application/json; charset=UTF-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
function mvlog_job_status_log(string $message): void {
$log = __DIR__ . '/logs/job_status_cleanup.log';
$line = '[' . date('c') . '] ' . $message . "\n";
@file_put_contents($log, $line, FILE_APPEND | LOCK_EX);
@chmod($log, 0664);
}
function mvlog_worker_running(): bool {
$out = [];
$rc = 1;
@exec('systemctl is-active --quiet mvlog-worker.service 2>/dev/null', $out, $rc);
if ($rc === 0) return true;
$ps = @shell_exec('ps -eo args= 2>/dev/null');
if (!is_string($ps) || $ps === '') return false;
foreach (explode("\n", $ps) as $line) {
if (strpos($line, 'mvlog_worker.py') !== false && strpos($line, '--remote-root /var/www/html') !== false) {
return true;
}
}
return false;
}
function mvlog_rm_rf(string $path): void {
if (!is_dir($path)) return;
$items = scandir($path);
if (!is_array($items)) return;
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$child = $path . '/' . $item;
if (is_dir($child) && !is_link($child)) mvlog_rm_rf($child);
else @unlink($child);
}
@rmdir($path);
}
function mvlog_cleanup_stale_processing(string $dir, string $name, array $state, string $lockDir, bool $locked, string $status, string $articleId): array {
$stateFile = $dir . '/.movmaker-state.json';
$started = '';
if (is_file($lockDir . '/started_at')) $started = trim((string)@file_get_contents($lockDir . '/started_at'));
if ($started === '') $started = (string)($state['started_at'] ?? $state['updated_at'] ?? '');
$backup = '';
if (is_file($stateFile)) {
$backup = $stateFile . '.bak.' . date('YmdHis');
@copy($stateFile, $backup);
@chmod($backup, 0664);
}
$state['status'] = 'error';
$state['updated_at'] = gmdate('c');
if ($articleId !== '') $state['article_id'] = $articleId;
$state['message'] = 'Previous render appears abandoned: no mvlog-worker process is running; stale lock/processing state was cleaned by job_status.php. Re-enable Render to retry.';
$json = json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
@file_put_contents($stateFile, $json, LOCK_EX);
@chmod($stateFile, 0664);
if ($locked) mvlog_rm_rf($lockDir);
mvlog_job_status_log('cleaned stale processing job=' . $name . ' article_id=' . ($articleId ?: '-') . ' started_at=' . ($started ?: '-') . ' status=' . ($status ?: '-') . ' lock=' . ($locked ? 'yes' : 'no') . ' backup=' . ($backup ?: '-'));
return $state;
}
$jobs = [];
$switches = [];
$cleaned = [];
$workerRunning = mvlog_worker_running();
$dirs = glob($config['uploads_dir'] . '/*', GLOB_ONLYDIR) ?: [];
foreach ($dirs as $dir) {
$name = basename($dir);
@ -13,21 +78,31 @@ foreach ($dirs as $dir) {
$lockDir = $dir . '/.movmaker-lock';
$idFile = $dir . '/.mvlog-id';
$state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : [];
$status = is_array($state) ? (string)($state['status'] ?? '') : '';
$articleId = is_array($state) ? strtolower((string)($state['article_id'] ?? '')) : '';
if (!is_array($state)) $state = [];
$status = (string)($state['status'] ?? '');
$articleId = strtolower((string)($state['article_id'] ?? ''));
if ($articleId === '' && is_file($idFile)) $articleId = strtolower(trim((string)file_get_contents($idFile)));
if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) $articleId = '';
$locked = is_dir($lockDir);
if (!$workerRunning && ($locked || $status === 'processing')) {
$state = mvlog_cleanup_stale_processing($dir, $name, $state, $lockDir, $locked, $status, $articleId);
$status = (string)($state['status'] ?? '');
$locked = is_dir($lockDir);
$cleaned[] = ['dir' => $name, 'id' => $articleId, 'status' => $status];
}
$switches[] = [
'dir' => $name,
'id' => $articleId,
'enabled' => is_file($dir . '/.movmaker-enabled'),
'preview' => is_file($dir . '/.movmaker-preview'),
];
$locked = is_dir($lockDir);
if ($locked || $status === 'processing') {
$started = '';
if (is_file($lockDir . '/started_at')) $started = trim((string)file_get_contents($lockDir . '/started_at'));
if ($started === '' && is_array($state)) $started = (string)($state['started_at'] ?? $state['updated_at'] ?? '');
if ($started === '') $started = (string)($state['started_at'] ?? $state['updated_at'] ?? '');
$jobs[] = [
'name' => $name,
'id' => $articleId,
@ -36,4 +111,4 @@ foreach ($dirs as $dir) {
];
}
}
echo json_encode(['jobs' => $jobs, 'switches' => $switches, 'checked_at' => date('c')], JSON_UNESCAPED_UNICODE);
echo json_encode(['jobs' => $jobs, 'switches' => $switches, 'cleaned' => $cleaned, 'worker_running' => $workerRunning, 'checked_at' => date('c')], JSON_UNESCAPED_UNICODE);

View file

@ -5,11 +5,12 @@ require __DIR__ . '/../auth.php';
mvlog_require_login();
$MVLOG_ROOT = dirname(__DIR__);
const DESCRIBE_DEFAULT_MAX_FRAMES = 32;
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;
$max_frames = isset($_POST['max_frames']) ? intval($_POST['max_frames']) : DESCRIBE_DEFAULT_MAX_FRAMES;
$additional_prompt = trim((string)($_POST['additional_prompt'] ?? ''));
$personality = strtolower(trim((string)($_POST['personality'] ?? 'auto')));
if (!preg_match('/^[a-z0-9_-]+$/', $personality)) $personality = 'auto';