import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:uuid/uuid.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; class ComfyUiApi { final String host; final String clientId; final http.Client _httpClient; WebSocketChannel? _wsChannel; final StreamController> _progressController = StreamController.broadcast(); /// Stream of progress updates from ComfyUI Stream> get progressUpdates => _progressController.stream; /// Creates a new ComfyUI API client /// /// [host] The host of the ComfyUI server (e.g. 'http://localhost:8188') /// [clientId] Optional client ID, will be automatically generated if not provided ComfyUiApi({ required this.host, String? clientId, http.Client? httpClient, }) : clientId = clientId ?? const Uuid().v4(), _httpClient = httpClient ?? http.Client(); /// Connects to the WebSocket for progress updates Future connectWebSocket() async { final wsUrl = 'ws://${host.replaceFirst(RegExp(r'^https?://'), '')}/ws?clientId=$clientId'; _wsChannel = WebSocketChannel.connect(Uri.parse(wsUrl)); _wsChannel!.stream.listen((message) { final data = jsonDecode(message); _progressController.add(data); }, onError: (error) { print('WebSocket error: $error'); }, onDone: () { print('WebSocket connection closed'); }); } /// Closes the WebSocket connection and cleans up resources void dispose() { _wsChannel?.sink.close(); _progressController.close(); _httpClient.close(); } /// 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 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); return jsonDecode(response.body); } /// 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); return jsonDecode(response.body); } /// 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); return jsonDecode(response.body); } /// 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); } /// 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); return jsonDecode(response.body); } /// 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}'); } } } /// Exception thrown when the ComfyUI API returns an error class ComfyUiApiException implements Exception { final int statusCode; final String message; ComfyUiApiException({required this.statusCode, required this.message}); @override String toString() => 'ComfyUiApiException: $statusCode - $message'; }