Spaces:
Running
Running
Download _utils/_audio_generation.py from scikit-plots/ai: direct link, hf CLI and curl.
- Browser
- Download file 17 kB
-
https://huggingface.co/spaces/scikit-plots/ai/resolve/main/_utils/_audio_generation.py
- Command line
-
hf download hf://spaces/scikit-plots/ai/_utils/_audio_generation.py
-
curl -L -o _audio_generation.py https://huggingface.co/spaces/scikit-plots/ai/resolve/main/_utils/_audio_generation.py
17 kB
| # scikitplot/_externals/_sphinx_ext/_sphinx_ai_assistant/_hf_spaces_proxy/_utils/_audio_generation.py | |
| # | |
| # flake8: noqa: D213 | |
| # | |
| # Authors: The scikit-plots developers | |
| # SPDX-License-Identifier: BSD-3-Clause | |
| """ | |
| Server-owned asynchronous Audio generation over provider-artifact executors. | |
| The browser requests a workflow, never a renderer/provider/upstream. This | |
| module deliberately reuses ``ProviderArtifactOutputRegistry`` as the binary | |
| execution primitive while owning a separate job/artifact lifecycle suitable | |
| for conversational and AI Learn clients. | |
| """ | |
| from __future__ import annotations | |
| import asyncio | |
| import hashlib | |
| import json | |
| import secrets | |
| import time | |
| from dataclasses import dataclass, field | |
| from typing import Any, Callable | |
| from ._provider_artifact import ( | |
| PROVIDER_ARTIFACT_MAX_PROMPT_CHARS, | |
| ProviderArtifactError, | |
| ProviderArtifactRequest, | |
| ) | |
| AUDIO_GENERATION_REQUEST_CONTRACT = "assistant.audio-generation-request.v1" | |
| GENERATION_JOB_CONTRACT = "scikitplot-generation-job.v2" | |
| AUDIO_GENERATION_MAX_REQUEST_BYTES = 64 * 1024 | |
| AUDIO_GENERATION_CAPABILITY_VERSION = 1 | |
| AUDIO_GENERATION_JOB_TTL_SECONDS = 60 * 60 | |
| AUDIO_GENERATION_ARTIFACT_TTL_SECONDS = 60 * 60 | |
| AUDIO_GENERATION_MAX_JOBS = 256 | |
| AUDIO_GENERATION_MAX_TOTAL_ARTIFACT_BYTES = 64 * 1024 * 1024 | |
| class AudioGenerationError(RuntimeError): | |
| def __init__( | |
| self, | |
| code: str, | |
| message: str = "", | |
| *, | |
| retryable: bool = False, | |
| ) -> None: | |
| self.code = str(code or "AUDIO_GENERATION_ERROR") | |
| self.retryable = bool(retryable) | |
| # The authored, client-safe sentence, or "" when only a code was | |
| # given. Responses expose ``code`` and ``message`` and never | |
| # ``str(exc)``, which is the exception's own rendering of itself. | |
| self.message = message if message and message != self.code else "" | |
| super().__init__(message or self.code) | |
| class AudioGenerationRequest: | |
| text: str | |
| selected_model: str | |
| mode: str = "narration" | |
| def canonical_sha256(self) -> str: | |
| raw = json.dumps( | |
| { | |
| "contract": AUDIO_GENERATION_REQUEST_CONTRACT, | |
| "mode": self.mode, | |
| "selected_model": self.selected_model, | |
| "text": self.text, | |
| }, | |
| ensure_ascii=False, | |
| sort_keys=True, | |
| separators=(",", ":"), | |
| ).encode("utf-8") | |
| return hashlib.sha256(raw).hexdigest() | |
| class AudioArtifact: | |
| artifact_id: str | |
| capability: str | |
| mime_type: str | |
| data: bytes | |
| sha256: str | |
| created_at: float | |
| expires_at: float | |
| class AudioGenerationJob: | |
| generation_id: str | |
| capability: str | |
| request_sha256: str | |
| selected_model: str | |
| state: str = "queued" | |
| stage: str = "queued" | |
| progress: float = 0.0 | |
| strategy: str = "delegated" | |
| created_at: float = field(default_factory=time.time) | |
| updated_at: float = field(default_factory=time.time) | |
| artifact_id: str = "" | |
| error_code: str = "" | |
| error_message: str = "" | |
| retryable: bool = False | |
| def public(self, artifact: AudioArtifact | None = None) -> dict[str, Any]: | |
| out: dict[str, Any] = { | |
| "contract": GENERATION_JOB_CONTRACT, | |
| "generation_id": self.generation_id, | |
| "generation_capability": self.capability, | |
| "workflow": "assistant_audio_narration", | |
| "execution": { | |
| "state": self.state, | |
| "stage": self.stage, | |
| "progress": float(self.progress), | |
| }, | |
| "plan": {"strategy": self.strategy}, | |
| "selected_model": self.selected_model, | |
| "created_at": int(self.created_at), | |
| "updated_at": int(self.updated_at), | |
| "artifacts": [], | |
| } | |
| if artifact is not None and self.state == "ready": | |
| out["artifacts"] = [ | |
| { | |
| "artifact_id": artifact.artifact_id, | |
| "artifact_capability": artifact.capability, | |
| "modality": "audio", | |
| "mime_type": artifact.mime_type, | |
| "size_bytes": len(artifact.data), | |
| "sha256": artifact.sha256, | |
| } | |
| ] | |
| if self.error_code: | |
| out["error"] = { | |
| "type": "generation_error", | |
| "code": self.error_code, | |
| "layer": "execution", | |
| "message": self.error_message or "Audio generation failed.", | |
| "retryable": bool(self.retryable), | |
| } | |
| return out | |
| def parse_audio_generation_request(body: bytes) -> AudioGenerationRequest: | |
| if len(body) > AUDIO_GENERATION_MAX_REQUEST_BYTES: | |
| raise AudioGenerationError("REQUEST_TOO_LARGE") | |
| try: | |
| raw = json.loads(body) | |
| except (UnicodeDecodeError, json.JSONDecodeError, TypeError, ValueError) as exc: | |
| raise AudioGenerationError("REQUEST_INVALID") from exc | |
| if not isinstance(raw, dict): | |
| raise AudioGenerationError("REQUEST_INVALID") | |
| allowed = {"contract", "text", "selected_model", "mode"} | |
| if set(raw) - allowed or raw.get("contract") != AUDIO_GENERATION_REQUEST_CONTRACT: | |
| raise AudioGenerationError("REQUEST_INVALID") | |
| text = raw.get("text") | |
| selected_model = raw.get("selected_model") | |
| mode = raw.get("mode", "narration") | |
| if ( | |
| not isinstance(text, str) | |
| or not text.strip() | |
| or len(text.strip()) > PROVIDER_ARTIFACT_MAX_PROMPT_CHARS | |
| or "\x00" in text | |
| ): | |
| raise AudioGenerationError("REQUEST_INVALID") | |
| if ( | |
| not isinstance(selected_model, str) | |
| or not selected_model.strip() | |
| or len(selected_model.strip()) > 256 # ruff: ignore[magic-value-comparison] | |
| ): | |
| raise AudioGenerationError("MODEL_UNKNOWN") | |
| if mode != "narration": | |
| raise AudioGenerationError("REQUEST_INVALID") | |
| return AudioGenerationRequest( | |
| text=text.strip(), selected_model=selected_model.strip(), mode=mode | |
| ) | |
| class AudioGenerationService: | |
| """Bounded process-local job/artifact authority for Audio v1.""" | |
| def __init__( | |
| self, | |
| *, | |
| registry: Any, | |
| model_allowed: Callable[[str], bool], | |
| max_jobs: int = AUDIO_GENERATION_MAX_JOBS, | |
| max_total_artifact_bytes: int = AUDIO_GENERATION_MAX_TOTAL_ARTIFACT_BYTES, | |
| job_ttl_seconds: int = AUDIO_GENERATION_JOB_TTL_SECONDS, | |
| artifact_ttl_seconds: int = AUDIO_GENERATION_ARTIFACT_TTL_SECONDS, | |
| ) -> None: | |
| self._registry = registry | |
| self._model_allowed = model_allowed | |
| self._max_jobs = max(8, int(max_jobs)) | |
| self._max_total_artifact_bytes = max(1024 * 1024, int(max_total_artifact_bytes)) | |
| self._job_ttl = max(60, int(job_ttl_seconds)) | |
| self._artifact_ttl = max(60, int(artifact_ttl_seconds)) | |
| self._jobs: dict[str, AudioGenerationJob] = {} | |
| self._artifacts: dict[str, AudioArtifact] = {} | |
| self._idempotency: dict[str, tuple[str, str]] = {} | |
| self._tasks: dict[str, asyncio.Task[None]] = {} | |
| self._lock = asyncio.Lock() | |
| def manifest(self) -> dict[str, Any]: | |
| return { | |
| "backend": "memory", | |
| "shared": False, | |
| "durable": False, | |
| "max_jobs": self._max_jobs, | |
| "job_ttl_seconds": self._job_ttl, | |
| "artifact_ttl_seconds": self._artifact_ttl, | |
| "max_total_artifact_bytes": self._max_total_artifact_bytes, | |
| } | |
| def _choose_spec(self) -> Any: | |
| rows = [row for row in self._registry.public_specs() if row.kind == "audio"] | |
| live = sorted( | |
| (row for row in rows if not row.diagnostic), | |
| key=lambda row: row.id, | |
| ) | |
| diagnostic = sorted( | |
| (row for row in rows if row.diagnostic), | |
| key=lambda row: row.id, | |
| ) | |
| if live: | |
| return live[0] | |
| if diagnostic: | |
| return diagnostic[0] | |
| return None | |
| async def _cleanup_locked(self) -> None: | |
| now = time.time() | |
| for artifact_id, artifact in list(self._artifacts.items()): | |
| if artifact.expires_at <= now: | |
| self._artifacts.pop(artifact_id, None) | |
| for generation_id, job in list(self._jobs.items()): | |
| if ( | |
| job.state in {"ready", "failed", "cancelled"} | |
| and job.updated_at + self._job_ttl <= now | |
| ): | |
| self._jobs.pop(generation_id, None) | |
| self._tasks.pop(generation_id, None) | |
| for key, (_digest, gid) in list(self._idempotency.items()): | |
| if gid == generation_id: | |
| self._idempotency.pop(key, None) | |
| async def submit( | |
| self, | |
| request: AudioGenerationRequest, | |
| idempotency_key: str, | |
| ) -> AudioGenerationJob: | |
| key = str(idempotency_key or "").strip() | |
| if ( | |
| not key # lint | |
| or len(key) > 128 # ruff: ignore[magic-value-comparison] | |
| or any( | |
| ( | |
| ord(ch) < 33 # ruff: ignore[magic-value-comparison] | |
| or ord(ch) > 126 # ruff: ignore[magic-value-comparison] | |
| ) | |
| for ch in key | |
| ) | |
| ): | |
| raise AudioGenerationError("IDEMPOTENCY_INVALID") | |
| digest = request.canonical_sha256() | |
| async with self._lock: | |
| await self._cleanup_locked() | |
| previous = self._idempotency.get(key) | |
| if previous: | |
| old_digest, generation_id = previous | |
| if old_digest != digest: | |
| raise AudioGenerationError("IDEMPOTENCY_CONFLICT") | |
| job = self._jobs.get(generation_id) | |
| if job is not None: | |
| return job | |
| if not self._model_allowed(request.selected_model): | |
| raise AudioGenerationError("MODEL_UNKNOWN") | |
| spec = self._choose_spec() | |
| if spec is None: | |
| raise AudioGenerationError("NO_COMPATIBLE_DEPLOYMENT") | |
| if len(self._jobs) >= self._max_jobs: | |
| raise AudioGenerationError("QUEUE_FULL", retryable=True) | |
| generation_id = "gen_" + secrets.token_hex(16) | |
| capability = secrets.token_urlsafe(32) | |
| strategy = ( | |
| "stub" | |
| if bool(spec.diagnostic) | |
| else ("native" if spec.model == request.selected_model else "delegated") | |
| ) | |
| job = AudioGenerationJob( | |
| generation_id=generation_id, | |
| capability=capability, | |
| request_sha256=digest, | |
| selected_model=request.selected_model, | |
| strategy=strategy, | |
| ) | |
| self._jobs[generation_id] = job | |
| self._idempotency[key] = (digest, generation_id) | |
| task = asyncio.create_task(self._run(job, request, spec)) | |
| self._tasks[generation_id] = task | |
| return job | |
| async def _run( | |
| self, | |
| job: AudioGenerationJob, | |
| request: AudioGenerationRequest, | |
| spec: Any, | |
| ) -> None: | |
| try: | |
| async with self._lock: | |
| if job.state == "cancelled": | |
| return | |
| job.state = "running" | |
| job.stage = "synthesizing" | |
| job.progress = 0.25 | |
| job.updated_at = time.time() | |
| mime_type = "audio/mpeg" if "audio/mpeg" in spec.mime_types else "audio/wav" | |
| low = ProviderArtifactRequest( | |
| generator_id=spec.id, | |
| kind="audio", | |
| prompt=request.text, | |
| mime_type=mime_type, | |
| options={}, | |
| ) | |
| generated = await self._registry.generate(low) | |
| try: | |
| generated.file.seek(0) | |
| data = generated.file.read() | |
| finally: | |
| generated.close() | |
| if not data: | |
| raise AudioGenerationError("OUTPUT_INVALID") | |
| async with self._lock: | |
| if job.state == "cancelled": | |
| return | |
| await self._cleanup_locked() | |
| total = sum(len(row.data) for row in self._artifacts.values()) | |
| if total + len(data) > self._max_total_artifact_bytes: | |
| raise AudioGenerationError("QUEUE_FULL", retryable=True) | |
| artifact_id = "art_" + secrets.token_hex(16) | |
| artifact = AudioArtifact( | |
| artifact_id=artifact_id, | |
| capability=secrets.token_urlsafe(32), | |
| mime_type=mime_type, | |
| data=data, | |
| sha256=hashlib.sha256(data).hexdigest(), | |
| created_at=time.time(), | |
| expires_at=time.time() + self._artifact_ttl, | |
| ) | |
| self._artifacts[artifact_id] = artifact | |
| job.artifact_id = artifact_id | |
| job.state = "ready" | |
| job.stage = "ready" | |
| job.progress = 1.0 | |
| job.updated_at = time.time() | |
| except asyncio.CancelledError: | |
| async with self._lock: | |
| if job.state not in {"ready", "failed"}: | |
| job.state = "cancelled" | |
| job.stage = "cancelled" | |
| job.updated_at = time.time() | |
| except (ProviderArtifactError, AudioGenerationError) as exc: | |
| async with self._lock: | |
| if job.state == "cancelled": | |
| return | |
| code = getattr(exc, "code", "PROVIDER_UNAVAILABLE") | |
| public = { | |
| "PROVIDER_ARTIFACT_UPSTREAM_UNAVAILABLE": ( | |
| "The speech renderer is temporarily unavailable." | |
| ), | |
| "PROVIDER_ARTIFACT_UPSTREAM_REJECTED": ( | |
| "The speech renderer rejected the request." | |
| ), | |
| "PROVIDER_ARTIFACT_OUTPUT_TOO_LARGE": ( | |
| "The generated audio exceeded the configured limit." | |
| ), | |
| "QUEUE_FULL": ( | |
| "The audio generation service is temporarily at capacity." | |
| ), | |
| }.get( | |
| code, | |
| "Audio generation could not complete.", | |
| ) | |
| job.state = "failed" | |
| job.stage = "failed" | |
| job.progress = max(job.progress, 0.25) | |
| job.error_code = ( | |
| "PROVIDER_UNAVAILABLE" | |
| if code.startswith("PROVIDER_ARTIFACT_UPSTREAM") | |
| else code | |
| ) | |
| job.error_message = public | |
| job.retryable = code in { | |
| "PROVIDER_ARTIFACT_UPSTREAM_UNAVAILABLE", | |
| "QUEUE_FULL", | |
| } | |
| job.updated_at = time.time() | |
| async def get( | |
| self, | |
| generation_id: str, | |
| capability: str, | |
| ) -> tuple[AudioGenerationJob, AudioArtifact | None]: | |
| async with self._lock: | |
| await self._cleanup_locked() | |
| job = self._jobs.get(str(generation_id or "")) | |
| if job is None or not secrets.compare_digest( | |
| job.capability, | |
| str(capability or ""), | |
| ): | |
| raise AudioGenerationError("JOB_NOT_ACCESSIBLE") | |
| artifact = self._artifacts.get(job.artifact_id) if job.artifact_id else None | |
| return job, artifact | |
| async def cancel( | |
| self, | |
| generation_id: str, | |
| capability: str, | |
| ) -> AudioGenerationJob: | |
| async with self._lock: | |
| await self._cleanup_locked() | |
| job = self._jobs.get(str(generation_id or "")) | |
| if job is None or not secrets.compare_digest( | |
| job.capability, | |
| str(capability or ""), | |
| ): | |
| raise AudioGenerationError("JOB_NOT_ACCESSIBLE") | |
| if job.state in {"ready", "failed", "cancelled"}: | |
| raise AudioGenerationError("JOB_NOT_CANCELLABLE") | |
| job.state = "cancelled" | |
| job.stage = "cancelled" | |
| job.updated_at = time.time() | |
| task = self._tasks.get(job.generation_id) | |
| if task is not None and not task.done(): | |
| task.cancel() | |
| return job | |
| async def artifact(self, artifact_id: str, capability: str) -> AudioArtifact: | |
| async with self._lock: | |
| await self._cleanup_locked() | |
| artifact = self._artifacts.get(str(artifact_id or "")) | |
| if artifact is None or not secrets.compare_digest( | |
| artifact.capability, str(capability or "") | |
| ): | |
| raise AudioGenerationError("ARTIFACT_NOT_ACCESSIBLE") | |
| return artifact | |