Skip to content

feat(results): sous-commande results push (transcripts locaux → HF)#43

Merged
benoitvx merged 2 commits into
mainfrom
feat/results-push
Jun 19, 2026
Merged

feat(results): sous-commande results push (transcripts locaux → HF)#43
benoitvx merged 2 commits into
mainfrom
feat/results-push

Conversation

@benoitvx

Copy link
Copy Markdown
Contributor

Transforme le helper local de push en sous-commande propre et testée eval-transcript results push.

Pourquoi

La CI couvre les modèles API (Albert, Voxtral). Les modèles locaux (WhisperX, Kyutai, Cohere/MLX) tournent sur la machine ; il faut verser leurs transcripts sur le dataset de résultats pour que le WER et le juge couvrent tous les modèles.

Comportement

uv run eval-transcript results push --dry-run        # plan
uv run eval-transcript results push                  # push (1 commit)
uv run eval-transcript results push --include whisperx
  • Garde-fou RGPD structurel : l'allowlist est dérivée du corpus public (ground_truth/<id>). Tout sample absent (réunion interne, sample retiré) est ignoré → aucune donnée privée ne part sur le Hub. Vérifié en dry-run réel : 40 fichiers (5 officiels × 8 modèles) poussables, 10 samples privés/retirés ignorés.
  • --corpus/--results (ou EVAL_CORPUS_REPO/EVAL_RESULTS_REPO), --token (défaut HF_TOKEN).

