Python script for automatic scripting of Cock Hero with visual beat meters

Intro
I have created a python script, that can script cock hero videos, if they have a nice visual beat meter. I have used the script to create
https://discuss.eroscripts.com/t/cock-hero-seductive-reasoning/34245
https://discuss.eroscripts.com/t/cock-hero-freedom/34246
It takes about 5 minutes of actual work, to script a video.

Code

# Setup
import cv2
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn import svm
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from tqdm import tqdm
import os
%matplotlib inline

#Version 1.1
class BeatScripter:
    def __init__(self, filename):
        self.filename = filename
        self.file_path = f'D:\\ax156\Launch\Scripts\CH todo\{self.filename}.mp4'
        self.file_path_out = f'D:\\ax156\Launch\Scripts\CH todo\{self.filename}.funscript'
        self.box_top_corner = (0,0)
        self.box_size = (50,50)
        self.frames_list = []
        self.beats_trained_list = []
        self.model = KNeighborsClassifier(n_neighbors=5)
        self.predicted_beats = []
        
    def load_frames_list(self, start_times, number_of_frames = 100):
        for start_time in start_times:
            self.frames_list.append(self.load_frames(start_time * 1000, number_of_frames))
            print(f"{number_of_frames} frames loaded from {start_time} seconds" )
        print(f"dimension (height, length, colors) {self.frames_list[0][0][1].shape}")
        
    def load_frames(self, start_time_in_msec, number_of_frames):
        if not os.path.isfile(self.file_path):
            print(f'Video file not found! {self.file_path}')
            print(f'make sure self.file_path points at the correct folder')
            print(f'make sure filename is the correct filename (without ".mp4")')
            raise Exception(f'Video file not found! {self.file_path}')
        cap = cv2.VideoCapture(self.file_path, cv2.CAP_PROP_POS_MSEC)
        frames = []
        i = 0
        while True:
            read, frame = cap.read()
            if not read:
                break
            time = cap.get(cv2.CAP_PROP_POS_MSEC)
            if time >= start_time_in_msec:
                frames.append((i, frame, time))
                if len(frames) >= number_of_frames:
                    break
            i += 1
        cap.release()
        return frames 
    
    def show_frame(self, frame_set, frame_number, y_sub=None):
        if y_sub is None:
            self.do_show_frame(self.frames_list[frame_set][frame_number][1], x = None, y = None)
        else:
            self.do_show_frame(self.frames_list[frame_set][frame_number][1][y_sub:, :, :], x = None, y = None)
        
    def show_frame_with_box(self, frame_set, frame_number, y_sub=None):
        if y_sub is None:
            self.do_show_frame(self.frames_list[frame_set][frame_number][1], 
                               x = (self.box_top_corner[0], self.box_top_corner[0] + self.box_size[0]),
                               y = (self.box_top_corner[1], self.box_top_corner[1] + self.box_size[1]))
        else:
            self.do_show_frame(self.frames_list[frame_set][frame_number][1][y_sub:, :, :], 
                               x = (self.box_top_corner[0], self.box_top_corner[0] + self.box_size[0]),
                               y = (self.box_top_corner[1]-y_sub, self.box_top_corner[1] + self.box_size[1] - y_sub))
    
    def anotate(self, num_frames = 100, y_sub=None):
        print("Space: not beat")
        print("b: beat")
        for frames in self.frames_list:
            beats_trained = self.beat_marker(frames[0:num_frames], y_sub=y_sub)
            self.beats_trained_list.append(beats_trained)
    
    def get_sub_frame(self, frame):
        return frame[self.box_top_corner[1]:self.box_top_corner[1] + self.box_size[1],
                     self.box_top_corner[0]:self.box_top_corner[0] + self.box_size[0], :]
    
    def train(self):
        beats_train = []
        frames_train = []
        for b in self.beats_trained_list:
            beats_train = beats_train + b
        for f in self.frames_list:
            frames_train = frames_train + f
        
        X = []
        y = []
        for i, beat in beats_train:
            for j, frame, time in frames_train:
                if i == j:
                    X.append(self.get_sub_frame(frame).flatten())
            y.append(int(beat))
        self.model.fit(X, y) 
    
    def do_show_frame(self, frame, x = None, y = None):
        red = (0, 0, 255)
        frame_cp = frame.copy()
        if x is not None:
            cv2.rectangle(frame_cp, (x[0], y[0]), (x[1], y[1]), red, 2)
        cv2.imshow("frame", frame_cp)
        cv2.waitKey(0)
        cv2.destroyAllWindows()

    def beat_marker(self, frames, y_sub):
        red = (0, 0, 255)
        beats = []
        for i, frame, time in frames:
            frame_cp = frame.copy()
            if y_sub is None:
                x = (self.box_top_corner[0], self.box_top_corner[0] + self.box_size[0])
                y = (self.box_top_corner[1], self.box_top_corner[1] + self.box_size[1])
                cv2.rectangle(frame_cp, (x[0], y[0]), (x[1], y[1]), red, 2)
                cv2.imshow("frame", frame_cp)
            else:
                x = (self.box_top_corner[0], self.box_top_corner[0] + self.box_size[0])
                y = (self.box_top_corner[1]-y_sub, self.box_top_corner[1] + self.box_size[1] - y_sub)
                cv2.rectangle(frame_cp, (x[0], y[0]), (x[1], y[1]), red, 2)
                cv2.imshow("frame", frame_cp[y_sub:, :, :])
            while True:
                k = cv2.waitKey(0)
                if k == 32:         # wait for ESC key to exit
                    beats.append((i, False))
                    cv2.destroyAllWindows()
                    break
                elif k == ord('b'): # wait for 's' key to save and exit
                    beats.append((i, True))
                    cv2.destroyAllWindows()
                    break
        return beats

    def predict_beats(self, number=None):
        cap = cv2.VideoCapture(self.file_path, cv2.CAP_PROP_POS_MSEC)
        if number is None:
            number = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        beats = []
        for i in tqdm(range(number)):   
            _, frame = cap.read()
            try:
                X_pred = [self.get_sub_frame(frame).flatten()]
                beats.append((self.model.predict(X_pred)[0] == 1,
                          i,
                          cap.get(cv2.CAP_PROP_POS_MSEC)))
            except:
                break
        cap.release()
        self.predicted_beats = beats

    def export_funscript(self):
        initial = '{"version": "1.0","inverted": false,"range": 90,"actions": ['
        end = ']}'
        middle = ''
        pos = 0
        last_i = 0

        for beat, i, time in self.predicted_beats:
            if beat and (i-last_i) > 4:
                next_step = '{"pos": ' + str(pos) + ', "at": ' + str(round(time)) + '}'
                middle = ','.join([middle, next_step])
                pos = 100 - pos
                last_i = i
        middle = middle[1:]

        text_file = open(self.file_path_out, "w")
        text_file.write(initial + middle + end)
        text_file.close()

