← Zurück zum Blog

Tech

Flux.1-Dev in ComfyUI: ein Workflow von Grund auf

In meinem Beitrag über meinen lokalen KI-Stack hatte ich ja schon kurz erwähnt, dass ComfyUI für mich die spannendste Spielwiese ist, wenn es um Bildgenerierung geht. Der Ansatz mit den Nodes, die man wie ein Flussdiagramm zusammensteckt, wirkt am Anfang erschlagend - gibt einem aber eine Kontrolle über jeden einzelnen Schritt, die man bei den fertigen Ein-Klick-Lösungen einfach nicht hat.
Heute will ich das mal konkret machen und einen kompletten Text-zu-Bild-Workflow für Flux.1-Dev von Grund auf aufbauen. Node für Node, damit klar wird, was jeder Baustein eigentlich tut. Flux ist ein modernes Diffusionsmodell von Black Forest Labs und liefert für ein lokal laufendes Modell erstaunlich gute Ergebnisse - vor allem, weil man es tatsächlich in ganzen Sätzen ansprechen kann.

Der fertige Flux.1-Dev-Workflow in ComfyUI

So sieht der fertige Graph am Ende aus. Auf den ersten Blick ein ordentliches Geflecht aus Kästchen und Linien - aber wenn wir die Nodes einen nach dem anderen durchgehen, wird schnell klar, dass jeder davon eine ganz klar umrissene Aufgabe hat.

Load Diffusion Model

Der erste Node, den wir brauchen, um das Modell überhaupt zu laden, ist Load Diffusion Model. Damit wird Flux1-dev geladen, also stellt man bei unet_name entsprechend flux1-dev ein.
Das Modell selbst gibt es direkt bei Black Forest Labs: huggingface.co/black-forest-labs/FLUX.1-dev.

DualCLIPLoader

Als nächstes fügen wir einen DualCLIPLoader hinzu. Hier wählen wir clip_l.safetensors und t5xxl_fp8 aus. Wichtig: Den type unbedingt auf flux stellen, sonst passt später nichts zusammen.
Aber was ist CLIP eigentlich? CLIP steht für "Contrastive Language-Image Pretraining" - ein ursprünglich von OpenAI entwickeltes Modell, das gelernt hat, Text und Bilder miteinander zu verknüpfen. Vereinfacht gesagt ist ein CLIP-Modell ein Text-Encoder. Es nimmt deinen Prompt - also zum Beispiel "eine Katze im Regen" - und wandelt ihn in eine Reihe von Zahlen um, einen sogenannten Vektor bzw. ein Embedding, das die Bedeutung des Textes beschreibt. Das Diffusionsmodell selbst versteht nämlich keine Wörter. Es braucht diese Zahlen-Repräsentation, um zu wissen, was es überhaupt malen soll.
Moderne Modelle wie Flux verwenden gleich zwei Textencoder. Der kleine ist gut darin, Konzepte und Stile zu verstehen, versagt aber bei komplexen Satzstrukturen schnell. Deshalb kommt als zweiter Encoder das t5xxl von Google dazu. Dieses Modell ist deutlich größer und richtig gut darin, komplexe Sätze und Grammatik zu erfassen. Genau dadurch, dass Flux beide zusammen nutzt, kann man in ganz normalen Sätzen beschreiben, was erstellt werden soll - statt sich wie bei älteren Modellen durch endlose Stichwort-Listen zu quälen.
Die beiden Encoder gibt es hier: huggingface.co/comfyanonymous/flux_text_encoders.

VAE Loader

