GIMP

Un modelo maneja GIMP 3.0.4 a través del Hub para dibujar una pancarta en dos capas y exportarla como PNG. Esta página da el texto que se le dio, su script, las respuestas del Hub y la imagen, con el error que ocultó su primer script: se ejecutó sin ningún fallo y exportó un solo color uniforme.

Una chispa de luz cuyos rayos son líneas de texto, sobre un damero: GIMP quitó el fondo de la imagen y el damero muestra la transparencia.
El fondo de esta imagen se quitó en GIMP con un script del Hub; el damero se ve donde ya no hay fondo.

Iniciar GIMP con su puente

El puente se ejecuta dentro del Python de GIMP y nunca se inicia solo. Esta ejecución usó el comando de la guía para un GIMP iniciado sin su interfaz. La máquina no tenía pantalla, y GIMP se detiene sin una pantalla X incluso con --no-interface, así que xvfb-run -a le dio una virtual.

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

La ejecución en sí usó el puerto 9311, un directorio personal nuevo y un token nuevo. El puerto empezó a escuchar 5,5 s después del inicio. get_state sobre hub enumeró entonces GIMP como conectado, con la versión que informó GIMP:

Respuesta del 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": []
}

Texto dado al modelo

Texto dado al modelo
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.

Antes de escribir, el modelo llamó a get_state sobre gimp, como pide la guía para el modelo. GIMP no tenía ninguna imagen abierta: "images": [] y "active_image": null.

Script escrito

Un modelo, Claude Sonnet 5.5, escribió este script a partir de la guía y de ese estado vacío. No es un script tomado de las pruebas del proyecto.

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

Respuesta del Hub

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

status es ok, error es null y result es lo que fijó el script. Nada en la respuesta dice que la imagen esté mal. El modelo leyó el PNG que el script había exportado:

Una imagen de 800 por 450 rellena de un solo color ámbar, sin degradado ni círculo.
banner.png tal como lo exportó el primer script: cada píxel leído, en tres puntos, es srgba(242,179,68,1).

Lo que mostró la primera imagen

La imagen tiene en todas partes el color del círculo. Un segundo script, enviado a la imagen abierta, leyó dos cosas que la respuesta no daba: la selección tras select_ellipse y los colores de la capa Sky en su parte superior y en la inferior.

python · Script escrito
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())
Respuesta del 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 selección era correcta (x1=473, y1=90, x2=593, y2=210, de 120 por 120 píxeles) y el degradado también: el píxel superior de Sky es azul oscuro y el inferior verde azulado, en valores lineales. Así que la capa Sun se había rellenado entera.

Un tercer script midió la causa en una imagen nueva de 100 por 100, con la selección fijada. Rellenó una capa transparente con layer.fill y otra con layer.edit_fill, y luego leyó el alfa de un píxel del centro y de un píxel de la esquina:

python · Script escrito
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()
Respuesta del 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 ignora la selección: su píxel de la esquina es opaco. layer.edit_fill la respeta: su píxel de la esquina es transparente.

El script corregido y su respuesta

Solo cambió lo que falló. El script vacía la capa Sun, la rellena de nuevo con edit_fill mientras la selección sigue fijada y vuelve a exportar la imagen sobre banner.png.

python · Script escrito
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)}
Respuesta del 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
}

El archivo exportado pesa 232 912 bytes, frente a 3594 de la imagen uniforme. La capa Sun es opaca en el centro del círculo (sun_inside_alpha) y transparente en el píxel (5, 5) (sun_outside_alpha).

La imagen producida

Este es el banner.png exportado, de 800 por 450 píxeles. Los píxeles de arriba y de abajo leen srgba(28,57,92,1) y srgba(41,160,149,1), a un nivel de #1c385c y #29a196: el degradado lleva tramado, así que una repetición difiere en un nivel en algunos píxeles.

Un degradado vertical de azul oscuro arriba a verde azulado abajo, con un disco ámbar en la parte superior derecha.
banner.png tras el script corregido: el degradado de #1c385c a #29a196 y el círculo ámbar.

Guía de referencia en el repositorio →

SiguienteKrita →