genblaze-core 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.
Files changed (70) hide show
  1. genblaze_core/__init__.py +110 -0
  2. genblaze_core/_utils.py +158 -0
  3. genblaze_core/_version.py +1 -0
  4. genblaze_core/agents/__init__.py +20 -0
  5. genblaze_core/agents/evaluator.py +100 -0
  6. genblaze_core/agents/loop.py +307 -0
  7. genblaze_core/builders/__init__.py +6 -0
  8. genblaze_core/builders/run_builder.py +55 -0
  9. genblaze_core/builders/step_builder.py +67 -0
  10. genblaze_core/canonical/__init__.py +5 -0
  11. genblaze_core/canonical/_normalize.py +59 -0
  12. genblaze_core/canonical/json.py +23 -0
  13. genblaze_core/exceptions.py +44 -0
  14. genblaze_core/media/__init__.py +83 -0
  15. genblaze_core/media/aac.py +97 -0
  16. genblaze_core/media/base.py +66 -0
  17. genblaze_core/media/embedder.py +162 -0
  18. genblaze_core/media/flac.py +90 -0
  19. genblaze_core/media/jpeg.py +110 -0
  20. genblaze_core/media/mp3.py +82 -0
  21. genblaze_core/media/mp4.py +353 -0
  22. genblaze_core/media/png.py +71 -0
  23. genblaze_core/media/sidecar.py +124 -0
  24. genblaze_core/media/wav.py +173 -0
  25. genblaze_core/media/webp.py +78 -0
  26. genblaze_core/models/__init__.py +35 -0
  27. genblaze_core/models/asset.py +100 -0
  28. genblaze_core/models/enums.py +59 -0
  29. genblaze_core/models/manifest.py +300 -0
  30. genblaze_core/models/policy.py +16 -0
  31. genblaze_core/models/prompt_template.py +42 -0
  32. genblaze_core/models/run.py +41 -0
  33. genblaze_core/models/step.py +70 -0
  34. genblaze_core/observability/__init__.py +24 -0
  35. genblaze_core/observability/events.py +94 -0
  36. genblaze_core/observability/logger.py +59 -0
  37. genblaze_core/observability/span.py +70 -0
  38. genblaze_core/observability/tracer.py +297 -0
  39. genblaze_core/pipeline/__init__.py +7 -0
  40. genblaze_core/pipeline/cache.py +104 -0
  41. genblaze_core/pipeline/moderation.py +82 -0
  42. genblaze_core/pipeline/pipeline.py +1422 -0
  43. genblaze_core/pipeline/result.py +112 -0
  44. genblaze_core/pipeline/streaming.py +126 -0
  45. genblaze_core/pipeline/template.py +176 -0
  46. genblaze_core/providers/__init__.py +17 -0
  47. genblaze_core/providers/_ffmpeg_utils.py +112 -0
  48. genblaze_core/providers/base.py +635 -0
  49. genblaze_core/providers/compositor.py +147 -0
  50. genblaze_core/providers/progress.py +32 -0
  51. genblaze_core/providers/registry.py +32 -0
  52. genblaze_core/providers/transform.py +376 -0
  53. genblaze_core/py.typed +0 -0
  54. genblaze_core/runnable/__init__.py +6 -0
  55. genblaze_core/runnable/base.py +33 -0
  56. genblaze_core/runnable/config.py +24 -0
  57. genblaze_core/sinks/__init__.py +6 -0
  58. genblaze_core/sinks/base.py +43 -0
  59. genblaze_core/sinks/parquet.py +190 -0
  60. genblaze_core/storage/__init__.py +19 -0
  61. genblaze_core/storage/base.py +164 -0
  62. genblaze_core/storage/sink.py +262 -0
  63. genblaze_core/storage/transfer.py +549 -0
  64. genblaze_core/testing.py +338 -0
  65. genblaze_core/webhooks/__init__.py +6 -0
  66. genblaze_core/webhooks/notifier.py +294 -0
  67. genblaze_core/webhooks/sink.py +49 -0
  68. genblaze_core-0.1.0.dist-info/METADATA +167 -0
  69. genblaze_core-0.1.0.dist-info/RECORD +70 -0
  70. genblaze_core-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,110 @@
