Commit 1b0c1ed1 authored by Kourser's avatar Kourser
Browse files

L'installateur tombait quand on gardait son .env

Relancé sur un poste qui garde son .env, l'installateur levait l'instance puis
s'arrêtait sur « transcription_mode: unbound variable ». Les questions ne sont
posées qu'à l'écriture d'un .env neuf, leurs variables n'existent que là, et le
tirage du modèle de transcription les lisait sous « set -u ».

Le tirage lit désormais le mode et le modèle dans le .env, et dit un modèle
vide plutôt que de réclamer au moteur un modèle sans nom.

Le contrôle jouait le bloc en posant lui-même ces variables : il n'éprouvait que
le chemin où l'on répond aux questions. Il le joue maintenant avec le seul .env,
et un contrôle interdit la classe entière — aucune variable posée seulement en
répondant n'est lue quand on garde son .env.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent 364ae92e
Loading
Loading
Loading
Loading
+62 −15
Changes for apps/api/src/verification/installateur.ts: 62 added lines, 15 removed lines.
Original line number Diff line number Diff line
@@ -310,18 +310,22 @@ export async function verifierInstallateur(r: Rapport): Promise<void> {
   * quelle fait un segment d'URL de plus, et le serveur rend alors 404 : le
   * même code que « modèle absent », ce qui égarerait durablement.
   */
  const tirage = (() => {
    const lignes = script.split("\n");
    const debut = lignes.findIndex((l) =>
      l.startsWith('if [ "$transcription_mode" = "livre" ]; then'));
    if (debut < 0) throw new Error("Bloc de tirage du modèle introuvable");
    const fin = lignes.findIndex((l, i) => i > debut && l === "fi");
    if (fin < 0) throw new Error("Fin du bloc de tirage introuvable");
    return lignes.slice(debut, fin + 1).join("\n");
  })();
  // Deux « fi » : l'avertissement d'un modèle vide, puis le tirage lui-même.
  const tirage = morceau('modele_transcription="$(valeur_env TRANSCRIPTION_MODEL)"', "fi", 2);
  const lectureEnv = morceau("valeur_env() {", "}");

  /** Joue le bloc avec un « docker » qui obéit, ou qui renâcle. */
  async function tirer(obeissant: boolean): Promise<{ appels: string; sortie: string }> {
  /**
   * Joue le bloc avec un « docker » qui obéit, ou qui renâcle.
   *
   * Et avec ce que le script a réellement en main à cet endroit : un .env, et
   * rien d'autre. La première version de ce contrôle posait elle-même les
   * variables des questions avant de jouer le bloc — elle n'éprouvait donc que
   * le chemin où l'on répond aux questions. Sur un poste qui garde son .env,
   * elles n'existent pas, et « set -u » arrêtait l'installateur au bout de sa
   * course. Le contrôle passait ; l'installateur tombait.
   */
  async function tirer(obeissant: boolean, modele = "Systran/faster-whisper-small"):
    Promise<{ appels: string; sortie: string; echec: string | null }> {
    const dossier = await mkdtemp(join(tmpdir(), "kastell-modele-"));
    await mkdir(join(dossier, "faux"), { recursive: true });
    // Il consigne ce qu'on lui demande, et refuse le POST quand on le lui dit :
@@ -331,23 +335,33 @@ export async function verifierInstallateur(r: Rapport): Promise<void> {
      + `printf '%s\\n' "$*" >> "${dossier}/appels"\n`
      + `case "$*" in *POST*) ${obeissant ? "exit 0" : "exit 7"} ;; esac\n`
      + "exit 0\n", { mode: 0o755 });
    await writeFile(join(dossier, ".env"),
      `TRANSCRIPTION_MODE=livre\nTRANSCRIPTION_MODEL=${modele}\n`);
    await writeFile(join(dossier, "essai.sh"), [
      "set -eu",
      "LANGUE=fr; dire() { printf '%s\\n' \"$1\"; }",
      "COMPOSE=docker/compose.yaml",
      "transcription_mode='livre'",
      "transcription_modele='Systran/faster-whisper-small'",
      lectureEnv,
      'services=""; profils=""',
      plan,
      tirage,
    ].join("\n"));
    const { stdout } = await executer("sh", ["essai.sh"],
      { cwd: dossier, env: { ...process.env, PATH: `${dossier}/faux:${process.env.PATH ?? ""}` } });
    // Un bloc qui tombe doit faire tomber un contrôle, pas toute la suite.
    const { stdout, echec } = await executer("sh", ["essai.sh"],
      { cwd: dossier, env: { ...process.env, PATH: `${dossier}/faux:${process.env.PATH ?? ""}` } })
      .then((x) => ({ stdout: x.stdout, echec: null as string | null }),
        (e: { stdout?: string; stderr?: string; message?: string }) =>
          ({ stdout: e.stdout ?? "", echec: (e.stderr || e.message || "échec").trim() }));
    return {
      appels: await readFile(join(dossier, "appels"), "utf8").catch(() => ""),
      sortie: stdout,
      echec,
    };
  }

  const tire = await tirer(true);
  r.verifie("EF-806", "le tirage se joue jusqu'au bout avec le seul .env, sous « set -u »",
    tire.echec === null, tire.echec ?? "");
  r.verifie("EF-806", "l'installateur demande lui-même le modèle au moteur",
    /POST/.test(tire.appels) && /v1\/models\//.test(tire.appels),
    (tire.appels.split("\n").find((l) => l.includes("POST")) ?? "aucun appel").slice(0, 110));
@@ -369,6 +383,13 @@ export async function verifierInstallateur(r: Rapport): Promise<void> {
  // modèle absent serait perdre l'installation pour sauver la transcription.
  // Mais le taire serait pire — on découvrirait le manque à la première
  // réunion. On dit donc la commande à relancer.
  // Un .env gardé, mode « livre », modèle vide : on le dit, et l'on ne demande
  // rien au moteur plutôt que de lui réclamer un modèle sans nom.
  const sansModele = await tirer(true, "");
  r.verifie("EF-806", "un .env sans modèle nommé est signalé, et rien n'est demandé au moteur",
    sansModele.sortie.includes("TRANSCRIPTION_MODEL est vide") && !/POST/.test(sansModele.appels),
    sansModele.sortie.split("\n").find((l) => l.includes("⚠")) ?? "aucun avertissement");

  const rate = await tirer(false);
  r.verifie("EF-806", "un tirage raté n'emporte pas l'installation avec lui",
    rate.sortie.length > 0);
@@ -377,6 +398,32 @@ export async function verifierInstallateur(r: Rapport): Promise<void> {
      && /v1\/models\//.test(rate.sortie),
    (rate.sortie.split("\n").find((l) => l.includes("v1/models")) ?? "rien").trim().slice(0, 100));

  // ── Garder son .env ne fait pas tomber la fin de l'installation ──────────
  //
  // Les questions ne sont posées que si l'on écrit un .env neuf ; leurs
  // variables n'existent que dans ce bloc. Tout ce qui suit sert aussi à qui
  // garde le sien, sous « set -u » : une seule de ces variables lue après le
  // bloc arrête l'installateur, instance levée, sur une erreur de shell. C'est
  // arrivé avec le mode et le modèle de transcription. On interdit la classe
  // entière, pas l'occurrence.
  const orphelines = (() => {
    const debut = lignes.findIndex((l) => l.startsWith('if [ "$ecrire_env" = "oui" ]; then'));
    const fin = lignes.findIndex((l, i) => i > debut && l === "fi");
    const affectation = /^\s*(?:export\s+)?([a-z_][a-z0-9_]*)=|\b(?:for|read(?:\s+-r)?)\s+([a-z_][a-z0-9_]*)/;
    const dedans = new Set<string>();
    const dehors = new Set<string>();
    lignes.forEach((l, i) => {
      const m = affectation.exec(l);
      const nom = m?.[1] ?? m?.[2];
      if (nom) (i > debut && i < fin ? dedans : dehors).add(nom);
    });
    const apres = lignes.slice(fin + 1).join("\n");
    return [...dedans].filter((v) => !dehors.has(v)
      && new RegExp(`\\$\\{?${v}\\b`).test(apres));
  })();
  r.verifie("EF-806", "aucune variable des questions n'est lue quand on garde son .env",
    orphelines.length === 0, orphelines.join(", "));

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

+19 −6
Changes for outils/installer.sh: 19 added lines, 6 removed lines.
Original line number Diff line number Diff line
@@ -2297,13 +2297,26 @@ fi
# Un échec ne fait pas échouer l'installation : le reste de l'instance est
# debout et utile. On dit la commande à relancer, et le produit dira « non
# transcrite » en attendant plutôt que de laisser croire à un oubli.
if [ "$transcription_mode" = "livre" ]; then
#
# Le mode et le modèle se lisent dans le .env, pas dans les réponses aux
# questions : quand on garde un .env existant, les questions ne sont pas
# posées, leurs variables n'existent pas, et « set -u » arrêtait l'installateur
# ici — après avoir levé l'instance, sur une erreur de shell sans explication.
modele_transcription="$(valeur_env TRANSCRIPTION_MODEL)"
if [ "$mode_transcription" = "livre" ] && [ -z "$modele_transcription" ]; then
  echo
  dire "  ⚠  TRANSCRIPTION_MODEL est vide dans .env : aucun modèle à tirer, et" \
       "  ⚠  TRANSCRIPTION_MODEL is empty in .env: no model to pull, and"
  dire "     les réunions enregistrées ne seront pas transcrites." \
       "     recorded meetings will not be transcribed."
fi
if [ "$mode_transcription" = "livre" ] && [ -n "$modele_transcription" ]; then
  echo
  dire "── Modèle de transcription ────────────────────────────────────────────" \
       "── Transcription model ───────────────────────────────────────────────"
  echo
  dire "  Kastell tire « $transcription_modele » maintenant." \
       "  Kastell is pulling \"$transcription_modele\" now."
  dire "  Kastell tire « $modele_transcription » maintenant." \
       "  Kastell is pulling \"$modele_transcription\" now."
  dire "  De quelques centaines de mégaoctets à trois gigaoctets selon le" \
       "  From a few hundred megabytes to three gigabytes depending on the"
  dire "  modèle, une seule fois." \
@@ -2313,7 +2326,7 @@ if [ "$transcription_mode" = "livre" ]; then
  # Le chemin porte l'identifiant du modèle, qui contient une barre oblique :
  # sans l'encoder, le serveur ne voit qu'un segment d'URL de plus et rend 404
  # — le même code que « modèle absent », ce qui égarerait durablement.
  modele_encode="$(printf '%s' "$transcription_modele" | sed 's#/#%2F#g')"
  modele_encode="$(printf '%s' "$modele_transcription" | sed 's#/#%2F#g')"

  # L'image met du temps à se lever la première fois. On attend sa sonde avant
  # de demander quoi que ce soit, sinon on conclurait à un échec du moteur là
@@ -2344,8 +2357,8 @@ if [ "$transcription_mode" = "livre" ]; then
    if docker compose -f "$COMPOSE" --env-file .env exec -T transcription \
         curl -fsS --max-time 3600 -X POST \
         "http://127.0.0.1:8000/v1/models/$modele_encode" > /dev/null 2>&1; then
      dire "  ✓ « $transcription_modele » est en place." \
           "  ✓ \"$transcription_modele\" is in place."
      dire "  ✓ « $modele_transcription » est en place." \
           "  ✓ \"$modele_transcription\" is in place."
    else
      moteur="echec"
    fi