niahere 0.2.89 → 0.2.90
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/package.json
CHANGED
|
@@ -14,12 +14,14 @@ General-purpose image generation skill supporting **OpenAI** (default) and **Gem
|
|
|
14
14
|
## Setup
|
|
15
15
|
|
|
16
16
|
API keys in `~/.niahere/config.yaml`:
|
|
17
|
+
|
|
17
18
|
```yaml
|
|
18
19
|
openai_api_key: sk-...
|
|
19
20
|
gemini_api_key: AIza...
|
|
20
21
|
```
|
|
21
22
|
|
|
22
23
|
Or set via CLI or environment variables:
|
|
24
|
+
|
|
23
25
|
```bash
|
|
24
26
|
nia config set openai_api_key sk-...
|
|
25
27
|
nia config set gemini_api_key AIza...
|
|
@@ -29,25 +31,39 @@ Keys are resolved in order: `--api-key` flag > env var (`$OPENAI_API_KEY` / `$GE
|
|
|
29
31
|
|
|
30
32
|
## Providers & Models
|
|
31
33
|
|
|
32
|
-
| Provider
|
|
33
|
-
|
|
34
|
-
| **OpenAI** (default) | `gpt-image-
|
|
35
|
-
| **Gemini**
|
|
34
|
+
| Provider | Default Model | Alternatives |
|
|
35
|
+
| -------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------- |
|
|
36
|
+
| **OpenAI** (default) | `gpt-image-2` | `gpt-image-1.5`, `gpt-image-1-mini` |
|
|
37
|
+
| **Gemini** | `gemini-3.1-flash-image-preview` (Nano Banana 2) | `gemini-3-pro-image-preview` (Nano Banana Pro), `gemini-2.5-flash-image` (GA) |
|
|
38
|
+
|
|
39
|
+
`gpt-image-2` (Apr 2026) is the current OpenAI flagship — native reasoning, up to 16 reference images, native 2K. `dall-e-2`/`dall-e-3` were shut off on the API on May 12, 2026.
|
|
36
40
|
|
|
37
|
-
|
|
41
|
+
## Per-image pricing (May 2026)
|
|
42
|
+
|
|
43
|
+
| Model | Price |
|
|
44
|
+
| -------------------------------- | ------------------------------------------------------------------------------------ |
|
|
45
|
+
| `gpt-image-2` | ~$0.006 (low) → $0.053 (medium 1024) → $0.35 (high 4K). Token-based; Batch API −50%. |
|
|
46
|
+
| `gpt-image-1.5` | ~20% cheaper than gpt-image-1 across tiers |
|
|
47
|
+
| `gpt-image-1` | low $0.011, medium $0.042, high $0.167 (1024²) |
|
|
48
|
+
| `gemini-3-pro-image-preview` | $0.134 (1K/2K), $0.24 (4K) |
|
|
49
|
+
| `gemini-3.1-flash-image-preview` | $0.045 (512), $0.067 (1K), $0.101 (2K), $0.151 (4K) |
|
|
50
|
+
| `gemini-2.5-flash-image` | $0.039 standard, $0.0195 batch |
|
|
38
51
|
|
|
39
52
|
## Quick Reference
|
|
40
53
|
|
|
41
54
|
```bash
|
|
42
|
-
SCRIPT="/
|
|
55
|
+
SCRIPT="$(dirname "$0")/scripts/generate_image.py" # or the absolute path under your skills dir
|
|
43
56
|
|
|
44
|
-
# Basic generation (OpenAI, default)
|
|
57
|
+
# Basic generation (OpenAI gpt-image-2, default)
|
|
45
58
|
python3 $SCRIPT --prompt "A sunset over mountains"
|
|
46
59
|
|
|
47
60
|
# High quality
|
|
48
61
|
python3 $SCRIPT --prompt "Oil painting of a forest" --quality high
|
|
49
62
|
|
|
50
|
-
#
|
|
63
|
+
# 2K output (OpenAI gpt-image-2; Gemini 2K via --resolution)
|
|
64
|
+
python3 $SCRIPT --prompt "Detailed cityscape" --resolution 2K
|
|
65
|
+
|
|
66
|
+
# Aspect ratio
|
|
51
67
|
python3 $SCRIPT --prompt "Portrait photo" --aspect-ratio 3:4
|
|
52
68
|
|
|
53
69
|
# With reference image (OpenAI edit mode)
|
|
@@ -56,6 +72,10 @@ python3 $SCRIPT --prompt "Add a rainbow to this scene" --reference photo.png
|
|
|
56
72
|
# Gemini provider
|
|
57
73
|
python3 $SCRIPT --provider gemini --prompt "Watercolor sunset" --aspect-ratio 16:9
|
|
58
74
|
|
|
75
|
+
# Gemini 4K (use Pro — 3.1 Flash currently ignores 2K/4K and returns ~1K)
|
|
76
|
+
python3 $SCRIPT --provider gemini --model gemini-3-pro-image-preview \
|
|
77
|
+
--prompt "Cinematic landscape" --resolution 4K --aspect-ratio 16:9
|
|
78
|
+
|
|
59
79
|
# Gemini with reference
|
|
60
80
|
python3 $SCRIPT --provider gemini --reference face.png \
|
|
61
81
|
--prompt "Same person sitting in a cafe, natural lighting" --aspect-ratio 9:16
|
|
@@ -66,17 +86,24 @@ python3 $SCRIPT --prompt "A cat" --output /path/to/output/
|
|
|
66
86
|
|
|
67
87
|
## Aspect Ratios
|
|
68
88
|
|
|
69
|
-
| Use Case
|
|
70
|
-
|
|
71
|
-
| Square / social | `1:1`
|
|
72
|
-
| Portrait
|
|
73
|
-
| Landscape
|
|
74
|
-
| Phone / story
|
|
75
|
-
| Ultrawide
|
|
89
|
+
| Use Case | Ratio | Notes |
|
|
90
|
+
| --------------- | --------------- | ------------- |
|
|
91
|
+
| Square / social | `1:1` | Default |
|
|
92
|
+
| Portrait | `3:4` or `2:3` | Vertical |
|
|
93
|
+
| Landscape | `4:3` or `16:9` | Wide |
|
|
94
|
+
| Phone / story | `9:16` | Vertical tall |
|
|
95
|
+
| Ultrawide | `21:9` | Cinematic |
|
|
96
|
+
|
|
97
|
+
OpenAI maps ratios to closest supported size. At `--resolution 1K` (default): `1024x1024`, `1024x1536`, `1536x1024`. At `--resolution 2K`: `2048x2048`, `1536x2048`, `2048x1536` (gpt-image-2 only; dims must be multiples of 16, max ~2048).
|
|
98
|
+
|
|
99
|
+
## Resolution (`--resolution`)
|
|
76
100
|
|
|
77
|
-
OpenAI
|
|
101
|
+
- OpenAI: `1K` (default) or `2K` (gpt-image-2 only).
|
|
102
|
+
- Gemini: `1K` (default), `2K`, or `4K`. **Caveat:** `gemini-3.1-flash-image-preview` currently ignores 2K/4K and returns ~1K — use `gemini-3-pro-image-preview` for true 2K/4K.
|
|
78
103
|
|
|
79
|
-
## OpenAI Quality (
|
|
104
|
+
## OpenAI Quality (`--quality`)
|
|
105
|
+
|
|
106
|
+
Applies to all `gpt-image-*` models.
|
|
80
107
|
|
|
81
108
|
- `auto` (default) — let the model decide
|
|
82
109
|
- `high` — best quality, slower
|
|
@@ -100,6 +127,7 @@ For photorealistic results, use structured JSON prompts covering separate concer
|
|
|
100
127
|
```
|
|
101
128
|
|
|
102
129
|
Key principles:
|
|
130
|
+
|
|
103
131
|
1. **Separate concerns** — one aspect per block
|
|
104
132
|
2. **Specify camera** — lens mm and aperture drive realism
|
|
105
133
|
3. **Light direction** — "soft light from upper right" > "good lighting"
|
|
@@ -108,13 +136,14 @@ Key principles:
|
|
|
108
136
|
|
|
109
137
|
## Provider Selection Guide
|
|
110
138
|
|
|
111
|
-
| Need
|
|
112
|
-
|
|
113
|
-
| General image gen, highest quality
|
|
114
|
-
| Budget-friendly
|
|
115
|
-
| Reference-based identity (same face) | Gemini (better at preserving identity from reference)
|
|
116
|
-
| Image editing / inpainting
|
|
117
|
-
|
|
|
139
|
+
| Need | Use |
|
|
140
|
+
| ------------------------------------ | ------------------------------------------------------------ |
|
|
141
|
+
| General image gen, highest quality | OpenAI `gpt-image-2` |
|
|
142
|
+
| Budget-friendly | OpenAI `gpt-image-1-mini` or Gemini `gemini-2.5-flash-image` |
|
|
143
|
+
| Reference-based identity (same face) | Gemini (better at preserving identity from reference) |
|
|
144
|
+
| Image editing / inpainting | OpenAI edit mode (`--reference`) |
|
|
145
|
+
| 4K output | Gemini `gemini-3-pro-image-preview` (`--resolution 4K`) |
|
|
146
|
+
| Free tier / no OpenAI key | Gemini |
|
|
118
147
|
|
|
119
148
|
## Combining with Bella
|
|
120
149
|
|
|
@@ -3,21 +3,25 @@
|
|
|
3
3
|
General-purpose image generation using OpenAI (default) or Gemini.
|
|
4
4
|
|
|
5
5
|
Supports:
|
|
6
|
-
- OpenAI: gpt-image-
|
|
6
|
+
- OpenAI: gpt-image-2 (default), gpt-image-1.5, gpt-image-1-mini
|
|
7
7
|
- Gemini: gemini-3.1-flash-image-preview (default), gemini-3-pro-image-preview, gemini-2.5-flash-image
|
|
8
8
|
|
|
9
9
|
Usage:
|
|
10
10
|
# OpenAI (default)
|
|
11
11
|
python3 generate_image.py --prompt "A sunset over mountains"
|
|
12
12
|
|
|
13
|
+
# OpenAI 2K output (gpt-image-2 only)
|
|
14
|
+
python3 generate_image.py --prompt "Detailed cityscape" --resolution 2K
|
|
15
|
+
|
|
13
16
|
# OpenAI with reference image (edit mode)
|
|
14
17
|
python3 generate_image.py --prompt "Add a hot air balloon" --reference photo.png
|
|
15
18
|
|
|
16
19
|
# Gemini
|
|
17
20
|
python3 generate_image.py --provider gemini --prompt "A sunset over mountains"
|
|
18
21
|
|
|
19
|
-
# Gemini
|
|
20
|
-
python3 generate_image.py --provider gemini --
|
|
22
|
+
# Gemini Pro at 4K
|
|
23
|
+
python3 generate_image.py --provider gemini --model gemini-3-pro-image-preview \\
|
|
24
|
+
--prompt "Cinematic landscape" --resolution 4K --aspect-ratio 16:9
|
|
21
25
|
"""
|
|
22
26
|
|
|
23
27
|
from __future__ import annotations
|
|
@@ -40,17 +44,26 @@ NIA_CONFIG = NIA_HOME / "config.yaml"
|
|
|
40
44
|
TIMESTAMP_FORMAT = "%Y%m%d_%H%M%S"
|
|
41
45
|
|
|
42
46
|
# --- Provider defaults ---
|
|
43
|
-
OPENAI_DEFAULT_MODEL = "gpt-image-
|
|
47
|
+
OPENAI_DEFAULT_MODEL = "gpt-image-2"
|
|
44
48
|
GEMINI_DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
|
|
45
49
|
|
|
46
50
|
DEFAULT_ASPECT_RATIO = "1:1"
|
|
47
51
|
ALLOWED_ASPECT_RATIOS = (
|
|
48
|
-
"1:1",
|
|
49
|
-
"
|
|
52
|
+
"1:1",
|
|
53
|
+
"3:4",
|
|
54
|
+
"4:3",
|
|
55
|
+
"9:16",
|
|
56
|
+
"16:9",
|
|
57
|
+
"2:3",
|
|
58
|
+
"3:2",
|
|
59
|
+
"4:5",
|
|
60
|
+
"5:4",
|
|
61
|
+
"21:9",
|
|
50
62
|
)
|
|
51
63
|
|
|
52
|
-
# OpenAI size mappings (closest match for aspect ratio)
|
|
53
|
-
|
|
64
|
+
# OpenAI size mappings per resolution (closest match for aspect ratio).
|
|
65
|
+
# 2K caps at 2048; dims must be multiples of 16. gpt-image-2 only.
|
|
66
|
+
OPENAI_SIZE_MAP_1K = {
|
|
54
67
|
"1:1": "1024x1024",
|
|
55
68
|
"3:4": "1024x1536",
|
|
56
69
|
"4:3": "1536x1024",
|
|
@@ -62,10 +75,25 @@ OPENAI_SIZE_MAP = {
|
|
|
62
75
|
"5:4": "1536x1024",
|
|
63
76
|
"21:9": "1536x1024",
|
|
64
77
|
}
|
|
78
|
+
OPENAI_SIZE_MAP_2K = {
|
|
79
|
+
"1:1": "2048x2048",
|
|
80
|
+
"3:4": "1536x2048",
|
|
81
|
+
"4:3": "2048x1536",
|
|
82
|
+
"9:16": "1536x2048",
|
|
83
|
+
"16:9": "2048x1536",
|
|
84
|
+
"2:3": "1536x2048",
|
|
85
|
+
"3:2": "2048x1536",
|
|
86
|
+
"4:5": "1536x2048",
|
|
87
|
+
"5:4": "2048x1536",
|
|
88
|
+
"21:9": "2048x1536",
|
|
89
|
+
}
|
|
65
90
|
|
|
66
|
-
# OpenAI quality options
|
|
91
|
+
# OpenAI quality options (applies to all gpt-image-* models)
|
|
67
92
|
OPENAI_QUALITIES = ("auto", "high", "medium", "low")
|
|
68
93
|
|
|
94
|
+
# Resolution choices. OpenAI: 1K|2K (2K requires gpt-image-2). Gemini: 1K|2K|4K.
|
|
95
|
+
ALLOWED_RESOLUTIONS = ("1K", "2K", "4K")
|
|
96
|
+
|
|
69
97
|
|
|
70
98
|
def safe_mime(path: str) -> str:
|
|
71
99
|
mime, _ = mimetypes.guess_type(path)
|
|
@@ -92,6 +120,7 @@ def read_config_key(key: str) -> str:
|
|
|
92
120
|
return ""
|
|
93
121
|
try:
|
|
94
122
|
import importlib
|
|
123
|
+
|
|
95
124
|
yaml = importlib.import_module("yaml")
|
|
96
125
|
with NIA_CONFIG.open("r") as f:
|
|
97
126
|
config = yaml.safe_load(f)
|
|
@@ -110,10 +139,7 @@ def resolve_api_key(provider: str, cli_key: str | None) -> str:
|
|
|
110
139
|
return cli_key
|
|
111
140
|
|
|
112
141
|
if provider == "openai":
|
|
113
|
-
return (
|
|
114
|
-
os.environ.get("OPENAI_API_KEY", "")
|
|
115
|
-
or read_config_key("openai_api_key")
|
|
116
|
-
)
|
|
142
|
+
return os.environ.get("OPENAI_API_KEY", "") or read_config_key("openai_api_key")
|
|
117
143
|
else:
|
|
118
144
|
return (
|
|
119
145
|
os.environ.get("GEMINI_API_KEY", "")
|
|
@@ -124,6 +150,7 @@ def resolve_api_key(provider: str, cli_key: str | None) -> str:
|
|
|
124
150
|
|
|
125
151
|
# --- OpenAI Generation ---
|
|
126
152
|
|
|
153
|
+
|
|
127
154
|
def generate_openai(
|
|
128
155
|
api_key: str,
|
|
129
156
|
prompt: str,
|
|
@@ -139,18 +166,15 @@ def generate_openai(
|
|
|
139
166
|
return _openai_generate(api_key, prompt, model, size, quality, n)
|
|
140
167
|
|
|
141
168
|
|
|
142
|
-
def _openai_generate(
|
|
143
|
-
api_key: str, prompt: str, model: str, size: str, quality: str, n: int
|
|
144
|
-
) -> tuple[bytes, str]:
|
|
169
|
+
def _openai_generate(api_key: str, prompt: str, model: str, size: str, quality: str, n: int) -> tuple[bytes, str]:
|
|
145
170
|
url = "https://api.openai.com/v1/images/generations"
|
|
146
171
|
payload: dict = {
|
|
147
172
|
"model": model,
|
|
148
173
|
"prompt": prompt,
|
|
149
174
|
"n": n,
|
|
150
175
|
"size": size,
|
|
151
|
-
"response_format": "b64_json",
|
|
152
176
|
}
|
|
153
|
-
if model
|
|
177
|
+
if model.startswith("gpt-image-"):
|
|
154
178
|
payload["quality"] = quality
|
|
155
179
|
|
|
156
180
|
req = urllib.request.Request(
|
|
@@ -166,8 +190,7 @@ def _openai_generate(
|
|
|
166
190
|
|
|
167
191
|
|
|
168
192
|
def _openai_edit(
|
|
169
|
-
api_key: str, prompt: str, reference_path: str, model: str, size: str,
|
|
170
|
-
quality: str, n: int
|
|
193
|
+
api_key: str, prompt: str, reference_path: str, model: str, size: str, quality: str, n: int
|
|
171
194
|
) -> tuple[bytes, str]:
|
|
172
195
|
"""Use OpenAI images/edits endpoint with a reference image."""
|
|
173
196
|
import io
|
|
@@ -184,9 +207,7 @@ def _openai_edit(
|
|
|
184
207
|
filename = Path(filepath).name
|
|
185
208
|
mime = safe_mime(filepath)
|
|
186
209
|
body.write(f"--{boundary}\r\n".encode())
|
|
187
|
-
body.write(
|
|
188
|
-
f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n'.encode()
|
|
189
|
-
)
|
|
210
|
+
body.write(f'Content-Disposition: form-data; name="{name}"; filename="{filename}"\r\n'.encode())
|
|
190
211
|
body.write(f"Content-Type: {mime}\r\n\r\n".encode())
|
|
191
212
|
with open(filepath, "rb") as f:
|
|
192
213
|
body.write(f.read())
|
|
@@ -197,7 +218,7 @@ def _openai_edit(
|
|
|
197
218
|
add_field("model", model)
|
|
198
219
|
add_field("n", str(n))
|
|
199
220
|
add_field("size", size)
|
|
200
|
-
if model
|
|
221
|
+
if model.startswith("gpt-image-"):
|
|
201
222
|
add_field("quality", quality)
|
|
202
223
|
body.write(f"--{boundary}--\r\n".encode())
|
|
203
224
|
|
|
@@ -220,9 +241,7 @@ def _openai_request(req: urllib.request.Request) -> tuple[bytes, str]:
|
|
|
220
241
|
response = json.loads(resp.read().decode("utf-8"))
|
|
221
242
|
except urllib.error.HTTPError as e:
|
|
222
243
|
detail = e.read().decode("utf-8", errors="ignore")
|
|
223
|
-
raise RuntimeError(
|
|
224
|
-
f"OpenAI API error (HTTP {e.code}): {detail or e.reason}"
|
|
225
|
-
) from e
|
|
244
|
+
raise RuntimeError(f"OpenAI API error (HTTP {e.code}): {detail or e.reason}") from e
|
|
226
245
|
|
|
227
246
|
data_list = response.get("data", [])
|
|
228
247
|
if not data_list:
|
|
@@ -237,33 +256,40 @@ def _openai_request(req: urllib.request.Request) -> tuple[bytes, str]:
|
|
|
237
256
|
|
|
238
257
|
# --- Gemini Generation ---
|
|
239
258
|
|
|
259
|
+
|
|
240
260
|
def generate_gemini(
|
|
241
261
|
api_key: str,
|
|
242
262
|
prompt: str,
|
|
243
263
|
model: str,
|
|
244
264
|
aspect_ratio: str,
|
|
265
|
+
resolution: str = "1K",
|
|
245
266
|
reference_path: str | None = None,
|
|
246
267
|
) -> tuple[bytes, str]:
|
|
247
268
|
"""Generate image via Gemini API."""
|
|
248
|
-
url =
|
|
249
|
-
"https://generativelanguage.googleapis.com/"
|
|
250
|
-
f"v1beta/models/{model}:generateContent?key={api_key}"
|
|
251
|
-
)
|
|
269
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
|
|
252
270
|
|
|
253
271
|
parts: list[dict] = []
|
|
254
272
|
if reference_path and Path(reference_path).is_file():
|
|
255
|
-
parts.append(
|
|
256
|
-
|
|
257
|
-
"
|
|
258
|
-
|
|
273
|
+
parts.append(
|
|
274
|
+
{
|
|
275
|
+
"inlineData": {
|
|
276
|
+
"mimeType": safe_mime(reference_path),
|
|
277
|
+
"data": encode_file(reference_path),
|
|
278
|
+
}
|
|
259
279
|
}
|
|
260
|
-
|
|
280
|
+
)
|
|
261
281
|
parts.append({"text": prompt})
|
|
262
282
|
|
|
283
|
+
image_config: dict = {"aspectRatio": aspect_ratio}
|
|
284
|
+
if resolution and resolution != "1K":
|
|
285
|
+
# gemini-3.1-flash-image-preview currently ignores this and returns ~1K (Google bug);
|
|
286
|
+
# gemini-3-pro-image-preview honors it. Send anyway so it works once Flash is fixed.
|
|
287
|
+
image_config["imageSize"] = resolution
|
|
288
|
+
|
|
263
289
|
payload = {
|
|
264
290
|
"contents": [{"parts": parts}],
|
|
265
291
|
"generationConfig": {
|
|
266
|
-
"imageConfig":
|
|
292
|
+
"imageConfig": image_config,
|
|
267
293
|
"responseModalities": ["TEXT", "IMAGE"],
|
|
268
294
|
},
|
|
269
295
|
}
|
|
@@ -280,9 +306,7 @@ def generate_gemini(
|
|
|
280
306
|
response = json.loads(resp.read().decode("utf-8"))
|
|
281
307
|
except urllib.error.HTTPError as e:
|
|
282
308
|
detail = e.read().decode("utf-8", errors="ignore")
|
|
283
|
-
raise RuntimeError(
|
|
284
|
-
f"Gemini API error (HTTP {e.code}): {detail or e.reason}"
|
|
285
|
-
) from e
|
|
309
|
+
raise RuntimeError(f"Gemini API error (HTTP {e.code}): {detail or e.reason}") from e
|
|
286
310
|
|
|
287
311
|
candidates = response.get("candidates", [])
|
|
288
312
|
if not candidates:
|
|
@@ -298,13 +322,12 @@ def generate_gemini(
|
|
|
298
322
|
if mime and img_data:
|
|
299
323
|
return base64.b64decode(img_data), mime
|
|
300
324
|
|
|
301
|
-
raise RuntimeError(
|
|
302
|
-
f"No image in Gemini response: {json.dumps(response, indent=2)}"
|
|
303
|
-
)
|
|
325
|
+
raise RuntimeError(f"No image in Gemini response: {json.dumps(response, indent=2)}")
|
|
304
326
|
|
|
305
327
|
|
|
306
328
|
# --- CLI ---
|
|
307
329
|
|
|
330
|
+
|
|
308
331
|
def main() -> None:
|
|
309
332
|
parser = argparse.ArgumentParser(
|
|
310
333
|
description="Generate images using OpenAI (default) or Gemini.",
|
|
@@ -319,39 +342,58 @@ Examples:
|
|
|
319
342
|
""",
|
|
320
343
|
)
|
|
321
344
|
parser.add_argument(
|
|
322
|
-
"--provider",
|
|
345
|
+
"--provider",
|
|
346
|
+
choices=["openai", "gemini"],
|
|
347
|
+
default="openai",
|
|
323
348
|
help="Image generation provider. Default: openai.",
|
|
324
349
|
)
|
|
325
350
|
parser.add_argument(
|
|
326
|
-
"--prompt",
|
|
351
|
+
"--prompt",
|
|
352
|
+
required=True,
|
|
327
353
|
help="Text prompt describing the image to generate.",
|
|
328
354
|
)
|
|
329
355
|
parser.add_argument(
|
|
330
|
-
"--reference",
|
|
356
|
+
"--reference",
|
|
357
|
+
default=None,
|
|
331
358
|
help="Path to a reference image. OpenAI uses edit mode; Gemini includes it as context.",
|
|
332
359
|
)
|
|
333
360
|
parser.add_argument(
|
|
334
|
-
"--model",
|
|
361
|
+
"--model",
|
|
362
|
+
default=None,
|
|
335
363
|
help=f"Model override. Defaults: OpenAI={OPENAI_DEFAULT_MODEL}, Gemini={GEMINI_DEFAULT_MODEL}.",
|
|
336
364
|
)
|
|
337
365
|
parser.add_argument(
|
|
338
|
-
"--aspect-ratio",
|
|
366
|
+
"--aspect-ratio",
|
|
367
|
+
default=DEFAULT_ASPECT_RATIO,
|
|
368
|
+
choices=ALLOWED_ASPECT_RATIOS,
|
|
339
369
|
help=f"Aspect ratio. Default: {DEFAULT_ASPECT_RATIO}.",
|
|
340
370
|
)
|
|
341
371
|
parser.add_argument(
|
|
342
|
-
"--quality",
|
|
343
|
-
|
|
372
|
+
"--quality",
|
|
373
|
+
default="auto",
|
|
374
|
+
choices=OPENAI_QUALITIES,
|
|
375
|
+
help="OpenAI quality (all gpt-image-* models). Default: auto.",
|
|
376
|
+
)
|
|
377
|
+
parser.add_argument(
|
|
378
|
+
"--resolution",
|
|
379
|
+
default="1K",
|
|
380
|
+
choices=ALLOWED_RESOLUTIONS,
|
|
381
|
+
help="Output resolution. OpenAI: 1K|2K (2K needs gpt-image-2). Gemini: 1K|2K|4K (2K/4K reliable on Pro only). Default: 1K.",
|
|
344
382
|
)
|
|
345
383
|
parser.add_argument(
|
|
346
|
-
"--n",
|
|
384
|
+
"--n",
|
|
385
|
+
type=int,
|
|
386
|
+
default=1,
|
|
347
387
|
help="Number of images (OpenAI only). Default: 1.",
|
|
348
388
|
)
|
|
349
389
|
parser.add_argument(
|
|
350
|
-
"--output",
|
|
390
|
+
"--output",
|
|
391
|
+
default=None,
|
|
351
392
|
help="Output path. Directory = timestamped file. Default: /tmp/.",
|
|
352
393
|
)
|
|
353
394
|
parser.add_argument(
|
|
354
|
-
"--api-key",
|
|
395
|
+
"--api-key",
|
|
396
|
+
default=None,
|
|
355
397
|
help="API key override. Otherwise reads from env var or ~/.niahere/config.yaml.",
|
|
356
398
|
)
|
|
357
399
|
args = parser.parse_args()
|
|
@@ -364,8 +406,7 @@ Examples:
|
|
|
364
406
|
config_key = "openai_api_key" if provider == "openai" else "gemini_api_key"
|
|
365
407
|
env_var = "OPENAI_API_KEY" if provider == "openai" else "GEMINI_API_KEY"
|
|
366
408
|
raise SystemExit(
|
|
367
|
-
f"Missing API key. Provide --api-key, set {env_var} in environment, "
|
|
368
|
-
f"or add {config_key} to {NIA_CONFIG}."
|
|
409
|
+
f"Missing API key. Provide --api-key, set {env_var} in environment, or add {config_key} to {NIA_CONFIG}."
|
|
369
410
|
)
|
|
370
411
|
|
|
371
412
|
if args.reference and not Path(args.reference).expanduser().is_file():
|
|
@@ -375,15 +416,29 @@ Examples:
|
|
|
375
416
|
|
|
376
417
|
try:
|
|
377
418
|
if provider == "openai":
|
|
378
|
-
|
|
419
|
+
if args.resolution == "4K":
|
|
420
|
+
raise SystemExit("OpenAI does not support 4K via this script — use --provider gemini --resolution 4K.")
|
|
421
|
+
if args.resolution == "2K" and model != "gpt-image-2":
|
|
422
|
+
raise SystemExit(f"2K is only supported on gpt-image-2 (got --model {model}).")
|
|
423
|
+
size_map = OPENAI_SIZE_MAP_2K if args.resolution == "2K" else OPENAI_SIZE_MAP_1K
|
|
424
|
+
size = size_map.get(args.aspect_ratio, size_map["1:1"])
|
|
379
425
|
image_data, mime = generate_openai(
|
|
380
|
-
api_key=api_key,
|
|
381
|
-
|
|
426
|
+
api_key=api_key,
|
|
427
|
+
prompt=args.prompt,
|
|
428
|
+
model=model,
|
|
429
|
+
size=size,
|
|
430
|
+
quality=args.quality,
|
|
431
|
+
reference_path=ref,
|
|
432
|
+
n=args.n,
|
|
382
433
|
)
|
|
383
434
|
else:
|
|
384
435
|
image_data, mime = generate_gemini(
|
|
385
|
-
api_key=api_key,
|
|
386
|
-
|
|
436
|
+
api_key=api_key,
|
|
437
|
+
prompt=args.prompt,
|
|
438
|
+
model=model,
|
|
439
|
+
aspect_ratio=args.aspect_ratio,
|
|
440
|
+
resolution=args.resolution,
|
|
441
|
+
reference_path=ref,
|
|
387
442
|
)
|
|
388
443
|
|
|
389
444
|
ext = ".png" if "png" in mime else ".jpg"
|
|
@@ -391,7 +446,7 @@ Examples:
|
|
|
391
446
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
392
447
|
out.write_bytes(image_data)
|
|
393
448
|
print(f"Saved: {out}")
|
|
394
|
-
print(f"Provider: {provider} | Model: {model} |
|
|
449
|
+
print(f"Provider: {provider} | Model: {model} | Ratio: {args.aspect_ratio} | Resolution: {args.resolution}")
|
|
395
450
|
except Exception as exc:
|
|
396
451
|
print(f"Error: {exc}", file=sys.stderr)
|
|
397
452
|
raise SystemExit(1) from exc
|