Sync subtitles: Difference between revisions

→‎Less brutal: use grammar actions and add encapsulations
(Added Yabasic)
(→‎Less brutal: use grammar actions and add encapsulations)
 
(8 intermediate revisions by 6 users not shown)
Line 70:
</pre>
 
;References
<br><br>
 
:* Wikipedia: [[wp:SubRip|SubRip]]
 
=={{header|Amazing Hopper}}==
Line 735 ⟶ 737:
{{out}}
<pre>Same as FreeBASIC entry.</pre>
 
=={{header|COBOL}}==
<syntaxhighlight lang="cobol"> IDENTIFICATION DIVISION.
PROGRAM-ID. SubtitleSync.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT INPUT-FILE ASSIGN TO "movie.srt".
SELECT OUTPUT-FILE ASSIGN TO "movie_corrected.srt".
DATA DIVISION.
FILE SECTION.
FD INPUT-FILE.
01 IN-REC PIC X(100).
FD OUTPUT-FILE.
01 OUT-REC PIC X(100).
WORKING-STORAGE SECTION.
01 WS-SECS PIC S9(4) COMP VALUE 0.
01 WS-LINE PIC X(100) VALUE SPACES.
01 WS-START-TIME PIC X(12) VALUE SPACES.
01 WS-END-TIME PIC X(12) VALUE SPACES.
01 WS-HH PIC 99 VALUE 0.
01 WS-MM PIC 99 VALUE 0.
01 WS-SS PIC 99 VALUE 0.
01 WS-TTT PIC 999 VALUE 0.
PROCEDURE DIVISION.
DISPLAY "After fast-forwarding 9 seconds:"
MOVE 9 TO WS-SECS
OPEN INPUT INPUT-FILE
OPEN OUTPUT OUTPUT-FILE
PERFORM syncSubtitles
CLOSE INPUT-FILE
CLOSE OUTPUT-FILE
DISPLAY "After rolling-back 9 seconds:"
MOVE -9 TO WS-SECS
OPEN INPUT INPUT-FILE
OPEN OUTPUT OUTPUT-FILE
PERFORM syncSubtitles
CLOSE INPUT-FILE
CLOSE OUTPUT-FILE
STOP RUN.
syncSubtitles SECTION.
READ INPUT-FILE AT END GO TO CLOSE-FILES
IF WS-LINE(1:3) = "-->" THEN
MOVE WS-LINE(1:12) TO WS-START-TIME
MOVE WS-LINE(18:12) TO WS-END-TIME
PERFORM addSeconds
MOVE WS-START-TIME TO WS-LINE(1:12)
PERFORM addSeconds
MOVE WS-END-TIME TO WS-LINE(18:12)
STRING WS-START-TIME DELIMITED BY SIZE " --> "
DELIMITED BY SIZE WS-END-TIME DELIMITED BY SIZE INTO OUT-REC
WRITE OUT-REC
ELSE
MOVE WS-LINE TO OUT-REC
WRITE OUT-REC
END-IF
END-READ
CLOSE INPUT-FILE
CLOSE OUTPUT-FILE
EXIT.
addSeconds SECTION.
MOVE WS-START-TIME(1:2) TO WS-HH
MOVE WS-START-TIME(4:2) TO WS-MM
MOVE WS-START-TIME(7:2) TO WS-SS
MOVE WS-START-TIME(10:3) TO WS-TTT
ADD WS-SECS TO WS-SS
IF WS-SS < 0
ADD 60 TO WS-SS
SUBTRACT 1 FROM WS-MM
END-IF
IF WS-MM < 0
ADD 60 TO WS-MM
SUBTRACT 1 FROM WS-HH
END-IF
IF WS-HH < 0
ADD 24 TO WS-HH
END-IF
DIVIDE WS-SS BY 60 GIVING WS-MM REMAINDER WS-SS
DIVIDE WS-MM BY 60 GIVING WS-HH REMAINDER WS-MM
MOVE WS-HH TO WS-START-TIME(1:2)
MOVE WS-MM TO WS-START-TIME(4:2)
MOVE WS-SS TO WS-START-TIME(7:2)
MOVE WS-TTT TO WS-START-TIME(10:3)
EXIT.
CLOSE-FILES SECTION.
CLOSE INPUT-FILE
CLOSE OUTPUT-FILE
EXIT.
 
