refactor: split http endpoints from main api class

This commit is contained in:
Menno van Leeuwen 2025-03-28 16:23:31 +01:00
parent 2eebe2378f
commit c2065afbc4
Signed by: vleeuwenmenno
SSH Key Fingerprint: SHA256:OJFmjANpakwD3F2Rsws4GLtbdz1TJ5tkQF0RZmF0TRE
3 changed files with 266 additions and 163 deletions

View File

@ -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;

View File

@ -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<Map<String, dynamic>> getQueue() async {
final response = await _httpClient.get(Uri.parse('$host/queue'));
_validateResponse(response);
return jsonDecode(response.body);
}
/// Gets the history of the queue
Future<HistoryResponse> 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<List<int>> 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<Map<String, dynamic>> 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<List<Checkpoint>> getCheckpoints() async {
final response = await _httpClient
.get(Uri.parse('$host/api/experiment/models/checkpoints'));
_validateResponse(response);
final List<dynamic> jsonData = jsonDecode(response.body);
return jsonData.map((item) => Checkpoint.fromJson(item)).toList();
}
/// Gets details for a specific checkpoint
Future<Map<String, dynamic>> 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<List<Lora>> getLoras() async {
final response =
await _httpClient.get(Uri.parse('$host/api/experiment/models/loras'));
_validateResponse(response);
final List<dynamic> jsonData = jsonDecode(response.body);
return jsonData.map((item) => Lora.fromJson(item)).toList();
}
/// Gets details for a specific LoRA
Future<Map<String, dynamic>> 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<List<Vae>> getVaes() async {
final response =
await _httpClient.get(Uri.parse('$host/api/experiment/models/vae'));
_validateResponse(response);
final List<dynamic> jsonData = jsonDecode(response.body);
return jsonData.map((item) => Vae.fromJson(item)).toList();
}
/// Gets details for a specific VAE
Future<Map<String, dynamic>> 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<List<dynamic>> 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<Map<String, dynamic>> 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<List<dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<List<String>> getKSamplers() async {
final objectInfo = await getObjectInfo();
if (objectInfo.containsKey('KSampler') &&
objectInfo['KSampler']['input']['required']
.containsKey('sampler_name')) {
return List<String>.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<List<String>> getSchedulers() async {
final objectInfo = await getObjectInfo();
if (objectInfo.containsKey('KSampler') &&
objectInfo['KSampler']['input']['required'].containsKey('scheduler')) {
return List<String>.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<SubmitPromptResponse> submitPrompt(Map<String, dynamic> 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<bool> 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}');
}
}
}

View File

@ -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<WebSocketEvent> get events => _webSocketManager.events;
@ -45,7 +52,6 @@ class ComfyUiApi {
Stream<void> 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<Map<String, dynamic>> getQueue() async {
final response = await _httpClient.get(Uri.parse('$host/queue'));
_validateResponse(response);
return jsonDecode(response.body);
}
Future<Map<String, dynamic>> getQueue() => _httpEndpoints.getQueue();
/// Gets the history of the queue
Future<HistoryResponse> 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<HistoryResponse> getHistory({int maxItems = 64}) =>
_httpEndpoints.getHistory(maxItems: maxItems);
/// Gets image data by filename
Future<List<int>> getImage(String filename) async {
final response =
await _httpClient.get(Uri.parse('$host/api/view?filename=$filename'));
_validateResponse(response);
return response.bodyBytes;
}
Future<List<int>> getImage(String filename) =>
_httpEndpoints.getImage(filename);
/// Gets a list of all available models
Future<Map<String, dynamic>> getModels() async {
final response =
await _httpClient.get(Uri.parse('$host/api/experiment/models'));
_validateResponse(response);
return jsonDecode(response.body);
}
Future<Map<String, dynamic>> getModels() => _httpEndpoints.getModels();
/// Gets a list of checkpoints
Future<List<Checkpoint>> getCheckpoints() async {
final response = await _httpClient
.get(Uri.parse('$host/api/experiment/models/checkpoints'));
_validateResponse(response);
final List<dynamic> jsonData = jsonDecode(response.body);
return jsonData.map((item) => Checkpoint.fromJson(item)).toList();
}
Future<List<Checkpoint>> getCheckpoints() => _httpEndpoints.getCheckpoints();
/// Gets details for a specific checkpoint
Future<Map<String, dynamic>> 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<Map<String, dynamic>> getCheckpointDetails(String pathAndFileName) =>
_httpEndpoints.getCheckpointDetails(pathAndFileName);
/// Gets a list of LoRAs
Future<List<Lora>> getLoras() async {
final response =
await _httpClient.get(Uri.parse('$host/api/experiment/models/loras'));
_validateResponse(response);
final List<dynamic> jsonData = jsonDecode(response.body);
return jsonData.map((item) => Lora.fromJson(item)).toList();
}
Future<List<Lora>> getLoras() => _httpEndpoints.getLoras();
/// Gets details for a specific LoRA
Future<Map<String, dynamic>> 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<Map<String, dynamic>> getLoraDetails(String pathAndFileName) =>
_httpEndpoints.getLoraDetails(pathAndFileName);
/// Gets a list of VAEs
Future<List<Vae>> getVaes() async {
final response =
await _httpClient.get(Uri.parse('$host/api/experiment/models/vae'));
_validateResponse(response);
final List<dynamic> jsonData = jsonDecode(response.body);
return jsonData.map((item) => Vae.fromJson(item)).toList();
}
Future<List<Vae>> getVaes() => _httpEndpoints.getVaes();
/// Gets details for a specific VAE
Future<Map<String, dynamic>> 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<Map<String, dynamic>> getVaeDetails(String pathAndFileName) =>
_httpEndpoints.getVaeDetails(pathAndFileName);
/// Gets a list of upscale models
Future<List<dynamic>> getUpscaleModels() async {
final response = await _httpClient
.get(Uri.parse('$host/api/experiment/models/upscale_models'));
_validateResponse(response);
return jsonDecode(response.body);
}
Future<List<dynamic>> getUpscaleModels() => _httpEndpoints.getUpscaleModels();
/// Gets details for a specific upscale model
Future<Map<String, dynamic>> 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<Map<String, dynamic>> getUpscaleModelDetails(String pathAndFileName) =>
_httpEndpoints.getUpscaleModelDetails(pathAndFileName);
/// Gets a list of embeddings
Future<List<dynamic>> getEmbeddings() async {
final response = await _httpClient
.get(Uri.parse('$host/api/experiment/models/embeddings'));
_validateResponse(response);
return jsonDecode(response.body);
}
Future<List<dynamic>> getEmbeddings() => _httpEndpoints.getEmbeddings();
/// Gets details for a specific embedding
Future<Map<String, dynamic>> 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<Map<String, dynamic>> getEmbeddingDetails(String pathAndFileName) =>
_httpEndpoints.getEmbeddingDetails(pathAndFileName);
/// Gets information about all available objects (nodes)
Future<Map<String, dynamic>> getObjectInfo() async {
final response = await _httpClient.get(Uri.parse('$host/api/object_info'));
_validateResponse(response);
return jsonDecode(response.body);
}
Future<Map<String, dynamic>> getObjectInfo() =>
_httpEndpoints.getObjectInfo();
/// Gets a list of possible samplers from the object info
Future<List<String>> getKSamplers() async {
final objectInfo = await getObjectInfo();
if (objectInfo.containsKey('KSampler') &&
objectInfo['KSampler']['input']['required']
.containsKey('sampler_name')) {
return List<String>.from(
objectInfo['KSampler']['input']['required']['sampler_name'][0]);
}
throw ComfyUiApiException(
statusCode: 500,
message: 'KSampler information not found in object info.');
}
Future<List<String>> getKSamplers() => _httpEndpoints.getKSamplers();
/// Gets a list of possible schedulers from the object info
Future<List<String>> getSchedulers() async {
final objectInfo = await getObjectInfo();
if (objectInfo.containsKey('KSampler') &&
objectInfo['KSampler']['input']['required'].containsKey('scheduler')) {
return List<String>.from(
objectInfo['KSampler']['input']['required']['scheduler'][0]);
}
throw ComfyUiApiException(
statusCode: 500,
message: 'Scheduler information not found in object info.');
}
Future<List<String>> getSchedulers() => _httpEndpoints.getSchedulers();
/// Submits a prompt (workflow) to generate an image
Future<SubmitPromptResponse> submitPrompt(Map<String, dynamic> 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<SubmitPromptResponse> submitPrompt(Map<String, dynamic> 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<bool> 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<bool> interrupt({bool clearQueue = false}) =>
_httpEndpoints.interrupt(clearQueue: clearQueue);
}