Der dritte Loader, den man braucht, ist ein VAE Loader. Hier wählt man ae.safetensors aus. Jedes Modell hat idealerweise seinen eigenen, optimierten VAE.
VAE steht für "Variational Autoencoder" und ist quasi der Übersetzer, der die kodierten Zahlen, mit denen das Modell arbeitet, in ein anzeigbares Pixelbild verwandelt. Die Modelle selbst rechnen nämlich im Grunde nur mit Latents. Das sind komprimierte Repräsentationen, die viel schneller zu berechnen und zu bearbeiten sind als die fertigen Bilder in voller Auflösung.
Den passenden VAE gibt es ebenfalls im Flux-Repo: huggingface.co/black-forest-labs/FLUX.1-dev.

CLIP Text Encode (Prompt)

Hier gibt man dann seinen eigentlichen Prompt ein. Den DualCLIPLoader von vorhin verbindet man mit diesem Node - damit weiß der Text-Encoder, mit welchen Modellen er den Prompt in seine Zahlen-Repräsentation übersetzen soll.

FluxGuidance

Hier stellt man die Gewichtung ein. In älteren Modellen hat man das über den CFG-Wert gesteuert, der bei Flux aber nicht mehr verwendet wird und immer auf 1 stehen muss - den CFG findet man später übrigens im KSampler.
Für die Guidance ist 3.5 ein guter Mittelwert. Grundsätzlich bestimmt der Wert, wie stark sich das Modell an den Prompt hält. Wählt man einen kleineren Wert, hält sich das Modell nicht so strikt daran. Dadurch können die Bilder natürlicher wirken, es gehen aber auch Details verloren, die im Prompt eigentlich gefordert waren.
Höhere Werte bedeuten, dass sich das Modell strikt an den Prompt klammert. Das kann allerdings zu übersättigten, unnatürlichen Farben führen, es können seltsame Artefakte entstehen und so weiter. Details werden also auf Kosten der Bildqualität erzwungen. Man erkauft sich Prompt-Treue mit einem unnatürlicheren Bild - deshalb fährt man mit dem Mittelweg meist am besten.
Den Prompt-Node verbindet man mit FluxGuidance.

Damit man ein Gefühl dafür bekommt, was dieser eine Wert ausmacht, habe ich denselben Prompt - schlicht "Ein Bild von einem mittelalterlichen Gebäude" - bei drei verschiedenen Guidance-Werten laufen lassen.

Bei einem niedrigen Wert von 1.0 nimmt sich das Modell sehr viele Freiheiten. Die Bilder wirken locker und künstlerisch - eher wie ein nur grob hingeworfenes Gemälde mit sichtbarem Pinselstrich als eine klare Vorlage. Von einem eindeutig erkennbaren Gebäude ist der Prompt hier ein gutes Stück entfernt, dafür haben die Motive eine schöne, malerische Stimmung:

Mit 3.5 - meinem Standardwert - werden die Ergebnisse deutlich konkreter und fotorealistischer. Man erkennt jetzt klar ein solides, glaubwürdiges Gebäude, ohne dass das Bild überzeichnet wirkt. Für mich der beste Kompromiss aus Prompt-Treue und natürlicher Anmutung:

Bei 6.0 klammert sich das Modell dann sehr strikt an den Prompt. Die Gebäude wirken auffällig „sauber" und wie hingestellt, alles ist betont ordentlich und kantig. Interessanterweise kippt der Bildstil dabei manchmal ganz weg vom Fotorealismus - das letzte Beispiel ist plötzlich fast comichaft gezeichnet. Die erzwungene Genauigkeit geht hier also klar auf Kosten der natürlichen Wirkung:

Empty Latent Image

