Add initial implementation of comfyui_api_sdk with API models and examples

This commit is contained in:
2025-03-20 13:51:02 +01:00
commit 1f9409ce0e
28 changed files with 16525 additions and 0 deletions

206
lib/src/comfyui_api.dart Normal file
View File

@@ -0,0 +1,206 @@
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';
/// A Dart SDK for interacting with the ComfyUI API
class ComfyUiApi {
final String host;
final String clientId;
final http.Client _httpClient;
WebSocketChannel? _wsChannel;
final StreamController<Map<String, dynamic>> _progressController =
StreamController.broadcast();
/// Stream of progress updates from ComfyUI
Stream<Map<String, dynamic>> 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<void> 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<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<Map<String, dynamic>> 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<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<dynamic>> 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<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<dynamic>> 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<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<dynamic>> 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<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);
}
/// Submits a prompt (workflow) to generate an image
Future<Map<String, dynamic>> 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);
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';
}

View File

@@ -0,0 +1,87 @@
/// Models that represent ComfyUI API responses
/// Represents queue information from ComfyUI
class QueueInfo {
final int queueRunning;
final List<Map<String, dynamic>> queue;
final Map<String, dynamic> queuePending;
QueueInfo({
required this.queueRunning,
required this.queue,
required this.queuePending,
});
factory QueueInfo.fromJson(Map<String, dynamic> json) {
return QueueInfo(
queueRunning: json['queue_running'] as int,
queue: List<Map<String, dynamic>>.from(json['queue'] ?? []),
queuePending: Map<String, dynamic>.from(json['queue_pending'] ?? {}),
);
}
}
/// Represents a prompt execution status
class PromptExecutionStatus {
final String? promptId;
final int? number;
final String? status;
final dynamic error;
PromptExecutionStatus({
this.promptId,
this.number,
this.status,
this.error,
});
factory PromptExecutionStatus.fromJson(Map<String, dynamic> json) {
return PromptExecutionStatus(
promptId: json['prompt_id'] as String?,
number: json['number'] as int?,
status: json['status'] as String?,
error: json['error'],
);
}
}
/// Represents history data
class HistoryItem {
final String promptId;
final Map<String, dynamic> prompt;
final Map<String, dynamic>? outputs;
HistoryItem({
required this.promptId,
required this.prompt,
this.outputs,
});
factory HistoryItem.fromJson(Map<String, dynamic> json) {
return HistoryItem(
promptId: json['prompt_id'] as String,
prompt: Map<String, dynamic>.from(json['prompt'] ?? {}),
outputs: json['outputs'] != null
? Map<String, dynamic>.from(json['outputs'])
: null,
);
}
}
/// Represents a progress update received via WebSocket
class ProgressUpdate {
final String type;
final Map<String, dynamic> data;
ProgressUpdate({
required this.type,
required this.data,
});
factory ProgressUpdate.fromJson(Map<String, dynamic> json) {
return ProgressUpdate(
type: json['type'] as String,
data: Map<String, dynamic>.from(json['data'] ?? {}),
);
}
}