Files
ai-stylegallery/app/Api/Plugins/ComfyUi.php

263 lines
11 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 ?? [];
// 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'], '/');
try {
$response = Http::timeout(5)->get($apiUrl . '/queue');
return $response->successful();
} catch (\Exception $e) {
$this->logError('ComfyUI connection test failed.', ['error' => $e->getMessage()]);
return false;
}
}
public function searchModels(string $searchTerm): array
{
$this->logInfo('ComfyUI does not support model search. Returning empty list.', ['searchTerm' => $searchTerm]);
return [];
}
}