Jetzt brauchen wir noch eine leere Bildfläche, auf der überhaupt generiert werden soll. Dafür nimmt man ein Empty Latent Image und stellt dort die Größe ein, bei mir zum Beispiel 1216x832.
Man kann das Bild aber nicht beliebig groß machen, sondern muss bedenken, worauf das Modell optimiert wurde. Flux-dev wurde primär auf 1024x1024 Pixel trainiert. Wichtig ist dabei vor allem, dass die Gesamtfläche ungefähr bei einem Megapixel liegt. Sind die Bilder deutlich kleiner, werden sie oft unscharf oder die Komposition wirkt gequetscht. Liegt man dagegen deutlich über einem Megapixel, kann es zu seltsamen Wiederholungen und Strukturfehlern im Bild kommen - Flux malt dann gerne mal zwei Köpfe, wo eigentlich nur einer hingehört.
Noch wichtig ist die batch_size. Damit werden zum Beispiel vier Bilder gleichzeitig generiert. Das passiert dann aber wirklich parallel, und entsprechend steigt auch der VRAM-Verbrauch. Ich lasse das eigentlich immer auf 1 - lieber ein Bild nach dem anderen, als dass mir mitten in der Generierung der Grafikspeicher ausgeht.

KSampler

Hier kommt jetzt alles zusammen, was wir bisher aufgebaut haben. Der KSampler braucht allerdings auch einen Negative Prompt - der wird bei Flux zwar ohnehin ignoriert, gehört aber trotzdem verdrahtet. Damit das sauber ist, fügen wir noch einen ConditioningZeroOut-Node hinzu, verbinden den bestehenden Prompt zusätzlich mit diesem Zero-Node und den Zero-Node dann mit dem negativen Eingang des KSamplers.
Der KSampler hat relativ viele Parameter, die man durchaus verstehen sollte:

  • seed: ein zufälliger Wert, der die Bildkomposition würfelt. Wenn man mehrfach das exakt gleiche Motiv erzeugen will, muss man den Wert auf fixed stellen - sonst bekommt man bei jedem Durchlauf ein neues Bild.
  • steps: die Anzahl der Schritte, die der Sampler durchläuft, um das Rauschen zu entfernen. Bei jedem Step wird ein bisschen mehr vom Latent-Rauschen abgezogen und das Ergebnis nähert sich einem fertigen Bild. 20 Steps sind meist ein guter Wert. Mit wenigen Steps (4-8) kann man schnelle Testläufe machen, mehr als 30 bringen dagegen selten einen echten Mehrwert.
  • sampler_name: der mathematische Algorithmus, der zum Entfernen des Latent-Rauschens verwendet wird. Standard bei Flux ist euler. Aber auch mit dpmpp_2m habe ich schon gute Ergebnisse bekommen.
  • scheduler: dieser Algorithmus bestimmt, wie viel Latent-Rauschen pro Step entfernt werden soll. Der Standard ist hier simple.
  • denoise: letztendlich legt dieser Wert fest, wie viel vom Latent-Rauschen überhaupt entfernt wird. 1 bedeutet volle Stärke, bei 0.5 würde nur die Hälfte des Rauschens entfernt. Bei reinem Text-zu-Bild lässt man das immer auf 1.0. Interessant wird der Wert erst, wenn man ein vorhandenes Bild nur verändern will - je kleiner man ihn einstellt, desto weniger wird das ursprüngliche Bild angetastet.

VAE Decode

Den Output des KSampler verbindet man mit VAE Decode. Und unseren Load VAE von vorhin hängen wir hier ebenfalls an - denn genau das ist der Schritt, in dem aus den Latents wieder ein echtes Pixelbild wird. Was der VAE macht, hatte ich ja weiter oben schon beschrieben.

Save Image

Ich denke, das ist selbsterklärend. Der letzte Node nimmt das fertige Bild entgegen und speichert es ab.

Fazit

Wenn man das erste Mal all diese Nodes verbindet, fühlt es sich noch ganz schön nach Bastelei an. Aber genau darin liegt für mich der Reiz: Man versteht am Ende wirklich, was zwischen dem eingetippten Satz und dem fertigen Bild passiert - vom Text-Encoder über die Latents bis zum VAE, der das Ganze wieder in Pixel zurückübersetzt.
Und hat man den Workflow einmal stehen, speichert man ihn einfach als Vorlage ab und muss beim nächsten Mal nur noch den Prompt austauschen.