John's Anchor Mapping Resync [JAMR]

John’s Anchor Mapping Resync

J.A.M.R.

1234

.

Yo, this is the last of these tools I am making. It’s also the best one! I like, put effort into this and shit, and I was only mildly drunk during the coding process. It is hand written code, for better or worse. Mostly worse. It’s also packaged much better… I made an .exe file. That’s high effort shit.

What does JAMR do?

JAMR is an “anchor mapping resync” script. Unlike my previous resync software, this one edits the funscript rather than the videos. Essentially it requires 3 parts to function:

  1. The original video
  2. A script that is perfectly synced to the “original video”
  3. A new video (a 4k upscale, new online source, FPS increase, whatever…)

It does a very complicated process known as “piecemeal anchor mapping” to match frames between the two video sources, store timetable data in an array, compare that time-data between the videos and the funscript, and finally warp the funscript to fit the new video. Basically, it is to automate the resyncing that is sometimes required when someone posts a new video source, but the script is desynced.

NERD SHIT :index_pointing_up::nerd_face:: Uses ffmpeg and ffprobe, and a whole slew of python libraries to convert frame data to 8bit grayscale hashes at a rate you can set (from once every .25 seconds to once every year), compares them between the two video files in a cpu-friendly-data-pipeline, and then uses a confidence value to set temporally minded anchor points (read: anchor points that only go forward on the timeline, not backward, to avoid every black screen/fadeout ruining the entire script) that are shared across each video that the script’s position values are “warped” around. The difference in time between anchor points on the new video give a delta that can be used as a modifier for the position value of every nearby funscript position value. The goal of this program is to get as many high confidence anchor points as possible as that results in high quality resyncing of the funscript files.

NOTE: This does not work to sync CFR video to VFR video! You will need to run my other tool on VFR video in order to sync it. Variable Frame-Rate Detection + Re-Sync Tool + Duplicate Frame Fix (Python) [UPDATED, AGAIN]

WARNING: It has the potential to be anywhere from mildly destructive to extremely destructive of the actual funscript output, since in order to resync, point positions MUST be moved. Don’t delete your old files, and be sure to carefully check the entire output script for syncing. This software has the potential to perfectly sync scripts to videos under ideal conditions – you will not ever get ideal conditions. But, if it saves you hours and hours of moving points because one person decided to poorly upscale a video you scripted to 4k…

What do you need to run it?

Windows, I think. GPU would be good. That’s pretty much it. I put everything else in the .exe and the folder.

Folder

Let me know what you think. Something something tip jar.
https://payhip.com/b/0bwS3

Source Code for you pussies who won't click strange exe files on the internet

import json
import subprocess
import os
import io
import sys
import imagehash
from PIL import Image
import tkinter as tk
from tkinter import filedialog
import bisect