</syntaxhighlight>
 
=={{header|jq}}==
'''Works with gojq, the Go implementation of jq'''
 
The following program has been written for gojq, the Go implementation of jq,
and will almost surely need adapting for use with other versions of jq.
 
'''Usage'''
 
To fast-forward the subtitles by 9 seconds:
<pre>
< movie.srt gojq -nRr --argjson seconds 9 -f sync-subtitles.jq
</pre>
where sync-subtitles.jq is the name of the file containing the jq program.
To roll-back, specify `seconds` as a negative number, e.g.:
<pre>
< movie.srt gojq -nRr --argjson seconds -9 -f sync-subtitles.jq
</pre>
<syntaxhighlight lang="jq">
def syncSubtitles($secs):
def fmt: "%H:%M:%S";
def adjust: strptime(fmt) | .[5] += $seconds | strftime(fmt);
 
if ($secs|type) != "number" then "The number of seconds must be specified as an integer" | error end
| inputs as $line
| ($line
| capture("^(?<start>[^,]*),(?<startms>[0-9]*) *--> *(?<finish>[^,]*),(?<finishms>[0-9]*)")
| "\(.start|adjust),\(.startms) --> \(.finish|adjust),\(.finishms)" )
// $line ;
 
if $seconds > 0 then
"Fast-forwarding \($seconds) seconds" | debug
| syncSubtitles($seconds)
elif $seconds == 0 then
"No resynchronization is needed" | debug
else
"Rolling-back \(-$seconds) seconds" | debug
| syncSubtitles($seconds)
end
</syntaxhighlight>
{{output}}
As shown elsewhere on this page.
 
=={{header|Julia}}==
<syntaxhighlight lang="julia">using Dates
function syncsubtitles(intext::AbstractString, sec::Integer, msec::Integer)
outtext, fmt = "", dateformat"HH:MM:SS,sss"
deltatime = Dates.Second(sec) + Dates.Millisecond(msec)
for line in split(intext, r"\r?\n")
if !isempty(line) && length(begin times = split(line, " --> ") end) == 2
start, stop = DateTime.(times, fmt) .+ deltatime
line = join(Dates.format.((start, stop), fmt), " ==> ")
end
outtext *= line * "\n"
end
return outtext
end
 
function syncsubtitles(infile, outfile::AbstractString, sec, msec = 0)
outs = open(outfile, "w")
write(outs, syncsubtitles(read(infile, String), sec, msec))
close(outs)
end
 
println("After fast-forwarding 9 seconds:\n")
syncsubtitles("movie.srt", "movie_corrected.srt", 9)
println(read("movie_corrected.srt", String))
println("After rolling-back 9 seconds:\n")
syncsubtitles("movie.srt", "movie_corrected2.srt", -9)
println(read("movie_corrected2.srt", String))
</syntaxhighlight>{{out}}
<pre>
After fast-forwarding 9 seconds:
 
1
00:01:40,550 ==> 00:01:45,347
Four billion years ago,
the first marine life forms.
 
2
00:01:45,555 ==> 00:01:51,019
First came the fish...then slowly
other life forms evolved.
 
3
00:01:51,144 ==> 00:01:52,979
Therefore, our ancestors...
 
4
00:01:52,979 ==> 00:01:54,898
...came from fish.
 
5
00:01:55,232 ==> 00:01:56,608
Everyone, come and see this.
 
6
00:01:56,733 ==> 00:01:59,361
Cretaceous Tyrannosaurus.
 
7
00:01:59,736 ==> 00:02:01,488
Ferocious!
 
8
00:02:07,035 ==> 00:02:07,869
Red,
 
9
00:02:08,203 ==> 00:02:09,079
Pong!
 
10
00:02:11,540 ==> 00:02:12,999
Isn't this a gecko?
 
11
00:02:13,416 ==> 00:02:16,419
How else can I get a 15 ton T-Rex in here?
 
