Source code for personalitylinmult.model

"""The public, pip-install-only entry point: load a champion model by id.

``PersonalityModel.from_pretrained(model_id)`` downloads a champion checkpoint
from the public HF Hub model repo and wraps it for prediction -- no MLflow, no
repo clone, no external config. This is deliberately the *only* public model
class: a checkpoint already knows whether it is a single-stream LinT or a
cross-modal LinMulT (see
:class:`~personalitylinmult.inference.predictor.FiPredictor`), so exposing
separate ``PersonalityLinT``/``PersonalityLinMulT`` classes would force a
caller to track architecture per id for no benefit -- this class reads it off
the checkpoint instead.

Model ids follow ``{features joined by "_", multiword feature names use "-"}_best-{metric}``,
e.g. ``"wavlm_best-ccc"`` (LinT, audio only) or
``"wavlm-emotion2vec_best-ccc"`` (LinMulT, audio fusion) -- the same convention
used for the MLflow registered-model names (see
:mod:`personalitylinmult.inference.register_models`) and the HF model repo
filenames.
"""

from __future__ import annotations

import logging
from pathlib import Path
from typing import TYPE_CHECKING

from personalitylinmult.inference.predictor import FiPredictor

if TYPE_CHECKING:
    import numpy as np

logger = logging.getLogger(__name__)
"""Module-level logger."""

HF_MODEL_REPO = "fodorad/personalitylinmult-fi-models"
"""The public HF Hub model repo champion checkpoints are published to."""


def _checkpoint_filename(model_id: str) -> str:
    """Map a model id to its filename in the HF model repo.

    Args:
        model_id (str): e.g. ``"wavlm_best-ccc"``.

    Returns:
        str: e.g. ``"wavlm_best-ccc.ckpt"``.
    """
    return f"{model_id}.ckpt"


def _download(model_id: str, revision: str | None = None) -> Path:
    """Download a champion checkpoint from the HF model repo.

    A separate function (not inlined into :meth:`PersonalityModel.from_pretrained`)
    so tests can substitute a local checkpoint path without a network call --
    see ``tests/test_model.py``.

    Args:
        model_id (str): The champion id to download.
        revision (str | None): HF Hub revision (branch/tag/commit); ``None``
            uses the repo's default branch.

    Returns:
        Path: Local path to the downloaded ``.ckpt`` (cached under
        :data:`personalitylinmult.WEIGHTS_DIR` across calls).
    """
    from huggingface_hub import hf_hub_download

    # Imported lazily (not at module scope) so this module never forms an
    # import cycle with the package `__init__` that re-exports PersonalityModel.
    from personalitylinmult import WEIGHTS_DIR

    path = hf_hub_download(
        repo_id=HF_MODEL_REPO,
        filename=_checkpoint_filename(model_id),
        revision=revision,
        cache_dir=str(WEIGHTS_DIR),
    )
    # lgtm[py/log-injection] -- repr(), not str(): model_id is a public API
    # argument (caller-supplied), and repr() escapes newlines/control
    # characters so it cannot forge extra log lines.
    logger.info(f"{model_id!r}: downloaded {path}.")
    return Path(path)


[docs] class PersonalityModel: """A champion FI model, loaded by id from the public HF Hub repo. Args: predictor (FiPredictor): The loaded predictor to wrap. """ def __init__(self, predictor: FiPredictor): self._predictor = predictor @property def feature_names(self) -> list[str]: """Features this model consumes, in input order.""" return self._predictor.feature_names @property def traits(self) -> list[str]: """Trait names, in output-column order.""" return self._predictor.traits
[docs] @classmethod def from_pretrained(cls, model_id: str, revision: str | None = None) -> PersonalityModel: """Download (or reuse the cached) champion checkpoint and load it. Args: model_id (str): A champion id, e.g. ``"wavlm_best-ccc"`` or ``"avt_best-ccc"``. See the module docstring for the naming convention. revision (str | None): HF Hub revision (branch/tag/commit); ``None`` uses the repo's default branch. Returns: PersonalityModel: Ready to predict. """ checkpoint = _download(model_id, revision=revision) return cls(FiPredictor.from_checkpoint(checkpoint))
[docs] def predict(self, features: dict[str, np.ndarray]) -> dict[str, float]: """Predict Big Five scores for one sample. Args: features (dict[str, np.ndarray]): One ``(T, F)`` array per feature the model expects (see :attr:`feature_names`). Returns: dict[str, float]: ``{trait: score}``, in the model's trait order. """ return self._predictor.predict(features)