Commit 6128d211 authored by Kourser's avatar Kourser
Browse files

Une invite qui ne dit que le nom de la variable n'aide personne

Quand on garde un .env existant, l'installateur y verse les clés neuves de
.env.example puis les passe en revue. Il n'affichait que le nom et la valeur
d'exemple. Pour un port cela suffit ; pour TRANSCRIPTION_MODE, dont les valeurs
sont « livre », « externe » ou vide, l'invite ne disait rien du tout et il
fallait aller lire le fichier — ou deviner.

L'explication vient maintenant de .env.example lui-même, lue au moment de
demander. Recopiée dans le script, elle aurait fini par contredire le fichier,
et c'est la copie qu'on aurait lue.

La section transcription est redécoupée pour cela : un seul commentaire y
couvrait quatre clés, si bien que chacune héritait du texte des autres. Chacune
a désormais sa phrase, et TRANSCRIPTION_MODE annonce ses trois valeurs.

Le contrôle ne vérifie pas qu'on a pensé à documenter telle clé, mais qu'aucune
des 52 n'arrive nue devant l'exploitant : une clé ajoutée demain sans un mot
d'explication fera rougir la suite.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent 18b5ff3c
Loading
Loading
Loading
Loading
+44 −25
Changes for .env.example: 44 added lines, 25 removed lines.
Original line number Diff line number Diff line
@@ -181,37 +181,56 @@ LIVEKIT_WS_INTERNAL=ws://livekit:7880
# EN   — a few minutes during which uploads remain possible, but unscanned.
# ── Transcription des réunions / Meeting transcription ─────────────────────
#
# L'adresse d'un serveur parlant « /v1/audio/transcriptions ». Vide, les
# réunions s'enregistrent sans être transcrites, et le produit le dit.
#
# Sous le profil « transcription », la composition en lève un :
#   TRANSCRIPTION_URL=http://transcription:8000
#
# Le moteur est à vous. L'image se change par TRANSCRIPTION_IMAGE — whisper.cpp,
# faster-whisper, une enveloppe autour de Parakeet — et le modèle par
# TRANSCRIPTION_MODEL. Kastell ne connaît que le dialecte, pas le moteur.
# Kastell parle « /v1/audio/transcriptions » et ne connaît rien du moteur. Le
# vôtre est donc interchangeable : whisper.cpp, faster-whisper, une enveloppe
# autour de Parakeet. L'image se change par TRANSCRIPTION_IMAGE.
#
# L'image livrée par défaut est un faster-whisper : elle ne sert PAS Parakeet,
# qui demande NVIDIA NeMo. Ce qu'elle sert est relevé dans
# docker/transcription-modeles.txt, obtenu de l'image elle-même.
#
# EN — Kastell speaks "/v1/audio/transcriptions" and knows nothing of the
# EN   engine. The shipped image is a faster-whisper: it does NOT serve
# EN   Parakeet, which wants NVIDIA NeMo. What it does serve is listed in
# EN   docker/transcription-modeles.txt.

# Où tourne le moteur. Trois valeurs, et rien d'autre :
#   livre    le moteur de la composition, levé sous le profil « transcription »
#   externe  un moteur que vous exploitez déjà, ailleurs
#   (vide)   aucune transcription : les réunions s'enregistrent sans texte
#
# EN — Where the engine runs. Three values: "livre" (the composition's own,
# EN   under the "transcription" profile), "externe" (one you already run),
# EN   or empty (no transcription; meetings are recorded without text).
TRANSCRIPTION_MODE=

# L'adresse du moteur. Sous le profil « transcription », c'est le service de la
# composition : http://transcription:8000 — un nom qui ne résout que sur son
# réseau, ce qu'on veut d'un service qui entend tout.
#
# L'image livrée par défaut est un faster-whisper : elle ne sert pas Parakeet,
# qui demande NVIDIA NeMo. Les modèles qu'elle connaît sont relevés dans
# docker/transcription-modeles.txt.
# EN — The engine's address. Under the "transcription" profile, the
# EN   composition's own service: http://transcription:8000.
TRANSCRIPTION_URL=

