Skip to content

Commit 42004ef

Browse files
committed
v1.1.0 released
1 parent dd34dc1 commit 42004ef

File tree

5 files changed

+131
-56
lines changed

5 files changed

+131
-56
lines changed

.github/workflows/build-and-release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
pip install pyinstaller
2828
2929
- name: Build Executable
30-
run: pyinstaller --onefile --noconfirm --add-data "BadApple.mp4:." --add-data "ffmpeg_bin.7z:." vidminal.py
30+
run: pyinstaller --onefile --noconfirm --add-data "BadApple.mp4:." --add-data "linux.7z:." vidminal.py
3131

3232
- name: Rename Executable
3333
run: mv dist/vidminal dist/vidminal-linux
@@ -56,7 +56,7 @@ jobs:
5656
pip install pyinstaller
5757
5858
- name: Build Executable
59-
run: pyinstaller --onefile --noconfirm --add-data "BadApple.mp4;." --add-data "ffmpeg_bin.7z;." vidminal.py
59+
run: pyinstaller --onefile --noconfirm --add-data "BadApple.mp4;." --add-data "windows.7z;." vidminal.py
6060

6161
- name: Rename Executable
6262
run: Rename-Item -Path dist\vidminal.exe -NewName vidminal-windows.exe
@@ -85,7 +85,7 @@ jobs:
8585
pip install pyinstaller
8686
8787
- name: Build Executable
88-
run: pyinstaller --onefile --noconfirm --add-data "BadApple.mp4:." --add-data "ffmpeg_bin.7z:." vidminal.py
88+
run: pyinstaller --onefile --noconfirm --add-data "BadApple.mp4:." --add-data "mac.7z:." vidminal.py
8989

9090
- name: Rename Executable
9191
run: mv dist/vidminal dist/vidminal-macos
78.4 MB
Binary file not shown.

mac.7z

9.7 MB
Binary file not shown.

vidminal.py

Lines changed: 128 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
import os
22
import sys
3-
import time
4-
from PIL import Image
5-
import threading
6-
import platform
7-
import pygame
8-
from moviepy import VideoFileClip
3+
import warnings
4+
warnings.filterwarnings('ignore')
5+
os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1'
96
import contextlib
107
import io
11-
import py7zr
12-
import queue
13-
import colorama
14-
import multiprocessing
8+
# Suppress stderr globally (for moviepy, pygame, etc.)
9+
class SuppressStderr:
10+
def __enter__(self):
11+
self._stderr = sys.stderr
12+
sys.stderr = open(os.devnull, 'w')
13+
def __exit__(self, exc_type, exc_val, exc_tb):
14+
sys.stderr.close()
15+
sys.stderr = self._stderr
16+
17+
with SuppressStderr():
18+
import time
19+
from PIL import Image
20+
import threading
21+
import platform
22+
import pygame
23+
from moviepy import VideoFileClip
24+
import py7zr
25+
import queue
26+
import colorama
27+
import multiprocessing
28+
import json
1529

1630
# finds stuff, works with PyInstaller too (hopefully)
1731
def find_resource_path(rel):
@@ -23,42 +37,45 @@ def find_resource_path(rel):
2337

