pixelbudget 0.3.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.
- pixelbudget/__init__.py +45 -0
- pixelbudget/__main__.py +5 -0
- pixelbudget/cli.py +376 -0
- pixelbudget/data/models.json +466 -0
- pixelbudget/demo.py +45 -0
- pixelbudget/errors.py +71 -0
- pixelbudget/estimate.py +73 -0
- pixelbudget/fit.py +110 -0
- pixelbudget/i18n.py +43 -0
- pixelbudget/locales/en.json +127 -0
- pixelbudget/locales/ko.json +127 -0
- pixelbudget/measure.py +113 -0
- pixelbudget/py.typed +0 -0
- pixelbudget/registry.py +142 -0
- pixelbudget/report.py +88 -0
- pixelbudget/rules/__init__.py +13 -0
- pixelbudget/rules/fixed.py +9 -0
- pixelbudget/rules/patch.py +71 -0
- pixelbudget/rules/tile.py +32 -0
- pixelbudget/slicing.py +93 -0
- pixelbudget/types.py +175 -0
- pixelbudget-0.3.0.dist-info/METADATA +313 -0
- pixelbudget-0.3.0.dist-info/RECORD +26 -0
- pixelbudget-0.3.0.dist-info/WHEEL +4 -0
- pixelbudget-0.3.0.dist-info/entry_points.txt +2 -0
- pixelbudget-0.3.0.dist-info/licenses/LICENSE +21 -0
pixelbudget/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""pixelbudget: offline image-token estimates, budget fit and readable slicing for vision LLMs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__version__ = "0.3.0"
|
|
6
|
+
|
|
7
|
+
from .errors import (
|
|
8
|
+
ImageTooLargeError,
|
|
9
|
+
InvalidBudgetError,
|
|
10
|
+
InvalidOverlapError,
|
|
11
|
+
InvalidSizeError,
|
|
12
|
+
ModelTableError,
|
|
13
|
+
PixelBudgetError,
|
|
14
|
+
UnknownModelError,
|
|
15
|
+
UnreadableImageError,
|
|
16
|
+
UnsupportedDetailError,
|
|
17
|
+
)
|
|
18
|
+
from .estimate import estimate
|
|
19
|
+
from .fit import fit, fit_size
|
|
20
|
+
from .measure import measure
|
|
21
|
+
from .registry import get_model, list_models
|
|
22
|
+
from .report import build_report, report
|
|
23
|
+
from .slicing import plan_slices, slice_image
|
|
24
|
+
from .types import (
|
|
25
|
+
Estimate,
|
|
26
|
+
FitPlan,
|
|
27
|
+
FitResult,
|
|
28
|
+
ImageSource,
|
|
29
|
+
ModelSpec,
|
|
30
|
+
Piece,
|
|
31
|
+
ReportResult,
|
|
32
|
+
ReportRow,
|
|
33
|
+
Size,
|
|
34
|
+
SlicePlan,
|
|
35
|
+
SliceResult,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"__version__", "estimate", "fit", "fit_size", "measure", "get_model", "list_models", "report",
|
|
40
|
+
"build_report", "plan_slices", "slice_image", "Estimate", "FitPlan", "FitResult", "ImageSource",
|
|
41
|
+
"ModelSpec", "Piece", "ReportResult", "ReportRow", "Size", "SlicePlan", "SliceResult",
|
|
42
|
+
"PixelBudgetError", "UnknownModelError", "UnsupportedDetailError", "InvalidBudgetError",
|
|
43
|
+
"InvalidOverlapError", "InvalidSizeError", "UnreadableImageError", "ImageTooLargeError",
|
|
44
|
+
"ModelTableError",
|
|
45
|
+
]
|
pixelbudget/__main__.py
ADDED
pixelbudget/cli.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""Command line interface (argparse). All user-facing text comes from ``locales/*.json``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
import unicodedata
|
|
10
|
+
from functools import partial
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Sequence
|
|
13
|
+
|
|
14
|
+
from PIL import Image
|
|
15
|
+
|
|
16
|
+
from . import __version__
|
|
17
|
+
from .errors import PixelBudgetError
|
|
18
|
+
from .fit import fit, fit_size
|
|
19
|
+
from .i18n import LANGS, resolve_lang, t
|
|
20
|
+
from .measure import measure
|
|
21
|
+
from .registry import get_model, list_models, set_models_file
|
|
22
|
+
from .report import build_report, size_rows
|
|
23
|
+
from .types import ReportResult
|
|
24
|
+
from .slicing import plan_slices, slice_image
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _width(text: str) -> int:
|
|
28
|
+
"""Terminal display width: East Asian wide/fullwidth characters (Hangul, CJK) take two columns."""
|
|
29
|
+
return sum(2 if unicodedata.east_asian_width(c) in ("W", "F") else 1 for c in text)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _pad(text: str, width: int) -> str:
|
|
33
|
+
return text + " " * (width - _width(text))
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _table(headers: list[str], rows: list[list[str]]) -> str:
|
|
37
|
+
widths = [max(_width(h), *(_width(r[i]) for r in rows)) if rows else _width(h) for i, h in enumerate(headers)]
|
|
38
|
+
line = lambda cells: " ".join(_pad(c, w) for c, w in zip(cells, widths)).rstrip() # noqa: E731
|
|
39
|
+
return "\n".join([line(headers), line(["-" * w for w in widths]), *(line(r) for r in rows)])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class _Parser(argparse.ArgumentParser):
|
|
43
|
+
"""ArgumentParser whose usage prefix, section titles and usage errors follow ``--lang``."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, *args, lang: str = "en", **kwargs):
|
|
46
|
+
self._lang = lang
|
|
47
|
+
super().__init__(*args, add_help=False, **kwargs)
|
|
48
|
+
self._positionals.title = t(lang, "argparse.positional")
|
|
49
|
+
self._optionals.title = t(lang, "argparse.options")
|
|
50
|
+
self.add_argument("-h", "--help", action="help", help=t(lang, "help.help"))
|
|
51
|
+
|
|
52
|
+
def _get_formatter(self, *args, **kwargs):
|
|
53
|
+
formatter = super()._get_formatter(*args, **kwargs)
|
|
54
|
+
original = formatter.add_usage
|
|
55
|
+
prefix = t(self._lang, "argparse.usage")
|
|
56
|
+
|
|
57
|
+
def add_usage(usage, actions, groups, prefix_arg=None): # noqa: ARG001
|
|
58
|
+
return original(usage, actions, groups, prefix)
|
|
59
|
+
|
|
60
|
+
formatter.add_usage = add_usage
|
|
61
|
+
return formatter
|
|
62
|
+
|
|
63
|
+
def error(self, message: str):
|
|
64
|
+
self.print_usage(sys.stderr)
|
|
65
|
+
self.exit(2, f"{self.prog}: {t(self._lang, 'argparse.error')}: {_translate_argparse_error(self._lang, message)}\n")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_ERROR_PATTERNS = (
|
|
69
|
+
(re.compile(r"the following arguments are required: (.*)$"), "argparse.err.required"),
|
|
70
|
+
(re.compile(r"argument (.+?): invalid choice: (.+?) \(choose from (.*)\)$"), "argparse.err.choice"),
|
|
71
|
+
(re.compile(r"argument (.+?): expected one argument$"), "argparse.err.one_arg"),
|
|
72
|
+
(re.compile(r"argument (.+?): invalid (\w+) value: (.*)$"), "argparse.err.value"),
|
|
73
|
+
(re.compile(r"unrecognized arguments: (.*)$"), "argparse.err.unrecognized"),
|
|
74
|
+
(re.compile(r"ambiguous option: (.+?) could match (.*)$"), "argparse.err.ambiguous"),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _translate_argparse_error(lang: str, message: str) -> str:
|
|
79
|
+
if lang == "en":
|
|
80
|
+
return message
|
|
81
|
+
for pattern, key in _ERROR_PATTERNS:
|
|
82
|
+
m = pattern.search(message)
|
|
83
|
+
if m:
|
|
84
|
+
return t(lang, key, **{f"a{i}": g for i, g in enumerate(m.groups(), 1)})
|
|
85
|
+
return message
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _yn(lang: str, value: bool) -> str:
|
|
89
|
+
return t(lang, "yes" if value else "no")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _size(s: tuple[int, int]) -> str:
|
|
93
|
+
return f"{s[0]}x{s[1]}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _print_warnings(lang: str, codes: Sequence[str], out) -> None:
|
|
97
|
+
uniq = list(dict.fromkeys(codes))
|
|
98
|
+
if uniq:
|
|
99
|
+
print(t(lang, "warnings.title"), file=out)
|
|
100
|
+
for code in uniq:
|
|
101
|
+
print(f" - [{code}] {t(lang, 'warn.' + code)}", file=out)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _build_parser(lang: str) -> argparse.ArgumentParser:
|
|
105
|
+
p = _Parser(prog="pixelbudget", description=t(lang, "app.description"), lang=lang)
|
|
106
|
+
p.add_argument("--lang", choices=LANGS, help=t(lang, "help.lang"))
|
|
107
|
+
p.add_argument("--models-file", metavar="PATH", help=t(lang, "help.models_file"))
|
|
108
|
+
p.add_argument("--version", action="version", version=f"pixelbudget {__version__}", help=t(lang, "help.version"))
|
|
109
|
+
sub = p.add_subparsers(dest="command", required=True, metavar="{models,estimate,fit,slice,report,demo}",
|
|
110
|
+
parser_class=partial(_Parser, lang=lang))
|
|
111
|
+
|
|
112
|
+
def add(name: str) -> argparse.ArgumentParser:
|
|
113
|
+
return sub.add_parser(name, help=t(lang, f"help.{name}"), description=t(lang, f"help.{name}"))
|
|
114
|
+
|
|
115
|
+
m = add("models")
|
|
116
|
+
m.add_argument("--format", choices=("table", "json"), default="table", help=t(lang, "help.format"))
|
|
117
|
+
|
|
118
|
+
def multi(sp: argparse.ArgumentParser) -> None:
|
|
119
|
+
sp.add_argument("paths", nargs="+", metavar="PATH", help=t(lang, "help.paths_size" if sp.prog.endswith("estimate") else "help.paths"))
|
|
120
|
+
sp.add_argument("--model", action="append", help=t(lang, "help.model"))
|
|
121
|
+
sp.add_argument("--detail", help=t(lang, "help.detail"))
|
|
122
|
+
sp.add_argument("--format", choices=("table", "json"), default="table", help=t(lang, "help.format"))
|
|
123
|
+
sp.add_argument("--strict", action="store_true", help=t(lang, "help.strict"))
|
|
124
|
+
|
|
125
|
+
e = add("estimate")
|
|
126
|
+
multi(e)
|
|
127
|
+
r = add("report")
|
|
128
|
+
multi(r)
|
|
129
|
+
r.add_argument("--max-tokens", type=int, help=t(lang, "help.max_tokens"))
|
|
130
|
+
r.add_argument("--price", action="append", metavar="MODEL=USD", help=t(lang, "help.price"))
|
|
131
|
+
|
|
132
|
+
f = add("fit")
|
|
133
|
+
f.add_argument("path", metavar="PATH", help=t(lang, "help.path"))
|
|
134
|
+
f.add_argument("--model", required=True, help=t(lang, "help.model_one"))
|
|
135
|
+
f.add_argument("--budget", type=int, help=t(lang, "help.budget"))
|
|
136
|
+
f.add_argument("--detail", help=t(lang, "help.detail"))
|
|
137
|
+
f.add_argument("--out", help=t(lang, "help.out"))
|
|
138
|
+
f.add_argument("--max-pixels", type=int, help=t(lang, "help.max_pixels"))
|
|
139
|
+
|
|
140
|
+
s = add("slice")
|
|
141
|
+
s.add_argument("path", metavar="PATH", help=t(lang, "help.path"))
|
|
142
|
+
s.add_argument("--model", required=True, help=t(lang, "help.model_one"))
|
|
143
|
+
s.add_argument("--overlap", type=int, default=64, help=t(lang, "help.overlap"))
|
|
144
|
+
s.add_argument("--detail", help=t(lang, "help.detail"))
|
|
145
|
+
s.add_argument("--out-dir", help=t(lang, "help.out_dir"))
|
|
146
|
+
s.add_argument("--dry-run", action="store_true", help=t(lang, "help.dry_run"))
|
|
147
|
+
s.add_argument("--max-pixels", type=int, help=t(lang, "help.max_pixels"))
|
|
148
|
+
|
|
149
|
+
d = add("demo")
|
|
150
|
+
d.add_argument("--out", default="pixelbudget-demo", help=t(lang, "help.demo_out"))
|
|
151
|
+
d.add_argument("--model", default="claude-standard", help=t(lang, "help.model_one"))
|
|
152
|
+
return p
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _parse_prices(items: list[str] | None) -> dict[str, float]:
|
|
156
|
+
prices: dict[str, float] = {}
|
|
157
|
+
for item in items or []:
|
|
158
|
+
name, sep, value = item.partition("=")
|
|
159
|
+
if not sep:
|
|
160
|
+
raise PixelBudgetError(key="price_format", item=item)
|
|
161
|
+
try:
|
|
162
|
+
prices[name] = float(value)
|
|
163
|
+
except ValueError:
|
|
164
|
+
raise PixelBudgetError(key="price_number", item=item) from None
|
|
165
|
+
return prices
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _check_save_path(path: Path) -> None:
|
|
169
|
+
"""Fail before decoding when Pillow cannot pick a format from the output extension."""
|
|
170
|
+
ext = path.suffix.lower()
|
|
171
|
+
if ext not in Image.registered_extensions():
|
|
172
|
+
raise PixelBudgetError(key="unknown_extension", path=path.as_posix(), ext=path.suffix)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# OSError 종류별 메시지 키. 영어 템플릿은 파이썬 기본 형식("[Errno 2] ...")과 같아 영어 출력은 그대로다.
|
|
176
|
+
_OS_ERROR_KEYS = (
|
|
177
|
+
(FileNotFoundError, "os.not_found"),
|
|
178
|
+
(IsADirectoryError, "os.is_directory"),
|
|
179
|
+
(NotADirectoryError, "os.not_directory"),
|
|
180
|
+
(FileExistsError, "os.exists"),
|
|
181
|
+
(PermissionError, "os.permission"),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _error_text(lang: str, exc: Exception) -> str:
|
|
186
|
+
"""User-facing text of an error in ``lang`` (library errors and file system errors)."""
|
|
187
|
+
if isinstance(exc, PixelBudgetError):
|
|
188
|
+
return exc.render(lang) # 코드+인자로 언어별 템플릿을 채운다
|
|
189
|
+
if isinstance(exc, OSError) and exc.errno is not None and exc.filename is not None:
|
|
190
|
+
key = next((k for cls, k in _OS_ERROR_KEYS if isinstance(exc, cls)), "os.other")
|
|
191
|
+
return t(lang, key, errno=exc.errno, reason=exc.strerror, path=exc.filename)
|
|
192
|
+
return str(exc) # 형식을 알 수 없는 오류는 원문 그대로(영어일 수 있음)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _cmd_models(args, lang, out) -> int:
|
|
196
|
+
specs = list_models()
|
|
197
|
+
if args.format == "json":
|
|
198
|
+
json.dump([s.as_dict() for s in specs], out, indent=2, ensure_ascii=False)
|
|
199
|
+
print(file=out)
|
|
200
|
+
return 0
|
|
201
|
+
headers = [t(lang, k) for k in ("col.model", "col.provider", "col.method", "col.confidence", "col.checked", "col.source", "col.origin")]
|
|
202
|
+
rows = [[s.name, s.provider, s.method, s.confidence, s.checked, s.source_url, s.origin] for s in specs]
|
|
203
|
+
print(_table(headers, rows), file=out)
|
|
204
|
+
return 0
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
# estimate 전용: 같은 이름의 파일·폴더가 없고 WxH 모양이면 이미지 없이 크기로 계산한다(파일 우선).
|
|
208
|
+
_SIZE_RE = re.compile(r"^\d+[xX]\d+$")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _is_size_arg(arg: str) -> bool:
|
|
212
|
+
return bool(_SIZE_RE.match(arg)) and not Path(arg).exists()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _estimate_mixed(args, models: list[str]) -> ReportResult:
|
|
216
|
+
"""Handle estimate inputs one by one, in order: real paths via build_report, WxH via size_rows."""
|
|
217
|
+
rows: list = []
|
|
218
|
+
skipped: list[str] = []
|
|
219
|
+
for arg in args.paths:
|
|
220
|
+
if _is_size_arg(arg):
|
|
221
|
+
w, h = (int(n) for n in arg.lower().split("x"))
|
|
222
|
+
rows.extend(size_rows(arg, (w, h), models, detail=args.detail))
|
|
223
|
+
else:
|
|
224
|
+
part = build_report([arg], models, detail=args.detail)
|
|
225
|
+
rows.extend(part.rows)
|
|
226
|
+
skipped.extend(part.skipped)
|
|
227
|
+
return ReportResult(rows=tuple(rows), skipped=tuple(skipped))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _cmd_report(args, lang, out, with_budget: bool) -> int:
|
|
231
|
+
models = args.model or ["claude-standard"]
|
|
232
|
+
max_tokens = getattr(args, "max_tokens", None) if with_budget else None
|
|
233
|
+
prices = _parse_prices(getattr(args, "price", None)) if with_budget else {}
|
|
234
|
+
if with_budget or not any(_is_size_arg(a) for a in args.paths):
|
|
235
|
+
result = build_report(args.paths, models, detail=args.detail, max_tokens=max_tokens, prices=prices)
|
|
236
|
+
else:
|
|
237
|
+
result = _estimate_mixed(args, models)
|
|
238
|
+
over = sum(1 for r in result.rows if r.over_budget)
|
|
239
|
+
failed = bool(over or (args.strict and result.skipped)) # --strict: unreadable files fail the run too
|
|
240
|
+
if args.format == "json":
|
|
241
|
+
payload = {"rows": [r.as_dict() for r in result.rows], "skipped": list(result.skipped),
|
|
242
|
+
"summary": {"rows": len(result.rows), "over_budget": over, "skipped": len(result.skipped)}}
|
|
243
|
+
json.dump(payload, out, indent=2, ensure_ascii=False)
|
|
244
|
+
print(file=out)
|
|
245
|
+
return 1 if failed else 0
|
|
246
|
+
headers = [t(lang, k) for k in ("col.path", "col.model", "col.original", "col.sent", "col.tokens", "col.downscaled", "col.wasted")]
|
|
247
|
+
if with_budget and max_tokens is not None:
|
|
248
|
+
headers.append(t(lang, "col.over"))
|
|
249
|
+
if prices:
|
|
250
|
+
headers.append(t(lang, "col.cost"))
|
|
251
|
+
rows = []
|
|
252
|
+
for r in result.rows:
|
|
253
|
+
row = [r.path, r.model, _size(r.original_size), _size(r.sent_size), str(r.tokens),
|
|
254
|
+
_yn(lang, r.server_downscaled), str(r.wasted_pixels)]
|
|
255
|
+
if with_budget and max_tokens is not None:
|
|
256
|
+
row.append(_yn(lang, r.over_budget))
|
|
257
|
+
if prices:
|
|
258
|
+
row.append("-" if r.cost_usd is None else f"{r.cost_usd:.6f}")
|
|
259
|
+
rows.append(row)
|
|
260
|
+
if not rows:
|
|
261
|
+
print(t(lang, "report.skipped_only" if result.skipped else "report.no_images"), file=out)
|
|
262
|
+
else:
|
|
263
|
+
print(_table(headers, rows), file=out)
|
|
264
|
+
_print_warnings(lang, [c for r in result.rows for c in r.warnings], out)
|
|
265
|
+
if result.skipped:
|
|
266
|
+
print(t(lang, "report.skipped", paths=", ".join(result.skipped)), file=out)
|
|
267
|
+
print(t(lang, "report.summary_one" if len(result.rows) == 1 else "report.summary", rows=len(result.rows), over=over, skipped=len(result.skipped)), file=out)
|
|
268
|
+
return 1 if failed else 0
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _save(img: Image.Image, path: Path) -> None:
|
|
272
|
+
if path.suffix.lower() in (".jpg", ".jpeg") and img.mode not in ("RGB", "L"):
|
|
273
|
+
img = img.convert("RGB")
|
|
274
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
img.save(path)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _cmd_fit(args, lang, out) -> int:
|
|
279
|
+
size = measure(args.path)
|
|
280
|
+
plan = fit_size(size, args.model, budget=args.budget, detail=args.detail)
|
|
281
|
+
spec = get_model(args.model)
|
|
282
|
+
if plan.reason == "fixed_per_image":
|
|
283
|
+
print(t(lang, "fit.fixed_per_image", model=spec.name, tokens=plan.tokens), file=out)
|
|
284
|
+
if plan.suggest:
|
|
285
|
+
print(t(lang, "fit.suggest", budget=args.budget, detail=plan.suggest[0], tokens=plan.suggest[1]), file=out)
|
|
286
|
+
elif args.budget is not None and plan.tokens > args.budget:
|
|
287
|
+
print(t(lang, "fit.suggest_none", budget=args.budget), file=out)
|
|
288
|
+
return 0
|
|
289
|
+
if not plan.changed:
|
|
290
|
+
print(t(lang, "fit.unchanged", w=size[0], h=size[1], tokens=plan.tokens), file=out)
|
|
291
|
+
return 0
|
|
292
|
+
src = Path(args.path)
|
|
293
|
+
dest = Path(args.out) if args.out else src.with_name(f"{src.stem}.fit{src.suffix}")
|
|
294
|
+
_check_save_path(dest) # 디코드 전에 저장 형식부터 확인(트레이스백 대신 알기 쉬운 오류)
|
|
295
|
+
result = fit(args.path, args.model, budget=args.budget, detail=args.detail, max_pixels=args.max_pixels)
|
|
296
|
+
_save(result.image, dest)
|
|
297
|
+
print(t(lang, "fit.written", path=dest.as_posix(), w=plan.size[0], h=plan.size[1],
|
|
298
|
+
tokens=plan.tokens, scale=plan.scale), file=out)
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _cmd_slice(args, lang, out) -> int:
|
|
303
|
+
plan = plan_slices(measure(args.path), args.model, overlap=args.overlap, detail=args.detail)
|
|
304
|
+
print(t(lang, "slice.summary_one" if len(plan.pieces) == 1 else "slice.summary", n=len(plan.pieces), length=plan.piece_length, axis=t(lang, "axis." + plan.axis),
|
|
305
|
+
overlap=plan.overlap, total=plan.total_tokens, whole=plan.whole_tokens), file=out)
|
|
306
|
+
headers = [t(lang, k) for k in ("col.piece", "col.box", "col.size", "col.tokens", "col.scale", "col.downscaled")]
|
|
307
|
+
rows = [[str(p.index + 1), ",".join(map(str, p.box)), _size(p.size), str(p.tokens), f"{p.scale:.3f}",
|
|
308
|
+
_yn(lang, p.server_downscaled)] for p in plan.pieces]
|
|
309
|
+
print(_table(headers, rows), file=out)
|
|
310
|
+
_print_warnings(lang, plan.warnings, out)
|
|
311
|
+
if len(plan.pieces) == 1:
|
|
312
|
+
print(t(lang, "slice.single"), file=out)
|
|
313
|
+
return 0
|
|
314
|
+
if args.dry_run:
|
|
315
|
+
return 0
|
|
316
|
+
src = Path(args.path)
|
|
317
|
+
folder = Path(args.out_dir) if args.out_dir else src.with_name(f"{src.stem}-slices")
|
|
318
|
+
_check_save_path(folder / f"{src.stem}-01{src.suffix}")
|
|
319
|
+
result = slice_image(args.path, args.model, overlap=args.overlap, detail=args.detail, max_pixels=args.max_pixels)
|
|
320
|
+
for i, img in enumerate(result.images, 1):
|
|
321
|
+
_save(img, folder / f"{src.stem}-{i:02d}{src.suffix}")
|
|
322
|
+
print(t(lang, "slice.written", n=len(result.images), folder=folder.as_posix()), file=out)
|
|
323
|
+
return 0
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _cmd_demo(args, lang, out) -> int:
|
|
327
|
+
from .demo import run_demo
|
|
328
|
+
|
|
329
|
+
size, fitted, sliced = run_demo(args.out, args.model)
|
|
330
|
+
p0 = sliced.plan.pieces[0]
|
|
331
|
+
print(t(lang, "demo.title"), file=out)
|
|
332
|
+
print(t(lang, "demo.model", model=get_model(args.model).name), file=out)
|
|
333
|
+
print(t(lang, "demo.image", w=size[0], h=size[1]), file=out)
|
|
334
|
+
print(t(lang, "demo.fit", w=fitted.plan.size[0], h=fitted.plan.size[1], tokens=fitted.plan.tokens,
|
|
335
|
+
scale=fitted.plan.scale), file=out)
|
|
336
|
+
print(t(lang, "demo.slice", n=len(sliced.plan.pieces), pw=p0.size[0], ph=p0.size[1],
|
|
337
|
+
total=sliced.plan.total_tokens), file=out)
|
|
338
|
+
print(t(lang, "demo.tradeoff"), file=out)
|
|
339
|
+
print(t(lang, "demo.files", folder=Path(args.out).as_posix()), file=out)
|
|
340
|
+
_print_warnings(lang, sliced.plan.warnings, out)
|
|
341
|
+
return 0
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
345
|
+
"""Entry point for ``pixelbudget`` and ``python -m pixelbudget``. Returns the exit code."""
|
|
346
|
+
argv = list(sys.argv[1:] if argv is None else argv)
|
|
347
|
+
pre = argparse.ArgumentParser(add_help=False)
|
|
348
|
+
pre.add_argument("--lang")
|
|
349
|
+
known, _ = pre.parse_known_args(argv)
|
|
350
|
+
lang = resolve_lang(known.lang)
|
|
351
|
+
parser = _build_parser(lang)
|
|
352
|
+
args = parser.parse_args(argv)
|
|
353
|
+
out = sys.stdout
|
|
354
|
+
set_models_file(args.models_file) # --models-file 은 환경변수보다 우선한다
|
|
355
|
+
try:
|
|
356
|
+
if args.command == "models":
|
|
357
|
+
return _cmd_models(args, lang, out)
|
|
358
|
+
if args.command == "estimate":
|
|
359
|
+
return _cmd_report(args, lang, out, with_budget=False)
|
|
360
|
+
if args.command == "report":
|
|
361
|
+
return _cmd_report(args, lang, out, with_budget=True)
|
|
362
|
+
if args.command == "fit":
|
|
363
|
+
return _cmd_fit(args, lang, out)
|
|
364
|
+
if args.command == "slice":
|
|
365
|
+
return _cmd_slice(args, lang, out)
|
|
366
|
+
return _cmd_demo(args, lang, out)
|
|
367
|
+
except (PixelBudgetError, OSError) as exc:
|
|
368
|
+
# 라이브러리 예외는 영어 문구를 유지하고, 화면에는 고른 언어로 다시 렌더링해 보여 준다.
|
|
369
|
+
print(f"{t(lang, 'error.prefix')}: {_error_text(lang, exc)}", file=sys.stderr)
|
|
370
|
+
return 2
|
|
371
|
+
finally:
|
|
372
|
+
set_models_file(None)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
if __name__ == "__main__": # pragma: no cover
|
|
376
|
+
sys.exit(main())
|