feat: add markdown link extraction and HF URL normalization in ComfyModelAnalyzer and ModelSearchService

This commit is contained in:
Thomas Kippster
2026-04-22 19:39:11 +02:00
parent 6116eb6d63
commit 60320e13a2
24 changed files with 1661 additions and 250 deletions
+217 -61
View File
@@ -16,20 +16,17 @@ import org.springframework.stereotype.Component;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.event.TableModelListener;
import javax.swing.event.TableModelEvent;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.dnd.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
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 {
@@ -37,7 +34,7 @@ public class Main extends JFrame {
private final IDownloadManager downloadManager;
private final IWorkflowService workflowService;
private final IModelSearchService searchService;
@Autowired
private ConfigService configService;
@@ -53,13 +50,12 @@ public class Main extends JFrame {
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,
public Main(IModelAnalyzer analyzer, IDownloadManager downloadManager,
IWorkflowService workflowService, IModelSearchService searchService) {
this.analyzer = analyzer;
this.downloadManager = downloadManager;
@@ -67,11 +63,77 @@ public class Main extends JFrame {
this.searchService = searchService;
}
@Autowired
public void start() {
initUI();
updateAiModelDisplay();
SwingUtilities.invokeLater(() -> setVisible(true));
public void launch() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception e) {}
if (!promptForPassword()) {
System.exit(0);
}
SwingUtilities.invokeLater(() -> {
initUI();
loadSettingsIntoUI();
updateAiModelDisplay();
// Add WindowListener to stop downloads on close
addWindowListener(new java.awt.event.WindowAdapter() {
@Override
public void windowClosing(java.awt.event.WindowEvent e) {
downloadManager.stop();
System.exit(0);
}
});
setVisible(true);
});
}
private boolean promptForPassword() {
while (true) {
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.add(new JLabel("Enter Vault Password (to unlock API Keys):"), BorderLayout.NORTH);
JPasswordField pf = new JPasswordField();
panel.add(pf, BorderLayout.CENTER);
// Focus request
SwingUtilities.invokeLater(() -> pf.requestFocusInWindow());
int ok = JOptionPane.showConfirmDialog(null, panel, "Vault Unlock", JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE);
if (ok != JOptionPane.OK_OPTION) return false;
String pass = new String(pf.getPassword());
if (pass.trim().isEmpty()) {
JOptionPane.showMessageDialog(null, "Password cannot be empty.", "Validation", JOptionPane.WARNING_MESSAGE);
continue;
}
try {
configService.unlock(pass);
return true;
} catch (Exception e) {
JOptionPane.showMessageDialog(null, "Unlock Failed: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
}
}
}
private void loadSettingsIntoUI() {
if (modelsPathField != null) {
String path = configService.getModelsPath();
modelsPathField.setText(path);
System.out.println("GUI: Models Path loaded: " + path);
}
if (geminiKeyField != null) {
String key = configService.getGeminiApiKey();
geminiKeyField.setText(key);
System.out.println("GUI: Gemini Key loaded (length: " + (key != null ? key.length() : 0) + ")");
}
if (hfTokenField != null) {
String token = configService.getHfToken();
hfTokenField.setText(token);
System.out.println("GUI: HF Token loaded (length: " + (token != null ? token.length() : 0) + ")");
}
}
private void updateAiModelDisplay() {
@@ -82,30 +144,48 @@ public class Main extends JFrame {
}
private void initUI() {
setTitle("ComfyUI Downloader (ULTRA-VALIDATION)");
setTitle("ComfyUIModel-Downloader");
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");
modelsPathField = new JTextField();
JButton browsePathBtn = new JButton("Set Folder...");
browsePathBtn.addActionListener(e -> {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
String current = modelsPathField.getText();
if (current != null && !current.isEmpty()) {
File dir = new File(current);
if (dir.exists()) chooser.setCurrentDirectory(dir);
}
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
modelsPathField.setText(chooser.getSelectedFile().getAbsolutePath());
String selectedPath = chooser.getSelectedFile().getAbsolutePath();
modelsPathField.setText(selectedPath);
configService.setModelsPath(selectedPath);
}
});
JButton savePathBtn = new JButton("Save Path");
savePathBtn.addActionListener(e -> {
configService.setModelsPath(modelsPathField.getText());
statusLabel.setText("Models path saved to vault.");
});
JPanel pathButtons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
pathButtons.add(savePathBtn);
pathButtons.add(browsePathBtn);
pathRow.add(modelsPathField, BorderLayout.CENTER);
pathRow.add(browsePathBtn, BorderLayout.EAST);
pathRow.add(pathButtons, BorderLayout.EAST);
JPanel apiRow = new JPanel(new BorderLayout());
apiRow.setBorder(BorderFactory.createTitledBorder("Gemini AI API Key"));
geminiKeyField = new JPasswordField(configService.getGeminiApiKey());
geminiKeyField = new JPasswordField();
JButton saveGeminiBtn = new JButton("Save & Discover");
saveGeminiBtn.addActionListener(e -> {
configService.setGeminiApiKey(new String(geminiKeyField.getPassword()));
@@ -116,21 +196,20 @@ public class Main extends JFrame {
JPanel hfRow = new JPanel(new BorderLayout());
hfRow.setBorder(BorderFactory.createTitledBorder("Hugging Face Access Token"));
hfTokenField = new JPasswordField(configService.getHfToken());
hfTokenField = new JPasswordField();
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
analyzeJsonContent();
});
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);
JButton helpBtn = new JButton("ℹ Help & Security Information");
helpBtn.addActionListener(e -> showHelpDialog());
infoRow.add(helpBtn);
settingsPanel.add(pathRow);
settingsPanel.add(apiRow);
@@ -143,7 +222,7 @@ public class Main extends JFrame {
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 -> {
@@ -159,34 +238,52 @@ public class Main extends JFrame {
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
JOptionPane.showMessageDialog(this, "Model list imported successfully!");
analyzeJsonContent();
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, "Error importing model list: " + ex.getMessage());
JOptionPane.showMessageDialog(this, "Error: " + 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"};
String[] columnNames = {"Select", "Type", "Name", "Size", "AI Source", "Target Path", "URL", "Status"};
tableModel = new DefaultTableModel(columnNames, 0) {
@Override public Class<?> getColumnClass(int c) { return c == 0 ? Boolean.class : String.class; }
@Override public Class<?> getColumnClass(int c) { return c == 0 ? Boolean.class : String.class; }
@Override public boolean isCellEditable(int r, int c) { return c == 0; }
};
tableModel.addTableModelListener(e -> {
if (e.getType() == TableModelEvent.UPDATE && e.getColumn() == 0) {
updateDownloadManagerSelection();
}
});
JTable modelTable = new JTable(tableModel);
modelTable.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);
// Sofortiger Abbruch des Editors bei Klick, um Wert zu übernehmen
modelTable.addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
if (modelTable.isEditing()) {
modelTable.getCellEditor().stopCellEditing();
}
}
});
modelTable.getColumnModel().getColumn(0).setMaxWidth(60);
modelTable.getColumnModel().getColumn(4).setMinWidth(250);
modelTable.getColumnModel().getColumn(4).setMinWidth(150);
modelTable.getColumnModel().getColumn(5).setMinWidth(150);
JScrollPane tableScroll = new JScrollPane(modelTable);
JPanel tablePanel = new JPanel(new BorderLayout());
tablePanel.setBorder(BorderFactory.createTitledBorder("Identified Models"));
@@ -197,16 +294,25 @@ public class Main extends JFrame {
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 -> {
int rowCount = tableModel.getRowCount();
boolean[] selected = new boolean[rowCount];
for (int i = 0; i < rowCount; i++) {
selected[i] = (Boolean) tableModel.getValueAt(i, 0);
String urlFromTable = (String) tableModel.getValueAt(i, 6);
if (urlFromTable != null && !urlFromTable.equals("MISSING")) {
modelsToDownload.get(i).setUrl(urlFromTable);
}
}
downloadButton.setEnabled(false);
downloadManager.startQueue(modelsToDownload, modelsPathField.getText(),
(idx, status) -> SwingUtilities.invokeLater(() -> tableModel.setValueAt(status, idx, 6)),
downloadManager.startQueue(modelsToDownload, selected, modelsPathField.getText(),
(idx, status) -> SwingUtilities.invokeLater(() -> tableModel.setValueAt(status, idx, 7)),
() -> SwingUtilities.invokeLater(() -> {
downloadButton.setEnabled(true);
statusLabel.setText("Queue finished.");
@@ -216,20 +322,26 @@ public class Main extends JFrame {
pauseButton = new JButton("Pause");
pauseButton.addActionListener(e -> {
if (downloadManager.isPaused()) {
updateDownloadManagerSelection();
}
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));
activeAiModelLabel = new JLabel("Active AI: Detecting...");
activeAiModelLabel.setFont(new Font("SansSerif", Font.ITALIC, 11));
activeAiModelLabel.setForeground(Color.GRAY);
progressPanel.add(activeAiModelLabel);
progressPanel.add(statusLabel);
progressPanel.add(progressBar);
bottomPanel.add(progressPanel, BorderLayout.CENTER);
bottomPanel.add(actionButtons, BorderLayout.EAST);
@@ -238,6 +350,15 @@ public class Main extends JFrame {
add(bottomPanel, BorderLayout.SOUTH);
}
private void updateDownloadManagerSelection() {
int rowCount = tableModel.getRowCount();
boolean[] selected = new boolean[rowCount];
for (int i = 0; i < rowCount; i++) {
selected[i] = (Boolean) tableModel.getValueAt(i, 0);
}
downloadManager.updateSelection(selected);
}
private void setupDragAndDrop(JTextArea area) {
new DropTarget(area, new DropTargetListener() {
public void dragEnter(DropTargetDragEvent dtde) {}
@@ -247,7 +368,7 @@ public class Main extends JFrame {
public void drop(DropTargetDropEvent dtde) {
try {
dtde.acceptDrop(DnDConstants.ACTION_COPY);
List<File> files = (List<File>) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
java.util.List<File> files = (java.util.List<File>) dtde.getTransferable().getTransferData(DataFlavor.javaFileListFlavor);
if (!files.isEmpty()) loadFile(files.get(0));
} catch (Exception e) {}
}
@@ -270,7 +391,8 @@ public class Main extends JFrame {
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"});
String targetSubDir = "models" + File.separator + (info.getType() != null ? info.getType() : "checkpoints");
tableModel.addRow(new Object[]{true, info.getType(), info.getName(), info.getSize(), info.getPopularity(), targetSubDir, info.getUrl(), info.getUrl().equals("MISSING") ? "Idle" : "✅ Known Good"});
if (!info.getUrl().equals("MISSING")) fetchSizeInBackground(info, i);
}
downloadButton.setEnabled(!modelsToDownload.isEmpty());
@@ -278,32 +400,65 @@ public class Main extends JFrame {
private void searchMissingOnline() {
if (modelsToDownload == null) return;
int rowCount = tableModel.getRowCount();
boolean[] selected = new boolean[rowCount];
boolean anySelected = false;
boolean[] selectedForSearch = new boolean[rowCount];
boolean anyMissingSelected = false;
for (int i = 0; i < rowCount; i++) {
selected[i] = (Boolean) tableModel.getValueAt(i, 0);
if (selected[i]) anySelected = true;
boolean isChecked = (Boolean) tableModel.getValueAt(i, 0);
String currentUrl = (String) tableModel.getValueAt(i, 6);
if (isChecked && (currentUrl == null || currentUrl.equals("MISSING") || currentUrl.isEmpty())) {
selectedForSearch[i] = true;
anyMissingSelected = true;
} else {
selectedForSearch[i] = false;
}
}
if (!anySelected) {
JOptionPane.showMessageDialog(this, "Please select at least one model in the table to search.");
if (!anyMissingSelected) {
JOptionPane.showMessageDialog(this, "No missing models selected for Deep Search.");
return;
}
statusLabel.setText("Deep Search in progress...");
searchService.searchOnline(modelsToDownload, selected, jsonInputArea.getText(), currentFileName,
(idx, status) -> SwingUtilities.invokeLater(() -> tableModel.setValueAt(status, idx, 6)),
searchService.searchOnline(modelsToDownload, selectedForSearch, jsonInputArea.getText(), currentFileName,
(idx, status) -> SwingUtilities.invokeLater(() -> tableModel.setValueAt(status, idx, 7)),
(idx, info) -> SwingUtilities.invokeLater(() -> {
String targetSubDir = "models" + File.separator + (info.getType() != null ? info.getType() : "checkpoints");
tableModel.setValueAt(info.getSize(), idx, 3);
tableModel.setValueAt(info.getPopularity(), idx, 4);
tableModel.setValueAt(info.getUrl(), idx, 5);
tableModel.setValueAt("✅ Found", idx, 6);
})
tableModel.setValueAt(targetSubDir, idx, 5);
tableModel.setValueAt(info.getUrl(), idx, 6);
tableModel.setValueAt("✅ Found", idx, 7);
}),
() -> SwingUtilities.invokeLater(() -> statusLabel.setText("Deep Search finished."))
);
}
private void showHelpDialog() {
String helpText = "<html><body style='width: 500px; padding: 10px;'>" +
"<h1>ComfyUIModel-Downloader Help</h1>" +
"<h3>Core Features</h3>" +
"<ul>" +
" <li><b>Workflow Analysis:</b> Drag and drop ComfyUI JSON or PNG files to extract required models.</li>" +
" <li><b>Deep Search:</b> Uses AI and multi-platform searching (Hugging Face, Civitai) to find missing model URLs.</li>" +
" <li><b>Model List Integration:</b> Match models against your own known-good local lists.</li>" +
" <li><b>Queue Management:</b> Download multiple models simultaneously with pause/resume support.</li>" +
"</ul>" +
"<h3>API Keys & Security</h3>" +
"<ul>" +
" <li><b>Gemini API Key:</b> Required for the 'AI Scouting' feature. Gemini analyzes your workflow context " +
" to predict the best repositories for obscure models.</li>" +
" <li><b>Hugging Face Token:</b> Recommended to avoid rate limits and to access gated repositories " +
" (like some FLUX or StabilityAI models).</li>" +
" <li><b>Local Storage (Vault):</b> All keys and settings are stored <b>locally</b> on your machine " +
" in an encrypted file (<code>settings.vault</code>). They are never sent to any server except " +
" the official Google/Hugging Face APIs during requests.</li>" +
" <li><b>Password Protection:</b> Your vault is protected by the password you choose at startup. " +
" Without this password, the API keys cannot be decrypted.</li>" +
"</ul>" +
"</body></html>";
JOptionPane.showMessageDialog(this, helpText, "Application Help & Security", JOptionPane.INFORMATION_MESSAGE);
}
private void fetchSizeInBackground(ModelInfo info, int rowIndex) {
new Thread(() -> {
long size = searchService.getRemoteSize(info.getUrl());
@@ -314,11 +469,12 @@ public class Main extends JFrame {
}
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
context.getBean(Main.class);
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
Main main = context.getBean(Main.class);
main.launch();
}
@Configuration
@ComponentScan("de.tki.comfymodels")
public static class AppConfig {}
}
}