feat(profiling): add dance motion profiler + beat-energy profiles for adopted dances

tools/dance_profile.py samples world-space bone positions per frame in
Blender headless and emits a per-beat motion-energy timeline (accents,
phrases, totals) at a given BPM. Profiles generated for war_dance_01,
fertility_dance_01, taming_dance_01.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 09:52:06 -07:00
parent 7b44019c9a
commit f468d148bc
4 changed files with 1002 additions and 0 deletions
@@ -0,0 +1,193 @@
{
"name": "fertility_dance_01",
"glb": "exchange/converted-glb/fertility_dance_01.glb",
"duration_s": 30.0,
"fps": 24,
"frames": 720,
"bpm": 100,
"beats": 50,
"energy_per_beat": [
0.0671,
0.1187,
0.066,
0.0813,
0.1161,
0.234,
0.3293,
0.4352,
0.4132,
0.3849,
0.4206,
0.4606,
0.4989,
0.4251,
0.4806,
0.4737,
0.5986,
0.5101,
0.2858,
0.6089,
0.5596,
0.6887,
0.8083,
0.5382,
0.4154,
0.9773,
1.034,
0.5643,
1.1262,
0.4652,
0.4643,
0.9822,
1.2955,
0.6826,
0.9628,
1.3345,
0.9006,
0.5145,
0.6956,
0.6763,
0.3807,
0.4143,
0.4418,
0.489,
0.5304,
0.6733,
0.5457,
0.7964,
0.5111,
0.3799
],
"energy_norm": [
0.0009,
0.0415,
0.0,
0.012,
0.0395,
0.1325,
0.2075,
0.2911,
0.2737,
0.2514,
0.2795,
0.3111,
0.3413,
0.2831,
0.3269,
0.3214,
0.4199,
0.3501,
0.1733,
0.428,
0.3891,
0.4909,
0.5852,
0.3722,
0.2755,
0.7184,
0.7631,
0.3928,
0.8358,
0.3147,
0.314,
0.7223,
0.9693,
0.4861,
0.707,
1.0,
0.6579,
0.3535,
0.4963,
0.4811,
0.248,
0.2745,
0.2962,
0.3335,
0.3661,
0.4788,
0.3782,
0.5758,
0.3509,
0.2475
],
"accent_beats": [
6,
10,
11,
19,
20,
22,
23,
24,
25,
26,
28,
29,
30,
31,
32,
33,
34,
35,
36,
38,
39,
40,
44,
45,
47
],
"pelvis_drop_beats": [
6,
10,
11,
23,
24,
25,
26,
28,
29,
30,
31,
34,
39,
40,
44,
47
],
"hand_peak_beats": [
19,
20,
22,
25,
26,
28,
32,
33,
34,
35,
36,
38,
39,
45,
47
],
"phrase_beats": 3,
"totals": {
"hands_m": 26.653432767023645,
"forearms_m": 21.23157808829689,
"upper_arms_m": 13.863015059740327,
"feet_m": 9.769735456958077,
"shins_m": 13.50568795659558,
"pelvis_m": 10.606148213798214,
"head_m": 11.129225506410268,
"lr_symmetry_hands": 0.9986419895043533,
"lr_symmetry_feet": 0.9833129850891342,
"pelvis_vertical_range_m": 0.41678035259246826,
"footprint_m": [
0.536299467086792,
0.8529399633407593
]
},
"warnings": []
}
@@ -0,0 +1,200 @@
{
"name": "taming_dance_01",
"glb": "exchange/converted-glb/taming_dance_01.glb",
"duration_s": 30.0,
"fps": 24,
"frames": 720,
"bpm": 100,
"beats": 50,
"energy_per_beat": [
0.9916,
0.7155,
0.7886,
1.0957,
2.4134,
1.3599,
1.4711,
0.5712,
0.885,
0.9835,
2.12,
0.73,
0.3254,
0.9716,
1.4584,
0.8395,
0.368,
1.2858,
0.6909,
1.0359,
1.7734,
0.7137,
0.976,
1.7173,
1.3732,
0.6937,
0.444,
0.9441,
0.9548,
0.8954,
1.6452,
0.5804,
0.2176,
0.3835,
0.9297,
0.45,
0.4495,
1.0133,
0.8501,
0.9266,
1.2257,
1.0999,
0.338,
0.7127,
1.1935,
0.3326,
0.7522,
1.4484,
2.0448,
2.5482
],
"energy_norm": [
0.3321,
0.2137,
0.245,
0.3768,
0.9422,
0.4901,
0.5378,
0.1517,
0.2864,
0.3287,
0.8162,
0.2199,
0.0463,
0.3235,
0.5324,
0.2669,
0.0646,
0.4584,
0.2031,
0.3511,
0.6676,
0.2129,
0.3254,
0.6435,
0.4959,
0.2043,
0.0972,
0.3117,
0.3163,
0.2908,
0.6126,
0.1557,
0.0,
0.0712,
0.3055,
0.0998,
0.0995,
0.3414,
0.2714,
0.3042,
0.4326,
0.3786,
0.0517,
0.2124,
0.4187,
0.0494,
0.2294,
0.5281,
0.784,
1.0
],
"accent_beats": [
0,
1,
3,
4,
6,
9,
10,
11,
13,
14,
15,
17,
19,
20,
22,
23,
24,
27,
34,
36,
37,
40,
41,
43,
44,
47,
48,
49
],
"pelvis_drop_beats": [
1,
11,
14,
15,
22,
34,
36,
37,
48,
49
],
"hand_peak_beats": [
0,
1,
3,
4,
6,
9,
10,
13,
15,
17,
19,
20,
22,
23,
24,
27,
34,
37,
40,
41,
43,
44,
47,
48,
49
],
"phrase_beats": 10,
"totals": {
"hands_m": 49.32286386602802,
"forearms_m": 32.85868643786276,
"upper_arms_m": 20.531188024591692,
"feet_m": 18.62118179928407,
"shins_m": 25.7590113640975,
"pelvis_m": 16.053067387300725,
"head_m": 17.234514413654498,
"lr_symmetry_hands": 0.9871641314548004,
"lr_symmetry_feet": 0.8839844668246384,
"pelvis_vertical_range_m": 0.9356732368469238,
"footprint_m": [
1.2194406312191859,
1.6680276617407799
]
},
"warnings": []
}
@@ -0,0 +1,208 @@
{
"name": "war_dance_01",
"glb": "exchange/converted-glb/war_dance_01.glb",
"duration_s": 30.0,
"fps": 24,
"frames": 720,
"bpm": 100,
"beats": 50,
"energy_per_beat": [
0.1161,
0.0602,
0.0763,
0.0628,
0.6339,
0.7965,
0.8297,
0.6782,
0.1496,
0.0932,
0.1334,
0.8858,
0.9878,
0.8822,
0.2595,
0.1081,
0.1336,
0.2622,
0.2088,
0.232,
0.1623,
0.1525,
0.1841,
0.4061,
0.4191,
0.4014,
0.3346,
0.3014,
0.6334,
0.4097,
0.6412,
0.3384,
0.1109,
0.5587,
0.2789,
0.132,
0.588,
0.2621,
0.5738,
0.399,
0.4924,
0.4509,
0.6284,
0.3405,
0.0878,
0.0258,
0.026,
0.0,
0.0,
0.0
],
"energy_norm": [
0.1176,
0.0609,
0.0772,
0.0635,
0.6418,
0.8063,
0.84,
0.6866,
0.1514,
0.0944,
0.1351,
0.8967,
1.0,
0.8931,
0.2627,
0.1094,
0.1352,
0.2654,
0.2114,
0.2348,
0.1643,
0.1544,
0.1863,
0.4111,
0.4242,
0.4064,
0.3387,
0.3051,
0.6413,
0.4147,
0.6491,
0.3426,
0.1122,
0.5656,
0.2823,
0.1336,
0.5952,
0.2654,
0.5809,
0.404,
0.4985,
0.4564,
0.6361,
0.3447,
0.0889,
0.0261,
0.0263,
0.0,
0.0,
0.0
],
"accent_beats": [
4,
5,
6,
7,
11,
12,
13,
17,
20,
22,
23,
25,
26,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
44,
46,
48,
49
],
"pelvis_drop_beats": [
6,
11,
13,
17,
20,
22,
23,
25,
26,
28,
29,
31,
32,
34,
35,
37,
38,
40,
41,
42,
44,
46,
48,
49
],
"hand_peak_beats": [
4,
5,
6,
7,
11,
12,
13,
28,
30,
33,
36,
38,
39,
40,
42
],
"phrase_beats": null,
"totals": {
"hands_m": 21.006407802644276,
"forearms_m": 12.905461711283007,
"upper_arms_m": 6.91306398100012,
"feet_m": 1.521721729225423,
"shins_m": 5.302736997519221,
"pelvis_m": 5.633388137876223,
"head_m": 6.2322590229562564,
"lr_symmetry_hands": 0.9744965493013217,
"lr_symmetry_feet": 0.6518710884905415,
"pelvis_vertical_range_m": 0.19754374027252197,
"footprint_m": [
0.2731027752161026,
0.1643172651529312
]
},
"warnings": []
}
+401
View File
@@ -0,0 +1,401 @@
#!/usr/bin/env python
# dance_profile.py — Blender-headless dance motion profiler.
#
# Samples world-space pose-bone positions every frame of a dance clip's GLB, then
# derives a per-beat motion-energy timeline + accent / phrase / totals metrics. The
# method generalises the one-off analysis behind docs/dances/dance1.md (which sampled
# world-space bone positions per frame) into a reusable script that ALSO emits a beat
# timeline at a given musical BPM.
#
# Usage (Blender bundles Python + numpy — no pip installs):
# /Applications/Blender.app/Contents/MacOS/Blender --background \
# --python tools/dance_profile.py -- --glb <path.glb> --bpm 100 --out <profile.json>
#
# Output JSON schema: see docs/dances/dance1.md / task spec. Units are METERS (glTF is
# always meters; the retargeted Quaternius rigs bake object transform so Blender world
# == data space, Z-up). Pair groups (hands, forearms, ...) report the MEAN of the two
# sides' path lengths — this reproduces dance1.md ("hands ~49-50 m each") and the
# schema example (hands_m: 49.5); the pair sum would be ~2x.
import bpy
import sys
import os
import json
import numpy as np
# ---------------------------------------------------------------------------
# Bone name resolution
#
# Rigs in this repo are Quaternius UE-named (pelvis / Head / hand_l / lowerarm_l /
# calf_l ...). We also tolerate CC_Base_* (CC_Base_L_Hand / CC_Base_L_Calf ...) and
# Mixamo B-* (B-hand.L / B-shin.L ...) so the same script works on pre-retarget
# source rigs. Matching is case-insensitive "contains", first candidate wins.
# ---------------------------------------------------------------------------
# Center-line bones (no L/R).
CENTER_CANDIDATES = {
"pelvis": ["pelvis", "hips", "hip"],
"head": ["head"],
}
# Paired bones: group -> (left_candidates, right_candidates).
SIDE_CANDIDATES = {
"hand": (["hand_l", "hand.l", "l_hand", "cc_base_l_hand", "b-hand.l", "hand_left"],
["hand_r", "hand.r", "r_hand", "cc_base_r_hand", "b-hand.r", "hand_right"]),
"forearm": (["lowerarm_l", "lowerarm.l", "forearm_l", "forearm.l", "l_forearm",
"cc_base_l_forearm", "b-forearm.l", "lowerarm_left"],
["lowerarm_r", "lowerarm.r", "forearm_r", "forearm.r", "r_forearm",
"cc_base_r_forearm", "b-forearm.r", "lowerarm_right"]),
"upperarm": (["upperarm_l", "upperarm.l", "l_upperarm", "cc_base_l_upperarm",
"b-upperarm.l", "upperarm_left"],
["upperarm_r", "upperarm.r", "r_upperarm", "cc_base_r_upperarm",
"b-upperarm.r", "upperarm_right"]),
"foot": (["foot_l", "foot.l", "l_foot", "cc_base_l_foot", "b-foot.l", "foot_left"],
["foot_r", "foot.r", "r_foot", "cc_base_r_foot", "b-foot.r", "foot_right"]),
"shin": (["calf_l", "calf.l", "shin_l", "shin.l", "l_calf", "cc_base_l_calf",
"b-shin.l", "b-calf.l", "calf_left", "shin_left"],
["calf_r", "calf.r", "shin_r", "shin.r", "r_calf", "cc_base_r_calf",
"b-shin.r", "b-calf.r", "calf_right", "shin_right"]),
}
# All logical group keys the metrics expect.
ALL_GROUPS = ["pelvis", "head",
"hand_l", "hand_r", "forearm_l", "forearm_r",
"upperarm_l", "upperarm_r", "foot_l", "foot_r",
"shin_l", "shin_r"]
def resolve_by_candidates(pose_bone_names, candidates):
"""Return the first pose-bone name whose lowercased form contains any candidate
(candidates tried in priority order). None if nothing matches."""
lowered = [(n, n.lower()) for n in pose_bone_names]
for cand in candidates:
for name, low in lowered:
if cand in low:
return name
return None
def resolve_bones(armature, warnings):
"""Map every logical group to a pose-bone name (or None). Appends a warning per
missing group."""
pose_names = [pb.name for pb in armature.pose.bones]
resolved = {}
for group, cands in CENTER_CANDIDATES.items():
resolved[group] = resolve_by_candidates(pose_names, cands)
for group, (left_c, right_c) in SIDE_CANDIDATES.items():
resolved[f"{group}_l"] = resolve_by_candidates(pose_names, left_c)
resolved[f"{group}_r"] = resolve_by_candidates(pose_names, right_c)
for g in ALL_GROUPS:
if resolved.get(g) is None:
warnings.append(f"bone group not found: {g}")
return resolved
# ---------------------------------------------------------------------------
# Scene / action setup
# ---------------------------------------------------------------------------
def parse_argv():
"""Args after the `--` separator that Blender leaves for the script."""
argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
args = {}
i = 0
while i < len(argv):
key = argv[i]
if key.startswith("--"):
args[key] = argv[i + 1] if i + 1 < len(argv) else ""
i += 2
else:
i += 1
return args
def find_armature():
arms = [o for o in bpy.data.objects if o.type == "ARMATURE"]
if not arms:
raise RuntimeError("no armature object in imported GLB")
if len(arms) > 1:
raise RuntimeError(f"expected exactly one armature, found {len(arms)}: "
f"{[a.name for a in arms]}")
return arms[0]
def get_action(armature):
"""Active action on the armature, else the first action in bpy.data.actions."""
ad = armature.animation_data
if ad and ad.action:
return ad.action
if bpy.data.actions:
a = bpy.data.actions[0]
armature.animation_data_create()
armature.animation_data.action = a
return a
raise RuntimeError("armature has no animation data and bpy.data.actions is empty")
# ---------------------------------------------------------------------------
# Sampling
# ---------------------------------------------------------------------------
def sample_positions(armature, resolved, frame_list):
"""World-space translation of every resolved bone across frame_list.
Returns {group: np.ndarray (N,3)} for resolved groups only."""
scene = bpy.context.scene
mw = armature.matrix_world
groups = {g: name for g, name in resolved.items() if name is not None}
out = {g: np.empty((len(frame_list), 3), dtype=np.float64) for g in groups}
for fi, f in enumerate(frame_list):
scene.frame_set(f)
for g, name in groups.items():
pb = armature.pose.bones[name]
t = (mw @ pb.matrix).translation
out[g][fi, 0] = t.x
out[g][fi, 1] = t.y
out[g][fi, 2] = t.z
return out
# ---------------------------------------------------------------------------
# Metrics
# ---------------------------------------------------------------------------
def path_length(pos):
d = np.diff(pos, axis=0)
return float(np.sum(np.linalg.norm(d, axis=1)))
def per_interval_speed(pos, fps):
"""Speed (m/s) for each inter-frame interval. Length N-1."""
d = np.diff(pos, axis=0)
return np.linalg.norm(d, axis=1) * fps
def local_min_indices(sig):
return [i for i in range(1, len(sig) - 1)
if sig[i] < sig[i - 1] and sig[i] < sig[i + 1]]
def local_max_indices(sig):
return [i for i in range(1, len(sig) - 1)
if sig[i] > sig[i - 1] and sig[i] > sig[i + 1]]
def pearson(a, b):
a = a - a.mean()
b = b - b.mean()
da = np.sqrt(np.dot(a, a))
db = np.sqrt(np.dot(b, b))
if da < 1e-12 or db < 1e-12:
return 0.0
return float(np.dot(a, b) / (da * db))
def compute_profile(name, glb_arg, positions, resolved, fps, frames, f0, f1, bpm, warnings):
N = frames # number of sampled frames
duration_s = frames / fps
beats = int(round(duration_s * bpm / 60.0))
if beats < 1:
beats = 1
fpb = frames / float(beats) # frames per beat (float)
dt_index = lambda sample_idx: int(min(beats - 1, max(0, sample_idx / fpb)))
# ---- energy_per_beat: mean world speed over hands+feet+pelvis --------------
energy_groups = ["hand_l", "hand_r", "foot_l", "foot_r", "pelvis"]
energy_groups = [g for g in energy_groups if g in positions]
if not energy_groups:
warnings.append("no energy bones (hands/feet/pelvis) resolved — energy timeline zero")
energy_per_beat = []
for b in range(beats):
i_start = int(np.floor(b * fpb))
i_end = int(np.floor((b + 1) * fpb))
i_start = max(0, min(i_start, N - 1))
i_end = max(i_start + 1, min(i_end, N - 1)) # need >=1 delta
window_s = (i_end - i_start) / fps
bone_speeds = []
for g in energy_groups:
seg = positions[g][i_start:i_end + 1]
dist = path_length(seg)
bone_speeds.append(dist / window_s if window_s > 0 else 0.0)
energy_per_beat.append(float(np.mean(bone_speeds)) if bone_speeds else 0.0)
emin, emax = float(min(energy_per_beat)), float(max(energy_per_beat))
if emax - emin > 1e-12:
energy_norm = [(e - emin) / (emax - emin) for e in energy_per_beat]
else:
energy_norm = [0.0 for _ in energy_per_beat]
# ---- accent beats -------------------------------------------------------
pelvis_drop_beats = []
if "pelvis" in positions:
pz = positions["pelvis"][:, 2]
mean_z, std_z = float(pz.mean()), float(pz.std())
thresh_min = mean_z - 0.5 * std_z
for i in local_min_indices(pz):
if pz[i] < thresh_min:
pelvis_drop_beats.append(dt_index(i))
else:
warnings.append("pelvis missing — pelvis-drop accents skipped")
pelvis_drop_beats = sorted(set(pelvis_drop_beats))
hand_peak_beats = []
if "hand_l" in positions or "hand_r" in positions:
hs = np.zeros(max(0, N - 1), dtype=np.float64)
for g in ("hand_l", "hand_r"):
if g in positions:
hs = hs + per_interval_speed(positions[g], fps)
if len(hs) > 2:
mean_hs, std_hs = float(hs.mean()), float(hs.std())
thresh_max = mean_hs + 1.0 * std_hs
for i in local_max_indices(hs):
if hs[i] > thresh_max:
hand_peak_beats.append(dt_index(i))
else:
warnings.append("both hands missing — hand-peak accents skipped")
hand_peak_beats = sorted(set(hand_peak_beats))
accent_beats = sorted(set(pelvis_drop_beats) | set(hand_peak_beats))
# ---- phrase_beats: dominant period via autocorrelation of energy_norm ----
en = np.array(energy_norm, dtype=np.float64)
best_lag, best_r = None, -1.0
for k in range(2, max(2, beats // 2) + 1):
if k >= len(en):
break
r = pearson(en[:-k], en[k:])
if r > best_r:
best_r, best_lag = r, k
phrase_beats = int(best_lag) if (best_lag is not None and best_r >= 0.3) else None
# ---- totals -------------------------------------------------------------
def pair_path_mean(g):
ls = path_length(positions[g + "_l"]) if (g + "_l") in positions else None
rs = path_length(positions[g + "_r"]) if (g + "_r") in positions else None
vals = [v for v in (ls, rs) if v is not None]
return float(np.mean(vals)) if vals else None
def lr_symmetry(g):
ls = path_length(positions[g + "_l"]) if (g + "_l") in positions else 0.0
rs = path_length(positions[g + "_r"]) if (g + "_r") in positions else 0.0
if max(ls, rs) < 1e-9:
return 1.0
return float(min(ls, rs) / max(ls, rs))
totals = {
"hands_m": pair_path_mean("hand"),
"forearms_m": pair_path_mean("forearm"),
"upper_arms_m": pair_path_mean("upperarm"),
"feet_m": pair_path_mean("foot"),
"shins_m": pair_path_mean("shin"),
"pelvis_m": path_length(positions["pelvis"]) if "pelvis" in positions else None,
"head_m": path_length(positions["head"]) if "head" in positions else None,
"lr_symmetry_hands": lr_symmetry("hand"),
"lr_symmetry_feet": lr_symmetry("foot"),
}
if "pelvis" in positions:
p = positions["pelvis"]
# Blender is Z-up: vertical range = Z; the horizontal footprint = X,Y.
# (The spec's "[x_range, z_range]" is glTF Y-up notation — glTF's Z maps to
# Blender Y on import — so this reproduces the schema's [1.2, 1.7] footing.)
totals["pelvis_vertical_range_m"] = float(p[:, 2].max() - p[:, 2].min())
totals["footprint_m"] = [float(p[:, 0].max() - p[:, 0].min()),
float(p[:, 1].max() - p[:, 1].min())]
else:
totals["pelvis_vertical_range_m"] = None
totals["footprint_m"] = [None, None]
profile = {
"name": name,
"glb": glb_arg,
"duration_s": round(duration_s, 4),
"fps": fps,
"frames": frames,
"bpm": bpm,
"beats": beats,
"energy_per_beat": [round(e, 4) for e in energy_per_beat],
"energy_norm": [round(e, 4) for e in energy_norm],
"accent_beats": accent_beats,
"pelvis_drop_beats": pelvis_drop_beats,
"hand_peak_beats": hand_peak_beats,
"phrase_beats": phrase_beats,
"totals": totals,
"warnings": warnings,
}
return profile
def print_summary(profile):
accent = profile["accent_beats"]
top = accent[:8]
print("[profile] %s : %.2fs, %d frames @ %d fps"
% (profile["name"], profile["duration_s"], profile["frames"], profile["fps"]))
print("[profile] %d beats @ %d bpm (energy mean %.2f, peak %.2f)"
% (profile["beats"], profile["bpm"],
float(np.mean(profile["energy_per_beat"])),
float(max(profile["energy_per_beat"]))))
print("[profile] accent beats (%d): %s%s"
% (len(accent), top, " ..." if len(accent) > 8 else ""))
print("[profile] pelvis-drops: %s | hand-peaks: %s | phrase: %s beats"
% (profile["pelvis_drop_beats"], profile["hand_peak_beats"],
profile["phrase_beats"] if profile["phrase_beats"] is not None else "null"))
w = profile["warnings"]
print("[profile] warnings (%d): %s" % (len(w), w if w else "none"))
# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------
def main():
args = parse_argv()
glb_arg = args.get("--glb")
bpm = int(args.get("--bpm", "100"))
out = args.get("--out")
if not glb_arg or not out:
raise SystemExit("usage: dance_profile.py -- --glb <path> --bpm <n> --out <path>")
bpy.ops.wm.read_factory_settings(use_empty=True)
bpy.ops.import_scene.gltf(filepath=glb_arg)
bpy.context.view_layer.update()
armature = find_armature()
action = get_action(armature)
f0, f1 = action.frame_range
f0, f1 = int(f0), int(f1)
# Scene fps from the imported scene (glTF carries no per-action fps metadata;
# the importer bakes the clip at the scene rate, so frames/fps == source duration).
fps = bpy.context.scene.render.fps
warnings = []
resolved = resolve_bones(armature, warnings)
frame_list = list(range(f0, f1 + 1))
frames = len(frame_list)
positions = sample_positions(armature, resolved, frame_list)
name = os.path.splitext(os.path.basename(glb_arg))[0]
profile = compute_profile(name, glb_arg, positions, resolved, fps, frames,
f0, f1, bpm, warnings)
os.makedirs(os.path.dirname(os.path.abspath(out)), exist_ok=True)
with open(out, "w") as fh:
json.dump(profile, fh, indent=2)
# Sanity line so we can confirm units are meters on the first run.
if "pelvis" in positions:
pz = positions["pelvis"][:, 2]
print("[profile] pelvis z: mean %.3f m, range %.3f m (units check)"
% (float(pz.mean()), float(pz.max() - pz.min())))
print_summary(profile)
print("[profile] WROTE %s" % out)
if __name__ == "__main__":
main()