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 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
-
Go installé sur votre machine.
-
Un accès à un service LLM (Large Language Model) pour générer les haïkus et les prompts. Personnellement j’utilise ollama avec le modèle
llama3.2:3b
sur ma machine. -
Un service de génération d’images. Dans ce tutoriel, j’utiliserai le projet FastSD CPU en local également. Si vous avez Docker sur votre machine, vous pouvez utiliser l’image que j’ai créé pour l’occasion:
docker run -it --rm -p 8000:8000 docker.io/bornholm/fastsdcpu-api:v1.0.0-beta.100

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
-
haikuSystemPrompt : Ce prompt système définit le contexte pour le LLM, en lui indiquant de générer un haïku en suivant un format spécifique.
-
haikuUserPrompt : Ce prompt utilisateur fournit le thème inspirant le haïku.
-
extractHaiku : Une fonction utilisant une expression régulière pour extraire le haïku de la réponse du LLM.
var haikuRegExp = regexp.MustCompile(`(?msi)HAIKU\s*:\n+(.+)`) func extractHaiku(str string) string { matches := haikuRegExp.FindAllStringSubmatch(str, -1) if len(matches) < 1 || len(matches[0]) < 2 { return "" } return matches[0][1] }
É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 :
- Les modèles de langage peuvent avoir des préférences pour certains thèmes, structures ou émotions, influencés par les données sur lesquelles ils ont été entraînés.
- Les modèles de génération d’images peuvent produire des styles artistiques très différents, allant du réalisme à l’abstrait, en fonction des prompts et des paramètres utilisés.
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 artisanat
L’esprit en éveil

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

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

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

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