Skip to content

Commit

Permalink
Improve the video to ascii player
Browse files Browse the repository at this point in the history
  • Loading branch information
yorevs committed Jan 13, 2025
1 parent b96625d commit 783bcc7
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 33 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,4 @@ py-gradle-master/
build/
.venv/
venv
response_audio.mp3
Expand Down
Binary file added assets/images/robot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/videos/robot.mp4
Binary file not shown.
82 changes: 49 additions & 33 deletions src/demo/devel/animated_ascii.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
import sys
import threading
import time
from os.path import dirname
from os.path import dirname, expandvars
import os
from pathlib import Path
from threading import Thread
from typing import Optional

from hspylib.core.exception.exceptions import InvalidArgumentError
from hspylib.core.metaclass.classpath import AnyPath
from rich.console import Console
from rich.text import Text
import pause
Expand All @@ -30,7 +32,7 @@

DEFAULT_PALETTE = PALETTES[1]

VIDEO_DIR: Path = Path("/Users/hjunior/GIT-Repository/GitHub/askai/assets/videos")
VIDEO_DIR: Path = Path(expandvars("${HOME}/Movies"))
if not VIDEO_DIR.exists():
VIDEO_DIR.mkdir(parents=True, exist_ok=True)

Expand All @@ -39,7 +41,7 @@
DATA_PATH.mkdir(parents=True, exist_ok=True)