#Script
beat_scripter = BeatScripter("chhappyface")
beat_scripter.load_frames_list([0, 1.5*60, 5*60, 9*60])

# beat_scripter.show_frame(1, 0, y_sub=500)
beat_scripter.show_frame(1, 0)

beat_scripter.box_top_corner = (625, 669)
beat_scripter.box_size=(32, 50)
# beat_scripter.show_frame_with_box(1 ,0, y_sub=500)
beat_scripter.show_frame_with_box(1 ,0)

# beat_scripter.anotate(y_sub=500)
beat_scripter.anotate()

beat_scripter.train()

beat_scripter.predict_beats(15000)
# beat_scripter.predict_beats()

beat_scripter.export_funscript()

Tutorial

  1. Install and open jupyter https://jupyter.org/
    Maybe you need to install the following libraries
    scikit-learn Installing scikit-learn — scikit-learn 1.1.2 documentation
    cv2 opencv-python · PyPI
    matplotlib matplotlib.org/stable/users/installing.html
    pandas Installation — pandas 1.4.3 documentation
    tqdm tqdm · PyPI
  2. Create a new notebook and insert the code. The setup shold be in its own cell. And when you come to #Script an empty line, means the code should be divided in cells.
  • To enter a cell: Enter
  • To exit a cell: Escape
  • To create a new cell below, when outside: b
  • To create a new cell above, when outside: a
  • To run a cell: ctrl + Enter
  1. Change the paths self.file_path and self.file_path_out to the directory of the video. (keep \{self.filename}.mp4)
  2. Run the setup cell
  3. beat_scripter = BeatScripter("CH Freedom")
    Write the name of the file insted of “CH Freedom”
  4. beat_scripter.load_frames_list([0, 1.5*60, 5*60, 9*60])
    loads 100 frames from the video at the beginning, and at the 1.5, 5 and 9 minute marks
  5. beat_scripter.show_frame(1, 0)
    Opens a new window showing the first frame at the 1.5 minute mark. You can close the window by hitting enter. This frame should show the beat meter. If it does not, change the numbers. The first number corresponds to the above timestamps (starting at 0) 0 is 0 minutes, 1 is 1.5 minutes, 2 is 5 minutes and 3 is 9 minutes. The second number is the number of the frame in the series, also staring at 0.
  6. If the the resolution is so high, that you have problems having it on your screen, you can add for example “, y_sub=500” to your calls
