Skip to content

Commit

Permalink
Further fixes, add preset/custom list support
Browse files Browse the repository at this point in the history
  • Loading branch information
LightArrowsEXE committed Aug 27, 2024
1 parent 0a3c826 commit 2569dd0
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 28 deletions.
32 changes: 27 additions & 5 deletions vsdeinterlace/wobbly/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from typing import Sequence

from vstools import (CustomValueError, DependencyNotFoundError, FieldBased,
FuncExceptT, VSFunction, core, replace_ranges, vs)
FieldBasedT, FuncExceptT, VSFunction, core,
replace_ranges, vs)

from vsdeinterlace.combing import fix_interlaced_fades
from vsdeinterlace.wobbly.info import CustomList

from .info import FreezeFrame, InterlacedFade, OrphanField
from .types import Match
from .types import CustomPostFiltering, Match


class _WobblyProcessBase:
Expand All @@ -24,16 +26,18 @@ def _check_plugin_installed(self, plugin: str, func_except: FuncExceptT | None =

def _apply_fieldmatches(
self, clip: vs.VideoNode, matches: Sequence[Match],
tff: FieldBasedT | None = None,
func_except: FuncExceptT | None = None
) -> vs.VideoNode:
"""Apply fieldmatches to a clip."""

self._check_plugin_installed('fh', func_except)

match_clips = dict[str, vs.VideoNode]()
tff = FieldBased.from_param_or_video(tff, clip)

for match in set(matches):
match_clips |= {match: clip.std.SetFrameProps(wobbly_match=match)}
clip = clip.fh.FieldHint(None, tff, ''.join(matches))

match_clips = {match: clip.std.SetFrameProps(wobbly_match=match) for match in set(matches)}

return clip.std.FrameEval(lambda n: match_clips.get(matches[n]))

Expand Down Expand Up @@ -119,3 +123,21 @@ def _apply_interlaced_fades(
fix_interlaced_fades(clip, colors=0, planes=0, func=func).std.SetFrameProps(wobbly_fif=True),
[f.framenum for f in ifades]
)

def _apply_custom_list(
self, clip: vs.VideoNode, custom_list: list[CustomList], pos: CustomPostFiltering
) -> vs.VideoNode:
"""Apply a list of custom functions to a clip based on the pos."""

for custom in custom_list:
if custom.position == pos:
custom_clip = custom.preset.apply_preset(clip)

custom_clip = custom_clip.std.SetFrameProps(
wobbly_custom_list_name=custom.name,
wobbly_custom_list_position=str(pos)
)

clip = replace_ranges(clip, custom_clip, custom.frames)

return clip
146 changes: 136 additions & 10 deletions vsdeinterlace/wobbly/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
from typing import Any, Callable, NamedTuple

from vstools import (CustomValueError, FieldBased, FieldBasedT,
FileNotExistsError, FrameRangeN, FuncExceptT,
SceneChangeMode, SPath, SPathLike, VSCoreProxy, core, vs)
FileNotExistsError, FrameRangeN, FrameRangesN,
FuncExceptT, SceneChangeMode, SPath, SPathLike,
VSCoreProxy, core, vs)

from .exceptions import InvalidMatchError
from .types import Match, SectionPreset
from .types import CustomPostFiltering, Match, SectionPreset

