1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
|
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import argparse
from datetime import datetime
import logging
import os
import shutil
import subprocess
import sys
from typing import cast, Any, Callable, Dict, List, Optional, Tuple, Union
import numpy as np
from ffsubsync.aligners import (
FFTAligner,
MaxScoreAligner,
FailedToFindAlignmentException,
)
from ffsubsync.constants import (
DEFAULT_APPLY_OFFSET_SECONDS,
DEFAULT_FRAME_RATE,
DEFAULT_MAX_OFFSET_SECONDS,
DEFAULT_MAX_SUBTITLE_SECONDS,
DEFAULT_NON_SPEECH_LABEL,
DEFAULT_START_SECONDS,
DEFAULT_VAD,
DEFAULT_ENCODING,
FRAMERATE_RATIOS,
SAMPLE_RATE,
SUBTITLE_EXTENSIONS,
)
from ffsubsync.ffmpeg_utils import ffmpeg_bin_path
from ffsubsync.sklearn_shim import Pipeline, TransformerMixin
from ffsubsync.speech_transformers import (
VideoSpeechTransformer,
DeserializeSpeechTransformer,
make_subtitle_speech_pipeline,
)
from ffsubsync.subtitle_parser import make_subtitle_parser
from ffsubsync.subtitle_transformers import SubtitleMerger, SubtitleShifter
from ffsubsync.version import get_version
logger: logging.Logger = logging.getLogger(__name__)
def override(args: argparse.Namespace, **kwargs: Any) -> Dict[str, Any]:
args_dict = dict(args.__dict__)
args_dict.update(kwargs)
return args_dict
def _ref_format(ref_fname: Optional[str]) -> Optional[str]:
if ref_fname is None:
return None
return ref_fname[-3:]
def make_test_case(
args: argparse.Namespace, npy_savename: Optional[str], sync_was_successful: bool
) -> int:
if npy_savename is None:
raise ValueError("need non-null npy_savename")
tar_dir = "{}.{}".format(
args.reference, datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
)
logger.info("creating test archive {}.tar.gz...".format(tar_dir))
os.mkdir(tar_dir)
try:
log_path = "ffsubsync.log"
if args.log_dir_path is not None and os.path.isdir(args.log_dir_path):
log_path = os.path.join(args.log_dir_path, log_path)
shutil.copy(log_path, tar_dir)
shutil.copy(args.srtin[0], tar_dir)
if sync_was_successful:
shutil.move(args.srtout, tar_dir)
if _ref_format(args.reference) in SUBTITLE_EXTENSIONS:
shutil.copy(args.reference, tar_dir)
elif args.serialize_speech or args.reference == npy_savename:
shutil.copy(npy_savename, tar_dir)
else:
shutil.move(npy_savename, tar_dir)
supported_formats = set(list(zip(*shutil.get_archive_formats()))[0])
preferred_formats = ["gztar", "bztar", "xztar", "zip", "tar"]
for archive_format in preferred_formats:
if archive_format in supported_formats:
shutil.make_archive(tar_dir, archive_format, os.curdir, tar_dir)
break
else:
logger.error(
"failed to create test archive; no formats supported "
"(this should not happen)"
)
return 1
logger.info("...done")
finally:
shutil.rmtree(tar_dir)
return 0
def get_srt_pipe_maker(
args: argparse.Namespace, srtin: Optional[str]
) -> Callable[[Optional[float]], Union[Pipeline, Callable[[float], Pipeline]]]:
if srtin is None:
srtin_format = "srt"
else:
srtin_format = os.path.splitext(srtin)[-1][1:]
parser = make_subtitle_parser(fmt=srtin_format, caching=True, **args.__dict__)
return lambda scale_factor: make_subtitle_speech_pipeline(
**override(args, scale_factor=scale_factor, parser=parser)
)
def get_framerate_ratios_to_try(args: argparse.Namespace) -> List[Optional[float]]:
if args.no_fix_framerate:
return []
else:
framerate_ratios = list(
np.concatenate(
[np.array(FRAMERATE_RATIOS), 1.0 / np.array(FRAMERATE_RATIOS)]
)
)
if args.gss:
framerate_ratios.append(None)
return framerate_ratios
def try_sync(
args: argparse.Namespace, reference_pipe: Optional[Pipeline], result: Dict[str, Any]
) -> bool:
sync_was_successful = True
exc = None
try:
logger.info(
"extracting speech segments from %s...",
"stdin" if not args.srtin else "subtitles file(s) {}".format(args.srtin),
)
if not args.srtin:
args.srtin = [None]
for srtin in args.srtin:
skip_sync = args.skip_sync or reference_pipe is None
skip_infer_framerate_ratio = (
args.skip_infer_framerate_ratio or reference_pipe is None
)
srtout = srtin if args.overwrite_input else args.srtout
srt_pipe_maker = get_srt_pipe_maker(args, srtin)
framerate_ratios = get_framerate_ratios_to_try(args)
srt_pipes = [srt_pipe_maker(1.0)] + [
srt_pipe_maker(rat) for rat in framerate_ratios
]
for srt_pipe in srt_pipes:
if callable(srt_pipe):
continue
else:
srt_pipe.fit(srtin)
if not skip_infer_framerate_ratio and hasattr(
reference_pipe[-1], "num_frames"
):
inferred_framerate_ratio_from_length = (
float(reference_pipe[-1].num_frames)
/ cast(Pipeline, srt_pipes[0])[-1].num_frames
)
logger.info(
"inferred frameratio ratio: %.3f"
% inferred_framerate_ratio_from_length
)
srt_pipes.append(
cast(
Pipeline, srt_pipe_maker(inferred_framerate_ratio_from_length)
).fit(srtin)
)
logger.info("...done")
logger.info("computing alignments...")
if skip_sync:
best_score = 0.0
best_srt_pipe = cast(Pipeline, srt_pipes[0])
offset_samples = 0
else:
(best_score, offset_samples), best_srt_pipe = MaxScoreAligner(
FFTAligner, srtin, SAMPLE_RATE, args.max_offset_seconds
).fit_transform(
reference_pipe.transform(args.reference),
srt_pipes,
)
logger.info("...done")
offset_seconds = (
offset_samples / float(SAMPLE_RATE) + args.apply_offset_seconds
)
scale_step = best_srt_pipe.named_steps["scale"]
logger.info("score: %.3f", best_score)
logger.info("offset seconds: %.3f", offset_seconds)
logger.info("framerate scale factor: %.3f", scale_step.scale_factor)
output_steps: List[Tuple[str, TransformerMixin]] = [
("shift", SubtitleShifter(offset_seconds))
]
if args.merge_with_reference:
output_steps.append(
("merge", SubtitleMerger(reference_pipe.named_steps["parse"].subs_))
)
output_pipe = Pipeline(output_steps)
out_subs = output_pipe.fit_transform(scale_step.subs_)
if args.output_encoding != "same":
out_subs = out_subs.set_encoding(args.output_encoding)
suppress_output_thresh = args.suppress_output_if_offset_less_than
if offset_seconds >= (suppress_output_thresh or float("-inf")):
logger.info("writing output to {}".format(srtout or "stdout"))
out_subs.write_file(srtout)
else:
logger.warning(
"suppressing output because offset %s was less than suppression threshold %s",
offset_seconds,
args.suppress_output_if_offset_less_than,
)
except FailedToFindAlignmentException as e:
sync_was_successful = False
logger.error(str(e))
except Exception as e:
exc = e
sync_was_successful = False
else:
result["offset_seconds"] = offset_seconds
result["framerate_scale_factor"] = scale_step.scale_factor
finally:
if exc is not None:
raise exc
result["sync_was_successful"] = sync_was_successful
return sync_was_successful
def make_reference_pipe(args: argparse.Namespace) -> Pipeline:
ref_format = _ref_format(args.reference)
if ref_format in SUBTITLE_EXTENSIONS:
if args.vad is not None:
logger.warning("Vad specified, but reference was not a movie")
return cast(
Pipeline,
make_subtitle_speech_pipeline(
fmt=ref_format,
**override(args, encoding=args.reference_encoding or DEFAULT_ENCODING),
),
)
elif ref_format in ("npy", "npz"):
if args.vad is not None:
logger.warning("Vad specified, but reference was not a movie")
return Pipeline(
[("deserialize", DeserializeSpeechTransformer(args.non_speech_label))]
)
else:
vad = args.vad or DEFAULT_VAD
if args.reference_encoding is not None:
logger.warning(
"Reference srt encoding specified, but reference was a video file"
)
ref_stream = args.reference_stream
if ref_stream is not None and not ref_stream.startswith("0:"):
ref_stream = "0:" + ref_stream
return Pipeline(
[
(
"speech_extract",
VideoSpeechTransformer(
vad=vad,
sample_rate=SAMPLE_RATE,
frame_rate=args.frame_rate,
non_speech_label=args.non_speech_label,
start_seconds=args.start_seconds,
ffmpeg_path=args.ffmpeg_path,
ref_stream=ref_stream,
vlc_mode=args.vlc_mode,
gui_mode=args.gui_mode,
),
),
]
)
def extract_subtitles_from_reference(args: argparse.Namespace) -> int:
stream = args.extract_subs_from_stream
if not stream.startswith("0:s:"):
stream = "0:s:{}".format(stream)
elif not stream.startswith("0:") and stream.startswith("s:"):
stream = "0:{}".format(stream)
if not stream.startswith("0:s:"):
logger.error(
"invalid stream for subtitle extraction: %s", args.extract_subs_from_stream
)
ffmpeg_args = [
ffmpeg_bin_path("ffmpeg", args.gui_mode, ffmpeg_resources_path=args.ffmpeg_path)
]
ffmpeg_args.extend(
[
"-y",
"-nostdin",
"-loglevel",
"fatal",
"-i",
args.reference,
"-map",
"{}".format(stream),
"-f",
"srt",
]
)
if args.srtout is None:
ffmpeg_args.append("-")
else:
ffmpeg_args.append(args.srtout)
logger.info(
"attempting to extract subtitles to {} ...".format(
"stdout" if args.srtout is None else args.srtout
)
)
retcode = subprocess.call(ffmpeg_args)
if retcode == 0:
logger.info("...done")
else:
logger.error(
"ffmpeg unable to extract subtitles from reference; return code %d", retcode
)
return retcode
def validate_args(args: argparse.Namespace) -> None:
if args.vlc_mode:
logger.setLevel(logging.CRITICAL)
if args.reference is None:
if args.apply_offset_seconds == 0 or not args.srtin:
raise ValueError(
"`reference` required unless `--apply-offset-seconds` specified"
)
if args.apply_offset_seconds != 0:
if not args.srtin:
args.srtin = [args.reference]
if not args.srtin:
raise ValueError(
"at least one of `srtin` or `reference` must be specified to apply offset seconds"
)
if args.srtin:
if len(args.srtin) > 1 and not args.overwrite_input:
raise ValueError(
"cannot specify multiple input srt files without overwriting"
)
if len(args.srtin) > 1 and args.make_test_case:
raise ValueError("cannot specify multiple input srt files for test cases")
if len(args.srtin) > 1 and args.gui_mode:
raise ValueError("cannot specify multiple input srt files in GUI mode")
if (
args.make_test_case and not args.gui_mode
): # this validation not necessary for gui mode
if not args.srtin or args.srtout is None:
raise ValueError(
"need to specify input and output srt files for test cases"
)
if args.overwrite_input:
if args.extract_subs_from_stream is not None:
raise ValueError(
"input overwriting not allowed for extracting subtitles from reference"
)
if not args.srtin:
raise ValueError(
"need to specify input srt if --overwrite-input "
"is specified since we cannot overwrite stdin"
)
if args.srtout is not None:
raise ValueError(
"overwrite input set but output file specified; "
"refusing to run in case this was not intended"
)
if args.extract_subs_from_stream is not None:
if args.make_test_case:
raise ValueError("test case is for sync and not subtitle extraction")
if args.srtin:
raise ValueError(
"stream specified for reference subtitle extraction; "
"-i flag for sync input not allowed"
)
def validate_file_permissions(args: argparse.Namespace) -> None:
error_string_template = (
"unable to {action} {file}; "
"try ensuring file exists and has correct permissions"
)
if args.reference is not None and not os.access(args.reference, os.R_OK):
raise ValueError(
error_string_template.format(action="read reference", file=args.reference)
)
if args.srtin:
for srtin in args.srtin:
if srtin is not None and not os.access(srtin, os.R_OK):
raise ValueError(
error_string_template.format(
action="read input subtitles", file=srtin
)
)
if (
args.srtout is not None
and os.path.exists(args.srtout)
and not os.access(args.srtout, os.W_OK)
):
raise ValueError(
error_string_template.format(
action="write output subtitles", file=args.srtout
)
)
if args.make_test_case or args.serialize_speech:
npy_savename = os.path.splitext(args.reference)[0] + ".npz"
if os.path.exists(npy_savename) and not os.access(npy_savename, os.W_OK):
raise ValueError(
"unable to write test case file archive %s (try checking permissions)"
% npy_savename
)
def _setup_logging(
args: argparse.Namespace,
) -> Tuple[Optional[str], Optional[logging.FileHandler]]:
log_handler = None
log_path = None
if args.make_test_case or args.log_dir_path is not None:
log_path = "ffsubsync.log"
if args.log_dir_path is not None and os.path.isdir(args.log_dir_path):
log_path = os.path.join(args.log_dir_path, log_path)
log_handler = logging.FileHandler(log_path)
logger.addHandler(log_handler)
logger.info("this log will be written to %s", os.path.abspath(log_path))
return log_path, log_handler
def _npy_savename(args: argparse.Namespace) -> str:
return os.path.splitext(args.reference)[0] + ".npz"
def _run_impl(args: argparse.Namespace, result: Dict[str, Any]) -> bool:
if args.extract_subs_from_stream is not None:
result["retval"] = extract_subtitles_from_reference(args)
return True
if args.srtin is not None and (
args.reference is None
or (len(args.srtin) == 1 and args.srtin[0] == args.reference)
):
return try_sync(args, None, result)
reference_pipe = make_reference_pipe(args)
logger.info("extracting speech segments from reference '%s'...", args.reference)
reference_pipe.fit(args.reference)
logger.info("...done")
if args.make_test_case or args.serialize_speech:
logger.info("serializing speech...")
np.savez_compressed(
_npy_savename(args), speech=reference_pipe.transform(args.reference)
)
logger.info("...done")
if not args.srtin:
logger.info(
"unsynchronized subtitle file not specified; skipping synchronization"
)
return False
return try_sync(args, reference_pipe, result)
def validate_and_transform_args(
parser_or_args: Union[argparse.ArgumentParser, argparse.Namespace]
) -> Optional[argparse.Namespace]:
if isinstance(parser_or_args, argparse.Namespace):
parser = None
args = parser_or_args
else:
parser = parser_or_args
args = parser.parse_args()
try:
validate_args(args)
except ValueError as e:
logger.error(e)
if parser is not None:
parser.print_usage()
return None
if args.gui_mode and args.srtout is None:
args.srtout = "{}.synced.srt".format(os.path.splitext(args.srtin[0])[0])
try:
validate_file_permissions(args)
except ValueError as e:
logger.error(e)
return None
ref_format = _ref_format(args.reference)
if args.merge_with_reference and ref_format not in SUBTITLE_EXTENSIONS:
logger.error(
"merging synced output with reference only valid "
"when reference composed of subtitles"
)
return None
return args
def run(
parser_or_args: Union[argparse.ArgumentParser, argparse.Namespace]
) -> Dict[str, Any]:
sync_was_successful = False
result = {
"retval": 0,
"offset_seconds": None,
"framerate_scale_factor": None,
}
args = validate_and_transform_args(parser_or_args)
if args is None:
result["retval"] = 1
return result
log_path, log_handler = _setup_logging(args)
try:
sync_was_successful = _run_impl(args, result)
result["sync_was_successful"] = sync_was_successful
return result
finally:
if log_handler is not None and log_path is not None:
log_handler.close()
logger.removeHandler(log_handler)
if args.make_test_case:
result["retval"] += make_test_case(
args, _npy_savename(args), sync_was_successful
)
if args.log_dir_path is None or not os.path.isdir(args.log_dir_path):
os.remove(log_path)
def add_main_args_for_cli(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"reference",
nargs="?",
help=(
"Reference (video, subtitles, or a numpy array with VAD speech) "
"to which to synchronize input subtitles."
),
)
parser.add_argument(
"-i", "--srtin", nargs="*", help="Input subtitles file (default=stdin)."
)
parser.add_argument(
"-o", "--srtout", help="Output subtitles file (default=stdout)."
)
parser.add_argument(
"--merge-with-reference",
"--merge",
action="store_true",
help="Merge reference subtitles with synced output subtitles.",
)
parser.add_argument(
"--make-test-case",
"--create-test-case",
action="store_true",
help="If specified, serialize reference speech to a numpy array, "
"and create an archive with input/output subtitles "
"and serialized speech.",
)
parser.add_argument(
"--reference-stream",
"--refstream",
"--reference-track",
"--reftrack",
default=None,
help=(
"Which stream/track in the video file to use as reference, "
"formatted according to ffmpeg conventions. For example, 0:s:0 "
"uses the first subtitle track; 0:a:3 would use the third audio track. "
"You can also drop the leading `0:`; i.e. use s:0 or a:3, respectively. "
"Example: `ffs ref.mkv -i in.srt -o out.srt --reference-stream s:2`"
),
)
def add_cli_only_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-v",
"--version",
action="version",
version="{package} {version}".format(
package=__package__, version=get_version()
),
)
parser.add_argument(
"--overwrite-input",
action="store_true",
help=(
"If specified, will overwrite the input srt "
"instead of writing the output to a new file."
),
)
parser.add_argument(
"--encoding",
default=DEFAULT_ENCODING,
help="What encoding to use for reading input subtitles "
"(default=%s)." % DEFAULT_ENCODING,
)
parser.add_argument(
"--max-subtitle-seconds",
type=float,
default=DEFAULT_MAX_SUBTITLE_SECONDS,
help="Maximum duration for a subtitle to appear on-screen "
"(default=%.3f seconds)." % DEFAULT_MAX_SUBTITLE_SECONDS,
)
parser.add_argument(
"--start-seconds",
type=int,
default=DEFAULT_START_SECONDS,
help="Start time for processing "
"(default=%d seconds)." % DEFAULT_START_SECONDS,
)
parser.add_argument(
"--max-offset-seconds",
type=float,
default=DEFAULT_MAX_OFFSET_SECONDS,
help="The max allowed offset seconds for any subtitle segment "
"(default=%d seconds)." % DEFAULT_MAX_OFFSET_SECONDS,
)
parser.add_argument(
"--apply-offset-seconds",
type=float,
default=DEFAULT_APPLY_OFFSET_SECONDS,
help="Apply a predefined offset in seconds to all subtitle segments "
"(default=%d seconds)." % DEFAULT_APPLY_OFFSET_SECONDS,
)
parser.add_argument(
"--frame-rate",
type=int,
default=DEFAULT_FRAME_RATE,
help="Frame rate for audio extraction (default=%d)." % DEFAULT_FRAME_RATE,
)
parser.add_argument(
"--skip-infer-framerate-ratio",
action="store_true",
help="If set, do not try to infer framerate ratio based on duration ratio.",
)
parser.add_argument(
"--non-speech-label",
type=float,
default=DEFAULT_NON_SPEECH_LABEL,
help="Label to use for frames detected as non-speech (default=%f)"
% DEFAULT_NON_SPEECH_LABEL,
)
parser.add_argument(
"--output-encoding",
default="utf-8",
help="What encoding to use for writing output subtitles "
'(default=utf-8). Can indicate "same" to use same '
"encoding as that of the input.",
)
parser.add_argument(
"--reference-encoding",
help="What encoding to use for reading / writing reference subtitles "
"(if applicable, default=infer).",
)
parser.add_argument(
"--vad",
choices=[
"subs_then_webrtc",
"webrtc",
"subs_then_auditok",
"auditok",
"subs_then_silero",
"silero",
],
default=None,
help="Which voice activity detector to use for speech extraction "
"(if using video / audio as a reference, default={}).".format(DEFAULT_VAD),
)
parser.add_argument(
"--no-fix-framerate",
action="store_true",
help="If specified, subsync will not attempt to correct a framerate "
"mismatch between reference and subtitles.",
)
parser.add_argument(
"--serialize-speech",
action="store_true",
help="If specified, serialize reference speech to a numpy array.",
)
parser.add_argument(
"--extract-subs-from-stream",
"--extract-subtitles-from-stream",
default=None,
help="If specified, do not attempt sync; instead, just extract subtitles"
" from the specified stream using the reference.",
)
parser.add_argument(
"--suppress-output-if-offset-less-than",
type=float,
default=None,
help="If specified, do not produce output if offset below provided threshold.",
)
parser.add_argument(
"--ffmpeg-path",
"--ffmpegpath",
default=None,
help="Where to look for ffmpeg and ffprobe. Uses the system PATH by default.",
)
parser.add_argument(
"--log-dir-path",
default=None,
help=(
"If provided, will save log file ffsubsync.log to this path "
"(must be an existing directory)."
),
)
parser.add_argument(
"--gss",
action="store_true",
help="If specified, use golden-section search to try to find"
"the optimal framerate ratio between video and subtitles.",
)
parser.add_argument(
"--strict",
action="store_true",
help="If specified, refuse to parse srt files with formatting issues.",
)
parser.add_argument("--vlc-mode", action="store_true", help=argparse.SUPPRESS)
parser.add_argument("--gui-mode", action="store_true", help=argparse.SUPPRESS)
parser.add_argument("--skip-sync", action="store_true", help=argparse.SUPPRESS)
def make_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Synchronize subtitles with video.")
add_main_args_for_cli(parser)
add_cli_only_args(parser)
return parser
def main() -> int:
parser = make_parser()
return run(parser)["retval"]
if __name__ == "__main__":
sys.exit(main())
|