Commit 92076ab0 authored by Kourser's avatar Kourser
Browse files

Le modèle ne se téléchargeait pas tout seul, contre ce qu'on promettait

L'installateur affirmait : « Le modèle se télécharge à la première
transcription demandée. Celle-là sera lente ; les suivantes non. » C'était
faux. Mesuré contre l'image livrée : une piste soumise à un modèle absent est
refusée par « 404 — Model … is not installed locally », et rien n'est
entrepris. Le .env.example portait la même affirmation, et donnait la commande
correcte comme une simple avance à prendre.

Une instance neuve aurait donc enregistré sa première réunion et n'en aurait
jamais rendu le texte — le jour où l'on enregistre, c'est-à-dire en crise.

L'installateur tire donc le modèle lui-même, une fois la composition levée :
on est devant la machine, et une attente longue n'y coûte que de l'attente. Le
volume « modeles » le garde ensuite.

Un tirage raté n'emporte pas l'installation : le reste de l'instance est
debout et utile, et refuser de finir pour un modèle absent serait perdre
l'installation pour sauver la transcription. Le taire serait pire — on
découvrirait le manque à la première réunion. La commande à relancer est donc
affichée.

L'encodage de l'identifiant n'est pas un détail : une barre oblique laissée
telle quelle fait un segment d'URL de plus, et le serveur rend alors 404 —
c'est-à-dire le code de « modèle absent ». On chercherait la panne du mauvais
côté, longtemps. Le contrôle exécute le bloc contre un « docker » de paille et
lit ce qu'on lui a demandé ; il ne relit pas du shell.

Co-Authored-By: default avatarClaude Opus 5 <noreply@anthropic.com>
parent b576ca88
Loading
Loading
Loading
Loading
+12 −2
Changes for .env.example: 12 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -217,13 +217,23 @@ TRANSCRIPTION_URL=
# « large-v3 » sont plus justes et réclament davantage. Les identifiants
# acceptés sont ceux de docker/transcription-modeles.txt.
#
# Il se télécharge à la première transcription demandée. Pour l'avoir d'avance :
# Il NE se télécharge PAS tout seul, contrairement à ce qui était écrit ici.
# Mesuré : le moteur refuse la piste par « 404 — Model … is not installed
# locally » et n'entreprend rien. L'installateur le tire donc pendant
# l'installation ; si vous remplissez ce fichier à la main, tirez-le vous-même
# avant la première réunion enregistrée :
#
#   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|')"
#
# Une fois tiré, le volume « modeles » le garde d'un redémarrage à l'autre.
#
# 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.
# EN   docker/transcription-modeles.txt. It does NOT download by itself: the
# EN   engine refuses the track with a 404 and does nothing. The installer
# EN   pulls it; filling this file by hand means pulling it yourself, with the
# EN   command above, before the first recorded meeting.
TRANSCRIPTION_MODEL=Systran/faster-whisper-small