def frame_to_ascii(
def image_to_ascii(
frame_path: str,
width: int,
palette: str,
Expand All @@ -55,7 +57,7 @@ def frame_to_ascii(
num_chars = len(palette if not reverse else palette[::-1])
img: Image = Image.open(frame_path).convert("L")
aspect_ratio = img.height / img.width
new_height = int(width * aspect_ratio * 0.55)
new_height = int(width * aspect_ratio * 0.4)
img = img.resize((width, new_height), resample=Resampling.BILINEAR)
pixels = list(img.getdata())
ascii_str = "".join(palette[min(pixel * num_chars // 256, num_chars - 1)] for pixel in pixels)
Expand All @@ -75,18 +77,19 @@ def get_frames(frames_path: Path, width: int = 80, palette: str = DEFAULT_PALETT
ascii_frames: list[str] = []
for frame_file in sorted(os.listdir(frames_path)):
frame_path: str = os.path.join(frames_path, frame_file)
ascii_frame = frame_to_ascii(frame_path, width, palette, reverse)
ascii_frame = image_to_ascii(frame_path, width, palette, reverse)
ascii_frames.append(ascii_frame)

return ascii_frames


def extract_audio_and_video_frames(video_path: Path) -> Optional[tuple[Path, Path]]:
def extract_audio_and_video_frames(video_path: Path) -> tuple[Optional[Path], Path]:
"""Extracts audio and video frames from the given video path.
:param video_path: Path to the video file to extract audio and frames from.
:return: A tuple containing the path to the extracted audio and a list of paths to the video frames, or None if
the extraction fails.
"""
assert video_path.exists(), f"Video path does not exist: {video_path}"
video_name, _ = os.path.splitext(os.path.basename(video_path))
frame_dir: Path = Path(os.path.join(DATA_PATH, video_name, 'frames'))
audio_dir: Path = Path(os.path.join(DATA_PATH, video_name, 'audio'))
Expand All @@ -101,15 +104,15 @@ def extract_audio_and_video_frames(video_path: Path) -> Optional[tuple[Path, Pat
frame_command = f'ffmpeg -i "{video_path}" -vf "fps={FPS}" "{frame_dir}/frame%04d.png"'
_, _, exit_code = terminal.shell_exec(frame_command, shell=True)
if exit_code != ExitStatus.SUCCESS:
return None
raise InvalidArgumentError(f"Failed to extract video frames from: {video_path}")

# Extract audio
audio_command = f'ffmpeg -i "{video_path}" -q:a 0 -map a "{audio_path}"'
_, _, exit_code = terminal.shell_exec(audio_command, shell=True)
if exit_code != ExitStatus.SUCCESS:
return None
raise InvalidArgumentError(f"Failed to extract video frames from: {audio_path}")

return audio_path, frame_dir
return audio_path if audio_path.exists() else None, frame_dir


def play_ascii_frames(ascii_frames: list[str], fps: int) -> None:
Expand All @@ -119,21 +122,25 @@ def play_ascii_frames(ascii_frames: list[str], fps: int) -> None:
:return: None
:raises OSError: If unable to get terminal size.
"""
console = Console()
delay_ms: int = int(1000 / fps)
cols, _ = shutil.get_terminal_size()
for f in ascii_frames:
cols, rows = shutil.get_terminal_size()
cursor.write("%HOM%")
start_time = time.perf_counter() # Record the start time
for line in f.splitlines()[:cols]:
console.print(Text(line, justify="center"), end='')
cursor.write(f"%EL0%%EOL%")
for frame in ascii_frames:
start_time = print_ascii_image(frame)
end_time = time.perf_counter() # Record the end time
render_time: int = int((end_time - start_time) * 1000)
pause.milliseconds(delay_ms - render_time)


def print_ascii_image(image: str):
console = Console()
cursor.write("%HOM%")
cols, _ = shutil.get_terminal_size()
start_time = time.perf_counter() # Record the start time
for line in image.splitlines()[:cols]:
console.print(Text(line, justify="center"), end='')
cursor.write(f"%EL0%%EOL%")
return start_time


def play_video(ascii_frames: list[str], fps: int) -> Thread:
"""Plays a list of ASCII art frames as a video in a separate thread.
:param ascii_frames: List of ASCII art frames to display.
Expand All @@ -146,15 +153,17 @@ def play_video(ascii_frames: list[str], fps: int) -> Thread:
return thread


def play_audio(audio_path: str) -> Thread:
def play_audio(audio_path: AnyPath) -> Optional[Thread]:
"""Plays an audio file in a separate thread.
:param audio_path: Path to the audio file to be played.
:return: The thread running the audio playback.
"""
thread = threading.Thread(target=player.play_audio_file, args=(audio_path,))
thread.daemon = True
thread.start()
return thread
if os.path.exists(str(audio_path)):
thread = threading.Thread(target=player.play_audio_file, args=(str(audio_path),))
thread.daemon = True
thread.start()
return thread
return None


def setup_terminal() -> None:
Expand All @@ -179,23 +188,30 @@ def cleanup(*args) -> None:
exit()


def play(video_name: str) -> None:
def play(video_name: str, width: int = 80) -> None:
"""Plays a video in ASCII format with synchronized audio.
:param video_name: The name of the video file to play.
:param width: TODO
"""
cols, rows = shutil.get_terminal_size()
print(cols, rows, cols / rows)
exit()
# cols, rows = shutil.get_terminal_size()
# print(cols, rows, cols / rows)
# exit()
video_path: Path = Path(
os.path.join(VIDEO_DIR, video_name)
if not os.path.exists(expandvars(video_name)) else expandvars(video_name)
)
audio_path, frames_path = extract_audio_and_video_frames(video_path)
ascii_video = get_frames(frames_path, width, PALETTES[1], True)
setup_terminal()
video_path: Path = Path(os.path.join(VIDEO_DIR, video_name))
audio_path, video_path = extract_audio_and_video_frames(video_path)
ascii_video = get_frames(video_path, 150, PALETTES[1], True)
thv = play_video(ascii_video, FPS)
tha = play_audio(audio_path)
if audio_path is not None and audio_path.exists():
tha = play_audio(audio_path)
tha.join()
thv.join()
tha.join()
cleanup()


if __name__ == '__main__':
play("AskAI-Trailer.mp4")
play("${DESKTOP}/robot.mp4", 50)
# asc_img = image_to_ascii(expandvars("${DESKTOP}/robot.png"), 74, DEFAULT_PALETTE, True)
# print_ascii_image(asc_img)
Binary file removed src/demo/devel/response_audio.mp3
Binary file not shown.

0 comments on commit 783bcc7

Please sign in to comment.