# Le modèle demandé. « small » transcrit du français correctement sur un
# processeur ordinaire, à peu près deux fois le temps réel ; « medium » et
# « large-v3 » sont plus justes et réclament davantage. Les identifiants
# acceptés sont ceux de docker/transcription-modeles.txt.
#
# Le modèle se télécharge à la première demande, une fois :
# Il se télécharge à la première transcription demandée. Pour l'avoir d'avance :
#   docker compose -f docker/compose.yaml --env-file .env exec transcription \
#     curl -fsS -X POST "http://127.0.0.1:8000/v1/models/$(printf %s "$TRANSCRIPTION_MODEL" | sed 's|/|%2F|')"
#
# EN — Address of a server speaking "/v1/audio/transcriptions". Empty: meetings
# EN   are recorded but not transcribed, and the product says so. The engine is
# EN   yours: change TRANSCRIPTION_IMAGE and TRANSCRIPTION_MODEL. Kastell knows
# EN   the dialect, not the engine.
# « livre » lève le moteur de la composition et lui parle par le réseau de
# celle-ci ; « externe » s'adresse à un moteur que vous exploitez déjà. Vide,
# les réunions s'enregistrent sans être transcrites.
#
# EN — "livre" starts the composition's own engine; "externe" talks to one you
# EN   already run. Empty: recorded, not transcribed.
TRANSCRIPTION_MODE=
TRANSCRIPTION_URL=
# EN — The model requested. "small" handles French correctly on an ordinary
# EN   CPU, at roughly twice real time. Accepted identifiers are those in
# EN   docker/transcription-modeles.txt. It downloads on first use.
TRANSCRIPTION_MODEL=Systran/faster-whisper-small

# La clé, si le moteur en réclame une. Le moteur livré n'en demande pas : il
# n'est joignable que depuis le réseau de la composition.
#
# EN — The key, if the engine wants one. The shipped engine does not: it is
# EN   only reachable from the composition's network.
TRANSCRIPTION_KEY=

CLAMAV_HOST=clamav
+48 −0
Changes for apps/api/src/verification/installateur.ts: 48 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -260,4 +260,52 @@ export async function verifierInstallateur(r: Rapport): Promise<void> {
    r.verifie("EF-806", `le .env écrit porte ${cle}`,
      new RegExp(`^${cle}=\\$`, "m").test(script));
  }

  // ═════════════════════════════════════════════════════════════════════════
  r.titre("Ce que l'installateur explique avant de demander");

  /**
   * Le tour de révision des variables ajoutées.
   *
   * Quand on garde un .env existant, l'installateur y verse les clés neuves de
   * .env.example puis les passe en revue une par une. Il n'affichait que le nom
   * et la valeur d'exemple : pour un port cela suffit, pour TRANSCRIPTION_MODE
   * — « livre », « externe », ou vide — l'invite ne disait rien du tout, et il
   * fallait aller lire le fichier ou deviner.
   *
   * Le texte vient maintenant de .env.example lui-même. C'est ce qui rend le
   * contrôle ci-dessous tenable : il ne vérifie pas qu'on a pensé à documenter
   * telle clé, mais qu'aucune clé du fichier n'arrive nue devant l'exploitant.
   */
  const aide = morceau("aide_env() {   # aide_env <cle>", "}");

  async function expliquer(cle: string, langue = "fr"): Promise<string> {
    const dossier = await mkdtemp(join(tmpdir(), "kastell-aide-"));
    await writeFile(join(dossier, "essai.sh"),
      `LANGUE=${langue}\n${aide}\naide_env '${cle}'\n`);
    const { stdout } = await executer("sh", [join(dossier, "essai.sh")], { cwd: racine });
    return stdout;
  }

  const cles = [...(await readFile(`${racine}.env.example`, "utf8"))
    .matchAll(/^([A-Z][A-Z0-9_]*)=/gm)].map((m) => m[1]!);
  const nues: string[] = [];
  for (const cle of cles) {
    if ((await expliquer(cle)).trim().length === 0) nues.push(cle);
  }
  r.verifie("EX-30", `les ${cles.length} clés de .env.example arrivent expliquées, pas nues`,
    nues.length === 0 && cles.length > 20, nues.join(", ") || "aucune sans aide");

  // Celle qui a motivé le contrôle : une énumération dont l'invite ne disait
  // pas les valeurs acceptées.
  const mode = await expliquer("TRANSCRIPTION_MODE");
  r.verifie("EX-30", "et une énumération annonce ses valeurs, pas seulement son nom",
    /\blivre\b/.test(mode) && /\bexterne\b/.test(mode) && /vide/.test(mode),
    mode.trim().split("\n")[0] ?? "");

  // EX-21 : l'aide suit la langue choisie, sinon elle n'aide que la moitié.
  const anglais = await expliquer("TRANSCRIPTION_MODE", "en");
  r.verifie("EX-21", "l'explication suit la langue de l'installation",
    /Where the engine runs/.test(anglais) && !/Où tourne/.test(anglais),
    anglais.trim().split("\n")[0] ?? "");
}
+55 −0
Changes for outils/installer.sh: 55 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -72,6 +72,56 @@ demander_b() {
  if [ "$LANGUE" = "en" ]; then demander "$2" "$3"; else demander "$1" "$3"; fi
}

