diff --git a/CHANGELOG.md b/CHANGELOG.md index cfa8a91..c06e8be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ after its first published distribution. heads, combining only the targets each sample actually carries. - Internal hierarchical utterance sampling primitives: square-root corpus mass, inverse-square-root class mass, and bounded seeded window selection. +- Feature backends resolve and expose the exact model revision they loaded, and + accept a `repository@revision` model identifier to pin it. ### Changed diff --git a/ser/_internal/repr/emotion2vec.py b/ser/_internal/repr/emotion2vec.py index 2ab2a43..0e14e79 100644 --- a/ser/_internal/repr/emotion2vec.py +++ b/ser/_internal/repr/emotion2vec.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import importlib import importlib.util import logging @@ -157,6 +158,29 @@ def backend_id(self) -> str: """Stable backend identifier used by runtime capability registry.""" return "emotion2vec" + @property + def model_revision(self) -> str | None: + """Returns an exact hub commit when exposed by the loaded backend.""" + if self._model is not None: + revision = getattr(self._model.config, "_commit_hash", None) + if isinstance(revision, str) and revision.strip(): + return revision.strip() + source, is_local = self._resolve_funasr_model_source() + source_path = Path(source) + if not is_local or not source_path.is_dir(): + return None + digest = hashlib.sha256() + for path in sorted( + source_path.rglob("*"), key=lambda item: str(item.relative_to(source_path)) + ): + if not path.is_file(): + continue + digest.update(str(path.relative_to(source_path)).encode("utf-8")) + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return f"content-sha256:{digest.hexdigest()}" + @property def feature_dim(self) -> int: """Returns dynamic embedding size from model configuration.""" diff --git a/ser/_internal/repr/hf_whisper.py b/ser/_internal/repr/hf_whisper.py index 2123a6e..1739797 100644 --- a/ser/_internal/repr/hf_whisper.py +++ b/ser/_internal/repr/hf_whisper.py @@ -137,7 +137,13 @@ def __init__( raise ValueError( "feature_extractor and model must be provided together or omitted together." ) - self._model_id = model_id + self._model_id, separator, requested_revision = model_id.rpartition("@") + if not separator: + self._model_id = model_id + requested_revision = "" + if not self._model_id: + raise ValueError("model_id repository must be non-empty.") + self._requested_revision = requested_revision or None self._max_chunk_seconds = max_chunk_seconds self._cache_dir = cache_dir self._device = device @@ -151,6 +157,13 @@ def backend_id(self) -> str: """Stable backend identifier used by runtime capability registry.""" return "hf_whisper" + @property + def model_revision(self) -> str | None: + """Returns the exact Hugging Face commit resolved by the loaded model.""" + _, model = self._ensure_runtime_components() + revision = getattr(model.config, "_commit_hash", None) + return revision.strip() if isinstance(revision, str) and revision.strip() else None + @property def feature_dim(self) -> int: """Returns dynamic embedding size from model configuration.""" @@ -328,6 +341,8 @@ def _ensure_runtime_components(self) -> tuple[_FeatureExtractor, _EncoderModel]: if self._cache_dir is not None: self._cache_dir.mkdir(parents=True, exist_ok=True) cache_kwargs["cache_dir"] = str(self._cache_dir) + if self._requested_revision is not None: + cache_kwargs["revision"] = self._requested_revision feature_extractor = cast( _FeatureExtractor, diff --git a/ser/_internal/repr/hf_xlsr.py b/ser/_internal/repr/hf_xlsr.py index 34ecd1d..8cea286 100644 --- a/ser/_internal/repr/hf_xlsr.py +++ b/ser/_internal/repr/hf_xlsr.py @@ -116,7 +116,13 @@ def __init__( raise ValueError( "feature_extractor and model must be provided together or omitted together." ) - self._model_id = model_id + self._model_id, separator, requested_revision = model_id.rpartition("@") + if not separator: + self._model_id = model_id + requested_revision = "" + if not self._model_id: + raise ValueError("model_id repository must be non-empty.") + self._requested_revision = requested_revision or None self._max_chunk_seconds = max_chunk_seconds self._cache_dir = cache_dir self._device = device @@ -131,6 +137,13 @@ def backend_id(self) -> str: """Stable backend identifier used by runtime capability registry.""" return "hf_xlsr" + @property + def model_revision(self) -> str | None: + """Returns the exact Hugging Face commit resolved by the loaded model.""" + _, model = self._ensure_runtime_components() + revision = getattr(model.config, "_commit_hash", None) + return revision.strip() if isinstance(revision, str) and revision.strip() else None + @property def feature_dim(self) -> int: """Returns dynamic embedding size from model configuration.""" @@ -307,6 +320,8 @@ def _ensure_runtime_components(self) -> tuple[_FeatureExtractor, _EncoderModel]: if self._cache_dir is not None: self._cache_dir.mkdir(parents=True, exist_ok=True) cache_kwargs["cache_dir"] = str(self._cache_dir) + if self._requested_revision is not None: + cache_kwargs["revision"] = self._requested_revision feature_extractor = cast( _FeatureExtractor,