beat_scripter.box_top_corner = (625, 669)
beat_scripter.box_size=(32, 50)
# beat_scripter.show_frame_with_box(1 ,0, y_sub=500)
beat_scripter.show_frame_with_box(1 ,0)

Now we need to tell the script where the beat meter is located. This code shows a frame with a red box. The box needs to cover the small area, where the beats “activate”. This is where the script will look for beats.
To move the box change the values in beat_scripter.box_top_corner = (625, 669) (x pixels from the top left corner, y pixels from the top left corner)
To change the size of the box change the numbers in beat_scripter.box_size=(32, 50)
Change the numbers and run the cell, until you are satisfied.
10. beat_scripter.anotate()
Now you need to tell the script what a beat and what a not-beat looks like.
The code will open a new window showing the first frame of the video. If the frame is a beat press “b” if not press “space”. Then it will show the next frame, until alle the frames we loaded in step 6 have been shown. It is okay to press b two-three times in a row, when it is almost a beat, and when it is a beat, and when it just was a beat. It will help the script.
11. beat_scripter.train()
This will let the script analyze what you have just told it.
12. beat_scripter.predict_beats(15000)
This will analyze the video and find the beats. The number is the number of frames, to analyze. This takes some time, so it is best just to do part of the video, until it finds the beats as it should.
If the script is ok, then remove the number, and run again.
13. beat_scripter.export_funscript()
Exports the funscript in the video directory

What to do if the funscript is not ok?
You can fine tune the placement of the red box.
If there is a part of the video, where the script is bad, then load som frames from that part (add the minute mark to beat_scripter.load_frames_list([0, 1.5*60, 5*60, 9*60]))
If this doesn’t work, it is probably better to look for another video, some videos are just dificult.

Final notes
I hope you can use the script, and share the funscripts you create. The script is quite basic, so you are very welcome to improve it, and upload your version of it.
It would for example be nice to create the red box with the mouse instead of changing the numbers. The algorithm used for detecting beats (k-nearest neighbours) could also be improved.

35 Likes

First off, this is great and appreciated. I’m trying it out but ran into a couple hiccups:

  1. You should include a list of the libraries used for this project. I know its implied with the “import” stuff but a user may not have scikit-learn in their environment by default, and it takes some googling to figure out that I need to install scikit-learn in order to import sklearn. I have some prior knowledge using Python so it wasn’t that hard for me to figure out, but it might be good to include instructions on how to add these specific libraries to a python environment.

  2. I’m getting the following error and not sure where the index mismatch is. Any ideas?

When I run this:
beat_scripter.load_frames_list([0, 1.5*60, 5*60, 15*60])

I get this output:

100 frames loaded from 0 seconds
100 frames loaded from 90.0 seconds
100 frames loaded from 300 seconds
100 frames loaded from 900 seconds

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_21456/2029814210.py in <module>
----> 1 beat_scripter.load_frames_list([0, 1.5*60, 5*60, 15*60])

~\AppData\Local\Temp/ipykernel_21456/201628164.py in load_frames_list(self, start_times, number_of_frames)
     26             self.frames_list.append(self.load_frames(start_time * 1000, number_of_frames))
     27             print(f"{number_of_frames} frames loaded from {start_time} seconds" )
---> 28         print(f"dimension (height, length, colors) {self.frames_list[0][0][1].shape}")
     29 
     30     def load_frames(self, start_time_in_msec, number_of_frames):

IndexError: list index out of range
1 Like

Are you planning to package it?
I want to use it easily

Good point I will add that.

I am unsure which of the needed libraries are part of the standard python installation. Did you have to install other packages than scikit-learn?

I am unsure why you get the error. But the line is not important, it just gives the resolution of the video, to make it easier to place the red box. You can just comment out the line with a “#”

Maybe, if I figure out how to do it

1 Like

Had to install all of these

