GIMP
Ein Modell steuert GIMP 3.0.4 über den Hub, um auf zwei Ebenen ein Banner zu zeichnen und als PNG zu exportieren. Die Seite nennt den Text, den es erhielt, sein Skript, die Antworten des Hubs und das Bild, samt dem Fehler, den sein erstes Skript verbarg: Es lief ohne Fehlschlag und exportierte eine einzige einfarbige Fläche.

Auf dieser Seite
GIMP mit seiner Brücke starten
Die Brücke läuft im Python von GIMP und startet nie von selbst. Dieser Lauf nutzte den Befehl des Leitfadens für ein GIMP, das ohne Benutzeroberfläche gestartet wird. Der Rechner hatte keinen Bildschirm, und GIMP bricht ohne X-Display selbst mit --no-interface ab; deshalb gab xvfb-run -a ihm ein virtuelles.
LIBREMODULAR_BRIDGE_PORT=9301 \
LIBREMODULAR_TOKEN_PATH=$HOME/.config/libremodular-mcp/token \
xvfb-run -a gimp-3.0 --new-instance --no-data --no-fonts --no-interface --batch-interpreter python-fu-eval \
-b "exec(open('/usr/share/libremodular-mcp/bridges/gimp_bridge.py', encoding='utf-8').read()); serve_main_thread()"
Der Lauf selbst nutzte Port 9311, ein neues Home-Verzeichnis und ein neues Token. Der Port lauschte 5,5 s nach dem Start. get_state für hub listete GIMP danach als verbunden auf, mit der Version, die GIMP meldete:
{
"app": "hub",
"app_version": "0.1.0",
"bridge_version": null,
"captured_at": "2026-10-21T12:47:26.453Z",
"connected": true,
"content_origin": "hub",
"screenshot": null,
"state": {
"modules": [
{
"app_version": "3.0.4",
"bridge_version": "1",
"connected": true,
"mode": "tcp",
"name": "gimp"
}
]
},
"warnings": []
}Dem Modell gegebener Text
Vor dem Schreiben rief das Modell get_state für gimp auf, wie es der Leitfaden für das Modell verlangt. GIMP hielt kein Bild: "images": [] und "active_image": null.
Geschriebenes Skript
Ein Modell, Claude Sonnet 5.5, schrieb dieses Skript nach dem Leitfaden und nach diesem leeren Zustand. Es ist kein Skript aus den Tests des Projekts.
import os
image = Gimp.Image.new(800, 450, Gimp.ImageBaseType.RGB)
sky = Gimp.Layer.new(image, "Sky", 800, 450, Gimp.ImageType.RGBA_IMAGE, 100.0, Gimp.LayerMode.NORMAL)
image.insert_layer(sky, None, 0)
Gimp.context_set_foreground(Gimp.color_parse_hex("#1c385c"))
Gimp.context_set_background(Gimp.color_parse_hex("#29a196"))
sky.edit_gradient_fill(Gimp.GradientType.LINEAR, 0.0, False, 1, 0.0, True, 0.0, 0.0, 0.0, 450.0)
sun = Gimp.Layer.new(image, "Sun", 800, 450, Gimp.ImageType.RGBA_IMAGE, 100.0, Gimp.LayerMode.NORMAL)
image.insert_layer(sun, None, 0)
sun.fill(Gimp.FillType.TRANSPARENT)
image.select_ellipse(Gimp.ChannelOps.REPLACE, 533 - 60, 150 - 60, 120, 120)
Gimp.context_set_foreground(Gimp.color_parse_hex("#f2b344"))
sun.fill(Gimp.FillType.FOREGROUND)
Gimp.Selection.none(image)
path = os.path.join(LMM_WORK_DIR, "banner.png")
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, image, Gio.File.new_for_path(path), None)
result = {"layers": [layer.get_name() for layer in image.get_layers()], "png_bytes": os.path.getsize(path)}Antwort des Hubs
{
"app": "gimp",
"duration_ms": 432,
"error": null,
"result": {
"layers": [
"Sun",
"Sky"
],
"png_bytes": 3594
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}status ist ok, error ist null, und result ist, was das Skript gesetzt hat. Nichts in der Antwort sagt, dass das Bild falsch ist. Das Modell las das PNG, das das Skript exportiert hatte:

Was das erste Bild zeigte
Das Bild hat überall die Farbe des Kreises. Ein zweites Skript, an das geöffnete Bild gesendet, las zwei Dinge, die die Antwort nicht lieferte: die Auswahl nach select_ellipse und die Farben der Ebene Sky an ihrem oberen und an ihrem unteren Rand.
image = images()[0]
sun = [layer for layer in image.get_layers() if layer.get_name() == "Sun"][0]
sky = [layer for layer in image.get_layers() if layer.get_name() == "Sky"][0]
ok = image.select_ellipse(Gimp.ChannelOps.REPLACE, 473, 90, 120, 120)
print("select_ellipse returned", ok)
print("bounds", Gimp.Selection.bounds(image))
print("is_empty", Gimp.Selection.is_empty(image))
sky_top = sky.get_pixel(5, 5)
sky_bottom = sky.get_pixel(5, 444)
print("sky pixels", sky_top.get_rgba(), sky_bottom.get_rgba()){
"app": "gimp",
"duration_ms": 141,
"error": null,
"result": null,
"status": "ok",
"stderr": "",
"stdout": "select_ellipse returned True\nbounds (True, non_empty=True, x1=473, y1=90, x2=593, y2=210)\nis_empty False\nsky pixels (red=0.011612247675657272, green=0.04091521352529526, blue=0.10702310502529144, alpha=1.0) (red=0.022173885256052017, green=0.35153278708457947, blue=0.3005438446998596, alpha=1.0)\n",
"truncated": false
}Die Auswahl war richtig (x1=473, y1=90, x2=593, y2=210, 120 × 120 Pixel), und der Farbverlauf ebenso: Das oberste Pixel von Sky ist dunkelblau und das unterste türkis, in linearen Werten. Also war die Ebene Sun ganz gefüllt worden.
Ein drittes Skript maß die Ursache an einem neuen Bild von 100 × 100 Pixeln, bei gesetzter Auswahl. Es füllte eine transparente Ebene mit layer.fill und eine andere mit layer.edit_fill und las dann das Alpha eines Pixels in der Mitte und eines Pixels in der Ecke:
image = Gimp.Image.new(100, 100, Gimp.ImageBaseType.RGB)
Gimp.context_set_foreground(Gimp.color_parse_hex("#f2b344"))
image.select_ellipse(Gimp.ChannelOps.REPLACE, 30, 30, 40, 40)
result = {}
for name, method in (("fill", "fill"), ("edit_fill", "edit_fill")):
layer = Gimp.Layer.new(image, name, 100, 100, Gimp.ImageType.RGBA_IMAGE, 100.0, Gimp.LayerMode.NORMAL)
image.insert_layer(layer, None, 0)
layer.fill(Gimp.FillType.TRANSPARENT)
getattr(layer, method)(Gimp.FillType.FOREGROUND)
result[name] = {"alpha_centre": layer.get_pixel(50, 50).get_rgba().alpha, "alpha_corner": layer.get_pixel(2, 2).get_rgba().alpha}
image.delete(){
"app": "gimp",
"duration_ms": 27,
"error": null,
"result": {
"edit_fill": {
"alpha_centre": 1.0,
"alpha_corner": 0.0
},
"fill": {
"alpha_centre": 1.0,
"alpha_corner": 1.0
}
},
"status": "ok",
"stderr": "",
"stdout": "",
"truncated": false
}layer.fill ignoriert die Auswahl: Sein Eckpixel ist deckend. layer.edit_fill hält sich an sie: Sein Eckpixel ist transparent.
Das korrigierte Skript und seine Antwort
Nur das, was fehlschlug, änderte sich. Das Skript leert die Ebene Sun, füllt sie bei weiterhin gesetzter Auswahl erneut mit edit_fill und exportiert das Bild noch einmal über banner.png.
import os
image = images()[0]
sun = [layer for layer in image.get_layers() if layer.get_name() == "Sun"][0]
print("is_empty before", Gimp.Selection.is_empty(image))
sun.fill(Gimp.FillType.TRANSPARENT)
Gimp.context_set_foreground(Gimp.color_parse_hex("#f2b344"))
sun.edit_fill(Gimp.FillType.FOREGROUND)
Gimp.Selection.none(image)
inside = sun.get_pixel(533, 150).get_rgba()
outside = sun.get_pixel(5, 5).get_rgba()
path = os.path.join(LMM_WORK_DIR, "banner.png")
Gimp.file_save(Gimp.RunMode.NONINTERACTIVE, image, Gio.File.new_for_path(path), None)
result = {"sun_inside_alpha": inside.alpha, "sun_outside_alpha": outside.alpha, "png_bytes": os.path.getsize(path)}{
"app": "gimp",
"duration_ms": 2246,
"error": null,
"result": {
"png_bytes": 232912,
"sun_inside_alpha": 1.0,
"sun_outside_alpha": 0.0
},
"status": "ok",
"stderr": "",
"stdout": "is_empty before False\n",
"truncated": false
}Die exportierte Datei hat 232 912 Bytes, gegenüber 3 594 beim einfarbigen Bild. Die Ebene Sun ist in der Mitte des Kreises deckend (sun_inside_alpha) und am Pixel (5, 5) transparent (sun_outside_alpha).
Das erzeugte Bild
Das ist das exportierte banner.png, 800 × 450 Pixel. Die Pixel oben und unten lesen sich srgba(28,57,92,1) und srgba(41,160,149,1), eine Stufe neben #1c385c und #29a196: Der Farbverlauf ist mit Dithering gerastert, daher unterscheidet sich eine Wiederholung bei einigen Pixeln um eine Stufe.
