Small python script for generating HMV funscripts

I have used funscript dancer in the past and i really liked it, though it was very crash-y.
Now i don’t really know any programming since i am just a stupid mechanical engineer and not a software dev, and frankly i also dont really have the time to delve too deep into programming ( matlab is already enough for me xD) so i am asking gpt for stuff until it somewhat works.

it is a lot faster than the funscript dancer, but also lacking its features.

So if any skilled programmer wants to pick that up please feel free to do so

Here is the powershell script for installing the libraries, save it as .ps1 and run it with powershell

$packages = @(
    "opencv-python",
    "librosa",
    "moviepy"
)

foreach ($package in $packages) {
    pip install $package
}

Here is the code for the python script save that as .py and run it after installing the requierements.

Its straight forward, select your video/audiofile and it will do stuff for you. The shift slider is not really working from what i have seen but the amplitude is. You can also modify the base amplitude in the script itself, or make the slider bigger/ smaller. I dont know if the moan and slap functions work as imagined, but even without its mostly good enough.

import tkinter as tk
from tkinter import filedialog
import librosa
import numpy as np
import moviepy.editor as mp
import json
from scipy.signal import find_peaks

def generate_funscript(audio_waveform, sample_rate, shift, amplitude, min_time_between_positions=200, max_points=5000):
    # Apply shift and amplitude modification
    shifted_waveform = np.roll(audio_waveform, shift)
    scaled_waveform = shifted_waveform * amplitude

    # Perform downsampling to limit the number of points
    downsample_factor = max(len(scaled_waveform) // max_points, 1)
    downsampled_waveform = scaled_waveform[::downsample_factor]

    # Generate funscript
    actions = []
    prev_timestamp = -min_time_between_positions  # Initialize prev_timestamp to ensure the first action is written
    for i, amplitude in enumerate(downsampled_waveform):
        timestamp = int((i * downsample_factor / sample_rate) * 1000)  # Convert time to milliseconds
        intensity = int((amplitude * 200) + 50)  # Scale amplitude to intensity range
        
        # Check if the time difference between consecutive positions is less than the minimum
        if timestamp - prev_timestamp < min_time_between_positions:
            timestamp = prev_timestamp + min_time_between_positions
        
        actions.append({"at": timestamp, "pos": intensity})
        prev_timestamp = timestamp
    
    funscript = {"actions": actions}

    output_funscript = 'output.funscript'
    with open(output_funscript, 'w') as f:
        json.dump(funscript, f)
    print(f'Funscript generated: {output_funscript}')

def detect_moans_slaps(audio_waveform, sample_rate):
    # Example: Simple peak detection for demonstration purposes
    peaks, _ = find_peaks(audio_waveform, distance=sample_rate)  # Adjust distance as needed
    return peaks

def open_file():
    filename = filedialog.askopenfilename(filetypes=[("Video files", "*.mp4")])
    if filename:
        video_file_entry.delete(0, tk.END)
        video_file_entry.insert(0, filename)

def generate_funscript_from_video():
    video_file = video_file_entry.get()
    if video_file:
        # Load video file and extract audio
        video = mp.VideoFileClip(video_file)
        output_audio_file = 'output_audio.wav'
        video.audio.write_audiofile(output_audio_file)

        # Load extracted audio
        audio_waveform, sample_rate = librosa.load(output_audio_file, sr=None, mono=True)

        # Detect moans and slaps
        moan_slap_timestamps = detect_moans_slaps(audio_waveform, sample_rate)

        # Get shift and amplitude values from sliders
        shift_value = shift_slider.get()
        amplitude_value = amplitude_slider.get()

        # Generate funscript with modified audio
        generate_funscript(audio_waveform, sample_rate, shift_value, amplitude_value)

# Create GUI
root = tk.Tk()
root.title("Funscript Generator")

# Video file selection
video_file_label = tk.Label(root, text="Video File:")
video_file_label.grid(row=0, column=0)
video_file_entry = tk.Entry(root, width=50)
video_file_entry.grid(row=0, column=1)
browse_button = tk.Button(root, text="Browse", command=open_file)
browse_button.grid(row=0, column=2)

# Shift slider
shift_label = tk.Label(root, text="Shift:")
shift_label.grid(row=1, column=0)
shift_slider = tk.Scale(root, from_=0, to=1000, orient=tk.HORIZONTAL)
shift_slider.grid(row=1, column=1)

# Amplitude slider
amplitude_label = tk.Label(root, text="Amplitude:")
amplitude_label.grid(row=5, column=0)
amplitude_slider = tk.Scale(root, from_=0.1, to=5.0, resolution=0.1, orient=tk.HORIZONTAL)
amplitude_slider.grid(row=5, column=1)

# Generate button
generate_button = tk.Button(root, text="Generate Funscript", command=generate_funscript_from_video)
generate_button.grid(row=3, column=0, columnspan=3)

root.mainloop()

Here is the funscript i have done with the python script and the corresponding video
ashe vs wraith
ashe vs wraith hmv.funscript (131.2 KB)

2 Likes

It works! I think program like this can also help the scripter find the position of each beat easier. But sometimes after I choose a certain value of the amp it freezes. I tried to input a .mp4 with no image. Then the output only has a very slightly bump for the whole video.
Then well, I am also not a programmer, I’m an 3d modeler. I have come up with a program to create the pos simply through analyze the bass drum beat of the audio, then when goes to the climax part of the song. Use more aggressive way to generate node. But it’s pretty hard to identify climax part of the song and distinguish either the pos goes up or down. My current strategy is very dumb and doesn’t work well. Plus, the downsampling part is pretty inspiring.