# ============================================================ # Omika - Generation / verification du manifeste des packs de langue # (manifest.json de Omika.Langues.Api) # ============================================================ # Le manifeste decrit les packs ONNX telechargeables par le mobile. Ce n'est PAS # un fichier de publication : il decrit les fichiers REELLEMENT presents sous # Langues__RootPath. Il se genere donc la ou sont les modeles, pas dans le depot. # # Arborescence attendue : \\\, ou vaut # versFrancais, depuisFrancais, ou commun (poids partages par les deux sens d'un # modele familial ; le mobile ne les telecharge qu'une fois). # # Deux champs ne peuvent pas etre saisis a la main sans risque : # - tailleOctets : taille reelle (progression du telechargement mobile) ; # - sha256 : empreinte d'integrite, reverifiee par le mobile APRES # telechargement. Une empreinte fausse ou absente ne se voit # QUE sur le telephone : le pack est refuse et pack.json # n'est jamais ecrit (aucune erreur ne remonte au serveur). # Ce script les calcule depuis les fichiers. Aucune valeur n'est inventee. # # Usage : # .\New-OmikaLanguesManifeste.ps1 # genere sous RootPath # .\New-OmikaLanguesManifeste.ps1 -WhatIf # simulation, affiche # .\New-OmikaLanguesManifeste.ps1 -RootPathLangues D:\Modeles # .\New-OmikaLanguesManifeste.ps1 -Verifier # controle d'integrite # .\New-OmikaLanguesManifeste.ps1 -Libelles @{ es = 'Espagnol (Espagne)' } # # -Verifier compare le manifeste EXISTANT aux fichiers du disque et sort en # erreur (code 1) au moindre ecart. Aucun droit administrateur n'est requis : # c'est le controle a passer apres chaque ajout ou remplacement de modele. # # La racine par defaut est lue dans Langues__RootPath (MACHINE), donc la meme # source de verite que le service : le manifeste est ecrit la ou l'API le lit. # ============================================================ [CmdletBinding(SupportsShouldProcess = $true)] param( # Dossier racine des modeles. Par defaut : variable MACHINE Langues__RootPath, # puis C:\Omika\Data\Langues (recommandation du DEPLOIEMENT_API_LANGUES.md §6). [string]$RootPathLangues, # Fichier manifeste. Par defaut : \manifest.json. [string]$Sortie, # Version ecrite dans chaque pack (le mobile la compare a celle installee). [string]$Version = '1.0.0', # Libelles affiches, par code de langue. Complete/ecrase la table par defaut. [hashtable]$Libelles = @{}, # Autorise l'ecriture d'un manifeste SANS aucun pack (deconseille : le mobile # affiche alors « Aucun modele de langue n'est disponible sur le serveur »). [switch]$AutoriserVide, # Controle le manifeste existant au lieu de le regenerer. [switch]$Verifier ) $ErrorActionPreference = 'Stop' # Sens canoniques : le mobile compare ces valeurs de facon SENSIBLE A LA CASSE. # « commun » porte les poids PARTAGES par les deux sens d'un modele familial # (opus-mt-tc-big-itc-itc) : le telephone les telecharge une seule fois, et # chaque sens ne contient que son modele.json (jeton de cible). $SensCanoniques = @('versFrancais', 'depuisFrancais', 'commun') # Noms de vocabulaire reconnus par l'application (OnnxTranslationService) : # vocab.spm = vocabulaire commun, source.spm / target.spm = vocabulaire par # langue d'un modele Marian/OPUS-MT. Tout autre nom .spm n'est qu'un repli. $NomsVocabulaire = @('vocab.spm', 'source.spm', 'target.spm') $LibellesParDefaut = @{ 'ar' = 'Arabe' 'bg' = 'Bulgare' 'ca' = 'Catalan' 'cs' = 'Tchèque' 'da' = 'Danois' 'de' = 'Allemand' 'el' = 'Grec' 'en' = 'Anglais' 'es' = 'Espagnol' 'et' = 'Estonien' 'fi' = 'Finnois' 'gl' = 'Galicien' 'hr' = 'Croate' 'hu' = 'Hongrois' 'is' = 'Islandais' 'it' = 'Italien' 'lt' = 'Lituanien' 'lv' = 'Letton' 'mk' = 'Macédonien' 'mt' = 'Maltais' 'nl' = 'Néerlandais' 'no' = 'Norvégien' 'pl' = 'Polonais' 'pt' = 'Portugais' 'ro' = 'Roumain' 'ru' = 'Russe' 'sk' = 'Slovaque' 'sl' = 'Slovène' 'sv' = 'Suédois' 'tr' = 'Turc' 'uk' = 'Ukrainien' 'zh' = 'Chinois' } function Write-ManifesteInfo([string]$message) { Write-Host (' ' + $message) } function Write-ManifesteOk([string]$message) { Write-Host ('[OK] ' + $message) -ForegroundColor Green } function Write-ManifesteAlerte([string]$message) { Write-Host ('[ALERTE] ' + $message) -ForegroundColor Yellow } function Write-ManifesteEchec([string]$message) { Write-Host ('[ECHEC] ' + $message) -ForegroundColor Red } function Format-ManifesteTaille([long]$octets) { if ($octets -ge 1073741824) { return ('{0:N2} Gio' -f ($octets / 1073741824)) } if ($octets -ge 1048576) { return ('{0:N1} Mio' -f ($octets / 1048576)) } if ($octets -ge 1024) { return ('{0:N0} Kio' -f ($octets / 1024)) } return ('{0} octets' -f $octets) } # Code de langue : memes regles que LanguagePaths.EstCodeValide cote application # (2 a 10 caracteres, minuscules ASCII, chiffres, tiret). Un code invalide fait # IGNORER le pack SILENCIEUSEMENT par le mobile : on refuse de l'ecrire. function Test-ManifesteCodeValide([string]$code) { if ([string]::IsNullOrWhiteSpace($code)) { return $false } $valeur = $code.Trim() if ($valeur.Length -lt 2 -or $valeur.Length -gt 10) { return $false } if ($valeur[0] -eq '-' -or $valeur[-1] -eq '-') { return $false } foreach ($c in $valeur.ToCharArray()) { $autorise = (($c -ge 'a' -and $c -le 'z') -or ($c -ge '0' -and $c -le '9') -or $c -eq '-') if (-not $autorise) { return $false } } return $true } # Retourne le sens canonique, ou $null si le dossier n'est pas un sens connu. function Resolve-ManifesteSens([string]$nomDossier) { foreach ($canonique in $SensCanoniques) { if ([string]::Equals($nomDossier, $canonique, [System.StringComparison]::OrdinalIgnoreCase)) { return $canonique } } return $null } function Resolve-ManifesteLibelle([string]$code, [hashtable]$surcharge) { if ($surcharge -and $surcharge.ContainsKey($code)) { return [string]$surcharge[$code] } if ($LibellesParDefaut.ContainsKey($code)) { return [string]$LibellesParDefaut[$code] } return $code.ToUpperInvariant() } # SHA-256 hexadecimal minuscule : exactement ce que le mobile recalcule. function Get-ManifesteEmpreinte([string]$chemin) { return (Get-FileHash -LiteralPath $chemin -Algorithm SHA256).Hash.ToLowerInvariant() } # Echappe une chaine pour du JSON (guillemets, antislash, caracteres de controle). function ConvertTo-ManifesteJsonChaine([string]$valeur) { $sb = New-Object System.Text.StringBuilder [void]$sb.Append('"') foreach ($c in $valeur.ToCharArray()) { $code = [int]$c if ($c -eq '"') { [void]$sb.Append('\"') } elseif ($c -eq '\') { [void]$sb.Append('\\') } elseif ($code -lt 32) { [void]$sb.Append(('\u{0:x4}' -f $code)) } else { [void]$sb.Append($c) } } [void]$sb.Append('"') return $sb.ToString() } function New-ManifesteBlocFichier([string]$sens, [string]$nomFichier, [long]$taille, [string]$sha256) { $lignes = @( ' {' (' "sens": ' + (ConvertTo-ManifesteJsonChaine $sens) + ',') (' "nomFichier": ' + (ConvertTo-ManifesteJsonChaine $nomFichier) + ',') (' "tailleOctets": ' + $taille + ',') (' "sha256": ' + (ConvertTo-ManifesteJsonChaine $sha256)) ' }' ) return ($lignes -join "`r`n") } function New-ManifesteBlocPack([string]$code, [string]$libelle, [string]$version, $blocsFichiers) { $lignes = @( ' {' (' "code": ' + (ConvertTo-ManifesteJsonChaine $code) + ',') (' "libelle": ' + (ConvertTo-ManifesteJsonChaine $libelle) + ',') (' "version": ' + (ConvertTo-ManifesteJsonChaine $version) + ',') ' "fichiers": [' ($blocsFichiers -join (",`r`n")) ' ]' ' }' ) return ($lignes -join "`r`n") } function New-ManifesteJsonText($blocsPacks) { # Tableau vide ecrit « [] » et non « [\n\n] » : lisible et sans ligne blanche. $corps = if ($blocsPacks.Count -eq 0) { @(' "packs": []') } else { @( ' "packs": [' ($blocsPacks -join (",`r`n")) ' ]' ) } $lignes = @('{') + $corps + @('}') return (($lignes -join "`r`n") + "`r`n") } # JSON ecrit en UTF-8 SANS BOM : un manifeste est un fichier de donnees lu par # l'API et par des outils externes, jamais un script. function Write-ManifesteFichier([string]$chemin, [string]$contenu) { $encodage = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($chemin, $contenu, $encodage) } function Write-ManifesteBilan($echecs, $alertes) { foreach ($alerte in $alertes) { Write-ManifesteAlerte $alerte } foreach ($echec in $echecs) { Write-ManifesteEchec $echec } Write-Host '' if ($echecs.Count -gt 0) { Write-Host ("=== RESULTAT : {0} ecart(s), {1} alerte(s) ===" -f $echecs.Count, $alertes.Count) -ForegroundColor Red return $false } Write-Host ("=== RESULTAT : conforme ({0} alerte(s)) ===" -f $alertes.Count) -ForegroundColor Green return $true } # --- Racine effective -------------------------------------------------------- if (-not $RootPathLangues) { $RootPathLangues = [Environment]::GetEnvironmentVariable('Langues__RootPath', 'Machine') } if (-not $RootPathLangues) { $RootPathLangues = [Environment]::GetEnvironmentVariable('Langues__RootPath', 'Process') } if (-not $RootPathLangues) { $RootPathLangues = 'C:\Omika\Data\Langues' } $racine = $RootPathLangues.Trim().TrimEnd([char[]]@('\', '/')) if (-not $Sortie) { $Sortie = Join-Path $racine 'manifest.json' } Write-Host '=== Manifeste des packs de langue (Omika Langues API) ===' Write-ManifesteInfo ("Racine des modeles : {0}" -f $racine) Write-ManifesteInfo ("Manifeste : {0}" -f $Sortie) if ($Verifier) { Write-ManifesteInfo 'Mode : VERIFICATION (aucune ecriture)' } Write-Host '' if (-not (Test-Path -LiteralPath $racine -PathType Container)) { Write-ManifesteEchec ("Racine introuvable : {0}" -f $racine) Write-ManifesteInfo 'Verifier Langues__RootPath (MACHINE), ou passer -RootPathLangues.' exit 1 } $echecs = New-Object 'System.Collections.Generic.List[string]' $alertes = New-Object 'System.Collections.Generic.List[string]' # ============================================================================ # MODE VERIFICATION # ============================================================================ if ($Verifier) { if (-not (Test-Path -LiteralPath $Sortie -PathType Leaf)) { Write-ManifesteEchec ("Manifeste ABSENT : {0}" -f $Sortie) Write-ManifesteInfo 'Sans ce fichier, GET /api/langues repond 404 (et le mobile n''a aucune langue).' Write-ManifesteInfo 'Generer avec : .\New-OmikaLanguesManifeste.ps1' exit 1 } $manifeste = $null try { $manifeste = (Get-Content -LiteralPath $Sortie -Raw -Encoding UTF8 | ConvertFrom-Json) } catch { Write-ManifesteEchec ("Manifeste illisible (JSON invalide) : {0}" -f $_.Exception.Message) Write-ManifesteInfo 'Cote service : « Manifeste des langues illisible » puis 404.' exit 1 } $packs = @($manifeste.packs) if ($packs.Count -eq 0) { $echecs.Add('Le manifeste ne declare aucun pack : le mobile affichera « Aucun modele de langue n''est disponible sur le serveur ».') } $declare = New-Object 'System.Collections.Generic.List[string]' $nbFichiers = 0 $octetsTotal = [long]0 foreach ($pack in $packs) { $code = ([string]$pack.code).Trim().ToLowerInvariant() if (-not (Test-ManifesteCodeValide $code)) { $echecs.Add(("Pack « {0} » : code de langue invalide -> IGNORE SILENCIEUSEMENT par le mobile." -f $pack.code)) continue } if ([string]::IsNullOrWhiteSpace([string]$pack.libelle)) { $alertes.Add(("Pack « {0} » : libelle vide (le code s'affichera a la place)." -f $code)) } if ([string]::IsNullOrWhiteSpace([string]$pack.version)) { $alertes.Add(("Pack « {0} » : version vide (comparaison d'installation impossible)." -f $code)) } $fichiers = @($pack.fichiers) if ($fichiers.Count -eq 0) { $echecs.Add(("Pack « {0} » : aucun fichier declare." -f $code)) continue } foreach ($fichier in $fichiers) { $sensManifeste = [string]$fichier.sens $sens = Resolve-ManifesteSens $sensManifeste if (-not $sens) { $echecs.Add(("Pack « {0} » : sens inconnu « {1} » (attendu versFrancais, depuisFrancais ou commun)." -f $code, $sensManifeste)) continue } if ($sensManifeste -cne $sens) { $echecs.Add(("Pack « {0} » : sens « {1} » a la mauvaise casse -> REFUSE par le mobile (comparaison sensible a la casse)." -f $code, $sensManifeste)) continue } $nomFichier = [string]$fichier.nomFichier if ([string]::IsNullOrWhiteSpace($nomFichier)) { $echecs.Add(("Pack « {0} » : nomFichier vide." -f $code)) continue } if ($nomFichier -ne [System.IO.Path]::GetFileName($nomFichier)) { $echecs.Add(("Pack « {0} » : nomFichier « {1} » contient un chemin (interdit)." -f $code, $nomFichier)) continue } $chemin = Join-Path (Join-Path (Join-Path $racine $code) $sens) $nomFichier $declare.Add($chemin.ToLowerInvariant()) $relatif = ('{0}/{1}/{2}' -f $code, $sens, $nomFichier) # Un fichier declare est un fichier TELECHARGE par chaque telephone : # signaler ceux qui ne servent a rien. if ($nomFichier.EndsWith('.onnx', [System.StringComparison]::OrdinalIgnoreCase) ` -and $nomFichier -match 'past') { $alertes.Add(("{0} : decodeur a cache (past_key_values) IGNORE par l'application, mais declare donc TELECHARGE pour rien." -f $relatif)) } elseif ($nomFichier.EndsWith('.spm', [System.StringComparison]::OrdinalIgnoreCase) ` -and $NomsVocabulaire -notcontains $nomFichier) { $alertes.Add(("{0} : nom de vocabulaire non reconnu (attendu vocab.spm, source.spm ou target.spm) : pris comme repli « premier .spm »." -f $relatif)) } if (-not (Test-Path -LiteralPath $chemin -PathType Leaf)) { $echecs.Add(("Fichier ABSENT sur le disque : {0} -> le mobile recevra 404." -f $relatif)) continue } $info = Get-Item -LiteralPath $chemin $taille = [long]$fichier.tailleOctets if ($taille -le 0) { $alertes.Add(("{0} : tailleOctets non renseignee (0) -> pas de controle de taille ni de progression." -f $relatif)) } elseif ($taille -ne $info.Length) { $echecs.Add(("{0} : taille differente -> manifeste {1} octet(s), disque {2}." -f $relatif, $taille, $info.Length)) } $sha = ([string]$fichier.sha256).Trim() if ([string]::IsNullOrWhiteSpace($sha)) { $echecs.Add(("{0} : empreinte SHA-256 absente -> le pack sera REFUSE par le mobile." -f $relatif)) continue } $reel = Get-ManifesteEmpreinte $chemin if (-not [string]::Equals($sha, $reel, [System.StringComparison]::OrdinalIgnoreCase)) { $echecs.Add(("{0} : empreinte differente ->`r`n manifeste {1}`r`n disque {2}" -f $relatif, $sha, $reel)) } $nbFichiers++ $octetsTotal += $info.Length } } # Fichiers presents mais non declares : jamais telecharges, poids mort. foreach ($fichierDisque in @(Get-ChildItem -LiteralPath $racine -Recurse -File)) { if ($fichierDisque.FullName.ToLowerInvariant() -eq $Sortie.ToLowerInvariant()) { continue } # La sauvegarde du manifeste ecrite par ce script n'est evidemment pas un # fichier de modele : la signaler serait un faux positif a chaque controle. if ($fichierDisque.FullName.ToLowerInvariant() -eq ($Sortie + '.bak').ToLowerInvariant()) { continue } $relatifDisque = $fichierDisque.FullName.Substring($racine.Length).TrimStart('\', '/') # Dossiers techniques (ex. .hf-cache du script d'export, .tmp-*) : ce ne # sont pas des modeles, et un cache peut contenir des milliers de fichiers. if ($relatifDisque -match '(^|\\)\.[^\\]') { continue } if (-not $declare.Contains($fichierDisque.FullName.ToLowerInvariant())) { $alertes.Add(("Fichier non declare dans le manifeste (jamais telecharge) : {0}" -f $relatifDisque)) } } Write-ManifesteInfo ("{0} pack(s), {1} fichier(s) verifie(s), {2}" -f $packs.Count, $nbFichiers, (Format-ManifesteTaille $octetsTotal)) Write-Host '' if (Write-ManifesteBilan $echecs $alertes) { exit 0 } else { exit 1 } } # ============================================================================ # MODE GENERATION # ============================================================================ $dossiersPacks = @(Get-ChildItem -LiteralPath $racine -Directory | Where-Object { -not $_.Name.StartsWith('.') } | Sort-Object -Property Name) if ($dossiersPacks.Count -eq 0) { $message = ("Aucun sous-dossier de langue sous {0} (attendu : {0}\es\versFrancais\encoder.onnx)." -f $racine) # -AutoriserVide couvre les DEUX causes de manifeste vide : racine sans # dossier de langue, et dossiers sans aucun fichier exploitable. if ($AutoriserVide) { $alertes.Add($message) } else { $echecs.Add($message) } } $blocsPacks = New-Object 'System.Collections.Generic.List[string]' $recap = New-Object 'System.Collections.Generic.List[string]' foreach ($dossierPack in $dossiersPacks) { $nomDossier = $dossierPack.Name $code = $nomDossier.Trim().ToLowerInvariant() if (-not (Test-ManifesteCodeValide $code)) { $alertes.Add(("Dossier ignore « {0} » : code de langue invalide (2 a 10 caracteres, minuscules a-z, chiffres, tiret)." -f $nomDossier)) continue } # -cne : comparaison SENSIBLE A LA CASSE. « -ne » considererait « ru » et # « RU » comme egaux et ne signalerait rien. if ($code -cne $nomDossier) { $alertes.Add(("Dossier « {0} » : le manifeste ecrit « {1} ». Renommer le dossier si le volume peut etre sensible a la casse." -f $nomDossier, $code)) } $libelle = Resolve-ManifesteLibelle $code $Libelles $blocsFichiers = New-Object 'System.Collections.Generic.List[string]' $nbFichiersPack = 0 $octetsPack = [long]0 foreach ($dossierSens in @(Get-ChildItem -LiteralPath $dossierPack.FullName -Directory | Sort-Object -Property Name)) { $sens = Resolve-ManifesteSens $dossierSens.Name if (-not $sens) { $alertes.Add(("Dossier ignore {0}\{1} : sens inconnu (attendu versFrancais, depuisFrancais ou commun)." -f $nomDossier, $dossierSens.Name)) continue } if ($sens -cne $dossierSens.Name) { $alertes.Add(("Dossier {0}\{1} : le manifeste ecrit « {2} » (casse canonique exigee par le mobile)." -f $nomDossier, $dossierSens.Name, $sens)) } # L'API ne sert que {code}/{sens}/{fichier} : un sous-dossier n'est jamais atteignable. foreach ($sousDossier in @(Get-ChildItem -LiteralPath $dossierSens.FullName -Directory)) { $alertes.Add(("Dossier ignore {0}\{1}\{2} : l'API ne sert que {{code}}/{{sens}}/{{fichier}}." -f $nomDossier, $dossierSens.Name, $sousDossier.Name)) } foreach ($fichier in @(Get-ChildItem -LiteralPath $dossierSens.FullName -File | Sort-Object -Property Name)) { # Deux fichiers inutiles qu'il ne faut PAS livrer : declares ici, ils # seraient telecharges par chaque telephone pour rien. if ($fichier.Name.EndsWith('.onnx', [System.StringComparison]::OrdinalIgnoreCase) ` -and $fichier.Name -match 'past') { $alertes.Add(("{0}\{1}\{2} (~{3}) : decodeur a cache (past_key_values) IGNORE par l'application, mais il serait TELECHARGE. Ne pas livrer ce fichier." -f $nomDossier, $dossierSens.Name, $fichier.Name, (Format-ManifesteTaille $fichier.Length))) } elseif ($fichier.Name.EndsWith('.spm', [System.StringComparison]::OrdinalIgnoreCase) ` -and $NomsVocabulaire -notcontains $fichier.Name) { $alertes.Add(("{0}\{1}\{2} : nom de vocabulaire non reconnu (attendu vocab.spm, source.spm ou target.spm). L'application le prendrait comme repli « premier .spm » : verifier qu'il est bien le bon." -f $nomDossier, $dossierSens.Name, $fichier.Name)) } $sha = Get-ManifesteEmpreinte $fichier.FullName $blocsFichiers.Add((New-ManifesteBlocFichier -sens $sens -nomFichier $fichier.Name -taille $fichier.Length -sha256 $sha)) $nbFichiersPack++ $octetsPack += $fichier.Length } } if ($nbFichiersPack -eq 0) { $alertes.Add(("Pack « {0} » ignore : aucun fichier utilisable." -f $code)) continue } $blocsPacks.Add((New-ManifesteBlocPack -code $code -libelle $libelle -version $Version -blocsFichiers $blocsFichiers)) $recap.Add((' {0,-6} {1,-16} {2,3} fichier(s) {3,12}' -f $code, $libelle, $nbFichiersPack, (Format-ManifesteTaille $octetsPack))) } if ($blocsPacks.Count -eq 0 -and -not $AutoriserVide) { $echecs.Add('Aucun pack exploitable : manifeste NON ecrit (un manifeste vide ferait afficher au mobile « Aucun modele de langue n''est disponible sur le serveur »).') $echecs.Add('Corriger l''arborescence, ou forcer avec -AutoriserVide si c''est volontaire.') } if ($echecs.Count -gt 0) { Write-ManifesteBilan $echecs $alertes | Out-Null exit 1 } $json = New-ManifesteJsonText $blocsPacks foreach ($ligne in $recap) { Write-ManifesteInfo $ligne } Write-Host '' if (-not $PSCmdlet.ShouldProcess($Sortie, 'Ecriture du manifeste des packs de langue')) { Write-ManifesteInfo 'Simulation (-WhatIf) : aucun fichier ecrit. Contenu qui aurait ete ecrit :' Write-Host '' Write-Host $json Write-Host '' Write-ManifesteBilan $echecs $alertes | Out-Null exit 0 } if (Test-Path -LiteralPath $Sortie -PathType Leaf) { $sauvegarde = $Sortie + '.bak' Copy-Item -LiteralPath $Sortie -Destination $sauvegarde -Force Write-ManifesteInfo ("Manifeste precedent conserve : {0}" -f $sauvegarde) } Write-ManifesteFichier -chemin $Sortie -contenu $json Write-ManifesteOk ("Manifeste ecrit : {0}" -f $Sortie) Write-Host '' foreach ($alerte in $alertes) { Write-ManifesteAlerte $alerte } Write-Host '' Write-ManifesteInfo 'Aucun demarrage/redemarrage de service n''est necessaire : l''API relit le' Write-ManifesteInfo 'manifeste a CHAQUE appel de GET /api/langues.' Write-ManifesteInfo 'Le mobile, lui, garde son index memorise : il faut un rafraichissement' Write-ManifesteInfo '(liste des langues -> actualiser) pour voir la nouvelle liste.' Write-Host '' Write-ManifesteInfo 'Controles a enchainer :' Write-ManifesteInfo (' 1. .\New-OmikaLanguesManifeste.ps1 -Verifier') Write-ManifesteInfo ' 2. Invoke-RestMethod https:///api/langues | ConvertTo-Json -Depth 6' exit 0