genblaze-cli 0.2.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.
@@ -0,0 +1 @@
1
+ """genblaze CLI."""
File without changes
@@ -0,0 +1,40 @@
1
+ """Extract command — print embedded manifest from a media file."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from genblaze_core.exceptions import EmbeddingError
7
+ from genblaze_core.media import get_handler, guess_mime
8
+ from genblaze_core.media.sidecar import SidecarHandler
9
+
10
+
11
+ def _extract_manifest(file: Path):
12
+ """Try format-specific handler first, then sidecar fallback."""
13
+ mime = guess_mime(file)
14
+ handler = get_handler(mime)
15
+ if handler is not None:
16
+ try:
17
+ return handler.extract(file)
18
+ except EmbeddingError:
19
+ pass # Expected: no manifest in this format, try sidecar
20
+ # Try sidecar fallback
21
+ sidecar = SidecarHandler()
22
+ return sidecar.extract(file)
23
+
24
+
25
+ @click.command()
26
+ @click.argument("file", type=click.Path(exists=True, path_type=Path))
27
+ @click.option("--format", "fmt", type=click.Choice(["json", "summary"]), default="json")
28
+ def extract(file: Path, fmt: str) -> None:
29
+ """Extract and display the genblaze manifest from a media file."""
30
+ try:
31
+ manifest = _extract_manifest(file)
32
+ if fmt == "json":
33
+ click.echo(manifest.to_canonical_json())
34
+ else:
35
+ click.echo(f"Run ID: {manifest.run.run_id}")
36
+ click.echo(f"Steps: {len(manifest.run.steps)}")
37
+ click.echo(f"Hash: {manifest.canonical_hash}")
38
+ click.echo(f"Verified: {manifest.verify()}")
39
+ except Exception as exc:
40
+ raise click.ClickException(f"{type(exc).__name__}: {exc}") from exc
@@ -0,0 +1,34 @@
1
+ """Index command — write manifest data to a Parquet sink."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import click
7
+
8
+
9
+ @click.command()
10
+ @click.argument("manifest_file", type=click.Path(exists=True, path_type=Path))
11
+ @click.option(
12
+ "--output",
13
+ "-o",
14
+ type=click.Path(path_type=Path),
15
+ default="./genblaze_index",
16
+ help="Base directory for Parquet output.",
17
+ )
18
+ def index(manifest_file: Path, output: Path) -> None:
19
+ """Write manifest data to a Parquet sink for querying."""
20
+ from genblaze_core.models.manifest import Manifest
21
+ from genblaze_core.sinks.parquet import ParquetSink
22
+
23
+ try:
24
+ data = json.loads(manifest_file.read_text(encoding="utf-8"))
25
+ manifest = Manifest.model_validate(data)
26
+ except Exception as exc:
27
+ raise click.ClickException(f"Failed to load manifest: {exc}") from exc
28
+
29
+ try:
30
+ sink = ParquetSink(output)
31
+ sink.write_run(manifest.run, manifest)
32
+ click.echo(f"Indexed run {manifest.run.run_id} to {output}")
33
+ except Exception as exc:
34
+ raise click.ClickException(f"Failed to write Parquet: {exc}") from exc
@@ -0,0 +1,211 @@
1
+ """Replay command — re-execute a pipeline from a manifest file."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import click
7
+ from genblaze_core.models.enums import PromptVisibility
8
+
9
+
10
+ def _load_provider(provider_name: str, allowed: tuple[str, ...] | None):
11
+ """Load a provider by name using entry point discovery.
12
+
13
+ If allowed is set, reject providers not in the allowlist.
14
+ """
15
+ if allowed and provider_name not in allowed:
16
+ raise click.ClickException(
17
+ f"Provider '{provider_name}' not in allowlist: {', '.join(allowed)}"
18
+ )
19
+ from genblaze_core.providers.registry import discover_providers
20
+
21
+ registry = discover_providers()
22
+ cls = registry.get(provider_name)
23
+ if cls is None:
24
+ available = ", ".join(sorted(registry.keys())) or "(none installed)"
25
+ raise click.ClickException(
26
+ f"Unknown provider '{provider_name}'. Installed providers: {available}"
27
+ )
28
+ click.echo(f" Loading provider: {provider_name}", err=True)
29
+ return cls()
30
+
31
+
32
+ def _detect_chain_mode(run) -> bool:
33
+ """Detect if the original pipeline used chain mode by checking step inputs.
34
+
35
+ If any step has _input_from metadata, this is a fan-in pipeline (not chain).
36
+ Otherwise, if every non-first step has inputs, it was likely chain mode.
37
+ """
38
+ has_input_from = any(step.metadata.get("_input_from") is not None for step in run.steps)
39
+ if has_input_from:
40
+ return False
41
+ non_first = run.steps[1:]
42
+ return bool(non_first) and all(len(step.inputs) > 0 for step in non_first)
43
+
44
+
45
+ def _print_summary(run, *, show_prompts: bool) -> None:
46
+ """Print manifest run summary.
47
+
48
+ Prompts are only shown when ``show_prompts`` is True AND the step's
49
+ prompt_visibility is PUBLIC. Non-public prompts are redacted to avoid
50
+ leaking private content to terminals/CI logs when replaying manifests
51
+ pulled from shared storage.
52
+ """
53
+ click.echo(f"Run: {run.run_id}")
54
+ click.echo(f"Name: {run.name or '(unnamed)'}")
55
+ click.echo(f"Steps: {len(run.steps)}")
56
+ click.echo()
57
+
58
+ for i, step in enumerate(run.steps, 1):
59
+ click.echo(f" Step {i}: {step.provider}/{step.model}")
60
+ click.echo(f" Type: {step.step_type}")
61
+ if show_prompts and step.prompt_visibility == PromptVisibility.PUBLIC:
62
+ click.echo(f" Prompt: {step.prompt or '(none)'}")
63
+ else:
64
+ click.echo(
65
+ f" Prompt: [redacted — visibility={step.prompt_visibility};"
66
+ " pass --show-prompts to reveal public prompts]"
67
+ )
68
+ click.echo(f" Modality: {step.modality}")
69
+ if step.params:
70
+ click.echo(f" Params: {step.params}")
71
+
72
+
73
+ # Fields that cannot be faithfully replayed (logged as warnings)
74
+ _UNREPLAYABLE_FIELDS = [
75
+ ("prompt_visibility", lambda s: s.prompt_visibility != "public"),
76
+ ]
77
+
78
+
79
+ @click.command()
80
+ @click.argument("manifest_file", type=click.Path(exists=True, path_type=Path))
81
+ @click.option("--dry-run/--no-dry-run", default=True, help="Preview without executing.")
82
+ @click.option(
83
+ "--allow-provider",
84
+ multiple=True,
85
+ help="Only allow these providers (can be specified multiple times).",
86
+ )
87
+ @click.option(
88
+ "--show-prompts",
89
+ is_flag=True,
90
+ help="Show public-visibility prompts in the summary. Non-public prompts are always redacted.",
91
+ )
92
+ @click.option("--force", is_flag=True, help="Skip manifest hash verification.")
93
+ def replay(
94
+ manifest_file: Path,
95
+ dry_run: bool,
96
+ allow_provider: tuple[str, ...],
97
+ show_prompts: bool,
98
+ force: bool,
99
+ ) -> None:
100
+ """Re-execute a pipeline from a manifest JSON file."""
101
+ from genblaze_core.models.manifest import parse_manifest
102
+ from genblaze_core.pipeline import Pipeline
103
+
104
+ try:
105
+ data = json.loads(manifest_file.read_text(encoding="utf-8"))
106
+ manifest = parse_manifest(data)
107
+ except Exception as exc:
108
+ raise click.ClickException(f"Failed to load manifest: {exc}") from exc
109
+
110
+ # Verify manifest hash integrity
111
+ if not force and not manifest.verify():
112
+ click.echo(
113
+ "WARNING: Manifest hash does not match content. The file may have been modified.",
114
+ err=True,
115
+ )
116
+ if not click.confirm("Continue anyway?"):
117
+ raise click.Abort()
118
+
119
+ run = manifest.run
120
+ _print_summary(run, show_prompts=show_prompts)
121
+
122
+ if dry_run:
123
+ click.echo()
124
+ click.echo("Dry run — no steps executed. Use --no-dry-run to execute.")
125
+ return
126
+
127
+ # Build and execute pipeline from manifest steps
128
+ click.echo()
129
+ click.echo("Replaying pipeline...")
130
+
131
+ allowed: tuple[str, ...] | None = allow_provider if allow_provider else None
132
+
133
+ # Safety: when no allowlist is provided, confirm each unique provider.
134
+ # A hostile manifest could reference any installed provider and execute
135
+ # paid API calls on the user's credentials without consent.
136
+ if allowed is None:
137
+ unique_providers = sorted({step.provider for step in run.steps})
138
+ click.echo(
139
+ "No --allow-provider allowlist set. Confirm each provider before execution:",
140
+ err=True,
141
+ )
142
+ confirmed: list[str] = []
143
+ for name in unique_providers:
144
+ if click.confirm(f" Execute with provider '{name}'?", default=False):
145
+ confirmed.append(name)
146
+ else:
147
+ raise click.Abort()
148
+ allowed = tuple(confirmed)
149
+
150
+ # Detect chain mode from step inputs in the manifest
151
+ chain = _detect_chain_mode(run)
152
+ if chain:
153
+ click.echo(" Detected chain mode from manifest step inputs.", err=True)
154
+
155
+ # Cache providers by name to avoid re-instantiating
156
+ providers: dict[str, object] = {}
157
+ pipe = Pipeline(
158
+ run.name,
159
+ tenant_id=run.tenant_id,
160
+ project_id=run.project_id,
161
+ chain=chain,
162
+ )
163
+
164
+ # Reserved kwargs that Pipeline.step() accepts explicitly
165
+ _reserved = {"model", "prompt", "modality", "step_type", "seed", "negative_prompt"}
166
+
167
+ for step_idx, step in enumerate(run.steps):
168
+ if step.provider not in providers:
169
+ providers[step.provider] = _load_provider(step.provider, allowed)
170
+
171
+ # Warn about fields that can't be faithfully replayed
172
+ for field_name, has_value in _UNREPLAYABLE_FIELDS:
173
+ if has_value(step):
174
+ click.echo(
175
+ f" WARNING: Step {step_idx + 1} has {field_name}={getattr(step, field_name)}"
176
+ f" which may not replay identically.",
177
+ err=True,
178
+ )
179
+
180
+ # Strip reserved keys from params to avoid TypeError on collision
181
+ extra_params = {k: v for k, v in step.params.items() if k not in _reserved}
182
+
183
+ # Restore seed and negative_prompt if present
184
+ if step.seed is not None:
185
+ extra_params["seed"] = step.seed
186
+ if step.negative_prompt is not None:
187
+ extra_params["negative_prompt"] = step.negative_prompt
188
+
189
+ # Restore fallback_models and input_from from step metadata
190
+ fallback_models = step.metadata.get("_fallback_models")
191
+ input_from = step.metadata.get("_input_from")
192
+
193
+ pipe.step(
194
+ providers[step.provider],
195
+ model=step.model,
196
+ prompt=step.prompt,
197
+ modality=step.modality,
198
+ step_type=step.step_type,
199
+ fallback_models=fallback_models,
200
+ input_from=input_from,
201
+ **extra_params,
202
+ )
203
+
204
+ try:
205
+ result = pipe.run()
206
+ click.echo()
207
+ click.echo(f"Replay complete. New run ID: {result.run.run_id}")
208
+ click.echo(f"Hash: {result.manifest.canonical_hash}")
209
+ click.echo(f"Status: {result.run.status}")
210
+ except Exception as exc:
211
+ raise click.ClickException(f"Replay failed: {exc}") from exc
@@ -0,0 +1,41 @@
1
+ """Verify command — check manifest hash integrity."""
2
+
3
+ from pathlib import Path
4
+
5
+ import click
6
+ from genblaze_core.exceptions import EmbeddingError
7
+ from genblaze_core.media import get_handler, guess_mime
8
+ from genblaze_core.media.sidecar import SidecarHandler
9
+
10
+
11
+ def _verify_manifest(file: Path) -> bool:
12
+ """Extract and verify manifest, trying format-specific handler then sidecar."""
13
+ mime = guess_mime(file)
14
+ handler = get_handler(mime)
15
+ if handler is not None:
16
+ try:
17
+ return handler.verify(file)
18
+ except EmbeddingError:
19
+ pass # Expected: no manifest in this format, try sidecar
20
+ # Try sidecar fallback
21
+ sidecar = SidecarHandler()
22
+ return sidecar.verify(file)
23
+
24
+
25
+ @click.command()
26
+ @click.argument("file", type=click.Path(exists=True, path_type=Path))
27
+ def verify(file: Path) -> None:
28
+ """Verify the integrity of a genblaze manifest in a media file."""
29
+ try:
30
+ valid = _verify_manifest(file)
31
+ if valid:
32
+ click.echo("OK — manifest hash verified.")
33
+ else:
34
+ click.echo("FAIL — manifest hash mismatch.", err=True)
35
+ raise click.exceptions.Exit(1)
36
+ except click.exceptions.Exit:
37
+ raise
38
+ except Exception as exc:
39
+ # Prefix with exception type so "PermissionError: ..." and
40
+ # "EmbeddingError: ..." are distinguishable at a glance.
41
+ raise click.ClickException(f"{type(exc).__name__}: {exc}") from exc
genblaze_cli/main.py ADDED
@@ -0,0 +1,20 @@
1
+ """CLI entry point."""
2
+
3
+ import click
4
+
5
+ from genblaze_cli.commands.extract import extract
6
+ from genblaze_cli.commands.index import index
7
+ from genblaze_cli.commands.replay import replay
8
+ from genblaze_cli.commands.verify import verify
9
+
10
+
11
+ @click.group()
12
+ @click.version_option(package_name="genblaze-cli")
13
+ def cli() -> None:
14
+ """genblaze — media generation manifest toolkit."""
15
+
16
+
17
+ cli.add_command(extract)
18
+ cli.add_command(index)
19
+ cli.add_command(replay)
20
+ cli.add_command(verify)
genblaze_cli/py.typed ADDED
File without changes
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.4
2
+ Name: genblaze-cli
3
+ Version: 0.2.0
4
+ Summary: CLI for genblaze manifest operations
5
+ Project-URL: Homepage, https://github.com/backblaze-labs/genblaze
6
+ Project-URL: Repository, https://github.com/backblaze-labs/genblaze
7
+ Project-URL: Changelog, https://github.com/backblaze-labs/genblaze/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/backblaze-labs/genblaze/issues
9
+ Author-email: Jeronimo De Leon <jdeleon@backblaze.com>
10
+ License-Expression: MIT
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Requires-Python: >=3.11
15
+ Requires-Dist: click>=8.0
16
+ Requires-Dist: genblaze-core<0.3,>=0.2.0
17
+ Provides-Extra: dev
18
+ Requires-Dist: pillow>=10.0; extra == 'dev'
19
+ Requires-Dist: pytest>=7.0; extra == 'dev'
20
+ Description-Content-Type: text/markdown
21
+
22
+ <!-- last_verified: 2026-04-22 -->
23
+ # genblaze-cli
24
+
25
+ **Command-line toolkit for inspecting, verifying, and indexing genblaze AI-generated-media provenance manifests.**
26
+
27
+ `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 SHA-256 hash, replay a run, or index manifests into Parquet for downstream analytics.
28
+
29
+ ## Why genblaze-cli
30
+
31
+ - **Audit AI-generated media in one command** — `genblaze verify video.mp4` confirms a file's manifest hash hasn't been tampered with.
32
+ - **Works on any genblaze output** — PNG, JPEG, WebP, MP4, MP3, WAV with embedded manifests.
33
+ - **Analytics-ready** — `genblaze index` emits partitioned Parquet tables (runs, steps, assets) for BI tools and data warehouses.
34
+ - **Zero provider dependencies** — reads manifests; doesn't call any AI API.
35
+ - **Shell-friendly** — non-zero exit codes on verification failure, pipeable JSON output.
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install genblaze-cli
41
+ ```
42
+
43
+ Installs the `genblaze` console script.
44
+
45
+ ## Usage
46
+
47
+ ```bash
48
+ genblaze --help
49
+
50
+ genblaze extract video.mp4 # Extract embedded manifest → stdout (JSON)
51
+ genblaze extract video.mp4 -o m.json # …or to a file
52
+
53
+ genblaze verify video.mp4 # Verify the embedded manifest's SHA-256
54
+ genblaze verify manifest.json # Or verify a standalone manifest file
55
+
56
+ genblaze replay manifest.json # Show what a replay would do (dry run)
57
+
58
+ genblaze index manifest.json -o data/ # Index into partitioned Parquet tables
59
+ ```
60
+
61
+ Exit codes are non-zero on verification failure — safe to drop into CI pipelines, release checks, or content-moderation workflows.
62
+
63
+ ## Typical flow
64
+
65
+ ```bash
66
+ # 1. Someone ships you an AI-generated video
67
+ genblaze extract delivered-ad.mp4 -o manifest.json
68
+
69
+ # 2. Confirm it hasn't been tampered with
70
+ genblaze verify delivered-ad.mp4
71
+
72
+ # 3. Index into your analytics warehouse
73
+ genblaze index manifest.json -o s3://analytics/genblaze/
74
+ ```
75
+
76
+ ## Documentation
77
+
78
+ - **Main repo**: https://github.com/backblaze-labs/genblaze
79
+ - **CLI reference**: https://github.com/backblaze-labs/genblaze/tree/main/cli
80
+
81
+ ## Related packages
82
+
83
+ - [`genblaze-core`](https://pypi.org/project/genblaze-core/) — the pipeline SDK that produces these manifests
84
+ - [`genblaze-s3`](https://pypi.org/project/genblaze-s3/) — durable storage on [Backblaze B2](https://www.backblaze.com/cloud-storage?utm_source=github&utm_medium=referral&utm_campaign=ai_artifacts&utm_content=genblaze), AWS S3, Cloudflare R2, MinIO
85
+
86
+ ## License
87
+
88
+ MIT
@@ -0,0 +1,12 @@
1
+ genblaze_cli/__init__.py,sha256=bugOiMl926GX70kdp9IhfgjC4Wa7nJ1Q0ej3OXPp0oQ,20
2
+ genblaze_cli/main.py,sha256=7rqLrPhC5oNAftoyOzGIQM-xyuSZ7Ca34gmPUYIk724,473
3
+ genblaze_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ genblaze_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ genblaze_cli/commands/extract.py,sha256=Wp_LJHMtwXNysB4aWA8_eE5F2K0m6mXjRkvhPxvsaB0,1499
6
+ genblaze_cli/commands/index.py,sha256=wyfSha0oXWkv89TNVzj9rXboCZRU0dS3IQv2zfyE8Tk,1104
7
+ genblaze_cli/commands/replay.py,sha256=E2ujYBfyN7Z2CFBoJlqYZ6v7rCvu5lM5bx7Ycg9eXdg,7779
8
+ genblaze_cli/commands/verify.py,sha256=qLi68LFevBL5Yj5LoeWr60F0TfR2bFpgl1gbc5Mvk5I,1445
9
+ genblaze_cli-0.2.0.dist-info/METADATA,sha256=XAsvE5WqGWLEI-mhB9DcMtjqFomLEWMjbss6gso0J7k,3580
10
+ genblaze_cli-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ genblaze_cli-0.2.0.dist-info/entry_points.txt,sha256=BhMtCFvHqDssP-lleKOdxIBzN3oVqWjDGr45uP9tzzQ,51
12
+ genblaze_cli-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ genblaze = genblaze_cli.main:cli