antigravity-seo-kit 2.0.0
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.
Potentially problematic release.
This version of antigravity-seo-kit might be problematic. Click here for more details.
- package/.agent/agent.md +96 -0
- package/.agent/skills/seo/SKILL.md +153 -0
- package/.agent/skills/seo/references/cwv-thresholds.md +108 -0
- package/.agent/skills/seo/references/eeat-framework.md +214 -0
- package/.agent/skills/seo/references/local-schema-types.md +230 -0
- package/.agent/skills/seo/references/local-seo-signals.md +218 -0
- package/.agent/skills/seo/references/maps-api-endpoints.md +160 -0
- package/.agent/skills/seo/references/maps-free-apis.md +176 -0
- package/.agent/skills/seo/references/maps-gbp-checklist.md +150 -0
- package/.agent/skills/seo/references/maps-geo-grid.md +154 -0
- package/.agent/skills/seo/references/quality-gates.md +155 -0
- package/.agent/skills/seo/references/schema-types.md +118 -0
- package/.agent/skills/seo/schema/templates.json +213 -0
- package/.agent/skills/seo/scripts/analyze_visual.py +217 -0
- package/.agent/skills/seo/scripts/capture_screenshot.py +181 -0
- package/.agent/skills/seo/scripts/fetch_page.py +196 -0
- package/.agent/skills/seo/scripts/parse_html.py +201 -0
- package/.agent/skills/seo-audit/SKILL.md +278 -0
- package/.agent/skills/seo-competitor-pages/SKILL.md +212 -0
- package/.agent/skills/seo-content/SKILL.md +230 -0
- package/.agent/skills/seo-dataforseo/SKILL.md +418 -0
- package/.agent/skills/seo-geo/SKILL.md +305 -0
- package/.agent/skills/seo-google/SKILL.md +405 -0
- package/.agent/skills/seo-google/assets/templates/cwv-audit-report.md +48 -0
- package/.agent/skills/seo-google/assets/templates/gsc-performance-report.md +44 -0
- package/.agent/skills/seo-google/assets/templates/indexation-status-report.md +43 -0
- package/.agent/skills/seo-google/references/auth-setup.md +154 -0
- package/.agent/skills/seo-google/references/ga4-data-api.md +184 -0
- package/.agent/skills/seo-google/references/indexing-api.md +107 -0
- package/.agent/skills/seo-google/references/keyword-planner-api.md +66 -0
- package/.agent/skills/seo-google/references/nlp-api.md +55 -0
- package/.agent/skills/seo-google/references/pagespeed-crux-api.md +204 -0
- package/.agent/skills/seo-google/references/rate-limits-quotas.md +75 -0
- package/.agent/skills/seo-google/references/search-console-api.md +156 -0
- package/.agent/skills/seo-google/references/supplementary-apis.md +99 -0
- package/.agent/skills/seo-google/references/youtube-api.md +49 -0
- package/.agent/skills/seo-google/scripts/crux_history.py +321 -0
- package/.agent/skills/seo-google/scripts/ga4_report.py +478 -0
- package/.agent/skills/seo-google/scripts/google_auth.py +795 -0
- package/.agent/skills/seo-google/scripts/google_report.py +2273 -0
- package/.agent/skills/seo-google/scripts/gsc_inspect.py +340 -0
- package/.agent/skills/seo-google/scripts/gsc_query.py +378 -0
- package/.agent/skills/seo-google/scripts/indexing_notify.py +313 -0
- package/.agent/skills/seo-google/scripts/keyword_planner.py +297 -0
- package/.agent/skills/seo-google/scripts/nlp_analyze.py +309 -0
- package/.agent/skills/seo-google/scripts/pagespeed_check.py +649 -0
- package/.agent/skills/seo-google/scripts/youtube_search.py +355 -0
- package/.agent/skills/seo-hreflang/SKILL.md +192 -0
- package/.agent/skills/seo-image-gen/SKILL.md +211 -0
- package/.agent/skills/seo-image-gen/references/cost-tracking.md +47 -0
- package/.agent/skills/seo-image-gen/references/gemini-models.md +200 -0
- package/.agent/skills/seo-image-gen/references/mcp-tools.md +115 -0
- package/.agent/skills/seo-image-gen/references/post-processing.md +192 -0
- package/.agent/skills/seo-image-gen/references/presets.md +69 -0
- package/.agent/skills/seo-image-gen/references/prompt-engineering.md +411 -0
- package/.agent/skills/seo-image-gen/references/seo-image-presets.md +137 -0
- package/.agent/skills/seo-image-gen/scripts/batch.py +97 -0
- package/.agent/skills/seo-image-gen/scripts/cost_tracker.py +191 -0
- package/.agent/skills/seo-image-gen/scripts/edit.py +141 -0
- package/.agent/skills/seo-image-gen/scripts/generate.py +149 -0
- package/.agent/skills/seo-image-gen/scripts/presets.py +153 -0
- package/.agent/skills/seo-image-gen/scripts/setup_mcp.py +151 -0
- package/.agent/skills/seo-image-gen/scripts/validate_setup.py +133 -0
- package/.agent/skills/seo-images/SKILL.md +176 -0
- package/.agent/skills/seo-local/SKILL.md +381 -0
- package/.agent/skills/seo-maps/SKILL.md +328 -0
- package/.agent/skills/seo-page/SKILL.md +86 -0
- package/.agent/skills/seo-plan/SKILL.md +118 -0
- package/.agent/skills/seo-plan/assets/agency.md +175 -0
- package/.agent/skills/seo-plan/assets/ecommerce.md +167 -0
- package/.agent/skills/seo-plan/assets/generic.md +144 -0
- package/.agent/skills/seo-plan/assets/local-service.md +160 -0
- package/.agent/skills/seo-plan/assets/publisher.md +153 -0
- package/.agent/skills/seo-plan/assets/saas.md +135 -0
- package/.agent/skills/seo-programmatic/SKILL.md +171 -0
- package/.agent/skills/seo-schema/SKILL.md +223 -0
- package/.agent/skills/seo-sitemap/SKILL.md +180 -0
- package/.agent/skills/seo-technical/SKILL.md +211 -0
- package/.agent/workflows/seo-audit.md +17 -0
- package/.agent/workflows/seo-competitor-pages.md +12 -0
- package/.agent/workflows/seo-content.md +14 -0
- package/.agent/workflows/seo-geo.md +12 -0
- package/.agent/workflows/seo-google.md +12 -0
- package/.agent/workflows/seo-hreflang.md +12 -0
- package/.agent/workflows/seo-images.md +13 -0
- package/.agent/workflows/seo-local.md +12 -0
- package/.agent/workflows/seo-maps.md +11 -0
- package/.agent/workflows/seo-page.md +13 -0
- package/.agent/workflows/seo-plan.md +13 -0
- package/.agent/workflows/seo-programmatic.md +12 -0
- package/.agent/workflows/seo-schema.md +11 -0
- package/.agent/workflows/seo-sitemap.md +9 -0
- package/.agent/workflows/seo-technical.md +18 -0
- package/LICENSE +88 -0
- package/README.md +122 -0
- package/bin/cli.js +117 -0
- package/docs/ARCHITECTURE.md +218 -0
- package/docs/COMMANDS.md +184 -0
- package/docs/INSTALLATION.md +100 -0
- package/docs/MCP-INTEGRATION.md +153 -0
- package/docs/TROUBLESHOOTING.md +151 -0
- package/docs/superpowers/plans/2026-03-13-github-audit-fixes.md +511 -0
- package/extensions/banana/README.md +95 -0
- package/extensions/banana/docs/BANANA-SETUP.md +86 -0
- package/extensions/banana/install.sh +170 -0
- package/extensions/banana/references/cost-tracking.md +47 -0
- package/extensions/banana/references/gemini-models.md +200 -0
- package/extensions/banana/references/mcp-tools.md +115 -0
- package/extensions/banana/references/post-processing.md +192 -0
- package/extensions/banana/references/presets.md +69 -0
- package/extensions/banana/references/prompt-engineering.md +411 -0
- package/extensions/banana/references/seo-image-presets.md +137 -0
- package/extensions/banana/scripts/batch.py +97 -0
- package/extensions/banana/scripts/cost_tracker.py +191 -0
- package/extensions/banana/scripts/edit.py +141 -0
- package/extensions/banana/scripts/generate.py +149 -0
- package/extensions/banana/scripts/presets.py +153 -0
- package/extensions/banana/scripts/setup_mcp.py +151 -0
- package/extensions/banana/scripts/validate_setup.py +133 -0
- package/extensions/banana/uninstall.sh +43 -0
- package/extensions/dataforseo/README.md +169 -0
- package/extensions/dataforseo/docs/DATAFORSEO-SETUP.md +74 -0
- package/extensions/dataforseo/field-config.json +280 -0
- package/extensions/dataforseo/install.ps1 +110 -0
- package/extensions/dataforseo/install.sh +161 -0
- package/extensions/dataforseo/uninstall.ps1 +35 -0
- package/extensions/dataforseo/uninstall.sh +39 -0
- package/lib/api.js +190 -0
- package/lib/fingerprint.js +68 -0
- package/lib/installer.js +486 -0
- package/lib/utils.js +254 -0
- package/package.json +40 -0
- package/pyproject.toml +11 -0
- package/requirements-google.txt +15 -0
- package/requirements.txt +11 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# SEO Image Presets
|
|
2
|
+
|
|
3
|
+
Pre-configured presets for common SEO image use cases. These map to banana's
|
|
4
|
+
preset format (see `references/presets.md` for schema details).
|
|
5
|
+
|
|
6
|
+
## Preset Templates
|
|
7
|
+
|
|
8
|
+
### og-default:Standard OG/Social Preview
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"name": "og-default",
|
|
13
|
+
"description": "Clean, professional OG image for social sharing",
|
|
14
|
+
"aspect_ratio": "16:9",
|
|
15
|
+
"resolution": "1K",
|
|
16
|
+
"domain_mode": "Product",
|
|
17
|
+
"style": {
|
|
18
|
+
"colors": ["#FFFFFF", "#F5F5F5"],
|
|
19
|
+
"mood": "Professional, clean, trustworthy",
|
|
20
|
+
"lighting": "Bright, even studio lighting with soft shadows",
|
|
21
|
+
"typography": "Modern sans-serif if text needed"
|
|
22
|
+
},
|
|
23
|
+
"post_processing": "magick output.png -resize 1200x630^ -gravity center -extent 1200x630 output-og.webp"
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### blog-hero:Widescreen Blog Hero Image
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"name": "blog-hero",
|
|
32
|
+
"description": "Dramatic widescreen hero for blog posts",
|
|
33
|
+
"aspect_ratio": "16:9",
|
|
34
|
+
"resolution": "2K",
|
|
35
|
+
"domain_mode": "Cinema",
|
|
36
|
+
"style": {
|
|
37
|
+
"colors": ["contextual"],
|
|
38
|
+
"mood": "Dramatic, atmospheric, editorial",
|
|
39
|
+
"lighting": "Golden hour or moody blue hour, directional",
|
|
40
|
+
"typography": "None:image only"
|
|
41
|
+
},
|
|
42
|
+
"post_processing": "magick output.png -quality 85 output-hero.webp"
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### product-white:E-commerce Product Shot
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"name": "product-white",
|
|
51
|
+
"description": "Clean white background product photography",
|
|
52
|
+
"aspect_ratio": "4:3",
|
|
53
|
+
"resolution": "2K",
|
|
54
|
+
"domain_mode": "Product",
|
|
55
|
+
"style": {
|
|
56
|
+
"colors": ["#FFFFFF"],
|
|
57
|
+
"mood": "Clean, professional, catalog-ready",
|
|
58
|
+
"lighting": "360-degree soft studio lighting, minimal shadows",
|
|
59
|
+
"typography": "None"
|
|
60
|
+
},
|
|
61
|
+
"prompt_suffix": "Studio product photography, clean white background, professional catalog shot, high resolution",
|
|
62
|
+
"post_processing": "magick output.png -fuzz 5% -transparent white output-transparent.png"
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### social-square:Social Media Square
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"name": "social-square",
|
|
71
|
+
"description": "1:1 square image for social media platforms",
|
|
72
|
+
"aspect_ratio": "1:1",
|
|
73
|
+
"resolution": "1K",
|
|
74
|
+
"domain_mode": "UI/Web",
|
|
75
|
+
"style": {
|
|
76
|
+
"colors": ["brand-contextual"],
|
|
77
|
+
"mood": "Engaging, scroll-stopping, platform-native",
|
|
78
|
+
"lighting": "Bright, even, high-contrast",
|
|
79
|
+
"typography": "Bold sans-serif if text needed, under 25 characters"
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### infographic-vertical:Data-Heavy Infographic
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"name": "infographic-vertical",
|
|
89
|
+
"description": "Tall vertical infographic for data visualization",
|
|
90
|
+
"aspect_ratio": "2:3",
|
|
91
|
+
"resolution": "4K",
|
|
92
|
+
"domain_mode": "Infographic",
|
|
93
|
+
"style": {
|
|
94
|
+
"colors": ["brand-contextual", "data-visualization palette"],
|
|
95
|
+
"mood": "Informative, structured, authoritative",
|
|
96
|
+
"lighting": "Flat, even, no dramatic shadows",
|
|
97
|
+
"typography": "Clear hierarchy:headline, subheads, body, captions"
|
|
98
|
+
},
|
|
99
|
+
"notes": "Use thinking: high for better text rendering accuracy"
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### favicon-mark:Favicon / App Icon
|
|
104
|
+
|
|
105
|
+
```json
|
|
106
|
+
{
|
|
107
|
+
"name": "favicon-mark",
|
|
108
|
+
"description": "Minimal iconic mark for favicon or app icon",
|
|
109
|
+
"aspect_ratio": "1:1",
|
|
110
|
+
"resolution": "512",
|
|
111
|
+
"domain_mode": "Logo",
|
|
112
|
+
"style": {
|
|
113
|
+
"colors": ["2-3 brand colors max"],
|
|
114
|
+
"mood": "Minimal, recognizable, scalable",
|
|
115
|
+
"lighting": "Flat, no shadows",
|
|
116
|
+
"typography": "Single letter or symbol only"
|
|
117
|
+
},
|
|
118
|
+
"post_processing": "magick output.png -resize 32x32 favicon.ico && magick output.png -resize 180x180 apple-touch-icon.png"
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Creating Custom Presets
|
|
123
|
+
|
|
124
|
+
Users can create their own presets:
|
|
125
|
+
```bash
|
|
126
|
+
python3 .agent/skills/seo-image-gen/scripts/presets.py create my-brand
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
This creates `~/.banana/presets/my-brand.json` with the full schema.
|
|
130
|
+
Custom presets override SEO defaults when specified.
|
|
131
|
+
|
|
132
|
+
## Preset Selection Logic
|
|
133
|
+
|
|
134
|
+
1. If user specifies a use case command (og, hero, product), load the matching preset
|
|
135
|
+
2. If user mentions a brand preset name, load from `~/.banana/presets/`
|
|
136
|
+
3. Brand presets override SEO presets for colors, mood, and typography
|
|
137
|
+
4. SEO presets always provide aspect ratio and resolution defaults
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Claude Banana - CSV Batch Workflow
|
|
3
|
+
|
|
4
|
+
Parse a CSV file of image generation requests and output a structured plan.
|
|
5
|
+
Claude then executes each row via MCP.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
batch.py --csv path/to/file.csv
|
|
9
|
+
|
|
10
|
+
CSV columns:
|
|
11
|
+
prompt (required), ratio, resolution, model, preset (all optional)
|
|
12
|
+
|
|
13
|
+
Example CSV:
|
|
14
|
+
prompt,ratio,resolution
|
|
15
|
+
"coffee shop hero image",16:9,2K
|
|
16
|
+
"team photo placeholder",1:1,1K
|
|
17
|
+
"product shot on marble",4:3,2K
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import csv
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
# Inline pricing for estimates
|
|
27
|
+
PRICING = {
|
|
28
|
+
"gemini-3.1-flash-image-preview": {"512": 0.020, "1K": 0.039, "2K": 0.078, "4K": 0.156},
|
|
29
|
+
"gemini-2.5-flash-image": {"512": 0.020, "1K": 0.039},
|
|
30
|
+
}
|
|
31
|
+
DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
|
|
32
|
+
DEFAULT_RESOLUTION = "1K"
|
|
33
|
+
DEFAULT_RATIO = "1:1"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def estimate_cost(model, resolution):
|
|
37
|
+
"""Estimate cost for a single image."""
|
|
38
|
+
model_pricing = PRICING.get(model, PRICING[DEFAULT_MODEL])
|
|
39
|
+
return model_pricing.get(resolution, model_pricing.get("1K", 0.039))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main():
|
|
43
|
+
parser = argparse.ArgumentParser(description="Parse CSV batch and output generation plan")
|
|
44
|
+
parser.add_argument("--csv", required=True, help="Path to CSV file")
|
|
45
|
+
args = parser.parse_args()
|
|
46
|
+
|
|
47
|
+
csv_path = Path(args.csv).resolve()
|
|
48
|
+
if not csv_path.exists():
|
|
49
|
+
print(json.dumps({"error": True, "message": f"CSV not found: {csv_path}"}))
|
|
50
|
+
sys.exit(1)
|
|
51
|
+
|
|
52
|
+
rows = []
|
|
53
|
+
errors = []
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
with open(csv_path, "r", newline="") as f:
|
|
57
|
+
reader = csv.DictReader(f)
|
|
58
|
+
if not reader.fieldnames or "prompt" not in reader.fieldnames:
|
|
59
|
+
print(json.dumps({"error": True, "message": "CSV must have a 'prompt' column header"}))
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
for i, row in enumerate(reader, start=2): # Line 2+ (1 is header)
|
|
62
|
+
prompt = row.get("prompt", "").strip()
|
|
63
|
+
if not prompt:
|
|
64
|
+
errors.append(f"Row {i}: missing prompt")
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
rows.append({
|
|
68
|
+
"row": i,
|
|
69
|
+
"prompt": prompt,
|
|
70
|
+
"ratio": row.get("ratio", "").strip() or DEFAULT_RATIO,
|
|
71
|
+
"resolution": row.get("resolution", "").strip() or DEFAULT_RESOLUTION,
|
|
72
|
+
"model": row.get("model", "").strip() or DEFAULT_MODEL,
|
|
73
|
+
"preset": row.get("preset", "").strip() or None,
|
|
74
|
+
})
|
|
75
|
+
except (csv.Error, UnicodeDecodeError) as e:
|
|
76
|
+
print(json.dumps({"error": True, "message": f"Failed to parse CSV: {e}"}))
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
if errors:
|
|
80
|
+
print("Validation errors:")
|
|
81
|
+
for e in errors:
|
|
82
|
+
print(f" - {e}")
|
|
83
|
+
if not rows:
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
print()
|
|
86
|
+
|
|
87
|
+
# Cost estimate
|
|
88
|
+
total_cost = sum(estimate_cost(r["model"], r["resolution"]) for r in rows)
|
|
89
|
+
|
|
90
|
+
# Output structured JSON for Claude to consume
|
|
91
|
+
print(json.dumps({"rows": rows, "total_count": len(rows),
|
|
92
|
+
"estimated_cost": round(total_cost, 3),
|
|
93
|
+
"errors": errors}, indent=2))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
main()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Claude Banana - Cost Tracker
|
|
3
|
+
|
|
4
|
+
Track image generation costs, view summaries, and estimate batch costs.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
cost_tracker.py log --model MODEL --resolution RES --prompt "summary"
|
|
8
|
+
cost_tracker.py summary
|
|
9
|
+
cost_tracker.py today
|
|
10
|
+
cost_tracker.py estimate --model MODEL --resolution RES --count N
|
|
11
|
+
cost_tracker.py reset --confirm
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
LEDGER_PATH = Path.home() / ".banana" / "costs.json"
|
|
21
|
+
|
|
22
|
+
# Cost per image in USD (approximate, based on ~1,290 output tokens)
|
|
23
|
+
PRICING = {
|
|
24
|
+
"gemini-3.1-flash-image-preview": {
|
|
25
|
+
"512": 0.020,
|
|
26
|
+
"1K": 0.039,
|
|
27
|
+
"2K": 0.078,
|
|
28
|
+
"4K": 0.156,
|
|
29
|
+
},
|
|
30
|
+
"gemini-2.5-flash-image": {
|
|
31
|
+
"512": 0.020,
|
|
32
|
+
"1K": 0.039,
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Batch API gets 50% discount
|
|
37
|
+
BATCH_DISCOUNT = 0.5
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_ledger():
|
|
41
|
+
"""Load the cost ledger from disk."""
|
|
42
|
+
if not LEDGER_PATH.exists():
|
|
43
|
+
return {"total_cost": 0.0, "total_images": 0, "entries": [], "daily": {}}
|
|
44
|
+
with open(LEDGER_PATH, "r") as f:
|
|
45
|
+
return json.load(f)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _save_ledger(ledger):
|
|
49
|
+
"""Save the cost ledger to disk."""
|
|
50
|
+
LEDGER_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
with open(LEDGER_PATH, "w") as f:
|
|
52
|
+
json.dump(ledger, f, indent=2)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _lookup_cost(model, resolution, batch=False):
|
|
56
|
+
"""Look up cost for a model+resolution combination."""
|
|
57
|
+
model_pricing = PRICING.get(model)
|
|
58
|
+
if not model_pricing:
|
|
59
|
+
# Try partial match
|
|
60
|
+
for key in PRICING:
|
|
61
|
+
if key in model or model in key:
|
|
62
|
+
model_pricing = PRICING[key]
|
|
63
|
+
break
|
|
64
|
+
if not model_pricing:
|
|
65
|
+
print(f"Warning: Unknown model '{model}', using 3.1 Flash pricing", file=sys.stderr)
|
|
66
|
+
model_pricing = PRICING["gemini-3.1-flash-image-preview"]
|
|
67
|
+
|
|
68
|
+
valid_resolutions = {"512", "1K", "2K", "4K"}
|
|
69
|
+
if resolution not in valid_resolutions:
|
|
70
|
+
print(f"Warning: Unknown resolution '{resolution}', using 1K pricing", file=sys.stderr)
|
|
71
|
+
cost = model_pricing.get(resolution, model_pricing.get("1K", 0.039))
|
|
72
|
+
if batch:
|
|
73
|
+
cost *= BATCH_DISCOUNT
|
|
74
|
+
return cost
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cmd_log(args):
|
|
78
|
+
"""Log a generation to the ledger."""
|
|
79
|
+
ledger = _load_ledger()
|
|
80
|
+
cost = _lookup_cost(args.model, args.resolution, getattr(args, "batch", False))
|
|
81
|
+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
82
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S")
|
|
83
|
+
|
|
84
|
+
entry = {
|
|
85
|
+
"ts": now,
|
|
86
|
+
"model": args.model,
|
|
87
|
+
"res": args.resolution,
|
|
88
|
+
"cost": cost,
|
|
89
|
+
"prompt": args.prompt[:100],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ledger["entries"].append(entry)
|
|
93
|
+
ledger["total_cost"] = round(ledger["total_cost"] + cost, 4)
|
|
94
|
+
ledger["total_images"] += 1
|
|
95
|
+
|
|
96
|
+
if today not in ledger["daily"]:
|
|
97
|
+
ledger["daily"][today] = {"count": 0, "cost": 0.0}
|
|
98
|
+
ledger["daily"][today]["count"] += 1
|
|
99
|
+
ledger["daily"][today]["cost"] = round(ledger["daily"][today]["cost"] + cost, 4)
|
|
100
|
+
|
|
101
|
+
_save_ledger(ledger)
|
|
102
|
+
print(json.dumps({"logged": True, "cost": cost, "total_cost": ledger["total_cost"],
|
|
103
|
+
"total_images": ledger["total_images"]}))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cmd_summary(args):
|
|
107
|
+
"""Show cost summary."""
|
|
108
|
+
ledger = _load_ledger()
|
|
109
|
+
print(f"Total images: {ledger['total_images']}")
|
|
110
|
+
print(f"Total cost: ${ledger['total_cost']:.3f}")
|
|
111
|
+
print()
|
|
112
|
+
|
|
113
|
+
daily = ledger.get("daily", {})
|
|
114
|
+
if daily:
|
|
115
|
+
# Show last 7 days
|
|
116
|
+
sorted_days = sorted(daily.keys(), reverse=True)[:7]
|
|
117
|
+
print("Last 7 days:")
|
|
118
|
+
for day in sorted_days:
|
|
119
|
+
d = daily[day]
|
|
120
|
+
print(f" {day}: {d['count']} images, ${d['cost']:.3f}")
|
|
121
|
+
else:
|
|
122
|
+
print("No usage recorded yet.")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_today(args):
|
|
126
|
+
"""Show today's usage."""
|
|
127
|
+
ledger = _load_ledger()
|
|
128
|
+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
129
|
+
daily = ledger.get("daily", {}).get(today, {"count": 0, "cost": 0.0})
|
|
130
|
+
print(f"Today ({today}): {daily['count']} images, ${daily['cost']:.3f}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def cmd_estimate(args):
|
|
134
|
+
"""Estimate cost for a batch."""
|
|
135
|
+
cost_per = _lookup_cost(args.model, args.resolution, getattr(args, "batch", False))
|
|
136
|
+
total = round(cost_per * args.count, 3)
|
|
137
|
+
print(f"Model: {args.model}")
|
|
138
|
+
print(f"Resolution: {args.resolution}")
|
|
139
|
+
print(f"Count: {args.count}")
|
|
140
|
+
print(f"Cost/image: ${cost_per:.3f}")
|
|
141
|
+
print(f"Total est: ${total:.3f}")
|
|
142
|
+
if not getattr(args, "batch", False):
|
|
143
|
+
batch_total = round(cost_per * BATCH_DISCOUNT * args.count, 3)
|
|
144
|
+
print(f"Batch est: ${batch_total:.3f} (50% discount)")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cmd_reset(args):
|
|
148
|
+
"""Reset the ledger."""
|
|
149
|
+
if not args.confirm:
|
|
150
|
+
print("Error: Pass --confirm to reset the cost ledger.", file=sys.stderr)
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
_save_ledger({"total_cost": 0.0, "total_images": 0, "entries": [], "daily": {}})
|
|
153
|
+
print("Cost ledger reset.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main():
|
|
157
|
+
parser = argparse.ArgumentParser(description="Claude Banana Cost Tracker")
|
|
158
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
159
|
+
|
|
160
|
+
# log
|
|
161
|
+
p_log = sub.add_parser("log", help="Log a generation")
|
|
162
|
+
p_log.add_argument("--model", required=True, help="Model ID")
|
|
163
|
+
p_log.add_argument("--resolution", required=True, help="Resolution (512, 1K, 2K, 4K)")
|
|
164
|
+
p_log.add_argument("--prompt", required=True, help="Brief prompt description")
|
|
165
|
+
p_log.add_argument("--batch", action="store_true", help="Batch API (50%% discount)")
|
|
166
|
+
|
|
167
|
+
# summary
|
|
168
|
+
sub.add_parser("summary", help="Show cost summary")
|
|
169
|
+
|
|
170
|
+
# today
|
|
171
|
+
sub.add_parser("today", help="Show today's usage")
|
|
172
|
+
|
|
173
|
+
# estimate
|
|
174
|
+
p_est = sub.add_parser("estimate", help="Estimate batch cost")
|
|
175
|
+
p_est.add_argument("--model", required=True, help="Model ID")
|
|
176
|
+
p_est.add_argument("--resolution", required=True, help="Resolution (512, 1K, 2K, 4K)")
|
|
177
|
+
p_est.add_argument("--count", required=True, type=int, help="Number of images")
|
|
178
|
+
p_est.add_argument("--batch", action="store_true", help="Use batch pricing (50%% discount)")
|
|
179
|
+
|
|
180
|
+
# reset
|
|
181
|
+
p_reset = sub.add_parser("reset", help="Reset cost ledger")
|
|
182
|
+
p_reset.add_argument("--confirm", action="store_true", help="Confirm reset")
|
|
183
|
+
|
|
184
|
+
args = parser.parse_args()
|
|
185
|
+
cmds = {"log": cmd_log, "summary": cmd_summary, "today": cmd_today,
|
|
186
|
+
"estimate": cmd_estimate, "reset": cmd_reset}
|
|
187
|
+
cmds[args.command](args)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
if __name__ == "__main__":
|
|
191
|
+
main()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Claude Banana - Direct API Fallback: Image Editing
|
|
3
|
+
|
|
4
|
+
Edit images via Gemini REST API when MCP is unavailable.
|
|
5
|
+
Uses only Python stdlib (no pip dependencies).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
edit.py --image path/to/image.png --prompt "remove the background"
|
|
9
|
+
[--model MODEL] [--api-key KEY]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import base64
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import urllib.request
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
|
|
22
|
+
OUTPUT_DIR = Path.home() / "Documents" / "nanobanana_generated"
|
|
23
|
+
API_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def edit_image(image_path, prompt, model, api_key):
|
|
27
|
+
"""Call Gemini API to edit an image."""
|
|
28
|
+
image_path = Path(image_path).resolve()
|
|
29
|
+
if not image_path.exists():
|
|
30
|
+
print(json.dumps({"error": True, "message": f"Image not found: {image_path}"}))
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
# Read and encode image
|
|
34
|
+
with open(image_path, "rb") as f:
|
|
35
|
+
image_b64 = base64.b64encode(f.read()).decode("utf-8")
|
|
36
|
+
|
|
37
|
+
# Determine MIME type
|
|
38
|
+
suffix = image_path.suffix.lower()
|
|
39
|
+
mime_types = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
40
|
+
".webp": "image/webp", ".gif": "image/gif"}
|
|
41
|
+
mime_type = mime_types.get(suffix, "image/png")
|
|
42
|
+
|
|
43
|
+
url = f"{API_BASE}/{model}:generateContent?key={api_key}"
|
|
44
|
+
|
|
45
|
+
body = {
|
|
46
|
+
"contents": [
|
|
47
|
+
{
|
|
48
|
+
"parts": [
|
|
49
|
+
{"text": prompt},
|
|
50
|
+
{"inlineData": {"mimeType": mime_type, "data": image_b64}},
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"generationConfig": {
|
|
55
|
+
"responseModalities": ["TEXT", "IMAGE"],
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
data = json.dumps(body).encode("utf-8")
|
|
60
|
+
req = urllib.request.Request(
|
|
61
|
+
url,
|
|
62
|
+
data=data,
|
|
63
|
+
headers={"Content-Type": "application/json"},
|
|
64
|
+
method="POST",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
69
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
70
|
+
except urllib.error.HTTPError as e:
|
|
71
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
72
|
+
print(json.dumps({"error": True, "status": e.code, "message": error_body}))
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
except urllib.error.URLError as e:
|
|
75
|
+
print(json.dumps({"error": True, "message": str(e.reason)}))
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
# Extract image from response
|
|
79
|
+
candidates = result.get("candidates", [])
|
|
80
|
+
if not candidates:
|
|
81
|
+
finish_reason = result.get("promptFeedback", {}).get("blockReason", "UNKNOWN")
|
|
82
|
+
print(json.dumps({"error": True, "message": f"No candidates returned. Reason: {finish_reason}"}))
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
parts = candidates[0].get("content", {}).get("parts", [])
|
|
86
|
+
image_data = None
|
|
87
|
+
text_response = ""
|
|
88
|
+
|
|
89
|
+
for part in parts:
|
|
90
|
+
if "inlineData" in part:
|
|
91
|
+
image_data = part["inlineData"]["data"]
|
|
92
|
+
elif "text" in part:
|
|
93
|
+
text_response = part["text"]
|
|
94
|
+
|
|
95
|
+
if not image_data:
|
|
96
|
+
finish_reason = candidates[0].get("finishReason", "UNKNOWN")
|
|
97
|
+
print(json.dumps({"error": True, "message": f"No image in response. finishReason: {finish_reason}"}))
|
|
98
|
+
sys.exit(1)
|
|
99
|
+
|
|
100
|
+
# Save image
|
|
101
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
103
|
+
filename = f"banana_edit_{timestamp}.png"
|
|
104
|
+
output_path = (OUTPUT_DIR / filename).resolve()
|
|
105
|
+
|
|
106
|
+
with open(output_path, "wb") as f:
|
|
107
|
+
f.write(base64.b64decode(image_data))
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
"path": str(output_path),
|
|
111
|
+
"model": model,
|
|
112
|
+
"source": str(image_path),
|
|
113
|
+
"text": text_response,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def main():
|
|
118
|
+
parser = argparse.ArgumentParser(description="Edit images via Gemini REST API")
|
|
119
|
+
parser.add_argument("--image", required=True, help="Path to input image")
|
|
120
|
+
parser.add_argument("--prompt", required=True, help="Edit instruction")
|
|
121
|
+
parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Model ID (default: {DEFAULT_MODEL})")
|
|
122
|
+
parser.add_argument("--api-key", default=None, help="Google AI API key (or set GOOGLE_AI_API_KEY env)")
|
|
123
|
+
|
|
124
|
+
args = parser.parse_args()
|
|
125
|
+
|
|
126
|
+
api_key = args.api_key or os.environ.get("GOOGLE_AI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
|
|
127
|
+
if not api_key:
|
|
128
|
+
print(json.dumps({"error": True, "message": "No API key. Set GOOGLE_AI_API_KEY env or pass --api-key"}))
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
result = edit_image(
|
|
132
|
+
image_path=args.image,
|
|
133
|
+
prompt=args.prompt,
|
|
134
|
+
model=args.model,
|
|
135
|
+
api_key=api_key,
|
|
136
|
+
)
|
|
137
|
+
print(json.dumps(result, indent=2))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
main()
|