assets-metadata-remover 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assets_metadata_remover-1.0.0/.agents/kyro/kyro.json +18 -0
- assets_metadata_remover-1.0.0/.atl/.skill-registry.cache.json +3 -0
- assets_metadata_remover-1.0.0/.atl/skill-registry.md +74 -0
- assets_metadata_remover-1.0.0/.gitignore +28 -0
- assets_metadata_remover-1.0.0/Makefile +40 -0
- assets_metadata_remover-1.0.0/PKG-INFO +122 -0
- assets_metadata_remover-1.0.0/README.md +97 -0
- assets_metadata_remover-1.0.0/pyproject.toml +40 -0
- assets_metadata_remover-1.0.0/src/metadata_remover/__init__.py +3 -0
- assets_metadata_remover-1.0.0/src/metadata_remover/cli.py +273 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"artifactRoot": ".agents/kyro/scopes",
|
|
4
|
+
"scopes": [],
|
|
5
|
+
"activeScope": null,
|
|
6
|
+
"runtimeVersion": "3.2.2",
|
|
7
|
+
"runtimePath": "~/.agents/kyro/current",
|
|
8
|
+
"installedAdapters": [
|
|
9
|
+
{
|
|
10
|
+
"agent": "standard",
|
|
11
|
+
"scope": "workspace",
|
|
12
|
+
"installedAt": "2026-06-14T03:55:56.047Z",
|
|
13
|
+
"corePath": "~/.agents/kyro/current",
|
|
14
|
+
"commandsPath": "~/.agents/skills",
|
|
15
|
+
"skillsPath": "~/.agents/skills"
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Skill Registry — metadata-remover
|
|
2
|
+
|
|
3
|
+
<!-- Auto-generated by gentle-ai skill-registry refresh. Run `gentle-ai skill-registry refresh --force` to regenerate. -->
|
|
4
|
+
|
|
5
|
+
Last updated: 2026-06-14
|
|
6
|
+
|
|
7
|
+
## Sources scanned
|
|
8
|
+
|
|
9
|
+
- /Users/rperaza/.agents/skills
|
|
10
|
+
- /Users/rperaza/.config/opencode/skills
|
|
11
|
+
- /Users/rperaza/.claude/skills
|
|
12
|
+
- /Users/rperaza/.gemini/skills
|
|
13
|
+
- /Users/rperaza/.gemini/antigravity/skills
|
|
14
|
+
- /Users/rperaza/.cursor/skills
|
|
15
|
+
- /Users/rperaza/.copilot/skills
|
|
16
|
+
- /Users/rperaza/.codex/skills
|
|
17
|
+
- /Users/rperaza/.codeium/windsurf/skills
|
|
18
|
+
- /Users/rperaza/.qwen/skills
|
|
19
|
+
- /Users/rperaza/.kiro/skills
|
|
20
|
+
|
|
21
|
+
## Contract
|
|
22
|
+
|
|
23
|
+
**Delegator use only.** This registry is an index, not a summary. Any agent that launches subagents reads it to select relevant skills, then passes exact `SKILL.md` paths for the subagent to read before work.
|
|
24
|
+
|
|
25
|
+
`SKILL.md` remains the source of truth. Do not inject generated summaries or compact rules by default; pass paths so subagents load the full runtime contract and preserve author intent.
|
|
26
|
+
|
|
27
|
+
## Skills
|
|
28
|
+
|
|
29
|
+
| Skill | Trigger / description | Scope | Path |
|
|
30
|
+
| --- | --- | --- | --- |
|
|
31
|
+
| `branch-pr` | Create Gentle AI pull requests with issue-first checks. Trigger: creating, opening, or preparing PRs for review. | user | `/Users/rperaza/.config/opencode/skills/branch-pr/SKILL.md` |
|
|
32
|
+
| `chained-pr` | Trigger: PRs over 400 lines, stacked PRs, review slices. Split oversized changes into chained PRs that protect review focus. | user | `/Users/rperaza/.config/opencode/skills/chained-pr/SKILL.md` |
|
|
33
|
+
| `clean-ddd-hexagonal` | Proactively apply when designing APIs, microservices, or scalable backend structure. Triggers on DDD, Clean Architecture, Hexagonal, ports and adapters, entities, value objects, domain events, CQRS, event sourcing, repository pattern, use cases, onion architecture, outbox pattern, aggregate root, anti-corruption layer. Use when working with domain models, aggregates, repositories, or bounded contexts. Clean Architecture + DDD + Hexagonal patterns for backend services, language-agnostic (Go, Rust, Python, TypeScript, Java, C#). | user | `/Users/rperaza/.agents/skills/clean-ddd-hexagonal/SKILL.md` |
|
|
34
|
+
| `code-analyzer` | Analyzes code modules and generates structured technical reports with architecture diagrams. Trigger: When the user asks to analyze, explain, or document a module, file, or codebase section. | user | `/Users/rperaza/.agents/skills/code-analyzer/SKILL.md` |
|
|
35
|
+
| `cognitive-doc-design` | Design docs that reduce cognitive load. Trigger: writing guides, READMEs, RFCs, onboarding, architecture, or review-facing docs. | user | `/Users/rperaza/.config/opencode/skills/cognitive-doc-design/SKILL.md` |
|
|
36
|
+
| `comment-writer` | Write warm, direct collaboration comments. Trigger: PR feedback, issue replies, reviews, Slack messages, or GitHub comments. | user | `/Users/rperaza/.config/opencode/skills/comment-writer/SKILL.md` |
|
|
37
|
+
| `dead-code-auditor` | Rigorous dead code audit for any module, folder, or file in any programming language. Detects orphan files never imported anywhere, classes/functions/ methods declared but never called, constructor parameters received but never consumed, unused imports/requires, private fields with no references, and commented-out code blocks. Use this skill whenever the user asks to: review unused code, clean up a feature after a refactor, find dead code, detect orphan files or classes, audit what can be deleted, find what's left over after a big change, or any variation of "what's not being used / what can I remove". Also triggers when the user says they made large changes and wants to know what became obsolete. IMPORTANT: This skill only reports — it never deletes anything. At the end it always offers to generate a removal plan with /plan. | user | `/Users/rperaza/.agents/skills/dead-code-auditor/SKILL.md` |
|
|
38
|
+
| `file-search` | Use when working with ANY codebase search — text patterns, structural/AST code search, finding files by name, searching non-code files (PDFs, archives), or analyzing codebase size and language breakdown. | user | `/Users/rperaza/.agents/skills/file-search/SKILL.md` |
|
|
39
|
+
| `find-skills` | Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill. | user | `/Users/rperaza/.agents/skills/find-skills/SKILL.md` |
|
|
40
|
+
| `go-testing` | Trigger: Go tests, go test coverage, Bubbletea teatest, golden files. Apply focused Go testing patterns. | user | `/Users/rperaza/.config/opencode/skills/go-testing/SKILL.md` |
|
|
41
|
+
| `golang-pro` | Implements concurrent Go patterns using goroutines and channels, designs and builds microservices with gRPC or REST, optimizes Go application performance with pprof, and enforces idiomatic Go with generics, interfaces, and robust error handling. Use when building Go applications requiring concurrent programming, microservices architecture, or high-performance systems. Invoke for goroutines, channels, Go generics, gRPC integration, CLI tools, benchmarks, or table-driven testing. | user | `/Users/rperaza/.agents/skills/golang-pro/SKILL.md` |
|
|
42
|
+
| `growth-architect` | AI Co-Founder & Growth Architect: strategic clarity, product vision, MVP design, and architecture decisions (ADRs) before execution begins. Trigger: When user needs strategic advice, MVP validation, market analysis, product vision, or architecture decisions — before generating any execution plan. | user | `/Users/rperaza/.agents/skills/growth-architect/SKILL.md` |
|
|
43
|
+
| `growth-ceo` | Elite tech CEO strategist that thinks like Musk, Bezos, Altman, Huang, and Thiel combined. Generates billion-dollar-scale strategic initiatives, product visions, and growth plays using first principles, 7 Powers, flywheels, and exponential thinking. Use this skill whenever the user discusses product strategy, business decisions, growth challenges, competitive positioning, or asks "what should we build" — even if they don't explicitly ask for "strategy". This includes: scaling from N to 10N users, what to build vs NOT build, MVP decisions, feature prioritization, competitive differentiation, enterprise vs self-serve, go-to-market, pivoting, revenue strategy, reducing churn, positioning against competitors, fundraising strategy, team building, platform plays, or any question where the user needs a founder/CEO-level perspective. If the user describes their product and asks "what should I do" — use this skill. Think big. Resources can be acquired. The vision comes first. | user | `/Users/rperaza/.agents/skills/growth-ceo/SKILL.md` |
|
|
44
|
+
| `hatch-pet` | Create, repair, validate, visually QA, and package Codex-compatible animated pets and pet spritesheets from character art, generated images, company or prospect brand cues, or visual references. Use when a user wants a lightweight-worker Codex pet workflow, a non-pixel custom pet style, a prospect or company mascot pet, or a full 8x9 animated pet atlas with transparent unused cells, QA contact sheets, and pet.json packaging. This skill composes the installed $imagegen system skill for visual generation and uses bundled scripts for deterministic spritesheet assembly. | user | `/Users/rperaza/.codex/skills/hatch-pet/SKILL.md` |
|
|
45
|
+
| `imagegen` | Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas. | user | `/Users/rperaza/.codex/skills/.system/imagegen/SKILL.md` |
|
|
46
|
+
| `issue-creation` | Create Gentle AI issues with issue-first checks. Trigger: creating GitHub issues, bug reports, or feature requests. | user | `/Users/rperaza/.config/opencode/skills/issue-creation/SKILL.md` |
|
|
47
|
+
| `judgment-day` | Trigger: judgment day, dual review, adversarial review, juzgar. Run blind dual review, fix confirmed issues, then re-judge. | user | `/Users/rperaza/.config/opencode/skills/judgment-day/SKILL.md` |
|
|
48
|
+
| `kyro-forge` | Run the Kyro forge workflow through the installed workspace harness | user | `/Users/rperaza/.agents/skills/kyro-forge/SKILL.md` |
|
|
49
|
+
| `kyro-status` | Show Kyro project status through the installed workspace harness | user | `/Users/rperaza/.agents/skills/kyro-status/SKILL.md` |
|
|
50
|
+
| `kyro-wrap-up` | Close the Kyro session through the installed workspace harness | user | `/Users/rperaza/.agents/skills/kyro-wrap-up/SKILL.md` |
|
|
51
|
+
| `microservices-patterns` | Design microservices architectures with service boundaries, event-driven communication, and resilience patterns. Use when building distributed systems, decomposing monoliths, or implementing microservices. | user | `/Users/rperaza/.agents/skills/microservices-patterns/SKILL.md` |
|
|
52
|
+
| `migrate-to-codex` | Migrate supported instruction files, skills, agents, and MCP config into Codex project and global files. | user | `/Users/rperaza/.codex/skills/migrate-to-codex/SKILL.md` |
|
|
53
|
+
| `obsidian` | Unified Obsidian vault operations: sync documents to vault, read notes for context, search knowledge, and validate markdown standards. Filesystem-based, no MCP required. Trigger: When user wants to read from or write to Obsidian vault. | user | `/Users/rperaza/.agents/skills/obsidian/SKILL.md` |
|
|
54
|
+
| `openai-docs` | Use when the user asks how to build with OpenAI products or APIs, asks about Codex itself or choosing Codex surfaces, needs up-to-date official documentation with citations, help choosing the latest model for a use case, or model upgrade and prompt-upgrade guidance; use OpenAI docs MCP tools for non-Codex docs questions, use the Codex manual helper first for broad Codex self-knowledge, and restrict fallback browsing to official OpenAI domains. | user | `/Users/rperaza/.codex/skills/.system/openai-docs/SKILL.md` |
|
|
55
|
+
| `plugin-creator` | Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow. | user | `/Users/rperaza/.codex/skills/.system/plugin-creator/SKILL.md` |
|
|
56
|
+
| `pretty-mermaid` | Render beautiful Mermaid diagrams as SVG or ASCII art using the beautiful-mermaid library. Supports 15+ themes, 5 diagram types (flowchart, sequence, state, class, ER), and ultra-fast rendering. Use this skill when: 1. User asks to "render a mermaid diagram" or provides .mmd files 2. User requests "create a flowchart/sequence diagram/state diagram" 3. User wants to "apply a theme" or "beautify a diagram" 4. User needs to "batch process multiple diagrams" 5. User mentions "ASCII diagram" or "terminal-friendly diagram" 6. User wants to visualize architecture, workflows, or data models | user | `/Users/rperaza/.agents/skills/pretty-mermaid/SKILL.md` |
|
|
57
|
+
| `project-brain` | Session memory for AI agents — load context at the start, save sessions at the end, evolve knowledge across sessions. Like a professional's notebook: open before work, write a summary when done, persist between sessions. Trigger: When starting a session and need to recover context, or ending a session and want to save what happened. | user | `/Users/rperaza/.agents/skills/project-brain/SKILL.md` |
|
|
58
|
+
| `prompt-improver` | Analyze and improve prompts using Claude's official prompting best practices. Use this skill whenever the user wants to improve, refine, review, or optimize a prompt — whether it's a system prompt, a user prompt, an API prompt, or instructions for an AI agent. Also trigger when the user shares a raw prompt and asks for feedback, says 'make this prompt better', 'optimize my prompt', 'review this prompt', or pastes a prompt and asks what's wrong with it. Even if the user just says 'improve this' while sharing text that looks like a prompt or instruction set, use this skill. | user | `/Users/rperaza/.agents/skills/prompt-improver/SKILL.md` |
|
|
59
|
+
| `qa-review` | — | user | `/Users/rperaza/.agents/skills/qa-review/SKILL.md` |
|
|
60
|
+
| `quant-playbook` | Quantitative prediction & trading playbook: proper evaluation metrics, model selection, backtesting methodology, risk management, and calibration for binary prediction systems. Trigger: When building, evaluating, or improving any prediction model, trading bot, forecasting system, or binary market strategy. | user | `/Users/rperaza/.agents/skills/quant-playbook/SKILL.md` |
|
|
61
|
+
| `react-19` | React 19 patterns with React Compiler. Trigger: When writing React 19 components/hooks in .tsx (React Compiler rules, hook patterns, refs as props). If using Next.js App Router/Server Actions, also use nextjs-15. | user | `/Users/rperaza/.agents/skills/react-19/SKILL.md` |
|
|
62
|
+
| `skill-creator` | Trigger: new skills, agent instructions, documenting AI usage patterns. Create LLM-first skills with valid frontmatter. | user | `/Users/rperaza/.config/opencode/skills/skill-creator/SKILL.md` |
|
|
63
|
+
| `skill-improver` | Trigger: improve skills, audit skills, refactor skills, skill quality. Audit and upgrade existing LLM-first skills. | user | `/Users/rperaza/.config/opencode/skills/skill-improver/SKILL.md` |
|
|
64
|
+
| `skill-installer` | Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). | user | `/Users/rperaza/.codex/skills/.system/skill-installer/SKILL.md` |
|
|
65
|
+
| `typescript` | TypeScript strict patterns and best practices. Trigger: When implementing or refactoring TypeScript in .ts/.tsx (types, interfaces, generics, const maps, type guards, removing any, tightening unknown). | user | `/Users/rperaza/.agents/skills/typescript/SKILL.md` |
|
|
66
|
+
| `typescript-advanced-types` | Master TypeScript's advanced type system including generics, conditional types, mapped types, template literals, and utility types for building type-safe applications. Use when implementing complex type logic, creating reusable type utilities, or ensuring compile-time type safety in TypeScript projects. | user | `/Users/rperaza/.agents/skills/typescript-advanced-types.bak/SKILL.md` |
|
|
67
|
+
| `work-unit-commits` | Plan commits as reviewable work units. Trigger: implementation, commit splitting, chained PRs, or keeping tests and docs with code. | user | `/Users/rperaza/.config/opencode/skills/work-unit-commits/SKILL.md` |
|
|
68
|
+
|
|
69
|
+
## Loading protocol
|
|
70
|
+
|
|
71
|
+
1. Match task context and target files against the `Trigger / description` column.
|
|
72
|
+
2. Pass only the matching `Path` values to the subagent under `## Skills to load before work`.
|
|
73
|
+
3. Instruct the subagent to read those exact `SKILL.md` files before reading, writing, reviewing, testing, or creating artifacts.
|
|
74
|
+
4. If no matching skill exists, proceed without project skill injection and report `skill_resolution: none`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
PLAN.md
|
|
2
|
+
|
|
3
|
+
# Python
|
|
4
|
+
__pycache__/
|
|
5
|
+
*.py[cod]
|
|
6
|
+
*$py.class
|
|
7
|
+
*.so
|
|
8
|
+
.Python
|
|
9
|
+
build/
|
|
10
|
+
dist/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
*.egg
|
|
13
|
+
|
|
14
|
+
# Virtual environments
|
|
15
|
+
.venv/
|
|
16
|
+
venv/
|
|
17
|
+
ENV/
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
*.swp
|
|
23
|
+
*.swo
|
|
24
|
+
*~
|
|
25
|
+
|
|
26
|
+
# OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
PYTHON := python3
|
|
2
|
+
SCRIPT := metadata_remover.py
|
|
3
|
+
INPUT ?= .
|
|
4
|
+
OUTPUT ?=
|
|
5
|
+
|
|
6
|
+
FLAGS :=
|
|
7
|
+
ifdef VERBOSE
|
|
8
|
+
FLAGS += -v
|
|
9
|
+
endif
|
|
10
|
+
ifdef VERIFY
|
|
11
|
+
FLAGS += --verify
|
|
12
|
+
endif
|
|
13
|
+
ifdef OUTPUT
|
|
14
|
+
FLAGS += -o $(OUTPUT)
|
|
15
|
+
endif
|
|
16
|
+
|
|
17
|
+
.PHONY: help run dry-run verify test clean
|
|
18
|
+
|
|
19
|
+
help:
|
|
20
|
+
@$(PYTHON) $(SCRIPT) --help
|
|
21
|
+
|
|
22
|
+
run:
|
|
23
|
+
$(PYTHON) $(SCRIPT) $(INPUT) $(FLAGS)
|
|
24
|
+
|
|
25
|
+
dry-run:
|
|
26
|
+
$(PYTHON) $(SCRIPT) $(INPUT) --dry-run $(FLAGS)
|
|
27
|
+
|
|
28
|
+
verify:
|
|
29
|
+
$(PYTHON) $(SCRIPT) $(INPUT) --verify $(FLAGS)
|
|
30
|
+
|
|
31
|
+
test:
|
|
32
|
+
@echo "=== Smoke test ===" && $(PYTHON) $(SCRIPT) --help > /dev/null && echo "PASS"
|
|
33
|
+
@echo "=== Dry run ===" && mkdir -p /tmp/mr_test/sub && $(PYTHON) $(SCRIPT) /tmp/mr_test --dry-run -v && echo "PASS"
|
|
34
|
+
@echo "=== Real run ===" && rm -rf /tmp/mr_test_clean && $(PYTHON) $(SCRIPT) /tmp/mr_test -v --verify && echo "PASS"
|
|
35
|
+
@echo "=== Empty dir ===" && mkdir -p /tmp/mr_empty && $(PYTHON) $(SCRIPT) /tmp/mr_empty && echo "PASS"
|
|
36
|
+
@echo "=== Non-existent ===" && $(PYTHON) $(SCRIPT) /tmp/mr_nonexistent 2>&1; echo "Exit: $$?"
|
|
37
|
+
@echo "=== All tests done ==="
|
|
38
|
+
|
|
39
|
+
clean:
|
|
40
|
+
rm -rf /tmp/mr_test /tmp/mr_test_clean /tmp/mr_empty /tmp/mr_empty_clean
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: assets-metadata-remover
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Remove metadata from images and videos recursively
|
|
5
|
+
Project-URL: Homepage, https://github.com/synapsync/assets-metadata-remover
|
|
6
|
+
Project-URL: Repository, https://github.com/synapsync/assets-metadata-remover
|
|
7
|
+
Author: Synapsync
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: exif,images,metadata,privacy,videos
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
21
|
+
Classifier: Topic :: Multimedia :: Video
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# assets-metadata-remover
|
|
27
|
+
|
|
28
|
+
Command-line tool that removes metadata from images and videos recursively, generating clean copies in an output folder that mirrors the original structure. Original files are **never** modified.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
### With pipx (recommended)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pipx install assets-metadata-remover
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### With pip
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install assets-metadata-remover
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### System dependencies
|
|
45
|
+
|
|
46
|
+
- **ffmpeg** — required for video processing
|
|
47
|
+
- **exiftool** — required for image processing
|
|
48
|
+
|
|
49
|
+
Install with Homebrew:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
brew install ffmpeg exiftool
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> If a tool is missing, the script detects it at startup and processes only the files it can, showing a clear warning.
|
|
56
|
+
|
|
57
|
+
## Basic usage
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
assets-metadata-remover /path/to/your_files
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
This creates `/path/to/your_files_clean/` with the same folder structure and metadata-free files.
|
|
64
|
+
|
|
65
|
+
### Options
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
usage: assets-metadata-remover [-h] [-o OUTPUT] [--dry-run] [-v] [--verify] input
|
|
69
|
+
|
|
70
|
+
positional:
|
|
71
|
+
input Directory (or file) to process
|
|
72
|
+
|
|
73
|
+
options:
|
|
74
|
+
-o, --output DIR Output directory (default: <input>_clean)
|
|
75
|
+
--dry-run Simulate without writing
|
|
76
|
+
-v, --verbose Per-file logging
|
|
77
|
+
--verify Re-inspect copies and report residual metadata
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Examples
|
|
81
|
+
|
|
82
|
+
Process a directory with custom output:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
assets-metadata-remover ~/Photos -o ~/Photos_clean
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Simulate without writing (dry run):
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
assets-metadata-remover ~/Photos --dry-run
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Verbose mode with cleanup verification:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
assets-metadata-remover ~/Photos -v --verify
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Process a single file:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
assets-metadata-remover ~/photo.jpg -o ~/clean/
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Safety
|
|
107
|
+
|
|
108
|
+
Original files are **never** modified. The script always writes to a separate output folder.
|
|
109
|
+
|
|
110
|
+
## Supported formats
|
|
111
|
+
|
|
112
|
+
**Images:** JPG, JPEG, PNG, TIFF, TIF, WebP, HEIC, HEIF, GIF, BMP
|
|
113
|
+
|
|
114
|
+
**Videos:** MP4, MOV, MKV, AVI, M4V, WebM, WMV, FLV, 3GP
|
|
115
|
+
|
|
116
|
+
Extension comparison is case-insensitive (`.JPG` and `.jpg` are equivalent).
|
|
117
|
+
|
|
118
|
+
## Limitations
|
|
119
|
+
|
|
120
|
+
- Videos are copied without re-encoding (`-c copy`), which is fast and lossless, but some exotic formats may not be compatible with the output container.
|
|
121
|
+
- Some metadata embedded in proprietary formats may not be fully removed by `exiftool`.
|
|
122
|
+
- Directory symlinks are not followed to avoid loops.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# assets-metadata-remover
|
|
2
|
+
|
|
3
|
+
Command-line tool that removes metadata from images and videos recursively, generating clean copies in an output folder that mirrors the original structure. Original files are **never** modified.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
### With pipx (recommended)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pipx install assets-metadata-remover
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### With pip
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install assets-metadata-remover
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### System dependencies
|
|
20
|
+
|
|
21
|
+
- **ffmpeg** — required for video processing
|
|
22
|
+
- **exiftool** — required for image processing
|
|
23
|
+
|
|
24
|
+
Install with Homebrew:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
brew install ffmpeg exiftool
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
> If a tool is missing, the script detects it at startup and processes only the files it can, showing a clear warning.
|
|
31
|
+
|
|
32
|
+
## Basic usage
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
assets-metadata-remover /path/to/your_files
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This creates `/path/to/your_files_clean/` with the same folder structure and metadata-free files.
|
|
39
|
+
|
|
40
|
+
### Options
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
usage: assets-metadata-remover [-h] [-o OUTPUT] [--dry-run] [-v] [--verify] input
|
|
44
|
+
|
|
45
|
+
positional:
|
|
46
|
+
input Directory (or file) to process
|
|
47
|
+
|
|
48
|
+
options:
|
|
49
|
+
-o, --output DIR Output directory (default: <input>_clean)
|
|
50
|
+
--dry-run Simulate without writing
|
|
51
|
+
-v, --verbose Per-file logging
|
|
52
|
+
--verify Re-inspect copies and report residual metadata
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Examples
|
|
56
|
+
|
|
57
|
+
Process a directory with custom output:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
assets-metadata-remover ~/Photos -o ~/Photos_clean
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Simulate without writing (dry run):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
assets-metadata-remover ~/Photos --dry-run
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Verbose mode with cleanup verification:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
assets-metadata-remover ~/Photos -v --verify
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Process a single file:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
assets-metadata-remover ~/photo.jpg -o ~/clean/
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Safety
|
|
82
|
+
|
|
83
|
+
Original files are **never** modified. The script always writes to a separate output folder.
|
|
84
|
+
|
|
85
|
+
## Supported formats
|
|
86
|
+
|
|
87
|
+
**Images:** JPG, JPEG, PNG, TIFF, TIF, WebP, HEIC, HEIF, GIF, BMP
|
|
88
|
+
|
|
89
|
+
**Videos:** MP4, MOV, MKV, AVI, M4V, WebM, WMV, FLV, 3GP
|
|
90
|
+
|
|
91
|
+
Extension comparison is case-insensitive (`.JPG` and `.jpg` are equivalent).
|
|
92
|
+
|
|
93
|
+
## Limitations
|
|
94
|
+
|
|
95
|
+
- Videos are copied without re-encoding (`-c copy`), which is fast and lossless, but some exotic formats may not be compatible with the output container.
|
|
96
|
+
- Some metadata embedded in proprietary formats may not be fully removed by `exiftool`.
|
|
97
|
+
- Directory symlinks are not followed to avoid loops.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "assets-metadata-remover"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Remove metadata from images and videos recursively"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Synapsync" }
|
|
14
|
+
]
|
|
15
|
+
keywords = ["metadata", "exif", "privacy", "images", "videos"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 5 - Production/Stable",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: End Users/Desktop",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: Multimedia :: Graphics",
|
|
28
|
+
"Topic :: Multimedia :: Video",
|
|
29
|
+
"Topic :: Security",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
assets-metadata-remover = "metadata_remover.cli:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/synapsync/assets-metadata-remover"
|
|
37
|
+
Repository = "https://github.com/synapsync/assets-metadata-remover"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/metadata_remover"]
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import os
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
IMAGE_EXTS = {".jpg", ".jpeg", ".png", ".tiff", ".tif", ".webp", ".heic", ".heif", ".gif", ".bmp"}
|
|
10
|
+
VIDEO_EXTS = {".mp4", ".mov", ".mkv", ".avi", ".m4v", ".webm", ".wmv", ".flv", ".3gp"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_args():
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
description="Remove metadata from images and videos recursively."
|
|
16
|
+
)
|
|
17
|
+
parser.add_argument("input", help="Directory (or file) to process")
|
|
18
|
+
parser.add_argument("-o", "--output", help="Output directory (default: <input>_clean)")
|
|
19
|
+
parser.add_argument("--dry-run", action="store_true", help="Simulate without writing")
|
|
20
|
+
parser.add_argument("-v", "--verbose", action="store_true", help="Per-file logging")
|
|
21
|
+
parser.add_argument("--verify", action="store_true", help="Re-inspect copies and report residual metadata")
|
|
22
|
+
return parser.parse_args()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check_tools():
|
|
26
|
+
return {
|
|
27
|
+
"exiftool": shutil.which("exiftool") is not None,
|
|
28
|
+
"ffmpeg": shutil.which("ffmpeg") is not None,
|
|
29
|
+
"ffprobe": shutil.which("ffprobe") is not None,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def collect_files(root):
|
|
34
|
+
root = Path(root).resolve()
|
|
35
|
+
results = []
|
|
36
|
+
if root.is_file():
|
|
37
|
+
ext = root.suffix.lower()
|
|
38
|
+
if ext in IMAGE_EXTS:
|
|
39
|
+
results.append((root, "image"))
|
|
40
|
+
elif ext in VIDEO_EXTS:
|
|
41
|
+
results.append((root, "video"))
|
|
42
|
+
else:
|
|
43
|
+
results.append((root, "unsupported"))
|
|
44
|
+
return results
|
|
45
|
+
|
|
46
|
+
for dirpath, dirnames, filenames in os.walk(str(root), followlinks=False):
|
|
47
|
+
dirnames[:] = [d for d in dirnames if not os.path.islink(os.path.join(dirpath, d))]
|
|
48
|
+
for fname in filenames:
|
|
49
|
+
fpath = Path(dirpath) / fname
|
|
50
|
+
ext = fpath.suffix.lower()
|
|
51
|
+
if ext in IMAGE_EXTS:
|
|
52
|
+
results.append((fpath, "image"))
|
|
53
|
+
elif ext in VIDEO_EXTS:
|
|
54
|
+
results.append((fpath, "video"))
|
|
55
|
+
else:
|
|
56
|
+
results.append((fpath, "unsupported"))
|
|
57
|
+
return results
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def clean_image(src, dst):
|
|
61
|
+
if dst.exists():
|
|
62
|
+
dst.unlink()
|
|
63
|
+
cmd = ["exiftool", "-all=", "-o", str(dst), str(src)]
|
|
64
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
65
|
+
if result.returncode != 0:
|
|
66
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip())
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def clean_video(src, dst):
|
|
70
|
+
if dst.exists():
|
|
71
|
+
dst.unlink()
|
|
72
|
+
cmd = [
|
|
73
|
+
"ffmpeg", "-y", "-i", str(src),
|
|
74
|
+
"-map_metadata", "-1",
|
|
75
|
+
"-map_chapters", "-1",
|
|
76
|
+
"-c", "copy",
|
|
77
|
+
str(dst),
|
|
78
|
+
]
|
|
79
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
80
|
+
if result.returncode != 0:
|
|
81
|
+
raise RuntimeError(result.stderr.strip() or "ffmpeg error")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def verify_clean(path, category, tools):
|
|
85
|
+
if category == "image":
|
|
86
|
+
if not tools["exiftool"]:
|
|
87
|
+
return None
|
|
88
|
+
cmd = ["exiftool", str(path)]
|
|
89
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
90
|
+
structural_prefixes = (
|
|
91
|
+
"exiftool version", "file name", "directory", "file size",
|
|
92
|
+
"file modification", "file access", "file inode", "file permissions",
|
|
93
|
+
"file type", "mime type", "image width", "image height",
|
|
94
|
+
"bits per sample", "color components", "compression", "orientation",
|
|
95
|
+
"x resolution", "y resolution", "resolution unit", "exif byte order",
|
|
96
|
+
"jfif version", "jfif resolution", "color space", "pixel aspect ratio",
|
|
97
|
+
"duration", "avg bitrate", "bit depth", "color type", "filter",
|
|
98
|
+
"interlace", "image size", "megapixels",
|
|
99
|
+
)
|
|
100
|
+
lines = [
|
|
101
|
+
l.strip() for l in result.stdout.splitlines()
|
|
102
|
+
if l.strip() and not l.lower().startswith(structural_prefixes)
|
|
103
|
+
]
|
|
104
|
+
return lines
|
|
105
|
+
elif category == "video":
|
|
106
|
+
if not tools["ffmpeg"]:
|
|
107
|
+
return None
|
|
108
|
+
if not tools["ffprobe"]:
|
|
109
|
+
return None
|
|
110
|
+
cmd = ["ffprobe", "-v", "quiet", "-show_entries", "format_tags:stream_tags", str(path)]
|
|
111
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
112
|
+
structural_tags = {
|
|
113
|
+
"major_brand", "minor_version", "compatible_brands",
|
|
114
|
+
"handler_name", "vendor_id", "encoder", "language",
|
|
115
|
+
}
|
|
116
|
+
lines = []
|
|
117
|
+
for l in result.stdout.splitlines():
|
|
118
|
+
l = l.strip()
|
|
119
|
+
if not l or l.startswith("["):
|
|
120
|
+
continue
|
|
121
|
+
upper = l.upper()
|
|
122
|
+
if "TAG:" not in upper:
|
|
123
|
+
continue
|
|
124
|
+
tag_name = l.split("=", 1)[0].replace("TAG:", "").strip().lower()
|
|
125
|
+
if tag_name not in structural_tags:
|
|
126
|
+
lines.append(l)
|
|
127
|
+
return lines
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def print_summary(stats):
|
|
132
|
+
print()
|
|
133
|
+
print(f" \u2714 Images cleaned: {stats['images_cleaned']}")
|
|
134
|
+
print(f" \u2714 Videos cleaned: {stats['videos_cleaned']}")
|
|
135
|
+
|
|
136
|
+
skip_parts = []
|
|
137
|
+
if stats["unsupported"] > 0:
|
|
138
|
+
skip_parts.append(f"{stats['unsupported']} unsupported")
|
|
139
|
+
if stats["missing_tool"] > 0:
|
|
140
|
+
skip_parts.append(f"{stats['missing_tool']} missing tool")
|
|
141
|
+
skip_detail = f" ({', '.join(skip_parts)})" if skip_parts else ""
|
|
142
|
+
print(f" \u23ed Skipped: {stats['skipped']}{skip_detail}")
|
|
143
|
+
print(f" \u2716 Errors: {stats['errors']}")
|
|
144
|
+
print(f" \U0001F4C1 Output: {stats['output_dir']}")
|
|
145
|
+
print()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main():
|
|
149
|
+
args = parse_args()
|
|
150
|
+
input_path = Path(args.input).resolve()
|
|
151
|
+
|
|
152
|
+
if not input_path.exists():
|
|
153
|
+
print(f"Error: '{args.input}' does not exist.", file=sys.stderr)
|
|
154
|
+
sys.exit(2)
|
|
155
|
+
|
|
156
|
+
if args.output:
|
|
157
|
+
output_path = Path(args.output).resolve()
|
|
158
|
+
else:
|
|
159
|
+
output_path = Path(str(input_path) + "_clean")
|
|
160
|
+
|
|
161
|
+
tools = check_tools()
|
|
162
|
+
|
|
163
|
+
if not tools["exiftool"]:
|
|
164
|
+
print("WARNING: exiftool not found. Images will be skipped.")
|
|
165
|
+
print(" Install with: brew install exiftool")
|
|
166
|
+
print()
|
|
167
|
+
if not tools["ffmpeg"]:
|
|
168
|
+
print("WARNING: ffmpeg not found. Videos will be skipped.")
|
|
169
|
+
print(" Install with: brew install ffmpeg")
|
|
170
|
+
print()
|
|
171
|
+
if not tools["ffprobe"]:
|
|
172
|
+
if args.verify:
|
|
173
|
+
print("WARNING: ffprobe not found. Video verification will not work.")
|
|
174
|
+
print(" Install with: brew install ffmpeg (includes ffprobe)")
|
|
175
|
+
print()
|
|
176
|
+
|
|
177
|
+
files = collect_files(input_path)
|
|
178
|
+
|
|
179
|
+
input_real = os.path.realpath(str(input_path))
|
|
180
|
+
output_real = os.path.realpath(str(output_path))
|
|
181
|
+
|
|
182
|
+
if input_real.startswith(output_real + os.sep):
|
|
183
|
+
print(f"Error: output directory cannot be a parent of input.", file=sys.stderr)
|
|
184
|
+
print(f" Input: {input_real}", file=sys.stderr)
|
|
185
|
+
print(f" Output: {output_real}", file=sys.stderr)
|
|
186
|
+
sys.exit(2)
|
|
187
|
+
|
|
188
|
+
stats = {
|
|
189
|
+
"images_cleaned": 0,
|
|
190
|
+
"videos_cleaned": 0,
|
|
191
|
+
"skipped": 0,
|
|
192
|
+
"unsupported": 0,
|
|
193
|
+
"missing_tool": 0,
|
|
194
|
+
"errors": 0,
|
|
195
|
+
"output_dir": str(output_path),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
for src, category in files:
|
|
199
|
+
src_resolved = os.path.realpath(str(src))
|
|
200
|
+
|
|
201
|
+
if src_resolved.startswith(output_real + os.sep) or src_resolved == output_real:
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
if category == "unsupported":
|
|
205
|
+
stats["skipped"] += 1
|
|
206
|
+
stats["unsupported"] += 1
|
|
207
|
+
if args.verbose:
|
|
208
|
+
print(f" SKIP (unsupported): {src}")
|
|
209
|
+
continue
|
|
210
|
+
|
|
211
|
+
if category == "image" and not tools["exiftool"]:
|
|
212
|
+
stats["skipped"] += 1
|
|
213
|
+
stats["missing_tool"] += 1
|
|
214
|
+
if args.verbose:
|
|
215
|
+
print(f" SKIP (exiftool missing): {src}")
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
if category == "video" and not tools["ffmpeg"]:
|
|
219
|
+
stats["skipped"] += 1
|
|
220
|
+
stats["missing_tool"] += 1
|
|
221
|
+
if args.verbose:
|
|
222
|
+
print(f" SKIP (ffmpeg missing): {src}")
|
|
223
|
+
continue
|
|
224
|
+
|
|
225
|
+
if input_path.is_file():
|
|
226
|
+
dst = output_path / src.name
|
|
227
|
+
else:
|
|
228
|
+
rel = src.relative_to(input_path)
|
|
229
|
+
dst = output_path / rel
|
|
230
|
+
|
|
231
|
+
if args.dry_run:
|
|
232
|
+
print(f" WOULD CLEAN: {src} -> {dst}")
|
|
233
|
+
if category == "image":
|
|
234
|
+
stats["images_cleaned"] += 1
|
|
235
|
+
else:
|
|
236
|
+
stats["videos_cleaned"] += 1
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
try:
|
|
240
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
241
|
+
|
|
242
|
+
if args.verbose:
|
|
243
|
+
print(f" CLEANING: {src} -> {dst}")
|
|
244
|
+
|
|
245
|
+
if category == "image":
|
|
246
|
+
clean_image(src, dst)
|
|
247
|
+
stats["images_cleaned"] += 1
|
|
248
|
+
else:
|
|
249
|
+
clean_video(src, dst)
|
|
250
|
+
stats["videos_cleaned"] += 1
|
|
251
|
+
|
|
252
|
+
if args.verify:
|
|
253
|
+
residual = verify_clean(dst, category, tools)
|
|
254
|
+
if residual is None:
|
|
255
|
+
if args.verbose:
|
|
256
|
+
print(f" VERIFY: skipped (tool not available)")
|
|
257
|
+
elif residual:
|
|
258
|
+
print(f" VERIFY WARNING: residual metadata in {dst}:")
|
|
259
|
+
for line in residual:
|
|
260
|
+
print(f" {line}")
|
|
261
|
+
else:
|
|
262
|
+
if args.verbose:
|
|
263
|
+
print(f" VERIFY OK: no residual metadata")
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
stats["errors"] += 1
|
|
267
|
+
print(f" ERROR: {src}: {e}", file=sys.stderr)
|
|
268
|
+
|
|
269
|
+
print_summary(stats)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
if __name__ == "__main__":
|
|
273
|
+
main()
|