Générer des haïkus et leurs illustrations avec un LLM et Go



Code et poésie

Code libre s’éveille
Sous l’étoile du partageCréons des merveilles

Code libre s’éveille
Sous l’étoile du partage
Créons des merveilles

La programmation est souvent perçue à travers un prisme utilitariste, comme un simple moyen de produire des outils ou des services destinés à l’usage humain ou machine. Pourtant, dès les années 1960, des pionniers visionnaires comme Frieder Nake et Georg Nees ont su transcender cette vision réductrice en explorant l’informatique comme un véritable médium d’expression artistique et de recherche esthétique.

Aujourd’hui, l’émergence des modèles d’intelligence artificielle générative nous offre de nouvelles perspectives pour poursuivre cette quête créative. Dans cet article, je vous propose d’explorer une approche plus contemplative de la programmation. En combinant le langage Go et les grands modèles de langage (LLMs), nous créerons un pont entre tradition et modernité : la génération de haïkus, ces courts poèmes japonais, accompagnés d’illustrations générées par intelligence artificielle.

Dansent dans le processeur

TL;DR

Vous voulez voir directement le code complet ? C’est par ici !.

Les prérequis

Code ouvert s’éveille
Dans le flux des idées vivesLiberté en ligne

Code ouvert s’éveille
Dans le flux des idées vives
Liberté en ligne

Étape 1: générer le haïku

La première étape consiste à générer un haïku inspiré par un thème donné. Pour cela, nous utilisons un modèle de langage (LLM) pour créer un haïku en suivant un format spécifique.

Pour interroger le service de LLM, nous utiliserons la librairie github.com/openai/openai-go.

const haikuSystemPrompt = `
You are an AI assistant specialized in writing beautiful haikus, privileging creativity and avoiding repetitions.

Use the following layout:

HAIKU:

<haiku>

Example:

HAIKU:

Coder, compiler
Les bugs brillent dans la nuit
Mise en prod zen
`

const haikuUserPrompt = `
Write a meaningful haiku in french inspired by the following themes:
%s
`

params := openai.ChatCompletionNewParams{
    Model:       openai.F(llmModel),
    Temperature: openai.Float(0.3),
    Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
        openai.SystemMessage(haikuSystemPrompt),
        openai.UserMessage(fmt.Sprintf(haikuUserPrompt, theme)),
    }),
}

completion, err := client.Chat.Completions.New(ctx, params)
if err != nil {
    log.Fatalf("[FATAL] %+v", errors.WithStack(err))
}

response := completion.Choices[0].Message.Content
haiku := extractHaiku(response)

Explication

Étape 2: générer les prompts pour la génération de l’illustration

Une fois le haïku généré, nous devons créer un prompt et un negative prompt pour guider la génération de l’illustration.

const imagePromptSystemPrompt = `
You are an AI assistant specialized in writing image generation prompts. The prompt and the negative prompt should specify that no human character should be present.

Respond using the following pattern:

PROMPT:

<prompt>

NEGATIVE PROMPT:

<negative_prompt>

Example:

PROMPT:

A developer’s desk at night, softly lit by the gentle glow of a computer screen displaying code. Small glowing fireflies symbolizing 'bugs' float around, creating a magical and contemplative atmosphere. There is a steaming coffee cup, adding a cozy touch. The scene has a peaceful, stress-free vibe, as if the production release is going smoothly. The style is minimalist, with a soft color palette in shades of blue, purple, and orange.

NEGATIVE PROMPT:

blurry, disfigured, deformed, human character, watermark, text, signature, low quality, pixelated, overexposed, cropped, grainy, duplicate, bad proportions, mutation, clone, glitch
`

const imagePromptUserPrompt = `
Write a prompt in 60 words maximum illustrating the following haiku:

%s
`

params = openai.ChatCompletionNewParams{
  Model:       openai.F(llmModel),
  Temperature: openai.Float(0.7),
  Messages: openai.F([]openai.ChatCompletionMessageParamUnion{
    openai.SystemMessage(imagePromptSystemPrompt),
    openai.UserMessage(fmt.Sprintf(imagePromptUserPrompt, haiku)),
  }),
}

completion, err = client.Chat.Completions.New(ctx, params)
if err != nil {
  log.Fatalf("[FATAL] %+v", errors.WithStack(err))
}

response = completion.Choices[0].Message.Content

prompt := extractPrompt(response)
if prompt == "" {
  log.Fatalf("[FATAL] could not extract image prompt from llm response:\n%s", response)
}

negativePrompt := extractNegativePrompt(response)
if negativePrompt == "" {
  log.Fatalf("[FATAL] could not extract image negative prompt from llm response:\n%s", response)
}

Cette fois, on utilise le haïku précédemment généré pour l’injecter dans le “user prompt” envoyé au LLM. Les fonctions extractPrompt(response) et extractNegativePrompt(response) extraient elle les deux prompts de la réponse, de la même manière qu'extractHaiku(response).

Étape 3: générer l’illustration

Maintenant que nous avons les prompts pour notre IA génératrice d’image, nous pouvons lui envoyer ceux ci.

payload := map[string]any{
  "prompt":          prompt,
  "negative_prompt": negativePrompt,
}

var buff bytes.Buffer

encoder := json.NewEncoder(&buff)

if err := encoder.Encode(payload); err != nil {
  log.Fatalf("[FATAL] %+v", errors.WithStack(err))
}

