serigy 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- serigy/__init__.py +44 -0
- serigy/__main__.py +112 -0
- serigy/api/__init__.py +46 -0
- serigy/api/inferencia.py +484 -0
- serigy/api/predizer.py +233 -0
- serigy/api/schemas.py +217 -0
- serigy/checkpoint.py +195 -0
- serigy/cliente.py +349 -0
- serigy/components/__init__.py +41 -0
- serigy/components/asemantico.py +264 -0
- serigy/components/confianca.py +228 -0
- serigy/components/convergencia.py +180 -0
- serigy/components/sistema.py +210 -0
- serigy/config.py +210 -0
- serigy/dados/__init__.py +8 -0
- serigy/dados/extrator.py +283 -0
- serigy/primitivos/__init__.py +74 -0
- serigy/primitivos/adapters.py +164 -0
- serigy/primitivos/confianca.py +46 -0
- serigy/primitivos/encode.py +211 -0
- serigy/primitivos/heads.py +396 -0
- serigy/primitivos/persist.py +167 -0
- serigy/primitivos/prioridade_util.py +36 -0
- serigy/primitivos/schemas.py +388 -0
- serigy/primitivos/system_one.py +82 -0
- serigy/utils/__init__.py +27 -0
- serigy/utils/calibracao_h3.py +61 -0
- serigy/utils/metricas.py +137 -0
- serigy/utils/ordinal.py +129 -0
- serigy-0.2.0.dist-info/METADATA +131 -0
- serigy-0.2.0.dist-info/RECORD +35 -0
- serigy-0.2.0.dist-info/WHEEL +5 -0
- serigy-0.2.0.dist-info/entry_points.txt +2 -0
- serigy-0.2.0.dist-info/licenses/LICENSE +21 -0
- serigy-0.2.0.dist-info/top_level.txt +1 -0
serigy/__init__.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Serigy — inferência pública (pip).
|
|
2
|
+
|
|
3
|
+
Uso:
|
|
4
|
+
from serigy import SerigyModel, Escolha, Verdadeiro
|
|
5
|
+
model = SerigyModel("wendellperbar/serigy-jev")
|
|
6
|
+
out = model.predict("texto da notícia...")
|
|
7
|
+
result = model.system_one(state, {"q": Verdadeiro(instructions="…?")})
|
|
8
|
+
|
|
9
|
+
Após cada upload no Hub, atualize DEFAULT_HUB_REVISION em cliente.py
|
|
10
|
+
(pin atual: 785ef7e842fb82409af027b4f9ee217a4e72f9d7).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from serigy.cliente import DEFAULT_REPO, SerigyModel
|
|
16
|
+
from serigy.primitivos.schemas import (
|
|
17
|
+
Escolha,
|
|
18
|
+
Indice,
|
|
19
|
+
Prioridade,
|
|
20
|
+
SystemOneRequest,
|
|
21
|
+
SystemOneResult,
|
|
22
|
+
Verdadeiro,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"SerigyModel",
|
|
27
|
+
"DEFAULT_REPO",
|
|
28
|
+
"Escolha",
|
|
29
|
+
"Indice",
|
|
30
|
+
"Verdadeiro",
|
|
31
|
+
"Prioridade",
|
|
32
|
+
"SystemOneRequest",
|
|
33
|
+
"SystemOneResult",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
__version__ = version("serigy")
|
|
41
|
+
except PackageNotFoundError:
|
|
42
|
+
__version__ = "0.2.0"
|
|
43
|
+
except ImportError: # pragma: no cover
|
|
44
|
+
__version__ = "0.2.0"
|
serigy/__main__.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""CLI: python -m serigy --texto "..." | --json '{...}' | --stdin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from serigy.cliente import DEFAULT_REPO, SerigyModel, _saida_publica_system_one
|
|
12
|
+
from serigy.primitivos.schemas import SystemOneRequest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _carregar_json(args: argparse.Namespace) -> dict[str, Any]:
|
|
16
|
+
if args.stdin:
|
|
17
|
+
return json.load(sys.stdin)
|
|
18
|
+
assert args.json is not None
|
|
19
|
+
raw = args.json
|
|
20
|
+
caminho = Path(raw)
|
|
21
|
+
if caminho.is_file():
|
|
22
|
+
return json.loads(caminho.read_text(encoding="utf-8"))
|
|
23
|
+
return json.loads(raw)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main(argv: list[str] | None = None) -> int:
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
prog="serigy",
|
|
29
|
+
description="Inferência Serigy (schema público I/O)",
|
|
30
|
+
)
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"texto_posicional",
|
|
33
|
+
nargs="?",
|
|
34
|
+
default=None,
|
|
35
|
+
help="Texto da notícia (alternativa a --texto)",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument("--texto", default=None, help="Texto da notícia (predict)")
|
|
38
|
+
parser.add_argument("--fonte", default=None, help="Fonte opcional (predict)")
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"--json",
|
|
41
|
+
default=None,
|
|
42
|
+
help="System One: JSON (arquivo ou string) com state + questions",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--stdin",
|
|
46
|
+
action="store_true",
|
|
47
|
+
help="System One: ler envelope JSON de stdin",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--model",
|
|
51
|
+
default=DEFAULT_REPO,
|
|
52
|
+
help=f"Hub id ou pasta local (default: {DEFAULT_REPO})",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--revision",
|
|
56
|
+
default=None,
|
|
57
|
+
help="Revision Hub (commit/tag); default: SERIGY_HUB_REVISION / pin do pacote",
|
|
58
|
+
)
|
|
59
|
+
parser.add_argument(
|
|
60
|
+
"--allow-custom-models",
|
|
61
|
+
action="store_true",
|
|
62
|
+
help="Permite model_id/embedding fora da allowlist (só se confiar no checkpoint)",
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--hf-token",
|
|
66
|
+
default=None,
|
|
67
|
+
help="Token HF (ou env HF_TOKEN)",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--warm",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="Pré-carrega o sensor antes da predição",
|
|
73
|
+
)
|
|
74
|
+
args = parser.parse_args(argv)
|
|
75
|
+
|
|
76
|
+
modo_system_one = bool(args.json or args.stdin)
|
|
77
|
+
texto = args.texto or args.texto_posicional
|
|
78
|
+
if modo_system_one and texto:
|
|
79
|
+
parser.error("use --texto OU --json/--stdin, não ambos")
|
|
80
|
+
if not modo_system_one and not texto:
|
|
81
|
+
parser.error("informe --texto, argumento posicional, --json ou --stdin")
|
|
82
|
+
|
|
83
|
+
print(f"carregando {args.model}…", file=sys.stderr)
|
|
84
|
+
model = SerigyModel(
|
|
85
|
+
args.model,
|
|
86
|
+
token=args.hf_token,
|
|
87
|
+
warm=args.warm,
|
|
88
|
+
revision=args.revision,
|
|
89
|
+
allow_custom_models=args.allow_custom_models,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if modo_system_one:
|
|
93
|
+
payload = _carregar_json(args)
|
|
94
|
+
req = SystemOneRequest.model_validate(payload)
|
|
95
|
+
result = model.system_one(req.state, req.questions)
|
|
96
|
+
out = _saida_publica_system_one(result)
|
|
97
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
98
|
+
return 0
|
|
99
|
+
|
|
100
|
+
out = model.predict(texto, fonte=args.fonte)
|
|
101
|
+
print(
|
|
102
|
+
f"→ {out['predicao']} | classe={out['classe_ordinal']} "
|
|
103
|
+
f"| score={out['score_final']:.3f} "
|
|
104
|
+
f"| confiança={out['confianca_calibrada']:.3f}",
|
|
105
|
+
file=sys.stderr,
|
|
106
|
+
)
|
|
107
|
+
print(json.dumps(out, ensure_ascii=False, indent=2))
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
raise SystemExit(main())
|
serigy/api/__init__.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Schemas + motor de inferência (uso interno; API pública = SerigyModel)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from serigy.api.schemas import (
|
|
8
|
+
ComponentesConfianca,
|
|
9
|
+
CriteriosAvaliacao,
|
|
10
|
+
InputEco,
|
|
11
|
+
MetricasInferencia,
|
|
12
|
+
NivelOrdinal,
|
|
13
|
+
Noticia,
|
|
14
|
+
NoticiaRequest,
|
|
15
|
+
OutputPredicao,
|
|
16
|
+
PesosComponentes,
|
|
17
|
+
PredicaoResponse,
|
|
18
|
+
ProbabilidadesOrdinais,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from serigy.api.inferencia import MotorInferencia, predizer
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Noticia",
|
|
26
|
+
"NivelOrdinal",
|
|
27
|
+
"CriteriosAvaliacao",
|
|
28
|
+
"NoticiaRequest",
|
|
29
|
+
"InputEco",
|
|
30
|
+
"ComponentesConfianca",
|
|
31
|
+
"PesosComponentes",
|
|
32
|
+
"ProbabilidadesOrdinais",
|
|
33
|
+
"OutputPredicao",
|
|
34
|
+
"MetricasInferencia",
|
|
35
|
+
"PredicaoResponse",
|
|
36
|
+
"MotorInferencia",
|
|
37
|
+
"predizer",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def __getattr__(name: str) -> Any:
|
|
42
|
+
if name in ("MotorInferencia", "predizer"):
|
|
43
|
+
from serigy.api import inferencia as _inf
|
|
44
|
+
|
|
45
|
+
return getattr(_inf, name)
|
|
46
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
serigy/api/inferencia.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""Inferência end-to-end: NoticiaRequest → PredicaoResponse (FASE 5)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import time
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
from serigy.api.schemas import (
|
|
15
|
+
ComponentesConfianca,
|
|
16
|
+
InputEco,
|
|
17
|
+
MetricasInferencia,
|
|
18
|
+
NoticiaRequest,
|
|
19
|
+
OutputPredicao,
|
|
20
|
+
PesosComponentes,
|
|
21
|
+
PredicaoResponse,
|
|
22
|
+
ProbabilidadesOrdinais,
|
|
23
|
+
)
|
|
24
|
+
from serigy.components.sistema import SistemaConfianca
|
|
25
|
+
from serigy.config import CONFIG_PADRAO, ConfiguracaoSerigy, _raiz_repositorio
|
|
26
|
+
from serigy.dados.extrator import ExtratorHiddenStates, ResultadoExtracao
|
|
27
|
+
from serigy.checkpoint import carregar_sistema
|
|
28
|
+
from serigy.utils.ordinal import aplicar_cabeca_ordinal
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _como_float(x: Any) -> float:
|
|
32
|
+
"""Extrai escalar de array NumPy / lista / float."""
|
|
33
|
+
arr = np.asarray(x, dtype=np.float64)
|
|
34
|
+
if arr.size == 0:
|
|
35
|
+
raise ValueError("valor vazio")
|
|
36
|
+
return float(arr.reshape(-1)[0])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolver_checkpoint(config: ConfiguracaoSerigy | None = None) -> Path:
|
|
40
|
+
"""
|
|
41
|
+
Resolve caminho do `.safetensors`.
|
|
42
|
+
|
|
43
|
+
Aceita:
|
|
44
|
+
- arquivo `.safetensors`
|
|
45
|
+
- diretório com `serigy.safetensors` / `serigy_jev.safetensors`
|
|
46
|
+
- único `.safetensors` no diretório
|
|
47
|
+
"""
|
|
48
|
+
config = config or CONFIG_PADRAO
|
|
49
|
+
raiz = _raiz_repositorio()
|
|
50
|
+
base = Path(config.checkpoint_dir)
|
|
51
|
+
if not base.is_absolute():
|
|
52
|
+
base = raiz / base
|
|
53
|
+
|
|
54
|
+
if base.is_file() and base.suffix == ".safetensors":
|
|
55
|
+
return base
|
|
56
|
+
|
|
57
|
+
preferidos = (
|
|
58
|
+
"serigy.safetensors",
|
|
59
|
+
"serigy_jev.safetensors",
|
|
60
|
+
"modelo.safetensors",
|
|
61
|
+
"checkpoint.safetensors",
|
|
62
|
+
)
|
|
63
|
+
if base.is_dir():
|
|
64
|
+
for nome in preferidos:
|
|
65
|
+
candidato = base / nome
|
|
66
|
+
if candidato.is_file():
|
|
67
|
+
return candidato
|
|
68
|
+
encontrados = sorted(base.glob("*.safetensors"))
|
|
69
|
+
if encontrados:
|
|
70
|
+
return encontrados[0]
|
|
71
|
+
|
|
72
|
+
raise FileNotFoundError(
|
|
73
|
+
f"Checkpoint não encontrado em '{config.checkpoint_dir}'. "
|
|
74
|
+
"Baixe pesos do Hub (wendellperbar/serigy-jev) ou ajuste "
|
|
75
|
+
"SERIGY_CHECKPOINT_DIR / SerigyModel(caminho_local)."
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def gerar_request_id(texto: str) -> str:
|
|
80
|
+
"""ID: serigy_<sha256[:16]><uuid8>."""
|
|
81
|
+
digest = hashlib.sha256(texto.encode("utf-8")).hexdigest()[:16]
|
|
82
|
+
return f"serigy_{digest}{uuid.uuid4().hex[:8]}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _preparar_batch(extracao: ResultadoExtracao) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
86
|
+
"""Garante shapes de batch=1 para o SistemaConfianca."""
|
|
87
|
+
h = np.asarray(extracao.h_final, dtype=np.float64)
|
|
88
|
+
e = np.asarray(extracao.e_ref, dtype=np.float64)
|
|
89
|
+
layers = np.asarray(extracao.h_layers, dtype=np.float64)
|
|
90
|
+
|
|
91
|
+
if h.ndim == 1:
|
|
92
|
+
h = h[None, :]
|
|
93
|
+
if e.ndim == 1:
|
|
94
|
+
e = e[None, :]
|
|
95
|
+
# Cconv aceita (L, dim); se vier (L, 1, dim) ok também
|
|
96
|
+
if layers.ndim == 2:
|
|
97
|
+
pass
|
|
98
|
+
elif layers.ndim == 3 and layers.shape[1] == 1:
|
|
99
|
+
layers = layers[:, 0, :]
|
|
100
|
+
return h, e, layers
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class MotorInferencia:
|
|
104
|
+
"""
|
|
105
|
+
Orquestra extrator (sensor) + SistemaConfianca + cabeça ordinal.
|
|
106
|
+
|
|
107
|
+
Lazy-load do checkpoint; sensor sob demanda na primeira `predizer`.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
config: ConfiguracaoSerigy | None = None,
|
|
113
|
+
*,
|
|
114
|
+
caminho_checkpoint: Path | str | None = None,
|
|
115
|
+
extrator: ExtratorHiddenStates | None = None,
|
|
116
|
+
sistema: SistemaConfianca | None = None,
|
|
117
|
+
sigma: float | None = None,
|
|
118
|
+
prioridade_via_heads: bool | None = None,
|
|
119
|
+
):
|
|
120
|
+
"""
|
|
121
|
+
``prioridade_via_heads``:
|
|
122
|
+
- ``None`` (auto): usa head nativa se existir ``serigy_heads.safetensors``
|
|
123
|
+
e ``prioridade_modo == "nativa"``; senão RBF legado.
|
|
124
|
+
- ``True`` / ``False``: força o caminho.
|
|
125
|
+
"""
|
|
126
|
+
self.config = config or CONFIG_PADRAO
|
|
127
|
+
self._caminho_checkpoint = (
|
|
128
|
+
Path(caminho_checkpoint) if caminho_checkpoint else None
|
|
129
|
+
)
|
|
130
|
+
self._extrator = extrator
|
|
131
|
+
self._sistema = sistema
|
|
132
|
+
self._sigma = sigma
|
|
133
|
+
self._heads = None
|
|
134
|
+
self._prioridade_via_heads = prioridade_via_heads
|
|
135
|
+
self._heads_carregadas_do_disco = False
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def sigma(self) -> float:
|
|
139
|
+
self._garantir_sistema()
|
|
140
|
+
assert self._sigma is not None
|
|
141
|
+
return self._sigma
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def sistema(self) -> SistemaConfianca:
|
|
145
|
+
self._garantir_sistema()
|
|
146
|
+
assert self._sistema is not None
|
|
147
|
+
return self._sistema
|
|
148
|
+
|
|
149
|
+
def _garantir_sistema(self) -> None:
|
|
150
|
+
if self._sistema is not None and self._sigma is not None:
|
|
151
|
+
return
|
|
152
|
+
caminho = self._caminho_checkpoint or resolver_checkpoint(self.config)
|
|
153
|
+
self._sistema, self._sigma = carregar_sistema(caminho, config=self.config)
|
|
154
|
+
|
|
155
|
+
def _garantir_extrator(self) -> ExtratorHiddenStates:
|
|
156
|
+
if self._extrator is None:
|
|
157
|
+
self._extrator = ExtratorHiddenStates(self.config)
|
|
158
|
+
return self._extrator
|
|
159
|
+
|
|
160
|
+
def aquecer(self, *, carregar_sensor: bool = True) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Carrega checkpoint (+ sensor) antecipadamente.
|
|
163
|
+
|
|
164
|
+
Use no REPL para pagar o custo de Qwen uma vez só.
|
|
165
|
+
Com sensor, anexa MiniLM às heads (perguntas/opções/níveis).
|
|
166
|
+
"""
|
|
167
|
+
self._garantir_sistema()
|
|
168
|
+
if carregar_sensor:
|
|
169
|
+
self._garantir_extrator().aquecer()
|
|
170
|
+
self._garantir_heads()
|
|
171
|
+
self._sincronizar_embedder_heads()
|
|
172
|
+
|
|
173
|
+
def _deve_usar_prioridade_heads(self) -> bool:
|
|
174
|
+
if self._prioridade_via_heads is False:
|
|
175
|
+
return False
|
|
176
|
+
if self._prioridade_via_heads is True:
|
|
177
|
+
return True
|
|
178
|
+
# auto: só se pesos treinados estiverem no disco (evita regressão com init aleatório)
|
|
179
|
+
path = self._caminho_heads()
|
|
180
|
+
return path is not None and path.is_file()
|
|
181
|
+
|
|
182
|
+
def _ordinal_de_heads(
|
|
183
|
+
self,
|
|
184
|
+
h_vec: np.ndarray,
|
|
185
|
+
criterios: Any,
|
|
186
|
+
*,
|
|
187
|
+
score_torre: float,
|
|
188
|
+
) -> Any:
|
|
189
|
+
"""Prioridade nativa no tronco; ``score_final`` da resposta = torre (compat API)."""
|
|
190
|
+
from serigy.primitivos.prioridade_util import criterios_para_prioridade_question
|
|
191
|
+
from serigy.utils.ordinal import ResultadoOrdinal
|
|
192
|
+
|
|
193
|
+
heads = self._garantir_heads()
|
|
194
|
+
if getattr(heads, "prioridade_modo", "nativa") == "rbf":
|
|
195
|
+
return aplicar_cabeca_ordinal(
|
|
196
|
+
score_torre, criterios, sigma=self._sigma or CONFIG_PADRAO.ordinal_sigma
|
|
197
|
+
)
|
|
198
|
+
q = criterios_para_prioridade_question(criterios)
|
|
199
|
+
ans = heads.avaliar_prioridade(h_vec, q, score_final=score_torre)
|
|
200
|
+
probs = ans.probabilities
|
|
201
|
+
return ResultadoOrdinal(
|
|
202
|
+
score_final=float(np.clip(score_torre, 0.0, 1.0)),
|
|
203
|
+
probabilidades=(
|
|
204
|
+
float(probs["0"]),
|
|
205
|
+
float(probs["1"]),
|
|
206
|
+
float(probs["2"]),
|
|
207
|
+
),
|
|
208
|
+
classe_ordinal=int(ans.classe_ordinal),
|
|
209
|
+
predicao=ans.predicao,
|
|
210
|
+
entropia_shannon=float(ans.entropia_shannon),
|
|
211
|
+
confianca_calibrada=float(ans.confianca_calibrada),
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
def predizer_de_features(
|
|
215
|
+
self,
|
|
216
|
+
request: NoticiaRequest | dict[str, Any],
|
|
217
|
+
extracao: ResultadoExtracao,
|
|
218
|
+
*,
|
|
219
|
+
tempo_ms: float = 0.0,
|
|
220
|
+
request_id: str | None = None,
|
|
221
|
+
) -> PredicaoResponse:
|
|
222
|
+
"""Forward a partir de features já extraídas (testes / cache)."""
|
|
223
|
+
if isinstance(request, dict):
|
|
224
|
+
request = NoticiaRequest.model_validate(request)
|
|
225
|
+
|
|
226
|
+
self._garantir_sistema()
|
|
227
|
+
assert self._sistema is not None and self._sigma is not None
|
|
228
|
+
|
|
229
|
+
h, e, layers = _preparar_batch(extracao)
|
|
230
|
+
bruto = self._sistema(
|
|
231
|
+
estado_oculto=h,
|
|
232
|
+
embedding_referencia=e,
|
|
233
|
+
estados_camadas=layers,
|
|
234
|
+
)
|
|
235
|
+
csem = _como_float(bruto["score_csem"])
|
|
236
|
+
cconv = _como_float(bruto["score_cconv"])
|
|
237
|
+
clearned = _como_float(bruto["score_clearned"])
|
|
238
|
+
score_final = _como_float(bruto["score_final"])
|
|
239
|
+
h_vec = np.asarray(h, dtype=np.float64).reshape(-1)
|
|
240
|
+
|
|
241
|
+
w = self._sistema.config.pesos_normalizados
|
|
242
|
+
if self._deve_usar_prioridade_heads():
|
|
243
|
+
ordinal = self._ordinal_de_heads(
|
|
244
|
+
h_vec, request.criterios_avaliacao, score_torre=score_final
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
ordinal = aplicar_cabeca_ordinal(
|
|
248
|
+
score_final,
|
|
249
|
+
request.criterios_avaliacao,
|
|
250
|
+
sigma=self._sigma,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
return PredicaoResponse(
|
|
254
|
+
model=self.config.model_version,
|
|
255
|
+
timestamp=datetime.now(timezone.utc),
|
|
256
|
+
input=InputEco(
|
|
257
|
+
texto=request.noticia.texto,
|
|
258
|
+
fonte=request.noticia.fonte,
|
|
259
|
+
timestamp=request.noticia.timestamp,
|
|
260
|
+
),
|
|
261
|
+
criterios=request.criterios_avaliacao.mapa_labels(),
|
|
262
|
+
output=OutputPredicao(
|
|
263
|
+
predicao=ordinal.predicao,
|
|
264
|
+
classe_ordinal=ordinal.classe_ordinal, # type: ignore[arg-type]
|
|
265
|
+
score_final=ordinal.score_final,
|
|
266
|
+
confianca_calibrada=ordinal.confianca_calibrada,
|
|
267
|
+
entropia_shannon=ordinal.entropia_shannon,
|
|
268
|
+
componentes=ComponentesConfianca(
|
|
269
|
+
csem=csem, cconv=cconv, clearned=clearned
|
|
270
|
+
),
|
|
271
|
+
pesos=PesosComponentes(
|
|
272
|
+
w_csem=w[0], w_cconv=w[1], w_clearned=w[2]
|
|
273
|
+
),
|
|
274
|
+
probabilidades=ProbabilidadesOrdinais.model_validate(
|
|
275
|
+
ordinal.probs_dict()
|
|
276
|
+
),
|
|
277
|
+
),
|
|
278
|
+
metricas=MetricasInferencia(
|
|
279
|
+
tempo_ms=float(tempo_ms),
|
|
280
|
+
input_tokens=int(extracao.input_tokens),
|
|
281
|
+
output_tokens=0,
|
|
282
|
+
),
|
|
283
|
+
request_id=request_id or gerar_request_id(request.noticia.texto),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
def predizer(
|
|
287
|
+
self,
|
|
288
|
+
request: NoticiaRequest | dict[str, Any],
|
|
289
|
+
*,
|
|
290
|
+
request_id: str | None = None,
|
|
291
|
+
) -> PredicaoResponse:
|
|
292
|
+
"""Pipeline completo: texto → sensor → componentes → ordinal → response."""
|
|
293
|
+
if isinstance(request, dict):
|
|
294
|
+
request = NoticiaRequest.model_validate(request)
|
|
295
|
+
|
|
296
|
+
t0 = time.perf_counter()
|
|
297
|
+
extracao = self._garantir_extrator().extrair(request.noticia.texto)
|
|
298
|
+
tempo_ms = (time.perf_counter() - t0) * 1000.0
|
|
299
|
+
|
|
300
|
+
return self.predizer_de_features(
|
|
301
|
+
request,
|
|
302
|
+
extracao,
|
|
303
|
+
tempo_ms=tempo_ms,
|
|
304
|
+
request_id=request_id,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def computar_score_final(
|
|
308
|
+
self,
|
|
309
|
+
texto: str,
|
|
310
|
+
*,
|
|
311
|
+
extracao: ResultadoExtracao | None = None,
|
|
312
|
+
) -> tuple[float, int, float]:
|
|
313
|
+
"""
|
|
314
|
+
Uma passagem sensor+sistema → (score_final, input_tokens, tempo_ms).
|
|
315
|
+
|
|
316
|
+
Usado por system_one (adapters) para não reextrair por pergunta.
|
|
317
|
+
"""
|
|
318
|
+
score_final, _h, tokens, tempo = self.computar_representacao(
|
|
319
|
+
texto, extracao=extracao
|
|
320
|
+
)
|
|
321
|
+
return score_final, tokens, tempo
|
|
322
|
+
|
|
323
|
+
def computar_representacao(
|
|
324
|
+
self,
|
|
325
|
+
texto: str,
|
|
326
|
+
*,
|
|
327
|
+
extracao: ResultadoExtracao | None = None,
|
|
328
|
+
) -> tuple[float, np.ndarray, int, float]:
|
|
329
|
+
"""
|
|
330
|
+
Sensor+sistema → (score_final, h_state, input_tokens, tempo_ms).
|
|
331
|
+
|
|
332
|
+
``h_state`` alimenta as perguntas tipadas System One.
|
|
333
|
+
"""
|
|
334
|
+
self._garantir_sistema()
|
|
335
|
+
assert self._sistema is not None
|
|
336
|
+
|
|
337
|
+
t0 = time.perf_counter()
|
|
338
|
+
if extracao is None:
|
|
339
|
+
extracao = self._garantir_extrator().extrair(texto)
|
|
340
|
+
tempo_ms = (time.perf_counter() - t0) * 1000.0
|
|
341
|
+
else:
|
|
342
|
+
tempo_ms = 0.0
|
|
343
|
+
|
|
344
|
+
h, e, layers = _preparar_batch(extracao)
|
|
345
|
+
bruto = self._sistema(
|
|
346
|
+
estado_oculto=h,
|
|
347
|
+
embedding_referencia=e,
|
|
348
|
+
estados_camadas=layers,
|
|
349
|
+
)
|
|
350
|
+
score_final = _como_float(bruto["score_final"])
|
|
351
|
+
h_vec = np.asarray(h, dtype=np.float64).reshape(-1)
|
|
352
|
+
return score_final, h_vec, int(extracao.input_tokens), float(tempo_ms)
|
|
353
|
+
|
|
354
|
+
def _caminho_heads(self) -> Path | None:
|
|
355
|
+
caminho = self._caminho_checkpoint
|
|
356
|
+
if caminho is None:
|
|
357
|
+
try:
|
|
358
|
+
caminho = resolver_checkpoint(self.config)
|
|
359
|
+
except FileNotFoundError:
|
|
360
|
+
return None
|
|
361
|
+
from serigy.primitivos.persist import caminho_heads_ao_lado
|
|
362
|
+
|
|
363
|
+
return caminho_heads_ao_lado(caminho)
|
|
364
|
+
|
|
365
|
+
def _garantir_heads(self) -> Any:
|
|
366
|
+
if getattr(self, "_heads", None) is not None:
|
|
367
|
+
return self._heads
|
|
368
|
+
|
|
369
|
+
from serigy.primitivos.encode import HashTextEmbedder
|
|
370
|
+
from serigy.primitivos.heads import ConfigHeads, SistemaHeads
|
|
371
|
+
from serigy.primitivos.persist import carregar_heads
|
|
372
|
+
|
|
373
|
+
dim_q = int(self.config.embedding_dim)
|
|
374
|
+
heads_path = self._caminho_heads()
|
|
375
|
+
if heads_path is not None and heads_path.is_file():
|
|
376
|
+
# embedder=None → HashTextEmbedder com seed gravado no checkpoint
|
|
377
|
+
self._heads = carregar_heads(heads_path, embedder=None)
|
|
378
|
+
self._heads_carregadas_do_disco = True
|
|
379
|
+
else:
|
|
380
|
+
# dim_h: projeção do h do sensor; dim_q alinhado ao MiniLM
|
|
381
|
+
self._heads = SistemaHeads(
|
|
382
|
+
ConfigHeads(dim_h=256, dim_q=dim_q, dim_z=256),
|
|
383
|
+
embedder=HashTextEmbedder(dim_q, seed=7),
|
|
384
|
+
)
|
|
385
|
+
self._heads_carregadas_do_disco = False
|
|
386
|
+
self._sincronizar_embedder_heads()
|
|
387
|
+
return self._heads
|
|
388
|
+
|
|
389
|
+
def _sincronizar_embedder_heads(self) -> None:
|
|
390
|
+
"""Se MiniLM já estiver carregado no extrator, usa-o nas perguntas."""
|
|
391
|
+
if self._heads is None:
|
|
392
|
+
return
|
|
393
|
+
if getattr(self._heads, "_embedder_fonte", None) == "minilm":
|
|
394
|
+
return
|
|
395
|
+
ext = self._extrator
|
|
396
|
+
if ext is None or getattr(ext, "_sentence_model", None) is None:
|
|
397
|
+
return
|
|
398
|
+
from serigy.primitivos.encode import CallableTextEmbedder
|
|
399
|
+
|
|
400
|
+
dim_q = int(self._heads.config.dim_q)
|
|
401
|
+
|
|
402
|
+
def _batch(textos: list[str]) -> np.ndarray:
|
|
403
|
+
return np.asarray(ext.embed_texts(textos), dtype=np.float64)
|
|
404
|
+
|
|
405
|
+
self._heads.definir_embedder(
|
|
406
|
+
CallableTextEmbedder(_batch, dim_q, batch=True),
|
|
407
|
+
fonte="minilm",
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
def system_one(
|
|
411
|
+
self,
|
|
412
|
+
state: str | dict[str, Any] | list[Any] | Any,
|
|
413
|
+
questions: dict[str, Any] | None = None,
|
|
414
|
+
*,
|
|
415
|
+
extracao: ResultadoExtracao | None = None,
|
|
416
|
+
usar_heads: bool | None = None,
|
|
417
|
+
) -> Any:
|
|
418
|
+
"""
|
|
419
|
+
System One: um state, N perguntas tipadas.
|
|
420
|
+
|
|
421
|
+
``usar_heads`` é interno (testes/monorepo):
|
|
422
|
+
- ``None`` (default): usa sidecar de heads se existir; senão caminho legado
|
|
423
|
+
- ``True`` / ``False``: força o caminho
|
|
424
|
+
"""
|
|
425
|
+
from serigy.primitivos.system_one import (
|
|
426
|
+
normalizar_request,
|
|
427
|
+
system_one_de_heads,
|
|
428
|
+
system_one_de_score,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
texto, qs, model = normalizar_request(
|
|
432
|
+
state,
|
|
433
|
+
questions,
|
|
434
|
+
model=self.config.model_version,
|
|
435
|
+
)
|
|
436
|
+
self._garantir_sistema()
|
|
437
|
+
assert self._sigma is not None
|
|
438
|
+
score_final, h_vec, tokens, _tempo = self.computar_representacao(
|
|
439
|
+
texto, extracao=extracao
|
|
440
|
+
)
|
|
441
|
+
modelo = model or self.config.model_version
|
|
442
|
+
if usar_heads is None:
|
|
443
|
+
heads_path = self._caminho_heads()
|
|
444
|
+
if heads_path is not None and heads_path.is_file():
|
|
445
|
+
usar_heads = True
|
|
446
|
+
else:
|
|
447
|
+
# Adapters legados só cobrem Escolha N=3; N≠3 exige heads.
|
|
448
|
+
from serigy.primitivos.schemas import EscolhaQuestion
|
|
449
|
+
|
|
450
|
+
usar_heads = any(
|
|
451
|
+
isinstance(q, EscolhaQuestion) and len(q.criteria) != 3
|
|
452
|
+
for q in qs.values()
|
|
453
|
+
)
|
|
454
|
+
if usar_heads:
|
|
455
|
+
heads = self._garantir_heads()
|
|
456
|
+
self._sincronizar_embedder_heads()
|
|
457
|
+
return system_one_de_heads(
|
|
458
|
+
h_vec,
|
|
459
|
+
qs,
|
|
460
|
+
heads,
|
|
461
|
+
score_final=score_final,
|
|
462
|
+
model=modelo,
|
|
463
|
+
input_tokens=tokens,
|
|
464
|
+
sigma=self._sigma,
|
|
465
|
+
)
|
|
466
|
+
return system_one_de_score(
|
|
467
|
+
score_final,
|
|
468
|
+
qs,
|
|
469
|
+
model=modelo,
|
|
470
|
+
input_tokens=tokens,
|
|
471
|
+
sigma=self._sigma,
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def predizer(
|
|
476
|
+
request: NoticiaRequest | dict[str, Any],
|
|
477
|
+
*,
|
|
478
|
+
config: ConfiguracaoSerigy | None = None,
|
|
479
|
+
caminho_checkpoint: Path | str | None = None,
|
|
480
|
+
) -> PredicaoResponse:
|
|
481
|
+
"""Atalho one-shot (preferir `MotorInferencia` para reuso do sensor)."""
|
|
482
|
+
return MotorInferencia(
|
|
483
|
+
config=config, caminho_checkpoint=caminho_checkpoint
|
|
484
|
+
).predizer(request)
|