""" blender_author_hoodie.py (T-1089 — hoodie_modern, per-body offset-shell) Hooded-top companion to blender_author_offset_shell.py: reuses the base module's segment join / bone-ratio thresholds / offset / solidify / logo-UV2 / export machinery and layers the hoodie-specific geometry + texture identity on top. Everything spatial is derived from bone landmarks (ratios of neck_01 length, shoulder |x|, spine span) so the same parameters produce a proportionally identical garment on all 11 bodies. The parameter block below is the reusable seam for the rest of the hooded/long-sleeve family (track_jacket, sweater_modern): import this module and override. Hoodie deltas over the base t-shirt shell: * covers torso + torso_upper + FULL arms (long sleeves, wrist-cut on the lowerarm bone instead of the upperarm short-sleeve cut) * HOOD DOWN — a rolled collar bulk ring: collar-band verts are inflated radially away from the neck axis (back-biased, slight upward lift) before solidify, so the roll gets real thickness and caps into a ring * looser standoff (16 mm vs the 12 mm per-body tee) + thicker cloth (6 mm) * painted albedo identity (texture-carried, per body because UV0 face classification is per body): kangaroo pocket fill + stitch outline, drawstrings, ribbed hem + cuff bands — all painted as LUMINANCE detail so the luma-preserving toon_garment recolor keeps them under any tint * region mask: R = collar/hood roll (asymmetric drop — drapes lower on the back), G = body, B = sleeves + kangaroo pocket * logo-capable chest via the base UV2 channel (unchanged) PER-BODY ONLY (Q-060: offset-shells are authored per body, never SD-fit): tooling/blender --background --python \ tooling/scripts/blender/blender_author_hoodie.py -- \ client/assets/characters/bodies client/assets/characters/clothing/hoodie_modern \ [--bodies a,b,c] [--offset M] Writes per body: /.glb, _mask.png, _base_albedo.png Plus fallbacks: /base_albedo.png + reference_mask.png (= average_m's). Decisions: D-162 (pre-fitted per body), D-251 (in-house wardrobe), Q-060. """ import importlib.util import math import os import shutil import sys import bpy import bmesh import numpy as np from mathutils import Vector _HERE = os.path.dirname(os.path.abspath(__file__)) def _load_base(): spec = importlib.util.spec_from_file_location( "offset_shell_base", os.path.join(_HERE, "blender_author_offset_shell.py")) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod base = _load_base() # -------------------------------------------------------------------------- # Hoodie parameters (the reusable seam — override for track_jacket/sweater) # -------------------------------------------------------------------------- GARMENT_ID = "hoodie_modern" SEGMENTS = [ "seg_torso", "seg_torso_upper", "seg_arm_upper_l", "seg_arm_upper_r", "seg_arm_lower_l", "seg_arm_lower_r", ] OFFSET_M = 0.016 # loose standoff (per-body construction guarantees it) THICKNESS_M = 0.006 # fleece-weight cloth WRIST_FRAC = 0.92 # fraction of the lowerarm kept (sleeve ends at wrist) # Hood-down roll: fractions of neck_01 length unless noted. ROLL_OUT_FRAC = 0.40 # radial bulge magnitude ROLL_LIFT_FRAC = 0.14 # upward lift at the rim ROLL_Z_START_FRAC = 0.45 # roll influence starts this far below collar_z_min ROLL_REACH_COLLAR_X = 1.7 # candidate radius around the neck axis (x collar_x_abs) ROLL_FRONT_GAIN = 0.55 # bulge scale at the front... ROLL_BACK_GAIN = 1.10 # ...and at the back (hood mass hangs behind) ROLL_EXPONENT = 1.7 # falloff sharpness toward the rim # Region mask R (roll) band: drop below collar_z_min, x neck_len, per side. R_Z_DROP_FRONT = 0.15 R_Z_DROP_BACK = 0.55 # Kangaroo pocket: z as fractions of the waistband-top -> chest_z_lo span # (sits ABOVE the ribbed hem band), x as fractions of shoulder |x|. POCKET_Z0_FRAC = 0.06 POCKET_Z1_FRAC = 0.88 POCKET_WB_FRAC = 0.56 # bottom half-width POCKET_WT_FRAC = 0.30 # top half-width (diagonal hand openings) POCKET_FILL_MULT = 0.95 # subtle patch shading POCKET_LINE_MULT = 0.70 # stitch outline darkening POCKET_LINE_PX = 2 # outline thickness (erosion iterations) # Drawstrings (front only): x offset / half-width as fractions of collar_x_abs, # z as fractions of neck_len. STRING_X_FRAC = 0.30 STRING_HALF_W_M = 0.005 STRING_TOP_DROP_FRAC = 0.10 STRING_LEN_FRAC = 0.75 STRING_MULT = 0.50 # Ribbed bands: hem (x spine span) and cuffs (x lowerarm length). The cuff is # anchored to the mesh's ACTUAL post-cut reach, not the nominal wrist plane — # the vert-threshold cut leaves a jagged face boundary that stops 1-3 cm short # of the plane, so a nominal-anchored band would mostly land on deleted faces. HEM_BAND_FRAC = 0.08 CUFF_LEN_FRAC = 0.16 BAND_MULT = 0.85 BAND_LINE_MULT = 0.72 BAND_LINE_M = 0.004 # Muted street-tone fabric (luma drives the toon_garment recolor). FABRIC_RGB = (0.55, 0.56, 0.58) FABRIC_NOISE = 0.035 ALBEDO_SEED = 1091 def log(msg): print(f"[hoodie] {msg}") # -------------------------------------------------------------------------- # Landmarks beyond the base thresholds # -------------------------------------------------------------------------- def derive_landmarks(armature, thr): """Hoodie-specific bone landmarks (all in body-local metres).""" bones = armature.data.bones neck = bones.get("neck_01") la_l = bones.get("lowerarm_l") la_r = bones.get("lowerarm_r") if not all([neck, la_l, la_r]): raise RuntimeError("landmark bones missing (neck_01 / lowerarm_l/r)") neck_len = neck.tail_local.z - neck.head_local.z lm = { "neck_y": neck.head_local.y, "neck_len": neck_len, "roll_out": ROLL_OUT_FRAC * neck_len, "roll_lift": ROLL_LIFT_FRAC * neck_len, "roll_reach": ROLL_REACH_COLLAR_X * thr["collar_x_abs"], # wrist cut thresholds per side: (sign, x threshold) "wrist_cuts": [], "lowerarm_len": 0.0, } for b, sign in [(la_l, +1), (la_r, -1)]: head_x, tail_x = b.head_local.x, b.tail_local.x thr_x = head_x + WRIST_FRAC * (tail_x - head_x) lm["wrist_cuts"].append((sign, thr_x)) lm["lowerarm_len"] = abs(tail_x - head_x) log(f"landmarks: neck_len {neck_len:.4f} roll_out {lm['roll_out']*1000:.0f}mm " f"reach {lm['roll_reach']:.3f} wrist cuts " + " ".join(f"{s:+d}@{t:.3f}" for s, t in lm["wrist_cuts"])) return lm # -------------------------------------------------------------------------- # Geometry: wrist cut + hood roll # -------------------------------------------------------------------------- def wrist_cut(shell, lm): """Trim the sleeve tubes at the wrist plane (same pattern as base.sleeve_cut, but on the lowerarm bone so the sleeves stay full length).""" bm = bmesh.new() bm.from_mesh(shell.data) bm.verts.ensure_lookup_table() to_delete = [] for v in bm.verts: for sign, thr_x in lm["wrist_cuts"]: if sign > 0 and v.co.x > thr_x: to_delete.append(v) break if sign < 0 and v.co.x < thr_x: to_delete.append(v) break bmesh.ops.delete(bm, geom=to_delete, context='VERTS') bm.to_mesh(shell.data) bm.free() shell.data.update() log(f"wrist cut removed {len(to_delete)} verts; " f"{len(shell.data.vertices)} remain") def hood_roll(shell, thr, lm): """Inflate collar-band verts radially away from the neck axis to read as a rolled-down hood: back-biased bulge + slight rim lift. Runs after the outward offset and before solidify (the roll then gets cloth thickness).""" z_start = thr["collar_z_min"] - ROLL_Z_START_FRAC * lm["neck_len"] reach = lm["roll_reach"] me = shell.data bm = bmesh.new() bm.from_mesh(me) bm.verts.ensure_lookup_table() candidates = [] z_rim = z_start for v in bm.verts: if v.co.z < z_start: continue hd = math.hypot(v.co.x, v.co.y - lm["neck_y"]) if hd > reach or hd < 1e-6: continue candidates.append((v, hd)) z_rim = max(z_rim, v.co.z) if z_rim <= z_start or not candidates: log("WARNING: no hood-roll candidates found — roll skipped") bm.free() return moved = 0 for v, hd in candidates: t = (v.co.z - z_start) / (z_rim - z_start) w = max(0.0, min(1.0, t)) ** ROLL_EXPONENT if w <= 0.0: continue dir_h = Vector((v.co.x, v.co.y - lm["neck_y"], 0.0)) / hd backness = 0.5 * (1.0 + dir_h.y * -base.FRONT_Y_SIGN) # +Y = back gain = ROLL_FRONT_GAIN + (ROLL_BACK_GAIN - ROLL_FRONT_GAIN) * backness v.co += dir_h * (lm["roll_out"] * w * gain) v.co.z += lm["roll_lift"] * w moved += 1 bm.to_mesh(me) bm.free() me.update() log(f"hood roll: {moved} verts inflated (rim z {z_rim:.3f}, " f"start z {z_start:.3f})") # -------------------------------------------------------------------------- # Paint maps: rasterize body-space (x, z) into UV0 texel space # -------------------------------------------------------------------------- def _raster_tri_attr(maps, a, b, c, xs, zs, is_front, is_back, W, H): """Barycentric rasterization interpolating body-space x/z per texel.""" ax, ay = a.x * (W - 1), a.y * (H - 1) bx, by = b.x * (W - 1), b.y * (H - 1) cx, cy = c.x * (W - 1), c.y * (H - 1) minx = max(int(np.floor(min(ax, bx, cx))), 0) maxx = min(int(np.ceil(max(ax, bx, cx))), W - 1) miny = max(int(np.floor(min(ay, by, cy))), 0) maxy = min(int(np.ceil(max(ay, by, cy))), H - 1) if minx > maxx or miny > maxy: return denom = (by - cy) * (ax - cx) + (cx - bx) * (ay - cy) if abs(denom) < 1e-9: return ys, px_x = np.mgrid[miny:maxy + 1, minx:maxx + 1] px = px_x + 0.5 py = ys + 0.5 w0 = ((by - cy) * (px - cx) + (cx - bx) * (py - cy)) / denom w1 = ((cy - ay) * (px - cx) + (ax - cx) * (py - cy)) / denom w2 = 1.0 - w0 - w1 inside = (w0 >= -1e-4) & (w1 >= -1e-4) & (w2 >= -1e-4) if not inside.any(): return x_val = w0 * xs[0] + w1 * xs[1] + w2 * xs[2] z_val = w0 * zs[0] + w1 * zs[1] + w2 * zs[2] sl = (slice(miny, maxy + 1), slice(minx, maxx + 1)) maps["X"][sl][inside] = x_val[inside] maps["Z"][sl][inside] = z_val[inside] maps["valid"][sl][inside] = True if is_front: maps["front"][sl][inside] = True if is_back: maps["back"][sl][inside] = True def bake_paint_maps(shell, W, H): """Rasterize the shell's UV0 layout into per-texel body-space X/Z maps, with front/back facing flags (front = -Y on this rig).""" maps = { "X": np.zeros((H, W), dtype=np.float32), "Z": np.zeros((H, W), dtype=np.float32), "valid": np.zeros((H, W), dtype=bool), "front": np.zeros((H, W), dtype=bool), "back": np.zeros((H, W), dtype=bool), } bm = bmesh.new() bm.from_mesh(shell.data) bm.faces.ensure_lookup_table() bm.normal_update() uv_layer = bm.loops.layers.uv.active if uv_layer is None: raise RuntimeError("no active UV layer for paint-map bake") for face in bm.faces: n_front = face.normal.y * base.FRONT_Y_SIGN c_front = face.calc_center_median().y * base.FRONT_Y_SIGN is_front = n_front > 0.15 and c_front > 0.0 is_back = n_front < -0.15 and c_front < 0.0 loops = face.loops[:] uvs = [lp[uv_layer].uv.copy() for lp in loops] cos = [lp.vert.co for lp in loops] for i in range(1, len(uvs) - 1): _raster_tri_attr( maps, uvs[0], uvs[i], uvs[i + 1], (cos[0].x, cos[i].x, cos[i + 1].x), (cos[0].z, cos[i].z, cos[i + 1].z), is_front, is_back, W, H) bm.free() overlap = int((maps["front"] & maps["back"]).sum()) log(f"paint maps: {int(maps['valid'].sum())} texels " f"(front {int(maps['front'].sum())}, back {int(maps['back'].sum())}, " f"front/back UV overlap {overlap})") dump = os.environ.get("HOODIE_DEBUG_MAPS", "") if dump: np.savez_compressed(dump, **maps) log(f"debug maps dumped -> {dump}") return maps # -------------------------------------------------------------------------- # Painted albedo (texture-carried modern identity) # -------------------------------------------------------------------------- def derive_paint_params(shell, thr, lm): """Body-space paint geometry, derived from thresholds + final shell mesh.""" sleeve_x = thr["sleeve_x_abs"] hem_z = min((v.co.z for v in shell.data.vertices if abs(v.co.x) < sleeve_x), default=1.0) # Actual sleeve reach from the final mesh (jagged post-cut boundary). wrist_x = max((abs(v.co.x) for v in shell.data.vertices), default=sleeve_x) chest_lo = thr["chest_z"][0] hem_band = HEM_BAND_FRAC * (thr["collar_z_min"] - hem_z) hem_top = hem_z + hem_band span = chest_lo - hem_top shoulder_x = thr["chest_x"][1] / base.CHEST_X_FRAC # invert base ratio p = { "hem_z": hem_z, "hem_band": hem_band, "pocket_z0": hem_top + POCKET_Z0_FRAC * span, "pocket_z1": hem_top + POCKET_Z1_FRAC * span, "pocket_wb": POCKET_WB_FRAC * shoulder_x, "pocket_wt": POCKET_WT_FRAC * shoulder_x, "string_x": STRING_X_FRAC * thr["collar_x_abs"], "string_z_top": thr["collar_z_min"] - STRING_TOP_DROP_FRAC * lm["neck_len"], "string_len": STRING_LEN_FRAC * lm["neck_len"], "cuff_len": CUFF_LEN_FRAC * lm["lowerarm_len"], "wrist_x": wrist_x, "sleeve_x": sleeve_x, } log(f"paint params: hem {hem_z:.3f} pocket z ({p['pocket_z0']:.3f}," f"{p['pocket_z1']:.3f}) w ({p['pocket_wt']:.3f}->{p['pocket_wb']:.3f})") return p def _erode(m, iters): for _ in range(iters): m = (m & np.roll(m, 1, 0) & np.roll(m, -1, 0) & np.roll(m, 1, 1) & np.roll(m, -1, 1)) return m def pocket_mask(maps, p): """Kangaroo-pocket texel mask: front-only trapezoid with diagonal sides.""" X, Z = maps["X"], maps["Z"] paintable = maps["front"] & ~maps["back"] z0, z1 = p["pocket_z0"], p["pocket_z1"] if z1 <= z0: return np.zeros_like(paintable) t = np.clip((z1 - Z) / (z1 - z0), 0.0, 1.0) # 1 at bottom, 0 at top half_w = p["pocket_wt"] + (p["pocket_wb"] - p["pocket_wt"]) * t inside = paintable & (Z >= z0) & (Z <= z1) & (np.abs(X) <= half_w) log(f"pocket mask: {int(inside.sum())} texels") return inside def make_painted_albedo(maps, pocket_px, p, out_dir, body): """Flat street-tone fabric + painted luminance detail; returns bpy image.""" W = H = base.ALBEDO_SIZE rng = np.random.default_rng(ALBEDO_SEED) fabric = np.array(FABRIC_RGB, dtype=np.float32) noise = (rng.random((H, W, 1), dtype=np.float32) - 0.5) * 2.0 * FABRIC_NOISE rgb = np.clip(fabric[None, None, :] + noise, 0.0, 1.0) X, Z = maps["X"], maps["Z"] valid = maps["valid"] front_only = maps["front"] & ~maps["back"] # Ribbed hem band (all around) + top stitch line. hem_top = p["hem_z"] + p["hem_band"] band = valid & (Z <= hem_top) & (np.abs(X) <= p["sleeve_x"]) rgb[band] *= BAND_MULT line = valid & (np.abs(Z - hem_top) <= BAND_LINE_M) & (np.abs(X) <= p["sleeve_x"]) rgb[line] *= BAND_LINE_MULT # Ribbed cuffs (all around) + inner stitch line. cuff_x0 = p["wrist_x"] - p["cuff_len"] cuff = valid & (np.abs(X) >= cuff_x0) rgb[cuff] *= BAND_MULT cline = valid & (np.abs(np.abs(X) - cuff_x0) <= BAND_LINE_M) rgb[cline] *= BAND_LINE_MULT log(f"paint counts: hem {int(band.sum())} hemline {int(line.sum())} " f"cuff {int(cuff.sum())} cuffline {int(cline.sum())} " f"(hem_top {hem_top:.3f}, cuff_x0 {cuff_x0:.3f})") zs = Z[valid] xs_v = np.abs(X[valid]) log(f"map ranges: Z [{zs.min():.3f},{zs.max():.3f}] " f"|X| [{xs_v.min():.3f},{xs_v.max():.3f}] " f"Z<=hem_top {int((zs <= hem_top).sum())} " f"|X|>=cuff_x0 {int((xs_v >= cuff_x0).sum())}") # Kangaroo pocket: subtle fill + dark stitch outline. rgb[pocket_px] *= POCKET_FILL_MULT outline = pocket_px & ~_erode(pocket_px, POCKET_LINE_PX) rgb[outline] *= POCKET_LINE_MULT # Drawstrings (front only, hanging from the collar). z_top = p["string_z_top"] z_bot = z_top - p["string_len"] for sx in (+p["string_x"], -p["string_x"]): s = front_only & (np.abs(X - sx) <= STRING_HALF_W_M) \ & (Z >= z_bot) & (Z <= z_top) rgb[s] *= STRING_MULT rgba = np.concatenate([rgb, np.ones((H, W, 1), dtype=np.float32)], axis=2) img = bpy.data.images.new(f"{GARMENT_ID}_albedo_{body}", W, H, alpha=False) img.pixels.foreach_set(rgba.reshape(-1)) img.update() sidecar = os.path.join(out_dir, f"{body}_base_albedo.png") img.filepath_raw = sidecar img.file_format = 'PNG' img.save() log(f"painted albedo -> {sidecar}") # The glTF exporter names the embedded image after the file basename, and # the Godot import EXTRACTS it as _.png. Point the image at # the shared base_albedo.png so the extraction lands exactly on the sidecar # name above (_base_albedo.png, same pixels — tshirt convention), # instead of a doubled __base_albedo.png. The shared file is # re-pointed to the reference body's paint at the end of the run. img.filepath_raw = os.path.join(out_dir, "base_albedo.png") img.save() return img # -------------------------------------------------------------------------- # Region mask: R = hood roll, G = body, B = sleeves + pocket # -------------------------------------------------------------------------- def _classify_hoodie(center, thr, lm): x, y, z = center.x, center.y, center.z if abs(x) >= thr["sleeve_x_abs"]: return (0.0, 0.0, 1.0, 0.0) # sleeves -> B hd = math.hypot(x, y - lm["neck_y"]) is_front = y * base.FRONT_Y_SIGN > 0.0 drop = R_Z_DROP_FRONT if is_front else R_Z_DROP_BACK z_min = thr["collar_z_min"] - drop * lm["neck_len"] if z >= z_min and hd <= lm["roll_reach"] + lm["roll_out"] + 0.01: return (1.0, 0.0, 0.0, 0.0) # hood roll -> R return (0.0, 1.0, 0.0, 0.0) # body -> G def bake_hoodie_mask(shell, out_path, thr, lm, pocket_px): """Per-face region rasterization (like base.bake_region_mask) with the hoodie classifier, then the pocket texels overlaid into B.""" W = H = base.MASK_SIZE buf = np.zeros((H, W, 4), dtype=np.float32) buf[:, :, 1] = 1.0 # body-green background (bleed safety) bm = bmesh.new() bm.from_mesh(shell.data) bm.faces.ensure_lookup_table() uv_layer = bm.loops.layers.uv.active if uv_layer is None: raise RuntimeError("no active UV layer for region mask bake") counts = {"roll": 0, "body": 0, "sleeve": 0} for face in bm.faces: color = _classify_hoodie(face.calc_center_median(), thr, lm) if color[0] > 0.5: counts["roll"] += 1 elif color[2] > 0.5: counts["sleeve"] += 1 else: counts["body"] += 1 for a, b, c in base._tris_from_face(face, uv_layer): base._raster_tri(buf, a, b, c, color, W, H) bm.free() total = max(sum(counts.values()), 1) log("region faces: " + " ".join( f"{k}={v} ({100.0 * v / total:.1f}%)" for k, v in counts.items())) # Pocket joins the sleeve tint region (B), per the spec. if pocket_px is not None and pocket_px.shape == (H, W): buf[pocket_px] = (0.0, 0.0, 1.0, 0.0) img = bpy.data.images.new(f"{GARMENT_ID}_mask", W, H, alpha=True) img.pixels.foreach_set(buf.reshape(-1)) img.update() img.filepath_raw = out_path img.file_format = 'PNG' img.save() log(f"baked region mask -> {out_path}") # -------------------------------------------------------------------------- # Per-body authoring # -------------------------------------------------------------------------- def author_hoodie(body_dir, out_dir, body, offset): base.clear_scene() shell, armature = base.build_covered_mesh(body_dir) thr = base.derive_thresholds(armature) lm = derive_landmarks(armature, thr) wrist_cut(shell, lm) base.offset_outward(shell, offset) hood_roll(shell, thr, lm) base.solidify(shell, THICKNESS_M) maps = bake_paint_maps(shell, base.ALBEDO_SIZE, base.ALBEDO_SIZE) p = derive_paint_params(shell, thr, lm) pocket_px = pocket_mask(maps, p) albedo_img = make_painted_albedo(maps, pocket_px, p, out_dir, body) base.assign_fabric_material(shell, albedo_img) base.author_logo_uv(shell, thr) # Mask texels are MASK_SIZE; paint maps are ALBEDO_SIZE. Sizes match (512) # today; rebake the pocket mask if they ever diverge. mask_pocket = pocket_px if base.MASK_SIZE == base.ALBEDO_SIZE else None bake_hoodie_mask(shell, os.path.join(out_dir, f"{body}_mask.png"), thr, lm, mask_pocket) base.export_reference(shell, armature, os.path.join(out_dir, f"{body}.glb")) def main(): argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] if len(argv) < 2: print("Usage: -- [--bodies a,b,c] [--offset M]") sys.exit(1) bodies_root = argv[0] out_dir = argv[1] offset = OFFSET_M if "--offset" in argv: offset = float(argv[argv.index("--offset") + 1]) bodies = base.BODY_TYPES if "--bodies" in argv: bodies = [s.strip() for s in argv[argv.index("--bodies") + 1].split(",")] os.makedirs(out_dir, exist_ok=True) base.COVERED_SEGMENTS = SEGMENTS log(f"per-body hoodie: {len(bodies)} bodies, offset {offset*1000:.0f} mm") results = [] for body in bodies: body_dir = os.path.join(bodies_root, body) log(f"=== {body} ===") if not os.path.isdir(body_dir): results.append((body, "skipped: body dir missing")) continue try: author_hoodie(body_dir, out_dir, body, offset) results.append((body, "ok")) except Exception as exc: log(f"ERROR {body}: {exc}") import traceback traceback.print_exc() results.append((body, f"error: {exc}")) # Runtime fallbacks mirror the reference body (average_m). ref_mask = os.path.join(out_dir, f"{base.REFERENCE_BODY}_mask.png") if os.path.isfile(ref_mask): shutil.copy2(ref_mask, os.path.join(out_dir, "reference_mask.png")) log("copied reference_mask.png fallback") ref_albedo = os.path.join(out_dir, f"{base.REFERENCE_BODY}_base_albedo.png") if os.path.isfile(ref_albedo): shutil.copy2(ref_albedo, os.path.join(out_dir, "base_albedo.png")) log("copied base_albedo.png fallback") log("=" * 50) for body, status in results: log(f" {body:12s} {status}") ok = sum(1 for _, s in results if s == "ok") log(f"OK={ok}/{len(results)}") if ok != len(results): sys.exit(1) log("DONE") if __name__ == "__main__": main()