Refactor package structure to de.tki.comfymodels, fix compilation errors, and prioritize imported model list in search

This commit is contained in:
Thomas
2026-04-19 11:30:23 +02:00
parent 19d7eb98e3
commit 9d722bf251
22 changed files with 1715 additions and 510 deletions
+13 -9
View File
@@ -1,8 +1,8 @@
# Build artifacts
target/
.gradle/
*.jar
*.class
dependency-reduced-pom.xml
# IDE files
.idea/
@@ -10,16 +10,20 @@ target/
.settings/
.project
.classpath
*.shfbproj
*.user
*.suo
*.userosscache
*.sln.docstates
# OS files
Thumbs.db
.DS_Store
# Temporary files
temp/
dependency-reduced-pom.xml
# --- Project Specific ---
# Sensitive API keys
app_settings.json
# User Model List (internal DB)
uploaded_models.json
# Local config & assets
workflow.json
*.png
*.bat
!src/main/resources/known_good_urls.json
+24 -2
View File
@@ -4,7 +4,7 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<groupId>de.tki</groupId>
<artifactId>comfymodeldownloader</artifactId>
<version>1.0-SNAPSHOT</version>
@@ -20,6 +20,28 @@
<artifactId>json</artifactId>
<version>20240303</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.5</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
@@ -42,7 +64,7 @@
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.comfymodels.Main</mainClass>
<mainClass>de.tki.comfymodels.Main</mainClass>
</transformer>
</transformers>
</configuration>
@@ -1,499 +0,0 @@
package com.example.comfymodels;
import org.json.JSONArray;
import org.json.JSONObject;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.io.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Main extends JFrame {
private JTextField modelsPathField;
private JTextArea jsonInputArea;
private JTable modelTable;
private DefaultTableModel tableModel;
private JProgressBar progressBar;
private JLabel statusLabel;
private JButton downloadButton;
private JButton pauseButton;
private JButton stopButton;
private List<ModelInfo> modelsToDownload = new ArrayList<>();
private final Set<String> processedModelNames = new HashSet<>();
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
private volatile boolean isPaused = false;
private volatile boolean isStopped = false;
private static final Pattern MODEL_FILE_PATTERN = Pattern.compile("([^\\\\/]+\\.(?:safetensors|ckpt|pt|bin|pth|onnx))", Pattern.CASE_INSENSITIVE);
public Main() {
setTitle("ComfyUI Model Download Manager (Pause/Resume/Stop)");
setSize(1200, 850);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout());
// Top Panel: Models Directory
JPanel topPanel = new JPanel(new BorderLayout());
topPanel.setBorder(BorderFactory.createTitledBorder("Models Directory"));
modelsPathField = new JTextField("C:\\pinokio\\api\\comfy.git\\app\\models");
JButton browsePathButton = new JButton("Browse...");
browsePathButton.addActionListener(e -> chooseDirectory());
topPanel.add(modelsPathField, BorderLayout.CENTER);
topPanel.add(browsePathButton, BorderLayout.EAST);
// Center Panel: JSON Input and Model List
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
// JSON Input
JPanel jsonPanel = new JPanel(new BorderLayout());
jsonPanel.setBorder(BorderFactory.createTitledBorder("ComfyUI JSON Workflow"));
jsonInputArea = new JTextArea();
JScrollPane jsonScroll = new JScrollPane(jsonInputArea);
JPanel jsonButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton loadJsonButton = new JButton("Load File...");
loadJsonButton.addActionListener(e -> chooseJsonFile());
JButton analyzeButton = new JButton("Analyze Models");
analyzeButton.addActionListener(e -> analyzeJson());
jsonButtons.add(loadJsonButton);
jsonButtons.add(analyzeButton);
jsonPanel.add(jsonScroll, BorderLayout.CENTER);
jsonPanel.add(jsonButtons, BorderLayout.SOUTH);
// Model List
String[] columnNames = {"Select", "Type", "Name", "URL", "Status"};
tableModel = new DefaultTableModel(columnNames, 0) {
@Override
public Class<?> getColumnClass(int columnIndex) {
return columnIndex == 0 ? Boolean.class : String.class;
}
@Override
public boolean isCellEditable(int row, int column) {
return column == 0;
}
};
modelTable = new JTable(tableModel);
modelTable.getColumnModel().getColumn(0).setMaxWidth(60);
modelTable.getColumnModel().getColumn(1).setPreferredWidth(100);
modelTable.getColumnModel().getColumn(2).setPreferredWidth(250);
modelTable.getColumnModel().getColumn(4).setPreferredWidth(300);
JScrollPane tableScroll = new JScrollPane(modelTable);
JPanel tablePanel = new JPanel(new BorderLayout());
tablePanel.setBorder(BorderFactory.createTitledBorder("Identified Models"));
JPanel selectionPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JButton selectAllBtn = new JButton("Select All");
selectAllBtn.addActionListener(e -> setAllSelected(true));
JButton deselectAllBtn = new JButton("Deselect All");
deselectAllBtn.addActionListener(e -> setAllSelected(false));
selectionPanel.add(selectAllBtn);
selectionPanel.add(deselectAllBtn);
tablePanel.add(selectionPanel, BorderLayout.NORTH);
tablePanel.add(tableScroll, BorderLayout.CENTER);
splitPane.setTopComponent(jsonPanel);
splitPane.setBottomComponent(tablePanel);
splitPane.setDividerLocation(300);
// Bottom Panel: Actions and Progress
JPanel bottomPanel = new JPanel(new BorderLayout());
progressBar = new JProgressBar(0, 100);
statusLabel = new JLabel("Ready");
JPanel actionButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
downloadButton = new JButton("Start Queue");
downloadButton.setEnabled(false);
downloadButton.addActionListener(e -> startDownloads());
pauseButton = new JButton("Pause");
pauseButton.setEnabled(false);
pauseButton.addActionListener(e -> togglePause());
stopButton = new JButton("Stop");
stopButton.setEnabled(false);
stopButton.addActionListener(e -> stopDownloads());
actionButtons.add(downloadButton);
actionButtons.add(pauseButton);
actionButtons.add(stopButton);
JPanel progressPanel = new JPanel(new GridLayout(2, 1));
progressPanel.add(statusLabel);
progressPanel.add(progressBar);
bottomPanel.add(progressPanel, BorderLayout.CENTER);
bottomPanel.add(actionButtons, BorderLayout.EAST);
add(topPanel, BorderLayout.NORTH);
add(splitPane, BorderLayout.CENTER);
add(bottomPanel, BorderLayout.SOUTH);
}
private void togglePause() {
isPaused = !isPaused;
pauseButton.setText(isPaused ? "Resume" : "Pause");
if (!isPaused) {
statusLabel.setText("Resuming...");
} else {
statusLabel.setText("Paused.");
}
}
private void stopDownloads() {
isStopped = true;
isPaused = false;
pauseButton.setText("Pause");
statusLabel.setText("Stopping queue...");
}
private void setAllSelected(boolean selected) {
for (int i = 0; i < tableModel.getRowCount(); i++) {
tableModel.setValueAt(selected, i, 0);
}
}
private void chooseDirectory() {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
modelsPathField.setText(chooser.getSelectedFile().getAbsolutePath());
}
}
private void chooseJsonFile() {
JFileChooser chooser = new JFileChooser();
chooser.setFileFilter(new FileNameExtensionFilter("JSON Files", "json"));
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
try {
String content = Files.readString(chooser.getSelectedFile().toPath());
jsonInputArea.setText(content);
analyzeJson();
} catch (IOException e) {
JOptionPane.showMessageDialog(this, "Error reading file: " + e.getMessage());
}
}
}
private void analyzeJson() {
String jsonText = jsonInputArea.getText();
if (jsonText.isEmpty()) return;
try {
modelsToDownload.clear();
processedModelNames.clear();
tableModel.setRowCount(0);
JSONObject jsonObject = new JSONObject(jsonText);
findModelsMetadata(jsonObject);
scanForModelFiles(jsonObject);
for (ModelInfo info : modelsToDownload) {
tableModel.addRow(new Object[]{true, info.type, info.name, info.url, "Pending"});
}
downloadButton.setEnabled(!modelsToDownload.isEmpty());
statusLabel.setText("Found " + modelsToDownload.size() + " models.");
} catch (Exception e) {
JOptionPane.showMessageDialog(this, "Error analyzing JSON: " + e.getMessage());
}
}
private void findModelsMetadata(Object obj) {
if (obj instanceof JSONObject) {
JSONObject jo = (JSONObject) obj;
if (jo.has("models") && jo.get("models") instanceof JSONArray) {
JSONArray models = jo.getJSONArray("models");
for (int i = 0; i < models.length(); i++) {
Object mObj = models.get(i);
if (mObj instanceof JSONObject) {
JSONObject m = (JSONObject) mObj;
String name = m.optString("name");
if (name != null && !name.isEmpty()) {
String url = m.optString("url", "MISSING");
String dir = m.optString("directory", "checkpoints");
addModelInfo(new ModelInfo(dir, name, url));
}
}
}
}
for (String key : jo.keySet()) {
findModelsMetadata(jo.get(key));
}
} else if (obj instanceof JSONArray) {
JSONArray ja = (JSONArray) obj;
for (int i = 0; i < ja.length(); i++) {
findModelsMetadata(ja.get(i));
}
}
}
private void scanForModelFiles(Object obj) {
if (obj instanceof JSONObject) {
JSONObject jo = (JSONObject) obj;
for (String key : jo.keySet()) {
Object val = jo.get(key);
if (val instanceof String) {
checkAndAddModelFromFileName((String) val, inferTypeFromKey(key));
} else {
scanForModelFiles(val);
}
}
} else if (obj instanceof JSONArray) {
JSONArray ja = (JSONArray) obj;
for (int i = 0; i < ja.length(); i++) {
Object val = ja.get(i);
if (val instanceof String) {
checkAndAddModelFromFileName((String) val, "checkpoints");
} else {
scanForModelFiles(val);
}
}
}
}
private void checkAndAddModelFromFileName(String text, String type) {
Matcher m = MODEL_FILE_PATTERN.matcher(text);
while (m.find()) {
String fileName = m.group(1);
if (!processedModelNames.contains(fileName)) {
addModelInfo(new ModelInfo(type, fileName, "MISSING (Needs manual URL)"));
}
}
}
private String inferTypeFromKey(String key) {
if (key.contains("ckpt") || key.contains("checkpoint")) return "checkpoints";
if (key.contains("lora")) return "loras";
if (key.contains("vae")) return "vae";
if (key.contains("control") && key.contains("net")) return "controlnet";
if (key.contains("upscale")) return "upscale_models";
if (key.contains("clip")) return "clip";
if (key.contains("unet") || key.contains("diffusion")) return "unet";
return "checkpoints";
}
private void addModelInfo(ModelInfo info) {
for (int i = 0; i < modelsToDownload.size(); i++) {
ModelInfo existing = modelsToDownload.get(i);
if (existing.name.equals(info.name)) {
if (existing.url.startsWith("MISSING") && !info.url.startsWith("MISSING")) {
modelsToDownload.set(i, info);
}
return;
}
}
modelsToDownload.add(info);
processedModelNames.add(info.name);
}
private void startDownloads() {
String baseDir = modelsPathField.getText();
if (baseDir.isEmpty()) {
JOptionPane.showMessageDialog(this, "Please specify a models directory.");
return;
}
isStopped = false;
isPaused = false;
downloadButton.setEnabled(false);
pauseButton.setEnabled(true);
stopButton.setEnabled(true);
executor.submit(() -> {
try {
for (int i = 0; i < tableModel.getRowCount(); i++) {
if (isStopped) {
updateStatus(i, "Stopped");
continue;
}
boolean selected = (Boolean) tableModel.getValueAt(i, 0);
if (!selected) continue;
ModelInfo info = modelsToDownload.get(i);
if (info.url.startsWith("MISSING")) {
updateStatus(i, "Skipped (No URL)");
continue;
}
try {
processDownload(info, baseDir, i);
} catch (Exception e) {
updateStatus(i, "Error: " + e.getMessage());
}
}
} finally {
SwingUtilities.invokeLater(() -> {
statusLabel.setText(isStopped ? "Queue stopped." : "Queue finished.");
downloadButton.setEnabled(true);
pauseButton.setEnabled(false);
stopButton.setEnabled(false);
pauseButton.setText("Pause");
});
}
});
}
private void updateStatus(int row, String status) {
SwingUtilities.invokeLater(() -> tableModel.setValueAt(status, row, 4));
}
private void processDownload(ModelInfo info, String baseDir, int rowIndex) throws Exception {
Path targetDir = Paths.get(baseDir, info.type);
Files.createDirectories(targetDir);
Path targetFile = targetDir.resolve(info.name);
long existingSize = 0;
if (Files.exists(targetFile)) {
existingSize = Files.size(targetFile);
long totalSize = getServerFileSize(info.url);
if (totalSize != -1 && existingSize == totalSize) {
updateStatus(rowIndex, "Already exists (Complete)");
return;
}
final long fTotalSize = totalSize;
final long fExistingSize = existingSize;
int choice = showChoiceDialog(info.name, fExistingSize, fTotalSize);
if (choice == 2) { // Skip
updateStatus(rowIndex, "Skipped");
return;
} else if (choice == 1) { // Overwrite
Files.delete(targetFile);
existingSize = 0;
}
}
downloadWithResume(info, targetFile, existingSize, rowIndex);
}
private long getServerFileSize(String url) {
try {
HttpRequest headRequest = HttpRequest.newBuilder()
.uri(URI.create(url))
.method("HEAD", HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<Void> response = httpClient.send(headRequest, HttpResponse.BodyHandlers.discarding());
return response.headers().firstValueAsLong("Content-Length").orElse(-1L);
} catch (Exception e) {
return -1;
}
}
private int showChoiceDialog(String name, long current, long total) throws Exception {
final int[] result = new int[1];
String totalStr = total == -1 ? "unknown" : (total / 1024 / 1024) + " MB";
String currentStr = (current / 1024 / 1024) + " MB";
SwingUtilities.invokeAndWait(() -> {
String message = String.format("File '%s' already exists.\nLocal size: %s\nRemote size: %s", name, currentStr, totalStr);
Object[] options = {"Resume", "Overwrite", "Skip"};
result[0] = JOptionPane.showOptionDialog(this, message, "Existing File Found",
JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]);
});
return result[0];
}
private void downloadWithResume(ModelInfo info, Path targetFile, long startByte, int rowIndex) throws IOException, InterruptedException {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().uri(URI.create(info.url));
if (startByte > 0) {
requestBuilder.header("Range", "bytes=" + startByte + "-");
}
HttpRequest request = requestBuilder.build();
HttpResponse<InputStream> response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());
if (response.statusCode() != 200 && response.statusCode() != 206) {
throw new IOException("HTTP " + response.statusCode());
}
long contentLen = response.headers().firstValueAsLong("Content-Length").orElse(-1L);
long totalBytes = startByte + (contentLen != -1 ? contentLen : 0);
try (InputStream is = response.body();
OutputStream os = new FileOutputStream(targetFile.toFile(), startByte > 0)) {
byte[] buffer = new byte[16384];
long downloaded = startByte;
long lastUpdate = System.currentTimeMillis();
long lastDownloaded = startByte;
int read;
while ((read = is.read(buffer)) != -1) {
if (isStopped) {
updateStatus(rowIndex, "Stopped at " + (downloaded * 100 / (totalBytes > 0 ? totalBytes : 1)) + "%");
return;
}
while (isPaused) {
Thread.sleep(500);
if (isStopped) return;
}
os.write(buffer, 0, read);
downloaded += read;
long now = System.currentTimeMillis();
if (now - lastUpdate > 500) {
long diff = downloaded - lastDownloaded;
double speed = diff / ((now - lastUpdate) / 1000.0) / 1024.0 / 1024.0;
final long fDownloaded = downloaded;
final double fSpeed = speed;
final int progress = totalBytes > 0 ? (int) (fDownloaded * 100 / totalBytes) : 0;
SwingUtilities.invokeLater(() -> {
progressBar.setValue(progress);
String statusText = String.format("Downloading: %d%% (%.2f MB/s)", progress, fSpeed);
tableModel.setValueAt(statusText, rowIndex, 4);
statusLabel.setText("Current: " + info.name + " [" + statusText + "]");
});
lastUpdate = now;
lastDownloaded = downloaded;
}
}
}
if (!isStopped) updateStatus(rowIndex, "Finished");
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
Main main = new Main();
main.setLocationRelativeTo(null);
main.setVisible(true);
});
}
static class ModelInfo {
String type;
String name;
String url;
public ModelInfo(String type, String name, String url) {
this.type = type;
this.name = name;
this.url = url;
}
}
}
@@ -0,0 +1,88 @@
package de.tki.comfymodels;
import de.tki.comfymodels.domain.ModelInfo;
import de.tki.comfymodels.service.impl.ComfyModelAnalyzer;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.List;
import java.util.zip.InflaterInputStream;
public class BatchVerification {
public static void main(String[] args) throws Exception {
ComfyModelAnalyzer analyzer = new ComfyModelAnalyzer();
Path root = Paths.get("C:/pinokio/api/comfy.git/workflows/ComfyUI_examples");
System.out.println("=== BATCH VERIFICATION START ===");
Files.walk(root)
.filter(p -> p.toString().endsWith(".json") || p.toString().endsWith(".png"))
.limit(30) // Stichprobe von 30 verschiedenen Workflows
.forEach(path -> {
try {
System.out.println("\nFILE: " + path.getFileName());
String content;
if (path.toString().endsWith(".png")) {
content = extractWorkflowFromPng(path.toFile());
} else {
content = Files.readString(path);
}
if (content == null) {
System.out.println(" [!] No workflow found in PNG");
return;
}
List<ModelInfo> results = analyzer.analyze(content, path.getFileName().toString());
if (results.isEmpty()) {
System.out.println(" [?] No models detected");
} else {
for (ModelInfo info : results) {
System.out.printf(" [%s] %s | URL: %s | Source: %s\n",
info.getType(), info.getName(),
info.getUrl().equals("MISSING") ? "Missing" : "FOUND",
info.getPopularity());
}
}
} catch (Exception e) {
System.out.println(" [ERROR] " + e.getMessage());
}
});
System.out.println("\n=== BATCH VERIFICATION END ===");
}
private static String extractWorkflowFromPng(File file) {
try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
if (dis.readLong() != 0x89504E470D0A1A0AL) return null;
while (true) {
int length = dis.readInt();
byte[] typeBytes = new byte[4];
dis.readFully(typeBytes);
String type = new String(typeBytes, StandardCharsets.US_ASCII);
if ("tEXt".equals(type) || "iTXt".equals(type) || "zTXt".equals(type)) {
byte[] data = new byte[length];
dis.readFully(data);
int nullPos = 0; while (nullPos < data.length && data[nullPos] != 0) nullPos++;
String key = new String(data, 0, nullPos, StandardCharsets.UTF_8);
String value = null;
if ("tEXt".equals(type)) value = new String(data, nullPos + 1, data.length - (nullPos + 1), StandardCharsets.UTF_8);
else if ("zTXt".equals(type)) {
try (InflaterInputStream iis = new InflaterInputStream(new ByteArrayInputStream(data, nullPos + 2, data.length - (nullPos + 2)))) { value = new String(iis.readAllBytes(), StandardCharsets.UTF_8); }
} else if ("iTXt".equals(type)) {
int currentPos = nullPos + 3; int nullCount = 0;
while (currentPos < data.length && nullCount < 2) { if (data[currentPos] == 0) nullCount++; currentPos++; }
if (currentPos < data.length) {
if (data[nullPos + 1] != 0) {
try (InflaterInputStream iis = new InflaterInputStream(new ByteArrayInputStream(data, currentPos, data.length - currentPos))) { value = new String(iis.readAllBytes(), StandardCharsets.UTF_8); }
} else value = new String(data, currentPos, data.length - currentPos, StandardCharsets.UTF_8);
}
}
if ("workflow".equalsIgnoreCase(key) || "prompt".equalsIgnoreCase(key)) return value;
dis.readInt();
} else if ("IEND".equals(type)) break; else dis.skipBytes(length + 4);
}
} catch (Exception e) {}
return null;
}
}
+324
View File
@@ -0,0 +1,324 @@
package de.tki.comfymodels;
import de.tki.comfymodels.domain.ModelInfo;
import de.tki.comfymodels.service.IDownloadManager;
import de.tki.comfymodels.service.IModelAnalyzer;
import de.tki.comfymodels.service.IWorkflowService;
import de.tki.comfymodels.service.IModelSearchService;
import de.tki.comfymodels.service.impl.ConfigService;
import de.tki.comfymodels.service.impl.GeminiAIService;
import de.tki.comfymodels.service.impl.ModelListService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.*;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.InflaterInputStream;
@Component
public class Main extends JFrame {
private final IModelAnalyzer analyzer;
private final IDownloadManager downloadManager;
private final IWorkflowService workflowService;
private final IModelSearchService searchService;
@Autowired
private ConfigService configService;
@Autowired
private GeminiAIService geminiService;
@Autowired
private ModelListService modelListService;
private JTextField modelsPathField;
private JPasswordField geminiKeyField;
private JPasswordField hfTokenField;
private JLabel activeAiModelLabel;
private JTextArea jsonInputArea;
private DefaultTableModel tableModel;
private JProgressBar progressBar;
private JLabel statusLabel;
private JButton downloadButton, pauseButton, stopButton;
private List<ModelInfo> modelsToDownload;
private String currentFileName = "input.json";
public Main(IModelAnalyzer analyzer, IDownloadManager downloadManager,
IWorkflowService workflowService, IModelSearchService searchService) {
this.analyzer = analyzer;
this.downloadManager = downloadManager;
this.workflowService = workflowService;
this.searchService = searchService;
}
@Autowired
public void start() {
initUI();
updateAiModelDisplay();
SwingUtilities.invokeLater(() -> setVisible(true));
}
private void updateAiModelDisplay() {
new Thread(() -> {
String model = geminiService.discoverBestModel();
SwingUtilities.invokeLater(() -> activeAiModelLabel.setText("Active AI: " + model));
}).start();
}
private void initUI() {
setTitle("ComfyUI Downloader (ULTRA-VALIDATION)");
setSize(1450, 1000);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new BorderLayout());
JPanel settingsPanel = new JPanel(new GridLayout(4, 1));
JPanel pathRow = new JPanel(new BorderLayout());
pathRow.setBorder(BorderFactory.createTitledBorder("Models Directory"));
modelsPathField = new JTextField("C:\\pinokio\\api\\comfy.git\\app\\models");
JButton browsePathBtn = new JButton("Set Folder...");
browsePathBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
modelsPathField.setText(chooser.getSelectedFile().getAbsolutePath());
}
});
pathRow.add(modelsPathField, BorderLayout.CENTER);
pathRow.add(browsePathBtn, BorderLayout.EAST);
JPanel apiRow = new JPanel(new BorderLayout());
apiRow.setBorder(BorderFactory.createTitledBorder("Gemini AI API Key"));
geminiKeyField = new JPasswordField(configService.getGeminiApiKey());
JButton saveGeminiBtn = new JButton("Save & Discover");
saveGeminiBtn.addActionListener(e -> {
configService.setGeminiApiKey(new String(geminiKeyField.getPassword()));
updateAiModelDisplay();
});
apiRow.add(geminiKeyField, BorderLayout.CENTER);
apiRow.add(saveGeminiBtn, BorderLayout.EAST);
JPanel hfRow = new JPanel(new BorderLayout());
hfRow.setBorder(BorderFactory.createTitledBorder("Hugging Face Access Token"));
hfTokenField = new JPasswordField(configService.getHfToken());
JButton saveHfBtn = new JButton("Save HF Token");
saveHfBtn.addActionListener(e -> {
configService.setHfToken(new String(hfTokenField.getPassword()));
statusLabel.setText("HF Token saved.");
analyzeJsonContent(); // Re-validate sizes with token
});
hfRow.add(hfTokenField, BorderLayout.CENTER);
hfRow.add(saveHfBtn, BorderLayout.EAST);
JPanel infoRow = new JPanel(new FlowLayout(FlowLayout.LEFT));
activeAiModelLabel = new JLabel("Active AI: Detecting...");
activeAiModelLabel.setFont(new Font("SansSerif", Font.BOLD, 12));
activeAiModelLabel.setForeground(new Color(0, 100, 200));
infoRow.add(activeAiModelLabel);
settingsPanel.add(pathRow);
settingsPanel.add(apiRow);
settingsPanel.add(hfRow);
settingsPanel.add(infoRow);
JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
JPanel jsonPanel = new JPanel(new BorderLayout());
jsonPanel.setBorder(BorderFactory.createTitledBorder("Workflow (Drag & Drop JSON/PNG)"));
jsonInputArea = new JTextArea();
setupDragAndDrop(jsonInputArea);
JScrollPane jsonScroll = new JScrollPane(jsonInputArea);
JPanel jsonButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
JButton loadJsonBtn = new JButton("Load Workflow...");
loadJsonBtn.addActionListener(e -> {
FileDialog fd = new FileDialog(this, "Select Workflow", FileDialog.LOAD);
fd.setVisible(true);
if (fd.getFile() != null) loadFile(new File(fd.getDirectory(), fd.getFile()));
});
JButton importModelListBtn = new JButton("Import Model List (JSON)...");
importModelListBtn.addActionListener(e -> {
FileDialog fd = new FileDialog(this, "Select Model List JSON", FileDialog.LOAD);
fd.setVisible(true);
if (fd.getFile() != null) {
try {
modelListService.importJson(new File(fd.getDirectory(), fd.getFile()));
JOptionPane.showMessageDialog(this, "Model list imported successfully! (" + modelListService.getModels().size() + " models)");
analyzeJsonContent(); // Re-analyze current workflow with new list
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Error importing model list: " + ex.getMessage());
}
}
});
JButton analyzeBtn = new JButton("Deep Search (Strict Validation)");
analyzeBtn.addActionListener(e -> {
analyzeJsonContent();
searchMissingOnline();
});
jsonButtons.add(loadJsonBtn);
jsonButtons.add(importModelListBtn);
jsonButtons.add(analyzeBtn);
jsonPanel.add(jsonScroll, BorderLayout.CENTER);
jsonPanel.add(jsonButtons, BorderLayout.SOUTH);
String[] columnNames = {"Select", "Type", "Name", "Size", "AI Trust & Source", "URL", "Status"};
tableModel = new DefaultTableModel(columnNames, 0) {
@Override public Class<?> getColumnClass(int c) { return c == 0 ? Boolean.class : String.class; }
@Override public boolean isCellEditable(int r, int c) { return c == 0; }
};
JTable modelTable = new JTable(tableModel);
modelTable.getColumnModel().getColumn(0).setMaxWidth(60);
modelTable.getColumnModel().getColumn(4).setMinWidth(250);
JScrollPane tableScroll = new JScrollPane(modelTable);
JPanel tablePanel = new JPanel(new BorderLayout());
tablePanel.setBorder(BorderFactory.createTitledBorder("Identified Models"));
tablePanel.add(tableScroll, BorderLayout.CENTER);
splitPane.setTopComponent(jsonPanel);
splitPane.setBottomComponent(tablePanel);
splitPane.setDividerLocation(300);
JPanel bottomPanel = new JPanel(new BorderLayout());
progressBar = new JProgressBar(0, 100);
statusLabel = new JLabel("Ready");
JPanel actionButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT));
downloadButton = new JButton("Start Queue");
downloadButton.setEnabled(false);
downloadButton.addActionListener(e -> {
downloadButton.setEnabled(false);
downloadManager.startQueue(modelsToDownload, modelsPathField.getText(),
(idx, status) -> SwingUtilities.invokeLater(() -> tableModel.setValueAt(status, idx, 6)),
() -> SwingUtilities.invokeLater(() -> {
downloadButton.setEnabled(true);
statusLabel.setText("Queue finished.");
})
);
});
pauseButton = new JButton("Pause");
pauseButton.addActionListener(e -> {
downloadManager.togglePause();
pauseButton.setText(downloadManager.isPaused() ? "Resume" : "Pause");
});
stopButton = new JButton("Stop");
stopButton.addActionListener(e -> downloadManager.stop());
actionButtons.add(downloadButton);
actionButtons.add(pauseButton);
actionButtons.add(stopButton);
JPanel progressPanel = new JPanel(new GridLayout(2, 1));
progressPanel.add(statusLabel);
progressPanel.add(progressBar);
bottomPanel.add(progressPanel, BorderLayout.CENTER);
bottomPanel.add(actionButtons, BorderLayout.EAST);
add(settingsPanel, BorderLayout.NORTH);
add(splitPane, BorderLayout.CENTER);
add(bottomPanel, BorderLayout.SOUTH);
}
private void setupDragAndDrop(JTextArea area) {
new DropTarget(area, new DropTargetListener() {
public void dragEnter(DropTargetDragEvent dtde) {}
public void dragOver(DropTargetDragEvent dtde) {}
public void dropActionChanged(DropTargetDragEvent dtde) {}
public void dragExit(DropTargetEvent dte) {}
public void drop(DropTargetDropEvent dtde) {
try {
dtde.acceptDrop(DnDConstants.ACTION_COPY);
List<File> files = (List<File>) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
if (!files.isEmpty()) loadFile(files.get(0));
} catch (Exception e) {}
}
});
}
private void loadFile(File file) {
try {
currentFileName = file.getName();
String content = workflowService.extractWorkflow(file);
jsonInputArea.setText(content);
analyzeJsonContent();
} catch (IOException ex) { JOptionPane.showMessageDialog(this, "Error: " + ex.getMessage()); }
}
private void analyzeJsonContent() {
String text = jsonInputArea.getText();
if (text.isEmpty()) return;
modelsToDownload = analyzer.analyze(text, currentFileName);
tableModel.setRowCount(0);
for (int i = 0; i < modelsToDownload.size(); i++) {
ModelInfo info = modelsToDownload.get(i);
tableModel.addRow(new Object[]{true, info.getType(), info.getName(), info.getSize(), info.getPopularity(), info.getUrl(), info.getUrl().equals("MISSING") ? "Idle" : "✅ Known Good"});
if (!info.getUrl().equals("MISSING")) fetchSizeInBackground(info, i);
}
downloadButton.setEnabled(!modelsToDownload.isEmpty());
}
private void searchMissingOnline() {
if (modelsToDownload == null) return;
int rowCount = tableModel.getRowCount();
boolean[] selected = new boolean[rowCount];
boolean anySelected = false;
for (int i = 0; i < rowCount; i++) {
selected[i] = (Boolean) tableModel.getValueAt(i, 0);
if (selected[i]) anySelected = true;
}
if (!anySelected) {
JOptionPane.showMessageDialog(this, "Please select at least one model in the table to search.");
return;
}
statusLabel.setText("Deep Search in progress...");
searchService.searchOnline(modelsToDownload, selected, jsonInputArea.getText(), currentFileName,
(idx, status) -> SwingUtilities.invokeLater(() -> tableModel.setValueAt(status, idx, 6)),
(idx, info) -> SwingUtilities.invokeLater(() -> {
tableModel.setValueAt(info.getSize(), idx, 3);
tableModel.setValueAt(info.getPopularity(), idx, 4);
tableModel.setValueAt(info.getUrl(), idx, 5);
tableModel.setValueAt("✅ Found", idx, 6);
})
);
}
private void fetchSizeInBackground(ModelInfo info, int rowIndex) {
new Thread(() -> {
long size = searchService.getRemoteSize(info.getUrl());
String s = searchService.formatSize(size);
info.setSize(s);
SwingUtilities.invokeLater(() -> tableModel.setValueAt(s, rowIndex, 3));
}).start();
}
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(Main.class);
}
@Configuration
@ComponentScan("de.tki.comfymodels")
public static class AppConfig {}
}
@@ -0,0 +1,49 @@
package de.tki.comfymodels;
import java.io.*;
import java.nio.charset.StandardCharsets;
public class PngTest {
public static void main(String[] args) {
File file = new File("flux_canny_model_example.png");
String workflow = extractWorkflowFromPng(file);
if (workflow != null) {
System.out.println("SUCCESS! Extracted workflow length: " + workflow.length());
System.out.println("Snippet: " + workflow.substring(0, Math.min(200, workflow.length())));
} else {
System.out.println("FAILED! No workflow found.");
}
}
private static String extractWorkflowFromPng(File file) {
try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
if (dis.readLong() != 0x89504E470D0A1A0AL) return null;
while (true) {
int length = dis.readInt();
byte[] typeBytes = new byte[4];
dis.readFully(typeBytes);
String type = new String(typeBytes, StandardCharsets.US_ASCII);
if ("tEXt".equals(type) || "iTXt".equals(type) || "zTXt".equals(type)) {
byte[] data = new byte[length];
dis.readFully(data);
String content = new String(data, StandardCharsets.UTF_8);
if (content.contains("workflow") || content.contains("prompt")) {
for (int i = 0; i < data.length; i++) {
if (data[i] == 0) {
return new String(data, i + 1, data.length - i - 1, StandardCharsets.UTF_8).trim();
}
}
}
dis.readInt(); // CRC
} else if ("IEND".equals(type)) {
break;
} else {
dis.skipBytes(length + 4);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
@@ -0,0 +1,56 @@
package de.tki.comfymodels.domain;
import java.util.Objects;
public class ModelInfo {
private String type;
private String name;
private String url;
private String size = "Unknown";
private String popularity = ""; // Download-Zahlen zur Sicherheit
private String base;
private String save_path;
private String description;
private String reference;
private String filename;
public ModelInfo(String type, String name, String url) {
this.type = type;
this.name = name;
this.url = url;
}
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getName() { return name; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getSize() { return size; }
public void setSize(String size) { this.size = size; }
public String getPopularity() { return popularity; }
public void setPopularity(String popularity) { this.popularity = popularity; }
public String getBase() { return base; }
public void setBase(String base) { this.base = base; }
public String getSave_path() { return save_path; }
public void setSave_path(String save_path) { this.save_path = save_path; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getReference() { return reference; }
public void setReference(String reference) { this.reference = reference; }
public String getFilename() { return filename; }
public void setFilename(String filename) { this.filename = filename; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ModelInfo info = (ModelInfo) o;
return Objects.equals(name, info.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
@@ -0,0 +1,12 @@
package de.tki.comfymodels.service;
import de.tki.comfymodels.domain.ModelInfo;
import java.util.List;
import java.util.function.BiConsumer;
public interface IDownloadManager {
void startQueue(List<ModelInfo> models, String baseDir, BiConsumer<Integer, String> statusUpdater, Runnable onFinished);
void togglePause();
void stop();
boolean isPaused();
}
@@ -0,0 +1,8 @@
package de.tki.comfymodels.service;
import de.tki.comfymodels.domain.ModelInfo;
import java.util.List;
public interface IModelAnalyzer {
List<ModelInfo> analyze(String jsonText, String fileName);
}
@@ -0,0 +1,23 @@
package de.tki.comfymodels.service;
import de.tki.comfymodels.domain.ModelInfo;
import java.util.function.BiConsumer;
import java.util.List;
public interface IModelSearchService {
/**
* Searches for models online.
* @param modelsToDownload List of models to check.
* @param selectedIndices Boolean array indicating which models are selected for search.
* @param workflowContext The context of the workflow (JSON string).
* @param fileName The name of the file being processed.
* @param onStatusUpdate Callback for status updates (index, status).
* @param onModelFound Callback when a model URL is found (index, modelInfo).
*/
void searchOnline(List<ModelInfo> modelsToDownload, boolean[] selectedIndices, String workflowContext, String fileName,
BiConsumer<Integer, String> onStatusUpdate,
BiConsumer<Integer, ModelInfo> onModelFound);
long getRemoteSize(String url);
String formatSize(long bytes);
}
@@ -0,0 +1,11 @@
package de.tki.comfymodels.service;
import java.io.File;
import java.io.IOException;
public interface IWorkflowService {
/**
* Extracts the workflow JSON string from a file (JSON or PNG).
*/
String extractWorkflow(File file) throws IOException;
}
@@ -0,0 +1,192 @@
package de.tki.comfymodels.service.impl;
import de.tki.comfymodels.domain.ModelInfo;
import de.tki.comfymodels.service.IModelAnalyzer;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jakarta.annotation.PostConstruct;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@Service
public class ComfyModelAnalyzer implements IModelAnalyzer {
@Autowired(required = false)
private LocalAIService aiService;
@Autowired
private ModelListService modelListService;
@Autowired(required = false)
private GeminiAIService geminiService;
private static final class ExpertPatterns {
static final Pattern FILE = Pattern.compile("([a-zA-Z0-9_\\-\\.\\/]+\\.(?:safetensors|sft|ckpt|pt|bin|pth|onnx|yaml))", Pattern.CASE_INSENSITIVE);
static final Pattern COMP_VAE = Pattern.compile("(vae|tokenizer|autoencoder)", Pattern.CASE_INSENSITIVE);
static final Pattern COMP_CLIP = Pattern.compile("(clip|t5|text_encoder|llama|gemma|mistral)", Pattern.CASE_INSENSITIVE);
static final Pattern COMP_UNET = Pattern.compile("(unet|diffusion|model|transformer|base|transformer|upscale|esrgan)", Pattern.CASE_INSENSITIVE);
}
private final Map<String, String> knownGoodUrls = new HashMap<>();
@PostConstruct
public void init() {
try (InputStream is = getClass().getResourceAsStream("/known_good_urls.json")) {
if (is != null) {
String json = new String(is.readAllBytes(), StandardCharsets.UTF_8);
JSONObject jo = new JSONObject(json);
for (String key : jo.keySet()) {
knownGoodUrls.put(key.toLowerCase(), jo.getString(key));
}
}
} catch (Exception e) {
System.err.println("Failed to load known_good_urls.json: " + e.getMessage());
}
}
@Override
public List<ModelInfo> analyze(String jsonText, String fileName) {
// Fallback falls Spring Injection fehlt (Test-Modus)
if (aiService == null) aiService = new LocalAIService();
if (knownGoodUrls.isEmpty()) init(); // Fallback für Tests ohne Spring Context
List<ModelInfo> results = new ArrayList<>();
if (jsonText == null || jsonText.trim().isEmpty()) return results;
try {
JSONObject jo = parseJson(jsonText);
if (jo == null) return results;
findModelsMetadata(jo, results);
scanForModelFiles(jo, null, results);
applyContext(results, fileName);
for (ModelInfo info : results) {
String low = info.getName().toLowerCase();
Optional<ModelInfo> uploadedMatch = (modelListService != null) ? modelListService.findByFilename(info.getName()) : Optional.empty();
if (uploadedMatch.isPresent()) {
info.setUrl(uploadedMatch.get().getUrl());
info.setPopularity("📂 USER DEFINED LIST");
} else if (knownGoodUrls.containsKey(low)) {
info.setUrl(knownGoodUrls.get(low));
info.setPopularity("🧠 AI VERIFIED OFFICIAL");
} else {
LocalAIService.Prediction prediction = aiService.predictProvider(info.getName());
info.setPopularity(prediction.getLabel());
}
}
} catch (Exception e) {}
return results;
}
private JSONObject parseJson(String text) {
try {
String trimmed = text.trim();
if (trimmed.startsWith("{")) return new JSONObject(trimmed);
int s = trimmed.indexOf("{"), e = trimmed.lastIndexOf("}");
if (s != -1 && e != -1) return new JSONObject(trimmed.substring(s, e + 1));
} catch (Exception e) {}
return null;
}
private void applyContext(List<ModelInfo> res, String fn) {
if (fn == null) return;
String ctx = fn.toLowerCase().split("[_\\- ]")[0];
if (ctx.length() < 3) return;
for (ModelInfo i : res) if (i.getName().toLowerCase().contains(ctx) && i.getPopularity().contains("Community")) i.setPopularity("📌 Context Match: " + ctx);
}
private void findModelsMetadata(Object obj, List<ModelInfo> res) {
if (obj instanceof JSONObject) {
JSONObject jo = (JSONObject) obj;
if (jo.has("models") && jo.get("models") instanceof JSONArray) {
JSONArray ms = jo.getJSONArray("models");
for (int i = 0; i < ms.length(); i++) {
JSONObject m = ms.optJSONObject(i);
if (m != null) {
String n = m.optString("name", m.optString("filename"));
if (n != null && !n.isEmpty()) addModelInfo(res, new ModelInfo(inferTypeFromKey(m.optString("type", "checkpoints")), n, m.optString("url", "MISSING")));
}
}
}
for (String k : jo.keySet()) findModelsMetadata(jo.get(k), res);
} else if (obj instanceof JSONArray) {
JSONArray ja = (JSONArray) obj;
for (int i = 0; i < ja.length(); i++) findModelsMetadata(ja.get(i), res);
}
}
private void scanForModelFiles(Object obj, String ctx, List<ModelInfo> res) {
if (obj instanceof JSONObject) {
JSONObject jo = (JSONObject) obj;
String node = jo.optString("class_type", jo.optString("type", null));
String curCtx = node != null ? (inferTypeFromNode(node) != null ? inferTypeFromNode(node) : ctx) : ctx;
if (jo.has("widgets_values")) {
JSONArray ws = jo.optJSONArray("widgets_values");
if (ws != null) for (int i = 0; i < ws.length(); i++) if (ws.get(i) instanceof String) processVal((String) ws.get(i), curCtx, node, res);
}
for (String k : jo.keySet()) {
if (k.equals("widgets_values")) continue;
Object v = jo.get(k);
if (v instanceof String) processVal((String) v, curCtx, k, res);
else if (v instanceof JSONObject || v instanceof JSONArray) scanForModelFiles(v, curCtx, res);
}
} else if (obj instanceof JSONArray) {
JSONArray ja = (JSONArray) obj;
for (int i = 0; i < ja.length(); i++) scanForModelFiles(ja.get(i), ctx, res);
}
}
private void processVal(String v, String ctx, String hint, List<ModelInfo> res) {
Matcher m = ExpertPatterns.FILE.matcher(v);
while (m.find()) {
String f = m.group(1);
if (f.length() > 3) {
String type = hint != null ? inferTypeFromKey(hint) : null;
if (type == null) type = ctx;
String fLow = f.toLowerCase();
if (ExpertPatterns.COMP_VAE.matcher(fLow).find()) type = "vae";
else if (ExpertPatterns.COMP_CLIP.matcher(fLow).find()) type = "clip";
else if (ExpertPatterns.COMP_UNET.matcher(fLow).find() && (type == null || type.equals("checkpoints"))) type = "unet";
addModelInfo(res, new ModelInfo(type != null ? type : "checkpoints", f, "MISSING"));
}
}
}
private String inferTypeFromNode(String nt) {
nt = nt.toLowerCase();
if (nt.contains("lora")) return "loras";
if (nt.contains("vae")) return "vae";
if (nt.contains("controlnet") || nt.contains("t2i_adapter")) return "controlnet";
if (nt.contains("upscale") || nt.contains("esrgan")) return "upscale_models";
if (nt.contains("clip") || nt.contains("text_encoder") || nt.contains("cliploader") || nt.contains("llama") || nt.contains("mistral")) return "clip";
if (nt.contains("unet") || nt.contains("diffusion") || nt.contains("ltxv") || nt.contains("cosmos") || nt.contains("hunyuan") || nt.contains("wan") || nt.contains("hidream") || nt.contains("aura") || nt.contains("mochi") || nt.contains("svd")) return "unet";
return null;
}
private String inferTypeFromKey(String k) {
k = k.toLowerCase();
if (k.contains("lora")) return "loras";
if (k.contains("vae")) return "vae";
if (k.contains("clip") || k.contains("text_encoder") || k.contains("llama") || k.contains("mistral")) return "clip";
if (k.contains("unet") || k.contains("diffusion") || k.contains("hidream") || k.contains("aura") || k.contains("mochi") || k.contains("svd")) return "unet";
if (k.contains("control") && k.contains("net")) return "controlnet";
if (k.contains("upscale") || k.contains("esrgan")) return "upscale_models";
return null;
}
private void addModelInfo(List<ModelInfo> res, ModelInfo info) {
for (ModelInfo e : res) {
if (e.getName().equalsIgnoreCase(info.getName())) {
if (e.getUrl().equals("MISSING") && !info.getUrl().equals("MISSING")) e.setUrl(info.getUrl());
if (e.getType().equals("checkpoints") && !info.getType().equals("checkpoints")) e.setType(info.getType());
return;
}
}
res.add(info);
}
}
@@ -0,0 +1,45 @@
package de.tki.comfymodels.service.impl;
import org.json.JSONObject;
import org.springframework.stereotype.Service;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
@Service
public class ConfigService {
private static final String CONFIG_FILE = "app_settings.json";
private JSONObject settings;
public ConfigService() {
load();
}
private void load() {
try {
File file = new File(CONFIG_FILE);
if (file.exists()) {
settings = new JSONObject(Files.readString(file.toPath()));
} else {
settings = new JSONObject();
}
} catch (Exception e) {
settings = new JSONObject();
}
}
public void save() {
try {
Files.writeString(Paths.get(CONFIG_FILE), settings.toString(4));
} catch (Exception e) {
e.printStackTrace();
}
}
public String getGeminiApiKey() { return settings.optString("gemini_api_key", ""); }
public void setGeminiApiKey(String key) { settings.put("gemini_api_key", key); save(); }
public String getHfToken() { return settings.optString("hf_token", ""); }
public void setHfToken(String token) { settings.put("hf_token", token); save(); }
}
@@ -0,0 +1,121 @@
package de.tki.comfymodels.service.impl;
import de.tki.comfymodels.domain.ModelInfo;
import de.tki.comfymodels.service.IDownloadManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.*;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.*;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.BiConsumer;
@Service
public class DefaultDownloadManager implements IDownloadManager {
private final ExecutorService executor = Executors.newFixedThreadPool(3);
private final HttpClient httpClient = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build();
private volatile boolean isPaused = false;
private volatile boolean isStopped = false;
@Autowired
private ConfigService configService;
@Override
public void startQueue(List<ModelInfo> models, String baseDir, BiConsumer<Integer, String> statusUpdater, Runnable onFinished) {
isStopped = false;
isPaused = false;
new Thread(() -> {
try {
java.util.concurrent.CompletableFuture<?>[] futures = new java.util.concurrent.CompletableFuture[models.size()];
for (int i = 0; i < models.size(); i++) {
final int index = i;
ModelInfo info = models.get(i);
futures[i] = java.util.concurrent.CompletableFuture.runAsync(() -> {
if (isStopped) { statusUpdater.accept(index, "Stopped"); return; }
if (info.getUrl() == null || info.getUrl().startsWith("MISSING")) { statusUpdater.accept(index, "Skipped (No URL)"); return; }
try {
Path targetDir = Paths.get(baseDir, info.getType());
Files.createDirectories(targetDir);
downloadWithResume(info, targetDir.resolve(info.getName()), index, statusUpdater);
} catch (Exception e) { statusUpdater.accept(index, "Error: " + e.getMessage()); }
}, executor);
}
java.util.concurrent.CompletableFuture.allOf(futures).join();
} finally { onFinished.run(); }
}).start();
}
private void downloadWithResume(ModelInfo info, Path targetFile, int index, BiConsumer<Integer, String> statusUpdater) throws Exception {
File file = targetFile.toFile();
// ANTI-STUB LOGIK: Wenn Datei < 10KB, löschen wir sie und fangen neu an (da es wahrscheinlich ein LFS-Pointer ist)
if (file.exists() && file.length() < 10240) {
file.delete();
}
long existingFileSize = file.exists() ? file.length() : 0;
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(URI.create(info.getUrl())).header("User-Agent", "Mozilla/5.0");
String hfToken = configService.getHfToken();
if (info.getUrl().contains("huggingface.co") && !hfToken.isEmpty()) {
builder.header("Authorization", "Bearer " + hfToken);
}
if (existingFileSize > 0) builder.header("Range", "bytes=" + existingFileSize + "-");
HttpResponse<InputStream> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofInputStream());
int statusCode = response.statusCode();
if (statusCode == 401 || statusCode == 403) {
statusUpdater.accept(index, "❌ Auth Required (Token?)");
return;
}
long contentLen = response.headers().firstValueAsLong("Content-Length").orElse(0L);
long totalBytes = (statusCode == 206) ? contentLen + existingFileSize : contentLen;
// VERIFIZIERUNG: Falls die Gesamtgröße extrem klein ist, ist es ein LFS Stub
if (totalBytes > 0 && totalBytes < 5000 && info.getName().endsWith(".safetensors")) {
statusUpdater.accept(index, "❌ LFS Stub detected (Token?)");
return;
}
if (statusCode == 200) existingFileSize = 0;
try (InputStream is = response.body(); RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
raf.seek(existingFileSize);
byte[] buffer = new byte[65536];
long downloaded = existingFileSize;
int read;
long lastUpdate = 0;
while ((read = is.read(buffer)) != -1) {
if (isStopped) return;
while (isPaused) { statusUpdater.accept(index, "Paused..."); Thread.sleep(1000); }
raf.write(buffer, 0, read);
downloaded += read;
long now = System.currentTimeMillis();
if (now - lastUpdate > 800) {
statusUpdater.accept(index, "Downloading: " + (totalBytes > 0 ? (downloaded * 100 / totalBytes) : "?") + "% (" + formatSize(downloaded) + ")");
lastUpdate = now;
}
}
}
statusUpdater.accept(index, "Finished");
}
private String formatSize(long bytes) {
if (bytes < 1024 * 1024) return (bytes / 1024) + " KB";
if (bytes < 1024 * 1024 * 1024) return String.format("%.1f MB", bytes / (1024.0 * 1024.0));
return String.format("%.2f GB", bytes / (1024.0 * 1024.0 * 1024.0));
}
@Override public void togglePause() { isPaused = !isPaused; }
@Override public void stop() { isStopped = true; isPaused = false; }
@Override public boolean isPaused() { return isPaused; }
}
@@ -0,0 +1,120 @@
package de.tki.comfymodels.service.impl;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
public class GeminiAIService {
@Autowired
private ConfigService configService;
private final HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(15))
.build();
private String activeModel = "gemini-1.5-flash";
public String getActiveModel() {
return activeModel;
}
private static final List<String> MODEL_PRIORITY = Arrays.asList(
"gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3.1-flash-lite-preview",
"gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite",
"gemini-1.5-pro", "gemini-1.5-flash"
);
public String discoverBestModel() {
String apiKey = configService.getGeminiApiKey();
if (apiKey == null || apiKey.trim().isEmpty()) return "None";
try {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://generativelanguage.googleapis.com/v1beta/models?key=" + apiKey))
.GET().build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
JSONObject json = new JSONObject(response.body());
JSONArray models = json.getJSONArray("models");
List<String> available = new ArrayList<>();
for (int i = 0; i < models.length(); i++) available.add(models.getJSONObject(i).getString("name").replace("models/", ""));
for (String preferred : MODEL_PRIORITY) {
if (available.contains(preferred)) { activeModel = preferred; return activeModel; }
}
}
} catch (Exception e) {}
return activeModel;
}
public String discoverBestRepo(String modelName, String fileName, String metadataContext) {
String apiKey = configService.getGeminiApiKey();
if (apiKey == null || apiKey.trim().isEmpty()) return null;
try {
String shortContext = metadataContext.length() > 25000 ? metadataContext.substring(0, 25000) : metadataContext;
String prompt = "Websuche: Ermittle das offizielle Hugging Face Repository für die Datei '" + modelName + "'.\n\n" +
"KONTEXT:\n" +
"Workflow-Datei: " + fileName + "\n" +
"Workflow-Daten: " + shortContext + "\n\n" +
"ANWEISUNG:\n" +
"1. Identifiziere das exakte Hugging Face Repository (z.B. black-forest-labs/FLUX.1-schnell).\n" +
"2. Antworte NUR mit der Repository ID (Format: creator/repo) oder 'UNKNOWN'.\n" +
"3. Wenn du einen direkten Download-Link findest, gib diesen stattdessen aus.";
JSONObject payload = new JSONObject();
JSONArray contents = new JSONArray();
contents.put(new JSONObject().put("role", "user")
.put("parts", new JSONArray().put(new JSONObject().put("text", prompt))));
payload.put("contents", contents);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://generativelanguage.googleapis.com/v1beta/models/" + activeModel + ":generateContent?key=" + apiKey))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload.toString()))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
String result = new JSONObject(response.body()).getJSONArray("candidates")
.getJSONObject(0).getJSONObject("content").getJSONArray("parts")
.getJSONObject(0).getString("text").trim();
return result;
}
} catch (Exception e) {}
return null;
}
public String analyzeModel(String modelName) {
String apiKey = configService.getGeminiApiKey();
if (apiKey == null || apiKey.isEmpty()) return null;
try {
JSONObject payload = new JSONObject();
JSONArray contents = new JSONArray();
contents.put(new JSONObject().put("role", "user")
.put("parts", new JSONArray().put(new JSONObject().put("text", "Analyze: " + modelName + ". Return 'Creator | Arch'."))));
payload.put("contents", contents);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://generativelanguage.googleapis.com/v1beta/models/" + activeModel + ":generateContent?key=" + apiKey))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload.toString()))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
return new JSONObject(response.body()).getJSONArray("candidates").getJSONObject(0).getJSONObject("content").getJSONArray("parts").getJSONObject(0).getString("text").trim();
}
} catch (Exception ignored) {}
return null;
}
}
@@ -0,0 +1,91 @@
package de.tki.comfymodels.service.impl;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
public class LocalAIService {
private static final Map<String, String[]> KNOWLEDGE_BASE = new LinkedHashMap<>();
private static final Map<String, Double> GLOBAL_IDF = new HashMap<>();
static {
// --- TOP RESEARCH LABS ---
add("black-forest-labs", "flux, flux1, flux2, bfl, schnell, dev, pro, mistral");
add("stabilityai", "sdxl, sd15, sd21, sd3, svd, cascade, turbo, lightning, stable, diffusion");
add("Wan-AI", "wan, wan2, wan2.1, wan2.2, ti2v, t2v");
add("nvidia", "cosmos, world, tokenizer");
add("genmo", "mochi");
add("Comfy-Org", "repack, flux_text_encoders, wan_repackaged, mochi_repackaged, lumina");
add("TencentARC", "hunyuan, hyt");
// --- TOP COMMUNITY CREATORS ---
add("Lykon", "dreamshaper, absolute, reality, anytime, alys");
add("PonyDiffusion", "pony, v6, v7, score_9, rating_explicit");
add("XpucT", "deliberate, real-vision");
add("city96", "gguf, quantized");
add("Kijai", "kj, nodes, flux-fp8");
add("6tZ", "aesthetic, flux_lora");
add("Peli86", "realism, enhance");
add("lllyasviel", "controlnet, adapter, annotator, depth, canny, scribble");
calculateIdf();
}
private static void add(String provider, String keywords) {
KNOWLEDGE_BASE.put(provider, keywords.split(", "));
}
private static void calculateIdf() {
Set<String> allTerms = new HashSet<>();
for (String[] terms : KNOWLEDGE_BASE.values()) allTerms.addAll(Arrays.asList(terms));
int docCount = KNOWLEDGE_BASE.size();
for (String term : allTerms) {
long count = KNOWLEDGE_BASE.values().stream().filter(list -> Arrays.asList(list).contains(term)).count();
GLOBAL_IDF.put(term, Math.log((double) docCount / (1.0 + count)));
}
}
public Prediction predictProvider(String fileName) {
String name = fileName.toLowerCase();
// Spezial-Check für komplexe Dateinamen
if (name.contains("mistral") && name.contains("flux2")) return new Prediction("black-forest-labs", 0.95);
if (name.contains("pony") || name.contains("v6")) return new Prediction("PonyDiffusion", 0.85);
List<String> queryTokens = Arrays.asList(name.split("[_\\- \\.]"));
Map<String, Double> similarityScores = new HashMap<>();
for (Map.Entry<String, String[]> entry : KNOWLEDGE_BASE.entrySet()) {
String provider = entry.getKey();
double score = 0.0;
for (String token : queryTokens) {
if (token.length() < 2) continue;
for (String docTerm : entry.getValue()) {
if (token.equals(docTerm) || (token.contains(docTerm) && docTerm.length() > 3)) {
score += GLOBAL_IDF.getOrDefault(docTerm, 1.0) * (docTerm.length() / 4.0);
}
}
}
if (score > 0) similarityScores.put(provider, score);
}
String bestProvider = "community";
double maxScore = 0.0;
for (Map.Entry<String, Double> entry : similarityScores.entrySet()) {
if (entry.getValue() > maxScore) { maxScore = entry.getValue(); bestProvider = entry.getKey(); }
}
return new Prediction(bestProvider, Math.min(0.99, maxScore / 3.5));
}
public static class Prediction {
public final String provider;
public final double confidence;
public Prediction(String provider, double confidence) { this.provider = provider; this.confidence = confidence; }
public String getLabel() {
if (confidence > 0.75) return "🧠 AI Verified: " + provider;
if (confidence > 0.35) return "🎯 AI Predicted: " + provider;
return "👥 Community";
}
}
}
@@ -0,0 +1,110 @@
package de.tki.comfymodels.service.impl;
import de.tki.comfymodels.domain.ModelInfo;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.stereotype.Service;
import jakarta.annotation.PostConstruct;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@Service
public class ModelListService {
private static final String STORAGE_FILE = "uploaded_models.json";
private List<ModelInfo> models = new ArrayList<>();
@PostConstruct
public void init() {
loadFromStorage();
}
public void importJson(File file) throws Exception {
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
JSONObject root = new JSONObject(content);
JSONArray modelsArray = root.getJSONArray("models");
List<ModelInfo> newModels = new ArrayList<>();
for (int i = 0; i < modelsArray.length(); i++) {
JSONObject m = modelsArray.getJSONObject(i);
ModelInfo info = new ModelInfo(
m.optString("type", "Unknown"),
m.optString("name", "Unknown"),
m.optString("url", "MISSING")
);
info.setBase(m.optString("base", ""));
info.setSave_path(m.optString("save_path", ""));
info.setDescription(m.optString("description", ""));
info.setReference(m.optString("reference", ""));
info.setFilename(m.optString("filename", ""));
info.setSize(m.optString("size", "Unknown"));
newModels.add(info);
}
this.models = newModels;
saveToStorage();
}
public Optional<ModelInfo> findByFilename(String filename) {
return models.stream()
.filter(m -> filename.equalsIgnoreCase(m.getFilename()))
.findFirst();
}
private void loadFromStorage() {
try {
File file = new File(STORAGE_FILE);
if (file.exists()) {
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
JSONArray array = new JSONArray(content);
for (int i = 0; i < array.length(); i++) {
JSONObject m = array.getJSONObject(i);
ModelInfo info = new ModelInfo(
m.optString("type"),
m.optString("name"),
m.optString("url")
);
info.setBase(m.optString("base"));
info.setSave_path(m.optString("save_path"));
info.setDescription(m.optString("description"));
info.setReference(m.optString("reference"));
info.setFilename(m.optString("filename"));
info.setSize(m.optString("size"));
models.add(info);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private void saveToStorage() {
try {
JSONArray array = new JSONArray();
for (ModelInfo m : models) {
JSONObject jo = new JSONObject();
jo.put("type", m.getType());
jo.put("name", m.getName());
jo.put("url", m.getUrl());
jo.put("base", m.getBase());
jo.put("save_path", m.getSave_path());
jo.put("description", m.getDescription());
jo.put("reference", m.getReference());
jo.put("filename", m.getFilename());
jo.put("size", m.getSize());
array.put(jo);
}
Files.writeString(new File(STORAGE_FILE).toPath(), array.toString(4), StandardCharsets.UTF_8);
} catch (Exception e) {
e.printStackTrace();
}
}
public List<ModelInfo> getModels() {
return models;
}
}
@@ -0,0 +1,253 @@
package de.tki.comfymodels.service.impl;
import de.tki.comfymodels.domain.ModelInfo;
import de.tki.comfymodels.service.IModelSearchService;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.BiConsumer;
@Service
public class ModelSearchService implements IModelSearchService {
@Autowired
private ConfigService configService;
@Autowired
private GeminiAIService geminiService;
@Autowired
private ModelListService modelListService;
private static final Set<String> OFFICIAL_AUTHORS = new HashSet<>(Arrays.asList(
"black-forest-labs", "stabilityai", "runwayml", "comfyanonymous", "Comfy-Org",
"lllyasviel", "Kwai-Kolors", "Kwai-VGI", "InstantX", "ByteDance", "apple",
"google", "facebook", "TencentARC", "DeepFloyd", "microsoft", "nvidia", "fal", "genmo",
"ostris", "XLabs-AI", "THUDM", "city96", "Kijai", "BartoszGawlik"
));
private final HttpClient httpClient = HttpClient.newBuilder()
.followRedirects(HttpClient.Redirect.ALWAYS)
.build();
@Override
public void searchOnline(List<ModelInfo> modelsToDownload, boolean[] selectedIndices, String workflowContext, String fileName,
BiConsumer<Integer, String> onStatusUpdate,
BiConsumer<Integer, ModelInfo> onModelFound) {
if (modelsToDownload == null) return;
for (int i = 0; i < modelsToDownload.size(); i++) {
if (!selectedIndices[i]) continue;
ModelInfo info = modelsToDownload.get(i);
final int index = i;
new Thread(() -> {
// Priority 1: User defined Model List
Optional<ModelInfo> manualMatch = modelListService.findByFilename(info.getName());
if (manualMatch.isPresent() && !manualMatch.get().getUrl().equals("MISSING")) {
onStatusUpdate.accept(index, "📂 Found in Model List");
if (validateAndSetUrl(info, index, manualMatch.get().getUrl(), "📂 USER DEFINED", onStatusUpdate, onModelFound)) return;
}
onStatusUpdate.accept(index, "✨ Gemini Scouting...");
String aiHint = geminiService.discoverBestRepo(info.getName(), fileName, workflowContext);
if (aiHint != null && !aiHint.equalsIgnoreCase("UNKNOWN")) {
if (aiHint.startsWith("http")) {
if (validateAndSetUrl(info, index, aiHint, "✨ AI DIRECT", onStatusUpdate, onModelFound)) return;
}
onStatusUpdate.accept(index, "🔍 Validating Repo: " + aiHint);
if (fetchHuggingFaceUrlInSpecificRepo(info, index, aiHint, onStatusUpdate, onModelFound)) return;
}
String modelName = info.getName();
String cleanName = modelName.replaceAll("(_fp8|_fp16|_bf16|_v\\d+|\\d+v|_fix|\\.safetensors|\\.sft|\\.ckpt)", "");
String baseName = cleanName.split("[_\\- ]")[0];
String contextPrefix = fileName.toLowerCase().split("[_\\- ]")[0];
if (contextPrefix.length() < 3) contextPrefix = "";
LinkedHashSet<String> attempts = new LinkedHashSet<>();
if (!contextPrefix.isEmpty()) {
attempts.add(contextPrefix + " " + modelName);
attempts.add(contextPrefix + " " + cleanName);
}
attempts.add(modelName);
attempts.add(cleanName);
attempts.add(baseName);
// Suche in offiziellen Repos (Top 100)
for (String query : attempts) {
if (query.length() < 3) continue;
onStatusUpdate.accept(index, "🔍 Searching Official: " + query);
if (fetchHuggingFaceUrl(info, index, query, true, onStatusUpdate, onModelFound)) return;
}
// Suche in Community Repos (Top 100)
for (String query : attempts) {
if (query.length() < 3) continue;
onStatusUpdate.accept(index, "🔍 Searching Community: " + query);
if (fetchHuggingFaceUrl(info, index, query, false, onStatusUpdate, onModelFound)) return;
}
// Civitai Fallback
for (String query : attempts) {
if (query.length() < 3) continue;
onStatusUpdate.accept(index, "🔍 Searching Civitai: " + query);
if (fetchCivitaiUrl(info, index, query, onStatusUpdate, onModelFound)) return;
}
onStatusUpdate.accept(index, "❌ No trusted match found");
}).start();
}
}
private boolean validateAndSetUrl(ModelInfo info, int rowIndex, String url, String popPrefix,
BiConsumer<Integer, String> onStatusUpdate,
BiConsumer<Integer, ModelInfo> onModelFound) {
long size = getRemoteSize(url);
if (size > 100) {
info.setUrl(url);
info.setPopularity(popPrefix);
info.setSize(formatSize(size));
onModelFound.accept(rowIndex, info);
return true;
}
return false;
}
@Override
public long getRemoteSize(String url) {
try {
if (url.contains("huggingface.co") && url.contains("/resolve/")) {
String apiUrl = url.replace("/resolve/", "/api/models/").replace("/main/", "/file/");
HttpRequest.Builder apiBuilder = HttpRequest.newBuilder().uri(URI.create(apiUrl)).GET().header("User-Agent", "Mozilla/5.0");
String token = configService.getHfToken();
if (!token.isEmpty()) apiBuilder.header("Authorization", "Bearer " + token);
HttpResponse<String> apiRes = httpClient.send(apiBuilder.build(), HttpResponse.BodyHandlers.ofString());
if (apiRes.statusCode() == 200) {
JSONObject fileData = new JSONObject(apiRes.body());
if (fileData.has("size")) return fileData.getLong("size");
}
}
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(URI.create(url)).method("HEAD", HttpRequest.BodyPublishers.noBody()).header("User-Agent", "Mozilla/5.0");
String token = configService.getHfToken();
if (url.contains("huggingface.co") && !token.isEmpty()) builder.header("Authorization", "Bearer " + token);
HttpResponse<Void> res = httpClient.send(builder.build(), HttpResponse.BodyHandlers.discarding());
if (res.statusCode() == 401 || res.statusCode() == 403) return -401;
long bytes = res.headers().firstValueAsLong("Content-Length").orElse(-1L);
if (bytes <= 2000) {
HttpRequest.Builder rBuilder = HttpRequest.newBuilder().uri(URI.create(url)).header("Range", "bytes=0-0").header("User-Agent", "Mozilla/5.0");
if (url.contains("huggingface.co") && !token.isEmpty()) rBuilder.header("Authorization", "Bearer " + token);
HttpResponse<Void> rRes = httpClient.send(rBuilder.build(), HttpResponse.BodyHandlers.discarding());
String cr = rRes.headers().firstValue("Content-Range").orElse("");
bytes = cr.contains("/") ? Long.parseLong(cr.substring(cr.lastIndexOf("/") + 1)) : rRes.headers().firstValueAsLong("Content-Length").orElse(-1L);
}
return bytes;
} catch (Exception e) {
return -1;
}
}
@Override
public String formatSize(long bytes) {
if (bytes == -401) return "🔒 Auth Required";
if (bytes <= 0) return "Unknown";
if (bytes > 1024L * 1024L * 1024L) return String.format("%.2f GB", bytes / 1024.0 / 1024.0 / 1024.0);
return String.format("%.2f MB", bytes / 1024.0 / 1024.0);
}
private boolean fetchHuggingFaceUrlInSpecificRepo(ModelInfo info, int rowIndex, String repoId,
BiConsumer<Integer, String> onStatusUpdate,
BiConsumer<Integer, ModelInfo> onModelFound) {
try {
String treeUrl = "https://huggingface.co/api/models/" + repoId + "/tree/main?recursive=true";
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(URI.create(treeUrl)).GET();
String token = configService.getHfToken();
if (!token.isEmpty()) builder.header("Authorization", "Bearer " + token);
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
JSONArray files = new JSONArray(response.body());
String targetName = info.getName().toLowerCase();
for (int j = 0; j < files.length(); j++) {
JSONObject file = files.getJSONObject(j);
String path = file.optString("path");
String type = file.optString("type");
if ("file".equals(type)) {
String fileNameOnly = path.contains("/") ? path.substring(path.lastIndexOf("/") + 1) : path;
if (path.equalsIgnoreCase(info.getName()) || fileNameOnly.equalsIgnoreCase(targetName)) {
String downloadUrl = "https://huggingface.co/" + repoId + "/resolve/main/" + path;
return validateAndSetUrl(info, rowIndex, downloadUrl, "✨ AI RECURSIVE", onStatusUpdate, onModelFound);
}
}
}
}
} catch (Exception e) {}
return false;
}
private boolean fetchHuggingFaceUrl(ModelInfo info, int rowIndex, String query, boolean officialOnly,
BiConsumer<Integer, String> onStatusUpdate,
BiConsumer<Integer, ModelInfo> onModelFound) {
try {
String searchUrl = "https://huggingface.co/api/models?search=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&sort=downloads&direction=-1&limit=100";
HttpResponse<String> response = httpClient.send(HttpRequest.newBuilder().uri(URI.create(searchUrl)).build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
JSONArray models = new JSONArray(response.body());
for (int i = 0; i < models.length(); i++) {
String modelId = models.getJSONObject(i).getString("id");
String author = modelId.split("/")[0];
if (officialOnly && !OFFICIAL_AUTHORS.contains(author)) continue;
if (fetchHuggingFaceUrlInSpecificRepo(info, rowIndex, modelId, onStatusUpdate, onModelFound)) return true;
}
}
} catch (Exception e) {}
return false;
}
private boolean fetchCivitaiUrl(ModelInfo info, int rowIndex, String query,
BiConsumer<Integer, String> onStatusUpdate,
BiConsumer<Integer, ModelInfo> onModelFound) {
try {
String searchUrl = "https://civitai.com/api/v1/models?query=" + URLEncoder.encode(query, StandardCharsets.UTF_8) + "&sort=Most+Downloaded&limit=20";
HttpResponse<String> response = httpClient.send(HttpRequest.newBuilder().uri(URI.create(searchUrl)).build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
JSONObject json = new JSONObject(response.body());
JSONArray items = json.optJSONArray("items");
if (items != null) {
for (int i = 0; i < items.length(); i++) {
JSONArray versions = items.getJSONObject(i).optJSONArray("modelVersions");
if (versions != null) {
for (int v = 0; v < versions.length(); v++) {
JSONArray files = versions.getJSONObject(v).optJSONArray("files");
if (files != null) {
for (int j = 0; j < files.length(); j++) {
JSONObject file = files.getJSONObject(j);
if (file.optString("name").equalsIgnoreCase(info.getName()) || file.optString("name").toLowerCase().contains(query.toLowerCase())) {
return validateAndSetUrl(info, rowIndex, file.optString("downloadUrl"), "🛡️ Civitai", onStatusUpdate, onModelFound);
}
}
}
}
}
}
}
}
} catch (Exception e) {}
return false;
}
}
@@ -0,0 +1,73 @@
package de.tki.comfymodels.service.impl;
import de.tki.comfymodels.service.IWorkflowService;
import org.json.JSONObject;
import org.springframework.stereotype.Service;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.zip.InflaterInputStream;
@Service
public class WorkflowService implements IWorkflowService {
@Override
public String extractWorkflow(File file) throws IOException {
String name = file.getName().toLowerCase();
if (name.endsWith(".png")) {
return extractWorkflowFromPng(file);
} else {
return Files.readString(file.toPath());
}
}
private String extractWorkflowFromPng(File file) {
String workflow = null, prompt = null;
try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
if (dis.readLong() != 0x89504E470D0A1A0AL) return null;
while (true) {
int length = dis.readInt();
byte[] typeBytes = new byte[4];
dis.readFully(typeBytes);
String type = new String(typeBytes, StandardCharsets.US_ASCII);
if ("tEXt".equals(type) || "iTXt".equals(type) || "zTXt".equals(type)) {
byte[] data = new byte[length];
dis.readFully(data);
int nullPos = 0; while (nullPos < data.length && data[nullPos] != 0) nullPos++;
String key = new String(data, 0, nullPos, StandardCharsets.UTF_8);
String value = null;
if ("tEXt".equals(type)) value = new String(data, nullPos + 1, data.length - (nullPos + 1), StandardCharsets.UTF_8);
else if ("zTXt".equals(type)) {
if (nullPos + 2 < data.length) {
try (InflaterInputStream iis = new InflaterInputStream(new ByteArrayInputStream(data, nullPos + 2, data.length - (nullPos + 2)))) { value = new String(iis.readAllBytes(), StandardCharsets.UTF_8); }
}
} else if ("iTXt".equals(type)) {
int currentPos = nullPos + 3; int nullCount = 0;
while (currentPos < data.length && nullCount < 2) { if (data[currentPos] == 0) nullCount++; currentPos++; }
if (currentPos < data.length) {
if (data[nullPos + 1] != 0) {
try (InflaterInputStream iis = new InflaterInputStream(new ByteArrayInputStream(data, currentPos, data.length - currentPos))) { value = new String(iis.readAllBytes(), StandardCharsets.UTF_8); }
} else value = new String(data, currentPos, data.length - currentPos, StandardCharsets.UTF_8);
}
}
if (value != null && !value.isEmpty()) {
if (key.equalsIgnoreCase("workflow")) workflow = value;
else if (key.equalsIgnoreCase("prompt")) prompt = value;
else if (key.toLowerCase().contains("comfy") && workflow == null) workflow = value;
}
dis.readInt();
} else if ("IEND".equals(type)) break; else dis.skipBytes(length + 4);
}
} catch (Exception e) {
// Log error or rethrow as needed
}
if (workflow == null && prompt != null && prompt.trim().startsWith("{")) {
try {
JSONObject jo = new JSONObject(prompt);
if (jo.has("workflow")) return jo.get("workflow").toString();
} catch (Exception ignored) {}
}
return workflow != null ? workflow : prompt;
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"flux1-schnell.sft": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/flux1-schnell.safetensors",
"flux1-dev.sft": "https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors",
"flux1-dev.safetensors": "https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/flux1-dev.safetensors",
"ae.safetensors": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors",
"ae.sft": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors",
"flux2-dev.safetensors": "https://huggingface.co/black-forest-labs/FLUX.2-dev/resolve/main/flux2-dev.safetensors",
"clip_l.safetensors": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/clip_l.safetensors",
"t5xxl_fp16.safetensors": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp16.safetensors",
"t5xxl_fp8_e4m3fn.safetensors": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn.safetensors",
"clip_g.safetensors": "https://huggingface.co/Comfy-Org/stable-diffusion-v3-medium/resolve/main/text_encoders/clip_g.safetensors",
"v1-5-pruned-emaonly.safetensors": "https://huggingface.co/runwayml/stable-diffusion-v1-5/resolve/main/v1-5-pruned-emaonly.safetensors",
"sd_xl_base_1.0.safetensors": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors",
"sd_xl_refiner_1.0.safetensors": "https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors",
"sd3.5_large.safetensors": "https://huggingface.co/stabilityai/stable-diffusion-3.5-large/resolve/main/sd3.5_large.safetensors",
"sd3.5_medium.safetensors": "https://huggingface.co/stabilityai/stable-diffusion-3.5-medium/resolve/main/sd3.5_medium.safetensors",
"sd_xl_turbo_1.0_fp16.safetensors": "https://huggingface.co/stabilityai/sdxl-turbo/resolve/main/sd_xl_turbo_1.0_fp16.safetensors",
"svd.safetensors": "https://huggingface.co/stabilityai/stable-video-diffusion-img2vid/resolve/main/svd.safetensors",
"svd_xt.safetensors": "https://huggingface.co/stabilityai/stable-video-diffusion-img2vid-xt/resolve/main/svd_xt.safetensors",
"flux1-canny-dev.safetensors": "https://huggingface.co/black-forest-labs/FLUX.1-Canny-dev/resolve/main/flux1-canny-dev.safetensors",
"flux1-depth-dev.safetensors": "https://huggingface.co/black-forest-labs/FLUX.1-Depth-dev/resolve/main/flux1-depth-dev.safetensors",
"flux1-redux-dev.safetensors": "https://huggingface.co/black-forest-labs/FLUX.1-Redux-dev/resolve/main/flux1-redux-dev.safetensors",
"hunyuan_video_720_cfgdistill_fp8_e4m3fn.safetensors": "https://huggingface.co/Kwai-VGI/HunyuanVideo/resolve/main/hunyuan_video_720_cfgdistill_fp8_e4m3fn.safetensors",
"mochi_1_preview_fp8.safetensors": "https://huggingface.co/genmo/mochi-1-preview/resolve/main/mochi_1_preview_fp8.safetensors",
"ltx-video-2b-v0.9.safetensors": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltx-video-2b-v0.9.safetensors",
"vae-ft-mse-840000-ema-pruned.safetensors": "https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors",
"sdxl_vae.safetensors": "https://huggingface.co/madebyollin/sdxl-vae-fp16-fix/resolve/main/sdxl_vae.safetensors",
"RealESRGAN_x4plus.pth": "https://huggingface.co/ai-forever/Real-ESRGAN/resolve/main/RealESRGAN_x4plus.pth",
"RealESRGAN_x4plus_anime_6B.pth": "https://huggingface.co/ai-forever/Real-ESRGAN/resolve/main/RealESRGAN_x4plus_anime_6B.pth"
}
@@ -0,0 +1,46 @@
package de.tki.comfymodels.service;
import de.tki.comfymodels.domain.ModelInfo;
import de.tki.comfymodels.service.impl.ComfyModelAnalyzer;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
public class ComfyModelAnalyzerTest {
private final ComfyModelAnalyzer analyzer = new ComfyModelAnalyzer();
@Test
public void testAnalyzeBasicWorkflow() {
String json = "{\"nodes\": [{\"type\": \"CheckpointLoaderSimple\", \"widgets_values\": [\"flux1-schnell.sft\"]}]}";
List<ModelInfo> models = analyzer.analyze(json, "test.json");
assertNotNull(models);
assertFalse(models.isEmpty());
ModelInfo flux = models.stream()
.filter(m -> m.getName().equals("flux1-schnell.sft"))
.findFirst()
.orElse(null);
assertNotNull(flux);
assertEquals("checkpoints", flux.getType());
assertEquals("https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/flux1-schnell.safetensors", flux.getUrl());
}
@Test
public void testAnalyzeLora() {
String json = "{\"nodes\": [{\"type\": \"LoraLoader\", \"widgets_values\": [\"my_cool_lora.safetensors\"]}]}";
List<ModelInfo> models = analyzer.analyze(json, "test.json");
ModelInfo lora = models.stream()
.filter(m -> m.getName().equals("my_cool_lora.safetensors"))
.findFirst()
.orElse(null);
assertNotNull(lora);
assertEquals("loras", lora.getType());
}
}
@@ -0,0 +1,26 @@
package de.tki.comfymodels.service;
import de.tki.comfymodels.service.impl.WorkflowService;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import static org.junit.jupiter.api.Assertions.*;
public class WorkflowServiceTest {
private final WorkflowService workflowService = new WorkflowService();
@Test
public void testExtractFromPng() throws IOException {
File pngFile = new File("flux_canny_model_example.png");
if (pngFile.exists()) {
String workflow = workflowService.extractWorkflow(pngFile);
assertNotNull(workflow);
assertTrue(workflow.contains("{"));
} else {
System.out.println("Skipping test: flux_canny_model_example.png not found");
}
}
}