avatar del bot: búho con gafas, generado y aplicado por API
Build & Deploy ResearchOwl / build-and-push (push) Successful in 8s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 8s
Mismo patrón que hermes-bot y roswell-corpus: la foto de perfil se genera con PIL (dibujado a 4× y reducido) en vez de ser un PNG opaco en el repo. Telegram recorta en círculo y lo enseña a 48 px en la lista de chats, así que el dibujo es una silueta y no una ilustración. Las gafas son el único guiño a la investigación que sobrevive al tamaño chico —una lupa o un libro se vuelven una mancha—, y la montura va en tono oscuro: en ámbar sobre ámbar el aro se fundía con el plumaje y a 48 px desaparecía. Documentado en el README, con el gotcha de `setMyProfilePhoto`: el parámetro `photo` no admite el fichero suelto (responde `photo isn't specified`), hay que pasarle un InputProfilePhoto que apunte al adjunto. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Genera el avatar de @chemavx_researchowl_bot: búho con gafas de lectura.
|
||||
|
||||
Mismo método que los avatares de hermes-bot y roswell-corpus: se dibuja a 4× y
|
||||
se reduce, que es la forma barata de tener bordes suaves sin antialiasing
|
||||
propio en PIL. Telegram recorta en CÍRCULO, así que todo lo que importa vive
|
||||
dentro del círculo inscrito.
|
||||
|
||||
Por qué gafas y no un búho a secas: el búho solo dice "búho". El aro sobre cada
|
||||
ojo, con su puente, se lee como gafas de leer al tamaño grande y como disco
|
||||
facial al pequeño, y ninguna de las dos lecturas es errónea. Es el único guiño
|
||||
a la investigación que sobrevive a 48 px; una lupa o un libro se convierten en
|
||||
una mancha.
|
||||
|
||||
python3 make_avatar.py # deja el PNG en assets/avatar.png
|
||||
"""
|
||||
import math
|
||||
import os
|
||||
|
||||
from PIL import Image, ImageDraw, ImageFilter
|
||||
|
||||
SIZE = 640
|
||||
SS = 4 # supersampling
|
||||
W = SIZE * SS
|
||||
CX = CY = W // 2
|
||||
|
||||
AMBAR = (236, 173, 84) # plumaje
|
||||
AMBAR_OSC = (184, 116, 52) # alas
|
||||
MONTURA = (46, 26, 30) # gafas y pico: tienen que contrastar con el plumaje
|
||||
CREMA = (255, 246, 230) # ojos
|
||||
TINTA = (30, 16, 36) # pupilas y huecos
|
||||
|
||||
AQUI = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def fondo() -> Image.Image:
|
||||
"""Degradado radial ciruela, dibujado pequeño y ampliado."""
|
||||
n = 128
|
||||
g = Image.new("RGB", (n, n))
|
||||
px = g.load()
|
||||
for y in range(n):
|
||||
for x in range(n):
|
||||
d = min(1.0, math.hypot(x - n / 2, y - n / 2) / (n / 2 * 1.10))
|
||||
px[x, y] = (int(56 + (11 - 56) * d),
|
||||
int(33 + (6 - 33) * d),
|
||||
int(66 + (16 - 66) * d))
|
||||
return g.resize((W, W), Image.BICUBIC).convert("RGBA")
|
||||
|
||||
|
||||
def tinta(mascara: Image.Image, color) -> Image.Image:
|
||||
"""Convierte una máscara L en una capa RGBA del color dado."""
|
||||
capa = Image.new("RGBA", mascara.size, color + (0,))
|
||||
capa.putalpha(mascara)
|
||||
return capa
|
||||
|
||||
|
||||
def caja(x0, y0, x1, y1):
|
||||
"""Rectángulo en fracciones de W → píxeles."""
|
||||
return [x0 * W, y0 * W, x1 * W, y1 * W]
|
||||
|
||||
|
||||
def cuerpo() -> Image.Image:
|
||||
"""Silueta completa: penachos + cuerpo. Se reutiliza como máscara."""
|
||||
m = Image.new("L", (W, W), 0)
|
||||
d = ImageDraw.Draw(m)
|
||||
for signo in (-1, 1):
|
||||
d.polygon([
|
||||
(CX + signo * 0.262 * W, 0.352 * W),
|
||||
(CX + signo * 0.108 * W, 0.300 * W),
|
||||
(CX + signo * 0.196 * W, 0.148 * W),
|
||||
], fill=255)
|
||||
d.ellipse(caja(0.20, 0.28, 0.80, 0.84), fill=255)
|
||||
return m
|
||||
|
||||
|
||||
def alas(mascara: Image.Image) -> Image.Image:
|
||||
"""Dos alas plegadas a los lados, recortadas contra la silueta.
|
||||
|
||||
Van recortadas y no dibujadas a pelo porque el ala tiene que morir justo en
|
||||
el borde del cuerpo: si asoma, el búho deja de tener contorno limpio y a
|
||||
tamaño pequeño se convierte en un borrón con orejas.
|
||||
"""
|
||||
capa = Image.new("RGBA", (W, W), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(capa)
|
||||
for signo in (-1, 1):
|
||||
cx = CX + signo * 0.205 * W
|
||||
d.ellipse([cx - 0.115 * W, 0.455 * W, cx + 0.115 * W, 0.855 * W],
|
||||
fill=AMBAR_OSC + (255,))
|
||||
return Image.composite(capa, Image.new("RGBA", (W, W), (0, 0, 0, 0)), mascara)
|
||||
|
||||
|
||||
def cara() -> Image.Image:
|
||||
"""Ojos, gafas y pico."""
|
||||
capa = Image.new("RGBA", (W, W), (0, 0, 0, 0))
|
||||
d = ImageDraw.Draw(capa)
|
||||
y = 0.425
|
||||
r = 0.105
|
||||
grosor = int(W * 0.019)
|
||||
|
||||
for signo in (-1, 1):
|
||||
cx = CX + signo * 0.125 * W
|
||||
d.ellipse([cx - r * W, (y - r) * W, cx + r * W, (y + r) * W],
|
||||
fill=CREMA + (255,))
|
||||
d.ellipse([cx - r * W, (y - r) * W, cx + r * W, (y + r) * W],
|
||||
outline=MONTURA + (255,), width=grosor)
|
||||
pr = 0.046 * W
|
||||
d.ellipse([cx - pr, y * W - pr, cx + pr, y * W + pr], fill=TINTA + (255,))
|
||||
br = 0.017 * W # reflejo: sin él la mirada es de muñeco
|
||||
bx, by = cx - 0.020 * W, y * W - 0.030 * W
|
||||
d.ellipse([bx - br, by - br, bx + br, by + br], fill=(255, 255, 255, 210))
|
||||
|
||||
# puente de las gafas
|
||||
d.rectangle([CX - 0.022 * W, y * W - grosor / 2, CX + 0.022 * W, y * W + grosor / 2],
|
||||
fill=MONTURA + (255,))
|
||||
|
||||
d.polygon([(CX - 0.042 * W, 0.500 * W), (CX + 0.042 * W, 0.500 * W),
|
||||
(CX, 0.575 * W)], fill=MONTURA + (255,))
|
||||
return capa
|
||||
|
||||
|
||||
def main():
|
||||
img = fondo()
|
||||
|
||||
silueta = cuerpo()
|
||||
|
||||
# resplandor: la silueta desenfocada por debajo, para despegar al búho del
|
||||
# fondo cuando el icono se ve pequeño
|
||||
brillo = tinta(silueta.filter(ImageFilter.GaussianBlur(W * 0.030)), AMBAR)
|
||||
img.alpha_composite(Image.blend(Image.new("RGBA", (W, W), (0, 0, 0, 0)), brillo, 0.5))
|
||||
|
||||
img.alpha_composite(tinta(silueta, AMBAR))
|
||||
img.alpha_composite(alas(silueta))
|
||||
img.alpha_composite(cara())
|
||||
|
||||
# aro fino: le da borde al icono cuando Telegram lo recorta en círculo
|
||||
m = int(W * 0.045)
|
||||
ImageDraw.Draw(img).ellipse([m, m, W - m, W - m],
|
||||
outline=AMBAR + (110,), width=int(W * 0.008))
|
||||
|
||||
salida = os.path.join(AQUI, "avatar.png")
|
||||
img.convert("RGB").resize((SIZE, SIZE), Image.LANCZOS).save(salida, "PNG")
|
||||
print(salida)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user