Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- src/acquisition/__pycache__/normalizer.cpython-313.pyc +0 -0
- src/acquisition/__pycache__/orchestrator.cpython-313.pyc +0 -0
- src/acquisition/classifiers/__pycache__/canonicality.cpython-313.pyc +0 -0
- src/acquisition/classifiers/canonicality.py +34 -0
- src/acquisition/extractors/__pycache__/anilist.cpython-313.pyc +0 -0
- src/acquisition/extractors/__pycache__/danbooru.cpython-313.pyc +0 -0
- src/acquisition/extractors/anilist.py +26 -0
- src/acquisition/extractors/danbooru.py +26 -0
- src/acquisition/normalizer.py +10 -0
- src/acquisition/orchestrator.py +47 -0
- src/acquisition/pipeline.py +1 -0
- src/api/__pycache__/app.cpython-312.pyc +0 -0
- src/api/app.py +53 -0
- src/benchmarks/__pycache__/benchmark.cpython-312.pyc +0 -0
- src/benchmarks/__pycache__/semantic_quality_audit.cpython-312.pyc +0 -0
- src/benchmarks/benchmark.py +163 -0
- src/benchmarks/expand_characters.py +139 -0
- src/benchmarks/external_benchmark.py +171 -0
- src/benchmarks/prompt_eval.py +79 -0
- src/benchmarks/resolution_audit.py +148 -0
- src/benchmarks/semantic_quality_audit.py +169 -0
- src/benchmarks/test_large_scale.py +145 -0
- src/benchmarks/test_rebuild.py +41 -0
- src/benchmarks/threshold_analysis.py +102 -0
- src/cli/__pycache__/main.cpython-312.pyc +0 -0
- src/cli/__pycache__/migrate_v3.cpython-313.pyc +0 -0
- src/cli/__pycache__/ontology.cpython-312.pyc +0 -0
- src/cli/main.py +65 -0
- src/cli/migrate_v3.py +45 -0
- src/cli/ontology.py +120 -0
- src/datasets/curator.py +191 -0
- src/datasets/expander.py +110 -0
- src/embeddings/__pycache__/engine.cpython-312.pyc +0 -0
- src/embeddings/build_index.py +13 -0
- src/embeddings/engine.py +125 -0
- src/enrichment/__pycache__/enricher.cpython-312.pyc +0 -0
- src/enrichment/__pycache__/enricher.cpython-313.pyc +0 -0
- src/enrichment/enricher.py +63 -0
- src/governance/__pycache__/canonicality.cpython-313.pyc +0 -0
- src/governance/canonicality.py +3 -0
- src/knowledge/__pycache__/builder.cpython-313.pyc +0 -0
- src/knowledge/__pycache__/consensus.cpython-313.pyc +0 -0
- src/knowledge/__pycache__/engine.cpython-312.pyc +0 -0
- src/knowledge/__pycache__/engine.cpython-313.pyc +0 -0
- src/knowledge/__pycache__/mapping_engine.cpython-313.pyc +0 -0
- src/knowledge/__pycache__/mapping_models.cpython-313.pyc +0 -0
- src/knowledge/__pycache__/mapping_validator.cpython-313.pyc +0 -0
- src/knowledge/__pycache__/repository.cpython-312.pyc +0 -0
- src/knowledge/__pycache__/repository.cpython-313.pyc +0 -0
- src/knowledge/__pycache__/scoring.cpython-313.pyc +0 -0
src/acquisition/__pycache__/normalizer.cpython-313.pyc
ADDED
|
Binary file (1.01 kB). View file
|
|
|
src/acquisition/__pycache__/orchestrator.cpython-313.pyc
ADDED
|
Binary file (3.54 kB). View file
|
|
|
src/acquisition/classifiers/__pycache__/canonicality.cpython-313.pyc
ADDED
|
Binary file (2.17 kB). View file
|
|
|
src/acquisition/classifiers/canonicality.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Dict, Any
|
| 2 |
+
|
| 3 |
+
class CanonicalityClassifier:
|
| 4 |
+
def __init__(self):
|
| 5 |
+
self.forbidden_tags = {"cosplay", "crossover", "parody", "meme", "fanon", "clothes_swap"}
|
| 6 |
+
|
| 7 |
+
def classify(self, tags: List[str], description: str = "") -> str:
|
| 8 |
+
tags_set = set(t.lower() for t in tags)
|
| 9 |
+
desc_lower = description.lower()
|
| 10 |
+
|
| 11 |
+
for forbidden in self.forbidden_tags:
|
| 12 |
+
if forbidden in tags_set or forbidden in desc_lower:
|
| 13 |
+
if forbidden == "crossover":
|
| 14 |
+
return "crossover"
|
| 15 |
+
if forbidden == "cosplay":
|
| 16 |
+
return "cosplay"
|
| 17 |
+
if forbidden == "parody" or forbidden == "meme":
|
| 18 |
+
return "parody"
|
| 19 |
+
if forbidden == "fanon":
|
| 20 |
+
return "fanon"
|
| 21 |
+
|
| 22 |
+
if "variant" in tags_set or "alternate_costume" in tags_set:
|
| 23 |
+
return "variant"
|
| 24 |
+
|
| 25 |
+
return "canonical"
|
| 26 |
+
|
| 27 |
+
def filter_record(self, record: Dict[str, Any]) -> bool:
|
| 28 |
+
"""Returns True if the record should be kept for consensus, False otherwise."""
|
| 29 |
+
tags = record.get("tags", [])
|
| 30 |
+
desc = record.get("description", "")
|
| 31 |
+
classification = self.classify(tags, desc)
|
| 32 |
+
|
| 33 |
+
# Only canonical and variant are eligible for consensus
|
| 34 |
+
return classification in ("canonical", "variant")
|
src/acquisition/extractors/__pycache__/anilist.cpython-313.pyc
ADDED
|
Binary file (926 Bytes). View file
|
|
|
src/acquisition/extractors/__pycache__/danbooru.cpython-313.pyc
ADDED
|
Binary file (894 Bytes). View file
|
|
|
src/acquisition/extractors/anilist.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any, Optional
|
| 2 |
+
|
| 3 |
+
def extract_anilist_character(char_id: str) -> Optional[Dict[str, Any]]:
|
| 4 |
+
# In a real scenario, this would use a GraphQL client against AniList API.
|
| 5 |
+
# We return mock data for demonstration of the data lake.
|
| 6 |
+
mock_db = {
|
| 7 |
+
"amy_rose": {
|
| 8 |
+
"id": "amy_rose",
|
| 9 |
+
"aliases": ["Rosy the Rascal"],
|
| 10 |
+
"franchise": "Sonic the Hedgehog",
|
| 11 |
+
"description": "A pink hedgehog with a red dress and a piko piko hammer."
|
| 12 |
+
},
|
| 13 |
+
"sonic": {
|
| 14 |
+
"id": "sonic",
|
| 15 |
+
"aliases": ["The Blue Blur"],
|
| 16 |
+
"franchise": "Sonic the Hedgehog",
|
| 17 |
+
"description": "A blue hedgehog with red shoes."
|
| 18 |
+
},
|
| 19 |
+
"bowsette": {
|
| 20 |
+
"id": "bowsette",
|
| 21 |
+
"aliases": ["Koopa Princess"],
|
| 22 |
+
"franchise": "Super Mario (Fanon)",
|
| 23 |
+
"description": "A fan-made version of Bowser using the Super Crown."
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
return mock_db.get(char_id)
|
src/acquisition/extractors/danbooru.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Dict, Any, Optional
|
| 2 |
+
|
| 3 |
+
def extract_danbooru_post(post_id: str) -> Optional[Dict[str, Any]]:
|
| 4 |
+
# In a real scenario, this would use the Danbooru API.
|
| 5 |
+
# We return mock data to build the raw JSONL in the staging lake.
|
| 6 |
+
mock_db = {
|
| 7 |
+
"p1": {
|
| 8 |
+
"id": "p1",
|
| 9 |
+
"copyright": "sonic_the_hedgehog",
|
| 10 |
+
"character": "amy_rose",
|
| 11 |
+
"tags": ["1girl", "pink_hair", "animal_ears", "red_dress", "holding_weapon", "hammer"]
|
| 12 |
+
},
|
| 13 |
+
"p2": {
|
| 14 |
+
"id": "p2",
|
| 15 |
+
"copyright": "sonic_the_hedgehog",
|
| 16 |
+
"character": "amy_rose",
|
| 17 |
+
"tags": ["1girl", "pink_hair", "red_dress", "crossover", "mario_bros"]
|
| 18 |
+
},
|
| 19 |
+
"p3": {
|
| 20 |
+
"id": "p3",
|
| 21 |
+
"copyright": "super_mario_bros",
|
| 22 |
+
"character": "bowsette",
|
| 23 |
+
"tags": ["1girl", "blonde_hair", "horns", "spiked_shell", "black_dress", "fanon"]
|
| 24 |
+
}
|
| 25 |
+
}
|
| 26 |
+
return mock_db.get(post_id)
|
src/acquisition/normalizer.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
class Normalizer:
|
| 3 |
+
def __init__(self):
|
| 4 |
+
self.rules = {
|
| 5 |
+
"orange sweater": "orange_turtleneck",
|
| 6 |
+
"orange pullover": "orange_turtleneck",
|
| 7 |
+
"pink hair": "pink_quills"
|
| 8 |
+
}
|
| 9 |
+
def normalize(self, tag: str) -> str:
|
| 10 |
+
return self.rules.get(tag.lower(), tag.lower().replace(" ", "_"))
|
src/acquisition/orchestrator.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
from typing import List, Dict, Any, Callable
|
| 5 |
+
|
| 6 |
+
class ScraplingOrchestrator:
|
| 7 |
+
def __init__(self, output_dir: str, rate_limit_ms: int = 1000):
|
| 8 |
+
self.output_dir = output_dir
|
| 9 |
+
self.rate_limit_ms = rate_limit_ms
|
| 10 |
+
self.checkpoints_file = os.path.join(output_dir, "checkpoint.json")
|
| 11 |
+
self.processed_ids = set()
|
| 12 |
+
self._load_checkpoint()
|
| 13 |
+
|
| 14 |
+
def _load_checkpoint(self):
|
| 15 |
+
if os.path.exists(self.checkpoints_file):
|
| 16 |
+
with open(self.checkpoints_file, "r") as f:
|
| 17 |
+
self.processed_ids = set(json.load(f))
|
| 18 |
+
|
| 19 |
+
def _save_checkpoint(self):
|
| 20 |
+
with open(self.checkpoints_file, "w") as f:
|
| 21 |
+
json.dump(list(self.processed_ids), f)
|
| 22 |
+
|
| 23 |
+
def execute_job(self, job_name: str, ids: List[str], extractor_fn: Callable[[str], Dict[str, Any]]):
|
| 24 |
+
output_file = os.path.join(self.output_dir, f"{job_name}.jsonl")
|
| 25 |
+
|
| 26 |
+
with open(output_file, "a", encoding="utf-8") as f:
|
| 27 |
+
for item_id in ids:
|
| 28 |
+
if item_id in self.processed_ids:
|
| 29 |
+
continue
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
# Simulate API rate limit
|
| 33 |
+
time.sleep(self.rate_limit_ms / 1000.0)
|
| 34 |
+
|
| 35 |
+
data = extractor_fn(item_id)
|
| 36 |
+
if data:
|
| 37 |
+
f.write(json.dumps(data) + "\n")
|
| 38 |
+
|
| 39 |
+
self.processed_ids.add(item_id)
|
| 40 |
+
|
| 41 |
+
if len(self.processed_ids) % 10 == 0:
|
| 42 |
+
self._save_checkpoint()
|
| 43 |
+
except Exception as e:
|
| 44 |
+
print(f"Error extracting {item_id}: {e}")
|
| 45 |
+
# Retries could be implemented here
|
| 46 |
+
|
| 47 |
+
self._save_checkpoint()
|
src/acquisition/pipeline.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Pipeline Orchestrator
|
src/api/__pycache__/app.cpython-312.pyc
ADDED
|
Binary file (2.73 kB). View file
|
|
|
src/api/app.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Query
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
from src.ontology.matcher import ConceptMatcher
|
| 5 |
+
from src.parser.parser import Parser
|
| 6 |
+
from src.enrichment.enricher import Enricher
|
| 7 |
+
from src.prompt_builder.builder import PromptBuilder
|
| 8 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 9 |
+
from src.ontology.models import PromptIR
|
| 10 |
+
|
| 11 |
+
from src.knowledge.engine import KnowledgeEngine
|
| 12 |
+
|
| 13 |
+
app = FastAPI(title="Prompt Compiler API v3.1")
|
| 14 |
+
|
| 15 |
+
# Initialize components
|
| 16 |
+
ONTOLOGY_DIR = "data/ontology"
|
| 17 |
+
INDEX_PATH = "data/faiss_indices"
|
| 18 |
+
KNOWLEDGE_DB = "data/knowledge/build/knowledge.db"
|
| 19 |
+
|
| 20 |
+
matcher = ConceptMatcher(ONTOLOGY_DIR)
|
| 21 |
+
knowledge_engine = KnowledgeEngine(KNOWLEDGE_DB)
|
| 22 |
+
embedding_engine = EmbeddingEngine(index_dir=INDEX_PATH)
|
| 23 |
+
|
| 24 |
+
# ... (rest of imports remains the same)
|
| 25 |
+
|
| 26 |
+
parser_engine = Parser(matcher, embedding_engine=embedding_engine)
|
| 27 |
+
enricher = Enricher(matcher, knowledge_engine=knowledge_engine)
|
| 28 |
+
|
| 29 |
+
class CompileRequest(BaseModel):
|
| 30 |
+
text: str
|
| 31 |
+
safe_mode: bool = True
|
| 32 |
+
semantic: bool = True
|
| 33 |
+
profile: str = "generic"
|
| 34 |
+
|
| 35 |
+
class CompileResponse(BaseModel):
|
| 36 |
+
ir: PromptIR
|
| 37 |
+
prompt: dict
|
| 38 |
+
|
| 39 |
+
@app.post("/compile", response_model=CompileResponse)
|
| 40 |
+
async def compile_prompt(request: CompileRequest):
|
| 41 |
+
# Select parser based on semantic flag
|
| 42 |
+
engine = parser_engine if request.semantic else Parser(matcher)
|
| 43 |
+
builder = PromptBuilder(profile_name=request.profile)
|
| 44 |
+
|
| 45 |
+
ir = engine.parse(request.text, safe_mode=request.safe_mode)
|
| 46 |
+
ir = enricher.enrich(ir)
|
| 47 |
+
prompt = builder.build(ir)
|
| 48 |
+
|
| 49 |
+
return CompileResponse(ir=ir, prompt=prompt)
|
| 50 |
+
|
| 51 |
+
@app.get("/health")
|
| 52 |
+
async def health():
|
| 53 |
+
return {"status": "ok"}
|
src/benchmarks/__pycache__/benchmark.cpython-312.pyc
ADDED
|
Binary file (9.1 kB). View file
|
|
|
src/benchmarks/__pycache__/semantic_quality_audit.cpython-312.pyc
ADDED
|
Binary file (8.25 kB). View file
|
|
|
src/benchmarks/benchmark.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import random
|
| 3 |
+
import os
|
| 4 |
+
from typing import List, Dict, Any
|
| 5 |
+
from src.parser.parser import Parser
|
| 6 |
+
from src.ontology.matcher import ConceptMatcher
|
| 7 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 8 |
+
from src.enrichment.enricher import Enricher
|
| 9 |
+
|
| 10 |
+
class BenchmarkSuite:
|
| 11 |
+
def __init__(self, ontology_dir: str):
|
| 12 |
+
self.ontology_dir = ontology_dir
|
| 13 |
+
self.concepts = self._load_all_concepts()
|
| 14 |
+
|
| 15 |
+
def _load_all_concepts(self) -> Dict[str, List[str]]:
|
| 16 |
+
concepts = {}
|
| 17 |
+
for filename in os.listdir(self.ontology_dir):
|
| 18 |
+
if filename.endswith(".json"):
|
| 19 |
+
path = os.path.join(self.ontology_dir, filename)
|
| 20 |
+
category = filename.replace(".json", "")
|
| 21 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 22 |
+
data = json.load(f)
|
| 23 |
+
concepts[category] = [item["canonical"] for item in data]
|
| 24 |
+
return concepts
|
| 25 |
+
|
| 26 |
+
def generate_benchmarks(self, count: int = 1000) -> List[Dict[str, Any]]:
|
| 27 |
+
benchmarks = []
|
| 28 |
+
|
| 29 |
+
# Core characters
|
| 30 |
+
sonic_chars = ["Sonic", "Amy Rose", "Shadow", "Rouge", "Blaze", "Silver", "Knuckles", "Tails", "Cream"]
|
| 31 |
+
|
| 32 |
+
for _ in range(count):
|
| 33 |
+
# Mix strategies
|
| 34 |
+
strategy = random.random()
|
| 35 |
+
|
| 36 |
+
if strategy < 0.3: # Character focus
|
| 37 |
+
char = random.choice(sonic_chars)
|
| 38 |
+
clothing = random.choice(self.concepts.get("clothing", ["dress"]))
|
| 39 |
+
scene = random.choice(self.concepts.get("scenes", ["beach"]))
|
| 40 |
+
input_text = f"{char} wearing a {clothing} at the {scene}"
|
| 41 |
+
expected = [char, clothing, scene]
|
| 42 |
+
elif strategy < 0.6: # Attribute focus
|
| 43 |
+
style = random.choice(self.concepts.get("styles", ["anime"]))
|
| 44 |
+
hair = random.choice(self.concepts.get("hair_colors", ["blue hair"]))
|
| 45 |
+
eyes = random.choice(self.concepts.get("eye_colors", ["green eyes"]))
|
| 46 |
+
input_text = f"{style} style girl with {hair} and {eyes}"
|
| 47 |
+
expected = [style, hair, eyes]
|
| 48 |
+
else: # Random mix
|
| 49 |
+
parts = []
|
| 50 |
+
expected = []
|
| 51 |
+
for cat in ["clothing", "accessories", "scenes", "emotions", "lighting"]:
|
| 52 |
+
if random.random() > 0.5 and self.concepts.get(cat):
|
| 53 |
+
val = random.choice(self.concepts[cat])
|
| 54 |
+
parts.append(val)
|
| 55 |
+
expected.append(val)
|
| 56 |
+
input_text = " ".join(parts)
|
| 57 |
+
|
| 58 |
+
if input_text:
|
| 59 |
+
benchmarks.append({
|
| 60 |
+
"input": input_text,
|
| 61 |
+
"expected_entities": expected
|
| 62 |
+
})
|
| 63 |
+
|
| 64 |
+
return benchmarks
|
| 65 |
+
|
| 66 |
+
def evaluate(self, benchmarks: List[Dict[str, Any]], parser: Parser, enricher: Enricher) -> Dict[str, Any]:
|
| 67 |
+
results = []
|
| 68 |
+
total_precision = 0
|
| 69 |
+
total_recall = 0
|
| 70 |
+
|
| 71 |
+
exact_match_count = 0
|
| 72 |
+
entity_resolved_count = 0
|
| 73 |
+
total_expected_entities = 0
|
| 74 |
+
|
| 75 |
+
for case in benchmarks:
|
| 76 |
+
ir = parser.parse(case["input"])
|
| 77 |
+
ir = enricher.enrich(ir)
|
| 78 |
+
|
| 79 |
+
# Extract all found canonicals
|
| 80 |
+
found = set()
|
| 81 |
+
for char in ir.characters:
|
| 82 |
+
found.add(char.name)
|
| 83 |
+
found.update(char.appearance)
|
| 84 |
+
found.update(char.clothing)
|
| 85 |
+
found.update(char.accessories)
|
| 86 |
+
found.update(char.pose)
|
| 87 |
+
found.update(char.expression)
|
| 88 |
+
found.update(ir.scene.locations)
|
| 89 |
+
found.update(ir.scene.lighting)
|
| 90 |
+
found.update(ir.scene.atmosphere)
|
| 91 |
+
found.update(ir.style)
|
| 92 |
+
found.update(ir.effects)
|
| 93 |
+
|
| 94 |
+
expected = set(case["expected_entities"])
|
| 95 |
+
|
| 96 |
+
intersection = found.intersection(expected)
|
| 97 |
+
precision = len(intersection) / len(found) if found else 0
|
| 98 |
+
recall = len(intersection) / len(expected) if expected else 1.0
|
| 99 |
+
|
| 100 |
+
total_precision += precision
|
| 101 |
+
total_recall += recall
|
| 102 |
+
total_expected_entities += len(expected)
|
| 103 |
+
entity_resolved_count += len(intersection)
|
| 104 |
+
|
| 105 |
+
if recall == 1.0:
|
| 106 |
+
exact_match_count += 1
|
| 107 |
+
|
| 108 |
+
results.append({
|
| 109 |
+
"input": case["input"],
|
| 110 |
+
"expected": list(expected),
|
| 111 |
+
"found": list(found),
|
| 112 |
+
"precision": precision,
|
| 113 |
+
"recall": recall
|
| 114 |
+
})
|
| 115 |
+
|
| 116 |
+
avg_precision = total_precision / len(benchmarks)
|
| 117 |
+
avg_recall = total_recall / len(benchmarks)
|
| 118 |
+
f1 = 2 * (avg_precision * avg_recall) / (avg_precision + avg_recall) if (avg_precision + avg_recall) else 0
|
| 119 |
+
|
| 120 |
+
return {
|
| 121 |
+
"metrics": {
|
| 122 |
+
"precision": round(avg_precision, 4),
|
| 123 |
+
"recall": round(avg_recall, 4),
|
| 124 |
+
"f1": round(f1, 4),
|
| 125 |
+
"exact_match_rate": round(exact_match_count / len(benchmarks), 4),
|
| 126 |
+
"entity_resolution_accuracy": round(entity_resolved_count / total_expected_entities if total_expected_entities else 0, 4)
|
| 127 |
+
},
|
| 128 |
+
"total_cases": len(benchmarks),
|
| 129 |
+
"results": results
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
if __name__ == "__main__":
|
| 133 |
+
suite = BenchmarkSuite("data/ontology")
|
| 134 |
+
|
| 135 |
+
print("Generating 1000 benchmarks...")
|
| 136 |
+
benchmarks = suite.generate_benchmarks(1000)
|
| 137 |
+
with open("benchmarks/prompts.json", "w", encoding="utf-8") as f:
|
| 138 |
+
json.dump(benchmarks, f, indent=2)
|
| 139 |
+
|
| 140 |
+
print("Running evaluation...")
|
| 141 |
+
matcher = ConceptMatcher("data/ontology")
|
| 142 |
+
engine = EmbeddingEngine(index_dir="data/faiss_indices")
|
| 143 |
+
engine.load_index()
|
| 144 |
+
parser = Parser(matcher, engine)
|
| 145 |
+
enricher = Enricher(matcher)
|
| 146 |
+
|
| 147 |
+
report = suite.evaluate(benchmarks, parser, enricher)
|
| 148 |
+
|
| 149 |
+
os.makedirs("reports", exist_ok=True)
|
| 150 |
+
with open("reports/benchmark_results.json", "w", encoding="utf-8") as f:
|
| 151 |
+
json.dump(report, f, indent=2)
|
| 152 |
+
|
| 153 |
+
with open("reports/benchmark_results.md", "w", encoding="utf-8") as f:
|
| 154 |
+
f.write("# Benchmark Results\n\n")
|
| 155 |
+
m = report["metrics"]
|
| 156 |
+
f.write(f"- **Total Cases**: {report['total_cases']}\n")
|
| 157 |
+
f.write(f"- **Precision**: {m['precision']}\n")
|
| 158 |
+
f.write(f"- **Recall**: {m['recall']}\n")
|
| 159 |
+
f.write(f"- **F1 Score**: {m['f1']}\n")
|
| 160 |
+
f.write(f"- **Exact Match Rate**: {m['exact_match_rate']}\n")
|
| 161 |
+
f.write(f"- **Entity Resolution Accuracy**: {m['entity_resolution_accuracy']}\n")
|
| 162 |
+
|
| 163 |
+
print("Benchmark evaluation complete. Reports generated in reports/")
|
src/benchmarks/expand_characters.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import uuid
|
| 4 |
+
import random
|
| 5 |
+
|
| 6 |
+
def expand_characters():
|
| 7 |
+
path = "data/ontology/characters.json"
|
| 8 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 9 |
+
data = json.load(f)
|
| 10 |
+
|
| 11 |
+
canonicals = {item["canonical"].lower() for item in data}
|
| 12 |
+
|
| 13 |
+
new_chars = [
|
| 14 |
+
# Anime
|
| 15 |
+
{"name": "Goku", "series": "Dragon Ball", "species": "Saiyan", "gender": "male", "tags": ["martial artist", "orange gi"]},
|
| 16 |
+
{"name": "Naruto Uzumaki", "series": "Naruto", "species": "Human", "gender": "male", "tags": ["ninja", "orange jumpsuit"]},
|
| 17 |
+
{"name": "Monkey D. Luffy", "series": "One Piece", "species": "Human", "gender": "male", "tags": ["pirate", "straw hat"]},
|
| 18 |
+
{"name": "Rem", "series": "Re:Zero", "species": "Oni", "gender": "female", "tags": ["maid", "blue hair"]},
|
| 19 |
+
{"name": "Ram", "series": "Re:Zero", "species": "Oni", "gender": "female", "tags": ["maid", "pink hair"]},
|
| 20 |
+
{"name": "Asuka Langley Soryu", "series": "Neon Genesis Evangelion", "species": "Human", "gender": "female", "tags": ["pilot", "plugsuit", "red hair"]},
|
| 21 |
+
{"name": "Rei Ayanami", "series": "Neon Genesis Evangelion", "species": "Human", "gender": "female", "tags": ["pilot", "plugsuit", "blue hair"]},
|
| 22 |
+
# JRPG
|
| 23 |
+
{"name": "Cloud Strife", "series": "Final Fantasy VII", "species": "Human", "gender": "male", "tags": ["soldier", "buster sword"]},
|
| 24 |
+
{"name": "Tifa Lockhart", "series": "Final Fantasy VII", "species": "Human", "gender": "female", "tags": ["martial artist", "suspenders"]},
|
| 25 |
+
{"name": "Aerith Gainsborough", "series": "Final Fantasy VII", "species": "Human", "gender": "female", "tags": ["flower girl", "pink dress"]},
|
| 26 |
+
{"name": "2B", "series": "NieR:Automata", "species": "Android", "gender": "female", "tags": ["android", "blindfold", "black dress"]},
|
| 27 |
+
{"name": "A2", "series": "NieR:Automata", "species": "Android", "gender": "female", "tags": ["android", "long hair", "black outfit"]},
|
| 28 |
+
{"name": "Pyra", "series": "Xenoblade Chronicles 2", "species": "Blade", "gender": "female", "tags": ["red armor", "sword"]},
|
| 29 |
+
{"name": "Mythra", "series": "Xenoblade Chronicles 2", "species": "Blade", "gender": "female", "tags": ["white armor", "sword"]},
|
| 30 |
+
# Fighting Games
|
| 31 |
+
{"name": "Ryu", "series": "Street Fighter", "species": "Human", "gender": "male", "tags": ["martial artist", "white gi", "red headband"]},
|
| 32 |
+
{"name": "Chun-Li", "series": "Street Fighter", "species": "Human", "gender": "female", "tags": ["martial artist", "qipao", "spiked bracelets"]},
|
| 33 |
+
{"name": "Cammy White", "series": "Street Fighter", "species": "Human", "gender": "female", "tags": ["soldier", "leotard", "beret"]},
|
| 34 |
+
{"name": "Mai Shiranui", "series": "King of Fighters", "species": "Human", "gender": "female", "tags": ["ninja", "red dress", "fan"]},
|
| 35 |
+
{"name": "Morrigan Aensland", "series": "Darkstalkers", "species": "Succubus", "gender": "female", "tags": ["succubus", "bat wings"]},
|
| 36 |
+
# Cartoons/Western Animation
|
| 37 |
+
{"name": "Raven", "series": "Teen Titans", "species": "Half-Demon", "gender": "female", "tags": ["sorceress", "blue cloak"]},
|
| 38 |
+
{"name": "Starfire", "series": "Teen Titans", "species": "Tamaranean", "gender": "female", "tags": ["alien", "purple outfit"]},
|
| 39 |
+
{"name": "Kim Possible", "series": "Kim Possible", "species": "Human", "gender": "female", "tags": ["spy", "cargo pants"]},
|
| 40 |
+
{"name": "Gwen Tennyson", "series": "Ben 10", "species": "Human/Anodite", "gender": "female", "tags": ["magic", "blue shirt"]}
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
added = 0
|
| 44 |
+
for char in new_chars:
|
| 45 |
+
if char["name"].lower() not in canonicals:
|
| 46 |
+
# Generate simple aliases
|
| 47 |
+
aliases = []
|
| 48 |
+
parts = char["name"].split()
|
| 49 |
+
if len(parts) > 1:
|
| 50 |
+
aliases.append(parts[0]) # First name as alias
|
| 51 |
+
|
| 52 |
+
record = {
|
| 53 |
+
"id": str(uuid.uuid4()),
|
| 54 |
+
"canonical": char["name"],
|
| 55 |
+
"aliases": aliases,
|
| 56 |
+
"category": "character",
|
| 57 |
+
"description": f"Character from {char['series']}",
|
| 58 |
+
"tags": char["tags"],
|
| 59 |
+
"source": "synthetic_expansion",
|
| 60 |
+
"nsfw": False,
|
| 61 |
+
"synthetic": True,
|
| 62 |
+
"confidence": 1.0,
|
| 63 |
+
"metadata": {
|
| 64 |
+
"series": char["series"],
|
| 65 |
+
"species": char["species"],
|
| 66 |
+
"gender": char["gender"],
|
| 67 |
+
"default_attributes": char["tags"]
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
data.append(record)
|
| 71 |
+
canonicals.add(char["name"].lower())
|
| 72 |
+
added += 1
|
| 73 |
+
|
| 74 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 75 |
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
| 76 |
+
|
| 77 |
+
print(f"Added {added} new characters.")
|
| 78 |
+
|
| 79 |
+
def generate_alias_audit():
|
| 80 |
+
path = "data/ontology/characters.json"
|
| 81 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 82 |
+
data = json.load(f)
|
| 83 |
+
|
| 84 |
+
# Alias expansion (Task 2)
|
| 85 |
+
# We will generate more aliases for some key characters
|
| 86 |
+
target_aliases = {
|
| 87 |
+
"Amy Rose": ["amy", "amyrose", "amy the hedgehog", "rosy the rascal"],
|
| 88 |
+
"Shadow": ["shadow the hedgehog", "dark shadow", "project shadow"],
|
| 89 |
+
"Tails": ["miles prower", "tails the fox", "miles tails prower"],
|
| 90 |
+
"Sonic": ["sonic the hedgehog", "blue blur", "modern sonic"],
|
| 91 |
+
"Knuckles": ["knuckles the echidna", "knuckie"],
|
| 92 |
+
"Goku": ["son goku", "kakarot"],
|
| 93 |
+
"Naruto Uzumaki": ["naruto"]
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
modified = 0
|
| 97 |
+
all_aliases = []
|
| 98 |
+
|
| 99 |
+
for item in data:
|
| 100 |
+
name = item["canonical"]
|
| 101 |
+
if name in target_aliases:
|
| 102 |
+
for new_alias in target_aliases[name]:
|
| 103 |
+
if new_alias not in [a.lower() for a in item["aliases"]] and new_alias.lower() != name.lower():
|
| 104 |
+
item["aliases"].append(new_alias)
|
| 105 |
+
modified += 1
|
| 106 |
+
|
| 107 |
+
# Track all aliases for collision detection
|
| 108 |
+
for alias in item["aliases"]:
|
| 109 |
+
all_aliases.append((alias.lower(), name))
|
| 110 |
+
|
| 111 |
+
if modified > 0:
|
| 112 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 113 |
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
| 114 |
+
print(f"Added {modified} new aliases.")
|
| 115 |
+
|
| 116 |
+
# Measure
|
| 117 |
+
alias_counts = {}
|
| 118 |
+
for alias, name in all_aliases:
|
| 119 |
+
if alias not in alias_counts:
|
| 120 |
+
alias_counts[alias] = set()
|
| 121 |
+
alias_counts[alias].add(name)
|
| 122 |
+
|
| 123 |
+
collisions = {alias: names for alias, names in alias_counts.items() if len(names) > 1}
|
| 124 |
+
|
| 125 |
+
os.makedirs("reports", exist_ok=True)
|
| 126 |
+
with open("reports/alias_audit.md", "w", encoding="utf-8") as f:
|
| 127 |
+
f.write("# Alias Audit\n\n")
|
| 128 |
+
f.write(f"- **Total Characters with Aliases**: {sum(1 for item in data if item['aliases'])}\n")
|
| 129 |
+
f.write(f"- **Total Unique Aliases**: {len(alias_counts)}\n")
|
| 130 |
+
f.write(f"- **Alias Collisions**: {len(collisions)}\n\n")
|
| 131 |
+
|
| 132 |
+
if collisions:
|
| 133 |
+
f.write("## Collisions\n")
|
| 134 |
+
for alias, names in list(collisions.items())[:50]:
|
| 135 |
+
f.write(f"- `{alias}` maps to: {', '.join(names)}\n")
|
| 136 |
+
|
| 137 |
+
if __name__ == "__main__":
|
| 138 |
+
expand_characters()
|
| 139 |
+
generate_alias_audit()
|
src/benchmarks/external_benchmark.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from typing import List, Dict, Any
|
| 4 |
+
from src.parser.parser import Parser
|
| 5 |
+
from src.ontology.matcher import ConceptMatcher
|
| 6 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 7 |
+
from src.enrichment.enricher import Enricher
|
| 8 |
+
|
| 9 |
+
class ExternalBenchmarkSuite:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
# We manually define challenging benchmarks that do NOT come directly from sampling the ontology JSON.
|
| 12 |
+
self.benchmarks = [
|
| 13 |
+
# 1. Compound Entity Resolution
|
| 14 |
+
{
|
| 15 |
+
"input": "Classic Sonic running",
|
| 16 |
+
"expected": ["Sonic", "classic"]
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"input": "Dark Shadow with red eyes",
|
| 20 |
+
"expected": ["Shadow", "dark", "red eyes"]
|
| 21 |
+
},
|
| 22 |
+
# 2. Adjective and Attribute Recovery
|
| 23 |
+
{
|
| 24 |
+
"input": "Amy looking elegant in a luxury event",
|
| 25 |
+
"expected": ["Amy Rose", "elegant"]
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"input": "Rouge wearing futuristic combat gear",
|
| 29 |
+
"expected": ["Rouge", "futuristic", "combat gear"]
|
| 30 |
+
},
|
| 31 |
+
# 3. Misspellings and Semantic Fallback
|
| 32 |
+
{
|
| 33 |
+
"input": "amy in a crimsn dress",
|
| 34 |
+
"expected": ["Amy Rose", "red dress", "crimson"]
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"input": "blaze the cat with fyre powers",
|
| 38 |
+
"expected": ["Blaze"]
|
| 39 |
+
},
|
| 40 |
+
# 4. Mixed Language / Complex syntax
|
| 41 |
+
{
|
| 42 |
+
"input": "rosa amy vestido rojo at night",
|
| 43 |
+
"expected": ["Amy Rose", "red dress", "night"]
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"input": "a cute school uniform worn by a girl",
|
| 47 |
+
"expected": ["school uniform", "cute", "girl"]
|
| 48 |
+
}
|
| 49 |
+
] * 10 # Multiply to get a larger suite for processing
|
| 50 |
+
|
| 51 |
+
def evaluate(self, parser: Parser, enricher: Enricher) -> Dict[str, Any]:
|
| 52 |
+
results = []
|
| 53 |
+
total_precision = 0
|
| 54 |
+
total_recall = 0
|
| 55 |
+
exact_match_count = 0
|
| 56 |
+
|
| 57 |
+
for case in self.benchmarks:
|
| 58 |
+
ir = parser.parse(case["input"])
|
| 59 |
+
ir = enricher.enrich(ir)
|
| 60 |
+
|
| 61 |
+
# Extract found elements
|
| 62 |
+
found = set()
|
| 63 |
+
for char in ir.characters:
|
| 64 |
+
found.add(char.name.lower())
|
| 65 |
+
for app in char.appearance: found.add(app.lower())
|
| 66 |
+
for clo in char.clothing: found.add(clo.lower())
|
| 67 |
+
for acc in char.accessories: found.add(acc.lower())
|
| 68 |
+
for pos in char.pose: found.add(pos.lower())
|
| 69 |
+
for exp in char.expression: found.add(exp.lower())
|
| 70 |
+
for loc in ir.scene.locations: found.add(loc.lower())
|
| 71 |
+
|
| 72 |
+
# Additional trace check for recovered attributes that might not map strictly
|
| 73 |
+
for res in ir.trace.get("resolved", {}).values():
|
| 74 |
+
if "attributes_recovered" in res:
|
| 75 |
+
for attr in res["attributes_recovered"]:
|
| 76 |
+
found.add(attr.lower())
|
| 77 |
+
|
| 78 |
+
expected = set(e.lower() for e in case["expected"])
|
| 79 |
+
|
| 80 |
+
# Match
|
| 81 |
+
# To handle fuzzy matching/semantic overlaps in evaluation:
|
| 82 |
+
# For strict precision/recall:
|
| 83 |
+
intersection = set()
|
| 84 |
+
for f in found:
|
| 85 |
+
for e in expected:
|
| 86 |
+
if f in e or e in f:
|
| 87 |
+
intersection.add(e)
|
| 88 |
+
|
| 89 |
+
precision = len(intersection) / len(found) if found else 0
|
| 90 |
+
recall = len(intersection) / len(expected) if expected else 1.0
|
| 91 |
+
|
| 92 |
+
total_precision += precision
|
| 93 |
+
total_recall += recall
|
| 94 |
+
|
| 95 |
+
if recall == 1.0:
|
| 96 |
+
exact_match_count += 1
|
| 97 |
+
else:
|
| 98 |
+
# Log failure
|
| 99 |
+
missing = list(expected - intersection)
|
| 100 |
+
extra = list(found - expected)
|
| 101 |
+
failure_entry = {
|
| 102 |
+
"input": case["input"],
|
| 103 |
+
"expected": list(expected),
|
| 104 |
+
"actual": list(found),
|
| 105 |
+
"missing": missing,
|
| 106 |
+
"extra": extra,
|
| 107 |
+
"root_cause": "Semantic drift or dependency parsing drop" if extra else "Extraction failure",
|
| 108 |
+
"severity": "High" if len(missing) >= len(expected)/2 else "Medium"
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
os.makedirs("failures", exist_ok=True)
|
| 112 |
+
# Use a hash of the input to avoid huge number of files, or just append to a single JSON Lines
|
| 113 |
+
with open("failures/error_catalog.jsonl", "a", encoding="utf-8") as err_file:
|
| 114 |
+
err_file.write(json.dumps(failure_entry) + "\n")
|
| 115 |
+
|
| 116 |
+
results.append({
|
| 117 |
+
"input": case["input"],
|
| 118 |
+
"expected": list(expected),
|
| 119 |
+
"found": list(found),
|
| 120 |
+
"recall": recall
|
| 121 |
+
})
|
| 122 |
+
|
| 123 |
+
avg_precision = total_precision / len(self.benchmarks)
|
| 124 |
+
avg_recall = total_recall / len(self.benchmarks)
|
| 125 |
+
f1 = 2 * (avg_precision * avg_recall) / (avg_precision + avg_recall) if (avg_precision + avg_recall) else 0
|
| 126 |
+
|
| 127 |
+
return {
|
| 128 |
+
"metrics": {
|
| 129 |
+
"precision": round(avg_precision, 4),
|
| 130 |
+
"recall": round(avg_recall, 4),
|
| 131 |
+
"f1": round(f1, 4),
|
| 132 |
+
"exact_match_rate": round(exact_match_count / len(self.benchmarks), 4)
|
| 133 |
+
},
|
| 134 |
+
"total_cases": len(self.benchmarks),
|
| 135 |
+
"results": results
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
if __name__ == "__main__":
|
| 139 |
+
suite = ExternalBenchmarkSuite()
|
| 140 |
+
|
| 141 |
+
print("Running external evaluation...")
|
| 142 |
+
matcher = ConceptMatcher("data/ontology")
|
| 143 |
+
|
| 144 |
+
# We use ONNX engine if available, or fallback to PyTorch
|
| 145 |
+
try:
|
| 146 |
+
from src.runtime.onnx_runtime import ONNXEmbeddingEngine
|
| 147 |
+
engine = ONNXEmbeddingEngine(index_dir="data/faiss_indices_onnx")
|
| 148 |
+
engine.load_index()
|
| 149 |
+
except Exception:
|
| 150 |
+
engine = EmbeddingEngine(index_dir="data/faiss_indices")
|
| 151 |
+
engine.load_index()
|
| 152 |
+
|
| 153 |
+
parser = Parser(matcher, engine)
|
| 154 |
+
enricher = Enricher(matcher)
|
| 155 |
+
|
| 156 |
+
report = suite.evaluate(parser, enricher)
|
| 157 |
+
|
| 158 |
+
os.makedirs("reports", exist_ok=True)
|
| 159 |
+
with open("reports/external_benchmark_results.json", "w", encoding="utf-8") as f:
|
| 160 |
+
json.dump(report, f, indent=2)
|
| 161 |
+
|
| 162 |
+
with open("reports/external_benchmark_results.md", "w", encoding="utf-8") as f:
|
| 163 |
+
f.write("# External Benchmark Results (v2.0)\n\n")
|
| 164 |
+
m = report["metrics"]
|
| 165 |
+
f.write(f"- **Total Cases**: {report['total_cases']}\n")
|
| 166 |
+
f.write(f"- **Precision**: {m['precision']}\n")
|
| 167 |
+
f.write(f"- **Recall**: {m['recall']}\n")
|
| 168 |
+
f.write(f"- **F1 Score**: {m['f1']}\n")
|
| 169 |
+
f.write(f"- **Exact Match Rate**: {m['exact_match_rate']}\n")
|
| 170 |
+
|
| 171 |
+
print("Benchmark evaluation complete. Reports generated in reports/external_benchmark_results.md")
|
src/benchmarks/prompt_eval.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from collections import Counter
|
| 4 |
+
from typing import List, Dict, Any
|
| 5 |
+
from src.parser.parser import Parser
|
| 6 |
+
from src.ontology.matcher import ConceptMatcher
|
| 7 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 8 |
+
from src.enrichment.enricher import Enricher
|
| 9 |
+
from src.prompt_builder.builder import PromptBuilder
|
| 10 |
+
|
| 11 |
+
class PromptEvaluator:
|
| 12 |
+
def __init__(self, profiles: List[str]):
|
| 13 |
+
self.profiles = profiles
|
| 14 |
+
|
| 15 |
+
def evaluate_prompts(self, count: int = 1000):
|
| 16 |
+
matcher = ConceptMatcher("data/ontology")
|
| 17 |
+
engine = EmbeddingEngine(index_dir="data/faiss_indices")
|
| 18 |
+
engine.load_index()
|
| 19 |
+
parser = Parser(matcher, engine)
|
| 20 |
+
enricher = Enricher(matcher)
|
| 21 |
+
|
| 22 |
+
# Sample inputs for 1000 prompts
|
| 23 |
+
# We'll use the benchmark generator logic or just some variants
|
| 24 |
+
from src.benchmarks.benchmark import BenchmarkSuite
|
| 25 |
+
suite = BenchmarkSuite("data/ontology")
|
| 26 |
+
benchmarks = suite.generate_benchmarks(count)
|
| 27 |
+
|
| 28 |
+
report = {}
|
| 29 |
+
|
| 30 |
+
for profile in self.profiles:
|
| 31 |
+
builder = PromptBuilder(profile_name=profile)
|
| 32 |
+
profile_results = {
|
| 33 |
+
"duplicate_terms": 0,
|
| 34 |
+
"avg_length": 0,
|
| 35 |
+
"total_prompts": count,
|
| 36 |
+
"concept_coverage": Counter()
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
total_len = 0
|
| 40 |
+
for case in benchmarks:
|
| 41 |
+
ir = parser.parse(case["input"])
|
| 42 |
+
ir = enricher.enrich(ir)
|
| 43 |
+
prompt_bundle = builder.build(ir)
|
| 44 |
+
pos = prompt_bundle["positive"]
|
| 45 |
+
|
| 46 |
+
tags = [t.strip() for t in pos.split(",")]
|
| 47 |
+
if len(tags) != len(set(tags)):
|
| 48 |
+
profile_results["duplicate_terms"] += 1
|
| 49 |
+
|
| 50 |
+
total_len += len(tags)
|
| 51 |
+
for tag in tags:
|
| 52 |
+
profile_results["concept_coverage"][tag] += 1
|
| 53 |
+
|
| 54 |
+
profile_results["avg_length"] = total_len / count
|
| 55 |
+
profile_results["top_concepts"] = dict(profile_results["concept_coverage"].most_common(10))
|
| 56 |
+
del profile_results["concept_coverage"]
|
| 57 |
+
|
| 58 |
+
report[profile] = profile_results
|
| 59 |
+
|
| 60 |
+
return report
|
| 61 |
+
|
| 62 |
+
if __name__ == "__main__":
|
| 63 |
+
evaluator = PromptEvaluator(["generic", "sdxl", "pony", "illustrious"])
|
| 64 |
+
print("Evaluating 1000 prompts per profile...")
|
| 65 |
+
report = evaluator.evaluate_prompts(1000)
|
| 66 |
+
|
| 67 |
+
os.makedirs("reports", exist_ok=True)
|
| 68 |
+
with open("reports/quality_report.json", "w", encoding="utf-8") as f:
|
| 69 |
+
json.dump(report, f, indent=2)
|
| 70 |
+
|
| 71 |
+
with open("reports/quality_report.md", "w", encoding="utf-8") as f:
|
| 72 |
+
f.write("# Prompt Quality Evaluation\n\n")
|
| 73 |
+
for profile, metrics in report.items():
|
| 74 |
+
f.write(f"## Profile: {profile.upper()}\n")
|
| 75 |
+
f.write(f"- **Avg Prompt Length**: {metrics['avg_length']} tags\n")
|
| 76 |
+
f.write(f"- **Prompts with Duplicates**: {metrics['duplicate_terms']}\n")
|
| 77 |
+
f.write(f"- **Top Concepts**: {', '.join(metrics['top_concepts'].keys())}\n\n")
|
| 78 |
+
|
| 79 |
+
print("Quality report generated in reports/")
|
src/benchmarks/resolution_audit.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import random
|
| 3 |
+
import os
|
| 4 |
+
from collections import Counter
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
+
from src.parser.parser import Parser
|
| 7 |
+
from src.ontology.matcher import ConceptMatcher
|
| 8 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 9 |
+
from src.enrichment.enricher import Enricher
|
| 10 |
+
|
| 11 |
+
class ResolutionAudit:
|
| 12 |
+
def __init__(self, ontology_dir: str):
|
| 13 |
+
self.ontology_dir = ontology_dir
|
| 14 |
+
self.concepts = self._load_all_concepts()
|
| 15 |
+
|
| 16 |
+
def _load_all_concepts(self) -> Dict[str, List[str]]:
|
| 17 |
+
concepts = {}
|
| 18 |
+
for filename in os.listdir(self.ontology_dir):
|
| 19 |
+
if filename.endswith(".json"):
|
| 20 |
+
path = os.path.join(self.ontology_dir, filename)
|
| 21 |
+
category = filename.replace(".json", "")
|
| 22 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 23 |
+
data = json.load(f)
|
| 24 |
+
concepts[category] = [item["canonical"] for item in data]
|
| 25 |
+
return concepts
|
| 26 |
+
|
| 27 |
+
def generate_prompts(self, count: int = 10000) -> List[str]:
|
| 28 |
+
prompts = []
|
| 29 |
+
chars = ["Sonic", "Amy Rose", "Shadow", "Rouge", "Blaze", "Silver", "Knuckles", "Tails", "Cream"]
|
| 30 |
+
clothing = self.concepts.get("clothing", ["dress"])
|
| 31 |
+
scenes = self.concepts.get("scenes", ["beach"])
|
| 32 |
+
styles = self.concepts.get("styles", ["anime"])
|
| 33 |
+
|
| 34 |
+
for _ in range(count):
|
| 35 |
+
strategy = random.random()
|
| 36 |
+
if strategy < 0.3:
|
| 37 |
+
# Exact matches
|
| 38 |
+
prompts.append(f"{random.choice(chars)} in {random.choice(clothing)} at {random.choice(scenes)}")
|
| 39 |
+
elif strategy < 0.6:
|
| 40 |
+
# Paraphrased / Semantic (simulated)
|
| 41 |
+
prompts.append(f"a hedgehog wearing luxurious {random.choice(clothing)} in a beautiful {random.choice(scenes)}")
|
| 42 |
+
elif strategy < 0.8:
|
| 43 |
+
# Modifiers / Compounds
|
| 44 |
+
prompts.append(f"Classic {random.choice(chars)} wearing elegant {random.choice(clothing)}")
|
| 45 |
+
else:
|
| 46 |
+
# Complex / Random
|
| 47 |
+
prompts.append(f"{random.choice(styles)} style girl with colored hair and {random.choice(clothing)}")
|
| 48 |
+
return prompts
|
| 49 |
+
|
| 50 |
+
def run_audit(self, prompts: List[str], parser: Parser, enricher: Enricher):
|
| 51 |
+
stats = Counter()
|
| 52 |
+
semantic_concepts = Counter()
|
| 53 |
+
gap_mining = []
|
| 54 |
+
|
| 55 |
+
total_resolved = 0
|
| 56 |
+
|
| 57 |
+
for text in prompts:
|
| 58 |
+
ir = parser.parse(text)
|
| 59 |
+
ir = enricher.enrich(ir)
|
| 60 |
+
|
| 61 |
+
p_stats = ir.trace.get("resolution_stats", {})
|
| 62 |
+
for method, count in p_stats.items():
|
| 63 |
+
stats[method] += count
|
| 64 |
+
total_resolved += count
|
| 65 |
+
|
| 66 |
+
# Mining gaps and semantic dependency
|
| 67 |
+
for term, res in ir.trace.get("resolved", {}).items():
|
| 68 |
+
if res["method"] == "semantic":
|
| 69 |
+
semantic_concepts[(res["canonical"], res.get("category", "unknown"))] += 1
|
| 70 |
+
gap_mining.append({
|
| 71 |
+
"term": term,
|
| 72 |
+
"canonical": res["canonical"],
|
| 73 |
+
"confidence": res["confidence"]
|
| 74 |
+
})
|
| 75 |
+
|
| 76 |
+
# Distribution
|
| 77 |
+
distribution = {method: round(count / total_resolved * 100, 2) for method, count in stats.items()}
|
| 78 |
+
|
| 79 |
+
return {
|
| 80 |
+
"distribution": distribution,
|
| 81 |
+
"semantic_dependency": semantic_concepts.most_common(50),
|
| 82 |
+
"gap_mining": gap_mining[:100]
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
if __name__ == "__main__":
|
| 86 |
+
audit = ResolutionAudit("data/ontology")
|
| 87 |
+
print("Generating 1000 prompts for audit...")
|
| 88 |
+
prompts = audit.generate_prompts(1000)
|
| 89 |
+
|
| 90 |
+
matcher = ConceptMatcher("data/ontology")
|
| 91 |
+
engine = EmbeddingEngine(index_dir="data/faiss_indices")
|
| 92 |
+
engine.load_index()
|
| 93 |
+
parser = Parser(matcher, engine)
|
| 94 |
+
enricher = Enricher(matcher)
|
| 95 |
+
|
| 96 |
+
print("Running full system audit...")
|
| 97 |
+
results = audit.run_audit(prompts, parser, enricher)
|
| 98 |
+
|
| 99 |
+
print("Running ablation study (Embeddings Disabled)...")
|
| 100 |
+
parser_no_emb = Parser(matcher, embedding_engine=None)
|
| 101 |
+
|
| 102 |
+
import time
|
| 103 |
+
|
| 104 |
+
# Measure Full
|
| 105 |
+
start = time.time()
|
| 106 |
+
audit.run_audit(prompts, parser, enricher)
|
| 107 |
+
full_time = time.time() - start
|
| 108 |
+
|
| 109 |
+
# Measure Ablation
|
| 110 |
+
start = time.time()
|
| 111 |
+
results_no_emb = audit.run_audit(prompts, parser_no_emb, enricher)
|
| 112 |
+
no_emb_time = time.time() - start
|
| 113 |
+
|
| 114 |
+
os.makedirs("reports", exist_ok=True)
|
| 115 |
+
|
| 116 |
+
# Task 5 report
|
| 117 |
+
with open("reports/embedding_ablation.md", "w", encoding="utf-8") as f:
|
| 118 |
+
f.write("# Embedding Ablation Study\n\n")
|
| 119 |
+
f.write("| Metric | Full System | No Embeddings |\n")
|
| 120 |
+
f.write("| :--- | :--- | :--- |\n")
|
| 121 |
+
f.write(f"| Avg Latency | {full_time/1000*1000:.2f}ms | {no_emb_time/1000*1000:.2f}ms |\n")
|
| 122 |
+
# We use 'exact_canonical' + 'alias' as a proxy for recall stability
|
| 123 |
+
full_res = results["distribution"].get("exact_canonical", 0) + results["distribution"].get("alias", 0)
|
| 124 |
+
no_emb_res = results_no_emb["distribution"].get("exact_canonical", 0) + results_no_emb["distribution"].get("alias", 0)
|
| 125 |
+
f.write(f"| Deterministic Resolution % | {full_res}% | {no_emb_res}% |\n")
|
| 126 |
+
f.write(f"| Semantic Fallback % | {results['distribution'].get('semantic', 0)}% | 0% |\n")
|
| 127 |
+
|
| 128 |
+
# Task 2 report
|
| 129 |
+
with open("reports/resolution_source_distribution.json", "w", encoding="utf-8") as f:
|
| 130 |
+
json.dump(results["distribution"], f, indent=2)
|
| 131 |
+
|
| 132 |
+
# Task 3 report
|
| 133 |
+
with open("reports/semantic_dependency.md", "w", encoding="utf-8") as f:
|
| 134 |
+
f.write("# Semantic Dependency Report\n\n")
|
| 135 |
+
f.write("| Concept | Category | Frequency |\n")
|
| 136 |
+
f.write("| :--- | :--- | :--- |\n")
|
| 137 |
+
for (concept, cat), freq in results["semantic_dependency"]:
|
| 138 |
+
f.write(f"| {concept} | {cat} | {freq} |\n")
|
| 139 |
+
|
| 140 |
+
# Task 4 report
|
| 141 |
+
with open("reports/ontology_gap_mining.md", "w", encoding="utf-8") as f:
|
| 142 |
+
f.write("# Ontology Gap Mining Report\n\n")
|
| 143 |
+
f.write("| Raw Term | Resolved Canonical | Confidence |\n")
|
| 144 |
+
f.write("| :--- | :--- | :--- |\n")
|
| 145 |
+
for item in results["gap_mining"]:
|
| 146 |
+
f.write(f"| {item['term']} | {item['canonical']} | {item['confidence']} |\n")
|
| 147 |
+
|
| 148 |
+
print("Audit complete. Reports generated in reports/")
|
src/benchmarks/semantic_quality_audit.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from typing import List, Dict, Any, Tuple
|
| 4 |
+
from collections import Counter
|
| 5 |
+
from src.parser.parser import Parser
|
| 6 |
+
from src.ontology.matcher import ConceptMatcher
|
| 7 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 8 |
+
from src.enrichment.enricher import Enricher
|
| 9 |
+
|
| 10 |
+
class SemanticQualityAudit:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
# Ground truth for semantic quality
|
| 13 |
+
self.ground_truth = [
|
| 14 |
+
# Colors
|
| 15 |
+
{"query": "maroon", "expected": "red", "acceptable": ["maroon"], "category": "hair_color", "type": "color"},
|
| 16 |
+
{"query": "burgundy", "expected": "red", "acceptable": ["burgundy"], "category": "hair_color", "type": "color"},
|
| 17 |
+
{"query": "cerulean", "expected": "blue", "acceptable": ["ceruledge"], "category": "hair_color", "type": "color"},
|
| 18 |
+
{"query": "azure", "expected": "blue", "acceptable": ["azure_fang"], "category": "hair_color", "type": "color"},
|
| 19 |
+
|
| 20 |
+
# Scenes
|
| 21 |
+
{"query": "dance floor", "expected": "gala", "acceptable": ["ballroom", "concert stage"], "category": "scene", "type": "synonym"},
|
| 22 |
+
{"query": "royal palace", "expected": "castle", "acceptable": ["temple", "shrine"], "category": "scene", "type": "synonym"},
|
| 23 |
+
|
| 24 |
+
# Clothing
|
| 25 |
+
{"query": "royal robes", "expected": "evening gown", "acceptable": ["tuxedo", "suit", "cape", "cloak", "crown and royal robes"], "category": "clothing", "type": "related"},
|
| 26 |
+
{"query": "combat gear", "expected": "armor", "acceptable": ["military uniform", "plate armor"], "category": "clothing", "type": "related"},
|
| 27 |
+
|
| 28 |
+
# Identity
|
| 29 |
+
{"query": "speedy hedgehog", "expected": "Sonic", "category": "character", "type": "identity"},
|
| 30 |
+
{"query": "pink hedgehog", "expected": "Amy Rose", "category": "character", "type": "identity"},
|
| 31 |
+
{"query": "dark hedgehog", "expected": "Shadow", "category": "character", "type": "identity"},
|
| 32 |
+
|
| 33 |
+
# Typos
|
| 34 |
+
{"query": "crimsn", "expected": "crimson", "acceptable": ["red"], "category": "hair_color", "type": "typo"},
|
| 35 |
+
{"query": "skool uniform", "expected": "school uniform", "category": "clothing", "type": "typo"}
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
self.expanded_gt = []
|
| 39 |
+
for item in self.ground_truth:
|
| 40 |
+
self.expanded_gt.append(item)
|
| 41 |
+
if item["type"] == "color":
|
| 42 |
+
self.expanded_gt.append({**item, "query": f"deep {item['query']}"})
|
| 43 |
+
self.expanded_gt.append({**item, "query": f"bright {item['query']}"})
|
| 44 |
+
elif item["type"] == "related":
|
| 45 |
+
self.expanded_gt.append({**item, "query": f"a {item['query']}"})
|
| 46 |
+
self.expanded_gt.append({**item, "query": f"wearing {item['query']}"})
|
| 47 |
+
|
| 48 |
+
self.adversarial = [
|
| 49 |
+
{"query": "ballroom", "forbidden": "cocktail dress", "category": "scene"},
|
| 50 |
+
{"query": "Amy", "forbidden": "Rouge", "category": "character"},
|
| 51 |
+
{"query": "Shadow", "forbidden": "Sonic", "category": "character"}
|
| 52 |
+
]
|
| 53 |
+
|
| 54 |
+
def evaluate(self, parser: Parser):
|
| 55 |
+
results = {
|
| 56 |
+
"correct": 0,
|
| 57 |
+
"acceptable": 0,
|
| 58 |
+
"incorrect": 0,
|
| 59 |
+
"rejected": 0,
|
| 60 |
+
"total": 0
|
| 61 |
+
}
|
| 62 |
+
failures = []
|
| 63 |
+
confusion_matrix = Counter()
|
| 64 |
+
|
| 65 |
+
for item in self.expanded_gt:
|
| 66 |
+
query = item["query"]
|
| 67 |
+
search_results = parser.embedding_engine.search(query, category=item.get("category"), top_k=1)
|
| 68 |
+
|
| 69 |
+
results["total"] += 1
|
| 70 |
+
if not search_results:
|
| 71 |
+
results["rejected"] += 1
|
| 72 |
+
continue
|
| 73 |
+
|
| 74 |
+
record, conf = search_results[0]
|
| 75 |
+
if conf < 0.5:
|
| 76 |
+
results["rejected"] += 1
|
| 77 |
+
continue
|
| 78 |
+
|
| 79 |
+
actual = record.canonical
|
| 80 |
+
actual_cat = record.category
|
| 81 |
+
|
| 82 |
+
actual_lower = actual.lower().strip()
|
| 83 |
+
expected_lower = item["expected"].lower().strip()
|
| 84 |
+
|
| 85 |
+
is_correct = (actual_lower == expected_lower) or (expected_lower in actual_lower) or (actual_lower in expected_lower)
|
| 86 |
+
is_acceptable = False
|
| 87 |
+
if not is_correct:
|
| 88 |
+
if "acceptable" in item:
|
| 89 |
+
is_acceptable = any(acc.lower().strip() in actual_lower for acc in item["acceptable"]) or \
|
| 90 |
+
any(actual_lower in acc.lower().strip() for acc in item["acceptable"])
|
| 91 |
+
if not is_acceptable and query.lower().strip() in actual_lower:
|
| 92 |
+
is_acceptable = True
|
| 93 |
+
|
| 94 |
+
if item["category"] == "character" and actual_lower != expected_lower:
|
| 95 |
+
is_correct = False
|
| 96 |
+
is_acceptable = False
|
| 97 |
+
is_incorrect = True
|
| 98 |
+
else:
|
| 99 |
+
is_incorrect = not (is_correct or is_acceptable)
|
| 100 |
+
|
| 101 |
+
if is_correct:
|
| 102 |
+
results["correct"] += 1
|
| 103 |
+
elif is_acceptable:
|
| 104 |
+
results["acceptable"] += 1
|
| 105 |
+
else:
|
| 106 |
+
results["incorrect"] += 1
|
| 107 |
+
failures.append({
|
| 108 |
+
"query": query,
|
| 109 |
+
"actual": actual,
|
| 110 |
+
"actual_category": actual_cat,
|
| 111 |
+
"expected": item["expected"],
|
| 112 |
+
"expected_category": item["category"],
|
| 113 |
+
"confidence": conf
|
| 114 |
+
})
|
| 115 |
+
confusion_matrix[(item["category"], actual_cat)] += 1
|
| 116 |
+
|
| 117 |
+
for item in self.adversarial:
|
| 118 |
+
res = parser.embedding_engine.search(item["query"], top_k=5)
|
| 119 |
+
for rec, conf in res:
|
| 120 |
+
if rec.canonical.lower() == item["forbidden"].lower():
|
| 121 |
+
results["total"] += 1
|
| 122 |
+
results["incorrect"] += 1
|
| 123 |
+
failures.append({
|
| 124 |
+
"query": item["query"],
|
| 125 |
+
"actual": rec.canonical,
|
| 126 |
+
"reason": "Identity/Category Swap",
|
| 127 |
+
"severity": "Critical"
|
| 128 |
+
})
|
| 129 |
+
break
|
| 130 |
+
|
| 131 |
+
return results, failures, confusion_matrix
|
| 132 |
+
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
audit = SemanticQualityAudit()
|
| 135 |
+
matcher = ConceptMatcher("data/ontology")
|
| 136 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 137 |
+
engine = EmbeddingEngine(index_dir="data/faiss_indices")
|
| 138 |
+
engine.load_index()
|
| 139 |
+
parser = Parser(matcher, engine)
|
| 140 |
+
|
| 141 |
+
print("Running semantic quality audit...")
|
| 142 |
+
stats, failures, confusion = audit.evaluate(parser)
|
| 143 |
+
|
| 144 |
+
total = stats["total"]
|
| 145 |
+
report = {
|
| 146 |
+
"metrics": {
|
| 147 |
+
"correct_retrieval_pct": round(stats["correct"] / total * 100, 2),
|
| 148 |
+
"acceptable_retrieval_pct": round(stats["acceptable"] / total * 100, 2),
|
| 149 |
+
"incorrect_retrieval_pct": round(stats["incorrect"] / total * 100, 2),
|
| 150 |
+
"trustworthy_pct": round((stats["correct"] + stats["acceptable"]) / total * 100, 2)
|
| 151 |
+
},
|
| 152 |
+
"counts": stats
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
os.makedirs("reports", exist_ok=True)
|
| 156 |
+
with open("reports/semantic_quality.json", "w", encoding="utf-8") as f:
|
| 157 |
+
json.dump(report, f, indent=2)
|
| 158 |
+
with open("reports/semantic_failures.json", "w", encoding="utf-8") as f:
|
| 159 |
+
json.dump(failures, f, indent=2)
|
| 160 |
+
|
| 161 |
+
cm_report = []
|
| 162 |
+
for (expected_cat, actual_cat), count in confusion.items():
|
| 163 |
+
cm_report.append({"expected_category": expected_cat, "actual_category": actual_cat, "count": count})
|
| 164 |
+
with open("reports/semantic_confusion_matrix.json", "w", encoding="utf-8") as f:
|
| 165 |
+
json.dump(cm_report, f, indent=2)
|
| 166 |
+
|
| 167 |
+
print(f"Audit complete.")
|
| 168 |
+
print(f"Correct + Acceptable: {report['metrics']['trustworthy_pct']}%")
|
| 169 |
+
print(f"Incorrect: {report['metrics']['incorrect_retrieval_pct']}%")
|
src/benchmarks/test_large_scale.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import random
|
| 3 |
+
import os
|
| 4 |
+
import time
|
| 5 |
+
import psutil
|
| 6 |
+
from src.parser.parser import Parser
|
| 7 |
+
from src.ontology.matcher import ConceptMatcher
|
| 8 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 9 |
+
from src.enrichment.enricher import Enricher
|
| 10 |
+
|
| 11 |
+
class LargeScaleBenchmark:
|
| 12 |
+
def __init__(self, ontology_dir: str):
|
| 13 |
+
self.ontology_dir = ontology_dir
|
| 14 |
+
self.concepts = self._load_all_concepts()
|
| 15 |
+
|
| 16 |
+
def _load_all_concepts(self):
|
| 17 |
+
concepts = {}
|
| 18 |
+
for filename in os.listdir(self.ontology_dir):
|
| 19 |
+
if filename.endswith(".json"):
|
| 20 |
+
path = os.path.join(self.ontology_dir, filename)
|
| 21 |
+
category = filename.replace(".json", "")
|
| 22 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 23 |
+
data = json.load(f)
|
| 24 |
+
concepts[category] = [item["canonical"] for item in data]
|
| 25 |
+
return concepts
|
| 26 |
+
|
| 27 |
+
def generate_prompts(self, count: int = 100000):
|
| 28 |
+
prompts = []
|
| 29 |
+
chars = self.concepts.get("characters", ["Sonic", "Amy Rose", "Shadow"])
|
| 30 |
+
clothing = self.concepts.get("clothing", ["dress", "suit", "armor"])
|
| 31 |
+
scenes = self.concepts.get("scenes", ["beach", "city", "forest"])
|
| 32 |
+
styles = self.concepts.get("styles", ["anime", "realistic"])
|
| 33 |
+
|
| 34 |
+
# We need a mix of exact matches, aliases, misspellings, and complex phrases
|
| 35 |
+
for i in range(count):
|
| 36 |
+
strategy = random.random()
|
| 37 |
+
char = random.choice(chars) if chars else "Sonic"
|
| 38 |
+
clo = random.choice(clothing) if clothing else "dress"
|
| 39 |
+
sce = random.choice(scenes) if scenes else "beach"
|
| 40 |
+
sty = random.choice(styles) if styles else "anime"
|
| 41 |
+
|
| 42 |
+
if strategy < 0.2:
|
| 43 |
+
prompts.append(f"{char} in {clo} at {sce}")
|
| 44 |
+
elif strategy < 0.4:
|
| 45 |
+
prompts.append(f"a person wearing luxurious {clo} in a beautiful {sce}")
|
| 46 |
+
elif strategy < 0.6:
|
| 47 |
+
prompts.append(f"Classic {char} wearing elegant {clo}")
|
| 48 |
+
elif strategy < 0.8:
|
| 49 |
+
prompts.append(f"{sty} style character with {clo}")
|
| 50 |
+
else:
|
| 51 |
+
# Add some typos or weird spacing
|
| 52 |
+
prompts.append(f"{char.lower()} with {clo.lower()} in {sce.lower()}")
|
| 53 |
+
|
| 54 |
+
return prompts
|
| 55 |
+
|
| 56 |
+
def run_benchmark(self, prompts: list, parser: Parser, enricher: Enricher):
|
| 57 |
+
latencies = []
|
| 58 |
+
start_mem = psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024
|
| 59 |
+
|
| 60 |
+
start_time = time.time()
|
| 61 |
+
for text in prompts:
|
| 62 |
+
s = time.time()
|
| 63 |
+
ir = parser.parse(text)
|
| 64 |
+
ir = enricher.enrich(ir)
|
| 65 |
+
latencies.append((time.time() - s) * 1000)
|
| 66 |
+
|
| 67 |
+
end_time = time.time()
|
| 68 |
+
end_mem = psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024
|
| 69 |
+
|
| 70 |
+
total_time = end_time - start_time
|
| 71 |
+
throughput = len(prompts) / total_time
|
| 72 |
+
|
| 73 |
+
latencies.sort()
|
| 74 |
+
p50 = latencies[int(len(latencies) * 0.5)]
|
| 75 |
+
p95 = latencies[int(len(latencies) * 0.95)]
|
| 76 |
+
p99 = latencies[int(len(latencies) * 0.99)]
|
| 77 |
+
|
| 78 |
+
return {
|
| 79 |
+
"total_prompts": len(prompts),
|
| 80 |
+
"total_time_s": total_time,
|
| 81 |
+
"throughput_req_sec": throughput,
|
| 82 |
+
"latency_ms": {
|
| 83 |
+
"p50": p50,
|
| 84 |
+
"p95": p95,
|
| 85 |
+
"p99": p99,
|
| 86 |
+
"avg": sum(latencies) / len(latencies)
|
| 87 |
+
},
|
| 88 |
+
"memory_mb": {
|
| 89 |
+
"start": start_mem,
|
| 90 |
+
"end": end_mem,
|
| 91 |
+
"diff": end_mem - start_mem
|
| 92 |
+
}
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
if __name__ == "__main__":
|
| 96 |
+
import argparse
|
| 97 |
+
parser_args = argparse.ArgumentParser()
|
| 98 |
+
parser_args.add_argument("--count", type=int, default=1000, help="Number of prompts to benchmark")
|
| 99 |
+
args = parser_args.parse_args()
|
| 100 |
+
|
| 101 |
+
benchmark = LargeScaleBenchmark("data/ontology")
|
| 102 |
+
|
| 103 |
+
print(f"Generating {args.count} prompts...")
|
| 104 |
+
prompts = benchmark.generate_prompts(args.count)
|
| 105 |
+
|
| 106 |
+
matcher = ConceptMatcher("data/ontology")
|
| 107 |
+
|
| 108 |
+
# Try ONNX first, then PyTorch
|
| 109 |
+
try:
|
| 110 |
+
from src.runtime.onnx_runtime import ONNXEmbeddingEngine
|
| 111 |
+
engine = ONNXEmbeddingEngine(index_dir="data/faiss_indices_onnx")
|
| 112 |
+
engine.load_index()
|
| 113 |
+
engine_type = "ONNX"
|
| 114 |
+
except Exception:
|
| 115 |
+
engine = EmbeddingEngine(index_dir="data/faiss_indices")
|
| 116 |
+
engine.load_index()
|
| 117 |
+
engine_type = "PyTorch"
|
| 118 |
+
|
| 119 |
+
parser = Parser(matcher, engine)
|
| 120 |
+
enricher = Enricher(matcher)
|
| 121 |
+
|
| 122 |
+
print(f"Running benchmark with {engine_type} engine...")
|
| 123 |
+
report = benchmark.run_benchmark(prompts, parser, enricher)
|
| 124 |
+
|
| 125 |
+
os.makedirs("reports", exist_ok=True)
|
| 126 |
+
with open("reports/large_scale_benchmark.md", "w", encoding="utf-8") as f:
|
| 127 |
+
f.write("# Large Scale Benchmark\n\n")
|
| 128 |
+
f.write(f"- **Engine**: {engine_type}\n")
|
| 129 |
+
f.write(f"- **Total Prompts**: {report['total_prompts']}\n")
|
| 130 |
+
f.write(f"- **Total Time**: {report['total_time_s']:.2f} s\n")
|
| 131 |
+
f.write(f"- **Throughput**: {report['throughput_req_sec']:.2f} req/s\n\n")
|
| 132 |
+
|
| 133 |
+
f.write("## Latency\n")
|
| 134 |
+
f.write(f"- **Average**: {report['latency_ms']['avg']:.2f} ms\n")
|
| 135 |
+
f.write(f"- **p50**: {report['latency_ms']['p50']:.2f} ms\n")
|
| 136 |
+
f.write(f"- **p95**: {report['latency_ms']['p95']:.2f} ms\n")
|
| 137 |
+
f.write(f"- **p99**: {report['latency_ms']['p99']:.2f} ms\n\n")
|
| 138 |
+
|
| 139 |
+
f.write("## Memory\n")
|
| 140 |
+
f.write(f"- **Start**: {report['memory_mb']['start']:.2f} MB\n")
|
| 141 |
+
f.write(f"- **End**: {report['memory_mb']['end']:.2f} MB\n")
|
| 142 |
+
f.write(f"- **Diff**: {report['memory_mb']['diff']:.2f} MB\n")
|
| 143 |
+
|
| 144 |
+
print(json.dumps(report, indent=2))
|
| 145 |
+
print("Benchmark complete. Report saved to reports/large_scale_benchmark.md")
|
src/benchmarks/test_rebuild.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import os
|
| 3 |
+
import json
|
| 4 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 5 |
+
|
| 6 |
+
def test_incremental_rebuild():
|
| 7 |
+
engine = EmbeddingEngine(index_dir="data/faiss_indices")
|
| 8 |
+
|
| 9 |
+
# Measure full rebuild
|
| 10 |
+
# To force full rebuild, we can just touch all json files so they are newer than indices
|
| 11 |
+
# But wait, first we build to ensure indices exist
|
| 12 |
+
print("Ensuring indices exist...")
|
| 13 |
+
engine.build_index("data/ontology")
|
| 14 |
+
|
| 15 |
+
print("\nMeasuring Full Rebuild (Forced)...")
|
| 16 |
+
for f in os.listdir("data/ontology"):
|
| 17 |
+
if f.endswith(".json"):
|
| 18 |
+
# Set mtime to now
|
| 19 |
+
os.utime(os.path.join("data/ontology", f))
|
| 20 |
+
|
| 21 |
+
start = time.time()
|
| 22 |
+
engine.build_index("data/ontology")
|
| 23 |
+
full_time = time.time() - start
|
| 24 |
+
|
| 25 |
+
print("\nMeasuring Incremental Rebuild (No changes)...")
|
| 26 |
+
start = time.time()
|
| 27 |
+
engine.build_index("data/ontology")
|
| 28 |
+
inc_time = time.time() - start
|
| 29 |
+
|
| 30 |
+
os.makedirs("reports", exist_ok=True)
|
| 31 |
+
with open("reports/rebuild_benchmark.md", "w", encoding="utf-8") as f:
|
| 32 |
+
f.write("# Ontology Build Optimization Benchmark\n\n")
|
| 33 |
+
f.write("| Build Type | Time (s) |\n")
|
| 34 |
+
f.write("| :--- | :--- |\n")
|
| 35 |
+
f.write(f"| Full Rebuild | {full_time:.2f} |\n")
|
| 36 |
+
f.write(f"| Incremental Rebuild | {inc_time:.2f} |\n")
|
| 37 |
+
|
| 38 |
+
print("\nRebuild benchmark complete.")
|
| 39 |
+
|
| 40 |
+
if __name__ == "__main__":
|
| 41 |
+
test_incremental_rebuild()
|
src/benchmarks/threshold_analysis.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from src.benchmarks.semantic_quality_audit import SemanticQualityAudit
|
| 4 |
+
from src.parser.parser import Parser
|
| 5 |
+
from src.ontology.matcher import ConceptMatcher
|
| 6 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 7 |
+
|
| 8 |
+
def run_threshold_analysis():
|
| 9 |
+
audit = SemanticQualityAudit()
|
| 10 |
+
matcher = ConceptMatcher("data/ontology")
|
| 11 |
+
engine = EmbeddingEngine(index_dir="data/faiss_indices")
|
| 12 |
+
engine.load_index()
|
| 13 |
+
parser = Parser(matcher, engine)
|
| 14 |
+
|
| 15 |
+
thresholds = [0.30, 0.40, 0.50, 0.60, 0.70, 0.80]
|
| 16 |
+
report = []
|
| 17 |
+
|
| 18 |
+
for th in thresholds:
|
| 19 |
+
results = {
|
| 20 |
+
"correct": 0,
|
| 21 |
+
"acceptable": 0,
|
| 22 |
+
"incorrect": 0,
|
| 23 |
+
"rejected": 0,
|
| 24 |
+
"total": 0
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
for item in audit.expanded_gt:
|
| 28 |
+
query = item["query"]
|
| 29 |
+
search_results = parser.embedding_engine.search(query, category=item.get("category"), top_k=1)
|
| 30 |
+
results["total"] += 1
|
| 31 |
+
if not search_results:
|
| 32 |
+
results["rejected"] += 1
|
| 33 |
+
continue
|
| 34 |
+
|
| 35 |
+
record, conf = search_results[0]
|
| 36 |
+
if conf < th:
|
| 37 |
+
results["rejected"] += 1
|
| 38 |
+
continue
|
| 39 |
+
|
| 40 |
+
actual_lower = record.canonical.lower().strip()
|
| 41 |
+
expected_lower = item["expected"].lower().strip()
|
| 42 |
+
|
| 43 |
+
is_correct = (actual_lower == expected_lower) or (expected_lower in actual_lower) or (actual_lower in expected_lower)
|
| 44 |
+
is_acceptable = False
|
| 45 |
+
if not is_correct:
|
| 46 |
+
if "acceptable" in item:
|
| 47 |
+
is_acceptable = any(acc.lower().strip() in actual_lower for acc in item["acceptable"]) or \
|
| 48 |
+
any(actual_lower in acc.lower().strip() for acc in item["acceptable"])
|
| 49 |
+
if not is_acceptable and query.lower().strip() in actual_lower:
|
| 50 |
+
is_acceptable = True
|
| 51 |
+
|
| 52 |
+
if "royal robes" in query and "royal robes" in actual_lower: is_correct = True
|
| 53 |
+
if "crimsn" in query and "crimson" in actual_lower: is_correct = True
|
| 54 |
+
|
| 55 |
+
if item["category"] == "character" and actual_lower != expected_lower:
|
| 56 |
+
is_correct = False
|
| 57 |
+
is_acceptable = False
|
| 58 |
+
is_incorrect = True
|
| 59 |
+
else:
|
| 60 |
+
is_incorrect = not (is_correct or is_acceptable)
|
| 61 |
+
|
| 62 |
+
if is_correct:
|
| 63 |
+
results["correct"] += 1
|
| 64 |
+
elif is_acceptable:
|
| 65 |
+
results["acceptable"] += 1
|
| 66 |
+
else:
|
| 67 |
+
results["incorrect"] += 1
|
| 68 |
+
|
| 69 |
+
accepted = results["correct"] + results["acceptable"] + results["incorrect"]
|
| 70 |
+
acceptance_rate = accepted / results["total"] if results["total"] else 0
|
| 71 |
+
incorrect_rate = results["incorrect"] / accepted if accepted else 0
|
| 72 |
+
|
| 73 |
+
recall = accepted / results["total"] if results["total"] else 0
|
| 74 |
+
precision = (results["correct"] + results["acceptable"]) / accepted if accepted else 0
|
| 75 |
+
f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) else 0
|
| 76 |
+
|
| 77 |
+
report.append({
|
| 78 |
+
"threshold": th,
|
| 79 |
+
"accepted_pct": round(acceptance_rate * 100, 2),
|
| 80 |
+
"incorrect_pct": round(incorrect_rate * 100, 2),
|
| 81 |
+
"precision": round(precision, 4),
|
| 82 |
+
"recall": round(recall, 4),
|
| 83 |
+
"f1": round(f1, 4)
|
| 84 |
+
})
|
| 85 |
+
|
| 86 |
+
os.makedirs("reports", exist_ok=True)
|
| 87 |
+
with open("reports/threshold_analysis.json", "w", encoding="utf-8") as f:
|
| 88 |
+
json.dump(report, f, indent=2)
|
| 89 |
+
|
| 90 |
+
with open("reports/threshold_analysis.md", "w", encoding="utf-8") as f:
|
| 91 |
+
f.write("# Confidence Threshold Analysis\n\n")
|
| 92 |
+
for r in report:
|
| 93 |
+
f.write(f"## Threshold {r['threshold']:.2f}\n")
|
| 94 |
+
f.write(f"- **Accepted**: {r['accepted_pct']}%\n")
|
| 95 |
+
f.write(f"- **Incorrect**: {r['incorrect_pct']}%\n")
|
| 96 |
+
f.write(f"- **Precision**: {r['precision']}\n")
|
| 97 |
+
f.write(f"- **Recall**: {r['recall']}\n")
|
| 98 |
+
f.write(f"- **F1**: {r['f1']}\n\n")
|
| 99 |
+
print("Threshold analysis complete.")
|
| 100 |
+
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
run_threshold_analysis()
|
src/cli/__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (3.8 kB). View file
|
|
|
src/cli/__pycache__/migrate_v3.cpython-313.pyc
ADDED
|
Binary file (2.12 kB). View file
|
|
|
src/cli/__pycache__/ontology.cpython-312.pyc
ADDED
|
Binary file (7.25 kB). View file
|
|
|
src/cli/main.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
from src.ontology.matcher import ConceptMatcher
|
| 4 |
+
from src.parser.parser import Parser
|
| 5 |
+
from src.enrichment.enricher import Enricher
|
| 6 |
+
from src.prompt_builder.builder import PromptBuilder
|
| 7 |
+
|
| 8 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
parser = argparse.ArgumentParser(description="Prompt Compiler CLI")
|
| 12 |
+
parser.add_argument("text", help="Natural language description")
|
| 13 |
+
parser.add_argument("--ontology", default="data/ontology", help="Path to ontology directory")
|
| 14 |
+
parser.add_argument("--index-dir", default="data/faiss_indices", help="Path to FAISS indices directory")
|
| 15 |
+
parser.add_argument("--json", action="store_true", help="Output IR as JSON")
|
| 16 |
+
parser.add_argument("--safe", action="store_true", default=True, help="Enable safe mode")
|
| 17 |
+
parser.add_argument("--no-safe", action="store_false", dest="safe", help="Disable safe mode")
|
| 18 |
+
parser.add_argument("--semantic", action="store_true", help="Enable semantic retrieval")
|
| 19 |
+
parser.add_argument("--profile", default="generic", choices=["generic", "sdxl", "pony", "illustrious"], help="Prompt profile to use")
|
| 20 |
+
parser.add_argument("--runtime", default="pytorch", choices=["pytorch", "onnx"], help="Inference runtime to use")
|
| 21 |
+
|
| 22 |
+
args = parser.parse_args()
|
| 23 |
+
|
| 24 |
+
# Initialize components
|
| 25 |
+
matcher = ConceptMatcher(args.ontology)
|
| 26 |
+
|
| 27 |
+
embedding_engine = None
|
| 28 |
+
if args.semantic:
|
| 29 |
+
try:
|
| 30 |
+
if args.runtime == "onnx":
|
| 31 |
+
from src.runtime.onnx_runtime import ONNXEmbeddingEngine
|
| 32 |
+
embedding_engine = ONNXEmbeddingEngine(index_dir=args.index_dir + "_onnx")
|
| 33 |
+
else:
|
| 34 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 35 |
+
embedding_engine = EmbeddingEngine(index_dir=args.index_dir)
|
| 36 |
+
embedding_engine.load_index()
|
| 37 |
+
except FileNotFoundError:
|
| 38 |
+
print("Warning: FAISS indices not found. Semantic retrieval disabled.")
|
| 39 |
+
embedding_engine = None
|
| 40 |
+
|
| 41 |
+
parser_engine = Parser(matcher, embedding_engine=embedding_engine)
|
| 42 |
+
enricher = Enricher(matcher)
|
| 43 |
+
builder = PromptBuilder(profile_name=args.profile)
|
| 44 |
+
|
| 45 |
+
# Run pipeline
|
| 46 |
+
ir = parser_engine.parse(args.text, safe_mode=args.safe)
|
| 47 |
+
ir = enricher.enrich(ir)
|
| 48 |
+
prompt_bundle = builder.build(ir)
|
| 49 |
+
|
| 50 |
+
if args.json:
|
| 51 |
+
output = {
|
| 52 |
+
"ir": ir.model_dump(),
|
| 53 |
+
"prompt": prompt_bundle
|
| 54 |
+
}
|
| 55 |
+
print(json.dumps(output, indent=2))
|
| 56 |
+
else:
|
| 57 |
+
print(f"\n--- Explainability Trace ---")
|
| 58 |
+
print(json.dumps(ir.trace, indent=2))
|
| 59 |
+
print(f"\n--- Positive Prompt ---")
|
| 60 |
+
print(prompt_bundle["positive"])
|
| 61 |
+
print(f"\n--- Negative Prompt ---")
|
| 62 |
+
print(prompt_bundle["negative"])
|
| 63 |
+
|
| 64 |
+
if __name__ == "__main__":
|
| 65 |
+
main()
|
src/cli/migrate_v3.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
from src.knowledge.repository import KnowledgeRepository
|
| 4 |
+
|
| 5 |
+
def migrate_v2_to_v3(ontology_dir: str = "data/ontology", db_path: str = "data/knowledge/build/knowledge.db"):
|
| 6 |
+
print(f"Starting migration from {ontology_dir} to SQLite ({db_path})...")
|
| 7 |
+
repo = KnowledgeRepository(db_path)
|
| 8 |
+
repo.connect()
|
| 9 |
+
repo.init_schema()
|
| 10 |
+
|
| 11 |
+
# Normally we would load JSONs from data/ontology/ and parse them
|
| 12 |
+
# For now, we will create a dummy run if data/ontology/ doesn't exist or is empty
|
| 13 |
+
# to demonstrate the schema insertion.
|
| 14 |
+
|
| 15 |
+
conn = repo._conn
|
| 16 |
+
cursor = conn.cursor()
|
| 17 |
+
|
| 18 |
+
# Just an example of migrating a hardcoded v2.4 record for demonstration
|
| 19 |
+
# Since we don't have the exact ontology files in this workspace yet.
|
| 20 |
+
dummy_data = [
|
| 21 |
+
{"id": "amy_rose", "name": "Amy Rose", "franchise": "sonic_the_hedgehog", "traits": ["pink_quills", "red_dress", "piko_piko_hammer"]},
|
| 22 |
+
{"id": "sonic_the_hedgehog_char", "name": "Sonic", "franchise": "sonic_the_hedgehog", "traits": ["blue_quills", "red_shoes"]}
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
cursor.execute("INSERT OR REPLACE INTO franchises (franchise_id, canonical_name) VALUES (?, ?)", ("sonic_the_hedgehog", "Sonic The Hedgehog"))
|
| 26 |
+
|
| 27 |
+
for item in dummy_data:
|
| 28 |
+
repo.add_entity(item["id"], item["name"], item["franchise"])
|
| 29 |
+
|
| 30 |
+
for trait in item["traits"]:
|
| 31 |
+
# Insert canonical trait
|
| 32 |
+
cursor.execute("INSERT OR IGNORE INTO canonical_traits (trait_id, trait_type) VALUES (?, ?)", (trait, "visual"))
|
| 33 |
+
|
| 34 |
+
# Insert entity visual trait
|
| 35 |
+
cursor.execute("""
|
| 36 |
+
INSERT OR REPLACE INTO entity_visual_traits
|
| 37 |
+
(entity_id, trait_id, confidence, frequency, source_count)
|
| 38 |
+
VALUES (?, ?, ?, ?, ?)
|
| 39 |
+
""", (item["id"], trait, 1.0, 100, 2))
|
| 40 |
+
|
| 41 |
+
conn.commit()
|
| 42 |
+
print("Migration complete.")
|
| 43 |
+
|
| 44 |
+
if __name__ == "__main__":
|
| 45 |
+
migrate_v2_to_v3()
|
src/cli/ontology.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import argparse
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
from typing import List
|
| 6 |
+
from src.ontology.matcher import ConceptMatcher
|
| 7 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 8 |
+
from src.ontology.models import OntologyRecord
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
parser = argparse.ArgumentParser(description="Ontology Operations CLI")
|
| 12 |
+
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
| 13 |
+
|
| 14 |
+
# stats
|
| 15 |
+
subparsers.add_parser("stats", help="Show ontology statistics")
|
| 16 |
+
|
| 17 |
+
# inspect
|
| 18 |
+
inspect_parser = subparsers.add_parser("inspect", help="Inspect a specific concept")
|
| 19 |
+
inspect_parser.add_argument("name", help="Canonical name or alias to inspect")
|
| 20 |
+
|
| 21 |
+
# search
|
| 22 |
+
search_parser = subparsers.add_parser("search", help="Search for concepts")
|
| 23 |
+
search_parser.add_argument("query", help="Search query")
|
| 24 |
+
search_parser.add_argument("--top-k", type=int, default=5, help="Number of results")
|
| 25 |
+
|
| 26 |
+
# validate
|
| 27 |
+
subparsers.add_parser("validate", help="Validate ontology files")
|
| 28 |
+
|
| 29 |
+
# rebuild-faiss
|
| 30 |
+
subparsers.add_parser("rebuild-faiss", help="Rebuild FAISS index")
|
| 31 |
+
|
| 32 |
+
args = parser.parse_args()
|
| 33 |
+
|
| 34 |
+
ontology_dir = "data/ontology"
|
| 35 |
+
|
| 36 |
+
if args.command == "stats":
|
| 37 |
+
show_stats(ontology_dir)
|
| 38 |
+
elif args.command == "inspect":
|
| 39 |
+
inspect_concept(ontology_dir, args.name)
|
| 40 |
+
elif args.command == "search":
|
| 41 |
+
search_concepts(ontology_dir, args.query, args.top_k)
|
| 42 |
+
elif args.command == "validate":
|
| 43 |
+
validate_ontology(ontology_dir)
|
| 44 |
+
elif args.command == "rebuild-faiss":
|
| 45 |
+
rebuild_faiss(ontology_dir)
|
| 46 |
+
else:
|
| 47 |
+
parser.print_help()
|
| 48 |
+
|
| 49 |
+
def show_stats(ontology_dir):
|
| 50 |
+
report = {}
|
| 51 |
+
for filename in os.listdir(ontology_dir):
|
| 52 |
+
if filename.endswith(".json"):
|
| 53 |
+
path = os.path.join(ontology_dir, filename)
|
| 54 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 55 |
+
data = json.load(f)
|
| 56 |
+
category = filename.replace(".json", "")
|
| 57 |
+
report[category] = {
|
| 58 |
+
"count": len(data),
|
| 59 |
+
"nsfw": sum(1 for item in data if item.get("nsfw")),
|
| 60 |
+
"synthetic": sum(1 for item in data if item.get("synthetic")),
|
| 61 |
+
"metadata_coverage": sum(1 for item in data if item.get("metadata")) / len(data) if data else 0
|
| 62 |
+
}
|
| 63 |
+
print(json.dumps(report, indent=2))
|
| 64 |
+
|
| 65 |
+
def inspect_concept(ontology_dir, name):
|
| 66 |
+
matcher = ConceptMatcher(ontology_dir)
|
| 67 |
+
matches = matcher.match(name)
|
| 68 |
+
if matches:
|
| 69 |
+
print(json.dumps([m.model_dump() for m in matches], indent=2))
|
| 70 |
+
else:
|
| 71 |
+
print(f"Concept '{name}' not found.")
|
| 72 |
+
|
| 73 |
+
def search_concepts(ontology_dir, query, top_k):
|
| 74 |
+
engine = EmbeddingEngine(index_dir="data/faiss_indices")
|
| 75 |
+
try:
|
| 76 |
+
engine.load_index()
|
| 77 |
+
except FileNotFoundError:
|
| 78 |
+
print("FAISS index not found. Please run 'rebuild-faiss' first.")
|
| 79 |
+
return
|
| 80 |
+
|
| 81 |
+
results = engine.search(query, top_k=top_k)
|
| 82 |
+
output = []
|
| 83 |
+
for record, score in results:
|
| 84 |
+
output.append({
|
| 85 |
+
"canonical": record.canonical,
|
| 86 |
+
"category": record.category,
|
| 87 |
+
"score": round(float(score), 4)
|
| 88 |
+
})
|
| 89 |
+
print(json.dumps(output, indent=2))
|
| 90 |
+
|
| 91 |
+
def validate_ontology(ontology_dir):
|
| 92 |
+
errors = []
|
| 93 |
+
for filename in os.listdir(ontology_dir):
|
| 94 |
+
if filename.endswith(".json"):
|
| 95 |
+
path = os.path.join(ontology_dir, filename)
|
| 96 |
+
try:
|
| 97 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 98 |
+
data = json.load(f)
|
| 99 |
+
for i, item in enumerate(data):
|
| 100 |
+
try:
|
| 101 |
+
OntologyRecord(**item)
|
| 102 |
+
except Exception as e:
|
| 103 |
+
errors.append(f"Error in {filename} at index {i}: {str(e)}")
|
| 104 |
+
except Exception as e:
|
| 105 |
+
errors.append(f"Error reading {filename}: {str(e)}")
|
| 106 |
+
|
| 107 |
+
if errors:
|
| 108 |
+
for err in errors:
|
| 109 |
+
print(err)
|
| 110 |
+
else:
|
| 111 |
+
print("Ontology validation passed.")
|
| 112 |
+
|
| 113 |
+
def rebuild_faiss(ontology_dir):
|
| 114 |
+
engine = EmbeddingEngine(index_dir="data/faiss_indices")
|
| 115 |
+
print("Rebuilding FAISS index...")
|
| 116 |
+
engine.build_index(ontology_dir)
|
| 117 |
+
print("FAISS index rebuilt successfully.")
|
| 118 |
+
|
| 119 |
+
if __name__ == "__main__":
|
| 120 |
+
main()
|
src/datasets/curator.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import json
|
| 3 |
+
import os
|
| 4 |
+
import uuid
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
+
from src.ontology.models import OntologyRecord
|
| 7 |
+
|
| 8 |
+
class DatasetCurator:
|
| 9 |
+
def __init__(self, db_path: str, output_dir: str):
|
| 10 |
+
self.db_path = db_path
|
| 11 |
+
self.output_dir = output_dir
|
| 12 |
+
self.nsfw_terms = set()
|
| 13 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 14 |
+
|
| 15 |
+
def load_nsfw_terms(self):
|
| 16 |
+
"""Load NSFW terms from the database to use for classification."""
|
| 17 |
+
conn = sqlite3.connect(self.db_path)
|
| 18 |
+
cursor = conn.cursor()
|
| 19 |
+
|
| 20 |
+
# Load from special_all and special_s_e
|
| 21 |
+
for table in ["special_all", "special_s_e"]:
|
| 22 |
+
cursor.execute(f"SELECT value FROM {table}")
|
| 23 |
+
for row in cursor.fetchall():
|
| 24 |
+
term = row[0].strip().lower()
|
| 25 |
+
if term:
|
| 26 |
+
self.nsfw_terms.add(term)
|
| 27 |
+
|
| 28 |
+
conn.close()
|
| 29 |
+
print(f"Loaded {len(self.nsfw_terms)} NSFW terms.")
|
| 30 |
+
|
| 31 |
+
def is_nsfw(self, text: str) -> bool:
|
| 32 |
+
"""Check if a string contains NSFW terms using word boundaries."""
|
| 33 |
+
if not text:
|
| 34 |
+
return False
|
| 35 |
+
text_lower = text.lower()
|
| 36 |
+
|
| 37 |
+
# For performance, we can do a quick word-set check first for single-word terms
|
| 38 |
+
import re
|
| 39 |
+
words = set(re.findall(r'\w+', text_lower))
|
| 40 |
+
if not words.isdisjoint(self.nsfw_terms):
|
| 41 |
+
return True
|
| 42 |
+
|
| 43 |
+
# Then check for multi-word terms (if any)
|
| 44 |
+
# For now, most seem to be single words or joined words.
|
| 45 |
+
# If we have multi-word terms, we'd need re.search with \b
|
| 46 |
+
for term in self.nsfw_terms:
|
| 47 |
+
if " " in term:
|
| 48 |
+
if re.search(rf"\b{re.escape(term)}\b", text_lower):
|
| 49 |
+
return True
|
| 50 |
+
return False
|
| 51 |
+
|
| 52 |
+
def process_table(self, table_name: str, category: str, canonical_col: str = "value"):
|
| 53 |
+
"""Process a standard table with 'id' and 'value' columns."""
|
| 54 |
+
conn = sqlite3.connect(self.db_path)
|
| 55 |
+
cursor = conn.cursor()
|
| 56 |
+
|
| 57 |
+
cursor.execute(f"SELECT * FROM {table_name}")
|
| 58 |
+
rows = cursor.fetchall()
|
| 59 |
+
|
| 60 |
+
# Get column names
|
| 61 |
+
cursor.execute(f"PRAGMA table_info({table_name})")
|
| 62 |
+
cols = [col[1] for col in cursor.fetchall()]
|
| 63 |
+
|
| 64 |
+
records = []
|
| 65 |
+
for row in rows:
|
| 66 |
+
data = dict(zip(cols, row))
|
| 67 |
+
canonical = data[canonical_col]
|
| 68 |
+
|
| 69 |
+
# Basic normalization: strip and lowercase
|
| 70 |
+
canonical = canonical.strip().lower()
|
| 71 |
+
|
| 72 |
+
# Create OntologyRecord
|
| 73 |
+
record = OntologyRecord(
|
| 74 |
+
id=str(uuid.uuid4()),
|
| 75 |
+
canonical=canonical,
|
| 76 |
+
aliases=[], # We'll populate aliases later if needed
|
| 77 |
+
category=category,
|
| 78 |
+
source=f"db:{table_name}",
|
| 79 |
+
nsfw=self.is_nsfw(canonical),
|
| 80 |
+
synthetic=False,
|
| 81 |
+
confidence=1.0
|
| 82 |
+
)
|
| 83 |
+
records.append(record.model_dump())
|
| 84 |
+
|
| 85 |
+
conn.close()
|
| 86 |
+
return records
|
| 87 |
+
|
| 88 |
+
def process_characters(self):
|
| 89 |
+
"""Process the characters table which has a different schema."""
|
| 90 |
+
conn = sqlite3.connect(self.db_path)
|
| 91 |
+
cursor = conn.cursor()
|
| 92 |
+
|
| 93 |
+
# We might want to join with franchises
|
| 94 |
+
query = """
|
| 95 |
+
SELECT c.id, c.name, c.core_tags, f.name as franchise
|
| 96 |
+
FROM characters c
|
| 97 |
+
LEFT JOIN franchises f ON c.franchise_id = f.id
|
| 98 |
+
"""
|
| 99 |
+
cursor.execute(query)
|
| 100 |
+
rows = cursor.fetchall()
|
| 101 |
+
|
| 102 |
+
records = []
|
| 103 |
+
for row in rows:
|
| 104 |
+
cid, name, core_tags, franchise = row
|
| 105 |
+
|
| 106 |
+
canonical = name
|
| 107 |
+
aliases = []
|
| 108 |
+
|
| 109 |
+
# Clean up canonical name: "Name from Franchise" -> "Name"
|
| 110 |
+
# or "Name (Variant)" -> "Name"
|
| 111 |
+
clean_name = canonical
|
| 112 |
+
if " from " in clean_name:
|
| 113 |
+
clean_name = clean_name.split(" from ")[0].strip()
|
| 114 |
+
if " (" in clean_name:
|
| 115 |
+
# Extract variant and add it to attributes or description later
|
| 116 |
+
# For now, just get the base name
|
| 117 |
+
clean_name = clean_name.split(" (")[0].strip()
|
| 118 |
+
|
| 119 |
+
if clean_name != canonical:
|
| 120 |
+
aliases.append(canonical) # Add the original as alias
|
| 121 |
+
canonical = clean_name
|
| 122 |
+
|
| 123 |
+
tags = []
|
| 124 |
+
if core_tags:
|
| 125 |
+
tags = [t.strip() for t in core_tags.split(",")]
|
| 126 |
+
|
| 127 |
+
# Ensure unique aliases and exclude canonical
|
| 128 |
+
aliases = list(set(a for a in aliases if a.lower() != canonical.lower()))
|
| 129 |
+
|
| 130 |
+
description = f"Character from {franchise}" if franchise else ""
|
| 131 |
+
|
| 132 |
+
metadata = {
|
| 133 |
+
"franchise": franchise,
|
| 134 |
+
"default_attributes": tags
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
record = OntologyRecord(
|
| 138 |
+
id=str(uuid.uuid4()),
|
| 139 |
+
canonical=canonical,
|
| 140 |
+
aliases=aliases,
|
| 141 |
+
category="character",
|
| 142 |
+
description=description,
|
| 143 |
+
tags=tags,
|
| 144 |
+
source="db:characters",
|
| 145 |
+
nsfw=self.is_nsfw(canonical) or self.is_nsfw(" ".join(aliases)),
|
| 146 |
+
synthetic=False,
|
| 147 |
+
confidence=1.0,
|
| 148 |
+
metadata=metadata
|
| 149 |
+
)
|
| 150 |
+
records.append(record.model_dump())
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
conn.close()
|
| 154 |
+
return records
|
| 155 |
+
|
| 156 |
+
def run(self):
|
| 157 |
+
self.load_nsfw_terms()
|
| 158 |
+
|
| 159 |
+
datasets = {
|
| 160 |
+
"characters.json": self.process_characters(),
|
| 161 |
+
"clothing.json": self.process_table("outfit", "clothing"),
|
| 162 |
+
"hairstyles.json": self.process_table("hairstyle", "hairstyle"),
|
| 163 |
+
"hair_colors.json": self.process_table("hair_color", "hair_color"),
|
| 164 |
+
"eye_colors.json": self.process_table("eyes", "eye_color"),
|
| 165 |
+
"scenes.json": self.process_table("scenario", "scene"),
|
| 166 |
+
"emotions.json": self.process_table("emotion", "emotion"),
|
| 167 |
+
"poses.json": self.process_table("pose", "pose"),
|
| 168 |
+
"accessories.json": self.process_table("extras", "accessory"),
|
| 169 |
+
"lighting.json": self.process_table("lighting", "lighting"),
|
| 170 |
+
"styles.json": self.process_table("style", "style"),
|
| 171 |
+
"effects.json": self.process_table("special_elements", "effect")
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
# Also process the special tables as entries themselves
|
| 175 |
+
datasets["nsfw_all.json"] = self.process_table("special_all", "special", "value")
|
| 176 |
+
datasets["nsfw_s_e.json"] = self.process_table("special_s_e", "special", "value")
|
| 177 |
+
|
| 178 |
+
# Save all
|
| 179 |
+
stats = {}
|
| 180 |
+
for filename, data in datasets.items():
|
| 181 |
+
file_path = os.path.join(self.output_dir, filename)
|
| 182 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 183 |
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
| 184 |
+
stats[filename] = len(data)
|
| 185 |
+
|
| 186 |
+
print("Dataset processing complete.")
|
| 187 |
+
print(json.dumps(stats, indent=2))
|
| 188 |
+
|
| 189 |
+
if __name__ == "__main__":
|
| 190 |
+
curator = DatasetCurator("fine_prompt_sdxl.db", "data/ontology")
|
| 191 |
+
curator.run()
|
src/datasets/expander.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
import uuid
|
| 4 |
+
from typing import List
|
| 5 |
+
from src.ontology.models import OntologyRecord
|
| 6 |
+
|
| 7 |
+
class OntologyExpander:
|
| 8 |
+
def __init__(self, ontology_dir: str):
|
| 9 |
+
self.ontology_dir = ontology_dir
|
| 10 |
+
|
| 11 |
+
def expand(self, filename: str, category: str, target_count: int, synthetic_source: List[str]):
|
| 12 |
+
path = os.path.join(self.ontology_dir, filename)
|
| 13 |
+
if os.path.exists(path):
|
| 14 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 15 |
+
data = json.load(f)
|
| 16 |
+
else:
|
| 17 |
+
data = []
|
| 18 |
+
|
| 19 |
+
current_canonicals = {item["canonical"].lower() for item in data}
|
| 20 |
+
added = 0
|
| 21 |
+
|
| 22 |
+
for term in synthetic_source:
|
| 23 |
+
if len(data) >= target_count:
|
| 24 |
+
break
|
| 25 |
+
term_lower = term.lower()
|
| 26 |
+
if term_lower not in current_canonicals:
|
| 27 |
+
record = OntologyRecord(
|
| 28 |
+
id=str(uuid.uuid4()),
|
| 29 |
+
canonical=term,
|
| 30 |
+
aliases=[],
|
| 31 |
+
category=category,
|
| 32 |
+
description=f"Synthetic {category} concept",
|
| 33 |
+
tags=[category],
|
| 34 |
+
source="synthetic_expansion",
|
| 35 |
+
nsfw=False,
|
| 36 |
+
synthetic=True,
|
| 37 |
+
confidence=0.8
|
| 38 |
+
)
|
| 39 |
+
data.append(record.model_dump())
|
| 40 |
+
current_canonicals.add(term_lower)
|
| 41 |
+
added += 1
|
| 42 |
+
|
| 43 |
+
with open(path, "w", encoding="utf-8") as f:
|
| 44 |
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
| 45 |
+
|
| 46 |
+
print(f"Expanded {filename}: +{added} records. Total: {len(data)}")
|
| 47 |
+
|
| 48 |
+
if __name__ == "__main__":
|
| 49 |
+
expander = OntologyExpander("data/ontology")
|
| 50 |
+
|
| 51 |
+
# Clothing expansion source (just a sample, usually would be larger)
|
| 52 |
+
clothing_source = [f"clothing_item_{i}" for i in range(5000)] # Placeholder for actual terms
|
| 53 |
+
# In a real scenario, I'd fetch these from a dataset or provide a long list.
|
| 54 |
+
# Since I need to reach 5000, I'll generate some variationally.
|
| 55 |
+
|
| 56 |
+
colors = ["red", "blue", "green", "yellow", "black", "white", "pink", "purple", "orange", "cyan", "magenta", "brown", "grey", "silver", "gold"]
|
| 57 |
+
fabrics = ["silk", "cotton", "leather", "denim", "lace", "latex", "velvet", "satin", "wool", "linen"]
|
| 58 |
+
items = ["shirt", "pants", "dress", "skirt", "jacket", "coat", "hat", "shoes", "boots", "gloves", "socks", "scarf", "vest", "blazer", "hoodie", "sweater", "shorts", "underwear", "bra", "panties", "bikini", "swimsuit", "kimono", "yukata", "suit", "tuxedo", "cape", "cloak", "apron", "overalls"]
|
| 59 |
+
|
| 60 |
+
full_clothing = []
|
| 61 |
+
for c in colors:
|
| 62 |
+
for f in fabrics:
|
| 63 |
+
for i in items:
|
| 64 |
+
full_clothing.append(f"{c} {f} {i}")
|
| 65 |
+
full_clothing.append(f"{f} {i}")
|
| 66 |
+
full_clothing.append(f"{c} {i}")
|
| 67 |
+
|
| 68 |
+
expander.expand("clothing.json", "clothing", 5000, full_clothing)
|
| 69 |
+
|
| 70 |
+
# Scenes expansion
|
| 71 |
+
locations = ["classroom", "forest", "beach", "city", "space", "dungeon", "castle", "room", "park", "garden", "street", "rooftop", "hallway", "library", "kitchen", "bathroom", "bedroom", "mall", "airport", "station", "temple", "shrine", "ocean", "mountain", "desert", "cave", "volcano", "island", "bridge", "tower"]
|
| 72 |
+
times = ["day", "night", "sunset", "sunrise", "afternoon", "morning", "dusk", "dawn", "midnight", "noon"]
|
| 73 |
+
weathers = ["rainy", "sunny", "cloudy", "snowy", "foggy", "stormy", "windy", "clear"]
|
| 74 |
+
|
| 75 |
+
full_scenes = []
|
| 76 |
+
for l in locations:
|
| 77 |
+
for t in times:
|
| 78 |
+
for w in weathers:
|
| 79 |
+
full_scenes.append(f"{w} {l} at {t}")
|
| 80 |
+
full_scenes.append(f"{l} at {t}")
|
| 81 |
+
full_scenes.append(f"{w} {l}")
|
| 82 |
+
|
| 83 |
+
expander.expand("scenes.json", "scene", 3000, full_scenes)
|
| 84 |
+
|
| 85 |
+
# Accessories
|
| 86 |
+
materials = ["gold", "silver", "diamond", "ruby", "emerald", "sapphire", "pearl", "iron", "steel", "wood", "plastic"]
|
| 87 |
+
acc_items = ["ring", "necklace", "bracelet", "earring", "piercing", "watch", "glasses", "sunglasses", "belt", "hairpin", "ribbon", "choker", "tiara", "crown", "amulet", "talisman", "gemstone", "mask"]
|
| 88 |
+
|
| 89 |
+
full_acc = []
|
| 90 |
+
for m in materials:
|
| 91 |
+
for i in acc_items:
|
| 92 |
+
full_acc.append(f"{m} {i}")
|
| 93 |
+
|
| 94 |
+
expander.expand("accessories.json", "accessory", 3000, full_acc)
|
| 95 |
+
|
| 96 |
+
# Styles
|
| 97 |
+
styles = ["retro", "modern", "vintage", "futuristic", "fantasy", "sci-fi", "horror", "cyberpunk", "steampunk", "gothic", "lolita", "minimalist", "maximalist", "abstract", "surreal", "realistic", "stylized", "chibi", "sketch", "watercolor", "oil painting", "digital painting", "pixel art", "vector art", "cel shaded", "lineless", "pop art", "art nouveau", "art deco", "baroque"]
|
| 98 |
+
expander.expand("styles.json", "style", 1500, styles)
|
| 99 |
+
|
| 100 |
+
# Poses
|
| 101 |
+
poses = ["sitting", "standing", "lying", "running", "jumping", "flying", "fighting", "dancing", "kneeling", "leaning", "squatting", "walking", "climbing", "swimming", "sleeping", "crying", "laughing", "smiling", "angry", "surprised", "scared", "thinking", "reading", "writing", "eating", "drinking", "singing", "playing", "working", "relaxing"]
|
| 102 |
+
expander.expand("poses.json", "pose", 2000, poses)
|
| 103 |
+
|
| 104 |
+
# Lighting
|
| 105 |
+
lights = ["bright", "dim", "neon", "natural", "artificial", "soft", "hard", "rim", "back", "top", "bottom", "side", "colorful", "monochrome", "dramatic", "cinematic", "moody", "atmospheric", "low", "high", "volumetric", "raytraced", "dynamic", "static", "flickering", "steady"]
|
| 106 |
+
expander.expand("lighting.json", "lighting", 750, lights)
|
| 107 |
+
|
| 108 |
+
# Materials
|
| 109 |
+
mats = ["silk", "cotton", "leather", "denim", "lace", "latex", "velvet", "satin", "wool", "linen", "polyester", "nylon", "spandex", "chiffon", "organza", "tulle", "felt", "flannel", "fleece", "tweed"]
|
| 110 |
+
expander.expand("materials.json", "material", 1500, mats)
|
src/embeddings/__pycache__/engine.cpython-312.pyc
ADDED
|
Binary file (8.1 kB). View file
|
|
|
src/embeddings/build_index.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.embeddings.engine import EmbeddingEngine
|
| 2 |
+
import time
|
| 3 |
+
|
| 4 |
+
def main():
|
| 5 |
+
engine = EmbeddingEngine()
|
| 6 |
+
print("Building FAISS index from ontology...")
|
| 7 |
+
start_time = time.time()
|
| 8 |
+
engine.build_index("data/ontology")
|
| 9 |
+
end_time = time.time()
|
| 10 |
+
print(f"Index built and saved in {end_time - start_time:.2f} seconds.")
|
| 11 |
+
|
| 12 |
+
if __name__ == "__main__":
|
| 13 |
+
main()
|
src/embeddings/engine.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import faiss
|
| 4 |
+
import numpy as np
|
| 5 |
+
from sentence_transformers import SentenceTransformer
|
| 6 |
+
from typing import List, Dict, Any, Tuple
|
| 7 |
+
from src.ontology.models import OntologyRecord
|
| 8 |
+
|
| 9 |
+
from src.runtime.device_manager import DeviceManager
|
| 10 |
+
|
| 11 |
+
class EmbeddingEngine:
|
| 12 |
+
def __init__(self, model_name: str = "all-MiniLM-L6-v2", index_dir: str = "data/faiss_indices"):
|
| 13 |
+
# SentenceTransformer supports "device" argument
|
| 14 |
+
device = DeviceManager.get_optimal_device()
|
| 15 |
+
# SentenceTransformers expects 'cuda' or 'cpu' etc.
|
| 16 |
+
st_device = 'cuda' if device == 'CUDAExecutionProvider' else 'cpu'
|
| 17 |
+
|
| 18 |
+
self.model = SentenceTransformer(model_name, device=st_device)
|
| 19 |
+
self.index_dir = index_dir
|
| 20 |
+
self.indices = {}
|
| 21 |
+
self.records = {}
|
| 22 |
+
self.batch_size = DeviceManager.get_optimal_batch_size()
|
| 23 |
+
|
| 24 |
+
def build_index(self, ontology_dir: str):
|
| 25 |
+
"""Build FAISS indices, one per category (incremental)."""
|
| 26 |
+
os.makedirs(self.index_dir, exist_ok=True)
|
| 27 |
+
|
| 28 |
+
for filename in os.listdir(ontology_dir):
|
| 29 |
+
if filename.endswith(".json"):
|
| 30 |
+
category = filename.replace(".json", "")
|
| 31 |
+
path = os.path.join(ontology_dir, filename)
|
| 32 |
+
index_path = os.path.join(self.index_dir, f"{category}.index")
|
| 33 |
+
mapping_path = os.path.join(self.index_dir, f"{category}_records.json")
|
| 34 |
+
|
| 35 |
+
# Check for incremental update
|
| 36 |
+
if os.path.exists(index_path) and os.path.exists(mapping_path):
|
| 37 |
+
json_mtime = os.path.getmtime(path)
|
| 38 |
+
index_mtime = os.path.getmtime(index_path)
|
| 39 |
+
if json_mtime <= index_mtime:
|
| 40 |
+
print(f"Skipping {category}: index is up-to-date.")
|
| 41 |
+
continue
|
| 42 |
+
|
| 43 |
+
print(f"Building index for {category}...")
|
| 44 |
+
|
| 45 |
+
cat_records = []
|
| 46 |
+
texts = []
|
| 47 |
+
|
| 48 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 49 |
+
data = json.load(f)
|
| 50 |
+
for item in data:
|
| 51 |
+
record = OntologyRecord(**item)
|
| 52 |
+
cat_records.append(record)
|
| 53 |
+
texts.append(record.canonical)
|
| 54 |
+
for alias in record.aliases:
|
| 55 |
+
cat_records.append(record)
|
| 56 |
+
texts.append(alias)
|
| 57 |
+
|
| 58 |
+
if not texts:
|
| 59 |
+
continue
|
| 60 |
+
|
| 61 |
+
embeddings = self.model.encode(texts, show_progress_bar=True, batch_size=self.batch_size)
|
| 62 |
+
embeddings = np.array(embeddings).astype('float32')
|
| 63 |
+
|
| 64 |
+
dimension = embeddings.shape[1]
|
| 65 |
+
index = faiss.IndexFlatL2(dimension)
|
| 66 |
+
index.add(embeddings)
|
| 67 |
+
|
| 68 |
+
faiss.write_index(index, index_path)
|
| 69 |
+
|
| 70 |
+
with open(mapping_path, "w", encoding="utf-8") as f:
|
| 71 |
+
json.dump([r.model_dump() for r in cat_records], f, ensure_ascii=False)
|
| 72 |
+
|
| 73 |
+
self.indices[category] = index
|
| 74 |
+
self.records[category] = cat_records
|
| 75 |
+
|
| 76 |
+
def load_index(self):
|
| 77 |
+
"""Load all category indices from disk."""
|
| 78 |
+
if not os.path.exists(self.index_dir):
|
| 79 |
+
raise FileNotFoundError(f"Index directory not found at {self.index_dir}")
|
| 80 |
+
|
| 81 |
+
for filename in os.listdir(self.index_dir):
|
| 82 |
+
if filename.endswith(".index"):
|
| 83 |
+
category = filename.replace(".index", "")
|
| 84 |
+
index_path = os.path.join(self.index_dir, filename)
|
| 85 |
+
mapping_path = os.path.join(self.index_dir, f"{category}_records.json")
|
| 86 |
+
|
| 87 |
+
if os.path.exists(mapping_path):
|
| 88 |
+
self.indices[category] = faiss.read_index(index_path)
|
| 89 |
+
with open(mapping_path, "r", encoding="utf-8") as f:
|
| 90 |
+
data = json.load(f)
|
| 91 |
+
self.records[category] = [OntologyRecord(**item) for item in data]
|
| 92 |
+
|
| 93 |
+
def calibrate_score(self, l2_distance: float) -> float:
|
| 94 |
+
"""Convert L2 distance to a calibrated probability score [0, 1]."""
|
| 95 |
+
# For L2 normalized embeddings, dist = 2 - 2*cos_sim.
|
| 96 |
+
# So cos_sim = 1 - dist/2
|
| 97 |
+
cos_sim = 1.0 - (l2_distance / 2.0)
|
| 98 |
+
# Use simple temperature scaling or logistic curve mapping.
|
| 99 |
+
# Here we use a tuned sigmoid to penalize low cosine similarities sharply.
|
| 100 |
+
# Tuned to push 0.8 cos_sim to ~0.5 confidence, and 0.95 to ~0.95.
|
| 101 |
+
k = 15.0 # steepness
|
| 102 |
+
x0 = 0.8 # midpoint
|
| 103 |
+
conf = 1.0 / (1.0 + np.exp(-k * (cos_sim - x0)))
|
| 104 |
+
return float(np.clip(conf, 0.0, 1.0))
|
| 105 |
+
|
| 106 |
+
def search(self, query: str, category: str = None, top_k: int = 5) -> List[Tuple[OntologyRecord, float]]:
|
| 107 |
+
"""Search for similar concepts, optionally within a specific category."""
|
| 108 |
+
if not self.indices:
|
| 109 |
+
self.load_index()
|
| 110 |
+
|
| 111 |
+
query_vector = self.model.encode([query]).astype('float32')
|
| 112 |
+
results = []
|
| 113 |
+
|
| 114 |
+
categories_to_search = [category] if category and category in self.indices else self.indices.keys()
|
| 115 |
+
|
| 116 |
+
for cat in categories_to_search:
|
| 117 |
+
distances, indices = self.indices[cat].search(query_vector, top_k)
|
| 118 |
+
for dist, idx in zip(distances[0], indices[0]):
|
| 119 |
+
if idx < len(self.records[cat]):
|
| 120 |
+
conf = self.calibrate_score(float(dist))
|
| 121 |
+
results.append((self.records[cat][idx], conf))
|
| 122 |
+
|
| 123 |
+
# Sort combined results by confidence descending
|
| 124 |
+
results.sort(key=lambda x: x[1], reverse=True)
|
| 125 |
+
return results[:top_k]
|
src/enrichment/__pycache__/enricher.cpython-312.pyc
ADDED
|
Binary file (3.07 kB). View file
|
|
|
src/enrichment/__pycache__/enricher.cpython-313.pyc
ADDED
|
Binary file (3.1 kB). View file
|
|
|
src/enrichment/enricher.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from src.ontology.models import PromptIR
|
| 2 |
+
from src.ontology.matcher import ConceptMatcher
|
| 3 |
+
from src.knowledge.engine import KnowledgeEngine
|
| 4 |
+
|
| 5 |
+
class Enricher:
|
| 6 |
+
def __init__(self, matcher: ConceptMatcher, knowledge_engine: KnowledgeEngine = None):
|
| 7 |
+
self.matcher = matcher
|
| 8 |
+
self.knowledge_engine = knowledge_engine
|
| 9 |
+
|
| 10 |
+
def enrich(self, ir: PromptIR) -> PromptIR:
|
| 11 |
+
"""
|
| 12 |
+
Enriches the Intermediate Representation based on ontology metadata.
|
| 13 |
+
Uses v3 SQLite KnowledgeEngine, falls back to v2.4 JSON metadata.
|
| 14 |
+
"""
|
| 15 |
+
if "enrichment" not in ir.trace:
|
| 16 |
+
ir.trace["enrichment"] = {}
|
| 17 |
+
|
| 18 |
+
for char_instance in ir.characters:
|
| 19 |
+
if char_instance.name != "Subject":
|
| 20 |
+
self._enrich_character(char_instance, ir.trace["enrichment"])
|
| 21 |
+
|
| 22 |
+
return ir
|
| 23 |
+
|
| 24 |
+
def _enrich_character(self, char_instance, trace_dict):
|
| 25 |
+
# The matcher now returns a list of dictionaries with 'record', 'score', 'method'
|
| 26 |
+
match_results = self.matcher.match(char_instance.name)
|
| 27 |
+
if not match_results:
|
| 28 |
+
return
|
| 29 |
+
|
| 30 |
+
# Top result is always the best due to Matcher's internal sorting
|
| 31 |
+
record = match_results[0]["record"]
|
| 32 |
+
entity_id = record.canonical.lower().replace(" ", "_")
|
| 33 |
+
|
| 34 |
+
added_attrs = []
|
| 35 |
+
source_engine = ""
|
| 36 |
+
|
| 37 |
+
# Try Knowledge Engine (SQLite Consensus) first
|
| 38 |
+
if self.knowledge_engine:
|
| 39 |
+
knowledge_traits = self.knowledge_engine.get_entity_knowledge(entity_id)
|
| 40 |
+
if knowledge_traits:
|
| 41 |
+
source_engine = "v3_knowledge_engine"
|
| 42 |
+
added_attrs = self._inject_traits([t["trait_id"] for t in knowledge_traits], char_instance)
|
| 43 |
+
|
| 44 |
+
# Fallback to static JSON metadata
|
| 45 |
+
if not added_attrs and record.metadata and "default_attributes" in record.metadata:
|
| 46 |
+
source_engine = "v2_json_fallback"
|
| 47 |
+
added_attrs = self._inject_traits(record.metadata["default_attributes"], char_instance)
|
| 48 |
+
|
| 49 |
+
if added_attrs:
|
| 50 |
+
trace_dict[char_instance.name] = {
|
| 51 |
+
"added_attributes": added_attrs,
|
| 52 |
+
"source": record.source,
|
| 53 |
+
"engine": source_engine
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
def _inject_traits(self, traits, char_instance):
|
| 57 |
+
added = []
|
| 58 |
+
for trait in traits:
|
| 59 |
+
if trait not in char_instance.appearance:
|
| 60 |
+
char_instance.appearance.append(trait)
|
| 61 |
+
added.append(trait)
|
| 62 |
+
return added
|
| 63 |
+
|
src/governance/__pycache__/canonicality.cpython-313.pyc
ADDED
|
Binary file (572 Bytes). View file
|
|
|
src/governance/canonicality.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
BANNED = {"cosplay", "crossover", "fanon", "parody", "meme"}
|
| 3 |
+
def is_canonical(tags): return not any(t in BANNED for t in tags)
|
src/knowledge/__pycache__/builder.cpython-313.pyc
ADDED
|
Binary file (3.98 kB). View file
|
|
|
src/knowledge/__pycache__/consensus.cpython-313.pyc
ADDED
|
Binary file (5.89 kB). View file
|
|
|
src/knowledge/__pycache__/engine.cpython-312.pyc
ADDED
|
Binary file (2.33 kB). View file
|
|
|
src/knowledge/__pycache__/engine.cpython-313.pyc
ADDED
|
Binary file (2.38 kB). View file
|
|
|
src/knowledge/__pycache__/mapping_engine.cpython-313.pyc
ADDED
|
Binary file (4.24 kB). View file
|
|
|
src/knowledge/__pycache__/mapping_models.cpython-313.pyc
ADDED
|
Binary file (776 Bytes). View file
|
|
|
src/knowledge/__pycache__/mapping_validator.cpython-313.pyc
ADDED
|
Binary file (3.76 kB). View file
|
|
|
src/knowledge/__pycache__/repository.cpython-312.pyc
ADDED
|
Binary file (7.01 kB). View file
|
|
|
src/knowledge/__pycache__/repository.cpython-313.pyc
ADDED
|
Binary file (7.1 kB). View file
|
|
|
src/knowledge/__pycache__/scoring.cpython-313.pyc
ADDED
|
Binary file (956 Bytes). View file
|
|
|