Skip to content

Multithread#17

Open
Immelancholy wants to merge 16 commits intoNotenlish:mainfrom
Immelancholy:multithread
Open

Multithread#17
Immelancholy wants to merge 16 commits intoNotenlish:mainfrom
Immelancholy:multithread

Conversation

@Immelancholy
Copy link
Contributor

@Immelancholy Immelancholy commented May 13, 2025

I added the threading to close: #9

It probably could be a lot better, but it is implemented.
Here's the benchmark output info for my machine:
Threading:

ANIFETCH NOCACHE (Neofetch)
Caching...
It took 4.252040863037109 seconds.
Caching...
It took 4.289308786392212 seconds.
Caching...
It took 4.343307733535767 seconds.
Caching...
It took 4.245228052139282 seconds.
Caching...
It took 4.25253438949585 seconds.
Caching...
It took 4.265721082687378 seconds.
Caching...
It took 4.245808839797974 seconds.
Caching...
It took 4.287856340408325 seconds.
Caching...
It took 4.290659189224243 seconds.
Caching...
It took 4.221307039260864 seconds.
ANIFETCH CACHED (Neofetch)
It took 0.8123478889465332 seconds.
It took 0.8097891807556152 seconds.
It took 0.80167555809021 seconds.
It took 0.8192436695098877 seconds.
It took 0.8283965587615967 seconds.
It took 0.8217740058898926 seconds.
It took 0.814246654510498 seconds.
It took 0.8140480518341064 seconds.
It took 0.826866626739502 seconds.
It took 0.8288557529449463 seconds.
It took 0.8326723575592041 seconds.
ANIFETCH NOCACHE (Fastfetch)
Caching...
It took 3.53835129737854 seconds.
Caching...
It took 3.6349759101867676 seconds.
Caching...
It took 3.5685672760009766 seconds.
Caching...
It took 3.600893259048462 seconds.
Caching...
It took 3.5953798294067383 seconds.
Caching...
It took 3.685676097869873 seconds.
Caching...
It took 3.654515027999878 seconds.
Caching...
It took 3.6432554721832275 seconds.
Caching...
It took 3.588575601577759 seconds.
Caching...
It took 3.5591068267822266 seconds.
ANIFETCH CACHED (Fastfetch)
It took 0.11572027206420898 seconds.
It took 0.1129767894744873 seconds.
It took 0.10892343521118164 seconds.
It took 0.11934375762939453 seconds.
It took 0.11336588859558105 seconds.
It took 0.10922598838806152 seconds.
It took 0.11066150665283203 seconds.
It took 0.11399245262145996 seconds.
It took 0.11400175094604492 seconds.
It took 0.11601495742797852 seconds.
It took 0.11469292640686035 seconds.
Neofetch
9.228115320205688
Fastfetch
1.138763427734375
Anifetch(No Cache)(neofetch)
43.53014373779297
Anifetch(Cached)(neofetch)
8.540022850036621
Anifetch(No Cache)(fastfetch)
36.89965796470642
Anifetch(Cached)(fastfetch)
1.474745750427246

No threading:


