ulf 0.1.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.
- ulf/__init__.py +116 -0
- ulf/__main__.py +39 -0
- ulf/_optional.py +23 -0
- ulf-0.1.0.dist-info/METADATA +142 -0
- ulf-0.1.0.dist-info/RECORD +11 -0
- ulf-0.1.0.dist-info/WHEEL +5 -0
- ulf-0.1.0.dist-info/entry_points.txt +4 -0
- ulf-0.1.0.dist-info/licenses/LICENSE +674 -0
- ulf-0.1.0.dist-info/top_level.txt +3 -0
- ulf_encode.py +1305 -0
- ulf_play.py +2484 -0
ulf/__init__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Python API for Unicode Lossful Format (ULF).
|
|
2
|
+
|
|
3
|
+
The functions in this module expose the same settings as the command-line
|
|
4
|
+
tools. Optional features need their matching extra: ``ulf[all]`` installs
|
|
5
|
+
GPU playback/matching and live audio playback support.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from os import PathLike
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
Pathish = str | PathLike[str]
|
|
16
|
+
Backend = Literal["auto", "vulkan", "cpu"]
|
|
17
|
+
Quality = Literal["high", "medium", "low"]
|
|
18
|
+
Encoder = Literal["auto", "nvenc", "amf", "qsv", "vaapi", "cpu"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _path(value: Pathish) -> str:
|
|
22
|
+
return str(value)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def encode(
|
|
26
|
+
input: Pathish,
|
|
27
|
+
output: Pathish | None = None,
|
|
28
|
+
*,
|
|
29
|
+
size: str = "80x40",
|
|
30
|
+
fps: float = 0,
|
|
31
|
+
font: Pathish | None = None,
|
|
32
|
+
every: int = 1,
|
|
33
|
+
backend: Backend = "auto",
|
|
34
|
+
keyframe_interval: int = 24,
|
|
35
|
+
emojiless: bool = False,
|
|
36
|
+
hackerman: bool = False,
|
|
37
|
+
audio_waves: int = 8,
|
|
38
|
+
audio_segment: float = 0.05,
|
|
39
|
+
mute: bool = False,
|
|
40
|
+
debug: bool = False,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Encode a video as ULF.
|
|
43
|
+
|
|
44
|
+
``output`` defaults to the input filename with a ``.ulf`` suffix.
|
|
45
|
+
Keyword names correspond to the CLI options with hyphens replaced by
|
|
46
|
+
underscores.
|
|
47
|
+
"""
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
from ulf_encode import main
|
|
50
|
+
|
|
51
|
+
source = _path(input)
|
|
52
|
+
destination = _path(output) if output is not None else str(Path(source).with_suffix(".ulf"))
|
|
53
|
+
argv = [source, destination, "--size", size, "--fps", str(fps), "--every", str(every),
|
|
54
|
+
"--backend", backend, "--keyframe-interval", str(keyframe_interval),
|
|
55
|
+
"--audio-waves", str(audio_waves), "--audio-segment", str(audio_segment)]
|
|
56
|
+
if font is not None:
|
|
57
|
+
argv += ["--font", _path(font)]
|
|
58
|
+
if emojiless:
|
|
59
|
+
argv.append("--emojiless")
|
|
60
|
+
if hackerman:
|
|
61
|
+
argv.append("--hackerman")
|
|
62
|
+
if mute:
|
|
63
|
+
argv.append("--mute")
|
|
64
|
+
if debug:
|
|
65
|
+
argv.append("--debug")
|
|
66
|
+
main(argv)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def play(
|
|
70
|
+
input: Pathish,
|
|
71
|
+
*,
|
|
72
|
+
window: str = "1000x700",
|
|
73
|
+
font: Pathish | None = None,
|
|
74
|
+
backend: Backend = "auto",
|
|
75
|
+
mute: bool = False,
|
|
76
|
+
debug: bool = False,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Play a ULF file in a window."""
|
|
79
|
+
from ulf_play import main
|
|
80
|
+
|
|
81
|
+
argv = [_path(input), "--window", window, "--backend", backend]
|
|
82
|
+
if font is not None:
|
|
83
|
+
argv += ["--font", _path(font)]
|
|
84
|
+
if mute:
|
|
85
|
+
argv.append("--mute")
|
|
86
|
+
if debug:
|
|
87
|
+
argv.append("--debug")
|
|
88
|
+
main(argv)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def convert(
|
|
92
|
+
input: Pathish,
|
|
93
|
+
output: Pathish,
|
|
94
|
+
*,
|
|
95
|
+
font: Pathish | None = None,
|
|
96
|
+
backend: Backend = "auto",
|
|
97
|
+
mute: bool = False,
|
|
98
|
+
export_size: str | None = None,
|
|
99
|
+
export_quality: Quality = "high",
|
|
100
|
+
encoder: Encoder = "auto",
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Export a ULF file to MP4 using FFmpeg."""
|
|
103
|
+
from ulf_play import main
|
|
104
|
+
|
|
105
|
+
argv = [_path(input), "--backend", backend, "--export", _path(output),
|
|
106
|
+
"--export-quality", export_quality, "--encoder", encoder]
|
|
107
|
+
if font is not None:
|
|
108
|
+
argv += ["--font", _path(font)]
|
|
109
|
+
if mute:
|
|
110
|
+
argv.append("--mute")
|
|
111
|
+
if export_size is not None:
|
|
112
|
+
argv += ["--export-size", export_size]
|
|
113
|
+
main(argv)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
__all__ = ["convert", "encode", "play", "__version__"]
|
ulf/__main__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Unified ``ulf`` command line entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main(argv=None) -> None:
|
|
9
|
+
parser = argparse.ArgumentParser(prog="ulf", description="Unicode Lossful Format tools")
|
|
10
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
11
|
+
# Disable subcommand help here so it is forwarded to the established
|
|
12
|
+
# encoder/player parsers, which own the option documentation.
|
|
13
|
+
commands.add_parser("encode", help="Encode video as ULF", add_help=False)
|
|
14
|
+
commands.add_parser("play", help="Play a ULF file", add_help=False)
|
|
15
|
+
convert_parser = commands.add_parser(
|
|
16
|
+
"convert", help="Export a ULF file to MP4", add_help=False
|
|
17
|
+
)
|
|
18
|
+
convert_parser.add_argument("input", nargs="?")
|
|
19
|
+
convert_parser.add_argument("output", nargs="?")
|
|
20
|
+
namespace, remaining = parser.parse_known_args(argv)
|
|
21
|
+
|
|
22
|
+
if namespace.command == "encode":
|
|
23
|
+
from ulf_encode import main as command
|
|
24
|
+
command(remaining)
|
|
25
|
+
elif namespace.command == "play":
|
|
26
|
+
from ulf_play import main as command
|
|
27
|
+
command(remaining)
|
|
28
|
+
else:
|
|
29
|
+
from ulf_play import main as command
|
|
30
|
+
if "-h" in remaining or "--help" in remaining:
|
|
31
|
+
command(["--help"])
|
|
32
|
+
return
|
|
33
|
+
if namespace.input is None or namespace.output is None:
|
|
34
|
+
convert_parser.error("the following arguments are required: input, output")
|
|
35
|
+
command([namespace.input, "--export", namespace.output, *remaining])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|
ulf/_optional.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
"""Checks and messages for features supplied by optional dependencies."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import importlib.util
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
INSTALL_ALL = 'python -m pip install "unicode-lossful-format[all]"'
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def has_optional_dependencies(*modules: str) -> bool:
|
|
13
|
+
"""Return True when all requested optional modules are available."""
|
|
14
|
+
return all(importlib.util.find_spec(module) is not None for module in modules)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def warn_optional(feature: str) -> None:
|
|
18
|
+
"""Tell users how to enable an optional enhancement."""
|
|
19
|
+
print(
|
|
20
|
+
f"WARNING: {feature} will not work without optional dependencies. "
|
|
21
|
+
f"Install every optional feature with: {INSTALL_ALL}",
|
|
22
|
+
file=sys.stderr,
|
|
23
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ulf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Encode, play, and export Unicode Lossful Format video files.
|
|
5
|
+
Author: ULF contributors
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/EquinoxDaFurry/ULF
|
|
8
|
+
Project-URL: Repository, https://github.com/EquinoxDaFurry/ULF
|
|
9
|
+
Project-URL: Issues, https://github.com/EquinoxDaFurry/ULF/issues
|
|
10
|
+
Keywords: video,unicode,encoder,terminal,ulf
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: fonttools
|
|
23
|
+
Requires-Dist: numpy
|
|
24
|
+
Requires-Dist: opencv-python
|
|
25
|
+
Requires-Dist: pillow
|
|
26
|
+
Requires-Dist: regex
|
|
27
|
+
Provides-Extra: gpu
|
|
28
|
+
Requires-Dist: wgpu; extra == "gpu"
|
|
29
|
+
Requires-Dist: rendercanvas; extra == "gpu"
|
|
30
|
+
Requires-Dist: glfw; extra == "gpu"
|
|
31
|
+
Provides-Extra: audio
|
|
32
|
+
Requires-Dist: sounddevice; extra == "audio"
|
|
33
|
+
Provides-Extra: all
|
|
34
|
+
Requires-Dist: wgpu; extra == "all"
|
|
35
|
+
Requires-Dist: rendercanvas; extra == "all"
|
|
36
|
+
Requires-Dist: glfw; extra == "all"
|
|
37
|
+
Requires-Dist: sounddevice; extra == "all"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# Unicode Lossful Format (ULF)
|
|
41
|
+
|
|
42
|
+
A deliberately silly text-based video format. ULF represents video frames using Unicode characters, choosing glyphs that visually resemble parts of the original image.
|
|
43
|
+
|
|
44
|
+
It supports audio, delta-frame compression, GPU acceleration, and MP4 export.
|
|
45
|
+
|
|
46
|
+
## Comparison
|
|
47
|
+
|
|
48
|
+
<img src="ULF_comparison.gif" alt="ULF Comparison" width="600">
|
|
49
|
+
|
|
50
|
+
## Requirements
|
|
51
|
+
|
|
52
|
+
Python 3.10+.
|
|
53
|
+
|
|
54
|
+
Install the package (with all optional playback features):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
python -m pip install "unicode-lossful-format[all]"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
FFmpeg must be installed and available on your system PATH for MP4 export.
|
|
61
|
+
It is an external system dependency and is not installed by pip or by any ULF
|
|
62
|
+
extra. Verify it with `ffmpeg -version` before using `ulf convert` or
|
|
63
|
+
`ulf.convert()`.
|
|
64
|
+
|
|
65
|
+
## Encode
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
ulf-encode input.mp4 output.ulf --size 80x40
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Options
|
|
72
|
+
|
|
73
|
+
- `--size WIDTHxHEIGHT`: Character-grid resolution (default: `80x40`).
|
|
74
|
+
- `--fps N`: Output frame rate.
|
|
75
|
+
- `--font PATH`: Font used for glyph matching.
|
|
76
|
+
- `--every N`: Encode every Nth source frame.
|
|
77
|
+
- `--backend auto|vulkan|cpu`: Select processing backend.
|
|
78
|
+
- `--keyframe-interval N`: Set the interval between full frames.
|
|
79
|
+
- `--mute`: Disable audio.
|
|
80
|
+
- `--audio-waves N`: Set audio reconstruction detail.
|
|
81
|
+
- `--emojiless`: Exclude emoji characters.
|
|
82
|
+
- `--hackerman`: Use printable keyboard ASCII characters only.
|
|
83
|
+
- `--debug`: Enable diagnostic output.
|
|
84
|
+
|
|
85
|
+
## Play
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
ulf-play output.ulf
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Options
|
|
92
|
+
|
|
93
|
+
- `--window WIDTHxHEIGHT`: Set playback window size.
|
|
94
|
+
- `--font PATH`: Select playback font.
|
|
95
|
+
- `--backend auto|vulkan|cpu`: Select playback backend.
|
|
96
|
+
- `--mute`: Disable audio.
|
|
97
|
+
- `--debug`: Enable diagnostic output.
|
|
98
|
+
|
|
99
|
+
## Convert to MP4
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
ulf convert output.ulf output.mp4 --encoder auto --export-quality high
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The legacy equivalent remains `ulf-play output.ulf --export output.mp4`.
|
|
106
|
+
|
|
107
|
+
## Python API
|
|
108
|
+
|
|
109
|
+
The same options are available as keyword arguments, using underscores in
|
|
110
|
+
place of CLI hyphens:
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import ulf
|
|
114
|
+
|
|
115
|
+
ulf.encode("example.mp4", "example.ulf", size="80x40", backend="auto")
|
|
116
|
+
ulf.play("example.ulf", window="1280x720", mute=False)
|
|
117
|
+
ulf.convert("example.ulf", "example.mp4", encoder="auto", export_quality="high")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`encode()` derives an `.ulf` output path when its `output` argument is omitted.
|
|
121
|
+
`convert()` requires FFmpeg on your system `PATH`, just like the CLI.
|
|
122
|
+
|
|
123
|
+
## File format
|
|
124
|
+
|
|
125
|
+
ULF files use the `.ulf` extension and are stored as UTF-8 text.
|
|
126
|
+
|
|
127
|
+
The format uses the `ULF/1.0` signature and supports full-frame and delta-frame video, with optional audio data.
|
|
128
|
+
|
|
129
|
+
## Limitations
|
|
130
|
+
|
|
131
|
+
- Video is grayscale.
|
|
132
|
+
- Audio reconstruction is lossy.
|
|
133
|
+
- Visual quality depends on the selected font and renderer.
|
|
134
|
+
- Vulkan requires compatible hardware and drivers.
|
|
135
|
+
- MP4 export requires FFmpeg.
|
|
136
|
+
- Large resolutions can increase processing time and file size.
|
|
137
|
+
|
|
138
|
+
## Have fun
|
|
139
|
+
|
|
140
|
+
ULF makes video less efficient on purpose.
|
|
141
|
+
|
|
142
|
+
Now with compression, GPU acceleration, sound, and MP4 export!
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
ulf_encode.py,sha256=QN16ruYiV0V7WOd-3Cdv5LK_JXPdltUpCa0T_BDnzU8,33888
|
|
2
|
+
ulf_play.py,sha256=I5_2eXmq4rIlBgUQ_aKDuIqHm5SY927UZ_drsy-unGg,57224
|
|
3
|
+
ulf/__init__.py,sha256=rg_IwHyWM_-jvwonivtRGOiat_UVWRYrUOxl2aJKL-o,3189
|
|
4
|
+
ulf/__main__.py,sha256=H4GnPXVM8o_O0CXuAA0CeIxq3lnyLL3KNoQBKn4ACgs,1512
|
|
5
|
+
ulf/_optional.py,sha256=6hJVgK50liDsj7YMuZYMOdaU4bU4KW_ymRN4KipIWgg,710
|
|
6
|
+
ulf-0.1.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
7
|
+
ulf-0.1.0.dist-info/METADATA,sha256=u1qqJvnKnLDSsXGg_orW5iGHr9K4OBpGaLbUtuqdzs4,4487
|
|
8
|
+
ulf-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
9
|
+
ulf-0.1.0.dist-info/entry_points.txt,sha256=BjmilIMsQTtvd0hzEw8dDkBItyeCvBcg-Y3yq1U0b1s,96
|
|
10
|
+
ulf-0.1.0.dist-info/top_level.txt,sha256=yiDrSbNvzVHvoxW6gsxkz_1ZJ0vhZneEpvTGxsZan3c,24
|
|
11
|
+
ulf-0.1.0.dist-info/RECORD,,
|