# Architecture

Le Hub est un programme Rust qui donne à un modèle deux outils, `execute_script` et `get_state`, et atteint chaque application par un module côté Hub et un pont côté application. Cette page montre comment les éléments s'enchaînent, les trois façons d'atteindre une application et un script mesuré contre des appels séparés.

## Les éléments

- **Client MCP** : de votre côté. Il lance `libremodular-mcp serve` et appelle les deux outils.
- **Hub** : un seul processus, `libremodular-mcp`. Il répond au client, vérifie la cible, lit le script avant de l'envoyer et écrit le journal d'audit.
- **Module** : dans le Hub, un par application. Il sait comment atteindre l'application.
- **Pont** : dans l'application, à côté d'elle, ou un processus Python par script. Il exécute le script et renvoie ce qu'il a imprimé, son `result`, ou l'erreur propre à l'application.
- **Application** : GIMP, Krita, Blender, Inkscape ou LibreOffice. Elle fait le travail.

Le Hub ne connaît l'interface d'aucune application. Un script s'écrit dans le Python propre à l'application, et le module et le pont sont les seules pièces qui dépendent de l'application.

Le Hub est un exécutable unique d'environ 4,3 Mo. En dehors du chargeur dynamique, `ldd` liste deux bibliothèques : `libc.so.6` et `libgcc_s.so.1`. Le paquet installe un second exécutable, `libremodular-launch`, qui lance LibreOffice et son pont.

## Deux outils

La réponse du Hub à `tools/list` nomme exactement `["execute_script", "get_state"]`.

`execute_script(target_app, script_content)` exécute un script Python complet dans l'application que désigne `target_app` et renvoie un objet JSON : `status`, `duration_ms`, `stdout`, `stderr`, `result`, `error` avec la trace d'erreur propre à l'application, `truncated`, et `reason` quand le Hub a refusé le script. `get_state(target_app)` renvoie l'état de l'application : ses documents, ses calques ou ses objets, le chemin d'une capture PNG quand il y en a une, et `content_origin`, qui dit que le texte vient d'un document et non du Hub. La cible `hub` liste les applications configurées sans en contacter aucune.

## Trois façons d'atteindre une application

Le Hub a été lancé avec toutes les applications activées et aucun pont démarré. `get_state` sur `hub` a listé sept cibles, dans ces modes :

- `tcp` : `gimp`, `krita`, `blender`. Un pont chargé dans l'application écoute sur un port local, et le Hub s'y connecte.
- `uno` : `libreoffice-writer`, `libreoffice-calc`, `libreoffice-impress`. Un pont autonome se connecte au port UNO sur lequel LibreOffice écoute, et les trois cibles le partagent.
- `cli` : `inkscape`. Le Hub lance un processus Python par script ; il n'y a pas de pont à démarrer.

Seul `inkscape` affichait `connected: true` : en mode `cli`, rien n'a besoin d'être lancé, et le Hub avait trouvé le programme. Dans les modes `tcp` et `uno`, le Hub ne démarre, ne ferme ni n'arrête jamais une application à la demande du modèle ; l'opérateur démarre le pont, comme le dit le guide de chaque application.