ANIFETCH NOCACHE (Neofetch)
Caching...
It took 9.127363443374634 seconds.
Caching...
It took 9.068744897842407 seconds.
Caching...
It took 9.083215236663818 seconds.
Caching...
It took 8.991373538970947 seconds.
Caching...
It took 9.084569692611694 seconds.
Caching...
It took 9.035122871398926 seconds.
Caching...
It took 9.07432746887207 seconds.
Caching...
It took 9.095402002334595 seconds.
Caching...
It took 9.057516813278198 seconds.
Caching...
It took 9.248551368713379 seconds.
ANIFETCH CACHED (Neofetch)
It took 0.7868955135345459 seconds.
It took 0.8182957172393799 seconds.
It took 0.8180320262908936 seconds.
It took 0.7933425903320312 seconds.
It took 0.7964498996734619 seconds.
It took 0.7842671871185303 seconds.
It took 0.7907185554504395 seconds.
It took 0.7916460037231445 seconds.
It took 0.8200128078460693 seconds.
It took 0.7926642894744873 seconds.
It took 0.7995319366455078 seconds.
ANIFETCH NOCACHE (Fastfetch)
Caching...
It took 8.363066911697388 seconds.
Caching...
It took 8.444665431976318 seconds.
Caching...
It took 8.425288438796997 seconds.
Caching...
It took 8.39452314376831 seconds.
Caching...
It took 8.571038484573364 seconds.
Caching...
It took 8.549327850341797 seconds.
Caching...
It took 8.377037048339844 seconds.
Caching...
It took 8.519249200820923 seconds.
Caching...
It took 8.846063375473022 seconds.
Caching...
It took 8.81434154510498 seconds.
ANIFETCH CACHED (Fastfetch)
It took 0.11263847351074219 seconds.
It took 0.11906599998474121 seconds.
It took 0.11680197715759277 seconds.
It took 0.11721587181091309 seconds.
It took 0.1172788143157959 seconds.
It took 0.11991477012634277 seconds.
It took 0.11539649963378906 seconds.
It took 0.11403751373291016 seconds.
It took 0.1140604019165039 seconds.
It took 0.11395621299743652 seconds.
It took 0.12470054626464844 seconds.
Neofetch
9.1406090259552
Fastfetch
1.1368329524993896
Anifetch(No Cache)(neofetch)
91.65770888328552
Anifetch(Cached)(neofetch)
8.351279735565186
Anifetch(No Cache)(fastfetch)
86.14819645881653
Anifetch(Cached)(fastfetch)
1.5313568115234375

Every now and again benchmark.py gives an error on one of the runs to do with the png not being a valid file tho, most likely due to chafa trying to open an image before it can be rendered by ffmpeg. I've not had this issue once running the program normally though so idrk. Could probably fix it by adding a sleep command in somewhere to give ffmpeg more time to process the image before chafa tries to open it. I am tired tho lol so probably won't get that done today.

EDIT:
Nvm think I fixed the race condition between chafa and ffmpeg. Added a 0.02s sleep call at the start of the loop to run the chafa threads.

@Immelancholy
Copy link
Contributor Author

The most recent commit uses the pillow library to check if the png file is a valid png, as the path for the file will exist before it's fully generated from ffmpeg. This should hopefully stop the race condition of chafa trying to open the png file before it's properly created by ffmpeg.

@Immelancholy
Copy link
Contributor Author

Caveat is you'd have to add the pillow library to the project in a venv and probably a pyproject.toml lol

@Immelancholy
Copy link
Contributor Author

weird bug with fastfetch in general in the devshell where the shell variable is a bit messed up but otherwise the devshell works good as an environment for users with nix

@Notenlish
Copy link
Owner

The most recent commit uses the pillow library to check if the png file is a valid png, as the path for the file will exist before it's fully generated from ffmpeg. This should hopefully stop the race condition of chafa trying to open the png file before it's properly created by ffmpeg.

why not just check the size?

@Immelancholy
Copy link
Contributor Author

Immelancholy commented May 20, 2025

The most recent commit uses the pillow library to check if the png file is a valid png, as the path for the file will exist before it's fully generated from ffmpeg. This should hopefully stop the race condition of chafa trying to open the png file before it's properly created by ffmpeg.

why not just check the size?

Honestly I just wasn't sure what a good minimum filesize was. (weirdly enough in the time I walked away from my computer and came back the check using the pillow library no longer works as it did? It's failing every other benchmark now lol when I ran it like 10 times in a row). So I'm back to square 1 again lol.

This is the error in particular that occurs at times with the filesize (and now pillow) check, most commonly when running the benchmark as it runs the program multiple times. It only shows up in about 1 in 4 benchmark runs but that's still not ideal.

