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