GIMP

Un modèle pilote GIMP 3.0.4 par le Hub pour dessiner une bannière sur deux calques et l’exporter en PNG. Cette page donne le texte qu’il a reçu, son script, les réponses du Hub et l’image, avec l’erreur que son premier script a cachée : il s’est exécuté sans échec et a exporté une seule couleur unie.

Une étincelle de lumière dont les rayons sont des lignes de texte, sur un damier : GIMP a retiré le fond de l'image, et le damier montre la transparence.
Le fond de cette image a été retiré dans GIMP par un script du Hub ; le damier se voit là où il n'y a plus de fond.

Démarrer GIMP avec son pont

Le pont tourne dans le Python de GIMP et ne démarre jamais de lui-même. Cette exécution a utilisé la commande du guide pour un GIMP lancé sans son interface. La machine n’avait pas d’écran, et GIMP s’arrête sans affichage X même avec --no-interface : xvfb-run -a lui en a donc donné un virtuel.

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()"

L’exécution elle-même a utilisé le port 9311, un répertoire personnel neuf et un jeton neuf. Le port s’est mis à l’écoute 5,5 s après le démarrage de GIMP. get_state sur hub a alors listé GIMP comme connecté, avec la version que GIMP déclarait :

Réponse du Hub
{
  "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": []
}

Texte donné au modèle

Texte donné au modèle
In GIMP, make a banner of 800 by 450 pixels: a vertical gradient from dark blue (#1c385c) at the top to teal (#29a196) at the bottom, on a layer named “Sky”, and an amber (#f2b344) filled circle 120 pixels across on a separate layer named “Sun”, about two thirds of the way across and a third of the way down. Export it as banner.png and leave the image open.

Avant d’écrire, le modèle a appelé get_state sur gimp, comme le demande le guide du modèle. GIMP ne contenait aucune image : "images": [] et "active_image": null.

Script écrit

Un modèle, Claude Sonnet 5.5, a écrit ce script à partir du guide et de cet état vide. Ce n’est pas un script tiré des tests du projet.

python · Script écrit
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)}

La réponse du Hub

Réponse du Hub
{
  "app": "gimp",
  "duration_ms": 432,
  "error": null,
  "result": {
    "layers": [
      "Sun",
      "Sky"
    ],
    "png_bytes": 3594
  },
  "status": "ok",
  "stderr": "",
  "stdout": "",
  "truncated": false
}

status vaut ok, error vaut null et result est ce que le script a défini. Rien dans la réponse ne dit que l’image est fausse. Le modèle a lu le PNG que le script avait exporté :

Une image de 800 par 450 remplie d'une seule couleur ambre, sans dégradé ni cercle.
banner.png tel que le premier script l'a exporté : chaque pixel lu, en trois points, vaut srgba(242,179,68,1).

Ce que la première image a montré

L’image a partout la couleur du cercle. Un deuxième script, envoyé à l’image ouverte, a lu deux choses que la réponse ne donnait pas : la sélection après select_ellipse, et les couleurs du calque Sky en haut et en bas.

python · Script écrit
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())
Réponse du Hub
{
  "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
}

La sélection était juste (x1=473, y1=90, x2=593, y2=210, 120 par 120 pixels) et le dégradé aussi : le pixel du haut de Sky est bleu foncé et celui du bas turquoise, en valeurs linéaires. Le calque Sun avait donc été rempli en entier.

Un troisième script a mesuré la cause sur une image neuve de 100 par 100, la sélection étant définie. Il a rempli un calque transparent avec layer.fill et un autre avec layer.edit_fill, puis a lu l’alpha d’un pixel au centre et d’un pixel dans le coin :

python · Script écrit
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()
Réponse du Hub
{
  "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 ignore la sélection : son pixel de coin est opaque. layer.edit_fill la respecte : son pixel de coin est transparent.

Le script corrigé et sa réponse

Seul ce qui a échoué a changé. Le script vide le calque Sun, le remplit de nouveau avec edit_fill alors que la sélection est toujours définie, et exporte l’image de nouveau par-dessus banner.png.

python · Script écrit
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)}
Réponse du Hub
{
  "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
}

Le fichier exporté fait 232 912 octets, contre 3 594 pour l’image unie. Le calque Sun est opaque au centre du cercle (sun_inside_alpha) et transparent au pixel (5, 5) (sun_outside_alpha).

L’image produite

Voici le banner.png exporté, de 800 par 450 pixels. Les pixels du haut et du bas valent srgba(28,57,92,1) et srgba(41,160,149,1), à un niveau près de #1c385c et de #29a196 : le dégradé est tramé, si bien qu’un rejeu diffère d’un niveau sur certains pixels.

Un dégradé vertical du bleu foncé en haut au turquoise en bas, avec un disque ambre en haut à droite.
banner.png après le script corrigé : le dégradé de #1c385c à #29a196 et le cercle ambre.

Guide de référence dans le dépôt →

SuiteKrita →