230 lines
9.6 KiB
PHP
230 lines
9.6 KiB
PHP
<?php
|
|
|
|
namespace App\Api\Plugins;
|
|
|
|
use App\Models\ApiProvider;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
class ComfyUi implements ApiPluginInterface
|
|
{
|
|
use LoggablePlugin;
|
|
|
|
protected $apiProvider;
|
|
|
|
public function __construct(ApiProvider $apiProvider)
|
|
{
|
|
$this->apiProvider = $apiProvider;
|
|
$this->logInfo('ComfyUi plugin initialized.', ['provider_name' => $apiProvider->name]);
|
|
}
|
|
|
|
public function getIdentifier(): string
|
|
{
|
|
return 'comfyui';
|
|
}
|
|
|
|
public function getName(): string
|
|
{
|
|
return 'ComfyUI';
|
|
}
|
|
|
|
public function isEnabled(): bool
|
|
{
|
|
return $this->apiProvider->enabled;
|
|
}
|
|
|
|
public function enable(): bool
|
|
{
|
|
$this->apiProvider->enabled = true;
|
|
$result = $this->apiProvider->save();
|
|
if ($result) {
|
|
$this->logInfo('ComfyUi plugin enabled.', ['provider_name' => $this->apiProvider->name]);
|
|
} else {
|
|
$this->logError('Failed to enable ComfyUi plugin.', ['provider_name' => $this->apiProvider->name]);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public function disable(): bool
|
|
{
|
|
$this->apiProvider->enabled = false;
|
|
$result = $this->apiProvider->save();
|
|
if ($result) {
|
|
$this->logInfo('ComfyUi plugin disabled.', ['provider_name' => $this->apiProvider->name]);
|
|
} else {
|
|
$this->logError('Failed to disable ComfyUi plugin.', ['provider_name' => $this->apiProvider->name]);
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
public function getStatus(string $imageUUID): array
|
|
{
|
|
$this->logDebug('Getting status for image.', ['image_uuid' => $imageUUID]);
|
|
// Implement ComfyUI specific status check
|
|
return ['status' => 'unknown'];
|
|
}
|
|
|
|
public function getProgress(string $imageUUID): array
|
|
{
|
|
$this->logDebug('Progress updates are handled via WebSocket.', ['image_uuid' => $imageUUID]);
|
|
return ['progress' => 0]; // Progress is now handled by WebSocket
|
|
}
|
|
|
|
private function getHistory(string $promptId): array
|
|
{
|
|
// This method is no longer used for progress polling, but might be used for final result retrieval
|
|
$apiUrl = rtrim($this->apiProvider->api_url, '/');
|
|
$timeout = 60; // seconds
|
|
$this->logDebug('ComfyUI History API URL:', ['url' => $apiUrl . '/history/' . $promptId, 'timeout' => $timeout]);
|
|
$response = Http::timeout($timeout)->get($apiUrl . '/history/' . $promptId);
|
|
if ($response->failed()) {
|
|
throw new \Exception('Failed to get history from ComfyUI');
|
|
}
|
|
return $response->json();
|
|
}
|
|
|
|
public function processImageStyleChange(\App\Models\Image $image, \App\Models\Style $style): array
|
|
{
|
|
$this->logInfo('Starting ComfyUI style change process.', ['image_id' => $image->id, 'style_id' => $style->id]);
|
|
|
|
// 1. Upload image to ComfyUI
|
|
$uploadResponse = $this->uploadImage(public_path('storage/' . $image->path));
|
|
$filename = $uploadResponse['name'];
|
|
|
|
// 2. Construct the prompt
|
|
$promptData = $this->constructPrompt($style, $filename);
|
|
|
|
// 3. Queue the prompt
|
|
$queueResponse = $this->queuePrompt($promptData);
|
|
$promptId = $queueResponse['prompt_id'];
|
|
|
|
// Return the prompt_id for frontend WebSocket tracking
|
|
return ['prompt_id' => $promptId];
|
|
}
|
|
|
|
private function uploadImage(string $imagePath): array
|
|
{
|
|
$this->logInfo('Uploading image to ComfyUI.', ['image_path' => $imagePath]);
|
|
$response = Http::attach(
|
|
'image', file_get_contents($imagePath), basename($imagePath)
|
|
)->timeout(60)->post(rtrim($this->apiProvider->api_url, '/') . '/upload/image', [
|
|
'type' => 'input',
|
|
'overwrite' => 'false',
|
|
]);
|
|
|
|
if ($response->failed()) {
|
|
$this->logError('ComfyUI image upload failed.', ['response' => $response->body()]);
|
|
throw new \Exception('Failed to upload image to ComfyUI');
|
|
}
|
|
|
|
return $response->json();
|
|
}
|
|
|
|
private function constructPrompt(\App\Models\Style $style, string $filename): array
|
|
{
|
|
$modelParams = $style->aiModel->parameters ?? [];
|
|
$styleParams = $style->parameters ?? [];
|
|
|
|
if (empty($modelParams) && empty($styleParams)) {
|
|
throw new \Exception('ComfyUI workflow (parameters) is missing.');
|
|
}
|
|
|
|
// Use array_replace_recursive for a deep merge
|
|
$mergedParams = array_replace_recursive($modelParams, $styleParams);
|
|
$workflow = json_encode($mergedParams);
|
|
|
|
// Properly escape the values for JSON injection
|
|
$prompt = substr(json_encode($style->prompt, JSON_UNESCAPED_SLASHES), 1, -1);
|
|
$filename_escaped = substr(json_encode($filename, JSON_UNESCAPED_SLASHES), 1, -1);
|
|
$modelId_escaped = substr(json_encode($style->aiModel->model_id, JSON_UNESCAPED_SLASHES), 1, -1);
|
|
|
|
$workflow = str_replace('__PROMPT__', $prompt, $workflow);
|
|
$workflow = str_replace('__FILENAME__', $filename_escaped, $workflow);
|
|
$workflow = str_replace('__MODEL_ID__', $modelId_escaped, $workflow);
|
|
|
|
$decodedWorkflow = json_decode($workflow, true);
|
|
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
$this->logError('Failed to decode workflow JSON after placeholder replacement.', [
|
|
'json_error' => json_last_error_msg(),
|
|
'workflow_string' => $workflow
|
|
]);
|
|
throw new \Exception('Failed to construct valid ComfyUI workflow JSON: ' . json_last_error_msg());
|
|
}
|
|
|
|
return $decodedWorkflow;
|
|
}
|
|
|
|
private function queuePrompt(array $promptData): array
|
|
{
|
|
$this->logInfo('Queueing prompt in ComfyUI.');
|
|
$response = Http::timeout(60)->post(rtrim($this->apiProvider->api_url, '/') . '/prompt', ['prompt' => $promptData]);
|
|
|
|
if ($response->failed()) {
|
|
$this->logError('Failed to queue prompt in ComfyUI.', ['response' => $response->body()]);
|
|
throw new \Exception('Failed to queue prompt in ComfyUI');
|
|
}
|
|
|
|
return $response->json();
|
|
}
|
|
|
|
public function waitForResult(string $promptId): string
|
|
{
|
|
set_time_limit(120); // Set maximum execution time for this function
|
|
$this->logInfo('waitForResult: Waiting for ComfyUI result.', ['prompt_id' => $promptId]);
|
|
$startTime = microtime(true);
|
|
$timeout = 180; // seconds
|
|
|
|
while (true) {
|
|
if (microtime(true) - $startTime > $timeout) {
|
|
$this->logError('waitForResult: ComfyUI result polling timed out.', ['prompt_id' => $promptId]);
|
|
throw new \Exception('ComfyUI result polling timed out.');
|
|
}
|
|
|
|
try {
|
|
$response = Http::timeout(60)->get(rtrim($this->apiProvider->api_url, '/') . '/history/' . $promptId);
|
|
$this->logDebug('waitForResult: History API response status.', ['status' => $response->status(), 'prompt_id' => $promptId]);
|
|
|
|
if ($response->failed()) {
|
|
$this->logError('waitForResult: Failed to get history from ComfyUI.', ['prompt_id' => $promptId, 'response' => $response->body()]);
|
|
throw new \Exception('Failed to get history from ComfyUI');
|
|
}
|
|
|
|
$data = $response->json();
|
|
$this->logDebug('waitForResult: History API response data.', ['data' => $data, 'prompt_id' => $promptId]);
|
|
|
|
if (isset($data[$promptId]['outputs'])) {
|
|
$outputs = $data[$promptId]['outputs'];
|
|
$this->logInfo('waitForResult: Found outputs in history.', ['prompt_id' => $promptId, 'outputs_count' => count($outputs)]);
|
|
|
|
foreach ($outputs as $output) {
|
|
if (isset($output['images'][0]['type']) && $output['images'][0]['type'] === 'output') {
|
|
$imageUrl = sprintf('%s/view?filename=%s&subfolder=%s&type=output',
|
|
rtrim($this->apiProvider->api_url, '/'),
|
|
$output['images'][0]['filename'],
|
|
$output['images'][0]['subfolder']
|
|
);
|
|
$this->logInfo('waitForResult: Constructed image URL.', ['imageUrl' => $imageUrl, 'prompt_id' => $promptId]);
|
|
|
|
$imageResponse = Http::timeout(60)->get($imageUrl);
|
|
$this->logDebug('waitForResult: Image fetch response status.', ['status' => $imageResponse->status(), 'imageUrl' => $imageUrl]);
|
|
|
|
if ($imageResponse->failed()) {
|
|
$this->logError('waitForResult: Failed to retrieve image data from ComfyUI.', ['imageUrl' => $imageUrl, 'response' => $imageResponse->body()]);
|
|
throw new \Exception('Failed to retrieve image data from ComfyUI');
|
|
}
|
|
$this->logInfo('waitForResult: Successfully retrieved image data.', ['prompt_id' => $promptId]);
|
|
return base64_encode($imageResponse->body());
|
|
}
|
|
}
|
|
} else {
|
|
$this->logDebug('waitForResult: No outputs found yet for prompt.', ['prompt_id' => $promptId]);
|
|
}
|
|
} catch (\Exception $e) {
|
|
$this->logError('waitForResult: Exception caught during polling.', ['prompt_id' => $promptId, 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString()]);
|
|
throw $e; // Re-throw the exception to be caught by ImageController
|
|
}
|
|
|
|
usleep(500000); // Wait for 0.5 seconds before polling again
|
|
}
|
|
}
|
|
} |