字幕キューをモデル化して時刻をずらす

RustBeginner
オンラインで実践に進む

はじめに

短編映像から冒頭の 2 秒間のタイトルが削除されましたが、字幕は元のタイムラインのままです。最初の緑のシーンは現在 2 秒から始まるのに、対応する字幕は 4 秒から始まります。画面内のシーンカードで時刻が分かるため、音を聞いて推測する必要はありません。

字幕の時刻を整数ミリ秒で表し、完全な Unicode と複数行のテキストを保ちながら、すべての字幕キューを移動する Rust の変換処理を実装します。その後、実際にエクスポートしたファイルをブラウザー動画の字幕トラックとして読み込みます。次のラボでは、出力を保護する書き込みの境界と、通常の失敗からの復旧に取り組みます。映像と字幕はこのコース用に独自に作成した合成データであり、実際の出来事の録画ではありません。

Rust の構造体、列挙型、ベクター、Result、文字列、ファイル操作、基本的なテストとループを理解していることを前提とします。既存の Rust CLI コースを修了しておくと役立ちますが、必須ではありません。引数解析は提供されており、clap の知識は不要です。著者の見積もりは、読解と通常の修正を含めて 45 分です。初心者の実測に基づく所要時間ではありません。

同期を確認し、ミリ秒をモデル化する

このステップでは、正しく動く字幕サンプルを再生し、元のタイムラインとの関係を調べて、数値としての時間変換を実装します。

Preview タブを開きます。準備済みのプレイヤーでは、最初に data/edited.webm と data/reference.vtt が選択されています。この参照ファイルは既に動く例であり、あなたが提出する出力ではありません。Load actual files をクリックし、動画の Play をクリックします。緑のシーンカードと、Café 林地 🌿 で始まる 2 行の字幕が見えるはずです。動画を一時停止します。Time (seconds) に 2.5 と入力して Go to time をクリックすると、そのシーンを直接確認できます。この操作は実際の動画をシークし、字幕は実際に読み込まれた字幕トラックが表示します。

編集後の映像では、2000 ms のタイトルが削除されています。元の最初のキューは 4000–7000 ms なので、同期後は 2000–5000 ms でなければなりません。両端の時刻に -2000 を適用すると、長さを変えずに表示を早められます。正のオフセットでは字幕が遅くなります。後で確認する 4.5 秒の青いシーンには意図的な重なりがあり、60.5 秒の琥珀色のシーンでは分の境界を確認します。これらの観察時刻は公開されており、音を聞いて解く問題ではありません。

Terminal に切り替えます。プロジェクトは /home/labex/project/subtitle-sync です。準備済み環境には Rust 1.99.1 が含まれます。Cargo のディレクトリをこのシェルのコマンド検索パスに加え、このセッションで利用可能な UTF-8 ロケールを選択します。

export PATH=/home/labex/.cargo/bin:$PATH
export LANG=C.UTF-8 LC_ALL=C.UTF-8

$PATH は既存の検索ディレクトリを保持します。これらのエクスポートは現在のターミナルセッションだけで有効で、シェルの起動ファイルは変更しません。新しいターミナルでは毎回実行してください。ターミナルの字体によっては、一部の Unicode 文字が代替記号で表示されることがあります。ここでは保存されたバイトと、実際のブラウザー字幕でテキストを確実に確認します。

プロジェクトに移動します。以後のすべてのコマンドはこのディレクトリで実行します。現在地が分からなくなったら、このコマンドを再実行してください。

cd /home/labex/project/subtitle-sync

準備済みプロジェクトでは役割を分けています。src/cues.rs は Cue を定義し、このコースの制限された WebVTT 形式を検証して、数値の時間要素を抽出し、出力を整形します。src/main.rs はコマンドを解析し、成功または復旧可能なエラーを表示します。src/export.rs は実際に指定された入力を読み取り、変換全体の完了後に新しい出力を作ります。このファイル操作の境界は次のラボで学びます。編集するのは src/time.rs だけで、公開関数の名前とシグネチャを保持してください。他のすべてのソースファイル、Cargo.toml、rust-toolchain.toml、および data/ 内の元ファイルは変更しません。同等の内部実装は認められます。変更しない準備済みファイルは、バイト、権限、所有者/グループ、変更時刻を保持します。通常の読み取りで更新されるアクセス時刻は、この要件の対象外です。