Notes

  • Ajoute huggingface-hub aux dépendances du projet (la commande en a besoin au runtime ; n'était pas sur main).
  • Module results.py testable via un Protocol HfApi (même pattern que le provider HF). 8 tests ajoutés, suite complète 117 tests verte.
  • README mis à jour (section CI).
  • À terme, cette capacité pourra fusionner avec le provider HF (feat: add Hugging Face dataset pull/push provider #36) à la reprise de Luis.

🤖 Generated with Claude Code

Ajoute 'eval-transcript results push' pour verser les sorties des modeles
locaux (WhisperX, Kyutai, Cohere/MLX) sur le dataset de resultats HF, afin
que le WER et le juge couvrent tous les modeles.

Garde-fou RGPD : l'allowlist est derivee du corpus PUBLIC (ground_truth/<id>) ;
tout sample absent (reunion interne, sample retire) est ignore -> aucune donnee
privee ne part sur le Hub. Dry-run, filtre --include, repos overridables par
flag ou env. Ajoute huggingface-hub aux dependances + tests (FakeHfApi).

Remplace le script local jetable par une vraie sous-commande, testee.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new results push command to safely upload local model transcripts to a Hugging Face results dataset, using the public corpus as an allowlist to prevent private data leaks. It adds the huggingface-hub dependency, implements the ResultsClient and push planning logic, updates the documentation, and includes comprehensive unit tests. The reviewer feedback suggests several robustness and usability improvements, including wrapping Hugging Face API calls in try-except blocks to handle exceptions gracefully, using is_dir() and is_file() checks to prevent potential runtime errors, ignoring hidden directories, providing clearer CLI output when no files are available to push, and removing an unused helper function in the test suite.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread src/eval_transcript/results.py Outdated
Comment on lines +148 to +180
def plan(
self, *, transcriptions_dir: Path = DEFAULT_TRANSCRIPTIONS_DIR, include: str | None = None
) -> PushPlan:
return build_push_plan(
api=self._get_api(),
corpus_repo=self.corpus_repo,
results_repo=self.results_repo,
transcriptions_dir=transcriptions_dir,
include=include,
)

def push(self, plan: PushPlan, *, message: str = "Push local transcripts (officiels only)") -> None:
if not plan.uploads:
return
if not self.token:
raise ResultsError(
"HF_TOKEN is required to push results; set the HF_TOKEN environment variable or pass token=..."
)
try:
from huggingface_hub import CommitOperationAdd # type: ignore[import-not-found]
except ImportError as exc: # pragma: no cover - exercised only in production
raise ResultsError("huggingface_hub is required for live HF Hub operations") from exc
operations = [
CommitOperationAdd(path_in_repo=u.path_in_repo, path_or_fileobj=str(u.local_path))
for u in plan.uploads
]
self._get_api().create_commit(
repo_id=plan.results_repo,
repo_type=DATASET_REPO_TYPE,
operations=operations,
commit_message=message,
token=self.token,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Les appels à l'API Hugging Face (list_repo_files via plan et create_commit via push) peuvent lever diverses exceptions en cas d'erreur réseau, de dépôt inexistant ou de jeton invalide (par exemple des exceptions de requests ou spécifiques à huggingface_hub). Comme la CLI ne capture pas ces exceptions (elle ne capture que ResultsError et httpx.HTTPError), cela provoquera un plantage avec une trace d'exécution (traceback) brute pour l'utilisateur.

Il est fortement recommandé d'envelopper ces appels dans des blocs try...except au sein de ResultsClient pour intercepter toutes les erreurs et les encapsuler dans une ResultsError claire.

    def plan(
        self, *, transcriptions_dir: Path = DEFAULT_TRANSCRIPTIONS_DIR, include: str | None = None
    ) -> PushPlan:
        try:
            return build_push_plan(
                api=self._get_api(),
                corpus_repo=self.corpus_repo,
                results_repo=self.results_repo,
                transcriptions_dir=transcriptions_dir,
                include=include,
            )
        except Exception as exc:
            if isinstance(exc, ResultsError):
                raise
            raise ResultsError(f"Impossible de planifier l'envoi des résultats : {exc}") from exc

    def push(self, plan: PushPlan, *, message: str = "Push local transcripts (officiels only)") -> None:
        if not plan.uploads:
            return
        if not self.token:
            raise ResultsError(
                "HF_TOKEN is required to push results; set the HF_TOKEN environment variable or pass token=..."
            )
        try:
            from huggingface_hub import CommitOperationAdd  # type: ignore[import-not-found]
        except ImportError as exc:  # pragma: no cover - exercised only in production
            raise ResultsError("huggingface_hub is required for live HF Hub operations") from exc
        operations = [
            CommitOperationAdd(path_in_repo=u.path_in_repo, path_or_fileobj=str(u.local_path))
            for u in plan.uploads
        ]
        try:
            self._get_api().create_commit(
                repo_id=plan.results_repo,
                repo_type=DATASET_REPO_TYPE,
                operations=operations,
                commit_message=message,
                token=self.token,
            )
        except Exception as exc:
            raise ResultsError(f"Échec de l'envoi des résultats vers Hugging Face : {exc}") from exc
References
  1. When designing API clients, perform early validation on required credentials and IDs to fail fast.

Comment on lines +95 to +96
if not transcriptions_dir.exists():
raise ResultsError(f"Transcriptions directory not found: {transcriptions_dir}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Si transcriptions_dir existe mais n'est pas un dossier (par exemple, si c'est un fichier), l'appel à iterdir() à la ligne 100 lèvera une exception NotADirectoryError. Il est plus robuste de vérifier is_dir() plutôt que exists().

Suggested change
if not transcriptions_dir.exists():
raise ResultsError(f"Transcriptions directory not found: {transcriptions_dir}")
if not transcriptions_dir.is_dir():
raise ResultsError(f"Transcriptions directory not found or is not a directory: {transcriptions_dir}")

Comment on lines +100 to +101
for sample_dir in sorted(p for p in transcriptions_dir.iterdir() if p.is_dir()):
sid = sample_dir.name

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Pour éviter de polluer la liste des dossiers ignorés avec des dossiers système ou cachés (comme .DS_Store, .ipynb_checkpoints ou .git), il est recommandé de filtrer les dossiers commençant par un point ..

Suggested change
for sample_dir in sorted(p for p in transcriptions_dir.iterdir() if p.is_dir()):
sid = sample_dir.name
for sample_dir in sorted(p for p in transcriptions_dir.iterdir() if p.is_dir() and not p.name.startswith(".")):

Comment on lines +105 to +107
for txt in sorted(sample_dir.glob(f"*{TRANSCRIPT_SUFFIX}")):
if include and include not in txt.name:
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Il est possible que glob retourne des dossiers s'ils correspondent au motif (par exemple, un sous-dossier nommé avec l'extension .txt). Pour éviter des erreurs lors de la création du commit Hugging Face (qui attend des fichiers), il est plus sûr de filtrer explicitement pour ne garder que les fichiers avec txt.is_file().

        for txt in sorted(sample_dir.glob(f"*{TRANSCRIPT_SUFFIX}")):
            if not txt.is_file():
                continue
            if include and include not in txt.name:
                continue

Comment on lines +460 to +466
if args.dry_run:
print("[dry-run] relancer sans --dry-run pour pousser.")
return
client.push(plan, message=args.message)
if plan.uploads:
print(f"Poussé {len(plan.uploads)} fichier(s) vers {plan.results_repo}")
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Si aucun fichier n'est à pousser (liste plan.uploads vide), la commande se termine silencieusement sans informer explicitement l'utilisateur. De plus, il est inutile d'afficher le message de dry-run s'il n'y a rien à envoyer.

Il serait plus clair d'interrompre l'exécution tôt avec un message explicite si plan.uploads est vide.

Suggested change
if args.dry_run:
print("[dry-run] relancer sans --dry-run pour pousser.")
return
client.push(plan, message=args.message)
if plan.uploads:
print(f"Poussé {len(plan.uploads)} fichier(s) vers {plan.results_repo}")
return
if not plan.uploads:
print("Aucun fichier à pousser.")
return
if args.dry_run:
print("[dry-run] relancer sans --dry-run pour pousser.")
return
client.push(plan, message=args.message)
print(f"Poussé {len(plan.uploads)} fichier(s) vers {plan.results_repo}")
return

Comment thread tests/test_results.py Outdated
Comment on lines +23 to +30
@contextlib.contextmanager
def chdir(path: Path):
previous = Path.cwd()
os.chdir(path)
try:
yield
finally:
os.chdir(previous)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

La fonction utilitaire chdir est définie mais n'est jamais utilisée dans les tests. Il est préférable de supprimer ce code mort pour garder le fichier propre.

… de test mort

- Emballe list_repo_files / create_commit : un HfHubHTTPError (token invalide,
  repo introuvable, reseau) remonte en ResultsError -> 'Error: ...' propre + exit 1
  au lieu d'un traceback. Helper _hf_http_error_types() (import paresseux, tuple
  vide si hf absent -> garde les tests sans dependance dure).
- Supprime le context manager chdir inutilise dans test_results.py.
- Ajoute 2 tests du chemin d'erreur (lecture corpus + push).

Repond aux points #1 et #2 de la review.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@benoitvx
benoitvx merged commit 6c183e0 into main Jun 19, 2026
@benoitvx
benoitvx deleted the feat/results-push branch June 19, 2026 19:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant