/
DemienMedich
/
BodyweightBase
Обзор
Документация
Войти
/
DemienMedich
/
BodyweightBase
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
tools/slice_generated_exercise_sheets.py
423 строки
13 KB
DemienMedich
Improve Baduanjin movement guidance
18 июл 2026, 16:20
18 июл 2026, 16:20
86d0a76
Код
Авторство
О чём код?
from __future__ import annotations import argparse import json import math from pathlib import Path from PIL import Image ROOT = Path(__file__).resolve().parents[1] SHEET_DIR = ROOT / "Assets" / "GeneratedSheets" FRAME_DIR = ROOT / "Assets" / "ExerciseFrames" MANIFEST_PATH = SHEET_DIR / "exercise_generation_manifest.json" CATALOG_PATH = ROOT / "resources" / "exercises.json" SHEETS = [ { "file": "baduanjin_new_sheet.png", "columns": 4, "rows": 2, "cells": [ ("baduanjin_1", 1), ("baduanjin_2", 1), ("baduanjin_3", 1), ("baduanjin_4", 1), ("baduanjin_5", 1), ("baduanjin_6", 1), ("baduanjin_7", 1), ("baduanjin_8", 1), ], }, { "file": "baduanjin_1_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("baduanjin_1", frame) for frame in range(1, 5)], }, { "file": "baduanjin_2_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("baduanjin_2", frame) for frame in range(1, 5)], }, { "file": "baduanjin_3_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("baduanjin_3", frame) for frame in range(1, 5)], }, { "file": "baduanjin_4_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("baduanjin_4", frame) for frame in range(1, 5)], }, { "file": "qigong_forms_new_sheet.png", "columns": 3, "rows": 3, "cells": [ ("wuqin_xi_crane", 1), ("wuqin_xi_tiger", 1), ("wuqin_xi_monkey", 1), ("wuqin_xi_bear", 1), ("wuqin_xi_deer", 1), ("zhan_zhuang", 1), ("yijinjing_press", 1), ("liuzijue_breath", 1), ("silk_reeling", 1), ], }, { "file": "wing_chun_new_sheet.png", "columns": 5, "rows": 2, "cells": [ ("siu_lim_tao", 1), ("chum_kiu", 1), ("dan_chi_sao", 1), ("tan_sao_drill", 1), ("bong_sao_drill", 1), ("pak_sa_drill", 1), ("gaun_sa_drill", 1), ("footer_kick", 1), ("chain_punch_wall", 1), ("wu_sau_guard", 1), ], }, { "file": "siu_lim_tao_keyposes_a_sheet.png", "columns": 2, "rows": 2, "cells": [("siu_lim_tao", frame) for frame in range(1, 5)], }, { "file": "siu_lim_tao_keyposes_b_sheet.png", "columns": 2, "rows": 2, "cells": [("siu_lim_tao", frame) for frame in range(5, 9)], }, { "file": "tan_bong_drills_motion_sheet.png", "columns": 2, "rows": 2, "cells": [ ("tan_sao_drill", 1), ("tan_sao_drill", 2), ("bong_sao_drill", 1), ("bong_sao_drill", 2), ], }, { "file": "pak_gaun_drills_motion_sheet.png", "columns": 2, "rows": 2, "cells": [ ("pak_sa_drill", 1), ("pak_sa_drill", 2), ("gaun_sa_drill", 1), ("gaun_sa_drill", 2), ], }, { "file": "dan_chi_sao_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("dan_chi_sao", frame) for frame in range(1, 5)], }, { "file": "bodyweight_strength_new_sheet.png", "columns": 4, "rows": 3, "cells": [ ("prisoner_squat", 1), ("prisoner_squat", 2), ("pistol_squat_assist", 1), ("pistol_squat_assist", 2), ("glute_bridge_single", 1), ("glute_bridge_single", 2), ("step_up", 1), ("step_up", 2), ("pushup_archer", 1), ("pushup_archer", 2), ("pushup_pseudo_planche", 1), ("pushup_pseudo_planche", 2), ], }, { "file": "bodyweight_mobility_new_sheet.png", "columns": 4, "rows": 3, "cells": [ ("plank_to_pike", 1), ("plank_to_pike", 2), ("l_sit_hold", 1), ("wall_sit", 1), ("cossack_squat", 1), ("cossack_squat", 2), ("single_leg_calf", 1), ("single_leg_calf", 2), ("nordic_curl_neg", 1), ("nordic_curl_neg", 2), ("inchworm", 1), ("inchworm", 2), ], }, { "file": "inchworm_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("inchworm", frame) for frame in range(1, 5)], }, { "file": "warmup_arm_swing_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("warmup_arm_swing", frame) for frame in range(1, 5)], }, { "file": "warmup_march_place_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("warmup_march_place", frame) for frame in range(1, 5)], }, { "file": "dance_cardio_warmup_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("dance_cardio_warmup", frame) for frame in range(1, 5)], }, { "file": "warmup_shoulder_circle_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("warmup_shoulder_circle", frame) for frame in range(1, 5)], }, { "file": "warmup_wrist_roll_motion_sheet.png", "columns": 2, "rows": 2, "cells": [("warmup_wrist_roll", frame) for frame in range(1, 5)], }, { "file": "world_greatest_stretch_sheet.png", "columns": 4, "rows": 1, "cells": [ ("world_greatest_stretch", 1), ("world_greatest_stretch", 2), ("world_greatest_stretch", 3), ("world_greatest_stretch", 4), ], }, { "file": "hip_flexor_strength_sheet.png", "columns": 5, "rows": 2, "cells": [ ("standing_knee_drive", 1), ("standing_knee_drive", 2), ("high_knee_march", 1), ("high_knee_march", 2), ("supine_straight_leg_raise", 1), ("supine_straight_leg_raise", 2), ("seated_knee_tuck", 1), ("seated_knee_tuck", 2), ("dead_bug_hip_flexor_hold", 1), ("dead_bug_hip_flexor_hold", 2), ], }, { "file": "yijkym_stance_auditfix.png", "columns": 1, "rows": 1, "cells": [("yijkym_stance", 1)], }, { "file": "pistol_squat_assist_auditfix_sheet.png", "columns": 2, "rows": 1, "cells": [("pistol_squat_assist", 1), ("pistol_squat_assist", 2)], }, { "file": "burpee_interval_auditfix_sheet.png", "columns": 5, "rows": 1, "min_aspect": 0.45, "cells": [("burpee_interval", frame) for frame in range(1, 6)], }, { "file": "wall_handstand_auditfix_sheet.png", "columns": 4, "rows": 1, "cells": [("wall_handstand_hold", frame) for frame in range(1, 5)], }, { "file": "pad_strike_block_auditfix_sheet.png", "columns": 4, "rows": 1, "cells": [("pad_strike_block_round", frame) for frame in range(1, 5)], }, { "file": "mountain_climber_auditfix_sheet.png", "columns": 3, "rows": 1, "cells": [("mountain_climber", frame) for frame in range(1, 4)], }, { "file": "cossack_squat_auditfix_sheet.png", "columns": 2, "rows": 1, "cells": [("cossack_squat", 1), ("cossack_squat", 2)], }, { "file": "warmup_squat_auditfix_sheet.png", "columns": 2, "rows": 1, "cells": [("warmup_squat_to_stand", 1), ("warmup_squat_to_stand", 2)], }, { "file": "middle_split_auditfix_sheet.png", "columns": 2, "rows": 1, "cells": [("middle_split_prep", 1), ("middle_split_prep", 2)], }, { "file": "strikes_auditfix_grid.png", "columns": 3, "rows": 3, "cells": [ ("chain_punch_round", 1), ("chain_punch_round", 2), ("chain_punch_round", 3), ("footer_kick", 1), ("footer_kick", 2), ("footer_kick", 3), ("shadow_box_round", 1), ("shadow_box_round", 2), ("shadow_box_round", 3), ], }, ] def crop_cell(image: Image.Image, columns: int, rows: int, index: int) -> Image.Image: cell_width = image.width / columns cell_height = image.height / rows column = index % columns row = index // columns box = ( round(column * cell_width), round(row * cell_height), round((column + 1) * cell_width), round((row + 1) * cell_height), ) return image.crop(box) def pad_to_min_aspect(image: Image.Image, min_aspect: float) -> Image.Image: target_width = math.ceil(image.height * min_aspect) if image.width >= target_width: return image background = image.getpixel((0, 0)) padded = Image.new(image.mode, (target_width, image.height), background) padded.paste(image, ((target_width - image.width) // 2, 0)) return padded def load_manifest() -> list[dict[str, object]]: if not MANIFEST_PATH.exists(): return [] with MANIFEST_PATH.open("r", encoding="utf-8") as handle: data = json.load(handle) if not isinstance(data, list): raise ValueError(f"Unexpected manifest shape in {MANIFEST_PATH}") return data def load_catalog_metadata() -> dict[str, dict[str, object]]: with CATALOG_PATH.open("r", encoding="utf-8") as handle: catalog = json.load(handle) metadata: dict[str, dict[str, object]] = {} for exercise in catalog.get("exercises", []): exercise_id = exercise.get("id") if not exercise_id: continue metadata[str(exercise_id)] = { "name": exercise.get("name", ""), "category": exercise.get("category", ""), "equipment": exercise.get("equipment", ""), } return metadata def write_manifest(manifest: list[dict[str, object]]) -> None: MANIFEST_PATH.write_text( json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8", ) def main() -> None: parser = argparse.ArgumentParser(description="Slice generated exercise sheets into runtime frames.") parser.add_argument("--sheet", action="append", default=[], help="Process only this sheet filename; may be repeated.") args = parser.parse_args() FRAME_DIR.mkdir(parents=True, exist_ok=True) manifest = load_manifest() catalog_metadata = load_catalog_metadata() by_id = {str(item.get("id")): item for item in manifest} written: list[Path] = [] selected_sheets = [sheet for sheet in SHEETS if not args.sheet or sheet["file"] in args.sheet] if args.sheet and len(selected_sheets) != len(set(args.sheet)): known = {str(sheet["file"]) for sheet in selected_sheets} missing = sorted(set(args.sheet) - known) parser.error(f"unknown sheet(s): {', '.join(missing)}") for sheet in selected_sheets: sheet_path = SHEET_DIR / str(sheet["file"]) if not sheet_path.exists(): raise FileNotFoundError(sheet_path) with Image.open(sheet_path) as image: image = image.convert("RGB") cells = sheet["cells"] columns = int(sheet["columns"]) rows = int(sheet["rows"]) if len(cells) > columns * rows: raise ValueError(f"Too many cells configured for {sheet_path.name}") frame_counts: dict[str, int] = {} for index, (exercise_id, frame_number) in enumerate(cells): frame = crop_cell(image, columns, rows, index) if "min_aspect" in sheet: frame = pad_to_min_aspect(frame, float(sheet["min_aspect"])) target = FRAME_DIR / f"{exercise_id}_{frame_number}.png" frame.save(target, optimize=True) written.append(target) frame_counts[exercise_id] = max(frame_counts.get(exercise_id, 0), frame_number) for exercise_id, frames in frame_counts.items(): item = by_id.get(exercise_id, {"id": exercise_id}) item.update({key: value for key, value in catalog_metadata.get(exercise_id, {}).items() if value}) item["frames"] = frames item["source"] = sheet_path.name by_id[exercise_id] = item existing_order = [str(item.get("id")) for item in manifest if item.get("id")] new_ids = [exercise_id for sheet in selected_sheets for exercise_id, _ in sheet["cells"]] ordered_ids = existing_order + [exercise_id for exercise_id in dict.fromkeys(new_ids) if exercise_id not in existing_order] write_manifest([by_id[exercise_id] for exercise_id in ordered_ids if exercise_id in by_id]) print(f"Wrote {len(written)} frames") for path in written: print(path.relative_to(ROOT)) if __name__ == "__main__": main()