2438
# pulls ffmpeg from zip, dumps in root, sets env, done
2539
def extract_and_set_ffmpeg_bin():
26-
zip_path = find_resource_path('ffmpeg_bin.7z')
2740
sysname = platform.system().lower()
2841
arch = platform.machine().lower()
2942
if sysname == 'windows':
30-
ffmpeg_in_zip = 'windows/ffmpeg.exe'
43+
zip_path = find_resource_path('windows.7z')
44+
ffmpeg_in_zip = 'ffmpeg.exe'
3145
out_name = 'ffmpeg.exe'
3246
elif sysname == 'darwin':
33-
ffmpeg_in_zip = 'mac/ffmpeg'
47+
zip_path = find_resource_path('mac.7z')
48+
ffmpeg_in_zip = 'ffmpeg'
3449
out_name = 'ffmpeg'
3550
elif sysname == 'linux':
51+
zip_path = find_resource_path('linux.7z')
3652
if 'arm' in arch:
3753
if '64' in arch:
38-
ffmpeg_in_zip = 'linux/linux-arm-64/ffmpeg'
54+
ffmpeg_in_zip = 'linux-arm-64/ffmpeg'
3955
else:
40-
ffmpeg_in_zip = 'linux/linux-armhf-32/ffmpeg'
56+
ffmpeg_in_zip = 'linux-armhf-32/ffmpeg'
4157
elif '64' in arch:
42-
ffmpeg_in_zip = 'linux/linux-64/ffmpeg'
58+
ffmpeg_in_zip = 'linux-64/ffmpeg'
4359
else:
44-
ffmpeg_in_zip = 'linux/linux-32/ffmpeg'
60+
ffmpeg_in_zip = 'linux-32/ffmpeg'
4561
out_name = 'ffmpeg'
4662
else:
63+
zip_path = None
4764
ffmpeg_in_zip = None
4865
out_name = 'ffmpeg'
49-
if ffmpeg_in_zip:
66+
if zip_path and ffmpeg_in_zip:
5067
out_path = os.path.abspath(out_name)
5168
if not os.path.exists(out_path):
5269
with py7zr.SevenZipFile(zip_path, 'r') as archive:
5370
archive.extract(targets=[ffmpeg_in_zip], path='.')
5471
if sysname != 'windows':
55-
os.chmod(out_path, 0o755) # make executable, I guess
72+
os.chmod(out_path, 0o755) # make executable
5673
os.environ['FFMPEG_BINARY'] = out_path
5774
else:
5875
os.environ['FFMPEG_BINARY'] = 'ffmpeg' # fallback, yolo
5976

6077
# turns video into frames & audio, dumps in folder
61-
def get_stuff_from_video(vid, out, speed=24, wide=80):
78+
def get_stuff_from_video(vid, out, speed=24, wide=160):
6279
if not os.path.exists(out):
6380
os.makedirs(out)
6481
audio = os.path.join(out, 'audio.ogg')
@@ -87,18 +104,60 @@ def get_stuff_from_video(vid, out, speed=24, wide=80):
87104
print('Done.')
88105
return out, audio
89106

90-
def pic_to_ascii_from_pil(pic, wide=80):
91-
# High-fidelity ASCII ramp using only block and shading characters for smooth gradients
92-
chars = "█▓▒░▌▖ " # 7 levels, all block/shading for best depth
93-
gamma = 0.8
94-
contrast = 1.2
107+
def load_options(options_path='options.json'):
108+
# Load options from options.json, fallback to defaults if missing/empty
109+
defaults = {
110+
'chars': "█▓▒░",
111+
'gamma': 1.2,
112+
'contrast': 1.5,
113+
'temp': 'temp',
114+
'wide': 160,
115+
'fps': 24
116+
}
117+
if not os.path.exists(options_path):
118+
with open(options_path, 'w', encoding='utf-8') as f:
119+
json.dump(defaults, f, indent=2)
120+
return defaults
121+
try:
122+
with open(options_path, 'r', encoding='utf-8') as f:
123+
opts = json.load(f)
124+
for k in defaults:
125+
if k not in opts or opts[k] == '' or opts[k] is None:
126+
opts[k] = defaults[k]
127+
return opts
128+
except Exception:
129+
return defaults
130+
131+
def pic_to_ascii_from_pil(pic, wide=None, high=None):
132+
import shutil
95133
from colorama import Style
96-
gray = pic.convert('L')
97-
color = pic.convert('RGB')
98-
ratio = gray.height / gray.width
134+
opts = load_options()
135+
chars = opts['chars']
136+
gamma = float(opts['gamma'])
137+
contrast = float(opts['contrast'])
138+
# Dynamically determine width and height as 90% of terminal size if not provided
139+
if wide is None or high is None:
140+
try:
141+
size = shutil.get_terminal_size()
142+
term_w = int(size.columns * 0.9)
143+
term_h = int(size.lines * 0.9)
144+
if wide is None:
145+
wide = max(20, term_w)
146+
if high is None:
147+
high = max(10, term_h)
148+
except Exception:
149+
if wide is None:
150+
wide = 160
151+
if high is None:
152+
high = 24
153+
# Maintain aspect ratio
154+
ratio = pic.height / pic.width
99155
tall = int(ratio * wide * 0.55)
100-
gray = gray.resize((wide, tall))
101-
color = color.resize((wide, tall))
156+
if tall > high:
157+
tall = high
158+
wide = int(tall / (ratio * 0.55))
159+
gray = pic.convert('L').resize((wide, tall))
160+
color = pic.convert('RGB').resize((wide, tall))
102161
px = list(gray.getdata())
103162
px_color = list(color.getdata())
104163
out = ''
@@ -116,10 +175,10 @@ def pic_to_ascii_from_pil(pic, wide=80):
116175
out += Style.RESET_ALL
117176
return out
118177

