cvf 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.
cvf/__init__.py ADDED
@@ -0,0 +1,297 @@
1
+ """
2
+ cvf — Console Video File (.cvf) Making and Executing.
3
+ Author: Suleiman
4
+ Version: 1.0.0
5
+ License: MIT.
6
+ Copyright (c) Suleiman 2026
7
+ """
8
+
9
+ import os
10
+ import json
11
+ import time
12
+ import lzma
13
+ import zipfile
14
+ from datetime import datetime
15
+
16
+ try:
17
+ import cuifw
18
+ _HAVE_CUIFW = True
19
+ except ImportError:
20
+ _HAVE_CUIFW = False
21
+
22
+
23
+ __version__ = "1.0.0"
24
+ __author__ = "Suleiman"
25
+ __all__ = ["CreateVideo", "PlayVideo", "VideoData"]
26
+
27
+
28
+ DEFAULT_CHARS = " .:-=+*#%@"
29
+ MAGIC_FORMAT = "CVF"
30
+ FORMAT_VERSION = "1.0"
31
+
32
+
33
+ # ═════════════════════════════════════════════════════════
34
+ # Writer
35
+ # ═════════════════════════════════════════════════════════
36
+
37
+ def _write_cvf(path, frames, width, height,
38
+ fps, chars, metadata, compress_level):
39
+ if not frames:
40
+ raise ValueError("cvf: no frames")
41
+
42
+ expected = width * height
43
+ for i, f in enumerate(frames):
44
+ if len(f) != expected:
45
+ raise ValueError(
46
+ "cvf: frame %d size %d, expected %d"
47
+ % (i, len(f), expected)
48
+ )
49
+
50
+ raw = b"".join(frames)
51
+ compressed = lzma.compress(
52
+ raw, format=lzma.FORMAT_XZ, preset=int(compress_level)
53
+ )
54
+
55
+ meta = {
56
+ "format": MAGIC_FORMAT,
57
+ "version": FORMAT_VERSION,
58
+ "width": int(width),
59
+ "height": int(height),
60
+ "fps": int(fps),
61
+ "frame_count": len(frames),
62
+ "chars": chars,
63
+ "compression": "lzma-xz",
64
+ "raw_size": len(raw),
65
+ "compressed_size": len(compressed),
66
+ "created": datetime.now().isoformat(),
67
+ }
68
+ if metadata:
69
+ meta["user"] = metadata
70
+
71
+ with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z:
72
+ z.writestr("meta.json", json.dumps(meta, indent=2, ensure_ascii=False))
73
+ z.writestr("chars.txt", chars)
74
+ z.writestr("frames.lzma", compressed)
75
+
76
+ return path
77
+
78
+
79
+ # ═════════════════════════════════════════════════════════
80
+ # Reader
81
+ # ═════════════════════════════════════════════════════════
82
+
83
+ def _read_cvf(path, load_frames=True):
84
+ with zipfile.ZipFile(path, "r") as z:
85
+ meta = json.loads(z.read("meta.json").decode("utf-8"))
86
+ chars = z.read("chars.txt").decode("utf-8")
87
+ compressed = z.read("frames.lzma")
88
+
89
+ result = {
90
+ "meta": meta,
91
+ "chars": chars,
92
+ "frames": None,
93
+ "frame_count": meta.get("frame_count", 0),
94
+ }
95
+
96
+ if load_frames:
97
+ raw = lzma.decompress(compressed)
98
+ w = meta["width"]
99
+ h = meta["height"]
100
+ size = w * h
101
+ count = meta["frame_count"]
102
+ result["frames"] = [
103
+ raw[i * size:(i + 1) * size] for i in range(count)
104
+ ]
105
+
106
+ return result
107
+
108
+
109
+ # ═════════════════════════════════════════════════════════
110
+ # Player (CuiFW widget)
111
+ # ═════════════════════════════════════════════════════════
112
+
113
+ if _HAVE_CUIFW:
114
+
115
+ class _CVFWidget(cuifw.RenderWidget):
116
+ def __init__(self, x, y, w, h, chars, color, bg):
117
+ super().__init__(x, y, w, h)
118
+ self.chars = chars
119
+ self.color = color or cuifw.Colors.WHITE
120
+ self.bg = bg
121
+ self.frame = None
122
+ self.fw = 0
123
+ self.fh = 0
124
+
125
+ def set_frame(self, frame, w, h):
126
+ self.frame = frame
127
+ self.fw = w
128
+ self.fh = h
129
+
130
+ def clear_frame(self):
131
+ self.frame = None
132
+ self.fw = 0
133
+ self.fh = 0
134
+
135
+ def draw(self, canvas):
136
+ if self.frame is None:
137
+ return
138
+ w = min(self.fw, self.w)
139
+ h = min(self.fh, self.h)
140
+ nchars = len(self.chars)
141
+ for row in range(h):
142
+ base = row * self.fw
143
+ for col in range(w):
144
+ idx = self.frame[base + col]
145
+ if idx >= nchars:
146
+ idx = 0
147
+ ch = self.chars[idx]
148
+ if ch != " ":
149
+ canvas.set(self.x + col, self.y + row, ch,
150
+ self.color, self.bg)
151
+
152
+
153
+ class _CVFPlayer:
154
+ def __init__(self, path, color=None, bg=None,
155
+ show_status=True, loop=False):
156
+ data = _read_cvf(path, load_frames=True)
157
+ self.path = path
158
+ self.meta = data["meta"]
159
+ self.chars = data["chars"]
160
+ self.frames = data["frames"]
161
+ self.width = self.meta["width"]
162
+ self.height = self.meta["height"]
163
+ self.fps = self.meta["fps"] or 15
164
+ self.frame_time = 1.0 / self.fps
165
+ self.loop = loop
166
+ self.show_status = show_status
167
+
168
+ self.app = cuifw.App(alt_screen=True)
169
+ self.W, self.H = self.app.size()
170
+ h = self.H - 1 if show_status else self.H
171
+
172
+ self.widget = _CVFWidget(0, 0, self.W, h,
173
+ chars=self.chars,
174
+ color=color, bg=bg)
175
+ self.app.add(self.widget)
176
+
177
+ self.index = 0
178
+ self.last = 0.0
179
+ self.shown = 0
180
+ self.start = 0.0
181
+
182
+ def _draw_status(self):
183
+ if not self.show_status:
184
+ return
185
+ canvas = self.app.canvas
186
+ if canvas is None:
187
+ return
188
+
189
+ elapsed = time.time() - self.start
190
+ fps = self.shown / elapsed if elapsed > 0 else 0
191
+ total = len(self.frames)
192
+ name = os.path.basename(self.path)
193
+
194
+ text = " %s | %d/%d | %.1f FPS " % (name, self.index, total, fps)
195
+
196
+ y = self.H - 1
197
+ canvas.fill_rect(0, y, self.W - 1, y, " ",
198
+ cuifw.Colors.BLACK, cuifw.Colors.BG_BLACK)
199
+ canvas.text(0, y, text[:self.W],
200
+ cuifw.Colors.CYAN, cuifw.Colors.BG_BLACK)
201
+
202
+ def _on_tick(self):
203
+ now = time.time()
204
+ if now - self.last < self.frame_time:
205
+ return
206
+ self.last = now
207
+
208
+ if self.index >= len(self.frames):
209
+ if self.loop:
210
+ self.index = 0
211
+ else:
212
+ self.app.quit()
213
+ return
214
+
215
+ frame = self.frames[self.index]
216
+ self.widget.set_frame(frame, self.width, self.height)
217
+ self.index += 1
218
+ self.shown += 1
219
+
220
+ self.app.redraw()
221
+ self._draw_status()
222
+
223
+ def run(self):
224
+ self.start = time.time()
225
+ try:
226
+ self.app.run(on_tick=self._on_tick, tick_interval=0.005)
227
+ except KeyboardInterrupt:
228
+ pass
229
+
230
+
231
+ # ═════════════════════════════════════════════════════════
232
+ # Public API
233
+ # ═════════════════════════════════════════════════════════
234
+
235
+ def CreateVideo(path, frames, width, height,
236
+ fps=15, chars=DEFAULT_CHARS,
237
+ metadata=None, compress_level=6):
238
+ """
239
+ Create a .cvf video file.
240
+
241
+ path — output .cvf path
242
+ frames — list of bytes (each width*height)
243
+ width, height — frame dimensions
244
+ fps — playback framerate
245
+ chars — character palette (max 256)
246
+ metadata — optional dict of extra info
247
+ compress_level — LZMA 0-9 (default 6)
248
+
249
+ Returns path.
250
+ """
251
+ return _write_cvf(path, frames, width, height,
252
+ fps=fps, chars=chars,
253
+ metadata=metadata,
254
+ compress_level=compress_level)
255
+
256
+
257
+ def PlayVideo(path, color=None, bg=None,
258
+ show_status=True, loop=False):
259
+ """
260
+ Play a .cvf file in the terminal using CuiFW.
261
+
262
+ path — input .cvf file
263
+ color — ANSI color (default: WHITE)
264
+ bg — background color (default: none)
265
+ show_status — show filename + FPS at bottom
266
+ loop — loop playback
267
+
268
+ Requires CuiFW. Raises RuntimeError if CuiFW is not installed.
269
+
270
+ Returns the player object.
271
+ """
272
+ if not _HAVE_CUIFW:
273
+ raise RuntimeError(
274
+ "cvf: CuiFW is required for PlayVideo(). "
275
+ "Install CuiFW or use VideoData() to read frames manually."
276
+ )
277
+ player = _CVFPlayer(path, color=color, bg=bg,
278
+ show_status=show_status, loop=loop)
279
+ player.run()
280
+ return player
281
+
282
+
283
+ def VideoData(path, load_frames=True):
284
+ """
285
+ Read a .cvf file.
286
+
287
+ Returns dict:
288
+ {
289
+ "meta": {...},
290
+ "chars": "...",
291
+ "frames": [bytes, ...] or None,
292
+ "frame_count": int,
293
+ }
294
+
295
+ Set load_frames=False to read only metadata.
296
+ """
297
+ return _read_cvf(path, load_frames=load_frames)
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: cvf
3
+ Version: 1.0.0
4
+ Summary: Console Video File - ASCII video format for the terminal
5
+ Home-page:
6
+ Author: Suleiman
7
+ Author-email:
8
+ License: MIT
9
+ Keywords: terminal ascii video cui cvf cuifw
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.7
16
+ Classifier: Programming Language :: Python :: 3.8
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Multimedia :: Video
22
+ Classifier: Topic :: Terminals
23
+ Classifier: Topic :: Software Development :: Libraries
24
+ Requires-Python: >=3.7
25
+ Description-Content-Type: text/markdown
26
+ License-File: LICENSE
27
+ Requires-Dist: cuifw==1.7.0
28
+ Dynamic: author
29
+ Dynamic: classifier
30
+ Dynamic: description
31
+ Dynamic: description-content-type
32
+ Dynamic: keywords
33
+ Dynamic: license
34
+ Dynamic: license-file
35
+ Dynamic: requires-dist
36
+ Dynamic: requires-python
37
+ Dynamic: summary
38
+
39
+ # cvf
40
+
41
+ Console Video File — ASCII video format for the terminal.
42
+
43
+ Stores ASCII frames in a ZIP container with JSON metadata and LZMA-compressed
44
+ frames. Playback uses CuiFW to render frames in the terminal.
45
+
46
+ ## Features
47
+
48
+ - JSON metadata
49
+ - LZMA-compressed frames
50
+ - ZIP container
51
+ - No ffmpeg for playback
52
+ - Stdlib only
53
+ - Windows, Linux, macOS, Termux
54
+
55
+ ## Install
56
+
57
+ pip install cvf
58
+
59
+ Optional, for playback:
60
+
61
+ pip install cuifw
62
+
63
+ ## API
64
+
65
+ import cvf
66
+
67
+ cvf.CreateVideo(path, frames, width, height, fps=15, chars=..., metadata=None, compress_level=6)
68
+ cvf.PlayVideo(path, color=None, bg=None, show_status=True, loop=False)
69
+ cvf.VideoData(path, load_frames=True)
70
+
71
+ ### CreateVideo
72
+
73
+ Write a .cvf file.
74
+
75
+ import cvf
76
+
77
+ frames = [...] # list of bytes, each width*height
78
+ cvf.CreateVideo("out.cvf", frames, width=80, height=24, fps=15)
79
+
80
+ Returns the output path.
81
+
82
+ ### PlayVideo
83
+
84
+ Play a .cvf file using CuiFW. Requires CuiFW installed.
85
+
86
+ import cvf
87
+
88
+ cvf.PlayVideo("out.cvf")
89
+
90
+ Returns the player object.
91
+
92
+ ### VideoData
93
+
94
+ Read a .cvf file.
95
+
96
+ import cvf
97
+
98
+ data = cvf.VideoData("out.cvf")
99
+ print(data["meta"]["fps"])
100
+ print(data["frame_count"])
101
+
102
+ for frame in data["frames"]:
103
+ pass
104
+
105
+ Metadata only:
106
+
107
+ data = cvf.VideoData("out.cvf", load_frames=False)
108
+
109
+ ## File format
110
+
111
+ A .cvf file is a standard ZIP archive:
112
+
113
+ file.cvf
114
+ ├── meta.json
115
+ ├── chars.txt
116
+ └── frames.lzma
117
+
118
+ ### meta.json
119
+
120
+ {
121
+ "format": "CVF",
122
+ "version": "1.0",
123
+ "width": 80,
124
+ "height": 24,
125
+ "fps": 15,
126
+ "frame_count": 450,
127
+ "chars": " .:-=+*#%@",
128
+ "compression": "lzma-xz",
129
+ "raw_size": 864000,
130
+ "compressed_size": 312450,
131
+ "created": "2026-10-07T12:34:56"
132
+ }
133
+
134
+ ### chars.txt
135
+
136
+ Character palette. Each byte in the frame data is an index into this string.
137
+
138
+ Default:
139
+
140
+ .:-=+*#%@
141
+
142
+ ### frames.lzma
143
+
144
+ All frames concatenated, then compressed with LZMA (XZ).
145
+
146
+ Frame size = width * height bytes.
147
+ Total raw size = width * height * frame_count bytes.
148
+
149
+ ## Size
150
+
151
+ 80x24, 15 FPS, 10s raw 288 KB cvf ~110 KB
152
+ 80x24, 15 FPS, 60s raw 1.7 MB cvf ~600 KB
153
+ 120x40, 15 FPS, 60s raw 4.3 MB cvf ~1.4 MB
154
+
155
+ ## Requirements
156
+
157
+ - Python 3.7+
158
+ - Stdlib only
159
+ - Optional: CuiFW for playback
160
+
161
+ ## Limitations
162
+
163
+ - ASCII only
164
+ - Fixed frame size
165
+ - No audio
166
+ - Fixed FPS
167
+ - No seek during playback
168
+
169
+ ## License
170
+
171
+ MIT — see LICENSE.
172
+
173
+ ## Author
174
+
175
+ Suleiman
176
+
177
+ ## Changelog
178
+
179
+ ### 1.0.0
180
+
181
+ - Initial release
182
+ - CreateVideo, PlayVideo, VideoData
183
+ - JSON metadata
184
+ - LZMA-compressed frames
185
+ - ZIP container
@@ -0,0 +1,6 @@
1
+ cvf/__init__.py,sha256=g2_KfULX7O86x7V-Vqgb4DvRTGZawBF1P3wNI5IyJA4,9517
2
+ cvf-1.0.0.dist-info/licenses/LICENSE,sha256=XABCXMEhX5-sSW6dVui3V35LHqtlwNF4Rj0Rfhuo4_8,1064
3
+ cvf-1.0.0.dist-info/METADATA,sha256=o0u1Ma8AnmNRGg4u2hqSX9aI8bM_CcjjYZ9DPvzqyD8,3788
4
+ cvf-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ cvf-1.0.0.dist-info/top_level.txt,sha256=naqdI6HXCxjbrjA_gr-Yg4TJLsPhkV-O9jDZarmgLOE,4
6
+ cvf-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Suleiman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ cvf