img-cli 0.1.0__py3-none-any.whl
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.
- img_cli/__init__.py +13 -0
- img_cli/_version.py +24 -0
- img_cli/cli.py +264 -0
- img_cli/client.py +189 -0
- img_cli/config.py +71 -0
- img_cli/image_utils.py +159 -0
- img_cli/logger.py +90 -0
- img_cli-0.1.0.dist-info/METADATA +139 -0
- img_cli-0.1.0.dist-info/RECORD +12 -0
- img_cli-0.1.0.dist-info/WHEEL +4 -0
- img_cli-0.1.0.dist-info/entry_points.txt +2 -0
- img_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
img_cli/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""img-cli: CLI tool for OpenAI-compatible Image Generation and Editing."""
|
|
2
|
+
|
|
3
|
+
try:
|
|
4
|
+
from img_cli._version import __version__, __version_tuple__
|
|
5
|
+
except ImportError:
|
|
6
|
+
try:
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
|
|
9
|
+
__version__ = version("img-cli")
|
|
10
|
+
__version_tuple__ = (0, 0, 0)
|
|
11
|
+
except Exception:
|
|
12
|
+
__version__ = "0.0.0.dev0"
|
|
13
|
+
__version_tuple__ = (0, 0, 0, "dev0")
|
img_cli/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
img_cli/cli.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"""Command line interface for img-cli."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from img_cli import __version__
|
|
10
|
+
from img_cli.client import ImageClient
|
|
11
|
+
from img_cli.config import AuthError, load_auth_config
|
|
12
|
+
from img_cli.image_utils import save_images
|
|
13
|
+
from img_cli.logger import create_audit_record, log_audit
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
err_console = Console(stderr=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.group(context_settings={"help_option_names": ["-h", "--help"]})
|
|
20
|
+
@click.version_option(version=__version__, message="img-cli %(version)s")
|
|
21
|
+
def main():
|
|
22
|
+
"""img-cli: 通过 OpenAI Images API 生成或编辑图片。"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@main.command(name="generate", help="根据文字描述生成图片。")
|
|
26
|
+
@click.argument("prompt", type=str)
|
|
27
|
+
@click.option(
|
|
28
|
+
"-m",
|
|
29
|
+
"--model",
|
|
30
|
+
default="gpt-image-2",
|
|
31
|
+
show_default=True,
|
|
32
|
+
help="模型名称(如 gpt-image-2, dall-e-3)",
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"-s",
|
|
36
|
+
"--size",
|
|
37
|
+
default="1024x1024",
|
|
38
|
+
show_default=True,
|
|
39
|
+
help="图片尺寸(如 1024x1024, 1536x1024, 1024x1536, auto)",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"-q", "--quality", default=None, help="质量设置(low/medium/high/auto 或 standard/hd)"
|
|
43
|
+
)
|
|
44
|
+
@click.option("-n", "--n", default=1, type=int, show_default=True, help="生成数量")
|
|
45
|
+
@click.option("-o", "--output", default=".", show_default=True, help="输出目录")
|
|
46
|
+
@click.option(
|
|
47
|
+
"--format",
|
|
48
|
+
"format_",
|
|
49
|
+
default="png",
|
|
50
|
+
show_default=True,
|
|
51
|
+
type=click.Choice(["png", "jpeg", "webp"], case_sensitive=False),
|
|
52
|
+
help="输出格式",
|
|
53
|
+
)
|
|
54
|
+
def generate(
|
|
55
|
+
prompt: str,
|
|
56
|
+
model: str,
|
|
57
|
+
size: str,
|
|
58
|
+
quality: str | None,
|
|
59
|
+
n: int,
|
|
60
|
+
output: str,
|
|
61
|
+
format_: str,
|
|
62
|
+
):
|
|
63
|
+
"""根据文字描述生成图片。"""
|
|
64
|
+
start_time = time.time()
|
|
65
|
+
saved_files: list[str] = []
|
|
66
|
+
error_msg: str | None = None
|
|
67
|
+
revised_prompt: str | None = None
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
auth = load_auth_config()
|
|
71
|
+
except AuthError as e:
|
|
72
|
+
err_console.print(f"[bold red]{e}[/bold red]")
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
console.print(f"[cyan]正在请求模型 [bold]{model}[/bold] 生成图片...[/cyan]")
|
|
77
|
+
client = ImageClient(auth)
|
|
78
|
+
result = client.generate(
|
|
79
|
+
prompt=prompt,
|
|
80
|
+
model=model,
|
|
81
|
+
size=size,
|
|
82
|
+
quality=quality,
|
|
83
|
+
n=n,
|
|
84
|
+
)
|
|
85
|
+
revised_prompt = result.revised_prompt
|
|
86
|
+
|
|
87
|
+
# Save images
|
|
88
|
+
saved_paths = save_images(result.images_bytes, output_dir=output, format=format_)
|
|
89
|
+
saved_files = [str(p) for p in saved_paths]
|
|
90
|
+
|
|
91
|
+
duration = time.time() - start_time
|
|
92
|
+
|
|
93
|
+
# Print revised prompt if available
|
|
94
|
+
if revised_prompt:
|
|
95
|
+
console.print("\n[bold yellow]优化后的 Prompt (revised_prompt):[/bold yellow]")
|
|
96
|
+
console.print(f"[dim]{revised_prompt}[/dim]")
|
|
97
|
+
|
|
98
|
+
# Print success info
|
|
99
|
+
console.print(f"\n[bold green]✓ 成功生成 {len(saved_files)} 张图片:[/bold green]")
|
|
100
|
+
for f in saved_files:
|
|
101
|
+
console.print(f" - [underline]{f}[/underline]")
|
|
102
|
+
console.print(f"[dim]耗时: {duration:.2f} 秒[/dim]")
|
|
103
|
+
|
|
104
|
+
# Log success audit
|
|
105
|
+
audit = create_audit_record(
|
|
106
|
+
command="generate",
|
|
107
|
+
prompt=prompt,
|
|
108
|
+
model=model,
|
|
109
|
+
duration_seconds=duration,
|
|
110
|
+
status="success",
|
|
111
|
+
size=size,
|
|
112
|
+
quality=quality,
|
|
113
|
+
n=n,
|
|
114
|
+
output_dir=output,
|
|
115
|
+
files=saved_files,
|
|
116
|
+
revised_prompt=revised_prompt,
|
|
117
|
+
)
|
|
118
|
+
log_audit(audit)
|
|
119
|
+
|
|
120
|
+
except Exception as e:
|
|
121
|
+
duration = time.time() - start_time
|
|
122
|
+
error_msg = str(e)
|
|
123
|
+
err_console.print(f"[bold red]生成图片失败:[/bold red] {error_msg}")
|
|
124
|
+
|
|
125
|
+
# Log error audit
|
|
126
|
+
audit = create_audit_record(
|
|
127
|
+
command="generate",
|
|
128
|
+
prompt=prompt,
|
|
129
|
+
model=model,
|
|
130
|
+
duration_seconds=duration,
|
|
131
|
+
status="error",
|
|
132
|
+
size=size,
|
|
133
|
+
quality=quality,
|
|
134
|
+
n=n,
|
|
135
|
+
output_dir=output,
|
|
136
|
+
error=error_msg,
|
|
137
|
+
)
|
|
138
|
+
log_audit(audit)
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@main.command(name="edit", help="根据文字描述修改已有图片。支持本地文件路径和网络图片 URL。")
|
|
143
|
+
@click.argument("prompt", type=str)
|
|
144
|
+
@click.option("-i", "--image", "image_path", required=True, help="原图路径或 HTTP URL")
|
|
145
|
+
@click.option("--mask", default=None, help="蒙版图路径或 HTTP URL(PNG,透明区域为待编辑区域)")
|
|
146
|
+
@click.option(
|
|
147
|
+
"-m",
|
|
148
|
+
"--model",
|
|
149
|
+
default="gpt-image-2",
|
|
150
|
+
show_default=True,
|
|
151
|
+
help="模型名称(如 gpt-image-2, dall-e-2)",
|
|
152
|
+
)
|
|
153
|
+
@click.option("-s", "--size", default="auto", show_default=True, help="尺寸(如 auto, 1024x1024)")
|
|
154
|
+
@click.option(
|
|
155
|
+
"-q", "--quality", default="auto", show_default=True, help="质量(如 auto, low, medium, high)"
|
|
156
|
+
)
|
|
157
|
+
@click.option("-n", "--n", default=1, type=int, show_default=True, help="生成数量")
|
|
158
|
+
@click.option("-o", "--output", default=".", show_default=True, help="输出目录")
|
|
159
|
+
@click.option(
|
|
160
|
+
"--format",
|
|
161
|
+
"format_",
|
|
162
|
+
default="png",
|
|
163
|
+
show_default=True,
|
|
164
|
+
type=click.Choice(["png", "jpeg", "webp"], case_sensitive=False),
|
|
165
|
+
help="输出格式",
|
|
166
|
+
)
|
|
167
|
+
def edit(
|
|
168
|
+
prompt: str,
|
|
169
|
+
image_path: str,
|
|
170
|
+
mask: str | None,
|
|
171
|
+
model: str,
|
|
172
|
+
size: str,
|
|
173
|
+
quality: str,
|
|
174
|
+
n: int,
|
|
175
|
+
output: str,
|
|
176
|
+
format_: str,
|
|
177
|
+
):
|
|
178
|
+
"""根据文字描述修改已有图片。"""
|
|
179
|
+
start_time = time.time()
|
|
180
|
+
saved_files: list[str] = []
|
|
181
|
+
error_msg: str | None = None
|
|
182
|
+
revised_prompt: str | None = None
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
auth = load_auth_config()
|
|
186
|
+
except AuthError as e:
|
|
187
|
+
err_console.print(f"[bold red]{e}[/bold red]")
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
console.print(f"[cyan]正在使用模型 [bold]{model}[/bold] 编辑图片...[/cyan]")
|
|
192
|
+
client = ImageClient(auth)
|
|
193
|
+
result = client.edit(
|
|
194
|
+
prompt=prompt,
|
|
195
|
+
image_source=image_path,
|
|
196
|
+
mask_source=mask,
|
|
197
|
+
model=model,
|
|
198
|
+
size=size,
|
|
199
|
+
quality=quality,
|
|
200
|
+
n=n,
|
|
201
|
+
)
|
|
202
|
+
revised_prompt = result.revised_prompt
|
|
203
|
+
|
|
204
|
+
# Save images
|
|
205
|
+
saved_paths = save_images(result.images_bytes, output_dir=output, format=format_)
|
|
206
|
+
saved_files = [str(p) for p in saved_paths]
|
|
207
|
+
|
|
208
|
+
duration = time.time() - start_time
|
|
209
|
+
|
|
210
|
+
# Print revised prompt if available
|
|
211
|
+
if revised_prompt:
|
|
212
|
+
console.print("\n[bold yellow]优化后的 Prompt (revised_prompt):[/bold yellow]")
|
|
213
|
+
console.print(f"[dim]{revised_prompt}[/dim]")
|
|
214
|
+
|
|
215
|
+
# Print success info
|
|
216
|
+
console.print(f"\n[bold green]✓ 成功编辑并保存 {len(saved_files)} 张图片:[/bold green]")
|
|
217
|
+
for f in saved_files:
|
|
218
|
+
console.print(f" - [underline]{f}[/underline]")
|
|
219
|
+
console.print(f"[dim]耗时: {duration:.2f} 秒[/dim]")
|
|
220
|
+
|
|
221
|
+
# Log success audit
|
|
222
|
+
audit = create_audit_record(
|
|
223
|
+
command="edit",
|
|
224
|
+
prompt=prompt,
|
|
225
|
+
model=model,
|
|
226
|
+
duration_seconds=duration,
|
|
227
|
+
status="success",
|
|
228
|
+
size=size,
|
|
229
|
+
quality=quality,
|
|
230
|
+
n=n,
|
|
231
|
+
output_dir=output,
|
|
232
|
+
input_image=image_path,
|
|
233
|
+
mask=mask,
|
|
234
|
+
files=saved_files,
|
|
235
|
+
revised_prompt=revised_prompt,
|
|
236
|
+
)
|
|
237
|
+
log_audit(audit)
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
duration = time.time() - start_time
|
|
241
|
+
error_msg = str(e)
|
|
242
|
+
err_console.print(f"[bold red]编辑图片失败:[/bold red] {error_msg}")
|
|
243
|
+
|
|
244
|
+
# Log error audit
|
|
245
|
+
audit = create_audit_record(
|
|
246
|
+
command="edit",
|
|
247
|
+
prompt=prompt,
|
|
248
|
+
model=model,
|
|
249
|
+
duration_seconds=duration,
|
|
250
|
+
status="error",
|
|
251
|
+
size=size,
|
|
252
|
+
quality=quality,
|
|
253
|
+
n=n,
|
|
254
|
+
output_dir=output,
|
|
255
|
+
input_image=image_path,
|
|
256
|
+
mask=mask,
|
|
257
|
+
error=error_msg,
|
|
258
|
+
)
|
|
259
|
+
log_audit(audit)
|
|
260
|
+
sys.exit(1)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
main()
|
img_cli/client.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""OpenAI Images API client wrapper for generate and edit operations."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from openai import OpenAI
|
|
8
|
+
|
|
9
|
+
from img_cli.config import AuthConfig
|
|
10
|
+
from img_cli.image_utils import ensure_png_bytes, fetch_image_data
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class GenerationResult:
|
|
15
|
+
images_bytes: list[bytes]
|
|
16
|
+
revised_prompt: str | None = None
|
|
17
|
+
duration_seconds: float = 0.0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ImageClient:
|
|
21
|
+
"""Wrapper around OpenAI Images API."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, auth: AuthConfig, timeout: float = 300.0):
|
|
24
|
+
self.auth = auth
|
|
25
|
+
self.client = OpenAI(
|
|
26
|
+
api_key=auth.api_key,
|
|
27
|
+
base_url=auth.api_base,
|
|
28
|
+
timeout=timeout,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def _extract_images(self, response_data: list) -> tuple[list[bytes], str | None]:
|
|
32
|
+
"""Extract raw image bytes and revised_prompt from API response data."""
|
|
33
|
+
images_bytes: list[bytes] = []
|
|
34
|
+
revised_prompt: str | None = None
|
|
35
|
+
|
|
36
|
+
for item in response_data:
|
|
37
|
+
if hasattr(item, "revised_prompt") and item.revised_prompt:
|
|
38
|
+
revised_prompt = item.revised_prompt
|
|
39
|
+
|
|
40
|
+
if getattr(item, "b64_json", None):
|
|
41
|
+
images_bytes.append(base64.b64decode(item.b64_json))
|
|
42
|
+
elif getattr(item, "url", None):
|
|
43
|
+
# Download image from URL
|
|
44
|
+
data = fetch_image_data(item.url)
|
|
45
|
+
images_bytes.append(data)
|
|
46
|
+
else:
|
|
47
|
+
raise ValueError("API 响应中未包含有效的 b64_json 或 url 数据")
|
|
48
|
+
|
|
49
|
+
return images_bytes, revised_prompt
|
|
50
|
+
|
|
51
|
+
def generate(
|
|
52
|
+
self,
|
|
53
|
+
prompt: str,
|
|
54
|
+
model: str = "gpt-image-2",
|
|
55
|
+
size: str | None = "1024x1024",
|
|
56
|
+
quality: str | None = None,
|
|
57
|
+
n: int = 1,
|
|
58
|
+
response_format: str = "b64_json",
|
|
59
|
+
) -> GenerationResult:
|
|
60
|
+
"""Generate images from prompt.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
prompt: Text description of the image.
|
|
64
|
+
model: Model name (gpt-image-2, dall-e-3, etc.).
|
|
65
|
+
size: Image dimensions (e.g. 1024x1024, auto).
|
|
66
|
+
quality: Quality setting (low/medium/high/auto or standard/hd).
|
|
67
|
+
n: Number of images to generate.
|
|
68
|
+
response_format: 'b64_json' or 'url'.
|
|
69
|
+
"""
|
|
70
|
+
start_time = time.time()
|
|
71
|
+
kwargs: dict = {
|
|
72
|
+
"model": model,
|
|
73
|
+
"prompt": prompt,
|
|
74
|
+
"n": n,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Size handling
|
|
78
|
+
if size and size.lower() != "auto":
|
|
79
|
+
kwargs["size"] = size
|
|
80
|
+
elif model.startswith("gpt-image"):
|
|
81
|
+
# gpt-image models support 'auto' size
|
|
82
|
+
kwargs["size"] = size or "1024x1024"
|
|
83
|
+
elif size and size.lower() == "auto":
|
|
84
|
+
# For dall-e models, 'auto' is not accepted, fallback to 1024x1024
|
|
85
|
+
kwargs["size"] = "1024x1024"
|
|
86
|
+
|
|
87
|
+
# Quality handling
|
|
88
|
+
if quality:
|
|
89
|
+
if model == "dall-e-3":
|
|
90
|
+
# dall-e-3 only accepts standard or hd
|
|
91
|
+
if quality.lower() in ("hd", "high"):
|
|
92
|
+
kwargs["quality"] = "hd"
|
|
93
|
+
else:
|
|
94
|
+
kwargs["quality"] = "standard"
|
|
95
|
+
elif model == "dall-e-2":
|
|
96
|
+
# dall-e-2 does not support quality
|
|
97
|
+
pass
|
|
98
|
+
else:
|
|
99
|
+
kwargs["quality"] = quality
|
|
100
|
+
|
|
101
|
+
# Response format
|
|
102
|
+
if response_format:
|
|
103
|
+
kwargs["response_format"] = response_format
|
|
104
|
+
|
|
105
|
+
# For dall-e-3, n must be 1
|
|
106
|
+
if model == "dall-e-3":
|
|
107
|
+
kwargs["n"] = 1
|
|
108
|
+
|
|
109
|
+
response = self.client.images.generate(**kwargs)
|
|
110
|
+
duration = time.time() - start_time
|
|
111
|
+
|
|
112
|
+
images_bytes, revised_prompt = self._extract_images(response.data)
|
|
113
|
+
return GenerationResult(
|
|
114
|
+
images_bytes=images_bytes,
|
|
115
|
+
revised_prompt=revised_prompt,
|
|
116
|
+
duration_seconds=duration,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def edit(
|
|
120
|
+
self,
|
|
121
|
+
prompt: str,
|
|
122
|
+
image_source: str,
|
|
123
|
+
mask_source: str | None = None,
|
|
124
|
+
model: str = "gpt-image-2",
|
|
125
|
+
size: str | None = "auto",
|
|
126
|
+
quality: str | None = "auto",
|
|
127
|
+
n: int = 1,
|
|
128
|
+
response_format: str = "b64_json",
|
|
129
|
+
) -> GenerationResult:
|
|
130
|
+
"""Edit an existing image using text prompt and optional mask.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
prompt: Text description of the modification.
|
|
134
|
+
image_source: Local path or HTTP URL of the input image.
|
|
135
|
+
mask_source: Optional local path or HTTP URL of the mask image.
|
|
136
|
+
model: Model name.
|
|
137
|
+
size: Size parameter.
|
|
138
|
+
quality: Quality parameter.
|
|
139
|
+
n: Number of images.
|
|
140
|
+
response_format: 'b64_json' or 'url'.
|
|
141
|
+
"""
|
|
142
|
+
start_time = time.time()
|
|
143
|
+
|
|
144
|
+
# Fetch and prepare base image
|
|
145
|
+
raw_image = fetch_image_data(image_source)
|
|
146
|
+
png_image = ensure_png_bytes(raw_image)
|
|
147
|
+
|
|
148
|
+
kwargs: dict = {
|
|
149
|
+
"model": model,
|
|
150
|
+
"prompt": prompt,
|
|
151
|
+
"image": ("image.png", png_image, "image/png"),
|
|
152
|
+
"n": n,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Mask handling
|
|
156
|
+
if mask_source:
|
|
157
|
+
raw_mask = fetch_image_data(mask_source)
|
|
158
|
+
png_mask = ensure_png_bytes(raw_mask, force_rgba=True)
|
|
159
|
+
kwargs["mask"] = ("mask.png", png_mask, "image/png")
|
|
160
|
+
|
|
161
|
+
# Size handling
|
|
162
|
+
if size and size.lower() != "auto":
|
|
163
|
+
kwargs["size"] = size
|
|
164
|
+
elif model.startswith("gpt-image"):
|
|
165
|
+
if size:
|
|
166
|
+
kwargs["size"] = size
|
|
167
|
+
elif size and size.lower() == "auto":
|
|
168
|
+
# For dall-e-2, size must be explicit
|
|
169
|
+
kwargs["size"] = "1024x1024"
|
|
170
|
+
|
|
171
|
+
# Quality handling
|
|
172
|
+
if quality and quality.lower() != "auto":
|
|
173
|
+
if model != "dall-e-2":
|
|
174
|
+
kwargs["quality"] = quality
|
|
175
|
+
elif quality and model.startswith("gpt-image"):
|
|
176
|
+
kwargs["quality"] = quality
|
|
177
|
+
|
|
178
|
+
if response_format:
|
|
179
|
+
kwargs["response_format"] = response_format
|
|
180
|
+
|
|
181
|
+
response = self.client.images.edit(**kwargs)
|
|
182
|
+
duration = time.time() - start_time
|
|
183
|
+
|
|
184
|
+
images_bytes, revised_prompt = self._extract_images(response.data)
|
|
185
|
+
return GenerationResult(
|
|
186
|
+
images_bytes=images_bytes,
|
|
187
|
+
revised_prompt=revised_prompt,
|
|
188
|
+
duration_seconds=duration,
|
|
189
|
+
)
|
img_cli/config.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Configuration and authentication management for img-cli."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
AUTH_DIR = Path.home() / ".img_gen"
|
|
9
|
+
AUTH_FILE = AUTH_DIR / "auth.json"
|
|
10
|
+
DEFAULT_API_BASE = "https://api.openai.com/v1"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AuthError(Exception):
|
|
14
|
+
"""Raised when authentication credentials cannot be found or are invalid."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AuthConfig:
|
|
19
|
+
api_key: str
|
|
20
|
+
api_base: str = DEFAULT_API_BASE
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def load_auth_config(custom_auth_file: Path | None = None) -> AuthConfig:
|
|
24
|
+
"""Load authentication credentials from auth.json or environment variables.
|
|
25
|
+
|
|
26
|
+
Loading precedence:
|
|
27
|
+
1. ~/.img_gen/auth.json (or custom_auth_file if specified)
|
|
28
|
+
2. Environment variables (OPENAI_API_KEY / IMG_GEN_API_KEY, OPENAI_BASE_URL / OPENAI_API_BASE)
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
AuthError: If no valid credentials can be found.
|
|
32
|
+
"""
|
|
33
|
+
target_file = custom_auth_file or AUTH_FILE
|
|
34
|
+
api_key: str | None = None
|
|
35
|
+
api_base: str | None = None
|
|
36
|
+
|
|
37
|
+
if target_file.exists() and target_file.is_file():
|
|
38
|
+
try:
|
|
39
|
+
with open(target_file, encoding="utf-8") as f:
|
|
40
|
+
data = json.load(f)
|
|
41
|
+
if isinstance(data, dict):
|
|
42
|
+
api_key = data.get("api_key")
|
|
43
|
+
api_base = data.get("api_base") or data.get("base_url")
|
|
44
|
+
except Exception as e:
|
|
45
|
+
raise AuthError(f"读取认证文件失败 ({target_file}): {e}") from e
|
|
46
|
+
|
|
47
|
+
# Fallback to environment variables if not found in file
|
|
48
|
+
if not api_key:
|
|
49
|
+
api_key = os.environ.get("IMG_GEN_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
|
50
|
+
|
|
51
|
+
if not api_base:
|
|
52
|
+
api_base = (
|
|
53
|
+
os.environ.get("IMG_GEN_API_BASE")
|
|
54
|
+
or os.environ.get("OPENAI_BASE_URL")
|
|
55
|
+
or os.environ.get("OPENAI_API_BASE")
|
|
56
|
+
or DEFAULT_API_BASE
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if not api_key or not api_key.strip():
|
|
60
|
+
error_msg = (
|
|
61
|
+
"未找到认证文件\n"
|
|
62
|
+
"请创建配置文件 ~/.img_gen/auth.json:\n"
|
|
63
|
+
" mkdir -p ~/.img_gen\n"
|
|
64
|
+
' echo \'{"api_key":"sk-xxx","api_base":"https://api.openai.com/v1"}\' > ~/.img_gen/auth.json'
|
|
65
|
+
)
|
|
66
|
+
raise AuthError(error_msg)
|
|
67
|
+
|
|
68
|
+
# Normalize api_base (strip trailing slash)
|
|
69
|
+
api_base = api_base.rstrip("/")
|
|
70
|
+
|
|
71
|
+
return AuthConfig(api_key=api_key.strip(), api_base=api_base)
|
img_cli/image_utils.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Image utilities for fetching, format conversion, and saving."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from PIL import Image
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ImageFetchError(Exception):
|
|
12
|
+
"""Raised when an image cannot be fetched or read."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def fetch_image_data(source: str, timeout: float = 60.0) -> bytes:
|
|
16
|
+
"""Fetch image bytes from a local path or a remote HTTP/HTTPS URL.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
source: Local file path (e.g. ~/Desktop/img.png) or URL (http/https).
|
|
20
|
+
timeout: Request timeout for URL downloads in seconds.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Image bytes.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ImageFetchError: If the file does not exist or URL download fails.
|
|
27
|
+
"""
|
|
28
|
+
if source.startswith(("http://", "https://")):
|
|
29
|
+
try:
|
|
30
|
+
with httpx.Client(timeout=timeout, follow_redirects=True) as client:
|
|
31
|
+
response = client.get(source)
|
|
32
|
+
response.raise_for_status()
|
|
33
|
+
return response.content
|
|
34
|
+
except httpx.HTTPStatusError as e:
|
|
35
|
+
raise ImageFetchError(f"下载图片失败 (HTTP {e.response.status_code}): {source}") from e
|
|
36
|
+
except Exception as e:
|
|
37
|
+
raise ImageFetchError(f"下载图片失败: {source} ({e})") from e
|
|
38
|
+
else:
|
|
39
|
+
path = Path(source).expanduser().resolve()
|
|
40
|
+
if not path.exists():
|
|
41
|
+
raise ImageFetchError(f"图片文件不存在: {source}")
|
|
42
|
+
if not path.is_file():
|
|
43
|
+
raise ImageFetchError(f"指定路径不是文件: {source}")
|
|
44
|
+
try:
|
|
45
|
+
return path.read_bytes()
|
|
46
|
+
except Exception as e:
|
|
47
|
+
raise ImageFetchError(f"读取图片文件失败 ({path}): {e}") from e
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ensure_png_bytes(image_data: bytes, force_rgba: bool = False) -> bytes:
|
|
51
|
+
"""Ensure the given image data is in valid PNG format (required by OpenAI Edit API).
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
image_data: Raw image bytes.
|
|
55
|
+
force_rgba: If True, convert image mode to RGBA.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
PNG image bytes.
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
img = Image.open(io.BytesIO(image_data))
|
|
62
|
+
# If it's already PNG and mode matches, check if conversion needed
|
|
63
|
+
if img.format == "PNG" and (not force_rgba or img.mode == "RGBA"):
|
|
64
|
+
return image_data
|
|
65
|
+
|
|
66
|
+
target_mode = "RGBA" if (force_rgba or "A" in img.mode) else "RGB"
|
|
67
|
+
if img.mode != target_mode:
|
|
68
|
+
img = img.convert(target_mode)
|
|
69
|
+
|
|
70
|
+
buf = io.BytesIO()
|
|
71
|
+
img.save(buf, format="PNG")
|
|
72
|
+
return buf.getvalue()
|
|
73
|
+
except Exception as e:
|
|
74
|
+
raise ValueError(f"处理图片格式失败: {e}") from e
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def convert_and_save_image(
|
|
78
|
+
image_data: bytes,
|
|
79
|
+
target_path: Path,
|
|
80
|
+
target_format: str = "png",
|
|
81
|
+
) -> Path:
|
|
82
|
+
"""Save image bytes to target_path, converting format if requested."""
|
|
83
|
+
fmt = target_format.lower().strip(".")
|
|
84
|
+
if fmt == "jpg":
|
|
85
|
+
fmt = "jpeg"
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
img = Image.open(io.BytesIO(image_data))
|
|
89
|
+
orig_fmt = (img.format or "").lower()
|
|
90
|
+
if orig_fmt == "jpg":
|
|
91
|
+
orig_fmt = "jpeg"
|
|
92
|
+
|
|
93
|
+
# If formats already match, write directly
|
|
94
|
+
if orig_fmt == fmt:
|
|
95
|
+
target_path.write_bytes(image_data)
|
|
96
|
+
return target_path
|
|
97
|
+
|
|
98
|
+
# Handle RGBA conversion for JPEG
|
|
99
|
+
if fmt == "jpeg" and img.mode in ("RGBA", "LA", "P"):
|
|
100
|
+
background = Image.new("RGB", img.size, (255, 255, 255))
|
|
101
|
+
if img.mode == "RGBA":
|
|
102
|
+
background.paste(img, mask=img.split()[3])
|
|
103
|
+
else:
|
|
104
|
+
background.paste(img.convert("RGBA"))
|
|
105
|
+
background.save(target_path, format="JPEG", quality=95)
|
|
106
|
+
else:
|
|
107
|
+
save_format = "JPEG" if fmt == "jpeg" else fmt.upper()
|
|
108
|
+
img.save(target_path, format=save_format)
|
|
109
|
+
|
|
110
|
+
return target_path
|
|
111
|
+
except Exception:
|
|
112
|
+
# Fallback to direct bytes write if Pillow fails
|
|
113
|
+
target_path.write_bytes(image_data)
|
|
114
|
+
return target_path
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def save_images(
|
|
118
|
+
images_data: list[bytes],
|
|
119
|
+
output_dir: str | Path = ".",
|
|
120
|
+
format: str = "png",
|
|
121
|
+
) -> list[Path]:
|
|
122
|
+
"""Save a list of image byte buffers to output directory with standard naming.
|
|
123
|
+
|
|
124
|
+
File naming format: img_<timestamp>_<index>.<format>
|
|
125
|
+
Example: img_20260829_210700_1.png
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
images_data: List of raw image data bytes.
|
|
129
|
+
output_dir: Directory path to save files into.
|
|
130
|
+
format: Target image extension/format ('png', 'jpeg', 'webp').
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of absolute Paths of the saved images.
|
|
134
|
+
"""
|
|
135
|
+
out_dir = Path(output_dir).expanduser().resolve()
|
|
136
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
137
|
+
|
|
138
|
+
fmt = format.lower().strip(".")
|
|
139
|
+
ext = "jpg" if fmt == "jpeg" else fmt
|
|
140
|
+
|
|
141
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
142
|
+
saved_paths: list[Path] = []
|
|
143
|
+
|
|
144
|
+
for i, data in enumerate(images_data, start=1):
|
|
145
|
+
filename = f"img_{timestamp}_{i}.{ext}"
|
|
146
|
+
target_path = out_dir / filename
|
|
147
|
+
|
|
148
|
+
# Handle conflict if file already exists in current second
|
|
149
|
+
if target_path.exists():
|
|
150
|
+
counter = 1
|
|
151
|
+
while target_path.exists():
|
|
152
|
+
filename = f"img_{timestamp}_{i}_{counter}.{ext}"
|
|
153
|
+
target_path = out_dir / filename
|
|
154
|
+
counter += 1
|
|
155
|
+
|
|
156
|
+
saved_file = convert_and_save_image(data, target_path, target_format=fmt)
|
|
157
|
+
saved_paths.append(saved_file)
|
|
158
|
+
|
|
159
|
+
return saved_paths
|
img_cli/logger.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Audit logging for img-cli operations (JSON Lines format)."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, dataclass
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
LOG_DIR = Path.home() / ".img_gen"
|
|
9
|
+
LOG_FILE = LOG_DIR / "img.log"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AuditRecord:
|
|
14
|
+
timestamp: str
|
|
15
|
+
command: str
|
|
16
|
+
prompt: str
|
|
17
|
+
model: str
|
|
18
|
+
size: str | None = None
|
|
19
|
+
quality: str | None = None
|
|
20
|
+
n: int = 1
|
|
21
|
+
output_dir: str = "."
|
|
22
|
+
input_image: str | None = None
|
|
23
|
+
mask: str | None = None
|
|
24
|
+
status: str = "success"
|
|
25
|
+
duration_seconds: float = 0.0
|
|
26
|
+
files: list[str] | None = None
|
|
27
|
+
revised_prompt: str | None = None
|
|
28
|
+
error: str | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def log_audit(
|
|
32
|
+
record: AuditRecord,
|
|
33
|
+
custom_log_file: Path | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
"""Append an audit record to the JSON Lines log file.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
record: The AuditRecord to serialize and write.
|
|
39
|
+
custom_log_file: Optional path override for testing.
|
|
40
|
+
"""
|
|
41
|
+
target_file = custom_log_file or LOG_FILE
|
|
42
|
+
try:
|
|
43
|
+
target_file.parent.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
data = asdict(record)
|
|
45
|
+
# Ensure files is a list
|
|
46
|
+
if data.get("files") is None:
|
|
47
|
+
data["files"] = []
|
|
48
|
+
|
|
49
|
+
line = json.dumps(data, ensure_ascii=False)
|
|
50
|
+
with open(target_file, "a", encoding="utf-8") as f:
|
|
51
|
+
f.write(line + "\n")
|
|
52
|
+
except Exception:
|
|
53
|
+
# Non-fatal: logging failure should not crash the command
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def create_audit_record(
|
|
58
|
+
command: str,
|
|
59
|
+
prompt: str,
|
|
60
|
+
model: str,
|
|
61
|
+
duration_seconds: float,
|
|
62
|
+
status: str = "success",
|
|
63
|
+
size: str | None = None,
|
|
64
|
+
quality: str | None = None,
|
|
65
|
+
n: int = 1,
|
|
66
|
+
output_dir: str = ".",
|
|
67
|
+
input_image: str | None = None,
|
|
68
|
+
mask: str | None = None,
|
|
69
|
+
files: list[str] | None = None,
|
|
70
|
+
revised_prompt: str | None = None,
|
|
71
|
+
error: str | None = None,
|
|
72
|
+
) -> AuditRecord:
|
|
73
|
+
"""Helper to create an AuditRecord with current UTC timestamp."""
|
|
74
|
+
return AuditRecord(
|
|
75
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
76
|
+
command=command,
|
|
77
|
+
prompt=prompt,
|
|
78
|
+
model=model,
|
|
79
|
+
size=size,
|
|
80
|
+
quality=quality,
|
|
81
|
+
n=n,
|
|
82
|
+
output_dir=output_dir,
|
|
83
|
+
input_image=input_image,
|
|
84
|
+
mask=mask,
|
|
85
|
+
status=status,
|
|
86
|
+
duration_seconds=round(duration_seconds, 2),
|
|
87
|
+
files=files or [],
|
|
88
|
+
revised_prompt=revised_prompt,
|
|
89
|
+
error=error,
|
|
90
|
+
)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: img-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI tool for OpenAI-compatible Image Generation and Editing
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Requires-Dist: click>=8.1.7
|
|
8
|
+
Requires-Dist: httpx>=0.27.0
|
|
9
|
+
Requires-Dist: openai>=1.50.0
|
|
10
|
+
Requires-Dist: pillow>=10.2.0
|
|
11
|
+
Requires-Dist: pydantic>=2.6.0
|
|
12
|
+
Requires-Dist: rich>=13.7.0
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# img-cli
|
|
16
|
+
|
|
17
|
+
命令行图片生成与编辑工具,基于 Python + uv 开发,封装 OpenAI 兼容的 Images API(文生图 `generate` 与图生图编辑 `edit`)。
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<img src="assets/poster.png" alt="img-cli Poster" width="600" />
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 特性
|
|
26
|
+
|
|
27
|
+
- 🚀 **简单易用**:一行命令完成图片生成和图片编辑。
|
|
28
|
+
- 🔑 **灵活认证**:支持 `~/.img_gen/auth.json` 配置文件及环境变量。
|
|
29
|
+
- 📝 **审计日志**:自动在 `~/.img_gen/img.log` 记录每次请求详情(JSON Lines 格式)。
|
|
30
|
+
- 🖼️ **格式与尺寸自适应**:支持 PNG / JPEG / WEBP,自动进行 RGBA 蒙版和格式转换。
|
|
31
|
+
- 🌐 **网络与本地图片支持**:编辑模式下原图和蒙版均支持本地路径(含 `~`)及 HTTP/HTTPS URL。
|
|
32
|
+
- 🤖 **Agent 友好**:清晰的标准输出与结构化错误码,便于自动化工作流接入。
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 安装与环境准备
|
|
37
|
+
|
|
38
|
+
### 通过 PyPI 安装
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# 使用 uv 工具安装(推荐)
|
|
42
|
+
uv tool install img-cli
|
|
43
|
+
|
|
44
|
+
# 或通过 pip 安装
|
|
45
|
+
pip install img-cli
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 本地开发与运行
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 1. 运行测试 / 调试
|
|
52
|
+
uv run img-cli --help
|
|
53
|
+
|
|
54
|
+
# 2. 从源码安装 CLI
|
|
55
|
+
uv tool install .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 认证配置
|
|
61
|
+
|
|
62
|
+
首次使用前,请创建配置文件 `~/.img_gen/auth.json`:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mkdir -p ~/.img_gen
|
|
66
|
+
echo '{"api_key":"sk-xxx","api_base":"https://api.openai.com/v1"}' > ~/.img_gen/auth.json
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
也可通过环境变量传入:
|
|
70
|
+
- `OPENAI_API_KEY`(或 `IMG_GEN_API_KEY`)
|
|
71
|
+
- `OPENAI_BASE_URL`(或 `OPENAI_API_BASE` / `IMG_GEN_API_BASE`)
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 快速使用
|
|
76
|
+
|
|
77
|
+
### 1. 文生图 (generate)
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# 基础生成(默认模型 gpt-image-2,保存至当前目录)
|
|
81
|
+
img-cli generate "一只戴墨镜的猫坐在沙滩上"
|
|
82
|
+
|
|
83
|
+
# 指定模型、尺寸与输出目录
|
|
84
|
+
img-cli generate "a futuristic city at sunset" -m dall-e-3 -s 1792x1024 -q hd -o ~/Desktop
|
|
85
|
+
|
|
86
|
+
# 指定输出格式为 JPEG
|
|
87
|
+
img-cli generate "产品白底宣传图,极简风格" -m gpt-image-1 -q high --format jpeg -o ./output
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 2. 图片编辑 (edit)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# 本地图片全图编辑
|
|
94
|
+
img-cli edit "给猫戴上一顶红色圣诞帽" --image ./cat.png -o ./output
|
|
95
|
+
|
|
96
|
+
# 网络图片直接编辑
|
|
97
|
+
img-cli edit "替换背景为星空" --image https://example.com/photo.jpg -o ./output
|
|
98
|
+
|
|
99
|
+
# 带蒙版的局部编辑(蒙版透明区域为待修改区域)
|
|
100
|
+
img-cli edit "用一朵玫瑰替换这里" --image ./photo.png --mask ./mask.png -o ./output
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 命令参数参考
|
|
106
|
+
|
|
107
|
+
### `img-cli generate <prompt>`
|
|
108
|
+
|
|
109
|
+
| 参数 | 默认值 | 说明 |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `prompt` | 必填 | 提示词(支持中文与英文) |
|
|
112
|
+
| `-m`, `--model` | `gpt-image-2` | 模型名称(如 `gpt-image-2`, `dall-e-3`, `gpt-image-1`) |
|
|
113
|
+
| `-s`, `--size` | `1024x1024` | 尺寸(如 `1024x1024`, `1536x1024`, `1024x1536`, `auto`) |
|
|
114
|
+
| `-q`, `--quality` | 无 | 质量(`low` / `medium` / `high` / `auto` / `standard` / `hd`) |
|
|
115
|
+
| `-n`, `--n` | `1` | 生成张数 |
|
|
116
|
+
| `-o`, `--output` | `.` | 图片输出目录 |
|
|
117
|
+
| `--format` | `png` | 输出格式:`png` / `jpeg` / `webp` |
|
|
118
|
+
|
|
119
|
+
### `img-cli edit <prompt>`
|
|
120
|
+
|
|
121
|
+
| 参数 | 默认值 | 说明 |
|
|
122
|
+
|---|---|---|
|
|
123
|
+
| `prompt` | 必填 | 修改描述 |
|
|
124
|
+
| `-i`, `--image` | 必填 | 原图:本地路径或 HTTP/HTTPS URL |
|
|
125
|
+
| `--mask` | 无 | 蒙版:本地路径或 URL(PNG 透明区域待修改) |
|
|
126
|
+
| `-m`, `--model` | `gpt-image-2` | 模型名称 |
|
|
127
|
+
| `-s`, `--size` | `auto` | 尺寸 |
|
|
128
|
+
| `-q`, `--quality` | `auto` | 质量 |
|
|
129
|
+
| `-n`, `--n` | `1` | 生成张数 |
|
|
130
|
+
| `-o`, `--output` | `.` | 图片输出目录 |
|
|
131
|
+
| `--format` | `png` | 输出格式:`png` / `jpeg` / `webp` |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 运行测试
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
uv run pytest -v
|
|
139
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
img_cli/__init__.py,sha256=vBugPxeKBKESQ9GPph61LPDDw7RdQ05XZ0v8Z11tcL8,405
|
|
2
|
+
img_cli/_version.py,sha256=n_5vdJsPNu7wZ57LGuRL585uvll-hiuvZUBWzdG0RQU,520
|
|
3
|
+
img_cli/cli.py,sha256=ej4rW3hF224tVVk6CYflyfY1EW-UE17_mYVNpvh9UaY,7963
|
|
4
|
+
img_cli/client.py,sha256=af0RU21tXlQrkj5vrztTona1ktnajWqyX4jA0ie-Zfg,6227
|
|
5
|
+
img_cli/config.py,sha256=a7M6U2Dk-5I9kUPA2Yq9cm87YaIW_lchCADcQTJdqZM,2350
|
|
6
|
+
img_cli/image_utils.py,sha256=XPRcw55fe3byixDwxxeHYP9TcQ54S1UZHstywRXXd-E,5309
|
|
7
|
+
img_cli/logger.py,sha256=InI38rc7PIJtPi7vQ76DF5yZ3uRzISuwO5jz4ghc4GA,2419
|
|
8
|
+
img_cli-0.1.0.dist-info/METADATA,sha256=Y_BAsVTtPzahxFMFrt3DAzSrXUU9TDEnEilIbXJT0b0,4027
|
|
9
|
+
img_cli-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
10
|
+
img_cli-0.1.0.dist-info/entry_points.txt,sha256=VRZPH6rNa4O0BkTUOmX2KcX6vHMi8Wj2mSKqhO5U4wE,45
|
|
11
|
+
img_cli-0.1.0.dist-info/licenses/LICENSE,sha256=DWcolN0n9Am669uQA1E04dsdunFqNcqXk5YsOR8xS5o,1065
|
|
12
|
+
img_cli-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 8DE4732A
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|