oledgifstudio 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MaticeMrll
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,245 @@
1
+ Metadata-Version: 2.4
2
+ Name: oledgifstudio
3
+ Version: 0.1.0
4
+ Summary: Générateur de GIFs animés 1 bit pour écrans OLED (SteelSeries, SSD1306, ...)
5
+ Author-email: MaticeMrll <marill.matice@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 MaticeMrll
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/MaticeMrll/oled-gif-studio
29
+ Project-URL: Repository, https://github.com/MaticeMrll/oled-gif-studio
30
+ Project-URL: Issues, https://github.com/MaticeMrll/oled-gif-studio/issues
31
+ Keywords: oled,gif,steelseries,ssd1306,keyboard,pillow
32
+ Classifier: Programming Language :: Python :: 3
33
+ Classifier: License :: OSI Approved :: MIT License
34
+ Classifier: Operating System :: OS Independent
35
+ Classifier: Topic :: Multimedia :: Graphics
36
+ Classifier: Environment :: Console
37
+ Requires-Python: >=3.10
38
+ Description-Content-Type: text/markdown
39
+ License-File: LICENSE
40
+ Requires-Dist: Pillow>=9.0
41
+ Dynamic: license-file
42
+
43
+ # oled-gif-studio
44
+
45
+ <p align="center">
46
+ <img src="docs/hero.gif" width="384" alt="OLED GIF STUDIO qui défile sur un écran 128x40">
47
+ </p>
48
+
49
+ Générateur de GIFs animés **1 bit** pour les petits écrans OLED : claviers/souris
50
+ SteelSeries, modules SSD1306/SH1106 (Arduino, Raspberry Pi, macropads QMK)...
51
+
52
+ Pas de modèle IA, pas d'API payante : tout est **procédural** (Python + Pillow),
53
+ donc instantané, gratuit et pixel-perfect. Un mode « description en langage
54
+ naturel » (français ou anglais) choisit l'effet et les paramètres pour toi.
55
+
56
+ > Tous les aperçus de ce README sont agrandis ×3 — la taille réelle des GIFs
57
+ > est celle de l'écran cible (128×40 par défaut).
58
+
59
+ ## Installation
60
+
61
+ Aucune, si Python (≥ 3.10) + Pillow sont déjà installés. Sinon :
62
+
63
+ ```
64
+ pip install Pillow
65
+ ```
66
+
67
+ Optionnel, pour avoir la commande `oledgif` partout :
68
+
69
+ ```
70
+ pip install -e .
71
+ ```
72
+
73
+ ## Usage rapide
74
+
75
+ ```powershell
76
+ # Depuis le dossier du projet :
77
+ python -m oledgif "HELLO WORLD" # effet auto → hello.gif
78
+ python -m oledgif "GG" -e slot -o gg.gif # machine à sous
79
+ python -m oledgif "PWNED" -e glitch -p rival # pour l'OLED d'une souris
80
+ python -m oledgif "42" -e matrix --size 128x64 --fps 20 # taille custom
81
+
82
+ # En langage naturel :
83
+ python -m oledgif -d "le texte 'BONJOUR' défile lentement"
84
+ python -m oledgif -d "'GAME OVER' qui clignote vite pendant 3s"
85
+ python -m oledgif -d "un radar qui balaie l'écran"
86
+
87
+ # À partir d'une image :
88
+ python -m oledgif -i photo.jpg --fit cover # photo plein écran, effet vhs
89
+ python -m oledgif -i comic.jpg --fit cover --style comic # illustration/BD
90
+ python -m oledgif -i logo.png -e bounce # le logo rebondit
91
+ python -m oledgif -i meme.gif # GIF animé converti tel quel
92
+
93
+ # Motifs sans texte (écrans de veille) :
94
+ python -m oledgif -e starfield
95
+ python -m oledgif -e plasma --seconds 6
96
+
97
+ # Un exemple de chaque effet dans ./samples :
98
+ python -m oledgif --demo
99
+ ```
100
+
101
+ ## Effets texte / image (`--list-effects`)
102
+
103
+ <table>
104
+ <tr>
105
+ <td align="center"><img src="docs/scroll.gif" alt="scroll"><br><code>scroll</code> — défilement en boucle parfaite</td>
106
+ <td align="center"><img src="docs/typewriter.gif" alt="typewriter"><br><code>typewriter</code> — machine à écrire avec curseur</td>
107
+ </tr>
108
+ <tr>
109
+ <td align="center"><img src="docs/wave.gif" alt="wave"><br><code>wave</code> — lettres en vague</td>
110
+ <td align="center"><img src="docs/blink.gif" alt="blink"><br><code>blink</code> — clignotement</td>
111
+ </tr>
112
+ <tr>
113
+ <td align="center"><img src="docs/bounce.gif" alt="bounce"><br><code>bounce</code> — rebond façon logo DVD</td>
114
+ <td align="center"><img src="docs/matrix.gif" alt="matrix"><br><code>matrix</code> — pluie de caractères qui révèle le texte</td>
115
+ </tr>
116
+ <tr>
117
+ <td align="center"><img src="docs/slot.gif" alt="slot"><br><code>slot</code> — chaque lettre cycle puis se fige</td>
118
+ <td align="center"><img src="docs/glitch.gif" alt="glitch"><br><code>glitch</code> — bandes décalées + bruit</td>
119
+ </tr>
120
+ <tr>
121
+ <td align="center"><img src="docs/pulse.gif" alt="pulse"><br><code>pulse</code> — battement de cœur</td>
122
+ <td align="center"><img src="docs/vhs.gif" alt="vhs"><br><code>vhs</code> — tracking façon cassette vidéo</td>
123
+ </tr>
124
+ <tr>
125
+ <td align="center"><img src="docs/slide.gif" alt="slide"><br><code>slide</code> — générique bas → haut</td>
126
+ <td></td>
127
+ </tr>
128
+ </table>
129
+
130
+ `typewriter`, `wave`, `matrix` et `slot` sont réservés au texte ; les autres
131
+ acceptent aussi une image (`--image`).
132
+
133
+ `--effect auto` (défaut) : `scroll` si le texte est trop large pour l'écran,
134
+ sinon `wave` ; `vhs` pour une image ; conversion directe pour un GIF animé.
135
+
136
+ ## Motifs sans texte (écrans de veille)
137
+
138
+ <table>
139
+ <tr>
140
+ <td align="center"><img src="docs/starfield.gif" alt="starfield"><br><code>starfield</code> — hyperespace</td>
141
+ <td align="center"><img src="docs/plasma.gif" alt="plasma"><br><code>plasma</code> — plasma rétro tramé</td>
142
+ </tr>
143
+ <tr>
144
+ <td align="center"><img src="docs/life.gif" alt="life"><br><code>life</code> — jeu de la vie de Conway</td>
145
+ <td align="center"><img src="docs/eq.gif" alt="eq"><br><code>eq</code> — égaliseur audio</td>
146
+ </tr>
147
+ <tr>
148
+ <td align="center"><img src="docs/scope.gif" alt="scope"><br><code>scope</code> — oscilloscope</td>
149
+ <td align="center"><img src="docs/radar.gif" alt="radar"><br><code>radar</code> — balayage avec échos</td>
150
+ </tr>
151
+ </table>
152
+
153
+ ## Images : les 4 styles de rendu
154
+
155
+ Convertir une image couleur en 1 bit sur 5120 pixels est un exercice de
156
+ sacrifice — le bon `--style` dépend de la source. Démonstration sur la même
157
+ scène (lune, silhouettes sur ciel en dégradé, lumières de ville) :
158
+
159
+ | Rendu | Style |
160
+ |---|---|
161
+ | <img src="docs/style_source.png" alt="source" width="288"> | **Source** (niveaux de gris) |
162
+ | <img src="docs/style_photo.png" alt="photo" width="288"> | `--style photo` (défaut) — auto-contraste + netteté + trame Floyd-Steinberg. Le bon choix pour les **photos réelles** : la trame simule les niveaux de gris. Sur une illustration, elle devient du bruit. |
163
+ | <img src="docs/style_solid.png" alt="solid" width="288"> | `--style solid` — seuillage net (alias `--no-dither`). Parfait pour les **logos** et dessins au trait… mais tout ce qui est sombre-sur-sombre disparaît dans le noir. |
164
+ | <img src="docs/style_comic.png" alt="comic" width="288"> | `--style comic` — comme `solid`, mais un 2ᵉ seuil automatique (Otsu) sépare les tons sombres et retrace en **contour blanc** les formes noyées dans le noir. Idéal pour les **illustrations/BD**. |
165
+ | <img src="docs/style_edges.png" alt="edges" width="288"> | `--style edges` — détection de contours, traits blancs sur fond noir. Look néon/filaire, souvent le plus lisible pour un **paysage ou un visage**. |
166
+
167
+ Autres options d'image :
168
+
169
+ - `--fit cover` — l'image remplit tout l'écran (recadrée) au lieu d'être
170
+ réduite à un timbre-poste au milieu. Recommandé pour les photos paysage.
171
+ - **Dé-bruitage** (actif par défaut) : un filtre médian + une suppression des
172
+ pixels isolés éliminent le « poivre et sel » (reflets, lampadaires, étoiles).
173
+ `--no-denoise` le désactive si tu veux justement garder ces points.
174
+ - Un **GIF animé** en entrée est converti frame par frame en conservant les
175
+ durées d'origine (avec `--effect auto`).
176
+
177
+ ## Langage naturel (`--describe`)
178
+
179
+ ```powershell
180
+ python -m oledgif -d "le texte 'BRB' qui rebondit pendant 5 secondes"
181
+ python -m oledgif -d "'REC' en mode vhs vintage"
182
+ python -m oledgif -d "a fast blinking 'GO' for 3 seconds"
183
+ ```
184
+
185
+ Le parseur (FR/EN, insensible aux accents) reconnaît l'effet par mots-clés
186
+ (« défile », « clignote », « tape », « radar », « vintage »…), le texte entre
187
+ guillemets, la durée (« pendant 3s ») et la vitesse (« lentement », « vite »).
188
+
189
+ ## Écrans préconfigurés (`--list-presets`)
190
+
191
+ | Preset | Taille | Matériel |
192
+ |---------------|---------|----------|
193
+ | `apex` (défaut) | 128×40 | SteelSeries Apex 5 / 7 / Pro (OLED clavier) |
194
+ | `rival` | 128×36 | SteelSeries Rival 700 / 710 (OLED souris) |
195
+ | `oled-128x64` | 128×64 | SSD1306 / SH1106 / SSD1309 |
196
+ | `oled-128x32` | 128×32 | SSD1306 |
197
+ | `oled-96x16` | 96×16 | SSD1306 |
198
+ | `oled-256x64` | 256×64 | SSD1322 |
199
+
200
+ N'importe quelle autre taille : `--size LARGEURxHAUTEUR`.
201
+
202
+ ## Options utiles
203
+
204
+ - `--charset alnum|digits|upper|letters|ascii` ou une chaîne littérale — le
205
+ jeu de caractères utilisé par `matrix` et `slot` (défaut : les 62
206
+ alphanumériques `0-9A-Za-z`).
207
+ - `--font chemin.ttf --font-size N` — police custom (défaut : Consolas,
208
+ taille ajustée à l'écran).
209
+ - `--invert` — noir sur blanc.
210
+ - `--scale 4` — GIF agrandi ×4 (pour prévisualiser confortablement).
211
+ - `--seed 42` — rendu reproductible pour les effets aléatoires.
212
+ - `--fps`, `--seconds`, `--speed` (px/s pour scroll).
213
+
214
+ ## Envoyer le GIF sur l'écran SteelSeries
215
+
216
+ Deux options :
217
+
218
+ 1. **SteelSeries GG** : dans les réglages de ton clavier (section OLED),
219
+ tu peux importer une image/GIF 128×40 — les GIFs générés ici sont au bon
220
+ format (1 bit, taille exacte).
221
+ 2. **GameSense API** (programmatique) : comme le fait
222
+ [SteelseriesAnimGif](https://github.com/bolner/SteelseriesAnimGif), on peut
223
+ streamer les frames vers l'écran via l'API locale de SteelSeries GG.
224
+ Les GIFs produits ici sont directement exploitables frame par frame.
225
+
226
+ ## Structure du projet
227
+
228
+ ```
229
+ oledgif/
230
+ cli.py # ligne de commande + make_gif()
231
+ effects.py # les 11 effets texte/image (registre @effect)
232
+ patterns.py # les 6 motifs sans texte
233
+ render.py # préparation d'images, binarisation, écriture GIF
234
+ describe.py # parseur langage naturel FR/EN
235
+ presets.py # tailles d'écrans du marché
236
+ fonts.py # chargement de police
237
+ samples/ # un GIF d'exemple par effet (taille réelle, --demo)
238
+ docs/ # illustrations du README (agrandies ×3)
239
+ ```
240
+ ---
241
+ ## Mon choix
242
+
243
+ <img src="docs/keyboard.jpg" alt="source" width="288">
244
+
245
+ Ninja Turtles !
@@ -0,0 +1,203 @@
1
+ # oled-gif-studio
2
+
3
+ <p align="center">
4
+ <img src="docs/hero.gif" width="384" alt="OLED GIF STUDIO qui défile sur un écran 128x40">
5
+ </p>
6
+
7
+ Générateur de GIFs animés **1 bit** pour les petits écrans OLED : claviers/souris
8
+ SteelSeries, modules SSD1306/SH1106 (Arduino, Raspberry Pi, macropads QMK)...
9
+
10
+ Pas de modèle IA, pas d'API payante : tout est **procédural** (Python + Pillow),
11
+ donc instantané, gratuit et pixel-perfect. Un mode « description en langage
12
+ naturel » (français ou anglais) choisit l'effet et les paramètres pour toi.
13
+
14
+ > Tous les aperçus de ce README sont agrandis ×3 — la taille réelle des GIFs
15
+ > est celle de l'écran cible (128×40 par défaut).
16
+
17
+ ## Installation
18
+
19
+ Aucune, si Python (≥ 3.10) + Pillow sont déjà installés. Sinon :
20
+
21
+ ```
22
+ pip install Pillow
23
+ ```
24
+
25
+ Optionnel, pour avoir la commande `oledgif` partout :
26
+
27
+ ```
28
+ pip install -e .
29
+ ```
30
+
31
+ ## Usage rapide
32
+
33
+ ```powershell
34
+ # Depuis le dossier du projet :
35
+ python -m oledgif "HELLO WORLD" # effet auto → hello.gif
36
+ python -m oledgif "GG" -e slot -o gg.gif # machine à sous
37
+ python -m oledgif "PWNED" -e glitch -p rival # pour l'OLED d'une souris
38
+ python -m oledgif "42" -e matrix --size 128x64 --fps 20 # taille custom
39
+
40
+ # En langage naturel :
41
+ python -m oledgif -d "le texte 'BONJOUR' défile lentement"
42
+ python -m oledgif -d "'GAME OVER' qui clignote vite pendant 3s"
43
+ python -m oledgif -d "un radar qui balaie l'écran"
44
+
45
+ # À partir d'une image :
46
+ python -m oledgif -i photo.jpg --fit cover # photo plein écran, effet vhs
47
+ python -m oledgif -i comic.jpg --fit cover --style comic # illustration/BD
48
+ python -m oledgif -i logo.png -e bounce # le logo rebondit
49
+ python -m oledgif -i meme.gif # GIF animé converti tel quel
50
+
51
+ # Motifs sans texte (écrans de veille) :
52
+ python -m oledgif -e starfield
53
+ python -m oledgif -e plasma --seconds 6
54
+
55
+ # Un exemple de chaque effet dans ./samples :
56
+ python -m oledgif --demo
57
+ ```
58
+
59
+ ## Effets texte / image (`--list-effects`)
60
+
61
+ <table>
62
+ <tr>
63
+ <td align="center"><img src="docs/scroll.gif" alt="scroll"><br><code>scroll</code> — défilement en boucle parfaite</td>
64
+ <td align="center"><img src="docs/typewriter.gif" alt="typewriter"><br><code>typewriter</code> — machine à écrire avec curseur</td>
65
+ </tr>
66
+ <tr>
67
+ <td align="center"><img src="docs/wave.gif" alt="wave"><br><code>wave</code> — lettres en vague</td>
68
+ <td align="center"><img src="docs/blink.gif" alt="blink"><br><code>blink</code> — clignotement</td>
69
+ </tr>
70
+ <tr>
71
+ <td align="center"><img src="docs/bounce.gif" alt="bounce"><br><code>bounce</code> — rebond façon logo DVD</td>
72
+ <td align="center"><img src="docs/matrix.gif" alt="matrix"><br><code>matrix</code> — pluie de caractères qui révèle le texte</td>
73
+ </tr>
74
+ <tr>
75
+ <td align="center"><img src="docs/slot.gif" alt="slot"><br><code>slot</code> — chaque lettre cycle puis se fige</td>
76
+ <td align="center"><img src="docs/glitch.gif" alt="glitch"><br><code>glitch</code> — bandes décalées + bruit</td>
77
+ </tr>
78
+ <tr>
79
+ <td align="center"><img src="docs/pulse.gif" alt="pulse"><br><code>pulse</code> — battement de cœur</td>
80
+ <td align="center"><img src="docs/vhs.gif" alt="vhs"><br><code>vhs</code> — tracking façon cassette vidéo</td>
81
+ </tr>
82
+ <tr>
83
+ <td align="center"><img src="docs/slide.gif" alt="slide"><br><code>slide</code> — générique bas → haut</td>
84
+ <td></td>
85
+ </tr>
86
+ </table>
87
+
88
+ `typewriter`, `wave`, `matrix` et `slot` sont réservés au texte ; les autres
89
+ acceptent aussi une image (`--image`).
90
+
91
+ `--effect auto` (défaut) : `scroll` si le texte est trop large pour l'écran,
92
+ sinon `wave` ; `vhs` pour une image ; conversion directe pour un GIF animé.
93
+
94
+ ## Motifs sans texte (écrans de veille)
95
+
96
+ <table>
97
+ <tr>
98
+ <td align="center"><img src="docs/starfield.gif" alt="starfield"><br><code>starfield</code> — hyperespace</td>
99
+ <td align="center"><img src="docs/plasma.gif" alt="plasma"><br><code>plasma</code> — plasma rétro tramé</td>
100
+ </tr>
101
+ <tr>
102
+ <td align="center"><img src="docs/life.gif" alt="life"><br><code>life</code> — jeu de la vie de Conway</td>
103
+ <td align="center"><img src="docs/eq.gif" alt="eq"><br><code>eq</code> — égaliseur audio</td>
104
+ </tr>
105
+ <tr>
106
+ <td align="center"><img src="docs/scope.gif" alt="scope"><br><code>scope</code> — oscilloscope</td>
107
+ <td align="center"><img src="docs/radar.gif" alt="radar"><br><code>radar</code> — balayage avec échos</td>
108
+ </tr>
109
+ </table>
110
+
111
+ ## Images : les 4 styles de rendu
112
+
113
+ Convertir une image couleur en 1 bit sur 5120 pixels est un exercice de
114
+ sacrifice — le bon `--style` dépend de la source. Démonstration sur la même
115
+ scène (lune, silhouettes sur ciel en dégradé, lumières de ville) :
116
+
117
+ | Rendu | Style |
118
+ |---|---|
119
+ | <img src="docs/style_source.png" alt="source" width="288"> | **Source** (niveaux de gris) |
120
+ | <img src="docs/style_photo.png" alt="photo" width="288"> | `--style photo` (défaut) — auto-contraste + netteté + trame Floyd-Steinberg. Le bon choix pour les **photos réelles** : la trame simule les niveaux de gris. Sur une illustration, elle devient du bruit. |
121
+ | <img src="docs/style_solid.png" alt="solid" width="288"> | `--style solid` — seuillage net (alias `--no-dither`). Parfait pour les **logos** et dessins au trait… mais tout ce qui est sombre-sur-sombre disparaît dans le noir. |
122
+ | <img src="docs/style_comic.png" alt="comic" width="288"> | `--style comic` — comme `solid`, mais un 2ᵉ seuil automatique (Otsu) sépare les tons sombres et retrace en **contour blanc** les formes noyées dans le noir. Idéal pour les **illustrations/BD**. |
123
+ | <img src="docs/style_edges.png" alt="edges" width="288"> | `--style edges` — détection de contours, traits blancs sur fond noir. Look néon/filaire, souvent le plus lisible pour un **paysage ou un visage**. |
124
+
125
+ Autres options d'image :
126
+
127
+ - `--fit cover` — l'image remplit tout l'écran (recadrée) au lieu d'être
128
+ réduite à un timbre-poste au milieu. Recommandé pour les photos paysage.
129
+ - **Dé-bruitage** (actif par défaut) : un filtre médian + une suppression des
130
+ pixels isolés éliminent le « poivre et sel » (reflets, lampadaires, étoiles).
131
+ `--no-denoise` le désactive si tu veux justement garder ces points.
132
+ - Un **GIF animé** en entrée est converti frame par frame en conservant les
133
+ durées d'origine (avec `--effect auto`).
134
+
135
+ ## Langage naturel (`--describe`)
136
+
137
+ ```powershell
138
+ python -m oledgif -d "le texte 'BRB' qui rebondit pendant 5 secondes"
139
+ python -m oledgif -d "'REC' en mode vhs vintage"
140
+ python -m oledgif -d "a fast blinking 'GO' for 3 seconds"
141
+ ```
142
+
143
+ Le parseur (FR/EN, insensible aux accents) reconnaît l'effet par mots-clés
144
+ (« défile », « clignote », « tape », « radar », « vintage »…), le texte entre
145
+ guillemets, la durée (« pendant 3s ») et la vitesse (« lentement », « vite »).
146
+
147
+ ## Écrans préconfigurés (`--list-presets`)
148
+
149
+ | Preset | Taille | Matériel |
150
+ |---------------|---------|----------|
151
+ | `apex` (défaut) | 128×40 | SteelSeries Apex 5 / 7 / Pro (OLED clavier) |
152
+ | `rival` | 128×36 | SteelSeries Rival 700 / 710 (OLED souris) |
153
+ | `oled-128x64` | 128×64 | SSD1306 / SH1106 / SSD1309 |
154
+ | `oled-128x32` | 128×32 | SSD1306 |
155
+ | `oled-96x16` | 96×16 | SSD1306 |
156
+ | `oled-256x64` | 256×64 | SSD1322 |
157
+
158
+ N'importe quelle autre taille : `--size LARGEURxHAUTEUR`.
159
+
160
+ ## Options utiles
161
+
162
+ - `--charset alnum|digits|upper|letters|ascii` ou une chaîne littérale — le
163
+ jeu de caractères utilisé par `matrix` et `slot` (défaut : les 62
164
+ alphanumériques `0-9A-Za-z`).
165
+ - `--font chemin.ttf --font-size N` — police custom (défaut : Consolas,
166
+ taille ajustée à l'écran).
167
+ - `--invert` — noir sur blanc.
168
+ - `--scale 4` — GIF agrandi ×4 (pour prévisualiser confortablement).
169
+ - `--seed 42` — rendu reproductible pour les effets aléatoires.
170
+ - `--fps`, `--seconds`, `--speed` (px/s pour scroll).
171
+
172
+ ## Envoyer le GIF sur l'écran SteelSeries
173
+
174
+ Deux options :
175
+
176
+ 1. **SteelSeries GG** : dans les réglages de ton clavier (section OLED),
177
+ tu peux importer une image/GIF 128×40 — les GIFs générés ici sont au bon
178
+ format (1 bit, taille exacte).
179
+ 2. **GameSense API** (programmatique) : comme le fait
180
+ [SteelseriesAnimGif](https://github.com/bolner/SteelseriesAnimGif), on peut
181
+ streamer les frames vers l'écran via l'API locale de SteelSeries GG.
182
+ Les GIFs produits ici sont directement exploitables frame par frame.
183
+
184
+ ## Structure du projet
185
+
186
+ ```
187
+ oledgif/
188
+ cli.py # ligne de commande + make_gif()
189
+ effects.py # les 11 effets texte/image (registre @effect)
190
+ patterns.py # les 6 motifs sans texte
191
+ render.py # préparation d'images, binarisation, écriture GIF
192
+ describe.py # parseur langage naturel FR/EN
193
+ presets.py # tailles d'écrans du marché
194
+ fonts.py # chargement de police
195
+ samples/ # un GIF d'exemple par effet (taille réelle, --demo)
196
+ docs/ # illustrations du README (agrandies ×3)
197
+ ```
198
+ ---
199
+ ## Mon choix
200
+
201
+ <img src="docs/keyboard.jpg" alt="source" width="288">
202
+
203
+ Ninja Turtles !
@@ -0,0 +1,3 @@
1
+ """oledgif — générateur de GIFs animés pour écrans OLED (SteelSeries, SSD1306, ...)."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,176 @@
1
+ """CLI : python -m oledgif "TEXTE" [options]"""
2
+
3
+ import argparse
4
+ import os
5
+ import random
6
+ import string
7
+ import sys
8
+
9
+ from . import __version__
10
+ from .describe import parse as parse_description
11
+ from .effects import ALNUM, DESCRIPTIONS, EFFECTS, Ctx
12
+ from .fonts import load_font
13
+ from .patterns import PATTERNS
14
+ from .presets import DEFAULT_PRESET, PRESETS, resolve_size
15
+ from .render import convert_animation, load_source, save_gif
16
+
17
+ from PIL import Image
18
+
19
+ CHARSETS = {
20
+ "alnum": ALNUM,
21
+ "digits": string.digits,
22
+ "upper": string.digits + string.ascii_uppercase,
23
+ "letters": string.ascii_letters,
24
+ "ascii": "".join(c for c in string.printable if c.isprintable() and c != " "),
25
+ }
26
+
27
+
28
+ def build_parser():
29
+ p = argparse.ArgumentParser(
30
+ prog="oledgif",
31
+ description="Génère des GIFs animés 1 bit pour écrans OLED (SteelSeries, SSD1306...).",
32
+ )
33
+ p.add_argument("text", nargs="?", help="texte à animer (optionnel avec --image ou un motif)")
34
+ p.add_argument("-d", "--describe", metavar="PHRASE",
35
+ help="décrire l'animation en langage naturel, ex: 'GG qui clignote vite'")
36
+ p.add_argument("-i", "--image", metavar="FICHIER",
37
+ help="image source (PNG/JPG/GIF...) : convertie en 1 bit avec tramage ; "
38
+ "un GIF animé est converti tel quel si --effect vaut auto/convert")
39
+ p.add_argument("--style", default="photo", choices=["photo", "solid", "comic", "edges"],
40
+ help="rendu de --image : photo = contraste+netteté+tramage (défaut), "
41
+ "solid = seuillage net (logos), comic = solid + contours blancs "
42
+ "des formes perdues dans le noir (illustrations), "
43
+ "edges = contours blancs sur noir")
44
+ p.add_argument("--fit", default="contain", choices=["contain", "cover"],
45
+ help="contain = image entière (défaut), cover = remplit l'écran en "
46
+ "recadrant — recommandé pour les photos paysage")
47
+ p.add_argument("--no-dither", action="store_true",
48
+ help="alias de --style solid")
49
+ p.add_argument("--no-denoise", action="store_true",
50
+ help="garde le bruit : désactive le filtre médian et la suppression "
51
+ "des pixels isolés (reflets, sources de lumière ponctuelles)")
52
+ p.add_argument("-o", "--out", default=None, help="fichier de sortie (.gif)")
53
+ p.add_argument("-p", "--preset", default=None,
54
+ help=f"écran cible (défaut: {DEFAULT_PRESET}) — voir --list-presets")
55
+ p.add_argument("--size", default=None, metavar="WxH", help="taille custom, ex: 128x40")
56
+ p.add_argument("-e", "--effect", default="auto",
57
+ help="effet (défaut: auto) — voir --list-effects")
58
+ p.add_argument("--fps", type=int, default=15)
59
+ p.add_argument("--seconds", type=float, default=4.0, help="durée cible (certains effets la déduisent)")
60
+ p.add_argument("--speed", type=float, default=None, help="vitesse de défilement en px/s (scroll)")
61
+ p.add_argument("--font", default=None, help="chemin d'une police .ttf")
62
+ p.add_argument("--font-size", type=int, default=None)
63
+ p.add_argument("--charset", default="alnum",
64
+ help="alnum|digits|upper|letters|ascii ou chaîne littérale (matrix/slot)")
65
+ p.add_argument("--invert", action="store_true", help="noir sur blanc")
66
+ p.add_argument("--scale", type=int, default=1, help="agrandit le GIF xN (aperçu)")
67
+ p.add_argument("--seed", type=int, default=None, help="graine aléatoire (rendu reproductible)")
68
+ p.add_argument("--demo", action="store_true", help="génère un exemple de chaque effet dans ./samples")
69
+ p.add_argument("--list-effects", action="store_true")
70
+ p.add_argument("--list-presets", action="store_true")
71
+ p.add_argument("--version", action="version", version=f"oledgif {__version__}")
72
+ return p
73
+
74
+
75
+ def make_gif(text, out, *, preset=None, size=None, effect="auto", fps=15, seconds=4.0,
76
+ speed=None, font_path=None, font_size=None, charset=ALNUM,
77
+ invert=False, scale=1, seed=None, image=None, style="photo", fit="contain",
78
+ denoise=True):
79
+ W, H = resolve_size(preset, size)
80
+ font = load_font(font_path, font_size, screen_h=H)
81
+
82
+ src = None
83
+ if image:
84
+ im = Image.open(image)
85
+ if getattr(im, "is_animated", False) and effect in ("auto", "convert"):
86
+ # GIF animé -> conversion frame par frame, durées d'origine conservées
87
+ frames, durations = convert_animation(image, W, H, style, fit, denoise)
88
+ n, duration = save_gif(frames, out, fps=fps, invert=invert,
89
+ scale=scale, durations=durations)
90
+ return out, "convert", W, H, n, duration
91
+ im.close()
92
+ src = load_source(image, W, H, style, fit, denoise)
93
+ if effect in ("auto", "convert"):
94
+ effect = "vhs"
95
+
96
+ if effect == "auto":
97
+ try:
98
+ wide = font.getlength(text) > W - 4
99
+ except AttributeError:
100
+ wide = len(text) * 7 > W - 4
101
+ effect = "scroll" if wide else "wave"
102
+ if effect not in EFFECTS:
103
+ raise SystemExit(f"Effet inconnu: {effect!r}. Effets: {', '.join(EFFECTS)}")
104
+
105
+ ctx = Ctx(text=text or "", W=W, H=H, fps=fps, seconds=seconds, font=font,
106
+ font_path=font_path, font_size=font_size, charset=charset,
107
+ speed=speed, src=src, rng=random.Random(seed))
108
+ frames = EFFECTS[effect](ctx)
109
+ n, duration = save_gif(frames, out, fps=fps, invert=invert, scale=scale)
110
+ return out, effect, W, H, n, duration
111
+
112
+
113
+ def main(argv=None):
114
+ args = build_parser().parse_args(argv)
115
+
116
+ if args.list_effects:
117
+ for name, desc in DESCRIPTIONS.items():
118
+ print(f" {name:<11} {desc}")
119
+ return 0
120
+ if args.list_presets:
121
+ for name, (w, h, desc) in PRESETS.items():
122
+ print(f" {name:<12} {w}x{h:<4} {desc}")
123
+ return 0
124
+
125
+ charset = CHARSETS.get(args.charset, args.charset)
126
+
127
+ if args.demo:
128
+ outdir = "samples"
129
+ os.makedirs(outdir, exist_ok=True)
130
+ text = args.text or "GG WP 42"
131
+ for name in EFFECTS:
132
+ out = os.path.join(outdir, f"{name}.gif")
133
+ make_gif(text, out, preset=args.preset, size=args.size, effect=name,
134
+ fps=args.fps, seconds=args.seconds, speed=args.speed,
135
+ font_path=args.font, font_size=args.font_size, charset=charset,
136
+ invert=args.invert, scale=args.scale, seed=args.seed or 42)
137
+ print(f" {out}")
138
+ return 0
139
+
140
+ text, effect, fps, seconds = args.text, args.effect, args.fps, args.seconds
141
+ if args.describe:
142
+ parsed = parse_description(args.describe)
143
+ text = parsed.get("text", text)
144
+ if effect == "auto":
145
+ effect = parsed.get("effect", "auto")
146
+ fps = parsed.get("fps", fps)
147
+ seconds = parsed.get("seconds", seconds)
148
+
149
+ textless_ok = args.image or effect in PATTERNS or effect == "matrix"
150
+ if not text and not textless_ok:
151
+ print("Erreur: aucun texte. Passe-le en argument, entre guillemets dans --describe, "
152
+ "ou utilise --image / un motif (starfield, plasma, life, eq, scope, radar).",
153
+ file=sys.stderr)
154
+ return 2
155
+
156
+ if args.out:
157
+ out = args.out
158
+ elif text:
159
+ out = "".join(c if c.isalnum() else "_" for c in text)[:24] + ".gif"
160
+ elif args.image:
161
+ out = os.path.splitext(os.path.basename(args.image))[0] + "_oled.gif"
162
+ else:
163
+ out = f"{effect}.gif"
164
+ out, effect, W, H, n, duration = make_gif(
165
+ text, out, preset=args.preset, size=args.size, effect=effect, fps=fps,
166
+ seconds=seconds, speed=args.speed, font_path=args.font,
167
+ font_size=args.font_size, charset=charset, invert=args.invert,
168
+ scale=args.scale, seed=args.seed, image=args.image,
169
+ style="solid" if args.no_dither else args.style, fit=args.fit,
170
+ denoise=not args.no_denoise)
171
+ print(f"{out} — {W}x{H}, effet {effect}, {n} frames @ {duration} ms")
172
+ return 0
173
+
174
+
175
+ if __name__ == "__main__":
176
+ raise SystemExit(main())