import cv2
import matplotlib.pyplot as plt
import pandas as pd
from tqdm import tqdm

1 Like

I am having the same issue but after commenting out the line I get this

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_26520/3174591542.py in <module>
      4 
      5 # beat_scripter.show_frame(1, 0, y_sub=500)
----> 6 beat_scripter.show_frame(1, 0)
      7 
      8 beat_scripter.box_top_corner = (625, 669)

~\AppData\Local\Temp/ipykernel_26520/1070883719.py in show_frame(self, frame_set, frame_number, y_sub)
     47     def show_frame(self, frame_set, frame_number, y_sub=None):
     48         if y_sub is None:
---> 49             self.do_show_frame(self.frames_list[frame_set][frame_number][1], x = None, y = None)
     50         else:
     51             self.do_show_frame(self.frames_list[frame_set][frame_number][1][y_sub:, :, :], x = None, y = None)

IndexError: list index out of range

It looks like your videos are structured differently from the ones I have tried. Try posting the result of

print(beat_scripter.frames_list[0][0])

after loading the frames

I found the problem. but now I am running in to new problems

That looks like the same problem again. What is the output of the following?

beat_scripter = BeatScripter("CHSloots")
beat_scripter.load_frames_list([0])
print(beat_scripter.frames_list[0][0])
> beat_scripter = BeatScripter("Main-1")
> beat_scripter.load_frames_list([0])
> print(beat_scripter.frames_list[0][0])

returns

> 
> 100 frames loaded from 0 seconds
> 
> ---------------------------------------------------------------------------
> IndexError                                Traceback (most recent call last)
> ~\AppData\Local\Temp/ipykernel_15256/843545175.py in <module>
>       1 #Script
>       2 beat_scripter = BeatScripter("Main-1")
> ----> 3 beat_scripter.load_frames_list([0])
>       4 print(beat_scripter.frames_list[0][0])
> 
> ~\AppData\Local\Temp/ipykernel_15256/201628164.py in load_frames_list(self, start_times, number_of_frames)
>      26             self.frames_list.append(self.load_frames(start_time * 1000, number_of_frames))
>      27             print(f"{number_of_frames} frames loaded from {start_time} seconds" )
> ---> 28         print(f"dimension (height, length, colors) {self.frames_list[0][0][1].shape}")
>      29 
>      30     def load_frames(self, start_time_in_msec, number_of_frames):
> 
> IndexError: list index out of range

Sorry. Try to outcomment the line

print(f"dimension (height, length, colors) {self.frames_list[0][0][1].shape}")

in load_frames_list like this

#print(f"dimension (height, length, colors) {self.frames_list[0][0][1].shape}")

And then try

beat_scripter = BeatScripter("Main-1")
beat_scripter.load_frames_list([0])
print(beat_scripter.frames_list[0][0])
100 frames loaded from 0 seconds

---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_15256/843545175.py in <module>
      2 beat_scripter = BeatScripter("Main-1")
      3 beat_scripter.load_frames_list([0])
----> 4 print(beat_scripter.frames_list[0][0])

IndexError: list index out of range


Used this last night and had great results. My only gripe is that by default the progress bar will go to 15000 rather than being capped to the amount of frames in the video, leading to the progress bar being incorrect on videos with fewer than 15000 frames. The funscript was generated perfectly, I just thought it had frozen since the progress bar was stuck at 41%.

Does it literally just create 1 to 100 without any regard for speed or creativity?

Yes, it creates a single movement per beat script from 0 - 100. You can then run it through FunExpander to convert it into a hard mode script.

Personally I’d make some changes before sharing the script, but it saves time creating a very simple outline which you can build upon.

1 Like

There must be a library that you and OP have installed that we don’t. Can you post a list of the modules you have installed in the environment where this works?

It could be that cap.read() is returning false before adding any frames to the frames array, have you tried multiple videos, incase cap is unable to read yours for some reason?
Did you try printing self.frames_list to make sure it has the expected shape? It should look like this:
image

Here’s my packages:

Packages

