Compare commits

..

No commits in common. "e1a54259df029ca2f43de74f7293e1da15662f7f" and "981c56ed24f92f5a2f60d713507c7c4bcb440246" have entirely different histories.

4 changed files with 25 additions and 92 deletions

View file

@ -2,8 +2,6 @@
<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>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> <script>
(function(){ (function(){
const DESCRIBE_MAX_FRAMES = 32;
function attachDescribeButtons(){ function attachDescribeButtons(){
document.querySelectorAll('.admin-video-row').forEach(function(row){ document.querySelectorAll('.admin-video-row').forEach(function(row){
if (row.querySelector('.describe-button')) return; if (row.querySelector('.describe-button')) return;
@ -161,7 +159,7 @@
function runDescribeRequest(job, articleId, additionalPrompt, selectedPersonality){ function runDescribeRequest(job, articleId, additionalPrompt, selectedPersonality){
showDescribeBusy(); showDescribeBusy();
var body = 'id=' + encodeURIComponent(articleId) + '&job=' + encodeURIComponent(job) + '&max_frames=' + encodeURIComponent(DESCRIBE_MAX_FRAMES); var body = 'id=' + encodeURIComponent(articleId) + '&job=' + encodeURIComponent(job) + '&max_frames=16';
body += '&personality=' + encodeURIComponent(selectedPersonality || 'auto'); body += '&personality=' + encodeURIComponent(selectedPersonality || 'auto');
if (additionalPrompt && additionalPrompt.trim()) { if (additionalPrompt && additionalPrompt.trim()) {
body += '&additional_prompt=' + encodeURIComponent(additionalPrompt); 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]}" IN_DIR="${1:?Usage: $0 <input-dir> [\"Title\"] [\"Location\"] [max_frames] [additional_prompt]}"
TITLE="${2:-}" TITLE="${2:-}"
LOCATION="${3:-}" LOCATION="${3:-}"
MAX_FRAMES="${4:-16}" MAX_FRAMES="${4:-12}"
ADDITIONAL_PROMPT="${5:-}" ADDITIONAL_PROMPT="${5:-}"
SELECTED_PERSONALITY="${6:-}" SELECTED_PERSONALITY="${6:-}"
if [ "$ADDITIONAL_PROMPT" = "no-append" ]; then if [ "$ADDITIONAL_PROMPT" = "no-append" ]; then
@ -202,6 +202,14 @@ PROMPT=$(cat <<EOF
Look at these captured frames from a motorcycle trip. Look at these captured frames from a motorcycle trip.
You are writing as the main character of a motorcycle/travel blog. 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: Use the following personality style, but never mention the character name:
"$PERSONALITY" "$PERSONALITY"
@ -213,22 +221,25 @@ ${ADDITIONAL_PROMPT_BLOCK}
Create valid JSON only. Create valid JSON only.
Required fields: Required fields:
- 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"
- personality: exactly "$PERSONALITY_KEY", used for teaser and description generation. - quote_da: exactly one natural Danish sentence
- teaser: English, max 32 words - teaser: English, max 32 words
- description: English, up to 448 words - description: English, up to 448 words
- tags: 5 to 8 lowercase short tags - tags: 5 to 8 lowercase short tags
Content rules: Content rules:
- Use the selected personality at maximum intensity. - Do not simply summarize the trip.
- Do not be subtle. - Try to be positive, unless the personality is inherently negative, in which case embrace that negativity fully.
- The voice MUST BE provocative, opinionated, funny, and slightly over the top.
- Highlight the funniest, strangest, most beautiful, or most ridiculous visible aspect. - 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. - Be challenging, provocative, witty, and playful.
- Roasting is allowed. - Roasting is allowed.
- Profanity is allowed only if it fits the selected personality. - Profanity is allowed only if it fits the selected personality.
- Do not invent specific facts that are not visible or provided. - 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. 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. It's ok to use the same word mutliple times. Don't use them in teaser, only in description.
JSON rules: JSON rules:
- Output JSON only. - Output JSON only.

View file

@ -4,73 +4,8 @@ mvlog_require_login();
$config = require __DIR__ . '/config.php'; $config = require __DIR__ . '/config.php';
header('Content-Type: application/json; charset=UTF-8'); header('Content-Type: application/json; charset=UTF-8');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); 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 = []; $jobs = [];
$switches = []; $switches = [];
$cleaned = [];
$workerRunning = mvlog_worker_running();
$dirs = glob($config['uploads_dir'] . '/*', GLOB_ONLYDIR) ?: []; $dirs = glob($config['uploads_dir'] . '/*', GLOB_ONLYDIR) ?: [];
foreach ($dirs as $dir) { foreach ($dirs as $dir) {
$name = basename($dir); $name = basename($dir);
@ -78,31 +13,21 @@ foreach ($dirs as $dir) {
$lockDir = $dir . '/.movmaker-lock'; $lockDir = $dir . '/.movmaker-lock';
$idFile = $dir . '/.mvlog-id'; $idFile = $dir . '/.mvlog-id';
$state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : []; $state = is_file($stateFile) ? json_decode((string)file_get_contents($stateFile), true) : [];
if (!is_array($state)) $state = []; $status = is_array($state) ? (string)($state['status'] ?? '') : '';
$status = (string)($state['status'] ?? ''); $articleId = is_array($state) ? strtolower((string)($state['article_id'] ?? '')) : '';
$articleId = strtolower((string)($state['article_id'] ?? ''));
if ($articleId === '' && is_file($idFile)) $articleId = strtolower(trim((string)file_get_contents($idFile))); 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 = ''; 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[] = [ $switches[] = [
'dir' => $name, 'dir' => $name,
'id' => $articleId, 'id' => $articleId,
'enabled' => is_file($dir . '/.movmaker-enabled'), 'enabled' => is_file($dir . '/.movmaker-enabled'),
'preview' => is_file($dir . '/.movmaker-preview'), 'preview' => is_file($dir . '/.movmaker-preview'),
]; ];
$locked = is_dir($lockDir);
if ($locked || $status === 'processing') { if ($locked || $status === 'processing') {
$started = ''; $started = '';
if (is_file($lockDir . '/started_at')) $started = trim((string)file_get_contents($lockDir . '/started_at')); 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'] ?? ''); if ($started === '' && is_array($state)) $started = (string)($state['started_at'] ?? $state['updated_at'] ?? '');
$jobs[] = [ $jobs[] = [
'name' => $name, 'name' => $name,
'id' => $articleId, 'id' => $articleId,
@ -111,4 +36,4 @@ foreach ($dirs as $dir) {
]; ];
} }
} }
echo json_encode(['jobs' => $jobs, 'switches' => $switches, 'cleaned' => $cleaned, 'worker_running' => $workerRunning, 'checked_at' => date('c')], JSON_UNESCAPED_UNICODE); echo json_encode(['jobs' => $jobs, 'switches' => $switches, 'checked_at' => date('c')], JSON_UNESCAPED_UNICODE);

View file

@ -5,12 +5,11 @@ require __DIR__ . '/../auth.php';
mvlog_require_login(); mvlog_require_login();
$MVLOG_ROOT = dirname(__DIR__); $MVLOG_ROOT = dirname(__DIR__);
const DESCRIBE_DEFAULT_MAX_FRAMES = 32;
header('Content-Type: application/json; charset=utf-8'); header('Content-Type: application/json; charset=utf-8');
$id = strtolower(trim((string)($_POST['id'] ?? ''))); $id = strtolower(trim((string)($_POST['id'] ?? '')));
$job = trim((string)($_POST['job'] ?? '')); $job = trim((string)($_POST['job'] ?? ''));
$max_frames = isset($_POST['max_frames']) ? intval($_POST['max_frames']) : DESCRIBE_DEFAULT_MAX_FRAMES; $max_frames = isset($_POST['max_frames']) ? intval($_POST['max_frames']) : 16;
$additional_prompt = trim((string)($_POST['additional_prompt'] ?? '')); $additional_prompt = trim((string)($_POST['additional_prompt'] ?? ''));
$personality = strtolower(trim((string)($_POST['personality'] ?? 'auto'))); $personality = strtolower(trim((string)($_POST['personality'] ?? 'auto')));
if (!preg_match('/^[a-z0-9_-]+$/', $personality)) $personality = 'auto'; if (!preg_match('/^[a-z0-9_-]+$/', $personality)) $personality = 'auto';