119-
def pic_to_ascii(img, wide=80):
178+
def pic_to_ascii(img, wide=None, high=None):
120179
from PIL import Image
121180
pic = Image.open(img)
122-
return pic_to_ascii_from_pil(pic, wide)
181+
return pic_to_ascii_from_pil(pic, wide, high)
123182

124183
# Multiprocessing helper for frame conversion
125184

@@ -177,7 +236,7 @@ def clear_terminal():
177236
os.system('cls' if platform.system() == 'Windows' else 'clear')
178237

179238
# plays ascii frames + sound, handles pause/quit
180-
def play_ascii_video_stream(folder, audio, speed=24, wide=80, buffer_size=24):
239+
def play_ascii_video_stream(folder, audio, speed=24, wide=160, buffer_size=24):
181240
pygame.mixer.init()
182241
delay = 1.0 / speed
183242
frames = sorted([f for f in os.listdir(folder) if f.startswith('frame_') and f.endswith('.txt')])
@@ -244,6 +303,7 @@ def get_stuff_from_video_stream(vid, out, speed=24, buffer_size=24):
244303
clip.audio.write_audiofile(audio, codec='libvorbis')
245304
frame_queue = queue.Queue(maxsize=buffer_size*2)
246305
total_frames = int(clip.fps * clip.duration)
306+
video_duration = clip.duration # <-- add this
247307
def extract_frames():
248308
for i, frame in enumerate(clip.iter_frames(fps=speed, dtype='uint8')):
249309
frame_path = os.path.join(out, f'frame_{i+1:05d}.png')
@@ -254,10 +314,10 @@ def extract_frames():
254314
pass # Don't block extraction, just skip putting in queue
255315
frame_queue.put(None) # Sentinel for end
256316
threading.Thread(target=extract_frames, daemon=True).start()
257-
return out, audio, frame_queue, total_frames
317+
return out, audio, frame_queue, total_frames, video_duration
258318

