Raiju/includes/ai/image-job-runner.php
2026-10-25 06:05:42 +03:00

121 lines
5.9 KiB
PHP

<?php
/**
* Shared AI image job runner.
*
* Used by admin/api.php and the local CLI worker so long-running image
* generation does not have to occupy the PHP built-in web server request.
*/
function raijuAiRunImageJobCore(string $jobId, ?callable $assertAccess = null): array {
$job = raijuAiRefreshImageJobState(raijuAiLoadImageJob($jobId));
if ($assertAccess) $assertAccess($job);
$worker = fopen(raijuAiImageJobPath($jobId) . '.worker.lock', 'c');
if (!$worker) throw new RuntimeException('Could not lock image worker.');
if (!flock($worker, LOCK_EX | LOCK_NB)) { fclose($worker); return raijuAiPublicImageJob($job); }
try { return raijuAiRunImageJobClaimed($jobId, $assertAccess); }
finally { flock($worker, LOCK_UN); fclose($worker); }
}
function raijuAiRunImageJobClaimed(string $jobId, ?callable $assertAccess = null): array {
$existingJob = raijuAiLoadImageJob($jobId);
if ($assertAccess) {
$assertAccess($existingJob);
}
if ((string)($existingJob['status'] ?? '') === 'running') {
return raijuAiPublicImageJob($existingJob);
}
if (in_array((string)($existingJob['status'] ?? ''), ['success', 'error'], true)) {
return raijuAiPublicImageJob($existingJob);
}
$claimed = false;
$job = raijuAiMarkImageJobRunning($jobId, $claimed);
if (!$claimed) {
return raijuAiPublicImageJob($job);
}
$payload = is_array($job['payload'] ?? null) ? $job['payload'] : [];
$options = is_array($payload['options'] ?? null) ? $payload['options'] : [];
$options['_requestId'] = 'image_' . $jobId;
$prompt = trim((string)($payload['prompt'] ?? ''));
$kind = (string)($job['kind'] ?? 'dashboard');
try {
if ($kind === 'copilot') {
$contentPage = trim((string)($payload['contentPage'] ?? ''));
$fieldRef = trim((string)($payload['fieldRef'] ?? ''));
$instruction = trim((string)($payload['instruction'] ?? ''));
if ($contentPage === '' || $fieldRef === '' || $instruction === '' || $prompt === '') {
throw new RuntimeException('Image job is missing required copilot data.');
}
$settings = raijuAiLoadSettings(false);
raijuAiEnsureImageReady($settings, raijuAiImageServiceResolve($settings));
$context = raijuCopilotBuildContext($contentPage, raijuAiLoadSettings(true));
$fields = raijuCopilotAllowedImageFields($context, $fieldRef);
if (!$fields) {
throw new RuntimeException('Target field does not accept AI image generation.');
}
$field = $fields[0];
$result = raijuAiGenerateImage($prompt, $options);
$proposal = raijuCopilotImageProposal($context, $field['id'], $instruction, $result);
raijuAiAudit('copilot-generate-image', true, [
'jobId' => $jobId,
'contentPage' => $contentPage,
'path' => $proposal['path'],
'imageMode' => (string)($payload['imageMode'] ?? 'generate'),
'requestedCount' => (int)($options['count'] ?? 1),
'count' => count($proposal['paths'] ?? []),
'proposals' => raijuCopilotProposalAuditSummary([$proposal])
]);
return raijuAiFinishImageJob($jobId, [
'proposal' => $proposal,
'usage' => $result['usage'] ?? null,
'limits' => $result['limits'] ?? null,
'context' => $context
]);
}
if ($prompt === '') {
throw new RuntimeException('Image job prompt is missing.');
}
if ($kind === 'agent' && empty($options['storageDir'])) {
throw new RuntimeException('An agent image job needs its staging folder.');
}
$result = raijuAiGenerateImage($prompt, $options);
return raijuAiFinishImageJob($jobId, $result);
} catch (Throwable $e) {
$entry = raijuAiLoadUsage()['reservations']['image_' . $jobId] ?? [];
$tasks = $entry['tasks'] ?? [];
$pending = array_filter($tasks, fn($task) => !in_array($task['state'] ?? '', ['success', 'fail', 'failed', 'error'], true));
if ($tasks && !array_filter($tasks, fn($task) => empty($task['id'])) && ($pending || !empty($entry['outputs'])) && (int)($job['attempts'] ?? 0) < 10) {
raijuJsonUpdate(raijuAiImageJobPath($jobId), static function (&$current) use ($e) {
$current['status'] = 'queued';
$current['retryAt'] = time() + 60;
$current['updatedAt'] = gmdate('c');
$current['error'] = raijuAiUtf8Prefix($e->getMessage(), 1000);
});
return raijuAiPublicImageJob(raijuAiLoadImageJob($jobId));
}
if ($kind === 'dashboard') {
$settings = raijuAiLoadSettings(false);
raijuAiRecordImageHistory([
'status' => 'error',
'model' => (string)($options['model'] ?? raijuAiEffectiveImageModel($settings)),
'prompt' => $prompt,
'size' => (string)($options['size'] ?? ''),
'aspectRatio' => (string)($options['aspectRatio'] ?? ''),
'quality' => (string)($options['quality'] ?? ''),
'format' => (string)($options['outputFormat'] ?? ''),
'moderation' => (string)($options['moderation'] ?? ''),
'compression' => (int)($options['outputCompression'] ?? 0),
'count' => (int)($options['count'] ?? 0),
'referenceImages' => raijuAiPublicReferenceList($options),
'outputs' => [],
'error' => $e->getMessage(),
'estimatedCostCents' => 0
]);
}
raijuAiAudit($kind === 'copilot' ? 'copilot-generate-image' : 'image', false, ['jobId' => $jobId, 'message' => $e->getMessage()]);
return raijuAiFailImageJob($jobId, $e->getMessage());
}
}