__all__: list[str] = [
"WobblyMeta", "WobblyVideo",
Expand Down Expand Up @@ -79,13 +80,37 @@ def source(self, func_except: FuncExceptT | None = None, **kwargs: Any) -> vs.Vi

src = self.source_filter(sfile.to_str(), **kwargs) # type:ignore[operator]

if src.fps != self.framerate:
src = src.std.AssumeFPS(src, self.framerate.numerator, self.framerate.denominator)
src = self.set_framerate(src)

return self.trim(src)

def set_framerate(self, clip: vs.VideoNode) -> vs.VideoNode:
"""
Set the framerate of a clip to the base framerate.
:param clip: The clip to set the framerate of.
:return: The clip with the framerate set.
"""

if clip.fps != self.framerate:
return clip.std.AssumeFPS(clip, self.framerate.numerator, self.framerate.denominator)

return clip

def trim(self, clip: vs.VideoNode) -> vs.VideoNode:
"""
Apply trims to a clip.
:param clip: The clip to apply the trims to.
:return: The trimmed clip.
"""

if not self.trims:
return src
return clip

return core.std.Splice([src.std.Trim(s, e) for s, e in self.trims])
return core.std.Splice([clip.std.Trim(s, e) for s, e in self.trims])


# TODO: refactor this
Expand Down Expand Up @@ -268,7 +293,7 @@ def as_frame_range(self) -> FrameRangeN:
return (self.start_frame, self.end_frame)


@dataclass
@dataclass(unsafe_hash=True)
class FreezeFrame(_HoldsStartEndFrames):
"""Frame ranges to freeze."""

Expand All @@ -291,7 +316,7 @@ def __post_init__(self) -> None:
raise CustomValueError("Frame number must be greater than or equal to 0!")


@dataclass
@dataclass(unsafe_hash=True)
class InterlacedFade(_HoldsFrameNum):
"""Information about interlaced fades."""

Expand All @@ -307,7 +332,7 @@ def __post_init__(self) -> None:
super().__post_init__()


@dataclass
@dataclass(unsafe_hash=True)
class OrphanField(_HoldsFrameNum):
"""Information about the orphan fields."""

Expand All @@ -329,3 +354,104 @@ def deinterlace_order(self) -> FieldBased:
"""The fieldorder to deinterlace in to properly deinterlace the orphan field."""

return FieldBased.TFF if self.match in ('n', 'p') else FieldBased.BFF


@dataclass(unsafe_hash=True)
class Preset:
"""A filtering preset."""

name: str
"""The section the preset applies to."""

contents: str
"""The preset to apply to the section."""

def __str__(self) -> str:
return f"Preset({self.name=}, {self.contents=})"

def __post_init__(self) -> None:
clip = core.std.BlankClip()

local_namespace = {
"clip": clip,
"core": core
}

try:
exec(self.contents, {}, local_namespace)
except Exception as e:
raise CustomValueError(
f"Invalid preset contents ({self.contents=})! Original error: {e}", Preset
) from e

def apply_preset(self, clip: vs.VideoNode) -> vs.VideoNode:
"""
Apply the preset to a clip.
:param clip: The clip to apply the preset to.
:return: The clip with the preset applied.
"""

local_namespace = {"clip": clip}

try:
exec(self.contents, {}, local_namespace)

clip = local_namespace.get("clip", clip)
except Exception as e:
raise CustomValueError(f"Could not apply preset ({self.contents=})!", self.apply_preset) from e

return clip


@dataclass
class CustomList:
"""Custom filtering applied to a given frame range."""

name: str
"""The name of the custom list."""

preset: Preset
"""The preset used for the custom list."""

position: CustomPostFiltering
"""The position to apply the custom filter."""

frames: FrameRangesN = field(default_factory=lambda: [])
"""The frame ranges to apply the custom filter to."""

def __init__(
self, name: str,
preset: Preset | str,
position: CustomPostFiltering | str,
frames: list[list[int]] | None = None,
presets: set[Preset] = set(),
) -> None:
self.name = name

if not preset:
raise CustomValueError("A preset must be set!", CustomList)

if not position:
raise CustomValueError("A position must be set!", CustomList)

self.position = CustomPostFiltering.from_str(position) if isinstance(position, str) else position
self.preset = self._get_preset_from_presets(preset, presets) if isinstance(preset, str) else preset

self.frames = []

for frame_range in frames or []:
self.frames.append(tuple(frame_range)) # type:ignore

def __str__(self) -> str:
return f"CustomList({self.name=}, {self.preset=}, {self.position=}, {self.frames=})"

def _get_preset_from_presets(self, name: str, presets: set[Preset]) -> Preset:
"""Get the preset from the list of presets."""

for preset in presets:
if preset.name == name:
return preset

raise CustomValueError(f"Could not find the preset in the list of presets ({name=})!", CustomList)
37 changes: 35 additions & 2 deletions vsdeinterlace/wobbly/types.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from __future__ import annotations

from enum import Enum
from typing import Callable, Literal, TypeAlias

from vstools import vs
from vstools import NotFoundEnumValue, vs

__all__ = [
"Match", "OrphanMatch",
"SectionPreset"
"SectionPreset",
"CustomPostFiltering",
]


Expand All @@ -16,3 +20,32 @@

SectionPreset = Callable[[vs.VideoNode], vs.VideoNode]
"""A callable preset applied to a section."""


class CustomPostFiltering(Enum):
"""When to perform custom filtering."""

SOURCE = -1
"""Apply the custom filter after the source clip is loaded."""

FIELD_MATCH = 0
"""Apply the custom filter after the field match is applied."""

DECIMATE = 1
"""Apply the custom filter after the decimation is applied."""

@classmethod
def from_str(cls, value: str) -> CustomPostFiltering:
"""Convert a string to a CustomPosition."""

norm_val = value.upper().replace('POST', '').strip().replace(' ', '_')

try:
return cls[norm_val]
except KeyError:
raise NotFoundEnumValue(
"Could not find a matching CustomPostFiltering value!", cls.from_str, value
)

def __str__(self) -> str:
return self.name.replace('_', ' ').title()
2 changes: 1 addition & 1 deletion vsdeinterlace/wobbly/wibbly.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def _get_clip(self, display: bool = False) -> vs.VideoNode:
odd_avg = separated[1::2].std.PlaneStats()

if hasattr(core, 'akarin'):
wclip = core.akarin.PropExpr( # type:ignore
wclip = core.akarin.PropExpr(
[wclip, even_avg, odd_avg],
lambda: {'WibblyFieldDiff': 'y.PlaneStatsAverage z.PlaneStatsAverage - abs'}
)
Expand Down
Loading

0 comments on commit 2569dd0

Please sign in to comment.