キューには開始時刻、終了時刻、1 行以上のテキストがあります。診断用の line は元の時間行の位置を記録するもので、字幕本文の一部ではありません。提供される構造体は次のとおりです。

pub struct Cue {
    pub start: i64,
    pub end: i64,
    pub text: String,
    pub line: usize,
}

i64 は符号付き 64 ビット整数です。整数ミリ秒を保存するため、分をまたぐ処理はタイムスタンプの区切り記号を編集する操作ではなく、整数の計算になります。開始と終了は非負で、終了は開始より後であり、この関係はパーサーが検証します。String は完全なテキストを所有します。その Unicode 文字がすべて 1 バイトに収まるわけではありません。

ファイルを変更せずに読み取る cat で、実際の元字幕を確認します。

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

このコースは、意図的に小さく限定したプレーンテキストの WebVTT サブセットを扱います。

  • 最初の行は正確に WEBVTT で、その後に空行が必要です。ヘッダーのメタデータとバイトオーダーマークは、このコースでは未対応です。
  • 各時間行は HH:MM:SS.mmm --> HH:MM:SS.mmm の形式で、矢印の両側にはそれぞれ正確に 1 個の空白を置きます。数字はすべて ASCII 数字です。時は 2 桁以上の十進数字、分と秒は 00–59 の正確に 2 桁、ミリ秒は正確に 3 桁です。完全なタイムスタンプは、非負の i64 のミリ秒値に収まる必要があります。
  • ファイル順でキューの開始時刻が減少してはいけません。同一開始時刻と区間の重なりは有効です。終了は開始を上回る必要があります。プログラムはファイル順を保持します。
  • 各キューには、空でない論理的な字幕行が 1 行以上必要です。Unicode、タブ、先頭/末尾の空白、複数行を保持します。空白やタブだけの内容も、見た目が空白でも認められます。本当に空の行はキューの終わりを表し、本文内部の空行にはできません。その境界の後のテキストは、新たな有効キューを開始する必要があり、時間行のない孤立した続きにはできません。
  • 入力は LF と CRLF の改行、末尾の改行の有無、複数の空のキュー区切りを認めます。準備済みの出力形式は LF を使い、0–99 時間では時を 2 桁で表し、それより大きい時は必要な桁だけ追加します。ヘッダー後に空行を 1 行、最後を含む各キューの後にも空行を 1 行置きます。タイムスタンプの表記と区切り数は正規化しますが、論理的な字幕テキストは変更しません。単独のキャリッジリターンは未対応です。
  • キュー ID、設定、NOTE、STYLE、REGION、マークアップ、文字参照、本文の <、>、& は未対応です。NUL を含む他の制御文字も未対応で、例外はタブだけです。未対応の内容は黙って削除するのではなく、拒否します。

これらの制限はこのコースの形式を定めるもので、WebVTT 標準全体ではありません。準備済みパーサーが構造とテキストの規則を適用しますが、未実装のミリ秒変換は代行しません。補助関数 time_fields はタイムスタンプの表記と各要素の範囲を確認し、[hours, minutes, seconds, milliseconds] を 4 つの非負の i64 値として返します。あなたの milliseconds 関数が受け取るのは、検証済みの要素であり、任意の負数や範囲外の分ではありません。ただし、それらを組み合わせる際のオーバーフローは拒否する必要があります。

最初の実装は、それぞれの要素を単位ごとの倍率で変換します。配列 scales は時、分、秒、ミリ秒ごとのミリ秒数を持ちます。範囲 0..4 は 0 から 3 を含み、対応する配列位置で各要素と倍率を組み合わせます。アンダースコアは大きな数値リテラルを読みやすくします。0_i64 は累積値の型を明示します。

checked_mul と checked_add は、結果が収まる場合に Some(value)、整数オーバーフロー時に None を返します。ok_or はこの Option を、テキストを所有する String にエラーメッセージを入れた Result に変換します。? は成功値なら処理を続け、エラーなら直ちに返します。オーバーフローは通常の入力拒否として扱い、panic や時刻値の桁あふれによる折り返しにはしません。

コードを書く前に、00:01:01.750 の合計を予想します。ゼロでない各要素から何ミリ秒ずつ加わるでしょうか。期待する整数を書き、その後の実行で予想を確かめます。

