Skip to content

Commit

Permalink
blacken
Browse files Browse the repository at this point in the history
  • Loading branch information
smacke committed Jan 8, 2022
1 parent 6e4b90a commit eabe5a2
Show file tree
Hide file tree
Showing 26 changed files with 1,019 additions and 667 deletions.
9 changes: 2 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
name: ffsubsync

on: [push]
on: [push, pull_request]

jobs:
build:

runs-on: ${{ matrix.os }}

strategy:
matrix:
exclude:
- os: windows-latest
python-version: 2.7.x
- os: windows-latest
python-version: 3.5.x
os: [ 'macos-latest', 'ubuntu-latest', 'windows-latest' ]
python-version: [ '3.6.x', '3.7.x', '3.8.x', '3.9.x' ]

Expand Down
14 changes: 12 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
.PHONY: clean build bump deploy check test tests deps devdeps
.PHONY: clean build bump deploy black blackcheck check test tests deps devdeps

clean:
rm -rf dist/ build/ *.egg-info/
Expand All @@ -13,13 +13,22 @@ bump:
deploy: build
./scripts/deploy.sh

black:
./scripts/blacken.sh

blackcheck:
./scripts/blacken.sh --check

lint:
flake8

typecheck:
mypy ffsubsync

check_no_typing:
INTEGRATION=1 pytest --cov-config=.coveragerc --cov=ffsubsync

check: typecheck check_no_typing
check: blackcheck typecheck check_no_typing

test: check
tests: check
Expand All @@ -30,3 +39,4 @@ deps:
devdeps:
pip install -e .
pip install -r requirements-dev.txt

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ FFsubsync

