PDASC 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.
pdasc/main.py ADDED
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env python3
2
+
3
+ # Suppress resource_tracker semaphore warnings from sounddevice
4
+ # Must be done before any other imports
5
+ import os
6
+ os.environ['PYTHONWARNINGS'] = 'ignore'
7
+
8
+ import warnings
9
+ warnings.filterwarnings(
10
+ "ignore",
11
+ message=".*resource_tracker.*semaphore.*"
12
+ )
13
+
14
+ from core import AsciiConverter, AsciiDisplayer, AsciiEncoder, generate_color_ramp, get_charmap, render_charmap, VideoAsciiConverter, process_video
15
+ from web import create_video_server, ImageServer
16
+ from PIL import Image
17
+ import argparse
18
+ import sys
19
+ from importlib.resources import files
20
+
21
+ def add_common_args(parser):
22
+ """Add arguments common to both play and encode"""
23
+ parser.add_argument(
24
+ "-b", "--block-size",
25
+ type=int,
26
+ default=8,
27
+ help="Size of character blocks (default: 8)"
28
+ )
29
+
30
+ parser.add_argument(
31
+ "-n", "--num-ascii",
32
+ type=int,
33
+ default=8,
34
+ help="Number of ASCII characters to use (default: 8)"
35
+ )
36
+
37
+ parser.add_argument(
38
+ "-f", "--font",
39
+ type=str,
40
+ default=os.path.join(os.path.dirname(__file__), "fonts", "CascadiaMono.ttf"),
41
+ help="Path to font file to create the ASCII character set from and to display on the website"
42
+ )
43
+
44
+ parser.add_argument(
45
+ "--no-color",
46
+ action="store_true",
47
+ help="Disable color output"
48
+ )
49
+
50
+ parser.add_argument(
51
+ "--no-audio",
52
+ action="store_true",
53
+ help="Disable audio playback for videos"
54
+ )
55
+
56
+ def cmd_play(args):
57
+ """Play command - display images/videos/camera"""
58
+ # Validate font
59
+ if not os.path.exists(args.font):
60
+ print(f"Error: Font file '{args.font}' not found", file=sys.stderr)
61
+ sys.exit(1)
62
+
63
+ converter = AsciiConverter(num_ascii=args.num_ascii, chunk_size=args.block_size, font_path=args.font)
64
+ displayer = AsciiDisplayer(converter, args.debug)
65
+
66
+ try:
67
+ if args.input == "camera":
68
+ print(f"Starting camera {args.camera} (press Ctrl+C to stop)")
69
+ displayer.display_camera(camera_index=args.camera, color=not args.no_color)
70
+ else:
71
+ # Validate input file
72
+ if not os.path.exists(args.input):
73
+ print(f"Error: File '{args.input}' not found", file=sys.stderr)
74
+ sys.exit(1)
75
+
76
+ # Determine file type
77
+ video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
78
+ image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp'}
79
+ special_extensions = {'.asc'}
80
+
81
+ ext = os.path.splitext(args.input)[-1].lower()
82
+
83
+ if ext not in video_extensions and ext not in image_extensions and ext not in special_extensions:
84
+ print(f"Error: Unsupported file extension '{ext}'", file=sys.stderr)
85
+ print(f"Supported: .asc, {', '.join(sorted(video_extensions | image_extensions))}", file=sys.stderr)
86
+ sys.exit(1)
87
+
88
+ if ext == '.asc':
89
+ print(f"Playing .asc file: {args.input}")
90
+ displayer.display_asc_file(args.input, not args.no_audio)
91
+ elif ext in video_extensions:
92
+ print(f"Playing video: {args.input}")
93
+ displayer.display_video(
94
+ video_path=args.input,
95
+ play_audio=not args.no_audio,
96
+ color=not args.no_color
97
+ )
98
+ else:
99
+ print(f"Displaying image: {args.input}")
100
+ displayer.display_image(image=Image.open(args.input), color=not args.no_color)
101
+
102
+ except KeyboardInterrupt:
103
+ print("\nInterrupted by user", file=sys.stderr)
104
+ sys.exit(0)
105
+ except Exception as e:
106
+ print(f"Error: {e}", file=sys.stderr)
107
+ sys.exit(1)
108
+
109
+ def cmd_encode(args):
110
+ """Encode command - save encoded ASCII to file"""
111
+ # Validate font
112
+ if not os.path.exists(args.font):
113
+ print(f"Error: Font file '{args.font}' not found", file=sys.stderr)
114
+ sys.exit(1)
115
+
116
+ print(f"Encoding {args.input} to {args.output}")
117
+ # Validate input file
118
+ if not os.path.exists(args.input):
119
+ print(f"Error: File '{args.input}' not found", file=sys.stderr)
120
+ sys.exit(1)
121
+
122
+ # Determine file type
123
+ video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
124
+ image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.webp'}
125
+
126
+ ext = os.path.splitext(args.input)[-1].lower()
127
+
128
+ if ext not in video_extensions and ext not in image_extensions:
129
+ print(f"Error: Unsupported file extension '{ext}'", file=sys.stderr)
130
+ print(f"Supported: {', '.join(sorted(video_extensions | image_extensions))}", file=sys.stderr)
131
+ sys.exit(1)
132
+
133
+ encoder = AsciiEncoder()
134
+ converter = AsciiConverter(num_ascii=args.num_ascii, chunk_size=args.block_size, font_path=args.font)
135
+
136
+ if ext in video_extensions:
137
+ encoder.encode_video_to_asc(args.input, args.output, not args.no_audio, not args.no_color, converter)
138
+ else:
139
+ encoder.encode_image_to_asc(args.input, args.output, not args.no_color, converter)
140
+
141
+ def cmd_website(args):
142
+ if args.input:
143
+ video_extensions = {'.mp4', '.avi', '.mov', '.mkv', '.webm', '.flv', '.wmv', '.m4v'}
144
+ ext = os.path.splitext(args.input)[-1].lower()
145
+ if os.path.splitext(os.path.splitext(args.input)[0])[-1].lower() == '.asc':
146
+ app = create_video_server(args.input)
147
+ app.run()
148
+ elif ext in video_extensions:
149
+ with open("shaders/ascii.frag") as file:
150
+ frac_src = file.read()
151
+ font_path = str(files("pdasc.fonts").joinpath("font8x8.ttf"))
152
+ charmap_img = render_charmap(get_charmap(generate_color_ramp(font_path=font_path), levels=16), font_path=font_path)
153
+ converter = VideoAsciiConverter(frac_src, charmap_img, not args.no_color)
154
+ out_path = process_video(converter, args.input, args.output, not args.no_audio)
155
+ app = create_video_server(out_path)
156
+ app.run()
157
+ else:
158
+ print("Invalid input for website. Must be a valid video file or not specified")
159
+ else:
160
+ app = ImageServer(font_path=str(files("pdasc.fonts").joinpath("CascadiaMono.ttf")))
161
+ app.run(port=args.port)
162
+
163
+ def main():
164
+ parser = argparse.ArgumentParser(
165
+ description="Convert images and videos to ASCII art",
166
+ formatter_class=argparse.RawDescriptionHelpFormatter
167
+ )
168
+
169
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
170
+
171
+ # Play subcommand
172
+ play_parser = subparsers.add_parser(
173
+ 'play',
174
+ help='Display images/videos/camera as ASCII art',
175
+ formatter_class=argparse.RawDescriptionHelpFormatter,
176
+ epilog="""
177
+ Examples:
178
+ %(prog)s image.png
179
+ %(prog)s video.mp4 --no-audio
180
+ %(prog)s camera -c 0
181
+ %(prog)s image.jpg -b 16 -n 70
182
+ """
183
+ )
184
+
185
+ add_common_args(play_parser)
186
+
187
+ play_parser.add_argument(
188
+ "input",
189
+ type=str,
190
+ help='Path to input file or "camera" for camera input'
191
+ )
192
+
193
+ play_parser.add_argument(
194
+ "-c", "--camera",
195
+ type=int,
196
+ default=0,
197
+ help="Camera index when using camera input (default: 0)"
198
+ )
199
+
200
+ play_parser.add_argument(
201
+ "--debug",
202
+ action="store_true",
203
+ help="Enable debug mode to show FPS and other debug info"
204
+ )
205
+
206
+ play_parser.set_defaults(func=cmd_play)
207
+
208
+ # Encode subcommand
209
+ encode_parser = subparsers.add_parser(
210
+ 'encode',
211
+ help='Encode video/image to compressed ASCII file',
212
+ formatter_class=argparse.RawDescriptionHelpFormatter,
213
+ epilog="""
214
+ Examples:
215
+ %(prog)s video.mp4 -o output.asc
216
+ %(prog)s image.png -o output.asc --no-color
217
+ """
218
+ )
219
+
220
+ encode_parser.add_argument(
221
+ "input",
222
+ type=str,
223
+ help="Path to input video or image file"
224
+ )
225
+
226
+ encode_parser.add_argument(
227
+ "-o", "--output",
228
+ type=str,
229
+ default="ascii_out.asc",
230
+ help="Output file path"
231
+ )
232
+
233
+ add_common_args(encode_parser)
234
+
235
+ encode_parser.set_defaults(func=cmd_encode)
236
+
237
+ website_parser = subparsers.add_parser(
238
+ 'website',
239
+ help='Open image demo website or website to display ASCII shaded video',
240
+ formatter_class=argparse.RawDescriptionHelpFormatter,
241
+ epilog="""
242
+ Usage:
243
+ %(prog)s
244
+ %(prog)s video.mp4
245
+ %(prog)s ascii_video.asc.mp4
246
+ """
247
+ )
248
+
249
+ website_parser.add_argument(
250
+ "input",
251
+ nargs='?',
252
+ type=str,
253
+ default=None,
254
+ help='Path to video to display on website (optional)'
255
+ )
256
+
257
+ website_parser.add_argument(
258
+ "-o", "--output",
259
+ type=str,
260
+ default="",
261
+ help="Output file path"
262
+ )
263
+
264
+ website_parser.add_argument(
265
+ "-p", "--port",
266
+ type=int,
267
+ default=5000,
268
+ help="Port to run website on"
269
+ )
270
+
271
+ website_parser.add_argument(
272
+ "--no-color",
273
+ action="store_true",
274
+ help="Disable color output"
275
+ )
276
+
277
+ website_parser.add_argument(
278
+ "--no-audio",
279
+ action="store_true",
280
+ help="Disable audio playback for videos"
281
+ )
282
+
283
+ website_parser.set_defaults(func=cmd_website)
284
+
285
+ args = parser.parse_args()
286
+
287
+ # Show help if no command specified
288
+ if not args.command:
289
+ parser.print_help()
290
+ sys.exit(1)
291
+
292
+ # Call correct sub-command
293
+ args.func(args)
294
+
295
+ if __name__ == "__main__":
296
+ main()
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: PDASC
3
+ Version: 0.1.0
4
+ Summary: Made for Intersession 2026
5
+ Author-email: Colin Politi <urboycolinthepanda@gmail.com>
6
+ Requires-Python: >=3.10
7
+ License-File: LICENSE
8
+ Requires-Dist: numpy>=1.20.0
9
+ Requires-Dist: Pillow>=9.0.0
10
+ Requires-Dist: numba>=0.56.0
11
+ Requires-Dist: pyaudio>=0.2.11
12
+ Requires-Dist: opencv-python>=4.5.0
13
+ Requires-Dist: zstandard>=0.18.0
14
+ Requires-Dist: flask>=2.0.0
15
+ Requires-Dist: moderngl>=5.6.0
16
+ Dynamic: license-file
@@ -0,0 +1,26 @@
1
+ core/__init__.py,sha256=YuZURZG4sVzajPjDD4infnkBunphk2n1vOPEbrhGa3M,785
2
+ core/ascii_converter.py,sha256=Jh4Rl-40jckmcoSixpEEs7NuQplL60LYqjSPkfEck_0,3969
3
+ core/ascii_displayer.py,sha256=Dp1JmCFJW0BfY-8MqeGifqMEvLH7Xw703p5ImE54cpA,11891
4
+ core/ascii_file_encoding.py,sha256=qHrlZwGV058hjbVBaPmVAeL-_dIKe88x3PveVod_5H0,9695
5
+ core/audio_player.py,sha256=a0IQBft4h2fGMTVRm8EapZJhL3lVMW3hn8lj2N0obB8,5594
6
+ core/generate_color_ramp.py,sha256=UBnrOmjG0G8mFXVWehG1wKfIM8dDllnBuwopia9DZho,2336
7
+ core/utils.py,sha256=aL3lmt7uX6NZB6EqqNLt4SiJfBFlfhhqXwKuJnhuQTs,868
8
+ core/video_ascii_video.py,sha256=8EIaBc96oT0FYMKSIRsKlreSPVRq2hkRwrRkAREdsGs,7490
9
+ core/video_extractor.py,sha256=Gqyrj53qbHeRR7WDD70qvia2OowCbmxq8LIMwaox3Pg,4206
10
+ pdasc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ pdasc/main.py,sha256=hoeR6oHvddnAjsK1pCLFk6mGWCTzu29Nml5rAl3Krn8,9937
12
+ pdasc/fonts/CascadiaMono.ttf,sha256=CAJvYX7qMVyspZiYeE6Zn2zbLQw00gQN7rBCvNw1n6I,714804
13
+ pdasc/fonts/font8x8.ttf,sha256=YQF0gD089-zxbgsLg_hklnec42cSvjfsOOehDZVSuRg,66484
14
+ pdasc-0.1.0.dist-info/licenses/LICENSE,sha256=1ss2MifclFKIfzZXjfcFh-LuQuHvNLhKG8fAnKWy7SM,1090
15
+ web/__init__.py,sha256=4-_vwOO6HfcyeKPs4oU2lACf8lWZb3GijVu-wM1l4Qc,140
16
+ web/image_controller/__init__.py,sha256=Q72s3s3C_5iYRHScILKs7DD2dBw1_E2NFOt4ab51poc,28
17
+ web/image_controller/app.py,sha256=V2DFsvroQ4_aT8mdmhSWCjnM_kGu6fxfNwE-dwupOQY,3914
18
+ web/image_controller/templates/index.html,sha256=2pjUlsN9jfNOxubnApiYnSj1IYl4uMqqIBgr9NUNYpA,10669
19
+ web/video_player/__init__.py,sha256=g3L-NXzSsLTh3w40QSzU5U4rqxVmZgI0cAmiyiU4czQ,36
20
+ web/video_player/app.py,sha256=K8I87xLMlRAOGSaSa7DU8SbjCFZpX27aMOnf5UchADE,1296
21
+ web/video_player/templates/index.html,sha256=f6qpVInwXAushhHwZIwLunJgTa1ixSu1LoNFKkqzMkk,8208
22
+ pdasc-0.1.0.dist-info/METADATA,sha256=n-AFMuMVOurWh1Kw9x_YODeTymL-UH0Q77FTlmctOFU,473
23
+ pdasc-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
24
+ pdasc-0.1.0.dist-info/entry_points.txt,sha256=DjIudun90vxvj4UvWXM07l07CvevQrPHn9LGwRyTjHQ,42
25
+ pdasc-0.1.0.dist-info/top_level.txt,sha256=swqiWpl30iX2Jy2LDoWFkBD7mIHD_cPgzOVd5mT833A,15
26
+ pdasc-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pdasc = pdasc.main:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ColinThePanda
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,3 @@
1
+ core
2
+ pdasc
3
+ web
web/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .video_player import create_video_server
2
+ from .image_controller import ImageServer
3
+
4
+ __all__ = ["create_video_server", "ImageServer"]
@@ -0,0 +1 @@
1
+ from .app import ImageServer
@@ -0,0 +1,99 @@
1
+ import sys
2
+ import os
3
+ # add root to path to import from core
4
+ sys.path.insert(0, os.path.join(__file__, "../../../"))
5
+
6
+ from flask import Flask, render_template, request
7
+ from PIL import Image
8
+ from core import AsciiConverter, AsciiDisplayer
9
+ from typing import Any
10
+
11
+ class ImageServer:
12
+ def __init__(self, font_path: str | None = None) -> None:
13
+ self.app = Flask(__name__)
14
+ self._setup_app()
15
+ self.converter = AsciiConverter(font_path=font_path) if font_path else AsciiConverter()
16
+ self.displayer = AsciiDisplayer(self.converter)
17
+ self.colored = True
18
+
19
+ def _html_from_ansi(self, ansi: str) -> str:
20
+ if not self.colored:
21
+ # Escape HTML characters and replace newlines with <br> for plain ASCII
22
+ import html
23
+ escaped = html.escape(ansi)
24
+ return f'<span>{escaped.replace("\n", "<br>")}</span>'
25
+
26
+ import re
27
+
28
+ html_parts = []
29
+ current_color = None
30
+
31
+ # Pattern to match ANSI escape codes
32
+ ansi_pattern = re.compile(r'\x1b\[([0-9;]+)m')
33
+
34
+ last_end = 0
35
+ for match in ansi_pattern.finditer(ansi):
36
+ # Add any text before this escape code
37
+ text = ansi[last_end:match.start()]
38
+ if text:
39
+ if current_color:
40
+ html_parts.append(f'<span style="color:{current_color}">{text}</span>')
41
+ else:
42
+ html_parts.append(text)
43
+
44
+ # Parse the escape code
45
+ codes = match.group(1).split(';')
46
+ i = 0
47
+ while i < len(codes):
48
+ code = codes[i]
49
+
50
+ if code == '0' or code == '': # Reset
51
+ current_color = None
52
+ elif code == '38' and i + 2 < len(codes) and codes[i + 1] == '2': # RGB foreground
53
+ r, g, b = codes[i + 2], codes[i + 3], codes[i + 4]
54
+ current_color = f'rgb({r},{g},{b})'
55
+ i += 4
56
+
57
+ i += 1
58
+
59
+ last_end = match.end()
60
+
61
+ # Add any remaining text
62
+ text = ansi[last_end:]
63
+ if text:
64
+ if current_color:
65
+ html_parts.append(f'<span style="color:{current_color}">{text}</span>')
66
+ else:
67
+ html_parts.append(text)
68
+
69
+ return ''.join(html_parts).replace('\n', '<br>')
70
+
71
+ def _setup_app(self):
72
+ self.app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
73
+ @self.app.route('/')
74
+ def index():
75
+ return render_template(
76
+ "index.html"
77
+ )
78
+
79
+ @self.app.route('/ascii', methods=['POST'])
80
+ def ascii():
81
+ num_ascii = request.form.get('num_ascii')
82
+ block_size = request.form.get('block_size')
83
+ colored = request.form.get('colored')
84
+ image_file = request.files.get('image') # This is base64 encoded
85
+
86
+ if not num_ascii or not block_size or not colored or not image_file:
87
+ return "Error one of the inputs is not defined"
88
+
89
+ self.converter.num_ascii = int(num_ascii)
90
+ self.converter.chunk_size = int(block_size)
91
+ self.colored = bool(int(colored))
92
+ image = Image.open(image_file.stream)
93
+ self.converter.regen_charmap()
94
+ ascii = self.converter.get_ascii(image, self.colored)
95
+ frame = self.displayer.render_ascii(ascii, self.colored)
96
+ return self._html_from_ansi(frame)
97
+
98
+ def run(self, host: str | None = None, port: int | None = None, debug: bool | None = None, load_dotenv: bool = True, **options: Any):
99
+ self.app.run(host=host, port=port, debug=debug, load_dotenv=load_dotenv, **options)