anyio 3.3.1
argon2-cffi 21.1.0
attrs 21.2.0
Babel 2.9.1
backcall 0.2.0
bleach 4.1.0
certifi 2021.5.30
cffi 1.14.6
charset-normalizer 2.0.6
colorama 0.4.4
cycler 0.10.0
debugpy 1.4.3
decorator 5.1.0
defusedxml 0.7.1
entrypoints 0.3
idna 3.2
ipykernel 6.4.1
ipython 7.27.0
ipython-genutils 0.2.0
jedi 0.18.0
Jinja2 3.0.1
joblib 1.0.1
json5 0.9.6
jsonschema 3.2.0
jupyter-client 7.0.3
jupyter-core 4.8.1
jupyter-server 1.11.0
jupyterlab 3.1.12
jupyterlab-pygments 0.1.2
jupyterlab-server 2.8.1
kiwisolver 1.3.2
MarkupSafe 2.0.1
matplotlib 3.4.3
matplotlib-inline 0.1.3
mistune 0.8.4
nbclassic 0.3.2
nbclient 0.5.4
nbconvert 6.1.0
nbformat 5.1.3
nest-asyncio 1.5.1
notebook 6.4.4
numpy 1.21.2
opencv-python 4.5.3.56
packaging 21.0
pandas 1.3.3
pandocfilters 1.5.0
parso 0.8.2
pickleshare 0.7.5
Pillow 8.3.2
pip 20.0.2
prometheus-client 0.11.0
prompt-toolkit 3.0.20
pycparser 2.20
Pygments 2.10.0
pyparsing 2.4.7
pyrsistent 0.18.0
python-dateutil 2.8.2
pytz 2021.1
pywin32 301
pywinpty 1.1.4
pyzmq 22.3.0
requests 2.26.0
requests-unixsocket 0.2.0
scikit-learn 0.24.2
scipy 1.7.1
Send2Trash 1.8.0
setuptools 46.1.3
six 1.16.0
sniffio 1.2.0
terminado 0.12.1
testpath 0.5.0
threadpoolctl 2.2.0
tornado 6.1
tqdm 4.62.3
traitlets 5.1.0
urllib3 1.26.6
watchdog 2.1.5
wcwidth 0.2.5
webencodings 0.5.1
websocket-client 1.2.1

I have tried multiple videos and one that OP scripted. I tried to print self.frame_list and it did not work I may just be dumb though. watchdog is the only pack I was missing.

# Setup
import cv2
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from sklearn import svm
from sklearn.neighbors import KNeighborsClassifier
from sklearn.ensemble import RandomForestClassifier
from tqdm import tqdm
%matplotlib inline

