cue-ai 0.9.2 → 0.9.4
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.
- package/CHANGELOG.md +4 -3
- package/README.md +154 -394
- package/bin/cue-learnings +30 -4
- package/bin/cue-review-progress +0 -0
- package/bin/cue-review-watch +0 -0
- package/dist/cue.js +4328 -3108
- package/package.json +1 -1
- package/plugins/cue/commands/cue-switch.md +1 -1
- package/plugins/cue/commands/cue.md +1 -1
- package/profiles/backend/profile.yaml +4 -0
- package/profiles/browser/profile.yaml +4 -0
- package/profiles/career/profile.yaml +2 -13
- package/profiles/commerce/profile.yaml +0 -2
- package/profiles/coolify/profile.yaml +0 -1
- package/profiles/core/profile.yaml +78 -11
- package/profiles/dash-merge-test/profile.yaml +6 -1
- package/profiles/designer/profile.yaml +9 -1
- package/profiles/dropshipping/profile.yaml +69 -0
- package/profiles/frontend/profile.yaml +4 -0
- package/profiles/google-ads/profile.yaml +34 -0
- package/profiles/google-analytics/profile.yaml +34 -0
- package/profiles/google-drive/profile.yaml +34 -0
- package/profiles/gstack/profile.yaml +117 -29
- package/profiles/marketing/profile.yaml +0 -1
- package/profiles/media/README.md +70 -0
- package/profiles/media/profile.yaml +104 -0
- package/profiles/nano-banana/profile.yaml +52 -0
- package/profiles/ops/profile.yaml +1 -2
- package/profiles/secops/profile.yaml +3 -0
- package/profiles/skill-writer/profile.yaml +15 -0
- package/profiles/video/profile.yaml +3 -0
- package/profiles/web-frontend-base/profile.yaml +6 -0
- package/profiles/webshop/profile.yaml +0 -1
- package/profiles/webshop-google/profile.yaml +1 -0
- package/profiles/x-growth-bot/profile.yaml +2 -0
- package/resources/icons/generate-icons.py +2 -128
- package/resources/mcps/configs/claude.sanitized.json +88 -20
- package/resources/mcps/configs/claude_runtime.sanitized.json +40 -1
- package/resources/mcps/configs/codex.sanitized.json +29 -0
- package/resources/skills/skills/career/job-hunter/LICENSE +21 -0
- package/resources/skills/skills/career/job-hunter/README.md +323 -0
- package/resources/skills/skills/career/job-hunter/SKILL.md +91 -0
- package/resources/skills/skills/career/job-hunter/agents/README.md +96 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-assessment-prep.md +195 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-ats-scan.md +155 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-bias-audit.md +224 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-cover-letter.md +69 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-decode-jd.md +117 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-fit-score.md +183 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-linkedin-audit.md +74 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-linkedin-scrape.md +255 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-portfolio-brief.md +123 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-reality-check.md +164 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-reference-prep.md +150 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-rejection-analysis.md +172 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-resume.md +70 -0
- package/resources/skills/skills/career/job-hunter/agents/apply-skills-gap-filler.md +109 -0
- package/resources/skills/skills/career/job-hunter/agents/career-internal.md +94 -0
- package/resources/skills/skills/career/job-hunter/agents/career-linkedin-content.md +173 -0
- package/resources/skills/skills/career/job-hunter/agents/career-linkedin-scanner.md +262 -0
- package/resources/skills/skills/career/job-hunter/agents/career-network-message.md +108 -0
- package/resources/skills/skills/career/job-hunter/agents/career-promote.md +102 -0
- package/resources/skills/skills/career/job-hunter/agents/career-review.md +71 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-debrief.md +117 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-mock.md +171 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-panel-decoder.md +152 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-prep.md +184 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-question-bank.md +133 -0
- package/resources/skills/skills/career/job-hunter/agents/interview-research.md +148 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-compare.md +117 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-counteroffer.md +144 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-deadline-manager.md +148 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-negotiate.md +126 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-schedule.md +99 -0
- package/resources/skills/skills/career/job-hunter/agents/offer-thankyou.md +80 -0
- package/resources/skills/skills/career/job-hunter/agents/search-company-research.md +146 -0
- package/resources/skills/skills/career/job-hunter/agents/search-follow-up.md +129 -0
- package/resources/skills/skills/career/job-hunter/agents/search-ghost-job-detector.md +152 -0
- package/resources/skills/skills/career/job-hunter/agents/search-inbox-scan.md +193 -0
- package/resources/skills/skills/career/job-hunter/agents/search-interview-scorecard.md +164 -0
- package/resources/skills/skills/career/job-hunter/agents/search-jobs.md +149 -0
- package/resources/skills/skills/career/job-hunter/agents/search-momentum-check.md +194 -0
- package/resources/skills/skills/career/job-hunter/agents/search-outreach.md +85 -0
- package/resources/skills/skills/career/job-hunter/agents/search-referral-finder.md +124 -0
- package/resources/skills/skills/career/job-hunter/agents/search-salary.md +96 -0
- package/resources/skills/skills/career/job-hunter/agents/search-send-email.md +109 -0
- package/resources/skills/skills/career/job-hunter/agents/search-tracker-update.md +127 -0
- package/resources/skills/skills/career/job-hunter/inputs/README.md +26 -0
- package/resources/skills/skills/career/job-hunter/inputs/apply-linkedin-url.txt +8 -0
- package/resources/skills/skills/career/job-hunter/inputs/interview-context.md +24 -0
- package/resources/skills/skills/career/job-hunter/inputs/job-description.md +20 -0
- package/resources/skills/skills/career/job-hunter/inputs/job-search-criteria.md +36 -0
- package/resources/skills/skills/career/job-hunter/inputs/my-linkedin.md +24 -0
- package/resources/skills/skills/career/job-hunter/inputs/my-resume.md +28 -0
- package/resources/skills/skills/career/job-hunter/inputs/search-outreach-target.md +24 -0
- package/resources/skills/skills/career/job-hunter/rules/README.md +37 -0
- package/resources/skills/skills/career/job-hunter/rules/writing-rules.md +81 -0
- package/resources/skills/skills/design/banana/SKILL.md +375 -0
- package/resources/skills/skills/design/banana/references/cost-tracking.md +47 -0
- package/resources/skills/skills/design/banana/references/gemini-models.md +236 -0
- package/resources/skills/skills/design/banana/references/mcp-tools.md +145 -0
- package/resources/skills/skills/design/banana/references/post-processing.md +192 -0
- package/resources/skills/skills/design/banana/references/presets.md +69 -0
- package/resources/skills/skills/design/banana/references/prompt-engineering.md +481 -0
- package/resources/skills/skills/design/banana/scripts/batch.py +97 -0
- package/resources/skills/skills/design/banana/scripts/cost_tracker.py +191 -0
- package/resources/skills/skills/design/banana/scripts/edit.py +159 -0
- package/resources/skills/skills/design/banana/scripts/generate.py +168 -0
- package/resources/skills/skills/design/banana/scripts/presets.py +154 -0
- package/resources/skills/skills/design/banana/scripts/setup_mcp.py +151 -0
- package/resources/skills/skills/design/banana/scripts/validate_setup.py +133 -0
- package/resources/skills/skills/gstack/ship/SKILL.md +13 -0
- package/resources/skills/skills/media/3d-logo-animation/SKILL.md +59 -0
- package/resources/skills/skills/media/action-figure-generator/SKILL.md +48 -0
- package/resources/skills/skills/media/ad-creative/SKILL.md +79 -0
- package/resources/skills/skills/media/ai-clipping/SKILL.md +194 -0
- package/resources/skills/skills/media/ai-clipping/scripts/run-ai-clipping.sh +200 -0
- package/resources/skills/skills/media/ai-fight-scene/SKILL.md +132 -0
- package/resources/skills/skills/media/amazon-product-listing/SKILL.md +68 -0
- package/resources/skills/skills/media/animal-video-generator/SKILL.md +59 -0
- package/resources/skills/skills/media/award-ceremony-video/SKILL.md +87 -0
- package/resources/skills/skills/media/blog-header/SKILL.md +61 -0
- package/resources/skills/skills/media/brand-kit/SKILL.md +72 -0
- package/resources/skills/skills/media/brochures/SKILL.md +65 -0
- package/resources/skills/skills/media/cartoon-dance-animation/SKILL.md +62 -0
- package/resources/skills/skills/media/character-story-video/SKILL.md +84 -0
- package/resources/skills/skills/media/chibi-collage-effect/SKILL.md +63 -0
- package/resources/skills/skills/media/cinema-director/SKILL.md +93 -0
- package/resources/skills/skills/media/cinema-director/scripts/generate-film.sh +78 -0
- package/resources/skills/skills/media/color-analysis-board/SKILL.md +71 -0
- package/resources/skills/skills/media/core-edit/SKILL.md +48 -0
- package/resources/skills/skills/media/core-edit/edit-image.sh +54 -0
- package/resources/skills/skills/media/core-edit/enhance-image.sh +191 -0
- package/resources/skills/skills/media/core-edit/lipsync.sh +144 -0
- package/resources/skills/skills/media/core-edit/video-effects.sh +193 -0
- package/resources/skills/skills/media/core-media/SKILL.md +49 -0
- package/resources/skills/skills/media/core-media/create-music.sh +169 -0
- package/resources/skills/skills/media/core-media/generate-image.sh +161 -0
- package/resources/skills/skills/media/core-media/generate-video.sh +137 -0
- package/resources/skills/skills/media/core-media/image-to-video.sh +228 -0
- package/resources/skills/skills/media/core-media/schema_data.json +18708 -0
- package/resources/skills/skills/media/core-media/upload.sh +41 -0
- package/resources/skills/skills/media/core-platform/SKILL.md +41 -0
- package/resources/skills/skills/media/core-platform/check-result.sh +37 -0
- package/resources/skills/skills/media/core-platform/setup.sh +31 -0
- package/resources/skills/skills/media/couple-grid-creator/SKILL.md +47 -0
- package/resources/skills/skills/media/design-guide/SKILL.md +73 -0
- package/resources/skills/skills/media/drone-style-video/SKILL.md +61 -0
- package/resources/skills/skills/media/fashion-try-on/SKILL.md +61 -0
- package/resources/skills/skills/media/floor-plan-rendering/SKILL.md +56 -0
- package/resources/skills/skills/media/freeze-effect-video/SKILL.md +100 -0
- package/resources/skills/skills/media/giant-product-showcase/SKILL.md +61 -0
- package/resources/skills/skills/media/instagram-post/SKILL.md +58 -0
- package/resources/skills/skills/media/interior-design/SKILL.md +61 -0
- package/resources/skills/skills/media/interior-design-visualizer/SKILL.md +57 -0
- package/resources/skills/skills/media/jewelry-product-video/SKILL.md +61 -0
- package/resources/skills/skills/media/kdenlive/SKILL.md +106 -0
- package/resources/skills/skills/media/kdenlive/scripts/assemble.sh +57 -0
- package/resources/skills/skills/media/kdenlive/scripts/common.sh +30 -0
- package/resources/skills/skills/media/kdenlive/scripts/inspect.sh +19 -0
- package/resources/skills/skills/media/kdenlive/scripts/reframe.sh +22 -0
- package/resources/skills/skills/media/kdenlive/scripts/render.sh +16 -0
- package/resources/skills/skills/media/kdenlive/scripts/title-card.sh +25 -0
- package/resources/skills/skills/media/keyboard-art-maker/SKILL.md +44 -0
- package/resources/skills/skills/media/logo-branding/SKILL.md +70 -0
- package/resources/skills/skills/media/logo-creator/SKILL.md +80 -0
- package/resources/skills/skills/media/logo-creator/scripts/create-logo.sh +38 -0
- package/resources/skills/skills/media/logo-generator/SKILL.md +56 -0
- package/resources/skills/skills/media/multi-angle-reshoot/SKILL.md +70 -0
- package/resources/skills/skills/media/multi-angle-shots/SKILL.md +73 -0
- package/resources/skills/skills/media/music-video/SKILL.md +61 -0
- package/resources/skills/skills/media/nano-banana/SKILL.md +80 -0
- package/resources/skills/skills/media/nano-banana/scripts/generate-nano-art.sh +54 -0
- package/resources/skills/skills/media/one-shot-video/SKILL.md +56 -0
- package/resources/skills/skills/media/photo-pack-generator/SKILL.md +205 -0
- package/resources/skills/skills/media/photo-pack-generator/scripts/generate-pack.sh +241 -0
- package/resources/skills/skills/media/product-ad-cinematic/SKILL.md +78 -0
- package/resources/skills/skills/media/product-campaign/SKILL.md +76 -0
- package/resources/skills/skills/media/product-showcase-video/SKILL.md +60 -0
- package/resources/skills/skills/media/product-video-ad-maker/SKILL.md +59 -0
- package/resources/skills/skills/media/rednote-cover/SKILL.md +57 -0
- package/resources/skills/skills/media/seedance-2/SKILL.md +632 -0
- package/resources/skills/skills/media/seedance-2/scripts/generate-seedance.sh +701 -0
- package/resources/skills/skills/media/selfie-with-celebrities/SKILL.md +64 -0
- package/resources/skills/skills/media/social-media-video/SKILL.md +277 -0
- package/resources/skills/skills/media/social-media-video/scripts/run-social-video.sh +316 -0
- package/resources/skills/skills/media/social-pack/SKILL.md +58 -0
- package/resources/skills/skills/media/storyboard/SKILL.md +57 -0
- package/resources/skills/skills/media/storyboard-to-cooking-video/SKILL.md +143 -0
- package/resources/skills/skills/media/talking-baby-video/SKILL.md +57 -0
- package/resources/skills/skills/media/ugc-ads-workflow/SKILL.md +70 -0
- package/resources/skills/skills/media/ugc-lifestyle-try-on/SKILL.md +65 -0
- package/resources/skills/skills/media/ugc-video-factory/SKILL.md +134 -0
- package/resources/skills/skills/media/ui-design/SKILL.md +81 -0
- package/resources/skills/skills/media/ui-design/scripts/generate-mockup.sh +49 -0
- package/resources/skills/skills/media/url-to-design/SKILL.md +61 -0
- package/resources/skills/skills/media/workflow/SKILL.md +197 -0
- package/resources/skills/skills/media/workflow/scripts/discover-workflow.sh +18 -0
- package/resources/skills/skills/media/workflow/scripts/generate-workflow.sh +33 -0
- package/resources/skills/skills/media/workflow/scripts/interactive-run.sh +16 -0
- package/resources/skills/skills/media/workflow/scripts/list-workflows.sh +20 -0
- package/resources/skills/skills/media/workflow/scripts/run-workflow.sh +34 -0
- package/resources/skills/skills/media/youtube-shorts/SKILL.md +173 -0
- package/resources/skills/skills/media/youtube-shorts/scripts/run-youtube-shorts.sh +141 -0
- package/resources/skills/skills/media/youtube-thumbnail/SKILL.md +66 -0
- package/resources/skills/skills/meta/cue-developer/references/architecture.md +2 -2
- package/resources/skills/skills/meta/cue-usage/SKILL.md +1 -1
- package/resources/skills/skills/meta/profile-fit-monitor/SKILL.md +2 -2
- package/resources/skills/skills/meta/profile-optimizer/SKILL.md +1 -1
- package/resources/skills/skills/meta/profile-suggest/SKILL.md +7 -7
- package/resources/skills/skills/meta/profile-summon/SKILL.md +159 -0
- package/resources/skills/skills/meta/profile-summon/evals/evals.json +53 -0
- package/resources/skills/skills/meta/save-profile/SKILL.md +1 -1
- package/resources/skills/skills/meta/skill-reviewer/SKILL.md +3 -0
- package/resources/skills/skills/meta/skill-reviewer/references/tdd-for-skills.md +55 -0
- package/resources/skills/skills/research/find-skills/SKILL.md +1 -1
- package/resources/skills/skills/review/code-review-deep/SKILL.md +20 -0
- package/resources/skills/skills/security/trivy-scan/SKILL.md +139 -0
- package/resources/skills/skills/security/trivy-scan/scripts/ensure-trivy.sh +21 -0
- package/resources/skills/skills/tools/ccusage/SKILL.md +142 -0
- package/src/commands/_index.ts +8 -0
- package/src/commands/ai.ts +2 -2
- package/src/commands/auto-detect.test.ts +74 -0
- package/src/commands/auto-detect.ts +9 -7
- package/src/commands/cli.test.ts +20 -4
- package/src/commands/cli.ts +36 -20
- package/src/commands/create-profile.ts +2 -2
- package/src/commands/debug.ts +2 -2
- package/src/commands/discover.ts +14 -4
- package/src/commands/export-docker.ts +1 -1
- package/src/commands/features-batch1.test.ts +1 -1
- package/src/commands/gates.ts +1 -1
- package/src/commands/import-profile.ts +1 -1
- package/src/commands/init.ts +15 -11
- package/src/commands/install.test.ts +192 -0
- package/src/commands/install.ts +610 -0
- package/src/commands/launch-handoff.e2e.test.ts +33 -1
- package/src/commands/launch.e2e.test.ts +15 -10
- package/src/commands/launch.ts +73 -116
- package/src/commands/materialize.ts +2 -2
- package/src/commands/prune.ts +1 -1
- package/src/commands/security-audit.ts +1 -1
- package/src/commands/shell.ts +7 -7
- package/src/commands/skill-report.ts +1 -1
- package/src/commands/skills.ts +3 -3
- package/src/commands/snapshot.ts +2 -2
- package/src/commands/summon.test.ts +116 -0
- package/src/commands/summon.ts +338 -0
- package/src/commands/trigger-gaps.ts +1 -1
- package/src/commands/use.ts +47 -3
- package/src/commands/watch-live.ts +5 -5
- package/src/commands/watch.ts +8 -8
- package/src/index.ts +2 -0
- package/src/lib/active-sessions.test.ts +3 -3
- package/src/lib/active-sessions.ts +4 -4
- package/src/lib/auto-detect.test.ts +172 -8
- package/src/lib/auto-detect.ts +191 -136
- package/src/lib/codex-persona-parity.test.ts +58 -0
- package/src/lib/companion-detect.test.ts +43 -1
- package/src/lib/companion-detect.ts +35 -0
- package/src/lib/credentials-sync.test.ts +121 -1
- package/src/lib/credentials-sync.ts +95 -1
- package/src/lib/cwd-resolver.test.ts +8 -8
- package/src/lib/cwd-resolver.ts +2 -2
- package/src/lib/dashboard-merge.test.ts +9 -4
- package/src/lib/dashboard-server.ts +1 -1
- package/src/lib/picker.test.ts +1 -1
- package/src/lib/picker.ts +5 -5
- package/src/lib/profile-merge.test.ts +8 -0
- package/src/lib/profile-names.test.ts +3 -3
- package/src/lib/runtime-install.ts +166 -0
- package/src/lib/runtime-materializer.test.ts +137 -0
- package/src/lib/runtime-materializer.ts +105 -2
- package/src/lib/skill-router.test.ts +38 -0
- package/src/lib/skill-router.ts +65 -4
- package/profiles/eu-tender-research/README.md +0 -48
- package/profiles/eu-tender-research/logo.png +0 -0
- package/profiles/eu-tender-research/profile.yaml +0 -108
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Banana Claude -- 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="Banana Claude 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,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Banana Claude -- 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 time
|
|
18
|
+
import urllib.request
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
|
|
23
|
+
OUTPUT_DIR = Path.home() / "Documents" / "nanobanana_generated"
|
|
24
|
+
API_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def edit_image(image_path, prompt, model, api_key):
|
|
28
|
+
"""Call Gemini API to edit an image."""
|
|
29
|
+
image_path = Path(image_path).resolve()
|
|
30
|
+
if not image_path.exists():
|
|
31
|
+
print(json.dumps({"error": True, "message": f"Image not found: {image_path}"}))
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
# Read and encode image
|
|
35
|
+
with open(image_path, "rb") as f:
|
|
36
|
+
image_b64 = base64.b64encode(f.read()).decode("utf-8")
|
|
37
|
+
|
|
38
|
+
# Determine MIME type
|
|
39
|
+
suffix = image_path.suffix.lower()
|
|
40
|
+
mime_types = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
41
|
+
".webp": "image/webp", ".gif": "image/gif"}
|
|
42
|
+
mime_type = mime_types.get(suffix, "image/png")
|
|
43
|
+
|
|
44
|
+
url = f"{API_BASE}/{model}:generateContent?key={api_key}"
|
|
45
|
+
|
|
46
|
+
body = {
|
|
47
|
+
"contents": [
|
|
48
|
+
{
|
|
49
|
+
"parts": [
|
|
50
|
+
{"text": prompt},
|
|
51
|
+
{"inlineData": {"mimeType": mime_type, "data": image_b64}},
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"generationConfig": {
|
|
56
|
+
"responseModalities": ["TEXT", "IMAGE"],
|
|
57
|
+
},
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
data = json.dumps(body).encode("utf-8")
|
|
61
|
+
req = urllib.request.Request(
|
|
62
|
+
url,
|
|
63
|
+
data=data,
|
|
64
|
+
headers={"Content-Type": "application/json"},
|
|
65
|
+
method="POST",
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
max_retries = 3
|
|
69
|
+
result = None
|
|
70
|
+
for attempt in range(max_retries):
|
|
71
|
+
try:
|
|
72
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
73
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
74
|
+
break # Success
|
|
75
|
+
except urllib.error.HTTPError as e:
|
|
76
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
77
|
+
if e.code == 429 and attempt < max_retries - 1:
|
|
78
|
+
wait = 2 ** (attempt + 1)
|
|
79
|
+
print(json.dumps({"retry": True, "attempt": attempt + 1, "wait_seconds": wait, "reason": "rate_limited"}), file=sys.stderr)
|
|
80
|
+
time.sleep(wait)
|
|
81
|
+
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
|
|
82
|
+
continue
|
|
83
|
+
if e.code == 400 and "FAILED_PRECONDITION" in error_body:
|
|
84
|
+
print(json.dumps({"error": True, "status": 400, "message": "Billing not enabled. Enable billing at https://aistudio.google.com/apikey"}))
|
|
85
|
+
sys.exit(1)
|
|
86
|
+
print(json.dumps({"error": True, "status": e.code, "message": error_body}))
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
except urllib.error.URLError as e:
|
|
89
|
+
print(json.dumps({"error": True, "message": str(e.reason)}))
|
|
90
|
+
sys.exit(1)
|
|
91
|
+
|
|
92
|
+
if result is None:
|
|
93
|
+
print(json.dumps({"error": True, "message": "Max retries exceeded"}))
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
# Extract image from response
|
|
97
|
+
candidates = result.get("candidates", [])
|
|
98
|
+
if not candidates:
|
|
99
|
+
finish_reason = result.get("promptFeedback", {}).get("blockReason", "UNKNOWN")
|
|
100
|
+
print(json.dumps({"error": True, "message": f"No candidates returned. Reason: {finish_reason}"}))
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
parts = candidates[0].get("content", {}).get("parts", [])
|
|
104
|
+
image_data = None
|
|
105
|
+
text_response = ""
|
|
106
|
+
|
|
107
|
+
for part in parts:
|
|
108
|
+
if "inlineData" in part:
|
|
109
|
+
image_data = part["inlineData"]["data"]
|
|
110
|
+
elif "text" in part:
|
|
111
|
+
text_response = part["text"]
|
|
112
|
+
|
|
113
|
+
if not image_data:
|
|
114
|
+
finish_reason = candidates[0].get("finishReason", "UNKNOWN")
|
|
115
|
+
print(json.dumps({"error": True, "message": f"No image in response. finishReason: {finish_reason}"}))
|
|
116
|
+
sys.exit(1)
|
|
117
|
+
|
|
118
|
+
# Save image
|
|
119
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
121
|
+
filename = f"banana_edit_{timestamp}.png"
|
|
122
|
+
output_path = (OUTPUT_DIR / filename).resolve()
|
|
123
|
+
|
|
124
|
+
with open(output_path, "wb") as f:
|
|
125
|
+
f.write(base64.b64decode(image_data))
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
"path": str(output_path),
|
|
129
|
+
"model": model,
|
|
130
|
+
"source": str(image_path),
|
|
131
|
+
"text": text_response,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main():
|
|
136
|
+
parser = argparse.ArgumentParser(description="Edit images via Gemini REST API")
|
|
137
|
+
parser.add_argument("--image", required=True, help="Path to input image")
|
|
138
|
+
parser.add_argument("--prompt", required=True, help="Edit instruction")
|
|
139
|
+
parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Model ID (default: {DEFAULT_MODEL})")
|
|
140
|
+
parser.add_argument("--api-key", default=None, help="Google AI API key (or set GOOGLE_AI_API_KEY env)")
|
|
141
|
+
|
|
142
|
+
args = parser.parse_args()
|
|
143
|
+
|
|
144
|
+
api_key = args.api_key or os.environ.get("GOOGLE_AI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
|
|
145
|
+
if not api_key:
|
|
146
|
+
print(json.dumps({"error": True, "message": "No API key. Set GOOGLE_AI_API_KEY env or pass --api-key"}))
|
|
147
|
+
sys.exit(1)
|
|
148
|
+
|
|
149
|
+
result = edit_image(
|
|
150
|
+
image_path=args.image,
|
|
151
|
+
prompt=args.prompt,
|
|
152
|
+
model=args.model,
|
|
153
|
+
api_key=api_key,
|
|
154
|
+
)
|
|
155
|
+
print(json.dumps(result, indent=2))
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__":
|
|
159
|
+
main()
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Banana Claude -- Direct API Fallback: Image Generation
|
|
3
|
+
|
|
4
|
+
Generate images via Gemini REST API when MCP is unavailable.
|
|
5
|
+
Uses only Python stdlib (no pip dependencies).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
generate.py --prompt "a cat in space" [--aspect-ratio 16:9] [--resolution 1K]
|
|
9
|
+
[--model MODEL] [--api-key KEY] [--thinking LEVEL] [--image-only]
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import base64
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
import urllib.request
|
|
19
|
+
from datetime import datetime
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
|
|
23
|
+
DEFAULT_RESOLUTION = "2K" # Must be uppercase -- lowercase values are silently rejected by the API
|
|
24
|
+
DEFAULT_RATIO = "1:1"
|
|
25
|
+
OUTPUT_DIR = Path.home() / "Documents" / "nanobanana_generated"
|
|
26
|
+
API_BASE = "https://generativelanguage.googleapis.com/v1beta/models"
|
|
27
|
+
|
|
28
|
+
VALID_RATIOS = {"1:1", "16:9", "9:16", "4:3", "3:4", "2:3", "3:2",
|
|
29
|
+
"4:5", "5:4", "1:4", "4:1", "1:8", "8:1", "21:9"}
|
|
30
|
+
VALID_RESOLUTIONS = {"512", "1K", "2K", "4K"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def generate_image(prompt, model, aspect_ratio, resolution, api_key,
|
|
34
|
+
thinking_level=None, image_only=False):
|
|
35
|
+
"""Call Gemini API to generate an image."""
|
|
36
|
+
url = f"{API_BASE}/{model}:generateContent?key={api_key}"
|
|
37
|
+
|
|
38
|
+
modalities = ["IMAGE"] if image_only else ["TEXT", "IMAGE"]
|
|
39
|
+
body = {
|
|
40
|
+
"contents": [{"parts": [{"text": prompt}]}],
|
|
41
|
+
"generationConfig": {
|
|
42
|
+
"responseModalities": modalities,
|
|
43
|
+
"imageConfig": {
|
|
44
|
+
"aspectRatio": aspect_ratio,
|
|
45
|
+
"imageSize": resolution,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if thinking_level:
|
|
51
|
+
body["generationConfig"]["thinkingConfig"] = {"thinkingLevel": thinking_level}
|
|
52
|
+
|
|
53
|
+
data = json.dumps(body).encode("utf-8")
|
|
54
|
+
req = urllib.request.Request(
|
|
55
|
+
url,
|
|
56
|
+
data=data,
|
|
57
|
+
headers={"Content-Type": "application/json"},
|
|
58
|
+
method="POST",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
max_retries = 3
|
|
62
|
+
result = None
|
|
63
|
+
for attempt in range(max_retries):
|
|
64
|
+
try:
|
|
65
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
66
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
67
|
+
break # Success
|
|
68
|
+
except urllib.error.HTTPError as e:
|
|
69
|
+
error_body = e.read().decode("utf-8") if e.fp else ""
|
|
70
|
+
if e.code == 429 and attempt < max_retries - 1:
|
|
71
|
+
wait = 2 ** (attempt + 1)
|
|
72
|
+
print(json.dumps({"retry": True, "attempt": attempt + 1, "wait_seconds": wait, "reason": "rate_limited"}), file=sys.stderr)
|
|
73
|
+
time.sleep(wait)
|
|
74
|
+
# Rebuild request for retry
|
|
75
|
+
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"}, method="POST")
|
|
76
|
+
continue
|
|
77
|
+
if e.code == 400 and "FAILED_PRECONDITION" in error_body:
|
|
78
|
+
print(json.dumps({"error": True, "status": 400, "message": "Billing not enabled. Enable billing at https://aistudio.google.com/apikey"}))
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
print(json.dumps({"error": True, "status": e.code, "message": error_body}))
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
except urllib.error.URLError as e:
|
|
83
|
+
print(json.dumps({"error": True, "message": str(e.reason)}))
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
if result is None:
|
|
87
|
+
print(json.dumps({"error": True, "message": "Max retries exceeded"}))
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
|
|
90
|
+
# Extract image from response
|
|
91
|
+
candidates = result.get("candidates", [])
|
|
92
|
+
if not candidates:
|
|
93
|
+
finish_reason = result.get("promptFeedback", {}).get("blockReason", "UNKNOWN")
|
|
94
|
+
print(json.dumps({"error": True, "message": f"No candidates returned. Reason: {finish_reason}"}))
|
|
95
|
+
sys.exit(1)
|
|
96
|
+
|
|
97
|
+
parts = candidates[0].get("content", {}).get("parts", [])
|
|
98
|
+
image_data = None
|
|
99
|
+
text_response = ""
|
|
100
|
+
|
|
101
|
+
for part in parts:
|
|
102
|
+
if "inlineData" in part:
|
|
103
|
+
image_data = part["inlineData"]["data"]
|
|
104
|
+
elif "text" in part:
|
|
105
|
+
text_response = part["text"]
|
|
106
|
+
|
|
107
|
+
if not image_data:
|
|
108
|
+
finish_reason = candidates[0].get("finishReason", "UNKNOWN")
|
|
109
|
+
print(json.dumps({"error": True, "message": f"No image in response. finishReason: {finish_reason}"}))
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
|
|
112
|
+
# Save image
|
|
113
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
114
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")
|
|
115
|
+
filename = f"banana_{timestamp}.png"
|
|
116
|
+
output_path = (OUTPUT_DIR / filename).resolve()
|
|
117
|
+
|
|
118
|
+
with open(output_path, "wb") as f:
|
|
119
|
+
f.write(base64.b64decode(image_data))
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
"path": str(output_path),
|
|
123
|
+
"model": model,
|
|
124
|
+
"aspect_ratio": aspect_ratio,
|
|
125
|
+
"resolution": resolution,
|
|
126
|
+
"text": text_response,
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def main():
|
|
131
|
+
parser = argparse.ArgumentParser(description="Generate images via Gemini REST API")
|
|
132
|
+
parser.add_argument("--prompt", required=True, help="Image generation prompt")
|
|
133
|
+
parser.add_argument("--aspect-ratio", default=DEFAULT_RATIO, help=f"Aspect ratio (default: {DEFAULT_RATIO})")
|
|
134
|
+
parser.add_argument("--resolution", default=DEFAULT_RESOLUTION, help=f"Resolution: 512, 1K, 2K, 4K (default: {DEFAULT_RESOLUTION})")
|
|
135
|
+
parser.add_argument("--model", default=DEFAULT_MODEL, help=f"Model ID (default: {DEFAULT_MODEL})")
|
|
136
|
+
parser.add_argument("--api-key", default=None, help="Google AI API key (or set GOOGLE_AI_API_KEY env)")
|
|
137
|
+
parser.add_argument("--thinking", default=None, choices=["minimal", "low", "medium", "high"], help="Thinking level")
|
|
138
|
+
parser.add_argument("--image-only", action="store_true", help="Return image only (no text)")
|
|
139
|
+
|
|
140
|
+
args = parser.parse_args()
|
|
141
|
+
|
|
142
|
+
if args.aspect_ratio not in VALID_RATIOS:
|
|
143
|
+
print(json.dumps({"error": True, "message": f"Invalid aspect ratio '{args.aspect_ratio}'. Valid: {sorted(VALID_RATIOS)}"}))
|
|
144
|
+
sys.exit(1)
|
|
145
|
+
|
|
146
|
+
if args.resolution not in VALID_RESOLUTIONS:
|
|
147
|
+
print(json.dumps({"error": True, "message": f"Invalid resolution '{args.resolution}'. Valid: {sorted(VALID_RESOLUTIONS)}"}))
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
|
|
150
|
+
api_key = args.api_key or os.environ.get("GOOGLE_AI_API_KEY") or os.environ.get("GOOGLE_API_KEY")
|
|
151
|
+
if not api_key:
|
|
152
|
+
print(json.dumps({"error": True, "message": "No API key. Set GOOGLE_AI_API_KEY env or pass --api-key"}))
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
|
|
155
|
+
result = generate_image(
|
|
156
|
+
prompt=args.prompt,
|
|
157
|
+
model=args.model,
|
|
158
|
+
aspect_ratio=args.aspect_ratio,
|
|
159
|
+
resolution=args.resolution,
|
|
160
|
+
api_key=api_key,
|
|
161
|
+
thinking_level=args.thinking,
|
|
162
|
+
image_only=args.image_only,
|
|
163
|
+
)
|
|
164
|
+
print(json.dumps(result, indent=2))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == "__main__":
|
|
168
|
+
main()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Banana Claude -- Brand/Style Presets
|
|
3
|
+
|
|
4
|
+
Manage reusable brand and style presets for consistent image generation.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
presets.py list
|
|
8
|
+
presets.py show NAME
|
|
9
|
+
presets.py create NAME --colors "#hex,#hex" --style "..." [options]
|
|
10
|
+
presets.py delete NAME --confirm
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import argparse
|
|
14
|
+
import json
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
PRESETS_DIR = Path.home() / ".banana" / "presets"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ensure_dir():
|
|
23
|
+
"""Ensure presets directory exists."""
|
|
24
|
+
PRESETS_DIR.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _sanitize_name(name):
|
|
28
|
+
"""Sanitize preset name to prevent path traversal."""
|
|
29
|
+
# Strip path separators and keep only safe characters
|
|
30
|
+
safe = re.sub(r'[^a-zA-Z0-9_\-]', '', name)
|
|
31
|
+
if not safe:
|
|
32
|
+
print("Error: Preset name must contain only letters, numbers, hyphens, and underscores.", file=sys.stderr)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
return safe
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _preset_path(name):
|
|
38
|
+
"""Get path for a preset file."""
|
|
39
|
+
safe_name = _sanitize_name(name)
|
|
40
|
+
return PRESETS_DIR / f"{safe_name}.json"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _load_preset(name):
|
|
44
|
+
"""Load a preset by name."""
|
|
45
|
+
path = _preset_path(name)
|
|
46
|
+
if not path.exists():
|
|
47
|
+
print(f"Error: Preset '{name}' not found.", file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
with open(path, "r") as f:
|
|
50
|
+
return json.load(f)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cmd_list(args):
|
|
54
|
+
"""List available presets."""
|
|
55
|
+
_ensure_dir()
|
|
56
|
+
presets = sorted(PRESETS_DIR.glob("*.json"))
|
|
57
|
+
if not presets:
|
|
58
|
+
print("No presets found. Create one with: presets.py create NAME --style \"...\"")
|
|
59
|
+
return
|
|
60
|
+
print(f"Available presets ({len(presets)}):\n")
|
|
61
|
+
for p in presets:
|
|
62
|
+
try:
|
|
63
|
+
with open(p, "r") as f:
|
|
64
|
+
data = json.load(f)
|
|
65
|
+
desc = data.get("description", "No description")
|
|
66
|
+
print(f" {p.stem:20s} -- {desc}")
|
|
67
|
+
except (json.JSONDecodeError, KeyError):
|
|
68
|
+
print(f" {p.stem:20s} -- (invalid preset file)")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_show(args):
|
|
72
|
+
"""Show full preset details."""
|
|
73
|
+
preset = _load_preset(args.name)
|
|
74
|
+
print(json.dumps(preset, indent=2))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def cmd_create(args):
|
|
78
|
+
"""Create a new preset."""
|
|
79
|
+
_ensure_dir()
|
|
80
|
+
path = _preset_path(args.name)
|
|
81
|
+
if path.exists():
|
|
82
|
+
print(f"Error: Preset '{args.name}' already exists. Use a different name.", file=sys.stderr)
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
colors = [c.strip() for c in args.colors.split(",")] if args.colors else []
|
|
86
|
+
|
|
87
|
+
preset = {
|
|
88
|
+
"name": args.name,
|
|
89
|
+
"description": args.description or f"Custom preset: {args.name}",
|
|
90
|
+
"colors": colors,
|
|
91
|
+
"style": args.style or "",
|
|
92
|
+
"typography": args.typography or "",
|
|
93
|
+
"lighting": args.lighting or "",
|
|
94
|
+
"mood": args.mood or "",
|
|
95
|
+
"default_ratio": args.ratio or "16:9",
|
|
96
|
+
"default_resolution": args.resolution or "2K",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
with open(path, "w") as f:
|
|
100
|
+
json.dump(preset, f, indent=2)
|
|
101
|
+
|
|
102
|
+
print(f"Preset '{args.name}' created at {path}")
|
|
103
|
+
print(json.dumps(preset, indent=2))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cmd_delete(args):
|
|
107
|
+
"""Delete a preset."""
|
|
108
|
+
if not args.confirm:
|
|
109
|
+
print("Error: Pass --confirm to delete the preset.", file=sys.stderr)
|
|
110
|
+
sys.exit(1)
|
|
111
|
+
path = _preset_path(args.name)
|
|
112
|
+
if not path.exists():
|
|
113
|
+
print(f"Error: Preset '{args.name}' not found.", file=sys.stderr)
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
path.unlink()
|
|
116
|
+
print(f"Preset '{args.name}' deleted.")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def main():
|
|
120
|
+
parser = argparse.ArgumentParser(description="Banana Claude Brand/Style Presets")
|
|
121
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
122
|
+
|
|
123
|
+
# list
|
|
124
|
+
sub.add_parser("list", help="List available presets")
|
|
125
|
+
|
|
126
|
+
# show
|
|
127
|
+
p_show = sub.add_parser("show", help="Show preset details")
|
|
128
|
+
p_show.add_argument("name", help="Preset name")
|
|
129
|
+
|
|
130
|
+
# create
|
|
131
|
+
p_create = sub.add_parser("create", help="Create a new preset")
|
|
132
|
+
p_create.add_argument("name", help="Preset name (e.g., tech-saas, luxury-brand)")
|
|
133
|
+
p_create.add_argument("--colors", default="", help="Comma-separated hex colors")
|
|
134
|
+
p_create.add_argument("--style", default="", help="Visual style description")
|
|
135
|
+
p_create.add_argument("--typography", default="", help="Typography description")
|
|
136
|
+
p_create.add_argument("--lighting", default="", help="Lighting description")
|
|
137
|
+
p_create.add_argument("--mood", default="", help="Mood/emotion description")
|
|
138
|
+
p_create.add_argument("--description", default="", help="Brief preset description")
|
|
139
|
+
p_create.add_argument("--ratio", default="16:9", help="Default aspect ratio")
|
|
140
|
+
p_create.add_argument("--resolution", default="2K", help="Default resolution")
|
|
141
|
+
p_create.add_argument("--force", action="store_true", help="Overwrite existing preset")
|
|
142
|
+
|
|
143
|
+
# delete
|
|
144
|
+
p_delete = sub.add_parser("delete", help="Delete a preset")
|
|
145
|
+
p_delete.add_argument("name", help="Preset name")
|
|
146
|
+
p_delete.add_argument("--confirm", action="store_true", help="Confirm deletion")
|
|
147
|
+
|
|
148
|
+
args = parser.parse_args()
|
|
149
|
+
cmds = {"list": cmd_list, "show": cmd_show, "create": cmd_create, "delete": cmd_delete}
|
|
150
|
+
cmds[args.command](args)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
main()
|