#!/usr/bin/env bash
# Miroir du catalogue officiel des modèles Vosk (Bash, aucun Python).
set -Eeuo pipefail
SOURCE='https://alphacephei.com/vosk/models'
OUTPUT='./vosk-mirror'
MODE='download'
FILTER=''
while (($#)); do
  case "$1" in
    -o|--output) OUTPUT="${2:?Répertoire requis}"; shift 2;;
    --only) FILTER="${2:?Langue requise}"; shift 2;;
    --index-only) MODE='index'; shift;;
    --offline) MODE='offline'; shift;;
    -h|--help) cat <<'HELP'
Usage : ./vosk_mirror.sh [options]
  -o, --output DIR   Répertoire de destination (./vosk-mirror)
  --index-only       Génère index.html et catalog.tsv sans téléchargement
  --offline          Reconstruit index.html à partir de catalog.tsv déjà présent
  --only TEXTE       Télécharge seulement les catégories contenant TEXTE
  -h, --help         Affiche cette aide
Prérequis : bash, curl, wget, perl, sed, iconv (aucun module externe), coreutils
HELP
      exit 0;;
    *) echo "Option inconnue : $1" >&2; exit 2;;
  esac
done
for cmd in curl wget perl sed iconv; do command -v "$cmd" >/dev/null || { echo "Commande manquante: $cmd" >&2; exit 2; }; done
mkdir -p "$OUTPUT"
OUTPUT="$(cd "$OUTPUT" && pwd)"
CATALOG="$OUTPUT/catalog.tsv"