Une application exécute un script à la fois. Un script qui dépasse le délai du Hub reçoit la réponse `timeout`, et la requête suivante reçoit `busy` jusqu'à ce que l'application soit libre ; la page [Sécurité](https://libremodular.davalan.fr/fr/security/index.md) le mesure.

## Ce qu'un pont demande

Les quatre fichiers de pont que le paquet installe sont générés à partir d'un noyau commun et d'un adaptateur par application. Ils importent la bibliothèque standard de Python et, en dehors d'elle, la seule interface de l'application. Chaque ligne donne le fichier, sa longueur et ce qu'il importe hors de la bibliothèque standard :

- `blender_bridge.py`, 1 091 lignes : `bpy`.
- `gimp_bridge.py`, 1 130 lignes : `gi`.
- `krita_bridge.py`, 1 094 lignes : `krita`, `PyQt5`, `PyQt6`.
- `uno_bridge.py`, 1 203 lignes : `uno`, `com`.

La liste vient de l'analyse syntaxique de chaque fichier, pas de son exécution. La page [Compatibilité](https://libremodular.davalan.fr/fr/compatibility/index.md) donne, pour chaque application, la version avec laquelle ces ponts ont été essayés.

## Un script contre des appels séparés

Un modèle qui pilote une application geste par geste fait un appel par geste. Un modèle qui écrit tout le travail fait un seul appel. Le Hub traite les deux de la même façon : on peut donc chronométrer les deux sur le même Hub, la même application et le même travail.

Le travail consiste à ajouter N éléments à un document : N rectangles dans Inkscape, N cubes dans Blender. Pour chaque N, dix répétitions ont chronométré un appel de `execute_script` qui fait tout le travail, puis N appels qui font un geste chacun. Le document était vidé avant chaque mesure, et deux appels d'échauffement précédaient les répétitions. Le temps est lu dans le client, de l'écriture de la requête à la lecture de la réponse ; dans Inkscape, le travail est ce script,

**Script écrit**

```python
import json, os
import inkex
document = inkex.load_svg(os.environ['LMM_DOCUMENT'])
root = document.getroot()
for i in range(20):
    root.add(inkex.Rectangle.new(i * 12, 10, 10, 10))
document.write(os.environ['LMM_DOCUMENT'])
print('LMM_RESULT: ' + json.dumps({'count': len(root)}))
```


et chacun des vingt appels séparés est le même script, la boucle étant remplacée par une ligne, par exemple le troisième :

**Script écrit**

```python
import json, os
import inkex
document = inkex.load_svg(os.environ['LMM_DOCUMENT'])
root = document.getroot()
root.add(inkex.Rectangle.new(36, 10, 10, 10))
document.write(os.environ['LMM_DOCUMENT'])
print('LMM_RESULT: ' + json.dumps({'count': len(root)}))
```


Médiane de dix répétitions, avec la plus petite et la plus grande entre parenthèses. Le client est celui de la [démonstration](https://libremodular.davalan.fr/fr/demonstration/index.md), avec un chronomètre autour de chaque appel.

**Inkscape 1.4, mode `cli`** (un processus Python par appel) :

- N = 1 : un script 141,7 ms (141,7 à 151,7) ; un appel 141,7 ms (141,5 à 161,9) ; rapport ×1,0.
- N = 5 : un script 146,7 ms (141,6 à 152,0) ; cinq appels 738,7 ms (718,6 à 749,3) ; rapport ×5,0.
- N = 20 : un script 151,7 ms (141,6 à 151,9) ; vingt appels 2 933,8 ms (2 874,0 à 3 046,2) ; rapport ×19,3.

**Blender 4.3.2, mode `tcp`** (l'application reste en marche) :

- N = 1 : un script 1,5 ms (1,3 à 2,7) ; un appel 1,2 ms (1,0 à 2,1) ; rapport ×0,8.
- N = 5 : un script 3,5 ms (3,3 à 6,1) ; cinq appels 5,9 ms (5,5 à 8,3) ; rapport ×1,7.
- N = 20 : un script 17,8 ms (16,7 à 20,4) ; vingt appels 30,5 ms (27,2 à 32,3) ; rapport ×1,7.

En mode `cli`, chaque appel lance un processus Python et charge `inkex`, ce qui coûte environ 0,14 s quel que soit le travail : vingt gestes en appels séparés coûtent 19,3 fois l'appel unique. En mode `tcp`, l'application tourne déjà, et les 19 appels supplémentaires ont ajouté 12,7 ms, soit environ 0,7 ms chacun. Une seconde exécution de toute la mesure a donné des médianes qui différaient d'au plus 10 ms sur Inkscape, sauf pour les vingt appels séparés (87 ms), et d'au plus 1,3 ms sur Blender ; le rapport à N = 20 était de ×19,9 sur Inkscape et de ×1,7 sur Blender.

> **Limite**
>
> Ces chiffres ne couvrent que le Hub et l'application. Le temps qu'un modèle met à écrire et à lire chacun des N appels n'est ni mesuré ni compté. Ils viennent d'une seule machine (Debian 13, 32 processeurs), d'un travail par application, avec N allant jusqu'à 20, et de deux applications sur cinq : GIMP, Krita et LibreOffice n'ont pas été chronométrés.

