Introduction
Un court métrage a perdu son titre d’ouverture de deux secondes, mais ses sous-titres utilisent encore la chronologie d’origine. La première scène verte commence désormais à 2 secondes, alors que son sous-titre commence à 4 secondes. Les panneaux visibles dans les scènes indiquent le temps ; inutile de le deviner à l’oreille.
Vous représenterez les temps des sous-titres en millisecondes entières et implémenterez une transformation Rust qui décale chaque bloc, en conservant tout le texte Unicode et ses différentes lignes. Vous chargerez ensuite le fichier réellement exporté comme piste de sous-titres d’une vidéo dans le navigateur. Le laboratoire suivant se concentre sur la frontière d’écriture protégée et la récupération après les erreurs courantes. Les films et les sous-titres sont des données de test synthétiques originales du cours, et non des enregistrements d’événements réels.
Vous devez déjà connaître les structures, énumérations, vecteurs, Result, chaînes, opérations sur les fichiers, ainsi que les tests et boucles de base de Rust. Suivre le cours existant sur la CLI Rust est utile mais facultatif ; ce laboratoire fournit l’analyse des arguments et ne nécessite aucune connaissance de clap. L’auteur estime la durée à 45 minutes, lecture et corrections courantes comprises ; ce n’est pas un temps de réalisation mesuré auprès de débutants.
Observer l’alignement et modéliser les millisecondes
Dans cette étape, vous regarderez un exemple de sous-titres qui fonctionne, examinerez sa relation avec la chronologie originale et implémenterez la conversion numérique du temps.
Ouvrez l’onglet Preview. Le lecteur préparé sélectionne initialement data/edited.webm et data/reference.vtt. La référence est un exemple déjà fonctionnel, pas le résultat que vous devez remettre. Cliquez sur Load actual files, puis sur Play dans la vidéo. Vous devriez voir le panneau d’une scène verte et le sous-titre de deux lignes commençant par Café 林地 🌿. Mettez la vidéo en pause. Dans Time (seconds), saisissez 2.5 et cliquez sur Go to time pour examiner directement cette scène. Ce contrôle positionne la vraie vidéo ; le sous-titre est affiché par sa véritable piste de sous-titres.
Le film monté a supprimé un titre d’ouverture de 2000 ms. Le premier bloc original couvre 4000–7000 ms ; le bloc aligné doit couvrir 2000–5000 ms. Appliquer -2000 aux deux extrémités l’avance sans changer sa durée. Les décalages positifs retardent les sous-titres. La scène bleue à 4.5 secondes montrera ensuite un chevauchement volontaire, et la scène ambrée à 60.5 secondes permettra de vérifier un changement de minute. Ces points d’observation sont publics ; il n’y a aucune énigme sonore à résoudre.
Passez à Terminal. Le projet se trouve dans /home/labex/project/subtitle-sync. L’environnement préparé comprend Rust 1.99.1. Ajoutez le répertoire de Cargo au chemin de recherche des commandes de ce shell et sélectionnez une locale UTF-8 disponible pour cette session :
export PATH=/home/labex/.cargo/bin:$PATH
export LANG=C.UTF-8 LC_ALL=C.UTF-8
$PATH conserve les répertoires de recherche existants. Ces exports durent pendant la session de terminal et ne modifient aucun fichier de démarrage du shell ; répétez-les dans tout nouveau terminal. Le rendu des glyphes du terminal peut encore afficher des symboles de remplacement pour certains caractères Unicode. Ici, les vérifications fiables du texte sont les octets enregistrés et les vrais sous-titres du navigateur.
Entrez dans le projet. Toutes les commandes suivantes utilisent ce répertoire ; répétez la commande si vous ne savez plus où vous êtes :
cd /home/labex/project/subtitle-sync
Le projet préparé sépare les responsabilités. src/cues.rs définit Cue, valide notre format WebVTT contrôlé, extrait les champs temporels numériques et met en forme la sortie. src/main.rs analyse les commandes et affiche les réussites ou les erreurs récupérables. src/export.rs lit l’entrée réellement demandée et crée une nouvelle sortie après la transformation complète. Le laboratoire suivant enseigne cette frontière sur les fichiers. Vous ne modifierez que src/time.rs, en conservant les noms et signatures des fonctions publiques. Gardez inchangés tous les autres fichiers source, Cargo.toml, rust-toolchain.toml et tous les originaux sous data/. Les implémentations internes équivalentes sont autorisées. Les fichiers préparés laissés inchangés conservent leurs octets, permissions, propriétaire/groupe et date de modification ; les lectures ordinaires peuvent modifier la date d’accès, qui ne fait pas partie de cette exigence.
Un bloc contient un temps de début, un temps de fin et une ou plusieurs lignes de texte. Son champ de diagnostic line indique la position de la ligne d’horodatage dans le fichier original ; il ne fait pas partie du sous-titre. La structure fournie est :
pub struct Cue {
pub start: i64,
pub end: i64,
pub text: String,
pub line: usize,
}
i64 est un entier signé de 64 bits. Nous stockons des millisecondes entières : franchir une minute relève donc du calcul entier plutôt que de la modification de la ponctuation d’un horodatage. Le début et la fin sont non négatifs, la fin suit le début et l’analyseur valide ces relations. String possède le texte complet ; ses caractères Unicode n’occupent pas tous un seul octet.
Examinez les vrais sous-titres originaux avec cat, qui lit un fichier sans le modifier :
cat data/original.vtt
WEBVTT
00:00:04.000 --> 00:00:07.000
Café 林地 🌿
Second line intact
00:00:06.000 --> 00:00:09.000
Overlap stays
00:01:01.750 --> 00:01:04.250
Across a minute
Ce cours prend volontairement en charge un petit sous-ensemble de WebVTT en texte brut :
- La première ligne est exactement
WEBVTT, suivie d’une ligne vide. Les métadonnées d’en-tête et la marque d’ordre des octets ne sont pas prises en charge par ce cours. - Chaque ligne de temps a la forme
HH:MM:SS.mmm --> HH:MM:SS.mmm, avec exactement une espace de chaque côté de la flèche. Tous les chiffres sont ASCII. Les heures comportent au moins deux chiffres décimaux, les minutes et secondes exactement deux entre 00 et 59, et les millisecondes exactement trois. Chaque horodatage complet doit tenir dans une valeur i64 non négative de millisecondes. - Les débuts des blocs sont non décroissants dans l’ordre du fichier ; les débuts identiques et les intervalles qui se chevauchent sont légaux. La fin doit dépasser le début. Le programme conserve l’ordre du fichier.
- Chaque bloc possède au moins une ligne logique de contenu non vide. Unicode, tabulations, espaces de début/fin et lignes multiples sont conservés. Même un contenu composé uniquement d’espaces ou de tabulations est autorisé, bien qu’il puisse paraître vide. Une ligne véritablement vide termine un bloc ; elle ne peut pas représenter une ligne vide interne à son contenu. Le texte après cette frontière doit commencer un autre bloc valide, pas être une continuation orpheline.
- L’entrée accepte les fins de ligne LF et CRLF, une fin de ligne finale facultative et plusieurs séparateurs vides entre les blocs. Le format de sortie préparé utilise LF, deux chiffres d’heures pour 0–99 heures (uniquement les chiffres supplémentaires nécessaires au-delà), une ligne vide après l’en-tête et une après chaque bloc, y compris le dernier. Il normalise l’écriture des horodatages et le nombre de séparateurs ; le texte logique du contenu ne change pas. Les retours chariot isolés ne sont pas pris en charge.
- Les identifiants et paramètres de bloc, NOTE, STYLE, REGION, le balisage, les références de caractères et les caractères de contenu
<,>ou&ne sont pas pris en charge. Les autres caractères de contrôle, notamment NUL, ne sont pas pris en charge ; seules les tabulations font exception. Le contenu non pris en charge est rejeté au lieu d’être supprimé silencieusement.
Ces restrictions décrivent le format de ce cours, pas l’ensemble de la norme WebVTT. L’analyseur préparé applique les règles de structure et de texte. Il n’effectue pas la conversion en millisecondes qu’il vous reste à implémenter. Son auxiliaire time_fields vérifie l’écriture de l’horodatage et les plages des champs, puis renvoie [hours, minutes, seconds, milliseconds] sous forme de quatre valeurs i64 non négatives. Votre fonction milliseconds reçoit ces champs validés, pas des nombres négatifs arbitraires ni des minutes hors plage. Elle doit néanmoins rejeter le débordement lors de leur combinaison.
La première implémentation convertit chaque champ avec le facteur de son unité. Le tableau scales contient le nombre de millisecondes par heure, minute, seconde et milliseconde. La plage 0..4 inclut 0 à 3 ; les mêmes positions dans les tableaux associent chaque champ à son facteur. Les traits de soulignement facilitent la lecture des grands littéraux numériques. 0_i64 choisit explicitement le type de l’accumulateur.
checked_mul et checked_add renvoient Some(value) lorsque le résultat tient dans le type, ou None en cas de débordement entier. ok_or convertit cette Option en Result avec un message d’erreur placé dans une chaîne String qui possède le texte. ? poursuit avec la valeur de réussite ou renvoie immédiatement l’erreur. Le débordement est un rejet normal de l’entrée, pas un panic ni une valeur temporelle qui repart à une autre valeur par débordement.
Avant de coder, prévoyez le total pour 00:01:01.750 : quelle contribution apporte chaque champ non nul ? Notez l’entier attendu, puis testez votre prévision avec l’exécution ci-dessous.
Lisez d’abord le vrai code de départ :
cat src/time.rs
use crate::Cue importe le type préparé depuis cette bibliothèque. Conservez-le. Il y a trois fonctions ; modifiez uniquement milliseconds pour le moment. Son accolade ouvrante { se trouve sur la ligne de signature et son accolade fermante correspondante } juste avant shift_one. Les deux fonctions de décalage doivent garder leurs corps inachevés pendant cette phase.
Avant toute modification, créez un point de restauration contenant uniquement le code source autorisé. mktemp -d crée un répertoire unique en remplaçant les six caractères X ; il refuse un nom existant. $() capture ce chemin dans une variable de cette session shell. Les guillemets gardent le chemin en un seul morceau et && copie seulement après la réussite de la création du répertoire. cp -p copie le fichier en conservant ses permissions et sa date de modification. Il s’agit d’un point de restauration du code source, pas d’une sauvegarde des sous-titres produits. Affichez la variable et gardez le vrai nom ; ne devinez pas le suffixe.
rs01_before_milliseconds=$(mktemp -d .edit-before-milliseconds.XXXXXX) && cp -p src/time.rs "$rs01_before_milliseconds/time.rs"
printf '%s\n' "$rs01_before_milliseconds"
printf affiche ce nom de répertoire suivi d’un saut de ligne. Ouvrez le code source existant avec nano ; -c affiche la position du curseur :
nano -c src/time.rs
Appuyez sur Ctrl+W, tapez pub fn milliseconds, puis appuyez sur Entrée pour rechercher. Les flèches permettent de se déplacer dans le fichier. Dans cette signature, supprimez le trait de soulignement initial de _parts pour que le corps puisse utiliser parts ; gardez [i64; 4] et le type de retour inchangés. Remplacez uniquement la ligne Err(...) de cette fonction par votre logique de conversion, dans ses accolades existantes. Ne supprimez pas la fonction suivante.
Construisez les quatre facteurs dans le même ordre que les champs validés. Initialisez un total i64 mutable à zéro. Pour chaque indice dans 0..4, multipliez le champ par son facteur avec contrôle de débordement, puis ajoutez cette contribution au total avec le même contrôle. Convertissez tout débordement en erreur avec ok_or(String::from("time integer overflow"))?. Par exemple, la première partie de la boucle calcule une seule contribution :
let piece = parts[i]
.checked_mul(scales[i])
.ok_or(String::from("time integer overflow"))?;
Utilisez let scales = [3_600_000, 60_000, 1_000, 1]; et let mut total = 0_i64; avant cette boucle. L’addition suit le même modèle vérifié avec total.checked_add(piece). Après la boucle, renvoyez Ok(total) comme expression finale sans point-virgule. Ne touchez pas aux deux fonctions encore inachevées.
Appuyez sur Ctrl+O pour écrire, sur Entrée pour confirmer le nom existant, puis sur Ctrl+X pour quitter. Si nano demande d’enregistrer à la sortie, répondez Y, puis confirmez le nom. Lisez ce que vous avez enregistré avant de l’exécuter :
cat src/time.rs
Exécutez une conversion. cargo run compile et lance le programme préparé, --quiet masque la progression de Cargo et -- transmet les arguments suivants au programme. Sa commande time utilise votre conversion :
cargo run --quiet -- time 00:01:01.750
00:01:01.750 = 61750 ms
Soixante secondes plus une seconde et 750 millisecondes donnent 61750. Examinez maintenant toutes les valeurs des blocs originaux ; inspect lit le fichier réellement nommé et appelle l’analyseur :
cargo run --quiet -- inspect data/original.vtt
Cue 1: 4000 --> 7000 ms
Café 林地 🌿
Second line intact
Cue 2: 6000 --> 9000 ms
Overlap stays
Cue 3: 61750 --> 64250 ms
Across a minute
Les trois enregistrements et les deux lignes du premier bloc sont conservés. Comparez la conversion à votre prévision.
Si vous n’avez pas enregistré une modification accidentelle, appuyez sur Ctrl+X, puis sur N à la question d’enregistrement pour laisser le fichier inchangé. Pour une erreur enregistrée, lisez le fichier et la ligne indiqués par Cargo, rouvrez ce fichier source et corrigez l’expression locale. Vous pouvez aussi restaurer votre propre point de restauration. Le point de restauration milliseconds contient l’état avant cette phase ; le restaurer implique de refaire cette phase et les fonctions suivantes.
Si vous avez ouvert un nouveau Terminal, ses variables et son répertoire courant ne sont pas conservés. Répétez d’abord les commandes visibles d’environnement et de répertoire :
export PATH=/home/labex/.cargo/bin:$PATH
export LANG=C.UTF-8 LC_ALL=C.UTF-8
cd /home/labex/project/subtitle-sync
Les commandes de récupération suivantes sont conditionnelles : utilisez-les uniquement pour annuler une erreur enregistrée. ls -d liste les noms des répertoires eux-mêmes ; * correspond à leurs suffixes générés. Si aucun point de restauration n’existe, lisez cette erreur et corrigez le code source au lieu d’inventer un chemin.
ls -d .edit-before-milliseconds.*
read attend une ligne et la stocke dans une variable de session. Exécutez-le, puis saisissez ou collez un vrai nom complet de répertoire affiché ci-dessus et appuyez sur Entrée. Ne saisissez pas un suffixe d’exemple ni le signe dollar :
read rs01_restore
Confirmez que le fichier contient bien le code source antérieur attendu avant de le restaurer :
cat "$rs01_restore/time.rs"
cp -p "$rs01_restore/time.rs" src/time.rs
Rouvrez ensuite nano, refaites l’implémentation manquante et relancez la commande ayant échoué. Ne relancez pas la préparation de l’environnement et ne modifiez pas les fichiers d’entrée protégés. Corriger le code ne remplace ni un VTT de sortie existant ni le fichier déjà chargé dans un Blob du navigateur ; conservez l’ancien résultat avant de régénérer son chemin requis, puis sélectionnez cette vraie sortie et cliquez à nouveau sur Load actual files.
Décaler toute la collection de sous-titres
Dans cette étape, vous décalerez un bloc typé, appliquerez l’opération à chaque bloc et regarderez la sortie réellement enregistrée. Continuez dans Terminal, dans /home/labex/project/subtitle-sync.
shift_one emprunte un bloc validé par l’analyseur avec &Cue ; elle doit renvoyer un nouveau bloc sans modifier l’original. Ajoutez indépendamment le décalage signé aux deux extrémités avec checked_add, puis rejetez les résultats négatifs. Ne convertissez pas les valeurs négatives en entiers non signés, ne prenez pas leur valeur absolue et ne les ramenez pas à zéro. Ces raccourcis changent les temps demandés.
La structure renvoyée conserve les nouveaux temps, clone tout le texte dont elle est propriétaire et copie sa ligne de diagnostic. .clone() conserve le String entier, Unicode et toutes les lignes comprises. Les champs abrégés start, end reçoivent les variables de même nom. || signifie qu’une seule extrémité négative suffit à rejeter le décalage.
Avant toute modification, créez un point de restauration contenant uniquement le code source autorisé. mktemp -d crée un répertoire unique en remplaçant les six caractères X ; il refuse un nom existant. $() capture ce chemin dans une variable de cette session shell. Les guillemets gardent le chemin en un seul morceau et && copie seulement après la réussite de la création du répertoire. cp -p copie le fichier en conservant ses permissions et sa date de modification. Il s’agit d’un point de restauration du code source, pas d’une sauvegarde des sous-titres produits. Affichez la variable et gardez le vrai nom ; ne devinez pas le suffixe.
rs01_before_one_cue=$(mktemp -d .edit-before-one-cue.XXXXXX) && cp -p src/time.rs "$rs01_before_one_cue/time.rs"
printf '%s\n' "$rs01_before_one_cue"
printf affiche ce nom de répertoire suivi d’un saut de ligne. Ouvrez le code source existant avec nano ; -c affiche la position du curseur :
nano -c src/time.rs
Recherchez pub fn shift_one avec Ctrl+W, puis Entrée. Son corps se termine par l’accolade } du niveau de la fonction juste avant pub fn shift_all, pas par une accolade d’un if interne. Gardez milliseconds inchangée. Supprimez les traits de soulignement initiaux de _cue et _delta, en conservant les types et la signature de retour. Implémentez uniquement cette fonction pour le moment ; laissez shift_all inachevée.
Obtenez une variable locale start avec cue.start.checked_add(delta) et une variable end avec l’opération correspondante sur cue.end. Donnez à chaque débordement sa propre erreur String via ok_or(...)?. Vérifiez les deux nouvelles valeurs avec if start < 0 || end < 0 ; renvoyez une erreur depuis cette branche. Sinon, construisez Ok(Cue { start, end, text: cue.text.clone(), line: cue.line }). Vous préservez ainsi le bloc original tout en renvoyant sa copie décalée. Enregistrez avec Ctrl+O, Entrée, puis quittez avec Ctrl+X.
Prévoyez le résultat du bloc unique préparé avant de l’exécuter : 4000–7000 ms, -2000, texte A green scene :
cargo run --quiet -- cue -2000
2000 --> 5000 ms | A green scene
Cela vérifie un bloc, pas le film entier. La tentative volontaire suivante renvoie encore un état non nul, car shift_all est inachevée. Elle ne doit ni afficher une réussite de création ni créer le fichier nommé :
cargo run --quiet -- shift data/original.vtt output/unfinished-collection.vtt -2000
ls liste un chemin nommé. Ici, il doit lui aussi renvoyer un état non nul, car aucune sortie partielle ne doit exister :
ls output/unfinished-collection.vtt
Construisez maintenant l’opération sur la collection sans remplacer les fonctions terminées. Une tranche empruntée &[Cue] expose les enregistrements d’entrée ; Vec<Cue> possède le nouveau résultat. Vec::new crée un vecteur vide et push ajoute un bloc complet. iter().enumerate() visite chaque bloc emprunté avec un indice à partir de zéro. map_err transforme un message d’erreur ; sa fermeture |error| ... reçoit l’erreur précédente et format! construit un String avec la position du bloc à partir de un et sa ligne de diagnostic originale.
Avant toute modification, créez un point de restauration contenant uniquement le code source autorisé. mktemp -d crée un répertoire unique en remplaçant les six caractères X ; il refuse un nom existant. $() capture ce chemin dans une variable de cette session shell. Les guillemets gardent le chemin en un seul morceau et && copie seulement après la réussite de la création du répertoire. cp -p copie le fichier en conservant ses permissions et sa date de modification. Il s’agit d’un point de restauration du code source, pas d’une sauvegarde des sous-titres produits. Affichez la variable et gardez le vrai nom ; ne devinez pas le suffixe.
rs01_before_all_cues=$(mktemp -d .edit-before-all-cues.XXXXXX) && cp -p src/time.rs "$rs01_before_all_cues/time.rs"
printf '%s\n' "$rs01_before_all_cues"
printf affiche ce nom de répertoire suivi d’un saut de ligne. Ouvrez le code source existant avec nano ; -c affiche la position du curseur :
nano -c src/time.rs
Recherchez pub fn shift_all. C’est la dernière fonction du fichier ; son corps se termine par la dernière accolade } correspondante au niveau de la fonction. Les blocs for et if internes ont leurs propres accolades fermantes ; ne les confondez pas avec la limite de la fonction. Supprimez les traits de soulignement de _cues et _delta. Remplacez uniquement son corps Err(...) inachevé. Commencez par let mut shifted = Vec::new();, parcourez for (index, cue) in cues.iter().enumerate() et appelez la fonction que vous venez de terminer. Ajoutez du contexte aux échecs :
let next = shift_one(cue, delta)
.map_err(|error| format!("cue {} at line {}: {error}", index + 1, cue.line))?;
Ajoutez next dans la boucle avec shifted.push(next);. Renvoyez Ok(shifted) après la boucle. Le ? doit renvoyer une erreur plutôt que sauter un bloc incorrect ou conserver un préfixe réussi. Enregistrez, confirmez le nom et quittez nano. Votre logique précédente de milliseconds et de shift_one reste en place.
cat src/time.rs
Répétez le test du bloc unique avec le module désormais complet :
cargo run --quiet -- cue -2000
Utilisez maintenant la commande shift avec le vrai chemin d’entrée, un nouveau chemin de sortie et le décalage signé en millisecondes :
cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000
Created output/aligned.vtt with 3 cues (offset -2000 ms)
La frontière sur les fichiers préparée lit, analyse et décale toute la collection avant de créer un nouveau fichier. Observez son refus en répétant volontairement la même commande. La commande suivante doit renvoyer un état non nul et nommer la sortie existante :
cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000
Un fichier existant peut provenir d’une exécution précédente correcte ou d’une ancienne modification incorrecte. Corriger le code ne le réécrit pas. Conservez ce travail avant de réessayer le chemin de sortie requis. La commande suivante crée un nouveau répertoire de sauvegarde et y déplace le fichier existant :
mktemp -d crée un nouveau répertoire en remplaçant les six caractères X par un suffixe unique ; il ne réutilise pas un répertoire de sauvegarde existant. $() récupère le chemin affiché dans la variable de session rs01_backup_dir. Les guillemets autour de la variable gardent le chemin en un seul morceau. && exécute mv seulement si le répertoire a été créé ; mv déplace le fichier existant dans le nouveau répertoire vide. Une ancienne sauvegarde n’est donc pas écrasée. Si la création échoue, le déplacement est ignoré et la sortie originale reste à sa place ; lisez l’erreur avant de réessayer. Si vous reprenez après un premier essai ayant échoué, utilisez cette même sauvegarde dès qu’une sortie existe.
rs01_backup_dir=$(mktemp -d output/aligned-backup.XXXXXX) &&
mv output/aligned.vtt "$rs01_backup_dir/aligned.vtt"
Générez maintenant à nouveau le chemin requis avec votre code actuel :
cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000
Trois blocs doivent être créés. L’ancien résultat reste disponible dans le répertoire de sauvegarde unique. Le laboratoire suivant enseigne et implémente la frontière d’écriture protégée elle-même.
Examinez le fichier réellement enregistré :
cat output/aligned.vtt
WEBVTT
00:00:02.000 --> 00:00:05.000
Café 林地 🌿
Second line intact
00:00:04.000 --> 00:00:07.000
Overlap stays
00:00:59.750 --> 00:01:02.250
Across a minute
Les deux premiers blocs se chevauchent toujours. Le troisième est passé de la minute 1 à la minute précédente, et chaque ligne complète de texte a été conservée. Le programme a décalé toute la collection ; un premier bloc correct ne suffit pas.
Passez à Preview. Gardez Video sur data/edited.webm, remplacez Caption file par output/aligned.vtt et cliquez sur Load actual files. Le nom sélectionné dans l’état de chargement doit identifier votre sortie. L’aperçu lit les vrais octets de ce fichier ; il ne recrée pas les sous-titres et ne revient pas à l’exemple.
Saisissez 2.5 dans Time (seconds) et cliquez sur Go to time : la scène verte et les deux lignes de sous-titres Unicode doivent coïncider. Examinez ensuite 4.5 : la scène bleue doit montrer le premier bloc ainsi que Overlap stays. À 60.5, la scène ambrée doit montrer Across a minute. Un navigateur peut ordonner les blocs actifs de même début autrement que le fichier ; tous leurs textes complets doivent rester présents. Cliquez sur Play pour confirmer la vraie lecture, puis mettez en pause. Download captions enregistre le fichier de sortie chargé et Download video le film chargé ; aucun bouton ne transforme le contenu.
L’exemple ci-dessous montre le fichier réellement enregistré sur la scène bleue. Repérez le bloc Unicode complet sur deux lignes, l’autre bloc qui le chevauche et le nom du fichier de sortie chargé.

