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.
- core/__init__.py +10 -0
- core/ascii_converter.py +111 -0
- core/ascii_displayer.py +317 -0
- core/ascii_file_encoding.py +258 -0
- core/audio_player.py +150 -0
- core/generate_color_ramp.py +69 -0
- core/utils.py +26 -0
- core/video_ascii_video.py +220 -0
- core/video_extractor.py +128 -0
- pdasc/__init__.py +0 -0
- pdasc/fonts/CascadiaMono.ttf +0 -0
- pdasc/fonts/font8x8.ttf +0 -0
- pdasc/main.py +296 -0
- pdasc-0.1.0.dist-info/METADATA +16 -0
- pdasc-0.1.0.dist-info/RECORD +26 -0
- pdasc-0.1.0.dist-info/WHEEL +5 -0
- pdasc-0.1.0.dist-info/entry_points.txt +2 -0
- pdasc-0.1.0.dist-info/licenses/LICENSE +21 -0
- pdasc-0.1.0.dist-info/top_level.txt +3 -0
- web/__init__.py +4 -0
- web/image_controller/__init__.py +1 -0
- web/image_controller/app.py +99 -0
- web/image_controller/templates/index.html +383 -0
- web/video_player/__init__.py +1 -0
- web/video_player/app.py +47 -0
- web/video_player/templates/index.html +296 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>ASCII Art Converter</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
|
16
|
+
background: #0a0a0a;
|
|
17
|
+
color: #e0e0e0;
|
|
18
|
+
display: flex;
|
|
19
|
+
height: 100vh;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
font-size: 16px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.sidebar {
|
|
25
|
+
width: 320px;
|
|
26
|
+
background: #1a1a1a;
|
|
27
|
+
padding: 40px;
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: 32px;
|
|
31
|
+
box-shadow: 2px 0 20px rgba(0, 0, 0, 0.5);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
h1 {
|
|
35
|
+
font-size: 28px;
|
|
36
|
+
color: #e0e0e0;
|
|
37
|
+
margin-bottom: 10px;
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.upload-section {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
gap: 12px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.upload-btn {
|
|
48
|
+
background: #2a2a2a;
|
|
49
|
+
color: #e0e0e0;
|
|
50
|
+
padding: 16px 24px;
|
|
51
|
+
border: 1px solid #3a3a3a;
|
|
52
|
+
border-radius: 8px;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
font-size: 16px;
|
|
55
|
+
font-weight: 500;
|
|
56
|
+
transition: all 0.3s;
|
|
57
|
+
text-align: center;
|
|
58
|
+
text-justify: center;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.upload-btn:hover {
|
|
62
|
+
background: #333;
|
|
63
|
+
border-color: #4a4a4a;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
input[type="file"] {
|
|
67
|
+
display: none;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.file-name {
|
|
71
|
+
font-size: 14px;
|
|
72
|
+
color: #888;
|
|
73
|
+
font-style: italic;
|
|
74
|
+
min-height: 20px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.control-group {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: 12px;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
label {
|
|
84
|
+
font-size: 16px;
|
|
85
|
+
font-weight: 500;
|
|
86
|
+
color: #e0e0e0;
|
|
87
|
+
display: flex;
|
|
88
|
+
justify-content: space-between;
|
|
89
|
+
align-items: center;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.value-display {
|
|
93
|
+
color: #7cb3e9;
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
font-size: 16px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
input[type="range"] {
|
|
99
|
+
width: 100%;
|
|
100
|
+
height: 8px;
|
|
101
|
+
background: #2a2a2a;
|
|
102
|
+
outline: none;
|
|
103
|
+
border-radius: 4px;
|
|
104
|
+
-webkit-appearance: none;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
input[type="range"]::-webkit-slider-thumb {
|
|
108
|
+
-webkit-appearance: none;
|
|
109
|
+
appearance: none;
|
|
110
|
+
width: 20px;
|
|
111
|
+
height: 20px;
|
|
112
|
+
background: #7cb3e9;
|
|
113
|
+
cursor: pointer;
|
|
114
|
+
border-radius: 50%;
|
|
115
|
+
transition: background 0.3s;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
input[type="range"]::-webkit-slider-thumb:hover {
|
|
119
|
+
background: #5a9bd5;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
input[type="range"]::-moz-range-thumb {
|
|
123
|
+
width: 20px;
|
|
124
|
+
height: 20px;
|
|
125
|
+
background: #7cb3e9;
|
|
126
|
+
cursor: pointer;
|
|
127
|
+
border-radius: 50%;
|
|
128
|
+
border: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.checkbox-container {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: 12px;
|
|
135
|
+
cursor: pointer;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.checkbox-container span {
|
|
139
|
+
order: 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
input[type="checkbox"] {
|
|
143
|
+
order: 2;
|
|
144
|
+
width: 22px;
|
|
145
|
+
height: 22px;
|
|
146
|
+
cursor: pointer;
|
|
147
|
+
accent-color: #7cb3e9;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.main-content {
|
|
151
|
+
flex: 1;
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
justify-content: center;
|
|
155
|
+
padding: 50px;
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
background: #0f0f0f;
|
|
158
|
+
position: relative;
|
|
159
|
+
transition: background 0.3s;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.main-content.drag-over {
|
|
163
|
+
background: #1a1a1a;
|
|
164
|
+
border: 3px dashed #7cb3e9;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#ascii-output {
|
|
168
|
+
line-height: 1;
|
|
169
|
+
font-family: "Courier New", monospace;
|
|
170
|
+
white-space: pre;
|
|
171
|
+
text-align: center;
|
|
172
|
+
transform-origin: center center;
|
|
173
|
+
visibility: hidden;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
#ascii-output.visible {
|
|
177
|
+
visibility: visible;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.placeholder {
|
|
181
|
+
color: #4a4a4a;
|
|
182
|
+
font-size: 20px;
|
|
183
|
+
text-align: center;
|
|
184
|
+
}
|
|
185
|
+
</style>
|
|
186
|
+
</head>
|
|
187
|
+
<body>
|
|
188
|
+
<div class="sidebar">
|
|
189
|
+
<div>
|
|
190
|
+
<h1>ASCII Art Converter</h1>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="upload-section">
|
|
194
|
+
<label for="file-input" class="upload-btn"> Upload Image </label>
|
|
195
|
+
<input type="file" id="file-input" accept="image/*" />
|
|
196
|
+
<div class="file-name" id="file-name"></div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div class="control-group">
|
|
200
|
+
<label>
|
|
201
|
+
ASCII Characters
|
|
202
|
+
<span class="value-display" id="num-ascii-value">8</span>
|
|
203
|
+
</label>
|
|
204
|
+
<input type="range" id="num-ascii" min="2" max="64" value="8" />
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div class="control-group">
|
|
208
|
+
<label>
|
|
209
|
+
Block Size
|
|
210
|
+
<span class="value-display" id="block-size-value">8</span>
|
|
211
|
+
</label>
|
|
212
|
+
<input type="range" id="block-size" min="2" max="32" value="8" />
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div class="control-group">
|
|
216
|
+
<label class="checkbox-container">
|
|
217
|
+
<span>Colored</span>
|
|
218
|
+
<input type="checkbox" id="colored" checked />
|
|
219
|
+
</label>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<button class="upload-btn" id="download-btn" style="display: none">
|
|
223
|
+
Download ASCII
|
|
224
|
+
</button>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div class="main-content" id="drop-zone">
|
|
228
|
+
<div id="ascii-output" class="placeholder">
|
|
229
|
+
Upload an image to get started
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<script>
|
|
234
|
+
let uploadedImage = null;
|
|
235
|
+
|
|
236
|
+
const fileInput = document.getElementById("file-input");
|
|
237
|
+
const fileName = document.getElementById("file-name");
|
|
238
|
+
const numAsciiSlider = document.getElementById("num-ascii");
|
|
239
|
+
const numAsciiValue = document.getElementById("num-ascii-value");
|
|
240
|
+
const blockSizeSlider = document.getElementById("block-size");
|
|
241
|
+
const blockSizeValue = document.getElementById("block-size-value");
|
|
242
|
+
const coloredCheckbox = document.getElementById("colored");
|
|
243
|
+
const asciiOutput = document.getElementById("ascii-output");
|
|
244
|
+
const dropZone = document.getElementById("drop-zone");
|
|
245
|
+
const downloadBtn = document.getElementById("download-btn");
|
|
246
|
+
|
|
247
|
+
// Drag and drop handlers
|
|
248
|
+
dropZone.addEventListener("dragover", (e) => {
|
|
249
|
+
e.preventDefault();
|
|
250
|
+
dropZone.classList.add("drag-over");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
dropZone.addEventListener("dragleave", (e) => {
|
|
254
|
+
e.preventDefault();
|
|
255
|
+
dropZone.classList.remove("drag-over");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
dropZone.addEventListener("drop", (e) => {
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
dropZone.classList.remove("drag-over");
|
|
261
|
+
|
|
262
|
+
const file = e.dataTransfer.files[0];
|
|
263
|
+
if (file && file.type.startsWith("image/")) {
|
|
264
|
+
fileName.textContent = file.name;
|
|
265
|
+
uploadedImage = file;
|
|
266
|
+
generateAscii();
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Update slider value displays
|
|
271
|
+
numAsciiSlider.addEventListener("input", (e) => {
|
|
272
|
+
numAsciiValue.textContent = e.target.value;
|
|
273
|
+
if (uploadedImage) generateAscii();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
blockSizeSlider.addEventListener("input", (e) => {
|
|
277
|
+
blockSizeValue.textContent = e.target.value;
|
|
278
|
+
if (uploadedImage) generateAscii();
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
coloredCheckbox.addEventListener("change", () => {
|
|
282
|
+
if (uploadedImage) generateAscii();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// Handle file upload
|
|
286
|
+
fileInput.addEventListener("change", (e) => {
|
|
287
|
+
const file = e.target.files[0];
|
|
288
|
+
if (file) {
|
|
289
|
+
fileName.textContent = file.name;
|
|
290
|
+
uploadedImage = file;
|
|
291
|
+
generateAscii();
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Download ASCII art
|
|
296
|
+
downloadBtn.addEventListener("click", () => {
|
|
297
|
+
const asciiText = asciiOutput.innerText;
|
|
298
|
+
const blob = new Blob([asciiText], { type: "text/plain" });
|
|
299
|
+
const url = URL.createObjectURL(blob);
|
|
300
|
+
const a = document.createElement("a");
|
|
301
|
+
a.href = url;
|
|
302
|
+
a.download = "ascii-art.txt";
|
|
303
|
+
document.body.appendChild(a);
|
|
304
|
+
a.click();
|
|
305
|
+
document.body.removeChild(a);
|
|
306
|
+
URL.revokeObjectURL(url);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
// Scale ASCII to fit container
|
|
310
|
+
function scaleAsciiToFit() {
|
|
311
|
+
const container = document.querySelector(".main-content");
|
|
312
|
+
const output = asciiOutput;
|
|
313
|
+
|
|
314
|
+
if (output.classList.contains("placeholder")) return;
|
|
315
|
+
|
|
316
|
+
output.classList.remove("visible");
|
|
317
|
+
|
|
318
|
+
output.style.transform = "scale(1)";
|
|
319
|
+
output.style.fontSize = "10px";
|
|
320
|
+
void output.offsetHeight;
|
|
321
|
+
|
|
322
|
+
const containerWidth = container.clientWidth - 100;
|
|
323
|
+
const containerHeight = container.clientHeight - 100;
|
|
324
|
+
const outputWidth = output.scrollWidth;
|
|
325
|
+
const outputHeight = output.scrollHeight;
|
|
326
|
+
|
|
327
|
+
const scaleX = containerWidth / outputWidth;
|
|
328
|
+
const scaleY = containerHeight / outputHeight;
|
|
329
|
+
|
|
330
|
+
const scale = Math.min(scaleX, scaleY, 3);
|
|
331
|
+
|
|
332
|
+
output.style.transform = `scale(${scale})`;
|
|
333
|
+
output.classList.add("visible");
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Generate ASCII art
|
|
337
|
+
async function generateAscii() {
|
|
338
|
+
if (!uploadedImage) return;
|
|
339
|
+
|
|
340
|
+
// Hide output immediately
|
|
341
|
+
asciiOutput.classList.remove("visible");
|
|
342
|
+
|
|
343
|
+
const formData = new FormData();
|
|
344
|
+
formData.append("num_ascii", numAsciiSlider.value);
|
|
345
|
+
formData.append("block_size", blockSizeSlider.value);
|
|
346
|
+
formData.append("colored", coloredCheckbox.checked ? "1" : "0");
|
|
347
|
+
formData.append("image", uploadedImage);
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const response = await fetch("/ascii", {
|
|
351
|
+
method: "POST",
|
|
352
|
+
body: formData,
|
|
353
|
+
});
|
|
354
|
+
const html = await response.text();
|
|
355
|
+
asciiOutput.innerHTML = html;
|
|
356
|
+
asciiOutput.classList.remove("placeholder");
|
|
357
|
+
|
|
358
|
+
// Show download button
|
|
359
|
+
downloadBtn.style.display = "block";
|
|
360
|
+
|
|
361
|
+
// Force immediate scale calculation before showing
|
|
362
|
+
requestAnimationFrame(() => {
|
|
363
|
+
requestAnimationFrame(() => {
|
|
364
|
+
scaleAsciiToFit();
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error("Error generating ASCII:", error);
|
|
369
|
+
asciiOutput.innerHTML =
|
|
370
|
+
'<span style="color: #ff6b6b;">Error generating ASCII art</span>';
|
|
371
|
+
asciiOutput.classList.add("visible");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Rescale on window resize
|
|
376
|
+
window.addEventListener("resize", () => {
|
|
377
|
+
if (uploadedImage && !asciiOutput.classList.contains("placeholder")) {
|
|
378
|
+
scaleAsciiToFit();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
</script>
|
|
382
|
+
</body>
|
|
383
|
+
</html>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .app import create_video_server
|
web/video_player/app.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from flask import Flask, render_template, send_file
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def create_video_server(video_path, title="ASCII Video Player", autoplay=False):
|
|
6
|
+
"""
|
|
7
|
+
Create a Flask app that serves a video file.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
video_path: Path to video file (str or Path)
|
|
11
|
+
title: Page title (default: "ASCII Video Player")
|
|
12
|
+
autoplay: Whether to autoplay video (default: False)
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Flask app instance
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
app = create_video_server("output.mp4")
|
|
19
|
+
app.run(host='0.0.0.0', port=5000)
|
|
20
|
+
"""
|
|
21
|
+
video_path = Path(video_path).resolve()
|
|
22
|
+
|
|
23
|
+
if not video_path.exists():
|
|
24
|
+
raise FileNotFoundError(f"Video file not found: {video_path}")
|
|
25
|
+
|
|
26
|
+
app = Flask(__name__)
|
|
27
|
+
|
|
28
|
+
@app.route('/')
|
|
29
|
+
def index():
|
|
30
|
+
"""Serve the video player page"""
|
|
31
|
+
return render_template(
|
|
32
|
+
"index.html",
|
|
33
|
+
title=title,
|
|
34
|
+
autoplay=autoplay
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@app.route('/video')
|
|
38
|
+
def video():
|
|
39
|
+
"""Serve the video file"""
|
|
40
|
+
return send_file(
|
|
41
|
+
video_path,
|
|
42
|
+
mimetype='video/mp4',
|
|
43
|
+
as_attachment=False,
|
|
44
|
+
conditional=True # Enable range requests for seeking
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return app
|