diff --git a/example/example.dart b/example/example.dart index eca1c1d..ba581bc 100644 --- a/example/example.dart +++ b/example/example.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:comfyui_api_sdk/comfyui_api_sdk.dart'; import 'package:uuid/uuid.dart'; -import 'package:comfyui_api_sdk/src/prompt_builder.dart'; bool jobFinished = false; diff --git a/lib/src/_http_endpoints.dart b/lib/src/_http_endpoints.dart new file mode 100644 index 0000000..ef8b84a --- /dev/null +++ b/lib/src/_http_endpoints.dart @@ -0,0 +1,223 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:logger/logger.dart'; + +import 'exceptions/comfyui_api_exception.dart'; +import 'models/history_response.dart'; +import 'models/checkpoint.dart'; +import 'models/vae.dart'; +import 'models/lora.dart'; +import 'models/submit_prompt_response.dart'; +import 'websocket_manager.dart'; // Needed for submitPrompt trigger + +/// Internal helper class for handling ComfyUI HTTP API endpoints. +class ComfyApiHttpEndpoints { + final String host; + final http.Client _httpClient; + final Logger logger; + final WebSocketManager _webSocketManager; // Needed for submitPrompt trigger + + ComfyApiHttpEndpoints({ + required this.host, + required http.Client httpClient, + required this.logger, + required WebSocketManager webSocketManager, + }) : _httpClient = httpClient, + _webSocketManager = webSocketManager; + + /// Gets the current queue status + Future> getQueue() async { + final response = await _httpClient.get(Uri.parse('$host/queue')); + _validateResponse(response); + return jsonDecode(response.body); + } + + /// Gets the history of the queue + Future getHistory({int maxItems = 64}) async { + final response = await _httpClient + .get(Uri.parse('$host/api/history?max_items=$maxItems')); + _validateResponse(response); + return HistoryResponse.fromJson(jsonDecode(response.body)); + } + + /// Gets image data by filename + Future> getImage(String filename) async { + final response = + await _httpClient.get(Uri.parse('$host/api/view?filename=$filename')); + _validateResponse(response); + return response.bodyBytes; + } + + /// Gets a list of all available models + Future> getModels() async { + final response = + await _httpClient.get(Uri.parse('$host/api/experiment/models')); + _validateResponse(response); + return jsonDecode(response.body); + } + + /// Gets a list of checkpoints + Future> getCheckpoints() async { + final response = await _httpClient + .get(Uri.parse('$host/api/experiment/models/checkpoints')); + _validateResponse(response); + final List jsonData = jsonDecode(response.body); + return jsonData.map((item) => Checkpoint.fromJson(item)).toList(); + } + + /// Gets details for a specific checkpoint + Future> getCheckpointDetails( + String pathAndFileName) async { + final response = await _httpClient.get(Uri.parse( + '$host/api/view_metadata/checkpoints?filename=$pathAndFileName')); + _validateResponse(response); + return jsonDecode(response.body); + } + + /// Gets a list of LoRAs + Future> getLoras() async { + final response = + await _httpClient.get(Uri.parse('$host/api/experiment/models/loras')); + _validateResponse(response); + final List jsonData = jsonDecode(response.body); + return jsonData.map((item) => Lora.fromJson(item)).toList(); + } + + /// Gets details for a specific LoRA + Future> getLoraDetails(String pathAndFileName) async { + final response = await _httpClient.get( + Uri.parse('$host/api/view_metadata/loras?filename=$pathAndFileName')); + _validateResponse(response); + return jsonDecode(response.body); + } + + /// Gets a list of VAEs + Future> getVaes() async { + final response = + await _httpClient.get(Uri.parse('$host/api/experiment/models/vae')); + _validateResponse(response); + final List jsonData = jsonDecode(response.body); + return jsonData.map((item) => Vae.fromJson(item)).toList(); + } + + /// Gets details for a specific VAE + Future> getVaeDetails(String pathAndFileName) async { + final response = await _httpClient.get( + Uri.parse('$host/api/view_metadata/vae?filename=$pathAndFileName')); + _validateResponse(response); + return jsonDecode(response.body); + } + + /// Gets a list of upscale models + Future> getUpscaleModels() async { + final response = await _httpClient + .get(Uri.parse('$host/api/experiment/models/upscale_models')); + _validateResponse(response); + return jsonDecode(response.body); + } + + /// Gets details for a specific upscale model + Future> getUpscaleModelDetails( + String pathAndFileName) async { + final response = await _httpClient.get(Uri.parse( + '$host/api/view_metadata/upscale_models?filename=$pathAndFileName')); + _validateResponse(response); + return jsonDecode(response.body); + } + + /// Gets a list of embeddings + Future> getEmbeddings() async { + final response = await _httpClient + .get(Uri.parse('$host/api/experiment/models/embeddings')); + _validateResponse(response); + return jsonDecode(response.body); + } + + /// Gets details for a specific embedding + Future> getEmbeddingDetails( + String pathAndFileName) async { + final response = await _httpClient.get(Uri.parse( + '$host/api/view_metadata/embeddings?filename=$pathAndFileName')); + _validateResponse(response); + return jsonDecode(response.body); + } + + /// Gets information about all available objects (nodes) + Future> getObjectInfo() async { + final response = await _httpClient.get(Uri.parse('$host/api/object_info')); + _validateResponse(response); + return jsonDecode(response.body); + } + + /// Gets a list of possible samplers from the object info + Future> getKSamplers() async { + final objectInfo = await getObjectInfo(); + if (objectInfo.containsKey('KSampler') && + objectInfo['KSampler']['input']['required'] + .containsKey('sampler_name')) { + return List.from( + objectInfo['KSampler']['input']['required']['sampler_name'][0]); + } + throw ComfyUiApiException( + statusCode: 500, + message: 'KSampler information not found in object info.'); + } + + /// Gets a list of possible schedulers from the object info + Future> getSchedulers() async { + final objectInfo = await getObjectInfo(); + if (objectInfo.containsKey('KSampler') && + objectInfo['KSampler']['input']['required'].containsKey('scheduler')) { + return List.from( + objectInfo['KSampler']['input']['required']['scheduler'][0]); + } + throw ComfyUiApiException( + statusCode: 500, + message: 'Scheduler information not found in object info.'); + } + + /// Submits a prompt (workflow) to generate an image + Future submitPrompt(Map prompt) async { + final response = await _httpClient.post( + Uri.parse('$host/api/prompt'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(prompt), + ); + _validateResponse(response); + final responseData = jsonDecode(response.body); + + // Trigger onPromptStart event if prompt_id exists + if (responseData.containsKey('prompt_id')) { + final promptId = responseData['prompt_id']; + _webSocketManager.triggerOnPromptStart(promptId); + } + + return SubmitPromptResponse.fromJson(responseData); + } + + Future interrupt({bool clearQueue = false}) async { + if (clearQueue) { + final clearQueueResponse = await _httpClient.post( + Uri.parse('$host/api/queue'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'clear': true}), + ); + _validateResponse(clearQueueResponse); + } + + final response = await _httpClient.post(Uri.parse('$host/api/interrupt')); + _validateResponse(response); + return true; + } + + /// Validates HTTP response and throws an exception if needed + void _validateResponse(http.Response response) { + if (response.statusCode < 200 || response.statusCode >= 300) { + throw ComfyUiApiException( + statusCode: response.statusCode, + message: 'API request failed: ${response.body}'); + } + } +} diff --git a/lib/src/comfyui_api.dart b/lib/src/comfyui_api.dart index 27def7d..f6a15b6 100644 --- a/lib/src/comfyui_api.dart +++ b/lib/src/comfyui_api.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'package:http/http.dart' as http; @@ -7,7 +6,6 @@ import 'package:logger/logger.dart'; import 'models/websocket_event.dart'; import 'models/progress_event.dart'; import 'models/execution_event.dart'; -import 'exceptions/comfyui_api_exception.dart'; import 'types/callback_types.dart'; import 'models/history_response.dart'; import 'models/checkpoint.dart'; @@ -15,14 +13,16 @@ import 'models/vae.dart'; import 'models/lora.dart'; import 'websocket_manager.dart'; import 'models/submit_prompt_response.dart'; +import '_http_endpoints.dart'; // Import the new helper class /// A Dart SDK for interacting with the ComfyUI API class ComfyUiApi { final String host; final String clientId; - final http.Client _httpClient; + final http.Client _httpClient; // Keep for potential direct use or disposal final WebSocketManager _webSocketManager; final Logger logger; + late final ComfyApiHttpEndpoints _httpEndpoints; // Add the helper instance /// Creates a new ComfyUI API client ComfyUiApi({ @@ -32,7 +32,14 @@ class ComfyUiApi { http.Client? httpClient, }) : _httpClient = httpClient ?? http.Client(), _webSocketManager = WebSocketManager(host: host, clientId: clientId), - logger = logger ?? Logger(); + logger = logger ?? Logger() { + _httpEndpoints = ComfyApiHttpEndpoints( + host: host, + httpClient: _httpClient, + logger: this.logger, // Use the instance logger + webSocketManager: _webSocketManager, + ); + } /// Expose WebSocketManager streams and methods Stream get events => _webSocketManager.events; @@ -45,7 +52,6 @@ class ComfyUiApi { Stream get executionInterruptedStream => _webSocketManager.executionInterruptedStream; - /// Register a callback for when the executing node changes void onExecutingNodeChanged(void Function(int nodeId) callback) { _webSocketManager.executingNodeStream.listen(callback); } @@ -78,197 +84,72 @@ class ComfyUiApi { _httpClient.close(); } + // --- HTTP Endpoint Methods (Delegated) --- + /// Gets the current queue status - Future> getQueue() async { - final response = await _httpClient.get(Uri.parse('$host/queue')); - _validateResponse(response); - return jsonDecode(response.body); - } + Future> getQueue() => _httpEndpoints.getQueue(); /// Gets the history of the queue - Future getHistory({int maxItems = 64}) async { - final response = await _httpClient - .get(Uri.parse('$host/api/history?max_items=$maxItems')); - _validateResponse(response); - return HistoryResponse.fromJson(jsonDecode(response.body)); - } + Future getHistory({int maxItems = 64}) => + _httpEndpoints.getHistory(maxItems: maxItems); /// Gets image data by filename - Future> getImage(String filename) async { - final response = - await _httpClient.get(Uri.parse('$host/api/view?filename=$filename')); - _validateResponse(response); - return response.bodyBytes; - } + Future> getImage(String filename) => + _httpEndpoints.getImage(filename); /// Gets a list of all available models - Future> getModels() async { - final response = - await _httpClient.get(Uri.parse('$host/api/experiment/models')); - _validateResponse(response); - return jsonDecode(response.body); - } + Future> getModels() => _httpEndpoints.getModels(); /// Gets a list of checkpoints - Future> getCheckpoints() async { - final response = await _httpClient - .get(Uri.parse('$host/api/experiment/models/checkpoints')); - _validateResponse(response); - final List jsonData = jsonDecode(response.body); - return jsonData.map((item) => Checkpoint.fromJson(item)).toList(); - } + Future> getCheckpoints() => _httpEndpoints.getCheckpoints(); /// Gets details for a specific checkpoint - Future> getCheckpointDetails( - String pathAndFileName) async { - final response = await _httpClient.get(Uri.parse( - '$host/api/view_metadata/checkpoints?filename=$pathAndFileName')); - _validateResponse(response); - return jsonDecode(response.body); - } + Future> getCheckpointDetails(String pathAndFileName) => + _httpEndpoints.getCheckpointDetails(pathAndFileName); /// Gets a list of LoRAs - Future> getLoras() async { - final response = - await _httpClient.get(Uri.parse('$host/api/experiment/models/loras')); - _validateResponse(response); - final List jsonData = jsonDecode(response.body); - return jsonData.map((item) => Lora.fromJson(item)).toList(); - } + Future> getLoras() => _httpEndpoints.getLoras(); /// Gets details for a specific LoRA - Future> getLoraDetails(String pathAndFileName) async { - final response = await _httpClient.get( - Uri.parse('$host/api/view_metadata/loras?filename=$pathAndFileName')); - _validateResponse(response); - return jsonDecode(response.body); - } + Future> getLoraDetails(String pathAndFileName) => + _httpEndpoints.getLoraDetails(pathAndFileName); /// Gets a list of VAEs - Future> getVaes() async { - final response = - await _httpClient.get(Uri.parse('$host/api/experiment/models/vae')); - _validateResponse(response); - final List jsonData = jsonDecode(response.body); - return jsonData.map((item) => Vae.fromJson(item)).toList(); - } + Future> getVaes() => _httpEndpoints.getVaes(); /// Gets details for a specific VAE - Future> getVaeDetails(String pathAndFileName) async { - final response = await _httpClient.get( - Uri.parse('$host/api/view_metadata/vae?filename=$pathAndFileName')); - _validateResponse(response); - return jsonDecode(response.body); - } + Future> getVaeDetails(String pathAndFileName) => + _httpEndpoints.getVaeDetails(pathAndFileName); /// Gets a list of upscale models - Future> getUpscaleModels() async { - final response = await _httpClient - .get(Uri.parse('$host/api/experiment/models/upscale_models')); - _validateResponse(response); - return jsonDecode(response.body); - } + Future> getUpscaleModels() => _httpEndpoints.getUpscaleModels(); /// Gets details for a specific upscale model - Future> getUpscaleModelDetails( - String pathAndFileName) async { - final response = await _httpClient.get(Uri.parse( - '$host/api/view_metadata/upscale_models?filename=$pathAndFileName')); - _validateResponse(response); - return jsonDecode(response.body); - } + Future> getUpscaleModelDetails(String pathAndFileName) => + _httpEndpoints.getUpscaleModelDetails(pathAndFileName); /// Gets a list of embeddings - Future> getEmbeddings() async { - final response = await _httpClient - .get(Uri.parse('$host/api/experiment/models/embeddings')); - _validateResponse(response); - return jsonDecode(response.body); - } + Future> getEmbeddings() => _httpEndpoints.getEmbeddings(); /// Gets details for a specific embedding - Future> getEmbeddingDetails( - String pathAndFileName) async { - final response = await _httpClient.get(Uri.parse( - '$host/api/view_metadata/embeddings?filename=$pathAndFileName')); - _validateResponse(response); - return jsonDecode(response.body); - } + Future> getEmbeddingDetails(String pathAndFileName) => + _httpEndpoints.getEmbeddingDetails(pathAndFileName); /// Gets information about all available objects (nodes) - Future> getObjectInfo() async { - final response = await _httpClient.get(Uri.parse('$host/api/object_info')); - _validateResponse(response); - return jsonDecode(response.body); - } + Future> getObjectInfo() => + _httpEndpoints.getObjectInfo(); /// Gets a list of possible samplers from the object info - Future> getKSamplers() async { - final objectInfo = await getObjectInfo(); - if (objectInfo.containsKey('KSampler') && - objectInfo['KSampler']['input']['required'] - .containsKey('sampler_name')) { - return List.from( - objectInfo['KSampler']['input']['required']['sampler_name'][0]); - } - throw ComfyUiApiException( - statusCode: 500, - message: 'KSampler information not found in object info.'); - } + Future> getKSamplers() => _httpEndpoints.getKSamplers(); /// Gets a list of possible schedulers from the object info - Future> getSchedulers() async { - final objectInfo = await getObjectInfo(); - if (objectInfo.containsKey('KSampler') && - objectInfo['KSampler']['input']['required'].containsKey('scheduler')) { - return List.from( - objectInfo['KSampler']['input']['required']['scheduler'][0]); - } - throw ComfyUiApiException( - statusCode: 500, - message: 'Scheduler information not found in object info.'); - } + Future> getSchedulers() => _httpEndpoints.getSchedulers(); /// Submits a prompt (workflow) to generate an image - Future submitPrompt(Map prompt) async { - final response = await _httpClient.post( - Uri.parse('$host/api/prompt'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode(prompt), - ); - _validateResponse(response); - final responseData = jsonDecode(response.body); + Future submitPrompt(Map prompt) => + _httpEndpoints.submitPrompt(prompt); - // Trigger onPromptStart event if prompt_id exists - if (responseData.containsKey('prompt_id')) { - final promptId = responseData['prompt_id']; - _webSocketManager.triggerOnPromptStart(promptId); - } - - return SubmitPromptResponse.fromJson(responseData); - } - - Future interrupt({bool clearQueue = false}) async { - if (clearQueue) { - final clearQueueResponse = await _httpClient.post( - Uri.parse('$host/api/queue'), - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({'clear': true}), - ); - _validateResponse(clearQueueResponse); - } - - final response = await _httpClient.post(Uri.parse('$host/api/interrupt')); - _validateResponse(response); - return true; - } - - /// Validates HTTP response and throws an exception if needed - void _validateResponse(http.Response response) { - if (response.statusCode < 200 || response.statusCode >= 300) { - throw ComfyUiApiException( - statusCode: response.statusCode, - message: 'API request failed: ${response.body}'); - } - } + /// Interrupts the current execution and optionally clears the queue + Future interrupt({bool clearQueue = false}) => + _httpEndpoints.interrupt(clearQueue: clearQueue); }