chafa: Failed to open '/home/mela/.local/share/anifetch/video/30.png': Unknown file format
Exception in thread Thread-33 (chafa_process):
Traceback (most recent call last):
  File "/nix/store/kjvgj2n3yn70hmjifg6y0bk9m4rf7jba-python3-3.12.10/lib/python3.12/threading.py", line 1075, in _bootstrap_inner
    self.run()
  File "/nix/store/kjvgj2n3yn70hmjifg6y0bk9m4rf7jba-python3-3.12.10/lib/python3.12/threading.py", line 1012, in run
    self._target(*self._args, **self._kwargs)
  File "/home/mela/Documents/Projects/anifetch/anifetch.py", line 255, in chafa_process
    frame = subprocess.check_output(
            ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/kjvgj2n3yn70hmjifg6y0bk9m4rf7jba-python3-3.12.10/lib/python3.12/subprocess.py", line 466, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/nix/store/kjvgj2n3yn70hmjifg6y0bk9m4rf7jba-python3-3.12.10/lib/python3.12/subprocess.py", line 571, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['chafa', '--symbols', 'ascii', '--fg-only', '--format', 'symbols', '--size=60x30', '/home/mela/.local/share/anifetch/video/30.png']' returned non-zero exit status 2.

This wasn't a concern before as we were only starting the chafa process after ffmpeg had finished processing the video, but now that it is running concurrently it is an issue.

I'll keep trying stuff.

@Immelancholy
Copy link
Contributor Author

I think I fixed the image open with pillow check? Running benchmark like 10 times to be absolutely sure.
I'll push that when I'm done running the benchmarks (if they're successful) just so there's a commit that should reasonably work and then I'll try and see if I can get it working without the library and just checking filesize.

@Immelancholy
Copy link
Contributor Author

Okay, gonna work on file size check and see if I can get that working. If not well we have one that works. I ran the benchmark for the pillow check one like 15 times with no errors lol.

@Immelancholy
Copy link
Contributor Author

Immelancholy commented May 20, 2025

Been playing around with checking filesize and so far no dice. Setting it to check if size is greater than 0 still yields the error, so does 1000 (it's size in bytes). I dont't know if there will be a filesize we can check against that reliably for every image size will get the output. The only way ik of to make sure chafa opens a valid file outside of using the pillow library is to add a sleep of like 0.02, but that drastically increases time and we may as well just wait for the ffmpeg process to finish before doing chafa stuff at that point.

@Immelancholy
Copy link
Contributor Author

Immelancholy commented May 20, 2025

On the bright side this is the result of benchmarking bad apple with multithreading:

󰅂 anifetch -f "BadApple.mp4" -s -W 100 -H 50 -r 20 -c "--symbols wide --fg-only" -ff -fr -b
Caching...
It took 21.132929801940918 seconds.

vs
no threading:

󰅂 anifetch -f "BadApple.mp4" -s -W 100 -H 50 -r 20 -c "--symbols wide --fg-only" -ff -fr -b
Caching...
It took 79.69268226623535 seconds.

EDIT: redid the commands with fps of 20 fps to match yours on the youtube video

@Immelancholy
Copy link
Contributor Author

I might have an idea for a more efficient way of doing stuff with detecting when ffmpeg has processed the frame. Hold off on merging this for now, I'll be experimenting with it tomorrow(today but not 4am lol) and I'll get back to you with results

@Immelancholy
Copy link
Contributor Author

Immelancholy commented May 21, 2025

Okay while this new commit adds another dependency in numpy, it allows me to set the ffmpeg process to pipe out raw frame data in which I use numpy and pillow to save to an image file, this allows for chafa to generate a new ascii frame exactly when ffmpeg finishes making one.

The times are a lot better too as I can set the png compression level which I have set to 0 for maximum speed
times:


ANIFETCH NOCACHE (Neofetch)
Caching...
It took 3.376347780227661 seconds.
Caching...
It took 3.362424373626709 seconds.
Caching...
It took 3.46295428276062 seconds.
Caching...
It took 3.4383935928344727 seconds.
Caching...
It took 3.377904176712036 seconds.
Caching...
It took 3.448399305343628 seconds.
Caching...
It took 3.417130947113037 seconds.
Caching...
It took 3.4184792041778564 seconds.
Caching...
It took 3.5227513313293457 seconds.
Caching...
It took 3.5576205253601074 seconds.
ANIFETCH CACHED (Neofetch)
It took 0.7788999080657959 seconds.
It took 0.7659366130828857 seconds.
It took 0.759606122970581 seconds.
It took 0.7735459804534912 seconds.
It took 0.7638590335845947 seconds.
It took 0.7829630374908447 seconds.
It took 0.7735710144042969 seconds.
It took 0.7752327919006348 seconds.
It took 0.7810699939727783 seconds.
It took 0.7637450695037842 seconds.
It took 0.7584354877471924 seconds.
ANIFETCH NOCACHE (Fastfetch)
Caching...
It took 2.8311853408813477 seconds.
Caching...
It took 2.805241107940674 seconds.
Caching...
It took 2.8291704654693604 seconds.
Caching...
It took 2.8222577571868896 seconds.
Caching...
It took 2.8060975074768066 seconds.
Caching...
It took 2.7944743633270264 seconds.
Caching...
It took 2.8612220287323 seconds.
Caching...
It took 2.793316125869751 seconds.
Caching...
It took 2.810335397720337 seconds.
Caching...
It took 2.8009262084960938 seconds.
ANIFETCH CACHED (Fastfetch)
It took 0.1152489185333252 seconds.
It took 0.11454248428344727 seconds.
It took 0.11801314353942871 seconds.
It took 0.11607766151428223 seconds.
It took 0.11216092109680176 seconds.
It took 0.11953067779541016 seconds.
It took 0.11699485778808594 seconds.
It took 0.11797547340393066 seconds.
It took 0.11660337448120117 seconds.
It took 0.11969470977783203 seconds.
It took 0.11596083641052246 seconds.
Neofetch
8.771350383758545
Fastfetch
1.160266637802124
Anifetch(No Cache)(neofetch)
37.3344144821167
Anifetch(Cached)(neofetch)
8.789425611495972
Anifetch(No Cache)(fastfetch)
31.153046369552612
Anifetch(Cached)(fastfetch)
2.2344913482666016

This is what the new ffmpeg function looks like so you can check it out without having to search for it in the file itself:

def ffmpeg_process():
    threads = []
    stderr = None if args.verbose else subprocess.PIPE
    vid_width = get_width_of_video(args.filename)     
    vid_height = get_height_of_video(args.filename)     
    if args.chroma_flag_given:
        ffmpeg_cmd = [
            "ffmpeg",
            "-i",
            f"{args.filename}",
            "-f",
            "image2pipe",
            "-vf",
            f"fps={args.framerate},chromakey={args.chroma}",
            "-pix_fmt",
            "rgb24",
            "-vcodec",
            "rawvideo",
            "-",
        ]
    else:
        ffmpeg_cmd = [
            "ffmpeg",
            "-i",
            f"{args.filename}",
            "-f",
            "image2pipe",
            "-vf",
            f"fps={args.framerate}",
            "-pix_fmt",
            "rgb24",
            "-vcodec",
            "rawvideo",
            "-",
        ]
    print_verbose(vid_width)
    print_verbose(vid_height)
    proc = subprocess.Popen(ffmpeg_cmd, stdout = subprocess.PIPE, stderr = stderr, bufsize=10**8)
    frame_size = vid_width * vid_height * 3
    i=1
    while True:
        raw_frame = proc.stdout.read(frame_size)
        if not raw_frame:
            break
        ffmpeg_frame = np.frombuffer(raw_frame, np.uint8).reshape((vid_height, vid_width, 3))
        proc.stdout.flush()
        f = str(i) + ".png"
        thread_chafa = threading.Thread(target=chafa_process, args=(f, ffmpeg_frame ))
        thread_chafa.start()
        threads.append(thread_chafa)
        print_verbose("Launching chafa thread")
        i=i+1

and the chafa process:

def chafa_process(f, ffmpeg_frame):
    WIDTH = args.width
    HEIGHT = args.height

    chafa_args = args.chafa_arguments.strip()
    chafa_args += " --format symbols"  # Fixes https://github.com/Notenlish/anifetch/issues/1

    path = BASE_PATH / "video" / f
    im = Image.fromarray(ffmpeg_frame)
    im.save(path, compress_level=0)
    chafa_cmd = [
        "chafa",
        *chafa_args.split(" "),
        # "--color-space=rgb",
        f"--size={WIDTH}x{HEIGHT}",
        path.as_posix(),
    ]
    frame = subprocess.check_output(
        chafa_cmd,
        text=True,
    )

    with open((BASE_PATH / "output" / f).with_suffix(".txt"), "w") as file:
        file.write(frame)

        # if wanted aspect ratio doesnt match source, chafa makes width as high as it can, and adjusts height accordingly.
        # AKA: even if I specify 40x20, chafa might give me 40x11 or something like that.
    HEIGHT = len(frame.splitlines())
    frames.append(frame) # dont question this, I need frames to have at least a single item

@Immelancholy
Copy link
Contributor Author

Immelancholy commented May 21, 2025

I think adding these libraries are worth it for cutting the uncached time down from 8 seconds to 2-3 seconds.

@Immelancholy
Copy link
Contributor Author

Immelancholy commented May 21, 2025

For a slight increase in time in exchange for drastically lower video frame file sizes (I had the compression set to 0) we can also set compression to 1 which brought image files of 8.2mb down to like 1.3

ANIFETCH NOCACHE (Neofetch)
Caching...
It took 4.004655599594116 seconds.
Caching...
It took 3.944424867630005 seconds.
Caching...
It took 3.9705989360809326 seconds.
Caching...
It took 3.996516466140747 seconds.
Caching...
It took 3.992807388305664 seconds.
Caching...
It took 3.9953219890594482 seconds.
Caching...
It took 3.946988344192505 seconds.
Caching...
It took 3.981804370880127 seconds.
Caching...
It took 3.9814791679382324 seconds.
Caching...
It took 4.0027570724487305 seconds.
ANIFETCH CACHED (Neofetch)
It took 0.7623195648193359 seconds.
It took 0.7922379970550537 seconds.
It took 0.7689464092254639 seconds.
It took 0.7625601291656494 seconds.
It took 0.7509744167327881 seconds.
It took 0.7434890270233154 seconds.
It took 0.7647373676300049 seconds.
It took 0.7601959705352783 seconds.
It took 0.7528376579284668 seconds.
It took 0.7504618167877197 seconds.
It took 0.7576508522033691 seconds.
ANIFETCH NOCACHE (Fastfetch)
Caching...
It took 3.4076969623565674 seconds.
Caching...
It took 3.325083017349243 seconds.
Caching...
It took 3.304715633392334 seconds.
Caching...
It took 3.471226215362549 seconds.
Caching...
It took 3.4496920108795166 seconds.
Caching...
It took 3.333001136779785 seconds.
Caching...
It took 3.4345691204071045 seconds.
Caching...
It took 3.504530906677246 seconds.
Caching...
It took 3.493168592453003 seconds.
Caching...
It took 3.333864212036133 seconds.
ANIFETCH CACHED (Fastfetch)
It took 0.11867547035217285 seconds.
It took 0.11205816268920898 seconds.
It took 0.12760448455810547 seconds.
It took 0.11674213409423828 seconds.
It took 0.1210641860961914 seconds.
It took 0.11839962005615234 seconds.
It took 0.11387252807617188 seconds.
It took 0.11552286148071289 seconds.
It took 0.11917996406555176 seconds.
It took 0.11806058883666992 seconds.
It took 0.12114429473876953 seconds.
Neofetch
8.961909294128418
Fastfetch
1.2287859916687012
Anifetch(No Cache)(neofetch)
41.702621936798096
Anifetch(Cached)(neofetch)
8.708495616912842
Anifetch(No Cache)(fastfetch)
35.93653845787048
Anifetch(Cached)(fastfetch)
2.3021371364593506

This is the time with compression at 0.

If you'd prefer this so the image file size is smaller, lmk and I'll push this change.

@Immelancholy
Copy link
Contributor Author

Sadly couldn't find a way to get it to work with filesize only, and I honestly think this is the least hacky way to do this

@Immelancholy
Copy link
Contributor Author

@Notenlish I was conferring with someone else about some stuff with this. First suggestion was to add a threadpool. The other one is that there is actually a Chafa python module someone made. If we could have that I think I could reduce time some more as instead of using pil to save the image to png to open with chafa, I think we might be able to just pipe the raw ffmpeg stream into chafa using it, meaning no image would ever need to be saved. So it would either be an extra dependency for efficiency and speed(?) but may still need numpy and PIL. Or it would be able to cut both of those out entirely and the raw ffmpeg frame extraction straight into chafa using it. It's python bindings for the chafa C API which has that capability afaik.

@Notenlish
Copy link
Owner

I think both of those could work

@Notenlish Notenlish mentioned this pull request Jun 12, 2025
@Immelancholy
Copy link
Contributor Author

@Notenlish As I said in prev pr may not be available for a while. Probs gonna start from scratch with this to re-implement it with the refactor. Ty for your patience. :3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Threaded Caching

2 participants