1
+ """genblaze-core — orchestration framework for media generation."""
2
+
3
+ from genblaze_core._version import __version__
4
+
5
+ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
6
+ # pipeline (primary entry point)
7
+ "Pipeline": ("genblaze_core.pipeline.pipeline", "Pipeline"),
8
+ "PipelineResult": ("genblaze_core.pipeline.result", "PipelineResult"),
9
+ "StepCompleteEvent": ("genblaze_core.pipeline.result", "StepCompleteEvent"),
10
+ # pipeline cache
11
+ "StepCache": ("genblaze_core.pipeline.cache", "StepCache"),
12
+ # pipeline templates
13
+ "PipelineTemplate": ("genblaze_core.pipeline.template", "PipelineTemplate"),
14
+ "StepTemplate": ("genblaze_core.pipeline.template", "StepTemplate"),
15
+ # moderation
16
+ "ModerationHook": ("genblaze_core.pipeline.moderation", "ModerationHook"),
17
+ "ModerationResult": ("genblaze_core.pipeline.moderation", "ModerationResult"),
18
+ # builders
19
+ "RunBuilder": ("genblaze_core.builders.run_builder", "RunBuilder"),
20
+ "StepBuilder": ("genblaze_core.builders.step_builder", "StepBuilder"),
21
+ # providers
22
+ "BaseProvider": ("genblaze_core.providers.base", "BaseProvider"),
23
+ "ProviderCapabilities": ("genblaze_core.providers.base", "ProviderCapabilities"),
24
+ "SubmitResult": ("genblaze_core.providers.base", "SubmitResult"),
25
+ "SyncProvider": ("genblaze_core.providers.base", "SyncProvider"),
26
+ "validate_asset_url": ("genblaze_core.providers.base", "validate_asset_url"),
27
+ "validate_chain_input_url": ("genblaze_core.providers.base", "validate_chain_input_url"),
28
+ "FFmpegCompositor": ("genblaze_core.providers.compositor", "FFmpegCompositor"),
29
+ "FFmpegTransform": ("genblaze_core.providers.transform", "FFmpegTransform"),
30
+ "ProgressEvent": ("genblaze_core.providers.progress", "ProgressEvent"),
31
+ # observability
32
+ "StreamEvent": ("genblaze_core.observability.events", "StreamEvent"),
33
+ "Tracer": ("genblaze_core.observability.tracer", "Tracer"),
34
+ "NoOpTracer": ("genblaze_core.observability.tracer", "NoOpTracer"),
35
+ "LoggingTracer": ("genblaze_core.observability.tracer", "LoggingTracer"),
36
+ "OTelTracer": ("genblaze_core.observability.tracer", "OTelTracer"),
37
+ "CompositeTracer": ("genblaze_core.observability.tracer", "CompositeTracer"),
38
+ "StructuredLogger": ("genblaze_core.observability.logger", "StructuredLogger"),
39
+ # agents
40
+ "AgentLoop": ("genblaze_core.agents.loop", "AgentLoop"),
41
+ "AgentContext": ("genblaze_core.agents.loop", "AgentContext"),
42
+ "AgentIteration": ("genblaze_core.agents.loop", "AgentIteration"),
43
+ "AgentResult": ("genblaze_core.agents.loop", "AgentResult"),
44
+ "Evaluator": ("genblaze_core.agents.evaluator", "Evaluator"),
45
+ "EvaluationResult": ("genblaze_core.agents.evaluator", "EvaluationResult"),
46
+ "CallableEvaluator": ("genblaze_core.agents.evaluator", "CallableEvaluator"),
47
+ "ThresholdEvaluator": ("genblaze_core.agents.evaluator", "ThresholdEvaluator"),
48
+ # models
49
+ "Manifest": ("genblaze_core.models.manifest", "Manifest"),
50
+ "Run": ("genblaze_core.models.run", "Run"),
51
+ "Step": ("genblaze_core.models.step", "Step"),
52
+ "Asset": ("genblaze_core.models.asset", "Asset"),
53
+ "AudioMetadata": ("genblaze_core.models.asset", "AudioMetadata"),
54
+ "Track": ("genblaze_core.models.asset", "Track"),
55
+ "VideoMetadata": ("genblaze_core.models.asset", "VideoMetadata"),
56
+ "WordTiming": ("genblaze_core.models.asset", "WordTiming"),
57
+ # enums
58
+ "Modality": ("genblaze_core.models.enums", "Modality"),
59
+ "StepType": ("genblaze_core.models.enums", "StepType"),
60
+ "RunStatus": ("genblaze_core.models.enums", "RunStatus"),
61
+ "StepStatus": ("genblaze_core.models.enums", "StepStatus"),
62
+ "PromptVisibility": ("genblaze_core.models.enums", "PromptVisibility"),
63
+ "ProviderErrorCode": ("genblaze_core.models.enums", "ProviderErrorCode"),
64
+ "EmbedPolicy": ("genblaze_core.models.policy", "EmbedPolicy"),
65
+ "PromptTemplate": ("genblaze_core.models.prompt_template", "PromptTemplate"),
66
+ # provider discovery
67
+ "discover_providers": ("genblaze_core.providers.registry", "discover_providers"),
68
+ # sinks
69
+ "BaseSink": ("genblaze_core.sinks.base", "BaseSink"),
70
+ "ParquetSink": ("genblaze_core.sinks.parquet", "ParquetSink"),
71
+ # storage
72
+ "StorageBackend": ("genblaze_core.storage.base", "StorageBackend"),
73
+ "ObjectStorageSink": ("genblaze_core.storage.sink", "ObjectStorageSink"),
74
+ "AssetTransfer": ("genblaze_core.storage.transfer", "AssetTransfer"),
75
+ "KeyStrategy": ("genblaze_core.storage.base", "KeyStrategy"),
76
+ "ObjectLockConfig": ("genblaze_core.storage.base", "ObjectLockConfig"),
77
+ # testing
78
+ "MockProvider": ("genblaze_core.testing", "MockProvider"),
79
+ "MockVideoProvider": ("genblaze_core.testing", "MockVideoProvider"),
80
+ "MockAudioProvider": ("genblaze_core.testing", "MockAudioProvider"),
81
+ "ProviderComplianceTests": ("genblaze_core.testing", "ProviderComplianceTests"),
82
+ # exceptions
83
+ "GenblazeError": ("genblaze_core.exceptions", "GenblazeError"),
84
+ "PipelineTimeoutError": ("genblaze_core.exceptions", "PipelineTimeoutError"),
85
+ "EmbeddingError": ("genblaze_core.exceptions", "EmbeddingError"),
86
+ "StorageError": ("genblaze_core.exceptions", "StorageError"),
87
+ "ProviderError": ("genblaze_core.exceptions", "ProviderError"),
88
+ "ManifestError": ("genblaze_core.exceptions", "ManifestError"),
89
+ "SinkError": ("genblaze_core.exceptions", "SinkError"),
90
+ "WebhookError": ("genblaze_core.exceptions", "WebhookError"),
91
+ # webhooks
92
+ "WebhookNotifier": ("genblaze_core.webhooks.notifier", "WebhookNotifier"),
93
+ "WebhookConfig": ("genblaze_core.webhooks.notifier", "WebhookConfig"),
94
+ "WebhookEvent": ("genblaze_core.webhooks.notifier", "WebhookEvent"),
95
+ "WebhookSink": ("genblaze_core.webhooks.sink", "WebhookSink"),
96
+ }
97
+
98
+ __all__ = [*_LAZY_IMPORTS.keys(), "__version__"]
99
+
100
+
101
+ def __getattr__(name: str):
102
+ if name in _LAZY_IMPORTS:
103
+ module_path, attr = _LAZY_IMPORTS[name]
104
+ import importlib
105
+
106
+ mod = importlib.import_module(module_path)
107
+ val = getattr(mod, attr)
108
+ globals()[name] = val
109
+ return val
110
+ raise AttributeError(f"module 'genblaze_core' has no attribute {name!r}")
@@ -0,0 +1,158 @@
1
+ """Internal utilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import hashlib
7
+ import ipaddress
8
+ import random
9
+ import re
10
+ import socket
11
+ import tempfile
12
+ import uuid
13
+ from collections.abc import Coroutine
14
+ from concurrent.futures import ThreadPoolExecutor
15
+ from datetime import UTC, datetime
16
+ from pathlib import Path
17
+ from typing import Any
18
+ from urllib.parse import urlparse as _urlparse
19
+
20
+
21
+ def new_id() -> str:
22
+ """Generate a new UUID4 string."""
23
+ return str(uuid.uuid4())
24
+
25
+
26
+ def utc_now() -> datetime:
27
+ """Return current UTC datetime."""
28
+ return datetime.now(UTC)
29
+
30
+
31
+ def compute_sha256(data: bytes) -> str:
32
+ """Compute SHA-256 hex digest of raw bytes."""
33
+ return hashlib.sha256(data).hexdigest()
34
+
35
+
36
+ def _run_async(coro: Coroutine) -> Any:
37
+ """Run an async coroutine from sync code safely.
38
+
39
+ If there's already a running event loop (e.g. inside Jupyter or an async provider
40
+ called from BaseProvider.invoke), runs in a new thread to avoid RuntimeError.
41
+ Otherwise uses asyncio.run().
42
+ """
43
+ try:
44
+ loop = asyncio.get_running_loop()
45
+ except RuntimeError:
46
+ loop = None
47
+
48
+ if loop is not None and loop.is_running():
49
+ # Already inside an event loop — run in a separate thread
50
+ with ThreadPoolExecutor(max_workers=1) as pool:
51
+ return pool.submit(asyncio.run, coro).result()
52
+ return asyncio.run(coro)
53
+
54
+
55
+ # ---------------------------------------------------------------------------
56
+ # SSRF protection — shared by storage/transfer.py and webhooks/notifier.py
57
+ # ---------------------------------------------------------------------------
58
+
59
+ # Private/reserved IP ranges blocked to prevent SSRF
60
+ BLOCKED_NETWORKS = [
61
+ ipaddress.ip_network("0.0.0.0/8"), # "This host" — resolves to loopback on Linux
62
+ ipaddress.ip_network("10.0.0.0/8"),
63
+ ipaddress.ip_network("172.16.0.0/12"),
64
+ ipaddress.ip_network("192.168.0.0/16"),
65
+ ipaddress.ip_network("127.0.0.0/8"),
66
+ ipaddress.ip_network("169.254.0.0/16"), # Link-local / IMDS
67
+ ipaddress.ip_network("100.64.0.0/10"), # Carrier-grade NAT
68
+ ipaddress.ip_network("::1/128"), # IPv6 loopback
69
+ ipaddress.ip_network("fc00::/7"), # IPv6 unique local
70
+ ipaddress.ip_network("fe80::/10"), # IPv6 link-local
71
+ ]
72
+
73
+
74
+ # Allowed parent directories for file:// inputs (shared by storage + ffmpeg providers)
75
+ # Deduplicate: /tmp and gettempdir() may resolve to the same or different paths
76
+ ALLOWED_FILE_ROOTS: tuple[Path, ...] = tuple(
77
+ {Path(tempfile.gettempdir()).resolve(), Path("/tmp").resolve()} # noqa: S108
78
+ )
79
+
80
+
81
+ def check_ssrf(url: str, *, exc_type: type[Exception] = ValueError) -> None:
82
+ """Reject non-HTTPS URLs and hostnames resolving to private IP ranges.
83
+
84
+ Shared SSRF guard used by storage transfers and webhook dispatch.
85
+ Callers pass their domain-specific exception type via ``exc_type``.
86
+ """
87
+ parsed = _urlparse(url)
88
+ if parsed.scheme not in ("https",):
89
+ raise exc_type(f"Only HTTPS URLs are allowed, got: {parsed.scheme}://")
90
+
91
+ host = parsed.hostname or ""
92
+ if host.lower() == "localhost":
93
+ raise exc_type(f"Private/loopback URLs are not allowed: {host}")
94
+
95
+ try:
96
+ addrinfos = socket.getaddrinfo(host, None, proto=socket.IPPROTO_TCP)
97
+ except socket.gaierror as exc:
98
+ raise exc_type(f"Cannot resolve hostname: {host}") from exc
99
+
100
+ for _, _, _, _, sockaddr in addrinfos:
101
+ try:
102
+ ip = ipaddress.ip_address(str(sockaddr[0]))
103
+ except ValueError:
104
+ continue
105
+ if any(ip in net for net in BLOCKED_NETWORKS):
106
+ raise exc_type(f"Private/loopback URLs are not allowed: {host}")
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # Manifest size cap — bounds the JSON payload accepted from disk/media
111
+ # ---------------------------------------------------------------------------
112
+ # Real manifests are O(KB). 16 MiB is generous and bounds OOM blast from
113
+ # malicious media or sidecars that declare absurd payload sizes.
114
+ MAX_MANIFEST_BYTES = 16 * 1024 * 1024
115
+
116
+
117
+ # ---------------------------------------------------------------------------
118
+ # Credential pattern detection — used by error sanitization (providers/base.py)
119
+ # AND by Pipeline.step build-time rejection of secret-shaped params values.
120
+ # Centralized here so both call sites share one regex of record.
121
+ # ---------------------------------------------------------------------------
122
+ _SECRET_PATTERNS = re.compile(
123
+ r"(r8_[A-Za-z0-9]{20,})" # Replicate tokens
124
+ r"|(sk-ant-[A-Za-z0-9\-]{20,})" # Anthropic API keys (before generic sk-)
125
+ r"|(sk-[A-Za-z0-9]{20,})" # OpenAI-style keys
126
+ r"|(AIza[A-Za-z0-9_\-]{30,})" # Google API keys
127
+ r"|(AKIA[A-Z0-9]{16})" # AWS access key IDs
128
+ r"|(Bearer\s+[A-Za-z0-9._\-]{20,})" # Bearer tokens
129
+ r"|(Token\s+[A-Za-z0-9._\-]{20,})" # Token auth headers
130
+ r"|(\bapi[_-]key[=:]\s*[A-Za-z0-9._\-]{20,})", # api_key=... / api-key:...
131
+ re.IGNORECASE,
132
+ )
133
+
134
+
135
+ def jittered_backoff(attempt: int) -> float:
136
+ """Compute exponential backoff with jitter to avoid thundering herd.
137
+
138
+ Starts at 1s, doubles per attempt, capped at 30s base with up to 25% jitter.
139
+ """
140
+ base = min(2**attempt, 30)
141
+ return base * (1 + random.uniform(0, 0.25)) # noqa: S311 — jitter, not crypto
142
+
143
+
144
+ def probe_audio_duration(path: str | Any) -> float | None:
145
+ """Try to read audio duration from a file using mutagen (optional dep).
146
+
147
+ Returns duration in seconds, or None if mutagen is not installed or
148
+ the file format is not recognized.
149
+ """
150
+ try:
151
+ from mutagen import File as MutagenFile
152
+
153
+ audio = MutagenFile(str(path))
154
+ if audio is not None and audio.info is not None:
155
+ return audio.info.length
156
+ except Exception: # noqa: S110 — mutagen is optional, fail gracefully
157
+ pass
158
+ return None
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,20 @@
1
+ """Agent / reasoning layer — evaluate outputs, refine prompts, retry."""
2
+
3
+ from genblaze_core.agents.evaluator import (
4
+ CallableEvaluator,
5
+ EvaluationResult,
6
+ Evaluator,
7
+ ThresholdEvaluator,
8
+ )
9
+ from genblaze_core.agents.loop import AgentContext, AgentIteration, AgentLoop, AgentResult
10
+
11
+ __all__ = [
12
+ "AgentContext",
13
+ "AgentIteration",
14
+ "AgentLoop",
15
+ "AgentResult",
16
+ "CallableEvaluator",
17
+ "EvaluationResult",
18
+ "Evaluator",
19
+ "ThresholdEvaluator",
20
+ ]
@@ -0,0 +1,100 @@
1
+ """Evaluator — pluggable quality-gate for pipeline outputs.
2
+
3
+ An ``Evaluator`` inspects a :class:`PipelineResult` and returns an
4
+ :class:`EvaluationResult` describing whether the output meets quality
5
+ requirements. Evaluators can be simple threshold checks, LLM judges,
6
+ vision-model classifiers, or composed chains.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ from abc import ABC, abstractmethod
13
+ from dataclasses import dataclass, field
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Callable
18
+
19
+ from genblaze_core.pipeline.result import PipelineResult
20
+
21
+
22
+ @dataclass
23
+ class EvaluationResult:
24
+ """Outcome of evaluating a pipeline result.
25
+
26
+ Attributes:
27
+ passed: Whether the output meets quality requirements.
28
+ score: Optional numeric score (typically 0.0–1.0). None if not applicable.
29
+ feedback: Human-readable explanation to feed back into a refinement step.
30
+ metadata: Arbitrary structured data (e.g. per-dimension scores, detected issues).
31
+ """
32
+
33
+ passed: bool
34
+ score: float | None = None
35
+ feedback: str | None = None
36
+ metadata: dict[str, Any] = field(default_factory=dict)
37
+
38
+
39
+ class Evaluator(ABC):
40
+ """Abstract evaluator. Subclasses implement at least ``evaluate``."""
41
+
42
+ @abstractmethod
43
+ def evaluate(self, result: PipelineResult) -> EvaluationResult:
44
+ """Judge a completed pipeline result. Must return an EvaluationResult."""
45
+
46
+ async def aevaluate(self, result: PipelineResult) -> EvaluationResult:
47
+ """Async variant. Default runs ``evaluate`` in a thread."""
48
+ return await asyncio.to_thread(self.evaluate, result)
49
+
50
+
51
+ class CallableEvaluator(Evaluator):
52
+ """Wraps a plain callable as an :class:`Evaluator`.
53
+
54
+ The callable receives the :class:`PipelineResult` and returns either
55
+ an :class:`EvaluationResult` or a bool (passed/not).
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ fn: Callable[[PipelineResult], EvaluationResult | bool],
61
+ ) -> None:
62
+ self._fn = fn
63
+
64
+ def evaluate(self, result: PipelineResult) -> EvaluationResult:
65
+ out = self._fn(result)
66
+ if isinstance(out, EvaluationResult):
67
+ return out
68
+ return EvaluationResult(passed=bool(out))
69
+
70
+
71
+ class ThresholdEvaluator(Evaluator):
72
+ """Pass/fail based on a numeric score crossing a threshold.
73
+
74
+ ``score_fn`` receives the :class:`PipelineResult` and returns a float.
75
+ Useful for wrapping a vision or text quality model that emits a score.
76
+ """
77
+
78
+ def __init__(
79
+ self,
80
+ score_fn: Callable[[PipelineResult], float],
81
+ threshold: float,
82
+ *,
83
+ higher_is_better: bool = True,
84
+ feedback_fn: Callable[[PipelineResult, float], str] | None = None,
85
+ ) -> None:
86
+ self._score_fn = score_fn
87
+ self._threshold = threshold
88
+ self._higher_is_better = higher_is_better
89
+ self._feedback_fn = feedback_fn
90
+
91
+ def evaluate(self, result: PipelineResult) -> EvaluationResult:
92
+ score = float(self._score_fn(result))
93
+ passed = score >= self._threshold if self._higher_is_better else score <= self._threshold
94
+ feedback = self._feedback_fn(result, score) if self._feedback_fn else None
95
+ return EvaluationResult(
96
+ passed=passed,
97
+ score=score,
98
+ feedback=feedback,
99
+ metadata={"threshold": self._threshold, "higher_is_better": self._higher_is_better},
100
+ )