まず実際の起点のコードを読みます。

cat src/time.rs

use crate::Cue は、このライブラリの準備済みの型を取り込みます。そのまま残します。関数は三つありますが、今は milliseconds だけを編集します。開き波かっこ { はシグネチャ行にあり、対応する閉じ波かっこ } は shift_one の直前です。この段階では、二つのシフト関数の本体は未完成のままにします。

編集前に、許可されたソースだけのチェックポイントを作ります。mktemp -d は六個の X を置き換えて一意のディレクトリを作り、既存の名前は拒否します。$() はそのパスを、このシェルセッションの変数に取り込みます。引用符はパスを一まとまりに保ち、&& はディレクトリ作成成功時だけコピーします。cp -p は権限と変更時刻を保持してファイルをコピーします。これはソースのチェックポイントであり、出力字幕のバックアップではありません。変数を表示し、実際の名前を控えてください。接尾辞を推測してはいけません。

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 は、このディレクトリ名とその後の改行を出力します。既存のソースを nano で開きます。-c はカーソル位置を表示します。

nano -c src/time.rs

Ctrl+W を押して pub fn milliseconds を入力し、Enter で検索します。矢印キーでファイル内を移動できます。このシグネチャの _parts から先頭のアンダースコアを取り除き、本体で parts を使えるようにします。[i64; 4] と戻り値の型は変えません。既存の波かっこの内側で、この関数の Err(...) 行だけを変換ロジックに置き換えます。次の関数を削除してはいけません。

検証済みの要素と同じ順序で四つの倍率を用意します。変更可能な i64 の合計をゼロから始めます。0..4 の各インデックスについて、要素と倍率をオーバーフロー検査付きで乗算し、その寄与分を同じく検査付きで合計に加えます。どちらのオーバーフローも ok_or(String::from("time integer overflow"))? でエラーに変換します。たとえば、ループの最初の部分では一つの寄与分を計算します。

let piece = parts[i]
    .checked_mul(scales[i])
    .ok_or(String::from("time integer overflow"))?;

そのループの前に let scales = [3_600_000, 60_000, 1_000, 1]; と let mut total = 0_i64; を置きます。加算も同じ検査付きのパターンで total.checked_add(piece) を使います。ループの後は、セミコロンのない最後の式として Ok(total) を返します。残り二つの未実装部分はそのままにします。

Ctrl+O で書き込み、Enter で既存のファイル名を確定し、Ctrl+X で終了します。終了時に保存するか尋ねられたら Y と答え、ファイル名を確定します。実行前に保存した内容を読みます。

cat src/time.rs

一つの変換を実行します。cargo run は準備済みプログラムをコンパイルして起動し、--quiet は Cargo の進捗を抑制し、-- は後の引数をプログラムに渡します。プログラムの time コマンドがあなたの変換を使います。

cargo run --quiet -- time 00:01:01.750
00:01:01.750 = 61750 ms

60 秒に 1 秒と 750 ミリ秒を足すと 61750 です。次に、元の全キューの値を確認します。inspect は実際に指定したファイルを読み、パーサーを呼びます。

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

三つのレコードと最初のキューの両方のテキスト行が残っています。変換結果と自分の予想を比較します。

誤った編集をまだ保存していなければ、Ctrl+X を押し、保存の質問に N と答えるとファイルを変更せずに終了できます。保存済みのミスでは、Cargo が示したファイル名と行を読み、このソースを開き直して該当する式を修正します。自分のチェックポイントから復元する方法もあります。milliseconds チェックポイントは、この段階の前の状態を含みます。復元すると、この段階と、それ以降の関数をやり直す必要があります。

新しい Terminal を開いた場合、変数と作業ディレクトリは引き継がれません。まず、示された環境とディレクトリのコマンドを繰り返します。

export PATH=/home/labex/.cargo/bin:$PATH
export LANG=C.UTF-8 LC_ALL=C.UTF-8
cd /home/labex/project/subtitle-sync

以下の復旧コマンドは条件付きです。保存済みのミスを元に戻す場合だけ使います。ls -d はディレクトリ名自体を一覧表示し、* は生成された接尾辞に一致します。チェックポイントがなければ、そのエラーを読み、架空のパスを作らずにソースを修正します。

ls -d .edit-before-milliseconds.*

