everypixel-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.
- everypixel_cli/__init__.py +1 -0
- everypixel_cli/__main__.py +5 -0
- everypixel_cli/application/__init__.py +10 -0
- everypixel_cli/application/models.py +59 -0
- everypixel_cli/application/serialization.py +20 -0
- everypixel_cli/application/services.py +1376 -0
- everypixel_cli/cli.py +1520 -0
- everypixel_cli/client.py +159 -0
- everypixel_cli/config.py +221 -0
- everypixel_cli/errors.py +262 -0
- everypixel_cli/files.py +298 -0
- everypixel_cli/mcp_server.py +828 -0
- everypixel_cli/openapi.py +338 -0
- everypixel_cli/output.py +159 -0
- everypixel_cli/resources/__init__.py +1 -0
- everypixel_cli/resources/openapi.json +5402 -0
- everypixel_cli/schemas.py +846 -0
- everypixel_cli-0.1.0.dist-info/METADATA +454 -0
- everypixel_cli-0.1.0.dist-info/RECORD +22 -0
- everypixel_cli-0.1.0.dist-info/WHEEL +4 -0
- everypixel_cli-0.1.0.dist-info/entry_points.txt +3 -0
- everypixel_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
everypixel_cli/cli.py
ADDED
|
@@ -0,0 +1,1520 @@
|
|
|
1
|
+
"""Everypixel CLI Typer application.
|
|
2
|
+
|
|
3
|
+
This module defines the command tree and runtime option resolution.
|
|
4
|
+
HTTP logic lives in client.py; Pydantic payload validation lives in schemas.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import traceback
|
|
11
|
+
import webbrowser
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Annotated, Any, NoReturn, Optional
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from click import BadParameter, ClickException
|
|
20
|
+
from typer.core import TyperGroup
|
|
21
|
+
|
|
22
|
+
try: # Typer 0.26+ vendors Click while older supported releases import it.
|
|
23
|
+
from typer._click.exceptions import ClickException as TyperClickException
|
|
24
|
+
except ImportError: # pragma: no cover - exercised with older Typer versions.
|
|
25
|
+
TyperClickException = ClickException # type: ignore[misc, assignment]
|
|
26
|
+
|
|
27
|
+
from .application import ApplicationServices, ExecutionOptions
|
|
28
|
+
from .application.serialization import serialize_operation_result
|
|
29
|
+
from .application.services import parse_generic_payload
|
|
30
|
+
from .client import APIClient
|
|
31
|
+
from .config import (
|
|
32
|
+
DEFAULT_BASE_URL,
|
|
33
|
+
ProfileConfig,
|
|
34
|
+
delete_credentials,
|
|
35
|
+
load_config,
|
|
36
|
+
resolved_settings,
|
|
37
|
+
safe_config_payload,
|
|
38
|
+
save_config,
|
|
39
|
+
save_credentials,
|
|
40
|
+
set_config_value,
|
|
41
|
+
use_profile,
|
|
42
|
+
)
|
|
43
|
+
from .errors import (
|
|
44
|
+
CLIError,
|
|
45
|
+
InternalCLIError,
|
|
46
|
+
ValidationCLIError,
|
|
47
|
+
mask_secret,
|
|
48
|
+
normalize_exception,
|
|
49
|
+
)
|
|
50
|
+
from .openapi import load_schema
|
|
51
|
+
from .output import emit_error, emit_human, emit_json
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def json_output_requested(args: Sequence[str]) -> bool:
|
|
55
|
+
"""Detect JSON mode before Click has parsed the full command line."""
|
|
56
|
+
|
|
57
|
+
for index, argument in enumerate(args):
|
|
58
|
+
if argument == "--":
|
|
59
|
+
break
|
|
60
|
+
if argument in {"--output-json", "-j", "--jq"}:
|
|
61
|
+
return True
|
|
62
|
+
if argument.startswith("--jq="):
|
|
63
|
+
return True
|
|
64
|
+
if (
|
|
65
|
+
argument == "--output"
|
|
66
|
+
and index + 1 < len(args)
|
|
67
|
+
and args[index + 1] == "json"
|
|
68
|
+
):
|
|
69
|
+
return True
|
|
70
|
+
if argument == "--output=json":
|
|
71
|
+
return True
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class JSONErrorBoundaryGroup(TyperGroup):
|
|
76
|
+
"""Render Click parsing failures through the shared JSON error contract."""
|
|
77
|
+
|
|
78
|
+
def main(
|
|
79
|
+
self,
|
|
80
|
+
args: Sequence[str] | None = None,
|
|
81
|
+
prog_name: str | None = None,
|
|
82
|
+
complete_var: str | None = None,
|
|
83
|
+
standalone_mode: bool = True,
|
|
84
|
+
windows_expand_args: bool = True,
|
|
85
|
+
**extra: Any,
|
|
86
|
+
) -> Any:
|
|
87
|
+
raw_args = list(args) if args is not None else sys.argv[1:]
|
|
88
|
+
if not json_output_requested(raw_args):
|
|
89
|
+
return super().main(
|
|
90
|
+
args=args,
|
|
91
|
+
prog_name=prog_name,
|
|
92
|
+
complete_var=complete_var,
|
|
93
|
+
standalone_mode=standalone_mode,
|
|
94
|
+
windows_expand_args=windows_expand_args,
|
|
95
|
+
**extra,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
result = super().main(
|
|
100
|
+
args=args,
|
|
101
|
+
prog_name=prog_name,
|
|
102
|
+
complete_var=complete_var,
|
|
103
|
+
standalone_mode=False,
|
|
104
|
+
windows_expand_args=windows_expand_args,
|
|
105
|
+
**extra,
|
|
106
|
+
)
|
|
107
|
+
except (ClickException, TyperClickException) as exc:
|
|
108
|
+
emit_error(
|
|
109
|
+
ValidationCLIError(exc.format_message()),
|
|
110
|
+
output_json=True,
|
|
111
|
+
)
|
|
112
|
+
if not standalone_mode:
|
|
113
|
+
raise
|
|
114
|
+
raise SystemExit(exc.exit_code) from exc
|
|
115
|
+
|
|
116
|
+
if standalone_mode:
|
|
117
|
+
raise SystemExit(result if isinstance(result, int) else 0)
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
app = typer.Typer(
|
|
122
|
+
cls=JSONErrorBoundaryGroup,
|
|
123
|
+
no_args_is_help=True,
|
|
124
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
125
|
+
)
|
|
126
|
+
auth_app = typer.Typer(no_args_is_help=True)
|
|
127
|
+
config_app = typer.Typer(no_args_is_help=True)
|
|
128
|
+
profile_app = typer.Typer(no_args_is_help=True)
|
|
129
|
+
image_app = typer.Typer(no_args_is_help=True)
|
|
130
|
+
video_app = typer.Typer(no_args_is_help=True)
|
|
131
|
+
lipsync_app = typer.Typer(no_args_is_help=True)
|
|
132
|
+
audio_app = typer.Typer(no_args_is_help=True)
|
|
133
|
+
docs_app = typer.Typer(no_args_is_help=True)
|
|
134
|
+
schema_app = typer.Typer(no_args_is_help=True)
|
|
135
|
+
mcp_app = typer.Typer(no_args_is_help=True)
|
|
136
|
+
|
|
137
|
+
app.add_typer(auth_app, name="auth")
|
|
138
|
+
app.add_typer(config_app, name="config")
|
|
139
|
+
config_app.add_typer(profile_app, name="profile")
|
|
140
|
+
app.add_typer(image_app, name="image")
|
|
141
|
+
app.add_typer(video_app, name="video")
|
|
142
|
+
app.add_typer(lipsync_app, name="lipsync")
|
|
143
|
+
app.add_typer(audio_app, name="audio")
|
|
144
|
+
app.add_typer(docs_app, name="docs")
|
|
145
|
+
app.add_typer(schema_app, name="schema")
|
|
146
|
+
app.add_typer(mcp_app, name="mcp")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class OutputMode(str, Enum):
|
|
150
|
+
human = "human"
|
|
151
|
+
json = "json"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def positive_float(value: float) -> float:
|
|
155
|
+
"""Reject zero and negative wait controls at the CLI boundary."""
|
|
156
|
+
|
|
157
|
+
if value <= 0:
|
|
158
|
+
raise typer.BadParameter("must be greater than 0")
|
|
159
|
+
return value
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
WaitOption = Annotated[
|
|
163
|
+
Optional[bool], typer.Option("--wait/--no-wait", help="Override wait mode.")
|
|
164
|
+
]
|
|
165
|
+
DownloadOption = Annotated[
|
|
166
|
+
Optional[Path],
|
|
167
|
+
typer.Option("--download", "-d", help="Download result URLs into a directory."),
|
|
168
|
+
]
|
|
169
|
+
OutputJsonOption = Annotated[
|
|
170
|
+
bool, typer.Option("--output-json", "-j", help="Alias for --output json.")
|
|
171
|
+
]
|
|
172
|
+
JqOption = Annotated[
|
|
173
|
+
Optional[str], typer.Option("--jq", help="Apply jq expression to JSON output.")
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class Runtime:
|
|
179
|
+
"""Resolved settings for one CLI invocation."""
|
|
180
|
+
|
|
181
|
+
base_url: str
|
|
182
|
+
profile: str
|
|
183
|
+
client_id: str | None
|
|
184
|
+
client_secret: str | None
|
|
185
|
+
output_json: bool
|
|
186
|
+
jq: str | None
|
|
187
|
+
wait: bool
|
|
188
|
+
timeout: float
|
|
189
|
+
poll_interval: float
|
|
190
|
+
download: Path | None
|
|
191
|
+
no_color: bool
|
|
192
|
+
debug: bool
|
|
193
|
+
_services: ApplicationServices | None = field(
|
|
194
|
+
default=None,
|
|
195
|
+
init=False,
|
|
196
|
+
repr=False,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
def client(self) -> APIClient:
|
|
200
|
+
"""Create an API client with the current credentials."""
|
|
201
|
+
|
|
202
|
+
return APIClient(
|
|
203
|
+
base_url=self.base_url,
|
|
204
|
+
client_id=self.client_id,
|
|
205
|
+
client_secret=self.client_secret,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def services(self) -> ApplicationServices:
|
|
209
|
+
"""Create the CLI-independent operation services for this invocation."""
|
|
210
|
+
|
|
211
|
+
if self._services is None:
|
|
212
|
+
self._services = ApplicationServices.with_client(self.client())
|
|
213
|
+
return self._services
|
|
214
|
+
|
|
215
|
+
def close(self) -> None:
|
|
216
|
+
"""Close transport resources created for this invocation."""
|
|
217
|
+
|
|
218
|
+
if self._services is not None:
|
|
219
|
+
self._services.close()
|
|
220
|
+
|
|
221
|
+
def execution_options(self, *, wait: bool | None = None) -> ExecutionOptions:
|
|
222
|
+
"""Build application execution options from the resolved CLI runtime."""
|
|
223
|
+
|
|
224
|
+
return ExecutionOptions(
|
|
225
|
+
wait=self.wait if wait is None else wait,
|
|
226
|
+
download_directory=self.download,
|
|
227
|
+
timeout=self.timeout,
|
|
228
|
+
poll_interval=self.poll_interval,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_runtime(ctx: typer.Context) -> Runtime:
|
|
233
|
+
"""Read Runtime from Typer context or fail with a CLI error."""
|
|
234
|
+
|
|
235
|
+
if not isinstance(ctx.obj, Runtime):
|
|
236
|
+
raise CLIError("Runtime context is not initialized")
|
|
237
|
+
return ctx.obj
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@app.callback()
|
|
241
|
+
def main(
|
|
242
|
+
ctx: typer.Context,
|
|
243
|
+
base_url: Annotated[
|
|
244
|
+
Optional[str],
|
|
245
|
+
typer.Option("--base-url", help="Override API base URL for this run."),
|
|
246
|
+
] = None,
|
|
247
|
+
profile: Annotated[
|
|
248
|
+
Optional[str], typer.Option("--profile", help="Config profile name.")
|
|
249
|
+
] = None,
|
|
250
|
+
dev: Annotated[bool, typer.Option("--dev", help="Use the dev profile.")] = False,
|
|
251
|
+
output: Annotated[
|
|
252
|
+
OutputMode, typer.Option("--output", help="Output mode: human or json.")
|
|
253
|
+
] = OutputMode.human,
|
|
254
|
+
output_json: Annotated[
|
|
255
|
+
bool, typer.Option("--output-json", "-j", help="Alias for --output json.")
|
|
256
|
+
] = False,
|
|
257
|
+
jq: Annotated[
|
|
258
|
+
Optional[str], typer.Option("--jq", help="Apply jq expression to JSON output.")
|
|
259
|
+
] = None,
|
|
260
|
+
wait: Annotated[
|
|
261
|
+
bool, typer.Option("--wait/--no-wait", help="Wait for async task completion.")
|
|
262
|
+
] = True,
|
|
263
|
+
timeout: Annotated[
|
|
264
|
+
float,
|
|
265
|
+
typer.Option(
|
|
266
|
+
"--timeout",
|
|
267
|
+
callback=positive_float,
|
|
268
|
+
help="Task wait timeout in seconds.",
|
|
269
|
+
),
|
|
270
|
+
] = 300.0,
|
|
271
|
+
poll_interval: Annotated[
|
|
272
|
+
float,
|
|
273
|
+
typer.Option(
|
|
274
|
+
"--poll-interval",
|
|
275
|
+
callback=positive_float,
|
|
276
|
+
help="Task polling interval in seconds.",
|
|
277
|
+
),
|
|
278
|
+
] = 2.0,
|
|
279
|
+
download: Annotated[
|
|
280
|
+
Optional[Path],
|
|
281
|
+
typer.Option("--download", "-d", help="Download result URLs into a directory."),
|
|
282
|
+
] = None,
|
|
283
|
+
no_color: Annotated[
|
|
284
|
+
bool, typer.Option("--no-color", help="Disable colored human output.")
|
|
285
|
+
] = False,
|
|
286
|
+
debug: Annotated[
|
|
287
|
+
bool,
|
|
288
|
+
typer.Option(
|
|
289
|
+
"--debug",
|
|
290
|
+
help="Include sanitized stack frames for unexpected internal errors.",
|
|
291
|
+
),
|
|
292
|
+
] = False,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Global callback that resolves settings shared by all commands."""
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
settings = resolved_settings(profile=profile, dev=dev, base_url=base_url)
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
error = error_with_debug_context(normalized_error(exc), exc, enabled=debug)
|
|
300
|
+
emit_error(
|
|
301
|
+
error,
|
|
302
|
+
output_json=output_json or output == OutputMode.json or jq is not None,
|
|
303
|
+
no_color=no_color,
|
|
304
|
+
)
|
|
305
|
+
raise typer.Exit(error.exit_code) from exc
|
|
306
|
+
runtime = Runtime(
|
|
307
|
+
base_url=settings["base_url"],
|
|
308
|
+
profile=settings["profile"],
|
|
309
|
+
client_id=settings["client_id"],
|
|
310
|
+
client_secret=settings["client_secret"],
|
|
311
|
+
output_json=output_json or output == OutputMode.json or jq is not None,
|
|
312
|
+
jq=jq,
|
|
313
|
+
wait=wait,
|
|
314
|
+
timeout=timeout,
|
|
315
|
+
poll_interval=poll_interval,
|
|
316
|
+
download=download,
|
|
317
|
+
no_color=no_color,
|
|
318
|
+
debug=debug,
|
|
319
|
+
)
|
|
320
|
+
ctx.obj = runtime
|
|
321
|
+
ctx.call_on_close(runtime.close)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def finish(ctx: typer.Context, payload: Any, *, title: str | None = None) -> None:
|
|
325
|
+
"""Print payload in the selected output format."""
|
|
326
|
+
|
|
327
|
+
runtime = get_runtime(ctx)
|
|
328
|
+
if hasattr(payload, "saved_files") and hasattr(payload, "value"):
|
|
329
|
+
payload = serialize_operation_result(payload)
|
|
330
|
+
if runtime.output_json:
|
|
331
|
+
emit_json(payload, runtime.jq)
|
|
332
|
+
else:
|
|
333
|
+
emit_human(payload, title=title, no_color=runtime.no_color)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def normalized_error(exc: Exception) -> CLIError:
|
|
337
|
+
"""Convert expected technical exceptions to application errors."""
|
|
338
|
+
|
|
339
|
+
if isinstance(exc, BadParameter):
|
|
340
|
+
return ValidationCLIError(str(exc))
|
|
341
|
+
return normalize_exception(exc)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def error_with_debug_context(
|
|
345
|
+
error: CLIError,
|
|
346
|
+
exc: Exception,
|
|
347
|
+
*,
|
|
348
|
+
enabled: bool,
|
|
349
|
+
) -> CLIError:
|
|
350
|
+
"""Attach stack locations without exception messages, locals, or payloads."""
|
|
351
|
+
|
|
352
|
+
if not enabled or not isinstance(error, InternalCLIError):
|
|
353
|
+
return error
|
|
354
|
+
frames = traceback.extract_tb(exc.__traceback__)[-20:]
|
|
355
|
+
return InternalCLIError(
|
|
356
|
+
error.message,
|
|
357
|
+
details={
|
|
358
|
+
"exception_type": type(exc).__name__,
|
|
359
|
+
"stack": [
|
|
360
|
+
{
|
|
361
|
+
"file": sanitized_stack_filename(frame.filename),
|
|
362
|
+
"line": frame.lineno,
|
|
363
|
+
"function": frame.name,
|
|
364
|
+
}
|
|
365
|
+
for frame in frames
|
|
366
|
+
],
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def sanitized_stack_filename(filename: str) -> str:
|
|
372
|
+
"""Keep project-relative locations while hiding host-specific path prefixes."""
|
|
373
|
+
|
|
374
|
+
path = Path(filename)
|
|
375
|
+
try:
|
|
376
|
+
return str(path.resolve().relative_to(Path.cwd().resolve()))
|
|
377
|
+
except (OSError, ValueError):
|
|
378
|
+
return path.name
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def stop_with_error(ctx: typer.Context, exc: Exception) -> NoReturn:
|
|
382
|
+
"""Render one error through the sole CLI output boundary and exit."""
|
|
383
|
+
|
|
384
|
+
runtime = ctx.obj if isinstance(ctx.obj, Runtime) else None
|
|
385
|
+
error = error_with_debug_context(
|
|
386
|
+
normalized_error(exc),
|
|
387
|
+
exc,
|
|
388
|
+
enabled=bool(runtime and runtime.debug),
|
|
389
|
+
)
|
|
390
|
+
emit_error(
|
|
391
|
+
error,
|
|
392
|
+
output_json=bool(runtime and runtime.output_json),
|
|
393
|
+
no_color=bool(runtime and runtime.no_color),
|
|
394
|
+
)
|
|
395
|
+
raise typer.Exit(error.exit_code) from exc
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def run_action(ctx: typer.Context, action: Callable[[], Any]) -> None:
|
|
399
|
+
"""Run a command action and handle all expected application errors."""
|
|
400
|
+
|
|
401
|
+
try:
|
|
402
|
+
finish(ctx, action())
|
|
403
|
+
except typer.Exit:
|
|
404
|
+
raise
|
|
405
|
+
except (
|
|
406
|
+
Exception
|
|
407
|
+
) as exc: # Boundary deliberately turns unexpected errors into clean CLI failures.
|
|
408
|
+
stop_with_error(ctx, exc)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def apply_common_options(
|
|
412
|
+
ctx: typer.Context,
|
|
413
|
+
*,
|
|
414
|
+
wait_option: bool | None = None,
|
|
415
|
+
download: Path | None = None,
|
|
416
|
+
output_json: bool = False,
|
|
417
|
+
jq_expr: str | None = None,
|
|
418
|
+
) -> Runtime:
|
|
419
|
+
"""Apply command-local common flags on top of global runtime settings."""
|
|
420
|
+
|
|
421
|
+
runtime = get_runtime(ctx)
|
|
422
|
+
if output_json or jq_expr is not None:
|
|
423
|
+
runtime.output_json = True
|
|
424
|
+
if jq_expr is not None:
|
|
425
|
+
runtime.jq = jq_expr
|
|
426
|
+
if download is not None:
|
|
427
|
+
runtime.download = download
|
|
428
|
+
if wait_option is not None:
|
|
429
|
+
runtime.wait = wait_option
|
|
430
|
+
return runtime
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
@auth_app.command("configure")
|
|
434
|
+
def auth_configure(
|
|
435
|
+
ctx: typer.Context,
|
|
436
|
+
client_id: Annotated[str, typer.Option("--client-id", prompt=True)],
|
|
437
|
+
client_secret: Annotated[
|
|
438
|
+
str, typer.Option("--client-secret", prompt=True, hide_input=True)
|
|
439
|
+
],
|
|
440
|
+
base_url: Annotated[
|
|
441
|
+
Optional[str],
|
|
442
|
+
typer.Option("--base-url", help="Persist base URL for the current profile."),
|
|
443
|
+
] = None,
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Save Basic Auth credentials for the current profile."""
|
|
446
|
+
|
|
447
|
+
runtime = get_runtime(ctx)
|
|
448
|
+
|
|
449
|
+
def action() -> dict[str, Any]:
|
|
450
|
+
keyring_ok = save_credentials(runtime.profile, client_id, client_secret)
|
|
451
|
+
if base_url:
|
|
452
|
+
set_config_value(runtime.profile, "base_url", base_url)
|
|
453
|
+
return {
|
|
454
|
+
"profile": runtime.profile,
|
|
455
|
+
"client_id": client_id,
|
|
456
|
+
"base_url": base_url or runtime.base_url,
|
|
457
|
+
"storage": "keyring" if keyring_ok else "config_file",
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
run_action(ctx, action)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@auth_app.command("check")
|
|
464
|
+
def auth_check(ctx: typer.Context) -> None:
|
|
465
|
+
"""Check that stored credentials are accepted by the API."""
|
|
466
|
+
|
|
467
|
+
runtime = get_runtime(ctx)
|
|
468
|
+
|
|
469
|
+
def action() -> dict[str, Any]:
|
|
470
|
+
payload = serialize_operation_result(runtime.services().check_auth())
|
|
471
|
+
return {
|
|
472
|
+
**payload,
|
|
473
|
+
"profile": runtime.profile,
|
|
474
|
+
"base_url": runtime.base_url,
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
run_action(ctx, action)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@auth_app.command("whoami")
|
|
481
|
+
def auth_whoami(ctx: typer.Context) -> None:
|
|
482
|
+
"""Show the active profile and masked credentials."""
|
|
483
|
+
|
|
484
|
+
runtime = get_runtime(ctx)
|
|
485
|
+
run_action(
|
|
486
|
+
ctx,
|
|
487
|
+
lambda: {
|
|
488
|
+
"profile": runtime.profile,
|
|
489
|
+
"base_url": runtime.base_url,
|
|
490
|
+
"client_id": runtime.client_id,
|
|
491
|
+
"client_secret": mask_secret(runtime.client_secret),
|
|
492
|
+
},
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
@auth_app.command("logout")
|
|
497
|
+
def auth_logout(ctx: typer.Context) -> None:
|
|
498
|
+
"""Remove credentials from the current profile."""
|
|
499
|
+
|
|
500
|
+
runtime = get_runtime(ctx)
|
|
501
|
+
|
|
502
|
+
def action() -> dict[str, Any]:
|
|
503
|
+
delete_credentials(runtime.profile)
|
|
504
|
+
return {"status": "ok", "profile": runtime.profile}
|
|
505
|
+
|
|
506
|
+
run_action(ctx, action)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
@config_app.command("set")
|
|
510
|
+
def config_set(ctx: typer.Context, key: str, value: str) -> None:
|
|
511
|
+
"""Set a supported option for the current profile."""
|
|
512
|
+
|
|
513
|
+
runtime = get_runtime(ctx)
|
|
514
|
+
|
|
515
|
+
def action() -> dict[str, Any]:
|
|
516
|
+
set_config_value(runtime.profile, key, value)
|
|
517
|
+
return {"status": "ok", "profile": runtime.profile, key: value}
|
|
518
|
+
|
|
519
|
+
run_action(ctx, action)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
@config_app.command("get")
|
|
523
|
+
def config_get(ctx: typer.Context, key: str) -> None:
|
|
524
|
+
"""Show a setting from the current profile."""
|
|
525
|
+
|
|
526
|
+
runtime = get_runtime(ctx)
|
|
527
|
+
|
|
528
|
+
def action() -> dict[str, Any]:
|
|
529
|
+
if key != "base_url":
|
|
530
|
+
raise ValidationCLIError("Only base_url is supported")
|
|
531
|
+
return {"profile": runtime.profile, key: runtime.base_url}
|
|
532
|
+
|
|
533
|
+
run_action(ctx, action)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@config_app.command("list")
|
|
537
|
+
def config_list(ctx: typer.Context) -> None:
|
|
538
|
+
"""Show local configuration without exposing secrets."""
|
|
539
|
+
|
|
540
|
+
run_action(ctx, lambda: safe_config_payload(load_config()))
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
@profile_app.command("create")
|
|
544
|
+
def profile_create(
|
|
545
|
+
ctx: typer.Context,
|
|
546
|
+
name: str,
|
|
547
|
+
base_url: Annotated[str, typer.Option("--base-url")] = DEFAULT_BASE_URL,
|
|
548
|
+
) -> None:
|
|
549
|
+
run_action(ctx, lambda: create_profile_payload(name, base_url))
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def create_profile_payload(name: str, base_url: str) -> dict[str, Any]:
|
|
553
|
+
"""Create or update a profile and return a result payload."""
|
|
554
|
+
|
|
555
|
+
config = load_config()
|
|
556
|
+
config.profiles[name] = config.profiles.get(name) or ProfileConfig()
|
|
557
|
+
config.profiles[name].base_url = base_url
|
|
558
|
+
save_config(config)
|
|
559
|
+
return {"status": "ok", "profile": name, "base_url": base_url}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@profile_app.command("use")
|
|
563
|
+
def profile_use(ctx: typer.Context, name: str) -> None:
|
|
564
|
+
"""Make a profile active for future runs."""
|
|
565
|
+
|
|
566
|
+
def action() -> dict[str, Any]:
|
|
567
|
+
use_profile(name)
|
|
568
|
+
return {"status": "ok", "profile": name}
|
|
569
|
+
|
|
570
|
+
run_action(ctx, action)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@image_app.command("generate")
|
|
574
|
+
def image_generate(
|
|
575
|
+
ctx: typer.Context,
|
|
576
|
+
prompt: str,
|
|
577
|
+
model: Annotated[str, typer.Option("--model")] = "zimage",
|
|
578
|
+
size: Annotated[str, typer.Option("--size")] = "square",
|
|
579
|
+
style: Annotated[Optional[str], typer.Option("--style")] = None,
|
|
580
|
+
image: Annotated[Optional[str], typer.Option("--image")] = None,
|
|
581
|
+
resolution: Annotated[Optional[str], typer.Option("--resolution")] = None,
|
|
582
|
+
seed: Annotated[int, typer.Option("--seed")] = -1,
|
|
583
|
+
callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
|
|
584
|
+
wait_option: WaitOption = None,
|
|
585
|
+
download: DownloadOption = None,
|
|
586
|
+
output_json: OutputJsonOption = False,
|
|
587
|
+
jq_expr: JqOption = None,
|
|
588
|
+
) -> None:
|
|
589
|
+
"""Start image generation."""
|
|
590
|
+
|
|
591
|
+
runtime = apply_common_options(
|
|
592
|
+
ctx,
|
|
593
|
+
wait_option=wait_option,
|
|
594
|
+
download=download,
|
|
595
|
+
output_json=output_json,
|
|
596
|
+
jq_expr=jq_expr,
|
|
597
|
+
)
|
|
598
|
+
run_action(
|
|
599
|
+
ctx,
|
|
600
|
+
lambda: runtime.services().execute_image_generate(
|
|
601
|
+
prompt=prompt,
|
|
602
|
+
model=model,
|
|
603
|
+
image_size=size,
|
|
604
|
+
style=style,
|
|
605
|
+
image=image,
|
|
606
|
+
resolution=resolution,
|
|
607
|
+
seed=seed,
|
|
608
|
+
callback_url=callback_url,
|
|
609
|
+
execution=runtime.execution_options(),
|
|
610
|
+
),
|
|
611
|
+
)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@image_app.command("edit")
|
|
615
|
+
def image_edit(
|
|
616
|
+
ctx: typer.Context,
|
|
617
|
+
prompt: str,
|
|
618
|
+
image: Annotated[
|
|
619
|
+
list[str],
|
|
620
|
+
typer.Option("--image", help="Image URL or local path. Can be repeated."),
|
|
621
|
+
],
|
|
622
|
+
model: Annotated[str, typer.Option("--model")] = "flux2",
|
|
623
|
+
size: Annotated[Optional[str], typer.Option("--size")] = None,
|
|
624
|
+
resolution: Annotated[Optional[str], typer.Option("--resolution")] = None,
|
|
625
|
+
seed: Annotated[int, typer.Option("--seed")] = -1,
|
|
626
|
+
callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
|
|
627
|
+
wait_option: WaitOption = None,
|
|
628
|
+
download: DownloadOption = None,
|
|
629
|
+
output_json: OutputJsonOption = False,
|
|
630
|
+
jq_expr: JqOption = None,
|
|
631
|
+
) -> None:
|
|
632
|
+
"""Start image editing."""
|
|
633
|
+
|
|
634
|
+
runtime = apply_common_options(
|
|
635
|
+
ctx,
|
|
636
|
+
wait_option=wait_option,
|
|
637
|
+
download=download,
|
|
638
|
+
output_json=output_json,
|
|
639
|
+
jq_expr=jq_expr,
|
|
640
|
+
)
|
|
641
|
+
run_action(
|
|
642
|
+
ctx,
|
|
643
|
+
lambda: runtime.services().execute_image_edit(
|
|
644
|
+
prompt=prompt,
|
|
645
|
+
images=image,
|
|
646
|
+
model=model,
|
|
647
|
+
image_size=size,
|
|
648
|
+
resolution=resolution,
|
|
649
|
+
seed=seed,
|
|
650
|
+
callback_url=callback_url,
|
|
651
|
+
execution=runtime.execution_options(),
|
|
652
|
+
),
|
|
653
|
+
)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
@image_app.command("upscale")
|
|
657
|
+
def image_upscale(
|
|
658
|
+
ctx: typer.Context,
|
|
659
|
+
image: Annotated[Optional[str], typer.Option("--image")] = None,
|
|
660
|
+
task_id: Annotated[Optional[str], typer.Option("--task-id")] = None,
|
|
661
|
+
model: Annotated[str, typer.Option("--model")] = "seedvr2",
|
|
662
|
+
callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
|
|
663
|
+
wait_option: WaitOption = None,
|
|
664
|
+
download: DownloadOption = None,
|
|
665
|
+
output_json: OutputJsonOption = False,
|
|
666
|
+
jq_expr: JqOption = None,
|
|
667
|
+
) -> None:
|
|
668
|
+
"""Start image upscale from a URL/file or task_id."""
|
|
669
|
+
|
|
670
|
+
runtime = apply_common_options(
|
|
671
|
+
ctx,
|
|
672
|
+
wait_option=wait_option,
|
|
673
|
+
download=download,
|
|
674
|
+
output_json=output_json,
|
|
675
|
+
jq_expr=jq_expr,
|
|
676
|
+
)
|
|
677
|
+
run_action(
|
|
678
|
+
ctx,
|
|
679
|
+
lambda: runtime.services().execute_image_upscale(
|
|
680
|
+
image=image,
|
|
681
|
+
task_id=task_id,
|
|
682
|
+
model=model,
|
|
683
|
+
callback_url=callback_url,
|
|
684
|
+
execution=runtime.execution_options(),
|
|
685
|
+
),
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
@image_app.command("angles")
|
|
690
|
+
def image_angles(
|
|
691
|
+
ctx: typer.Context,
|
|
692
|
+
image: Annotated[str, typer.Option("--image")],
|
|
693
|
+
azimuth: str = "front",
|
|
694
|
+
elevation: str = "eye_level",
|
|
695
|
+
distance: str = "medium",
|
|
696
|
+
prompt: Optional[str] = None,
|
|
697
|
+
wait_option: WaitOption = None,
|
|
698
|
+
download: DownloadOption = None,
|
|
699
|
+
output_json: OutputJsonOption = False,
|
|
700
|
+
jq_expr: JqOption = None,
|
|
701
|
+
) -> None:
|
|
702
|
+
"""Change image angle through a dedicated endpoint."""
|
|
703
|
+
|
|
704
|
+
runtime = apply_common_options(
|
|
705
|
+
ctx,
|
|
706
|
+
wait_option=wait_option,
|
|
707
|
+
download=download,
|
|
708
|
+
output_json=output_json,
|
|
709
|
+
jq_expr=jq_expr,
|
|
710
|
+
)
|
|
711
|
+
run_action(
|
|
712
|
+
ctx,
|
|
713
|
+
lambda: runtime.services().execute_image_angles(
|
|
714
|
+
image=image,
|
|
715
|
+
azimuth=azimuth,
|
|
716
|
+
elevation=elevation,
|
|
717
|
+
distance=distance,
|
|
718
|
+
prompt=prompt,
|
|
719
|
+
execution=runtime.execution_options(),
|
|
720
|
+
),
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
@image_app.command("colors")
|
|
725
|
+
def image_colors(
|
|
726
|
+
ctx: typer.Context,
|
|
727
|
+
image: Annotated[str, typer.Option("--image")],
|
|
728
|
+
reference: Annotated[str, typer.Option("--reference")],
|
|
729
|
+
wait_option: WaitOption = None,
|
|
730
|
+
download: DownloadOption = None,
|
|
731
|
+
output_json: OutputJsonOption = False,
|
|
732
|
+
jq_expr: JqOption = None,
|
|
733
|
+
) -> None:
|
|
734
|
+
"""Transfer reference colors to the source image."""
|
|
735
|
+
|
|
736
|
+
runtime = apply_common_options(
|
|
737
|
+
ctx,
|
|
738
|
+
wait_option=wait_option,
|
|
739
|
+
download=download,
|
|
740
|
+
output_json=output_json,
|
|
741
|
+
jq_expr=jq_expr,
|
|
742
|
+
)
|
|
743
|
+
run_action(
|
|
744
|
+
ctx,
|
|
745
|
+
lambda: runtime.services().execute_image_colors(
|
|
746
|
+
image=image,
|
|
747
|
+
reference=reference,
|
|
748
|
+
execution=runtime.execution_options(),
|
|
749
|
+
),
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
@video_app.command("generate")
|
|
754
|
+
def video_generate(
|
|
755
|
+
ctx: typer.Context,
|
|
756
|
+
prompt: str,
|
|
757
|
+
model: str = "ltx23",
|
|
758
|
+
duration: Optional[int] = None,
|
|
759
|
+
resolution: Optional[str] = None,
|
|
760
|
+
aspect_ratio: Annotated[str, typer.Option("--aspect-ratio")] = "16:9",
|
|
761
|
+
lora_high_url: Annotated[Optional[str], typer.Option("--lora-high-url")] = None,
|
|
762
|
+
lora_low_url: Annotated[Optional[str], typer.Option("--lora-low-url")] = None,
|
|
763
|
+
reference_image: Annotated[
|
|
764
|
+
list[str],
|
|
765
|
+
typer.Option(
|
|
766
|
+
"--reference-image",
|
|
767
|
+
help="Wan or Veo reference image URL/local path. Can be repeated.",
|
|
768
|
+
),
|
|
769
|
+
] = [],
|
|
770
|
+
reference_video: Annotated[
|
|
771
|
+
list[str],
|
|
772
|
+
typer.Option(
|
|
773
|
+
"--reference-video",
|
|
774
|
+
help="Wan reference video URL or local path. Can be repeated.",
|
|
775
|
+
),
|
|
776
|
+
] = [],
|
|
777
|
+
seed: Optional[int] = None,
|
|
778
|
+
generate_audio: Annotated[
|
|
779
|
+
Optional[bool], typer.Option("--generate-audio/--no-generate-audio")
|
|
780
|
+
] = None,
|
|
781
|
+
callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
|
|
782
|
+
wait_option: WaitOption = None,
|
|
783
|
+
download: DownloadOption = None,
|
|
784
|
+
output_json: OutputJsonOption = False,
|
|
785
|
+
jq_expr: JqOption = None,
|
|
786
|
+
) -> None:
|
|
787
|
+
"""Start text-to-video generation."""
|
|
788
|
+
|
|
789
|
+
runtime = apply_common_options(
|
|
790
|
+
ctx,
|
|
791
|
+
wait_option=wait_option,
|
|
792
|
+
download=download,
|
|
793
|
+
output_json=output_json,
|
|
794
|
+
jq_expr=jq_expr,
|
|
795
|
+
)
|
|
796
|
+
run_action(
|
|
797
|
+
ctx,
|
|
798
|
+
lambda: runtime.services().execute_video_generate(
|
|
799
|
+
prompt=prompt,
|
|
800
|
+
model=model,
|
|
801
|
+
duration=duration,
|
|
802
|
+
resolution=resolution,
|
|
803
|
+
aspect_ratio=aspect_ratio,
|
|
804
|
+
lora_high_url=lora_high_url,
|
|
805
|
+
lora_low_url=lora_low_url,
|
|
806
|
+
reference_images=reference_image,
|
|
807
|
+
reference_videos=reference_video,
|
|
808
|
+
seed=seed,
|
|
809
|
+
generate_audio=generate_audio,
|
|
810
|
+
callback_url=callback_url,
|
|
811
|
+
execution=runtime.execution_options(),
|
|
812
|
+
),
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
@video_app.command("edit")
|
|
817
|
+
def video_edit(
|
|
818
|
+
ctx: typer.Context,
|
|
819
|
+
prompt: Annotated[
|
|
820
|
+
Optional[str], typer.Argument(help="Edit prompt; optional for Aleph 2.")
|
|
821
|
+
] = None,
|
|
822
|
+
model: Annotated[str, typer.Option("--model")] = "seedance2",
|
|
823
|
+
image: Annotated[
|
|
824
|
+
list[str], typer.Option("--image", help="Reference image URL or local path.")
|
|
825
|
+
] = [],
|
|
826
|
+
video: Annotated[
|
|
827
|
+
Optional[str],
|
|
828
|
+
typer.Option("--video", help="Reference video URL or local path."),
|
|
829
|
+
] = None,
|
|
830
|
+
audio: Annotated[
|
|
831
|
+
Optional[str],
|
|
832
|
+
typer.Option("--audio", help="Reference audio URL or local path."),
|
|
833
|
+
] = None,
|
|
834
|
+
duration: Optional[int] = None,
|
|
835
|
+
resolution: Optional[str] = None,
|
|
836
|
+
aspect_ratio: Annotated[Optional[str], typer.Option("--aspect-ratio")] = None,
|
|
837
|
+
seed: Annotated[Optional[int], typer.Option("--seed")] = None,
|
|
838
|
+
keyframe: Annotated[
|
|
839
|
+
list[str],
|
|
840
|
+
typer.Option(
|
|
841
|
+
"--keyframe",
|
|
842
|
+
help=(
|
|
843
|
+
"Aleph 2 keyframe JSON with image_url and seconds or at. "
|
|
844
|
+
"Can be repeated."
|
|
845
|
+
),
|
|
846
|
+
),
|
|
847
|
+
] = [],
|
|
848
|
+
public_figure_threshold: Annotated[
|
|
849
|
+
Optional[str], typer.Option("--public-figure-threshold")
|
|
850
|
+
] = None,
|
|
851
|
+
generate_audio: Annotated[
|
|
852
|
+
Optional[bool], typer.Option("--generate-audio/--no-generate-audio")
|
|
853
|
+
] = None,
|
|
854
|
+
callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
|
|
855
|
+
wait_option: WaitOption = None,
|
|
856
|
+
download: DownloadOption = None,
|
|
857
|
+
output_json: OutputJsonOption = False,
|
|
858
|
+
jq_expr: JqOption = None,
|
|
859
|
+
) -> None:
|
|
860
|
+
"""Edit video with Seedance, Kling, Wan, or Aleph media."""
|
|
861
|
+
|
|
862
|
+
runtime = apply_common_options(
|
|
863
|
+
ctx,
|
|
864
|
+
wait_option=wait_option,
|
|
865
|
+
download=download,
|
|
866
|
+
output_json=output_json,
|
|
867
|
+
jq_expr=jq_expr,
|
|
868
|
+
)
|
|
869
|
+
run_action(
|
|
870
|
+
ctx,
|
|
871
|
+
lambda: runtime.services().execute_video_edit(
|
|
872
|
+
prompt=prompt,
|
|
873
|
+
model=model,
|
|
874
|
+
images=image,
|
|
875
|
+
video=video,
|
|
876
|
+
audio=audio,
|
|
877
|
+
duration=duration,
|
|
878
|
+
resolution=resolution,
|
|
879
|
+
aspect_ratio=aspect_ratio,
|
|
880
|
+
seed=seed,
|
|
881
|
+
keyframes=keyframe,
|
|
882
|
+
public_figure_threshold=public_figure_threshold,
|
|
883
|
+
generate_audio=generate_audio,
|
|
884
|
+
callback_url=callback_url,
|
|
885
|
+
execution=runtime.execution_options(),
|
|
886
|
+
),
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
@video_app.command("from-image")
|
|
891
|
+
def video_from_image(
|
|
892
|
+
ctx: typer.Context,
|
|
893
|
+
prompt: str,
|
|
894
|
+
image: Annotated[str, typer.Option("--image")],
|
|
895
|
+
model: str = "ltx23",
|
|
896
|
+
duration: Optional[int] = None,
|
|
897
|
+
resolution: Optional[str] = None,
|
|
898
|
+
aspect_ratio: Annotated[str, typer.Option("--aspect-ratio")] = "16:9",
|
|
899
|
+
seed: Optional[int] = None,
|
|
900
|
+
callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
|
|
901
|
+
wait_option: WaitOption = None,
|
|
902
|
+
download: DownloadOption = None,
|
|
903
|
+
output_json: OutputJsonOption = False,
|
|
904
|
+
jq_expr: JqOption = None,
|
|
905
|
+
) -> None:
|
|
906
|
+
"""Start image-to-video generation."""
|
|
907
|
+
|
|
908
|
+
runtime = apply_common_options(
|
|
909
|
+
ctx,
|
|
910
|
+
wait_option=wait_option,
|
|
911
|
+
download=download,
|
|
912
|
+
output_json=output_json,
|
|
913
|
+
jq_expr=jq_expr,
|
|
914
|
+
)
|
|
915
|
+
run_action(
|
|
916
|
+
ctx,
|
|
917
|
+
lambda: runtime.services().execute_video_generate(
|
|
918
|
+
prompt=prompt,
|
|
919
|
+
model=model,
|
|
920
|
+
duration=duration,
|
|
921
|
+
resolution=resolution,
|
|
922
|
+
aspect_ratio=aspect_ratio,
|
|
923
|
+
image=image,
|
|
924
|
+
seed=seed,
|
|
925
|
+
callback_url=callback_url,
|
|
926
|
+
execution=runtime.execution_options(),
|
|
927
|
+
),
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
@video_app.command("first-last")
|
|
932
|
+
def video_first_last(
|
|
933
|
+
ctx: typer.Context,
|
|
934
|
+
prompt: str,
|
|
935
|
+
image: Annotated[str, typer.Option("--image")],
|
|
936
|
+
last_image: Annotated[str, typer.Option("--last-image")],
|
|
937
|
+
model: str = "ltx23",
|
|
938
|
+
duration: Optional[int] = None,
|
|
939
|
+
resolution: Optional[str] = None,
|
|
940
|
+
aspect_ratio: Annotated[str, typer.Option("--aspect-ratio")] = "16:9",
|
|
941
|
+
seed: Optional[int] = None,
|
|
942
|
+
callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
|
|
943
|
+
wait_option: WaitOption = None,
|
|
944
|
+
download: DownloadOption = None,
|
|
945
|
+
output_json: OutputJsonOption = False,
|
|
946
|
+
jq_expr: JqOption = None,
|
|
947
|
+
) -> None:
|
|
948
|
+
"""Generate video between first and last frames."""
|
|
949
|
+
|
|
950
|
+
runtime = apply_common_options(
|
|
951
|
+
ctx,
|
|
952
|
+
wait_option=wait_option,
|
|
953
|
+
download=download,
|
|
954
|
+
output_json=output_json,
|
|
955
|
+
jq_expr=jq_expr,
|
|
956
|
+
)
|
|
957
|
+
run_action(
|
|
958
|
+
ctx,
|
|
959
|
+
lambda: runtime.services().execute_video_generate(
|
|
960
|
+
prompt=prompt,
|
|
961
|
+
model=model,
|
|
962
|
+
duration=duration,
|
|
963
|
+
resolution=resolution,
|
|
964
|
+
aspect_ratio=aspect_ratio,
|
|
965
|
+
image=image,
|
|
966
|
+
last_image=last_image,
|
|
967
|
+
seed=seed,
|
|
968
|
+
callback_url=callback_url,
|
|
969
|
+
execution=runtime.execution_options(),
|
|
970
|
+
),
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
@video_app.command("upscale")
|
|
975
|
+
def video_upscale(
|
|
976
|
+
ctx: typer.Context,
|
|
977
|
+
video: Annotated[Optional[str], typer.Option("--video")] = None,
|
|
978
|
+
task_id: Annotated[Optional[str], typer.Option("--task-id")] = None,
|
|
979
|
+
resolution: Annotated[
|
|
980
|
+
str,
|
|
981
|
+
typer.Option(
|
|
982
|
+
"--resolution",
|
|
983
|
+
help="Output resolution: 720p, 1080p, or 1440p (up to 20s at 1440p).",
|
|
984
|
+
),
|
|
985
|
+
] = "1080p",
|
|
986
|
+
wait_option: WaitOption = None,
|
|
987
|
+
download: DownloadOption = None,
|
|
988
|
+
output_json: OutputJsonOption = False,
|
|
989
|
+
jq_expr: JqOption = None,
|
|
990
|
+
) -> None:
|
|
991
|
+
"""Start video upscale from a URL/file or task_id.
|
|
992
|
+
|
|
993
|
+
Source videos longer than 20 seconds cannot be upscaled to 1440p.
|
|
994
|
+
"""
|
|
995
|
+
|
|
996
|
+
runtime = apply_common_options(
|
|
997
|
+
ctx,
|
|
998
|
+
wait_option=wait_option,
|
|
999
|
+
download=download,
|
|
1000
|
+
output_json=output_json,
|
|
1001
|
+
jq_expr=jq_expr,
|
|
1002
|
+
)
|
|
1003
|
+
run_action(
|
|
1004
|
+
ctx,
|
|
1005
|
+
lambda: runtime.services().execute_video_upscale(
|
|
1006
|
+
video=video,
|
|
1007
|
+
task_id=task_id,
|
|
1008
|
+
resolution=resolution,
|
|
1009
|
+
execution=runtime.execution_options(),
|
|
1010
|
+
),
|
|
1011
|
+
)
|
|
1012
|
+
|
|
1013
|
+
|
|
1014
|
+
@lipsync_app.command("video")
|
|
1015
|
+
def lipsync_video(
|
|
1016
|
+
ctx: typer.Context,
|
|
1017
|
+
video: Annotated[str, typer.Option("--video")],
|
|
1018
|
+
audio: Annotated[str, typer.Option("--audio")],
|
|
1019
|
+
resolution: str = "480p",
|
|
1020
|
+
prompt: Optional[str] = None,
|
|
1021
|
+
seed: int = -1,
|
|
1022
|
+
callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
|
|
1023
|
+
wait_option: WaitOption = None,
|
|
1024
|
+
download: DownloadOption = None,
|
|
1025
|
+
output_json: OutputJsonOption = False,
|
|
1026
|
+
jq_expr: JqOption = None,
|
|
1027
|
+
) -> None:
|
|
1028
|
+
"""Synchronize lips in a video using an audio track."""
|
|
1029
|
+
|
|
1030
|
+
runtime = apply_common_options(
|
|
1031
|
+
ctx,
|
|
1032
|
+
wait_option=wait_option,
|
|
1033
|
+
download=download,
|
|
1034
|
+
output_json=output_json,
|
|
1035
|
+
jq_expr=jq_expr,
|
|
1036
|
+
)
|
|
1037
|
+
run_action(
|
|
1038
|
+
ctx,
|
|
1039
|
+
lambda: runtime.services().execute_lipsync_video(
|
|
1040
|
+
video=video,
|
|
1041
|
+
audio=audio,
|
|
1042
|
+
resolution=resolution,
|
|
1043
|
+
prompt=prompt,
|
|
1044
|
+
seed=seed,
|
|
1045
|
+
callback_url=callback_url,
|
|
1046
|
+
execution=runtime.execution_options(),
|
|
1047
|
+
),
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
@lipsync_app.command("image")
|
|
1052
|
+
def lipsync_image(
|
|
1053
|
+
ctx: typer.Context,
|
|
1054
|
+
image: Annotated[str, typer.Option("--image")],
|
|
1055
|
+
audio: Annotated[str, typer.Option("--audio")],
|
|
1056
|
+
model: str = "inftalk",
|
|
1057
|
+
resolution: str = "480p",
|
|
1058
|
+
prompt: Optional[str] = None,
|
|
1059
|
+
seed: int = -1,
|
|
1060
|
+
callback_url: Annotated[Optional[str], typer.Option("--callback-url")] = None,
|
|
1061
|
+
wait_option: WaitOption = None,
|
|
1062
|
+
download: DownloadOption = None,
|
|
1063
|
+
output_json: OutputJsonOption = False,
|
|
1064
|
+
jq_expr: JqOption = None,
|
|
1065
|
+
) -> None:
|
|
1066
|
+
"""Create a lipsync video from a still image and audio."""
|
|
1067
|
+
|
|
1068
|
+
runtime = apply_common_options(
|
|
1069
|
+
ctx,
|
|
1070
|
+
wait_option=wait_option,
|
|
1071
|
+
download=download,
|
|
1072
|
+
output_json=output_json,
|
|
1073
|
+
jq_expr=jq_expr,
|
|
1074
|
+
)
|
|
1075
|
+
run_action(
|
|
1076
|
+
ctx,
|
|
1077
|
+
lambda: runtime.services().execute_lipsync_image(
|
|
1078
|
+
image=image,
|
|
1079
|
+
audio=audio,
|
|
1080
|
+
model=model,
|
|
1081
|
+
resolution=resolution,
|
|
1082
|
+
prompt=prompt,
|
|
1083
|
+
seed=seed,
|
|
1084
|
+
callback_url=callback_url,
|
|
1085
|
+
execution=runtime.execution_options(),
|
|
1086
|
+
),
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
@audio_app.command("transcribe")
|
|
1091
|
+
def audio_transcribe(
|
|
1092
|
+
ctx: typer.Context,
|
|
1093
|
+
audio: Annotated[str, typer.Option("--audio")],
|
|
1094
|
+
language: str = "auto",
|
|
1095
|
+
hints: str = "",
|
|
1096
|
+
denoise: bool = True,
|
|
1097
|
+
wait_option: WaitOption = None,
|
|
1098
|
+
download: DownloadOption = None,
|
|
1099
|
+
output_json: OutputJsonOption = False,
|
|
1100
|
+
jq_expr: JqOption = None,
|
|
1101
|
+
) -> None:
|
|
1102
|
+
"""Start speech transcription for an audio file."""
|
|
1103
|
+
|
|
1104
|
+
runtime = apply_common_options(
|
|
1105
|
+
ctx,
|
|
1106
|
+
wait_option=wait_option,
|
|
1107
|
+
download=download,
|
|
1108
|
+
output_json=output_json,
|
|
1109
|
+
jq_expr=jq_expr,
|
|
1110
|
+
)
|
|
1111
|
+
run_action(
|
|
1112
|
+
ctx,
|
|
1113
|
+
lambda: runtime.services().execute_audio_transcribe(
|
|
1114
|
+
audio=audio,
|
|
1115
|
+
language=language,
|
|
1116
|
+
hints=hints,
|
|
1117
|
+
denoise=denoise,
|
|
1118
|
+
execution=runtime.execution_options(),
|
|
1119
|
+
),
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
@audio_app.command("tts-create")
|
|
1124
|
+
def tts_create(
|
|
1125
|
+
ctx: typer.Context,
|
|
1126
|
+
text: Annotated[Optional[str], typer.Option("--text")] = None,
|
|
1127
|
+
text_file: Annotated[Optional[Path], typer.Option("--text-file")] = None,
|
|
1128
|
+
speaker: str = "Ryan",
|
|
1129
|
+
style: str = "Auto",
|
|
1130
|
+
language: str = "Auto",
|
|
1131
|
+
prompt: str = "",
|
|
1132
|
+
seed: int = -1,
|
|
1133
|
+
wait_option: WaitOption = None,
|
|
1134
|
+
download: DownloadOption = None,
|
|
1135
|
+
output_json: OutputJsonOption = False,
|
|
1136
|
+
jq_expr: JqOption = None,
|
|
1137
|
+
) -> None:
|
|
1138
|
+
"""Create speech from text using a selected speaker."""
|
|
1139
|
+
|
|
1140
|
+
runtime = apply_common_options(
|
|
1141
|
+
ctx,
|
|
1142
|
+
wait_option=wait_option,
|
|
1143
|
+
download=download,
|
|
1144
|
+
output_json=output_json,
|
|
1145
|
+
jq_expr=jq_expr,
|
|
1146
|
+
)
|
|
1147
|
+
run_action(
|
|
1148
|
+
ctx,
|
|
1149
|
+
lambda: runtime.services().execute_tts_create(
|
|
1150
|
+
text=text,
|
|
1151
|
+
text_file=text_file,
|
|
1152
|
+
speaker=speaker,
|
|
1153
|
+
style=style,
|
|
1154
|
+
language=language,
|
|
1155
|
+
prompt=prompt,
|
|
1156
|
+
seed=seed,
|
|
1157
|
+
execution=runtime.execution_options(),
|
|
1158
|
+
),
|
|
1159
|
+
)
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
@audio_app.command("tts-clone")
|
|
1163
|
+
def tts_clone(
|
|
1164
|
+
ctx: typer.Context,
|
|
1165
|
+
audio: Annotated[str, typer.Option("--audio")],
|
|
1166
|
+
text: Annotated[Optional[str], typer.Option("--text")] = None,
|
|
1167
|
+
text_file: Annotated[Optional[Path], typer.Option("--text-file")] = None,
|
|
1168
|
+
language: str = "Auto",
|
|
1169
|
+
seed: int = -1,
|
|
1170
|
+
wait_option: WaitOption = None,
|
|
1171
|
+
download: DownloadOption = None,
|
|
1172
|
+
output_json: OutputJsonOption = False,
|
|
1173
|
+
jq_expr: JqOption = None,
|
|
1174
|
+
) -> None:
|
|
1175
|
+
"""Create speech from text using a cloned voice sample."""
|
|
1176
|
+
|
|
1177
|
+
runtime = apply_common_options(
|
|
1178
|
+
ctx,
|
|
1179
|
+
wait_option=wait_option,
|
|
1180
|
+
download=download,
|
|
1181
|
+
output_json=output_json,
|
|
1182
|
+
jq_expr=jq_expr,
|
|
1183
|
+
)
|
|
1184
|
+
run_action(
|
|
1185
|
+
ctx,
|
|
1186
|
+
lambda: runtime.services().execute_tts_clone(
|
|
1187
|
+
audio=audio,
|
|
1188
|
+
text=text,
|
|
1189
|
+
text_file=text_file,
|
|
1190
|
+
language=language,
|
|
1191
|
+
seed=seed,
|
|
1192
|
+
execution=runtime.execution_options(),
|
|
1193
|
+
),
|
|
1194
|
+
)
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
@audio_app.command("tts-voice")
|
|
1198
|
+
def tts_voice(
|
|
1199
|
+
ctx: typer.Context,
|
|
1200
|
+
text: Annotated[Optional[str], typer.Option("--text")] = None,
|
|
1201
|
+
text_file: Annotated[Optional[Path], typer.Option("--text-file")] = None,
|
|
1202
|
+
character: str = "Female",
|
|
1203
|
+
style: str = "Auto",
|
|
1204
|
+
language: str = "Auto",
|
|
1205
|
+
prompt: str = "",
|
|
1206
|
+
seed: int = -1,
|
|
1207
|
+
wait_option: WaitOption = None,
|
|
1208
|
+
download: DownloadOption = None,
|
|
1209
|
+
output_json: OutputJsonOption = False,
|
|
1210
|
+
jq_expr: JqOption = None,
|
|
1211
|
+
) -> None:
|
|
1212
|
+
"""Create speech from text using a character voice."""
|
|
1213
|
+
|
|
1214
|
+
runtime = apply_common_options(
|
|
1215
|
+
ctx,
|
|
1216
|
+
wait_option=wait_option,
|
|
1217
|
+
download=download,
|
|
1218
|
+
output_json=output_json,
|
|
1219
|
+
jq_expr=jq_expr,
|
|
1220
|
+
)
|
|
1221
|
+
run_action(
|
|
1222
|
+
ctx,
|
|
1223
|
+
lambda: runtime.services().execute_tts_voice(
|
|
1224
|
+
text=text,
|
|
1225
|
+
text_file=text_file,
|
|
1226
|
+
character=character,
|
|
1227
|
+
style=style,
|
|
1228
|
+
language=language,
|
|
1229
|
+
prompt=prompt,
|
|
1230
|
+
seed=seed,
|
|
1231
|
+
execution=runtime.execution_options(),
|
|
1232
|
+
),
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
|
|
1236
|
+
@app.command("status")
|
|
1237
|
+
def status(
|
|
1238
|
+
ctx: typer.Context,
|
|
1239
|
+
task_id: str,
|
|
1240
|
+
download: DownloadOption = None,
|
|
1241
|
+
output_json: OutputJsonOption = False,
|
|
1242
|
+
jq_expr: JqOption = None,
|
|
1243
|
+
) -> None:
|
|
1244
|
+
"""Show task status once; --download waits for completion first."""
|
|
1245
|
+
|
|
1246
|
+
runtime = apply_common_options(
|
|
1247
|
+
ctx, download=download, output_json=output_json, jq_expr=jq_expr
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
def action() -> Any:
|
|
1251
|
+
return runtime.services().get_task_status(
|
|
1252
|
+
task_id=task_id,
|
|
1253
|
+
execution=runtime.execution_options(wait=False),
|
|
1254
|
+
)
|
|
1255
|
+
|
|
1256
|
+
run_action(ctx, action)
|
|
1257
|
+
|
|
1258
|
+
|
|
1259
|
+
@app.command("wait")
|
|
1260
|
+
def wait_command(
|
|
1261
|
+
ctx: typer.Context,
|
|
1262
|
+
task_id: str,
|
|
1263
|
+
download: DownloadOption = None,
|
|
1264
|
+
output_json: OutputJsonOption = False,
|
|
1265
|
+
jq_expr: JqOption = None,
|
|
1266
|
+
) -> None:
|
|
1267
|
+
"""Wait for an existing async task to finish."""
|
|
1268
|
+
|
|
1269
|
+
runtime = apply_common_options(
|
|
1270
|
+
ctx, download=download, output_json=output_json, jq_expr=jq_expr
|
|
1271
|
+
)
|
|
1272
|
+
run_action(
|
|
1273
|
+
ctx,
|
|
1274
|
+
lambda: runtime.services().wait_for_task(
|
|
1275
|
+
task_id=task_id,
|
|
1276
|
+
execution=runtime.execution_options(wait=True),
|
|
1277
|
+
),
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
@app.command("keywords")
|
|
1282
|
+
def keywords(
|
|
1283
|
+
ctx: typer.Context,
|
|
1284
|
+
image: Annotated[str, typer.Option("--image")],
|
|
1285
|
+
lang: str = "en",
|
|
1286
|
+
num_keywords: Optional[int] = None,
|
|
1287
|
+
colors: bool = False,
|
|
1288
|
+
output_json: OutputJsonOption = False,
|
|
1289
|
+
jq_expr: JqOption = None,
|
|
1290
|
+
) -> None:
|
|
1291
|
+
"""Extract keywords from an image."""
|
|
1292
|
+
|
|
1293
|
+
runtime = apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
|
|
1294
|
+
run_action(
|
|
1295
|
+
ctx,
|
|
1296
|
+
lambda: runtime.services().execute_keywords(
|
|
1297
|
+
image=image,
|
|
1298
|
+
lang=lang,
|
|
1299
|
+
num_keywords=num_keywords,
|
|
1300
|
+
colors=colors,
|
|
1301
|
+
),
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
@app.command("quality")
|
|
1306
|
+
def quality(
|
|
1307
|
+
ctx: typer.Context,
|
|
1308
|
+
image: Annotated[str, typer.Option("--image")],
|
|
1309
|
+
output_json: OutputJsonOption = False,
|
|
1310
|
+
jq_expr: JqOption = None,
|
|
1311
|
+
) -> None:
|
|
1312
|
+
"""Score technical image quality."""
|
|
1313
|
+
|
|
1314
|
+
runtime = apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
|
|
1315
|
+
run_action(
|
|
1316
|
+
ctx,
|
|
1317
|
+
lambda: runtime.services().execute_quality(image=image),
|
|
1318
|
+
)
|
|
1319
|
+
|
|
1320
|
+
|
|
1321
|
+
@app.command("quality-ugc")
|
|
1322
|
+
def quality_ugc(
|
|
1323
|
+
ctx: typer.Context,
|
|
1324
|
+
image: Annotated[str, typer.Option("--image")],
|
|
1325
|
+
output_json: OutputJsonOption = False,
|
|
1326
|
+
jq_expr: JqOption = None,
|
|
1327
|
+
) -> None:
|
|
1328
|
+
"""Score UGC image quality."""
|
|
1329
|
+
|
|
1330
|
+
runtime = apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
|
|
1331
|
+
run_action(
|
|
1332
|
+
ctx,
|
|
1333
|
+
lambda: runtime.services().execute_quality_ugc(image=image),
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
@app.command("faces")
|
|
1338
|
+
def faces(
|
|
1339
|
+
ctx: typer.Context,
|
|
1340
|
+
image: Annotated[str, typer.Option("--image")],
|
|
1341
|
+
output_json: OutputJsonOption = False,
|
|
1342
|
+
jq_expr: JqOption = None,
|
|
1343
|
+
) -> None:
|
|
1344
|
+
"""Detect faces in an image."""
|
|
1345
|
+
|
|
1346
|
+
runtime = apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
|
|
1347
|
+
run_action(ctx, lambda: runtime.services().execute_faces(image=image))
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
@app.command("captioning")
|
|
1351
|
+
def captioning(
|
|
1352
|
+
ctx: typer.Context,
|
|
1353
|
+
image: Annotated[str, typer.Option("--image")],
|
|
1354
|
+
output_json: OutputJsonOption = False,
|
|
1355
|
+
jq_expr: JqOption = None,
|
|
1356
|
+
) -> None:
|
|
1357
|
+
"""Generate an image caption."""
|
|
1358
|
+
|
|
1359
|
+
runtime = apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
|
|
1360
|
+
run_action(
|
|
1361
|
+
ctx,
|
|
1362
|
+
lambda: runtime.services().execute_captioning(image=image),
|
|
1363
|
+
)
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
@app.command("video-keywords")
|
|
1367
|
+
def video_keywords(
|
|
1368
|
+
ctx: typer.Context,
|
|
1369
|
+
video: Annotated[str, typer.Option("--video")],
|
|
1370
|
+
output_json: OutputJsonOption = False,
|
|
1371
|
+
jq_expr: JqOption = None,
|
|
1372
|
+
) -> None:
|
|
1373
|
+
"""Extract keywords from a video."""
|
|
1374
|
+
|
|
1375
|
+
runtime = apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
|
|
1376
|
+
run_action(
|
|
1377
|
+
ctx,
|
|
1378
|
+
lambda: runtime.services().execute_video_keywords(video=video),
|
|
1379
|
+
)
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
@app.command("run")
|
|
1383
|
+
def generic_run(
|
|
1384
|
+
ctx: typer.Context,
|
|
1385
|
+
endpoint: str,
|
|
1386
|
+
prompt: Annotated[Optional[str], typer.Option("--prompt", "-p")] = None,
|
|
1387
|
+
item: Annotated[
|
|
1388
|
+
list[str],
|
|
1389
|
+
typer.Option(
|
|
1390
|
+
"--input",
|
|
1391
|
+
"-i",
|
|
1392
|
+
help="key=value input. Value is parsed as JSON when possible.",
|
|
1393
|
+
),
|
|
1394
|
+
] = [],
|
|
1395
|
+
input_file: Annotated[Optional[Path], typer.Option("--input-file")] = None,
|
|
1396
|
+
method: Annotated[Optional[str], typer.Option("--method")] = None,
|
|
1397
|
+
dry_run: Annotated[bool, typer.Option("--dry-run")] = False,
|
|
1398
|
+
help_schema: Annotated[bool, typer.Option("--help-schema")] = False,
|
|
1399
|
+
wait_option: WaitOption = None,
|
|
1400
|
+
download: DownloadOption = None,
|
|
1401
|
+
output_json: OutputJsonOption = False,
|
|
1402
|
+
jq_expr: JqOption = None,
|
|
1403
|
+
) -> None:
|
|
1404
|
+
"""Call an endpoint by name/path with a payload from CLI options."""
|
|
1405
|
+
|
|
1406
|
+
runtime = apply_common_options(
|
|
1407
|
+
ctx,
|
|
1408
|
+
wait_option=wait_option,
|
|
1409
|
+
download=download,
|
|
1410
|
+
output_json=output_json,
|
|
1411
|
+
jq_expr=jq_expr,
|
|
1412
|
+
)
|
|
1413
|
+
|
|
1414
|
+
def action() -> Any:
|
|
1415
|
+
return runtime.services().execute_generic(
|
|
1416
|
+
endpoint=endpoint,
|
|
1417
|
+
payload=parse_generic_payload(
|
|
1418
|
+
prompt=prompt, items=item, input_file=input_file
|
|
1419
|
+
),
|
|
1420
|
+
method=method,
|
|
1421
|
+
execution=runtime.execution_options(),
|
|
1422
|
+
dry_run=dry_run,
|
|
1423
|
+
help_schema=help_schema,
|
|
1424
|
+
client_id_present=runtime.client_id is not None,
|
|
1425
|
+
schema_loader=load_schema,
|
|
1426
|
+
)
|
|
1427
|
+
|
|
1428
|
+
run_action(ctx, action)
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
def apply_local_output_options(
|
|
1432
|
+
ctx: typer.Context,
|
|
1433
|
+
output_json: bool,
|
|
1434
|
+
jq_expr: str | None,
|
|
1435
|
+
) -> None:
|
|
1436
|
+
"""Apply command-local output flags."""
|
|
1437
|
+
|
|
1438
|
+
apply_common_options(ctx, output_json=output_json, jq_expr=jq_expr)
|
|
1439
|
+
|
|
1440
|
+
|
|
1441
|
+
@mcp_app.command("serve")
|
|
1442
|
+
def mcp_serve(ctx: typer.Context) -> None:
|
|
1443
|
+
"""Start the Everypixel MCP server using the stdio transport."""
|
|
1444
|
+
|
|
1445
|
+
runtime = get_runtime(ctx)
|
|
1446
|
+
from .mcp_server import create_mcp_server, run_mcp_server
|
|
1447
|
+
|
|
1448
|
+
server = create_mcp_server(
|
|
1449
|
+
runtime.services,
|
|
1450
|
+
client_id_present=runtime.client_id is not None,
|
|
1451
|
+
)
|
|
1452
|
+
run_mcp_server(server)
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
@docs_app.command("openapi")
|
|
1456
|
+
def docs_openapi(
|
|
1457
|
+
ctx: typer.Context,
|
|
1458
|
+
output_json: Annotated[bool, typer.Option("--output-json", "-j")] = False,
|
|
1459
|
+
jq_expr: Annotated[Optional[str], typer.Option("--jq")] = None,
|
|
1460
|
+
) -> None:
|
|
1461
|
+
"""Print the OpenAPI schema selected through live/cache/bundled fallback."""
|
|
1462
|
+
|
|
1463
|
+
apply_local_output_options(ctx, output_json, jq_expr)
|
|
1464
|
+
runtime = get_runtime(ctx)
|
|
1465
|
+
run_action(ctx, lambda: runtime.services().openapi())
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
@docs_app.command("open")
|
|
1469
|
+
def docs_open(ctx: typer.Context) -> None:
|
|
1470
|
+
"""Open API Swagger UI in a browser."""
|
|
1471
|
+
|
|
1472
|
+
runtime = get_runtime(ctx)
|
|
1473
|
+
|
|
1474
|
+
def action() -> dict[str, Any]:
|
|
1475
|
+
url = f"{runtime.base_url.rstrip('/')}/v1/docs"
|
|
1476
|
+
webbrowser.open(url)
|
|
1477
|
+
return {"status": "ok", "url": url}
|
|
1478
|
+
|
|
1479
|
+
run_action(ctx, action)
|
|
1480
|
+
|
|
1481
|
+
|
|
1482
|
+
@docs_app.command("refresh")
|
|
1483
|
+
def docs_refresh(ctx: typer.Context) -> None:
|
|
1484
|
+
"""Force-refresh the local OpenAPI schema cache."""
|
|
1485
|
+
|
|
1486
|
+
runtime = get_runtime(ctx)
|
|
1487
|
+
|
|
1488
|
+
def action() -> dict[str, Any]:
|
|
1489
|
+
result, path = runtime.services().refresh_openapi()
|
|
1490
|
+
schema = serialize_operation_result(result)
|
|
1491
|
+
return {
|
|
1492
|
+
"status": "ok",
|
|
1493
|
+
"source": "live",
|
|
1494
|
+
"cache_path": str(path),
|
|
1495
|
+
"paths": len(schema.get("paths", {})),
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
run_action(ctx, action)
|
|
1499
|
+
|
|
1500
|
+
|
|
1501
|
+
@schema_app.command("refresh")
|
|
1502
|
+
def schema_refresh(ctx: typer.Context) -> None:
|
|
1503
|
+
"""Alias for `docs refresh`."""
|
|
1504
|
+
|
|
1505
|
+
docs_refresh(ctx)
|
|
1506
|
+
|
|
1507
|
+
|
|
1508
|
+
@schema_app.command("openapi")
|
|
1509
|
+
def schema_openapi(
|
|
1510
|
+
ctx: typer.Context,
|
|
1511
|
+
output_json: Annotated[bool, typer.Option("--output-json", "-j")] = False,
|
|
1512
|
+
jq_expr: Annotated[Optional[str], typer.Option("--jq")] = None,
|
|
1513
|
+
) -> None:
|
|
1514
|
+
"""Alias for `docs openapi`."""
|
|
1515
|
+
|
|
1516
|
+
docs_openapi(ctx, output_json=output_json, jq_expr=jq_expr)
|
|
1517
|
+
|
|
1518
|
+
|
|
1519
|
+
if __name__ == "__main__":
|
|
1520
|
+
app()
|