slug() { printf '%s' "$1" | iconv -f UTF-8 -t ASCII//TRANSLIT 2>/dev/null | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//'; }
escape_html() { local t="$1"; t="${t//&/&amp;}"; t="${t//</&lt;}"; t="${t//>/&gt;}"; t="${t//\"/&quot;}"; printf '%s' "$t"; }
# TSV via jq : remplacement des tabulations / sauts de ligne dans les textes.
if [[ "$MODE" != 'offline' ]]; then
  page="$(mktemp)"; rows="$(mktemp)"; trap 'rm -f "${page:-}" "${rows:-}"' EXIT
  echo "Récupération : $SOURCE"
  curl -fL --retry 3 --connect-timeout 20 --max-time 120 -A 'VoskMirrorBash/1.0' -o "$page" "$SOURCE"
  # Une ligne par <tr> avec colonnes séparées par tabulations :
  # type: G = groupe, M = modèle ; première colonne: lien URL éventuel.
  # Analyse les tableaux HTML sans pup, jq ni module Perl additionnel.
  # Perl est utilisé ici pour reconnaître les balises indépendamment de leur mise en lignes.
  perl -0777 - "$page" > "$rows" <<'PERL'
use strict;
use warnings;
use utf8;
use open ':std', ':encoding(UTF-8)';
my $file = shift @ARGV;
open my $fh, '<:encoding(UTF-8)', $file or die "$file: $!";
local $/;
my $source = <$fh>;
sub decode_text {
  my ($v) = @_;
  $v =~ s/<script\b[^>]*>.*?<\/script>//gis;
  $v =~ s/<style\b[^>]*>.*?<\/style>//gis;
  $v =~ s/<[^>]+>/ /gs;
  $v =~ s/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos|nbsp|ndash|mdash|eacute|egrave|agrave|uuml);/entity($1)/gei;
  $v =~ s/[\t\r\n]+/ /g;
  $v =~ s/\s+/ /g;
  $v =~ s/^\s+|\s+$//g;
  return $v;
}
sub entity {
  my ($e) = @_;
  return chr(hex($1)) if $e =~ /^#x([0-9a-f]+)$/i;
  return chr($1) if $e =~ /^#(\d+)$/;
  my %n=(amp=>'&',lt=>'<',gt=>'>',quot=>'"',apos=>"'",nbsp=>' ',ndash=>'–',mdash=>'—',eacute=>'é',egrave=>'è',agrave=>'à',uuml=>'ü');
  return $n{lc $e} // "&$e;";
}
for my $table ($source =~ m{<table\b[^>]*>(.*?)</table\s*>}gis) {
  for my $tr ($table =~ m{<tr\b[^>]*>(.*?)</tr\s*>}gis) {
    my @cells = $tr =~ m{<(?:td|th)\b[^>]*>(.*?)</(?:td|th)\s*>}gis;
    next unless @cells;
    my @values = map { decode_text($_) } @cells;
    my ($link) = $cells[0] =~ m{<a\b[^>]*\bhref\s*=\s*["']([^"']+)["']}i;
    $link //= '';
    if ($link ne '' && $values[0] =~ /^vosk-(?:model|recasepunc)/i) {
      @values = (@values, ('') x 5);
      my @out = ('M', $link, @values[0..4]);
      for (@out) { $_ //= ''; s/[\t\r\n]+/ /g; s/^\s+|\s+$//g; $_ = '—' if $_ eq ''; }
      print join("\t", @out), "\n";
    } elsif ($link eq '' && $values[0] ne '' && $values[0] !~ /^model$/i) {
      print "G\t$values[0]\n";
    }
  }
}
PERL
  printf 'section\tgroup\tname\tsize\tperformance\tnotes\tlicense\turl\tpath\n' > "$CATALOG"
  group=''; count=0
  while IFS=$'\t' read -r kind a b c d e f g; do
    if [[ "$kind" == 'G' ]]; then
      group="$a"; continue
    fi
    [[ "$kind" == 'M' ]] || continue
    url="$a"; name="$b"; size="${c:-}"; perf="${d:-}"; notes="${e:-}"; license="${f:-}"
    [[ -n "$group" ]] || group='Uncategorized'
    # Rejeter les liens ne correspondant pas à une archive Vosk ZIP.
    if [[ "$url" != http://* && "$url" != https://* ]]; then
      if [[ "$url" == /* ]]; then url="https://alphacephei.com$url"; else url="https://alphacephei.com/vosk/$url"; fi
    fi
    filename="${url%%\?*}"; filename="${filename##*/}"
    [[ "$filename" =~ ^[A-Za-z0-9][A-Za-z0-9._+-]*\.zip$ ]] || { echo "Ignoré (pas un ZIP): $url" >&2; continue; }
    section='models'; relgroup="$group"; sub=''
    if [[ "$name" == vosk-recasepunc-* ]]; then section='punctuation'; fi
    if [[ "$name" == vosk-model-spk-* ]]; then section='speaker-identification'; relgroup=''; fi
    if [[ "$relgroup" == *' Other' ]]; then relgroup="${relgroup% Other}"; sub='/other'; fi
    folder="$section"; [[ -z "$relgroup" ]] || folder+="/$(slug "$relgroup")"
    path="$folder$sub/$filename"
    printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$section" "$group" "$name" "$size" "$perf" "$notes" "$license" "$url" "$path" >> "$CATALOG"
    ((count+=1))
  done < "$rows"
  ((count>0)) || { echo 'Aucun ZIP identifié : le HTML a peut-être changé.' >&2; exit 1; }
  echo "$count archives référencées dans $CATALOG"
fi

if [[ "$MODE" == 'download' ]]; then
  while IFS=$'\t' read -r section group name size perf notes license url path; do
    [[ "$section" == 'section' ]] && continue
    if [[ -n "$FILTER" && "${group,,}" != *"${FILTER,,}"* ]]; then continue; fi
    dest="$OUTPUT/$path"
    mkdir -p "$(dirname "$dest")"
    if [[ -s "$dest" ]]; then echo "Déjà présent : $path"; continue; fi
    echo "Téléchargement : $group / $name ($size)"
    # -c reprend les .zip partiellement téléchargés ; -t 3 réessaie en cas d'erreur.
    if ! wget -c -t 3 --timeout=90 --read-timeout=180 -O "$dest" "$url"; then
      echo "ERREUR : $url (le ZIP partiel peut être repris)" >&2
    fi
  done < "$CATALOG"
fi

# Page autonome, sans CDN ni dépendance JavaScript externe.
INDEX="$OUTPUT/index.html"
cat > "$INDEX" <<'HTML'
<!doctype html><html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Vosk — catalogue local</title><style>
body{font:15px system-ui, sans-serif;background:#f5f7fb;color:#142338;margin:0 auto;padding:24px;max-width:1400px}
header{background:#172e4e;color:#fff;padding:25px;border-radius:14px}h1{margin:0 0 8px}header a{color:#b5e1ff}
input{margin:20px 0;padding:12px;border:1px solid #bdcadb;border-radius:8px;width:min(650px,90%)}
section{background:white;border:1px solid #e1e6f0;border-radius:12px;margin:15px 0;padding:16px;overflow-x:auto}
h2{margin:4px 0 14px}table{border-collapse:collapse;width:100%;font-size:13px}th{text-align:left;background:#edf3fa}
th,td{padding:10px;border-bottom:1px solid #e8edf4;vertical-align:top}td:first-child{min-width:210px}td:nth-child(4){min-width:200px}
a{color:#135db3}.yes{color:#087849;font-weight:bold}.no{color:#a04c19}small{color:#a8bed4}
</style></head><body><header><h1>Catalogue local Vosk</h1><p>Classement par langue et catégorie, fidèle aux rubriques de la <a href="https://alphacephei.com/vosk/models">source officielle</a>.</p></header>
<input type="search" id="search" placeholder="Rechercher une langue, une licence ou un modèle…"><main>
HTML
old_group=''; old_section=''; open_table=0; total=0; available=0
while IFS=$'\t' read -r section group name size perf notes license url path; do
  [[ "$section" == 'section' ]] && continue
  if [[ "$group" != "$old_group" || "$section" != "$old_section" ]]; then
    if ((open_table)); then printf '</tbody></table></section>\n' >> "$INDEX"; fi
    printf '<section><h2>%s</h2><table><thead><tr><th>Modèle</th><th>Taille</th><th>Performance / WER</th><th>Description</th><th>Licence</th><th>Archive locale</th></tr></thead><tbody>\n' "$(escape_html "$group")" >> "$INDEX"
    old_group="$group";old_section="$section";open_table=1
  fi
  ((total+=1))
  link='<span class="no">Non téléchargé</span>'
  if [[ -s "$OUTPUT/$path" ]]; then
    link="<a class=\"yes\" href=\"$(escape_html "$path")\" download>Télécharger le ZIP</a>"
    ((available+=1))
  fi
  printf '<tr><td><strong>%s</strong></td><td>%s</td><td>%s</td><td>%s</td><td>%s</td><td>%s</td></tr>\n' \
   "$(escape_html "$name")" "$(escape_html "$size")" "$(escape_html "$perf")" "$(escape_html "$notes")" "$(escape_html "$license")" "$link" >> "$INDEX"
done < "$CATALOG"
if ((open_table)); then printf '</tbody></table></section>\n' >> "$INDEX"; fi
cat >> "$INDEX" <<'HTML'
</main><script>
document.getElementById('search').oninput=function(){const q=this.value.toLowerCase();document.querySelectorAll('section').forEach(s=>{let found=false;s.querySelectorAll('tbody tr').forEach(tr=>{const yes=tr.textContent.toLowerCase().includes(q)||s.querySelector('h2').textContent.toLowerCase().includes(q);tr.hidden=!yes;found ||=yes});s.hidden=!found})};
</script></body></html>
HTML
printf 'Terminé : %s modèles, %s ZIP disponibles.\nIndex : %s\nCatalogue TSV : %s\n' "$total" "$available" "$INDEX" "$CATALOG"