# La clé, si le moteur en réclame une. Le moteur livré n'en demande pas : il
+84 −0
Changes for apps/api/src/verification/installateur.ts: 84 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -293,6 +293,90 @@ export async function verifierInstallateur(r: Rapport): Promise<void> {
      new RegExp(`^${cle}=\\$`, "m").test(script));
  }

  // ═════════════════════════════════════════════════════════════════════════
  r.titre("Le modèle de transcription, tiré à l'installation");

  /**
   * Le moteur ne télécharge rien de lui-même, contre ce que l'écran promettait.
   *
   * Mesuré contre l'image livrée : une piste soumise à un modèle absent est
   * refusée par « 404 — Model … is not installed locally », et rien n'est
   * entrepris. Une instance neuve enregistrait donc sa première réunion et n'en
   * rendait jamais le texte — le jour où l'on enregistre, c'est-à-dire en crise.
   *
   * On n'éprouve pas le texte du script : on exécute le bloc, avec un « docker »
   * de paille qui note ce qu'on lui demande. C'est la seule manière de voir que
   * l'identifiant du modèle est bien encodé — une barre oblique laissée telle
   * 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");
  })();

  /** Joue le bloc avec un « docker » qui obéit, ou qui renâcle. */
  async function tirer(obeissant: boolean): Promise<{ appels: string; sortie: string }> {
    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 :
    // c'est la panne réelle — le moteur est là, le téléchargement échoue.
    await writeFile(join(dossier, "faux", "docker"),
      "#!/bin/sh\n"
      + `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, "essai.sh"), [
      "set -eu",
      "LANGUE=fr; dire() { printf '%s\\n' \"$1\"; }",
      "COMPOSE=docker/compose.yaml",
      "transcription_mode='livre'",
      "transcription_modele='Systran/faster-whisper-small'",
      tirage,
    ].join("\n"));
    const { stdout } = await executer("sh", ["essai.sh"],
      { cwd: dossier, env: { ...process.env, PATH: `${dossier}/faux:${process.env.PATH ?? ""}` } });
    return {
      appels: await readFile(join(dossier, "appels"), "utf8").catch(() => ""),
      sortie: stdout,
    };
  }

  const tire = await tirer(true);
  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));
  // La barre oblique de « Systran/faster-whisper-small » doit partir encodée :
  // laissée telle quelle, elle fait un segment de plus et le serveur rend 404,
  // c'est-à-dire le code de « modèle absent ». On chercherait la panne du
  // mauvais côté.
  r.verifie("EF-806", "et l'identifiant part encodé, sans quoi le 404 désignerait autre chose",
    tire.appels.includes("Systran%2Ffaster-whisper-small")
      && !/models\/Systran\/faster/.test(tire.appels),
    (tire.appels.match(/v1\/models\/\S*/) ?? ["aucun chemin"])[0]!);
  r.verifie("EF-806", "il attend que le moteur réponde avant de lui demander quoi que ce soit",
    tire.appels.indexOf("health") >= 0
      && tire.appels.indexOf("health") < tire.appels.indexOf("POST"));

  // ── Un téléchargement raté ne fait pas rater l'installation ──────────────
  //
  // Le reste de l'instance est debout et utile : refuser de finir pour un
  // 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.
  const rate = await tirer(false);
  r.verifie("EF-806", "un tirage raté n'emporte pas l'installation avec lui",
    rate.sortie.length > 0);
  r.verifie("EF-806", "et la commande à relancer est donnée, encodée elle aussi",
    rate.sortie.includes("Systran%2Ffaster-whisper-small")
      && /v1\/models\//.test(rate.sortie),
    (rate.sortie.split("\n").find((l) => l.includes("v1/models")) ?? "rien").trim().slice(0, 100));

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

+83 −4
Changes for outils/installer.sh: 83 added lines, 4 removed lines.
Original line number Diff line number Diff line
@@ -1548,10 +1548,12 @@ if [ "$ecrire_env" = "oui" ]; then
          break
        done
        echo
        dire "  Le modèle se télécharge à la première transcription demandée." \
             "  The model downloads on the first transcription requested. That"
        dire "  Celle-là sera lente ; les suivantes non." \
             "  first one will be slow; the next ones will not."
        dire "  Le modèle sera tiré à la fin de cette installation : le moteur" \
             "  The model will be pulled at the end of this install: the engine"
        dire "  ne le télécharge pas de lui-même, il refuse la piste tant qu'il" \
             "  does not download it by itself — it refuses the track until it"
        dire "  ne l'a pas. Une seule fois : le volume le garde." \
             "  has it. Once only: the volume keeps it."
        ;;
      2)
        transcription_mode="externe"
@@ -2268,6 +2270,83 @@ else
       "  curl is missing: the status above is the only check possible here."
fi

# ── Le modèle de transcription, tiré maintenant plutôt qu'en crise ──────────
#
# Cet installateur promettait que « le modèle se télécharge à la première
# transcription demandée ». C'était faux, et mesuré : le moteur refuse la piste
# par « 404 — Model … is not installed locally » et n'entreprend rien. Une
# instance neuve aurait donc enregistré sa première réunion et n'en aurait
# jamais rendu le texte — le jour où l'on enregistre, c'est-à-dire en crise.
#
# On le tire ici, pendant qu'on est devant la machine et qu'un téléchargement
# long ne coûte que de l'attente. Le volume « modeles » le garde ensuite.
#
# 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
  echo
  dire "── Modèle de transcription ────────────────────────────────────────────" \
       "── Transcription model ───────────────────────────────────────────────"
  echo
  dire "  Kastell tire « $transcription_modele » maintenant." \
       "  Kastell is pulling \"$transcription_modele\" 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." \
       "  model, once only."
  echo

  # 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')"

  # 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à
  # où il n'a simplement pas fini de démarrer.
  printf '  Attente du moteur'
  i=0
  moteur="non"
  while [ "$i" -lt 90 ]; do
    if docker compose -f "$COMPOSE" --env-file .env exec -T transcription \
         curl -fsS "http://127.0.0.1:8000/health" > /dev/null 2>&1; then
      moteur="oui"
      break
    fi
    printf '.'
    i=$((i + 1))
    sleep 2
  done
  echo

  if [ "$moteur" != "oui" ]; then
    dire "  ⚠  Le moteur n'a pas répondu en trois minutes. Le modèle n'est pas" \
         "  ⚠  The engine did not answer within three minutes. The model is not"
    dire "     tiré : les réunions s'enregistreront et ne seront pas transcrites." \
         "     pulled: meetings will record and will not be transcribed."
  else
    dire "  Téléchargement en cours — cela peut prendre plusieurs minutes." \
         "  Downloading — this can take several minutes."
    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."
    else
      moteur="echec"
    fi
  fi

  if [ "$moteur" != "oui" ]; then
    echo
    dire "     À relancer quand la machine est disponible :" \
         "     To run again when the machine is free:"
    echo "       docker compose -f $COMPOSE --env-file .env exec transcription \\"
    echo "         curl -fsS -X POST \"http://127.0.0.1:8000/v1/models/$modele_encode\""
  fi
fi

echo
dire "── Ensuite ────────────────────────────────────────────────────────────" \
     "── Next ──────────────────────────────────────────────────────────────"