vsjetengine 1.0.0__py3-none-any.whl
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.
- vsengine/__init__.py +28 -0
- vsengine/_futures.py +372 -0
- vsengine/_helpers.py +34 -0
- vsengine/_hospice.py +121 -0
- vsengine/_nodes.py +116 -0
- vsengine/_version.py +2 -0
- vsengine/adapters/__init__.py +6 -0
- vsengine/adapters/asyncio.py +85 -0
- vsengine/adapters/trio.py +107 -0
- vsengine/loops.py +269 -0
- vsengine/policy.py +395 -0
- vsengine/py.typed +0 -0
- vsengine/video.py +180 -0
- vsengine/vpy.py +441 -0
- vsjetengine-1.0.0.dist-info/METADATA +350 -0
- vsjetengine-1.0.0.dist-info/RECORD +18 -0
- vsjetengine-1.0.0.dist-info/WHEEL +4 -0
- vsjetengine-1.0.0.dist-info/licenses/COPYING +287 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# vs-engine
|
|
2
|
+
# Copyright (C) 2022 cid-chan
|
|
3
|
+
# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
|
|
4
|
+
# This project is licensed under the EUPL-1.2
|
|
5
|
+
# SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import contextlib
|
|
9
|
+
import contextvars
|
|
10
|
+
from collections.abc import Callable, Iterator
|
|
11
|
+
from concurrent.futures import Future
|
|
12
|
+
|
|
13
|
+
from vsengine.loops import Cancelled, EventLoop
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AsyncIOLoop(EventLoop):
|
|
17
|
+
"""
|
|
18
|
+
Bridges vs-engine to AsyncIO.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None:
|
|
22
|
+
if loop is None:
|
|
23
|
+
loop = asyncio.get_event_loop()
|
|
24
|
+
self.loop = loop
|
|
25
|
+
|
|
26
|
+
def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
27
|
+
future = Future[R]()
|
|
28
|
+
|
|
29
|
+
ctx = contextvars.copy_context()
|
|
30
|
+
|
|
31
|
+
def _wrap() -> None:
|
|
32
|
+
if not future.set_running_or_notify_cancel():
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
result = ctx.run(func, *args, **kwargs)
|
|
37
|
+
except BaseException as e:
|
|
38
|
+
future.set_exception(e)
|
|
39
|
+
else:
|
|
40
|
+
future.set_result(result)
|
|
41
|
+
|
|
42
|
+
self.loop.call_soon_threadsafe(_wrap)
|
|
43
|
+
return future
|
|
44
|
+
|
|
45
|
+
def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
46
|
+
ctx = contextvars.copy_context()
|
|
47
|
+
future = Future[R]()
|
|
48
|
+
|
|
49
|
+
def _wrap() -> R:
|
|
50
|
+
return ctx.run(func, *args, **kwargs)
|
|
51
|
+
|
|
52
|
+
async def _run() -> None:
|
|
53
|
+
try:
|
|
54
|
+
result = await asyncio.to_thread(_wrap)
|
|
55
|
+
except BaseException as e:
|
|
56
|
+
future.set_exception(e)
|
|
57
|
+
else:
|
|
58
|
+
future.set_result(result)
|
|
59
|
+
|
|
60
|
+
self.loop.create_task(_run())
|
|
61
|
+
return future
|
|
62
|
+
|
|
63
|
+
def next_cycle(self) -> Future[None]:
|
|
64
|
+
future = Future[None]()
|
|
65
|
+
task = asyncio.current_task()
|
|
66
|
+
|
|
67
|
+
def continuation() -> None:
|
|
68
|
+
if task is None or not task.cancelled():
|
|
69
|
+
future.set_result(None)
|
|
70
|
+
else:
|
|
71
|
+
future.set_exception(Cancelled())
|
|
72
|
+
|
|
73
|
+
self.loop.call_soon(continuation)
|
|
74
|
+
return future
|
|
75
|
+
|
|
76
|
+
async def await_future[T](self, future: Future[T]) -> T:
|
|
77
|
+
with self.wrap_cancelled():
|
|
78
|
+
return await asyncio.wrap_future(future, loop=self.loop)
|
|
79
|
+
|
|
80
|
+
@contextlib.contextmanager
|
|
81
|
+
def wrap_cancelled(self) -> Iterator[None]:
|
|
82
|
+
try:
|
|
83
|
+
yield
|
|
84
|
+
except Cancelled:
|
|
85
|
+
raise asyncio.CancelledError() from None
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# vs-engine
|
|
2
|
+
# Copyright (C) 2022 cid-chan
|
|
3
|
+
# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
|
|
4
|
+
# This project is licensed under the EUPL-1.2
|
|
5
|
+
# SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
from collections.abc import Callable, Iterator
|
|
9
|
+
from concurrent.futures import Future
|
|
10
|
+
|
|
11
|
+
import trio
|
|
12
|
+
|
|
13
|
+
from vsengine.loops import Cancelled, EventLoop
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TrioEventLoop(EventLoop):
|
|
17
|
+
"""
|
|
18
|
+
Bridges vs-engine to Trio.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, nursery: trio.Nursery, limiter: trio.CapacityLimiter | None = None) -> None:
|
|
22
|
+
if limiter is None:
|
|
23
|
+
limiter = trio.to_thread.current_default_thread_limiter()
|
|
24
|
+
|
|
25
|
+
self.nursery = nursery
|
|
26
|
+
self.limiter = limiter
|
|
27
|
+
self._token: trio.lowlevel.TrioToken | None = None
|
|
28
|
+
|
|
29
|
+
def attach(self) -> None:
|
|
30
|
+
self._token = trio.lowlevel.current_trio_token()
|
|
31
|
+
|
|
32
|
+
def detach(self) -> None:
|
|
33
|
+
self.nursery.cancel_scope.cancel()
|
|
34
|
+
|
|
35
|
+
def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
36
|
+
assert self._token is not None
|
|
37
|
+
|
|
38
|
+
fut = Future[R]()
|
|
39
|
+
|
|
40
|
+
def _executor() -> None:
|
|
41
|
+
if not fut.set_running_or_notify_cancel():
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
result = func(*args, **kwargs)
|
|
46
|
+
except BaseException as e:
|
|
47
|
+
fut.set_exception(e)
|
|
48
|
+
else:
|
|
49
|
+
fut.set_result(result)
|
|
50
|
+
|
|
51
|
+
self._token.run_sync_soon(_executor)
|
|
52
|
+
return fut
|
|
53
|
+
|
|
54
|
+
def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
55
|
+
future = Future[R]()
|
|
56
|
+
|
|
57
|
+
async def _run() -> None:
|
|
58
|
+
def _executor() -> None:
|
|
59
|
+
try:
|
|
60
|
+
result = func(*args, **kwargs)
|
|
61
|
+
future.set_result(result)
|
|
62
|
+
except BaseException as e:
|
|
63
|
+
future.set_exception(e)
|
|
64
|
+
|
|
65
|
+
await trio.to_thread.run_sync(_executor, limiter=self.limiter)
|
|
66
|
+
|
|
67
|
+
self.nursery.start_soon(_run)
|
|
68
|
+
return future
|
|
69
|
+
|
|
70
|
+
def next_cycle(self) -> Future[None]:
|
|
71
|
+
scope = trio.CancelScope()
|
|
72
|
+
future = Future[None]()
|
|
73
|
+
|
|
74
|
+
def continuation() -> None:
|
|
75
|
+
if scope.cancel_called:
|
|
76
|
+
future.set_exception(Cancelled())
|
|
77
|
+
else:
|
|
78
|
+
future.set_result(None)
|
|
79
|
+
|
|
80
|
+
self.from_thread(continuation)
|
|
81
|
+
return future
|
|
82
|
+
|
|
83
|
+
async def await_future[T](self, future: Future[T]) -> T:
|
|
84
|
+
event = trio.Event()
|
|
85
|
+
|
|
86
|
+
def _when_done(_: Future[T]) -> None:
|
|
87
|
+
self.from_thread(event.set)
|
|
88
|
+
|
|
89
|
+
future.add_done_callback(_when_done)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
await event.wait()
|
|
93
|
+
except trio.Cancelled:
|
|
94
|
+
raise
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
return future.result()
|
|
98
|
+
except BaseException as exc:
|
|
99
|
+
with self.wrap_cancelled():
|
|
100
|
+
raise exc
|
|
101
|
+
|
|
102
|
+
@contextlib.contextmanager
|
|
103
|
+
def wrap_cancelled(self) -> Iterator[None]:
|
|
104
|
+
try:
|
|
105
|
+
yield
|
|
106
|
+
except Cancelled:
|
|
107
|
+
raise trio.Cancelled.__new__(trio.Cancelled) from None
|
vsengine/loops.py
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# vs-engine
|
|
2
|
+
# Copyright (C) 2022 cid-chan
|
|
3
|
+
# Copyright (C) 2025 Jaded-Encoding-Thaumaturgy
|
|
4
|
+
# This project is licensed under the EUPL-1.2
|
|
5
|
+
# SPDX-License-Identifier: EUPL-1.2
|
|
6
|
+
|
|
7
|
+
"""This module provides an abstraction layer to integrate VapourSynth with any event loop (asyncio, Qt, Trio, etc.)."""
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
from abc import abstractmethod
|
|
11
|
+
from collections.abc import Awaitable, Callable, Iterator
|
|
12
|
+
from concurrent.futures import CancelledError, Future
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from functools import wraps
|
|
15
|
+
|
|
16
|
+
import vapoursynth as vs
|
|
17
|
+
|
|
18
|
+
__all__ = ["Cancelled", "EventLoop", "from_thread", "get_loop", "keep_environment", "set_loop", "to_thread"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Cancelled(BaseException):
|
|
22
|
+
"""Exception raised when an operation has been cancelled."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@contextmanager
|
|
26
|
+
def _noop() -> Iterator[None]:
|
|
27
|
+
yield
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
DONE = Future[None]()
|
|
31
|
+
DONE.set_result(None)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class EventLoop:
|
|
35
|
+
"""
|
|
36
|
+
Abstract base class for event loop integration.
|
|
37
|
+
|
|
38
|
+
These functions must be implemented to bridge VapourSynth with the event-loop of your choice (e.g., asyncio, Qt).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def attach(self) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Initialize the event loop hooks.
|
|
44
|
+
|
|
45
|
+
Called automatically when :func:`set_loop` is run.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def detach(self) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Clean up event loop hooks.
|
|
51
|
+
|
|
52
|
+
Called when another event-loop takes over, or when the application
|
|
53
|
+
is shutting down/restarting.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
58
|
+
"""
|
|
59
|
+
Schedule a function to run on the event loop (usually the main thread).
|
|
60
|
+
|
|
61
|
+
This is typically called from VapourSynth threads to move data or
|
|
62
|
+
logic back to the main application loop.
|
|
63
|
+
|
|
64
|
+
:param func: The callable to execute.
|
|
65
|
+
:param args: Positional arguments for the callable.
|
|
66
|
+
:param kwargs: Keyword arguments for the callable.
|
|
67
|
+
:return: A Future representing the execution result.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def to_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
71
|
+
"""
|
|
72
|
+
Run a function in a separate worker thread.
|
|
73
|
+
|
|
74
|
+
This is used to offload blocking operations from the main event loop.
|
|
75
|
+
The default implementation utilizes :class:`threading.Thread`.
|
|
76
|
+
|
|
77
|
+
:param func: The callable to execute.
|
|
78
|
+
:param args: Positional arguments for the callable.
|
|
79
|
+
:param kwargs: Keyword arguments for the callable.
|
|
80
|
+
:return: A Future representing the execution result.
|
|
81
|
+
"""
|
|
82
|
+
fut = Future[R]()
|
|
83
|
+
|
|
84
|
+
def wrapper() -> None:
|
|
85
|
+
if not fut.set_running_or_notify_cancel():
|
|
86
|
+
return
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
result = func(*args, **kwargs)
|
|
90
|
+
except BaseException as e:
|
|
91
|
+
fut.set_exception(e)
|
|
92
|
+
else:
|
|
93
|
+
fut.set_result(result)
|
|
94
|
+
|
|
95
|
+
threading.Thread(target=wrapper).start()
|
|
96
|
+
|
|
97
|
+
return fut
|
|
98
|
+
|
|
99
|
+
def next_cycle(self) -> Future[None]:
|
|
100
|
+
"""
|
|
101
|
+
Pass control back to the event loop.
|
|
102
|
+
|
|
103
|
+
This allows the event loop to process pending events.
|
|
104
|
+
|
|
105
|
+
* If there is **no** event-loop, the function returns an immediately resolved future.
|
|
106
|
+
* If there **is** an event-loop, the function returns a pending future that
|
|
107
|
+
resolves after the next cycle.
|
|
108
|
+
|
|
109
|
+
:raises vsengine.loops.Cancelled: If the operation has been cancelled.
|
|
110
|
+
:return: A Future that resolves when the cycle is complete.
|
|
111
|
+
"""
|
|
112
|
+
future = Future[None]()
|
|
113
|
+
self.from_thread(future.set_result, None)
|
|
114
|
+
return future
|
|
115
|
+
|
|
116
|
+
def await_future[T](self, future: Future[T]) -> Awaitable[T]:
|
|
117
|
+
"""
|
|
118
|
+
Convert a concurrent Future into an Awaitable compatible with this loop.
|
|
119
|
+
|
|
120
|
+
This function does not need to be implemented if the event-loop
|
|
121
|
+
does not support ``async`` and ``await`` syntax.
|
|
122
|
+
|
|
123
|
+
:param future: The concurrent.futures.Future to await.
|
|
124
|
+
:return: An awaitable object.
|
|
125
|
+
"""
|
|
126
|
+
raise NotImplementedError
|
|
127
|
+
|
|
128
|
+
@contextmanager
|
|
129
|
+
def wrap_cancelled(self) -> Iterator[None]:
|
|
130
|
+
"""
|
|
131
|
+
Context manager to translate cancellation exceptions.
|
|
132
|
+
|
|
133
|
+
Wraps :exc:`vsengine.loops.Cancelled` into the native cancellation
|
|
134
|
+
error of the specific event loop implementation (e.g., ``asyncio.CancelledError``).
|
|
135
|
+
"""
|
|
136
|
+
try:
|
|
137
|
+
yield
|
|
138
|
+
except Cancelled:
|
|
139
|
+
raise CancelledError from None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class _NoEventLoop(EventLoop):
|
|
143
|
+
"""
|
|
144
|
+
The default event-loop implementation.
|
|
145
|
+
|
|
146
|
+
This is used when no specific loop is attached. It runs operations synchronously/inline.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def from_thread[**P, R](self, func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
150
|
+
fut = Future[R]()
|
|
151
|
+
try:
|
|
152
|
+
result = func(*args, **kwargs)
|
|
153
|
+
except BaseException as e:
|
|
154
|
+
fut.set_exception(e)
|
|
155
|
+
else:
|
|
156
|
+
fut.set_result(result)
|
|
157
|
+
return fut
|
|
158
|
+
|
|
159
|
+
def next_cycle(self) -> Future[None]:
|
|
160
|
+
return DONE
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
NO_LOOP = _NoEventLoop()
|
|
164
|
+
_current_loop: EventLoop = NO_LOOP
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def get_loop() -> EventLoop:
|
|
168
|
+
"""
|
|
169
|
+
Retrieve the currently active event loop.
|
|
170
|
+
|
|
171
|
+
:return: The currently running EventLoop instance.
|
|
172
|
+
"""
|
|
173
|
+
return _current_loop
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def set_loop(loop: EventLoop) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Set the currently running event loop.
|
|
179
|
+
|
|
180
|
+
This function will detach the previous loop first. If attaching the new
|
|
181
|
+
loop fails, it reverts to the ``_NoEventLoop`` implementation which runs
|
|
182
|
+
everything inline.
|
|
183
|
+
|
|
184
|
+
:param loop: The EventLoop instance to attach.
|
|
185
|
+
"""
|
|
186
|
+
global _current_loop
|
|
187
|
+
_current_loop.detach()
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
_current_loop = loop
|
|
191
|
+
loop.attach()
|
|
192
|
+
except:
|
|
193
|
+
_current_loop = NO_LOOP
|
|
194
|
+
raise
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def keep_environment[**P, R](func: Callable[P, R]) -> Callable[P, R]:
|
|
198
|
+
"""
|
|
199
|
+
Decorate a function to preserve the VapourSynth environment.
|
|
200
|
+
|
|
201
|
+
The returned function captures the VapourSynth environment active
|
|
202
|
+
at the moment the decorator is applied and restores it when the
|
|
203
|
+
function is executed.
|
|
204
|
+
|
|
205
|
+
:param func: The function to decorate.
|
|
206
|
+
:return: A wrapped function that maintains the captured environment.
|
|
207
|
+
"""
|
|
208
|
+
try:
|
|
209
|
+
environment = vs.get_current_environment().use
|
|
210
|
+
except RuntimeError:
|
|
211
|
+
environment = _noop
|
|
212
|
+
|
|
213
|
+
@wraps(func)
|
|
214
|
+
def _wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
|
|
215
|
+
with environment():
|
|
216
|
+
return func(*args, **kwargs)
|
|
217
|
+
|
|
218
|
+
return _wrapper
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def from_thread[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
222
|
+
"""
|
|
223
|
+
Run a function inside the current event-loop.
|
|
224
|
+
|
|
225
|
+
This preserves the currently running VapourSynth environment (if any).
|
|
226
|
+
|
|
227
|
+
.. note::
|
|
228
|
+
Depending on the loop implementation, the function might be called inline.
|
|
229
|
+
|
|
230
|
+
:param func: The function to call inside the current event loop.
|
|
231
|
+
:param args: The arguments for the function.
|
|
232
|
+
:param kwargs: The keyword arguments to pass to the function.
|
|
233
|
+
:return: A Future that resolves or rejects depending on the outcome.
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
@keep_environment
|
|
237
|
+
def _wrapper() -> R:
|
|
238
|
+
return func(*args, **kwargs)
|
|
239
|
+
|
|
240
|
+
return get_loop().from_thread(_wrapper)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def to_thread[**P, R](func: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> Future[R]:
|
|
244
|
+
"""
|
|
245
|
+
Run a function in a dedicated thread or worker.
|
|
246
|
+
|
|
247
|
+
This preserves the currently running VapourSynth environment (if any).
|
|
248
|
+
|
|
249
|
+
:param func: The function to call in a worker thread.
|
|
250
|
+
:param args: The arguments for the function.
|
|
251
|
+
:param kwargs: The keyword arguments to pass to the function.
|
|
252
|
+
:return: A Future representing the execution result.
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
@keep_environment
|
|
256
|
+
def _wrapper() -> R:
|
|
257
|
+
return func(*args, **kwargs)
|
|
258
|
+
|
|
259
|
+
return get_loop().to_thread(_wrapper)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
async def make_awaitable[T](future: Future[T]) -> T:
|
|
263
|
+
"""
|
|
264
|
+
Make a standard concurrent Future awaitable in the current loop.
|
|
265
|
+
|
|
266
|
+
:param future: The future object to make awaitable.
|
|
267
|
+
:return: The result of the future, once awaited.
|
|
268
|
+
"""
|
|
269
|
+
return await get_loop().await_future(future)
|