boneyard 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: boneyard
3
+ Version: 0.1.0
4
+ Summary:
5
+ Author: sgt0
6
+ Author-email: sgt0 <140186177+sgt0@users.noreply.github.com>
7
+ License-Expression: MIT
8
+ Classifier: Natural Language :: English
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Typing :: Typed
16
+ Requires-Dist: ass~=1.0.3
17
+ Requires-Dist: jetpytools~=3.0.0
18
+ Requires-Dist: muxtools~=0.4.2
19
+ Requires-Dist: vapoursynth>=77
20
+ Requires-Dist: vapoursynth-scxvid~=3.0
21
+ Requires-Dist: vsjetpack~=2.0.0
22
+ Requires-Dist: vapoursynth-fmtconv>=31
23
+ Requires-Python: >=3.13
24
+ Description-Content-Type: text/markdown
25
+
26
+ # boneyard
@@ -0,0 +1 @@
1
+ # boneyard
@@ -0,0 +1,55 @@
1
+ [project]
2
+ name = "boneyard"
3
+ version = "0.1.0"
4
+ description = ""
5
+ license = "MIT"
6
+ readme = "README.md"
7
+ authors = [
8
+ { name = "sgt0", email = "140186177+sgt0@users.noreply.github.com" }
9
+ ]
10
+ classifiers = [
11
+ "Natural Language :: English",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3 :: Only",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Programming Language :: Python :: 3.14",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Typing :: Typed",
19
+ ]
20
+ requires-python = ">=3.13"
21
+ dependencies = [
22
+ "ass~=1.0.3",
23
+ "jetpytools~=3.0.0",
24
+ "muxtools~=0.4.2",
25
+ "vapoursynth>=77",
26
+ "vapoursynth-scxvid~=3.0",
27
+ "vsjetpack~=2.0.0",
28
+
29
+ # Explicitly specify these so they're properly resolved through `uv --index`.
30
+ "vapoursynth-fmtconv>=31",
31
+ ]
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "mypy==2.1.0",
36
+ "ruff==0.15.20",
37
+ ]
38
+
39
+ [build-system]
40
+ requires = ["uv_build==0.11.7"]
41
+ build-backend = "uv_build"
42
+
43
+ [tool.uv.sources]
44
+ vapoursynth-fmtconv = { index = "vs-wheels" }
45
+
46
+ [[tool.uv.index]]
47
+ name = "vs-wheels"
48
+ url = "https://jaded-encoding-thaumaturgy.github.io/vs-wheels/simple"
49
+ explicit = true
50
+
51
+ [tool.basedpyright]
52
+ stubPath = "stubs"
53
+
54
+ [tool.mypy]
55
+ mypy_path = "$MYPY_CONFIG_FILE_DIR/stubs"
@@ -0,0 +1,315 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable, Iterator
4
+ from statistics import fmean
5
+ from typing import Any, Literal, Self, override
6
+
7
+ from ass import Style
8
+ from ass.data import Color
9
+ from jetpytools import (
10
+ CustomIntEnum,
11
+ CustomRuntimeError,
12
+ Sentinel,
13
+ SentinelT,
14
+ SingleOrArr,
15
+ )
16
+ from muxtools import default_style_args, edit_style
17
+ from vsdenoise import MVToolsPreset, Prefilter, bm3d, mc_degrain, nl_means
18
+ from vsdenoise.blockmatch import BM3D
19
+ from vsdenoise.nlm import NLMeans
20
+ from vstools import (
21
+ ChromaLocation,
22
+ clip_async_render,
23
+ clip_data_gather,
24
+ core,
25
+ get_prop,
26
+ get_w,
27
+ merge_clip_props,
28
+ replace_ranges,
29
+ vs,
30
+ )
31
+ from vstools import Keyframes as JetpackKeyframes
32
+ from vstools.functions.timecodes import SceneBasedDynamicCache
33
+
34
+ __all__ = (
35
+ "LOFT_MOON_FIRA_DEFAULT",
36
+ "LOFT_MOON_FIRA_ALT",
37
+ "LOFT_MOON_SIGNS",
38
+ "LOFT_MOON_FIRA_PRESET",
39
+ "denoise",
40
+ "SceneBasedCambi",
41
+ "SceneChangeMode",
42
+ "Keyframes",
43
+ )
44
+
45
+
46
+ LOFT_MOON_FIRA_DEFAULT = Style(
47
+ name="Default",
48
+ fontname="Fira Sans Medium",
49
+ fontsize=69.0,
50
+ outline=4.4,
51
+ shadow=2.2,
52
+ margin_l=275,
53
+ margin_r=275,
54
+ margin_v=60,
55
+ **(
56
+ default_style_args
57
+ | {
58
+ "bold": False,
59
+ "secondary_color": Color(r=0x00, g=0x00, b=0x00, a=0x00),
60
+ "outline_color": Color(r=0x1F, g=0x29, b=0x12, a=0x00),
61
+ "back_color": Color(r=0x1F, g=0x29, b=0x12, a=0xA0),
62
+ }
63
+ ),
64
+ )
65
+
66
+ LOFT_MOON_FIRA_ALT = edit_style(
67
+ LOFT_MOON_FIRA_DEFAULT,
68
+ "Alt",
69
+ outline_color=Color(r=0x4C, g=0x21, b=0x48, a=0x00),
70
+ back_color=Color(r=0x4C, g=0x21, b=0x48, a=0xA0),
71
+ )
72
+
73
+ LOFT_MOON_SIGNS = Style(
74
+ name="Signs",
75
+ fontname="Fira Sans Medium",
76
+ fontsize=80.0,
77
+ outline=0.0,
78
+ shadow=0.0,
79
+ margin_l=0,
80
+ margin_r=0,
81
+ margin_v=0,
82
+ **(
83
+ default_style_args
84
+ | {
85
+ "bold": False,
86
+ "alignment": 5,
87
+ "secondary_color": Color(r=0xFF, g=0xFF, b=0xFF, a=0x00),
88
+ "outline_color": Color(r=0x00, g=0x00, b=0x00, a=0x00),
89
+ "back_color": Color(r=0x00, g=0x00, b=0x00, a=0x00),
90
+ }
91
+ ),
92
+ )
93
+
94
+ LOFT_MOON_FIRA_PRESET = [
95
+ LOFT_MOON_FIRA_DEFAULT,
96
+ LOFT_MOON_FIRA_ALT,
97
+ LOFT_MOON_SIGNS,
98
+ ]
99
+
100
+
101
+ def denoise(
102
+ clip: vs.VideoNode,
103
+ block_size: int = 64,
104
+ limit: int | tuple[int | None, int | None] | None = None,
105
+ refine: int = 3,
106
+ sigma: SingleOrArr[float] = 0.7,
107
+ sr: int = 2,
108
+ strength: float = 0.2,
109
+ thSAD: int | tuple[int, int] = 115, # noqa: N803
110
+ tr: int = 2,
111
+ ) -> vs.VideoNode:
112
+ """MVTools + BM3D + NLMeans denoise."""
113
+
114
+ ref = mc_degrain(
115
+ clip,
116
+ prefilter=Prefilter.DFTTEST,
117
+ preset=MVToolsPreset.HQ_SAD,
118
+ blksize=block_size,
119
+ thsad=thSAD,
120
+ limit=limit,
121
+ refine=refine,
122
+ )
123
+
124
+ denoised_luma = bm3d(
125
+ clip, ref=ref, sigma=sigma, tr=tr, profile=BM3D.Profile.NORMAL, planes=0
126
+ )
127
+ denoised_luma = ChromaLocation.ensure_presence(
128
+ denoised_luma, ChromaLocation.from_video(clip, strict=True)
129
+ )
130
+
131
+ return nl_means(
132
+ denoised_luma,
133
+ ref=ref,
134
+ h=strength,
135
+ tr=tr,
136
+ a=sr,
137
+ wmode=NLMeans.WeightMode.BISQUARE_HR, # wmode=3
138
+ planes=[1, 2],
139
+ )
140
+
141
+
142
+ class SceneBasedCambi(SceneBasedDynamicCache):
143
+ def __init__(
144
+ self,
145
+ clip: vs.VideoNode,
146
+ keyframes: Keyframes | str,
147
+ cache_size: int = 5,
148
+ ) -> None:
149
+ super().__init__(core.cambi.Cambi(clip), keyframes, cache_size)
150
+
151
+ @override
152
+ def get_clip(self, key: int) -> vs.VideoNode:
153
+ frame_range = self.keyframes.scenes[key]
154
+ cut = self.clip[frame_range.start : frame_range.stop]
155
+ frames_cambis = clip_data_gather(
156
+ cut, None, lambda _, f: get_prop(f, "CAMBI", float)
157
+ )
158
+ avg_cambi = fmean(frames_cambis)
159
+ return self.clip.std.SetFrameProps(Scene_Avg_CAMBI=avg_cambi)
160
+
161
+
162
+ class SceneChangeMode(CustomIntEnum):
163
+ """Enum for various scene change modes."""
164
+
165
+ WWXD = 1
166
+ """Get the scene changes using the vapoursynth-wwxd plugin."""
167
+
168
+ SCXVID = 2
169
+ """Get the scene changes using the vapoursynth-scxvid plugin."""
170
+
171
+ WWXD_SCXVID_UNION = 3 # WWXD | SCXVID
172
+ """Get every scene change detected by both wwxd or scxvid."""
173
+
174
+ WWXD_SCXVID_INTERSECTION = 0 # WWXD & SCXVID
175
+ """
176
+ Only get the scene changes if both wwxd and scxvid mark a frame as being a
177
+ scene change.
178
+ """
179
+
180
+ @property
181
+ def is_WWXD(self) -> bool: # noqa: N802
182
+ """Check whether a mode that uses wwxd is used."""
183
+
184
+ return self in (
185
+ SceneChangeMode.WWXD,
186
+ SceneChangeMode.WWXD_SCXVID_UNION,
187
+ SceneChangeMode.WWXD_SCXVID_INTERSECTION,
188
+ )
189
+
190
+ @property
191
+ def is_SCXVID(self) -> bool: # noqa: N802
192
+ """Check whether a mode that uses scxvid is used."""
193
+
194
+ return self in (
195
+ SceneChangeMode.SCXVID,
196
+ SceneChangeMode.WWXD_SCXVID_UNION,
197
+ SceneChangeMode.WWXD_SCXVID_INTERSECTION,
198
+ )
199
+
200
+ def ensure_presence(self, clip: vs.VideoNode) -> vs.VideoNode:
201
+ """
202
+ Ensures all the frame properties necessary for scene change detection
203
+ are created.
204
+ """
205
+
206
+ stats_clip = list[vs.VideoNode]()
207
+
208
+ if self.is_SCXVID:
209
+ if not hasattr(vs.core, "scxvid"):
210
+ raise CustomRuntimeError("Missing scxvid plugin.", self.ensure_presence)
211
+ stats_clip.append(clip.scxvid.Scxvid())
212
+
213
+ if self.is_WWXD:
214
+ if not hasattr(vs.core, "wwxd"):
215
+ raise CustomRuntimeError("Missing wwxd plugin.", self.ensure_presence)
216
+ stats_clip.append(clip.wwxd.WWXD())
217
+
218
+ keys = tuple(self.prop_keys)
219
+
220
+ expr = " ".join([f"x.{k}" for k in keys]) + (" and" * (len(keys) - 1))
221
+
222
+ blank = clip.std.BlankClip(1, 1, vs.GRAY8, keep=True)
223
+
224
+ if len(stats_clip) > 1:
225
+ return merge_clip_props(blank, *stats_clip).akarin.Expr(expr)
226
+
227
+ return blank.std.CopyFrameProps(stats_clip[0]).akarin.Expr(expr)
228
+
229
+ @property
230
+ def prop_keys(self) -> Iterator[str]:
231
+ if self.is_WWXD:
232
+ yield "Scenechange"
233
+
234
+ if self.is_SCXVID:
235
+ yield "_SceneChangePrev"
236
+
237
+ def lambda_cb(self) -> Callable[[int, vs.VideoFrame], SentinelT | int]:
238
+ return lambda n, f: Sentinel.check(n, bool(f[0][0, 0]))
239
+
240
+ def prepare_clip(
241
+ self, clip: vs.VideoNode, height: int | Literal[False] = 360
242
+ ) -> vs.VideoNode:
243
+ """
244
+ Prepare a clip for scene change metric calculations.
245
+
246
+ The clip will always be resampled to YUV420 8bit if it's not already,
247
+ as that's what the plugins support.
248
+
249
+ Args:
250
+ clip: Clip to process.
251
+ height: Output height of the clip. Smaller frame sizes are faster to
252
+ process, but may miss more scene changes or introduce more false
253
+ positives. Width is automatically calculated. `False` means no
254
+ resizing operation is performed. Default: 360.
255
+
256
+ Returns:
257
+ A prepared clip for performing scene change metric calculations on.
258
+ """
259
+
260
+ if height:
261
+ clip = clip.resize.Bilinear(get_w(height, clip), height, vs.YUV420P8)
262
+ elif not clip.format or (clip.format and clip.format.id != vs.YUV420P8):
263
+ clip = clip.resize.Bilinear(format=vs.YUV420P8)
264
+
265
+ return self.ensure_presence(clip)
266
+
267
+
268
+ class Keyframes(JetpackKeyframes):
269
+ """
270
+ Class representing keyframes, or scenechanges.
271
+
272
+ They follow the convention of signaling the start of the new scene.
273
+ """
274
+
275
+ @classmethod
276
+ @override
277
+ def from_clip(
278
+ cls,
279
+ clip: vs.VideoNode,
280
+ mode: SceneChangeMode = SceneChangeMode.SCXVID,
281
+ height: int | Literal[False] = 360,
282
+ **kwargs: Any,
283
+ ) -> Self:
284
+ mode = SceneChangeMode(mode)
285
+
286
+ clip = mode.prepare_clip(clip, height)
287
+
288
+ frames = clip_async_render(
289
+ clip, None, "Detecting scene changes...", mode.lambda_cb(), **kwargs
290
+ )
291
+
292
+ return cls(Sentinel.filter(frames))
293
+
294
+ def to_clip(
295
+ self,
296
+ clip: vs.VideoNode,
297
+ *,
298
+ prop_key: str = next(iter(SceneChangeMode.SCXVID.prop_keys)),
299
+ scene_idx_prop: bool = False,
300
+ ) -> vs.VideoNode:
301
+ propset_clip = clip.std.SetFrameProp(prop_key, True)
302
+
303
+ out = replace_ranges(clip, propset_clip, self)
304
+
305
+ if not scene_idx_prop:
306
+ return out
307
+
308
+ def _add_scene_idx(n: int, f: vs.VideoFrame) -> vs.VideoFrame:
309
+ f = f.copy()
310
+
311
+ f.props._SceneIdx = self.scenes.indices[n]
312
+
313
+ return f
314
+
315
+ return out.std.ModifyFrame(out, _add_scene_idx)
File without changes