259319
# plays ascii video + audio from stream, handles pause/quit
260-
def play_ascii_video_stream_streaming(folder, audio, frame_queue, total_frames, speed=24, wide=80, buffer_size=24):
320+
def play_ascii_video_stream_streaming(folder, audio, frame_queue, total_frames, speed=24, wide=160, buffer_size=24, video_duration=None):
261321
import queue as pyqueue
262322
pygame.mixer.init()
263323
delay = 1.0 / speed
@@ -327,7 +387,8 @@ def format_time(t):
327387
if frame_path is None:
328388
break
329389
frames_buffer.append(frame_path)
330-
total_time = total_frames / speed if total_frames > 0 else 0
390+
# Use actual video duration if provided
391+
total_time = video_duration if video_duration is not None else (total_frames / speed if total_frames > 0 else 0)
331392
while not stop_flag.is_set() and frames_buffer:
332393
# Handle rewind/forward requests
333394
jump = 0
@@ -414,26 +475,40 @@ def pic_from_ascii_txt(txt_path):
414475
# main thing, asks stuff, runs stuff
415476
def main():
416477
extract_and_set_ffmpeg_bin() # pulls ffmpeg from zip, sets env, whatever
417-
print('Turns videos into ugly terminal art. With sound.')
418-
print('Made by a lazy coder. @github/SajagIN')
419-
vid_input = input('Video file? (default: BadApple.mp4): ').strip()
478+
from colorama import Fore, Style
479+
box_width = 64
480+
box_color = Fore.CYAN + Style.BRIGHT
481+
text_color = Fore.YELLOW + Style.BRIGHT
482+
reset = Style.RESET_ALL
483+
def box_line(text, color=text_color):
484+
# Remove ANSI codes for length calculation
485+
import re
486+
ansi_escape = re.compile(r'\x1b\[[0-9;]*m')
487+
visible = ansi_escape.sub('', text)
488+
pad = box_width - 2 - len(visible)
489+
return f"{box_color}{reset}{color}{text}{' ' * pad}{reset}{box_color}{reset}"
490+
lines = [
491+
"",
492+
f"{box_color}{'═'* (box_width-2)}{reset}",
493+
box_line(" Turns videos into ugly terminal art. With sound. "),
494+
box_line(" Made by a lazy coder. " + Fore.MAGENTA + "@github/SajagIN" + text_color + " "),
495+
box_line(f" {Fore.GREEN}Space{reset}{text_color} = pause {Fore.GREEN}Q{reset}{text_color} = quit {Fore.GREEN}A/D{reset}{text_color} = rewind/forward 5s "),
496+
f"{box_color}{'═'* (box_width-2)}{reset}",
497+
""
498+
]
499+
print("\n".join(lines))
500+
opts = load_options('options.json')
501+
vid_input = input(Fore.CYAN + Style.BRIGHT + 'Video file?' + reset + f' (default: {Fore.YELLOW}BadApple.mp4{reset}): ').strip()
420502
vid = find_resource_path(vid_input) if vid_input else find_resource_path('BadApple.mp4')
421-
temp = input('Temp folder? (default: temp): ').strip() or 'temp'
422-
try:
423-
width = int(input('How wide? (default: 80): ').strip() or 80)
424-
except ValueError:
425-
width = 80
426-
try:
427-
fps = int(input('FPS? (default: 24): ').strip() or 24)
428-
except ValueError:
429-
fps = 24
430-
print('Space = pause, Q = quit A/D = rewind/forward 5s, ←/→ = skip 1s')
503+
temp = opts['temp']
504+
width = int(opts['wide'])
505+
fps = int(opts['fps'])
431506
try:
432-
frames, audio, frame_queue, total_frames = get_stuff_from_video_stream(vid, temp, speed=fps, buffer_size=fps)
433-
print('Streaming ASCII video...')
434-
play_ascii_video_stream_streaming(frames, audio, frame_queue, total_frames, speed=fps, wide=width, buffer_size=fps)
507+
frames, audio, frame_queue, total_frames, video_duration = get_stuff_from_video_stream(vid, temp, speed=fps, buffer_size=fps)
508+
print(Fore.GREEN + Style.BRIGHT + 'Streaming ASCII video...' + reset)
509+
play_ascii_video_stream_streaming(frames, audio, frame_queue, total_frames, speed=fps, wide=width, buffer_size=fps, video_duration=video_duration)
435510
except Exception as e:
436-
print(f'Nope, broke: {e}')
511+
print(Fore.RED + f'Nope, broke: {e}' + reset)
437512
sys.exit(1)
438513

439514
if __name__ == '__main__':

windows.7z

11.8 MB
Binary file not shown.

0 commit comments

Comments
 (0)