read は一行の入力を待ち、セッション変数に保存します。実行してから、上に表示された実際の完全なディレクトリ名を一つ入力または貼り付け、Enter を押します。例の接尾辞やドル記号を入力してはいけません。

read rs01_restore

復元前に、このファイルが期待する以前のソースであることを確認します。

cat "$rs01_restore/time.rs"
cp -p "$rs01_restore/time.rs" src/time.rs

その後 nano を開き直し、足りない実装をやり直して、失敗したコマンドを再実行します。setup を再実行したり、保護された入力ファイルを変更したりしないでください。コードの修正では、既存の出力 VTT や、ブラウザーの Blob に読み込み済みのファイルは置き換わりません。必要なパスを再生成する前に古い結果を保ち、その後、実際の出力を選び直して Load actual files をクリックします。

字幕コレクション全体をシフトする

このステップでは、型を持つ 1 つのキューを移動し、その操作を全キューに適用して、実際に保存した出力を再生します。Terminal の /home/labex/project/subtitle-sync で続けてください。

shift_one は &Cue でパーサー検証済みのキューを借用し、元を変更せず新しいキューを返す必要があります。checked_add で符号付きオフセットを両端の時刻へ個別に加え、負の結果を拒否します。負値を符号なし整数に変換したり、絶対値を取ったり、ゼロへ切り詰めたりしてはいけません。そうした近道は要求された時刻を変えてしまいます。

返す構造体は新しい時刻を保持し、元のキューが所有するテキスト全体をクローンし、診断行をコピーします。.clone() は Unicode とすべてのテキスト行を含む String 全体を保ちます。構造体の省略記法 start, end は同名の変数を受け取ります。|| は、一方の端点が負であれば移動を拒否することを表します。

編集前に、許可されたソースだけのチェックポイントを作ります。mktemp -d は六個の X を置き換えて一意のディレクトリを作り、既存の名前は拒否します。$() はそのパスを、このシェルセッションの変数に取り込みます。引用符はパスを一まとまりに保ち、&& はディレクトリ作成成功時だけコピーします。cp -p は権限と変更時刻を保持してファイルをコピーします。これはソースのチェックポイントであり、出力字幕のバックアップではありません。変数を表示し、実際の名前を控えてください。接尾辞を推測してはいけません。

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 は、このディレクトリ名とその後の改行を出力します。既存のソースを nano で開きます。-c はカーソル位置を表示します。

nano -c src/time.rs

Ctrl+W で pub fn shift_one を検索し、Enter を押します。本体は pub fn shift_all の直前にあるトップレベルの閉じ波かっこ } までです。内側の if の波かっこではありません。milliseconds は変更しません。_cue と _delta の先頭のアンダースコアを取り除き、型と戻り値のシグネチャは保ちます。今はこの一つの関数だけを実装し、shift_all は未完成のままにします。

ローカル変数 start を cue.start.checked_add(delta) から、end を対応する cue.end の操作から作ります。各オーバーフローには ok_or(...)? で個別の String エラーを付けます。if start < 0 || end < 0 で両方の新しい値を確認し、その分岐ではエラーを返します。それ以外は Ok(Cue { start, end, text: cue.text.clone(), line: cue.line }) を作ります。元のキューを保持したまま、移動したコピーを返せます。Ctrl+O、Enter で保存し、Ctrl+X で終了します。

準備済みの単一キューを実行前に予想します。4000–7000 ms、-2000、テキスト A green scene です。

cargo run --quiet -- cue -2000
2000 --> 5000 ms | A green scene

確認できたのは一つのキューであり、映像全体ではありません。次の意図的な試行は、shift_all が未完成なので引き続きゼロ以外を返します。作成成功を表示したり、指定ファイルを作ったりしてはいけません。

cargo run --quiet -- shift data/original.vtt output/unfinished-collection.vtt -2000

ls は指定パスを一覧表示します。部分出力が存在してはならないため、ここでもゼロ以外を返すはずです。

ls output/unfinished-collection.vtt

次に、完成済みのどちらの関数も置き換えずに、コレクションの操作を作ります。借用したスライス &[Cue] は入力レコードへのアクセスを提供し、Vec<Cue> は新しい結果を所有します。Vec::new は空のベクターを作り、push は完全なキューを一つ追加します。iter().enumerate() は、ゼロ始まりのインデックスとともに各借用キューを順にたどります。map_err はエラーメッセージを変換します。そのクロージャ |error| ... は前のエラーを受け取り、format! は一始まりのキュー位置と元の診断行を持つ String を作ります。

