hipinis's picture
Upload 735 files
96d97a7 verified
Raw
History Blame Contribute Delete
25.1 kB
import json
import struct
import numpy as np
from PIL import Image
import io
import logging
def normalize(v):
norm = np.linalg.norm(v)
if norm == 0:
return v
return v / norm
def create_unit_sphere(segments=16, rings=16):
verts = []
normals = []
uvs = []
indices = []
for i in range(rings + 1):
lat = np.pi * i / rings
y = np.cos(lat)
r = np.sin(lat)
for j in range(segments + 1):
lon = 2 * np.pi * j / segments
x = r * np.cos(lon)
z = r * np.sin(lon)
p = [x, y, z]
verts.append(p)
normals.append(p) # Unit sphere normal is position
uvs.append([j / segments, i / rings])
for i in range(rings):
for j in range(segments):
idx0 = i * (segments + 1) + j
idx1 = idx0 + 1
idx2 = (i + 1) * (segments + 1) + j
idx3 = idx2 + 1
indices.append(idx0); indices.append(idx2); indices.append(idx1)
indices.append(idx1); indices.append(idx2); indices.append(idx3)
return np.array(verts, dtype=np.float32), np.array(normals, dtype=np.float32), np.array(uvs, dtype=np.float32), np.array(indices, dtype=np.uint32)
def create_open_cylinder(segments=16):
# Unit cylinder along Y axis, from 0 to 1. Radius 1.
# No caps.
verts = []
normals = []
uvs = []
indices = []
for i in range(segments + 1):
theta = 2 * np.pi * i / segments
x = np.cos(theta)
z = np.sin(theta)
n = [x, 0, z]
# Bottom vertex (y=0)
verts.append([x, 0, z])
normals.append(n)
uvs.append([i / segments, 0.0])
# Top vertex (y=1)
verts.append([x, 1, z])
normals.append(n)
uvs.append([i / segments, 1.0])
for i in range(segments):
idx0 = 2 * i
idx1 = 2 * i + 1
idx2 = 2 * (i + 1)
idx3 = 2 * (i + 1) + 1
# Triangle 1
indices.append(idx0); indices.append(idx2); indices.append(idx1)
# Triangle 2
indices.append(idx1); indices.append(idx2); indices.append(idx3)
return np.array(verts, dtype=np.float32), np.array(normals, dtype=np.float32), np.array(uvs, dtype=np.float32), np.array(indices, dtype=np.uint32)
def align_to_4bytes(b, pad_char=b'\x00'):
padding = (4 - (len(b) % 4)) % 4
return b + pad_char * padding
def save_cylinder_specs_as_glb_animation(cylinder_specs_list, filepath, radius=21.5, fps=30.0):
# 1. Create Meshes
# Sphere for Joints
s_verts, s_norms, s_uvs, s_indices = create_unit_sphere(segments=16, rings=16)
# Cylinder for Bones
c_verts, c_norms, c_uvs, c_indices = create_open_cylinder(segments=16)
# 2. Analyze Topology from Frame 0
num_frames = len(cylinder_specs_list)
if num_frames == 0:
return []
# We assume the number of cylinders is constant
num_cylinders = len(cylinder_specs_list[0])
if num_cylinders == 0:
return []
# Collect colors
unique_colors = []
color_map = {}
for specs in cylinder_specs_list:
for spec in specs:
color = tuple(spec[2])
if color not in color_map:
color_map[color] = len(unique_colors)
unique_colors.append(color)
if not unique_colors:
unique_colors = [(1.0, 1.0, 1.0, 1.0)]
color_map = {(1.0, 1.0, 1.0, 1.0): 0}
tex_width = len(unique_colors)
texture_img = Image.new('RGBA', (tex_width, 1))
pixels = []
for c in unique_colors:
pixels.append((int(c[0]*255), int(c[1]*255), int(c[2]*255), int(c[3]*255)))
texture_img.putdata(pixels)
# Calculate global bounding box (using all frames)
all_points = []
for specs in cylinder_specs_list:
for spec in specs:
start, end, _ = spec
if np.linalg.norm(end - start) > 1e-6:
# Apply Y-flip and Z-flip
s = np.array(start); s[1] = -s[1]; s[2] = -s[2]
e = np.array(end); e[1] = -e[1]; e[2] = -e[2]
all_points.append(s)
all_points.append(e)
if not all_points:
center = np.array([0, 0, 0])
scale_factor = 1.0
else:
all_points_np = np.array(all_points)
min_coords = np.min(all_points_np, axis=0)
max_coords = np.max(all_points_np, axis=0)
center = (min_coords + max_coords) / 2
size = max_coords - min_coords
max_dim = np.max(size)
scale_factor = 1.8 / max_dim if max_dim > 10 else 1.0
logging.info(f"Centering at {center}, Scaling by {scale_factor}")
# Build Skeleton Topology from Frame 0
# We identify unique joints by position
joints = [] # List of { 'pos': np.array, 'color': tuple }
# Map from (cyl_idx, endpoint_type) -> joint_idx
# endpoint_type: 0=start, 1=end
cyl_to_joint = {}
# Helper to find or add joint
def get_joint_idx(pos, color):
# Simple distance check
pos_np = np.array(pos)
# Apply transforms for consistency with animation loop
pos_np[1] = -pos_np[1]
pos_np[2] = -pos_np[2]
pos_np = (pos_np - center) * scale_factor
for idx, j in enumerate(joints):
if np.linalg.norm(j['pos'] - pos_np) < 1e-4: # Tolerance
return idx
joints.append({'pos': pos_np, 'color': color})
return len(joints) - 1
frame0 = cylinder_specs_list[0]
for i, spec in enumerate(frame0):
start, end, color = spec
s_idx = get_joint_idx(start, tuple(color))
e_idx = get_joint_idx(end, tuple(color))
cyl_to_joint[(i, 0)] = s_idx
cyl_to_joint[(i, 1)] = e_idx
num_joints = len(joints)
logging.info(f"Identified {num_joints} unique joints from {num_cylinders} bones.")
# 3. Prepare Animation Data
# Nodes:
# - Joints (Spheres): 0 to num_joints-1
# - Bones (Cylinders): num_joints to num_joints + num_cylinders - 1
total_nodes = num_joints + num_cylinders
translations = [] # total_nodes * num_frames * 3
rotations = [] # total_nodes * num_frames * 4
scales = [] # total_nodes * num_frames * 3
# Pre-fill joint colors
joint_colors = [j['color'] for j in joints]
# Bone colors
bone_colors = []
for i in range(num_cylinders):
# Find first valid color for this bone across frames
c = (1,1,1,1)
for f in range(num_frames):
if len(cylinder_specs_list[f]) > i:
spec = cylinder_specs_list[f][i]
if np.linalg.norm(spec[2]) > 0:
c = tuple(spec[2])
break
bone_colors.append(c)
for f in range(num_frames):
specs = cylinder_specs_list[f]
# 1. Calculate Joint Positions for this frame
# We use the first bone that references a joint to define its position
current_joint_positions = [None] * num_joints
# Pad specs if missing
if len(specs) < num_cylinders:
specs = specs + [(np.zeros(3), np.zeros(3), (0,0,0,0))] * (num_cylinders - len(specs))
for i in range(num_cylinders):
start, end, _ = specs[i]
# Transform
s = np.array(start); s[1] = -s[1]; s[2] = -s[2]
e = np.array(end); e[1] = -e[1]; e[2] = -e[2]
s = (s - center) * scale_factor
e = (e - center) * scale_factor
s_j_idx = cyl_to_joint.get((i, 0))
e_j_idx = cyl_to_joint.get((i, 1))
if s_j_idx is not None and current_joint_positions[s_j_idx] is None:
current_joint_positions[s_j_idx] = s
if e_j_idx is not None and current_joint_positions[e_j_idx] is None:
current_joint_positions[e_j_idx] = e
# Fill missing joints (if any) with 0 or previous?
# Just use 0 if missing (shouldn't happen if topology is constant)
for j in range(num_joints):
if current_joint_positions[j] is None:
current_joint_positions[j] = np.zeros(3)
# 2. Update Joint Nodes
scaled_radius = radius * scale_factor
for j in range(num_joints):
translations.append(current_joint_positions[j].tolist())
rotations.append([0,0,0,1])
scales.append([scaled_radius, scaled_radius, scaled_radius])
# 3. Update Bone Nodes
for i in range(num_cylinders):
s_j_idx = cyl_to_joint.get((i, 0))
e_j_idx = cyl_to_joint.get((i, 1))
if s_j_idx is not None and e_j_idx is not None:
p_start = current_joint_positions[s_j_idx]
p_end = current_joint_positions[e_j_idx]
vec = p_end - p_start
length = np.linalg.norm(vec)
if length < 1e-6:
translations.append([0,0,0])
rotations.append([0,0,0,1])
scales.append([0,0,0])
else:
translations.append(p_start.tolist())
scales.append([scaled_radius, length, scaled_radius])
# Rotation
v_from = np.array([0.0, 1.0, 0.0])
v_to = vec / length
d = np.dot(v_from, v_to)
if d < -0.999999:
tmp = np.cross(np.array([1.0, 0.0, 0.0]), v_from)
if np.linalg.norm(tmp) < 1e-6: tmp = np.cross(np.array([0.0, 0.0, 1.0]), v_from)
tmp = normalize(tmp)
q = [tmp[0], tmp[1], tmp[2], 0.0]
elif d > 0.999999:
q = [0.0, 0.0, 0.0, 1.0]
else:
s = np.sqrt((1+d) * 2)
invs = 1 / s
c = np.cross(v_from, v_to)
q = [c[0] * invs, c[1] * invs, c[2] * invs, s * 0.5]
q_norm = np.linalg.norm(q)
q = [x / q_norm for x in q]
rotations.append(q)
else:
# Orphaned bone?
translations.append([0,0,0])
rotations.append([0,0,0,1])
scales.append([0,0,0])
# 4. Construct GLB
unique_color_indices = sorted(list(set([color_map[c] for c in (joint_colors + bone_colors)])))
mesh_indices_by_color_idx = {} # color_idx -> {"cyl": idx, "sph": idx}
meshes = []
accessors = []
buffer_views = []
offset = 0
# Create meshes for each unique color
for c_idx in unique_color_indices:
u = (c_idx + 0.5) / tex_width
v = 0.5
mesh_pair = {}
# Cylinder Mesh
m_verts = c_verts
m_norms = c_norms
m_uvs = np.array([[u, v]] * len(c_verts), dtype=np.float32)
m_indices = c_indices
# Add to buffer (Indices)
bv_ind_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(m_indices.tobytes()), "target": 34963})
acc_ind_idx = len(accessors)
accessors.append({"bufferView": bv_ind_idx, "byteOffset": 0, "componentType": 5125, "count": len(m_indices), "type": "SCALAR", "min": [int(np.min(m_indices))], "max": [int(np.max(m_indices))]})
offset += len(m_indices.tobytes()); offset = (offset + 3) & ~3
# Vertices
bv_vert_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(m_verts.tobytes()), "target": 34962})
acc_vert_idx = len(accessors)
accessors.append({"bufferView": bv_vert_idx, "byteOffset": 0, "componentType": 5126, "count": len(m_verts), "type": "VEC3", "min": np.min(m_verts, axis=0).tolist(), "max": np.max(m_verts, axis=0).tolist()})
offset += len(m_verts.tobytes())
# Normals
bv_norm_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(m_norms.tobytes()), "target": 34962})
acc_norm_idx = len(accessors)
accessors.append({"bufferView": bv_norm_idx, "byteOffset": 0, "componentType": 5126, "count": len(m_norms), "type": "VEC3"})
offset += len(m_norms.tobytes())
# UVs
bv_uv_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(m_uvs.tobytes()), "target": 34962})
acc_uv_idx = len(accessors)
accessors.append({"bufferView": bv_uv_idx, "byteOffset": 0, "componentType": 5126, "count": len(m_uvs), "type": "VEC2"})
offset += len(m_uvs.tobytes())
mesh_pair["cyl"] = len(meshes)
meshes.append({"primitives": [{"attributes": {"POSITION": acc_vert_idx, "NORMAL": acc_norm_idx, "TEXCOORD_0": acc_uv_idx}, "indices": acc_ind_idx, "material": 0}]})
# Sphere Mesh
m_verts = s_verts
m_norms = s_norms
m_uvs = np.array([[u, v]] * len(s_verts), dtype=np.float32)
m_indices = s_indices
# Add to buffer (Indices)
bv_ind_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(m_indices.tobytes()), "target": 34963})
acc_ind_idx = len(accessors)
accessors.append({"bufferView": bv_ind_idx, "byteOffset": 0, "componentType": 5125, "count": len(m_indices), "type": "SCALAR", "min": [int(np.min(m_indices))], "max": [int(np.max(m_indices))]})
offset += len(m_indices.tobytes()); offset = (offset + 3) & ~3
# Vertices
bv_vert_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(m_verts.tobytes()), "target": 34962})
acc_vert_idx = len(accessors)
accessors.append({"bufferView": bv_vert_idx, "byteOffset": 0, "componentType": 5126, "count": len(m_verts), "type": "VEC3", "min": np.min(m_verts, axis=0).tolist(), "max": np.max(m_verts, axis=0).tolist()})
offset += len(m_verts.tobytes())
# Normals
bv_norm_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(m_norms.tobytes()), "target": 34962})
acc_norm_idx = len(accessors)
accessors.append({"bufferView": bv_norm_idx, "byteOffset": 0, "componentType": 5126, "count": len(m_norms), "type": "VEC3"})
offset += len(m_norms.tobytes())
# UVs
bv_uv_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(m_uvs.tobytes()), "target": 34962})
acc_uv_idx = len(accessors)
accessors.append({"bufferView": bv_uv_idx, "byteOffset": 0, "componentType": 5126, "count": len(m_uvs), "type": "VEC2"})
offset += len(m_uvs.tobytes())
mesh_pair["sph"] = len(meshes)
meshes.append({"primitives": [{"attributes": {"POSITION": acc_vert_idx, "NORMAL": acc_norm_idx, "TEXCOORD_0": acc_uv_idx}, "indices": acc_ind_idx, "material": 0}]})
mesh_indices_by_color_idx[c_idx] = mesh_pair
# Animation Data
times = np.array([i / fps for i in range(num_frames)], dtype=np.float32)
bv_time_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(times.tobytes())})
acc_time_idx = len(accessors)
accessors.append({"bufferView": bv_time_idx, "byteOffset": 0, "componentType": 5126, "count": len(times), "type": "SCALAR", "min": [float(times[0])], "max": [float(times[-1])]})
offset += len(times.tobytes())
translations_np = np.array(translations, dtype=np.float32).reshape(num_frames, total_nodes, 3)
rotations_np = np.array(rotations, dtype=np.float32).reshape(num_frames, total_nodes, 4)
scales_np = np.array(scales, dtype=np.float32).reshape(num_frames, total_nodes, 3)
animations = [{"channels": [], "samplers": []}]
nodes = []
scene_nodes = []
# Create Nodes
# 1. Joints
for i in range(num_joints):
c_idx = color_map[joint_colors[i]]
mesh_idx = mesh_indices_by_color_idx[c_idx]["sph"]
node_idx = len(nodes)
nodes.append({
"mesh": mesh_idx,
"name": f"joint_{i}",
"translation": translations_np[0, i].tolist(),
"rotation": rotations_np[0, i].tolist(),
"scale": scales_np[0, i].tolist()
})
scene_nodes.append(node_idx)
# Animation
# Translation
t_data = translations_np[:, i, :].flatten().tobytes()
bv_t_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(t_data)})
acc_t_idx = len(accessors)
accessors.append({"bufferView": bv_t_idx, "byteOffset": 0, "componentType": 5126, "count": num_frames, "type": "VEC3"})
offset += len(t_data)
sampler_t_idx = len(animations[0]["samplers"])
animations[0]["samplers"].append({"input": acc_time_idx, "interpolation": "LINEAR", "output": acc_t_idx})
animations[0]["channels"].append({"sampler": sampler_t_idx, "target": {"node": node_idx, "path": "translation"}})
# Scale (Joints scale is constant, but we animate it just in case or to simplify loop)
s_data = scales_np[:, i, :].flatten().tobytes()
bv_s_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(s_data)})
acc_s_idx = len(accessors)
accessors.append({"bufferView": bv_s_idx, "byteOffset": 0, "componentType": 5126, "count": num_frames, "type": "VEC3"})
offset += len(s_data)
sampler_s_idx = len(animations[0]["samplers"])
animations[0]["samplers"].append({"input": acc_time_idx, "interpolation": "LINEAR", "output": acc_s_idx})
animations[0]["channels"].append({"sampler": sampler_s_idx, "target": {"node": node_idx, "path": "scale"}})
# 2. Bones
for i in range(num_cylinders):
c_idx = color_map[bone_colors[i]]
mesh_idx = mesh_indices_by_color_idx[c_idx]["cyl"]
node_idx = len(nodes)
nodes.append({
"mesh": mesh_idx,
"name": f"bone_{i}",
"translation": translations_np[0, num_joints + i].tolist(),
"rotation": rotations_np[0, num_joints + i].tolist(),
"scale": scales_np[0, num_joints + i].tolist()
})
scene_nodes.append(node_idx)
# Animation
# Translation
t_data = translations_np[:, num_joints + i, :].flatten().tobytes()
bv_t_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(t_data)})
acc_t_idx = len(accessors)
accessors.append({"bufferView": bv_t_idx, "byteOffset": 0, "componentType": 5126, "count": num_frames, "type": "VEC3"})
offset += len(t_data)
sampler_t_idx = len(animations[0]["samplers"])
animations[0]["samplers"].append({"input": acc_time_idx, "interpolation": "LINEAR", "output": acc_t_idx})
animations[0]["channels"].append({"sampler": sampler_t_idx, "target": {"node": node_idx, "path": "translation"}})
# Rotation
r_data = rotations_np[:, num_joints + i, :].flatten().tobytes()
bv_r_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(r_data)})
acc_r_idx = len(accessors)
accessors.append({"bufferView": bv_r_idx, "byteOffset": 0, "componentType": 5126, "count": num_frames, "type": "VEC4"})
offset += len(r_data)
sampler_r_idx = len(animations[0]["samplers"])
animations[0]["samplers"].append({"input": acc_time_idx, "interpolation": "LINEAR", "output": acc_r_idx})
animations[0]["channels"].append({"sampler": sampler_r_idx, "target": {"node": node_idx, "path": "rotation"}})
# Scale
s_data = scales_np[:, num_joints + i, :].flatten().tobytes()
bv_s_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(s_data)})
acc_s_idx = len(accessors)
accessors.append({"bufferView": bv_s_idx, "byteOffset": 0, "componentType": 5126, "count": num_frames, "type": "VEC3"})
offset += len(s_data)
sampler_s_idx = len(animations[0]["samplers"])
animations[0]["samplers"].append({"input": acc_time_idx, "interpolation": "LINEAR", "output": acc_s_idx})
animations[0]["channels"].append({"sampler": sampler_s_idx, "target": {"node": node_idx, "path": "scale"}})
# Texture
img_byte_arr = io.BytesIO()
texture_img.save(img_byte_arr, format='PNG')
texture_bin = img_byte_arr.getvalue()
bv_tex_idx = len(buffer_views)
buffer_views.append({"buffer": 0, "byteOffset": offset, "byteLength": len(texture_bin)})
offset += len(texture_bin)
total_length = offset
binary_data = bytearray(total_length)
current_ptr = 0
# Fill Buffer
for c_idx in unique_color_indices:
u = (c_idx + 0.5) / tex_width
v = 0.5
# Cylinder
m_verts = c_verts
m_norms = c_norms
m_uvs = np.array([[u, v]] * len(c_verts), dtype=np.float32)
m_indices = c_indices
d = m_indices.tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d); current_ptr = (current_ptr + 3) & ~3
d = m_verts.tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
d = m_norms.tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
d = m_uvs.tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
# Sphere
m_verts = s_verts
m_norms = s_norms
m_uvs = np.array([[u, v]] * len(s_verts), dtype=np.float32)
m_indices = s_indices
d = m_indices.tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d); current_ptr = (current_ptr + 3) & ~3
d = m_verts.tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
d = m_norms.tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
d = m_uvs.tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
# Time
d = times.tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
# Animation Data
# Joints
for i in range(num_joints):
d = translations_np[:, i, :].flatten().tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
d = scales_np[:, i, :].flatten().tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
# Bones
for i in range(num_cylinders):
d = translations_np[:, num_joints + i, :].flatten().tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
d = rotations_np[:, num_joints + i, :].flatten().tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
d = scales_np[:, num_joints + i, :].flatten().tobytes(); binary_data[current_ptr:current_ptr+len(d)] = d; current_ptr += len(d)
# Texture
binary_data[current_ptr:current_ptr+len(texture_bin)] = texture_bin; current_ptr += len(texture_bin)
# JSON
gltf = {
"asset": {"version": "2.0", "generator": "ComfyUI-SCAIL-Pose"},
"buffers": [{"byteLength": total_length}],
"bufferViews": buffer_views,
"accessors": accessors,
"images": [{"bufferView": bv_tex_idx, "mimeType": "image/png"}],
"textures": [{"source": 0}],
"materials": [{"pbrMetallicRoughness": {"baseColorTexture": {"index": 0}, "baseColorFactor": [1, 1, 1, 1], "metallicFactor": 0.0, "roughnessFactor": 0.3}, "doubleSided": True}],
"meshes": meshes,
"nodes": nodes,
"scenes": [{"nodes": scene_nodes}],
"scene": 0,
"animations": animations
}
json_str = json.dumps(gltf)
json_bytes = align_to_4bytes(json_str.encode('utf-8'), pad_char=b' ')
binary_data = align_to_4bytes(binary_data, pad_char=b'\x00')
total_file_size = 12 + 8 + len(json_bytes) + 8 + len(binary_data)
with open(filepath, 'wb') as f:
f.write(b'glTF')
f.write(struct.pack('<I', 2))
f.write(struct.pack('<I', total_file_size))
f.write(struct.pack('<I', len(json_bytes)))
f.write(b'JSON')
f.write(json_bytes)
f.write(struct.pack('<I', len(binary_data)))
f.write(b'BIN\x00')
f.write(binary_data)
return [filepath]