res, err := http.Post(baseURL+"/api/generate", "application/json", &buff)
if err != nil {
  log.Fatalf("[FATAL] %+v", errors.WithStack(err))
}

defer res.Body.Close()

decoder := json.NewDecoder(res.Body)

type apiResult struct {
  Images  []string `json:"images"`
  Latency float64  `json:"latency"`
}

var result apiResult

if err := decoder.Decode(&result); err != nil {
  log.Fatalf("[FATAL] %+v", errors.WithStack(err))
}

if len(result.Images) < 1 {
  log.Fatalf("[FATAL] %+v", errors.Errorf("unexpected number of returned images %d", len(result.Images)))
}

imageData, err := base64.StdEncoding.DecodeString(result.Images[0])
if err != nil {
  log.Fatalf("[FATAL] %+v", errors.WithStack(err))
}

imageBuff := bytes.NewBuffer(imageData)

img, err := jpeg.Decode(imageBuff)
if err != nil {
  log.Fatalf("[FATAL] %+v", errors.WithStack(err))
}

return img, nil

Explication

L’API du service fastsdcpu renvoie l’image générée encodée en base64 dans une réponse au format JSON. Nous récupérons donc celle-ci, la décodons dans une struct avec les tags json:"..." correspondant à sa structure puis transformons en instance d'image.Image.

Si vous avez le conteneur Docker bornholm/fastsdcpu-api en cours d’exécution vous devriez pouvoir accéder à la documentation de l’API REST sur http://localhost:8000/api/docs#/

Étape 4: intégrer le haïku dans l’image puis l’enregistrer

Pour réaliser cette dernière opération, nous allons utiliser la librairie github.com/fogleman/gg qui va nous faciliter la manipulation de notre instance d'image.Image.

drawCtx := gg.NewContextForImage(img)

imgWidth := img.Bounds().Dx()
imgHeight := img.Bounds().Dy()

maxWidth := float64(imgWidth) * 0.666
maxHeight := float64(imgHeight) * 0.5

lines := drawCtx.WordWrap(text, maxWidth-(maxWidth*0.2))
textWidth, textHeight := drawCtx.MeasureMultilineString(strings.Join(lines, "\n"), 1.5)

deltaX := (maxWidth / 2) - (textWidth / 2)
deltaY := (maxHeight / 2) - (textHeight / 2)

drawCtx.SetRGBA(0.5, 0.5, 0.5, 0.5)
drawCtx.DrawRectangle(deltaX-10, deltaY-5, textWidth+20, textHeight+20)
drawCtx.Fill()

drawCtx.SetColor(color.Black)
drawCtx.DrawStringWrapped(text, deltaX+1, deltaY+1, 0, 0, maxWidth-(maxWidth*0.2), 1.5, gg.AlignLeft)

drawCtx.SetColor(color.White)
drawCtx.DrawStringWrapped(text, deltaX, deltaY, 0, 0, maxWidth-(maxWidth*0.2), 1.5, gg.AlignLeft)

file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0640)
if err != nil {
  log.Fatalf("[FATAL] %+v", errors.WithStack(err))
}

defer file.Close()

if err := jpeg.Encode(file, drawCtx.Image(), nil); err != nil {
  log.Fatalf("[FATAL] %+v", errors.WithStack(err))
}

Explication

On utilise les méthodes de notre instance drawCtx afin de positionner correctement notre texte dans l’image (ici plus ou moins dans le quart haut/gauche, en maitenant une légère marge).

Afin de faire ressortir correctement le texte sur l’image, on dessine un cartouche légèrement transparent, un ombrage noir et un texte blanc par dessus.

Une fois notre image modifiée, on l’enregistre sur le système de fichier local.

L’humain s’émerveille

Ces quelques centaines de lignes de code et ces petits poèmes illustrés peuvent ouvrir à une réflexion plus large sur la créativité assistée par l’intelligence artificielle et les possibilités (infinies) qu’elle offre.

En variant les modèles de langage et les services de génération d’images, nous pouvons également découvrir les biais rédactionnels et créatifs propres à chaque système. Par exemple :

Ces variations nous invitent à explorer comment les choix techniques et les données d’entraînement influencent la créativité de l’IA. C’est un terrain d’expérimentation riche, où chaque essai peut réserver des surprises et des découvertes inattendues, qui peut vite créer un effet quasi addictif.

Enfin, et pour finir en enfonçant une porte ouverte, il me semble que l’art génératif est avant tout un outil, une extension de notre propre créativité. Il ne remplace pas l’artiste humain, mais il l’accompagne et l’inspire.

Code comme un poème
Chaque ligne un artisanatL&rsquo;esprit en éveil

Code comme un poème
Chaque ligne un artisanat
L’esprit en éveil

Lignes de code dansent,
Bugs cachés sous la surface,Liberté s&rsquo;éveille

Lignes de code dansent,
Bugs cachés sous la surface,
Liberté s’éveille

Code en libre flux
Les erreurs s&rsquo;évanouissentSoleil des sources

Code en libre flux
Les erreurs s’évanouissent
Soleil des sources

Code ouvert et libre
L&rsquo;esprit des devs s&rsquo;envole hautCollaboration

Code ouvert et libre
L’esprit des devs s’envole haut
Collaboration

Serveurs en cadence
Les scripts dansent en silencePaix dans le réseau

Serveurs en cadence
Les scripts dansent en silence
Paix dans le réseau