Введение
Из короткого фильма удалили начальную заставку длиной две секунды, но субтитры всё ещё используют исходную шкалу времени. Первая зелёная сцена теперь начинается через 2 секунды от начала, а её субтитр — через 4 секунды. Видимые карточки сцен показывают время, поэтому угадывать на слух не нужно.
Вы представите время субтитров целыми миллисекундами и реализуете преобразование на Rust, которое сдвигает каждый блок, сохраняя полный Unicode- и многострочный текст. Затем вы загрузите фактически экспортированный файл как дорожку субтитров видео в браузере. Следующая работа посвящена границе защищённой записи результата и восстановлению после обычных ошибок. Фильмы и субтитры — оригинальные синтетические данные курса, а не записи реальных событий.
Предполагается, что вы уже понимаете структуры, перечисления, векторы, Result, строки, операции с файлами, а также простые тесты и циклы Rust. Прохождение существующего курса по CLI на Rust полезно, но необязательно: разбор аргументов предоставлен, знания clap не требуются. Автор оценивает работу в 45 минут с учётом чтения и обычных исправлений; это не время, измеренное на начинающих.
Проверьте синхронизацию и смоделируйте время в миллисекундах
На этом шаге вы посмотрите работающий пример субтитров, изучите его связь с исходной шкалой времени и реализуете числовое преобразование времени.
Откройте вкладку Preview. В подготовленном проигрывателе изначально выбраны data/edited.webm и data/reference.vtt. Эталон — уже работающий пример, а не ваш сдаваемый результат. Нажмите Load actual files, затем Play на видео. Должны появиться зелёная карточка сцены и двухстрочный субтитр, начинающийся с Café 林地 🌿. Приостановите видео. В 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/. Допустимы эквивалентные внутренние реализации. Для оставляемых без изменений подготовленных файлов должны сохраняться байты, права, владелец/группа и время изменения; обычное чтение может обновлять время доступа, которое в это требование не входит.
Блок содержит время начала, время окончания и одну или несколько строк текста. Диагностическое поле line записывает положение исходной строки времени, а не является частью текста субтитра. Предоставленная структура:
pub struct Cue {
pub start: i64,
pub end: i64,
pub text: String,
pub line: usize,
}
i64 — знаковое 64-битное целое. Мы храним целые миллисекунды, поэтому переход через минуту требует целочисленной арифметики, а не исправления разделителей временной метки. Начало и конец неотрицательны, конец позже начала, и анализатор проверяет эти отношения. String владеет полным текстом; не каждый его символ Unicode занимает один байт.
Посмотрите фактические исходные субтитры командой 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, ровно с одним пробелом по обе стороны стрелки. Все цифры — ASCII. Часы имеют не менее двух десятичных цифр, минуты и секунды — ровно две в диапазоне 00–59, миллисекунды — ровно три. Полная временная метка должна помещаться в неотрицательное число миллисекунд i64. - В порядке файла начала блоков не убывают; одинаковые начала и перекрывающиеся интервалы допустимы. Конец должен превышать начало. Программа сохраняет порядок файла.
- В каждом блоке есть хотя бы одна непустая логическая строка содержимого. Unicode, табуляции, начальные/конечные пробелы и несколько строк текста сохраняются. Допустимо даже содержимое только из пробелов или табуляций, хотя оно может выглядеть пустым. Действительно пустая строка завершает блок и не может обозначать пустую строку внутри его текста. Текст после этой границы должен начинать другой допустимый блок, а не быть отдельным продолжением без строки времени.
- Вход допускает окончания строк LF и CRLF, необязательный финальный перевод строки и несколько пустых разделителей блоков. Подготовленный формат вывода использует LF, две цифры часов для 0–99 часов (выше — только необходимые дополнительные цифры), одну пустую строку после заголовка и одну после каждого блока, включая последний. Он нормализует запись временных меток и количество разделителей; логический текст содержимого не меняется. Отдельные возвраты каретки не поддерживаются.
- Идентификаторы блоков, настройки, NOTE, STYLE, REGION, разметка, ссылки на символы и символы содержимого
<,>или&не поддерживаются. Остальные управляющие символы, включая NUL, также не поддерживаются; исключение — только табуляции. Неподдерживаемое содержимое отклоняется, а не молча удаляется.
Эти ограничения описывают формат курса, а не весь стандарт WebVTT. Подготовленный анализатор обеспечивает правила структуры и текста, но не выполняет вашу ещё не реализованную конверсию в миллисекунды. Его вспомогательная функция time_fields проверяет запись временной метки и диапазоны полей и возвращает [hours, minutes, seconds, milliseconds] как четыре неотрицательных значения i64. Ваша функция milliseconds получает эти уже проверенные поля, а не произвольные отрицательные числа или минуты вне диапазона. Однако при объединении полей она всё равно должна отклонять переполнение.
Первая реализация преобразует каждое поле с учётом масштаба его единицы. Массив scales содержит миллисекунды в часе, минуте, секунде и миллисекунде. Диапазон 0..4 включает 0–3; одинаковые позиции массивов связывают поле с его масштабом. Подчёркивания упрощают чтение крупных числовых литералов. 0_i64 явно выбирает тип накопителя.
checked_mul и checked_add возвращают Some(value), если результат помещается, и None при целочисленном переполнении. ok_or превращает этот Option в Result с сообщением об ошибке в строке String, владеющей текстом. ? продолжает работу с успешным значением или сразу возвращает ошибку. Переполнение — обычное отклонение входа, а не 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 для выхода. Если nano спросит о сохранении при выходе, ответьте Y, затем подтвердите имя файла. Прочитайте сохранённое до запуска:
cat src/time.rs
Выполните одно преобразование. cargo run компилирует и запускает подготовленную программу, --quiet скрывает сообщения о ходе работы Cargo, а -- передаёт следующие аргументы программе. Её команда time использует ваше преобразование:
cargo run --quiet -- time 00:01:01.750
00:01:01.750 = 61750 ms
Шестьдесят секунд плюс одна секунда и 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.
Сдвиньте всю коллекцию субтитров
На этом шаге вы сдвинете один типизированный блок, примените операцию ко всем блокам и посмотрите фактически сохранённый вывод. Продолжайте в Terminal, в /home/labex/project/subtitle-sync.
shift_one заимствует проверенный анализатором блок через &Cue и должна вернуть новый, не меняя оригинал. Независимо прибавьте знаковый сдвиг к обеим границам через checked_add, затем отклоните отрицательные результаты. Не переводите отрицательные значения в беззнаковые целые, не берите модуль и не обрезайте до нуля. Такие обходные пути меняют требуемое время.
Возвращаемая структура сохраняет новые значения начала и конца, клонирует полный текст, которым владеет исходный блок, и копирует диагностическую строку. .clone() сохраняет весь String, включая Unicode и каждую строку текста. Сокращённые поля структуры 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. Для каждого переполнения задайте собственную ошибку String через ok_or(...)?. Проверьте оба новых значения условием 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))?;
Добавляйте next внутри цикла через shifted.push(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 создаёт новый каталог, заменяя шесть символов 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
Должны появиться три блока. Старый результат остаётся в уникальном каталоге копии. Следующая работа объясняет и реализует саму границу защищённой записи.
Изучите фактически сохранённый файл:
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
Первые два блока всё ещё перекрываются. Начало третьего перешло с минуты 1 в предыдущую минуту, и каждая полная строка текста сохранилась. Программа сдвинула всю коллекцию; одного правильного первого блока недостаточно.
Перейдите в Preview. Оставьте Video равным data/edited.webm, измените Caption file на output/aligned.vtt и нажмите Load actual files. Выбранное имя файла в состоянии загрузки должно указывать на ваш результат. Предпросмотр читает фактические байты этого файла; он не создаёт субтитры заново и не возвращается к образцу.
Введите 2.5 в Time (seconds) и нажмите Go to time: зелёная сцена и двухстрочный Unicode-субтитр должны совпасть. Затем проверьте 4.5: синяя сцена должна показывать первый блок и Overlap stays. На 60.5 янтарная сцена должна показывать Across a minute. Браузер может упорядочить активные блоки с одинаковым началом иначе, чем файл; все их полные тексты должны остаться. Нажмите Play, чтобы подтвердить настоящее воспроизведение, затем поставьте на паузу. Download captions сохраняет загруженный файл результата, а Download video — загруженный фильм; ни одна кнопка не преобразует содержимое.
Пример ниже показывает фактически сохранённый файл на синей сцене. Найдите полный двухстрочный 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)
}
Резюме
Вы преобразовали проверенные поля времени в целые миллисекунды с проверкой переполнения, сдвинули один блок и полную коллекцию, сохранив текст, порядок, длительность и допустимые перекрытия. Затем загрузили настоящий новый файл как дорожку субтитров видео, проверили опубликованные визуальные точки синхронизации и исправили неправильный входной путь. Следующая работа объясняет файловую границу, которая отклоняет неправильный вход и существующие выходные пути до изменения файла результата.



