File size: 5,330 Bytes
aa0d853
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
02b6489
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
aa0d853
 
 
 
 
02b6489
aa0d853
 
 
 
 
 
 
02b6489
 
 
 
 
aa0d853
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
"""Publicación y descarga de los artefactos RAG en repos privados del Hub.

Se usa la API de Python (`huggingface_hub`) y no el CLI `hf` a propósito: en la versión instalada
(1.16.1) el CLI arrastra una incompatibilidad typer/click que hace que `ctx.exit()` propague un
`click.exceptions.Exit(0)` como traceback y **devuelva código 1 aunque la operación haya ido
bien**. Eso es inservible dentro de un Makefile, donde un exit≠0 aborta la cadena.

Artefactos:
  - índice  (~70 MB): derivado de libros con licencia, contiene su texto troceado → repo PRIVADO.
  - libros (~226 MB): los PDF originales → repo PRIVADO, sólo necesarios para reingerir.

Uso:
    python scripts/hub.py publish-index
    python scripts/hub.py fetch-index
    python scripts/hub.py publish-books
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

from huggingface_hub import HfApi, snapshot_download

RAIZ = Path(__file__).resolve().parents[1]
DIR_INDICE = RAIZ / "instance" / "rag_index"
DIR_LIBROS = RAIZ / "books"

REPO_INDICE = "blackmistcode/morphos-rag-index"
REPO_LIBROS = "blackmistcode/morphos-books"


def _asegurar_repo(api: HfApi, repo_id: str) -> None:
    """Crea el repo si falta. `private=True` sólo aplica en la creación: si el repo ya existe
    NO lo vuelve privado, así que se comprueba explícitamente y se aborta si es público."""
    api.create_repo(repo_id=repo_id, repo_type="dataset", private=True, exist_ok=True)
    if not api.dataset_info(repo_id).private:
        sys.exit(
            f"ABORTADO: {repo_id} es PÚBLICO. Contiene material con licencia; hazlo privado "
            f"antes de subir nada."
        )


def _comprobar_manifiesto(manifiesto: dict) -> None:
    """Aborta si `manifest.json` no coincide con la tabla que se va a publicar.

    El manifiesto lo escribe la ingesta, pero `curar_indice.py` cambia el número de filas
    después. Publicar uno desfasado no rompe nada visible —y por eso es peligroso—: el mensaje
    de commit del Hub y lo que imprime `fetch-index` anuncian un recuento que no es el del
    índice que sirve, y a partir de ahí nadie sabe qué hay publicado.
    """
    try:
        import lancedb
    except ImportError:
        print("AVISO: sin lancedb no se puede verificar el manifiesto; se publica a ciegas.")
        return
    tabla = lancedb.connect(str(DIR_INDICE)).open_table("literatura")
    reales = tabla.count_rows()
    declarados = manifiesto.get("n_fragmentos")
    if declarados != reales:
        sys.exit(
            f"ABORTADO: manifest.json declara {declarados} fragmentos y la tabla tiene {reales}.\n"
            f"Ejecuta 'make curar-indice ARGS=--aplicar' para reconciliarlo antes de publicar."
        )


def publicar_indice() -> None:
    if not DIR_INDICE.exists():
        sys.exit(f"ERROR: no existe {DIR_INDICE}. Ejecuta 'make ingest' primero.")
    manifiesto = json.loads((DIR_INDICE / "manifest.json").read_text(encoding="utf-8"))
    hash_corpus = manifiesto.get("hash_corpus", "desconocido")
    _comprobar_manifiesto(manifiesto)
    api = HfApi()
    _asegurar_repo(api, REPO_INDICE)
    api.upload_folder(
        repo_id=REPO_INDICE,
        repo_type="dataset",
        folder_path=str(DIR_INDICE),
        commit_message=f"Índice RAG · corpus {hash_corpus} · {manifiesto.get('n_fragmentos')} fragmentos",
        # El remoto debe ser un ESPEJO del local, no una acumulación. LanceDB versiona por
        # ficheros: al compactar desaparecen los datos viejos, y sin esto se quedarían en el Hub
        # para siempre. Medido: el repo tenía 2 ficheros de datos y 2 índices FTS huérfanos de
        # versiones anteriores — 73 MB para servir 33 MB, que además se hornean en la imagen.
        delete_patterns="*",
    )
    print(f"OK: índice publicado en {REPO_INDICE} (corpus {hash_corpus})")


def descargar_indice() -> None:
    DIR_INDICE.mkdir(parents=True, exist_ok=True)
    snapshot_download(
        repo_id=REPO_INDICE,
        repo_type="dataset",
        local_dir=str(DIR_INDICE),
    )
    manifiesto = DIR_INDICE / "manifest.json"
    if manifiesto.exists():
        m = json.loads(manifiesto.read_text(encoding="utf-8"))
        print(f"OK: índice descargado · corpus {m.get('hash_corpus')} · {m.get('n_fragmentos')} fragmentos")
    else:
        print("AVISO: descargado sin manifest.json; el índice puede estar incompleto.")


def publicar_libros() -> None:
    pdfs = sorted(DIR_LIBROS.glob("*.pdf"))
    if not pdfs:
        sys.exit(f"ERROR: no hay PDFs en {DIR_LIBROS}.")
    api = HfApi()
    _asegurar_repo(api, REPO_LIBROS)
    api.upload_folder(
        repo_id=REPO_LIBROS,
        repo_type="dataset",
        folder_path=str(DIR_LIBROS),
        allow_patterns=["*.pdf"],
        commit_message=f"Corpus veterinario con licencia ({len(pdfs)} PDF)",
    )
    print(f"OK: {len(pdfs)} libros publicados en {REPO_LIBROS}")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("accion", choices=["publish-index", "fetch-index", "publish-books"])
    args = parser.parse_args()
    {
        "publish-index": publicar_indice,
        "fetch-index": descargar_indice,
        "publish-books": publicar_libros,
    }[args.accion]()


if __name__ == "__main__":
    main()