12
00:02:16,503 ==> 00:02:20,048
With our advanced technology, I shrank it down.
 
 
After rolling-back 9 seconds:
 
1
00:01:22,550 ==> 00:01:27,347
Four billion years ago,
the first marine life forms.
 
2
00:01:27,555 ==> 00:01:33,019
First came the fish...then slowly
other life forms evolved.
 
3
00:01:33,144 ==> 00:01:34,979
Therefore, our ancestors...
 
4
00:01:34,979 ==> 00:01:36,898
...came from fish.
 
5
00:01:37,232 ==> 00:01:38,608
Everyone, come and see this.
 
6
00:01:38,733 ==> 00:01:41,361
Cretaceous Tyrannosaurus.
 
7
00:01:41,736 ==> 00:01:43,488
Ferocious!
 
8
00:01:49,035 ==> 00:01:49,869
Red,
 
9
00:01:50,203 ==> 00:01:51,079
Pong!
 
10
00:01:53,540 ==> 00:01:54,999
Isn't this a gecko?
 
11
00:01:55,416 ==> 00:01:58,419
How else can I get a 15 ton T-Rex in here?
 
12
00:01:58,503 ==> 00:02:02,048
With our advanced technology, I shrank it down.
 
</pre>
 
=={{header|Phix}}==
Needed a bugfix to adjust_timedate() for time-only handling, in that a y,m,d of 0,0,0 pre-dates the introduction of the Gregorian calendar.
There was also a floor(milliseconds) which should have been/is now round(milliseconds). Fixes can be grabbed from github if needed.
 
You could of course use sequence lines = read_lines("movie.srt") and write_lines("movie_corrected.srt",lines) for a file-based solution, but obviously that would not be compatible with JavaScript in a browser window.
<syntaxhighlight lang="phix">
requires("1.0.6") -- (a time-only handling bugfix in adjust_timedate)
Line 833 ⟶ 1,108:
{{out}}
<pre>Same as FreeBASIC entry.</pre>
 
=={{header|Raku}}==
===Brutal===
<syntaxhighlight lang="raku" line># 20240621 Raku programming solution
 
sub MAIN() {
my @lines = $*IN.lines;
for 9, -9 -> $seconds {
say "Original subtitle adjusted by {$seconds.fmt('%+d')} seconds.";
for @lines -> $line {
if $line ~~ /(\d ** 2 ':' \d ** 2 ':' \d ** 2 ',' \d ** 3) ' --> ' (\d ** 2 ':' \d ** 2 ':' \d ** 2 ',' \d ** 3)/ {
my $start = adjust-time($0.Str, $seconds);
my $end = adjust-time($1.Str, $seconds);
my $adjusted = $line;
$adjusted ~~ s/\d ** 2 ':' \d ** 2 ':' \d ** 2 ',' \d ** 3 ' --> ' \d ** 2 ':' \d ** 2 ':' \d ** 2 ',' \d ** 3/$start ~ ' --> ' ~ $end/.Str;
say $adjusted
} else {
say $line;
}
}
say()
}
}
 
sub adjust-time($time, $seconds) {
my ($time_str, $milliseconds_str) = $time.split(',');
my (\hh, \mm, \ss) = $time_str.split(':')>>.Int;
my $milliseconds = $milliseconds_str.Int;
my $datetime = DateTime.new(:year, :month, :day,
:hour(hh), :minute(mm), :second(ss));
given $datetime .= later(:seconds($seconds)) {
return sprintf('%02d:%02d:%02d,%03d',.hour,.minute,.second,$milliseconds)
}
}</syntaxhighlight>
 
