mvp
Browse files- .dockerignore +3 -0
- .gitattributes +1 -0
- .gitignore +5 -0
- README.md +5 -4
- articles/Java9 ce mal aimé.md +375 -0
- articles/Les differents Patterns de Transactions Distribuees.md +371 -0
- articles/mocked_java9_openai_tts.wav +3 -0
- articles/slidev.md +282 -0
- dev.sh +2 -0
- output/.gitkeep +0 -0
- requirements.txt +9 -3
- src/__init__.py +0 -0
- src/audio_generation.py +141 -0
- src/audio_generation_edge.py +141 -0
- src/config.py +42 -0
- src/mocked_script.py +19 -0
- src/script_generation.py +126 -0
- src/streamlit_app.py +177 -38
.dockerignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv
|
| 2 |
+
.env
|
| 3 |
+
output/*/
|
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
articles/mocked_java9_openai_tts.wav filter=lfs diff=lfs merge=lfs -text
|
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv
|
| 2 |
+
.env
|
| 3 |
+
output/*/
|
| 4 |
+
*.pyc
|
| 5 |
+
__pycache__/
|
README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
| 1 |
---
|
| 2 |
-
title: UpVoice
|
| 3 |
emoji: 🚀
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: red
|
| 6 |
sdk: docker
|
| 7 |
app_port: 8501
|
| 8 |
tags:
|
| 9 |
-
- streamlit
|
| 10 |
-
|
| 11 |
-
|
|
|
|
| 12 |
---
|
| 13 |
|
| 14 |
# Welcome to Streamlit!
|
|
|
|
| 1 |
---
|
| 2 |
+
title: UpVoice
|
| 3 |
emoji: 🚀
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: red
|
| 6 |
sdk: docker
|
| 7 |
app_port: 8501
|
| 8 |
tags:
|
| 9 |
+
- streamlit
|
| 10 |
+
- tts
|
| 11 |
+
pinned: true
|
| 12 |
+
short_description: Générez un podcast à partir d'un de nos articles Tech
|
| 13 |
---
|
| 14 |
|
| 15 |
# Welcome to Streamlit!
|
articles/Java9 ce mal aimé.md
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Java 9, ce mal aimé
|
| 2 |
+
|
| 3 |
+
Cet article n'a pas pour but de vous faire migrer vos applications vers Java 9, ni de faire l'apologie de Java 9, mais bien de montrer que cette version est méconnue et qu'elle a pu apporter énormément au langage et à notre quotidien. Elle a été précurseuse de certains sujets qui, aujourd'hui, font l'actualité de la communauté Java.
|
| 4 |
+
|
| 5 |
+
L'ensemble des javaistes considère Java 8 comme une révolution du langage Java. Beaucoup d'applications en production sont d'ailleurs encore basées sur cette version.
|
| 6 |
+
Peu ont migré leurs applications de Java 8 à Java 9.
|
| 7 |
+
Alors pourquoi écrire un article sur Java 9 me direz-vous ? Simplement parce que ce dernier a apporté des nouveautés radicales et annoncées comme quasi révolutionnaires pour Java : la modularisation, les JAR multi-releases et l'algorithme G1GC comme Garbage Collector par défaut.
|
| 8 |
+
|
| 9 |
+
Dans ce cas, pourquoi Java 9 n'a pas été aussi apprécié que Java 8 ? Connaissez-vous bien ce qu'il a réellement apporté au langage ?
|
| 10 |
+
À travers cet article, nous verrons une liste non exhaustive des JEP (JDK Enhancement Proposal) qui vous feront peut-être revoir votre copie. Saviez-vous par exemple que [Java 9 contient pas moins de 91 JEP](https://openjdk.org/projects/jdk9/) !!
|
| 11 |
+
|
| 12 |
+
## Condensé des meilleures JEP de Java 9
|
| 13 |
+
|
| 14 |
+
### [JEP 280: Indify String Concatenation](https://openjdk.org/jeps/280)
|
| 15 |
+
|
| 16 |
+
Avant Java 9, le compilateur convertissait chaque concaténation de chaînes `String` en instructions StringBuilder. Avec cette approche, le code de concaténation était fixe et n'offrait pas de souplesse d'optimisation au moment de l'exécution. `StringBuilder` s'est tellement ancré en nous aujourd'hui, que bon nombre de développeurs l'utilise pour concaténer les chaînes de caractères.
|
| 17 |
+
|
| 18 |
+
L'objectif de cette JEP était d'améliorer la concaténation de chaînes `String` en utilisant `invokedynamic` pour déléguer la concaténation à la factory `StringConcatFactory`.
|
| 19 |
+
|
| 20 |
+
_Pourquoi ?_ Afin de permettre à la JVM d’optimiser **dynamiquement** les opérations de concaténation selon le contexte d'exécution, tout en conservant la simplicité de l’opérateur `+`.
|
| 21 |
+
|
| 22 |
+
_Dans quel but ?_ En utilisant `invokedynamic`, la JVM peut désormais adapter les concaténations pour améliorer les performances, réduire les allocations intermédiaires et ajuster le code en fonction de l'environnement d'exécution.
|
| 23 |
+
|
| 24 |
+
`invokedynamic` ? Instruction introduite par Java7, elle permet au bytecode d’appeler une méthode qui peut être résolue dynamiquement au moment de l'exécution.
|
| 25 |
+
|
| 26 |
+
`StringConcatFactory` ? C’est la fabrique qui choisit la meilleure stratégie de concaténation (comme `StringBuilder`, `StringBuffer`, des concaténations de tableaux de bytes ou encore des structures de buffer internes à la JVM) selon les conditions d’exécution (nombre et type d'opérandes).
|
| 27 |
+
|
| 28 |
+
Qu'est-ce que ça veut dire pour nous, développeurs ? Prenons l'exemple très simple suivant :
|
| 29 |
+
|
| 30 |
+
```java
|
| 31 |
+
String result = "Hello, " + name + "!";
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
Depuis Java 9, le compilateur ne traduit plus ce code en une séquence d'instructions avec StringBuilder (comme c'était le cas avant Java 9). Au lieu de cela, le compilateur Java génère une instruction `invokedynamic` dans le bytecode pour la concaténation de chaînes, qui ressemble à ceci : `INVOKEDYNAMIC makeConcatWithConstants(...)`
|
| 35 |
+
|
| 36 |
+
Cette instruction demande à la JVM de décider au moment de l’exécution de la meilleure stratégie pour assembler les chaînes, en tenant compte du contexte exact. La JVM peut alors décider :
|
| 37 |
+
|
| 38 |
+
- d'utiliser une instance de `StringBuilder` si la concaténation est simple,
|
| 39 |
+
- de combiner directement les chaînes en une seule opération, si elles sont constantes et peuvent être optimisées ainsi, réduisant alors l'empreinte mémoire,
|
| 40 |
+
- d’optimiser la concaténation de manière plus efficace que `StringBuilder` dans certains cas.
|
| 41 |
+
|
| 42 |
+
Au moment de l'exécution, quand la JVM lit l’instruction `invokedynamic`, elle va déterminer dynamiquement (runtime) le meilleur moyen de réaliser l'opération de concaténation.
|
| 43 |
+
Elle invoque ensuite une méthode de "_bootstrap_" (ou méthode "d’assistance"), capable d’identifier l’implémentation la plus performante pour la concaténation.
|
| 44 |
+
Enfin, la JVM utilise cette stratégie pour toutes les concaténations similaires rencontrées, excepté si le contexte change, évidemment.
|
| 45 |
+
|
| 46 |
+
Attention cependant aux cas complexes :
|
| 47 |
+
|
| 48 |
+
- En concaténant un très grand nombre de chaînes (l'agrégation de données par exemple), même si Java optimise les concaténations, `StringBuilder` peut toujours être plus performant, car il permet de mieux gérer la mémoire et d'éviter les allocations répétées.
|
| 49 |
+
- En construisant des chaînes de caractères à partir de nombreuses parties dynamiques, comme avec une utilisation de conditions ou d'opérations diverses, `StringBuilder` peut offrir une lisibilité et une maintenabilité accrues.
|
| 50 |
+
- En concaténant des chaînes de caractères au sein d'une boucle, `StringBuilder` reste le plus efficace.
|
| 51 |
+
|
| 52 |
+
Pour résumer, vous pouvez généralement faire confiance à ces optimisations pour fonctionner efficacement. Cependant, pour des constructions plus complexes ou répétitives, `StringBuilder` reste l'option à privilégier étant donné qu'elle est souvent plus performante.
|
| 53 |
+
|
| 54 |
+
### [JEP 254: Compact Strings](https://openjdk.org/jeps/254)
|
| 55 |
+
|
| 56 |
+
Cette JEP vise à optimiser la manière dont les chaînes de caractères `String` sont représentées/stockées en mémoire.
|
| 57 |
+
|
| 58 |
+
Avant Java 9, les objets `String` étaient représentés par des tableaux de caractères `char[]`. Chaque caractère utilisait 2 octets (soit 16 bits) à cause de l'encodage utilisé : UTF-16.
|
| 59 |
+
Même si vos chaînes de caractères ne contenaient que des caractères ASCII (donc 1 octet par caractère), le fait que l'encodage soit réalisé avec UTF-16 entrainait une consommation mémoire excessive et inutile.
|
| 60 |
+
|
| 61 |
+
Plusieurs changements ont donc été opérés :
|
| 62 |
+
|
| 63 |
+
- La classe `String` utilise désormais un tableau d'octets `byte[]` au lieu d'un tableau de caractère `char[]` pour stocker les données.
|
| 64 |
+
- Un champ supplémentaire de 1 bit, appelé `coder`, a été ajouté. Il est utilisé pour indiquer si la chaîne a été encodée en LATIN-1 (ou ASCII) ou en UTF-16, en prenant respectivement la valeur 0 ou 1.
|
| 65 |
+
Si une chaîne contient uniquement des caractères ASCII (de 0 à 127), elle est alors stockée en utilisant 1 octet par caractère, sinon elle est stockée en UTF-16 comme auparavant.
|
| 66 |
+
|
| 67 |
+
Il y a évidemment des cas où _Compact Strings_ ne s'applique pas. Par exemple, si votre chaîne contient des caractères non-ASCII, l'encodage se fera en UTF-16.
|
| 68 |
+
|
| 69 |
+
Vous l'aurez compris, cette JEP réduit la consommation mémoire des chaînes `String`. Cette réduction de l'empreinte mémoire permet également un meilleur usage du cache CPU, pouvant améliorer les performances d'opérations sur les chaînes de caractères.
|
| 70 |
+
|
| 71 |
+
Moins de consommation mémoire et une réduction des objets volumineux en mémoire veut aussi dire moins de pression sur le Garbage Collector.
|
| 72 |
+
|
| 73 |
+
> **TIPS**
|
| 74 |
+
> Si votre application manipule beaucoup de chaînes de caractères (lecture de base de données, de fichiers...) et qu'elle n'a pas de contrainte en termes de latence ou de temps réel, il est conseillé d'activer l'option de la JVM de déduplication de `String` (`-XX:+UseStringDeduplication`) apportée par Java 8. Elle vous permettra de réduire encore l'empreinte mémoire de vos chaînes de caractères.
|
| 75 |
+
> Vous pouvez vérifier, lors de vos benchmarks, si cette option est réellement utile à votre application en affichant les statistiques sur cette déduplication via l'option `-XX:+PrintStringDeduplicationStatistics`.
|
| 76 |
+
|
| 77 |
+
### [JEP 266: More Concurrency Updates](https://openjdk.org/jeps/266)
|
| 78 |
+
|
| 79 |
+
Introduites en Java 8, les _CompletableFuture_ permettent de gérer des opérations asynchrones et non bloquantes (ie sans bloquer le thread principal).
|
| 80 |
+
|
| 81 |
+
Java 9 améliore les _CompletableFuture_ en introduisant de nouvelles méthodes :
|
| 82 |
+
|
| 83 |
+
- `delayedExecutor` permet d'exécuter une tâche après un délai spécifié,
|
| 84 |
+
- `orTimeout` définit un délai au bout duquel une tâche échouera si elle n'est pas terminée,
|
| 85 |
+
- `completeOnTimeout` permet de définir une valeur par défaut qui sera utilisée si la tâche n’est pas complétée avant le délai spécifié.
|
| 86 |
+
|
| 87 |
+
### [JEP 269: Convenience Factory Methods for Collections](https://openjdk.org/jeps/269)
|
| 88 |
+
|
| 89 |
+
Java 9 introduit des méthodes de factory pour créer des collections immuables de façon concise. Celles-ci sont beaucoup plus lisibles et pratiques, notamment pour des listes (ou ensembles). On y retrouve `List.of`, `Set.of` et `Map.of`.
|
| 90 |
+
|
| 91 |
+
```java
|
| 92 |
+
List<String> names = List.of("Foo", "Boo", "Fooboo");
|
| 93 |
+
Set<Integer> numbers = Set.of(1, 2, 3);
|
| 94 |
+
Map<String, Integer> map = Map.of("one", 1, "two", 2);
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
En plus d'être immuable (vous ne pouvez ni ajouter, ni modifier, ni supprimer des éléments), la collection n'allouera uniquement que le nombre d'éléments passés, et elle ne sera donc pas initialisée avec une capacité par défaut (10 pour un `ArrayList`, 16 pour une `HashSet` et 16 pour une `HashMap`). C'est également un gain en termes de consommation mémoire.
|
| 98 |
+
|
| 99 |
+
### [JEP 213: Milling Project Coin](https://openjdk.org/jeps/213)
|
| 100 |
+
|
| 101 |
+
#### Ajout de méthodes privées aux interfaces
|
| 102 |
+
|
| 103 |
+
Depuis Java 9, nous pouvons désormais définir des méthodes privées (ou privées statiques) dans les interfaces.
|
| 104 |
+
|
| 105 |
+
```java
|
| 106 |
+
public interface MyCustomInterface {
|
| 107 |
+
|
| 108 |
+
default void myPublicMethod() {
|
| 109 |
+
privateHelper();
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
private void myPrivateHelper() {
|
| 113 |
+
System.out.println("Helper method");
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
A première vue, ça semblait être une bonne approche. Mais nous pouvons vite nous demander quel avantage celle-ci pouvait réellement nous procurer...
|
| 119 |
+
La réponse était pourtant simple, cette fonctionnalité permet de factoriser du code commun aux méthodes `default` de l'interface.
|
| 120 |
+
Prenons l'exemple très simple d'une interface de calcul :
|
| 121 |
+
|
| 122 |
+
```java
|
| 123 |
+
public interface Calculator {
|
| 124 |
+
default int addAndDouble(int a, int b) {
|
| 125 |
+
int sum = a + b;
|
| 126 |
+
return sum * 2;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
default int subtractAndDouble(int a, int b) {
|
| 130 |
+
int difference = a - b;
|
| 131 |
+
return difference * 2;
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
Le code `return ... * 2` est dupliqué dans les deux méthodes par défaut. Cette JEP nous permet de factoriser comme suit :
|
| 137 |
+
|
| 138 |
+
```java
|
| 139 |
+
public interface Calculator {
|
| 140 |
+
default int addAndDouble(int a, int b) {
|
| 141 |
+
return doubleIt(a + b);
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
default int subtractAndDouble(int a, int b) {
|
| 145 |
+
return doubleIt(a - b);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
private int doubleIt(int value) {
|
| 149 |
+
return value * 2;
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
#### Amélioration du `try-with-resources`
|
| 155 |
+
|
| 156 |
+
Vous le savez certainement, Java 7 a introduit la notion de `try-with-resources` permettant de fermer automatiquement les ressources implémentant `AutoCloseable`. Mais pour être fermée automatiquement, la ressource devait être déclarée dans le bloc `try`.
|
| 157 |
+
|
| 158 |
+
Java 9 simplifie son utilisation en supprimant la déclaration d'une nouvelle variable locale au sein du bloc `try`.
|
| 159 |
+
Vous pouvez donc désormais déclarer une ressource en amont de votre `try-with-resources`, celle-ci sera toujours fermée en sortie de bloc.
|
| 160 |
+
|
| 161 |
+
Avant Java 9 :
|
| 162 |
+
|
| 163 |
+
```java
|
| 164 |
+
... // code
|
| 165 |
+
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
|
| 166 |
+
... // code
|
| 167 |
+
}
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
Avec Java 9 :
|
| 171 |
+
|
| 172 |
+
```java
|
| 173 |
+
BufferedReader br = new BufferedReader(new FileReader("file.txt"));
|
| 174 |
+
... // code
|
| 175 |
+
try (br) {
|
| 176 |
+
... // code
|
| 177 |
+
}
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
#### Inférence de type dans les classes anonymes
|
| 181 |
+
|
| 182 |
+
Depuis Java 9, l'inférence de type avec le diamant (`<>`) dans les classes anonymes est pris en charge.
|
| 183 |
+
Jusqu'à Java 9, nous déclarions une classe anonyme comme suit :
|
| 184 |
+
|
| 185 |
+
```java
|
| 186 |
+
Map<String, List<String>> myMap = new HashMap<String, List<String>>() {
|
| 187 |
+
// Classe anonyme
|
| 188 |
+
};
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
Nous pouvons désormais la modifier en :
|
| 192 |
+
|
| 193 |
+
```java
|
| 194 |
+
Map<String, List<String>> myMap = new HashMap<>() {
|
| 195 |
+
// Classe anonyme
|
| 196 |
+
};
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
L'avantage est relativement faible, mais cette amélioration réduit quand même la verbosité du code, tout en maintenant une inférence de type correcte.
|
| 200 |
+
|
| 201 |
+
### [JEP 248: Make G1 the Default Garbage Collector](https://openjdk.org/jeps/248)
|
| 202 |
+
|
| 203 |
+
Le G1GC, ou "Garbage First Garbage Collector", devient le Garbage Collector par défaut.
|
| 204 |
+
|
| 205 |
+
Contrairement aux autres garbage collectors, G1GC n’est pas "en temps réel". Nous pouvons lui fixer des objectifs de performances spécifiques. Nous pouvons lui demander que les Stop-The-World ne dépassent pas x millisecondes dans un intervalle de temps donné de y millisecondes. Il fera alors de son mieux pour atteindre cet objectif avec une forte probabilité.
|
| 206 |
+
|
| 207 |
+
Pour en savoir plus au sujet du G1GC et des garbage collectors en général, n'hésitez pas à lire nos articles [La Heap Java pour les nuls, mais pas que...](https://www.younup.fr/blog/la-heap-pour-les-nuls-mais-pas-que) et [La Heap Java pour les nuls, mais pas que... Round 2](https://www.younup.fr/blog/la-heap-java-pour-les-nuls-mais-pas-que-round-2).
|
| 208 |
+
|
| 209 |
+
### [JEP 238: Multi-Release JAR Files](https://openjdk.org/jeps/238)
|
| 210 |
+
|
| 211 |
+
Cette fonctionnalité permet de regrouper dans un seul JAR du code spécifique à différentes versions de Java. Cela facilite la compatibilité ascendante des bibliothèques et des frameworks. Par exemple, un JAR multi-releases peut contenir une classe compilée pour Java 8 et une version de cette même classe spécifique pour Java 9 et ultérieur.
|
| 212 |
+
|
| 213 |
+
"Génial !" me direz-vous, mais malgré un avenir très prometteur sur le papier, cette JEP a littéralement fait un flop chez les javaistes. Voyons pourquoi.
|
| 214 |
+
|
| 215 |
+
- La gestion de versions spécifiques dans un seul JAR rend clairement le code plus difficile à maintenir. Il faut s'assurer que les différentes versions de classes fonctionnent correctement ensemble et que les bonnes versions sont utilisées au bon moment.
|
| 216 |
+
Par exemple, si une librairie a des fonctionnalités spécifiques pour Java 9, 10 et 11, mais pas pour Java 8, il devient plus (voire très) compliqué de maintenir le code si un bug est introduit dans l'une des versions spécifiques, car il faut tester et corriger chaque version de la classe à travers différentes versions de Java.
|
| 217 |
+
- La prise en charge dans nos outils de build, comme Maven ou Gradle, ou dans certains de nos environnements d'exécution Java peut être incomplète ou nécessiter des configurations supplémentaires, et donc rendre la configuration du build bien plus complexe.
|
| 218 |
+
- L'ajout de différentes versions de classes pour différentes versions de Java entraîne une certaine redondance dans le code. Certes, ça augmente la taille des fichiers JAR, mais ça peut également avoir un impact sur nos performances, surtout si de nombreuses versions sont incluses pour différentes versions de Java. De plus, si les classes spécifiques à une version de Java sont peu utilisées par notre code, l'ajout de ces versions supplémentaires dans le JAR devient inutile.
|
| 219 |
+
La maintenance de ce code devient aussi plus complexe pour nous, car les modifications doivent être réalisées dans plusieurs versions de la même classe.
|
| 220 |
+
- Si une classe utilise une API spécifique de Java 9, mais qu'elle dépend également d'une API de Java 8 pour certaines opérations, le gérer dans un seul JAR peut devenir très complexe, et rendre les bugs difficiles à localiser et à diagnostiquer, et donc difficiles à reproduire.
|
| 221 |
+
- On ne va pas se le cacher, plutôt que de se fier aux JAR multi-releases, nous préférons tous gérer la compatibilité ascendante de manière explicite via nos chers gestionnaires de dépendances (Maven, Gradle...), où les différentes versions d'une bibliothèque peuvent être configurées explicitement. Ce n'est pas simple, mais c'est gérable.
|
| 222 |
+
- Et pour finir, l'un des principaux points négatifs de cette JEP est son absence d'utilisation par les frameworks et librairies les plus populaires... Si les grands noms des frameworks Java n'ont pas adhéré, ça n'a clairement pas poussé la communauté à s'y intéresser.
|
| 223 |
+
|
| 224 |
+
### [JEP 261: Module System](https://openjdk.org/jeps/261)
|
| 225 |
+
|
| 226 |
+
Son but était d’introduire un système modulaire pour le JDK et de rendre le langage Java modulaire. Cela a permis de structurer la plateforme Java en modules distincts, afin de favoriser une gestion plus fine des dépendances, de réduire la taille des applications et d'améliorer la sécurité et les performances. Des très bons points !
|
| 227 |
+
|
| 228 |
+
Pour modulariser nos modules Java au sein de notre application, chaque module Java est défini par un fichier `module-info.java`, placé à la racine de celui-ci. Ce fichier permet de spécifier le nom du module, ses dépendances (`requires`), et les paquets qu'il expose (`exports`). Assez simple, avouons-le.
|
| 229 |
+
|
| 230 |
+
Cette JEP est, sans aucun doute, la plus importante de Java 9, mais aussi la plus boudée des développeurs. Elle est la JEP clé qui a posé les bases du système de modules Java, et elle fait partie du projet nommé _Jigsaw_.
|
| 231 |
+
|
| 232 |
+
Mais sa complexité, son manque de compatibilité ascendante (librairies tierces non compatibles avec cette modularisation), mais aussi sans doute le fait que nous ayons tous perçus cette évolution comme une surcouche inutile (par rapport à la simplicité d'un monolithe codé en Java 8), ont freiné, voire empêché, son adoption par nous, les javaistes.
|
| 233 |
+
|
| 234 |
+
Depuis, la modularisation monolithique est devenue un vrai sujet. La communauté parle de plus en plus de _Clean Architecture_ (pour en savoir plus, il est recommandé de lire "_Clean Architecture: A Craftman's Guide to Software Structure and Design_", écrit par Robert C. Martin, communément appelé _Oncle Bob_), et même Spring s'est engouffré dans la brèche avec la librairie "_Spring Modulith_" (voir l'article [Spring Modulith ou comment redonner vie à l'Architecture Monolithique](https://www.younup.fr/blog/spring-modulith-ou-comment-redonner-vie-a-larchitecture-monolithique)).
|
| 235 |
+
|
| 236 |
+
### [JEP 295: Ahead-of-Time Compilation](https://openjdk.org/jeps/295)
|
| 237 |
+
|
| 238 |
+
La JEP 295 a été introduite par Java 9 afin de permettre la précompilation de code Java avant son exécution.
|
| 239 |
+
Contrairement à la compilation _Just-In-Time_ (JIT) qui se produit lors de l'exécution du programme (runtime), la compilation AOT permettait de compiler certains morceaux du code Java en code machine avant même que le programme ne soit lancé.
|
| 240 |
+
|
| 241 |
+
L’un des objectifs majeurs de cette JEP, expérimentale en Java 9, était de réduire le temps de démarrage des applications Java en se concentrant sur les classes et les méthodes peu (voire pas) dynamiques, afin de les rendre plus rapides à l'exécution, sans avoir besoin d'attendre que le compilateur _JIT_ effectue ses optimisations.
|
| 242 |
+
Etant donné que la JVM n'avait plus besoin, au runtime, de compiler les classes précompilées par AOT, l'empreinte mémoire était également plus faible.
|
| 243 |
+
|
| 244 |
+
Les résultats n'ont certes pas toujours été au rendez-vous, et la prise en charge des optimisations était encore partielle. Mais cette expérimentation a ouvert la voix à la communauté vers la construction d'images natives afin d'améliorer drastiquement les temps de démarrage et la consommation de ressources, CPU et RAM.
|
| 245 |
+
|
| 246 |
+
La JEP 483 fera partie de Java 24. Elle introduira une nouvelle approche pour le chargement et le linking des classes AOT. Contrairement à la JEP 295, elle ne reposera pas sur _jaotc_ mais sur un système basé sur CDS. Cette évolution visera à réduire les temps de démarrage en liant et chargeant les classes de manière prédictive pendant une phase de « training » avant exécution.
|
| 247 |
+
|
| 248 |
+
### [JEP 102: Process API Updates](https://openjdk.org/jeps/102)
|
| 249 |
+
|
| 250 |
+
A travers cette JEP, Java 9 a apporté des améliorations à l'API _Process_, afin de mieux interagir avec les processus système, avec des méthodes pour obtenir l’identifiant du processus (PID), surveiller l’état, gérer les I/O de manière non bloquante, etc.
|
| 251 |
+
|
| 252 |
+
```java
|
| 253 |
+
// Crée un processus externe (par exemple, un simple "ping" à google)
|
| 254 |
+
ProcessBuilder processBuilder = new ProcessBuilder("ping", "-c", "4", "google.com");
|
| 255 |
+
Process process = processBuilder.start();
|
| 256 |
+
|
| 257 |
+
// Obtenir et tracer l'ID du processus
|
| 258 |
+
long pid = process.pid();
|
| 259 |
+
System.out.println("ID du processus (PID) : " + pid);
|
| 260 |
+
|
| 261 |
+
// Utilise onExit() pour obtenir un retour lorsqu'il se termine
|
| 262 |
+
process.onExit().thenAccept(p -> {
|
| 263 |
+
try {
|
| 264 |
+
// Vérifie le code de sortie du processus
|
| 265 |
+
int exitCode = p.exitValue();
|
| 266 |
+
if (exitCode == 0) {
|
| 267 |
+
System.out.println("Le processus s'est terminé avec succès !");
|
| 268 |
+
} else {
|
| 269 |
+
System.out.println("Le processus a échoué avec le code de sortie : " + exitCode);
|
| 270 |
+
}
|
| 271 |
+
} catch (Exception e) {
|
| 272 |
+
... // Traitement de l'exception
|
| 273 |
+
}
|
| 274 |
+
});
|
| 275 |
+
|
| 276 |
+
// code exécuté en parallèle du "ping"
|
| 277 |
+
```
|
| 278 |
+
|
| 279 |
+
La récupération du PID peut s'avérer utile si vous devez surveiller ou gérer un processus au niveau système, pour tuer le processus (en cas de freeze ou d'incident), ou pour un suivi dans des logs et/ou pour calculer des métriques (de performances).
|
| 280 |
+
|
| 281 |
+
Contrairement à `process.waitFor()`, bloquant le thread principal jusqu'à ce que le processus se termine, l'utilisation de `onExit()` permet au programme de rester réactif et d'exécuter d'autres tâches pendant que le processus externe est en cours.
|
| 282 |
+
|
| 283 |
+
### [JEP 222: JShell](https://openjdk.org/jeps/222)
|
| 284 |
+
|
| 285 |
+
JShell est un REPL (Read-Eval-Print Loop) qui permet d’exécuter du code Java ligne par ligne, sans avoir besoin de compiler des classes entières. C’est un excellent outil pour tester du code rapidement, explorer des APIs ou apprendre Java.
|
| 286 |
+
|
| 287 |
+
```java
|
| 288 |
+
jshell>
|
| 289 |
+
int a = 10;
|
| 290 |
+
jshell>System.out.
|
| 291 |
+
|
| 292 |
+
println(a +5);
|
| 293 |
+
```
|
| 294 |
+
|
| 295 |
+
### [JEP 259: Stack-Walking API](https://openjdk.org/jeps/259)
|
| 296 |
+
|
| 297 |
+
Avant cette JEP, les méthodes existantes, comme `Thread.currentThread().getStackTrace()`, étaient coûteuses en termes de performance, car elles renvoyaient toujours une copie complète du tableau de _StackTraceElement_. Même si vous ne vouliez qu'une partie de la pile, vous étiez obligé d'obtenir et de parcourir tout le tableau.
|
| 298 |
+
Il n'y avait aucun moyen facile de filtrer les données sans parcourir manuellement tout le tableau.
|
| 299 |
+
L'accès était limité aux informations statiques de chaque élément, comme le fichier, le numéro de ligne, le nom de la méthode, mais pas à des informations dynamiques ou spécifiques au contexte.
|
| 300 |
+
|
| 301 |
+
Cette nouvelle API permet donc de parcourir et d'interagir avec la pile d’appels de manière plus flexible et performante. Elle expose un Stream, que vous pouvez filtrer ou transformer facilement grâce aux outils de la programmation fonctionnelle (filter, map...).
|
| 302 |
+
|
| 303 |
+
```java
|
| 304 |
+
StackWalker walker = StackWalker.getInstance();
|
| 305 |
+
walker.walk(stream ->
|
| 306 |
+
stream.filter(frame -> frame.getClassName().contains("MyClass"))
|
| 307 |
+
.forEach(System.out::println));
|
| 308 |
+
```
|
| 309 |
+
|
| 310 |
+
## Hors JEP
|
| 311 |
+
|
| 312 |
+
### Amélioration de Optional
|
| 313 |
+
|
| 314 |
+
Java 9 a ajouté des méthodes à `Optional` pour rendre le code plus concis :
|
| 315 |
+
|
| 316 |
+
- `ifPresentOrElse` exécute une action si une valeur est présente, sinon il exécute une action différente
|
| 317 |
+
|
| 318 |
+
```java
|
| 319 |
+
Optional<String> name = Optional.of("Cédric");
|
| 320 |
+
name.ifPresentOrElse( System.out::println, () -> System.out.println("No value")); // ici affiche Cédric
|
| 321 |
+
```
|
| 322 |
+
|
| 323 |
+
- `or` fournit un autre "Optional" si le premier est vide,
|
| 324 |
+
- `stream` transforme "Optional" en un "Stream",
|
| 325 |
+
- `orElseThrow` sans paramètre permet de lever une exception de type `NoSuchElementException` si aucune valeur n'est présente.
|
| 326 |
+
|
| 327 |
+
Les nouvelles méthodes de Optional, comme ifPresentOrElse, rendent le code plus lisible et fluide, mais il est bon de noter qu'elles peuvent introduire une légère surcharge en raison de la gestion des objets encapsulés. Pour des cas critiques en performance, des alternatives plus simples peuvent être préférables : une vérification conditionnelle directe, avec _null_, sera en effet plus rapide.
|
| 328 |
+
|
| 329 |
+
### Enrichissement de l'API Stream
|
| 330 |
+
|
| 331 |
+
L’API _Stream_ a été enrichie avec de nouvelles méthodes utiles.
|
| 332 |
+
|
| 333 |
+
- `takeWhile` et `dropWhile` permettent de limiter les éléments d’un flux en fonction d'une condition :
|
| 334 |
+
|
| 335 |
+
```java
|
| 336 |
+
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
|
| 337 |
+
numbers.stream()
|
| 338 |
+
.takeWhile(n -> n < 4)
|
| 339 |
+
.forEach(System.out::println); // Affiche 1, 2 et 3
|
| 340 |
+
```
|
| 341 |
+
|
| 342 |
+
```java
|
| 343 |
+
List<Integer> numbers = List.of(1, 3, 7, 12, 8, 5, 15);
|
| 344 |
+
List<Integer> result = numbers.stream()
|
| 345 |
+
.dropWhile(n -> n < 10)
|
| 346 |
+
.toList(); // result contient 12, 8, 5 et 15
|
| 347 |
+
```
|
| 348 |
+
|
| 349 |
+
- `iterate` avec condition, génère un flux fini ou infini avec une condition :
|
| 350 |
+
|
| 351 |
+
```java
|
| 352 |
+
List<Integer> evenNumbers = Stream.iterate(0, n -> n <= 10, n -> n + 2)
|
| 353 |
+
.toList(); // evenNumbers contient 0, 2, 4, 6, 8 et 10
|
| 354 |
+
```
|
| 355 |
+
|
| 356 |
+
- `ofNullable` crée un flux d'un élément ou un flux vide si l'élément est null :
|
| 357 |
+
|
| 358 |
+
```java
|
| 359 |
+
String nullableString = null;
|
| 360 |
+
Stream<String> stream = Stream.ofNullable(nullableString); // stream est vide
|
| 361 |
+
|
| 362 |
+
nullableString = "Hello, World!";
|
| 363 |
+
stream = Stream.ofNullable(nullableString); // stream contient "Hello, World!"
|
| 364 |
+
```
|
| 365 |
+
|
| 366 |
+
## Conclusion
|
| 367 |
+
|
| 368 |
+
Certaines améliorations apportées par Java 9, bien que souvent discrètes, simplifient le code, augmentent les performances, et offrent des possibilités modernes pour gérer les flux de données, les processus, et la concaténation de chaînes.
|
| 369 |
+
Comme vous avez pu le remarquer, ces JEP sont nombreuses, et parfois même, vous les utilisez sans savoir qu'elles sont apparues avec Java 9.
|
| 370 |
+
Elles méritent quasiment toutes d’être explorées, car beaucoup d'entre elles enrichissent le langage tout en réduisant le besoin d’écrire du code supplémentaire.
|
| 371 |
+
|
| 372 |
+
Le fait que Java 9 a été boudé par la communauté, celle-ci préférant migrer ses applications de Java 8 vers Java 11 ou Java 17, est principalement dû au fait que les JEP 261 (Modularisation) et 238 (Jar Multi-Releases) ont été les plus mises en avant, et considérées comme inutiles ou trop complexes par les développeurs.
|
| 373 |
+
|
| 374 |
+
Pour finir, vous avez pu découvrir que cette version de Java apporta de nombreuses JEP très précieuses, qui continuent de vivre, comme la "Concaténation de Strings", le "Compact Strings", le G1GC par défaut, l'API "StackWalker"...
|
| 375 |
+
La JEP expérimentale "Compilation AOT" a été abandonnée, mais ses principes ont influencé des technologies modernes comme CDS, GraalVM et la future JEP 483, visant à réduire la consommation des ressources et à améliorer les performances.
|
articles/Les differents Patterns de Transactions Distribuees.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Les différents Patterns de Transactions Distribuées
|
| 2 |
+
|
| 3 |
+
Depuis quelques années, les architectures microservices et les systèmes distribués se multiplient, devenant une norme. Mais chaque solution, parfaite sur le papier, apporte son lot de difficultés et de complexité, comme par exemple la gestion des transactions distribuées, c'est-à-dire à travers de multiples services et bases de données.
|
| 4 |
+
|
| 5 |
+
Les microservices, c'est top me direz-vous ! Les applications sont isolées, autonomes et gèrent leurs propres données... Ok, mais si chaque service fonctionne de manière autonome, avec ses propres ressources et bases de données, ayez bien à l'esprit qu'il est généralement essentiel que les données et les opérations restent cohérentes à travers l'ensemble du système. La gestion de la cohérence des données, surtout lorsqu'elles traversent plusieurs briques logicielles, entraîne de vrais défis, en particulier en termes de **cohérence**, de **fiabilité** et bien évidemment de **résilience**.
|
| 6 |
+
|
| 7 |
+
C'est là qu'interviennent les **patterns de transactions distribuées**. Ceux-ci offrent des solutions élégantes pour gérer les transactions et les erreurs dans des systèmes où plusieurs services ou bases de données sont impliqués. Parmi les plus populaires, on trouve _Two-Phase Commit_ (2PC), _Three-Phase Commit_ (3PC), _Saga_, et d'autres approches permettant de garantir la consistance éventuelle, tout en assurant une gestion fine des erreurs et des "compensations" (rollback ou presque...).
|
| 8 |
+
|
| 9 |
+
Dans cet article, j'explorerai en détail ces différents patterns, en mettant en lumière leurs avantages, les défis que représente leur mise en œuvre et des exemples d'applications concrètes. Nous verrons également comment ces approches s'intègrent dans des systèmes modernes utilisant des message brokers et des architectures asynchrones pour améliorer la résilience et la scalabilité des applications distribuées.
|
| 10 |
+
|
| 11 |
+
## Définitions et Rappels
|
| 12 |
+
|
| 13 |
+
Avant d'aborder les différents patterns, (re)voyons ensemble les différentes notions liées aux transactions.
|
| 14 |
+
|
| 15 |
+
### Les Transactions
|
| 16 |
+
|
| 17 |
+
Une **transaction** est _une séquence d'opérations traitées comme une unité indivisible. Elle garantit que les opérations qu’elle englobe sont soit entièrement exécutées, soit entièrement annulées. Si une transaction échoue à n'importe quel moment, toutes ses opérations doivent être annulées, afin de laisser le système dans un état cohérent._
|
| 18 |
+
|
| 19 |
+
Le modèle **ACID**, ça vous parle ? C'est tout à fait ça. Une transaction doit répondre à 4 règles clés :
|
| 20 |
+
|
| 21 |
+
- **A**tomicité : _Soit toutes les opérations de la transaction sont exécutées avec succès, soit aucune d’entre elles n’est exécutée._
|
| 22 |
+
Cette propriété garantit que la transaction est une unité dite indivisible.
|
| 23 |
+
- **C**ohérence : _la base de données passe d’un état cohérent à un autre état cohérent après une transaction_.
|
| 24 |
+
Pour simplifier, une transaction transforme les données de manière à respecter les règles et contraintes définies sur la base de données (clés primaires, contraintes d'intégrité, d'unicité...).
|
| 25 |
+
- **I**solation : _Les transactions doivent être exécutées de manière isolée les unes des autres._
|
| 26 |
+
Les opérations d'une transaction ne doivent pas être visibles aux autres transactions tant qu’elles ne sont pas terminées.
|
| 27 |
+
- **D**urabilité : dès qu’une transaction est validée (_commit_), les changements apportés par celle-ci doivent être permanents, même en cas de défaillance du système.
|
| 28 |
+
|
| 29 |
+
Sur certaines bases de données SQL, la définition de l'isolation n'est pas forcément vraie, tout dépend du niveau d'isolation des transactions que vous avez choisi parmi les 4 généralement disponibles :
|
| 30 |
+
|
| 31 |
+
- **Read UnCommitted** ne respecte pas complètement l'isolation
|
| 32 |
+
- **Read Committed** respecte partiellement l'isolation
|
| 33 |
+
- **Repeatable Read** respecte quasi-totalement l'isolation
|
| 34 |
+
- **Serializable** respecte complètement l'isolation
|
| 35 |
+
|
| 36 |
+
> Pour en savoir plus, je vous invite à lire cette [page](<https://en.wikipedia.org/wiki/Isolation_(database_systems)>) issue de Wikipédia.
|
| 37 |
+
|
| 38 |
+
### Les Transactions Distribuées
|
| 39 |
+
|
| 40 |
+
Une transaction distribuée implique plusieurs systèmes ou bases de données répartis sur plusieurs nœuds, serveurs ou même sites géographiques. Contrairement à une transaction classique, qui concerne une seule base de données, une transaction distribuée _doit garantir la cohérence et l'intégrité des données à travers plusieurs systèmes indépendants, voire hétérogènes_.
|
| 41 |
+
|
| 42 |
+
Dans un contexte distribué, _une transaction doit être capable de s’étendre sur plusieurs ressources, et chaque ressource doit participer à l’opération transactionnelle_. Par exemple, un système pourrait impliquer plusieurs bases de données ou services web qui doivent collaborer dans le cadre d'une seule transaction. L'objectif reste de maintenir les propriétés **ACID** dans un environnement réparti, ce qui est clairement plus complexe que dans un environnement monolithique.
|
| 43 |
+
|
| 44 |
+
Les transactions distribuées présentent **plusieurs défis de taille** par rapport aux transactions classiques :
|
| 45 |
+
|
| 46 |
+
- La **Coordination** : coordonner différentes bases de données et services est complexe. Par exemple, un service peut réussir à effectuer une opération alors qu'un autre service échoue. Comment gérer ce genre de cas ?
|
| 47 |
+
- La **Fiabilité et la Résilience** : les pannes peuvent se produire à tout moment dans un système distribué. Cela nécessite donc des mécanismes robustes pour gérer l’intégrité de la transaction.
|
| 48 |
+
- La **Latence** : les communications entre différents systèmes répartis peuvent entraîner des latences importantes. Les transactions doivent être conçues de manière à minimiser les délais trop longs.
|
| 49 |
+
|
| 50 |
+
> **A noter** : les bases de données NoSQL, souvent conçues pour des systèmes distribués, sacrifient certaines propriétés ACID pour garantir des performances élevées et la haute-disponibilité.
|
| 51 |
+
|
| 52 |
+
L'exemple auquel tout le monde pense assez logiquement en lisant ces objectifs, est une application de _e-commerce_ ayant par exemple 4 services : la commande, la gestion des stocks, le paiement et la livraison.
|
| 53 |
+
Chacun de ces services peut être sur un serveur différent et utiliser une base de données différente.
|
| 54 |
+
La transaction doit garantir que si l’un de ces services échoue (par exemple, le paiement échoue), toute l’opération - réservation du stock et enregistrement de la commande - doit être annulée, assurant ainsi une cohérence globale.
|
| 55 |
+
Il est évident que dans un tel système, la notion d'**Atomicité** au sens propre du terme est perdue. Ici on parle alors de **cohérence à terme** par opposition à la **cohérence immédiate**.
|
| 56 |
+
|
| 57 |
+
Maintenant, je suppose que vous vous rendez compte de la difficulté de tels systèmes distribués. Comment les rendre coordonnés, fiables, résilients et rapides ? Pour ça, il existe plusieurs patterns de coordination, tous assez différents, tous ayant leurs avantages et leurs inconvénients. Ouf ! 😮💨
|
| 58 |
+
|
| 59 |
+
## Les Patterns de Transactions Distribuées
|
| 60 |
+
|
| 61 |
+
Gardez en tête l'exemple d'un système applicatif d'e-commerce, c'est celui-ci qui servira à illustrer les différents patterns.
|
| 62 |
+
|
| 63 |
+
### 2PC (Two-Phase Commit)
|
| 64 |
+
|
| 65 |
+
Ce pattern est utilisé pour garantir l'atomicité des transactions distribuées en deux phases.
|
| 66 |
+
|
| 67 |
+
- **Phase de préparation** :
|
| 68 |
+
- Le coordinateur demande à tous les participants s'ils peuvent valider la transaction.
|
| 69 |
+
- Chaque participant répond avec un vote (prêt ou échec).
|
| 70 |
+
- Si un participant répond "échec" lors de la phase de préparation, le coordinateur envoie une commande d'annulation à tous les participants qui ont répondu "prêt".
|
| 71 |
+
- Si tous les participants sont prêts, le coordinateur passe à la phase suivante.
|
| 72 |
+
- **Phase de validation** :
|
| 73 |
+
- Si tous les participants répondent qu'ils sont prêts, le coordinateur envoie une commande de validation à tous les participants.
|
| 74 |
+
- Si un participant vote pour l'échec, le coordinateur envoie une commande d'annulation.
|
| 75 |
+
|
| 76 |
+
"_Mais qui est ce coordinateur ?_" me direz-vous, et vous avez raison, je n'en ai pas encore parlé. Le coordinateur va dépendre du pattern de transactions distribuées utilisé.
|
| 77 |
+
Pour **2PC**, il s'agira généralement d'un module/composant indépendant afin d'assurer la séparation des préoccupations (_separation of concerns_) au sein du système.
|
| 78 |
+
"_Quel est son rôle ?_", simplement de maintenir le cycle de vie de la transaction, en appelant les différents modules concernés lors des deux phases. Si, à un moment donné, un module ne se prépare pas, ou si un module ne valide pas la transaction, le coordinateur est en charge d'interrompre la transaction et il commence alors la phase de compensation (_rollback_). Il est plus communément appelé "_Transaction Manager_".
|
| 79 |
+
|
| 80 |
+
Le schéma ci-dessous représente les différents use-cases de 2PC dans un système e-commerce.
|
| 81 |
+
|
| 82 |
+

|
| 83 |
+
|
| 84 |
+
**Avantages** :
|
| 85 |
+
|
| 86 |
+
- 2PC est un protocole de cohérence très fort.
|
| 87 |
+
Tout d'abord, les phases de préparation et de validation garantissent que la transaction est atomique. La transaction se terminera soit par un retour réussi de tous les modules, soit par une absence de modification de tous les modules.
|
| 88 |
+
- 2PC permet l'isolation lecture-écriture.
|
| 89 |
+
Cela signifie que les modifications apportées à une entité ne sont pas visibles tant que le coordinateur ne les a pas validées.
|
| 90 |
+
|
| 91 |
+
**Inconvénients** :
|
| 92 |
+
|
| 93 |
+
- Bien que 2PC résolve le problème d'atomicité, il n'est pas vraiment recommandé pour de nombreux systèmes parce qu'il est synchrone, et donc bloquant.
|
| 94 |
+
Le protocole doit verrouiller l'objet qui sera modifié avant que la transaction ne se termine.
|
| 95 |
+
Si un objet "préparé" venait à être modifié, la phase de validation pourrait ne pas fonctionner.
|
| 96 |
+
- Les pannes réseau, ou d'un participant, peuvent entraîner des verrouillages prolongés.
|
| 97 |
+
|
| 98 |
+
Pour résumer, 2PC est un choix cohérent pour les systèmes nécessitant une **cohérence stricte**, il respecte le principe d'isolation des transactions, mais il n'est pas adapté aux systèmes à grande échelle ou à forte disponibilité, où les performances sont critiques.
|
| 99 |
+
|
| 100 |
+
### 3PC (Three-Phase Commit)
|
| 101 |
+
|
| 102 |
+
C'est une extension du 2PC avec une phase supplémentaire pour réduire les risques de blocage.
|
| 103 |
+
|
| 104 |
+
- **Phase de canCommit** :
|
| 105 |
+
- Le coordinateur demande si les participants peuvent préparer la transaction.
|
| 106 |
+
- Si un participant répond "échec" à cette étape, la transaction est annulée immédiatement.
|
| 107 |
+
- **Phase de preCommit** :
|
| 108 |
+
- Si tous les participants répondent "oui", le coordinateur envoie une pré-commande de validation.
|
| 109 |
+
- Si un participant ou le coordinateur échoue après cette phase, les participants passent en état de "_préparation à annuler_" ou à "_valider_" en fonction du dernier message reçu.
|
| 110 |
+
- Si une erreur survient, une commande d'annulation est envoyée.
|
| 111 |
+
- **Phase de doCommit** :
|
| 112 |
+
- Le coordinateur envoie la commande finale de validation.
|
| 113 |
+
- Si une erreur se produit ici, une commande d'annulation est envoyée.
|
| 114 |
+
|
| 115 |
+
Tout comme pour 2PC, le coordinateur ici est un module/composant indépendant, appelé "Transaction Manager".
|
| 116 |
+
|
| 117 |
+

|
| 118 |
+
|
| 119 |
+
**Avantages** :
|
| 120 |
+
|
| 121 |
+
- Contrairement à 2PC, 3PC introduit une phase supplémentaire (_CanCommit_), qui permet aux participants de confirmer s’ils peuvent effectuer une transaction avant de passer à la phase de préparation.
|
| 122 |
+
- En cas de panne réseau ou de défaillance du coordinateur, les participants peuvent prendre une décision autonome basée sur l'état de la transaction.
|
| 123 |
+
- 3PC est conçu pour minimiser les risques de blocage total du système, le rendant plus tolérant aux pannes dans des environnements distribués.
|
| 124 |
+
- Contrairement à 2PC, les participants peuvent revenir à un état cohérent sans attendre indéfiniment une réponse du coordinateur.
|
| 125 |
+
|
| 126 |
+
**Inconvénients** :
|
| 127 |
+
|
| 128 |
+
- 3PC est plus complexe que 2PC et il implique plus d'étapes de communication, introduisant donc plus de latence. Il est donc moins, voire peu, performant.
|
| 129 |
+
|
| 130 |
+
Pour résumer, en raison de sa complexité et de ses exigences, 3PC est rarement utilisé en pratique. Les systèmes modernes préfèrent des alternatives comme **SAGA** ou des approches basées sur la **cohérence éventuelle**.
|
| 131 |
+
|
| 132 |
+
> **A noter** : que ce soit 2PC ou 3PC, l'utilisation d'un message broker est inutile car les échanges sont gérés via les protocoles réseau (HTTP, TCP/IP...).
|
| 133 |
+
|
| 134 |
+
### SAGA
|
| 135 |
+
|
| 136 |
+
Le pattern **Saga** est sans doute le plus connu de tous. Il divise une transaction longue en une série de transactions plus petites, chacune avec une action compensatoire pour annuler l'effet en cas d'échec.
|
| 137 |
+
|
| 138 |
+
- Chaque étape est une transaction locale.
|
| 139 |
+
- Si une étape échoue, une série de transactions compensatoires est exécutée pour annuler les étapes précédentes (_rollback_).
|
| 140 |
+
|
| 141 |
+
Il y a deux manières de mettre en œuvre le pattern _Saga_ : par **Choreography** ou par **Orchestration**. Ces deux approches définissent comment les différents services s’organisent pour gérer "la Saga".
|
| 142 |
+
|
| 143 |
+
#### Saga Orchestration
|
| 144 |
+
|
| 145 |
+
Dans le modèle **Orchestration**, un coordinateur central (ou _orchestrateur_) prend en charge la gestion de "la saga".
|
| 146 |
+
Ce dernier appelle les services dans un ordre précis et attend les résultats de chaque étape avant de passer à la suivante.
|
| 147 |
+
Si une étape échoue, le coordinateur décide de lancer les actions de compensation pour les étapes précédentes.
|
| 148 |
+
|
| 149 |
+
Dans notre exemple d'e-commerce, l'orchestrateur gère la réservation, le paiement, et la livraison en appelant les services dans un ordre précis, en attendant les réponses de chaque service et en lançant des compensations si nécessaire.
|
| 150 |
+
|
| 151 |
+

|
| 152 |
+
|
| 153 |
+
**Avantages** :
|
| 154 |
+
|
| 155 |
+
- Conçu pour les environnements microservices où chaque service possède son propre domaine de responsabilité et sa propre base de données.
|
| 156 |
+
- La présence d'un orchestrateur centralisé simplifie la coordination inter-services.
|
| 157 |
+
- Contraiement à 2PC, "Saga Orchestration" est **non bloquant** car chaque étape de la Saga est indépendante.
|
| 158 |
+
- Il est capable de gérer les échecs partiels. L'orchestrateur peut appliquer des actions compensatoires en cas d'échec d'une étape, permettant de maintenir une **cohérence éventuelle**.
|
| 159 |
+
- L'orchestrateur a une vue complète de l'état de la saga, ce qui facilite la supervision et la détection des échecs.
|
| 160 |
+
|
| 161 |
+
**Inconvénients** :
|
| 162 |
+
|
| 163 |
+
- **Complexité accrue** dans la gestion des transactions compensatoires.
|
| 164 |
+
Écrire une logique de compensation pour chaque étape n’est pas trivial. Contrairement à un simple "_Ctrl+Z_" qui annule la dernière action de manière isolée, les transactions distribuées impliquent plusieurs services interconnectés, chacun ayant ses propres contraintes et effets secondaires. Il faut donc anticiper des cas comme l’annulation d’un paiement après l’expédition d’un produit, ou encore gérer des erreurs de compensation pour éviter des boucles infinies. 🤯
|
| 165 |
+
Des frameworks comme _Temporal.io_, _Camunda_, _Axon Framework_ ou encore _Spring Cloud Data Flow_ peuvent vous aider car ils permettent d'orchestrer des workflows transactionnels, tout en ayant un mécanisme de compensation.
|
| 166 |
+
- Non adapté pour toutes les opérations nécessitant une stricte atomicité. Ici, on parle de **cohérence éventuelle**.
|
| 167 |
+
- Si l'orchestrateur tombe en panne, le système entier peut être impacté. Sa résilience doit être au coeur des préoccupations lors de sa conception.
|
| 168 |
+
- Une **latence accrue**, l'orchestrateur introduisant une couche supplémentaire de communications et de traitements.
|
| 169 |
+
|
| 170 |
+
Pour résumer, le modèle **Orchestration** du pattern Saga est pensé pour une architecture Microservices, mais la cohérence reste **éventuelle** (non applicable au domaine bancaire par exemple) et l'orchestrateur, étant perçu comme un **SPOF** (_Single Point Of Failure_), doit être résilient.
|
| 171 |
+
|
| 172 |
+
#### Saga Choreography
|
| 173 |
+
|
| 174 |
+
Contrairement au modèle _Orchestration_, il n'y a pas de coordinateur central. Chaque service qui participe, connaît les autres services et réagit de manière autonome aux événements déclenchés par ces derniers.
|
| 175 |
+
|
| 176 |
+
Les services échangent des événements ou des messages et chacun est responsable de sa propre logique de compensation en cas d’échec.
|
| 177 |
+
|
| 178 |
+
Dans notre système e-commerce, chaque service réagit à des événements comme par exemple "_Paiement confirmé_" ou "_Réservation annulée_", sans qu'il y ait pour autant un coordinateur central pour orchestrer l'ensemble.
|
| 179 |
+
|
| 180 |
+
Avec ce modèle, un _message broker_ (Kafka, RabbitMQ...) est souvent utilisé, voire recommandé, pour publier et souscrire aux événements de chaque étape. Mais, fonction de vos appétences, ajouter un message broker à votre système est soit un avantage, soit un inconvénient 😊
|
| 181 |
+
|
| 182 |
+
Dans le schéma suivant, je n'ai pas illustré le message broker afin d'en simplifier sa compéhension, vous devez juste imaginer que chaque échange passe par celui-ci.
|
| 183 |
+
|
| 184 |
+

|
| 185 |
+
|
| 186 |
+
**Avantages** :
|
| 187 |
+
|
| 188 |
+
- Contrairement à la Saga de type Orchestration, chaque service est **responsable de son rôle**, réduisant la dépendance à un orchestrateur centralisé et supprimant ainsi le principal _SPOF_ du système applicatif.
|
| 189 |
+
- Chaque service publie des événements et réagit aux événements d'autres services, ce qui favorise la **scalabilité** et l'**indépendance** des composants du système (voire des équipes).
|
| 190 |
+
- La méthode Choregraphy facilite l'ajout de nouveaux services sans avoir à modifier un orchestrateur centralisé.
|
| 191 |
+
- Les services ne dépendent que des événements qu'ils consomment ou émettent, ce qui permet une **flexibilité accrue** de leur évolution.
|
| 192 |
+
|
| 193 |
+
**Inconvénients** :
|
| 194 |
+
|
| 195 |
+
- L'absence d'une vue centralisée rend **difficile la supervision** et donc la détection d'échecs, en particulier dans des systèmes complexes.
|
| 196 |
+
- Les services doivent connaître le **format des événements** et leur signification .
|
| 197 |
+
> 💡**Tips** : faites en sorte que les modifications des contrats d'interfaces soient rétrocompatibles.
|
| 198 |
+
|
| 199 |
+
Au final, la méthode **Choregraphy** permet un couplage plus faible qu'avec _Orchestration_.
|
| 200 |
+
Elle est plus adaptée à une application où de nouveaux services peuvent apparaitre et disparaitre.
|
| 201 |
+
Toutefois, veillez à bien penser les étapes compensatoires de chaque module afin d'éviter les échecs en cascade.
|
| 202 |
+
|
| 203 |
+
Vous l'aurez compris, quel que soit le modèle choisi, le pattern **Saga** se distingue des patterns classiques comme 2PC par sa capacité à garantir une **cohérence éventuelle** (donc non ACID) tout en évitant les blocages de ressources.
|
| 204 |
+
|
| 205 |
+
Chaque étape d'une transaction est indépendante et je vous invite à l'accompagner d'une action compensatoire en cas d'échec.
|
| 206 |
+
|
| 207 |
+
### TCC (Try-Confirm/Cancel)
|
| 208 |
+
|
| 209 |
+
**TCC** est un pattern pour les transactions distribuées qui consiste en trois étapes :
|
| 210 |
+
|
| 211 |
+
- **Try** : réserve les ressources nécessaires.
|
| 212 |
+
Si une réservation échoue, la transaction est annulée immédiatement.
|
| 213 |
+
- **Confirm** : Une fois que les ressources ont été réservées avec succès, la deuxième étape consiste à finaliser la transaction. À ce stade, la transaction est considérée comme terminée et les modifications sont appliquées de manière permanente.
|
| 214 |
+
Si une erreur survient après cette étape, la transaction ne peut pas être annulée, car elle est déjà validée.
|
| 215 |
+
- **Cancel** : Si une erreur se produit après la réservation des ressources, mais avant leur confirmation, une commande d'annulation est envoyée pour libérer les ressources réservées.
|
| 216 |
+
Cela permet de revenir en arrière de manière sûre et de rétablir l'état du système avant la tentative de transaction.
|
| 217 |
+
|
| 218 |
+

|
| 219 |
+
|
| 220 |
+
**Avantages** :
|
| 221 |
+
|
| 222 |
+
- **Flexibilité accrue** : la phase de réservation permet de réserver les ressources de manière temporaire et non bloquante, et seules les étapes de confirmation ou d'annulation prennent effet de manière définitive.
|
| 223 |
+
Il est donc plus flexible que le 2PC qui utilise un verrouillage strict des ressources, et ce, pendant l'ensemble du processus.
|
| 224 |
+
- **Isolation des étapes** : TCC garantit que les étapes de la transaction sont bien isolées, ce qui permet une gestion plus fiable des ressources dans un environnement distribué.
|
| 225 |
+
- **Compensation en cas d'échec** : TCC offre une compensation en cas d'échec (via l'annulation). Si quelque chose échoue après la phase de réservation, les ressources peuvent être libérées rapidement, ce qui minimise l'impact sur le système.
|
| 226 |
+
- **Plus flexible que 2PC** : chaque service gère indépendamment sa phase de validation ou d'annulation, alors que 2PC impose un verrouillage global et un coordinateur pour la gestion de la transaction.
|
| 227 |
+
|
| 228 |
+
**Inconvénients** :
|
| 229 |
+
|
| 230 |
+
- **Gestion complexe des compensations** : la gestion des réservations et des annulations peut devenir complexe car chaque service doit être capable de gérer les compensations de manière fiable et cohérente.
|
| 231 |
+
- **Pas d'atomicité globale** : bien que chaque étape soit atomique au sein d'un service donné, les différentes étapes de la transaction peuvent échouer de manière indépendante, nécessitant une gestion des compensations pour maintenir la cohérence des données à travers les services.
|
| 232 |
+
- **Complexité accrue** :
|
| 233 |
+
- un grand nombre de services doit être capable de gérer l'état de la transaction et de communiquer efficacement pour garantir que les annulations et confirmations se produisent au bon moment.
|
| 234 |
+
- des scénarios complexes, tels que des échecs simultanés dans différents services ou des situations où la gestion des compensations devient impossible, peuvent entraîner des états incohérents.
|
| 235 |
+
|
| 236 |
+
En fin de compte, le pattern **TCC** est une solution efficace pour gérer les transactions distribuées.
|
| 237 |
+
Il permet de réserver les ressources de manière temporaire, puis de confirmer ou annuler la transaction selon les résultats. Ce pattern est plus flexible que le 2PC car il n'impose pas un verrouillage global des ressources.
|
| 238 |
+
Cependant, sa mise en œuvre peut être complexe, surtout quand il y a beaucoup de services impliqués, car chacun d'eux doit gérer l'annulation ou la confirmation de manière fiable.
|
| 239 |
+
|
| 240 |
+
## Patterns de fiabilité et de cohérence
|
| 241 |
+
|
| 242 |
+
Nous avons vu les patterns de transactions distribuées **2PC**, **3PC**, **SAGA** et **TCC**.
|
| 243 |
+
Ces derniers privilégient la gestion de l'**atomicité** et de la **cohérence des transactions** réparties sur plusieurs services et bases de données.
|
| 244 |
+
Ils garantissent ainsi que l'ensemble de la transaction soit entièrement complété avec succès, ou complètement annulé en cas d'échec.
|
| 245 |
+
|
| 246 |
+
Cependant, il existe d'autres types de patterns qui ne visent pas une atomicité globale. N'ayant trouvé aucun terme pour qualifier cette catégorie de patterns, je les qualifierai de "**Patterns de fiabilité et cohérence**".
|
| 247 |
+
Ces approches se concentrent principalement sur la gestion des incohérences temporaires et la fiabilité des systèmes distribués. Les deux principaux patterns de cette catégorie sont **Transactional Outbox** et **Eventual Consistency**.
|
| 248 |
+
|
| 249 |
+
### Transactional Outbox
|
| 250 |
+
|
| 251 |
+
Méconnu, le pattern **Transactional Outbox** (que nous pourrions traduire par "_Boite aux lettres transactionnelle_") garantit que les mises à jour dans une base de données et les messages envoyés à un messages broker se produisent de manière atomique. On parle alors **d'atomicité locale**. La base de données garantit ainsi la cohérence transactionelle et le message broker gère la distribution asynchrone.
|
| 252 |
+
|
| 253 |
+
Dans un système d'e-commerce :
|
| 254 |
+
|
| 255 |
+
- Le service de commande crée une commande en base de données et écrit un message dans l'_outbox_ (table en base de données), et ce **dans la même transaction**.
|
| 256 |
+
- Si une transaction échoue avant de mettre à jour la base de données et l'écriture dans l'outbox, la transaction entière est annulée, y compris l'écriture de l'outbox.
|
| 257 |
+
- Un service dit "_Message Relay_" scanne régulièrement la table _outbox_ pour y trouver les messages non encore envoyés.
|
| 258 |
+
- Ces messages sont ensuite publiés dans un messages broker pour diffusion aux services consommateurs (stock, paiement, livraison).
|
| 259 |
+
- Si la transaction réussit mais l'envoi des messages échoue, les messages restent dans l'outbox jusqu'à ce qu'ils soient ré-envoyés avec succès.
|
| 260 |
+
- Une fois un message envoyé avec succès, il est marqué comme traité ou supprimé de l’outbox.
|
| 261 |
+
|
| 262 |
+
J'entends déjà votre question "_Pourquoi ne pas écrire directement dans le message broker plutôt que l'outbox ?_" 😁
|
| 263 |
+
|
| 264 |
+
Si la sauvegarde de la commande en base de données réussit, mais que l'écriture dans le message broker échoue, le message est perdu. Il vous faudrait dans ce cas implémenter un mécanisme de retry ou un mécanisme de rollback de la commande, et vous en conviendrez, c'est moins facile.
|
| 265 |
+
De plus, on pourrait également imaginer le cas où l'écriture dans le message broker réussit, mais la sauvegarde de la commande échoue. Dans ce cas, le système se retrouverait avec un message diffusé pour une opération inexistante, et potentiellement, les services de paiement, de réservation de stock et de planification de livraison auraient réalisé des actions à tort... 😥
|
| 266 |
+
|
| 267 |
+
Comme précédemment, je n'ai pas mentionné de message broker dans le schéma suivant afin d'en simplifier sa compéhension.
|
| 268 |
+
|
| 269 |
+

|
| 270 |
+
|
| 271 |
+
**Avantages** :
|
| 272 |
+
|
| 273 |
+
- La **simplicité** dans la mise en œuvre.
|
| 274 |
+
- La **garantie** que les messages sont envoyés grâce à la persistance dans l’outbox.
|
| 275 |
+
- La **cohérence stricte** de l'opération principale.
|
| 276 |
+
Dans notre cas, la création d'une commande et son insertion dans l'outbox sont réalisées de manière **atomique**.
|
| 277 |
+
- **Non bloquant**. Contrairement à 2PC, où l'ensemble des participants attendent la validation ou l'annulation, ici les opérations asynchrones évitent tout blocage.
|
| 278 |
+
- **Scalable**.
|
| 279 |
+
|
| 280 |
+
**Inconvénients** :
|
| 281 |
+
|
| 282 |
+
- Nécessite un **Message Relay** pour lire l'outbox.
|
| 283 |
+
- La table _outbox_ peut devenir un point de **contention** si le Message Relay est indisponible sur une longue période ou si les services consommateurs sont lents à traiter les messages.
|
| 284 |
+
|
| 285 |
+
En définitive, le pattern **Transactional Outbox** garantit l'intégrité des messages dans des systèmes distribués.
|
| 286 |
+
En s'appuyant sur les transactions ACID pour sécuriser les opérations locales et un envoi asynchrone des messages, il assure que les données mises à jour sont toujours accompagnées de messages fiables envoyés aux autres services.
|
| 287 |
+
Ce pattern se différencie des autres par sa simplicité et son fonctionnement non bloquant.
|
| 288 |
+
Attention cependant à gérer efficacement la taille de la table _outbox_ et faites en sorte que le Message Relay ne deviennent un SPOF pour votre système.
|
| 289 |
+
|
| 290 |
+
### Eventual Consistency
|
| 291 |
+
|
| 292 |
+
Le pattern **Eventual Consistency** (ou _Cohérence Éventuelle_) repose sur l'idée que _les différents réplicas de données peuvent temporairement être incohérents, mais finiront par converger vers un état cohérent après un certain temps_.
|
| 293 |
+
Ce modèle est particulièrement utilisé dans des systèmes où la performance et la disponibilité sont capitales, même si cela entraîne des incohérences temporaires.
|
| 294 |
+
|
| 295 |
+
Lors de l'édition des stocks dans un environnement avec de multiples points de vente (appelés _réplicas_ par la suite), la disponibilité et la rapidité d’accès sont primordiales, mais une petite incohérence temporaire dans les quantités disponibles est tolérable.
|
| 296 |
+
|
| 297 |
+
Dans un système d'e-commerce :
|
| 298 |
+
|
| 299 |
+
- Le service de commande crée une commande et publie un message via un message broker pour informer le service de gestion des stocks de la mise à jour des quantités disponibles, comme par exemple la décrémentation du stock.
|
| 300 |
+
- Le service de gestion des stocks consomme ce message et applique la mise à jour.
|
| 301 |
+
Cependant, cette mise à jour n'est pas immédiatement reflétée dans tous les réplicas, créant ainsi une incohérence temporaire dans les stocks entre les différents points de vente.
|
| 302 |
+
- Si des mises à jour concurrentes arrivent sur les stocks (par exemple, deux commandes simultanées pour le même produit), des règles de réconciliation sont appliquées pour résoudre ces conflits, comme la mise à jour du stock en fonction de l’ordre des messages ou de la dernière mise à jour.
|
| 303 |
+
- L’état du stock final est alors consigné et les mises à jour réussies sont enregistrées, ce qui permet d'éviter la duplication des événements et de **garantir que le système converge vers un état cohérent**.
|
| 304 |
+
|
| 305 |
+

|
| 306 |
+
|
| 307 |
+
**Avantages** :
|
| 308 |
+
|
| 309 |
+
- **Évolutif et résilient** : le système peut facilement s'adapter à une charge de travail croissante et maintenir une haute disponibilité.
|
| 310 |
+
- **Faible latence et des performances élevées** : les mises à jour peuvent se produire rapidement, sans attendre la synchronisation complète des réplicas.
|
| 311 |
+
|
| 312 |
+
**Inconvénients** :
|
| 313 |
+
|
| 314 |
+
- **Difficile à concevoir et à dépanner** : les incohérences temporaires peuvent rendre la logique du système complexe à comprendre et à maintenir.
|
| 315 |
+
- **Tolérance aux états intermédiaires inconsistants** : le système doit accepter que des données incohérentes puissent exister temporairement pendant que les réplicas convergent vers un état cohérent.
|
| 316 |
+
|
| 317 |
+
Vous l'aurez compris, bien que ces deux patterns ne garantissent pas l'atomicité globale comme les patterns de transactions distribuées, ils améliorent la résilience, la disponibilité et la tolérance aux erreurs.
|
| 318 |
+
Grâce à leur gestion asynchrone des événements, ils permettent de maintenir la cohérence des données tout en offrant flexibilité et bonnes performances.
|
| 319 |
+
Vous comprenez désormais pourquoi je les apelle "patterns de fiabilité et de cohérence".
|
| 320 |
+
|
| 321 |
+
## Et la Blockchain, on en parle ?
|
| 322 |
+
|
| 323 |
+
La blockchain n'est pas un pattern de transaction distribuée à proprement parler.
|
| 324 |
+
|
| 325 |
+
_C’est un registre de transactions distribuées, c’est-à-dire que l’information n’est ni centralisée, ni décentralisée, mais plutôt distribuée. La gouvernance des transactions se fait à partir de contrats intelligents, d’ententes entre les deux parties qui s’exécutent. Donc, si les conditions contractuelles ne sont pas remplies, la transaction n’est pas complétée._
|
| 326 |
+
|
| 327 |
+
Si je devais la comparer avec les transactions distribuées, je dirais que la blockchain est une approche alternative qui garantit la cohérence et l'intégrité des transactions dans un environnement distribué.
|
| 328 |
+
|
| 329 |
+
La blockchain ne nécessite pas de coordinateur central ni de mécanisme de rollback. À la place, elle utilise des algorithmes de **consensus distribué** ("_Proof of Work_", "_Proof of Stake_", "_Delegated Proof of Stake_", "_Proof of Autority_"...) pour valider et enregistrer les transactions **de manière immuable** (bien qu'il serait mieux de parler d'_immuabilité probabiliste_).
|
| 330 |
+
|
| 331 |
+
Ce modèle est particulièrement adapté aux environnements où les participants ne se font pas confiance, comme dans les transactions financières.
|
| 332 |
+
En revanche, il introduit une latence plus élevée, ce qui le rend moins efficace pour les systèmes nécessitant une forte réactivité, comme les microservices transactionnels.
|
| 333 |
+
|
| 334 |
+
Bien que les patterns de transactions distribuées et la blockchain partagent des objectifs similaires, ils répondent à des besoins différents : les premiers privilégient la rapidité et la flexibilité, tandis que la blockchain garantit la transparence et l’immutabilité au sein d'un réseau décentralisé.
|
| 335 |
+
|
| 336 |
+
## Conclusion
|
| 337 |
+
|
| 338 |
+
Les patterns de transactions distribuées comme _2PC_, _3PC_, _SAGA_ et _TCC_ visent à **garantir l'intégrité des données** dans des systèmes répartis.
|
| 339 |
+
|
| 340 |
+
_2PC_ et _3PC_ offrent des solutions très strictes en termes d'atomicité, mais au prix de complexité et de risques de blocage (notamment avec 2PC).
|
| 341 |
+
3PC améliore 2PC en réduisant les risques de blocage, mais il reste assez rigide.
|
| 342 |
+
|
| 343 |
+
_SAGA_ et _TCC_, quant à eux, privilégient la **flexibilité** en permettant une gestion asynchrone des transactions.
|
| 344 |
+
SAGA divise une grande transaction en sous-transactions avec des compensations possibles, tandis que TCC réserve d'abord les ressources, puis les confirme ou les annule selon le déroulement de la transaction.
|
| 345 |
+
|
| 346 |
+
En parallèle, les patterns de fiabilité et de cohérence, comme _Transactional Outbox_ et _Eventual Consistency_, se concentrent sur la **gestion des incohérences temporaires** et la **fiabilité** des systèmes distribués, plutôt que sur l'atomicité stricte.
|
| 347 |
+
Ces patterns permettent une gestion asynchrone des événements et garantissent que les systèmes finissent par converger vers un état cohérent, tout en sacrifiant une cohérence immédiate pour une meilleure performance et une meilleure disponibilité.
|
| 348 |
+
|
| 349 |
+
En résumé, le choix entre patterns de transactions distribuées et les patterns de fiabilité et de cohérence dépend des besoins spécifiques du système :
|
| 350 |
+
|
| 351 |
+
- gestion stricte de l'atomicité avec 2PC ou 3PC,
|
| 352 |
+
- flexibilité avec Saga ou TCC,
|
| 353 |
+
- résilience et tolérance aux incohérences temporaires avec les autres patterns.
|
| 354 |
+
|
| 355 |
+
Le compromis entre atomicité, performance et résilience est essentiel pour faire le bon choix. J'espère que cet article vous aidera à le faire... ce bon choix !
|
| 356 |
+
|
| 357 |
+
Une dernière chose. Si vous êtes plutôt architecture monolithique modulaire que architecture microservices, ces patterns sont aussi applicables 😁
|
| 358 |
+
|
| 359 |
+
> 📖 Sources :
|
| 360 |
+
>
|
| 361 |
+
> - Cohérence des données
|
| 362 |
+
> - [Wikipedia](<https://fr.wikipedia.org/wiki/Coh%C3%A9rence_(donn%C3%A9es)>)
|
| 363 |
+
> - Propriétés ACID
|
| 364 |
+
> - [IBM](https://www.ibm.com/docs/en/cics-tx/11.1?topic=processing-acid-properties-transactions)
|
| 365 |
+
> - Les Patterns
|
| 366 |
+
> - [Red Hat Developer Blog](https://developers.redhat.com/)
|
| 367 |
+
> - [Microservice Architecture](https://microservices.io/patterns/)
|
| 368 |
+
> - [Medium](https://medium.com/)
|
| 369 |
+
> - La Blockchain
|
| 370 |
+
> - [Gestion](https://www.revuegestion.ca/la-technologie-blockchain-une-revolution-pour-la-chaine-d-approvisionnement)
|
| 371 |
+
> - [Coin Academy](https://coinacademy.fr/academie/differents-algorithmes-consensus-blockchain/)
|
articles/mocked_java9_openai_tts.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:10ce314db22cd099ef1ba07b2b591dfec58339d87fb17761d767f599c969bf6d
|
| 3 |
+
size 8787500
|
articles/slidev.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Codez votre PPT avec Slidev
|
| 2 |
+
|
| 3 |
+
## Introduction
|
| 4 |
+
|
| 5 |
+
Vous qui avez déjà peaufiné des dizaines de présentations PowerPoint, que ce soit pour une soutenance, un support de formation, des solutions techniques, ou d'adorables petits exposés scolaires 😇, savez-vous qu'il existe des alternatives logicielles à PowerPoint ou Keynote mais **ultra orientées dev** ?
|
| 6 |
+
|
| 7 |
+
Il existe en réalité plusieurs projets comme [Reveal.js](https://revealjs.com/) ou [Marp](https://marp.app/) qui proposent concrètement d'utiliser un langage de programmation ou _équivalent_ pour préparer un diaporama avec du contenu multimédia et des animations.
|
| 8 |
+
Mais aujourd'hui je vous invite à en découvrir un en particulier : [Slidev](https://sli.dev/).
|
| 9 |
+
|
| 10 |
+
Slidev est un jeune projet Node.js Open Source et un peu "fou". Essentiellement écrit en TypeScript, il est doté d'une interface graphique en Vue.js.
|
| 11 |
+
Il propose de **coder ses diapos à l'aide de Markdown enrichi**, un format assez simple et déjà bien connu de tous.
|
| 12 |
+
|
| 13 |
+
À l'heure où j'écris ces lignes, Slidev est encore en version 0.49 – donc très expérimental – mais vous allez voir à travers ce tuto qu'il présente déjà quelques fonctionnalités qui valent vraiment le détour !
|
| 14 |
+
|
| 15 |
+
> Note : pour ceux qui sont pressés, vous pouvez directement tester Slidev [ici](https://sli.dev/new).
|
| 16 |
+
|
| 17 |
+
## Pourquoi diable s'embêter à coder un diaporama ?
|
| 18 |
+
|
| 19 |
+
On pourrait croire qu'un pseudo langage comme le Markdown nous ferait économiser du temps d'écriture, comparé aux déplacements sinueux de la souris dans un logiciel classique (PowerPoint, Keynote, …).
|
| 20 |
+
|
| 21 |
+
Oubliez ça.
|
| 22 |
+
|
| 23 |
+
Bien que l'on puisse gagner un peu de temps lorsqu'on se contente d'un _layout_ très simple (car Slidev positionne automatiquement les éléments sur chaque diapositive avec le layout et le thème choisis), il y a une autre _vraie_ bonne raison : les **possibilités**.
|
| 24 |
+
|
| 25 |
+
Les possibilités, c'est de pouvoir faire des présentations ultra techniques avec **une meilleure intégration de code** (une intégration de code tout court, en fait 😄). Vous pouvez par exemple montrer des extraits de code avec coloration syntaxique, créer des animations avancées sur des parties précises, intégrer des formules LaTeX, coder directement en [Vue.js](https://vuejs.org/) ou même ajouter des fonctionnalités dynamiques comme le "live coding" !
|
| 26 |
+
|
| 27 |
+
Enfin, une autre bonne raison est de pouvoir mettre en place un système de synchronisation entre **un thème de référence** et celui appliqué sur le diaporama. Ainsi, votre diaporama suivra automatiquement les évolutions de la charte graphique de l'entreprise !
|
| 28 |
+
|
| 29 |
+
Explorons maintenant ensemble toutes ces possibilités offertes par cette étonnante lib.
|
| 30 |
+
|
| 31 |
+
## Codons un diaporama
|
| 32 |
+
|
| 33 |
+
### La base
|
| 34 |
+
|
| 35 |
+
Commencez par initialiser un projet Node.js en suivant les instructions [ici](https://sli.dev/guide/#create-locally) et créez un fichier d'entrée `slides.md` à la racine.
|
| 36 |
+
|
| 37 |
+
Placez une image d'arrière-plan dans un répertoire `imgs`, et ajoutez alors ce contenu minimal dans votre fichier `slides.md` (pensez à adapter le chemin de votre image) :
|
| 38 |
+
|
| 39 |
+
```markdown
|
| 40 |
+
---
|
| 41 |
+
background: ./imgs/background.webp
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
# Le titre de ma première diapo
|
| 45 |
+
|
| 46 |
+
Créez des diapos tech et dynamiques !
|
| 47 |
+
|
| 48 |
+
<!--
|
| 49 |
+
Quelques notes réservées au présentateur pour la première diapo.
|
| 50 |
+
-->
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
Lancez la commande `npm exec slidev -- --port 3030 "slides.md"`.
|
| 54 |
+
Vous aurez alors accès à 3 espaces principaux :
|
| 55 |
+
|
| 56 |
+
- une interface web sur `http://localhost:3030/` pour montrer le diaporama à votre public (et avec une **vignette caméra** déplaçable, trop cool !) :
|
| 57 |
+
|
| 58 |
+

|
| 59 |
+
|
| 60 |
+
- une page `localhost:3030/presenter` pour contrôler votre diaporama en mode présentateur :
|
| 61 |
+
|
| 62 |
+

|
| 63 |
+
|
| 64 |
+
- et une dernière page `localhost:3030/overview` histoire d'avoir un aperçu global de toutes vos diapos, même si vous préfèrerez sûrement l'affichage en grille depuis le mode présentateur (icône "Show slide overview").
|
| 65 |
+
|
| 66 |
+
Vous pouvez également installer [une extension pour VS Code](https://marketplace.visualstudio.com/items?itemName=antfu.slidev) qui ajoute l'autocomplétion, vous permet de vous déplacer directement à une section depuis un titre, de réorganiser l'ordre des diapos, ou même d'avoir un aperçu en temps réel dans une vignette :
|
| 67 |
+
|
| 68 |
+

|
| 69 |
+
|
| 70 |
+
Et tous ces espaces sont globalement synchronisés entre eux !
|
| 71 |
+
|
| 72 |
+
> **Attention** : si vous utilisez Prettier, il faudra [le configurer](https://sli.dev/features/prettier-plugin) pour éviter les conflits avec ce Markdown un peu spécial !
|
| 73 |
+
|
| 74 |
+
### Configurez le thème et les layouts
|
| 75 |
+
|
| 76 |
+
Commençons à configurer les slides de notre diaporama.
|
| 77 |
+
|
| 78 |
+
Il faut savoir que Slidev construit chaque diaporama en composant le contenu des fichiers `.md` avec **un thème graphique** et un ensemble de **layouts**.
|
| 79 |
+
|
| 80 |
+
Tout d'abord je vous propose de choisir un thème dans la [galerie officielle](https://sli.dev/resources/theme-gallery).
|
| 81 |
+
Vous pouvez le spécifier une (et une seule) fois dans ce que l'on appelle la partie `headmatter`, tout en haut du fichier Markdown :
|
| 82 |
+
|
| 83 |
+
```markdown
|
| 84 |
+
---
|
| 85 |
+
theme: default
|
| 86 |
+
transition: slide-left
|
| 87 |
+
background: ./imgs/background.webp
|
| 88 |
+
title: Mon premier diaporama
|
| 89 |
+
---
|
| 90 |
+
|
| 91 |
+
# Le titre de ma première diapo
|
| 92 |
+
|
| 93 |
+
Créez des diapos tech et dynamiques !
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
Toutes les polices, tailles, couleurs et images par défaut seront automatiquement configurées !
|
| 97 |
+
|
| 98 |
+
Notez bien les `---` avant et après l'en-tête, car ils délimitent la partie "en-tête" de la partie "contenu" pour chaque slide.
|
| 99 |
+
|
| 100 |
+
Au passage, on en profite également pour configurer les animations entre chaque diapo : `transition: slide-left` qui va faire glisser la diapo suivante de la droite vers la gauche. Sobre, mais efficace.
|
| 101 |
+
|
| 102 |
+
Ensuite, créez une nouvelle slide et ajoutez un layout `image-right`, dans la partie que l'on appelle alors `frontmatter` (tout un vocabulaire 😌).
|
| 103 |
+
Ce layout vous permettra d'ajouter le chemin d'une image, qui peut tout à fait être une URL d'ailleurs !
|
| 104 |
+
|
| 105 |
+
```markdown
|
| 106 |
+
# Le titre de ma première diapo
|
| 107 |
+
|
| 108 |
+
Créez des diapos tech et dynamiques !
|
| 109 |
+
|
| 110 |
+
---
|
| 111 |
+
|
| 112 |
+
layout: image-right
|
| 113 |
+
image: https://domain.org/imgs/background.webp
|
| 114 |
+
|
| 115 |
+
---
|
| 116 |
+
|
| 117 |
+
Ici on ajoute du contenu uniquement sur la partie gauche de la diapo.
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+

|
| 121 |
+
|
| 122 |
+
Il [existe](https://sli.dev/builtin/layouts) également les layouts `center`, `full`, `fact`, `iframe-right`, `quote`, etc.
|
| 123 |
+
|
| 124 |
+
Il ne peut y avoir qu'**un seul thème par diaporama**, mais chaque slide peut avoir un layout différent.
|
| 125 |
+
|
| 126 |
+
De manière générale, les layouts disponibles par défaut restent très simples et la plupart du temps, ils sont suffisants.
|
| 127 |
+
Si toutefois vous cherchez une disposition personnalisée, il faudra soit aller chercher un plugin existant, soit écrire quelques lignes de code (cf. [doc officielle](https://sli.dev/guide/write-layout)).
|
| 128 |
+
|
| 129 |
+
Maintenant, entrons dans le vif du sujet et expérimentons des contenus riches autour de code et de schémas qu'un logiciel de diaporama classique ne serait pas en mesure de gérer !
|
| 130 |
+
|
| 131 |
+
### Présentez du code avec classe
|
| 132 |
+
|
| 133 |
+
#### Extraits de code et animations
|
| 134 |
+
|
| 135 |
+
Dans la diapo que vous venez de créer, commencez par ajouter un extrait de code de la même façon qu'en Markdown :
|
| 136 |
+
|
| 137 |
+

|
| 138 |
+
|
| 139 |
+

|
| 140 |
+
|
| 141 |
+
On obtient une présentation basique du code avec coloration syntaxique et le texte sélectionnable.
|
| 142 |
+
|
| 143 |
+
Mais avec le Markdown enrichi que nous propose Slidev, nous allons pouvoir ajouter des options très intéressantes, comme l'affichage du numéro des lignes et la **surbrillance successive** des lignes 5, 1 puis 2 au clic de souris (ou les flèches du clavier), comme si l'on simulait un débogage !
|
| 144 |
+
|
| 145 |
+

|
| 146 |
+
|
| 147 |
+

|
| 148 |
+
|
| 149 |
+
Et si on allait un peu plus loin dans les animations ?
|
| 150 |
+
|
| 151 |
+
Imaginez que vous souhaitiez présenter une refactorisation ou tout simplement un JSON, par exemple, qui évoluerait plusieurs fois, successivement.
|
| 152 |
+
Eh bien, vous allez pouvoir animer ce code automatiquement avec la feature `shiki-magic-move` pour mettre en évidence ces évolutions :
|
| 153 |
+
|
| 154 |
+

|
| 155 |
+
|
| 156 |
+
Et le rendu slidev, toujours animé au clic de souris ou flèches clavier :
|
| 157 |
+
|
| 158 |
+

|
| 159 |
+
|
| 160 |
+
Magique ! 😲
|
| 161 |
+
|
| 162 |
+
#### Live Coding
|
| 163 |
+
|
| 164 |
+
Nous venons de voir la présentation de code statique. Maintenant nous allons construire une slide qui va nous permettre de réaliser **une démonstration de code en direct**, et sans quitter la présentation.
|
| 165 |
+
|
| 166 |
+
Ajoutez une nouvelle slide avec un layout de type `full` pour un affichage confortable. Puis ajoutez la suite de Fibonacci dans un extrait de code avec la mention `{monaco-run}` pour activer l'éditeur.
|
| 167 |
+
|
| 168 |
+

|
| 169 |
+
|
| 170 |
+
L'éditeur Monaco intégré va alors nous permettre de modifier le code en direct avec le linter, l'autocomplétion, et même l'exécution en temps réel !
|
| 171 |
+
|
| 172 |
+

|
| 173 |
+
|
| 174 |
+
### Construisez des schémas avancés
|
| 175 |
+
|
| 176 |
+
Slidev prend en charge les graphes [PlantUML](https://plantuml.com/) et [Mermaid](https://mermaid.js.org/). Si vous ne connaissez pas encore ce dernier, je vous conseille d'aller faire un tour sur [notre article ici](https://www.younup.fr/blog/graphes-avec-mermaid).
|
| 177 |
+
|
| 178 |
+
Mermaid offre beaucoup plus de possibilités qu'un système de drag-n-drop de formes qu'on trouverait dans un logiciel classique : diagrammes de séquence, classes, mindmap, git graph…
|
| 179 |
+
|
| 180 |
+
Ici je vous propose de partir sur un diagramme de Gantt, car c'est typiquement le genre de diagramme qui pourrait décourager à dessiner dans un soft classique.
|
| 181 |
+
|
| 182 |
+
Créez une nouvelle slide intitulée "Organisation du projet", avec le layout `default` qui, ici, convient à un diagramme qui a tendance à s'étendre sur la longueur. Ensuite, vous allez pouvoir ajouter directement un extrait de code de type `mermaid` pour dessiner un [diagramme de Gantt](https://mermaid.js.org/syntax/gantt.html), par exemple, sur l'organisation des développements d'une appli web entre une équipe UI/UX, une front, et une back.
|
| 183 |
+
|
| 184 |
+

|
| 185 |
+
|
| 186 |
+
Ce qui nous génère un magnifique diagramme de Gantt dans notre diapo !
|
| 187 |
+
|
| 188 |
+

|
| 189 |
+
|
| 190 |
+
Comme vous pouvez le constater, la syntaxe nécessite quand même une _certaine_ prise en main. Mais avec un peu d'expérience, l'écriture, et surtout, la mise à jour du diagramme dans le diaporama deviennent très intéressantes !
|
| 191 |
+
|
| 192 |
+
### Multipliez les possibilités avec des composants externes
|
| 193 |
+
|
| 194 |
+
Dans cette partie nous allons pousser l'intégration d'éléments externes en combinant une scène 3D du système solaire avec un `Component` Vue.js dans une seule et même diapositive.
|
| 195 |
+
|
| 196 |
+
Tout d'abord, commençons par créer une diapo très simple, qui va juste se contenter d'afficher notre composant Vue.js :
|
| 197 |
+
|
| 198 |
+
```markdown
|
| 199 |
+
---
|
| 200 |
+
title: Contenus externes
|
| 201 |
+
layout: full
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
# Un composant Vue.js dynamique
|
| 205 |
+
|
| 206 |
+
<PlanetTeleporter />
|
| 207 |
+
```
|
| 208 |
+
|
| 209 |
+
Ensuite, créez un fichier `components/PlanetTeleporter.vue` à la racine du projet.
|
| 210 |
+
|
| 211 |
+
Dans ce nouveau composant, nous allons construire :
|
| 212 |
+
|
| 213 |
+
- un `iframe` ouvert sur une page web intégrant un système solaire en 3D (basé sur [Three.js](https://threejs.org/)) ;
|
| 214 |
+
- et un bouton superposé qui va légèrement modifier l'URL de la scène 3D pour se téléporter directement à la planète Saturne.
|
| 215 |
+
|
| 216 |
+
```html
|
| 217 |
+
<script setup lang="ts">
|
| 218 |
+
import { ref } from "vue";
|
| 219 |
+
|
| 220 |
+
const url = ref("https://eyes.nasa.gov/apps/solar-system/");
|
| 221 |
+
|
| 222 |
+
function toSaturn() {
|
| 223 |
+
url.value = "https://eyes.nasa.gov/apps/solar-system/#/saturn";
|
| 224 |
+
}
|
| 225 |
+
</script>
|
| 226 |
+
|
| 227 |
+
<template>
|
| 228 |
+
<section>
|
| 229 |
+
<iframe :src="url" title="Page externe système solaire"></iframe>
|
| 230 |
+
<button @click="toSaturn">Vers Saturne !</button>
|
| 231 |
+
</section>
|
| 232 |
+
</template>
|
| 233 |
+
|
| 234 |
+
<style scoped>
|
| 235 |
+
section {
|
| 236 |
+
position: relative;
|
| 237 |
+
height: 85%;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
button {
|
| 241 |
+
position: absolute;
|
| 242 |
+
left: 380px;
|
| 243 |
+
top: 15px;
|
| 244 |
+
border: 1px solid white;
|
| 245 |
+
color: white;
|
| 246 |
+
width: 120px;
|
| 247 |
+
padding: 5px;
|
| 248 |
+
font-family: mono;
|
| 249 |
+
background-color: black;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
button:hover {
|
| 253 |
+
background-color: gray;
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
iframe {
|
| 257 |
+
width: 100%;
|
| 258 |
+
height: 100%;
|
| 259 |
+
}
|
| 260 |
+
</style>
|
| 261 |
+
```
|
| 262 |
+
|
| 263 |
+
Si vous regardez alors le rendu :
|
| 264 |
+
|
| 265 |
+

|
| 266 |
+
|
| 267 |
+
… et toujours sans quitter votre slide !
|
| 268 |
+
|
| 269 |
+
Imaginez maintenant tout ce que vous allez pouvoir faire en intégrant des composants dynamiques encore plus avancés, ou en manipulant directement du SVG pour une qualité graphique toujours nickel ! 🤩
|
| 270 |
+
|
| 271 |
+
Maintenant, vous avez toutes les clés en main pour créer des diaporamas techniques complètement dingues !
|
| 272 |
+
|
| 273 |
+
Et si vous cherchez encore de l'inspiration, je vous invite à jeter un œil à [la galerie d'exemples](https://sli.dev/resources/showcases).
|
| 274 |
+
|
| 275 |
+
## Conclusion
|
| 276 |
+
|
| 277 |
+
À travers ce tuto, nous avons pu explorer de nombreuses fonctionnalités qu'offre la lib Slidev, surtout d'un point de vue **intégration de code dynamique, de schémas et contenus externes**.
|
| 278 |
+
|
| 279 |
+
Avec du recul, on constate également que Slidev est un projet encore très récent, qui manque peut-être encore un peu de stabilité et d'uniformité dans la syntaxe par rapport à d'autres projets comme Reveal.js.
|
| 280 |
+
Par ailleurs, dès que l'on souhaite une disposition ou une animation particulière, la tâche peut rapidement s'annoncer plus ardue que prévu et nécessiter un investissement assez important. Mais au bout du compte, le jeu en vaut peut-être la chandelle ?
|
| 281 |
+
|
| 282 |
+
Et comme je le disais au début de ce tuto, ce qui nous intéresse surtout ici, ce sont les possibilités !
|
dev.sh
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
PYTHONPATH=./src streamlit run ./src/streamlit_app.py
|
output/.gitkeep
ADDED
|
File without changes
|
requirements.txt
CHANGED
|
@@ -1,3 +1,9 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
streamlit
|
| 2 |
+
pydub
|
| 3 |
+
openai
|
| 4 |
+
pypdf
|
| 5 |
+
edge_tts
|
| 6 |
+
python_dotenv
|
| 7 |
+
langchain
|
| 8 |
+
langchain-openai
|
| 9 |
+
langchain-community
|
src/__init__.py
ADDED
|
File without changes
|
src/audio_generation.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import List, Tuple
|
| 3 |
+
import os
|
| 4 |
+
from openai import OpenAI
|
| 5 |
+
import tempfile
|
| 6 |
+
from pydub import AudioSegment
|
| 7 |
+
import base64
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from script_generation import PodScript
|
| 10 |
+
@dataclass
|
| 11 |
+
class AudioConfig:
|
| 12 |
+
output_path: str = "./output"
|
| 13 |
+
api_key: str = os.getenv("OPENAI_API_KEY", "")
|
| 14 |
+
model: str = "gpt-4o-mini-tts"
|
| 15 |
+
mocked: bool = False
|
| 16 |
+
|
| 17 |
+
class ScriptToAudio:
|
| 18 |
+
def __init__(self, config: AudioConfig):
|
| 19 |
+
self._config = config
|
| 20 |
+
self._llm_client = OpenAI(api_key=config.api_key)
|
| 21 |
+
|
| 22 |
+
async def _text_to_speech(self, script: PodScript, voice_1: str, voice_2: str) -> Tuple[List[str], str]:
|
| 23 |
+
"""Converts the text in the conversation JSON to speech using the specified voices.
|
| 24 |
+
Args:
|
| 25 |
+
conversation_json (Dict): The conversation JSON containing the text to be converted.
|
| 26 |
+
voice_1 (str): The voice for the first speaker.
|
| 27 |
+
voice_2 (str): The voice for the second speaker.
|
| 28 |
+
Returns:
|
| 29 |
+
Tuple[List[str], str]: A tuple containing a list of filenames of the generated audio files and the output directory.
|
| 30 |
+
Raises:
|
| 31 |
+
ValueError: If the conversation JSON is empty or if the voices are invalid.
|
| 32 |
+
RuntimeError: If the text-to-speech conversion fails.
|
| 33 |
+
"""
|
| 34 |
+
output_dir = Path(self._create_output_directory())
|
| 35 |
+
filenames = []
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
for i, line in enumerate(script.conversation):
|
| 39 |
+
filename = output_dir / f"output_{i}.wav"
|
| 40 |
+
voice = voice_1 if i % 2 == 0 else voice_2
|
| 41 |
+
|
| 42 |
+
tmp_path= await self._generate_audio(line.text, voice)
|
| 43 |
+
os.rename(tmp_path, filename)
|
| 44 |
+
filenames.append(str(filename))
|
| 45 |
+
|
| 46 |
+
return filenames, str(output_dir)
|
| 47 |
+
except Exception as e:
|
| 48 |
+
raise RuntimeError(f"Failed to convert text to speech: {e}")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
async def _generate_audio(self, text: str, voice: str) -> str:
|
| 52 |
+
"""Generates audio from the given text using the specified voice.
|
| 53 |
+
Args:
|
| 54 |
+
text (str): The input text to be converted to audio.
|
| 55 |
+
voice (str): The voice to be used for the audio generation.
|
| 56 |
+
Returns:
|
| 57 |
+
str: Astring containing the filename of the generated audio file.
|
| 58 |
+
Raises:
|
| 59 |
+
RuntimeError: If the audio generation fails.
|
| 60 |
+
"""
|
| 61 |
+
try:
|
| 62 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
|
| 63 |
+
|
| 64 |
+
response = self._llm_client.audio.speech.create(
|
| 65 |
+
model=self._config.model,
|
| 66 |
+
voice=voice.lower(),
|
| 67 |
+
input=text,
|
| 68 |
+
response_format="wav"
|
| 69 |
+
)
|
| 70 |
+
usage = response
|
| 71 |
+
tmp_path = tmp_file.name
|
| 72 |
+
with open(tmp_path, "wb") as f:
|
| 73 |
+
f.write(response.content)
|
| 74 |
+
""" with self._llm_client.audio.speech.with_streaming_response.create(
|
| 75 |
+
model=self._config.model,
|
| 76 |
+
voice=voice.lower(),
|
| 77 |
+
input=text,
|
| 78 |
+
) as response:
|
| 79 |
+
response.stream_to_file(tmp_path) """
|
| 80 |
+
|
| 81 |
+
return tmp_path
|
| 82 |
+
except Exception as e:
|
| 83 |
+
raise RuntimeError(f"Failed to generate audio: {e}")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _create_output_directory(self) -> str:
|
| 88 |
+
"""Creates a unique output directory for the generated audio files.
|
| 89 |
+
Returns:
|
| 90 |
+
str: The name of the created output directory.
|
| 91 |
+
Raises:
|
| 92 |
+
RuntimeError: If the directory creation fails.
|
| 93 |
+
"""
|
| 94 |
+
random_bytes = os.urandom(8)
|
| 95 |
+
folder_name = base64.urlsafe_b64encode(random_bytes).decode("utf-8")
|
| 96 |
+
final_folder_name = os.path.join(self._config.output_path, folder_name)
|
| 97 |
+
os.makedirs(final_folder_name, exist_ok=True)
|
| 98 |
+
return final_folder_name
|
| 99 |
+
|
| 100 |
+
def _combine_audio_files(self, filenames: List[str], output_file: str) -> None:
|
| 101 |
+
"""Combines multiple audio files into a single WAV file.
|
| 102 |
+
Args:
|
| 103 |
+
filenames (List[str]): A list of filenames of the audio files to be combined.
|
| 104 |
+
output_file (str): The name of the output WAV file.
|
| 105 |
+
Raises:
|
| 106 |
+
ValueError: If the input filenames list is empty.
|
| 107 |
+
RuntimeError: If the audio file combination fails.
|
| 108 |
+
"""
|
| 109 |
+
if not filenames:
|
| 110 |
+
raise ValueError("No input files provided")
|
| 111 |
+
|
| 112 |
+
try:
|
| 113 |
+
audio_segments = []
|
| 114 |
+
|
| 115 |
+
for filename in filenames:
|
| 116 |
+
audio_segment = AudioSegment.from_mp3(filename)
|
| 117 |
+
audio_segments.append(audio_segment)
|
| 118 |
+
|
| 119 |
+
combined = sum(audio_segments, AudioSegment.empty())
|
| 120 |
+
|
| 121 |
+
combined.export(output_file, format="wav")
|
| 122 |
+
|
| 123 |
+
for filename in filenames:
|
| 124 |
+
os.remove(filename)
|
| 125 |
+
|
| 126 |
+
except Exception as e:
|
| 127 |
+
raise RuntimeError(f"Failed to combine audio files: {e}")
|
| 128 |
+
|
| 129 |
+
async def run(self, script:PodScript ,voice_1: str, voice_2: str) -> str:
|
| 130 |
+
if self._config.mocked:
|
| 131 |
+
return './articles/mocked_java9_openai_tts.wav'
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
audio_files, folder_name = await self._text_to_speech(
|
| 135 |
+
script, voice_1, voice_2
|
| 136 |
+
)
|
| 137 |
+
final_output = os.path.join(folder_name, "combined_output.wav")
|
| 138 |
+
self._combine_audio_files(audio_files, final_output)
|
| 139 |
+
return final_output
|
| 140 |
+
except Exception as e:
|
| 141 |
+
raise RuntimeError(f"Failed to convert article to podcast: {e}")
|
src/audio_generation_edge.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from typing import List, Tuple
|
| 3 |
+
import os
|
| 4 |
+
from openai import OpenAI
|
| 5 |
+
import tempfile
|
| 6 |
+
from pydub import AudioSegment
|
| 7 |
+
import base64
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from script_generation import PodScript
|
| 10 |
+
@dataclass
|
| 11 |
+
class AudioConfig:
|
| 12 |
+
output_path: str = "./output"
|
| 13 |
+
api_key: str = os.getenv("OPENAI_API_KEY", "")
|
| 14 |
+
model: str = "gpt-4o-mini-tts"
|
| 15 |
+
mocked: bool = False
|
| 16 |
+
|
| 17 |
+
class ScriptToAudio:
|
| 18 |
+
def __init__(self, config: AudioConfig):
|
| 19 |
+
self._config = config
|
| 20 |
+
self._llm_client = OpenAI(api_key=config.api_key)
|
| 21 |
+
|
| 22 |
+
async def _text_to_speech(self, script: PodScript, voice_1: str, voice_2: str) -> Tuple[List[str], str]:
|
| 23 |
+
"""Converts the text in the conversation JSON to speech using the specified voices.
|
| 24 |
+
Args:
|
| 25 |
+
conversation_json (Dict): The conversation JSON containing the text to be converted.
|
| 26 |
+
voice_1 (str): The voice for the first speaker.
|
| 27 |
+
voice_2 (str): The voice for the second speaker.
|
| 28 |
+
Returns:
|
| 29 |
+
Tuple[List[str], str]: A tuple containing a list of filenames of the generated audio files and the output directory.
|
| 30 |
+
Raises:
|
| 31 |
+
ValueError: If the conversation JSON is empty or if the voices are invalid.
|
| 32 |
+
RuntimeError: If the text-to-speech conversion fails.
|
| 33 |
+
"""
|
| 34 |
+
output_dir = Path(self._create_output_directory())
|
| 35 |
+
filenames = []
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
for i, line in enumerate(script.conversation):
|
| 39 |
+
filename = output_dir / f"output_{i}.wav"
|
| 40 |
+
voice = voice_1 if i % 2 == 0 else voice_2
|
| 41 |
+
|
| 42 |
+
tmp_path= await self._generate_audio(line.text, voice)
|
| 43 |
+
os.rename(tmp_path, filename)
|
| 44 |
+
filenames.append(str(filename))
|
| 45 |
+
|
| 46 |
+
return filenames, str(output_dir)
|
| 47 |
+
except Exception as e:
|
| 48 |
+
raise RuntimeError(f"Failed to convert text to speech: {e}")
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
async def _generate_audio(self, text: str, voice: str) -> str:
|
| 52 |
+
"""Generates audio from the given text using the specified voice.
|
| 53 |
+
Args:
|
| 54 |
+
text (str): The input text to be converted to audio.
|
| 55 |
+
voice (str): The voice to be used for the audio generation.
|
| 56 |
+
Returns:
|
| 57 |
+
str: Astring containing the filename of the generated audio file.
|
| 58 |
+
Raises:
|
| 59 |
+
RuntimeError: If the audio generation fails.
|
| 60 |
+
"""
|
| 61 |
+
try:
|
| 62 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp_file:
|
| 63 |
+
|
| 64 |
+
response = self._llm_client.audio.speech.create(
|
| 65 |
+
model=self._config.model,
|
| 66 |
+
voice=voice.lower(),
|
| 67 |
+
input=text,
|
| 68 |
+
response_format="wav"
|
| 69 |
+
)
|
| 70 |
+
usage = response
|
| 71 |
+
tmp_path = tmp_file.name
|
| 72 |
+
with open(tmp_path, "wb") as f:
|
| 73 |
+
f.write(response.content)
|
| 74 |
+
""" with self._llm_client.audio.speech.with_streaming_response.create(
|
| 75 |
+
model=self._config.model,
|
| 76 |
+
voice=voice.lower(),
|
| 77 |
+
input=text,
|
| 78 |
+
) as response:
|
| 79 |
+
response.stream_to_file(tmp_path) """
|
| 80 |
+
|
| 81 |
+
return tmp_path
|
| 82 |
+
except Exception as e:
|
| 83 |
+
raise RuntimeError(f"Failed to generate audio: {e}")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _create_output_directory(self) -> str:
|
| 88 |
+
"""Creates a unique output directory for the generated audio files.
|
| 89 |
+
Returns:
|
| 90 |
+
str: The name of the created output directory.
|
| 91 |
+
Raises:
|
| 92 |
+
RuntimeError: If the directory creation fails.
|
| 93 |
+
"""
|
| 94 |
+
random_bytes = os.urandom(8)
|
| 95 |
+
folder_name = base64.urlsafe_b64encode(random_bytes).decode("utf-8")
|
| 96 |
+
final_folder_name = os.path.join(self._config.output_path, folder_name)
|
| 97 |
+
os.makedirs(final_folder_name, exist_ok=True)
|
| 98 |
+
return final_folder_name
|
| 99 |
+
|
| 100 |
+
def _combine_audio_files(self, filenames: List[str], output_file: str) -> None:
|
| 101 |
+
"""Combines multiple audio files into a single WAV file.
|
| 102 |
+
Args:
|
| 103 |
+
filenames (List[str]): A list of filenames of the audio files to be combined.
|
| 104 |
+
output_file (str): The name of the output WAV file.
|
| 105 |
+
Raises:
|
| 106 |
+
ValueError: If the input filenames list is empty.
|
| 107 |
+
RuntimeError: If the audio file combination fails.
|
| 108 |
+
"""
|
| 109 |
+
if not filenames:
|
| 110 |
+
raise ValueError("No input files provided")
|
| 111 |
+
|
| 112 |
+
try:
|
| 113 |
+
audio_segments = []
|
| 114 |
+
|
| 115 |
+
for filename in filenames:
|
| 116 |
+
audio_segment = AudioSegment.from_mp3(filename)
|
| 117 |
+
audio_segments.append(audio_segment)
|
| 118 |
+
|
| 119 |
+
combined = sum(audio_segments, AudioSegment.empty())
|
| 120 |
+
|
| 121 |
+
combined.export(output_file, format="wav")
|
| 122 |
+
|
| 123 |
+
for filename in filenames:
|
| 124 |
+
os.remove(filename)
|
| 125 |
+
|
| 126 |
+
except Exception as e:
|
| 127 |
+
raise RuntimeError(f"Failed to combine audio files: {e}")
|
| 128 |
+
|
| 129 |
+
async def run(self, script:PodScript ,voice_1: str, voice_2: str) -> str:
|
| 130 |
+
if self._config.mocked:
|
| 131 |
+
return './articles/mocked_java9_openai_tts.wav'
|
| 132 |
+
|
| 133 |
+
try:
|
| 134 |
+
audio_files, folder_name = await self._text_to_speech(
|
| 135 |
+
script, voice_1, voice_2
|
| 136 |
+
)
|
| 137 |
+
final_output = os.path.join(folder_name, "combined_output.wav")
|
| 138 |
+
self._combine_audio_files(audio_files, final_output)
|
| 139 |
+
return final_output
|
| 140 |
+
except Exception as e:
|
| 141 |
+
raise RuntimeError(f"Failed to convert article to podcast: {e}")
|
src/config.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
voice_names = {
|
| 2 |
+
"Alloy": "aloy",
|
| 3 |
+
"Ash": "ash",
|
| 4 |
+
"Ballad": "ballad",
|
| 5 |
+
"Coral": "coral",
|
| 6 |
+
"Echo": "echo",
|
| 7 |
+
"Fable": "fable",
|
| 8 |
+
"Onyx": "onyx",
|
| 9 |
+
"Nova": "nova",
|
| 10 |
+
"Sage": "sage",
|
| 11 |
+
"Shimmer": "shimmer",
|
| 12 |
+
"Verse": "verse"
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
quality_options = {
|
| 16 |
+
"Basse (gratuite)": "low",
|
| 17 |
+
"Haute (payante)": "high"
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
default_speaker_1 = 0
|
| 21 |
+
default_speaker_2 = 1
|
| 22 |
+
|
| 23 |
+
languages = [
|
| 24 |
+
"Langue de la source",
|
| 25 |
+
"Afrikaans", "Albanian", "Amharic", "Arabic", "Armenian", "Azerbaijani",
|
| 26 |
+
"Bahasa Indonesian", "Bangla", "Basque", "Bengali", "Bosnian", "Bulgarian",
|
| 27 |
+
"Burmese", "Catalan", "Chinese Cantonese", "Chinese Mandarin",
|
| 28 |
+
"Chinese Taiwanese", "Croatian", "Czech", "Danish", "Dutch", "English",
|
| 29 |
+
"Estonian", "Filipino", "Finnish", "French", "Galician", "Georgian",
|
| 30 |
+
"German", "Greek", "Hebrew", "Hindi", "Hungarian", "Icelandic", "Irish",
|
| 31 |
+
"Italian", "Japanese", "Javanese", "Kannada", "Kazakh", "Khmer", "Korean",
|
| 32 |
+
"Lao", "Latvian", "Lithuanian", "Macedonian", "Malay", "Malayalam",
|
| 33 |
+
"Maltese", "Mongolian", "Nepali", "Norwegian Bokmål", "Pashto", "Persian",
|
| 34 |
+
"Polish", "Portuguese", "Romanian", "Russian", "Serbian", "Sinhala",
|
| 35 |
+
"Slovak", "Slovene", "Somali", "Spanish", "Sundanese", "Swahili",
|
| 36 |
+
"Swedish", "Tamil", "Telugu", "Thai", "Turkish", "Ukrainian", "Urdu",
|
| 37 |
+
"Uzbek", "Vietnamese", "Welsh", "Zulu"
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
articles =[
|
| 41 |
+
("Java 9, ce mal aimé", "Java9 ce mal aimé.md")
|
| 42 |
+
]
|
src/mocked_script.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
mocked_raw_script = """
|
| 2 |
+
Alloy : Salut tout le monde, et bienvenue dans "Tech Talk with Alloy and Ash"! Aujourd'hui, on va traîner du côté des coulisses des technologies Java, et plus précisément, le mystérieux et souvent mal compris Java 9! [pause] Bonjour Ash!
|
| 3 |
+
Ash : Salut Alloy! Franchement, c'est marrant, mais Java 9, c'est un peu comme ce film culte que personne n'a regardé au cinéma mais qui est devenu un classique dans les bacs. Pourquoi as-tu pensé à Java 9 pour notre podcast aujourd'hui?
|
| 4 |
+
Alloy : Ah, super question! En fait, même si Java 9 n'a pas eu le succès fulgurant de Java 8, il a apporté des innovations radicales qui influencent encore notre utilisation du langage aujourd'hui. C’est presque de la science-fiction, quoi!
|
| 5 |
+
Ash : Une révolution silencieuse, hein? [pause] Dis-m'en plus, quelles sont les gros changements qu'on doit remercier Java 9 pour?
|
| 6 |
+
Alloy : Bien sûr! Tout d'abord, il y a la fameuse modularisation avec la JEP 261, qui, au passage, a un peu traumatisé les développeurs.
|
| 7 |
+
Ash : Ah la la, les bons vieux souvenirs des systèmes modulaires... un cauchemar pour certains, une bénédiction pour peu d'autres! [rire]
|
| 8 |
+
Alloy : Exactement! Et pour ceux qui avaient le courage de s'y plonger, c'était un vrai trésor pour structurer, sécuriser et optimiser ses applications. [emphasis] C’est surtout un super point pour les adeptes du Clean Architecture!
|
| 9 |
+
Ash : Waouh, je ne savais pas que c'était si impactant. Mais il n'y avait pas que la modularisation, non?
|
| 10 |
+
Alloy : Oui, il y avait aussi les JAR multi-releases! Des JAR capables de gérer plusieurs versions de Java dans un seul fichier. Super pratique, mais franchement... qui avait vraiment envie de jongler avec ça, hein?
|
| 11 |
+
Ash : Exactement! Ça sonnait bien sur le papier, mais en pratique, c’était une autre histoire. [pause] Et sinon, est-ce qu'il y a des fonctionnalités plus "cool" dont on pourrait profiter?
|
| 12 |
+
Alloy : Oh mais bien sûr! Le silenceux mais puissant [JEP 280: Indify String Concatenation], grâce auquel la JVM optimise la concaténation de chaînes à nous faire gagner en performance et en mémoire.
|
| 13 |
+
Ash : Génial! Et pour ceux qui aiment les performances pointues, il y a toujours le fameux G1GC, le Garbage Collector que Java 9 a adopté par défaut. Dire qu'il fait tout ça sans nous déranger, ça c'est beau!
|
| 14 |
+
Alloy : En plein dans le mille! Et je dois aussi mentionner les Compact Strings qui réduisent la consommation mémoire. Moins de mémoire utilisée, plus de vitesse. Qui ne voudrait pas de cela?
|
| 15 |
+
Ash : Chaque morceau de mémoire compte! On dirait que Java 9 était plein de trésors cachés. [pauser] Tu crois qu’on pourrait l'apprécier bien plus si on osait l'explorer un peu plus?
|
| 16 |
+
Alloy : [emphasis] Absolument Ash! Et ça me rappelle un petit dicton: "Ne pas juger un livre à sa couverture... ou une version de Java à sa réputation!" [rire]
|
| 17 |
+
Ash : Belle conclusion! Merci de nous avoir fait redécouvrir Java 9 aujourd'hui. Et pour nos auditeurs, n'oubliez pas de jeter un œil sur ces fonctionnalités dans vos prochains projets! Bisous tech à vous tous!
|
| 18 |
+
Alloy : Merci d’avoir été avec nous, et à la prochaine sur "Tech Talk with Alloy and Ash"! Bye bye!
|
| 19 |
+
"""
|
src/script_generation.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
import os
|
| 3 |
+
from langchain.chat_models import init_chat_model
|
| 4 |
+
from langchain_core.prompts import PromptTemplate
|
| 5 |
+
from pydantic import BaseModel, Field
|
| 6 |
+
from mocked_script import mocked_raw_script
|
| 7 |
+
@dataclass
|
| 8 |
+
class ScriptConfig:
|
| 9 |
+
articles_path: str = "./articles"
|
| 10 |
+
model: str = "gpt-4o"
|
| 11 |
+
model_provider: str = "openai"
|
| 12 |
+
api_key: str = os.getenv("OPENAI_API_KEY", "")
|
| 13 |
+
mocked: bool = False
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class PodLine(BaseModel):
|
| 17 |
+
"""Podcast line"""
|
| 18 |
+
speaker: str = Field(description="The name of the speaker")
|
| 19 |
+
text: str = Field(description="The text spoken by the speaker")
|
| 20 |
+
|
| 21 |
+
# Pydantic
|
| 22 |
+
class PodScript(BaseModel):
|
| 23 |
+
"""Podcast script"""
|
| 24 |
+
conversation: list[PodLine] = Field(description="The setup of the joke")
|
| 25 |
+
|
| 26 |
+
class MarkdownToScrip:
|
| 27 |
+
def __init__(self, config: ScriptConfig):
|
| 28 |
+
self._config = config
|
| 29 |
+
self._llm = init_chat_model(
|
| 30 |
+
model=config.model,
|
| 31 |
+
model_provider=config.model_provider,
|
| 32 |
+
api_key=config.api_key).with_structured_output(PodScript)
|
| 33 |
+
self._prompt = PromptTemplate.from_template( """You are a creative podcast scriptwriter specializing in tech content. Your task is to turn the following technical article into a spoken podcast script designed for two speakers
|
| 34 |
+
The goal is to create a clear, engaging, natural-sounding conversation that feels spontaneous but informative, as if recorded for a professional podcast. The tone should be friendly, curious, and energetic.
|
| 35 |
+
1. The podcast must feature two fictional hosts, **{speaker1_name}** and **{speaker2_name}**, who take turns discussing the content.
|
| 36 |
+
2. Add informal elements like light humor, reactions, rhetorical questions, and natural interjections (\"Wait, what?\", \"Exactly!\", \"That's wild\", etc.)
|
| 37 |
+
3. Emphasize key points or surprising facts by marking them with [pause], [emphasis], or *italicized phrases* to guide expressive TTS rendering.
|
| 38 |
+
4. Begin with a short intro to set the tone of the episode and end with a friendly closing.
|
| 39 |
+
5. Break the discussion into logical sections (e.g., introduction, main points, implications, etc.)
|
| 40 |
+
6. Keep the language conversational and oral (short sentences, contractions, and natural rhythm).
|
| 41 |
+
7. Keep the duration equivalent to approximately 3–4 minutes when read aloud.
|
| 42 |
+
8. {language_instruction}
|
| 43 |
+
Now write the full podcast script with style markers where relevant.
|
| 44 |
+
|
| 45 |
+
Here is the article text:
|
| 46 |
+
{article}""")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def _fetch_article(self, article: str) -> str:
|
| 50 |
+
"""Fetches the article content from the specified path.
|
| 51 |
+
Args:
|
| 52 |
+
article (str): The name of the article file.
|
| 53 |
+
Returns:
|
| 54 |
+
str: The content of the article.
|
| 55 |
+
Raises:
|
| 56 |
+
ValueError: If the article is empty or not found.
|
| 57 |
+
FileNotFoundError: If the article file does not exist.
|
| 58 |
+
"""
|
| 59 |
+
if not article:
|
| 60 |
+
raise ValueError("Article cannot be empty")
|
| 61 |
+
|
| 62 |
+
full_path = f"{self._config.articles_path}/{article}"
|
| 63 |
+
if not os.path.exists(full_path):
|
| 64 |
+
raise FileNotFoundError(f"Article not found: {full_path}")
|
| 65 |
+
with open(full_path, "r", encoding="utf-8") as file:
|
| 66 |
+
text = file.read()
|
| 67 |
+
if not text:
|
| 68 |
+
raise ValueError("Article content is empty")
|
| 69 |
+
return text
|
| 70 |
+
|
| 71 |
+
async def _generate_script(self, article: str, target_language, speaker1_name: str, speaker2_name: str) :
|
| 72 |
+
"""Generates a podcast script from the given text using the LLM.
|
| 73 |
+
Args:
|
| 74 |
+
text (str): The input text to be converted into a podcast script.
|
| 75 |
+
target_language (str): The target language for the podcast.
|
| 76 |
+
Returns:
|
| 77 |
+
str: The generated podcast script in JSON format.
|
| 78 |
+
Raises:
|
| 79 |
+
ValueError: If the input text is empty or if the LLM request fails.
|
| 80 |
+
"""
|
| 81 |
+
if target_language == "Auto Detect":
|
| 82 |
+
language_instruction = "The podcast MUST be in the same language as the article."
|
| 83 |
+
else:
|
| 84 |
+
language_instruction = f"The podcast MUST be in {target_language} language"
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
response = await self._prompt.pipe(self._llm).ainvoke(
|
| 88 |
+
{ "speaker1_name":speaker1_name,
|
| 89 |
+
"speaker2_name":speaker2_name,
|
| 90 |
+
"language_instruction":language_instruction,
|
| 91 |
+
"article":article}
|
| 92 |
+
)
|
| 93 |
+
if isinstance(response, PodScript):
|
| 94 |
+
return response
|
| 95 |
+
elif isinstance(response, dict):
|
| 96 |
+
return PodScript(**response)
|
| 97 |
+
except Exception as e:
|
| 98 |
+
raise RuntimeError(f"Failed to generate podcast script: {e}")
|
| 99 |
+
|
| 100 |
+
def _generate_mock_podcast_script(self) -> PodScript:
|
| 101 |
+
lines = []
|
| 102 |
+
for raw_line in mocked_raw_script.strip().splitlines():
|
| 103 |
+
if ':' in raw_line:
|
| 104 |
+
speaker, text = raw_line.split(':', 1)
|
| 105 |
+
lines.append(PodLine(speaker=speaker.strip(), text=text.strip()))
|
| 106 |
+
return PodScript(conversation=lines)
|
| 107 |
+
|
| 108 |
+
async def run(self, article: str, target_language: str, speaker1_name: str, speaker2_name: str):
|
| 109 |
+
"""Main method to convert an article to a podcast script.
|
| 110 |
+
Args:
|
| 111 |
+
article (str): The name of the article file.
|
| 112 |
+
target_language (str): The target language for the podcast.
|
| 113 |
+
speaker1_name (str): The name of the first speaker.
|
| 114 |
+
speaker2_name (str): The name of the second speaker.
|
| 115 |
+
Returns:
|
| 116 |
+
PodScript: The generated podcast script.
|
| 117 |
+
"""
|
| 118 |
+
print("Running script generation")
|
| 119 |
+
if self._config.mocked:
|
| 120 |
+
return self._generate_mock_podcast_script()
|
| 121 |
+
else:
|
| 122 |
+
text = self._fetch_article(article)
|
| 123 |
+
return await self._generate_script(text, target_language, speaker1_name, speaker2_name)
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
|
src/streamlit_app.py
CHANGED
|
@@ -1,40 +1,179 @@
|
|
| 1 |
-
import altair as alt
|
| 2 |
-
import numpy as np
|
| 3 |
-
import pandas as pd
|
| 4 |
import streamlit as st
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
.
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
+
import asyncio
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
load_dotenv()
|
| 5 |
|
| 6 |
+
from config import languages, voice_names, default_speaker_1, default_speaker_2, articles
|
| 7 |
+
from script_generation import MarkdownToScrip, ScriptConfig, PodScript
|
| 8 |
+
from audio_generation import ScriptToAudio, AudioConfig
|
| 9 |
+
|
| 10 |
+
# Generators config
|
| 11 |
+
script_config = ScriptConfig()
|
| 12 |
+
audio_config = AudioConfig()
|
| 13 |
+
# UI initial state
|
| 14 |
+
for key in ["generation_started", "script_ready", "audio_ready", "pending_generation", "audio_generation_started"]:
|
| 15 |
+
st.session_state.setdefault(key, False)
|
| 16 |
+
|
| 17 |
+
# Async helpers
|
| 18 |
+
async def generate_script(article: str, language: str, voice1: str, voice2: str):
|
| 19 |
+
generator = MarkdownToScrip(script_config)
|
| 20 |
+
return await generator.run(article, language, voice1, voice2)
|
| 21 |
+
|
| 22 |
+
async def generate_audio(script: PodScript, voice1: str, voice2: str):
|
| 23 |
+
generator = ScriptToAudio(audio_config)
|
| 24 |
+
return await generator.run(script, voice1, voice2)
|
| 25 |
+
|
| 26 |
+
# Sync wrappers for Streamlit
|
| 27 |
+
def generate_script_sync(article: str, language: str, voice1: str, voice2: str) -> PodScript | None:
|
| 28 |
+
return asyncio.run(generate_script(article, language, voice1, voice2))
|
| 29 |
+
|
| 30 |
+
def generate_audio_sync(script: PodScript, voice1: str, voice2: str):
|
| 31 |
+
return asyncio.run(generate_audio(script,voice1, voice2))
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def render_form():
|
| 35 |
+
"""Render the form for article selection and voice configuration."""
|
| 36 |
+
with st.form("generation_form"):
|
| 37 |
+
article = st.selectbox("Article", options=articles, format_func=lambda x: x[0])
|
| 38 |
+
language = st.selectbox("Langue de sortie", languages, index=0)
|
| 39 |
+
voice1 = st.selectbox("Voix Speaker 1", voice_names, index=default_speaker_1)
|
| 40 |
+
voice2 = st.selectbox("Voix Speaker 2", voice_names, index=default_speaker_2)
|
| 41 |
+
|
| 42 |
+
submitted = st.form_submit_button("📜 Générer le script")
|
| 43 |
+
|
| 44 |
+
if submitted:
|
| 45 |
+
st.session_state["generation_started"] = True
|
| 46 |
+
st.session_state["pending_generation"] = True
|
| 47 |
+
st.session_state["article"] = article[1]
|
| 48 |
+
st.session_state["language"] = language
|
| 49 |
+
st.session_state["voice1"] = voice1
|
| 50 |
+
st.session_state["voice2"] = voice2
|
| 51 |
+
st.rerun()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def render_script_panel():
|
| 55 |
+
"""Render the panel displaying the generated podcast script."""
|
| 56 |
+
st.markdown("### 🎧 Script du podcast")
|
| 57 |
+
|
| 58 |
+
podscript: PodScript = st.session_state["script"]
|
| 59 |
+
html = """
|
| 60 |
+
<div style='
|
| 61 |
+
height: 400px;
|
| 62 |
+
overflow-y: auto;
|
| 63 |
+
padding: 1em;
|
| 64 |
+
border: 1px solid #ccc;
|
| 65 |
+
border-radius: 8px;
|
| 66 |
+
margin-bottom: 1em;
|
| 67 |
+
'>
|
| 68 |
+
"""
|
| 69 |
+
for line in podscript.conversation:
|
| 70 |
+
html += f"<p><strong>{line.speaker}</strong> : {line.text}</p>"
|
| 71 |
+
html += "</div>"
|
| 72 |
+
|
| 73 |
+
st.markdown(html, unsafe_allow_html=True)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def render_audio_panel():
|
| 77 |
+
"""Render the panel for audio generation and playback."""
|
| 78 |
+
is_generating = st.session_state.get("audio_generation_started", False)
|
| 79 |
+
is_done = st.session_state.get("audio_ready", False)
|
| 80 |
+
|
| 81 |
+
# Disable the button if already generating or done
|
| 82 |
+
generate_audio_disabled = is_generating or is_done
|
| 83 |
+
|
| 84 |
+
# Display the button to generate audio
|
| 85 |
+
col1, col2, col3 = st.columns([1, 2, 1])
|
| 86 |
+
with col2:
|
| 87 |
+
clicked = st.button("🎙️ Générer l'audio", key="generate_audio_btn", disabled=generate_audio_disabled)
|
| 88 |
+
|
| 89 |
+
if clicked:
|
| 90 |
+
st.session_state["audio_generation_started"] = True
|
| 91 |
+
st.rerun()
|
| 92 |
+
|
| 93 |
+
# Generate audio if not already done
|
| 94 |
+
if is_generating and not is_done:
|
| 95 |
+
with st.spinner("Génération de l’audio en cours...", show_time=True):
|
| 96 |
+
st.markdown("""
|
| 97 |
+
> Cela peut prendre un minute, merci de patienter 🙂.
|
| 98 |
+
""")
|
| 99 |
+
audio_path = generate_audio_sync(
|
| 100 |
+
st.session_state["script"],
|
| 101 |
+
st.session_state["voice1"],
|
| 102 |
+
st.session_state["voice2"],
|
| 103 |
+
)
|
| 104 |
+
st.session_state["audio_path"] = audio_path
|
| 105 |
+
st.session_state["audio_ready"] = True
|
| 106 |
+
st.session_state["audio_generation_started"] = False
|
| 107 |
+
st.rerun()
|
| 108 |
+
|
| 109 |
+
if is_done:
|
| 110 |
+
st.markdown("### 🔊 Podcast généré")
|
| 111 |
+
st.audio(st.session_state["audio_path"])
|
| 112 |
+
|
| 113 |
+
def render_sidebar():
|
| 114 |
+
with st.sidebar:
|
| 115 |
+
st.title("🎙️ UpVoice")
|
| 116 |
+
st.markdown("Génèrez un podcast à partir d’un article tech")
|
| 117 |
+
content = """
|
| 118 |
+
|
| 119 |
+
* Pour le moment la génération est limitée à un podscast de ~3 minutes.
|
| 120 |
+
|
| 121 |
+
* L'audio est générée via [ gpt4o-mini-tts](https://platform.openai.com/docs/models/gpt-4o-mini-tts), d'autres modèles de génération seront bientôt proposés.
|
| 122 |
+
|
| 123 |
+
* N'hésitez pas à visiter 👉 [Le Blog by Younup](https://www.younup.fr/blog) pour plus de contenu tech !.
|
| 124 |
+
"""
|
| 125 |
+
st.markdown(f"""
|
| 126 |
+
<div style="
|
| 127 |
+
border: 1px solid rgba(250, 250, 250, 0.2);
|
| 128 |
+
border-radius: 10px;
|
| 129 |
+
padding: 1em;
|
| 130 |
+
box-shadow: 1px 1px 5px rgba(0,0,0,0.05);
|
| 131 |
+
margin-bottom: 1em;
|
| 132 |
+
">
|
| 133 |
+
<p style="margin: 0;">{content}</p>
|
| 134 |
+
</div>
|
| 135 |
+
""", unsafe_allow_html=True)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
# Reset
|
| 139 |
+
if st.session_state["generation_started"]:
|
| 140 |
+
if st.button("🔄 Nouvelle génération"):
|
| 141 |
+
for key in ["generation_started", "script_ready", "audio_ready", "pending_generation", "script", "audio_path", "audio_generation_started"]:
|
| 142 |
+
st.session_state.pop(key, None)
|
| 143 |
+
st.rerun()
|
| 144 |
+
|
| 145 |
+
def main():
|
| 146 |
+
st.set_page_config(page_title="UpVoice 🎙️", layout="wide")
|
| 147 |
+
render_sidebar()
|
| 148 |
+
|
| 149 |
+
# Affiche formulaire si aucune génération encore
|
| 150 |
+
if not st.session_state["generation_started"]:
|
| 151 |
+
render_form()
|
| 152 |
+
|
| 153 |
+
# Si soumission validée, mais script pas encore généré
|
| 154 |
+
if st.session_state.get("pending_generation"):
|
| 155 |
+
with st.spinner("Génération du script en cours...", show_time=True):
|
| 156 |
+
st.markdown("""
|
| 157 |
+
> Cela peut prendre une trentaine de seconde, merci de patienter 🙂.
|
| 158 |
+
""")
|
| 159 |
+
st.session_state["script"] = generate_script_sync(
|
| 160 |
+
st.session_state["article"],
|
| 161 |
+
st.session_state["language"],
|
| 162 |
+
st.session_state["voice1"],
|
| 163 |
+
st.session_state["voice2"]
|
| 164 |
+
)
|
| 165 |
+
st.session_state["script_ready"] = True
|
| 166 |
+
st.session_state["pending_generation"] = False
|
| 167 |
+
st.rerun()
|
| 168 |
+
|
| 169 |
+
# Affichage script et bouton audio
|
| 170 |
+
if st.session_state.get("script_ready"):
|
| 171 |
+
col1, col2 = st.columns([1, 1])
|
| 172 |
+
with col1:
|
| 173 |
+
render_script_panel()
|
| 174 |
+
with col2:
|
| 175 |
+
render_audio_panel()
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
if __name__ == "__main__":
|
| 179 |
+
main()
|