class BeatScripter:
    def __init__(self, filename):
        self.filename = filename
        self.file_path = f'F:\\Scripts\Cock Hero - Seductive Reasoning\{self.filename}.mp4'
        self.file_path_out = f'F:\\Scripts\Cock Hero - Seductive Reasoning\{self.filename}.funscript'
        self.box_top_corner = (0,0)
        self.box_size = (50,50)
        self.frames_list = []
        self.beats_trained_list = []
        self.model = KNeighborsClassifier(n_neighbors=5)
        self.predicted_beats = []
        
    def load_frames_list(self, start_times, number_of_frames = 100):
        for start_time in start_times:
            self.frames_list.append(self.load_frames(start_time * 1000, number_of_frames))
            print(f"{number_of_frames} frames loaded from {start_time} seconds" )
        #print(f"dimension (height, length, colors) {self.frames_list[0][0][1].shape}")
        
    def load_frames(self, start_time_in_msec, number_of_frames):
        cap = cv2.VideoCapture(self.file_path, cv2.CAP_PROP_POS_MSEC)
        frames = []
        i = 0
        while True:
            read, frame = cap.read()
            if not read:
                break
            time = cap.get(cv2.CAP_PROP_POS_MSEC)
            if time >= start_time_in_msec:
                frames.append((i, frame, time))
                if len(frames) >= number_of_frames:
                    break
            i += 1
        cap.release()
        return frames 
    
    def show_frame(self, frame_set, frame_number, y_sub=None):
        if y_sub is None:
            self.do_show_frame(self.frames_list[frame_set][frame_number][1], x = None, y = None)
        else:
            self.do_show_frame(self.frames_list[frame_set][frame_number][1][y_sub:, :, :], x = None, y = None)
        
    def show_frame_with_box(self, frame_set, frame_number, y_sub=None):
        if y_sub is None:
            self.do_show_frame(self.frames_list[frame_set][frame_number][1], 
                               x = (self.box_top_corner[0], self.box_top_corner[0] + self.box_size[0]),
                               y = (self.box_top_corner[1], self.box_top_corner[1] + self.box_size[1]))
        else:
            self.do_show_frame(self.frames_list[frame_set][frame_number][1][y_sub:, :, :], 
                               x = (self.box_top_corner[0], self.box_top_corner[0] + self.box_size[0]),
                               y = (self.box_top_corner[1]-y_sub, self.box_top_corner[1] + self.box_size[1] - y_sub))
    
    def anotate(self, num_frames = 100, y_sub=None):
        print("Space: not beat")
        print("b: beat")
        for frames in self.frames_list:
            beats_trained = self.beat_marker(frames[0:num_frames], y_sub=y_sub)
            self.beats_trained_list.append(beats_trained)
    
    def get_sub_frame(self, frame):
        return frame[self.box_top_corner[1]:self.box_top_corner[1] + self.box_size[1],
                     self.box_top_corner[0]:self.box_top_corner[0] + self.box_size[0], :]
    
    def train(self):
        beats_train = []
        frames_train = []
        for b in self.beats_trained_list:
            beats_train = beats_train + b
        for f in self.frames_list:
            frames_train = frames_train + f
        
        X = []
        y = []
        for i, beat in beats_train:
            for j, frame, time in frames_train:
                if i == j:
                    X.append(self.get_sub_frame(frame).flatten())
            y.append(int(beat))
        self.model.fit(X, y) 
    
    def do_show_frame(self, frame, x = None, y = None):
        red = (0, 0, 255)
        frame_cp = frame.copy()
        if x is not None:
            cv2.rectangle(frame_cp, (x[0], y[0]), (x[1], y[1]), red, -1)
        cv2.imshow("frame", frame_cp)
        cv2.waitKey(0)
        cv2.destroyAllWindows()

    def beat_marker(self, frames, y_sub):
        beats = []
        for i, frame, time in frames:
            if y_sub is None:
                cv2.imshow("frame", frame)
            else:
                cv2.imshow("frame", frame[y_sub:, :, :])
            while True:
                k = cv2.waitKey(0)
                if k == 32:         # wait for ESC key to exit
                    beats.append((i, False))
                    cv2.destroyAllWindows()
                    break
                elif k == ord('b'): # wait for 's' key to save and exit
                    beats.append((i, True))
                    cv2.destroyAllWindows()
                    break
        return beats

    def predict_beats(self, number):
        cap = cv2.VideoCapture(self.file_path, cv2.CAP_PROP_POS_MSEC)
        beats = []
        for i in tqdm(range(number)):   
            _, frame = cap.read()
            try:
                X_pred = [self.get_sub_frame(frame).flatten()]
                beats.append((self.model.predict(X_pred)[0] == 1,
                          i,
                          cap.get(cv2.CAP_PROP_POS_MSEC)))
            except:
                break
        cap.release()
        self.predicted_beats = beats

    def export_funscript(self):
        initial = '{"version": "1.0","inverted": false,"range": 90,"actions": ['
        end = ']}'
        middle = ''
        pos = 0
        last_i = 0

        for beat, i, time in self.predicted_beats:
            if beat and (i-last_i) > 4:
                next_step = '{"pos": ' + str(pos) + ', "at": ' + str(round(time)) + '}'
                middle = ','.join([middle, next_step])
                pos = 100 - pos
                last_i = i
        middle = middle[1:]

        text_file = open(self.file_path_out, "w")
        text_file.write(initial + middle + end)
        text_file.close()
#Script
beat_scripter = BeatScripter("Cock Hero - Seductive Reasoning")
beat_scripter.load_frames_list([0])
#print(beat_scripter.frames_list[0][0])
# beat_scripter.show_frame(1, 0, y_sub=500)
beat_scripter.show_frame(1, 0)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
~\AppData\Local\Temp/ipykernel_23860/2968537492.py in <module>
      1 # beat_scripter.show_frame(1, 0, y_sub=500)
----> 2 beat_scripter.show_frame(1, 0)

~\AppData\Local\Temp/ipykernel_23860/1918295539.py in show_frame(self, frame_set, frame_number, y_sub)
     47     def show_frame(self, frame_set, frame_number, y_sub=None):
     48         if y_sub is None:
---> 49             self.do_show_frame(self.frames_list[frame_set][frame_number][1], x = None, y = None)
     50         else:
     51             self.do_show_frame(self.frames_list[frame_set][frame_number][1][y_sub:, :, :], x = None, y = None)

IndexError: list index out of range

Happy to her that. I understand your gripe with the progress bar. I will look into that