def select_file(step_name, instruction, filetypes):
print(f"\n=== {step_name} ===“)
print(f”-> {instruction}")

root = tk.Tk()
root.withdraw()
root.attributes('-topmost', True)

file_path = filedialog.askopenfilename(title=instruction, filetypes=filetypes)
root.destroy()
return file_path

def get_precision_setting():
print(“\n— Select Precision Level —”)
print(“1. Faster (Sample every 2.0 seconds)”)
print(“2. Recommended / Balanced (Sample every 1.0 second)”)
print(“3. Custom interval”)

choice = input("\nEnter choice [1-3, default is 2]: ").strip()

if choice == '1': 
    return 2.0
elif choice == '3':
    try:
        return max(0.5, float(input("Enter custom interval in seconds, warning --  lower is not always better (minimum 0.5): ")))
    except ValueError:
        return 1.0
return 1.0

def get_video_duration(media_path):
try:
cmd = [
“ffprobe”, “-v”, “error”, “-show_entries”,
“format=duration”, “-of”,
“default=noprint_wrappers=1:nokey=1”, media_path
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return float(result.stdout.strip())
except Exception:
return 0.0

def extract_hash_timeline_ram(media_path, sample_interval_sec=1.0):
print(f"\nExtracting and hashing: {os.path.basename(media_path)}…")

duration = get_video_duration(media_path)
total_expected_frames = int(duration / sample_interval_sec) if duration > 0 else 0

vf_filters = f"fps=1/{sample_interval_sec},scale=256:256,format=gray"

cmd = [
    "ffmpeg", 
    "-hwaccel", "auto", 
    "-i", media_path, 
    "-vf", vf_filters, 
    "-f", "rawvideo", 
    "-pix_fmt", "gray", 
    "-"
]

process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)

timeline = []
frame_idx = 0
frame_size = 256 * 256 

clown_frames = [
    (
        r"   _.-._    ",
        r" /       \  ",
        r"|  o   o  | ",
        r"|   \v/   | ",
        r" \  ---  /  ",
        r"  | / \ |   ",
        r" _|_/_\_|_  "
    ),
    (
        r"   _.-._    ",
        r" /       \  ",
        r"|  -   -  | ",
        r"|   \v/   | ",
        r" \  ---  /  ",
        r"  | / \ |   ",
        r" _|_/_\_|_  "
    ),
    (
        r"   _.-._    ",
        r" /       \  ",
        r"|  .   .  | ",
        r"|   ~ ~   | ",
        r" \  ---  /  ",
        r"  | \ / |   ",
        r" _|_/_\_|_  "
    ),
    (
        r"   _.-._    ",
        r" /       \  ",
        r"|  v   v  | ",
        r"|   ---   | ",
        r" \  ___  /  ",
        r"  | / \ |   ",
        r" _|_/_\_|_  "
    )
]

clown_quotes = [
    "Jesus Christ, we're doing this today...",
    "Why is every fucking video file always broken?",
    "I swear, one more day of this job, and you're going to find pieces of me everywhere...",
    "Personally, I don't much care for coding...",
    "All work and no play make John a dull boy...",
    "Were you aware that there is nothing interesting going on here?...",
    "This might be one of the most pathetic things I've ever witnessed.",
    "Why are you reading this message? You can tab away you know."
]

first_print = True

while True:
    raw_bytes = process.stdout.read(frame_size)
    if len(raw_bytes) < frame_size:
        break
        
    try:
        img = Image.frombytes('L', (256, 256), raw_bytes)
        h = imagehash.phash(img, hash_size=8)
        
        timestamp_ms = int(frame_idx * sample_interval_sec * 1000)
        timeline.append((timestamp_ms, h))
        frame_idx += 1
        
        frame_art = clown_frames[(frame_idx // 3) % len(clown_frames)]
        quote = clown_quotes[(frame_idx // 4) % len(clown_quotes)]
        
        if total_expected_frames > 0:
            percent = min(100.0, (frame_idx / total_expected_frames) * 100)
            progress_str = f"[{percent:.1f}%]"
        else:
            progress_str = f"[Frame {frame_idx}]"
            
        block = (
            f" {frame_art[0]}\033[K\n"
            f" {frame_art[1]}\033[K\n"
            f" {frame_art[2]}     {progress_str}\033[K\n"
            f" {frame_art[3]}\033[K\n"
            f" {frame_art[4]}     \"{quote}\"\033[K\n"
            f" {frame_art[5]}\033[K\n"
            f" {frame_art[6]}\033[K\n"
        )
        
        if first_print:
            sys.stdout.write(block)
            sys.stdout.flush()
            first_print = False
        else:
            sys.stdout.write(f"\033[7A{block}")
            sys.stdout.flush()
            
    except Exception:
        pass
        
process.wait()
print() 
return timeline

def build_anchor_map(original_timeline, desynced_timeline, max_hash_diff=10):
raw_matches =

for t_orig, h_orig in original_timeline:
    best_match_t = None
    lowest_diff = 999
    
    for t_desync, h_desync in desynced_timeline:
        diff = h_orig - h_desync
        if diff <= max_hash_diff and diff < lowest_diff:
            lowest_diff = diff
            best_match_t = t_desync
            
    if best_match_t is not None:
        raw_matches.append((t_orig, best_match_t, lowest_diff))
        
anchors = []
anchor_diffs = []
last_t_orig = -1
last_t_desync = -1

for t_orig, t_desync, diff in raw_matches:
    if t_orig > last_t_orig and t_desync > last_t_desync:
        anchors.append((t_orig, t_desync))
        anchor_diffs.append(diff)
        last_t_orig = t_orig
        last_t_desync = t_desync
        
return anchors, anchor_diffs

def print_sync_diagnostics(anchors, anchor_diffs, orig_len, desync_len):
print(“\n” + “=”*45)
print(" VIDEO & SYNC DIAGNOSTICS AKA HOW SHIT OF A JOB DID WE DO “)
print(”="*45)

if not anchors:
    print("[ERROR] Confidence Score: ZERO (No matching anchors found, that's FUCKING BAD)")
    print("-> Try increasing the frequency of your points maybe? Probably ain't worth running again.")
    print("="*45)
    return

avg_diff = sum(anchor_diffs) / len(anchor_diffs) if anchor_diffs else 0

gaps = []
for i in range(len(anchors) - 1):
    gap_sec = (anchors[i+1][0] - anchors[i][0]) / 1000.0
    gaps.append(gap_sec)

max_gap_sec = max(gaps) if gaps else 0

if avg_diff <= 8.0 and max_gap_sec < 45.0:
    confidence = "[HIGH] (Somehow, this program did a REALLY good job anchoring points. Maybe it's a good video source. Maybe it's pure skill.)"
elif avg_diff <= 12.0 and max_gap_sec < 120.0:
    confidence = "[MODERATE] (We did alright boss. The anchors are reasonibly well aligned. For an amateur...)"
else:
    confidence = "[LOW / CAUTION] (Maybe it's okay? Sparse anchors or high visual variance. Verify manually! This is likely to be NOT GREAT. But maybe though.)"
    
print(f"Confidence Rating            : {confidence}")
print(f"* Original Video Frames Hashed : {orig_len}")
print(f"* Desynced Video Frames Hashed : {desync_len}")
print(f"* Total Anchors Locked       : {len(anchors)}")
print(f"* Avg Hamming Distance       : {avg_diff:.1f} / 64 bits (Lower is tighter, and as you know, tighter is more gooder)")
print(f"* Largest Gap Without Anchor : {max_gap_sec:.1f} seconds")

if max_gap_sec > 60.0:
    print("\n[WARNING] A large time gap was detected without an anchor point. That might mean nothing, that might mean something. Depends on your video ...")
    print("          If your video features long black screens, static loops, or heavy cuts")
    print("          consider spot-checking around those cuts or timestamps for desync.")
print("="*45 + "\n")

def warp_timestamp(t_orig, anchors):
if not anchors:
return t_orig

orig_times = [a[0] for a in anchors]

if t_orig <= orig_times[0]:
    return max(0, t_orig + (anchors[0][1] - anchors[0][0]))
if t_orig >= orig_times[-1]:
    return max(0, t_orig + (anchors[-1][1] - anchors[-1][0]))
    
idx = bisect.bisect_right(orig_times, t_orig) - 1

t_orig_start, t_desync_start = anchors[idx]
t_orig_end, t_desync_end = anchors[idx+1]

if t_orig_end == t_orig_start:
    return t_desync_start
    
progress = (t_orig - t_orig_start) / (t_orig_end - t_orig_start)
return int(t_desync_start + progress * (t_desync_end - t_desync_start))

def main():
try:
print(“— John’s Anchor Mapping Resync —”)
print(“— J. A. M. R. —”)
sample_interval = get_precision_setting()

    media_filetypes = [("Video Files", "*.mp4 *.mkv *.avi *.mov *.webm *.wmv"), ("All Files", "*.*")]
    
    original_video_path = select_file(
        step_name="STEP 1/3: ORIGINAL VIDEO",
        instruction="Select the perfectly synced ORIGINAL video file",
        filetypes=media_filetypes
    )
    if not original_video_path: 
        print("You didn't pick a video, dipshit.")
        return
    
    desynced_video_path = select_file(
        step_name="STEP 2/3: DESYNCED VIDEO",
        instruction="Select the NEW, desynced video file you want to fix",
        filetypes=media_filetypes
    )
    if not desynced_video_path: 
        print("You didn't pick a video, dipshit.")
        return

    funscript_path = select_file(
        step_name="STEP 3/3: FUNSCRIPT FILE",
        instruction="Select the original .funscript file that matches the original video",
        filetypes=[("Funscript", "*.funscript"), ("All Files", "*.*")]
    )
    if not funscript_path: 
        print("You didn't pick a funscript, dipshit.")
        return

    orig_timeline = extract_hash_timeline_ram(original_video_path, sample_interval_sec=sample_interval)
    desync_timeline = extract_hash_timeline_ram(desynced_video_path, sample_interval_sec=sample_interval)
    
    print("\n[3/4] Building anchor map and analyzing timeline...")
    anchors, anchor_diffs = build_anchor_map(orig_timeline, desync_timeline)
    
    print_sync_diagnostics(anchors, anchor_diffs, len(orig_timeline), len(desync_timeline))

    if len(anchors) < 2:
        print("[ERROR] Too few anchor points found to reliably warp the funscript. That means it fucked up, and that means BAD.")
        return

    print("[4/4] Warping funscript via binary search... Hopefully this doesn't ruin the script haha... KILL ME.")
    with open(funscript_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
        
    for action in data.get("actions", []):
        action["at"] = warp_timestamp(action["at"], anchors)
        
    output_path = funscript_path.rsplit('.', 1)[0] + "_resynced.funscript"
    with open(output_path, 'w', encoding='utf-8') as f:
        json.dump(data, f, indent=4)
        
    print(f"\nHere's hoping that worked. Saved to:\n{output_path}")

except Exception as e:
    print(f"\nError: {e}")
finally:
    input("\nPress Enter to close...")

if name == “main”:
main()

5 Likes

@TwistedPythonVR I made this code EXTRA shitty, just because of your comment on my last post haha :kissing_face_with_closed_eyes:

image
best line of code I’ve ever written

3 Likes

Hilarious

I had an upscale to my Jenna Jameson and Brittany Andrews video by the famous @NumeroUno but just now got back to my main PC. I havn’t had time to check and I love what you made. Note, I have not tried it yet. I am so deep in creating my Journey that I have blinders on. It’s just how I work. Will probably be a few weeks before I release my first chapters of that project and can begin to goof around with other stuff.

I am setting yet another bookmark, of hundreds already, and will return with a verdict on the godliness of this work. Thanks BTW for the breakdown AND solutions to the whole desync thing. I always thought it was a me thing (too much weed, BT vs Wifi, settings from past videos, jumping between scripts/videos, IDK) and never really tried hard to firgure it out. I was just trying to stay hard and get off. Too much analytical during sexy time kills the vibe.

1 Like

Nah cmon

For real though: picture it (or for your sanity don’t).

I am in VR, jumping to a video I think would be hot. It’s already downloaded. I am already high. Already hard. In the mood. Hardware is mounted dripping with lube. All engines go.

It isn’t reflection time. That “might” come after.

I don’t know who the script author is, where I got the video from, or what the delay I last set was. Is my handy still in legacy BT for that old game mod? Maybe my WiFi is shitty right now. Wait was this an frame rate upscale or a streaming sight rip video? Was this an AI slop script or from a noob that wont be in sync anyway? Did I last try messing with handy onboarding settings seeking a altered thrill? Is this script created with an offset in the first place for the script creators own setup? Maybe in a moment of video download overconsumption, a script and video got swapped in the renaming process.

The potential desync issues are vast, and at times, it’s user error. I am humble in that way… I guess?

Honestly, though. With this genre of work, if I am “testing” there is a “drive” and “interest” issue present. A trifecta that all needs to be aligned for it to work. If I stop sexy time to figure any of those technical things out too deeply… sexy time is over. So is my motivation to return. So, I try an offset or pull up a different video. I either get off and loose motivation, and recollection of what I did or didn’t try or move on for the moment… not really looking back to try to figure it out.

Plus with such a massive library of content, I often times just quickly move on to something else to not loose the moment if something is off.

1 Like

very important for everyone to realize:

UPSCALING CAN FUCKUP A SCRIPT

Well not the script but it will sometimes not fit anymore.

John can you write down what an upscale has to to be to still fit the script of the original video?

No promises that this will actually work, the processing takes a while so the testing I was able to do was only on 3 or 4 use cases (3/4 because one was VFR and never fucking worked.) I’m guessing it will usually mostly resync script. I’m sure you’re able to detect my confidence.

You’re welcome! Hopefully someone else might come along and create better solutions. This really was a spaghetti at wall type of situation.

I did my best boss

1 Like

Same. When I am actually using scripts, I am very quick to dump and move on. That analytical headspace isn’t ideal for that particular moment. I haven’t even had the urge to masturbate since I started working on this project because this nerd shit absolutely kills my sex drive. I’m thinking of this tool more as something for scripters who are unfortunate enough to have someone post a 4k upscale in their comments and get to deal with the notifications of “wow, there is so much desync” in perpetuity.

Also the shitty thing is that it can give folks a bad impression of a scripter if they use the wrong video source. I think that’s a big lesson I learned writing my g90ak review and what’s so smart about how he operates, since he makes sure to provide the highest quality video source he can and he scripts directly to it. Sort of guarentees there isn’t any of the wild-west video degradation of his work. (do you think anyone has ever used a streamed 720p screencapture when they had access to a g90aked 4k remaster? must be desperate times in their household)

Wish I had the GPU to be able to upscale all my content, and a mega that could fit it all haha.