編集前に、許可されたソースだけのチェックポイントを作ります。mktemp -d は六個の X を置き換えて一意のディレクトリを作り、既存の名前は拒否します。$() はそのパスを、このシェルセッションの変数に取り込みます。引用符はパスを一まとまりに保ち、&& はディレクトリ作成成功時だけコピーします。cp -p は権限と変更時刻を保持してファイルをコピーします。これはソースのチェックポイントであり、出力字幕のバックアップではありません。変数を表示し、実際の名前を控えてください。接尾辞を推測してはいけません。

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 は、このディレクトリ名とその後の改行を出力します。既存のソースを nano で開きます。-c はカーソル位置を表示します。

nano -c src/time.rs

pub fn shift_all を検索します。これはファイルの最後の関数で、本体は最後のトップレベルの対応する } までです。内側の for と if のブロックには、それぞれ独自の閉じ波かっこがあります。関数の境界と取り違えないでください。_cues と _delta のアンダースコアを取り除きます。未完成の Err(...) 本体だけを置き換えます。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); を使って next を追加します。ループの後に Ok(shifted) を返します。? は、不正なキューを飛ばしたり、成功した先頭部分を残したりせず、エラーを返さなければなりません。保存してファイル名を確定し、nano を終了します。先ほどの milliseconds と shift_one のロジックはそのまま残します。

cat src/time.rs

モジュールが完成した状態で、単一キューの回帰確認を繰り返します。

cargo run --quiet -- cue -2000

次に、実際の入力パス、新しい出力パス、符号付きのミリ秒オフセットを指定して shift コマンドを使います。

cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000
Created output/aligned.vtt with 3 cues (offset -2000 ms)

準備済みのファイル操作境界は、新規ファイルを作る前にコレクション全体を読み取り、解析し、シフトします。同じコマンドを意図的に繰り返して拒否を確認します。次のコマンドは非ゼロ状態で、既存の出力を示す必要があります。

cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000

既存ファイルは、以前の正しい実行結果かもしれませんし、以前の編集による誤った結果かもしれません。コードを直しても書き換わりません。要求された出力パスで再試行する前に、その作業を残します。次のコマンドは新しいバックアップディレクトリを作り、既存ファイルを移動します。

mktemp -d は 6 個の X を一意の接尾辞に置き換えて新しいディレクトリを作り、既存のバックアップディレクトリを再利用しません。$() は表示されたパスをセッション変数 rs01_backup_dir に取り込みます。変数を引用符で囲むとパスを一まとまりとして扱えます。&& はディレクトリ作成に成功した場合だけ mv を実行し、mv は既存ファイルを新しい空のディレクトリへ移します。そのため古いバックアップを上書きしません。ディレクトリ作成に失敗したら移動は省略され、元の出力はその場に残ります。再試行前にエラーを読んでください。最初の試行で失敗した後の復旧でも、既存出力がある場合はこの同じバックアップ操作を使います。

rs01_backup_dir=$(mktemp -d output/aligned-backup.XXXXXX) &&
mv output/aligned.vtt "$rs01_backup_dir/aligned.vtt"

現在のコードで、要求されたパスをもう一度生成します。

cargo run --quiet -- shift data/original.vtt output/aligned.vtt -2000

3 キューが作られるはずです。古い結果は一意のバックアップディレクトリ内に残ります。次のラボでは、この保護された書き込み境界自体を学び、実装します。

実際に保存されたファイルを確認します。

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

最初の 2 キューは引き続き重なっています。3 番目のキューの開始は 1 分台から前の分へ移り、すべてのテキスト行が完全に残っています。プログラムが移動したのはコレクション全体であり、最初のキューだけ正しくても不十分です。

Preview に切り替えます。Video を data/edited.webm のままにし、Caption file を output/aligned.vtt に変えて Load actual files をクリックします。読み込み状態に表示されるファイル名は、あなたの出力を指す必要があります。プレビューはそのファイルの実際のバイトを読み、字幕を再作成したりサンプルに戻ったりしません。