You may [https://ato.pxeger.com/run?1=lVTdbuNEFJYQV-cpDquu7JSJ49_EdtUuCKjIBQtaKnFTCbnxJDa1Z1aecUq0aiWeg5tewEvtI_AUnIlj1ykroc6FfXzm-87PN8fz519Ndts-Pv7d6vU0_vj5H6q9wR--Xr61J_gBELHe4VdVKbjCczw5Xb519h9nZmstG0wYThOcXuCJ4ispctWxaKlsh69-bMpNKbIKKawudcUxy39rleY53uzwQ09y1rW2rddf5tbkHnvfq7NDJJPnUINJZKwhC61yffA9PODMvs7x9BR9tFILP2mz3g4maOGUIlr4ItZsnLxT6ETprNGkUNfdVJc1t09c52fdsEGZydl_aFzkZD2jef9LGzQ87zo_RjxtkyBq9oLWBj1ewJkden8YyA_7vmami-PCzEQMxT3t3COvFH8m6h77rLd7OH4Txp7A3nEPYAb3SEfzHMk4jHO386vai1yXVVUeIMY1MZqafUe9r0oaSmZ1B2CI10XB8Lqu6aHUgDS0Hp1ak4sLZyl0zzlKYBjPEx6B80xzE5KA35J5ZeoQ_M5Odzyjas1Kayl00dmY5tmO4ZFwn1hpIdvGLooJI3YpWs3tujYfXRk29dL1uCm3XIyqcM6xIruxD0hlD2pOhvNquG4bgep9Uwq9pr_Y9fN0eLDXbpBbzDElMKfLzpwuCjsSoz_J7iY6XEiPHz_7xwPXTV0vDTwWRe5-yA6OOQvCBVxSaLwxkaRAI5TCbCMZ6ILjumyUxjprzP1QlWtuLpNaOYDgwxAliqJR2NBnrpfA5Z66ykiHLpIqHMchk3qt5F21A0kfzSgq8q2stjx3AAIYYnlhOA4esGSRwBUxOZFoQE31mVhxpWWjKANACGPsmByxOImBQPuy1o2su7oAop4zZ37gjzkLNndj-G7Lm50UlG8liZrRxaO46awkMWAOA3gRBCN25LJg7sE3dMjZistW4dWuyYSQKmub1lAXMCAXwXxM9VkYx3DJG7kqifkFQNxjY-YGY8nJEc8TeMdzBpD0qIT57lMxfuq6zCXtfpJiQ8E8Fw5un0WhO8aRbkkCSyUsve8QM9zw1a18QyyvZ4Us9OZj1oIcCXwv77r7aJUJXBJPE9uLUNNwXU3f8d-xFGhOzwTzYeBGR6V6HnPDGH4pddEdcL41Z5yj5qtCyEpu6L9doipIzFssNebyTjjdzP8L Attempt This Online!]
 
===Less brutal===
<syntaxhighlight lang="raku" line># 20240622 Raku programming solution
 
grammar SRT { # loc.gov/preservation/digital/formats/fdd/fdd000569.shtml
token TOP { ^ <subtitle>+ % \n $ }
token subtitle { <index> \n <timecode> \n <text> \n? }
token index { \d+ }
token timecode { <timestamp> ' --> ' <timestamp> }
token timestamp { \d ** 2 ':' \d ** 2 ':' \d ** 2 ',' \d ** 3 }
token text { <line>+ % \n }
token line { <-[\n]>+ }
}
 
class SRT::Actions { has @.subtitles;
 
method TOP($/) {
@.subtitles = $<subtitle>».made;
make @.subtitles
}
 
method subtitle($/) {
make {
index => $<index>.Str,
start => $<timecode><timestamp>[0].made,
end => $<timecode><timestamp>[1].made,
text => $<text>.made,
}
}
 
method timestamp($/) { make $/.Str }
 
method text($/) { make $<line>.join("\n") }
}
 
class SubtitleAdjuster {
 
method adjust-time($time, $seconds) {
my ($time-str, $milliseconds-str) = $time.split(',');
my (\hh, \mm, \ss) = $time-str.split(':')>>.Int;
my \mls = $milliseconds-str.Int;
my $datetime = DateTime.new( :year, :month, :day,
:hour(hh), :minute(mm), :second(ss));
given $datetime .= later(:seconds($seconds)) {
return sprintf('%02d:%02d:%02d,%03d', .hour, .minute, .second, mls)
}
}
 
method adjust-subtitles(@subtitles, Int $seconds) {
@subtitles.map({
$_<start> = self.adjust-time($_<start>, $seconds);
$_<end> = self.adjust-time($_<end>, $seconds);
$_;
});
}
 
method format-srt(@subtitles) {
@subtitles.map({
$_<index> ~ "\n"
~ $_<start> ~ " --> " ~ $_<end> ~ "\n"
~ $_<text> ~ "\n"
}).join("\n");
}
}
 
my $srt-content = $*IN.slurp;
my $parsed = SRT.parse($srt-content, :actions(SRT::Actions.new));
my @subtitles = $parsed.made;
 
my $adjuster = SubtitleAdjuster.new;
 
for 9, -9 -> \N {
my @adjusted-subtitles = $adjuster.adjust-subtitles(@subtitles.deepmap(*.clone), N);
say "Original subtitle adjusted by {N.fmt('%+d')} seconds.";
say $adjuster.format-srt(@adjusted-subtitles);
}</syntaxhighlight>
 
You may [https://ato.pxeger.com/run?1=jVbdbts2FAZ2NfApzjIHllNakfwvJ3FTbCuWXaRFG2AXdTewFm2rkUiDpJ0ahfsiu-nF9hS722P0EfYUO9Sf6SYtJsAydc75Pp4_HumPPxW7XX_8-NfazNujT9_-slAsy5iCly9u4D18D6mc-Qu5OV0prrnaMJNIcRoni8Sw9HQuVcaMPp3Hsf0FQdAfRL5emiwlAGDkLRdw8-w5Mv0G53r9xiQm5ZNHcAxTAQ3Y7a0qJZqeJyLm7ybW5NwkGZ_JmJdP_J2xq8cuMrdG2DR-5IorpCW0a21YtppAE9pte3dln6FyaU4IJyfQgea4-fCaVuvuAQU6aTdNE1GH6qit2Krbr6bi9cS6vCNkljKtbc7H4yczm2KNJkum4dKvEqPPiCXJuFnK2CbVa5y24L2V4eXYwQU09rn-528_YzE_K-0ydstdYyveucSV5oA9R1UPeBUpv5jgRkWt_JdG0b0eM6hMoa8L6CT8VfA6d8pBcBHj_cuI8B4iT3OJsG1xoN_di6vmKgIrQmqcWsc_M0SyA5uikP5bmQjvaCqOWgclK9P1JH671oYrzFLuQknGcnHbbu417J1CQ2N0Itb77EK2hULb1phGaGRJmialmRW1bEmt3terNDEeNl7rzAVPl0sK0yzDm9a1tYVWiHGzNZn4V8K4uGmWanCui_tbfw5pxMxwS47GP-Lyxnol-J0H4y1n6Pw4k8KgN-OYbZ1qfeUaL-Vaectly4ITsTbcyzL7ULjhYUT7aBfJBs_Q3gv_AlJcK6-01l6d35bbsaC4WSscMyuVCDP3msdBJx7XN3ocdOMmBd_6gn-FH7goyChgploV2_3mKstcnyrvsl5SwAw-UPS9BTbuynNcbfx-np-fCaZY83TuHzRRpXQa6ewAiydpktfyIaxVfglZPewK8UGAxaBva2Wc0P5nMOU0_wD28Ow1H5xAUZeP5aNCmofwoH3xAjhQ7VrO0Sw9R99tr6K7bQzUcKwA9vbJ1bWv07VaneXaFVOax6jAsevnD56LwAZkxSj23Llsm932IzJcHozcgq4ctvkGrJoJF_fGhGVBK0wrRBTaEWDw0-sin5a5hMbtgy0qQv8r_ebHnK9sEU78WSoFx4N0XaRFsy0cPVP46hYs3b9vq63gzRbeX_vzDKfF8aO42dpB2Sb-UY3fu-B2xH1vccdd8UlRfll8_PTNvyEJgnEQjrsh7feDvOClYEC7vSF5imcP3tgBJAXYaaKBLSQlZslhnihtcCAr-_ZMkznPW1L7BEiH1Cz9ft-h7XVoEEbkaQ6dMZwWBZNe-r6PS5wGqbxLt0Tig3JYgW9kusFiEtIlNVfY67nkXRoNI3KDSI4gnBXWeyZm-JaRSuMOhPSIa-uC-3QUjQga5W7NlcwKvwjpV5gB7XQ7LmZIB8GI_LThaot1pTCTCGX43tTcRpZgMsiA1MbDbtdB9wPaHYTkBxyDbMblWsPNVjEhpGZrtbbQIakth92BC-3Q3mhEnnIlZwkivyNkVNmOaNB1U46C0SAiL3hMCYkqq4h2gr0znXEQ0ABz91yKBZKFASnFHdrvBa4d5i2KyJUWTZNHCAwWfHYrHyMqrFA92gsHLmqIgoj8LO-Ap5pj5QVcIc4gOuzjVxh-krZf4PdLIsBWz5J1SI3tH7gahjTojciviVkWBY43tsb2M2G2FDKViy1OeNBLTOYtJAZieSf8ouf_Aw) Attempt This Online!]
 
=={{header|Rust}}==
{{libheader|chrono}}
{{libheader|lazy_static}}
{{libheader|regex}}
 
<syntaxhighlight lang="rust">
use chrono::{NaiveTime, TimeDelta};
use lazy_static::lazy_static;
use regex::Regex;
use std::env;
use std::fs::File;
use std::io::{self, BufRead};
use std::io::{stdout, Write};
use std::path::Path;
 
lazy_static! {
static ref RE: Regex =
Regex::new(r"(\d{2}:\d{2}:\d{2},\d{3})\s+-->\s+(\d{2}:\d{2}:\d{2},\d{3})").unwrap();
static ref FORMAT: String = String::from("%H:%M:%S,%3f");
}
 
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().collect();
 
match args.len() {
3 => {
let path = &args[1];
let secs: i64 = args[2].parse()?;
let mut lock = stdout().lock();
for line in sync_file(path, secs)? {
writeln!(lock, "{line}").unwrap()
}
}
_ => println!("usage: subrip-sync <filename> <seconds>"),
}
 
Ok(())
}
 
fn sync_file<P>(path: P, secs: i64) -> Result<impl Iterator<Item = String>, io::Error>
where
P: AsRef<Path>,
{
let file = File::open(path)?;
let reader = io::BufReader::new(file);
Ok(sync_lines(reader.lines().flatten(), secs))
}
 
fn sync_lines(lines: impl Iterator<Item = String>, secs: i64) -> impl Iterator<Item = String> {
let delta = TimeDelta::new(secs, 0).unwrap();
lines.map(move |line| {
if let Some(groups) = RE.captures(&line) {
format(&groups[1], &groups[2], &delta)
} else {
line
}
})
}
 
fn format(start: &str, stop: &str, delta: &TimeDelta) -> String {
format!(
"{} --> {}",
(NaiveTime::parse_from_str(start, &FORMAT).unwrap() + *delta).format(&FORMAT),
(NaiveTime::parse_from_str(stop, &FORMAT).unwrap() + *delta).format(&FORMAT)
)
}
</syntaxhighlight>
 
{{out}}
<pre>
$ subrip-sync movie.srt 9 > movie_plus_9.srt
$ cat movie_plus_9.srt
1
00:01:40,550 --> 00:01:45,347
Four billion years ago,
the first marine life forms.
 
2
00:01:45,555 --> 00:01:51,019
First came the fish...then slowly
other life forms evolved.
 
3
00:01:51,144 --> 00:01:52,979
Therefore, our ancestors...
 
4
00:01:52,979 --> 00:01:54,898
...came from fish.
 
5
00:01:55,232 --> 00:01:56,608
Everyone, come and see this.
 
6
00:01:56,733 --> 00:01:59,361
Cretaceous Tyrannosaurus.
 
7
00:01:59,736 --> 00:02:01,488
Ferocious!
 
8
00:02:07,035 --> 00:02:07,869
Red,
 
9
00:02:08,203 --> 00:02:09,079
Pong!
 
10
00:02:11,540 --> 00:02:12,999
Isn't this a gecko?
 
11
00:02:13,416 --> 00:02:16,419
How else can I get a 15 ton T-Rex in here?
 
12
00:02:16,503 --> 00:02:20,048
With our advanced technology, I shrank it down.
</pre>
 
=={{header|Wren}}==
354

edits