121 lines
5.9 KiB
PHP
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());
|
|
}
|
|
}
|