Resource
Click me
Then copy the text and save it as merge_axes.py (or any name you prefer).
#!/usr/bin/env python3
import os
import re
import json
import argparse
from pathlib import Path
# 轴后缀 → 轴 ID(L0 为主文件,不在此映射中)
AXIS_MAP = {
"surge": "L1",
"sway": "L2",
"twist": "R0",
"roll": "R1",
"pitch": "R2",
"vib": "V0",
"pump": "V1",
"valve": "A0",
"suck": "A1",
"lube": "A2",
}
# 输出顺序(可自定义)
AXIS_ORDER = ["L1", "L2", "R0", "R1", "R2", "V0", "V1", "A0", "A1", "A2"]
def compact_actions_in_json(json_str):
"""
将 JSON 字符串中所有的 "actions": [ ... ] 数组压缩为单行(紧凑格式)。
假设数组内元素不包含嵌套的方括号(本场景下均为简单对象)。
"""
pattern = r'("actions":\s*\[)([^\]]*)(\])'
def repl(match):
prefix = match.group(1)
content = match.group(2)
suffix = match.group(3)
# 删除所有换行和多余空白,只保留必要的空格
compressed = re.sub(r'\s+', ' ', content).strip()
return prefix + compressed + suffix
return re.sub(pattern, repl, json_str, flags=re.DOTALL)
def merge_funscripts(main_file_path, output_dir=None):
main_path = Path(main_file_path)
if not main_path.exists():
raise FileNotFoundError(f"主文件不存在: {main_file_path}")
base_name = main_path.stem
directory = main_path.parent
# 读取主文件 (L0)
with open(main_path, 'r', encoding='utf-8') as f:
main_data = json.load(f)
# 构建合并数据结构
merged = {
"version": main_data.get("version", "1.1"),
"range": main_data.get("range", 100),
"metadata": main_data.get("metadata", {}),
"actions": main_data.get("actions", []), # L0
"axes": []
}
# 扫描目录,查找所有轴文件
axis_files = {}
for file in directory.glob(f"{base_name}.*.funscript"):
# 提取后缀,例如 ".roll" -> "roll"
suffix = file.stem[len(base_name):]
if suffix.startswith('.'):
suffix = suffix[1:]
if suffix in AXIS_MAP:
axis_id = AXIS_MAP[suffix]
axis_files[axis_id] = file
# 按顺序添加轴(L1, L2, ...)
for axis_id in AXIS_ORDER:
if axis_id in axis_files:
with open(axis_files[axis_id], 'r', encoding='utf-8') as f:
axis_data = json.load(f)
merged["axes"].append({
"id": axis_id,
"actions": axis_data.get("actions", [])
})
# 生成带缩进的 JSON 字符串
json_str = json.dumps(merged, indent=2, ensure_ascii=False)
# 压缩所有 actions 数组为单行
json_str = compact_actions_in_json(json_str)
# 确定输出路径(当前工作目录)
if output_dir is None:
output_dir = os.getcwd()
output_path = Path(output_dir) / f"{base_name}_merged.funscript"
with open(output_path, 'w', encoding='utf-8') as f:
f.write(json_str)
print(f"合并完成,输出文件: {output_path}")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="合并主 .funscript 及其轴文件")
parser.add_argument("main", help="主 .funscript 文件路径")
parser.add_argument("-o", "--output-dir", help="输出目录(默认为当前目录)", default=None)
args = parser.parse_args()
merge_funscripts(args.main, args.output_dir)
Introduce
Multi‑Axis Funscript Merger is a lightweight Python script that automatically collects all related axis .funscript files from the same directory as the main file and merges them into a single, well‑structured JSON(funscript). The result preserves the main action track (L0) and appends up to ten auxiliary axes (L1‑L2, R0‑R2, V0‑V1, A0‑A2), making it easier to manage and share multi‑dimensional scripts.
Usage
(Python 3.6+ required. If you’re new to Python, just Google it or ask an AI.)
python merge_axes.py <main.funscript> [-o <output_directory>]
<main.funscript>– path to the primary.funscriptfile (theL0track).-o,--output-dir– (optional) directory where the merged file will be saved; defaults to the current working directory.
The output file name will just like main_merged.funscript. You need remove
_mergedby youself
All axis files must reside in the same folder as the main file and be named as main_name.suffix.funscript (e.g., example.roll.funscript). The script automatically ignores files whose suffix is not recognised.
Key Features and How It Works
Key Features
- Automatic axis discovery – scans the main file’s directory for any matching
*.suffix.funscriptfiles. - Broad axis support – recognises suffixes
surge,sway,twist,roll,pitch,vib,pump,valve,suck,lubeand maps them to standard IDs (L1,L2,R0, …). - Fixed output order – axes are always written in the sequence
L1, L2, R0, R1, R2, V0, V1, A0, A1, A2, regardless of the order in which files are found. - Compact yet readable output – the top‑level JSON is indented, while each
actionsarray is condensed to a single line, significantly reducing file size without sacrificing structure. - Metadata preservation –
version,range, and any custommetadatafrom the main file are carried over.
How It Works
- Parse the main file – read the primary
.funscript, extract itsactions(theL0track),version,range, andmetadata. - Discover axis files – use a glob pattern
{stem}.*.funscriptto find all candidates; for each, strip the stem to isolate the suffix (e.g.,roll). Map the suffix to an axis ID viaAXIS_MAP. - Load axis data – for each recognised axis, read its
.funscriptand extract theactionsarray. Unknown suffixes are silently skipped. - Build the merged object – create a dictionary with
actions(from the main file) and anaxeslist. Populate the list in the predefinedAXIS_ORDER, preserving the ID and actions of each axis found. - Compact the JSON – serialise with
json.dumps(indent=2), then apply a regex to collapse every"actions": [...]block onto a single line. This keeps the file both human‑inspectable and compact. - Write the output – save the result as
{stem}_merged.funscriptin the chosen directory (or the current working directory by default).
Comparison of Results
Before
Multiple separate files in the same folder:
example.funscript ← main (L0)
example.surge.funscript ← L1
example.sway.funscript ← L2
example.roll.funscript ← R1
example.pitch.funscript ← R2
example.vib.funscript ← V0
After
A single, self‑contained file: example_merged.funscript
{
"version": "1.1",
"range": 100,
"metadata": { … },
"actions": [{"at":0,"pos":50},{"at":500,"pos":100}, …],
"axes": [
{"id": "L1", "actions": [{"at":0,"pos":30}, …]},
{"id": "L2", "actions": [{"at":200,"pos":80}, …]},
{"id": "R1", "actions": [{"at":100,"pos":20}, …]},
…
]
}
All axis tracks are neatly organised inside the axes array, and each actions list is written as a single line for a clean, compact layout.