genblaze-cli 0.3.0__tar.gz → 0.3.4__tar.gz
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.
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/.gitignore +6 -1
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/PKG-INFO +6 -6
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/README.md +4 -4
- genblaze_cli-0.3.4/genblaze_cli/commands/extract.py +54 -0
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/genblaze_cli/commands/index.py +3 -3
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/genblaze_cli/commands/replay.py +48 -10
- genblaze_cli-0.3.4/genblaze_cli/commands/verify.py +56 -0
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/genblaze_cli/main.py +1 -1
- genblaze_cli-0.3.4/genblaze_cli/manifest_io.py +42 -0
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/pyproject.toml +7 -2
- genblaze_cli-0.3.4/tests/test_cli.py +665 -0
- genblaze_cli-0.3.0/genblaze_cli/commands/extract.py +0 -40
- genblaze_cli-0.3.0/genblaze_cli/commands/verify.py +0 -41
- genblaze_cli-0.3.0/tests/test_cli.py +0 -145
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/genblaze_cli/__init__.py +0 -0
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/genblaze_cli/commands/__init__.py +0 -0
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/genblaze_cli/py.typed +0 -0
- {genblaze_cli-0.3.0 → genblaze_cli-0.3.4}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: genblaze-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: CLI for genblaze manifest operations
|
|
5
5
|
Project-URL: Homepage, https://github.com/backblaze-labs/genblaze
|
|
6
6
|
Project-URL: Documentation, https://github.com/backblaze-labs/genblaze#readme
|
|
@@ -20,7 +20,7 @@ Classifier: Topic :: Multimedia
|
|
|
20
20
|
Classifier: Topic :: Software Development :: Libraries
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Requires-Dist: click>=8.0
|
|
23
|
-
Requires-Dist: genblaze-core<0.4,>=0.
|
|
23
|
+
Requires-Dist: genblaze-core<0.4,>=0.3.6
|
|
24
24
|
Provides-Extra: dev
|
|
25
25
|
Requires-Dist: pillow>=10.0; extra == 'dev'
|
|
26
26
|
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
@@ -31,11 +31,11 @@ Description-Content-Type: text/markdown
|
|
|
31
31
|
|
|
32
32
|
**Command-line toolkit for inspecting, verifying, and indexing genblaze AI-generated-media provenance manifests.**
|
|
33
33
|
|
|
34
|
-
`genblaze-cli` is the companion CLI for [genblaze](https://github.com/backblaze-labs/genblaze) — the Python SDK for generative AI pipelines across video, image, and audio. It lets anyone (not just Python developers) audit AI-generated media: extract the embedded provenance manifest from an MP4 / PNG / MP3, verify its
|
|
34
|
+
`genblaze-cli` is the companion CLI for [genblaze](https://github.com/backblaze-labs/genblaze) — the Python SDK for generative AI pipelines across video, image, and audio. It lets anyone (not just Python developers) audit AI-generated media: extract the embedded provenance manifest from an MP4 / PNG / MP3, verify its manifest hash and output sha256 coverage, replay a run, or index manifests into Parquet for downstream analytics.
|
|
35
35
|
|
|
36
36
|
## Why genblaze-cli
|
|
37
37
|
|
|
38
|
-
- **Audit AI-generated media in one command** — `genblaze verify video.mp4` confirms a file's manifest hash hasn't been tampered with.
|
|
38
|
+
- **Audit AI-generated media in one command** — `genblaze verify video.mp4` confirms a file's manifest hash hasn't been tampered with and all output assets declare sha256.
|
|
39
39
|
- **Works on any genblaze output** — PNG, JPEG, WebP, MP4, MP3, WAV with embedded manifests.
|
|
40
40
|
- **Analytics-ready** — `genblaze index` emits partitioned Parquet tables (runs, steps, assets) for BI tools and data warehouses.
|
|
41
41
|
- **Zero provider dependencies** — reads manifests; doesn't call any AI API.
|
|
@@ -57,7 +57,7 @@ genblaze --help
|
|
|
57
57
|
genblaze extract video.mp4 # Extract embedded manifest → stdout (JSON)
|
|
58
58
|
genblaze extract video.mp4 -o m.json # …or to a file
|
|
59
59
|
|
|
60
|
-
genblaze verify video.mp4 # Verify
|
|
60
|
+
genblaze verify video.mp4 # Verify manifest hash + output sha256 declarations
|
|
61
61
|
genblaze verify manifest.json # Or verify a standalone manifest file
|
|
62
62
|
|
|
63
63
|
genblaze replay manifest.json # Show what a replay would do (dry run)
|
|
@@ -65,7 +65,7 @@ genblaze replay manifest.json # Show what a replay would do (dry run
|
|
|
65
65
|
genblaze index manifest.json -o data/ # Index into partitioned Parquet tables
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
-
Exit codes are non-zero on verification failure — safe to drop into CI pipelines, release checks, or content-moderation workflows.
|
|
68
|
+
Exit codes are non-zero on verification failure — safe to drop into CI pipelines, release checks, or content-moderation workflows. The command does not fetch remote asset URLs; consumers that dereference `asset.url` must hash those bytes separately and compare them with the manifest's `asset.sha256`.
|
|
69
69
|
|
|
70
70
|
## Typical flow
|
|
71
71
|
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
|
|
4
4
|
**Command-line toolkit for inspecting, verifying, and indexing genblaze AI-generated-media provenance manifests.**
|
|
5
5
|
|
|
6
|
-
`genblaze-cli` is the companion CLI for [genblaze](https://github.com/backblaze-labs/genblaze) — the Python SDK for generative AI pipelines across video, image, and audio. It lets anyone (not just Python developers) audit AI-generated media: extract the embedded provenance manifest from an MP4 / PNG / MP3, verify its
|
|
6
|
+
`genblaze-cli` is the companion CLI for [genblaze](https://github.com/backblaze-labs/genblaze) — the Python SDK for generative AI pipelines across video, image, and audio. It lets anyone (not just Python developers) audit AI-generated media: extract the embedded provenance manifest from an MP4 / PNG / MP3, verify its manifest hash and output sha256 coverage, replay a run, or index manifests into Parquet for downstream analytics.
|
|
7
7
|
|
|
8
8
|
## Why genblaze-cli
|
|
9
9
|
|
|
10
|
-
- **Audit AI-generated media in one command** — `genblaze verify video.mp4` confirms a file's manifest hash hasn't been tampered with.
|
|
10
|
+
- **Audit AI-generated media in one command** — `genblaze verify video.mp4` confirms a file's manifest hash hasn't been tampered with and all output assets declare sha256.
|
|
11
11
|
- **Works on any genblaze output** — PNG, JPEG, WebP, MP4, MP3, WAV with embedded manifests.
|
|
12
12
|
- **Analytics-ready** — `genblaze index` emits partitioned Parquet tables (runs, steps, assets) for BI tools and data warehouses.
|
|
13
13
|
- **Zero provider dependencies** — reads manifests; doesn't call any AI API.
|
|
@@ -29,7 +29,7 @@ genblaze --help
|
|
|
29
29
|
genblaze extract video.mp4 # Extract embedded manifest → stdout (JSON)
|
|
30
30
|
genblaze extract video.mp4 -o m.json # …or to a file
|
|
31
31
|
|
|
32
|
-
genblaze verify video.mp4 # Verify
|
|
32
|
+
genblaze verify video.mp4 # Verify manifest hash + output sha256 declarations
|
|
33
33
|
genblaze verify manifest.json # Or verify a standalone manifest file
|
|
34
34
|
|
|
35
35
|
genblaze replay manifest.json # Show what a replay would do (dry run)
|
|
@@ -37,7 +37,7 @@ genblaze replay manifest.json # Show what a replay would do (dry run
|
|
|
37
37
|
genblaze index manifest.json -o data/ # Index into partitioned Parquet tables
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
Exit codes are non-zero on verification failure — safe to drop into CI pipelines, release checks, or content-moderation workflows.
|
|
40
|
+
Exit codes are non-zero on verification failure — safe to drop into CI pipelines, release checks, or content-moderation workflows. The command does not fetch remote asset URLs; consumers that dereference `asset.url` must hash those bytes separately and compare them with the manifest's `asset.sha256`.
|
|
41
41
|
|
|
42
42
|
## Typical flow
|
|
43
43
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Extract command — print embedded manifest from a media file."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
from genblaze_core.canonical.json import canonical_json
|
|
7
|
+
from genblaze_core.exceptions import ManifestError
|
|
8
|
+
from genblaze_core.models.manifest import Manifest
|
|
9
|
+
|
|
10
|
+
from genblaze_cli.manifest_io import extract_manifest
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _manifest_json_for_display(manifest: Manifest) -> str:
|
|
14
|
+
"""Serialize for inspection without bypassing write-schema guards."""
|
|
15
|
+
try:
|
|
16
|
+
manifest.assert_writable_schema()
|
|
17
|
+
except ManifestError:
|
|
18
|
+
return canonical_json(manifest.model_dump(mode="python"))
|
|
19
|
+
return manifest.to_canonical_json()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.command()
|
|
23
|
+
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
24
|
+
@click.option("--format", "fmt", type=click.Choice(["json", "summary"]), default="json")
|
|
25
|
+
@click.option(
|
|
26
|
+
"-o",
|
|
27
|
+
"--output",
|
|
28
|
+
type=click.Path(path_type=Path),
|
|
29
|
+
default=None,
|
|
30
|
+
help="Write extracted manifest JSON to this file instead of stdout.",
|
|
31
|
+
)
|
|
32
|
+
def extract(file: Path, fmt: str, output: Path | None) -> None:
|
|
33
|
+
"""Extract and display the genblaze manifest from a media file."""
|
|
34
|
+
try:
|
|
35
|
+
manifest = extract_manifest(file)
|
|
36
|
+
if fmt == "json":
|
|
37
|
+
json_str = _manifest_json_for_display(manifest)
|
|
38
|
+
if output is not None:
|
|
39
|
+
output.write_text(json_str, encoding="utf-8")
|
|
40
|
+
else:
|
|
41
|
+
click.echo(json_str)
|
|
42
|
+
else:
|
|
43
|
+
report = manifest.verification_report()
|
|
44
|
+
click.echo(f"Run ID: {manifest.run.run_id}")
|
|
45
|
+
click.echo(f"Steps: {len(manifest.run.steps)}")
|
|
46
|
+
click.echo(f"Hash: {manifest.canonical_hash}")
|
|
47
|
+
click.echo(f"Hash OK: {report.hash_ok}")
|
|
48
|
+
click.echo(f"Output sha256: {len(report.unverified_sha256_ids)} missing or malformed")
|
|
49
|
+
# Itemize metadata too, so a False `Verified:` line always has a
|
|
50
|
+
# visible reason (report.ok folds in invalid_metadata_ids, #149).
|
|
51
|
+
click.echo(f"Output metadata: {len(report.invalid_metadata_ids)} out of spec")
|
|
52
|
+
click.echo(f"Verified: {report.ok} (asset bytes were not fetched or compared)")
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
raise click.ClickException(f"{type(exc).__name__}: {exc}") from exc
|
|
@@ -7,7 +7,7 @@ import click
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
@click.command()
|
|
10
|
-
@click.argument("manifest_file", type=click.Path(exists=True, path_type=Path))
|
|
10
|
+
@click.argument("manifest_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
11
11
|
@click.option(
|
|
12
12
|
"--output",
|
|
13
13
|
"-o",
|
|
@@ -17,12 +17,12 @@ import click
|
|
|
17
17
|
)
|
|
18
18
|
def index(manifest_file: Path, output: Path) -> None:
|
|
19
19
|
"""Write manifest data to a Parquet sink for querying."""
|
|
20
|
-
from genblaze_core.models.manifest import
|
|
20
|
+
from genblaze_core.models.manifest import parse_manifest
|
|
21
21
|
from genblaze_core.sinks.parquet import ParquetSink
|
|
22
22
|
|
|
23
23
|
try:
|
|
24
24
|
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
25
|
-
manifest =
|
|
25
|
+
manifest = parse_manifest(data)
|
|
26
26
|
except Exception as exc:
|
|
27
27
|
raise click.ClickException(f"Failed to load manifest: {exc}") from exc
|
|
28
28
|
|
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
import click
|
|
7
|
-
from genblaze_core.models.enums import PromptVisibility
|
|
8
|
+
from genblaze_core.models.enums import PromptVisibility, RunStatus
|
|
8
9
|
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from genblaze_core.providers.base import BaseProvider
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
|
|
14
|
+
def _load_provider(provider_name: str, allowed: tuple[str, ...] | None) -> "BaseProvider":
|
|
11
15
|
"""Load a provider by name using entry point discovery.
|
|
12
16
|
|
|
13
17
|
If allowed is set, reject providers not in the allowlist.
|
|
@@ -77,7 +81,7 @@ _UNREPLAYABLE_FIELDS = [
|
|
|
77
81
|
|
|
78
82
|
|
|
79
83
|
@click.command()
|
|
80
|
-
@click.argument("manifest_file", type=click.Path(exists=True, path_type=Path))
|
|
84
|
+
@click.argument("manifest_file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
81
85
|
@click.option("--dry-run/--no-dry-run", default=True, help="Preview without executing.")
|
|
82
86
|
@click.option(
|
|
83
87
|
"--allow-provider",
|
|
@@ -99,7 +103,6 @@ def replay(
|
|
|
99
103
|
) -> None:
|
|
100
104
|
"""Re-execute a pipeline from a manifest JSON file."""
|
|
101
105
|
from genblaze_core.models.manifest import parse_manifest
|
|
102
|
-
from genblaze_core.pipeline import Pipeline
|
|
103
106
|
|
|
104
107
|
try:
|
|
105
108
|
data = json.loads(manifest_file.read_text(encoding="utf-8"))
|
|
@@ -107,14 +110,25 @@ def replay(
|
|
|
107
110
|
except Exception as exc:
|
|
108
111
|
raise click.ClickException(f"Failed to load manifest: {exc}") from exc
|
|
109
112
|
|
|
110
|
-
# Verify manifest hash integrity
|
|
111
|
-
|
|
113
|
+
# Verify manifest hash integrity before replay. Output sha256 coverage is
|
|
114
|
+
# reported separately because URL-only outputs can be metadata-valid but
|
|
115
|
+
# not sha256-covered.
|
|
116
|
+
report = manifest.verification_report()
|
|
117
|
+
if not force and not report.hash_ok:
|
|
112
118
|
click.echo(
|
|
113
119
|
"WARNING: Manifest hash does not match content. The file may have been modified.",
|
|
114
120
|
err=True,
|
|
115
121
|
)
|
|
116
122
|
if not click.confirm("Continue anyway?"):
|
|
117
123
|
raise click.Abort()
|
|
124
|
+
elif not force:
|
|
125
|
+
if report.unverified_sha256_ids:
|
|
126
|
+
click.echo(
|
|
127
|
+
"WARNING: Manifest hash matches, but output asset bytes are not "
|
|
128
|
+
f"bound for {len(report.unverified_sha256_ids)} asset(s) "
|
|
129
|
+
"missing or malformed sha256.",
|
|
130
|
+
err=True,
|
|
131
|
+
)
|
|
118
132
|
|
|
119
133
|
run = manifest.run
|
|
120
134
|
_print_summary(run, show_prompts=show_prompts)
|
|
@@ -132,9 +146,13 @@ def replay(
|
|
|
132
146
|
|
|
133
147
|
# Safety: when no allowlist is provided, confirm each unique provider.
|
|
134
148
|
# A hostile manifest could reference any installed provider and execute
|
|
135
|
-
# paid API calls on the user's credentials without consent.
|
|
149
|
+
# paid API calls on the user's credentials without consent. Steps with no
|
|
150
|
+
# provider (INGEST/IMPORT, see the loop below) don't invoke one, so they're
|
|
151
|
+
# excluded here rather than flowing `None` into sorted() (issue #43).
|
|
136
152
|
if allowed is None:
|
|
137
|
-
unique_providers = sorted(
|
|
153
|
+
unique_providers = sorted(
|
|
154
|
+
{step.provider for step in run.steps if step.provider is not None}
|
|
155
|
+
)
|
|
138
156
|
click.echo(
|
|
139
157
|
"No --allow-provider allowlist set. Confirm each provider before execution:",
|
|
140
158
|
err=True,
|
|
@@ -153,7 +171,9 @@ def replay(
|
|
|
153
171
|
click.echo(" Detected chain mode from manifest step inputs.", err=True)
|
|
154
172
|
|
|
155
173
|
# Cache providers by name to avoid re-instantiating
|
|
156
|
-
|
|
174
|
+
from genblaze_core.pipeline import Pipeline
|
|
175
|
+
|
|
176
|
+
providers: dict[str, BaseProvider] = {}
|
|
157
177
|
pipe = Pipeline(
|
|
158
178
|
run.name,
|
|
159
179
|
tenant_id=run.tenant_id,
|
|
@@ -165,6 +185,17 @@ def replay(
|
|
|
165
185
|
_reserved = {"model", "prompt", "modality", "step_type", "seed", "negative_prompt"}
|
|
166
186
|
|
|
167
187
|
for step_idx, step in enumerate(run.steps):
|
|
188
|
+
# A None provider (only valid for INGEST/IMPORT steps, see Step's
|
|
189
|
+
# own validator) has no upstream service to re-invoke here — replay
|
|
190
|
+
# only supports Pipeline.step()-shaped generative steps. Without this
|
|
191
|
+
# guard, None flowed into a dict[str, ...] key and _load_provider's
|
|
192
|
+
# str parameter, raising a confusing TypeError instead (issue #43).
|
|
193
|
+
if step.provider is None:
|
|
194
|
+
raise click.ClickException(
|
|
195
|
+
f"Step {step_idx + 1} (step_type={step.step_type}) has no provider; "
|
|
196
|
+
"replay does not support re-executing provider-less steps "
|
|
197
|
+
"(e.g. INGEST/IMPORT)."
|
|
198
|
+
)
|
|
168
199
|
if step.provider not in providers:
|
|
169
200
|
providers[step.provider] = _load_provider(step.provider, allowed)
|
|
170
201
|
|
|
@@ -202,10 +233,17 @@ def replay(
|
|
|
202
233
|
)
|
|
203
234
|
|
|
204
235
|
try:
|
|
205
|
-
result = pipe.run()
|
|
236
|
+
result = pipe.run(raise_on_failure=False)
|
|
206
237
|
click.echo()
|
|
238
|
+
if result.run.status in {RunStatus.FAILED, RunStatus.CANCELLED}:
|
|
239
|
+
click.echo(f"Replay failed. New run ID: {result.run.run_id}", err=True)
|
|
240
|
+
click.echo(f"Hash: {result.manifest.canonical_hash}", err=True)
|
|
241
|
+
click.echo(f"Status: {result.run.status}", err=True)
|
|
242
|
+
raise click.exceptions.Exit(1)
|
|
207
243
|
click.echo(f"Replay complete. New run ID: {result.run.run_id}")
|
|
208
244
|
click.echo(f"Hash: {result.manifest.canonical_hash}")
|
|
209
245
|
click.echo(f"Status: {result.run.status}")
|
|
246
|
+
except click.exceptions.Exit:
|
|
247
|
+
raise
|
|
210
248
|
except Exception as exc:
|
|
211
249
|
raise click.ClickException(f"Replay failed: {exc}") from exc
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Verify command — check manifest hash and output sha256 coverage."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from genblaze_cli.manifest_io import extract_manifest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.command()
|
|
11
|
+
@click.argument("file", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
|
12
|
+
@click.option(
|
|
13
|
+
"--hash-only",
|
|
14
|
+
is_flag=True,
|
|
15
|
+
help="Only verify canonical_hash; skip output sha256 and asset-metadata checks.",
|
|
16
|
+
)
|
|
17
|
+
def verify(file: Path, hash_only: bool) -> None:
|
|
18
|
+
"""Verify an embedded, sidecar, or standalone genblaze manifest."""
|
|
19
|
+
try:
|
|
20
|
+
manifest = extract_manifest(file)
|
|
21
|
+
report = manifest.verification_report()
|
|
22
|
+
if not report.hash_ok:
|
|
23
|
+
click.echo("FAIL: manifest hash mismatch.", err=True)
|
|
24
|
+
raise click.exceptions.Exit(1)
|
|
25
|
+
if hash_only:
|
|
26
|
+
click.echo("OK: manifest hash verified. Asset bytes were not fetched or compared.")
|
|
27
|
+
return
|
|
28
|
+
if report.unverified_sha256_ids:
|
|
29
|
+
click.echo(
|
|
30
|
+
f"FAIL: {len(report.unverified_sha256_ids)} output asset(s) "
|
|
31
|
+
"missing or malformed sha256.",
|
|
32
|
+
err=True,
|
|
33
|
+
)
|
|
34
|
+
raise click.exceptions.Exit(1)
|
|
35
|
+
# Out-of-spec numeric/media_type metadata (e.g. width=0) is tolerated on
|
|
36
|
+
# load by parse_manifest() but fails verify() (#149); surface it here so
|
|
37
|
+
# the CLI verdict matches Manifest.verify()/report.ok instead of a stale
|
|
38
|
+
# "OK" that only checked sha256.
|
|
39
|
+
if report.invalid_metadata_ids:
|
|
40
|
+
click.echo(
|
|
41
|
+
f"FAIL: {len(report.invalid_metadata_ids)} output asset(s) "
|
|
42
|
+
"carry out-of-spec numeric/media_type metadata "
|
|
43
|
+
"(e.g. width=0, or a malformed media_type).",
|
|
44
|
+
err=True,
|
|
45
|
+
)
|
|
46
|
+
raise click.exceptions.Exit(1)
|
|
47
|
+
click.echo(
|
|
48
|
+
"OK: manifest hash verified; all output assets declare sha256 and "
|
|
49
|
+
"carry in-spec metadata. Asset bytes were not fetched or compared."
|
|
50
|
+
)
|
|
51
|
+
except click.exceptions.Exit:
|
|
52
|
+
raise
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
# Prefix with exception type so "PermissionError: ..." and
|
|
55
|
+
# "EmbeddingError: ..." are distinguishable at a glance.
|
|
56
|
+
raise click.ClickException(f"{type(exc).__name__}: {exc}") from exc
|
|
@@ -9,7 +9,7 @@ from genblaze_cli.commands.verify import verify
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@click.group()
|
|
12
|
-
@click.version_option(package_name="genblaze-cli")
|
|
12
|
+
@click.version_option(package_name="genblaze-cli", prog_name="genblaze-cli")
|
|
13
13
|
def cli() -> None:
|
|
14
14
|
"""genblaze — media generation manifest toolkit."""
|
|
15
15
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Shared CLI manifest loading helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from genblaze_core._utils import MAX_MANIFEST_BYTES
|
|
9
|
+
from genblaze_core.exceptions import EmbeddingError
|
|
10
|
+
from genblaze_core.media import get_handler, guess_mime
|
|
11
|
+
from genblaze_core.media.sidecar import PointerSidecarError, SidecarHandler
|
|
12
|
+
from genblaze_core.models.manifest import Manifest, parse_manifest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_standalone_json_manifest(file: Path) -> Manifest:
|
|
16
|
+
size = file.stat().st_size
|
|
17
|
+
if size > MAX_MANIFEST_BYTES:
|
|
18
|
+
raise EmbeddingError(
|
|
19
|
+
f"Manifest JSON exceeds size limit: {size} > {MAX_MANIFEST_BYTES} bytes"
|
|
20
|
+
)
|
|
21
|
+
data = json.loads(file.read_text(encoding="utf-8"))
|
|
22
|
+
if "run" not in data and "manifest_uri" in data:
|
|
23
|
+
raise PointerSidecarError(
|
|
24
|
+
manifest_uri=data["manifest_uri"],
|
|
25
|
+
canonical_hash=data.get("canonical_hash", ""),
|
|
26
|
+
)
|
|
27
|
+
return parse_manifest(data)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def extract_manifest(file: Path) -> Manifest:
|
|
31
|
+
"""Extract a manifest from standalone JSON, media, or sidecar files."""
|
|
32
|
+
if file.suffix.lower() == ".json":
|
|
33
|
+
return load_standalone_json_manifest(file)
|
|
34
|
+
|
|
35
|
+
mime = guess_mime(file)
|
|
36
|
+
handler = get_handler(mime)
|
|
37
|
+
if handler is not None:
|
|
38
|
+
try:
|
|
39
|
+
return handler.extract(file)
|
|
40
|
+
except EmbeddingError:
|
|
41
|
+
pass
|
|
42
|
+
return SidecarHandler().extract(file)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "genblaze-cli"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.4"
|
|
8
8
|
description = "CLI for genblaze manifest operations"
|
|
9
9
|
authors = [{name = "Jeronimo De Leon", email = "jdeleon@backblaze.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
keywords = ["genblaze", "ai", "media", "manifest", "provenance", "c2pa-ready", "genai", "pipeline", "cli", "command-line"]
|
|
24
24
|
|
|
25
25
|
dependencies = [
|
|
26
|
-
"genblaze-core>=0.
|
|
26
|
+
"genblaze-core>=0.3.6,<0.4",
|
|
27
27
|
"click>=8.0",
|
|
28
28
|
]
|
|
29
29
|
|
|
@@ -48,3 +48,8 @@ packages = ["genblaze_cli"]
|
|
|
48
48
|
|
|
49
49
|
[tool.pytest.ini_options]
|
|
50
50
|
testpaths = ["tests"]
|
|
51
|
+
|
|
52
|
+
[tool.deptry]
|
|
53
|
+
# Treat the `dev` extra as dev-only tooling so deptry does not flag pytest
|
|
54
|
+
# (and other test-only deps) as unused declared deps (DEP002).
|
|
55
|
+
optional_dependencies_dev_groups = ["dev"]
|