# ============================================================ # Omika - Completion des packs de langue deja exportes # ============================================================ # Les packs exportes avant l'introduction du vocabulaire de modele contiennent # quatre fichiers : encoder_model.onnx, decoder_model.onnx, source.spm, # target.spm. Il en manque deux, sans lesquels le moteur ne peut pas traduire # correctement : # # vocab.json - les identifiants REELLEMENT attendus par le modele. Le .spm # numerote ses pieces dans un ordre qui lui est propre : sur # opus-mt-es-fr, la piece Ho vaut 5293 pour le .spm mais # 584 pour le modele. Envoyer les identifiants du .spm revient a # faire lire un autre texte, sans la moindre erreur levee. # config.json - l'amorce et la fin du decodeur (74821 et 0 sur opus-mt-es-fr), # que le .spm ne contient pas non plus. # # Ces deux fichiers pesent quelques mega-octets : rien n'est reexporte, les poids # deja sur le disque (23 Gio) restent valables. # # GARDE-FOU : le modele d'origine n'est pas note dans le pack. Il est deduit du # code de langue et du sens (es + versFrancais -> opus-mt-es-fr), puis VERIFIE : # le source.spm telecharge doit etre identique, octet pour octet, a celui du # pack. Sans cette comparaison, on attacherait le vocabulaire d'un autre modele # - exactement le defaut que ce script repare, en pire. # # Usage : # .\scripts\Completer-Packs-Langues.ps1 -Verifier # .\scripts\Completer-Packs-Langues.ps1 # .\scripts\Completer-Packs-Langues.ps1 -Codes "es,pt,it" # ============================================================ [CmdletBinding()] param( # Racine des packs. Par defaut : \packs [string]$RacinePacks = '', # N'ecrit rien : dit seulement quels packs sont deja alignes ou non. [switch]$Verifier, # Codes de langue a traiter (ex. "es,pt"). Vide = tous les dossiers trouves. [string]$Codes = '' ) $ErrorActionPreference = 'Stop' # --- Sortie ---------------------------------------------------------------- function Write-Etape { param([string]$Message) Write-Host ''; Write-Host $Message -ForegroundColor Cyan } function Write-Info { param([string]$Message) Write-Host (' ' + $Message) } function Write-Ok { param([string]$Message) Write-Host (' [OK] ' + $Message) -ForegroundColor Green } function Write-Alerte{ param([string]$Message) Write-Host (' [ALERTE] ' + $Message) -ForegroundColor Yellow } function Write-Ecart { param([string]$Message) Write-Host (' [ECART] ' + $Message) -ForegroundColor DarkYellow } function Stop-AvecMessage { param([string]$Message) Write-Host '' Write-Host '=== ARRET ===' -ForegroundColor Red Write-Host $Message -ForegroundColor Red exit 1 } # --- Logique --------------------------------------------------------------- # Nom du depot HuggingFace correspondant a un pack. Convention de l'exporteur : # versFrancais = -> francais, depuisFrancais = francais -> . function Get-NomModele { param( [Parameter(Mandatory = $true)][string]$Code, [Parameter(Mandatory = $true)][string]$Sens, [string]$Auteur = 'Helsinki-NLP' ) $codeNormalise = $Code.Trim().ToLowerInvariant() if ($Sens -eq 'versFrancais') { return ('{0}/opus-mt-{1}-fr' -f $Auteur, $codeNormalise) } return ('{0}/opus-mt-fr-{1}' -f $Auteur, $codeNormalise) } # Empreinte SHA-256 en minuscules, comparable telle quelle. function Get-Empreinte { param([Parameter(Mandatory = $true)][string]$Chemin) return (Get-FileHash -LiteralPath $Chemin -Algorithm SHA256).Hash.ToLowerInvariant() } # Inventaire des couples (code, sens) presents sur le disque. function Get-PacksPresents { param( [Parameter(Mandatory = $true)][string]$Racine, [string[]]$CodesFiltres = @() ) $presents = New-Object 'System.Collections.Generic.List[object]' if (-not (Test-Path -LiteralPath $Racine -PathType Container)) { return $presents } foreach ($dossierLangue in @(Get-ChildItem -LiteralPath $Racine -Directory | Sort-Object -Property Name)) { $code = $dossierLangue.Name # Dossiers techniques (.hf-cache) et fichiers d'essai : jamais des langues. if ($code.StartsWith('.')) { continue } if ($CodesFiltres.Count -gt 0 -and $CodesFiltres -notcontains $code) { continue } foreach ($dossierSens in @(Get-ChildItem -LiteralPath $dossierLangue.FullName -Directory | Sort-Object -Property Name)) { $sens = $dossierSens.Name if ($sens -ne 'versFrancais' -and $sens -ne 'depuisFrancais') { continue } # Un pack FAMILIAL (modele multilingue) range ses poids dans # \commun\ et ne laisse qu'un modele.json par sens : il n'a # aucun source.spm par sens, donc RIEN a aligner. Sans ce garde-fou # on lui cherchait une paire bilingue qui n'existe pas et on le # declarait « ecarte » a tort. $spm = Join-Path $dossierSens.FullName 'source.spm' if (-not (Test-Path -LiteralPath $spm -PathType Leaf)) { continue } $presents.Add([pscustomobject]@{ Code = $code Sens = $sens Dossier = $dossierSens.FullName Source = $spm }) } } return $presents } # Complete un pack. $Recuperer recoit (modele, fichier, destination) et doit # renvoyer $true si le fichier a ete obtenu. Il est injecte pour que les tests # s'executent sans reseau. function Invoke-Completion { param( [Parameter(Mandatory = $true)][string]$Racine, [Parameter(Mandatory = $true)][scriptblock]$Recuperer, [switch]$SansEcrire, [string[]]$CodesFiltres = @() ) $bilan = [pscustomobject]@{ Packs = 0 Alignes = 0 Ecartes = 0 Echecs = 0 Ecrits = 0 } $packs = @(Get-PacksPresents -Racine $Racine -CodesFiltres $CodesFiltres) if ($packs.Count -eq 0) { Write-Alerte ("aucun pack trouve sous {0}" -f $Racine) return $bilan } $bac = Join-Path ([System.IO.Path]::GetTempPath()) ('omika-completion-' + [guid]::NewGuid().ToString('N').Substring(0, 8)) $null = New-Item -ItemType Directory -Path $bac -Force try { foreach ($pack in $packs) { $bilan.Packs++ $modele = Get-NomModele -Code $pack.Code -Sens $pack.Sens $nomCourt = '{0}/{1}' -f $pack.Code, $pack.Sens $spmTelecharge = Join-Path $bac ('{0}-{1}-source.spm' -f $pack.Code, $pack.Sens) $vocabTelecharge = Join-Path $bac ('{0}-{1}-vocab.json' -f $pack.Code, $pack.Sens) $configTelecharge = Join-Path $bac ('{0}-{1}-config.json' -f $pack.Code, $pack.Sens) $obtenus = $true foreach ($paire in @( @{ Fichier = 'source.spm'; Destination = $spmTelecharge }, @{ Fichier = 'vocab.json'; Destination = $vocabTelecharge }, @{ Fichier = 'config.json'; Destination = $configTelecharge })) { if (-not (& $Recuperer $modele $paire.Fichier $paire.Destination)) { $obtenus = $false Write-Alerte ("{0} : {1} introuvable pour {2}" -f $nomCourt, $paire.Fichier, $modele) break } } if (-not $obtenus) { $bilan.Echecs++ continue } # LE garde-fou : le modele deduit doit etre celui du pack. $empreinteLocale = Get-Empreinte -Chemin $pack.Source $empreinteDistante = Get-Empreinte -Chemin $spmTelecharge if ($empreinteLocale -ne $empreinteDistante) { Write-Ecart ("{0} : {1} n'est PAS le modele de ce pack (source.spm different) - non complete" -f $nomCourt, $modele) $bilan.Ecartes++ continue } $bilan.Alignes++ if ($SansEcrire) { Write-Ok ("{0} : aligne sur {1}" -f $nomCourt, $modele) continue } Copy-Item -LiteralPath $vocabTelecharge -Destination (Join-Path $pack.Dossier 'vocab.json') -Force Copy-Item -LiteralPath $configTelecharge -Destination (Join-Path $pack.Dossier 'config.json') -Force $bilan.Ecrits++ Write-Ok ("{0} : vocabulaire et configuration ajoutes ({1})" -f $nomCourt, $modele) } } finally { Remove-Item -LiteralPath $bac -Recurse -Force -ErrorAction SilentlyContinue } return $bilan } # --- Execution ------------------------------------------------------------- # Les tests dot-sourcent ce fichier pour appeler les fonctions : dans ce cas, # rien ne s'execute. Seul un lancement direct declenche le traitement. if ($MyInvocation.InvocationName -ne '.') { if ([string]::IsNullOrWhiteSpace($RacinePacks)) { $RacinePacks = Join-Path (Split-Path -Parent $PSScriptRoot) 'packs' } $codesFiltres = @() if (-not [string]::IsNullOrWhiteSpace($Codes)) { $codesFiltres = @($Codes -split '[,;]' | ForEach-Object { $_.Trim().ToLowerInvariant() } | Where-Object { $_ }) } Write-Host '' Write-Host '=== Omika - Completion des packs de langue ===' -ForegroundColor Cyan Write-Info ('Racine des packs : {0}' -f $RacinePacks) Write-Info ('Mode : {0}' -f $(if ($Verifier) { 'verification seule (aucune ecriture)' } else { 'ecriture' })) if ($codesFiltres.Count -gt 0) { Write-Info ('Langues : {0}' -f ($codesFiltres -join ', ')) } # Recuperation reelle : HuggingFace, sans jeton (les depots sont publics). $recuperer = { param($modele, $fichier, $destination) $uri = 'https://huggingface.co/{0}/resolve/main/{1}' -f $modele, $fichier try { Invoke-WebRequest -Uri $uri -OutFile $destination -UseBasicParsing -ErrorAction Stop return (Test-Path -LiteralPath $destination -PathType Leaf) } catch { return $false } } $bilan = Invoke-Completion -Racine $RacinePacks -Recuperer $recuperer -SansEcrire:$Verifier -CodesFiltres $codesFiltres Write-Etape '=== RESULTAT ===' Write-Info ('Packs examines : {0}' -f $bilan.Packs) Write-Info ('Alignes : {0}' -f $bilan.Alignes) Write-Info ('Completes : {0}' -f $bilan.Ecrits) Write-Info ('Ecartes (autre modele) : {0}' -f $bilan.Ecartes) Write-Info ('Echecs : {0}' -f $bilan.Echecs) if ($bilan.Echecs -gt 0) { Stop-AvecMessage ("{0} pack(s) n'ont pas pu etre completes (reseau ou depots introuvables).`n" -f $bilan.Echecs + " Relancer la MEME commande : les packs deja completes ne sont pas retraites.") } if ($bilan.Ecartes -gt 0) { Write-Alerte 'Les packs ecartes viennent d''une autre famille de modeles que celle deduite du code.' Write-Alerte 'Ils doivent etre reexportes avec scripts\Exporter-Packs-Langues.ps1 (le modele d''origine y sera note).' } exit 0 }