|
|
|
@ -21,6 +21,18 @@ fn main() { |
|
|
|
|
let argv = |
|
|
|
|
clap::App::new("srtune") |
|
|
|
|
.version(env!("CARGO_PKG_VERSION")) |
|
|
|
|
.about("Modify a .srt file to match a video. Input and output can be a file or stream, \ |
|
|
|
|
so you pipe multiple invocations to create more complex operations. However, a single \ |
|
|
|
|
invocation should suffice in most cases.\n\ |
|
|
|
|
Times can be specified in any format: \ |
|
|
|
|
seconds (400, 14.52), hours:minutes:seconds (14:00, 15:51.12, 1:30:00). Decimal point \ |
|
|
|
|
can be period or comma; Times copied directly from the .srt file will also work.\n\ |
|
|
|
|
When a command allows both time and index as a value, index must be prefixed with '@'.\ |
|
|
|
|
The tool should be used iteratively, adjusting the invocation until the generated \ |
|
|
|
|
subtitle file meets expectations. As such, times and indices accepted by its parameters \ |
|
|
|
|
are, by default, the ones seen in the output file. Prefix a time or index with '^' \ |
|
|
|
|
to use the original value from the input instead (i.e. original index 14 is '^@14') |
|
|
|
|
") |
|
|
|
|
.arg(clap::Arg::with_name("input") |
|
|
|
|
.value_name("INFILE") |
|
|
|
|
.help("Input file, leave out for stdin"), |
|
|
|
@ -31,44 +43,43 @@ fn main() { |
|
|
|
|
.value_name("OUTFILE") |
|
|
|
|
.help("Output file, defaults to stdout"), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("fromtime") |
|
|
|
|
.arg(clap::Arg::with_name("from") |
|
|
|
|
.short("f") |
|
|
|
|
.long("from-time") |
|
|
|
|
.value_name("TIME") |
|
|
|
|
.help("Time of the first affected entry, defaults to 0. Use seconds, M:S or \ |
|
|
|
|
H:M:S, decimals are supported. Uses the original times before any shifts or \ |
|
|
|
|
scaling."), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("fromindex") |
|
|
|
|
.short("F") |
|
|
|
|
.long("from-index") |
|
|
|
|
.value_name("INDEX") |
|
|
|
|
.help("Time of the first affected entry, defaults to 0. Use seconds, M:S or \ |
|
|
|
|
H:M:S, decimals are supported. Uses the original times before any shifts or \ |
|
|
|
|
scaling."), |
|
|
|
|
.help("Time of the first affected entry, or its index. Defaults to 0/^@0."), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("move") |
|
|
|
|
.short("m") |
|
|
|
|
.long("move") |
|
|
|
|
.value_name("TIME") |
|
|
|
|
.help("Move subtitles in time. Use seconds, M:S or H:M:S, decimals and minus \ |
|
|
|
|
are supported."), |
|
|
|
|
are supported. Starts at the point specified by '--from'"), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("automove") |
|
|
|
|
.short("M") |
|
|
|
|
.long("automove") |
|
|
|
|
.value_name("SUBTIME=VIDEOTIME") |
|
|
|
|
.multiple(true) |
|
|
|
|
.help("Move subtitles following a given time or index to match video times. \ |
|
|
|
|
This automatically sets '--from' if not given explicitly."), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("scale") |
|
|
|
|
.short("s") |
|
|
|
|
.long("scale") |
|
|
|
|
.value_name("RATIO") |
|
|
|
|
.help("Scale subtitle times and durations to compensate for bitrate \ |
|
|
|
|
differences. 1 means identity, 1.1 makes all times 10% longer. If a start \ |
|
|
|
|
time was given, the scaling is relative to this point, otherwise to the first \ |
|
|
|
|
subtitle in the file. Has no effect if '--autoscale' is used."), |
|
|
|
|
differences. 1 means identity, 1.1 makes all times 10% longer. Scaling is \ |
|
|
|
|
relative to the first emitted subtitle with positive time (after shifting). \ |
|
|
|
|
Has no effect if '--autoscale' is used."), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("autoscale") |
|
|
|
|
.short("S") |
|
|
|
|
.long("autoscale") |
|
|
|
|
.value_name("SUBTIME=VIDEOTIME") |
|
|
|
|
.help("Calculate scaling based on a perceived difference. The scaling is \ |
|
|
|
|
related to the first subtitle, so ensure it is aligned properly with '--move'."), |
|
|
|
|
related to the first emitted subtitle, so ensure it is aligned properly \ |
|
|
|
|
with '--move'."), |
|
|
|
|
) |
|
|
|
|
.arg(clap::Arg::with_name("durscale") |
|
|
|
|
.short("d") |
|
|
|
@ -117,18 +128,27 @@ fn main() { |
|
|
|
|
} |
|
|
|
|
builder.init(); |
|
|
|
|
|
|
|
|
|
let from_time = match argv.value_of("fromtime") { |
|
|
|
|
Some(s) => { |
|
|
|
|
SubDuration::try_from(s).expect("Bad --from-time format").as_instant() |
|
|
|
|
let from = match argv.value_of("from") { |
|
|
|
|
Some(mut s) => { |
|
|
|
|
if s.starts_with('^') { |
|
|
|
|
s = &s[1..]; |
|
|
|
|
if s.starts_with('@') { |
|
|
|
|
let index = &s[1..].parse().expect("Bad --from format"); |
|
|
|
|
FromTag::ByIndexOrig(*index) |
|
|
|
|
} else { |
|
|
|
|
// this is always the orig time
|
|
|
|
|
FromTag::ByTime(SubDuration::try_from(s).expect("Bad --from format").as_instant()) |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
if s.starts_with('@') { |
|
|
|
|
let index = &s[1..].parse().expect("Bad --from format"); |
|
|
|
|
FromTag::ByIndex(*index) |
|
|
|
|
} else{ |
|
|
|
|
FromTag::ByTime(SubDuration::try_from(s).expect("Bad --from format").as_instant()) |
|
|
|
|
} |
|
|
|
|
None => SubInstant(0f32) |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let from_index = match argv.value_of("fromindex") { |
|
|
|
|
Some(s) => { |
|
|
|
|
s.parse().expect("Bad --from-index format") |
|
|
|
|
} |
|
|
|
|
None => 0u32 |
|
|
|
|
} |
|
|
|
|
None => FromTag::ByIndex(0) |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let shift = match argv.value_of("move") { |
|
|
|
@ -159,6 +179,9 @@ fn main() { |
|
|
|
|
panic!("Bad --autoscale format, should be SUBTIME=VIDEOTIME") |
|
|
|
|
} |
|
|
|
|
let (first, second) = (halves[0], halves[1]); |
|
|
|
|
if first.starts_with('^') { |
|
|
|
|
panic!("'--autoscale' always uses original times"); |
|
|
|
|
} |
|
|
|
|
let subtime = SubDuration::try_from(first).expect("Bad --autoscale format").as_instant(); |
|
|
|
|
let vidtime = SubDuration::try_from(second).expect("Bad --autoscale format").as_instant(); |
|
|
|
|
|
|
|
|
@ -167,6 +190,40 @@ fn main() { |
|
|
|
|
None => None |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
let mut automove = Vec::<AutoMoveTag>::new(); |
|
|
|
|
match argv.values_of("automove") { |
|
|
|
|
Some(ss) => { |
|
|
|
|
for s in ss { |
|
|
|
|
let halves: Vec<&str> = s.split("=").collect(); |
|
|
|
|
if halves.len() != 2 { |
|
|
|
|
panic!("Bad --automove format, should be SUBTIME=VIDEOTIME") |
|
|
|
|
} |
|
|
|
|
let (mut first, second) = (halves[0], halves[1]); |
|
|
|
|
let vidtime = SubDuration::try_from(second).expect("Bad --automove format").as_instant(); |
|
|
|
|
|
|
|
|
|
if first.starts_with('^') { |
|
|
|
|
first = &first[1..]; |
|
|
|
|
if s.starts_with('@') { |
|
|
|
|
let index = &first[1..].parse().expect("Bad --automove format"); |
|
|
|
|
automove.push(AutoMoveTag::ByIndexOrig(*index, vidtime)); |
|
|
|
|
} else { |
|
|
|
|
let subtime = SubDuration::try_from(first).expect("Bad --automove format").as_instant(); |
|
|
|
|
automove.push(AutoMoveTag::ByTimeOrig(subtime, vidtime)); |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
if s.starts_with('@') { |
|
|
|
|
let index = &first[1..].parse().expect("Bad --automove format"); |
|
|
|
|
automove.push(AutoMoveTag::ByIndex(*index, vidtime)); |
|
|
|
|
} else{ |
|
|
|
|
let subtime = SubDuration::try_from(first).expect("Bad --automove format").as_instant(); |
|
|
|
|
automove.push(AutoMoveTag::ByTime(subtime, vidtime)); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
None => (/* no automoves */) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let inf = argv.value_of("input"); |
|
|
|
|
let outf = argv.value_of("output"); |
|
|
|
|
let stdin = io::stdin(); |
|
|
|
@ -205,8 +262,8 @@ fn main() { |
|
|
|
|
durscale, |
|
|
|
|
scale, |
|
|
|
|
shift, |
|
|
|
|
from_index, |
|
|
|
|
from_time, |
|
|
|
|
from, |
|
|
|
|
automove, |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
@ -217,8 +274,30 @@ struct TransformOpts { |
|
|
|
|
durscale: f32, |
|
|
|
|
scale: f32, |
|
|
|
|
shift: SubDuration, |
|
|
|
|
from_index: u32, |
|
|
|
|
from_time: SubInstant, |
|
|
|
|
automove: Vec<AutoMoveTag>, |
|
|
|
|
from: FromTag, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Debug)] |
|
|
|
|
enum AutoMoveTag { |
|
|
|
|
ByTime(SubInstant, SubInstant), |
|
|
|
|
ByTimeOrig(SubInstant, SubInstant), |
|
|
|
|
ByIndex(u32, SubInstant), |
|
|
|
|
ByIndexOrig(u32, SubInstant), |
|
|
|
|
ByIndexRelative(u32, SubDuration), |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Debug)] |
|
|
|
|
enum FromTag { |
|
|
|
|
ByTime(SubInstant), |
|
|
|
|
ByIndex(u32), |
|
|
|
|
ByIndexOrig(u32) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
#[derive(Debug,Default,Clone,Copy)] |
|
|
|
|
struct IterState { |
|
|
|
|
start_time : Option<SubInstant>, |
|
|
|
|
renumber_i : u32, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io::Error>> + 'a>, |
|
|
|
@ -226,9 +305,7 @@ fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io: |
|
|
|
|
mut opts : TransformOpts) { |
|
|
|
|
debug!("Opts: {:#?}", opts); |
|
|
|
|
|
|
|
|
|
let mut start_time = SubInstant(0f32); |
|
|
|
|
let mut first_found = false; |
|
|
|
|
let mut renumber_i: u32 = 0; |
|
|
|
|
let mut istate = IterState::default(); |
|
|
|
|
|
|
|
|
|
let mut linebuf = vec![]; |
|
|
|
|
while let Some(Ok(x)) = lines.next() { |
|
|
|
@ -242,19 +319,21 @@ fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io: |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let istate_backup = istate; |
|
|
|
|
|
|
|
|
|
// 236
|
|
|
|
|
// 00:18:01,755 --> 00:18:03,774
|
|
|
|
|
// (掃除機の音)
|
|
|
|
|
// う~ん…。
|
|
|
|
|
match u32::from_str(x) { |
|
|
|
|
Ok(num) => { |
|
|
|
|
Ok(num_orig) => { |
|
|
|
|
// println!("Entry {}", num);
|
|
|
|
|
let datesrow = lines.next().unwrap().unwrap(); |
|
|
|
|
if datesrow.contains(" --> ") { |
|
|
|
|
let mut halves = datesrow.split(" --> "); |
|
|
|
|
let (first, second) = (halves.next().unwrap(), halves.next().unwrap()); |
|
|
|
|
let start = SubInstant::try_from(first).unwrap(); |
|
|
|
|
let end = SubInstant::try_from(second).unwrap(); |
|
|
|
|
let sub_start = SubInstant::try_from(first).unwrap(); |
|
|
|
|
let sub_end = SubInstant::try_from(second).unwrap(); |
|
|
|
|
|
|
|
|
|
linebuf.clear(); |
|
|
|
|
while let Some(Ok(x)) = lines.next() { |
|
|
|
@ -264,32 +343,45 @@ fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io: |
|
|
|
|
linebuf.push(x); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
let num_new = if opts.renumber { |
|
|
|
|
istate.renumber_i += 1; |
|
|
|
|
istate.renumber_i |
|
|
|
|
} else { |
|
|
|
|
num_orig |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// advance numbering only for the really emitted entries
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let mut subtitle = Subtitle { |
|
|
|
|
num, |
|
|
|
|
start, |
|
|
|
|
dur: SubDuration(end.0 - start.0), |
|
|
|
|
num : num_new, |
|
|
|
|
start: sub_start, |
|
|
|
|
dur: sub_end - sub_start, |
|
|
|
|
text: linebuf.join("\n"), |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if start >= opts.from_time && num >= opts.from_index { |
|
|
|
|
if !first_found { |
|
|
|
|
debug!("Scaling anchored at {} (#{}), editing starts", start, num); |
|
|
|
|
if match opts.from { |
|
|
|
|
FromTag::ByTime(ins) => ins <= sub_start, |
|
|
|
|
FromTag::ByIndex(idx) => idx <= num_new, |
|
|
|
|
FromTag::ByIndexOrig(idx) => idx <= num_orig, |
|
|
|
|
} { |
|
|
|
|
if istate.start_time.is_none() { |
|
|
|
|
debug!("Scaling anchored at {} (#{}), editing starts", sub_start, num_orig); |
|
|
|
|
debug!("Shifting by: {}", opts.shift); |
|
|
|
|
|
|
|
|
|
start_time = start; |
|
|
|
|
first_found = true; |
|
|
|
|
istate.start_time = Some(sub_start); |
|
|
|
|
|
|
|
|
|
if let Some((mut subt, mut vidt)) = opts.autoscale { |
|
|
|
|
debug!("Autoscale: VT {} -> ST {}", vidt, subt); |
|
|
|
|
subt -= start_time; |
|
|
|
|
vidt -= start_time + opts.shift; |
|
|
|
|
subt -= sub_start; |
|
|
|
|
vidt -= sub_start + opts.shift; |
|
|
|
|
if subt.0 <= 0f32 { |
|
|
|
|
panic!("Error in autoscale, start time is negative or zero."); |
|
|
|
|
} |
|
|
|
|
if vidt.0 <= 0f32 { |
|
|
|
|
panic!("Error in autoscale, end time is negative or zero."); |
|
|
|
|
} |
|
|
|
|
debug!(" relative to #{}, after \"move\": VT {} -> ST {}", num, vidt, subt); |
|
|
|
|
debug!(" relative to #{}, after \"move\": VT {} -> ST {}", num_orig, vidt, subt); |
|
|
|
|
opts.scale = vidt.0 / subt.0; |
|
|
|
|
debug!("Resolved scale as {}", opts.scale); |
|
|
|
|
} |
|
|
|
@ -299,24 +391,76 @@ fn transform_subtitles<'a>(lines : &mut Box<dyn Iterator<Item=Result<String, io: |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if opts.scale != 1f32 { |
|
|
|
|
subtitle.start = subtitle.start.scale(start_time, opts.scale); |
|
|
|
|
subtitle.start = subtitle.start.scale(istate.start_time.unwrap(), opts.scale); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
subtitle.dur *= opts.durscale; |
|
|
|
|
subtitle.start += opts.shift; |
|
|
|
|
|
|
|
|
|
for amove in opts.automove.iter_mut() { |
|
|
|
|
match amove { |
|
|
|
|
AutoMoveTag::ByIndex(idx, ref vidt) => { |
|
|
|
|
if num_new >= *idx { |
|
|
|
|
debug!("Move by new index starts, reached {}", idx); |
|
|
|
|
let vidt = *vidt; |
|
|
|
|
let dif = vidt - subtitle.start; |
|
|
|
|
subtitle.start = vidt; |
|
|
|
|
std::mem::replace(amove, AutoMoveTag::ByIndexRelative(num_new, dif)); |
|
|
|
|
} else if *vidt < subtitle.start && *idx > num_new { |
|
|
|
|
// istate = istate_backup;
|
|
|
|
|
warn!("Skip overlapped #{} (by index)", num_orig); |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
AutoMoveTag::ByIndexOrig(idx, ref vidt) => { |
|
|
|
|
if num_orig >= *idx { |
|
|
|
|
debug!("Move by orig index starts, reached {}", idx); |
|
|
|
|
let vidt = *vidt; |
|
|
|
|
let dif = vidt - subtitle.start; |
|
|
|
|
subtitle.start = vidt; |
|
|
|
|
std::mem::replace(amove, AutoMoveTag::ByIndexRelative(num_new, dif)); |
|
|
|
|
} else if *vidt < sub_start && *idx > num_orig { |
|
|
|
|
// istate = istate_backup;
|
|
|
|
|
warn!("Skip overlapped #{} (by orig index)", num_orig); |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
AutoMoveTag::ByTime(ref subt, ref vidt) => { |
|
|
|
|
if subtitle.start >= *vidt { |
|
|
|
|
// TODO verify
|
|
|
|
|
subtitle.start += *vidt - *subt; |
|
|
|
|
} else if *vidt < subtitle.start && *subt > subtitle.start { |
|
|
|
|
// istate = istate_backup;
|
|
|
|
|
warn!("Skip overlapped #{} (by time)", num_orig); |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
AutoMoveTag::ByTimeOrig(ref subt, ref vidt) => { |
|
|
|
|
if sub_start >= *subt { |
|
|
|
|
// TODO verify
|
|
|
|
|
subtitle.start += *vidt - *subt; |
|
|
|
|
} else if *vidt < subtitle.start && *subt > sub_start { |
|
|
|
|
// istate = istate_backup;
|
|
|
|
|
warn!("Skip overlapped #{} (by orig time)", num_orig); |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
// this is used internally
|
|
|
|
|
AutoMoveTag::ByIndexRelative(ref idx, ref dif) => { |
|
|
|
|
if num_new >= *idx { |
|
|
|
|
subtitle.start += *dif; |
|
|
|
|
} |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if subtitle.start.0 < 0f32 { |
|
|
|
|
warn!("Discarding negative time entry #{} @ {:.3}s", subtitle.num, subtitle.start.0); |
|
|
|
|
istate = istate_backup; |
|
|
|
|
continue; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// advance numbering only for the really emitted entries
|
|
|
|
|
if opts.renumber { |
|
|
|
|
renumber_i += 1; |
|
|
|
|
subtitle.num = renumber_i; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
outfile.write(subtitle.to_string().as_bytes()).expect("failed to write"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
@ -373,6 +517,14 @@ impl Sub<SubDuration> for SubInstant { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl Sub<SubInstant> for SubInstant { |
|
|
|
|
type Output = SubDuration; |
|
|
|
|
|
|
|
|
|
fn sub(self, rhs: SubInstant) -> Self::Output { |
|
|
|
|
SubDuration(self.0 - rhs.0) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
impl Mul<f32> for SubDuration { |
|
|
|
|
type Output = SubDuration; |
|
|
|
|
|
|
|
|
|