[![CI Status](https://github.com/smacke/ffsubsync/workflows/ffsubsync/badge.svg)](https://github.com/smacke/ffsubsync/actions)
[![Checked with mypy](http://www.mypy-lang.org/static/mypy_badge.svg)](http://mypy-lang.org/)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![License: MIT](https://img.shields.io/badge/License-MIT-maroon.svg)](https://opensource.org/licenses/MIT)
[![Python Versions](https://img.shields.io/pypi/pyversions/ffsubsync.svg)](https://pypi.org/project/ffsubsync)
[![Documentation Status](https://readthedocs.org/projects/ffsubsync/badge/?version=latest)](https://ffsubsync.readthedocs.io/en/latest/?badge=latest)
Expand Down
4 changes: 2 additions & 2 deletions ffsubsync/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# -*- coding: future_annotations -*-
# -*- coding: utf-8 -*-
import logging
import sys

Expand All @@ -12,7 +12,7 @@
level=logging.INFO,
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(console=Console(file=sys.stderr))]
handlers=[RichHandler(console=Console(file=sys.stderr))],
)
except ImportError:
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
Expand Down
82 changes: 52 additions & 30 deletions ffsubsync/aligners.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
# -*- coding: future_annotations -*-
# -*- coding: utf-8 -*-
import logging
import math
from typing import TYPE_CHECKING
from typing import List, Optional, Tuple, Type, Union

import numpy as np

from ffsubsync.golden_section_search import gss
from ffsubsync.sklearn_shim import Pipeline, TransformerMixin

if TYPE_CHECKING:
from typing import List, Optional, Tuple, Type, Union


logging.basicConfig(level=logging.INFO)
logger: logging.Logger = logging.getLogger(__name__)
Expand All @@ -31,35 +28,42 @@ def __init__(self, max_offset_samples: Optional[int] = None) -> None:
self.best_score_: Optional[float] = None
self.get_score_: bool = False

def _eliminate_extreme_offsets_from_solutions(self, convolve: np.ndarray, substring: np.ndarray) -> np.ndarray:
def _eliminate_extreme_offsets_from_solutions(
self, convolve: np.ndarray, substring: np.ndarray
) -> np.ndarray:
convolve = np.copy(convolve)
if self.max_offset_samples is None:
return convolve
offset_to_index = lambda offset: len(convolve) - 1 + offset - len(substring)
convolve[:offset_to_index(-self.max_offset_samples)] = float('-inf')
convolve[offset_to_index(self.max_offset_samples):] = float('-inf')
convolve[: offset_to_index(-self.max_offset_samples)] = float("-inf")
convolve[offset_to_index(self.max_offset_samples) :] = float("-inf")
return convolve

def _compute_argmax(self, convolve: np.ndarray, substring: np.ndarray) -> None:
best_idx = np.argmax(convolve)
self.best_offset_ = len(convolve) - 1 - best_idx - len(substring)
self.best_score_ = convolve[best_idx]

def fit(self, refstring, substring, get_score: bool = False) -> FFTAligner:
def fit(self, refstring, substring, get_score: bool = False) -> "FFTAligner":
refstring, substring = [
list(map(int, s))
if isinstance(s, str) else s
list(map(int, s)) if isinstance(s, str) else s
for s in [refstring, substring]
]
refstring, substring = map(
lambda s: 2 * np.array(s).astype(float) - 1, [refstring, substring])
lambda s: 2 * np.array(s).astype(float) - 1, [refstring, substring]
)
total_bits = math.log(len(substring) + len(refstring), 2)
total_length = int(2 ** math.ceil(total_bits))
extra_zeros = total_length - len(substring) - len(refstring)
subft = np.fft.fft(np.append(np.zeros(extra_zeros + len(refstring)), substring))
refft = np.fft.fft(np.flip(np.append(refstring, np.zeros(len(substring) + extra_zeros)), 0))
refft = np.fft.fft(
np.flip(np.append(refstring, np.zeros(len(substring) + extra_zeros)), 0)
)
convolve = np.real(np.fft.ifft(subft * refft))
self._compute_argmax(self._eliminate_extreme_offsets_from_solutions(convolve, substring), substring)
self._compute_argmax(
self._eliminate_extreme_offsets_from_solutions(convolve, substring),
substring,
)
self.get_score_ = get_score
return self

Expand All @@ -76,15 +80,17 @@ def __init__(
base_aligner: Union[FFTAligner, Type[FFTAligner]],
srtin: Optional[str] = None,
sample_rate=None,
max_offset_seconds=None
max_offset_seconds=None,
) -> None:
self.srtin: Optional[str] = srtin
if sample_rate is None or max_offset_seconds is None:
self.max_offset_samples: Optional[int] = None
else:
self.max_offset_samples = abs(int(max_offset_seconds * sample_rate))
if isinstance(base_aligner, type):
self.base_aligner: FFTAligner = base_aligner(max_offset_samples=self.max_offset_samples)
self.base_aligner: FFTAligner = base_aligner(
max_offset_samples=self.max_offset_samples
)
else:
self.base_aligner = base_aligner
self.max_offset_seconds: Optional[int] = max_offset_seconds
Expand All @@ -94,40 +100,56 @@ def fit_gss(self, refstring, subpipe_maker):
def opt_func(framerate_ratio, is_last_iter):
subpipe = subpipe_maker(framerate_ratio)
substring = subpipe.fit_transform(self.srtin)
score = self.base_aligner.fit_transform(refstring, substring, get_score=True)
logger.info('got score %.0f (offset %d) for ratio %.3f', score[0], score[1], framerate_ratio)
score = self.base_aligner.fit_transform(
refstring, substring, get_score=True
)
logger.info(
"got score %.0f (offset %d) for ratio %.3f",
score[0],
score[1],
framerate_ratio,
)
if is_last_iter:
self._scores.append((score, subpipe))
return -score[0]

gss(opt_func, MIN_FRAMERATE_RATIO, MAX_FRAMERATE_RATIO)
return self

def fit(self, refstring, subpipes: Union[Pipeline, List[Pipeline]]) -> MaxScoreAligner:
def fit(
self, refstring, subpipes: Union[Pipeline, List[Pipeline]]
) -> "MaxScoreAligner":
if not isinstance(subpipes, list):
subpipes = [subpipes]
for subpipe in subpipes:
if callable(subpipe):
self.fit_gss(refstring, subpipe)
continue
elif hasattr(subpipe, 'transform'):
elif hasattr(subpipe, "transform"):
substring = subpipe.transform(self.srtin)
else:
substring = subpipe
self._scores.append((
self.base_aligner.fit_transform(
refstring, substring, get_score=True
),
subpipe
))
self._scores.append(
(
self.base_aligner.fit_transform(
refstring, substring, get_score=True
),
subpipe,
)
)
return self

def transform(self, *_) -> Tuple[Tuple[float, float], Pipeline]:
scores = self._scores
if self.max_offset_samples is not None:
scores = list(filter(lambda s: abs(s[0][1]) <= self.max_offset_samples, scores))
scores = list(
filter(lambda s: abs(s[0][1]) <= self.max_offset_samples, scores)
)
if len(scores) == 0:
raise FailedToFindAlignmentException('Synchronization failed; consider passing '
'--max-offset-seconds with a number larger than '
'{}'.format(self.max_offset_seconds))
raise FailedToFindAlignmentException(
"Synchronization failed; consider passing "
"--max-offset-seconds with a number larger than "
"{}".format(self.max_offset_seconds)
)
(score, offset), subpipe = max(scores, key=lambda x: x[0][0])
return (score, offset), subpipe
45 changes: 24 additions & 21 deletions ffsubsync/constants.py
Original file line number Diff line number Diff line change
@@ -1,38 +1,41 @@
# -*- coding: future_annotations -*-
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import List, Tuple
# -*- coding: utf-8 -*-
from typing import List, Tuple


SUBSYNC_RESOURCES_ENV_MAGIC: str = "ffsubsync_resources_xj48gjdkl340"

SAMPLE_RATE: int = 100

FRAMERATE_RATIOS: List[float] = [24./23.976, 25./23.976, 25./24.]
FRAMERATE_RATIOS: List[float] = [24.0 / 23.976, 25.0 / 23.976, 25.0 / 24.0]

DEFAULT_FRAME_RATE: int = 48000
DEFAULT_NON_SPEECH_LABEL: float = 0.
DEFAULT_ENCODING: str = 'infer'
DEFAULT_NON_SPEECH_LABEL: float = 0.0
DEFAULT_ENCODING: str = "infer"
DEFAULT_MAX_SUBTITLE_SECONDS: int = 10
DEFAULT_START_SECONDS: int = 0
DEFAULT_SCALE_FACTOR: float = 1
DEFAULT_VAD: str = 'subs_then_webrtc'
DEFAULT_VAD: str = "subs_then_webrtc"
DEFAULT_MAX_OFFSET_SECONDS: int = 60
DEFAULT_APPLY_OFFSET_SECONDS: int = 0

SUBTITLE_EXTENSIONS: Tuple[str, ...] = ('srt', 'ass', 'ssa', 'sub')
SUBTITLE_EXTENSIONS: Tuple[str, ...] = ("srt", "ass", "ssa", "sub")

GITHUB_DEV_USER: str = 'smacke'
PROJECT_NAME: str = 'FFsubsync'
PROJECT_LICENSE: str = 'MIT'
COPYRIGHT_YEAR: str = '2019'
GITHUB_REPO: str = 'ffsubsync'
DESCRIPTION: str = 'Synchronize subtitles with video.'
LONG_DESCRIPTION: str = 'Automatic and language-agnostic synchronization of subtitles with video.'
WEBSITE: str = 'https://github.com/{}/{}/'.format(GITHUB_DEV_USER, GITHUB_REPO)
DEV_WEBSITE: str = 'https://smacke.net/'
GITHUB_DEV_USER: str = "smacke"
PROJECT_NAME: str = "FFsubsync"
PROJECT_LICENSE: str = "MIT"
COPYRIGHT_YEAR: str = "2019"
GITHUB_REPO: str = "ffsubsync"
DESCRIPTION: str = "Synchronize subtitles with video."
LONG_DESCRIPTION: str = (
"Automatic and language-agnostic synchronization of subtitles with video."
)
WEBSITE: str = "https://github.com/{}/{}/".format(GITHUB_DEV_USER, GITHUB_REPO)
DEV_WEBSITE: str = "https://smacke.net/"

# No trailing slash important for this one...
API_RELEASE_URL: str = 'https://api.github.com/repos/{}/{}/releases/latest'.format(GITHUB_DEV_USER, GITHUB_REPO)
RELEASE_URL: str = 'https://github.com/{}/{}/releases/latest/'.format(GITHUB_DEV_USER, GITHUB_REPO)
API_RELEASE_URL: str = "https://api.github.com/repos/{}/{}/releases/latest".format(
GITHUB_DEV_USER, GITHUB_REPO
)
RELEASE_URL: str = "https://github.com/{}/{}/releases/latest/".format(
GITHUB_DEV_USER, GITHUB_REPO
)
28 changes: 17 additions & 11 deletions ffsubsync/ffmpeg_utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# -*- coding: future_annotations -*-
# -*- coding: utf-8 -*-
import logging
import os
import platform
Expand All @@ -23,7 +23,7 @@
# **subprocess_args(False))
def subprocess_args(include_stdout=True):
# The following is true only on Windows.
if hasattr(subprocess, 'STARTUPINFO'):
if hasattr(subprocess, "STARTUPINFO"):
# On Windows, subprocess calls will pop up a command window by default
# when run from Pyinstaller with the ``--noconsole`` option. Avoid this
# distraction.
Expand All @@ -47,31 +47,37 @@ def subprocess_args(include_stdout=True):
#
# So, add it only if it's needed.
if include_stdout:
ret = {'stdout': subprocess.PIPE}
ret = {"stdout": subprocess.PIPE}
else:
ret = {}

# On Windows, running this from the binary produced by Pyinstaller
# with the ``--noconsole`` option requires redirecting everything
# (stdin, stdout, stderr) to avoid an OSError exception
# "[Error 6] the handle is invalid."
ret.update({'stdin': subprocess.PIPE,
'stderr': subprocess.PIPE,
'startupinfo': si,
'env': env})
ret.update(
{
"stdin": subprocess.PIPE,
"stderr": subprocess.PIPE,
"startupinfo": si,
"env": env,
}
)
return ret


def ffmpeg_bin_path(bin_name, gui_mode, ffmpeg_resources_path=None):
if platform.system() == 'Windows':
bin_name = '{}.exe'.format(bin_name)
if platform.system() == "Windows":
bin_name = "{}.exe".format(bin_name)
if ffmpeg_resources_path is not None:
return os.path.join(ffmpeg_resources_path, bin_name)
try:
resource_path = os.environ[SUBSYNC_RESOURCES_ENV_MAGIC]
if len(resource_path) > 0:
return os.path.join(resource_path, 'ffmpeg-bin', bin_name)
return os.path.join(resource_path, "ffmpeg-bin", bin_name)
except KeyError:
if gui_mode:
logger.info("Couldn't find resource path; falling back to searching system path")
logger.info(
"Couldn't find resource path; falling back to searching system path"
)
return bin_name
Loading

0 comments on commit eabe5a2

Please sign in to comment.