# ── Ce que .env.example dit d'une clé ──────────────────────────────────────
#
# Le tour de révision n'affichait que le nom de la variable et sa valeur
# d'exemple. Pour un port ou une adresse cela suffit ; pour TRANSCRIPTION_MODE,
# dont les valeurs sont « livre », « externe » ou vide, l'invite ne disait rien
# et l'exploitant devait aller lire le fichier — ou deviner.
#
# Le texte est lu dans .env.example plutôt que recopié ici. La documentation
# d'une variable vit à côté d'elle ; maintenue en double, elle finirait par la
# contredire, et c'est la copie qu'on lirait.
aide_env() {   # aide_env <cle>
  [ -f .env.example ] || return 0
  awk -v cle="$1" -v langue="$LANGUE" '
    function vider() { for (i in bloc) delete bloc[i]; n = 0 }
    # Une ligne vide referme le bloc : ce qui précède ne décrit plus ce qui suit.
    /^[[:space:]]*$/ { if (n) { for (i = 1; i <= n; i++) prec[i] = bloc[i]; m = n }
                       vider(); next }
    /^[A-Z][A-Z0-9_]*=/ {
      if ($0 ~ "^" cle "=") { fini = 1; exit }
      # Une affectation referme aussi, mais le bloc est retenu : un même
      # commentaire documente souvent tout un groupe de clés, et la deuxième
      # du groupe mérite de le lire plutôt que rien.
      if (n) { for (i = 1; i <= n; i++) prec[i] = bloc[i]; m = n }
      vider(); next
    }
    /^#/ { bloc[++n] = $0; next }
    END {
      if (!fini) exit
      # Le bloc collé à la clé prime ; à défaut, celui du groupe.
      if (!n) { for (i = 1; i <= m; i++) bloc[i] = prec[i]; n = m }
      for (i = 1; i <= n; i++) if (bloc[i] ~ /^# EN /) anglais = 1
      for (i = 1; i <= n; i++) {
        l = bloc[i]
        if (l ~ /^#[[:space:]]*[^[:alnum:]]*$/) continue   # filets et lignes nues
        if (langue == "en") { if (anglais && l !~ /^# EN /) continue }
        else if (l ~ /^# EN /) continue
        sub(/^# EN[[:space:]]+/, "", l)
        sub(/^—[[:space:]]+/, "", l)   # « EN — » : le tiret partait avec le préfixe
        sub(/^#[[:space:]]?/, "", l)
        utiles[++u] = l
      }
      # Dix lignes au plus, et les dernières : dans un bloc de groupe, ce qui
      # touche la clé se trouve en bas.
      debut = (u > 10) ? u - 9 : 1
      for (i = debut; i <= u; i++) print "     " utiles[i]
    }
  ' .env.example
}


# oui_non_b <question fr> <question en> <o|n par défaut>
oui_non_b() {
  if [ "$LANGUE" = "en" ]; then oui_non "$2" "$3"; else oui_non "$1" "$3"; fi
@@ -782,6 +832,11 @@ if [ -f .env ]; then
        echo
        if oui_non_b "Les passer en revue maintenant ?" "Review them now?" "o"; then
          for _c in $a_revoir; do
            _aide="$(aide_env "$_c")"
            if [ -n "$_aide" ]; then
              echo
              printf '%s\n' "$_aide"
            fi
            demander_b "$_c" "$_c" "$(valeur_env "$_c")"
            poser_valeur "$_c" "$REPONSE"
          done