feat: collega il lessico semantico al filler
This commit is contained in:
149
main.py
149
main.py
@@ -1,8 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
from typing import Dict, List
|
||||
|
||||
from build_vocabulary import (
|
||||
FILTERED_OUTPUT_PATH,
|
||||
@@ -10,39 +11,61 @@ from build_vocabulary import (
|
||||
OUTPUT_PATH,
|
||||
build_vocabulary,
|
||||
)
|
||||
from build_lexicon import LEXICON_OUTPUT_PATH, build_lexicon
|
||||
from build_semantic_lexicon import SEMANTIC_LEXICON_OUTPUT_PATH, build_semantic_lexicon
|
||||
from crossword_filler import CrosswordFiller, load_vocabulary, load_vocabulary_metadata
|
||||
from crossword_generator import CrosswordGenerator, WORDS, render_grid
|
||||
|
||||
|
||||
DIFFICULTY_ALIASES: Dict[str, int] = {
|
||||
"easy": 1,
|
||||
"medium": 2,
|
||||
"hard": 4,
|
||||
"expert": 5,
|
||||
}
|
||||
|
||||
DEFAULT_TOPIC = "general"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Generatore e filler di cruciverba.")
|
||||
parser.add_argument(
|
||||
"--build-vocabulary",
|
||||
action="store_true",
|
||||
help="Rigenera il vocabolario esteso, filtrato e i metadati prima dell'esecuzione.",
|
||||
help="Rigenera i file lessicali intermedi: vocabolario esteso, filtrato e metadati.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--build-lexicon",
|
||||
action="store_true",
|
||||
help="Rigenera `lexicon_it.json` prima dell'esecuzione.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-fill",
|
||||
action="store_true",
|
||||
help="Genera solo la griglia iniziale senza eseguire il filler.",
|
||||
help="Genera solo la griglia iniziale e salta il riempimento con il filler.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--build-semantic-lexicon",
|
||||
action="store_true",
|
||||
help="Rigenera `lexicon_it_semantic.json` arricchendo il lessico con IWN-OMW/ItalWordNet.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--vocabulary",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Percorso opzionale a un vocabolario personalizzato.",
|
||||
help="Percorso opzionale a un vocabolario testuale personalizzato da usare al posto di quello di default.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--target-empty-ratio",
|
||||
type=float,
|
||||
default=1 / 6,
|
||||
help="Rapporto target di celle vuote residue dopo il filler.",
|
||||
help="Rapporto target di celle vuote residue dopo il filler. Esempio: 0.1667 lascia circa un sesto di celle vuote.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--time-limit",
|
||||
type=float,
|
||||
default=8.0,
|
||||
help="Tempo massimo in secondi per la fase di generazione iniziale.",
|
||||
help="Tempo massimo in secondi per la fase di generazione iniziale della griglia.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-candidates",
|
||||
@@ -54,7 +77,23 @@ def parse_args() -> argparse.Namespace:
|
||||
"--diffxy",
|
||||
type=int,
|
||||
default=7,
|
||||
help="Differenza massima preferita tra larghezza e altezza della griglia.",
|
||||
help="Differenza massima preferita tra larghezza e altezza della griglia iniziale.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--seed",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Seed casuale per ottenere varianti riproducibili del cruciverba: stesso seed, stesso risultato.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--difficulty",
|
||||
default="medium",
|
||||
help="Difficolta lessicale del filler. Alias testuali: easy, medium, hard, expert. Internamente mappati a livelli numerici 1-5.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--topic",
|
||||
default=DEFAULT_TOPIC,
|
||||
help="Tema del cruciverba. Attualmente supporta i topic presenti nel lessico, ad esempio: general, nature, animals, actions, abstract.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -73,21 +112,110 @@ def ensure_vocabulary(args: argparse.Namespace) -> None:
|
||||
print(f"- parole filtrate: {totals['filtered_words']}")
|
||||
|
||||
|
||||
def ensure_lexicon(args: argparse.Namespace) -> None:
|
||||
needs_build = args.build_lexicon or not LEXICON_OUTPUT_PATH.exists()
|
||||
if not needs_build:
|
||||
return
|
||||
|
||||
lexicon = build_lexicon()
|
||||
LEXICON_OUTPUT_PATH.write_text(
|
||||
json.dumps(lexicon, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
print("Lessico rigenerato")
|
||||
print(f"- file: {LEXICON_OUTPUT_PATH}")
|
||||
print(f"- voci: {lexicon['meta']['entry_count']}")
|
||||
|
||||
|
||||
def ensure_semantic_lexicon(args: argparse.Namespace) -> None:
|
||||
needs_build = args.build_semantic_lexicon or not SEMANTIC_LEXICON_OUTPUT_PATH.exists()
|
||||
if not needs_build:
|
||||
return
|
||||
|
||||
lexicon = build_semantic_lexicon()
|
||||
SEMANTIC_LEXICON_OUTPUT_PATH.write_text(
|
||||
json.dumps(lexicon, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
matched = sum(1 for entry in lexicon["entries"] if entry.get("semantic", {}).get("matched"))
|
||||
print("Lessico semantico rigenerato")
|
||||
print(f"- file: {SEMANTIC_LEXICON_OUTPUT_PATH}")
|
||||
print(f"- voci: {lexicon['meta']['entry_count']}")
|
||||
print(f"- match semantici: {matched}")
|
||||
|
||||
|
||||
def parse_difficulty(value: str) -> int:
|
||||
text = str(value).strip().lower()
|
||||
if text in DIFFICULTY_ALIASES:
|
||||
return DIFFICULTY_ALIASES[text]
|
||||
try:
|
||||
level = int(text)
|
||||
except ValueError as exc:
|
||||
raise SystemExit(
|
||||
"Valore non valido per --difficulty. Usa easy, medium, hard, expert oppure un intero tra 1 e 5."
|
||||
) from exc
|
||||
if not 1 <= level <= 5:
|
||||
raise SystemExit("Il valore numerico di --difficulty deve essere compreso tra 1 e 5.")
|
||||
return level
|
||||
|
||||
|
||||
def load_selected_vocabulary(path: Path | None) -> List[str]:
|
||||
if path is None:
|
||||
return load_vocabulary()
|
||||
return path.read_text(encoding="utf-8").splitlines()
|
||||
|
||||
|
||||
def load_filtered_vocabulary(level: int, topic: str) -> List[str]:
|
||||
if not LEXICON_OUTPUT_PATH.exists():
|
||||
lexicon = build_lexicon()
|
||||
LEXICON_OUTPUT_PATH.write_text(
|
||||
json.dumps(lexicon, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
payload = json.loads(LEXICON_OUTPUT_PATH.read_text(encoding="utf-8"))
|
||||
normalized_topic = topic.strip().lower()
|
||||
|
||||
def matches(entry: Dict[str, object], selected_topic: str) -> bool:
|
||||
topics = [str(item).lower() for item in entry.get("topics", [])]
|
||||
return selected_topic in topics
|
||||
|
||||
words = [
|
||||
entry["form"]
|
||||
for entry in payload.get("entries", [])
|
||||
if entry.get("allowed_in_crossword", False)
|
||||
and int(entry.get("difficulty_word", 5)) <= level
|
||||
and matches(entry, normalized_topic)
|
||||
]
|
||||
|
||||
if words:
|
||||
return words
|
||||
|
||||
if normalized_topic != DEFAULT_TOPIC:
|
||||
return [
|
||||
entry["form"]
|
||||
for entry in payload.get("entries", [])
|
||||
if entry.get("allowed_in_crossword", False)
|
||||
and int(entry.get("difficulty_word", 5)) <= level
|
||||
and matches(entry, DEFAULT_TOPIC)
|
||||
]
|
||||
|
||||
return words
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
ensure_vocabulary(args)
|
||||
ensure_lexicon(args)
|
||||
ensure_semantic_lexicon(args)
|
||||
difficulty_level = parse_difficulty(args.difficulty)
|
||||
|
||||
generator = CrosswordGenerator(
|
||||
WORDS,
|
||||
diffxy=args.diffxy,
|
||||
time_limit_seconds=args.time_limit,
|
||||
max_candidates_per_word=args.max_candidates,
|
||||
seed=args.seed,
|
||||
)
|
||||
initial_state = generator.solve()
|
||||
|
||||
@@ -95,19 +223,24 @@ def main() -> None:
|
||||
print(f"Parole inserite: {initial_state.placed_words}/{len(generator.words)}")
|
||||
print(f"Intersezioni: {initial_state.intersections}")
|
||||
print(f"Dimensioni: {initial_state.width()} x {initial_state.height()} (diff={initial_state.shape_difference()})")
|
||||
print(f"Difficolta filler: {args.difficulty} -> livello {difficulty_level}")
|
||||
print(f"Tema filler: {args.topic}")
|
||||
if args.seed is not None:
|
||||
print(f"Seed: {args.seed}")
|
||||
print()
|
||||
print(render_grid(initial_state.grid, initial_state.placements))
|
||||
|
||||
if args.skip_fill:
|
||||
return
|
||||
|
||||
vocabulary = load_selected_vocabulary(args.vocabulary)
|
||||
vocabulary = load_selected_vocabulary(args.vocabulary) if args.vocabulary else load_filtered_vocabulary(difficulty_level, args.topic)
|
||||
metadata = load_vocabulary_metadata()
|
||||
filler = CrosswordFiller(
|
||||
initial_state,
|
||||
vocabulary,
|
||||
target_empty_ratio=args.target_empty_ratio,
|
||||
vocabulary_metadata=metadata,
|
||||
seed=args.seed,
|
||||
)
|
||||
final_state = filler.fill()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user