Update: 将子项目从 submodule 转为完整内容

- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
This commit is contained in:
freedak
2026-07-04 19:20:46 +08:00
parent 54d6465fa7
commit f7a720204a
3360 changed files with 802660 additions and 3 deletions
@@ -0,0 +1,463 @@
#!/usr/bin/env python3
"""
Morph PPT Helper Functions
Cross-platform replacement for morph-helpers.sh (Mac / Windows / Linux)
Usage (CLI):
python morph-helpers.py clone <deck> <from_slide> <to_slide>
python morph-helpers.py ghost <deck> <slide> <idx> [idx ...]
python morph-helpers.py verify <deck> <slide>
python morph-helpers.py final-check <deck>
Usage (import):
from morph_helpers import morph_clone_slide, morph_ghost_content, morph_verify_slide, morph_final_check
"""
import sys
import json
import subprocess
import argparse
import re
# Cross-platform color support (colorama optional)
try:
from colorama import init, Fore, Style
init(autoreset=True)
GREEN = Fore.GREEN
RED = Fore.RED
YELLOW = Fore.YELLOW
BLUE = Fore.CYAN
NC = Style.RESET_ALL
except ImportError:
GREEN = RED = YELLOW = BLUE = NC = ""
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _run(*args):
"""Run a command, return (returncode, stdout, stderr)."""
result = subprocess.run(list(args), capture_output=True, text=True)
return result.returncode, result.stdout, result.stderr
def _find_nested(data, key):
"""Recursively search a nested dict for a key, return its value or None."""
if isinstance(data, dict):
if key in data:
return data[key]
for v in data.values():
found = _find_nested(v, key)
if found is not None:
return found
return None
def _has_morph_transition(json_str):
"""Check whether JSON output from officecli contains transition=morph."""
if '"transition": "morph"' in json_str:
return True
try:
data = json.loads(json_str)
return _find_nested(data, "transition") == "morph"
except Exception:
return False
def _collect_shapes(children, callback):
"""Walk a shape tree depth-first, calling callback(child) for each node."""
for child in children:
callback(child)
if "children" in child:
_collect_shapes(child["children"], callback)
# ---------------------------------------------------------------------------
# morph_clone_slide
# ---------------------------------------------------------------------------
def morph_clone_slide(deck, from_slide, to_slide):
"""Clone slide and automatically set transition=morph, then verify.
Args:
deck: path to .pptx file
from_slide: source slide number (1-based)
to_slide: destination slide number (1-based)
"""
from_slide, to_slide = int(from_slide), int(to_slide)
print(f"{BLUE}Cloning slide {from_slide} -> {to_slide}...{NC}")
_run("officecli", "add", deck, "/", "--from", f"/slide[{from_slide}]")
print(f"{BLUE}Setting morph transition...{NC}")
_run("officecli", "set", deck, f"/slide[{to_slide}]", "--prop", "transition=morph")
print(f"{BLUE}Listing shapes for ghosting reference:{NC}")
rc, out, _ = _run("officecli", "get", deck, f"/slide[{to_slide}]", "--depth", "1")
print(out)
# Verify
print(f"{BLUE}Verifying transition...{NC}")
rc, out, _ = _run("officecli", "get", deck, f"/slide[{to_slide}]", "--json")
if not _has_morph_transition(out):
print(f"{RED}ERROR: Transition not set on slide {to_slide}!{NC}")
print(f"{RED} This slide will not have morph animation.{NC}")
sys.exit(1)
print(f"{GREEN}Transition verified on slide {to_slide}{NC}")
print()
# ---------------------------------------------------------------------------
# morph_ghost_content
# ---------------------------------------------------------------------------
def morph_ghost_content(deck, slide, *shapes):
"""Move shapes off-screen (x=36cm) to ghost them for morph animation.
Args:
deck: path to .pptx file
slide: slide number (1-based)
*shapes: one or more shape indices to ghost
"""
slide = int(slide)
shapes = [int(s) for s in shapes]
if not shapes:
print(f"{YELLOW}No shapes to ghost{NC}")
return
print(f"{BLUE}Ghosting {len(shapes)} content shape(s) on slide {slide}...{NC}")
for idx in shapes:
rc, _, _ = _run("officecli", "set", deck, f"/slide[{slide}]/shape[{idx}]", "--prop", "x=36cm")
if rc == 0:
print(f"{GREEN} Ghosted shape[{idx}]{NC}")
else:
print(f"{RED} Failed to ghost shape[{idx}]{NC}")
print(f"{GREEN}Ghosting complete{NC}")
print()
# ---------------------------------------------------------------------------
# morph_verify_slide
# ---------------------------------------------------------------------------
def _check_unghosted(data, prev_slide):
"""Return list of shapes with #s{prev_slide}- prefix not yet ghosted."""
unghosted = []
def visit(child):
name = child.get("format", {}).get("name", "")
x = child.get("format", {}).get("x", "")
path = child.get("path", "")
if f"#s{prev_slide}-" in name and x != "36cm":
unghosted.append(f"{path}: name={name}, x={x}")
if "children" in data:
_collect_shapes(data["children"], visit)
return unghosted
def _check_duplicates(prev_data, curr_data):
"""Return list of shapes with identical text+position on adjacent slides (excluding ghost zone)."""
SCENE_KEYWORDS = ["ring", "dot", "line", "circle", "rect", "slash",
"accent", "actor", "star", "triangle", "diamond"]
def extract(data):
boxes = []
def visit(child):
if child.get("type") != "textbox":
return
name = child.get("format", {}).get("name", "")
text = child.get("text", "").strip()
x = child.get("format", {}).get("x", "")
y = child.get("format", {}).get("y", "")
path = child.get("path", "")
if not text or len(text) < 6:
return
clean = name.replace("!!", "")
is_scene = any(kw in clean.lower() for kw in SCENE_KEYWORDS)
has_slide_pattern = any(f"s{i}-" in clean for i in range(1, 20))
if has_slide_pattern or not is_scene:
boxes.append({"path": path, "text": text[:50], "x": x, "y": y})
if "children" in data:
_collect_shapes(data["children"], visit)
return boxes
prev_boxes = extract(prev_data)
curr_boxes = extract(curr_data)
duplicates = []
for curr in curr_boxes:
for prev in prev_boxes:
if (curr["text"] == prev["text"]
and curr["x"] == prev["x"]
and curr["y"] == prev["y"]
and curr["x"] != "36cm"):
duplicates.append(
f"{curr['path']}: text='{curr['text']}...', pos=({curr['x']},{curr['y']})"
)
break
return duplicates
def morph_verify_slide(deck, slide):
"""Verify a slide has correct morph setup (transition + ghosting).
Uses two detection methods:
1. Name-based: shapes with #s{prev}- prefix must be at x=36cm
2. Duplicate text: same text+position on adjacent slides (catches missing # prefix)
Args:
deck: path to .pptx file
slide: slide number (1-based)
Returns:
True if all checks pass, False otherwise.
"""
slide = int(slide)
print(f"{BLUE}Verifying slide {slide}...{NC}")
has_error = False
# --- Check transition ---
rc, out, _ = _run("officecli", "get", deck, f"/slide[{slide}]", "--json")
curr_json_str = out
if not _has_morph_transition(curr_json_str):
print(f"{RED} Missing transition=morph{NC}")
print(f"{RED} Without this, slide will not animate!{NC}")
has_error = True
else:
print(f"{GREEN} Transition OK{NC}")
# --- Checks against previous slide ---
prev_slide = slide - 1
if prev_slide >= 1:
try:
curr_data = json.loads(curr_json_str).get("data", {})
# Method 1: name-based unghosted detection
unghosted = _check_unghosted(curr_data, prev_slide)
if unghosted:
print(f"{YELLOW} Warning: Found unghosted content from slide {prev_slide}:{NC}")
for item in unghosted:
print(f" {item}")
print(f"{YELLOW} These shapes should be ghosted to x=36cm{NC}")
has_error = True
else:
print(f"{GREEN} No unghosted content detected{NC}")
except Exception as e:
print(f"{RED} [helper] unghosted-check parse error: {e}{NC}", file=sys.stderr)
has_error = True
# Method 2: duplicate text/position detection (backup for missing # prefix)
try:
rc2, out2, _ = _run("officecli", "get", deck, f"/slide[{prev_slide}]", "--json")
prev_data = json.loads(out2).get("data", {})
curr_data = json.loads(curr_json_str).get("data", {})
duplicates = _check_duplicates(prev_data, curr_data)
if duplicates:
print(f"{YELLOW} Warning: Found duplicate content from slide {prev_slide} (same text at same position):{NC}")
for dup in duplicates:
print(f" {dup}")
print(f"{YELLOW} This might indicate:{NC}")
print(f"{YELLOW} 1. Content shapes missing '#sN-' prefix (can't detect for ghosting){NC}")
print(f"{YELLOW} 2. Forgot to ghost previous slide's content{NC}")
print(f"{YELLOW} 3. Forgot to add new content for this slide{NC}")
has_error = True
except Exception as e:
print(f"{RED} [helper] duplicate-check parse error: {e}{NC}", file=sys.stderr)
has_error = True
if not has_error:
print(f"{GREEN}Slide {slide} verification passed{NC}")
else:
print(f"{RED}Slide {slide} has issues - see above{NC}")
print()
return not has_error
# ---------------------------------------------------------------------------
# morph_final_check
# ---------------------------------------------------------------------------
def morph_final_check(deck):
"""Verify the entire deck: all slides (2+) must pass morph_verify_slide.
Also checks for M-2 ghost accumulation (shapes piled up at x≥34cm).
Args:
deck: path to .pptx file
Returns:
True if all slides pass, False otherwise.
"""
print(f"{BLUE}Final deck verification...{NC}")
print()
rc, out, _ = _run("officecli", "view", deck, "outline")
total_slides = 0
first_line = out.split("\n")[0] if out else ""
match = re.search(r"(\d+)\s+slides", first_line)
if match:
total_slides = int(match.group(1))
if total_slides == 0:
print(f"{RED}No slides found in deck{NC}")
return False
print(f"Total slides: {total_slides}")
print()
# --- New: Check for M-2 ghost accumulation ---
print(f"{BLUE}Checking ghost accumulation (M-2)...{NC}")
rc, out, _ = _run("officecli", "query", deck, "shape[x>=34cm]", "--json")
try:
data = json.loads(out).get("data", {})
ghost_count = len(data.get("results", []))
expected_max = max(50, total_slides * 4) # ~4 actors × slides
if ghost_count > expected_max:
print(f"{RED} REJECT: Found {ghost_count} accumulated ghost shapes (expected ≤ {expected_max}){NC}")
print(f"{RED} This is M-2 ghost accumulation — shapes moved to x≥34cm but not cleaned per-slide.{NC}")
print(f"{RED} See §Ghost Discipline & Actor Lifecycle in SKILL.md.{NC}")
return False
else:
print(f"{GREEN} Ghost count OK: {ghost_count} shapes (≤ {expected_max}){NC}")
except Exception as e:
print(f"{YELLOW} Warning: could not parse ghost count: {e}{NC}")
error_count = 0
for i in range(2, total_slides + 1):
if not morph_verify_slide(deck, i):
error_count += 1
print("=========================================")
if error_count == 0:
print(f"{GREEN}All slides verified successfully!{NC}")
print(f"{GREEN} Your morph animations should work correctly.{NC}")
return True
else:
print(f"{RED}Found issues in {error_count} slide(s){NC}")
print(f"{RED} Please fix the issues above before delivering.{NC}")
return False
# ---------------------------------------------------------------------------
# CLI entry point
# ---------------------------------------------------------------------------
def clean_ghost_accumulation(deck, threshold=50):
"""Remove ghost shapes exceeding threshold (M-2 fix).
Deletes shapes at x≥34cm, keeping only the first N (buffer for morph exit).
Args:
deck: path to .pptx
threshold: max ghosts to keep (default 50)
Returns:
Number of shapes deleted
"""
print(f"{BLUE}Cleaning ghost accumulation...{NC}")
rc, out, _ = _run("officecli", "query", deck, "shape[x>=34cm]", "--json")
try:
data = json.loads(out).get("data", {})
results = data.get("results", [])
ghost_count = len(results)
if ghost_count <= threshold:
print(f"{GREEN} Ghost count already OK: {ghost_count}{threshold}{NC}")
return 0
# Sort by slide (ascending) so we delete oldest/leftmost first
to_delete = results[threshold:]
print(f"{YELLOW} Deleting {len(to_delete)} shapes (keeping {threshold})...{NC}")
for shape in to_delete:
shape_id = shape.get("format", {}).get("id")
shape_name = shape.get("format", {}).get("name", "?")
if shape_id:
_run("officecli", "remove", deck, f"/shape[@id={shape_id}]")
print(f" Removed: {shape_name} ({shape_id})")
print(f"{GREEN} Cleaned {len(to_delete)} shapes. Verify with: final-check{NC}")
return len(to_delete)
except Exception as e:
print(f"{RED} Error: {e}{NC}", file=sys.stderr)
return 0
def main():
parser = argparse.ArgumentParser(
prog="morph-helpers.py",
description="Morph PPT Helper Functions — cross-platform (Mac / Windows / Linux)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
commands:
clone <deck> <from_slide> <to_slide> Clone slide and set morph transition
ghost <deck> <slide> <idx> [idx ...] Ghost multiple shapes off-screen (x=36cm)
verify <deck> <slide> Verify slide setup (transition + ghosting)
final-check <deck> Verify entire deck (+ M-2 ghost accumulation check)
clean-accumulation <deck> Remove excess ghost shapes (M-2 fix)
example:
python morph-helpers.py clone deck.pptx 1 2
python morph-helpers.py ghost deck.pptx 2 7 8 9
python morph-helpers.py verify deck.pptx 2
python morph-helpers.py final-check deck.pptx
python morph-helpers.py clean-accumulation deck.pptx
""",
)
sub = parser.add_subparsers(dest="command")
p = sub.add_parser("clone")
p.add_argument("deck")
p.add_argument("from_slide", type=int)
p.add_argument("to_slide", type=int)
p = sub.add_parser("ghost")
p.add_argument("deck")
p.add_argument("slide", type=int)
p.add_argument("shapes", nargs="+", type=int)
p = sub.add_parser("verify")
p.add_argument("deck")
p.add_argument("slide", type=int)
p = sub.add_parser("final-check")
p.add_argument("deck")
p = sub.add_parser("clean-accumulation")
p.add_argument("deck")
args = parser.parse_args()
if args.command == "clone":
morph_clone_slide(args.deck, args.from_slide, args.to_slide)
elif args.command == "ghost":
morph_ghost_content(args.deck, args.slide, *args.shapes)
elif args.command == "verify":
if not morph_verify_slide(args.deck, args.slide):
sys.exit(1)
elif args.command == "final-check":
if not morph_final_check(args.deck):
sys.exit(1)
elif args.command == "clean-accumulation":
clean_ghost_accumulation(args.deck)
else:
parser.print_help()
if __name__ == "__main__":
main()