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 ?? []; // Ensure both parameters are arrays, decode JSON if needed if (is_string($modelParams)) { $modelParams = json_decode($modelParams, true); if (json_last_error() !== JSON_ERROR_NONE) { $modelParams = []; } } if (is_string($styleParams)) { $styleParams = json_decode($styleParams, true); if (json_last_error() !== JSON_ERROR_NONE) { $styleParams = []; } } 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 } } public function testConnection(array $data): bool { $apiUrl = rtrim($data['api_url'] ?? '', '/'); if (! $apiUrl) { throw new \RuntimeException('ComfyUI API-URL fehlt.'); } try { $response = Http::timeout(5)->get($apiUrl.'/queue'); if ($response->successful()) { return true; } $body = $response->body(); $snippet = $body ? Str::limit($body, 200) : null; $this->logError('ComfyUI connection test failed: HTTP error.', [ 'status' => $response->status(), 'body' => $snippet, ]); throw new \RuntimeException('ComfyUI HTTP '.$response->status().($snippet ? ': '.$snippet : '')); } catch (\Exception $e) { $this->logError('ComfyUI connection test failed.', ['error' => $e->getMessage()]); throw $e; } } public function checkAvailability(): array { $this->logInfo('Checking ComfyUI availability.'); if (! $this->apiProvider->enabled) { $this->logDebug('ComfyUI provider is disabled.'); return [ 'available' => false, 'reason' => 'Provider is disabled', 'provider_id' => $this->apiProvider->id, 'provider_name' => $this->apiProvider->name, ]; } if (empty($this->apiProvider->api_url)) { $this->logDebug('ComfyUI API URL is not configured.'); return [ 'available' => false, 'reason' => 'API URL not configured', 'provider_id' => $this->apiProvider->id, 'provider_name' => $this->apiProvider->name, ]; } try { $response = Http::timeout(5)->get(rtrim($this->apiProvider->api_url, '/').'/queue'); if ($response->successful()) { $this->logInfo('ComfyUI is available.'); return [ 'available' => true, 'reason' => 'Connection successful', 'provider_id' => $this->apiProvider->id, 'provider_name' => $this->apiProvider->name, ]; } else { $this->logError('ComfyUI connection failed.', ['status' => $response->status()]); return [ 'available' => false, 'reason' => 'Connection failed: '.$response->status(), 'provider_id' => $this->apiProvider->id, 'provider_name' => $this->apiProvider->name, ]; } } catch (\Exception $e) { $this->logError('ComfyUI availability check failed.', ['error' => $e->getMessage()]); return [ 'available' => false, 'reason' => 'Connection error: '.$e->getMessage(), 'provider_id' => $this->apiProvider->id, 'provider_name' => $this->apiProvider->name, ]; } } public function searchModels(string $searchTerm): array { $this->logInfo('ComfyUI does not support model search. Returning empty list.', ['searchTerm' => $searchTerm]); return []; } public function getStyledImage(string $promptId): string { return $this->waitForResult($promptId); } }