Time (seconds) に 2.5 と入力して Go to time をクリックすると、緑のシーンと 2 行の Unicode 字幕が一致するはずです。次に 4.5 を確認します。青いシーンには最初のキューと Overlap stays の両方が表示されるはずです。60.5 では、琥珀色のシーンに Across a minute が表示されます。ブラウザーは、同一開始時刻のアクティブキューをファイルと異なる順に並べることがありますが、完全なテキストはすべて存在する必要があります。Play をクリックして実際の再生を確認し、一時停止します。Download captions は読み込んだ出力ファイルを、Download video は読み込んだ映像を保存します。どちらのボタンも内容を変換しません。

下の例は、実際の保存ファイルを青いシーンで表示したものです。完全な 2 行の Unicode キュー、それとは別に重なるキュー、読み込んだ出力ファイル名を確認してください。

実際の同期ファイルには重なる両キューの完全なテキストが残る

Terminal に戻ります。意図的にパスエラーを起こすため、存在しない単数形のファイル名を使います。

cargo run --quiet -- inspect data/caption.vtt

非ゼロ状態になり、存在しないパスを示す必要があります。ファイルが存在しない場合と、既存ファイル内のキューの形式が不正な場合は異なります。実際の出力を指定して復旧します。

cargo run --quiet -- inspect output/aligned.vtt

移動後の全三キューと完全なテキストが再び見えるはずです。元の字幕と動画は保持します。それらを変更すると、変換の効果を隠してしまいます。

誤った編集をまだ保存していなければ、Ctrl+X を押し、保存の質問に N と答えるとファイルを変更せずに終了できます。保存済みのミスでは、Cargo が示したファイル名と行を読み、このソースを開き直して該当する式を修正します。自分のチェックポイントから復元する方法もあります。one-cue チェックポイントは、この段階の前の状態を含みます。復元すると、この段階と、それ以降の関数をやり直す必要があります。

新しい Terminal を開いた場合、変数と作業ディレクトリは引き継がれません。まず、示された環境とディレクトリのコマンドを繰り返します。

export PATH=/home/labex/.cargo/bin:$PATH
export LANG=C.UTF-8 LC_ALL=C.UTF-8
cd /home/labex/project/subtitle-sync

以下の復旧コマンドは条件付きです。保存済みのミスを元に戻す場合だけ使います。ls -d はディレクトリ名自体を一覧表示し、* は生成された接尾辞に一致します。チェックポイントがなければ、そのエラーを読み、架空のパスを作らずにソースを修正します。

ls -d .edit-before-one-cue.*

read は一行の入力を待ち、セッション変数に保存します。実行してから、上に表示された実際の完全なディレクトリ名を一つ入力または貼り付け、Enter を押します。例の接尾辞やドル記号を入力してはいけません。

read rs01_restore

復元前に、このファイルが期待する以前のソースであることを確認します。

cat "$rs01_restore/time.rs"
cp -p "$rs01_restore/time.rs" src/time.rs

その後 nano を開き直し、足りない実装をやり直して、失敗したコマンドを再実行します。setup を再実行したり、保護された入力ファイルを変更したりしないでください。コードの修正では、既存の出力 VTT や、ブラウザーの Blob に読み込み済みのファイルは置き換わりません。必要なパスを再生成する前に古い結果を保ち、その後、実際の出力を選び直して Load actual files をクリックします。

.edit-before-one-cue.* チェックポイントでは、ミリ秒変換だけが完成しています。コレクションだけのミスには、.edit-before-all-cues.* で同じ確認手順を使います。こちらのチェックポイントには、完成済みの単一キュー関数も残っています。どちらも最終的な完全実装ではありません。

任意:完全な time.rs の比較・復旧用参照

この参照を開く前に、関数の編集を試して結果を確認します。これは完全な実装の一例であり、必須のコマンド手順ではありません。公開された契約の範囲内で同等のアルゴリズムが認められます。完成済みの作業を不必要に置き換えず、影響を受けた関数を比較します。

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)
}

まとめ

検証済み時間要素を、オーバーフローを検査する整数ミリ秒に変換し、1 キューとコレクション全体を移動して、テキスト、順序、長さ、有効な重なりを保ちました。さらに、実際の新規ファイルを動画の字幕トラックとして読み込み、公開された視覚的な同期点を確認し、誤った入力パスから復旧しました。次のラボでは、結果ファイルを変更する前に不正入力と既存の出力パスを拒否する、ファイル操作の境界を学びます。