Revenez à Terminal. Pour provoquer une erreur de chemin volontaire, utilisez le nom de fichier au singulier qui n’existe pas :
cargo run --quiet -- inspect data/caption.vtt
La commande doit renvoyer un état non nul et nommer le chemin manquant. Un fichier absent est différent d’un bloc mal formé dans un fichier existant. Reprenez en indiquant votre vraie sortie :
cargo run --quiet -- inspect output/aligned.vtt
Vous devriez retrouver les trois blocs décalés et tout leur texte. Conservez les sous-titres et la vidéo d’origine ; les modifier masquerait l’effet de la transformation.
Si vous n’avez pas enregistré une modification accidentelle, appuyez sur Ctrl+X, puis sur N à la question d’enregistrement pour laisser le fichier inchangé. Pour une erreur enregistrée, lisez le fichier et la ligne indiqués par Cargo, rouvrez ce fichier source et corrigez l’expression locale. Vous pouvez aussi restaurer votre propre point de restauration. Le point de restauration one-cue contient l’état avant cette phase ; le restaurer implique de refaire cette phase et les fonctions suivantes.
Si vous avez ouvert un nouveau Terminal, ses variables et son répertoire courant ne sont pas conservés. Répétez d’abord les commandes visibles d’environnement et de répertoire :
export PATH=/home/labex/.cargo/bin:$PATH
export LANG=C.UTF-8 LC_ALL=C.UTF-8
cd /home/labex/project/subtitle-sync
Les commandes de récupération suivantes sont conditionnelles : utilisez-les uniquement pour annuler une erreur enregistrée. ls -d liste les noms des répertoires eux-mêmes ; * correspond à leurs suffixes générés. Si aucun point de restauration n’existe, lisez cette erreur et corrigez le code source au lieu d’inventer un chemin.
ls -d .edit-before-one-cue.*
read attend une ligne et la stocke dans une variable de session. Exécutez-le, puis saisissez ou collez un vrai nom complet de répertoire affiché ci-dessus et appuyez sur Entrée. Ne saisissez pas un suffixe d’exemple ni le signe dollar :
read rs01_restore
Confirmez que le fichier contient bien le code source antérieur attendu avant de le restaurer :
cat "$rs01_restore/time.rs"
cp -p "$rs01_restore/time.rs" src/time.rs
Rouvrez ensuite nano, refaites l’implémentation manquante et relancez la commande ayant échoué. Ne relancez pas la préparation de l’environnement et ne modifiez pas les fichiers d’entrée protégés. Corriger le code ne remplace ni un VTT de sortie existant ni le fichier déjà chargé dans un Blob du navigateur ; conservez l’ancien résultat avant de régénérer son chemin requis, puis sélectionnez cette vraie sortie et cliquez à nouveau sur Load actual files.
Le point de restauration .edit-before-one-cue.* contient uniquement la conversion en millisecondes terminée. Pour une erreur limitée à la collection, utilisez la même procédure de confirmation avec .edit-before-all-cues.* : ce point de restauration conserve aussi la fonction de décalage d’un bloc terminée. Aucun des deux n’est une implémentation finale complète.
Référence complète facultative de time.rs pour comparaison et récupération
Essayez les modifications de fonctions et examinez leurs résultats avant d’ouvrir cette référence. Elle montre une implémentation complète, pas une suite de commandes obligatoire. Les algorithmes équivalents sont autorisés dans le cadre du contrat public. Comparez la fonction concernée plutôt que de remplacer inutilement du travail déjà terminé.
use crate::Cue;
pub fn milliseconds(parts: [i64; 4]) -> Result<i64, String> {
let scales = [3_600_000, 60_000, 1_000, 1];
let mut total = 0_i64;
for i in 0..4 {
let piece = parts[i]
.checked_mul(scales[i])
.ok_or(String::from("time integer overflow"))?;
total = total
.checked_add(piece)
.ok_or(String::from("time integer overflow"))?;
}
Ok(total)
}
pub fn shift_one(cue: &Cue, delta: i64) -> Result<Cue, String> {
let start = cue.start
.checked_add(delta)
.ok_or(String::from("start integer overflow"))?;
let end = cue.end
.checked_add(delta)
.ok_or(String::from("end integer overflow"))?;
if start < 0 || end < 0 {
return Err(String::from("shift would produce negative time"));
}
Ok(Cue { start, end, text: cue.text.clone(), line: cue.line })
}
pub fn shift_all(cues: &[Cue], delta: i64) -> Result<Vec<Cue>, String> {
let mut shifted = Vec::new();
for (index, cue) in cues.iter().enumerate() {
let next = shift_one(cue, delta)
.map_err(|error| format!("cue {} at line {}: {error}", index + 1, cue.line))?;
shifted.push(next);
}
Ok(shifted)
}
Résumé
Vous avez converti des champs temporels validés en millisecondes entières avec contrôle de débordement, décalé un bloc puis une collection complète, et conservé le texte, l’ordre, la durée et les chevauchements légitimes. Vous avez ensuite chargé le vrai nouveau fichier comme piste de sous-titres vidéo, examiné les points publics d’alignement visuel et corrigé un chemin d’entrée erroné. Le laboratoire suivant enseigne la frontière sur les fichiers qui refuse les mauvaises entrées et les chemins de sortie existants avant de modifier un fichier résultat.



