Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@
^synced_episode.srt/
^cran-comments\.md$
^codemeta\.json$
^CRAN-SUBMISSION$
^tests.R$
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,5 @@ $RECYCLE.BIN/

/.quarto/
**/*.quarto_ipynb
CRAN-SUBMISSION
README.html
10 changes: 6 additions & 4 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Package: subtools
Type: Package
Title: Read and Manipulate Video Subtitles
Version: 1.1.0
Date: 2026-03-11
Date: 2026-03-16
Authors@R: c(
person("Francois", "Keck", email = "francois.keck@gmail.com",
role = c("aut", "cre", "cph"),
Expand All @@ -13,9 +13,9 @@ Authors@R: c(
comment = c(ORCID = "0000-0002-3827-1063"))
)
Maintainer: Francois Keck <francois.keck@gmail.com>
Description: A collection of functions to read, write and manipulate video subtitles.
Supported formats include 'srt', 'subrip', 'sub', 'subviewer', 'microdvd',
'ssa', 'ass', 'substation', 'vtt', and 'webvtt'.
Description: A collection of functions to read, write and manipulate video
subtitles. Supported formats include 'srt', 'subrip', 'sub', 'subviewer',
'microdvd', 'ssa', 'ass', 'substation', 'vtt', and 'webvtt'.
Language: en-GB
License: GPL-3
LazyData: TRUE
Expand All @@ -40,3 +40,5 @@ Config/testthat/edition: 3
Config/testthat/parallel: false
Config/testthat/start-first: *assert*, *utils*, *read*
RoxygenNote: 7.3.3
X-schema.org-applicationCategory: Text Processing
X-schema.org-keywords: subtitles, video, text, srt, vtt, ssa, natural language processing
28 changes: 23 additions & 5 deletions R/assert_objects.R
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
# .assert_subtitles
# .extract_metadata

# Sanity check for subtitles objects
#' Validate a subtitles object
#'
#' Checks that \code{x} inherits from both \code{"subtitles"} and \code{"data.frame"},
#' has the four required columns (\code{ID}, \code{Timecode_in}, \code{Timecode_out},
#' \code{Text_content}), and that the two timecode columns are of class \code{"hms"}.
#' Stops with an informative error message if any check fails.
#'
#' @param x an object to validate.
#'
#' @returns Called for its side effects (validation). Returns \code{NULL} invisibly.
#'
#' @noRd
.assert_subtitles <- function(x) {
if (!is(x, "subtitles")) {
stop("A subtitles object must inherit from class \"subtitles\".")
Expand Down Expand Up @@ -35,7 +43,17 @@
}
}

#extract metadata columns from a subtitles object
#' Extract metadata columns from a subtitles object
#'
#' Returns all columns that are not part of the core subtitle structure
#' (\code{ID}, \code{Timecode_in}, \code{Timecode_out}, \code{Text_content}).
#'
#' @param x a \code{subtitles} object.
#'
#' @returns A data frame (or tibble) containing only the metadata columns of \code{x}.
#' If there are no metadata columns, an empty data frame is returned.
#'
#' @noRd
extract_metadata <- function(x) {
sub_names <- c("ID", "Timecode_in", "Timecode_out", "Text_content")
md_names <- setdiff(colnames(x), sub_names)
Expand Down
7 changes: 7 additions & 0 deletions R/read_subtitles.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
#' @param encoding the name of the encoding to be used. Default is "\code{auto}" and
#' uses \code{\link[readr]{guess_encoding}} to detect encoding.
#' @param ... passed on to downstream methods.
#'
#' @returns An object of class \code{subtitles} (see \code{\link{subtitles}}).
#'
#' @importFrom utils read.csv
#' @export
#' @examples
Expand Down Expand Up @@ -431,6 +434,8 @@ subtitles <- function(
#' @param printlen the maximum number of subtitles to print.
#' @param ... further arguments passed to or from other methods.
#'
#' @returns Called for its side effects (printing to the console). Returns \code{NULL} invisibly.
#'
#' @examples
#' f <- system.file("extdata", "ex_subrip.srt", package = "subtools")
#' s <- read_subtitles(f)
Expand All @@ -455,6 +460,8 @@ print.multisubtitles <- function(x, printlen = 10L, ...) {
#'
#' @param x a \code{subtitles} or \code{multisubtitles} object.
#'
#' @returns Called for its side effects (printing to the console). Returns \code{NULL} invisibly.
#'
#' @examples
#' s <- read_subtitles(
#' system.file("extdata", "ex_subrip.srt", package = "subtools")
Expand Down
3 changes: 3 additions & 0 deletions R/unnest_tokens.R
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ unnest_tokens.subtitles <- function(
#' The default method (\code{unnest_tokens.default}) delegates to the original implementation.
#' See "?unnest_tokens.subtitles" for the \code{subtools} specific documentation.
#' @inheritParams tidytext::unnest_tokens
#'
#' @returns A tibble.
#'
#' @export
unnest_tokens <- function(
tbl,
Expand Down
46 changes: 42 additions & 4 deletions R/utils_regexes.R
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
# Extract file name from dir paths
#' Extract the file name from a path
#'
#' Strips trailing slashes and returns the last path component.
#'
#' @param x a character vector of file or directory paths.
#'
#' @returns A character vector of file names, the same length as \code{x}.
#'
#' @noRd
.extr_filename <- function(x) {
x <- gsub("/+$", "", x)
x <- regmatches(x, regexpr("([^/]+$)", x))
return(x)
}

# Extract extension from file name
#' Extract the extension from a file name
#'
#' Returns the part of the file name after the last dot (lower-case alphanumeric only).
#'
#' @param x a character vector of file names or paths.
#'
#' @returns A character vector of file extensions (without the leading dot),
#' the same length as \code{x}.
#'
#' @noRd
.extr_extension <- function(x) {
x <- sub("[?#].*$", "", x)
is_url <- grepl("^[[:alpha:]][[:alnum:].+-]*://", x)
Expand Down Expand Up @@ -90,7 +107,18 @@
return(res)
}

# Extract Episode number
#' Extract episode number from file paths
#'
#' Parses the file name component of each path and attempts to detect the episode
#' number using four common naming conventions:
#' \code{E01}, \code{EPISODE.2}, \code{S03E05}, and \code{2x05}.
#' Returns \code{NA} for paths where no convention matches.
#'
#' @param x a character vector of file or directory paths.
#'
#' @returns A numeric vector of episode numbers, the same length as \code{x}.
#'
#' @noRd
.extr_enum <- function(x) {
x <- .extr_filename(x)
x <- toupper(x)
Expand Down Expand Up @@ -129,7 +157,17 @@
}


# For WebVTT parsing: select cue blocks only (drop REGION, STYLE, NOTE)
#' Test whether a string is a WebVTT cue timing line
#'
#' Used during WebVTT parsing to identify cue blocks and filter out
#' \code{REGION}, \code{STYLE}, and \code{NOTE} blocks.
#'
#' @param x a single character string (or \code{NA}).
#'
#' @returns A single logical: \code{TRUE} if \code{x} matches the WebVTT cue timing
#' pattern, \code{FALSE} otherwise (including when \code{x} is \code{NA}).
#'
#' @noRd
.test_cuetiming <- function(x) {
if (is.na(x)) {
res <- FALSE
Expand Down
12 changes: 11 additions & 1 deletion R/utils_time.R
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
# Format time as HH:MM:SS.mS
#' Parse subtitle timecode strings into hms objects
#'
#' Accepts timecode strings in \code{HH:MM:SS.mS} or \code{MM:SS.mS} format
#' (comma as decimal separator is also accepted). Missing hour components are
#' padded to zero.
#'
#' @param x a character vector of timecode strings.
#'
#' @returns An \code{hms} vector (from the \pkg{hms} package), the same length as \code{x}.
#'
#' @noRd
.format_subtime <- function(x) {
x <- gsub(",", ".", x)
x <- strsplit(x, split = ":")
Expand Down
2 changes: 2 additions & 0 deletions R/write_subtitles.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
#' Not used (only SubRip format is currently implemented).
#' @param encoding the name of the encoding to be used.
#'
#' @returns Called for its side effects (writing to \code{file}). Returns \code{NULL} invisibly.
#'
#' @export
#'
write_subtitles <- function(x, file, format = "srt", encoding = "UTF-8") {
Expand Down
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,19 @@ oss_sub <- read_subtitles("ex_OSS_117.srt")
``` r
rushmore_sub
#> # A tibble: 4 × 4
#> ID Timecode_in Timecode_out Text_content
#> <chr> <time> <time> <chr>
#> ID Timecode_in Timecode_out Text_content
#> <chr> <time> <time> <chr>
#> 1 180 20'40.969" 20'48.269" Rushmore deserves an aquarium. A first class a…
#> 2 181 20'48.269" 20'50.870" - I don't know. What do you think, Ernie - Aqu…
#> 3 182 20'50.946" 20'57.370" - What kind of fish? - Barracudas. Stingrays. …
#> 4 183 20'58.051" 21'01.770" - Piranhas? Really? - Yes, I'm talking to a gu…

oss_sub
#> # A tibble: 3 × 4
#> ID Timecode_in Timecode_out Text_content
#> <chr> <time> <time> <chr>
#> ID Timecode_in Timecode_out Text_content
#> <chr> <time> <time> <chr>
#> 1 264 20'22.967" 20'27.427" Si vous voulez. Ça sera surtout l'occasion de …
#> 2 265 20'30.347" 20'32.297" Et non pas le gratin de pommes de terre.
#> 2 265 20'30.347" 20'32.297" Et non pas le gratin de pommes de terre.
#> 3 266 20'35.587" 20'37.697" Parce que ça ressemble à carotte, cairote.
```

Expand Down Expand Up @@ -184,14 +184,14 @@ multi_sub
#> A multisubtitles object with 2 elements
#> subtitles object [[1]]
#> # A tibble: 4 × 4
#> ID Timecode_in Timecode_out Text_content
#> <chr> <time> <time> <chr>
#> ID Timecode_in Timecode_out Text_content
#> <chr> <time> <time> <chr>
#> 1 180 20'40.969" 20'48.269" Rushmore deserves an aquarium. A first class a…
#> 2 181 20'48.269" 20'50.870" - I don't know. What do you think, Ernie - Aqu…
#> 3 182 20'50.946" 20'57.370" - What kind of fish? - Barracudas. Stingrays. …
#> 4 183 20'58.051" 21'01.770" - Piranhas? Really? - Yes, I'm talking to a gu…
#>
#>
#>
#>
#> subtitles object [[2]]
#> # A tibble: 4 × 7
#> ID Timecode_in Timecode_out Text_content Name Season Episode
Expand Down Expand Up @@ -232,8 +232,8 @@ timecode remapping according to the tokenisation process.
``` r
rushmore_sub
#> # A tibble: 4 × 4
#> ID Timecode_in Timecode_out Text_content
#> <chr> <time> <time> <chr>
#> ID Timecode_in Timecode_out Text_content
#> <chr> <time> <time> <chr>
#> 1 180 20'40.969" 20'48.269" Rushmore deserves an aquarium. A first class a…
#> 2 181 20'48.269" 20'50.870" - I don't know. What do you think, Ernie - Aqu…
#> 3 182 20'50.946" 20'57.370" - What kind of fish? - Barracudas. Stingrays. …
Expand All @@ -242,17 +242,17 @@ rushmore_sub
unnest_tokens(rushmore_sub)
#> # A tibble: 49 × 4
#> ID Timecode_in Timecode_out Text_content
#> <chr> <time> <time> <chr>
#> 1 180 20'40.9700" 20'41.4858" rushmore
#> 2 180 20'41.4868" 20'42.0026" deserves
#> 3 180 20'42.0036" 20'42.1318" an
#> 4 180 20'42.1328" 20'42.6486" aquarium
#> 5 180 20'42.6496" 20'42.7132" a
#> 6 180 20'42.7142" 20'43.0363" first
#> 7 180 20'43.0373" 20'43.3593" class
#> 8 180 20'43.3603" 20'43.8761" aquarium
#> 9 180 20'43.8771" 20'44.1991" where
#> 10 180 20'44.2001" 20'44.8451" scientists
#> <chr> <time> <time> <chr>
#> 1 180 20'40.9700" 20'41.4858" rushmore
#> 2 180 20'41.4868" 20'42.0026" deserves
#> 3 180 20'42.0036" 20'42.1318" an
#> 4 180 20'42.1328" 20'42.6486" aquarium
#> 5 180 20'42.6496" 20'42.7132" a
#> 6 180 20'42.7142" 20'43.0363" first
#> 7 180 20'43.0373" 20'43.3593" class
#> 8 180 20'43.3603" 20'43.8761" aquarium
#> 9 180 20'43.8771" 20'44.1991" where
#> 10 180 20'44.2001" 20'44.8451" scientists
#> # ℹ 39 more rows

unnest_tokens(bb_sub_clean, token = "sentences")
Expand Down
4 changes: 3 additions & 1 deletion codemeta.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,9 @@
},
"SystemRequirements": null
},
"fileSize": "169.162KB",
"applicationCategory": "TextProcessing",
"keywords": ["subtitles", "video", "text", "srt", "vtt", "ssa", "naturallanguageprocessing"],
"fileSize": "186.925KB",
"codeRepository": "https://github.com/fkeck/subtools",
"releaseNotes": "https://github.com/fkeck/subtools/blob/master/NEWS.md",
"readme": "https://github.com/fkeck/subtools/blob/main/README.md",
Expand Down
6 changes: 6 additions & 0 deletions cran-comments.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@
0 errors | 0 warnings | 1 note

* This is a new release.

All functions now have a @returns value.

Thank you for taking the time to review our package.

François et Alban
3 changes: 3 additions & 0 deletions man/as_subtitle.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions man/get_subtitles_info.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions man/print.multisubtitles.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions man/unnest_tokens.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions man/write_subtitles.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion tests/testthat.R
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
library(testthat)
library(subtools)

test_check("subtools")
Loading