Multi‑Axis Funscript Splitter

Resource

Click me

Then copy the text and save it as split_axes.py (or any name you prefer).

#!/usr/bin/env python3
import os
import re
import json
import argparse
from pathlib import Path

# ID → 扩展名后缀(不含点)
ID_TO_SUFFIX = {
    "L1": "surge",
    "L2": "sway",
    "R0": "twist",
    "R1": "roll",
    "R2": "pitch",
    "V0": "vib",
    "V1": "pump",
    "A0": "valve",
    "A1": "suck",
    "A2": "lube",
}

def compact_actions_in_json(json_str):
    """将 JSON 字符串中所有的 "actions": [ ... ] 数组压缩为单行"""
    pattern = r'("actions":\s*\[)([^\]]*)(\])'
    def repl(match):
        prefix, content, suffix = match.groups()
        compressed = re.sub(r'\s+', ' ', content).strip()
        return prefix + compressed + suffix
    return re.sub(pattern, repl, json_str, flags=re.DOTALL)

def write_compressed_json(data, filepath):
    """写入 JSON 文件,actions 数组压缩,其他部分正常缩进"""
    json_str = json.dumps(data, indent=2, ensure_ascii=False)
    json_str = compact_actions_in_json(json_str)
    with open(filepath, 'w', encoding='utf-8') as f:
        f.write(json_str)

def split_funscript(input_file_path, output_dir=None):
    input_path = Path(input_file_path)
    if not input_path.exists():
        raise FileNotFoundError(f"输入文件不存在: {input_file_path}")

    with open(input_path, 'r', encoding='utf-8') as f:
        data = json.load(f)

    version = data.get("version", "1.1")
    range_val = data.get("range", 100)
    metadata = data.get("metadata", {})
    actions = data.get("actions", [])
    axes = data.get("axes", [])

    base_name = input_path.stem
    if base_name.endswith("_merged"):
        base_name = base_name[:-7]

    if output_dir is None:
        output_dir = os.getcwd()
    out_dir = Path(output_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    # 写入主文件 (L0)
    main_data = {
        "version": version,
        "range": range_val,
        "metadata": metadata,
        "actions": actions
    }
    main_file = out_dir / f"{base_name}.funscript"
    write_compressed_json(main_data, main_file)
    print(f"写入主文件: {main_file}")

    # 写入各轴文件
    for axis in axes:
        axis_id = axis.get("id")
        axis_actions = axis.get("actions", [])
        if axis_id not in ID_TO_SUFFIX:
            print(f"警告: 未知轴ID '{axis_id}',跳过")
            continue
        suffix = ID_TO_SUFFIX[axis_id]
        axis_data = {
            "version": version,
            "range": range_val,
            "metadata": metadata,
            "actions": axis_actions
        }
        axis_file = out_dir / f"{base_name}.{suffix}.funscript"
        write_compressed_json(axis_data, axis_file)
        print(f"写入轴文件: {axis_file}")

    print("拆分完成。")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="拆分合并的多轴 .funscript 文件")
    parser.add_argument("input", help="输入合并文件路径(如 main_merged.json)")
    parser.add_argument("-o", "--output-dir", help="输出目录(默认为当前目录)", default=None)
    args = parser.parse_args()
    split_funscript(args.input, args.output_dir)

Introduce

Multi‑Axis Funscript Splitter is the reverse counterpart of merge_axes.py. It takes a single merged multi‑axis .funscript file (produced by the merger) and explodes it back into individual, standalone files: one main track (L0) and up to ten auxiliary axis files. This is essential for editing, inspecting, or reprocessing individual axes with tools that expect classic single‑track scripts.

Script for unpacking merged multi‑axis funscripts back to separate files.

Usage

(Python 3.6+ required. If you’re new to Python, just Google it or ask an AI.)

python split_axes.py <input_merged.funscript> [-o <output_directory>]
  • <input_merged.funscript> – path to the merged file that contains "actions" (L0) and an "axes" array.
  • -o, --output-dir – (optional) directory where the split files will be created; defaults to the current working directory.

The script automatically strips the _merged suffix from the base name if present, so scene_merged.funscript becomes scene.funscript, scene.surge.funscript, etc.

Key Features and How It Works

Key Features

  • Exact reverse of merge – faithfully reconstructs the original file set from any merged file created by merge_axes.py.
  • Preserves all metadataversion, range, and any custom metadata are copied to every output file, ensuring full round‑trip fidelity.
  • Automatic naming – uses the predefined mapping (L1 → surge, R1 → roll, …) to generate intuitive file names like base.roll.funscript.
  • Compact output – each output file is written with "actions" arrays compressed to a single line, while the rest of the JSON remains nicely indented.
  • Safe overwrite handling – output files are written directly (no backup); run in an empty or dedicated directory if you want to keep the originals.

How It Works

  1. Load the merged file – parse the top‑level JSON, extracting the main actions (L0), the axes list, version, range, and metadata.
  2. Determine base name – strip the _merged suffix from the input file’s stem so that the output names match the original pre‑merge naming.
  3. Write the main file – create {base}.funscript containing version, range, metadata, and the L0 actions.
  4. Iterate over axes – for each entry in the "axes" array, look up its id in ID_TO_SUFFIX. If recognised, write {base}.{suffix}.funscript with the same header fields and the axis‑specific actions.
  5. Compact and save – every output file is serialised with json.dumps(indent=2) and then passed through compact_actions_in_json() so that the actions arrays are single‑line, keeping file sizes manageable while retaining overall readability.

Comparison of Results

Before
A single merged file: scene_merged.funscript

{
  "version": "1.1",
  "range": 100,
  "metadata": { … },
  "actions": [{"at":0,"pos":50}, …],
  "axes": [
    {"id": "L1", "actions": [{"at":0,"pos":30}, …]},
    {"id": "R1", "actions": [{"at":100,"pos":20}, …]},
    …
  ]
}

After
A set of classic single‑track files ready for individual use:

scene.funscript            ← main (L0)
scene.surge.funscript      ← L1
scene.roll.funscript       ← R1
scene.pitch.funscript      ← R2
scene.vib.funscript        ← V0
…

Each file contains the same version, range, and metadata, with only the relevant actions array. The split files can now be edited or re‑processed with tools that do not support multi‑axis format.

3 Likes