libre-webui 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -16
- package/backend/dist/db.d.ts.map +1 -1
- package/backend/dist/db.js +31 -0
- package/backend/dist/db.js.map +1 -1
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +13 -0
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/routes/imageGen.d.ts +3 -0
- package/backend/dist/routes/imageGen.d.ts.map +1 -0
- package/backend/dist/routes/imageGen.js +313 -0
- package/backend/dist/routes/imageGen.js.map +1 -0
- package/backend/dist/routes/plugins.d.ts.map +1 -1
- package/backend/dist/routes/plugins.js +122 -0
- package/backend/dist/routes/plugins.js.map +1 -1
- package/backend/dist/services/galleryService.d.ts +41 -0
- package/backend/dist/services/galleryService.d.ts.map +1 -0
- package/backend/dist/services/galleryService.js +175 -0
- package/backend/dist/services/galleryService.js.map +1 -0
- package/backend/dist/services/pluginCredentialsService.d.ts +46 -0
- package/backend/dist/services/pluginCredentialsService.d.ts.map +1 -0
- package/backend/dist/services/pluginCredentialsService.js +175 -0
- package/backend/dist/services/pluginCredentialsService.js.map +1 -0
- package/backend/dist/services/pluginService.d.ts +23 -1
- package/backend/dist/services/pluginService.d.ts.map +1 -1
- package/backend/dist/services/pluginService.js +440 -24
- package/backend/dist/services/pluginService.js.map +1 -1
- package/backend/dist/types/index.d.ts +39 -0
- package/backend/dist/types/index.d.ts.map +1 -1
- package/backend/dist/types/index.js.map +1 -1
- package/frontend/dist/assets/index-BAlYrgVl.js +26 -0
- package/frontend/dist/css/index-CYRSPYSA.css +1 -0
- package/frontend/dist/index.html +4 -4
- package/frontend/dist/js/ArtifactContainer-DOyRNr2K.js +24 -0
- package/frontend/dist/js/{ArtifactDemoPage-Cg6PD9Cv.js → ArtifactDemoPage-DuHnKjTy.js} +1 -1
- package/frontend/dist/js/ChatPage-D4AGvUr9.js +281 -0
- package/frontend/dist/js/GalleryPage-DPxs-JHF.js +1 -0
- package/frontend/dist/js/ModelsPage-CDx8DvZ8.js +2 -0
- package/frontend/dist/js/PersonasPage-CHDGv_Lh.js +13 -0
- package/frontend/dist/js/UserManagementPage-COb0e5_w.js +1 -0
- package/frontend/dist/js/{markdown-vendor-D-79K2xZ.js → markdown-vendor-DRtqGHm3.js} +1 -1
- package/frontend/dist/js/ui-vendor-DA07XX1l.js +177 -0
- package/package.json +1 -1
- package/plugins/comfyui.json +49 -0
- package/frontend/dist/assets/index-CE4fvnod.js +0 -3
- package/frontend/dist/css/index-B1OjddR-.css +0 -1
- package/frontend/dist/js/ArtifactContainer-BoHnMmlC.js +0 -23
- package/frontend/dist/js/ChatPage-Bwc5n8Z3.js +0 -281
- package/frontend/dist/js/ModelsPage-2HtOH51G.js +0 -2
- package/frontend/dist/js/PersonasPage-BESJgtJW.js +0 -13
- package/frontend/dist/js/UserManagementPage-DQaE_2Mt.js +0 -1
- package/frontend/dist/js/ui-vendor-VxSCY_bv.js +0 -177
|
@@ -18,6 +18,7 @@ import fs from 'fs';
|
|
|
18
18
|
import path from 'path';
|
|
19
19
|
import sanitize from 'sanitize-filename';
|
|
20
20
|
import axios from 'axios';
|
|
21
|
+
import pluginCredentialsService from './pluginCredentialsService.js';
|
|
21
22
|
class PluginService {
|
|
22
23
|
constructor() {
|
|
23
24
|
this.activePluginIds = new Set();
|
|
@@ -56,6 +57,15 @@ class PluginService {
|
|
|
56
57
|
};
|
|
57
58
|
fs.writeFileSync(statusFile, JSON.stringify(status, null, 2));
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Get API key for a plugin from database (per-user) or environment variable (fallback)
|
|
62
|
+
* @param plugin The plugin to get the API key for
|
|
63
|
+
* @param userId Optional user ID for per-user credentials
|
|
64
|
+
* @returns The API key or null if not found
|
|
65
|
+
*/
|
|
66
|
+
getApiKey(plugin, userId) {
|
|
67
|
+
return pluginCredentialsService.getApiKey(plugin.id, plugin.auth.key_env, userId);
|
|
68
|
+
}
|
|
59
69
|
// List all installed plugins
|
|
60
70
|
getAllPlugins() {
|
|
61
71
|
const plugins = [];
|
|
@@ -195,10 +205,10 @@ class PluginService {
|
|
|
195
205
|
console.log(`[DEBUG] Checking plugin ${plugin.id} with model_map:`, plugin.model_map);
|
|
196
206
|
if (plugin.model_map.includes(model)) {
|
|
197
207
|
console.log(`[DEBUG] Found plugin ${plugin.id} for model ${model}`);
|
|
198
|
-
// Check if we have the required API key
|
|
199
|
-
const apiKey =
|
|
208
|
+
// Check if we have the required API key (from DB or env)
|
|
209
|
+
const apiKey = this.getApiKey(plugin);
|
|
200
210
|
if (!apiKey) {
|
|
201
|
-
console.log(`[DEBUG] Plugin ${plugin.id} found but API key ${plugin.auth.key_env}
|
|
211
|
+
console.log(`[DEBUG] Plugin ${plugin.id} found but API key not set (checked DB and ${plugin.auth.key_env})`);
|
|
202
212
|
continue;
|
|
203
213
|
}
|
|
204
214
|
return plugin;
|
|
@@ -256,10 +266,10 @@ class PluginService {
|
|
|
256
266
|
if (!activePlugin) {
|
|
257
267
|
throw new Error(`No active plugin found for model: ${model}`);
|
|
258
268
|
}
|
|
259
|
-
// Get API key from environment
|
|
260
|
-
const apiKey =
|
|
269
|
+
// Get API key from database (per-user) or environment variable (fallback)
|
|
270
|
+
const apiKey = this.getApiKey(activePlugin);
|
|
261
271
|
if (!apiKey) {
|
|
262
|
-
throw new Error(`API key not found
|
|
272
|
+
throw new Error(`API key not found for plugin ${activePlugin.id} (set via Settings or ${activePlugin.auth.key_env} env var)`);
|
|
263
273
|
}
|
|
264
274
|
// Prepare headers
|
|
265
275
|
const headers = {
|
|
@@ -421,11 +431,12 @@ class PluginService {
|
|
|
421
431
|
// Validate the final endpoint URL
|
|
422
432
|
try {
|
|
423
433
|
const url = new URL(processedEndpoint);
|
|
424
|
-
// Allow HTTP
|
|
434
|
+
// Allow HTTP for localhost and private network IPs (safe for local/LAN development)
|
|
425
435
|
const isLocalhost = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
426
|
-
|
|
436
|
+
const isPrivateNetwork = /^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(url.hostname);
|
|
437
|
+
if (url.protocol !== 'https:' && !isLocalhost && !isPrivateNetwork) {
|
|
427
438
|
throw new Error(`Insecure endpoint protocol: ${url.protocol}. Only HTTPS is allowed for remote endpoints. ` +
|
|
428
|
-
`(HTTP is permitted
|
|
439
|
+
`(HTTP is permitted for localhost and private network IPs)`);
|
|
429
440
|
}
|
|
430
441
|
}
|
|
431
442
|
catch (_error) {
|
|
@@ -641,10 +652,10 @@ class PluginService {
|
|
|
641
652
|
const ttsCapability = plugin.capabilities.tts;
|
|
642
653
|
if (ttsCapability.model_map.includes(model)) {
|
|
643
654
|
console.log(`[DEBUG] Found TTS plugin ${plugin.id} for model ${model}`);
|
|
644
|
-
// Check if we have the required API key
|
|
645
|
-
const apiKey =
|
|
655
|
+
// Check if we have the required API key (from DB or env)
|
|
656
|
+
const apiKey = this.getApiKey(plugin);
|
|
646
657
|
if (!apiKey) {
|
|
647
|
-
console.log(`[DEBUG] Plugin ${plugin.id} found but API key ${plugin.auth.key_env}
|
|
658
|
+
console.log(`[DEBUG] Plugin ${plugin.id} found but API key not set (checked DB and ${plugin.auth.key_env})`);
|
|
648
659
|
continue;
|
|
649
660
|
}
|
|
650
661
|
return plugin;
|
|
@@ -653,9 +664,9 @@ class PluginService {
|
|
|
653
664
|
// Also check primary type for backward compatibility with TTS-only plugins
|
|
654
665
|
if (plugin.type === 'tts' && plugin.model_map.includes(model)) {
|
|
655
666
|
console.log(`[DEBUG] Found TTS-type plugin ${plugin.id} for model ${model}`);
|
|
656
|
-
const apiKey =
|
|
667
|
+
const apiKey = this.getApiKey(plugin);
|
|
657
668
|
if (!apiKey) {
|
|
658
|
-
console.log(`[DEBUG] Plugin ${plugin.id} found but API key ${plugin.auth.key_env}
|
|
669
|
+
console.log(`[DEBUG] Plugin ${plugin.id} found but API key not set (checked DB and ${plugin.auth.key_env})`);
|
|
659
670
|
continue;
|
|
660
671
|
}
|
|
661
672
|
return plugin;
|
|
@@ -672,8 +683,8 @@ class PluginService {
|
|
|
672
683
|
// Check capabilities-based TTS
|
|
673
684
|
if (plugin.capabilities?.tts) {
|
|
674
685
|
const ttsCapability = plugin.capabilities.tts;
|
|
675
|
-
// Check if API key is available
|
|
676
|
-
const apiKey =
|
|
686
|
+
// Check if API key is available (from DB or env)
|
|
687
|
+
const apiKey = this.getApiKey(plugin);
|
|
677
688
|
if (apiKey) {
|
|
678
689
|
for (const model of ttsCapability.model_map) {
|
|
679
690
|
models.push({
|
|
@@ -686,7 +697,7 @@ class PluginService {
|
|
|
686
697
|
}
|
|
687
698
|
// Check primary type for TTS-only plugins
|
|
688
699
|
if (plugin.type === 'tts') {
|
|
689
|
-
const apiKey =
|
|
700
|
+
const apiKey = this.getApiKey(plugin);
|
|
690
701
|
if (apiKey) {
|
|
691
702
|
for (const model of plugin.model_map) {
|
|
692
703
|
models.push({
|
|
@@ -718,10 +729,10 @@ class PluginService {
|
|
|
718
729
|
if (!plugin) {
|
|
719
730
|
throw new Error(`No TTS plugin found for model: ${model}`);
|
|
720
731
|
}
|
|
721
|
-
// Get API key from environment
|
|
722
|
-
const apiKey =
|
|
732
|
+
// Get API key from database (per-user) or environment variable (fallback)
|
|
733
|
+
const apiKey = this.getApiKey(plugin);
|
|
723
734
|
if (!apiKey) {
|
|
724
|
-
throw new Error(`API key not found
|
|
735
|
+
throw new Error(`API key not found for plugin ${plugin.id} (set via Settings or ${plugin.auth.key_env} env var)`);
|
|
725
736
|
}
|
|
726
737
|
// Determine endpoint
|
|
727
738
|
let endpoint;
|
|
@@ -823,8 +834,10 @@ class PluginService {
|
|
|
823
834
|
try {
|
|
824
835
|
const url = new URL(processedEndpoint);
|
|
825
836
|
const isLocalhost = ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
826
|
-
|
|
827
|
-
|
|
837
|
+
const isPrivateNetwork = /^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(url.hostname);
|
|
838
|
+
if (url.protocol !== 'https:' && !isLocalhost && !isPrivateNetwork) {
|
|
839
|
+
throw new Error(`Insecure endpoint protocol: ${url.protocol}. Only HTTPS is allowed for remote endpoints. ` +
|
|
840
|
+
`(HTTP is permitted for localhost and private network IPs)`);
|
|
828
841
|
}
|
|
829
842
|
}
|
|
830
843
|
catch (_error) {
|
|
@@ -912,6 +925,409 @@ class PluginService {
|
|
|
912
925
|
}
|
|
913
926
|
return null;
|
|
914
927
|
}
|
|
928
|
+
// ==================== Image Generation Methods ====================
|
|
929
|
+
// Get plugin that supports a specific image generation model
|
|
930
|
+
getPluginForImageGen(model) {
|
|
931
|
+
const allPlugins = this.getAllPlugins();
|
|
932
|
+
for (const plugin of allPlugins) {
|
|
933
|
+
// Check capabilities-based image generation
|
|
934
|
+
if (plugin.capabilities?.image) {
|
|
935
|
+
const imageCapability = plugin.capabilities.image;
|
|
936
|
+
if (imageCapability.model_map.includes(model)) {
|
|
937
|
+
return plugin;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
// Check primary type for image-only plugins
|
|
941
|
+
if (plugin.type === 'image' && plugin.model_map.includes(model)) {
|
|
942
|
+
return plugin;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
console.log(`[DEBUG] No image generation plugin found for model: ${model}`);
|
|
946
|
+
return null;
|
|
947
|
+
}
|
|
948
|
+
// Get all available image generation models from all plugins
|
|
949
|
+
getAvailableImageGenModels() {
|
|
950
|
+
const models = [];
|
|
951
|
+
const allPlugins = this.getAllPlugins();
|
|
952
|
+
for (const plugin of allPlugins) {
|
|
953
|
+
// Check capabilities-based image generation
|
|
954
|
+
if (plugin.capabilities?.image) {
|
|
955
|
+
const imageCapability = plugin.capabilities.image;
|
|
956
|
+
// Check if API key is available (from DB or env) or if no auth is required
|
|
957
|
+
const noAuthRequired = imageCapability.config
|
|
958
|
+
?.no_auth_required === true;
|
|
959
|
+
const apiKey = this.getApiKey(plugin);
|
|
960
|
+
if (apiKey || noAuthRequired) {
|
|
961
|
+
for (const model of imageCapability.model_map) {
|
|
962
|
+
models.push({
|
|
963
|
+
model,
|
|
964
|
+
plugin: plugin.id,
|
|
965
|
+
config: imageCapability.config,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
// Check primary type for image-only plugins
|
|
971
|
+
if (plugin.type === 'image') {
|
|
972
|
+
const apiKey = this.getApiKey(plugin);
|
|
973
|
+
if (apiKey) {
|
|
974
|
+
for (const model of plugin.model_map) {
|
|
975
|
+
models.push({
|
|
976
|
+
model,
|
|
977
|
+
plugin: plugin.id,
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return models;
|
|
984
|
+
}
|
|
985
|
+
// Execute an image generation request through the appropriate plugin
|
|
986
|
+
async executeImageGenRequest(model, prompt, options = {}) {
|
|
987
|
+
// Validate model parameter
|
|
988
|
+
if (!model || typeof model !== 'string') {
|
|
989
|
+
throw new Error('Invalid model parameter: must be a non-empty string');
|
|
990
|
+
}
|
|
991
|
+
// Sanitize model parameter
|
|
992
|
+
const modelPattern = /^[a-zA-Z0-9\-_:.]+$/;
|
|
993
|
+
if (!modelPattern.test(model)) {
|
|
994
|
+
throw new Error(`Invalid model parameter: ${model} contains invalid characters`);
|
|
995
|
+
}
|
|
996
|
+
// Validate prompt
|
|
997
|
+
if (!prompt || typeof prompt !== 'string') {
|
|
998
|
+
throw new Error('Invalid prompt: must be a non-empty string');
|
|
999
|
+
}
|
|
1000
|
+
const plugin = this.getPluginForImageGen(model);
|
|
1001
|
+
if (!plugin) {
|
|
1002
|
+
throw new Error(`No image generation plugin found for model: ${model}`);
|
|
1003
|
+
}
|
|
1004
|
+
// Determine endpoint and config first (needed for auth check)
|
|
1005
|
+
let endpoint;
|
|
1006
|
+
let imageConfig;
|
|
1007
|
+
if (plugin.capabilities?.image) {
|
|
1008
|
+
endpoint = plugin.capabilities.image.endpoint;
|
|
1009
|
+
imageConfig = plugin.capabilities.image.config;
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
endpoint = plugin.endpoint;
|
|
1013
|
+
}
|
|
1014
|
+
// Get API key from database (per-user) or environment variable (fallback)
|
|
1015
|
+
// Some plugins (like local ComfyUI) don't require auth
|
|
1016
|
+
const noAuthRequired = imageConfig?.no_auth_required ===
|
|
1017
|
+
true;
|
|
1018
|
+
const apiKey = this.getApiKey(plugin);
|
|
1019
|
+
if (!apiKey && !noAuthRequired) {
|
|
1020
|
+
throw new Error(`API key not found for plugin ${plugin.id} (set via Settings or ${plugin.auth.key_env} env var)`);
|
|
1021
|
+
}
|
|
1022
|
+
// Validate prompt length
|
|
1023
|
+
if (imageConfig?.max_prompt_length &&
|
|
1024
|
+
prompt.length > imageConfig.max_prompt_length) {
|
|
1025
|
+
throw new Error(`Prompt exceeds maximum length of ${imageConfig.max_prompt_length} characters`);
|
|
1026
|
+
}
|
|
1027
|
+
// Build headers
|
|
1028
|
+
const headers = {
|
|
1029
|
+
'Content-Type': 'application/json',
|
|
1030
|
+
};
|
|
1031
|
+
// Only add auth header if API key is available
|
|
1032
|
+
if (apiKey) {
|
|
1033
|
+
if (plugin.auth.prefix) {
|
|
1034
|
+
headers[plugin.auth.header] = `${plugin.auth.prefix}${apiKey}`;
|
|
1035
|
+
}
|
|
1036
|
+
else {
|
|
1037
|
+
headers[plugin.auth.header] = apiKey;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
// Build payload (OpenAI-compatible format)
|
|
1041
|
+
const payload = {
|
|
1042
|
+
model,
|
|
1043
|
+
prompt,
|
|
1044
|
+
size: options.size || imageConfig?.default_size || '1024x1024',
|
|
1045
|
+
quality: options.quality || imageConfig?.default_quality || 'standard',
|
|
1046
|
+
n: options.n || 1,
|
|
1047
|
+
response_format: options.response_format || 'url',
|
|
1048
|
+
};
|
|
1049
|
+
// Add style if supported
|
|
1050
|
+
if (options.style || imageConfig?.default_style) {
|
|
1051
|
+
payload.style = options.style || imageConfig?.default_style;
|
|
1052
|
+
}
|
|
1053
|
+
// Validate the final endpoint URL
|
|
1054
|
+
let baseUrl;
|
|
1055
|
+
try {
|
|
1056
|
+
baseUrl = new URL(endpoint);
|
|
1057
|
+
const isLocalhost = ['localhost', '127.0.0.1', '[::1]'].includes(baseUrl.hostname);
|
|
1058
|
+
const isPrivateNetwork = /^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(baseUrl.hostname);
|
|
1059
|
+
if (baseUrl.protocol !== 'https:' && !isLocalhost && !isPrivateNetwork) {
|
|
1060
|
+
throw new Error(`Insecure endpoint protocol: ${baseUrl.protocol}. Only HTTPS is allowed for remote endpoints.`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
catch (_error) {
|
|
1064
|
+
throw new Error(`Invalid endpoint URL: ${endpoint}`);
|
|
1065
|
+
}
|
|
1066
|
+
// Check if this is ComfyUI (special handling required)
|
|
1067
|
+
if (plugin.id === 'comfyui' || endpoint.includes('/prompt')) {
|
|
1068
|
+
return this.executeComfyUIRequest(baseUrl, prompt, { ...options, model });
|
|
1069
|
+
}
|
|
1070
|
+
try {
|
|
1071
|
+
const response = await axios.post(endpoint, payload, {
|
|
1072
|
+
headers,
|
|
1073
|
+
timeout: 120000, // 2 minute timeout for image generation
|
|
1074
|
+
});
|
|
1075
|
+
// Handle OpenAI-style response
|
|
1076
|
+
if (response.data?.data) {
|
|
1077
|
+
return {
|
|
1078
|
+
images: response.data.data.map((img) => ({
|
|
1079
|
+
url: img.url,
|
|
1080
|
+
b64_json: img.b64_json,
|
|
1081
|
+
revised_prompt: img.revised_prompt,
|
|
1082
|
+
})),
|
|
1083
|
+
model,
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
// Handle direct response format
|
|
1087
|
+
return {
|
|
1088
|
+
images: Array.isArray(response.data) ? response.data : [response.data],
|
|
1089
|
+
model,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
if (axios.isAxiosError(error)) {
|
|
1094
|
+
const message = error.response?.data?.error?.message ||
|
|
1095
|
+
error.response?.data?.message ||
|
|
1096
|
+
error.message;
|
|
1097
|
+
throw new Error(`Image generation failed: ${message}`);
|
|
1098
|
+
}
|
|
1099
|
+
throw error;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
// Execute ComfyUI image generation request (Flux.1 workflow)
|
|
1103
|
+
async executeComfyUIRequest(baseUrl, prompt, options = {}) {
|
|
1104
|
+
const comfyBaseUrl = `${baseUrl.protocol}//${baseUrl.host}`;
|
|
1105
|
+
// Parse size
|
|
1106
|
+
const size = options.size || '1024x1024';
|
|
1107
|
+
const [width, height] = size.split('x').map(Number);
|
|
1108
|
+
// Determine model-specific settings
|
|
1109
|
+
const model = options.model || 'flux1-dev';
|
|
1110
|
+
const modelConfigs = {
|
|
1111
|
+
'flux1-dev': {
|
|
1112
|
+
unetFile: 'flux1-dev.safetensors',
|
|
1113
|
+
t5File: 't5xxl_fp16.safetensors',
|
|
1114
|
+
steps: { draft: 12, standard: 20, high: 28, ultra: 40 },
|
|
1115
|
+
guidance: 3.5,
|
|
1116
|
+
useCheckpointLoader: false,
|
|
1117
|
+
},
|
|
1118
|
+
'flux1-dev-fp8': {
|
|
1119
|
+
unetFile: 'flux1-dev-fp8.safetensors',
|
|
1120
|
+
t5File: 't5xxl_fp8_e4m3fn_scaled.safetensors',
|
|
1121
|
+
steps: { draft: 12, standard: 20, high: 28, ultra: 40 },
|
|
1122
|
+
guidance: 3.5,
|
|
1123
|
+
useCheckpointLoader: false,
|
|
1124
|
+
},
|
|
1125
|
+
'flux1-schnell': {
|
|
1126
|
+
unetFile: 'flux1-schnell.safetensors',
|
|
1127
|
+
t5File: 't5xxl_fp16.safetensors',
|
|
1128
|
+
steps: { draft: 2, standard: 4, high: 6, ultra: 8 },
|
|
1129
|
+
guidance: 0, // Schnell doesn't use guidance
|
|
1130
|
+
useCheckpointLoader: false,
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
const config = modelConfigs[model] || modelConfigs['flux1-dev'];
|
|
1134
|
+
const quality = (options.quality ||
|
|
1135
|
+
'standard');
|
|
1136
|
+
const steps = config.steps[quality] || config.steps.standard;
|
|
1137
|
+
// Create a Flux.1 workflow for ComfyUI
|
|
1138
|
+
// Flux uses UNET loader + dual CLIP + VAE separately
|
|
1139
|
+
const workflow = {
|
|
1140
|
+
'6': {
|
|
1141
|
+
inputs: {
|
|
1142
|
+
text: prompt,
|
|
1143
|
+
clip: ['11', 0],
|
|
1144
|
+
},
|
|
1145
|
+
class_type: 'CLIPTextEncode',
|
|
1146
|
+
_meta: { title: 'CLIP Text Encode (Prompt)' },
|
|
1147
|
+
},
|
|
1148
|
+
'8': {
|
|
1149
|
+
inputs: {
|
|
1150
|
+
samples: ['13', 0],
|
|
1151
|
+
vae: ['10', 0],
|
|
1152
|
+
},
|
|
1153
|
+
class_type: 'VAEDecode',
|
|
1154
|
+
_meta: { title: 'VAE Decode' },
|
|
1155
|
+
},
|
|
1156
|
+
'9': {
|
|
1157
|
+
inputs: {
|
|
1158
|
+
filename_prefix: `LibreWebUI_${model}`,
|
|
1159
|
+
images: ['8', 0],
|
|
1160
|
+
},
|
|
1161
|
+
class_type: 'SaveImage',
|
|
1162
|
+
_meta: { title: 'Save Image' },
|
|
1163
|
+
},
|
|
1164
|
+
'10': {
|
|
1165
|
+
inputs: {
|
|
1166
|
+
vae_name: 'ae.safetensors',
|
|
1167
|
+
},
|
|
1168
|
+
class_type: 'VAELoader',
|
|
1169
|
+
_meta: { title: 'Load VAE' },
|
|
1170
|
+
},
|
|
1171
|
+
'11': {
|
|
1172
|
+
inputs: {
|
|
1173
|
+
clip_name1: 'clip_l.safetensors',
|
|
1174
|
+
clip_name2: config.t5File,
|
|
1175
|
+
type: 'flux',
|
|
1176
|
+
},
|
|
1177
|
+
class_type: 'DualCLIPLoader',
|
|
1178
|
+
_meta: { title: 'DualCLIPLoader' },
|
|
1179
|
+
},
|
|
1180
|
+
'12': {
|
|
1181
|
+
inputs: {
|
|
1182
|
+
unet_name: config.unetFile,
|
|
1183
|
+
weight_dtype: 'default',
|
|
1184
|
+
},
|
|
1185
|
+
class_type: 'UNETLoader',
|
|
1186
|
+
_meta: { title: 'Load Diffusion Model' },
|
|
1187
|
+
},
|
|
1188
|
+
'13': {
|
|
1189
|
+
inputs: {
|
|
1190
|
+
noise: ['25', 0],
|
|
1191
|
+
guider: ['22', 0],
|
|
1192
|
+
sampler: ['16', 0],
|
|
1193
|
+
sigmas: ['17', 0],
|
|
1194
|
+
latent_image: ['27', 0],
|
|
1195
|
+
},
|
|
1196
|
+
class_type: 'SamplerCustomAdvanced',
|
|
1197
|
+
_meta: { title: 'SamplerCustomAdvanced' },
|
|
1198
|
+
},
|
|
1199
|
+
'16': {
|
|
1200
|
+
inputs: {
|
|
1201
|
+
sampler_name: 'euler',
|
|
1202
|
+
},
|
|
1203
|
+
class_type: 'KSamplerSelect',
|
|
1204
|
+
_meta: { title: 'KSamplerSelect' },
|
|
1205
|
+
},
|
|
1206
|
+
'17': {
|
|
1207
|
+
inputs: {
|
|
1208
|
+
scheduler: 'simple',
|
|
1209
|
+
steps: steps,
|
|
1210
|
+
denoise: 1,
|
|
1211
|
+
model: ['12', 0],
|
|
1212
|
+
},
|
|
1213
|
+
class_type: 'BasicScheduler',
|
|
1214
|
+
_meta: { title: 'BasicScheduler' },
|
|
1215
|
+
},
|
|
1216
|
+
'22': {
|
|
1217
|
+
inputs: {
|
|
1218
|
+
model: ['12', 0],
|
|
1219
|
+
conditioning: config.guidance > 0 ? ['26', 0] : ['6', 0],
|
|
1220
|
+
},
|
|
1221
|
+
class_type: 'BasicGuider',
|
|
1222
|
+
_meta: { title: 'BasicGuider' },
|
|
1223
|
+
},
|
|
1224
|
+
'25': {
|
|
1225
|
+
inputs: {
|
|
1226
|
+
noise_seed: Math.floor(Math.random() * 1000000000000000),
|
|
1227
|
+
},
|
|
1228
|
+
class_type: 'RandomNoise',
|
|
1229
|
+
_meta: { title: 'RandomNoise' },
|
|
1230
|
+
},
|
|
1231
|
+
'27': {
|
|
1232
|
+
inputs: {
|
|
1233
|
+
width: width,
|
|
1234
|
+
height: height,
|
|
1235
|
+
batch_size: 1,
|
|
1236
|
+
},
|
|
1237
|
+
class_type: 'EmptySD3LatentImage',
|
|
1238
|
+
_meta: { title: 'EmptySD3LatentImage' },
|
|
1239
|
+
},
|
|
1240
|
+
};
|
|
1241
|
+
// Only add FluxGuidance node if guidance > 0 (not needed for schnell)
|
|
1242
|
+
if (config.guidance > 0) {
|
|
1243
|
+
workflow['26'] = {
|
|
1244
|
+
inputs: {
|
|
1245
|
+
guidance: config.guidance,
|
|
1246
|
+
conditioning: ['6', 0],
|
|
1247
|
+
},
|
|
1248
|
+
class_type: 'FluxGuidance',
|
|
1249
|
+
_meta: { title: 'FluxGuidance' },
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
try {
|
|
1253
|
+
// Generate a unique client ID
|
|
1254
|
+
const clientId = `libre-webui-${Date.now()}`;
|
|
1255
|
+
// Submit the workflow
|
|
1256
|
+
const promptResponse = await axios.post(`${comfyBaseUrl}/prompt`, {
|
|
1257
|
+
prompt: workflow,
|
|
1258
|
+
client_id: clientId,
|
|
1259
|
+
}, {
|
|
1260
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1261
|
+
timeout: 10000,
|
|
1262
|
+
});
|
|
1263
|
+
const promptId = promptResponse.data.prompt_id;
|
|
1264
|
+
if (!promptId) {
|
|
1265
|
+
throw new Error('Failed to get prompt ID from ComfyUI');
|
|
1266
|
+
}
|
|
1267
|
+
// Poll for completion
|
|
1268
|
+
let completed = false;
|
|
1269
|
+
let attempts = 0;
|
|
1270
|
+
const maxAttempts = 120; // 2 minutes with 1 second intervals
|
|
1271
|
+
while (!completed && attempts < maxAttempts) {
|
|
1272
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1273
|
+
attempts++;
|
|
1274
|
+
const historyResponse = await axios.get(`${comfyBaseUrl}/history/${promptId}`, { timeout: 5000 });
|
|
1275
|
+
if (historyResponse.data[promptId]) {
|
|
1276
|
+
const outputs = historyResponse.data[promptId].outputs;
|
|
1277
|
+
if (outputs && Object.keys(outputs).length > 0) {
|
|
1278
|
+
completed = true;
|
|
1279
|
+
// Find the SaveImage output
|
|
1280
|
+
for (const nodeId in outputs) {
|
|
1281
|
+
const nodeOutput = outputs[nodeId];
|
|
1282
|
+
if (nodeOutput.images && nodeOutput.images.length > 0) {
|
|
1283
|
+
const imageInfo = nodeOutput.images[0];
|
|
1284
|
+
// Get the image data
|
|
1285
|
+
const imageUrl = `${comfyBaseUrl}/view?filename=${encodeURIComponent(imageInfo.filename)}&subfolder=${encodeURIComponent(imageInfo.subfolder || '')}&type=${encodeURIComponent(imageInfo.type || 'output')}`;
|
|
1286
|
+
// Fetch image and convert to base64
|
|
1287
|
+
const imageResponse = await axios.get(imageUrl, {
|
|
1288
|
+
responseType: 'arraybuffer',
|
|
1289
|
+
timeout: 30000,
|
|
1290
|
+
});
|
|
1291
|
+
const base64Image = Buffer.from(imageResponse.data).toString('base64');
|
|
1292
|
+
return {
|
|
1293
|
+
images: [
|
|
1294
|
+
{
|
|
1295
|
+
b64_json: base64Image,
|
|
1296
|
+
revised_prompt: prompt,
|
|
1297
|
+
},
|
|
1298
|
+
],
|
|
1299
|
+
model,
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (!completed) {
|
|
1307
|
+
throw new Error('ComfyUI generation timed out');
|
|
1308
|
+
}
|
|
1309
|
+
throw new Error('No image output found from ComfyUI');
|
|
1310
|
+
}
|
|
1311
|
+
catch (error) {
|
|
1312
|
+
if (axios.isAxiosError(error)) {
|
|
1313
|
+
const message = error.response?.data?.error ||
|
|
1314
|
+
error.response?.data?.message ||
|
|
1315
|
+
error.message;
|
|
1316
|
+
throw new Error(`ComfyUI generation failed: ${message}`);
|
|
1317
|
+
}
|
|
1318
|
+
throw error;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
// Get image generation configuration for a specific plugin
|
|
1322
|
+
getImageGenConfig(pluginId) {
|
|
1323
|
+
const plugin = this.getPlugin(pluginId);
|
|
1324
|
+
if (!plugin)
|
|
1325
|
+
return null;
|
|
1326
|
+
if (plugin.capabilities?.image?.config) {
|
|
1327
|
+
return plugin.capabilities.image.config;
|
|
1328
|
+
}
|
|
1329
|
+
return null;
|
|
1330
|
+
}
|
|
915
1331
|
// Get all plugins that support a specific capability type
|
|
916
1332
|
getPluginsByCapability(capabilityType) {
|
|
917
1333
|
const allPlugins = this.getAllPlugins();
|
|
@@ -919,7 +1335,7 @@ class PluginService {
|
|
|
919
1335
|
for (const plugin of allPlugins) {
|
|
920
1336
|
// Check if primary type matches
|
|
921
1337
|
if (plugin.type === capabilityType) {
|
|
922
|
-
const apiKey =
|
|
1338
|
+
const apiKey = this.getApiKey(plugin);
|
|
923
1339
|
if (apiKey) {
|
|
924
1340
|
result.push(plugin);
|
|
925
1341
|
}
|
|
@@ -947,7 +1363,7 @@ class PluginService {
|
|
|
947
1363
|
break;
|
|
948
1364
|
}
|
|
949
1365
|
if (hasCapability) {
|
|
950
|
-
const apiKey =
|
|
1366
|
+
const apiKey = this.getApiKey(plugin);
|
|
951
1367
|
if (apiKey) {
|
|
952
1368
|
result.push(plugin);
|
|
953
1369
|
}
|