mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-15 21:46:29 +03:00
Complete EPUB creation, editing, validation, and knowledge extraction skill for the Agent Skills open format. Built from spec research, real EPUB testing on 2.1MB commercial Apress title, and Apple Books compatibility verification on macOS 26. Scripts (11): epub-scaffold — Create valid EPUB3 with cover XHTML, Apple Books CSS epub-edit — Surgical editing (8 subcommands, epublib) epub-info — Structure/metadata dump as JSON epub-text — Clean text extraction, per-chapter or single-file epub-extract-knowledge — Heuristic + LLM extraction (env var auto-detect) epub-validate — EPUBCheck or Python fallback validation epub-images — List/extract all images with cover detection epub-batch — Multi-file processing (extract-text, validate, metadata) epub-convert — EPUB2→EPUB3 conversion with validation epub-repair — Diagnose & auto-fix common structural issues epub-cover — Add cover XHTML wrapper for Apple Books compatibility References (9): epub-format-internals.md, python-libraries.md, spec-and-validation.md, tutorials-and-guides.md, agent-capability-discovery.md, fixed-layout-epub.md, accessibility.md, media-overlays.md, apple-books-compatibility.md (NEW — verified on macOS 26) Test: 46/46 passing (test_epub_skill.sh)
162 lines
5.9 KiB
Python
Executable File
162 lines
5.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""epub-batch — Process multiple EPUB files in batch.
|
|
|
|
Usage:
|
|
epub-batch extract-text <glob> --output DIR [--json] [--dry-run]
|
|
epub-batch validate <glob> [--json] [--dry-run]
|
|
epub-batch metadata <glob> --set-author "Name" [--set-title "Title"] [--output-dir DIR] [--json] [--dry-run]
|
|
epub-batch info <glob> [--json] [--dry-run]
|
|
|
|
Examples:
|
|
epub-batch extract-text "books/*.epub" --output texts/
|
|
epub-batch validate "books/*.epub" --json
|
|
epub-batch metadata "books/*.epub" --set-author "Fixed Author" --output-dir fixed/
|
|
|
|
Dependencies: EbookLib (pip install EbookLib)
|
|
"""
|
|
|
|
import argparse
|
|
import glob
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import warnings
|
|
|
|
warnings.filterwarnings("ignore")
|
|
|
|
DRY_RUN = False
|
|
JSON_OUTPUT = False
|
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
def die(msg):
|
|
print(f"Error: {msg}", file=sys.stderr)
|
|
sys.exit(1)
|
|
|
|
|
|
def emit(json_data, text=""):
|
|
if JSON_OUTPUT:
|
|
print(json.dumps(json_data, indent=2, default=str))
|
|
else:
|
|
print(text)
|
|
|
|
|
|
def find_epubs(pattern):
|
|
"""Expand glob pattern to list of EPUB files."""
|
|
files = sorted(glob.glob(pattern, recursive=True))
|
|
if not files:
|
|
die(f"No EPUB files found matching: {pattern}")
|
|
return [f for f in files if f.lower().endswith(".epub")]
|
|
|
|
|
|
def run_script(script_name, *args_list):
|
|
"""Run a sibling script from the scripts/ directory."""
|
|
script_path = os.path.join(SCRIPT_DIR, script_name)
|
|
cmd = [sys.executable, script_path] + list(args_list)
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
return result
|
|
|
|
|
|
def main():
|
|
global DRY_RUN, JSON_OUTPUT
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Process multiple EPUB files in batch.",
|
|
epilog="Examples:\n epub-batch extract-text 'books/*.epub' --output texts/\n epub-batch validate 'books/*.epub' --json")
|
|
sub = parser.add_subparsers(dest="command", help="Batch operation")
|
|
|
|
# extract-text
|
|
p = sub.add_parser("extract-text", help="Extract text from all EPUBs")
|
|
p.add_argument("pattern", help="Glob pattern for EPUB files")
|
|
p.add_argument("--output", "-o", required=True, help="Output directory")
|
|
p.add_argument("--json", action="store_true")
|
|
p.add_argument("--dry-run", "-n", action="store_true")
|
|
|
|
# validate
|
|
p = sub.add_parser("validate", help="Validate all EPUBs")
|
|
p.add_argument("pattern", help="Glob pattern for EPUB files")
|
|
p.add_argument("--json", action="store_true")
|
|
p.add_argument("--dry-run", "-n", action="store_true")
|
|
|
|
# metadata
|
|
p = sub.add_parser("metadata", help="Batch update metadata")
|
|
p.add_argument("pattern", help="Glob pattern for EPUB files")
|
|
p.add_argument("--set-author")
|
|
p.add_argument("--set-title")
|
|
p.add_argument("--output-dir", "-o", required=True, help="Output directory for updated EPUBs")
|
|
p.add_argument("--json", action="store_true")
|
|
p.add_argument("--dry-run", "-n", action="store_true")
|
|
|
|
# info
|
|
p = sub.add_parser("info", help="Dump info for all EPUBs")
|
|
p.add_argument("pattern", help="Glob pattern for EPUB files")
|
|
p.add_argument("--json", action="store_true")
|
|
p.add_argument("--dry-run", "-n", action="store_true")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if not args.command:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
JSON_OUTPUT = getattr(args, "json", False)
|
|
DRY_RUN = getattr(args, "dry_run", False)
|
|
|
|
if DRY_RUN:
|
|
emit({"status": "dry_run", "command": args.command, "pattern": args.pattern,
|
|
"files_found": "unknown (dry-run)"},
|
|
f"[dry-run] Would process EPUBs matching: {args.pattern}")
|
|
return
|
|
|
|
epub_files = find_epubs(args.pattern)
|
|
|
|
results = []
|
|
for i, epub_path in enumerate(epub_files):
|
|
print(f"[{i+1}/{len(epub_files)}] {os.path.basename(epub_path)}", file=sys.stderr, flush=True)
|
|
|
|
if args.command == "extract-text":
|
|
base = os.path.splitext(os.path.basename(epub_path))[0]
|
|
out_file = os.path.join(args.output, f"{base}.txt")
|
|
os.makedirs(args.output, exist_ok=True)
|
|
r = run_script("epub-text", epub_path, "--output", out_file)
|
|
results.append({"file": epub_path, "output": out_file, "success": r.returncode == 0})
|
|
|
|
elif args.command == "validate":
|
|
r = run_script("epub-validate", epub_path, "--json")
|
|
try:
|
|
vdata = json.loads(r.stdout)
|
|
results.append({"file": epub_path, "status": vdata.get("status", "unknown"),
|
|
"errors": vdata.get("errors", 0)})
|
|
except json.JSONDecodeError:
|
|
results.append({"file": epub_path, "status": "error", "errors": -1})
|
|
|
|
elif args.command == "metadata":
|
|
os.makedirs(args.output_dir, exist_ok=True)
|
|
out_epub = os.path.join(args.output_dir, os.path.basename(epub_path))
|
|
extra = []
|
|
if args.set_author:
|
|
extra += ["--author", args.set_author]
|
|
if args.set_title:
|
|
extra += ["--title", args.set_title]
|
|
r = run_script("epub-edit", "metadata", epub_path, *extra, "--output", out_epub)
|
|
results.append({"file": epub_path, "output": out_epub, "success": r.returncode == 0})
|
|
|
|
elif args.command == "info":
|
|
r = run_script("epub-info", epub_path, "--json")
|
|
try:
|
|
idata = json.loads(r.stdout)
|
|
results.append({"file": epub_path, "title": idata.get("metadata", {}).get("title", ""),
|
|
"manifest_count": idata.get("manifest_count", 0)})
|
|
except json.JSONDecodeError:
|
|
results.append({"file": epub_path, "error": "parse failed"})
|
|
|
|
# Summary
|
|
success = sum(1 for r in results if r.get("success", True))
|
|
summary = {"command": args.command, "total": len(epub_files), "results": results}
|
|
emit(summary, f"\n{success}/{len(epub_files)} processed successfully")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|