vimalk78 commited on
Commit
ca91f9d
·
1 Parent(s): d9c2fad

feat(crossword): generated crosswords with clues

Browse files

Signed-off-by: Vimal Kumar <vimal78@gmail.com>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +18 -1
  2. CLAUDE.md +3 -1
  3. Dockerfile +5 -5
  4. VOCABULARY_OPTIMIZATION.md +164 -0
  5. crossword-app/backend-py/CROSSWORD_GENERATION_WALKTHROUGH.md +434 -0
  6. crossword-app/backend-py/README.md +224 -49
  7. crossword-app/backend-py/all-packages.txt +69 -0
  8. crossword-app/backend-py/app.py +80 -24
  9. crossword-app/backend-py/data/data +0 -1
  10. crossword-app/backend-py/data/word-lists/animals.json +0 -165
  11. crossword-app/backend-py/data/word-lists/geography.json +0 -161
  12. crossword-app/backend-py/data/word-lists/science.json +0 -170
  13. crossword-app/backend-py/data/word-lists/technology.json +0 -221
  14. crossword-app/backend-py/debug_full_generation.py +0 -316
  15. crossword-app/backend-py/debug_grid_direct.py +0 -293
  16. crossword-app/backend-py/debug_index_error.py +0 -307
  17. crossword-app/backend-py/debug_simple.py +0 -142
  18. crossword-app/backend-py/public/assets/index-2XJqMaqu.js +10 -0
  19. crossword-app/backend-py/public/assets/index-2XJqMaqu.js.map +1 -0
  20. crossword-app/backend-py/public/assets/index-7dkEH9uQ.js +10 -0
  21. crossword-app/backend-py/public/assets/index-7dkEH9uQ.js.map +1 -0
  22. crossword-app/backend-py/public/assets/index-CWqdoNhy.css +1 -0
  23. crossword-app/backend-py/public/assets/index-DyT-gQda.css +1 -0
  24. crossword-app/backend-py/public/assets/index-V4v18wFW.css +1 -0
  25. crossword-app/backend-py/public/assets/index-uK3VdD5a.js +10 -0
  26. crossword-app/backend-py/public/assets/index-uK3VdD5a.js.map +1 -0
  27. crossword-app/backend-py/public/assets/vendor-nf7bT_Uh.js +0 -0
  28. crossword-app/backend-py/public/assets/vendor-nf7bT_Uh.js.map +0 -0
  29. crossword-app/backend-py/public/index.html +16 -0
  30. crossword-app/backend-py/requirements.txt +13 -10
  31. crossword-app/backend-py/src/routes/api.py +144 -27
  32. crossword-app/backend-py/src/services/// +12 -0
  33. crossword-app/backend-py/src/services/__pycache__/__init__.cpython-310.pyc +0 -0
  34. crossword-app/backend-py/src/services/__pycache__/__init__.cpython-313.pyc +0 -0
  35. crossword-app/backend-py/src/services/__pycache__/crossword_generator.cpython-310.pyc +0 -0
  36. crossword-app/backend-py/src/services/__pycache__/crossword_generator.cpython-313.pyc +0 -0
  37. crossword-app/backend-py/src/services/__pycache__/crossword_generator_fixed.cpython-313.pyc +0 -0
  38. crossword-app/backend-py/src/services/__pycache__/crossword_generator_wrapper.cpython-313.pyc +0 -0
  39. crossword-app/backend-py/src/services/__pycache__/vector_search.cpython-313.pyc +0 -0
  40. crossword-app/backend-py/src/services/__pycache__/word_cache.cpython-313.pyc +0 -0
  41. crossword-app/backend-py/src/services/clue_generator.py +35 -0
  42. crossword-app/backend-py/src/services/crossword_generator.py +150 -89
  43. crossword-app/backend-py/src/services/crossword_generator_wrapper.py +16 -12
  44. crossword-app/backend-py/src/services/thematic_word_service.py +1057 -0
  45. crossword-app/backend-py/src/services/unified_word_service.py +250 -0
  46. crossword-app/backend-py/src/services/vector_search.py +109 -106
  47. crossword-app/backend-py/src/services/wordnet_clue_generator.py +640 -0
  48. crossword-app/backend-py/test-integration/test_boundary_fix.py +0 -147
  49. crossword-app/backend-py/test-integration/test_bounds_comprehensive.py +0 -266
  50. crossword-app/backend-py/test-integration/test_bounds_fix.py +0 -90
.gitignore CHANGED
@@ -49,7 +49,24 @@ pids
49
  ehthumbs.db
50
  Thumbs.db
51
 
52
- hack
 
 
 
 
 
 
 
 
 
 
 
53
  issues/
54
  samples/
55
  venv/
 
 
 
 
 
 
 
49
  ehthumbs.db
50
  Thumbs.db
51
 
52
+ # Python
53
+ __pycache__/
54
+ *.py[cod]
55
+ *$py.class
56
+ *.so
57
+ .Python
58
+ *.egg-info/
59
+ .pytest_cache/
60
+ .coverage
61
+ htmlcov/
62
+
63
+ # hack
64
  issues/
65
  samples/
66
  venv/
67
+ crossword-app/backend-py/src/services/model_cache/
68
+ hack/model_cache/
69
+ cache-dir/
70
+ .KARO.md
71
+ CLAUDE.md
72
+ crossword-app/backend-py/faiss_cache/
CLAUDE.md CHANGED
@@ -214,4 +214,6 @@ DATABASE_URL=postgresql://user:pass@host:port/db # Optional
214
  - Docker build time: ~5-10 minutes (includes frontend build + Python deps)
215
  - Container size: ~1.5GB (includes ML models and dependencies)
216
  - Hugging Face Spaces deployment: Automatic on git push
217
- - run unit tests after fixing a bug
 
 
 
214
  - Docker build time: ~5-10 minutes (includes frontend build + Python deps)
215
  - Container size: ~1.5GB (includes ML models and dependencies)
216
  - Hugging Face Spaces deployment: Automatic on git push
217
+ - run unit tests after fixing a bug
218
+ - do not use any static files for any word generation or clue gebneration.
219
+ - we do not prefer inference api based solution
Dockerfile CHANGED
@@ -1,6 +1,6 @@
1
  # Multi-stage build to optimize performance and security
2
  # Stage 1: Builder - Install dependencies and build as root
3
- FROM python:3.11-slim as builder
4
 
5
  # Set working directory
6
  WORKDIR /app
@@ -43,7 +43,7 @@ RUN mkdir -p backend-py/public && cp -r frontend/dist/* backend-py/public/
43
  RUN cd backend-py && ln -sf ../backend/data data
44
 
45
  # Stage 2: Runtime - Copy only necessary files as non-root user
46
- FROM python:3.11-slim as runtime
47
 
48
  # Copy Python packages from builder stage
49
  COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
@@ -78,8 +78,8 @@ ENV PYTHONUNBUFFERED=1
78
  ENV PIP_NO_CACHE_DIR=1
79
 
80
  # Health check
81
- HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
82
- CMD curl -f http://localhost:7860/health || exit 1
83
 
84
  # Start the Python backend server with uvicorn for better production performance
85
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
 
1
  # Multi-stage build to optimize performance and security
2
  # Stage 1: Builder - Install dependencies and build as root
3
+ FROM python:3.11-slim AS builder
4
 
5
  # Set working directory
6
  WORKDIR /app
 
43
  RUN cd backend-py && ln -sf ../backend/data data
44
 
45
  # Stage 2: Runtime - Copy only necessary files as non-root user
46
+ FROM python:3.11-slim AS runtime
47
 
48
  # Copy Python packages from builder stage
49
  COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages
 
78
  ENV PIP_NO_CACHE_DIR=1
79
 
80
  # Health check
81
+ # HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
82
+ # CMD curl -f http://localhost:7860/health || exit 1
83
 
84
  # Start the Python backend server with uvicorn for better production performance
85
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
VOCABULARY_OPTIMIZATION.md ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Vocabulary Optimization & Unification
2
+
3
+ ## Problem Solved
4
+
5
+ Previously, the crossword system had **vocabulary redundancy** with 3 separate sources:
6
+ - **SentenceTransformer Model Vocabulary**: ~30K tokens → ~8-12K actual words after filtering
7
+ - **NLTK Words Corpus**: 41,998 words for embeddings in thematic generator
8
+ - **WordFreq Database**: 319,938 words for frequency data
9
+
10
+ This created inconsistencies, memory waste, and limited vocabulary coverage.
11
+
12
+ ## Solution: Unified Architecture
13
+
14
+ ### New Design
15
+ - **Single Vocabulary Source**: WordFreq database (319,938 words)
16
+ - **Single Embedding Model**: all-mpnet-base-v2 (generates embeddings for any text)
17
+ - **Unified Filtering**: Consistent crossword-suitable word filtering
18
+ - **Shared Caching**: Single vocabulary + embeddings + frequency cache
19
+
20
+ ### Key Components
21
+
22
+ #### 1. VocabularyManager (`hack/thematic_word_generator.py`)
23
+ - Loads and filters WordFreq vocabulary
24
+ - Applies crossword-suitable filtering (3-12 chars, alphabetic, excludes boring words)
25
+ - Generates frequency data with 10-tier classification
26
+ - Handles caching for performance
27
+
28
+ #### 2. UnifiedThematicWordGenerator (`hack/thematic_word_generator.py`)
29
+ - Uses WordFreq vocabulary instead of NLTK words
30
+ - Generates all-mpnet-base-v2 embeddings for WordFreq words
31
+ - Maintains 10-tier frequency classification system
32
+ - Provides both hack tool API and backend-compatible API
33
+
34
+ #### 3. UnifiedWordService (`crossword-app/backend-py/src/services/unified_word_service.py`)
35
+ - Bridge adapter for backend integration
36
+ - Compatible with existing VectorSearchService interface
37
+ - Uses comprehensive WordFreq vocabulary instead of limited model vocabulary
38
+
39
+ ## Usage
40
+
41
+ ### For Hack Tools
42
+ ```python
43
+ from thematic_word_generator import UnifiedThematicWordGenerator
44
+
45
+ # Initialize with desired vocabulary size
46
+ generator = UnifiedThematicWordGenerator(vocab_size_limit=100000)
47
+ generator.initialize()
48
+
49
+ # Generate thematic words with tier info
50
+ results = generator.generate_thematic_words(
51
+ topic="science",
52
+ num_words=10,
53
+ difficulty_tier="tier_5_common" # Optional tier filtering
54
+ )
55
+
56
+ for word, similarity, tier in results:
57
+ print(f"{word}: {similarity:.3f} ({tier})")
58
+ ```
59
+
60
+ ### For Backend Integration
61
+
62
+ #### Option 1: Replace VectorSearchService
63
+ ```python
64
+ # In crossword_generator.py
65
+ from .unified_word_service import create_unified_word_service
66
+
67
+ # Initialize
68
+ vector_service = await create_unified_word_service(vocab_size_limit=100000)
69
+ crossword_gen = CrosswordGenerator(vector_service=vector_service)
70
+ ```
71
+
72
+ #### Option 2: Direct Usage
73
+ ```python
74
+ from .unified_word_service import UnifiedWordService
75
+
76
+ service = UnifiedWordService(vocab_size_limit=100000)
77
+ await service.initialize()
78
+
79
+ # Compatible with existing interface
80
+ words = await service.find_similar_words("animal", "medium", max_words=15)
81
+ ```
82
+
83
+ ## Performance Improvements
84
+
85
+ ### Memory Usage
86
+ - **Before**: 3 separate vocabularies + embeddings (~500MB+)
87
+ - **After**: Single vocabulary + embeddings (~200MB)
88
+ - **Reduction**: ~60% memory usage reduction
89
+
90
+ ### Vocabulary Coverage
91
+ - **Before**: Limited to ~8-12K words from model tokenizer
92
+ - **After**: Up to 100K+ filtered words from WordFreq database
93
+ - **Improvement**: 10x+ vocabulary coverage
94
+
95
+ ### Consistency
96
+ - **Before**: Different words available in hack tools vs backend
97
+ - **After**: Same comprehensive vocabulary across all components
98
+ - **Benefit**: Consistent word quality and availability
99
+
100
+ ## Configuration
101
+
102
+ ### Environment Variables
103
+ - `MAX_VOCABULARY_SIZE`: Maximum vocabulary size (default: 100000)
104
+ - `EMBEDDING_MODEL`: Model name (default: all-mpnet-base-v2)
105
+ - `WORD_SIMILARITY_THRESHOLD`: Minimum similarity (default: 0.3)
106
+
107
+ ### Vocabulary Size Options
108
+ - **Small (10K)**: Fast initialization, basic vocabulary
109
+ - **Medium (50K)**: Balanced performance and coverage
110
+ - **Large (100K)**: Comprehensive coverage, slower initialization
111
+ - **Full (319K)**: Complete WordFreq database, longest initialization
112
+
113
+ ## Migration Guide
114
+
115
+ ### For Existing Hack Tools
116
+ 1. Update imports: `from thematic_word_generator import UnifiedThematicWordGenerator`
117
+ 2. Replace `ThematicWordGenerator` with `UnifiedThematicWordGenerator`
118
+ 3. API remains compatible, but now uses comprehensive WordFreq vocabulary
119
+
120
+ ### For Backend Services
121
+ 1. Import: `from .unified_word_service import UnifiedWordService`
122
+ 2. Replace `VectorSearchService` initialization with `UnifiedWordService`
123
+ 3. All existing methods remain compatible
124
+ 4. Benefits: Better vocabulary coverage, consistent frequency data
125
+
126
+ ### Backwards Compatibility
127
+ - All existing APIs maintained
128
+ - Same method signatures and return formats
129
+ - Gradual migration possible - can run both systems in parallel
130
+
131
+ ## Benefits Summary
132
+
133
+ ✅ **Eliminates Redundancy**: Single vocabulary source instead of 3 separate ones
134
+ ✅ **Improves Coverage**: 100K+ words vs previous 8-12K words
135
+ ✅ **Reduces Memory**: ~60% reduction in memory usage
136
+ ✅ **Ensures Consistency**: Same vocabulary across hack tools and backend
137
+ ✅ **Maintains Performance**: Smart caching and batch processing
138
+ ✅ **Preserves Features**: 10-tier frequency classification, difficulty filtering
139
+ ✅ **Enables Growth**: Easy to add new features with unified architecture
140
+
141
+ ## Cache Management
142
+
143
+ ### Cache Locations
144
+ - **Hack tools**: `hack/model_cache/`
145
+ - **Backend**: `crossword-app/backend-py/cache/unified_generator/`
146
+
147
+ ### Cache Files
148
+ - `unified_vocabulary_<size>.pkl`: Filtered vocabulary
149
+ - `unified_frequencies_<size>.pkl`: Frequency data
150
+ - `unified_embeddings_<model>_<size>.npy`: Pre-computed embeddings
151
+
152
+ ### Cache Invalidation
153
+ Caches are automatically rebuilt if:
154
+ - Vocabulary size limit changes
155
+ - Embedding model changes
156
+ - WordFreq database updates (rare)
157
+
158
+ ## Future Enhancements
159
+
160
+ 1. **Semantic Clustering**: Group words by semantic similarity
161
+ 2. **Dynamic Difficulty**: Real-time difficulty adjustment based on user performance
162
+ 3. **Topic Expansion**: Automatic topic discovery and expansion
163
+ 4. **Multilingual Support**: Extend to other languages using WordFreq
164
+ 5. **Custom Vocabularies**: Allow domain-specific vocabulary additions
crossword-app/backend-py/CROSSWORD_GENERATION_WALKTHROUGH.md ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Crossword Generation Code Walkthrough
2
+
3
+ This document provides a detailed, line-by-line walkthrough of how crossword puzzle generation works in the Python backend, tracing the complete flow from API request to finalized words and clues.
4
+
5
+ ## Overview
6
+
7
+ The Python backend implements AI-powered crossword generation using:
8
+ - **FastAPI** for web framework and API endpoints
9
+ - **Vector similarity search** with sentence-transformers and FAISS for intelligent word discovery
10
+ - **Backtracking algorithm** for word placement in the crossword grid
11
+ - **Multi-layer caching system** for performance and fallback mechanisms
12
+
13
+ ## 1. Application Startup (`app.py`)
14
+
15
+ ### Entry Point and Service Initialization
16
+
17
+ ```python
18
+ # Lines 66-95: Async lifespan context manager
19
+ @asynccontextmanager
20
+ async def lifespan(app: FastAPI):
21
+ global vector_service
22
+
23
+ # Startup initialization
24
+ vector_service = VectorSearchService()
25
+ await vector_service.initialize()
26
+ app.state.vector_service = vector_service
27
+ ```
28
+
29
+ **Flow:**
30
+ 1. FastAPI application starts with lifespan context manager
31
+ 2. Creates global `VectorSearchService` instance (line 79)
32
+ 3. Calls `vector_service.initialize()` (line 82) which:
33
+ - Loads sentence-transformer model (~30-60 seconds)
34
+ - Builds or loads cached FAISS index for word embeddings
35
+ - Initializes word cache manager for fallbacks
36
+ 4. Makes vector service available to all API routes via `app.state`
37
+
38
+ ## 2. API Request Handling (`src/routes/api.py`)
39
+
40
+ ### Crossword Generation Endpoint
41
+
42
+ ```python
43
+ # Lines 77-118: Main crossword generation endpoint
44
+ @router.post("/generate", response_model=PuzzleResponse)
45
+ async def generate_puzzle(
46
+ request: GeneratePuzzleRequest,
47
+ crossword_gen: CrosswordGenerator = Depends(get_crossword_generator)
48
+ ):
49
+ ```
50
+
51
+ **Flow:**
52
+ 1. Client POST request to `/api/generate` with JSON body:
53
+ ```json
54
+ {
55
+ "topics": ["animals", "science"],
56
+ "difficulty": "medium",
57
+ }
58
+ ```
59
+
60
+ 2. FastAPI validates request against `GeneratePuzzleRequest` model (lines 20-23)
61
+
62
+ 3. Dependency injection calls `get_crossword_generator()` (lines 57-63):
63
+ ```python
64
+ def get_crossword_generator(request: Request) -> CrosswordGenerator:
65
+ global generator
66
+ if generator is None:
67
+ vector_service = getattr(request.app.state, 'vector_service', None)
68
+ generator = CrosswordGenerator(vector_service)
69
+ return generator
70
+ ```
71
+
72
+ 4. Creates or reuses `CrosswordGenerator` wrapper with vector service reference
73
+
74
+ ## 3. Crossword Generation Wrapper (`src/services/crossword_generator_wrapper.py`)
75
+
76
+ ### Simple Delegation Layer
77
+
78
+ ```python
79
+ # Lines 20-51: Generate puzzle method
80
+ async def generate_puzzle(
81
+ self,
82
+ topics: List[str],
83
+ difficulty: str = "medium",
84
+ ) -> Dict[str, Any]:
85
+ ```
86
+
87
+ **Flow:**
88
+ 1. Wrapper receives request from API route
89
+ 2. **Line 41**: Imports actual generator to avoid circular imports:
90
+ ```python
91
+ from .crossword_generator import CrosswordGenerator as ActualGenerator
92
+ ```
93
+ 3. **Line 42**: Creates actual generator instance with vector service
94
+ 4. **Line 44**: Delegates to actual generator's `generate_puzzle()` method
95
+
96
+ ## 4. Core Crossword Generation (`src/services/crossword_generator.py`)
97
+
98
+ ### 4.1 Main Generation Method
99
+
100
+ ```python
101
+ # Lines 22-66: Core puzzle generation
102
+ async def generate_puzzle(self, topics: List[str], difficulty: str = "medium", = False):
103
+ ```
104
+
105
+ **Key steps:**
106
+
107
+ 1. **Line 37**: Select words using AI or static sources:
108
+ ```python
109
+ words = await self._select_words(topics, difficulty, use_ai)
110
+ ```
111
+
112
+ 2. **Line 44**: Create crossword grid from selected words:
113
+ ```python
114
+ grid_result = self._create_grid(words)
115
+ ```
116
+
117
+ 3. **Lines 52-62**: Return structured response with grid, clues, and metadata
118
+
119
+ ### 4.2 Word Selection Process
120
+
121
+ ```python
122
+ # Lines 68-97: Word selection logic
123
+ async def _select_words(self, topics: List[str], difficulty: str, ):
124
+ ```
125
+
126
+ **Flow branches based on `use_ai` parameter:**
127
+
128
+ #### AI-Powered Word Selection (Lines 72-83):
129
+ ```python
130
+ if use_ai and self.vector_service:
131
+ for topic in topics:
132
+ ai_words = await self.vector_service.find_similar_words(
133
+ topic, difficulty, self.max_words // len(topics)
134
+ )
135
+ all_words.extend(ai_words)
136
+ ```
137
+
138
+ #### Fallback to Cached Words (Lines 86-91):
139
+ ```python
140
+ if self.vector_service:
141
+ for topic in topics:
142
+ cached_words = await self.vector_service._get_cached_fallback(
143
+ topic, difficulty, self.max_words // len(topics)
144
+ )
145
+ ```
146
+
147
+ #### Final Fallback to Static JSON Files (Lines 93-95):
148
+ ```python
149
+ else:
150
+ all_words = await self._get_static_words(topics, difficulty)
151
+ ```
152
+
153
+ ### 4.3 Word Sorting for Crossword Viability
154
+
155
+ ```python
156
+ # Lines 129-168: Sort words by crossword suitability
157
+ def _sort_words_for_crossword(self, words: List[Dict[str, Any]]):
158
+ ```
159
+
160
+ **Scoring algorithm:**
161
+ - **Lines 138-147**: Length-based scoring (shorter words preferred)
162
+ - **Lines 150-153**: Common letter bonus (E, A, R, I, O, T, N, S)
163
+ - **Lines 156-158**: Vowel distribution bonus
164
+ - **Lines 161-162**: Penalty for very long words
165
+
166
+ ## 5. AI Word Discovery (`src/services/vector_search.py`)
167
+
168
+ ### 5.1 Vector Search Initialization
169
+
170
+ ```python
171
+ # Lines 71-143: Service initialization
172
+ async def initialize(self):
173
+ ```
174
+
175
+ **Initialization flow:**
176
+ 1. **Lines 90-92**: Load sentence-transformer model
177
+ 2. **Lines 95-119**: Build or load cached FAISS index
178
+ 3. **Lines 124-134**: Initialize word cache manager
179
+
180
+ ### 5.2 Core Word Finding Algorithm
181
+
182
+ ```python
183
+ # Lines 279-374: Main word discovery method
184
+ async def find_similar_words(
185
+ self,
186
+ topic: str,
187
+ difficulty: str = "medium",
188
+ max_words: int = 15
189
+ ):
190
+ ```
191
+
192
+ **Search strategy branches:**
193
+
194
+ #### Hierarchical Search (Lines 296-325):
195
+ ```python
196
+ if self.use_hierarchical_search:
197
+ all_candidates = await self._hierarchical_search(topic, difficulty, max_words)
198
+ combined_results = self._combine_hierarchical_results(all_candidates, max_words * 2)
199
+ ```
200
+
201
+ #### Traditional Single Search (Lines 328-337):
202
+ ```python
203
+ else:
204
+ traditional_results = await self._traditional_single_search(topic, difficulty, max_words * 2)
205
+ ```
206
+
207
+ ### 5.3 Hierarchical Search Process
208
+
209
+ ```python
210
+ # Lines 639-748: Multi-phase hierarchical search
211
+ async def _hierarchical_search(self, topic: str, difficulty: str, max_words: int):
212
+ ```
213
+
214
+ **Three-phase approach:**
215
+
216
+ #### Phase 1: Topic Variations (Lines 652-694)
217
+ ```python
218
+ topic_variations = self._expand_topic_variations(topic) # "Animal" → ["Animal", "Animals"]
219
+
220
+ for variation in topic_variations:
221
+ topic_embedding = self.model.encode([variation], convert_to_numpy=True)
222
+ scores, indices = self.faiss_index.search(topic_embedding, search_size)
223
+ variation_candidates = self._collect_candidates_with_threshold(scores, indices, threshold, variation, difficulty)
224
+ ```
225
+
226
+ #### Phase 2: Subcategory Identification (Lines 697-700)
227
+ ```python
228
+ subcategories = self._identify_subcategories(main_topic_candidates, topic)
229
+ ```
230
+
231
+ #### Phase 3: Subcategory Search (Lines 703-733)
232
+ ```python
233
+ for subcategory in subcategories:
234
+ subcat_embedding = self.model.encode([subcategory], convert_to_numpy=True)
235
+ sub_scores, sub_indices = self.faiss_index.search(subcat_embedding, sub_search_size)
236
+ ```
237
+
238
+ ### 5.4 Word Quality Filtering
239
+
240
+ ```python
241
+ # Lines 1164-1215: Candidate collection with filtering
242
+ def _collect_candidates_with_threshold(self, scores, indices, threshold, topic, difficulty):
243
+ ```
244
+
245
+ **Multi-stage filtering:**
246
+ 1. **Line 1176**: Similarity threshold check
247
+ 2. **Line 1183**: Difficulty matching (word length)
248
+ 3. **Line 1185**: Interest and topic relevance check:
249
+ ```python
250
+ if self._is_interesting_word(word, topic) and self._is_topic_relevant(word, topic):
251
+ ```
252
+
253
+ ## 6. Grid Creation and Word Placement
254
+
255
+ ### 6.1 Grid Generation Entry Point
256
+
257
+ ```python
258
+ # Lines 170-243: Main grid creation method
259
+ def _create_grid(self, words: List[Dict[str, Any]]):
260
+ ```
261
+
262
+ **Flow:**
263
+ 1. **Lines 184-203**: Process and sort words by length (longest first)
264
+ 2. **Lines 209-213**: Calculate appropriate grid size
265
+ 3. **Lines 212-237**: Multiple placement attempts with increasing grid size
266
+ 4. **Lines 240-241**: Fallback to simple two-word cross
267
+
268
+ ### 6.2 Word Placement Algorithm
269
+
270
+ ```python
271
+ # Lines 259-295: Backtracking word placement
272
+ def _place_words_in_grid(self, word_list: List[str], word_objs: List[Dict[str, Any]], size: int):
273
+ ```
274
+
275
+ **Setup:**
276
+ - **Line 263**: Initialize empty grid with dots
277
+ - **Line 270**: Call recursive backtracking algorithm
278
+ - **Lines 272-287**: Generate clues and assign crossword numbers
279
+
280
+ ### 6.3 Backtracking Algorithm
281
+
282
+ ```python
283
+ # Lines 297-357: Recursive backtracking placement
284
+ def _backtrack_placement(self, grid, word_list, word_objs, word_index, placed_words, start_time, timeout):
285
+ ```
286
+
287
+ **Algorithm flow:**
288
+
289
+ #### Base Cases:
290
+ - **Lines 302-303**: Timeout check every 50 calls
291
+ - **Lines 305-306**: Success when all words placed
292
+
293
+ #### First Word Placement (Lines 312-332):
294
+ ```python
295
+ if word_index == 0:
296
+ center_row = size // 2
297
+ center_col = (size - len(word)) // 2
298
+
299
+ if self._can_place_word(grid, word, center_row, center_col, "horizontal"):
300
+ original_state = self._place_word(grid, word, center_row, center_col, "horizontal")
301
+ ```
302
+
303
+ #### Subsequent Word Placement (Lines 334-356):
304
+ ```python
305
+ all_placements = self._find_all_intersection_placements(grid, word, placed_words)
306
+ all_placements.sort(key=lambda p: p["score"], reverse=True)
307
+
308
+ for placement in all_placements:
309
+ if self._can_place_word(grid, word, row, col, direction):
310
+ # Try placement and recurse
311
+ if self._backtrack_placement(...):
312
+ return True
313
+ # Backtrack if failed
314
+ self._remove_word(grid, original_state)
315
+ ```
316
+
317
+ ### 6.4 Word Placement Validation
318
+
319
+ ```python
320
+ # Lines 359-417: Comprehensive placement validation
321
+ def _can_place_word(self, grid: List[List[str]], word: str, row: int, col: int, direction: str):
322
+ ```
323
+
324
+ **Critical validation checks:**
325
+ 1. **Lines 364-365**: Boundary checks
326
+ 2. **Lines 372-375**: Word boundary enforcement (no adjacent letters)
327
+ 3. **Lines 378-390**: Letter-by-letter placement validation
328
+ 4. **Lines 388-390**: Perpendicular intersection validation
329
+
330
+ ### 6.5 Intersection Finding
331
+
332
+ ```python
333
+ # Lines 486-505: Find all possible intersections
334
+ def _find_all_intersection_placements(self, grid, word, placed_words):
335
+ ```
336
+
337
+ **Process:**
338
+ 1. **Lines 491-502**: For each placed word, find letter intersections
339
+ 2. **Lines 496-502**: Calculate placement position for each intersection
340
+ 3. **Lines 499-502**: Score placement quality
341
+
342
+ ## 7. Clue Generation and Final Assembly
343
+
344
+ ### 7.1 Grid Trimming and Optimization
345
+
346
+ ```python
347
+ # Lines 589-642: Remove excess empty space
348
+ def _trim_grid(self, grid, placed_words):
349
+ ```
350
+
351
+ **Process:**
352
+ 1. **Lines 595-610**: Find bounding box of all placed words
353
+ 2. **Lines 612-631**: Create trimmed grid with padding
354
+ 3. **Lines 634-641**: Update word positions relative to new grid
355
+
356
+ ### 7.2 Crossword Numbering
357
+
358
+ ```python
359
+ # Lines 698-750: Assign proper crossword numbers and create clues
360
+ def _assign_numbers_and_clues(self, placed_words, clues_data):
361
+ ```
362
+
363
+ **Crossword numbering rules:**
364
+ 1. **Lines 710-714**: Group words by starting position
365
+ 2. **Lines 716**: Sort by reading order (top-to-bottom, left-to-right)
366
+ 3. **Lines 725-749**: Assign shared numbers for words starting at same position
367
+ 4. **Lines 738-745**: Create clue objects with proper formatting
368
+
369
+ ### 7.3 Final Response Assembly
370
+
371
+ **Back in `crossword_generator.py` lines 52-62:**
372
+ ```python
373
+ return {
374
+ "grid": grid_result["grid"],
375
+ "clues": grid_result["clues"],
376
+ "metadata": {
377
+ "topics": topics,
378
+ "difficulty": difficulty,
379
+ "wordCount": len(grid_result["placed_words"]),
380
+ "size": len(grid_result["grid"]),
381
+ "aiGenerated": }
382
+ }
383
+ ```
384
+
385
+ ## 8. Caching System (`src/services/word_cache.py`)
386
+
387
+ ### 8.1 Cache Initialization
388
+
389
+ ```python
390
+ # Lines 75-113: Load existing cache files
391
+ async def initialize(self):
392
+ ```
393
+
394
+ **Process:**
395
+ 1. **Lines 86-108**: Load all `.json` cache files from disk
396
+ 2. **Lines 99-102**: Validate cache structure and load into memory
397
+ 3. **Lines 110**: Report loaded cache statistics
398
+
399
+ ### 8.2 Word Caching
400
+
401
+ ```python
402
+ # Lines 166-224: Cache successful word discoveries
403
+ async def cache_words(self, topic, difficulty, words, source="vector_search"):
404
+ ```
405
+
406
+ **Storage process:**
407
+ 1. **Lines 186-193**: Enhance words with caching metadata
408
+ 2. **Lines 196-207**: Create structured cache data with expiration
409
+ 3. **Lines 210-213**: Save to disk (if permissions allow)
410
+ 4. **Lines 216-217**: Update in-memory cache
411
+
412
+ ## Complete Data Flow Summary
413
+
414
+ 1. **API Request** → `/api/generate` with topics, difficulty, 2. **Route Handler** → Validates request, injects dependencies
415
+ 3. **Wrapper** → Delegates to actual generator with vector service
416
+ 4. **Word Selection** → AI vector search OR cached words OR static JSON fallback
417
+ 5. **Vector Search** (if AI enabled):
418
+ - Load sentence-transformer model
419
+ - Perform hierarchical semantic search
420
+ - Filter by similarity threshold, difficulty, relevance
421
+ - Apply word exclusions and variety filtering
422
+ 6. **Grid Creation**:
423
+ - Sort words by crossword viability
424
+ - Calculate appropriate grid size
425
+ - Use backtracking algorithm to place words
426
+ - Validate word boundaries and intersections
427
+ 7. **Grid Optimization**:
428
+ - Trim excess empty space
429
+ - Assign proper crossword numbers
430
+ - Generate clue objects
431
+ 8. **Response Assembly** → Return grid, clues, and metadata
432
+ 9. **Caching** → Store successful AI discoveries for future use
433
+
434
+ The system gracefully degrades from AI → cached words → static words, ensuring crossword generation always succeeds even when AI components fail.
crossword-app/backend-py/README.md CHANGED
@@ -1,24 +1,25 @@
1
- # Python Backend with Vector Similarity Search
2
 
3
- This is the Python implementation of the crossword generator backend, featuring true AI word generation via vector similarity search.
4
 
5
  ## 🚀 Features
6
 
7
- - **True Vector Search**: Uses sentence-transformers + FAISS for semantic word discovery
8
- - **30K+ Vocabulary**: Searches through full model vocabulary instead of limited static lists
 
 
9
  - **FastAPI**: Modern, fast Python web framework
10
  - **Same API**: Compatible with existing React frontend
11
- - **Hybrid Approach**: AI vector search with static word fallback
12
 
13
  ## 🔄 Differences from JavaScript Backend
14
 
15
  | Feature | JavaScript Backend | Python Backend |
16
  |---------|-------------------|----------------|
17
- | **Word Generation** | Embedding filtering of static lists | True vector similarity search |
18
- | **Vocabulary Size** | ~100 words per topic | 30K+ words from model |
19
- | **AI Approach** | Semantic similarity filtering | Nearest neighbor search |
20
- | **Performance** | Fast but limited | Slower startup, better results |
21
- | **Dependencies** | Node.js + HuggingFace API | Python + ML libraries |
22
 
23
  ## 🛠️ Setup & Installation
24
 
@@ -70,10 +71,11 @@ backend-py/
70
  ├── requirements-dev.txt # Full development dependencies
71
  ├── src/
72
  │ ├── services/
73
- │ │ ├── vector_search.py # Core vector similarity search
74
- │ │ └── crossword_generator.py # Puzzle generation logic
 
75
  │ └── routes/
76
- │ └── api.py # API endpoints (matches JS backend)
77
  ├── test-unit/ # Unit tests (pytest framework) - 5 files
78
  │ ├── test_crossword_generator.py
79
  │ ├── test_api_routes.py
@@ -90,8 +92,9 @@ backend-py/
90
 
91
  ### Core ML Stack
92
  - `sentence-transformers`: Local model loading and embeddings
93
- - `faiss-cpu`: Fast vector similarity search
94
  - `torch`: PyTorch for model inference
 
95
  - `numpy`: Vector operations
96
 
97
  ### Web Framework
@@ -203,39 +206,56 @@ pytest test-unit/ --cov=src --cov-report=html --ignore=test-unit/test_vector_sea
203
 
204
  ## 🔧 Configuration
205
 
206
- Environment variables (set in HuggingFace Spaces):
 
 
207
 
208
  ```bash
209
- # Core settings
210
- PORT=7860
211
- NODE_ENV=production
 
212
 
213
- # AI Configuration
214
- EMBEDDING_MODEL=sentence-transformers/all-mpnet-base-v2
215
- WORD_SIMILARITY_THRESHOLD=0.65
216
 
217
  # Optional
218
- LOG_LEVEL=INFO
219
  ```
220
 
221
- ## 🎯 Vector Search Process
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  1. **Initialization**:
 
 
224
  - Load sentence-transformers model locally
225
- - Extract 30K+ vocabulary from model tokenizer
226
- - Pre-compute embeddings for all vocabulary words
227
- - Build FAISS index for fast similarity search
228
 
229
  2. **Word Generation**:
230
  - Get topic embedding: `"Animals" → [768-dim vector]`
231
- - Search FAISS index for nearest neighbors
232
- - Filter by similarity threshold (0.65+)
233
- - Filter by difficulty (word length)
234
  - Return top matches with generated clues
235
 
236
- 3. **Fallback**:
237
- - If vector search fails → use static word lists
238
- - If insufficient AI words supplement with static words
 
239
 
240
  ## 🧪 Testing
241
 
@@ -248,16 +268,163 @@ python test_local.py
248
  python app.py
249
  ```
250
 
251
- ## 🐳 Docker Deployment
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
- The Dockerfile has been updated to use Python backend:
254
 
255
- ```dockerfile
256
- FROM python:3.9-slim
257
- # ... install dependencies
258
- # ... build frontend (same as before)
259
- # ... copy to backend-py/public/
260
- CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
261
  ```
262
 
263
  ## 🧪 Testing
@@ -301,19 +468,25 @@ pytest tests/ --cov=src --cov-report=html
301
 
302
  **Startup Time**:
303
  - JavaScript: ~2 seconds
304
- - Python: ~30-60 seconds (model download + index building)
 
305
 
306
  **Word Quality**:
307
- - JavaScript: Limited by static word lists
308
- - Python: Access to full model vocabulary with semantic understanding
309
 
310
  **Memory Usage**:
311
  - JavaScript: ~100MB
312
- - Python: ~500MB-1GB (model + embeddings + FAISS index)
 
313
 
314
  **API Response Time**:
315
- - JavaScript: ~100ms (after cache warm-up)
316
- - Python: ~200-500ms (vector search + filtering)
 
 
 
 
317
 
318
  ## 🔄 Migration Strategy
319
 
@@ -325,8 +498,10 @@ pytest tests/ --cov=src --cov-report=html
325
 
326
  ## 🎯 Next Steps
327
 
328
- - [ ] Test vector search with real model
329
- - [ ] Optimize FAISS index performance
 
 
330
  - [ ] Add more sophisticated crossword grid generation
331
  - [ ] Implement LLM-based clue generation
332
- - [ ] Add caching for frequently requested topics
 
1
+ # Python Backend with Thematic AI Word Generation
2
 
3
+ This is the Python implementation of the crossword generator backend, featuring AI-powered thematic word generation using WordFreq vocabulary and semantic embeddings.
4
 
5
  ## 🚀 Features
6
 
7
+ - **Thematic Word Generation**: Uses sentence-transformers for semantic word discovery from WordFreq vocabulary
8
+ - **319K+ Word Database**: Comprehensive vocabulary from WordFreq with frequency data
9
+ - **10-Tier Difficulty System**: Smart word selection based on frequency tiers
10
+ - **Environment Variable Configuration**: Flexible cache and model configuration
11
  - **FastAPI**: Modern, fast Python web framework
12
  - **Same API**: Compatible with existing React frontend
 
13
 
14
  ## 🔄 Differences from JavaScript Backend
15
 
16
  | Feature | JavaScript Backend | Python Backend |
17
  |---------|-------------------|----------------|
18
+ | **Word Generation** | Static word lists | Thematic AI word generation from 319K vocabulary |
19
+ | **Vocabulary Size** | ~100 words per topic | Filtered from 319K WordFreq database |
20
+ | **AI Approach** | Basic filtering | Semantic similarity with frequency tiers |
21
+ | **Performance** | Fast but limited | Slower startup, richer word selection |
22
+ | **Dependencies** | Node.js + static files | Python + ML libraries |
23
 
24
  ## 🛠️ Setup & Installation
25
 
 
71
  ├── requirements-dev.txt # Full development dependencies
72
  ├── src/
73
  │ ├── services/
74
+ │ │ ├── thematic_word_service.py # Thematic AI word generation
75
+ │ │ ├── crossword_generator.py # Puzzle generation logic
76
+ │ │ └── crossword_generator_wrapper.py # Service wrapper
77
  │ └── routes/
78
+ │ └── api.py # API endpoints (matches JS backend)
79
  ├── test-unit/ # Unit tests (pytest framework) - 5 files
80
  │ ├── test_crossword_generator.py
81
  │ ├── test_api_routes.py
 
92
 
93
  ### Core ML Stack
94
  - `sentence-transformers`: Local model loading and embeddings
95
+ - `wordfreq`: 319K word vocabulary with frequency data
96
  - `torch`: PyTorch for model inference
97
+ - `scikit-learn`: Cosine similarity and clustering
98
  - `numpy`: Vector operations
99
 
100
  ### Web Framework
 
206
 
207
  ## 🔧 Configuration
208
 
209
+ ### Environment Variables
210
+
211
+ The backend supports flexible configuration via environment variables:
212
 
213
  ```bash
214
+ # Cache Configuration
215
+ CACHE_DIR=/app/cache # Cache directory for all service files
216
+ THEMATIC_VOCAB_SIZE_LIMIT=50000 # Maximum vocabulary size (default: 100000)
217
+ THEMATIC_MODEL_NAME=all-mpnet-base-v2 # Sentence transformer model
218
 
219
+ # Core Application Settings
220
+ PORT=7860 # Server port
221
+ NODE_ENV=production # Environment mode
222
 
223
  # Optional
224
+ LOG_LEVEL=INFO # Logging level
225
  ```
226
 
227
+ ### Cache Structure
228
+
229
+ The service creates the following cache files:
230
+
231
+ ```
232
+ {CACHE_DIR}/
233
+ ├── vocabulary_{size}.pkl # Processed vocabulary words
234
+ ├── frequencies_{size}.pkl # Word frequency data
235
+ ├── embeddings_{model}_{size}.npy # Word embeddings
236
+ └── sentence-transformers/ # Hugging Face model cache
237
+ ```
238
+
239
+ ## 🎯 Thematic Word Generation Process
240
 
241
  1. **Initialization**:
242
+ - Load WordFreq vocabulary database (319K words)
243
+ - Filter words for crossword suitability (length, content)
244
  - Load sentence-transformers model locally
245
+ - Pre-compute embeddings for filtered vocabulary
246
+ - Create 10-tier frequency classification system
 
247
 
248
  2. **Word Generation**:
249
  - Get topic embedding: `"Animals" → [768-dim vector]`
250
+ - Compute cosine similarity with all vocabulary embeddings
251
+ - Filter by similarity threshold and difficulty tier
252
+ - Filter by crossword-specific criteria (length, etc.)
253
  - Return top matches with generated clues
254
 
255
+ 3. **Multi-Theme Support**:
256
+ - Detect multiple themes using clustering
257
+ - Generate words that relate to combined themes
258
+ - Balance word selection across different topics
259
 
260
  ## 🧪 Testing
261
 
 
268
  python app.py
269
  ```
270
 
271
+ ## 🐳 Container Deployment
272
+
273
+ ### Docker Run with Cache Configuration
274
+
275
+ ```bash
276
+ # Basic deployment
277
+ docker run -e CACHE_DIR=/app/cache \
278
+ -e THEMATIC_VOCAB_SIZE_LIMIT=50000 \
279
+ -v /host/cache:/app/cache \
280
+ -p 7860:7860 \
281
+ your-crossword-app
282
+
283
+ # With all configuration options
284
+ docker run -e CACHE_DIR=/app/cache \
285
+ -e THEMATIC_VOCAB_SIZE_LIMIT=25000 \
286
+ -e THEMATIC_MODEL_NAME=all-mpnet-base-v2 \
287
+ -e NODE_ENV=production \
288
+ -v /host/cache:/app/cache \
289
+ -p 7860:7860 \
290
+ your-crossword-app
291
+ ```
292
+
293
+ ### Docker Compose
294
+
295
+ ```yaml
296
+ version: '3.8'
297
+ services:
298
+ crossword-backend:
299
+ image: your-crossword-app
300
+ environment:
301
+ - CACHE_DIR=/app/cache
302
+ - THEMATIC_VOCAB_SIZE_LIMIT=50000
303
+ - THEMATIC_MODEL_NAME=all-mpnet-base-v2
304
+ - NODE_ENV=production
305
+ volumes:
306
+ - ./cache:/app/cache
307
+ ports:
308
+ - "7860:7860"
309
+ restart: unless-stopped
310
+ ```
311
+
312
+ ### Pre-built Cache Strategy (Recommended)
313
+
314
+ For production deployments, pre-build the cache to avoid long startup times:
315
+
316
+ ```bash
317
+ # 1. Build cache locally or in a build container
318
+ export CACHE_DIR=/local/cache
319
+ export THEMATIC_VOCAB_SIZE_LIMIT=50000
320
+ python -c "from src.services.thematic_word_service import ThematicWordService; s=ThematicWordService(); s.initialize()"
321
+
322
+ # 2. Deploy with pre-built cache (read-only mount)
323
+ docker run -e CACHE_DIR=/app/cache \
324
+ -v /local/cache:/app/cache:ro \
325
+ -p 7860:7860 \
326
+ your-crossword-app
327
+ ```
328
+
329
+ ### Debugging Cache Issues
330
 
331
+ If cache files are not being created in your container:
332
 
333
+ 1. **Check Health Endpoints:**
334
+ ```bash
335
+ # Basic health check
336
+ curl http://localhost:7860/api/health
337
+
338
+ # Detailed cache status
339
+ curl http://localhost:7860/api/health/cache
340
+
341
+ # Force cache re-initialization
342
+ curl -X POST http://localhost:7860/api/health/cache/reinitialize
343
+ ```
344
+
345
+ 2. **Check Container Logs:**
346
+ ```bash
347
+ docker logs your-container-name
348
+ ```
349
+ Look for cache directory permissions and initialization messages.
350
+
351
+ 3. **Test Cache Directory:**
352
+ ```bash
353
+ # Run test script to verify cache setup
354
+ docker exec your-container python test_cache_startup.py
355
+ ```
356
+
357
+ 4. **Common Issues:**
358
+ - **Permission denied**: Container user can't write to mounted volume
359
+ - **Missing dependencies**: ML libraries not installed in container
360
+ - **Volume not mounted**: Cache directory not properly mounted
361
+ - **Environment variables**: `CACHE_DIR` not set correctly
362
+
363
+ 5. **Fix Permission Issues:**
364
+ ```bash
365
+ # Option 1: Change ownership of host directory
366
+ sudo chown -R 1000:1000 /host/cache
367
+
368
+ # Option 2: Run container with specific user
369
+ docker run --user 1000:1000 ...
370
+
371
+ # Option 3: Set permissions in Dockerfile
372
+ RUN mkdir -p /app/cache && chmod 777 /app/cache
373
+ ```
374
+
375
+ ### Kubernetes Deployment
376
+
377
+ ```yaml
378
+ apiVersion: v1
379
+ kind: ConfigMap
380
+ metadata:
381
+ name: crossword-config
382
+ data:
383
+ CACHE_DIR: "/app/cache"
384
+ THEMATIC_VOCAB_SIZE_LIMIT: "50000"
385
+ THEMATIC_MODEL_NAME: "all-mpnet-base-v2"
386
+ NODE_ENV: "production"
387
+ ---
388
+ apiVersion: v1
389
+ kind: PersistentVolumeClaim
390
+ metadata:
391
+ name: crossword-cache
392
+ spec:
393
+ accessModes:
394
+ - ReadWriteOnce
395
+ resources:
396
+ requests:
397
+ storage: 5Gi
398
+ ---
399
+ apiVersion: apps/v1
400
+ kind: Deployment
401
+ metadata:
402
+ name: crossword-backend
403
+ spec:
404
+ replicas: 1
405
+ selector:
406
+ matchLabels:
407
+ app: crossword-backend
408
+ template:
409
+ metadata:
410
+ labels:
411
+ app: crossword-backend
412
+ spec:
413
+ containers:
414
+ - name: backend
415
+ image: your-crossword-app
416
+ envFrom:
417
+ - configMapRef:
418
+ name: crossword-config
419
+ volumeMounts:
420
+ - name: cache-volume
421
+ mountPath: /app/cache
422
+ ports:
423
+ - containerPort: 7860
424
+ volumes:
425
+ - name: cache-volume
426
+ persistentVolumeClaim:
427
+ claimName: crossword-cache
428
  ```
429
 
430
  ## 🧪 Testing
 
468
 
469
  **Startup Time**:
470
  - JavaScript: ~2 seconds
471
+ - Python: ~30-60 seconds (model download + embedding generation)
472
+ - Python (with cache): ~5-10 seconds
473
 
474
  **Word Quality**:
475
+ - JavaScript: Limited by static word lists (~100 words/topic)
476
+ - Python: Rich thematic generation from 319K word database
477
 
478
  **Memory Usage**:
479
  - JavaScript: ~100MB
480
+ - Python: ~500MB-1GB (model + embeddings)
481
+ - Cache Size: ~50-200MB per 50K vocabulary
482
 
483
  **API Response Time**:
484
+ - JavaScript: ~100ms (static word lookup)
485
+ - Python: ~200-500ms (semantic similarity computation)
486
+
487
+ **Cache Performance**:
488
+ - Vocabulary loading: ~1-2 seconds from cache vs 30+ seconds generation
489
+ - Embeddings loading: ~2-5 seconds from cache vs 60+ seconds generation
490
 
491
  ## 🔄 Migration Strategy
492
 
 
498
 
499
  ## 🎯 Next Steps
500
 
501
+ - [x] Replace vector search with thematic word generation
502
+ - [x] Implement environment variable cache configuration
503
+ - [x] Add 10-tier difficulty system based on word frequency
504
+ - [ ] Optimize embedding computation performance
505
  - [ ] Add more sophisticated crossword grid generation
506
  - [ ] Implement LLM-based clue generation
507
+ - [ ] Add cache warming strategies for production deployment
crossword-app/backend-py/all-packages.txt ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ annotated-types==0.7.0
2
+ anyio==4.10.0
3
+ certifi==2025.8.3
4
+ charset-normalizer==3.4.3
5
+ click==8.2.1
6
+ exceptiongroup==1.3.0
7
+ faiss-cpu==1.9.0
8
+ fastapi==0.115.0
9
+ filelock==3.19.1
10
+ fsspec==2025.7.0
11
+ h11==0.16.0
12
+ httpcore==1.0.9
13
+ httptools==0.6.4
14
+ httpx==0.28.1
15
+ huggingface-hub==0.26.2
16
+ idna==3.10
17
+ iniconfig==2.1.0
18
+ Jinja2==3.1.6
19
+ joblib==1.5.1
20
+ MarkupSafe==3.0.2
21
+ mpmath==1.3.0
22
+ networkx==3.4.2
23
+ numpy==1.26.4
24
+ nvidia-cublas-cu12==12.4.5.8
25
+ nvidia-cuda-cupti-cu12==12.4.127
26
+ nvidia-cuda-nvrtc-cu12==12.4.127
27
+ nvidia-cuda-runtime-cu12==12.4.127
28
+ nvidia-cudnn-cu12==9.1.0.70
29
+ nvidia-cufft-cu12==11.2.1.3
30
+ nvidia-curand-cu12==10.3.5.147
31
+ nvidia-cusolver-cu12==11.6.1.9
32
+ nvidia-cusparse-cu12==12.3.1.170
33
+ nvidia-nccl-cu12==2.21.5
34
+ nvidia-nvjitlink-cu12==12.4.127
35
+ nvidia-nvtx-cu12==12.4.127
36
+ packaging==25.0
37
+ pillow==11.3.0
38
+ pluggy==1.6.0
39
+ pydantic==2.9.2
40
+ pydantic-settings==2.5.2
41
+ pydantic_core==2.23.4
42
+ pytest==8.3.4
43
+ pytest-asyncio==0.25.0
44
+ python-dotenv==1.0.1
45
+ python-multipart==0.0.12
46
+ PyYAML==6.0.2
47
+ regex==2025.7.34
48
+ requests==2.32.4
49
+ safetensors==0.6.2
50
+ scikit-learn==1.5.2
51
+ scipy==1.15.3
52
+ sentence-transformers==3.3.0
53
+ sniffio==1.3.1
54
+ starlette==0.38.6
55
+ structlog==24.4.0
56
+ sympy==1.13.1
57
+ threadpoolctl==3.6.0
58
+ tokenizers==0.21.4
59
+ tomli==2.2.1
60
+ torch==2.5.1
61
+ tqdm==4.67.1
62
+ transformers==4.47.1
63
+ triton==3.1.0
64
+ typing_extensions==4.14.1
65
+ urllib3==2.5.0
66
+ uvicorn==0.32.1
67
+ uvloop==0.21.0
68
+ watchfiles==1.1.0
69
+ websockets==15.0.1
crossword-app/backend-py/app.py CHANGED
@@ -17,61 +17,117 @@ import uvicorn
17
  from dotenv import load_dotenv
18
 
19
  from src.routes.api import router as api_router
20
- from src.services.vector_search import VectorSearchService
21
 
22
  # Load environment variables
23
  load_dotenv()
24
 
25
- # Set up logging
26
- logging.basicConfig(level=logging.INFO)
 
 
 
 
27
  logger = logging.getLogger(__name__)
28
 
29
- def log_with_timestamp(message):
30
- """Helper to log with precise timestamp."""
31
- timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
32
- logger.info(f"[{timestamp}] {message}")
33
 
34
- # Global vector search service instance
35
- vector_service = None
36
 
37
  @asynccontextmanager
38
  async def lifespan(app: FastAPI):
39
  """Initialize and cleanup application resources."""
40
- global vector_service
41
 
42
  # Startup
43
  startup_time = time.time()
44
- log_with_timestamp("🚀 Initializing Python backend with vector search...")
45
 
46
- # Initialize vector search service
47
  try:
48
  service_start = time.time()
49
- log_with_timestamp("🔧 Creating VectorSearchService instance...")
50
- vector_service = VectorSearchService()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- log_with_timestamp("⚡ Starting vector search initialization...")
53
- await vector_service.initialize()
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  init_time = time.time() - service_start
56
- log_with_timestamp(f" Vector search service initialized in {init_time:.2f}s")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  except Exception as e:
58
- logger.error(f"❌ Failed to initialize vector search service: {e}")
59
- # Continue without vector search (will fallback to static words)
 
 
 
60
 
61
- # Make vector service available to routes
62
- app.state.vector_service = vector_service
63
 
64
  yield
65
 
66
  # Shutdown
67
  logger.info("🛑 Shutting down Python backend...")
68
- if vector_service:
69
- await vector_service.cleanup()
70
 
71
  # Create FastAPI app
72
  app = FastAPI(
73
  title="Crossword Puzzle Generator API",
74
- description="Python backend with AI-powered vector similarity search",
75
  version="2.0.0",
76
  lifespan=lifespan
77
  )
 
17
  from dotenv import load_dotenv
18
 
19
  from src.routes.api import router as api_router
20
+ from src.services.thematic_word_service import ThematicWordService
21
 
22
  # Load environment variables
23
  load_dotenv()
24
 
25
+ # Set up logging with filename and line numbers
26
+ logging.basicConfig(
27
+ level=logging.INFO,
28
+ format='%(asctime)s - %(name)s - %(filename)s:%(lineno)d - %(levelname)s - %(message)s',
29
+ datefmt='%H:%M:%S'
30
+ )
31
  logger = logging.getLogger(__name__)
32
 
33
+ # All services now use standard Python logging with filename/line numbers
 
 
 
34
 
35
+ # Global thematic service instance
36
+ thematic_service = None
37
 
38
  @asynccontextmanager
39
  async def lifespan(app: FastAPI):
40
  """Initialize and cleanup application resources."""
41
+ global thematic_service
42
 
43
  # Startup
44
  startup_time = time.time()
45
+ logger.info("🚀 Initializing Python backend with thematic word service...")
46
 
47
+ # Initialize thematic service
48
  try:
49
  service_start = time.time()
50
+ logger.info("🔧 Creating ThematicWordService instance...")
51
+ thematic_service = ThematicWordService()
52
+
53
+ # Log cache configuration for debugging
54
+ cache_status = thematic_service.get_cache_status()
55
+ logger.info(f"📁 Cache directory: {cache_status['cache_directory']}")
56
+ logger.info(f"🔍 Cache directory exists: {os.path.exists(cache_status['cache_directory'])}")
57
+ logger.info(f"✏️ Cache directory writable: {os.access(cache_status['cache_directory'], os.W_OK)}")
58
+
59
+ # Check for existing cache files
60
+ cache_complete = cache_status['complete']
61
+ logger.info(f"📦 Existing cache complete: {cache_complete}")
62
+ if not cache_complete:
63
+ for cache_type in ['vocabulary_cache', 'frequency_cache', 'embeddings_cache']:
64
+ cache_info = cache_status[cache_type]
65
+ logger.info(f" {cache_type}: exists={cache_info['exists']}, path={cache_info['path']}")
66
 
67
+ # Force eager initialization to create cache files
68
+ logger.info("⚡ Starting thematic service initialization (creating cache files)...")
69
+ await thematic_service.initialize_async()
70
+
71
+ # Verify cache files were created
72
+ cache_status_after = thematic_service.get_cache_status()
73
+ logger.info(f"✅ Cache status after initialization: complete={cache_status_after['complete']}")
74
+ for cache_type in ['vocabulary_cache', 'frequency_cache', 'embeddings_cache']:
75
+ cache_info = cache_status_after[cache_type]
76
+ if cache_info['exists']:
77
+ logger.info(f" ✅ {cache_type}: {cache_info.get('size_mb', 0):.1f}MB")
78
+ else:
79
+ logger.warning(f" ❌ {cache_type}: NOT CREATED")
80
 
81
  init_time = time.time() - service_start
82
+ logger.info(f"🎉 Thematic service initialized in {init_time:.2f}s")
83
+
84
+ # Initialize WordNet clue generator during startup
85
+ logger.info("🔧 Initializing WordNet clue generator...")
86
+ try:
87
+ wordnet_start = time.time()
88
+ from src.services.wordnet_clue_generator import WordNetClueGenerator
89
+ cache_dir = thematic_service.cache_dir if thematic_service else "./cache"
90
+ wordnet_generator = WordNetClueGenerator(cache_dir=str(cache_dir))
91
+ wordnet_generator.initialize()
92
+
93
+ # Store in thematic service for later use
94
+ if thematic_service:
95
+ thematic_service._wordnet_generator = wordnet_generator
96
+
97
+ wordnet_time = time.time() - wordnet_start
98
+ logger.info(f"✅ WordNet clue generator initialized in {wordnet_time:.2f}s")
99
+ except Exception as e:
100
+ logger.warning(f"⚠️ Failed to initialize WordNet clue generator during startup: {e}")
101
+ logger.info("📝 WordNet clue generator will be initialized on first use")
102
+
103
+ except ImportError as e:
104
+ logger.error(f"❌ Missing dependencies for thematic service: {e}")
105
+ logger.error("💡 Install missing packages: pip install wordfreq sentence-transformers torch scikit-learn")
106
+ raise # Fail fast on missing dependencies
107
+ except PermissionError as e:
108
+ logger.error(f"❌ Permission error with cache directory: {e}")
109
+ logger.error(f"💡 Check cache directory permissions: {thematic_service.cache_dir if 'thematic_service' in locals() else 'unknown'}")
110
+ raise # Fail fast on permission issues
111
  except Exception as e:
112
+ logger.error(f"❌ Failed to initialize thematic service: {e}")
113
+ logger.error(f"🔍 Error type: {type(e).__name__}")
114
+ import traceback
115
+ logger.error(f"📋 Full traceback: {traceback.format_exc()}")
116
+ raise # Fail fast instead of continuing without service
117
 
118
+ # Make thematic service available to routes
119
+ app.state.thematic_service = thematic_service
120
 
121
  yield
122
 
123
  # Shutdown
124
  logger.info("🛑 Shutting down Python backend...")
125
+ # Thematic service doesn't need cleanup, but we can add it if needed in the future
 
126
 
127
  # Create FastAPI app
128
  app = FastAPI(
129
  title="Crossword Puzzle Generator API",
130
+ description="Python backend with AI-powered thematic word generation",
131
  version="2.0.0",
132
  lifespan=lifespan
133
  )
crossword-app/backend-py/data/data DELETED
@@ -1 +0,0 @@
1
- ../backend/data
 
 
crossword-app/backend-py/data/word-lists/animals.json DELETED
@@ -1,165 +0,0 @@
1
- [
2
- { "word": "DOG", "clue": "Man's best friend" },
3
- { "word": "CAT", "clue": "Feline pet that purrs" },
4
- { "word": "ELEPHANT", "clue": "Large mammal with a trunk" },
5
- { "word": "TIGER", "clue": "Striped big cat" },
6
- { "word": "WHALE", "clue": "Largest marine mammal" },
7
- { "word": "BUTTERFLY", "clue": "Colorful flying insect" },
8
- { "word": "BIRD", "clue": "Flying creature with feathers" },
9
- { "word": "FISH", "clue": "Aquatic animal with gills" },
10
- { "word": "LION", "clue": "King of the jungle" },
11
- { "word": "BEAR", "clue": "Large mammal that hibernates" },
12
- { "word": "RABBIT", "clue": "Hopping mammal with long ears" },
13
- { "word": "HORSE", "clue": "Riding animal with hooves" },
14
- { "word": "SHEEP", "clue": "Woolly farm animal" },
15
- { "word": "GOAT", "clue": "Horned farm animal" },
16
- { "word": "DUCK", "clue": "Water bird that quacks" },
17
- { "word": "CHICKEN", "clue": "Farm bird that lays eggs" },
18
- { "word": "SNAKE", "clue": "Slithering reptile" },
19
- { "word": "TURTLE", "clue": "Shelled reptile" },
20
- { "word": "FROG", "clue": "Amphibian that croaks" },
21
- { "word": "SHARK", "clue": "Predatory ocean fish" },
22
- { "word": "DOLPHIN", "clue": "Intelligent marine mammal" },
23
- { "word": "PENGUIN", "clue": "Flightless Antarctic bird" },
24
- { "word": "MONKEY", "clue": "Primate that swings in trees" },
25
- { "word": "ZEBRA", "clue": "Striped African animal" },
26
- { "word": "GIRAFFE", "clue": "Tallest land animal" },
27
- { "word": "WOLF", "clue": "Wild canine that howls" },
28
- { "word": "FOX", "clue": "Cunning red-furred animal" },
29
- { "word": "DEER", "clue": "Graceful forest animal with antlers" },
30
- { "word": "MOOSE", "clue": "Large antlered animal" },
31
- { "word": "SQUIRREL", "clue": "Tree-climbing nut gatherer" },
32
- { "word": "RACCOON", "clue": "Masked nocturnal animal" },
33
- { "word": "BEAVER", "clue": "Dam-building rodent" },
34
- { "word": "OTTER", "clue": "Playful water mammal" },
35
- { "word": "SEAL", "clue": "Marine mammal with flippers" },
36
- { "word": "WALRUS", "clue": "Tusked Arctic marine mammal" },
37
- { "word": "RHINO", "clue": "Horned thick-skinned mammal" },
38
- { "word": "HIPPO", "clue": "Large African river mammal" },
39
- { "word": "CHEETAH", "clue": "Fastest land animal" },
40
- { "word": "LEOPARD", "clue": "Spotted big cat" },
41
- { "word": "JAGUAR", "clue": "South American big cat" },
42
- { "word": "PUMA", "clue": "Mountain lion" },
43
- { "word": "LYNX", "clue": "Wild cat with tufted ears" },
44
- { "word": "KANGAROO", "clue": "Hopping Australian marsupial" },
45
- { "word": "KOALA", "clue": "Eucalyptus-eating marsupial" },
46
- { "word": "PANDA", "clue": "Black and white bamboo eater" },
47
- { "word": "SLOTH", "clue": "Slow-moving tree dweller" },
48
- { "word": "ARMADILLO", "clue": "Armored mammal" },
49
- { "word": "ANTEATER", "clue": "Long-snouted insect eater" },
50
- { "word": "PLATYPUS", "clue": "Egg-laying mammal with a bill" },
51
- { "word": "BAT", "clue": "Flying mammal" },
52
- { "word": "MOLE", "clue": "Underground tunnel digger" },
53
- { "word": "HEDGEHOG", "clue": "Spiny small mammal" },
54
- { "word": "PORCUPINE", "clue": "Quill-covered rodent" },
55
- { "word": "SKUNK", "clue": "Black and white scent-spraying mammal" },
56
- { "word": "WEASEL", "clue": "Small carnivorous mammal" },
57
- { "word": "BADGER", "clue": "Burrowing black and white mammal" },
58
- { "word": "FERRET", "clue": "Domesticated hunting animal" },
59
- { "word": "MINK", "clue": "Valuable fur-bearing animal" },
60
- { "word": "EAGLE", "clue": "Majestic bird of prey" },
61
- { "word": "HAWK", "clue": "Sharp-eyed hunting bird" },
62
- { "word": "OWL", "clue": "Nocturnal bird with large eyes" },
63
- { "word": "FALCON", "clue": "Fast diving bird of prey" },
64
- { "word": "VULTURE", "clue": "Scavenging bird" },
65
- { "word": "CROW", "clue": "Black intelligent bird" },
66
- { "word": "RAVEN", "clue": "Large black corvid" },
67
- { "word": "ROBIN", "clue": "Red-breasted songbird" },
68
- { "word": "SPARROW", "clue": "Small brown songbird" },
69
- { "word": "CARDINAL", "clue": "Bright red songbird" },
70
- { "word": "BLUEJAY", "clue": "Blue crested bird" },
71
- { "word": "WOODPECKER", "clue": "Tree-pecking bird" },
72
- { "word": "HUMMINGBIRD", "clue": "Tiny fast-flying bird" },
73
- { "word": "PELICAN", "clue": "Large-billed water bird" },
74
- { "word": "FLAMINGO", "clue": "Pink wading bird" },
75
- { "word": "STORK", "clue": "Long-legged wading bird" },
76
- { "word": "HERON", "clue": "Tall fishing bird" },
77
- { "word": "CRANE", "clue": "Large wading bird" },
78
- { "word": "SWAN", "clue": "Elegant white water bird" },
79
- { "word": "GOOSE", "clue": "Large waterfowl" },
80
- { "word": "TURKEY", "clue": "Large ground bird" },
81
- { "word": "PHEASANT", "clue": "Colorful game bird" },
82
- { "word": "QUAIL", "clue": "Small ground bird" },
83
- { "word": "PEACOCK", "clue": "Bird with spectacular tail feathers" },
84
- { "word": "OSTRICH", "clue": "Largest flightless bird" },
85
- { "word": "EMU", "clue": "Australian flightless bird" },
86
- { "word": "KIWI", "clue": "Small flightless New Zealand bird" },
87
- { "word": "PARROT", "clue": "Colorful talking bird" },
88
- { "word": "TOUCAN", "clue": "Large-billed tropical bird" },
89
- { "word": "MACAW", "clue": "Large colorful parrot" },
90
- { "word": "COCKATOO", "clue": "Crested parrot" },
91
- { "word": "CANARY", "clue": "Yellow singing bird" },
92
- { "word": "FINCH", "clue": "Small seed-eating bird" },
93
- { "word": "PIGEON", "clue": "Common city bird" },
94
- { "word": "DOVE", "clue": "Symbol of peace" },
95
- { "word": "SEAGULL", "clue": "Coastal scavenging bird" },
96
- { "word": "ALBATROSS", "clue": "Large ocean bird" },
97
- { "word": "PUFFIN", "clue": "Colorful-billed seabird" },
98
- { "word": "LIZARD", "clue": "Small scaly reptile" },
99
- { "word": "IGUANA", "clue": "Large tropical lizard" },
100
- { "word": "GECKO", "clue": "Wall-climbing lizard" },
101
- { "word": "CHAMELEON", "clue": "Color-changing reptile" },
102
- { "word": "ALLIGATOR", "clue": "Large American crocodilian" },
103
- { "word": "CROCODILE", "clue": "Large aquatic reptile" },
104
- { "word": "PYTHON", "clue": "Large constricting snake" },
105
- { "word": "COBRA", "clue": "Venomous hooded snake" },
106
- { "word": "VIPER", "clue": "Poisonous snake" },
107
- { "word": "RATTLESNAKE", "clue": "Snake with warning tail" },
108
- { "word": "SALAMANDER", "clue": "Amphibian that can regrow limbs" },
109
- { "word": "NEWT", "clue": "Small aquatic salamander" },
110
- { "word": "TOAD", "clue": "Warty amphibian" },
111
- { "word": "TADPOLE", "clue": "Frog larva" },
112
- { "word": "SALMON", "clue": "Fish that swims upstream" },
113
- { "word": "TROUT", "clue": "Freshwater game fish" },
114
- { "word": "BASS", "clue": "Popular sport fish" },
115
- { "word": "TUNA", "clue": "Large ocean fish" },
116
- { "word": "SWORDFISH", "clue": "Fish with long pointed bill" },
117
- { "word": "MARLIN", "clue": "Large billfish" },
118
- { "word": "MANTA", "clue": "Large ray fish" },
119
- { "word": "STINGRAY", "clue": "Flat fish with barbed tail" },
120
- { "word": "EEL", "clue": "Snake-like fish" },
121
- { "word": "SEAHORSE", "clue": "Horse-shaped fish" },
122
- { "word": "ANGELFISH", "clue": "Colorful tropical fish" },
123
- { "word": "GOLDFISH", "clue": "Common pet fish" },
124
- { "word": "CLOWNFISH", "clue": "Orange and white anemone fish" },
125
- { "word": "JELLYFISH", "clue": "Transparent stinging sea creature" },
126
- { "word": "OCTOPUS", "clue": "Eight-armed sea creature" },
127
- { "word": "SQUID", "clue": "Ten-armed cephalopod" },
128
- { "word": "CRAB", "clue": "Sideways-walking crustacean" },
129
- { "word": "LOBSTER", "clue": "Large marine crustacean" },
130
- { "word": "SHRIMP", "clue": "Small crustacean" },
131
- { "word": "STARFISH", "clue": "Five-armed sea creature" },
132
- { "word": "URCHIN", "clue": "Spiny sea creature" },
133
- { "word": "CORAL", "clue": "Marine organism that builds reefs" },
134
- { "word": "SPONGE", "clue": "Simple marine animal" },
135
- { "word": "OYSTER", "clue": "Pearl-producing mollusk" },
136
- { "word": "CLAM", "clue": "Burrowing shellfish" },
137
- { "word": "MUSSEL", "clue": "Dark-shelled mollusk" },
138
- { "word": "SNAIL", "clue": "Spiral-shelled gastropod" },
139
- { "word": "SLUG", "clue": "Shell-less gastropod" },
140
- { "word": "WORM", "clue": "Segmented invertebrate" },
141
- { "word": "SPIDER", "clue": "Eight-legged web spinner" },
142
- { "word": "SCORPION", "clue": "Arachnid with stinging tail" },
143
- { "word": "ANT", "clue": "Social insect worker" },
144
- { "word": "BEE", "clue": "Honey-making insect" },
145
- { "word": "WASP", "clue": "Stinging flying insect" },
146
- { "word": "HORNET", "clue": "Large aggressive wasp" },
147
- { "word": "FLY", "clue": "Common buzzing insect" },
148
- { "word": "MOSQUITO", "clue": "Blood-sucking insect" },
149
- { "word": "BEETLE", "clue": "Hard-shelled insect" },
150
- { "word": "LADYBUG", "clue": "Red spotted beneficial insect" },
151
- { "word": "DRAGONFLY", "clue": "Large-winged flying insect" },
152
- { "word": "GRASSHOPPER", "clue": "Jumping green insect" },
153
- { "word": "CRICKET", "clue": "Chirping insect" },
154
- { "word": "MANTIS", "clue": "Praying insect predator" },
155
- { "word": "MOTH", "clue": "Nocturnal butterfly relative" },
156
- { "word": "CATERPILLAR", "clue": "Butterfly larva" },
157
- { "word": "COCOON", "clue": "Insect transformation casing" },
158
- { "word": "TERMITE", "clue": "Wood-eating social insect" },
159
- { "word": "TICK", "clue": "Blood-sucking parasite" },
160
- { "word": "FLEA", "clue": "Jumping parasite" },
161
- { "word": "LOUSE", "clue": "Small parasitic insect" },
162
- { "word": "APHID", "clue": "Plant-sucking insect" },
163
- { "word": "MAGGOT", "clue": "Fly larva" },
164
- { "word": "GRUB", "clue": "Beetle larva" }
165
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crossword-app/backend-py/data/word-lists/geography.json DELETED
@@ -1,161 +0,0 @@
1
- [
2
- { "word": "MOUNTAIN", "clue": "High elevation landform" },
3
- { "word": "OCEAN", "clue": "Large body of salt water" },
4
- { "word": "DESERT", "clue": "Dry, arid region" },
5
- { "word": "CONTINENT", "clue": "Large landmass" },
6
- { "word": "RIVER", "clue": "Flowing body of water" },
7
- { "word": "ISLAND", "clue": "Land surrounded by water" },
8
- { "word": "FOREST", "clue": "Dense area of trees" },
9
- { "word": "VALLEY", "clue": "Low area between hills" },
10
- { "word": "LAKE", "clue": "Body of freshwater" },
11
- { "word": "BEACH", "clue": "Sandy shore by water" },
12
- { "word": "CLIFF", "clue": "Steep rock face" },
13
- { "word": "PLATEAU", "clue": "Elevated flat area" },
14
- { "word": "CANYON", "clue": "Deep gorge with steep sides" },
15
- { "word": "GLACIER", "clue": "Moving mass of ice" },
16
- { "word": "VOLCANO", "clue": "Mountain that erupts" },
17
- { "word": "PENINSULA", "clue": "Land surrounded by water on three sides" },
18
- { "word": "ARCHIPELAGO", "clue": "Group of islands" },
19
- { "word": "PRAIRIE", "clue": "Grassland plain" },
20
- { "word": "TUNDRA", "clue": "Cold, treeless region" },
21
- { "word": "SAVANNA", "clue": "Tropical grassland" },
22
- { "word": "EQUATOR", "clue": "Earth's middle line" },
23
- { "word": "LATITUDE", "clue": "Distance from equator" },
24
- { "word": "LONGITUDE", "clue": "Distance from prime meridian" },
25
- { "word": "CLIMATE", "clue": "Long-term weather pattern" },
26
- { "word": "MONSOON", "clue": "Seasonal wind pattern" },
27
- { "word": "CAPITAL", "clue": "Main city of country" },
28
- { "word": "BORDER", "clue": "Boundary between countries" },
29
- { "word": "COAST", "clue": "Land meeting the sea" },
30
- { "word": "STRAIT", "clue": "Narrow water passage" },
31
- { "word": "DELTA", "clue": "River mouth formation" },
32
- { "word": "FJORD", "clue": "Narrow inlet between cliffs" },
33
- { "word": "ATOLL", "clue": "Ring-shaped coral island" },
34
- { "word": "MESA", "clue": "Flat-topped hill" },
35
- { "word": "BUTTE", "clue": "Isolated hill with steep sides" },
36
- { "word": "GORGE", "clue": "Deep narrow valley" },
37
- { "word": "RAVINE", "clue": "Small narrow gorge" },
38
- { "word": "RIDGE", "clue": "Long narrow hilltop" },
39
- { "word": "PEAK", "clue": "Mountain summit" },
40
- { "word": "SUMMIT", "clue": "Highest point" },
41
- { "word": "FOOTHILLS", "clue": "Hills at base of mountains" },
42
- { "word": "RANGE", "clue": "Chain of mountains" },
43
- { "word": "BASIN", "clue": "Low-lying area" },
44
- { "word": "WATERSHED", "clue": "Drainage area" },
45
- { "word": "ESTUARY", "clue": "Where river meets sea" },
46
- { "word": "BAY", "clue": "Curved inlet of water" },
47
- { "word": "GULF", "clue": "Large bay" },
48
- { "word": "CAPE", "clue": "Point of land into water" },
49
- { "word": "HEADLAND", "clue": "High point of land" },
50
- { "word": "LAGOON", "clue": "Shallow coastal body of water" },
51
- { "word": "REEF", "clue": "Underwater rock formation" },
52
- { "word": "SHOAL", "clue": "Shallow area in water" },
53
- { "word": "CHANNEL", "clue": "Deep water passage" },
54
- { "word": "SOUND", "clue": "Large sea inlet" },
55
- { "word": "HARBOR", "clue": "Sheltered port area" },
56
- { "word": "INLET", "clue": "Small bay" },
57
- { "word": "COVE", "clue": "Small sheltered bay" },
58
- { "word": "MARSH", "clue": "Wetland area" },
59
- { "word": "SWAMP", "clue": "Forested wetland" },
60
- { "word": "BOG", "clue": "Acidic wetland" },
61
- { "word": "OASIS", "clue": "Fertile spot in desert" },
62
- { "word": "DUNE", "clue": "Sand hill" },
63
- { "word": "PLAIN", "clue": "Flat grassland" },
64
- { "word": "STEPPE", "clue": "Dry grassland" },
65
- { "word": "TAIGA", "clue": "Northern coniferous forest" },
66
- { "word": "RAINFOREST", "clue": "Dense tropical forest" },
67
- { "word": "JUNGLE", "clue": "Dense tropical vegetation" },
68
- { "word": "WOODLAND", "clue": "Area with scattered trees" },
69
- { "word": "GROVE", "clue": "Small group of trees" },
70
- { "word": "MEADOW", "clue": "Grassy field" },
71
- { "word": "PASTURE", "clue": "Grazing land" },
72
- { "word": "FIELD", "clue": "Open area of land" },
73
- { "word": "MOOR", "clue": "Open uncultivated land" },
74
- { "word": "HEATH", "clue": "Shrubland area" },
75
- { "word": "ARCTIC", "clue": "Cold northern region" },
76
- { "word": "ANTARCTIC", "clue": "Cold southern region" },
77
- { "word": "POLAR", "clue": "Of the poles" },
78
- { "word": "TROPICAL", "clue": "Hot humid climate zone" },
79
- { "word": "TEMPERATE", "clue": "Moderate climate zone" },
80
- { "word": "ARID", "clue": "Very dry" },
81
- { "word": "HUMID", "clue": "Moist air" },
82
- { "word": "ALTITUDE", "clue": "Height above sea level" },
83
- { "word": "ELEVATION", "clue": "Height of land" },
84
- { "word": "TERRAIN", "clue": "Physical features of land" },
85
- { "word": "TOPOGRAPHY", "clue": "Surface features of area" },
86
- { "word": "GEOGRAPHY", "clue": "Study of Earth's features" },
87
- { "word": "CARTOGRAPHY", "clue": "Map making" },
88
- { "word": "MERIDIAN", "clue": "Longitude line" },
89
- { "word": "PARALLEL", "clue": "Latitude line" },
90
- { "word": "HEMISPHERE", "clue": "Half of Earth" },
91
- { "word": "TROPICS", "clue": "Hot climate zone" },
92
- { "word": "POLES", "clue": "Earth's endpoints" },
93
- { "word": "AXIS", "clue": "Earth's rotation line" },
94
- { "word": "ORBIT", "clue": "Path around sun" },
95
- { "word": "SEASON", "clue": "Time of year" },
96
- { "word": "SOLSTICE", "clue": "Longest or shortest day" },
97
- { "word": "EQUINOX", "clue": "Equal day and night" },
98
- { "word": "COMPASS", "clue": "Direction-finding tool" },
99
- { "word": "NAVIGATION", "clue": "Finding your way" },
100
- { "word": "BEARING", "clue": "Direction or course" },
101
- { "word": "AZIMUTH", "clue": "Compass direction" },
102
- { "word": "SCALE", "clue": "Map size ratio" },
103
- { "word": "LEGEND", "clue": "Map symbol key" },
104
- { "word": "CONTOUR", "clue": "Elevation line on map" },
105
- { "word": "GRID", "clue": "Map reference system" },
106
- { "word": "PROJECTION", "clue": "Map flattening method" },
107
- { "word": "SURVEY", "clue": "Land measurement" },
108
- { "word": "BOUNDARY", "clue": "Dividing line" },
109
- { "word": "FRONTIER", "clue": "Border region" },
110
- { "word": "TERRITORY", "clue": "Area of land" },
111
- { "word": "REGION", "clue": "Geographic area" },
112
- { "word": "ZONE", "clue": "Designated area" },
113
- { "word": "DISTRICT", "clue": "Administrative area" },
114
- { "word": "PROVINCE", "clue": "Political subdivision" },
115
- { "word": "STATE", "clue": "Political entity" },
116
- { "word": "COUNTY", "clue": "Local government area" },
117
- { "word": "CITY", "clue": "Large urban area" },
118
- { "word": "TOWN", "clue": "Small urban area" },
119
- { "word": "VILLAGE", "clue": "Small rural community" },
120
- { "word": "HAMLET", "clue": "Very small village" },
121
- { "word": "SUBURB", "clue": "Residential area outside city" },
122
- { "word": "URBAN", "clue": "City-like" },
123
- { "word": "RURAL", "clue": "Countryside" },
124
- { "word": "METROPOLITAN", "clue": "Large city area" },
125
- { "word": "POPULATION", "clue": "Number of people" },
126
- { "word": "DENSITY", "clue": "Crowdedness" },
127
- { "word": "SETTLEMENT", "clue": "Place where people live" },
128
- { "word": "COLONY", "clue": "Overseas territory" },
129
- { "word": "NATION", "clue": "Country" },
130
- { "word": "REPUBLIC", "clue": "Democratic state" },
131
- { "word": "KINGDOM", "clue": "Monarchy" },
132
- { "word": "EMPIRE", "clue": "Large political entity" },
133
- { "word": "FEDERATION", "clue": "Union of states" },
134
- { "word": "ALLIANCE", "clue": "Partnership of nations" },
135
- { "word": "TREATY", "clue": "International agreement" },
136
- { "word": "TRADE", "clue": "Commercial exchange" },
137
- { "word": "EXPORT", "clue": "Goods sent abroad" },
138
- { "word": "IMPORT", "clue": "Goods brought in" },
139
- { "word": "COMMERCE", "clue": "Business activity" },
140
- { "word": "INDUSTRY", "clue": "Manufacturing" },
141
- { "word": "AGRICULTURE", "clue": "Farming" },
142
- { "word": "MINING", "clue": "Extracting minerals" },
143
- { "word": "FORESTRY", "clue": "Tree management" },
144
- { "word": "FISHING", "clue": "Catching fish" },
145
- { "word": "TOURISM", "clue": "Travel industry" },
146
- { "word": "TRANSPORTATION", "clue": "Moving people and goods" },
147
- { "word": "INFRASTRUCTURE", "clue": "Basic facilities" },
148
- { "word": "COMMUNICATION", "clue": "Information exchange" },
149
- { "word": "CULTURE", "clue": "Way of life" },
150
- { "word": "LANGUAGE", "clue": "Communication system" },
151
- { "word": "RELIGION", "clue": "Belief system" },
152
- { "word": "ETHNICITY", "clue": "Cultural group" },
153
- { "word": "MIGRATION", "clue": "Movement of people" },
154
- { "word": "IMMIGRATION", "clue": "Moving into country" },
155
- { "word": "EMIGRATION", "clue": "Moving out of country" },
156
- { "word": "DIASPORA", "clue": "Scattered population" },
157
- { "word": "NOMAD", "clue": "Wandering person" },
158
- { "word": "REFUGEE", "clue": "Displaced person" },
159
- { "word": "CENSUS", "clue": "Population count" },
160
- { "word": "DEMOGRAPHIC", "clue": "Population characteristic" }
161
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crossword-app/backend-py/data/word-lists/science.json DELETED
@@ -1,170 +0,0 @@
1
- [
2
- { "word": "ATOM", "clue": "Smallest unit of matter" },
3
- { "word": "GRAVITY", "clue": "Force that pulls objects down" },
4
- { "word": "MOLECULE", "clue": "Group of atoms bonded together" },
5
- { "word": "PHOTON", "clue": "Particle of light" },
6
- { "word": "CHEMISTRY", "clue": "Study of matter and reactions" },
7
- { "word": "PHYSICS", "clue": "Study of matter and energy" },
8
- { "word": "BIOLOGY", "clue": "Study of living organisms" },
9
- { "word": "ELEMENT", "clue": "Pure chemical substance" },
10
- { "word": "OXYGEN", "clue": "Gas essential for breathing" },
11
- { "word": "CARBON", "clue": "Element found in all life" },
12
- { "word": "HYDROGEN", "clue": "Lightest chemical element" },
13
- { "word": "ENERGY", "clue": "Capacity to do work" },
14
- { "word": "FORCE", "clue": "Push or pull on an object" },
15
- { "word": "VELOCITY", "clue": "Speed with direction" },
16
- { "word": "MASS", "clue": "Amount of matter in object" },
17
- { "word": "VOLUME", "clue": "Amount of space occupied" },
18
- { "word": "DENSITY", "clue": "Mass per unit volume" },
19
- { "word": "PRESSURE", "clue": "Force per unit area" },
20
- { "word": "TEMPERATURE", "clue": "Measure of heat" },
21
- { "word": "ELECTRON", "clue": "Negatively charged particle" },
22
- { "word": "PROTON", "clue": "Positively charged particle" },
23
- { "word": "NEUTRON", "clue": "Neutral atomic particle" },
24
- { "word": "NUCLEUS", "clue": "Center of an atom" },
25
- { "word": "CELL", "clue": "Basic unit of life" },
26
- { "word": "DNA", "clue": "Genetic blueprint molecule" },
27
- { "word": "PROTEIN", "clue": "Complex biological molecule" },
28
- { "word": "ENZYME", "clue": "Biological catalyst" },
29
- { "word": "VIRUS", "clue": "Infectious agent" },
30
- { "word": "BACTERIA", "clue": "Single-celled organisms" },
31
- { "word": "EVOLUTION", "clue": "Change in species over time" },
32
- { "word": "ISOTOPE", "clue": "Atom variant with different neutrons" },
33
- { "word": "ION", "clue": "Charged atom or molecule" },
34
- { "word": "COMPOUND", "clue": "Chemical combination of elements" },
35
- { "word": "MIXTURE", "clue": "Combined substances retaining properties" },
36
- { "word": "SOLUTION", "clue": "Dissolved mixture" },
37
- { "word": "ACID", "clue": "Sour chemical with low pH" },
38
- { "word": "BASE", "clue": "Alkaline substance with high pH" },
39
- { "word": "SALT", "clue": "Ionic compound from acid-base reaction" },
40
- { "word": "CATALYST", "clue": "Substance that speeds reactions" },
41
- { "word": "RNA", "clue": "Genetic messenger molecule" },
42
- { "word": "GENE", "clue": "Heredity unit on chromosome" },
43
- { "word": "CHROMOSOME", "clue": "Gene-carrying structure" },
44
- { "word": "TISSUE", "clue": "Group of similar cells" },
45
- { "word": "ORGAN", "clue": "Body part with specific function" },
46
- { "word": "SYSTEM", "clue": "Group of organs working together" },
47
- { "word": "ORGANISM", "clue": "Living individual entity" },
48
- { "word": "SPECIES", "clue": "Group of similar organisms" },
49
- { "word": "ADAPTATION", "clue": "Survival-enhancing change" },
50
- { "word": "MUTATION", "clue": "Genetic change in DNA" },
51
- { "word": "HEREDITY", "clue": "Passing traits to offspring" },
52
- { "word": "ECOSYSTEM", "clue": "Community and environment" },
53
- { "word": "HABITAT", "clue": "Natural living environment" },
54
- { "word": "BIODIVERSITY", "clue": "Variety of life forms" },
55
- { "word": "PHOTOSYNTHESIS", "clue": "Plant energy-making process" },
56
- { "word": "RESPIRATION", "clue": "Cellular breathing process" },
57
- { "word": "METABOLISM", "clue": "Chemical processes in body" },
58
- { "word": "HOMEOSTASIS", "clue": "Body's internal balance" },
59
- { "word": "MITOSIS", "clue": "Cell division for growth" },
60
- { "word": "MEIOSIS", "clue": "Cell division for reproduction" },
61
- { "word": "EMBRYO", "clue": "Early development stage" },
62
- { "word": "FOSSIL", "clue": "Preserved ancient remains" },
63
- { "word": "GEOLOGY", "clue": "Study of Earth's structure" },
64
- { "word": "MINERAL", "clue": "Natural inorganic crystal" },
65
- { "word": "ROCK", "clue": "Solid earth material" },
66
- { "word": "SEDIMENT", "clue": "Settled particles" },
67
- { "word": "EROSION", "clue": "Gradual wearing away" },
68
- { "word": "VOLCANO", "clue": "Earth opening spewing lava" },
69
- { "word": "EARTHQUAKE", "clue": "Ground shaking from plate movement" },
70
- { "word": "PLATE", "clue": "Earth's crust section" },
71
- { "word": "MAGMA", "clue": "Molten rock beneath surface" },
72
- { "word": "LAVA", "clue": "Molten rock on surface" },
73
- { "word": "CRYSTAL", "clue": "Ordered atomic structure" },
74
- { "word": "ATMOSPHERE", "clue": "Layer of gases around Earth" },
75
- { "word": "CLIMATE", "clue": "Long-term weather pattern" },
76
- { "word": "WEATHER", "clue": "Short-term atmospheric conditions" },
77
- { "word": "PRECIPITATION", "clue": "Water falling from clouds" },
78
- { "word": "HUMIDITY", "clue": "Moisture in air" },
79
- { "word": "WIND", "clue": "Moving air mass" },
80
- { "word": "STORM", "clue": "Violent weather event" },
81
- { "word": "HURRICANE", "clue": "Powerful tropical cyclone" },
82
- { "word": "TORNADO", "clue": "Rotating column of air" },
83
- { "word": "LIGHTNING", "clue": "Electrical discharge in sky" },
84
- { "word": "THUNDER", "clue": "Sound of lightning" },
85
- { "word": "RAINBOW", "clue": "Spectrum of light in sky" },
86
- { "word": "ASTRONOMY", "clue": "Study of celestial objects" },
87
- { "word": "GALAXY", "clue": "Collection of stars and planets" },
88
- { "word": "PLANET", "clue": "Large orbiting celestial body" },
89
- { "word": "STAR", "clue": "Self-luminous celestial body" },
90
- { "word": "MOON", "clue": "Natural satellite of planet" },
91
- { "word": "COMET", "clue": "Icy body with tail" },
92
- { "word": "ASTEROID", "clue": "Rocky space object" },
93
- { "word": "METEOR", "clue": "Space rock entering atmosphere" },
94
- { "word": "ORBIT", "clue": "Curved path around object" },
95
- { "word": "LIGHT", "clue": "Electromagnetic radiation" },
96
- { "word": "SPECTRUM", "clue": "Range of electromagnetic radiation" },
97
- { "word": "WAVELENGTH", "clue": "Distance between wave peaks" },
98
- { "word": "FREQUENCY", "clue": "Waves per unit time" },
99
- { "word": "AMPLITUDE", "clue": "Wave height or intensity" },
100
- { "word": "SOUND", "clue": "Vibrations in air" },
101
- { "word": "ECHO", "clue": "Reflected sound" },
102
- { "word": "RESONANCE", "clue": "Vibration amplification" },
103
- { "word": "DOPPLER", "clue": "Wave frequency shift effect" },
104
- { "word": "MOTION", "clue": "Change in position" },
105
- { "word": "ACCELERATION", "clue": "Change in velocity" },
106
- { "word": "MOMENTUM", "clue": "Mass times velocity" },
107
- { "word": "INERTIA", "clue": "Resistance to motion change" },
108
- { "word": "FRICTION", "clue": "Resistance to sliding" },
109
- { "word": "HEAT", "clue": "Thermal energy transfer" },
110
- { "word": "COMBUSTION", "clue": "Burning chemical reaction" },
111
- { "word": "OXIDATION", "clue": "Reaction with oxygen" },
112
- { "word": "REDUCTION", "clue": "Gain of electrons" },
113
- { "word": "ELECTROLYSIS", "clue": "Chemical breakdown by electricity" },
114
- { "word": "CONDUCTIVITY", "clue": "Ability to transfer energy" },
115
- { "word": "INSULATOR", "clue": "Material blocking energy flow" },
116
- { "word": "SEMICONDUCTOR", "clue": "Partial electrical conductor" },
117
- { "word": "MAGNETISM", "clue": "Force of magnetic attraction" },
118
- { "word": "FIELD", "clue": "Region of force influence" },
119
- { "word": "CIRCUIT", "clue": "Closed electrical path" },
120
- { "word": "CURRENT", "clue": "Flow of electric charge" },
121
- { "word": "VOLTAGE", "clue": "Electric potential difference" },
122
- { "word": "RESISTANCE", "clue": "Opposition to current flow" },
123
- { "word": "CAPACITOR", "clue": "Device storing electric charge" },
124
- { "word": "INDUCTOR", "clue": "Device storing magnetic energy" },
125
- { "word": "TRANSISTOR", "clue": "Electronic switching device" },
126
- { "word": "LASER", "clue": "Focused beam of light" },
127
- { "word": "RADAR", "clue": "Radio detection system" },
128
- { "word": "SONAR", "clue": "Sound detection system" },
129
- { "word": "TELESCOPE", "clue": "Instrument for viewing distant objects" },
130
- { "word": "MICROSCOPE", "clue": "Instrument for viewing small objects" },
131
- { "word": "HYPOTHESIS", "clue": "Testable scientific prediction" },
132
- { "word": "THEORY", "clue": "Well-tested scientific explanation" },
133
- { "word": "LAW", "clue": "Consistently observed scientific rule" },
134
- { "word": "EXPERIMENT", "clue": "Controlled scientific test" },
135
- { "word": "OBSERVATION", "clue": "Careful scientific watching" },
136
- { "word": "MEASUREMENT", "clue": "Quantified observation" },
137
- { "word": "ANALYSIS", "clue": "Detailed examination of data" },
138
- { "word": "SYNTHESIS", "clue": "Combining elements into whole" },
139
- { "word": "VARIABLE", "clue": "Factor that can change" },
140
- { "word": "CONTROL", "clue": "Unchanged comparison group" },
141
- { "word": "DATA", "clue": "Information collected from tests" },
142
- { "word": "STATISTICS", "clue": "Mathematical analysis of data" },
143
- { "word": "PROBABILITY", "clue": "Likelihood of occurrence" },
144
- { "word": "PRECISION", "clue": "Exactness of measurement" },
145
- { "word": "ACCURACY", "clue": "Correctness of measurement" },
146
- { "word": "ERROR", "clue": "Difference from true value" },
147
- { "word": "UNCERTAINTY", "clue": "Range of doubt in measurement" },
148
- { "word": "CALIBRATION", "clue": "Adjusting instrument accuracy" },
149
- { "word": "STANDARD", "clue": "Reference for measurement" },
150
- { "word": "UNIT", "clue": "Base measure of quantity" },
151
- { "word": "METRIC", "clue": "Decimal measurement system" },
152
- { "word": "WEIGHT", "clue": "Force of gravity on mass" },
153
- { "word": "CONCENTRATION", "clue": "Amount of substance per volume" },
154
- { "word": "MOLARITY", "clue": "Moles of solute per liter" },
155
- { "word": "EQUILIBRIUM", "clue": "State of balanced forces" },
156
- { "word": "STABILITY", "clue": "Resistance to change" },
157
- { "word": "DECAY", "clue": "Gradual breakdown process" },
158
- { "word": "RADIATION", "clue": "Energy emitted from source" },
159
- { "word": "RADIOACTIVE", "clue": "Emitting nuclear radiation" },
160
- { "word": "HALFLIFE", "clue": "Time for half to decay" },
161
- { "word": "FUSION", "clue": "Nuclear combining reaction" },
162
- { "word": "FISSION", "clue": "Nuclear splitting reaction" },
163
- { "word": "QUANTUM", "clue": "Discrete packet of energy" },
164
- { "word": "PARTICLE", "clue": "Tiny piece of matter" },
165
- { "word": "WAVE", "clue": "Energy transfer disturbance" },
166
- { "word": "INTERFERENCE", "clue": "Wave interaction effect" },
167
- { "word": "DIFFRACTION", "clue": "Wave bending around obstacle" },
168
- { "word": "REFLECTION", "clue": "Bouncing back of waves" },
169
- { "word": "REFRACTION", "clue": "Bending of waves through medium" }
170
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crossword-app/backend-py/data/word-lists/technology.json DELETED
@@ -1,221 +0,0 @@
1
- [
2
- { "word": "COMPUTER", "clue": "Electronic processing device" },
3
- { "word": "INTERNET", "clue": "Global computer network" },
4
- { "word": "ALGORITHM", "clue": "Set of rules for solving problems" },
5
- { "word": "DATABASE", "clue": "Organized collection of data" },
6
- { "word": "SOFTWARE", "clue": "Computer programs" },
7
- { "word": "HARDWARE", "clue": "Physical computer components" },
8
- { "word": "NETWORK", "clue": "Connected system of computers" },
9
- { "word": "CODE", "clue": "Programming instructions" },
10
- { "word": "ROBOT", "clue": "Automated machine" },
11
- { "word": "ARTIFICIAL", "clue": "Made by humans, not natural" },
12
- { "word": "DIGITAL", "clue": "Using binary data" },
13
- { "word": "BINARY", "clue": "Base-2 number system" },
14
- { "word": "PROCESSOR", "clue": "Computer's brain" },
15
- { "word": "MEMORY", "clue": "Data storage component" },
16
- { "word": "KEYBOARD", "clue": "Input device with keys" },
17
- { "word": "MONITOR", "clue": "Computer display screen" },
18
- { "word": "MOUSE", "clue": "Pointing input device" },
19
- { "word": "PRINTER", "clue": "Device that prints documents" },
20
- { "word": "SCANNER", "clue": "Device that digitizes images" },
21
- { "word": "CAMERA", "clue": "Device that captures images" },
22
- { "word": "SMARTPHONE", "clue": "Portable computing device" },
23
- { "word": "TABLET", "clue": "Touchscreen computing device" },
24
- { "word": "LAPTOP", "clue": "Portable computer" },
25
- { "word": "SERVER", "clue": "Computer that serves data" },
26
- { "word": "CLOUD", "clue": "Internet-based computing" },
27
- { "word": "WEBSITE", "clue": "Collection of web pages" },
28
- { "word": "EMAIL", "clue": "Electronic mail" },
29
- { "word": "BROWSER", "clue": "Web navigation software" },
30
- { "word": "SEARCH", "clue": "Look for information" },
31
- { "word": "DOWNLOAD", "clue": "Transfer data to device" },
32
- { "word": "UPLOAD", "clue": "Transfer data from device" },
33
- { "word": "BANDWIDTH", "clue": "Data transfer capacity" },
34
- { "word": "PROTOCOL", "clue": "Communication rules" },
35
- { "word": "FIREWALL", "clue": "Network security barrier" },
36
- { "word": "ENCRYPTION", "clue": "Data scrambling for security" },
37
- { "word": "PASSWORD", "clue": "Secret access code" },
38
- { "word": "SECURITY", "clue": "Protection from threats" },
39
- { "word": "VIRUS", "clue": "Malicious computer program" },
40
- { "word": "MALWARE", "clue": "Harmful software" },
41
- { "word": "ANTIVIRUS", "clue": "Protection software" },
42
- { "word": "BACKUP", "clue": "Data safety copy" },
43
- { "word": "RECOVERY", "clue": "Data restoration process" },
44
- { "word": "STORAGE", "clue": "Data keeping capacity" },
45
- { "word": "HARDDRIVE", "clue": "Magnetic storage device" },
46
- { "word": "FLASH", "clue": "Solid state storage" },
47
- { "word": "RAM", "clue": "Random access memory" },
48
- { "word": "ROM", "clue": "Read-only memory" },
49
- { "word": "CPU", "clue": "Central processing unit" },
50
- { "word": "GPU", "clue": "Graphics processing unit" },
51
- { "word": "MOTHERBOARD", "clue": "Main circuit board" },
52
- { "word": "CHIP", "clue": "Integrated circuit" },
53
- { "word": "CIRCUIT", "clue": "Electronic pathway" },
54
- { "word": "TRANSISTOR", "clue": "Electronic switch" },
55
- { "word": "SILICON", "clue": "Semiconductor material" },
56
- { "word": "NANOTECHNOLOGY", "clue": "Extremely small scale tech" },
57
- { "word": "AUTOMATION", "clue": "Self-operating technology" },
58
- { "word": "MACHINE", "clue": "Mechanical device" },
59
- { "word": "SENSOR", "clue": "Detection device" },
60
- { "word": "ACTUATOR", "clue": "Movement device" },
61
- { "word": "FEEDBACK", "clue": "System response information" },
62
- { "word": "PROGRAMMING", "clue": "Writing computer instructions" },
63
- { "word": "FUNCTION", "clue": "Reusable code block" },
64
- { "word": "VARIABLE", "clue": "Data storage container" },
65
- { "word": "LOOP", "clue": "Repeating code structure" },
66
- { "word": "CONDITION", "clue": "Decision-making logic" },
67
- { "word": "DEBUG", "clue": "Find and fix errors" },
68
- { "word": "COMPILE", "clue": "Convert code to executable" },
69
- { "word": "RUNTIME", "clue": "Program execution time" },
70
- { "word": "API", "clue": "Application programming interface" },
71
- { "word": "FRAMEWORK", "clue": "Code structure foundation" },
72
- { "word": "LIBRARY", "clue": "Reusable code collection" },
73
- { "word": "MODULE", "clue": "Self-contained code unit" },
74
- { "word": "OBJECT", "clue": "Data and methods container" },
75
- { "word": "CLASS", "clue": "Object blueprint" },
76
- { "word": "INHERITANCE", "clue": "Code reuse mechanism" },
77
- { "word": "INTERFACE", "clue": "System interaction boundary" },
78
- { "word": "PROTOCOL", "clue": "Communication standard" },
79
- { "word": "FORMAT", "clue": "Data structure standard" },
80
- { "word": "SYNTAX", "clue": "Language rules" },
81
- { "word": "SEMANTIC", "clue": "Meaning in code" },
82
- { "word": "PARSING", "clue": "Analyzing code structure" },
83
- { "word": "COMPILER", "clue": "Code translation program" },
84
- { "word": "INTERPRETER", "clue": "Code execution program" },
85
- { "word": "VIRTUAL", "clue": "Simulated environment" },
86
- { "word": "SIMULATION", "clue": "Computer modeling" },
87
- { "word": "EMULATION", "clue": "System imitation" },
88
- { "word": "OPTIMIZATION", "clue": "Performance improvement" },
89
- { "word": "EFFICIENCY", "clue": "Resource usage effectiveness" },
90
- { "word": "PERFORMANCE", "clue": "System speed and quality" },
91
- { "word": "BENCHMARK", "clue": "Performance measurement" },
92
- { "word": "TESTING", "clue": "Quality verification process" },
93
- { "word": "VALIDATION", "clue": "Correctness checking" },
94
- { "word": "VERIFICATION", "clue": "Accuracy confirmation" },
95
- { "word": "QUALITY", "clue": "Standard of excellence" },
96
- { "word": "MAINTENANCE", "clue": "System upkeep" },
97
- { "word": "UPDATE", "clue": "Software improvement" },
98
- { "word": "PATCH", "clue": "Software fix" },
99
- { "word": "VERSION", "clue": "Software release number" },
100
- { "word": "RELEASE", "clue": "Software distribution" },
101
- { "word": "DEPLOYMENT", "clue": "Software installation" },
102
- { "word": "CONFIGURATION", "clue": "System setup" },
103
- { "word": "INSTALLATION", "clue": "Software setup process" },
104
- { "word": "MIGRATION", "clue": "System transition" },
105
- { "word": "INTEGRATION", "clue": "System combination" },
106
- { "word": "COMPATIBILITY", "clue": "System cooperation ability" },
107
- { "word": "INTEROPERABILITY", "clue": "Cross-system communication" },
108
- { "word": "SCALABILITY", "clue": "Growth accommodation ability" },
109
- { "word": "RELIABILITY", "clue": "Consistent performance" },
110
- { "word": "AVAILABILITY", "clue": "System accessibility" },
111
- { "word": "REDUNDANCY", "clue": "Backup system duplication" },
112
- { "word": "FAULT", "clue": "System error condition" },
113
- { "word": "TOLERANCE", "clue": "Error handling ability" },
114
- { "word": "RECOVERY", "clue": "System restoration" },
115
- { "word": "MONITORING", "clue": "System observation" },
116
- { "word": "LOGGING", "clue": "Event recording" },
117
- { "word": "ANALYTICS", "clue": "Data analysis" },
118
- { "word": "METRICS", "clue": "Measurement data" },
119
- { "word": "DASHBOARD", "clue": "Information display panel" },
120
- { "word": "INTERFACE", "clue": "User interaction design" },
121
- { "word": "EXPERIENCE", "clue": "User interaction quality" },
122
- { "word": "USABILITY", "clue": "Ease of use" },
123
- { "word": "ACCESSIBILITY", "clue": "Universal design principle" },
124
- { "word": "RESPONSIVE", "clue": "Adaptive design" },
125
- { "word": "MOBILE", "clue": "Portable device category" },
126
- { "word": "TOUCHSCREEN", "clue": "Touch-sensitive display" },
127
- { "word": "GESTURE", "clue": "Touch movement command" },
128
- { "word": "VOICE", "clue": "Speech interaction" },
129
- { "word": "RECOGNITION", "clue": "Pattern identification" },
130
- { "word": "LEARNING", "clue": "Adaptive improvement" },
131
- { "word": "INTELLIGENCE", "clue": "Artificial reasoning" },
132
- { "word": "NEURAL", "clue": "Brain-inspired network" },
133
- { "word": "DEEP", "clue": "Multi-layered learning" },
134
- { "word": "MACHINE", "clue": "Automated learning system" },
135
- { "word": "DATA", "clue": "Information collection" },
136
- { "word": "BIG", "clue": "Large scale data" },
137
- { "word": "MINING", "clue": "Data pattern extraction" },
138
- { "word": "ANALYSIS", "clue": "Data examination" },
139
- { "word": "VISUALIZATION", "clue": "Data graphic representation" },
140
- { "word": "DASHBOARD", "clue": "Data monitoring panel" },
141
- { "word": "REPORT", "clue": "Data summary document" },
142
- { "word": "QUERY", "clue": "Data search request" },
143
- { "word": "INDEX", "clue": "Data location reference" },
144
- { "word": "SCHEMA", "clue": "Data structure blueprint" },
145
- { "word": "TABLE", "clue": "Data organization structure" },
146
- { "word": "RECORD", "clue": "Data entry" },
147
- { "word": "FIELD", "clue": "Data element" },
148
- { "word": "PRIMARY", "clue": "Main identifier key" },
149
- { "word": "FOREIGN", "clue": "Reference relationship key" },
150
- { "word": "RELATION", "clue": "Data connection" },
151
- { "word": "JOIN", "clue": "Data combination operation" },
152
- { "word": "TRANSACTION", "clue": "Data operation sequence" },
153
- { "word": "COMMIT", "clue": "Data change confirmation" },
154
- { "word": "ROLLBACK", "clue": "Data change reversal" },
155
- { "word": "CONCURRENCY", "clue": "Simultaneous access handling" },
156
- { "word": "LOCK", "clue": "Data access control" },
157
- { "word": "SYNCHRONIZATION", "clue": "Timing coordination" },
158
- { "word": "THREAD", "clue": "Execution sequence" },
159
- { "word": "PROCESS", "clue": "Running program instance" },
160
- { "word": "MULTITASKING", "clue": "Multiple process handling" },
161
- { "word": "PARALLEL", "clue": "Simultaneous execution" },
162
- { "word": "DISTRIBUTED", "clue": "Spread across multiple systems" },
163
- { "word": "CLUSTER", "clue": "Group of connected computers" },
164
- { "word": "GRID", "clue": "Distributed computing network" },
165
- { "word": "PEER", "clue": "Equal network participant" },
166
- { "word": "CLIENT", "clue": "Service requesting system" },
167
- { "word": "SERVICE", "clue": "System functionality provider" },
168
- { "word": "MICROSERVICE", "clue": "Small independent service" },
169
- { "word": "CONTAINER", "clue": "Isolated application environment" },
170
- { "word": "DOCKER", "clue": "Containerization platform" },
171
- { "word": "KUBERNETES", "clue": "Container orchestration" },
172
- { "word": "DEVOPS", "clue": "Development operations practice" },
173
- { "word": "AGILE", "clue": "Flexible development method" },
174
- { "word": "SCRUM", "clue": "Iterative development framework" },
175
- { "word": "SPRINT", "clue": "Short development cycle" },
176
- { "word": "KANBAN", "clue": "Visual workflow management" },
177
- { "word": "CONTINUOUS", "clue": "Ongoing integration practice" },
178
- { "word": "PIPELINE", "clue": "Automated workflow" },
179
- { "word": "BUILD", "clue": "Software compilation process" },
180
- { "word": "TESTING", "clue": "Quality assurance process" },
181
- { "word": "AUTOMATION", "clue": "Manual task elimination" },
182
- { "word": "SCRIPT", "clue": "Automated task sequence" },
183
- { "word": "BATCH", "clue": "Group processing" },
184
- { "word": "STREAMING", "clue": "Continuous data flow" },
185
- { "word": "REALTIME", "clue": "Immediate processing" },
186
- { "word": "LATENCY", "clue": "Response delay time" },
187
- { "word": "THROUGHPUT", "clue": "Processing capacity" },
188
- { "word": "BOTTLENECK", "clue": "Performance limitation point" },
189
- { "word": "CACHE", "clue": "Fast temporary storage" },
190
- { "word": "BUFFER", "clue": "Temporary data holder" },
191
- { "word": "QUEUE", "clue": "Ordered waiting line" },
192
- { "word": "STACK", "clue": "Last-in-first-out structure" },
193
- { "word": "HEAP", "clue": "Dynamic memory area" },
194
- { "word": "POINTER", "clue": "Memory address reference" },
195
- { "word": "REFERENCE", "clue": "Object location indicator" },
196
- { "word": "GARBAGE", "clue": "Unused memory collection" },
197
- { "word": "ALLOCATION", "clue": "Memory assignment" },
198
- { "word": "DEALLOCATION", "clue": "Memory release" },
199
- { "word": "LEAK", "clue": "Memory usage error" },
200
- { "word": "OVERFLOW", "clue": "Capacity exceeding error" },
201
- { "word": "UNDERFLOW", "clue": "Insufficient data error" },
202
- { "word": "EXCEPTION", "clue": "Error handling mechanism" },
203
- { "word": "INTERRUPT", "clue": "Process suspension signal" },
204
- { "word": "SIGNAL", "clue": "Process communication" },
205
- { "word": "EVENT", "clue": "System occurrence" },
206
- { "word": "HANDLER", "clue": "Event processing function" },
207
- { "word": "CALLBACK", "clue": "Function reference" },
208
- { "word": "PROMISE", "clue": "Future value placeholder" },
209
- { "word": "ASYNC", "clue": "Non-blocking operation" },
210
- { "word": "AWAIT", "clue": "Pause for completion" },
211
- { "word": "YIELD", "clue": "Temporary function pause" },
212
- { "word": "GENERATOR", "clue": "Value sequence producer" },
213
- { "word": "ITERATOR", "clue": "Sequential access pattern" },
214
- { "word": "RECURSION", "clue": "Self-calling function" },
215
- { "word": "CLOSURE", "clue": "Function scope retention" },
216
- { "word": "LAMBDA", "clue": "Anonymous function" },
217
- { "word": "FUNCTIONAL", "clue": "Function-based programming" },
218
- { "word": "PROCEDURAL", "clue": "Step-by-step programming" },
219
- { "word": "DECLARATIVE", "clue": "What-not-how programming" },
220
- { "word": "IMPERATIVE", "clue": "Command-based programming" }
221
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crossword-app/backend-py/debug_full_generation.py DELETED
@@ -1,316 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Debug the complete crossword generation process to identify display/numbering issues.
4
- """
5
-
6
- import asyncio
7
- import sys
8
- import json
9
- from pathlib import Path
10
-
11
- # Add project root to path
12
- project_root = Path(__file__).parent
13
- sys.path.insert(0, str(project_root))
14
-
15
- from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
16
-
17
- async def debug_complete_generation():
18
- """Debug the complete crossword generation process."""
19
-
20
- print("🔍 Debugging Complete Crossword Generation Process\n")
21
-
22
- # Create generator with no vector service to use static words
23
- generator = CrosswordGeneratorFixed(vector_service=None)
24
-
25
- # Override the word selection to use controlled test words
26
- test_words = [
27
- {"word": "MACHINE", "clue": "Device with moving parts"},
28
- {"word": "COMPUTER", "clue": "Electronic device"},
29
- {"word": "EXPERT", "clue": "Person with specialized knowledge"},
30
- {"word": "SCIENCE", "clue": "Systematic study"},
31
- {"word": "TECHNOLOGY", "clue": "Applied science"},
32
- {"word": "RESEARCH", "clue": "Systematic investigation"},
33
- {"word": "ANALYSIS", "clue": "Detailed examination"},
34
- {"word": "METHOD", "clue": "Systematic approach"}
35
- ]
36
-
37
- # Mock the word selection method
38
- async def mock_select_words(topics, difficulty, use_ai):
39
- return test_words
40
- generator._select_words = mock_select_words
41
-
42
- print("=" * 70)
43
- print("GENERATING COMPLETE CROSSWORD")
44
- print("=" * 70)
45
-
46
- try:
47
- result = await generator.generate_puzzle(["technology"], "medium", use_ai=False)
48
-
49
- if result:
50
- print("✅ Crossword generation successful!")
51
-
52
- # Analyze the complete result
53
- analyze_crossword_result(result)
54
- else:
55
- print("❌ Crossword generation failed - returned None")
56
-
57
- except Exception as e:
58
- print(f"❌ Crossword generation failed with error: {e}")
59
- import traceback
60
- traceback.print_exc()
61
-
62
- def analyze_crossword_result(result):
63
- """Analyze the complete crossword result for potential issues."""
64
-
65
- print("\n" + "=" * 70)
66
- print("CROSSWORD RESULT ANALYSIS")
67
- print("=" * 70)
68
-
69
- # Print basic metadata
70
- metadata = result.get("metadata", {})
71
- print("Metadata:")
72
- for key, value in metadata.items():
73
- print(f" {key}: {value}")
74
-
75
- # Analyze the grid
76
- grid = result.get("grid", [])
77
- print(f"\nGrid dimensions: {len(grid)}x{len(grid[0]) if grid else 0}")
78
-
79
- print("\nGrid layout:")
80
- print_numbered_grid(grid)
81
-
82
- # Analyze placed words vs clues
83
- clues = result.get("clues", [])
84
- print(f"\nNumber of clues generated: {len(clues)}")
85
-
86
- print("\nClue analysis:")
87
- for i, clue in enumerate(clues):
88
- print(f" Clue {i+1}:")
89
- print(f" Number: {clue.get('number', 'MISSING')}")
90
- print(f" Word: {clue.get('word', 'MISSING')}")
91
- print(f" Direction: {clue.get('direction', 'MISSING')}")
92
- print(f" Position: {clue.get('position', 'MISSING')}")
93
- print(f" Text: {clue.get('text', 'MISSING')}")
94
-
95
- # Check for potential issues
96
- print("\n" + "=" * 70)
97
- print("ISSUE DETECTION")
98
- print("=" * 70)
99
-
100
- check_word_boundary_consistency(grid, clues)
101
- check_numbering_consistency(clues)
102
- check_grid_word_alignment(grid, clues)
103
-
104
- def print_numbered_grid(grid):
105
- """Print grid with coordinates for analysis."""
106
- if not grid:
107
- print(" Empty grid")
108
- return
109
-
110
- # Print column headers
111
- print(" ", end="")
112
- for c in range(len(grid[0])):
113
- print(f"{c:2d}", end="")
114
- print()
115
-
116
- # Print rows with row numbers
117
- for r in range(len(grid)):
118
- print(f" {r:2d}: ", end="")
119
- for c in range(len(grid[0])):
120
- cell = grid[r][c]
121
- if cell == ".":
122
- print(" .", end="")
123
- else:
124
- print(f" {cell}", end="")
125
- print()
126
-
127
- def check_word_boundary_consistency(grid, clues):
128
- """Check if words in clues match what's actually in the grid."""
129
-
130
- print("Checking word boundary consistency:")
131
-
132
- issues_found = []
133
-
134
- for clue in clues:
135
- word = clue.get("word", "")
136
- position = clue.get("position", {})
137
- direction = clue.get("direction", "")
138
-
139
- if not all([word, position, direction]):
140
- issues_found.append(f"Incomplete clue data: {clue}")
141
- continue
142
-
143
- row = position.get("row", -1)
144
- col = position.get("col", -1)
145
-
146
- if row < 0 or col < 0:
147
- issues_found.append(f"Invalid position for word '{word}': {position}")
148
- continue
149
-
150
- # Extract the actual word from the grid
151
- grid_word = extract_word_from_grid(grid, row, col, direction, len(word))
152
-
153
- if grid_word != word:
154
- issues_found.append(f"Mismatch for '{word}' at ({row}, {col}) {direction}: grid shows '{grid_word}'")
155
-
156
- if issues_found:
157
- print(" ❌ Issues found:")
158
- for issue in issues_found:
159
- print(f" {issue}")
160
- else:
161
- print(" ✅ All words match grid positions")
162
-
163
- def extract_word_from_grid(grid, row, col, direction, expected_length):
164
- """Extract a word from the grid at the given position and direction."""
165
-
166
- if row >= len(grid) or col >= len(grid[0]):
167
- return "OUT_OF_BOUNDS"
168
-
169
- word = ""
170
-
171
- if direction == "across": # horizontal
172
- for i in range(expected_length):
173
- if col + i >= len(grid[0]):
174
- return word + "TRUNCATED"
175
- word += grid[row][col + i]
176
-
177
- elif direction == "down": # vertical
178
- for i in range(expected_length):
179
- if row + i >= len(grid):
180
- return word + "TRUNCATED"
181
- word += grid[row + i][col]
182
-
183
- return word
184
-
185
- def check_numbering_consistency(clues):
186
- """Check if clue numbering is consistent and logical."""
187
-
188
- print("\nChecking numbering consistency:")
189
-
190
- numbers = [clue.get("number", -1) for clue in clues]
191
- issues = []
192
-
193
- # Check for duplicate numbers
194
- if len(numbers) != len(set(numbers)):
195
- issues.append("Duplicate clue numbers found")
196
-
197
- # Check for missing numbers in sequence
198
- if numbers:
199
- min_num = min(numbers)
200
- max_num = max(numbers)
201
- expected = set(range(min_num, max_num + 1))
202
- actual = set(numbers)
203
-
204
- if expected != actual:
205
- missing = expected - actual
206
- extra = actual - expected
207
- if missing:
208
- issues.append(f"Missing numbers: {sorted(missing)}")
209
- if extra:
210
- issues.append(f"Extra numbers: {sorted(extra)}")
211
-
212
- if issues:
213
- print(" ❌ Numbering issues:")
214
- for issue in issues:
215
- print(f" {issue}")
216
- else:
217
- print(" ✅ Numbering is consistent")
218
-
219
- def check_grid_word_alignment(grid, clues):
220
- """Check if all words are properly aligned and don't create unintended extensions."""
221
-
222
- print("\nChecking grid word alignment:")
223
-
224
- # Find all letter sequences in the grid
225
- horizontal_sequences = find_horizontal_sequences(grid)
226
- vertical_sequences = find_vertical_sequences(grid)
227
-
228
- print(f" Found {len(horizontal_sequences)} horizontal sequences")
229
- print(f" Found {len(vertical_sequences)} vertical sequences")
230
-
231
- # Check if each sequence corresponds to a clue
232
- clue_words = {}
233
- for clue in clues:
234
- pos = clue.get("position", {})
235
- key = (pos.get("row"), pos.get("col"), clue.get("direction"))
236
- clue_words[key] = clue.get("word", "")
237
-
238
- issues = []
239
-
240
- # Check horizontal sequences
241
- for seq in horizontal_sequences:
242
- row, start_col, word = seq
243
- key = (row, start_col, "across")
244
- if key not in clue_words:
245
- issues.append(f"Unaccounted horizontal sequence: '{word}' at ({row}, {start_col})")
246
- elif clue_words[key] != word:
247
- issues.append(f"Mismatch: clue says '{clue_words[key]}' but grid shows '{word}' at ({row}, {start_col})")
248
-
249
- # Check vertical sequences
250
- for seq in vertical_sequences:
251
- col, start_row, word = seq
252
- key = (start_row, col, "down")
253
- if key not in clue_words:
254
- issues.append(f"Unaccounted vertical sequence: '{word}' at ({start_row}, {col})")
255
- elif clue_words[key] != word:
256
- issues.append(f"Mismatch: clue says '{clue_words[key]}' but grid shows '{word}' at ({start_row}, {col})")
257
-
258
- if issues:
259
- print(" ❌ Alignment issues found:")
260
- for issue in issues:
261
- print(f" {issue}")
262
- else:
263
- print(" ✅ All words are properly aligned")
264
-
265
- def find_horizontal_sequences(grid):
266
- """Find all horizontal letter sequences of length > 1."""
267
- sequences = []
268
-
269
- for r in range(len(grid)):
270
- current_word = ""
271
- start_col = None
272
-
273
- for c in range(len(grid[0])):
274
- if grid[r][c] != ".":
275
- if start_col is None:
276
- start_col = c
277
- current_word += grid[r][c]
278
- else:
279
- if current_word and len(current_word) > 1:
280
- sequences.append((r, start_col, current_word))
281
- current_word = ""
282
- start_col = None
283
-
284
- # Handle word at end of row
285
- if current_word and len(current_word) > 1:
286
- sequences.append((r, start_col, current_word))
287
-
288
- return sequences
289
-
290
- def find_vertical_sequences(grid):
291
- """Find all vertical letter sequences of length > 1."""
292
- sequences = []
293
-
294
- for c in range(len(grid[0])):
295
- current_word = ""
296
- start_row = None
297
-
298
- for r in range(len(grid)):
299
- if grid[r][c] != ".":
300
- if start_row is None:
301
- start_row = r
302
- current_word += grid[r][c]
303
- else:
304
- if current_word and len(current_word) > 1:
305
- sequences.append((c, start_row, current_word))
306
- current_word = ""
307
- start_row = None
308
-
309
- # Handle word at end of column
310
- if current_word and len(current_word) > 1:
311
- sequences.append((c, start_row, current_word))
312
-
313
- return sequences
314
-
315
- if __name__ == "__main__":
316
- asyncio.run(debug_complete_generation())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crossword-app/backend-py/debug_grid_direct.py DELETED
@@ -1,293 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Direct grid generation test to identify word boundary/display issues.
4
- """
5
-
6
- import sys
7
- from pathlib import Path
8
-
9
- # Add project root to path
10
- project_root = Path(__file__).parent
11
- sys.path.insert(0, str(project_root))
12
-
13
- from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
14
-
15
- def test_direct_grid_generation():
16
- """Test grid generation directly with controlled words."""
17
-
18
- print("🔍 Direct Grid Generation Test\n")
19
-
20
- generator = CrosswordGeneratorFixed(vector_service=None)
21
-
22
- # Test words that might cause the issues seen in the images
23
- test_words = [
24
- {"word": "MACHINE", "clue": "Device with moving parts"},
25
- {"word": "COMPUTER", "clue": "Electronic device"},
26
- {"word": "EXPERT", "clue": "Person with specialized knowledge"},
27
- {"word": "SCIENCE", "clue": "Systematic study"},
28
- {"word": "CAMERA", "clue": "Device for taking photos"},
29
- {"word": "METHOD", "clue": "Systematic approach"}
30
- ]
31
-
32
- print("=" * 60)
33
- print("TEST 1: Direct grid creation")
34
- print("=" * 60)
35
-
36
- # Test the _create_grid method directly
37
- result = generator._create_grid(test_words)
38
-
39
- if result:
40
- print("✅ Grid generation successful!")
41
-
42
- grid = result["grid"]
43
- placed_words = result["placed_words"]
44
- clues = result["clues"]
45
-
46
- print(f"Grid size: {len(grid)}x{len(grid[0])}")
47
- print(f"Words placed: {len(placed_words)}")
48
- print(f"Clues generated: {len(clues)}")
49
-
50
- # Print the grid
51
- print("\nGenerated Grid:")
52
- print_grid_with_coordinates(grid)
53
-
54
- # Print placed words details
55
- print("\nPlaced Words:")
56
- for i, word_info in enumerate(placed_words):
57
- print(f" {i+1}. {word_info['word']} at ({word_info['row']}, {word_info['col']}) {word_info['direction']}")
58
-
59
- # Print clues
60
- print("\nGenerated Clues:")
61
- for clue in clues:
62
- print(f" {clue['number']}. {clue['direction']}: {clue['word']} - {clue['text']}")
63
-
64
- # Analyze for potential issues
65
- print("\n" + "=" * 60)
66
- print("ANALYSIS")
67
- print("=" * 60)
68
-
69
- analyze_grid_issues(grid, placed_words, clues)
70
-
71
- else:
72
- print("❌ Grid generation failed")
73
-
74
- # Test another scenario that might reproduce the image issues
75
- print("\n" + "=" * 60)
76
- print("TEST 2: Scenario with potential extension words")
77
- print("=" * 60)
78
-
79
- # Words that might create the "MACHINERY" type issue
80
- extension_words = [
81
- {"word": "MACHINE", "clue": "Device with moving parts"},
82
- {"word": "MACHINERY", "clue": "Mechanical equipment"}, # Might cause confusion
83
- {"word": "EXPERT", "clue": "Specialist"},
84
- {"word": "TECHNOLOGY", "clue": "Applied science"},
85
- ]
86
-
87
- result2 = generator._create_grid(extension_words)
88
-
89
- if result2:
90
- print("✅ Extension test grid generated!")
91
-
92
- grid2 = result2["grid"]
93
- placed_words2 = result2["placed_words"]
94
-
95
- print("\nExtension Test Grid:")
96
- print_grid_with_coordinates(grid2)
97
-
98
- print("\nPlaced Words:")
99
- for i, word_info in enumerate(placed_words2):
100
- print(f" {i+1}. {word_info['word']} at ({word_info['row']}, {word_info['col']}) {word_info['direction']}")
101
-
102
- # Check specifically for MACHINE vs MACHINERY issues
103
- check_machine_machinery_issue(grid2, placed_words2)
104
-
105
- else:
106
- print("❌ Extension test grid generation failed")
107
-
108
- def print_grid_with_coordinates(grid):
109
- """Print grid with row and column coordinates."""
110
- if not grid:
111
- print(" Empty grid")
112
- return
113
-
114
- # Print column headers
115
- print(" ", end="")
116
- for c in range(len(grid[0])):
117
- print(f"{c:2d}", end="")
118
- print()
119
-
120
- # Print rows
121
- for r in range(len(grid)):
122
- print(f" {r:2d}: ", end="")
123
- for c in range(len(grid[0])):
124
- cell = grid[r][c]
125
- if cell == ".":
126
- print(" .", end="")
127
- else:
128
- print(f" {cell}", end="")
129
- print()
130
-
131
- def analyze_grid_issues(grid, placed_words, clues):
132
- """Analyze the grid for potential boundary/display issues."""
133
-
134
- print("Checking for potential issues...")
135
-
136
- issues = []
137
-
138
- # Check 1: Verify each placed word actually exists in the grid
139
- for word_info in placed_words:
140
- word = word_info["word"]
141
- row = word_info["row"]
142
- col = word_info["col"]
143
- direction = word_info["direction"]
144
-
145
- grid_word = extract_word_from_grid(grid, row, col, direction, len(word))
146
-
147
- if grid_word != word:
148
- issues.append(f"Word mismatch: '{word}' expected at ({row},{col}) {direction}, but grid shows '{grid_word}'")
149
-
150
- # Check 2: Look for unintended letter sequences
151
- all_sequences = find_all_letter_sequences(grid)
152
- intended_words = {(w["row"], w["col"], w["direction"]): w["word"] for w in placed_words}
153
-
154
- for seq_info in all_sequences:
155
- row, col, direction, seq_word = seq_info
156
- key = (row, col, direction)
157
-
158
- if key not in intended_words:
159
- if len(seq_word) > 1: # Only care about multi-letter sequences
160
- issues.append(f"Unintended sequence: '{seq_word}' at ({row},{col}) {direction}")
161
- elif intended_words[key] != seq_word:
162
- issues.append(f"Sequence mismatch: expected '{intended_words[key]}' but found '{seq_word}' at ({row},{col}) {direction}")
163
-
164
- # Check 3: Verify clue consistency
165
- for clue in clues:
166
- clue_word = clue["word"]
167
- pos = clue["position"]
168
- clue_row = pos["row"]
169
- clue_col = pos["col"]
170
- clue_direction = clue["direction"]
171
-
172
- # Convert direction format if needed
173
- direction_map = {"across": "horizontal", "down": "vertical"}
174
- normalized_direction = direction_map.get(clue_direction, clue_direction)
175
-
176
- grid_word = extract_word_from_grid(grid, clue_row, clue_col, normalized_direction, len(clue_word))
177
-
178
- if grid_word != clue_word:
179
- issues.append(f"Clue mismatch: clue says '{clue_word}' at ({clue_row},{clue_col}) {clue_direction}, but grid shows '{grid_word}'")
180
-
181
- # Report results
182
- if issues:
183
- print("❌ Issues found:")
184
- for issue in issues:
185
- print(f" {issue}")
186
- else:
187
- print("✅ No issues detected - grid appears consistent")
188
-
189
- def extract_word_from_grid(grid, row, col, direction, expected_length):
190
- """Extract word from grid at given position and direction."""
191
- if row >= len(grid) or col >= len(grid[0]) or row < 0 or col < 0:
192
- return "OUT_OF_BOUNDS"
193
-
194
- word = ""
195
-
196
- if direction in ["horizontal", "across"]:
197
- for i in range(expected_length):
198
- if col + i >= len(grid[0]):
199
- return word + "[TRUNCATED]"
200
- word += grid[row][col + i]
201
- elif direction in ["vertical", "down"]:
202
- for i in range(expected_length):
203
- if row + i >= len(grid):
204
- return word + "[TRUNCATED]"
205
- word += grid[row + i][col]
206
-
207
- return word
208
-
209
- def find_all_letter_sequences(grid):
210
- """Find all letter sequences (horizontal and vertical) in the grid."""
211
- sequences = []
212
-
213
- # Horizontal sequences
214
- for r in range(len(grid)):
215
- current_word = ""
216
- start_col = None
217
-
218
- for c in range(len(grid[0])):
219
- if grid[r][c] != ".":
220
- if start_col is None:
221
- start_col = c
222
- current_word += grid[r][c]
223
- else:
224
- if current_word and len(current_word) > 1:
225
- sequences.append((r, start_col, "horizontal", current_word))
226
- current_word = ""
227
- start_col = None
228
-
229
- # Handle end of row
230
- if current_word and len(current_word) > 1:
231
- sequences.append((r, start_col, "horizontal", current_word))
232
-
233
- # Vertical sequences
234
- for c in range(len(grid[0])):
235
- current_word = ""
236
- start_row = None
237
-
238
- for r in range(len(grid)):
239
- if grid[r][c] != ".":
240
- if start_row is None:
241
- start_row = r
242
- current_word += grid[r][c]
243
- else:
244
- if current_word and len(current_word) > 1:
245
- sequences.append((start_row, c, "vertical", current_word))
246
- current_word = ""
247
- start_row = None
248
-
249
- # Handle end of column
250
- if current_word and len(current_word) > 1:
251
- sequences.append((start_row, c, "vertical", current_word))
252
-
253
- return sequences
254
-
255
- def check_machine_machinery_issue(grid, placed_words):
256
- """Specifically check for MACHINE vs MACHINERY confusion."""
257
-
258
- print("\nChecking for MACHINE/MACHINERY issue:")
259
-
260
- machine_words = [w for w in placed_words if "MACHINE" in w["word"]]
261
-
262
- if not machine_words:
263
- print(" No MACHINE-related words found")
264
- return
265
-
266
- for word_info in machine_words:
267
- word = word_info["word"]
268
- row = word_info["row"]
269
- col = word_info["col"]
270
- direction = word_info["direction"]
271
-
272
- print(f" Found: '{word}' at ({row},{col}) {direction}")
273
-
274
- # Check what's actually in the grid at this location
275
- grid_word = extract_word_from_grid(grid, row, col, direction, len(word))
276
- print(f" Grid shows: '{grid_word}'")
277
-
278
- # Check if there are extra letters that might create confusion
279
- if direction == "horizontal":
280
- # Check for letters after the word
281
- end_col = col + len(word)
282
- if end_col < len(grid[0]) and grid[row][end_col] != ".":
283
- extra_letters = ""
284
- check_col = end_col
285
- while check_col < len(grid[0]) and grid[row][check_col] != ".":
286
- extra_letters += grid[row][check_col]
287
- check_col += 1
288
- if extra_letters:
289
- print(f" ⚠️ Extra letters after word: '{extra_letters}'")
290
- print(f" This might make '{word}' appear as '{word + extra_letters}'")
291
-
292
- if __name__ == "__main__":
293
- test_direct_grid_generation()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crossword-app/backend-py/debug_index_error.py DELETED
@@ -1,307 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Debug the recurring index error by adding comprehensive bounds checking.
4
- """
5
-
6
- import asyncio
7
- import sys
8
- import logging
9
- from pathlib import Path
10
-
11
- # Add project root to path
12
- project_root = Path(__file__).parent
13
- sys.path.insert(0, str(project_root))
14
-
15
- from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
16
- from src.services.vector_search import VectorSearchService
17
-
18
- # Enable debug logging
19
- logging.basicConfig(level=logging.DEBUG)
20
- logger = logging.getLogger(__name__)
21
-
22
- class DebugCrosswordGenerator(CrosswordGeneratorFixed):
23
- """Debug version with comprehensive bounds checking."""
24
-
25
- def _can_place_word(self, grid, word, row, col, direction):
26
- """Enhanced _can_place_word with comprehensive bounds checking."""
27
- try:
28
- size = len(grid)
29
- logger.debug(f"_can_place_word: word={word}, row={row}, col={col}, direction={direction}, grid_size={size}")
30
-
31
- # Check initial boundaries
32
- if row < 0 or col < 0 or row >= size or col >= size:
33
- logger.debug(f"Initial bounds check failed: row={row}, col={col}, size={size}")
34
- return False
35
-
36
- if direction == "horizontal":
37
- if col + len(word) > size:
38
- logger.debug(f"Horizontal bounds check failed: col+len(word)={col + len(word)} > size={size}")
39
- return False
40
-
41
- # Check word boundaries (no adjacent letters) - with bounds check
42
- if col > 0:
43
- if row >= size or col - 1 >= size or row < 0 or col - 1 < 0:
44
- logger.debug(f"Horizontal left boundary check failed: row={row}, col-1={col-1}, size={size}")
45
- return False
46
- if grid[row][col - 1] != ".":
47
- logger.debug(f"Horizontal left boundary has adjacent letter")
48
- return False
49
-
50
- if col + len(word) < size:
51
- if row >= size or col + len(word) >= size or row < 0 or col + len(word) < 0:
52
- logger.debug(f"Horizontal right boundary check failed: row={row}, col+len={col + len(word)}, size={size}")
53
- return False
54
- if grid[row][col + len(word)] != ".":
55
- logger.debug(f"Horizontal right boundary has adjacent letter")
56
- return False
57
-
58
- # Check each letter position
59
- for i, letter in enumerate(word):
60
- check_row = row
61
- check_col = col + i
62
- if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
63
- logger.debug(f"Horizontal letter position check failed: letter {i}, row={check_row}, col={check_col}, size={size}")
64
- return False
65
- current_cell = grid[check_row][check_col]
66
- if current_cell != "." and current_cell != letter:
67
- logger.debug(f"Horizontal letter conflict: expected {letter}, found {current_cell}")
68
- return False
69
-
70
- else: # vertical
71
- if row + len(word) > size:
72
- logger.debug(f"Vertical bounds check failed: row+len(word)={row + len(word)} > size={size}")
73
- return False
74
-
75
- # Check word boundaries - with bounds check
76
- if row > 0:
77
- if row - 1 >= size or col >= size or row - 1 < 0 or col < 0:
78
- logger.debug(f"Vertical top boundary check failed: row-1={row-1}, col={col}, size={size}")
79
- return False
80
- if grid[row - 1][col] != ".":
81
- logger.debug(f"Vertical top boundary has adjacent letter")
82
- return False
83
-
84
- if row + len(word) < size:
85
- if row + len(word) >= size or col >= size or row + len(word) < 0 or col < 0:
86
- logger.debug(f"Vertical bottom boundary check failed: row+len={row + len(word)}, col={col}, size={size}")
87
- return False
88
- if grid[row + len(word)][col] != ".":
89
- logger.debug(f"Vertical bottom boundary has adjacent letter")
90
- return False
91
-
92
- # Check each letter position
93
- for i, letter in enumerate(word):
94
- check_row = row + i
95
- check_col = col
96
- if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
97
- logger.debug(f"Vertical letter position check failed: letter {i}, row={check_row}, col={check_col}, size={size}")
98
- return False
99
- current_cell = grid[check_row][check_col]
100
- if current_cell != "." and current_cell != letter:
101
- logger.debug(f"Vertical letter conflict: expected {letter}, found {current_cell}")
102
- return False
103
-
104
- logger.debug(f"_can_place_word: SUCCESS for word={word}")
105
- return True
106
-
107
- except Exception as e:
108
- logger.error(f"❌ ERROR in _can_place_word: {e}")
109
- logger.error(f" word={word}, row={row}, col={col}, direction={direction}")
110
- logger.error(f" grid_size={len(grid) if grid else 'None'}")
111
- import traceback
112
- traceback.print_exc()
113
- return False
114
-
115
- def _place_word(self, grid, word, row, col, direction):
116
- """Enhanced _place_word with comprehensive bounds checking."""
117
- try:
118
- size = len(grid)
119
- logger.debug(f"_place_word: word={word}, row={row}, col={col}, direction={direction}, grid_size={size}")
120
-
121
- original_state = []
122
-
123
- if direction == "horizontal":
124
- for i, letter in enumerate(word):
125
- check_row = row
126
- check_col = col + i
127
- if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
128
- logger.error(f"❌ _place_word horizontal bounds error: row={check_row}, col={check_col}, size={size}")
129
- raise IndexError(f"Grid index out of range: [{check_row}][{check_col}] in grid of size {size}")
130
-
131
- original_state.append({
132
- "row": check_row,
133
- "col": check_col,
134
- "value": grid[check_row][check_col]
135
- })
136
- grid[check_row][check_col] = letter
137
- else:
138
- for i, letter in enumerate(word):
139
- check_row = row + i
140
- check_col = col
141
- if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
142
- logger.error(f"❌ _place_word vertical bounds error: row={check_row}, col={check_col}, size={size}")
143
- raise IndexError(f"Grid index out of range: [{check_row}][{check_col}] in grid of size {size}")
144
-
145
- original_state.append({
146
- "row": check_row,
147
- "col": check_col,
148
- "value": grid[check_row][check_col]
149
- })
150
- grid[check_row][check_col] = letter
151
-
152
- logger.debug(f"_place_word: SUCCESS for word={word}")
153
- return original_state
154
-
155
- except Exception as e:
156
- logger.error(f"❌ ERROR in _place_word: {e}")
157
- logger.error(f" word={word}, row={row}, col={col}, direction={direction}")
158
- logger.error(f" grid_size={len(grid) if grid else 'None'}")
159
- import traceback
160
- traceback.print_exc()
161
- raise
162
-
163
- def _remove_word(self, grid, original_state):
164
- """Enhanced _remove_word with comprehensive bounds checking."""
165
- try:
166
- size = len(grid)
167
- logger.debug(f"_remove_word: restoring {len(original_state)} positions, grid_size={size}")
168
-
169
- for state in original_state:
170
- check_row = state["row"]
171
- check_col = state["col"]
172
- if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
173
- logger.error(f"❌ _remove_word bounds error: row={check_row}, col={check_col}, size={size}")
174
- raise IndexError(f"Grid index out of range: [{check_row}][{check_col}] in grid of size {size}")
175
-
176
- grid[check_row][check_col] = state["value"]
177
-
178
- logger.debug(f"_remove_word: SUCCESS")
179
-
180
- except Exception as e:
181
- logger.error(f"❌ ERROR in _remove_word: {e}")
182
- logger.error(f" grid_size={len(grid) if grid else 'None'}")
183
- logger.error(f" original_state={original_state}")
184
- import traceback
185
- traceback.print_exc()
186
- raise
187
-
188
- def _create_simple_cross(self, word_list, word_objs):
189
- """Enhanced _create_simple_cross with comprehensive bounds checking."""
190
- try:
191
- logger.debug(f"_create_simple_cross: words={word_list}")
192
-
193
- if len(word_list) < 2:
194
- logger.debug("Not enough words for simple cross")
195
- return None
196
-
197
- word1, word2 = word_list[0], word_list[1]
198
- intersections = self._find_word_intersections(word1, word2)
199
-
200
- if not intersections:
201
- logger.debug("No intersections found")
202
- return None
203
-
204
- # Use first intersection
205
- intersection = intersections[0]
206
- size = max(len(word1), len(word2)) + 4
207
- logger.debug(f"Creating grid of size {size} for simple cross")
208
-
209
- grid = [["." for _ in range(size)] for _ in range(size)]
210
-
211
- # Place first word horizontally in center
212
- center_row = size // 2
213
- center_col = (size - len(word1)) // 2
214
-
215
- logger.debug(f"Placing word1 '{word1}' at row={center_row}, col={center_col}")
216
-
217
- for i, letter in enumerate(word1):
218
- check_row = center_row
219
- check_col = center_col + i
220
- if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
221
- logger.error(f"❌ _create_simple_cross word1 bounds error: row={check_row}, col={check_col}, size={size}")
222
- raise IndexError(f"Grid index out of range: [{check_row}][{check_col}] in grid of size {size}")
223
- grid[check_row][check_col] = letter
224
-
225
- # Place second word vertically at intersection
226
- intersection_col = center_col + intersection["word_pos"]
227
- word2_start_row = center_row - intersection["placed_pos"]
228
-
229
- logger.debug(f"Placing word2 '{word2}' at row={word2_start_row}, col={intersection_col}")
230
-
231
- for i, letter in enumerate(word2):
232
- check_row = word2_start_row + i
233
- check_col = intersection_col
234
- if check_row >= size or check_col >= size or check_row < 0 or check_col < 0:
235
- logger.error(f"❌ _create_simple_cross word2 bounds error: row={check_row}, col={check_col}, size={size}")
236
- raise IndexError(f"Grid index out of range: [{check_row}][{check_col}] in grid of size {size}")
237
- grid[check_row][check_col] = letter
238
-
239
- placed_words = [
240
- {"word": word1, "row": center_row, "col": center_col, "direction": "horizontal", "number": 1},
241
- {"word": word2, "row": word2_start_row, "col": intersection_col, "direction": "vertical", "number": 2}
242
- ]
243
-
244
- logger.debug(f"_create_simple_cross: SUCCESS")
245
-
246
- trimmed = self._trim_grid(grid, placed_words)
247
- clues = self._generate_clues(word_objs[:2], trimmed["placed_words"])
248
-
249
- return {
250
- "grid": trimmed["grid"],
251
- "placed_words": trimmed["placed_words"],
252
- "clues": clues
253
- }
254
-
255
- except Exception as e:
256
- logger.error(f"❌ ERROR in _create_simple_cross: {e}")
257
- import traceback
258
- traceback.print_exc()
259
- raise
260
-
261
- async def test_debug_generator():
262
- """Test the debug generator to catch index errors."""
263
- try:
264
- print("🧪 Testing debug crossword generator...")
265
-
266
- # Create mock vector service
267
- vector_service = VectorSearchService()
268
-
269
- # Create debug generator
270
- generator = DebugCrosswordGenerator(vector_service)
271
-
272
- # Test with various topics and difficulties
273
- test_cases = [
274
- (["animals"], "medium"),
275
- (["science"], "hard"),
276
- (["technology"], "easy"),
277
- (["animals", "science"], "medium"),
278
- ]
279
-
280
- for i, (topics, difficulty) in enumerate(test_cases):
281
- print(f"\n🔬 Test {i+1}: topics={topics}, difficulty={difficulty}")
282
- try:
283
- result = await generator.generate_puzzle(topics, difficulty, use_ai=False)
284
- if result:
285
- print(f"✅ Test {i+1} succeeded")
286
- grid_size = len(result['grid'])
287
- word_count = len(result['clues'])
288
- print(f" Grid: {grid_size}x{grid_size}, Words: {word_count}")
289
- else:
290
- print(f"⚠️ Test {i+1} returned None")
291
- except Exception as e:
292
- print(f"❌ Test {i+1} failed: {e}")
293
- import traceback
294
- traceback.print_exc()
295
- return False
296
-
297
- print(f"\n✅ All debug tests completed!")
298
- return True
299
-
300
- except Exception as e:
301
- print(f"❌ Debug test setup failed: {e}")
302
- import traceback
303
- traceback.print_exc()
304
- return False
305
-
306
- if __name__ == "__main__":
307
- asyncio.run(test_debug_generator())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crossword-app/backend-py/debug_simple.py DELETED
@@ -1,142 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Simple debug test for crossword generator index errors.
4
- """
5
-
6
- import asyncio
7
- import sys
8
- import logging
9
- from pathlib import Path
10
-
11
- # Add project root to path
12
- project_root = Path(__file__).parent
13
- sys.path.insert(0, str(project_root))
14
-
15
- from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
16
-
17
- # Enable debug logging
18
- logging.basicConfig(level=logging.DEBUG)
19
- logger = logging.getLogger(__name__)
20
-
21
- async def test_with_static_words():
22
- """Test generator with static word lists."""
23
-
24
- # Create generator without vector service
25
- generator = CrosswordGeneratorFixed(vector_service=None)
26
-
27
- # Create test words
28
- test_words = [
29
- {"word": "CAT", "clue": "Feline pet"},
30
- {"word": "DOG", "clue": "Man's best friend"},
31
- {"word": "BIRD", "clue": "Flying animal"},
32
- {"word": "FISH", "clue": "Aquatic animal"},
33
- {"word": "ELEPHANT", "clue": "Large mammal"},
34
- {"word": "TIGER", "clue": "Striped cat"},
35
- {"word": "HORSE", "clue": "Riding animal"},
36
- {"word": "BEAR", "clue": "Large carnivore"}
37
- ]
38
-
39
- print(f"🧪 Testing crossword generation with {len(test_words)} words...")
40
-
41
- try:
42
- # Test multiple times to catch intermittent errors
43
- for attempt in range(10):
44
- print(f"\n🔬 Attempt {attempt + 1}/10")
45
-
46
- # Shuffle words to create different scenarios
47
- import random
48
- random.shuffle(test_words)
49
-
50
- # Override the word selection to use our test words
51
- generator._select_words = lambda topics, difficulty, use_ai: test_words
52
-
53
- result = await generator.generate_puzzle(["animals"], "medium", use_ai=False)
54
-
55
- if result:
56
- grid_size = len(result['grid'])
57
- word_count = len(result['clues'])
58
- print(f"✅ Attempt {attempt + 1} succeeded: {grid_size}x{grid_size} grid, {word_count} words")
59
- else:
60
- print(f"⚠️ Attempt {attempt + 1} returned None")
61
-
62
- except IndexError as e:
63
- print(f"❌ INDEX ERROR caught on attempt {attempt + 1}: {e}")
64
- import traceback
65
- traceback.print_exc()
66
- return False
67
- except Exception as e:
68
- print(f"❌ Other error on attempt {attempt + 1}: {e}")
69
- import traceback
70
- traceback.print_exc()
71
- return False
72
-
73
- print(f"\n✅ All 10 attempts completed successfully!")
74
- return True
75
-
76
- async def test_grid_placement_directly():
77
- """Test grid placement functions directly with problematic data."""
78
-
79
- generator = CrosswordGeneratorFixed(vector_service=None)
80
-
81
- # Test data that might cause issues
82
- test_cases = [
83
- {
84
- "words": ["A", "I"], # Very short words
85
- "description": "Very short words"
86
- },
87
- {
88
- "words": ["VERYLONGWORDTHATMIGHTCAUSEISSUES", "SHORT"],
89
- "description": "Very long word with short word"
90
- },
91
- {
92
- "words": ["ABCDEFGHIJKLMNOP", "QRSTUVWXYZ"], # Long words
93
- "description": "Two long words"
94
- },
95
- {
96
- "words": ["TEST", "SETS", "NETS", "PETS"], # Multiple similar words
97
- "description": "Similar words with same endings"
98
- }
99
- ]
100
-
101
- for i, test_case in enumerate(test_cases):
102
- print(f"\n🔬 Grid test {i+1}: {test_case['description']}")
103
-
104
- try:
105
- word_list = test_case["words"]
106
- word_objs = [{"word": w, "clue": f"Clue for {w}"} for w in word_list]
107
-
108
- result = generator._create_grid(word_objs)
109
-
110
- if result:
111
- grid_size = len(result['grid'])
112
- word_count = len(result['placed_words'])
113
- print(f"✅ Grid test {i+1} succeeded: {grid_size}x{grid_size} grid, {word_count} words")
114
- else:
115
- print(f"⚠️ Grid test {i+1} returned None")
116
-
117
- except IndexError as e:
118
- print(f"❌ INDEX ERROR in grid test {i+1}: {e}")
119
- import traceback
120
- traceback.print_exc()
121
- return False
122
- except Exception as e:
123
- print(f"❌ Other error in grid test {i+1}: {e}")
124
- import traceback
125
- traceback.print_exc()
126
- return False
127
-
128
- return True
129
-
130
- if __name__ == "__main__":
131
- print("🧪 Starting debug tests for crossword generator...")
132
-
133
- async def run_tests():
134
- success1 = await test_with_static_words()
135
- success2 = await test_grid_placement_directly()
136
-
137
- if success1 and success2:
138
- print("\n🎉 All debug tests passed! No index errors detected.")
139
- else:
140
- print("\n❌ Some debug tests failed.")
141
-
142
- asyncio.run(run_tests())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crossword-app/backend-py/public/assets/index-2XJqMaqu.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import{r as p,a as C,R as P}from"./vendor-nf7bT_Uh.js";(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))n(t);new MutationObserver(t=>{for(const r of t)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&n(o)}).observe(document,{childList:!0,subtree:!0});function c(t){const r={};return t.integrity&&(r.integrity=t.integrity),t.referrerPolicy&&(r.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?r.credentials="include":t.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function n(t){if(t.ep)return;t.ep=!0;const r=c(t);fetch(t.href,r)}})();var w={exports:{}},z={};/**
2
+ * @license React
3
+ * react-jsx-runtime.production.min.js
4
+ *
5
+ * Copyright (c) Facebook, Inc. and its affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */var R=p,_=Symbol.for("react.element"),$=Symbol.for("react.fragment"),E=Object.prototype.hasOwnProperty,O=R.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,k={key:!0,ref:!0,__self:!0,__source:!0};function S(s,a,c){var n,t={},r=null,o=null;c!==void 0&&(r=""+c),a.key!==void 0&&(r=""+a.key),a.ref!==void 0&&(o=a.ref);for(n in a)E.call(a,n)&&!k.hasOwnProperty(n)&&(t[n]=a[n]);if(s&&s.defaultProps)for(n in a=s.defaultProps,a)t[n]===void 0&&(t[n]=a[n]);return{$$typeof:_,type:s,key:r,ref:o,props:t,_owner:O.current}}z.Fragment=$;z.jsx=S;z.jsxs=S;w.exports=z;var e=w.exports,N={},b=C;N.createRoot=b.createRoot,N.hydrateRoot=b.hydrateRoot;const T=({onTopicsChange:s,availableTopics:a=[],selectedTopics:c=[]})=>{const n=t=>{const r=c.includes(t)?c.filter(o=>o!==t):[...c,t];s(r)};return e.jsxs("div",{className:"topic-selector",children:[e.jsx("h3",{children:"Select Topics"}),e.jsx("div",{className:"topic-buttons",children:a.map(t=>e.jsx("button",{className:`topic-btn ${c.includes(t.name)?"selected":""}`,onClick:()=>n(t.name),children:t.name},t.id))}),e.jsxs("p",{className:"selected-count",children:[c.length," topic",c.length!==1?"s":""," selected"]})]})},L=({grid:s,clues:a,showSolution:c,onCellChange:n})=>{const[t,r]=p.useState({}),o=(u,l,i)=>{const d=`${u}-${l}`,h={...t,[d]:i.toUpperCase()};r(h),n&&n(u,l,i)},f=(u,l)=>{if(c&&!m(u,l))return s[u][l];const i=`${u}-${l}`;return t[i]||""},m=(u,l)=>s[u][l]===".",g=(u,l)=>{if(!a)return null;const i=a.find(d=>d.position.row===u&&d.position.col===l);return i?i.number:null};if(!s||s.length===0)return e.jsx("div",{className:"puzzle-grid",children:"No puzzle loaded"});const x=s.length,y=s[0]?s[0].length:0;return e.jsx("div",{className:"puzzle-container",children:e.jsx("div",{className:"puzzle-grid",style:{gridTemplateColumns:`repeat(${y}, 35px)`,gridTemplateRows:`repeat(${x}, 35px)`},children:s.map((u,l)=>u.map((i,d)=>{const h=g(l,d);return m(l,d)?e.jsx("div",{className:"grid-cell empty-cell",style:{visibility:"hidden"}},`${l}-${d}`):e.jsxs("div",{className:"grid-cell white-cell",children:[h&&e.jsx("span",{className:"cell-number",children:h}),e.jsx("input",{type:"text",maxLength:"1",value:f(l,d),onChange:v=>o(l,d,v.target.value),className:`cell-input ${c?"solution-text":""}`,disabled:c})]},`${l}-${d}`)}))})})},A=({clues:s=[]})=>{const a=s.filter(t=>t.direction==="across"),c=s.filter(t=>t.direction==="down"),n=({title:t,clueList:r})=>e.jsxs("div",{className:"clue-section",children:[e.jsx("h4",{children:t}),e.jsx("ol",{children:r.map(o=>e.jsxs("li",{className:"clue-item",children:[e.jsx("span",{className:"clue-number",children:o.number}),e.jsx("span",{className:"clue-text",children:o.text})]},`${o.number}-${o.direction}`))})]});return e.jsxs("div",{className:"clue-list",children:[e.jsx(n,{title:"Across",clueList:a}),e.jsx(n,{title:"Down",clueList:c})]})},D=({message:s="Generating puzzle..."})=>e.jsxs("div",{className:"loading-spinner",children:[e.jsx("div",{className:"spinner"}),e.jsx("p",{className:"loading-message",children:s})]}),F=()=>{const[s,a]=p.useState(null),[c,n]=p.useState(!1),[t,r]=p.useState(null),[o,f]=p.useState([]),m="",g=p.useCallback(async()=>{try{n(!0);const l=await fetch(`${m}/api/topics`);if(!l.ok)throw new Error("Failed to fetch topics");const i=await l.json();f(i)}catch(l){r(l.message)}finally{n(!1)}},[m]),x=p.useCallback(async(l,i="medium",d=!1)=>{try{n(!0),r(null);const h=await fetch(`${m}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({topics:l,difficulty:i,useAI:d})});if(!h.ok){const v=await h.json().catch(()=>({}));throw new Error(v.message||"Failed to generate puzzle")}const j=await h.json();return a(j),j}catch(h){return r(h.message),null}finally{n(!1)}},[m]),y=p.useCallback(async l=>{try{const i=await fetch(`${m}/api/validate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({puzzle:s,answers:l})});if(!i.ok)throw new Error("Failed to validate answers");return await i.json()}catch(i){return r(i.message),null}},[m,s]),u=p.useCallback(()=>{a(null),r(null)},[]);return{puzzle:s,loading:c,error:t,topics:o,fetchTopics:g,generatePuzzle:x,validateAnswers:y,resetPuzzle:u}};function G(){const[s,a]=p.useState([]),[c,n]=p.useState("medium"),[t,r]=p.useState(!1),{puzzle:o,loading:f,error:m,topics:g,fetchTopics:x,generatePuzzle:y,resetPuzzle:u}=F();p.useEffect(()=>{x()},[x]);const l=async()=>{if(s.length===0){alert("Please select at least one topic");return}await y(s,c,!1)},i=j=>{a(j)},d=()=>{u(),a([]),r(!1),n("medium")},h=()=>{r(!0)};return e.jsxs("div",{className:"crossword-app",children:[e.jsxs("header",{className:"app-header",children:[e.jsx("h1",{className:"app-title",children:"Crossword Puzzle Generator"}),e.jsx("p",{children:"Select topics and generate your custom crossword puzzle!"})]}),e.jsx(T,{onTopicsChange:i,availableTopics:g,selectedTopics:s}),e.jsxs("div",{className:"puzzle-controls",children:[e.jsxs("select",{value:c,onChange:j=>n(j.target.value),className:"control-btn",children:[e.jsx("option",{value:"easy",children:"Easy"}),e.jsx("option",{value:"medium",children:"Medium"}),e.jsx("option",{value:"hard",children:"Hard"})]}),e.jsx("button",{onClick:l,disabled:f||s.length===0,className:"control-btn generate-btn",children:f?"Generating...":"Generate Puzzle"}),e.jsx("button",{onClick:d,className:"control-btn reset-btn",children:"Reset"}),o&&!t&&e.jsx("button",{onClick:h,className:"control-btn reveal-btn",children:"Reveal Solution"})]}),m&&e.jsxs("div",{className:"error-message",children:["Error: ",m]}),f&&e.jsx(D,{}),o&&!f&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"puzzle-info",children:e.jsxs("span",{className:"puzzle-stats",children:[o.metadata.wordCount," words • ",o.metadata.size,"×",o.metadata.size," grid"]})}),e.jsxs("div",{className:"puzzle-layout",children:[e.jsx(L,{grid:o.grid,clues:o.clues,showSolution:t}),e.jsx(A,{clues:o.clues})]})]}),!o&&!f&&!m&&e.jsx("div",{style:{textAlign:"center",padding:"40px",color:"#7f8c8d"},children:'Select topics and click "Generate Puzzle" to start!'})]})}N.createRoot(document.getElementById("root")).render(e.jsx(P.StrictMode,{children:e.jsx(G,{})}));
10
+ //# sourceMappingURL=index-2XJqMaqu.js.map
crossword-app/backend-py/public/assets/index-2XJqMaqu.js.map ADDED
@@ -0,0 +1 @@
 
 
1
+ {"version":3,"file":"index-2XJqMaqu.js","sources":["../../node_modules/react/cjs/react-jsx-runtime.production.min.js","../../node_modules/react/jsx-runtime.js","../../node_modules/react-dom/client.js","../../src/components/TopicSelector.jsx","../../src/components/PuzzleGrid.jsx","../../src/components/ClueList.jsx","../../src/components/LoadingSpinner.jsx","../../src/hooks/useCrossword.js","../../src/App.jsx","../../src/main.jsx"],"sourcesContent":["/**\n * @license React\n * react-jsx-runtime.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';var f=require(\"react\"),k=Symbol.for(\"react.element\"),l=Symbol.for(\"react.fragment\"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};\nfunction q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=\"\"+g);void 0!==a.key&&(e=\"\"+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}exports.Fragment=l;exports.jsx=q;exports.jsxs=q;\n","'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('./cjs/react-jsx-runtime.production.min.js');\n} else {\n module.exports = require('./cjs/react-jsx-runtime.development.js');\n}\n","'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n","import React from 'react';\n\nconst TopicSelector = ({ \n onTopicsChange, \n availableTopics = [], \n selectedTopics = []\n}) => {\n const handleTopicToggle = (topic) => {\n const newSelectedTopics = selectedTopics.includes(topic)\n ? selectedTopics.filter(t => t !== topic)\n : [...selectedTopics, topic];\n \n onTopicsChange(newSelectedTopics);\n };\n\n return (\n <div className=\"topic-selector\">\n <h3>Select Topics</h3>\n <div className=\"topic-buttons\">\n {availableTopics.map(topic => (\n <button\n key={topic.id}\n className={`topic-btn ${selectedTopics.includes(topic.name) ? 'selected' : ''}`}\n onClick={() => handleTopicToggle(topic.name)}\n >\n {topic.name}\n </button>\n ))}\n </div>\n \n <p className=\"selected-count\">\n {selectedTopics.length} topic{selectedTopics.length !== 1 ? 's' : ''} selected\n </p>\n </div>\n );\n};\n\nexport default TopicSelector;","import React, { useState } from 'react';\n\nconst PuzzleGrid = ({ grid, clues, showSolution, onCellChange }) => {\n const [userAnswers, setUserAnswers] = useState({});\n\n const handleCellInput = (row, col, value) => {\n const key = `${row}-${col}`;\n const newAnswers = { ...userAnswers, [key]: value.toUpperCase() };\n setUserAnswers(newAnswers);\n onCellChange && onCellChange(row, col, value);\n };\n\n const getCellValue = (row, col) => {\n if (showSolution && !isBlackCell(row, col)) {\n return grid[row][col];\n }\n const key = `${row}-${col}`;\n return userAnswers[key] || '';\n };\n\n const isBlackCell = (row, col) => {\n return grid[row][col] === '.';\n };\n\n const getCellNumber = (row, col) => {\n if (!clues) return null;\n const clue = clues.find(c => c.position.row === row && c.position.col === col);\n return clue ? clue.number : null;\n };\n\n if (!grid || grid.length === 0) {\n return <div className=\"puzzle-grid\">No puzzle loaded</div>;\n }\n\n const gridRows = grid.length;\n const gridCols = grid[0] ? grid[0].length : 0;\n\n return (\n <div className=\"puzzle-container\">\n <div \n className=\"puzzle-grid\"\n style={{\n gridTemplateColumns: `repeat(${gridCols}, 35px)`,\n gridTemplateRows: `repeat(${gridRows}, 35px)`\n }}\n >\n {grid.map((row, rowIndex) =>\n row.map((cell, colIndex) => {\n const cellNumber = getCellNumber(rowIndex, colIndex);\n const isBlack = isBlackCell(rowIndex, colIndex);\n \n // Only render cells that contain letters (not black/unused cells)\n if (isBlack) {\n return (\n <div\n key={`${rowIndex}-${colIndex}`}\n className=\"grid-cell empty-cell\"\n style={{ visibility: 'hidden' }}\n >\n </div>\n );\n }\n \n return (\n <div\n key={`${rowIndex}-${colIndex}`}\n className=\"grid-cell white-cell\"\n >\n {cellNumber && <span className=\"cell-number\">{cellNumber}</span>}\n <input\n type=\"text\"\n maxLength=\"1\"\n value={getCellValue(rowIndex, colIndex)}\n onChange={(e) => handleCellInput(rowIndex, colIndex, e.target.value)}\n className={`cell-input ${showSolution ? 'solution-text' : ''}`}\n disabled={showSolution}\n />\n </div>\n );\n })\n )}\n </div>\n </div>\n );\n};\n\nexport default PuzzleGrid;","import React from 'react';\n\nconst ClueList = ({ clues = [] }) => {\n const acrossClues = clues.filter(clue => clue.direction === 'across');\n const downClues = clues.filter(clue => clue.direction === 'down');\n\n const ClueSection = ({ title, clueList }) => (\n <div className=\"clue-section\">\n <h4>{title}</h4>\n <ol>\n {clueList.map(clue => (\n <li key={`${clue.number}-${clue.direction}`} className=\"clue-item\">\n <span className=\"clue-number\">{clue.number}</span>\n <span className=\"clue-text\">{clue.text}</span>\n </li>\n ))}\n </ol>\n </div>\n );\n\n return (\n <div className=\"clue-list\">\n <ClueSection title=\"Across\" clueList={acrossClues} />\n <ClueSection title=\"Down\" clueList={downClues} />\n </div>\n );\n};\n\nexport default ClueList;","import React from 'react';\n\nconst LoadingSpinner = ({ message = \"Generating puzzle...\" }) => {\n return (\n <div className=\"loading-spinner\">\n <div className=\"spinner\"></div>\n <p className=\"loading-message\">{message}</p>\n </div>\n );\n};\n\nexport default LoadingSpinner;","import { useState, useCallback } from 'react';\n\nconst useCrossword = () => {\n const [puzzle, setPuzzle] = useState(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState(null);\n const [topics, setTopics] = useState([]);\n\n const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || (import.meta.env.PROD ? '' : 'http://localhost:3000');\n\n const fetchTopics = useCallback(async () => {\n try {\n setLoading(true);\n const response = await fetch(`${API_BASE_URL}/api/topics`);\n if (!response.ok) throw new Error('Failed to fetch topics');\n const data = await response.json();\n setTopics(data);\n } catch (err) {\n setError(err.message);\n } finally {\n setLoading(false);\n }\n }, [API_BASE_URL]);\n\n const generatePuzzle = useCallback(async (selectedTopics, difficulty = 'medium', useAI = false) => {\n try {\n setLoading(true);\n setError(null);\n \n const response = await fetch(`${API_BASE_URL}/api/generate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n topics: selectedTopics,\n difficulty,\n useAI\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.message || 'Failed to generate puzzle');\n }\n \n const puzzleData = await response.json();\n setPuzzle(puzzleData);\n return puzzleData;\n } catch (err) {\n setError(err.message);\n return null;\n } finally {\n setLoading(false);\n }\n }, [API_BASE_URL]);\n\n const validateAnswers = useCallback(async (userAnswers) => {\n try {\n const response = await fetch(`${API_BASE_URL}/api/validate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n puzzle: puzzle,\n answers: userAnswers\n })\n });\n\n if (!response.ok) throw new Error('Failed to validate answers');\n \n return await response.json();\n } catch (err) {\n setError(err.message);\n return null;\n }\n }, [API_BASE_URL, puzzle]);\n\n const resetPuzzle = useCallback(() => {\n setPuzzle(null);\n setError(null);\n }, []);\n\n return {\n puzzle,\n loading,\n error,\n topics,\n fetchTopics,\n generatePuzzle,\n validateAnswers,\n resetPuzzle\n };\n};\n\nexport default useCrossword;","import React, { useState, useEffect } from 'react';\nimport TopicSelector from './components/TopicSelector';\nimport PuzzleGrid from './components/PuzzleGrid';\nimport ClueList from './components/ClueList';\nimport LoadingSpinner from './components/LoadingSpinner';\nimport useCrossword from './hooks/useCrossword';\nimport './styles/puzzle.css';\n\nfunction App() {\n const [selectedTopics, setSelectedTopics] = useState([]);\n const [difficulty, setDifficulty] = useState('medium');\n const [showSolution, setShowSolution] = useState(false);\n \n const {\n puzzle,\n loading,\n error,\n topics,\n fetchTopics,\n generatePuzzle,\n resetPuzzle\n } = useCrossword();\n\n useEffect(() => {\n fetchTopics();\n }, [fetchTopics]);\n\n const handleGeneratePuzzle = async () => {\n if (selectedTopics.length === 0) {\n alert('Please select at least one topic');\n return;\n }\n \n await generatePuzzle(selectedTopics, difficulty, false);\n };\n\n const handleTopicsChange = (topics) => {\n setSelectedTopics(topics);\n };\n\n\n const handleReset = () => {\n resetPuzzle();\n setSelectedTopics([]);\n setShowSolution(false);\n setDifficulty('medium');\n };\n\n const handleRevealSolution = () => {\n setShowSolution(true);\n };\n\n return (\n <div className=\"crossword-app\">\n <header className=\"app-header\">\n <h1 className=\"app-title\">Crossword Puzzle Generator</h1>\n <p>Select topics and generate your custom crossword puzzle!</p>\n </header>\n\n <TopicSelector \n onTopicsChange={handleTopicsChange}\n availableTopics={topics}\n selectedTopics={selectedTopics}\n />\n\n <div className=\"puzzle-controls\">\n <select \n value={difficulty} \n onChange={(e) => setDifficulty(e.target.value)}\n className=\"control-btn\"\n >\n <option value=\"easy\">Easy</option>\n <option value=\"medium\">Medium</option>\n <option value=\"hard\">Hard</option>\n </select>\n \n <button\n onClick={handleGeneratePuzzle}\n disabled={loading || selectedTopics.length === 0}\n className=\"control-btn generate-btn\"\n >\n {loading ? 'Generating...' : 'Generate Puzzle'}\n </button>\n \n <button\n onClick={handleReset}\n className=\"control-btn reset-btn\"\n >\n Reset\n </button>\n \n {puzzle && !showSolution && (\n <button\n onClick={handleRevealSolution}\n className=\"control-btn reveal-btn\"\n >\n Reveal Solution\n </button>\n )}\n </div>\n\n {error && (\n <div className=\"error-message\">\n Error: {error}\n </div>\n )}\n\n {loading && <LoadingSpinner />}\n\n {puzzle && !loading && (\n <>\n <div className=\"puzzle-info\">\n <span className=\"puzzle-stats\">\n {puzzle.metadata.wordCount} words • {puzzle.metadata.size}×{puzzle.metadata.size} grid\n </span>\n </div>\n <div className=\"puzzle-layout\">\n <PuzzleGrid \n grid={puzzle.grid} \n clues={puzzle.clues}\n showSolution={showSolution}\n />\n <ClueList clues={puzzle.clues} />\n </div>\n </>\n )}\n\n {!puzzle && !loading && !error && (\n <div style={{ textAlign: 'center', padding: '40px', color: '#7f8c8d' }}>\n Select topics and click \"Generate Puzzle\" to start!\n </div>\n )}\n </div>\n );\n}\n\nexport default App;","import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App.jsx'\n\nReactDOM.createRoot(document.getElementById('root')).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>,\n)"],"names":["f","require$$0","k","l","m","n","p","q","c","g","b","d","e","h","reactJsxRuntime_production_min","jsxRuntimeModule","client","TopicSelector","onTopicsChange","availableTopics","selectedTopics","handleTopicToggle","topic","newSelectedTopics","t","jsxs","jsx","PuzzleGrid","grid","clues","showSolution","onCellChange","userAnswers","setUserAnswers","useState","handleCellInput","row","col","value","key","newAnswers","getCellValue","isBlackCell","getCellNumber","clue","gridRows","gridCols","rowIndex","cell","colIndex","cellNumber","ClueList","acrossClues","downClues","ClueSection","title","clueList","LoadingSpinner","message","useCrossword","puzzle","setPuzzle","loading","setLoading","error","setError","topics","setTopics","API_BASE_URL","fetchTopics","useCallback","response","data","err","generatePuzzle","difficulty","useAI","errorData","puzzleData","validateAnswers","resetPuzzle","App","setSelectedTopics","setDifficulty","setShowSolution","useEffect","handleGeneratePuzzle","handleTopicsChange","handleReset","handleRevealSolution","Fragment","ReactDOM","React"],"mappings":"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","x_google_ignoreList":[0,1,2]}
crossword-app/backend-py/public/assets/index-7dkEH9uQ.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import{r as m,a as R,R as E}from"./vendor-nf7bT_Uh.js";(function(){const c=document.createElement("link").relList;if(c&&c.supports&&c.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))n(t);new MutationObserver(t=>{for(const r of t)if(r.type==="childList")for(const l of r.addedNodes)l.tagName==="LINK"&&l.rel==="modulepreload"&&n(l)}).observe(document,{childList:!0,subtree:!0});function o(t){const r={};return t.integrity&&(r.integrity=t.integrity),t.referrerPolicy&&(r.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?r.credentials="include":t.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function n(t){if(t.ep)return;t.ep=!0;const r=o(t);fetch(t.href,r)}})();var S={exports:{}},N={};/**
2
+ * @license React
3
+ * react-jsx-runtime.production.min.js
4
+ *
5
+ * Copyright (c) Facebook, Inc. and its affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */var _=m,$=Symbol.for("react.element"),k=Symbol.for("react.fragment"),O=Object.prototype.hasOwnProperty,T=_.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,L={key:!0,ref:!0,__self:!0,__source:!0};function P(s,c,o){var n,t={},r=null,l=null;o!==void 0&&(r=""+o),c.key!==void 0&&(r=""+c.key),c.ref!==void 0&&(l=c.ref);for(n in c)O.call(c,n)&&!L.hasOwnProperty(n)&&(t[n]=c[n]);if(s&&s.defaultProps)for(n in c=s.defaultProps,c)t[n]===void 0&&(t[n]=c[n]);return{$$typeof:$,type:s,key:r,ref:l,props:t,_owner:T.current}}N.Fragment=k;N.jsx=P;N.jsxs=P;S.exports=N;var e=S.exports,w={},C=R;w.createRoot=C.createRoot,w.hydrateRoot=C.hydrateRoot;const A=({onTopicsChange:s,availableTopics:c=[],selectedTopics:o=[],customSentence:n="",onSentenceChange:t})=>{const r=l=>{const x=o.includes(l)?o.filter(i=>i!==l):[...o,l];s(x)};return e.jsxs("div",{className:"topic-selector",children:[e.jsx("h3",{children:"Select Topics"}),e.jsx("div",{className:"topic-buttons",children:c.map(l=>e.jsx("button",{className:`topic-btn ${o.includes(l.name)?"selected":""}`,onClick:()=>r(l.name),children:l.name},l.id))}),e.jsxs("div",{className:"sentence-input-container",children:[e.jsx("label",{htmlFor:"custom-sentence",className:"sentence-label",children:"Custom Sentence (optional)"}),e.jsx("textarea",{id:"custom-sentence",className:"sentence-input",value:n,onChange:l=>t&&t(l.target.value),placeholder:"Enter a sentence to influence word selection...",rows:"3",maxLength:"200"}),e.jsxs("div",{className:"sentence-info",children:[e.jsxs("span",{className:"char-count",children:[n.length,"/200 characters"]}),n&&e.jsx("button",{type:"button",className:"clear-sentence-btn",onClick:()=>t&&t(""),title:"Clear sentence",children:"Clear"})]})]}),e.jsxs("p",{className:"selected-count",children:[o.length," topic",o.length!==1?"s":""," selected"]})]})},F=({grid:s,clues:c,showSolution:o,onCellChange:n})=>{const[t,r]=m.useState({}),l=(d,a,u)=>{const p=`${d}-${a}`,f={...t,[p]:u.toUpperCase()};r(f),n&&n(d,a,u)},x=(d,a)=>{if(o&&!i(d,a))return s[d][a];const u=`${d}-${a}`;return t[u]||""},i=(d,a)=>s[d][a]===".",h=(d,a)=>{if(!c)return null;const u=c.find(p=>p.position.row===d&&p.position.col===a);return u?u.number:null};if(!s||s.length===0)return e.jsx("div",{className:"puzzle-grid",children:"No puzzle loaded"});const g=s.length,z=s[0]?s[0].length:0;return e.jsx("div",{className:"puzzle-container",children:e.jsx("div",{className:"puzzle-grid",style:{gridTemplateColumns:`repeat(${z}, 35px)`,gridTemplateRows:`repeat(${g}, 35px)`},children:s.map((d,a)=>d.map((u,p)=>{const f=h(a,p);return i(a,p)?e.jsx("div",{className:"grid-cell empty-cell",style:{visibility:"hidden"}},`${a}-${p}`):e.jsxs("div",{className:"grid-cell white-cell",children:[f&&e.jsx("span",{className:"cell-number",children:f}),e.jsx("input",{type:"text",maxLength:"1",value:x(a,p),onChange:y=>l(a,p,y.target.value),className:`cell-input ${o?"solution-text":""}`,disabled:o})]},`${a}-${p}`)}))})})},D=({clues:s=[]})=>{const c=s.filter(t=>t.direction==="across"),o=s.filter(t=>t.direction==="down"),n=({title:t,clueList:r})=>e.jsxs("div",{className:"clue-section",children:[e.jsx("h4",{children:t}),e.jsx("ol",{children:r.map(l=>e.jsxs("li",{className:"clue-item",children:[e.jsx("span",{className:"clue-number",children:l.number}),e.jsx("span",{className:"clue-text",children:l.text})]},`${l.number}-${l.direction}`))})]});return e.jsxs("div",{className:"clue-list",children:[e.jsx(n,{title:"Across",clueList:c}),e.jsx(n,{title:"Down",clueList:o})]})},G=({message:s="Generating puzzle..."})=>e.jsxs("div",{className:"loading-spinner",children:[e.jsx("div",{className:"spinner"}),e.jsx("p",{className:"loading-message",children:s})]}),B=()=>{const[s,c]=m.useState(null),[o,n]=m.useState(!1),[t,r]=m.useState(null),[l,x]=m.useState([]),i="",h=m.useCallback(async()=>{try{n(!0);const a=await fetch(`${i}/api/topics`);if(!a.ok)throw new Error("Failed to fetch topics");const u=await a.json();x(u)}catch(a){r(a.message)}finally{n(!1)}},[i]),g=m.useCallback(async(a,u="medium",p=!1,f="")=>{try{n(!0),r(null);const j=await fetch(`${i}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({topics:a,difficulty:u,useAI:p,...f&&{customSentence:f}})});if(!j.ok){const b=await j.json().catch(()=>({}));throw new Error(b.message||"Failed to generate puzzle")}const y=await j.json();return c(y),y}catch(j){return r(j.message),null}finally{n(!1)}},[i]),z=m.useCallback(async a=>{try{const u=await fetch(`${i}/api/validate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({puzzle:s,answers:a})});if(!u.ok)throw new Error("Failed to validate answers");return await u.json()}catch(u){return r(u.message),null}},[i,s]),d=m.useCallback(()=>{c(null),r(null)},[]);return{puzzle:s,loading:o,error:t,topics:l,fetchTopics:h,generatePuzzle:g,validateAnswers:z,resetPuzzle:d}};function U(){const[s,c]=m.useState([]),[o,n]=m.useState("medium"),[t,r]=m.useState(!1),[l,x]=m.useState(""),{puzzle:i,loading:h,error:g,topics:z,fetchTopics:d,generatePuzzle:a,resetPuzzle:u}=B();m.useEffect(()=>{d()},[d]);const p=async()=>{if(s.length===0){alert("Please select at least one topic");return}await a(s,o,!1,l)},f=v=>{c(v)},j=v=>{x(v)},y=()=>{u(),c([]),r(!1),n("medium"),x("")},b=()=>{r(!0)};return e.jsxs("div",{className:"crossword-app",children:[e.jsxs("header",{className:"app-header",children:[e.jsx("h1",{className:"app-title",children:"Crossword Puzzle Generator"}),e.jsx("p",{children:"Select topics and generate your custom crossword puzzle!"})]}),e.jsx(A,{onTopicsChange:f,availableTopics:z,selectedTopics:s,customSentence:l,onSentenceChange:j}),e.jsxs("div",{className:"puzzle-controls",children:[e.jsxs("select",{value:o,onChange:v=>n(v.target.value),className:"control-btn",children:[e.jsx("option",{value:"easy",children:"Easy"}),e.jsx("option",{value:"medium",children:"Medium"}),e.jsx("option",{value:"hard",children:"Hard"})]}),e.jsx("button",{onClick:p,disabled:h||s.length===0,className:"control-btn generate-btn",children:h?"Generating...":"Generate Puzzle"}),e.jsx("button",{onClick:y,className:"control-btn reset-btn",children:"Reset"}),i&&!t&&e.jsx("button",{onClick:b,className:"control-btn reveal-btn",children:"Reveal Solution"})]}),g&&e.jsxs("div",{className:"error-message",children:["Error: ",g]}),h&&e.jsx(G,{}),i&&!h&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"puzzle-info",children:e.jsxs("span",{className:"puzzle-stats",children:[i.metadata.wordCount," words • ",i.metadata.size,"×",i.metadata.size," grid"]})}),e.jsxs("div",{className:"puzzle-layout",children:[e.jsx(F,{grid:i.grid,clues:i.clues,showSolution:t}),e.jsx(D,{clues:i.clues})]})]}),!i&&!h&&!g&&e.jsx("div",{style:{textAlign:"center",padding:"40px",color:"#7f8c8d"},children:'Select topics and click "Generate Puzzle" to start!'})]})}w.createRoot(document.getElementById("root")).render(e.jsx(E.StrictMode,{children:e.jsx(U,{})}));
10
+ //# sourceMappingURL=index-7dkEH9uQ.js.map
crossword-app/backend-py/public/assets/index-7dkEH9uQ.js.map ADDED
@@ -0,0 +1 @@
 
 
1
+ {"version":3,"file":"index-7dkEH9uQ.js","sources":["../../node_modules/react/cjs/react-jsx-runtime.production.min.js","../../node_modules/react/jsx-runtime.js","../../node_modules/react-dom/client.js","../../src/components/TopicSelector.jsx","../../src/components/PuzzleGrid.jsx","../../src/components/ClueList.jsx","../../src/components/LoadingSpinner.jsx","../../src/hooks/useCrossword.js","../../src/App.jsx","../../src/main.jsx"],"sourcesContent":["/**\n * @license React\n * react-jsx-runtime.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';var f=require(\"react\"),k=Symbol.for(\"react.element\"),l=Symbol.for(\"react.fragment\"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};\nfunction q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=\"\"+g);void 0!==a.key&&(e=\"\"+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}exports.Fragment=l;exports.jsx=q;exports.jsxs=q;\n","'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('./cjs/react-jsx-runtime.production.min.js');\n} else {\n module.exports = require('./cjs/react-jsx-runtime.development.js');\n}\n","'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n","import React from 'react';\n\nconst TopicSelector = ({ \n onTopicsChange, \n availableTopics = [], \n selectedTopics = [],\n customSentence = '',\n onSentenceChange\n}) => {\n const handleTopicToggle = (topic) => {\n const newSelectedTopics = selectedTopics.includes(topic)\n ? selectedTopics.filter(t => t !== topic)\n : [...selectedTopics, topic];\n \n onTopicsChange(newSelectedTopics);\n };\n\n return (\n <div className=\"topic-selector\">\n <h3>Select Topics</h3>\n <div className=\"topic-buttons\">\n {availableTopics.map(topic => (\n <button\n key={topic.id}\n className={`topic-btn ${selectedTopics.includes(topic.name) ? 'selected' : ''}`}\n onClick={() => handleTopicToggle(topic.name)}\n >\n {topic.name}\n </button>\n ))}\n </div>\n \n <div className=\"sentence-input-container\">\n <label htmlFor=\"custom-sentence\" className=\"sentence-label\">\n Custom Sentence (optional)\n </label>\n <textarea\n id=\"custom-sentence\"\n className=\"sentence-input\"\n value={customSentence}\n onChange={(e) => onSentenceChange && onSentenceChange(e.target.value)}\n placeholder=\"Enter a sentence to influence word selection...\"\n rows=\"3\"\n maxLength=\"200\"\n />\n <div className=\"sentence-info\">\n <span className=\"char-count\">{customSentence.length}/200 characters</span>\n {customSentence && (\n <button \n type=\"button\"\n className=\"clear-sentence-btn\"\n onClick={() => onSentenceChange && onSentenceChange('')}\n title=\"Clear sentence\"\n >\n Clear\n </button>\n )}\n </div>\n </div>\n \n <p className=\"selected-count\">\n {selectedTopics.length} topic{selectedTopics.length !== 1 ? 's' : ''} selected\n </p>\n </div>\n );\n};\n\nexport default TopicSelector;","import React, { useState } from 'react';\n\nconst PuzzleGrid = ({ grid, clues, showSolution, onCellChange }) => {\n const [userAnswers, setUserAnswers] = useState({});\n\n const handleCellInput = (row, col, value) => {\n const key = `${row}-${col}`;\n const newAnswers = { ...userAnswers, [key]: value.toUpperCase() };\n setUserAnswers(newAnswers);\n onCellChange && onCellChange(row, col, value);\n };\n\n const getCellValue = (row, col) => {\n if (showSolution && !isBlackCell(row, col)) {\n return grid[row][col];\n }\n const key = `${row}-${col}`;\n return userAnswers[key] || '';\n };\n\n const isBlackCell = (row, col) => {\n return grid[row][col] === '.';\n };\n\n const getCellNumber = (row, col) => {\n if (!clues) return null;\n const clue = clues.find(c => c.position.row === row && c.position.col === col);\n return clue ? clue.number : null;\n };\n\n if (!grid || grid.length === 0) {\n return <div className=\"puzzle-grid\">No puzzle loaded</div>;\n }\n\n const gridRows = grid.length;\n const gridCols = grid[0] ? grid[0].length : 0;\n\n return (\n <div className=\"puzzle-container\">\n <div \n className=\"puzzle-grid\"\n style={{\n gridTemplateColumns: `repeat(${gridCols}, 35px)`,\n gridTemplateRows: `repeat(${gridRows}, 35px)`\n }}\n >\n {grid.map((row, rowIndex) =>\n row.map((cell, colIndex) => {\n const cellNumber = getCellNumber(rowIndex, colIndex);\n const isBlack = isBlackCell(rowIndex, colIndex);\n \n // Only render cells that contain letters (not black/unused cells)\n if (isBlack) {\n return (\n <div\n key={`${rowIndex}-${colIndex}`}\n className=\"grid-cell empty-cell\"\n style={{ visibility: 'hidden' }}\n >\n </div>\n );\n }\n \n return (\n <div\n key={`${rowIndex}-${colIndex}`}\n className=\"grid-cell white-cell\"\n >\n {cellNumber && <span className=\"cell-number\">{cellNumber}</span>}\n <input\n type=\"text\"\n maxLength=\"1\"\n value={getCellValue(rowIndex, colIndex)}\n onChange={(e) => handleCellInput(rowIndex, colIndex, e.target.value)}\n className={`cell-input ${showSolution ? 'solution-text' : ''}`}\n disabled={showSolution}\n />\n </div>\n );\n })\n )}\n </div>\n </div>\n );\n};\n\nexport default PuzzleGrid;","import React from 'react';\n\nconst ClueList = ({ clues = [] }) => {\n const acrossClues = clues.filter(clue => clue.direction === 'across');\n const downClues = clues.filter(clue => clue.direction === 'down');\n\n const ClueSection = ({ title, clueList }) => (\n <div className=\"clue-section\">\n <h4>{title}</h4>\n <ol>\n {clueList.map(clue => (\n <li key={`${clue.number}-${clue.direction}`} className=\"clue-item\">\n <span className=\"clue-number\">{clue.number}</span>\n <span className=\"clue-text\">{clue.text}</span>\n </li>\n ))}\n </ol>\n </div>\n );\n\n return (\n <div className=\"clue-list\">\n <ClueSection title=\"Across\" clueList={acrossClues} />\n <ClueSection title=\"Down\" clueList={downClues} />\n </div>\n );\n};\n\nexport default ClueList;","import React from 'react';\n\nconst LoadingSpinner = ({ message = \"Generating puzzle...\" }) => {\n return (\n <div className=\"loading-spinner\">\n <div className=\"spinner\"></div>\n <p className=\"loading-message\">{message}</p>\n </div>\n );\n};\n\nexport default LoadingSpinner;","import { useState, useCallback } from 'react';\n\nconst useCrossword = () => {\n const [puzzle, setPuzzle] = useState(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState(null);\n const [topics, setTopics] = useState([]);\n\n const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || (import.meta.env.PROD ? '' : 'http://localhost:3000');\n\n const fetchTopics = useCallback(async () => {\n try {\n setLoading(true);\n const response = await fetch(`${API_BASE_URL}/api/topics`);\n if (!response.ok) throw new Error('Failed to fetch topics');\n const data = await response.json();\n setTopics(data);\n } catch (err) {\n setError(err.message);\n } finally {\n setLoading(false);\n }\n }, [API_BASE_URL]);\n\n const generatePuzzle = useCallback(async (selectedTopics, difficulty = 'medium', useAI = false, customSentence = '') => {\n try {\n setLoading(true);\n setError(null);\n \n const response = await fetch(`${API_BASE_URL}/api/generate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n topics: selectedTopics,\n difficulty,\n useAI,\n ...(customSentence && { customSentence })\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.message || 'Failed to generate puzzle');\n }\n \n const puzzleData = await response.json();\n setPuzzle(puzzleData);\n return puzzleData;\n } catch (err) {\n setError(err.message);\n return null;\n } finally {\n setLoading(false);\n }\n }, [API_BASE_URL]);\n\n const validateAnswers = useCallback(async (userAnswers) => {\n try {\n const response = await fetch(`${API_BASE_URL}/api/validate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n puzzle: puzzle,\n answers: userAnswers\n })\n });\n\n if (!response.ok) throw new Error('Failed to validate answers');\n \n return await response.json();\n } catch (err) {\n setError(err.message);\n return null;\n }\n }, [API_BASE_URL, puzzle]);\n\n const resetPuzzle = useCallback(() => {\n setPuzzle(null);\n setError(null);\n }, []);\n\n return {\n puzzle,\n loading,\n error,\n topics,\n fetchTopics,\n generatePuzzle,\n validateAnswers,\n resetPuzzle\n };\n};\n\nexport default useCrossword;","import React, { useState, useEffect } from 'react';\nimport TopicSelector from './components/TopicSelector';\nimport PuzzleGrid from './components/PuzzleGrid';\nimport ClueList from './components/ClueList';\nimport LoadingSpinner from './components/LoadingSpinner';\nimport useCrossword from './hooks/useCrossword';\nimport './styles/puzzle.css';\n\nfunction App() {\n const [selectedTopics, setSelectedTopics] = useState([]);\n const [difficulty, setDifficulty] = useState('medium');\n const [showSolution, setShowSolution] = useState(false);\n const [customSentence, setCustomSentence] = useState('');\n \n const {\n puzzle,\n loading,\n error,\n topics,\n fetchTopics,\n generatePuzzle,\n resetPuzzle\n } = useCrossword();\n\n useEffect(() => {\n fetchTopics();\n }, [fetchTopics]);\n\n const handleGeneratePuzzle = async () => {\n if (selectedTopics.length === 0) {\n alert('Please select at least one topic');\n return;\n }\n \n await generatePuzzle(selectedTopics, difficulty, false, customSentence);\n };\n\n const handleTopicsChange = (topics) => {\n setSelectedTopics(topics);\n };\n\n const handleSentenceChange = (sentence) => {\n setCustomSentence(sentence);\n };\n\n\n const handleReset = () => {\n resetPuzzle();\n setSelectedTopics([]);\n setShowSolution(false);\n setDifficulty('medium');\n setCustomSentence('');\n };\n\n const handleRevealSolution = () => {\n setShowSolution(true);\n };\n\n return (\n <div className=\"crossword-app\">\n <header className=\"app-header\">\n <h1 className=\"app-title\">Crossword Puzzle Generator</h1>\n <p>Select topics and generate your custom crossword puzzle!</p>\n </header>\n\n <TopicSelector \n onTopicsChange={handleTopicsChange}\n availableTopics={topics}\n selectedTopics={selectedTopics}\n customSentence={customSentence}\n onSentenceChange={handleSentenceChange}\n />\n\n <div className=\"puzzle-controls\">\n <select \n value={difficulty} \n onChange={(e) => setDifficulty(e.target.value)}\n className=\"control-btn\"\n >\n <option value=\"easy\">Easy</option>\n <option value=\"medium\">Medium</option>\n <option value=\"hard\">Hard</option>\n </select>\n \n <button\n onClick={handleGeneratePuzzle}\n disabled={loading || selectedTopics.length === 0}\n className=\"control-btn generate-btn\"\n >\n {loading ? 'Generating...' : 'Generate Puzzle'}\n </button>\n \n <button\n onClick={handleReset}\n className=\"control-btn reset-btn\"\n >\n Reset\n </button>\n \n {puzzle && !showSolution && (\n <button\n onClick={handleRevealSolution}\n className=\"control-btn reveal-btn\"\n >\n Reveal Solution\n </button>\n )}\n </div>\n\n {error && (\n <div className=\"error-message\">\n Error: {error}\n </div>\n )}\n\n {loading && <LoadingSpinner />}\n\n {puzzle && !loading && (\n <>\n <div className=\"puzzle-info\">\n <span className=\"puzzle-stats\">\n {puzzle.metadata.wordCount} words • {puzzle.metadata.size}×{puzzle.metadata.size} grid\n </span>\n </div>\n <div className=\"puzzle-layout\">\n <PuzzleGrid \n grid={puzzle.grid} \n clues={puzzle.clues}\n showSolution={showSolution}\n />\n <ClueList clues={puzzle.clues} />\n </div>\n </>\n )}\n\n {!puzzle && !loading && !error && (\n <div style={{ textAlign: 'center', padding: '40px', color: '#7f8c8d' }}>\n Select topics and click \"Generate Puzzle\" to start!\n </div>\n )}\n </div>\n );\n}\n\nexport default App;","import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App.jsx'\n\nReactDOM.createRoot(document.getElementById('root')).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>,\n)"],"names":["f","require$$0","k","l","m","n","p","q","c","a","g","b","d","e","h","reactJsxRuntime_production_min","jsxRuntimeModule","client","TopicSelector","onTopicsChange","availableTopics","selectedTopics","customSentence","onSentenceChange","handleTopicToggle","topic","newSelectedTopics","t","jsxs","jsx","PuzzleGrid","grid","clues","showSolution","onCellChange","userAnswers","setUserAnswers","useState","handleCellInput","row","col","value","key","newAnswers","getCellValue","isBlackCell","getCellNumber","clue","gridRows","gridCols","rowIndex","cell","colIndex","cellNumber","ClueList","acrossClues","downClues","ClueSection","title","clueList","LoadingSpinner","message","useCrossword","puzzle","setPuzzle","loading","setLoading","error","setError","topics","setTopics","API_BASE_URL","fetchTopics","useCallback","response","data","err","generatePuzzle","difficulty","useAI","errorData","puzzleData","validateAnswers","resetPuzzle","App","setSelectedTopics","setDifficulty","setShowSolution","setCustomSentence","useEffect","handleGeneratePuzzle","handleTopicsChange","handleSentenceChange","sentence","handleReset","handleRevealSolution","Fragment","ReactDOM","React"],"mappings":"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","x_google_ignoreList":[0,1,2]}
crossword-app/backend-py/public/assets/index-CWqdoNhy.css ADDED
@@ -0,0 +1 @@
 
 
1
+ .crossword-app{max-width:1200px;margin:0 auto;padding:20px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}.app-header{text-align:center;margin-bottom:30px}.app-title{color:#2c3e50;font-size:2.5rem;margin-bottom:10px}.topic-selector{background:#f8f9fa;padding:20px;border-radius:8px;margin-bottom:20px}.topic-selector h3{margin-top:0;color:#2c3e50}.topic-buttons{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:15px}.topic-btn{padding:8px 16px;border:2px solid #3498db;background:#fff;color:#3498db;border-radius:20px;cursor:pointer;transition:all .3s ease;font-weight:500}.topic-btn:hover,.topic-btn.selected{background:#3498db;color:#fff}.selected-count{color:#7f8c8d;font-size:.9rem;margin:0}.sentence-input-container{margin-top:20px;margin-bottom:15px}.sentence-label{display:block;margin-bottom:8px;color:#2c3e50;font-weight:500;font-size:.95rem}.sentence-input{width:100%;padding:12px;border:2px solid #e1e8ed;border-radius:8px;font-family:inherit;font-size:.9rem;line-height:1.4;resize:vertical;min-height:80px;background:#fff;transition:border-color .3s ease,box-shadow .3s ease;box-sizing:border-box}.sentence-input:focus{outline:none;border-color:#3498db;box-shadow:0 0 0 3px #3498db1a}.sentence-input::placeholder{color:#95a5a6;font-style:italic}.sentence-info{display:flex;justify-content:space-between;align-items:center;margin-top:6px;font-size:.8rem}.char-count{color:#7f8c8d}.clear-sentence-btn{background:#e74c3c;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:.75rem;transition:background-color .2s ease}.clear-sentence-btn:hover{background:#c0392b}.clear-sentence-btn:active{background:#a93226}.ai-toggle-container{margin:20px 0;padding:15px;background:#f8f9fa;border-radius:8px;border:2px solid #e9ecef;transition:all .3s ease}.ai-toggle-container:has(.ai-checkbox:checked){background:linear-gradient(135deg,#e3f2fd,#f3e5f5);border-color:#3498db}.ai-toggle{display:flex;align-items:center;cursor:pointer;font-weight:500;margin-bottom:8px}.ai-checkbox{width:20px;height:20px;margin-right:12px;cursor:pointer;accent-color:#3498db}.ai-label{font-size:1rem;color:#2c3e50;-webkit-user-select:none;user-select:none}.ai-status{color:#27ae60;font-weight:600;font-size:.9rem}.ai-description{margin:0;font-size:.85rem;color:#6c757d;line-height:1.4;padding-left:32px}.puzzle-controls{display:flex;gap:15px;margin-bottom:20px;justify-content:center}.control-btn{padding:10px 20px;border:none;border-radius:5px;cursor:pointer;font-weight:600;transition:background-color .3s ease}.control-btn:disabled{background:#bdc3c7!important;color:#7f8c8d!important;cursor:not-allowed;opacity:.7}.generate-btn{background:#27ae60;color:#fff}.generate-btn:hover{background:#229954}.generate-btn:disabled{background:#bdc3c7;cursor:not-allowed}.reset-btn{background:#e74c3c;color:#fff}.reset-btn:hover{background:#c0392b}.reveal-btn{background:#f39c12;color:#fff}.reveal-btn:hover{background:#e67e22}.loading-spinner{display:flex;flex-direction:column;align-items:center;padding:40px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:15px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-message{color:#7f8c8d;font-size:1.1rem}.puzzle-info{display:flex;justify-content:space-between;align-items:center;margin:20px 0 10px;padding:10px 15px;background:#f8f9fa;border-radius:6px;border-left:4px solid #3498db}.puzzle-stats{font-size:.9rem;color:#6c757d;font-weight:500}.ai-generated-badge{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:4px 12px;border-radius:15px;font-size:.8rem;font-weight:600;text-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 2px 4px #0000001a}.puzzle-layout{display:grid;grid-template-columns:1fr 300px;gap:30px;margin-top:20px}@media (max-width: 768px){.puzzle-layout{grid-template-columns:1fr;gap:20px}.puzzle-info{flex-direction:column;gap:8px;text-align:center}.ai-toggle-container{padding:12px}.ai-description{padding-left:0;text-align:center}}.puzzle-container{display:flex;justify-content:center}.puzzle-grid{display:grid;gap:0;margin:0 auto;width:fit-content;height:fit-content}.grid-cell{width:35px;height:35px;position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background:#fff}.grid-cell:before{content:"";position:absolute;top:0;left:0;right:-1px;bottom:-1px;border:1px solid #2c3e50;pointer-events:none;z-index:10}.black-cell{background:#f0f0f0}.black-cell:before{background:#f0f0f0;border:1px solid #2c3e50}.white-cell{background:#fff}.empty-cell{background:transparent;border:none;visibility:hidden}.empty-cell:before{display:none}.cell-input{width:100%;height:100%;border:none!important;text-align:center;font-size:16px;font-weight:700;background:transparent;outline:none;text-transform:uppercase;position:relative;z-index:5}.cell-input:focus{background:#e8f4fd;box-shadow:inset 0 0 0 2px #3498db}.cell-number{position:absolute;top:1px;left:2px;font-size:10px;font-weight:700;color:#2c3e50;line-height:1;z-index:15;pointer-events:none}.solution-text{color:#2c3e50!important;font-weight:700!important;background:#fff!important}.solution-text:disabled{opacity:1!important;cursor:default}.grid-cell .solution-text{border:none!important;background:#fff!important}.clue-list{background:#f8f9fa;padding:20px;border-radius:8px;max-height:600px;overflow-y:auto}.clue-section{margin-bottom:25px}.clue-section h4{color:#2c3e50;margin-bottom:15px;font-size:1.2rem;border-bottom:2px solid #3498db;padding-bottom:5px}.clue-section ol{padding-left:0;list-style:none}.clue-item{display:flex;margin-bottom:8px;padding:8px;border-radius:4px;cursor:pointer;transition:background-color .2s ease}.clue-item:hover{background:#e9ecef}.clue-number{font-weight:700;color:#3498db;margin-right:10px;min-width:25px}.clue-text{flex:1;color:#2c3e50}.error-message{background:#f8d7da;color:#721c24;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #f5c6cb}.success-message{background:#d4edda;color:#155724;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #c3e6cb;text-align:center;font-weight:600}
crossword-app/backend-py/public/assets/index-DyT-gQda.css ADDED
@@ -0,0 +1 @@
 
 
1
+ .crossword-app{max-width:1200px;margin:0 auto;padding:20px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}.app-header{text-align:center;margin-bottom:30px}.app-title{color:#2c3e50;font-size:2.5rem;margin-bottom:10px}.topic-selector{background:#f8f9fa;padding:20px;border-radius:8px;margin-bottom:20px}.topic-selector h3{margin-top:0;color:#2c3e50}.topic-buttons{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:15px}.topic-btn{padding:8px 16px;border:2px solid #3498db;background:#fff;color:#3498db;border-radius:20px;cursor:pointer;transition:all .3s ease;font-weight:500}.topic-btn:hover,.topic-btn.selected{background:#3498db;color:#fff}.selected-count{color:#7f8c8d;font-size:.9rem;margin:0}.sentence-input-container{margin-top:20px;margin-bottom:15px}.sentence-label{display:block;margin-bottom:8px;color:#2c3e50;font-weight:500;font-size:.95rem}.sentence-input{width:100%;padding:12px;border:2px solid #e1e8ed;border-radius:8px;font-family:inherit;font-size:.9rem;line-height:1.4;resize:vertical;min-height:80px;background:#fff;transition:border-color .3s ease,box-shadow .3s ease;box-sizing:border-box}.sentence-input:focus{outline:none;border-color:#3498db;box-shadow:0 0 0 3px #3498db1a}.sentence-input::placeholder{color:#95a5a6;font-style:italic}.sentence-info{display:flex;justify-content:space-between;align-items:center;margin-top:6px;font-size:.8rem}.char-count{color:#7f8c8d}.clear-sentence-btn{background:#e74c3c;color:#fff;border:none;padding:4px 8px;border-radius:4px;cursor:pointer;font-size:.75rem;transition:background-color .2s ease}.clear-sentence-btn:hover{background:#c0392b}.clear-sentence-btn:active{background:#a93226}.multi-theme-toggle-container{margin-top:20px;margin-bottom:15px;padding:15px;background:#f0f4f8;border:1px solid #e1e8ed;border-radius:8px}.multi-theme-toggle{display:flex;align-items:center;cursor:pointer;margin-bottom:8px}.multi-theme-checkbox{width:18px;height:18px;margin-right:10px;cursor:pointer;accent-color:#3498db}.multi-theme-label{font-weight:500;color:#2c3e50;font-size:.95rem;-webkit-user-select:none;user-select:none}.multi-theme-description{margin:0;font-size:.85rem;color:#5a6c7d;line-height:1.4;font-style:italic;padding-left:28px}.ai-toggle-container{margin:20px 0;padding:15px;background:#f8f9fa;border-radius:8px;border:2px solid #e9ecef;transition:all .3s ease}.ai-toggle-container:has(.ai-checkbox:checked){background:linear-gradient(135deg,#e3f2fd,#f3e5f5);border-color:#3498db}.ai-toggle{display:flex;align-items:center;cursor:pointer;font-weight:500;margin-bottom:8px}.ai-checkbox{width:20px;height:20px;margin-right:12px;cursor:pointer;accent-color:#3498db}.ai-label{font-size:1rem;color:#2c3e50;-webkit-user-select:none;user-select:none}.ai-status{color:#27ae60;font-weight:600;font-size:.9rem}.ai-description{margin:0;font-size:.85rem;color:#6c757d;line-height:1.4;padding-left:32px}.puzzle-controls{display:flex;gap:15px;margin-bottom:20px;justify-content:center}.control-btn{padding:10px 20px;border:none;border-radius:5px;cursor:pointer;font-weight:600;transition:background-color .3s ease}.control-btn:disabled{background:#bdc3c7!important;color:#7f8c8d!important;cursor:not-allowed;opacity:.7}.generate-btn{background:#27ae60;color:#fff}.generate-btn:hover{background:#229954}.generate-btn:disabled{background:#bdc3c7;cursor:not-allowed}.reset-btn{background:#e74c3c;color:#fff}.reset-btn:hover{background:#c0392b}.reveal-btn{background:#f39c12;color:#fff}.reveal-btn:hover{background:#e67e22}.loading-spinner{display:flex;flex-direction:column;align-items:center;padding:40px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:15px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-message{color:#7f8c8d;font-size:1.1rem}.puzzle-info{display:flex;justify-content:space-between;align-items:center;margin:20px 0 10px;padding:10px 15px;background:#f8f9fa;border-radius:6px;border-left:4px solid #3498db}.puzzle-stats{font-size:.9rem;color:#6c757d;font-weight:500}.ai-generated-badge{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:4px 12px;border-radius:15px;font-size:.8rem;font-weight:600;text-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 2px 4px #0000001a}.puzzle-layout{display:grid;grid-template-columns:1fr 300px;gap:30px;margin-top:20px}@media (max-width: 768px){.puzzle-layout{grid-template-columns:1fr;gap:20px}.puzzle-info{flex-direction:column;gap:8px;text-align:center}.ai-toggle-container{padding:12px}.ai-description{padding-left:0;text-align:center}}.puzzle-container{display:flex;justify-content:center}.puzzle-grid{display:grid;gap:0;margin:0 auto;width:fit-content;height:fit-content}.grid-cell{width:35px;height:35px;position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background:#fff}.grid-cell:before{content:"";position:absolute;top:0;left:0;right:-1px;bottom:-1px;border:1px solid #2c3e50;pointer-events:none;z-index:10}.black-cell{background:#f0f0f0}.black-cell:before{background:#f0f0f0;border:1px solid #2c3e50}.white-cell{background:#fff}.empty-cell{background:transparent;border:none;visibility:hidden}.empty-cell:before{display:none}.cell-input{width:100%;height:100%;border:none!important;text-align:center;font-size:16px;font-weight:700;background:transparent;outline:none;text-transform:uppercase;position:relative;z-index:5}.cell-input:focus{background:#e8f4fd;box-shadow:inset 0 0 0 2px #3498db}.cell-number{position:absolute;top:1px;left:2px;font-size:10px;font-weight:700;color:#2c3e50;line-height:1;z-index:15;pointer-events:none}.solution-text{color:#2c3e50!important;font-weight:700!important;background:#fff!important}.solution-text:disabled{opacity:1!important;cursor:default}.grid-cell .solution-text{border:none!important;background:#fff!important}.clue-list{background:#f8f9fa;padding:20px;border-radius:8px;max-height:600px;overflow-y:auto}.clue-section{margin-bottom:25px}.clue-section h4{color:#2c3e50;margin-bottom:15px;font-size:1.2rem;border-bottom:2px solid #3498db;padding-bottom:5px}.clue-section ol{padding-left:0;list-style:none}.clue-item{display:flex;margin-bottom:8px;padding:8px;border-radius:4px;cursor:pointer;transition:background-color .2s ease}.clue-item:hover{background:#e9ecef}.clue-number{font-weight:700;color:#3498db;margin-right:10px;min-width:25px}.clue-text{flex:1;color:#2c3e50}.error-message{background:#f8d7da;color:#721c24;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #f5c6cb}.success-message{background:#d4edda;color:#155724;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #c3e6cb;text-align:center;font-weight:600}
crossword-app/backend-py/public/assets/index-V4v18wFW.css ADDED
@@ -0,0 +1 @@
 
 
1
+ .crossword-app{max-width:1200px;margin:0 auto;padding:20px;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif}.app-header{text-align:center;margin-bottom:30px}.app-title{color:#2c3e50;font-size:2.5rem;margin-bottom:10px}.topic-selector{background:#f8f9fa;padding:20px;border-radius:8px;margin-bottom:20px}.topic-selector h3{margin-top:0;color:#2c3e50}.topic-buttons{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:15px}.topic-btn{padding:8px 16px;border:2px solid #3498db;background:#fff;color:#3498db;border-radius:20px;cursor:pointer;transition:all .3s ease;font-weight:500}.topic-btn:hover,.topic-btn.selected{background:#3498db;color:#fff}.selected-count{color:#7f8c8d;font-size:.9rem;margin:0}.ai-toggle-container{margin:20px 0;padding:15px;background:#f8f9fa;border-radius:8px;border:2px solid #e9ecef;transition:all .3s ease}.ai-toggle-container:has(.ai-checkbox:checked){background:linear-gradient(135deg,#e3f2fd,#f3e5f5);border-color:#3498db}.ai-toggle{display:flex;align-items:center;cursor:pointer;font-weight:500;margin-bottom:8px}.ai-checkbox{width:20px;height:20px;margin-right:12px;cursor:pointer;accent-color:#3498db}.ai-label{font-size:1rem;color:#2c3e50;-webkit-user-select:none;user-select:none}.ai-status{color:#27ae60;font-weight:600;font-size:.9rem}.ai-description{margin:0;font-size:.85rem;color:#6c757d;line-height:1.4;padding-left:32px}.puzzle-controls{display:flex;gap:15px;margin-bottom:20px;justify-content:center}.control-btn{padding:10px 20px;border:none;border-radius:5px;cursor:pointer;font-weight:600;transition:background-color .3s ease}.control-btn:disabled{background:#bdc3c7!important;color:#7f8c8d!important;cursor:not-allowed;opacity:.7}.generate-btn{background:#27ae60;color:#fff}.generate-btn:hover{background:#229954}.generate-btn:disabled{background:#bdc3c7;cursor:not-allowed}.reset-btn{background:#e74c3c;color:#fff}.reset-btn:hover{background:#c0392b}.reveal-btn{background:#f39c12;color:#fff}.reveal-btn:hover{background:#e67e22}.loading-spinner{display:flex;flex-direction:column;align-items:center;padding:40px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #3498db;border-radius:50%;animation:spin 1s linear infinite;margin-bottom:15px}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-message{color:#7f8c8d;font-size:1.1rem}.puzzle-info{display:flex;justify-content:space-between;align-items:center;margin:20px 0 10px;padding:10px 15px;background:#f8f9fa;border-radius:6px;border-left:4px solid #3498db}.puzzle-stats{font-size:.9rem;color:#6c757d;font-weight:500}.ai-generated-badge{background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;padding:4px 12px;border-radius:15px;font-size:.8rem;font-weight:600;text-shadow:0 1px 2px rgba(0,0,0,.2);box-shadow:0 2px 4px #0000001a}.puzzle-layout{display:grid;grid-template-columns:1fr 300px;gap:30px;margin-top:20px}@media (max-width: 768px){.puzzle-layout{grid-template-columns:1fr;gap:20px}.puzzle-info{flex-direction:column;gap:8px;text-align:center}.ai-toggle-container{padding:12px}.ai-description{padding-left:0;text-align:center}}.puzzle-container{display:flex;justify-content:center}.puzzle-grid{display:grid;gap:0;margin:0 auto;width:fit-content;height:fit-content}.grid-cell{width:35px;height:35px;position:relative;display:flex;align-items:center;justify-content:center;box-sizing:border-box;background:#fff}.grid-cell:before{content:"";position:absolute;top:0;left:0;right:-1px;bottom:-1px;border:1px solid #2c3e50;pointer-events:none;z-index:10}.black-cell{background:#f0f0f0}.black-cell:before{background:#f0f0f0;border:1px solid #2c3e50}.white-cell{background:#fff}.empty-cell{background:transparent;border:none;visibility:hidden}.empty-cell:before{display:none}.cell-input{width:100%;height:100%;border:none!important;text-align:center;font-size:16px;font-weight:700;background:transparent;outline:none;text-transform:uppercase;position:relative;z-index:5}.cell-input:focus{background:#e8f4fd;box-shadow:inset 0 0 0 2px #3498db}.cell-number{position:absolute;top:1px;left:2px;font-size:10px;font-weight:700;color:#2c3e50;line-height:1;z-index:15;pointer-events:none}.solution-text{color:#2c3e50!important;font-weight:700!important;background:#fff!important}.solution-text:disabled{opacity:1!important;cursor:default}.grid-cell .solution-text{border:none!important;background:#fff!important}.clue-list{background:#f8f9fa;padding:20px;border-radius:8px;max-height:600px;overflow-y:auto}.clue-section{margin-bottom:25px}.clue-section h4{color:#2c3e50;margin-bottom:15px;font-size:1.2rem;border-bottom:2px solid #3498db;padding-bottom:5px}.clue-section ol{padding-left:0;list-style:none}.clue-item{display:flex;margin-bottom:8px;padding:8px;border-radius:4px;cursor:pointer;transition:background-color .2s ease}.clue-item:hover{background:#e9ecef}.clue-number{font-weight:700;color:#3498db;margin-right:10px;min-width:25px}.clue-text{flex:1;color:#2c3e50}.error-message{background:#f8d7da;color:#721c24;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #f5c6cb}.success-message{background:#d4edda;color:#155724;padding:15px;border-radius:5px;margin:20px 0;border:1px solid #c3e6cb;text-align:center;font-weight:600}
crossword-app/backend-py/public/assets/index-uK3VdD5a.js ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import{r as m,a as T,R as _}from"./vendor-nf7bT_Uh.js";(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const t of document.querySelectorAll('link[rel="modulepreload"]'))l(t);new MutationObserver(t=>{for(const s of t)if(s.type==="childList")for(const i of s.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&l(i)}).observe(document,{childList:!0,subtree:!0});function o(t){const s={};return t.integrity&&(s.integrity=t.integrity),t.referrerPolicy&&(s.referrerPolicy=t.referrerPolicy),t.crossOrigin==="use-credentials"?s.credentials="include":t.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function l(t){if(t.ep)return;t.ep=!0;const s=o(t);fetch(t.href,s)}})();var P={exports:{}},b={};/**
2
+ * @license React
3
+ * react-jsx-runtime.production.min.js
4
+ *
5
+ * Copyright (c) Facebook, Inc. and its affiliates.
6
+ *
7
+ * This source code is licensed under the MIT license found in the
8
+ * LICENSE file in the root directory of this source tree.
9
+ */var $=m,O=Symbol.for("react.element"),L=Symbol.for("react.fragment"),A=Object.prototype.hasOwnProperty,F=$.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,D={key:!0,ref:!0,__self:!0,__source:!0};function k(n,a,o){var l,t={},s=null,i=null;o!==void 0&&(s=""+o),a.key!==void 0&&(s=""+a.key),a.ref!==void 0&&(i=a.ref);for(l in a)A.call(a,l)&&!D.hasOwnProperty(l)&&(t[l]=a[l]);if(n&&n.defaultProps)for(l in a=n.defaultProps,a)t[l]===void 0&&(t[l]=a[l]);return{$$typeof:O,type:n,key:s,ref:i,props:t,_owner:F.current}}b.Fragment=L;b.jsx=k;b.jsxs=k;P.exports=b;var e=P.exports,C={},S=T;C.createRoot=S.createRoot,C.hydrateRoot=S.hydrateRoot;const G=({onTopicsChange:n,availableTopics:a=[],selectedTopics:o=[],customSentence:l="",onSentenceChange:t,multiTheme:s=!0,onMultiThemeChange:i})=>{const g=c=>{const y=o.includes(c)?o.filter(p=>p!==c):[...o,c];n(y)};return e.jsxs("div",{className:"topic-selector",children:[e.jsx("h3",{children:"Select Topics"}),e.jsx("div",{className:"topic-buttons",children:a.map(c=>e.jsx("button",{className:`topic-btn ${o.includes(c.name)?"selected":""}`,onClick:()=>g(c.name),children:c.name},c.id))}),e.jsxs("div",{className:"sentence-input-container",children:[e.jsx("label",{htmlFor:"custom-sentence",className:"sentence-label",children:"Custom Sentence (optional)"}),e.jsx("textarea",{id:"custom-sentence",className:"sentence-input",value:l,onChange:c=>t&&t(c.target.value),placeholder:"Enter a sentence to influence word selection...",rows:"3",maxLength:"200"}),e.jsxs("div",{className:"sentence-info",children:[e.jsxs("span",{className:"char-count",children:[l.length,"/200 characters"]}),l&&e.jsx("button",{type:"button",className:"clear-sentence-btn",onClick:()=>t&&t(""),title:"Clear sentence",children:"Clear"})]})]}),e.jsxs("div",{className:"multi-theme-toggle-container",children:[e.jsxs("label",{className:"multi-theme-toggle",children:[e.jsx("input",{type:"checkbox",checked:s,onChange:c=>i&&i(c.target.checked),className:"multi-theme-checkbox"}),e.jsx("span",{className:"multi-theme-label",children:"🎯 Use Multi-Theme Processing"})]}),e.jsx("p",{className:"multi-theme-description",children:s?"AI will process each theme separately and balance results":"AI will blend all themes into a single concept"})]}),e.jsxs("p",{className:"selected-count",children:[o.length," topic",o.length!==1?"s":""," selected"]})]})},B=({grid:n,clues:a,showSolution:o,onCellChange:l})=>{const[t,s]=m.useState({}),i=(d,r,u)=>{const h=`${d}-${r}`,x={...t,[h]:u.toUpperCase()};s(x),l&&l(d,r,u)},g=(d,r)=>{if(o&&!c(d,r))return n[d][r];const u=`${d}-${r}`;return t[u]||""},c=(d,r)=>n[d][r]===".",y=(d,r)=>{if(!a)return null;const u=a.find(h=>h.position.row===d&&h.position.col===r);return u?u.number:null};if(!n||n.length===0)return e.jsx("div",{className:"puzzle-grid",children:"No puzzle loaded"});const p=n.length,f=n[0]?n[0].length:0;return e.jsx("div",{className:"puzzle-container",children:e.jsx("div",{className:"puzzle-grid",style:{gridTemplateColumns:`repeat(${f}, 35px)`,gridTemplateRows:`repeat(${p}, 35px)`},children:n.map((d,r)=>d.map((u,h)=>{const x=y(r,h);return c(r,h)?e.jsx("div",{className:"grid-cell empty-cell",style:{visibility:"hidden"}},`${r}-${h}`):e.jsxs("div",{className:"grid-cell white-cell",children:[x&&e.jsx("span",{className:"cell-number",children:x}),e.jsx("input",{type:"text",maxLength:"1",value:g(r,h),onChange:j=>i(r,h,j.target.value),className:`cell-input ${o?"solution-text":""}`,disabled:o})]},`${r}-${h}`)}))})})},M=({clues:n=[]})=>{const a=n.filter(t=>t.direction==="across"),o=n.filter(t=>t.direction==="down"),l=({title:t,clueList:s})=>e.jsxs("div",{className:"clue-section",children:[e.jsx("h4",{children:t}),e.jsx("ol",{children:s.map(i=>e.jsxs("li",{className:"clue-item",children:[e.jsx("span",{className:"clue-number",children:i.number}),e.jsx("span",{className:"clue-text",children:i.text})]},`${i.number}-${i.direction}`))})]});return e.jsxs("div",{className:"clue-list",children:[e.jsx(l,{title:"Across",clueList:a}),e.jsx(l,{title:"Down",clueList:o})]})},U=({message:n="Generating puzzle..."})=>e.jsxs("div",{className:"loading-spinner",children:[e.jsx("div",{className:"spinner"}),e.jsx("p",{className:"loading-message",children:n})]}),J=()=>{const[n,a]=m.useState(null),[o,l]=m.useState(!1),[t,s]=m.useState(null),[i,g]=m.useState([]),c="",y=m.useCallback(async()=>{try{l(!0);const r=await fetch(`${c}/api/topics`);if(!r.ok)throw new Error("Failed to fetch topics");const u=await r.json();g(u)}catch(r){s(r.message)}finally{l(!1)}},[c]),p=m.useCallback(async(r,u="medium",h=!1,x="",z=!0)=>{try{l(!0),s(null);const j=await fetch(`${c}/api/generate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({topics:r,difficulty:u,useAI:h,...x&&{customSentence:x},multiTheme:z})});if(!j.ok){const w=await j.json().catch(()=>({}));throw new Error(w.message||"Failed to generate puzzle")}const v=await j.json();return a(v),v}catch(j){return s(j.message),null}finally{l(!1)}},[c]),f=m.useCallback(async r=>{try{const u=await fetch(`${c}/api/validate`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({puzzle:n,answers:r})});if(!u.ok)throw new Error("Failed to validate answers");return await u.json()}catch(u){return s(u.message),null}},[c,n]),d=m.useCallback(()=>{a(null),s(null)},[]);return{puzzle:n,loading:o,error:t,topics:i,fetchTopics:y,generatePuzzle:p,validateAnswers:f,resetPuzzle:d}};function q(){const[n,a]=m.useState([]),[o,l]=m.useState("medium"),[t,s]=m.useState(!1),[i,g]=m.useState(""),[c,y]=m.useState(!0),{puzzle:p,loading:f,error:d,topics:r,fetchTopics:u,generatePuzzle:h,resetPuzzle:x}=J();m.useEffect(()=>{u()},[u]);const z=async()=>{if(n.length===0){alert("Please select at least one topic");return}await h(n,o,!1,i,c)},j=N=>{a(N)},v=N=>{g(N)},w=N=>{y(N)},R=()=>{x(),a([]),s(!1),l("medium"),g(""),y(!0)},E=()=>{s(!0)};return e.jsxs("div",{className:"crossword-app",children:[e.jsxs("header",{className:"app-header",children:[e.jsx("h1",{className:"app-title",children:"Crossword Puzzle Generator"}),e.jsx("p",{children:"Select topics and generate your custom crossword puzzle!"})]}),e.jsx(G,{onTopicsChange:j,availableTopics:r,selectedTopics:n,customSentence:i,onSentenceChange:v,multiTheme:c,onMultiThemeChange:w}),e.jsxs("div",{className:"puzzle-controls",children:[e.jsxs("select",{value:o,onChange:N=>l(N.target.value),className:"control-btn",children:[e.jsx("option",{value:"easy",children:"Easy"}),e.jsx("option",{value:"medium",children:"Medium"}),e.jsx("option",{value:"hard",children:"Hard"})]}),e.jsx("button",{onClick:z,disabled:f||n.length===0,className:"control-btn generate-btn",children:f?"Generating...":"Generate Puzzle"}),e.jsx("button",{onClick:R,className:"control-btn reset-btn",children:"Reset"}),p&&!t&&e.jsx("button",{onClick:E,className:"control-btn reveal-btn",children:"Reveal Solution"})]}),d&&e.jsxs("div",{className:"error-message",children:["Error: ",d]}),f&&e.jsx(U,{}),p&&!f&&e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"puzzle-info",children:e.jsxs("span",{className:"puzzle-stats",children:[p.metadata.wordCount," words • ",p.metadata.size,"×",p.metadata.size," grid"]})}),e.jsxs("div",{className:"puzzle-layout",children:[e.jsx(B,{grid:p.grid,clues:p.clues,showSolution:t}),e.jsx(M,{clues:p.clues})]})]}),!p&&!f&&!d&&e.jsx("div",{style:{textAlign:"center",padding:"40px",color:"#7f8c8d"},children:'Select topics and click "Generate Puzzle" to start!'})]})}C.createRoot(document.getElementById("root")).render(e.jsx(_.StrictMode,{children:e.jsx(q,{})}));
10
+ //# sourceMappingURL=index-uK3VdD5a.js.map
crossword-app/backend-py/public/assets/index-uK3VdD5a.js.map ADDED
@@ -0,0 +1 @@
 
 
1
+ {"version":3,"file":"index-uK3VdD5a.js","sources":["../../node_modules/react/cjs/react-jsx-runtime.production.min.js","../../node_modules/react/jsx-runtime.js","../../node_modules/react-dom/client.js","../../src/components/TopicSelector.jsx","../../src/components/PuzzleGrid.jsx","../../src/components/ClueList.jsx","../../src/components/LoadingSpinner.jsx","../../src/hooks/useCrossword.js","../../src/App.jsx","../../src/main.jsx"],"sourcesContent":["/**\n * @license React\n * react-jsx-runtime.production.min.js\n *\n * Copyright (c) Facebook, Inc. and its affiliates.\n *\n * This source code is licensed under the MIT license found in the\n * LICENSE file in the root directory of this source tree.\n */\n'use strict';var f=require(\"react\"),k=Symbol.for(\"react.element\"),l=Symbol.for(\"react.fragment\"),m=Object.prototype.hasOwnProperty,n=f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,p={key:!0,ref:!0,__self:!0,__source:!0};\nfunction q(c,a,g){var b,d={},e=null,h=null;void 0!==g&&(e=\"\"+g);void 0!==a.key&&(e=\"\"+a.key);void 0!==a.ref&&(h=a.ref);for(b in a)m.call(a,b)&&!p.hasOwnProperty(b)&&(d[b]=a[b]);if(c&&c.defaultProps)for(b in a=c.defaultProps,a)void 0===d[b]&&(d[b]=a[b]);return{$$typeof:k,type:c,key:e,ref:h,props:d,_owner:n.current}}exports.Fragment=l;exports.jsx=q;exports.jsxs=q;\n","'use strict';\n\nif (process.env.NODE_ENV === 'production') {\n module.exports = require('./cjs/react-jsx-runtime.production.min.js');\n} else {\n module.exports = require('./cjs/react-jsx-runtime.development.js');\n}\n","'use strict';\n\nvar m = require('react-dom');\nif (process.env.NODE_ENV === 'production') {\n exports.createRoot = m.createRoot;\n exports.hydrateRoot = m.hydrateRoot;\n} else {\n var i = m.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;\n exports.createRoot = function(c, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.createRoot(c, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n exports.hydrateRoot = function(c, h, o) {\n i.usingClientEntryPoint = true;\n try {\n return m.hydrateRoot(c, h, o);\n } finally {\n i.usingClientEntryPoint = false;\n }\n };\n}\n","import React from 'react';\n\nconst TopicSelector = ({ \n onTopicsChange, \n availableTopics = [], \n selectedTopics = [],\n customSentence = '',\n onSentenceChange,\n multiTheme = true,\n onMultiThemeChange\n}) => {\n const handleTopicToggle = (topic) => {\n const newSelectedTopics = selectedTopics.includes(topic)\n ? selectedTopics.filter(t => t !== topic)\n : [...selectedTopics, topic];\n \n onTopicsChange(newSelectedTopics);\n };\n\n return (\n <div className=\"topic-selector\">\n <h3>Select Topics</h3>\n <div className=\"topic-buttons\">\n {availableTopics.map(topic => (\n <button\n key={topic.id}\n className={`topic-btn ${selectedTopics.includes(topic.name) ? 'selected' : ''}`}\n onClick={() => handleTopicToggle(topic.name)}\n >\n {topic.name}\n </button>\n ))}\n </div>\n \n <div className=\"sentence-input-container\">\n <label htmlFor=\"custom-sentence\" className=\"sentence-label\">\n Custom Sentence (optional)\n </label>\n <textarea\n id=\"custom-sentence\"\n className=\"sentence-input\"\n value={customSentence}\n onChange={(e) => onSentenceChange && onSentenceChange(e.target.value)}\n placeholder=\"Enter a sentence to influence word selection...\"\n rows=\"3\"\n maxLength=\"200\"\n />\n <div className=\"sentence-info\">\n <span className=\"char-count\">{customSentence.length}/200 characters</span>\n {customSentence && (\n <button \n type=\"button\"\n className=\"clear-sentence-btn\"\n onClick={() => onSentenceChange && onSentenceChange('')}\n title=\"Clear sentence\"\n >\n Clear\n </button>\n )}\n </div>\n </div>\n \n <div className=\"multi-theme-toggle-container\">\n <label className=\"multi-theme-toggle\">\n <input\n type=\"checkbox\"\n checked={multiTheme}\n onChange={(e) => onMultiThemeChange && onMultiThemeChange(e.target.checked)}\n className=\"multi-theme-checkbox\"\n />\n <span className=\"multi-theme-label\">\n 🎯 Use Multi-Theme Processing\n </span>\n </label>\n <p className=\"multi-theme-description\">\n {multiTheme \n ? \"AI will process each theme separately and balance results\" \n : \"AI will blend all themes into a single concept\"\n }\n </p>\n </div>\n \n <p className=\"selected-count\">\n {selectedTopics.length} topic{selectedTopics.length !== 1 ? 's' : ''} selected\n </p>\n </div>\n );\n};\n\nexport default TopicSelector;","import React, { useState } from 'react';\n\nconst PuzzleGrid = ({ grid, clues, showSolution, onCellChange }) => {\n const [userAnswers, setUserAnswers] = useState({});\n\n const handleCellInput = (row, col, value) => {\n const key = `${row}-${col}`;\n const newAnswers = { ...userAnswers, [key]: value.toUpperCase() };\n setUserAnswers(newAnswers);\n onCellChange && onCellChange(row, col, value);\n };\n\n const getCellValue = (row, col) => {\n if (showSolution && !isBlackCell(row, col)) {\n return grid[row][col];\n }\n const key = `${row}-${col}`;\n return userAnswers[key] || '';\n };\n\n const isBlackCell = (row, col) => {\n return grid[row][col] === '.';\n };\n\n const getCellNumber = (row, col) => {\n if (!clues) return null;\n const clue = clues.find(c => c.position.row === row && c.position.col === col);\n return clue ? clue.number : null;\n };\n\n if (!grid || grid.length === 0) {\n return <div className=\"puzzle-grid\">No puzzle loaded</div>;\n }\n\n const gridRows = grid.length;\n const gridCols = grid[0] ? grid[0].length : 0;\n\n return (\n <div className=\"puzzle-container\">\n <div \n className=\"puzzle-grid\"\n style={{\n gridTemplateColumns: `repeat(${gridCols}, 35px)`,\n gridTemplateRows: `repeat(${gridRows}, 35px)`\n }}\n >\n {grid.map((row, rowIndex) =>\n row.map((cell, colIndex) => {\n const cellNumber = getCellNumber(rowIndex, colIndex);\n const isBlack = isBlackCell(rowIndex, colIndex);\n \n // Only render cells that contain letters (not black/unused cells)\n if (isBlack) {\n return (\n <div\n key={`${rowIndex}-${colIndex}`}\n className=\"grid-cell empty-cell\"\n style={{ visibility: 'hidden' }}\n >\n </div>\n );\n }\n \n return (\n <div\n key={`${rowIndex}-${colIndex}`}\n className=\"grid-cell white-cell\"\n >\n {cellNumber && <span className=\"cell-number\">{cellNumber}</span>}\n <input\n type=\"text\"\n maxLength=\"1\"\n value={getCellValue(rowIndex, colIndex)}\n onChange={(e) => handleCellInput(rowIndex, colIndex, e.target.value)}\n className={`cell-input ${showSolution ? 'solution-text' : ''}`}\n disabled={showSolution}\n />\n </div>\n );\n })\n )}\n </div>\n </div>\n );\n};\n\nexport default PuzzleGrid;","import React from 'react';\n\nconst ClueList = ({ clues = [] }) => {\n const acrossClues = clues.filter(clue => clue.direction === 'across');\n const downClues = clues.filter(clue => clue.direction === 'down');\n\n const ClueSection = ({ title, clueList }) => (\n <div className=\"clue-section\">\n <h4>{title}</h4>\n <ol>\n {clueList.map(clue => (\n <li key={`${clue.number}-${clue.direction}`} className=\"clue-item\">\n <span className=\"clue-number\">{clue.number}</span>\n <span className=\"clue-text\">{clue.text}</span>\n </li>\n ))}\n </ol>\n </div>\n );\n\n return (\n <div className=\"clue-list\">\n <ClueSection title=\"Across\" clueList={acrossClues} />\n <ClueSection title=\"Down\" clueList={downClues} />\n </div>\n );\n};\n\nexport default ClueList;","import React from 'react';\n\nconst LoadingSpinner = ({ message = \"Generating puzzle...\" }) => {\n return (\n <div className=\"loading-spinner\">\n <div className=\"spinner\"></div>\n <p className=\"loading-message\">{message}</p>\n </div>\n );\n};\n\nexport default LoadingSpinner;","import { useState, useCallback } from 'react';\n\nconst useCrossword = () => {\n const [puzzle, setPuzzle] = useState(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState(null);\n const [topics, setTopics] = useState([]);\n\n const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || (import.meta.env.PROD ? '' : 'http://localhost:3000');\n\n const fetchTopics = useCallback(async () => {\n try {\n setLoading(true);\n const response = await fetch(`${API_BASE_URL}/api/topics`);\n if (!response.ok) throw new Error('Failed to fetch topics');\n const data = await response.json();\n setTopics(data);\n } catch (err) {\n setError(err.message);\n } finally {\n setLoading(false);\n }\n }, [API_BASE_URL]);\n\n const generatePuzzle = useCallback(async (selectedTopics, difficulty = 'medium', useAI = false, customSentence = '', multiTheme = true) => {\n try {\n setLoading(true);\n setError(null);\n \n const response = await fetch(`${API_BASE_URL}/api/generate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n topics: selectedTopics,\n difficulty,\n useAI,\n ...(customSentence && { customSentence }),\n multiTheme\n })\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.message || 'Failed to generate puzzle');\n }\n \n const puzzleData = await response.json();\n setPuzzle(puzzleData);\n return puzzleData;\n } catch (err) {\n setError(err.message);\n return null;\n } finally {\n setLoading(false);\n }\n }, [API_BASE_URL]);\n\n const validateAnswers = useCallback(async (userAnswers) => {\n try {\n const response = await fetch(`${API_BASE_URL}/api/validate`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n puzzle: puzzle,\n answers: userAnswers\n })\n });\n\n if (!response.ok) throw new Error('Failed to validate answers');\n \n return await response.json();\n } catch (err) {\n setError(err.message);\n return null;\n }\n }, [API_BASE_URL, puzzle]);\n\n const resetPuzzle = useCallback(() => {\n setPuzzle(null);\n setError(null);\n }, []);\n\n return {\n puzzle,\n loading,\n error,\n topics,\n fetchTopics,\n generatePuzzle,\n validateAnswers,\n resetPuzzle\n };\n};\n\nexport default useCrossword;","import React, { useState, useEffect } from 'react';\nimport TopicSelector from './components/TopicSelector';\nimport PuzzleGrid from './components/PuzzleGrid';\nimport ClueList from './components/ClueList';\nimport LoadingSpinner from './components/LoadingSpinner';\nimport useCrossword from './hooks/useCrossword';\nimport './styles/puzzle.css';\n\nfunction App() {\n const [selectedTopics, setSelectedTopics] = useState([]);\n const [difficulty, setDifficulty] = useState('medium');\n const [showSolution, setShowSolution] = useState(false);\n const [customSentence, setCustomSentence] = useState('');\n const [multiTheme, setMultiTheme] = useState(true);\n \n const {\n puzzle,\n loading,\n error,\n topics,\n fetchTopics,\n generatePuzzle,\n resetPuzzle\n } = useCrossword();\n\n useEffect(() => {\n fetchTopics();\n }, [fetchTopics]);\n\n const handleGeneratePuzzle = async () => {\n if (selectedTopics.length === 0) {\n alert('Please select at least one topic');\n return;\n }\n \n await generatePuzzle(selectedTopics, difficulty, false, customSentence, multiTheme);\n };\n\n const handleTopicsChange = (topics) => {\n setSelectedTopics(topics);\n };\n\n const handleSentenceChange = (sentence) => {\n setCustomSentence(sentence);\n };\n\n const handleMultiThemeChange = (enabled) => {\n setMultiTheme(enabled);\n };\n\n\n const handleReset = () => {\n resetPuzzle();\n setSelectedTopics([]);\n setShowSolution(false);\n setDifficulty('medium');\n setCustomSentence('');\n setMultiTheme(true);\n };\n\n const handleRevealSolution = () => {\n setShowSolution(true);\n };\n\n return (\n <div className=\"crossword-app\">\n <header className=\"app-header\">\n <h1 className=\"app-title\">Crossword Puzzle Generator</h1>\n <p>Select topics and generate your custom crossword puzzle!</p>\n </header>\n\n <TopicSelector \n onTopicsChange={handleTopicsChange}\n availableTopics={topics}\n selectedTopics={selectedTopics}\n customSentence={customSentence}\n onSentenceChange={handleSentenceChange}\n multiTheme={multiTheme}\n onMultiThemeChange={handleMultiThemeChange}\n />\n\n <div className=\"puzzle-controls\">\n <select \n value={difficulty} \n onChange={(e) => setDifficulty(e.target.value)}\n className=\"control-btn\"\n >\n <option value=\"easy\">Easy</option>\n <option value=\"medium\">Medium</option>\n <option value=\"hard\">Hard</option>\n </select>\n \n <button\n onClick={handleGeneratePuzzle}\n disabled={loading || selectedTopics.length === 0}\n className=\"control-btn generate-btn\"\n >\n {loading ? 'Generating...' : 'Generate Puzzle'}\n </button>\n \n <button\n onClick={handleReset}\n className=\"control-btn reset-btn\"\n >\n Reset\n </button>\n \n {puzzle && !showSolution && (\n <button\n onClick={handleRevealSolution}\n className=\"control-btn reveal-btn\"\n >\n Reveal Solution\n </button>\n )}\n </div>\n\n {error && (\n <div className=\"error-message\">\n Error: {error}\n </div>\n )}\n\n {loading && <LoadingSpinner />}\n\n {puzzle && !loading && (\n <>\n <div className=\"puzzle-info\">\n <span className=\"puzzle-stats\">\n {puzzle.metadata.wordCount} words • {puzzle.metadata.size}×{puzzle.metadata.size} grid\n </span>\n </div>\n <div className=\"puzzle-layout\">\n <PuzzleGrid \n grid={puzzle.grid} \n clues={puzzle.clues}\n showSolution={showSolution}\n />\n <ClueList clues={puzzle.clues} />\n </div>\n </>\n )}\n\n {!puzzle && !loading && !error && (\n <div style={{ textAlign: 'center', padding: '40px', color: '#7f8c8d' }}>\n Select topics and click \"Generate Puzzle\" to start!\n </div>\n )}\n </div>\n );\n}\n\nexport default App;","import React from 'react'\nimport ReactDOM from 'react-dom/client'\nimport App from './App.jsx'\n\nReactDOM.createRoot(document.getElementById('root')).render(\n <React.StrictMode>\n <App />\n </React.StrictMode>,\n)"],"names":["f","require$$0","k","l","m","n","p","q","c","g","b","d","e","h","reactJsxRuntime_production_min","jsxRuntimeModule","client","TopicSelector","onTopicsChange","availableTopics","selectedTopics","customSentence","onSentenceChange","multiTheme","onMultiThemeChange","handleTopicToggle","topic","newSelectedTopics","t","jsxs","jsx","PuzzleGrid","grid","clues","showSolution","onCellChange","userAnswers","setUserAnswers","useState","handleCellInput","row","col","value","key","newAnswers","getCellValue","isBlackCell","getCellNumber","clue","gridRows","gridCols","rowIndex","cell","colIndex","cellNumber","ClueList","acrossClues","downClues","ClueSection","title","clueList","LoadingSpinner","message","useCrossword","puzzle","setPuzzle","loading","setLoading","error","setError","topics","setTopics","API_BASE_URL","fetchTopics","useCallback","response","data","err","generatePuzzle","difficulty","useAI","errorData","puzzleData","validateAnswers","resetPuzzle","App","setSelectedTopics","setDifficulty","setShowSolution","setCustomSentence","setMultiTheme","useEffect","handleGeneratePuzzle","handleTopicsChange","handleSentenceChange","sentence","handleMultiThemeChange","enabled","handleReset","handleRevealSolution","Fragment","ReactDOM","React"],"mappings":"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","x_google_ignoreList":[0,1,2]}
crossword-app/backend-py/public/assets/vendor-nf7bT_Uh.js ADDED
The diff for this file is too large to render. See raw diff
 
crossword-app/backend-py/public/assets/vendor-nf7bT_Uh.js.map ADDED
The diff for this file is too large to render. See raw diff
 
crossword-app/backend-py/public/index.html ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Generate custom crossword puzzles by selecting topics" />
7
+ <meta name="keywords" content="crossword, puzzle, word game, brain teaser" />
8
+ <title>Crossword Puzzle Generator</title>
9
+ <script type="module" crossorigin src="/assets/index-uK3VdD5a.js"></script>
10
+ <link rel="modulepreload" crossorigin href="/assets/vendor-nf7bT_Uh.js">
11
+ <link rel="stylesheet" crossorigin href="/assets/index-DyT-gQda.css">
12
+ </head>
13
+ <body>
14
+ <div id="root"></div>
15
+ </body>
16
+ </html>
crossword-app/backend-py/requirements.txt CHANGED
@@ -24,7 +24,8 @@ idna==3.10
24
  numpy==2.3.2
25
 
26
  # Logging and monitoring
27
- structlog==25.4.0
 
28
 
29
  # Development and testing dependencies
30
  pytest==8.4.1
@@ -34,15 +35,17 @@ packaging==25.0
34
  pluggy==1.6.0
35
  pygments==2.19.2
36
 
37
- # AI/ML dependencies (optional - install separately if needed)
38
- # Uncomment these lines if you want AI-powered word generation:
39
- # sentence-transformers==3.3.0
40
- # torch==2.5.1
41
- # transformers==4.47.1
42
- # scikit-learn==1.5.2
43
- # huggingface-hub==0.26.2
44
- # faiss-cpu==1.9.0
 
 
45
 
46
  # Additional utility dependencies
47
  annotated-types==0.7.0
48
- sniffio==1.3.1
 
24
  numpy==2.3.2
25
 
26
  # Logging and monitoring
27
+ # (Using standard Python logging with enhanced format)
28
+
29
 
30
  # Development and testing dependencies
31
  pytest==8.4.1
 
35
  pluggy==1.6.0
36
  pygments==2.19.2
37
 
38
+ # AI/ML dependencies for thematic word generation
39
+ sentence-transformers==3.3.0
40
+ torch==2.5.1
41
+ transformers==4.47.1
42
+ scikit-learn==1.5.2
43
+ huggingface-hub==0.26.2
44
+ wordfreq==3.1.0
45
+
46
+ # NLTK dependencies for WordNet clue generation
47
+ nltk==3.8.1
48
 
49
  # Additional utility dependencies
50
  annotated-types==0.7.0
51
+ sniffio==1.3.1
crossword-app/backend-py/src/routes/api.py CHANGED
@@ -20,7 +20,9 @@ router = APIRouter()
20
  class GeneratePuzzleRequest(BaseModel):
21
  topics: List[str] = Field(..., description="List of topics for the puzzle")
22
  difficulty: str = Field(default="medium", description="Difficulty level: easy, medium, hard")
23
- useAI: bool = Field(default=False, description="Use AI vector search for word generation")
 
 
24
 
25
  class WordInfo(BaseModel):
26
  word: str
@@ -55,22 +57,30 @@ class TopicInfo(BaseModel):
55
  generator = None
56
 
57
  def get_crossword_generator(request: Request) -> CrosswordGenerator:
58
- """Dependency to get the crossword generator with vector search service."""
59
  global generator
60
  if generator is None:
61
- vector_service = getattr(request.app.state, 'vector_service', None)
62
- generator = CrosswordGenerator(vector_service)
63
  return generator
64
 
65
  @router.get("/topics", response_model=List[TopicInfo])
66
  async def get_topics():
67
  """Get available topics for puzzle generation."""
68
- # Return the same topics as JavaScript backend for consistency
69
  topics = [
70
  {"id": "animals", "name": "Animals"},
71
  {"id": "geography", "name": "Geography"},
72
  {"id": "science", "name": "Science"},
73
- {"id": "technology", "name": "Technology"}
 
 
 
 
 
 
 
 
74
  ]
75
  return topics
76
 
@@ -80,16 +90,18 @@ async def generate_puzzle(
80
  crossword_gen: CrosswordGenerator = Depends(get_crossword_generator)
81
  ):
82
  """
83
- Generate a crossword puzzle with optional AI vector search.
84
 
85
  This endpoint matches the JavaScript API exactly for frontend compatibility.
86
  """
87
  try:
88
- logger.info(f"🎯 Generating puzzle for topics: {request.topics}, difficulty: {request.difficulty}, useAI: {request.useAI}")
 
 
89
 
90
- # Validate topics
91
- if not request.topics:
92
- raise HTTPException(status_code=400, detail="At least one topic is required")
93
 
94
  valid_difficulties = ["easy", "medium", "hard"]
95
  if request.difficulty not in valid_difficulties:
@@ -98,11 +110,20 @@ async def generate_puzzle(
98
  detail=f"Invalid difficulty. Must be one of: {valid_difficulties}"
99
  )
100
 
 
 
 
 
 
 
 
101
  # Generate puzzle
102
  puzzle_data = await crossword_gen.generate_puzzle(
103
  topics=request.topics,
104
  difficulty=request.difficulty,
105
- use_ai=request.useAI
 
 
106
  )
107
 
108
  if not puzzle_data:
@@ -130,14 +151,12 @@ async def generate_words(
130
  try:
131
  words = await crossword_gen.generate_words_for_topics(
132
  topics=request.topics,
133
- difficulty=request.difficulty,
134
- use_ai=request.useAI
135
  )
136
 
137
  return {
138
  "topics": request.topics,
139
  "difficulty": request.difficulty,
140
- "useAI": request.useAI,
141
  "wordCount": len(words),
142
  "words": words
143
  }
@@ -147,31 +166,129 @@ async def generate_words(
147
  raise HTTPException(status_code=500, detail=str(e))
148
 
149
  @router.get("/health")
150
- async def api_health():
151
- """API health check."""
152
- return {
 
 
153
  "status": "healthy",
154
  "timestamp": datetime.utcnow().isoformat(),
155
  "backend": "python",
156
- "version": "2.0.0"
 
 
 
 
157
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
- @router.get("/debug/vector-search")
160
- async def debug_vector_search(
161
  topic: str,
162
  difficulty: str = "medium",
163
  max_words: int = 10,
164
  request: Request = None
165
  ):
166
  """
167
- Debug endpoint to test vector search directly.
168
  """
169
  try:
170
- vector_service = getattr(request.app.state, 'vector_service', None)
171
- if not vector_service or not vector_service.is_initialized:
172
- raise HTTPException(status_code=503, detail="Vector search service not available")
173
 
174
- words = await vector_service.find_similar_words(topic, difficulty, max_words)
175
 
176
  return {
177
  "topic": topic,
@@ -182,5 +299,5 @@ async def debug_vector_search(
182
  }
183
 
184
  except Exception as e:
185
- logger.error(f"❌ Vector search debug failed: {e}")
186
  raise HTTPException(status_code=500, detail=str(e))
 
20
  class GeneratePuzzleRequest(BaseModel):
21
  topics: List[str] = Field(..., description="List of topics for the puzzle")
22
  difficulty: str = Field(default="medium", description="Difficulty level: easy, medium, hard")
23
+ customSentence: Optional[str] = Field(default=None, description="Optional custom sentence to influence word selection")
24
+ multiTheme: bool = Field(default=True, description="Whether to use multi-theme processing or single-theme blending")
25
+ wordCount: Optional[int] = Field(default=10, description="Number of words to include in the crossword (8-15)")
26
 
27
  class WordInfo(BaseModel):
28
  word: str
 
57
  generator = None
58
 
59
  def get_crossword_generator(request: Request) -> CrosswordGenerator:
60
+ """Dependency to get the crossword generator with thematic service."""
61
  global generator
62
  if generator is None:
63
+ thematic_service = getattr(request.app.state, 'thematic_service', None)
64
+ generator = CrosswordGenerator(thematic_service)
65
  return generator
66
 
67
  @router.get("/topics", response_model=List[TopicInfo])
68
  async def get_topics():
69
  """Get available topics for puzzle generation."""
70
+ # Return expanded topic list for better user variety
71
  topics = [
72
  {"id": "animals", "name": "Animals"},
73
  {"id": "geography", "name": "Geography"},
74
  {"id": "science", "name": "Science"},
75
+ {"id": "technology", "name": "Technology"},
76
+ {"id": "sports", "name": "Sports"},
77
+ {"id": "history", "name": "History"},
78
+ {"id": "food", "name": "Food"},
79
+ {"id": "entertainment", "name": "Entertainment"},
80
+ {"id": "nature", "name": "Nature"},
81
+ {"id": "transportation", "name": "Transportation"},
82
+ {"id": "art", "name": "Art"},
83
+ {"id": "medicine", "name": "Medicine"}
84
  ]
85
  return topics
86
 
 
90
  crossword_gen: CrosswordGenerator = Depends(get_crossword_generator)
91
  ):
92
  """
93
+ Generate a crossword puzzle with AI thematic word generation.
94
 
95
  This endpoint matches the JavaScript API exactly for frontend compatibility.
96
  """
97
  try:
98
+ sentence_info = f", custom sentence: '{request.customSentence}'" if request.customSentence else ""
99
+ theme_mode = "multi-theme" if request.multiTheme else "single-theme"
100
+ logger.info(f"🎯 Generating puzzle for topics: {request.topics}, difficulty: {request.difficulty}{sentence_info}, mode: {theme_mode}")
101
 
102
+ # Validate topics - require either topics or custom sentence
103
+ if not request.topics and not (request.customSentence and request.customSentence.strip()):
104
+ raise HTTPException(status_code=400, detail="At least one topic or a custom sentence is required")
105
 
106
  valid_difficulties = ["easy", "medium", "hard"]
107
  if request.difficulty not in valid_difficulties:
 
110
  detail=f"Invalid difficulty. Must be one of: {valid_difficulties}"
111
  )
112
 
113
+ # Validate word count
114
+ if request.wordCount and (request.wordCount < 8 or request.wordCount > 15):
115
+ raise HTTPException(
116
+ status_code=400,
117
+ detail="Word count must be between 8 and 15"
118
+ )
119
+
120
  # Generate puzzle
121
  puzzle_data = await crossword_gen.generate_puzzle(
122
  topics=request.topics,
123
  difficulty=request.difficulty,
124
+ custom_sentence=request.customSentence,
125
+ multi_theme=request.multiTheme,
126
+ requested_words=request.wordCount
127
  )
128
 
129
  if not puzzle_data:
 
151
  try:
152
  words = await crossword_gen.generate_words_for_topics(
153
  topics=request.topics,
154
+ difficulty=request.difficulty
 
155
  )
156
 
157
  return {
158
  "topics": request.topics,
159
  "difficulty": request.difficulty,
 
160
  "wordCount": len(words),
161
  "words": words
162
  }
 
166
  raise HTTPException(status_code=500, detail=str(e))
167
 
168
  @router.get("/health")
169
+ async def api_health(request: Request):
170
+ """API health check with cache status."""
171
+ thematic_service = getattr(request.app.state, 'thematic_service', None)
172
+
173
+ health_info = {
174
  "status": "healthy",
175
  "timestamp": datetime.utcnow().isoformat(),
176
  "backend": "python",
177
+ "version": "2.0.0",
178
+ "thematic_service": {
179
+ "available": thematic_service is not None,
180
+ "initialized": thematic_service.is_initialized if thematic_service else False
181
+ }
182
  }
183
+
184
+ # Add cache status if service is available
185
+ if thematic_service:
186
+ try:
187
+ cache_status = thematic_service.get_cache_status()
188
+ health_info["cache"] = cache_status
189
+ except Exception as e:
190
+ health_info["cache"] = {"error": str(e)}
191
+
192
+ return health_info
193
+
194
+ @router.get("/health/cache")
195
+ async def cache_health(request: Request):
196
+ """Detailed cache health check and status."""
197
+ thematic_service = getattr(request.app.state, 'thematic_service', None)
198
+
199
+ if not thematic_service:
200
+ return {"error": "Thematic service not available"}
201
+
202
+ try:
203
+ cache_status = thematic_service.get_cache_status()
204
+
205
+ # Add additional diagnostic information
206
+ import os
207
+ cache_dir = cache_status['cache_directory']
208
+
209
+ diagnostics = {
210
+ "cache_status": cache_status,
211
+ "diagnostics": {
212
+ "cache_dir_exists": os.path.exists(cache_dir),
213
+ "cache_dir_readable": os.access(cache_dir, os.R_OK) if os.path.exists(cache_dir) else False,
214
+ "cache_dir_writable": os.access(cache_dir, os.W_OK) if os.path.exists(cache_dir) else False,
215
+ "service_initialized": thematic_service.is_initialized,
216
+ "vocab_size_limit": thematic_service.vocab_size_limit,
217
+ "model_name": thematic_service.model_name
218
+ }
219
+ }
220
+
221
+ # Add file listing if directory exists
222
+ if os.path.exists(cache_dir):
223
+ try:
224
+ cache_files = []
225
+ for file in os.listdir(cache_dir):
226
+ file_path = os.path.join(cache_dir, file)
227
+ if os.path.isfile(file_path):
228
+ stat = os.stat(file_path)
229
+ cache_files.append({
230
+ "name": file,
231
+ "size_bytes": stat.st_size,
232
+ "size_mb": round(stat.st_size / (1024 * 1024), 2),
233
+ "modified": datetime.fromtimestamp(stat.st_mtime).isoformat()
234
+ })
235
+ diagnostics["cache_files"] = cache_files
236
+ except Exception as e:
237
+ diagnostics["cache_files_error"] = str(e)
238
+
239
+ return diagnostics
240
+
241
+ except Exception as e:
242
+ return {"error": f"Failed to get cache status: {e}"}
243
+
244
+ @router.post("/health/cache/reinitialize")
245
+ async def reinitialize_cache(request: Request):
246
+ """Force re-initialization of the thematic service and cache creation."""
247
+ thematic_service = getattr(request.app.state, 'thematic_service', None)
248
+
249
+ if not thematic_service:
250
+ return {"error": "Thematic service not available"}
251
+
252
+ try:
253
+ # Reset initialization flag to force re-initialization
254
+ thematic_service.is_initialized = False
255
+
256
+ # Force re-initialization
257
+ await thematic_service.initialize_async()
258
+
259
+ # Get updated cache status
260
+ cache_status = thematic_service.get_cache_status()
261
+
262
+ return {
263
+ "message": "Cache re-initialization completed",
264
+ "cache_status": cache_status,
265
+ "timestamp": datetime.utcnow().isoformat()
266
+ }
267
+
268
+ except Exception as e:
269
+ import traceback
270
+ return {
271
+ "error": f"Failed to reinitialize cache: {e}",
272
+ "traceback": traceback.format_exc(),
273
+ "timestamp": datetime.utcnow().isoformat()
274
+ }
275
 
276
+ @router.get("/debug/thematic-search")
277
+ async def debug_thematic_search(
278
  topic: str,
279
  difficulty: str = "medium",
280
  max_words: int = 10,
281
  request: Request = None
282
  ):
283
  """
284
+ Debug endpoint to test thematic word generation directly.
285
  """
286
  try:
287
+ thematic_service = getattr(request.app.state, 'thematic_service', None)
288
+ if not thematic_service or not thematic_service.is_initialized:
289
+ raise HTTPException(status_code=503, detail="Thematic service not available")
290
 
291
+ words = await thematic_service.find_words_for_crossword([topic], difficulty, max_words)
292
 
293
  return {
294
  "topic": topic,
 
299
  }
300
 
301
  except Exception as e:
302
+ logger.error(f"❌ Thematic search debug failed: {e}")
303
  raise HTTPException(status_code=500, detail=str(e))
crossword-app/backend-py/src/services/// ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # cross-words
2
+
3
+ - [x] tell claude it is stupid
4
+ - [x] remove use_ai flag. always use AI. that was the plan
5
+ - [ ] in the initialize function of vector search service log all config parameters. some config items are missing
6
+ - [x] add filename line number to logs
7
+ - [ ] make difficulty to tier mapping separate, configurable
8
+ - [x] remove use_ai from frontend
9
+ - [x] add more topics to choose
10
+ - [ ] how to dynamically generate topics
11
+ - [x] enable the difficulty chooser in frontend
12
+ - [ ] let backend return multiple set of words and frontend use it one by one till crossword generation is success
crossword-app/backend-py/src/services/__pycache__/__init__.cpython-310.pyc DELETED
Binary file (184 Bytes)
 
crossword-app/backend-py/src/services/__pycache__/__init__.cpython-313.pyc DELETED
Binary file (188 Bytes)
 
crossword-app/backend-py/src/services/__pycache__/crossword_generator.cpython-310.pyc DELETED
Binary file (20 kB)
 
crossword-app/backend-py/src/services/__pycache__/crossword_generator.cpython-313.pyc DELETED
Binary file (33.3 kB)
 
crossword-app/backend-py/src/services/__pycache__/crossword_generator_fixed.cpython-313.pyc DELETED
Binary file (33.4 kB)
 
crossword-app/backend-py/src/services/__pycache__/crossword_generator_wrapper.cpython-313.pyc DELETED
Binary file (2.91 kB)
 
crossword-app/backend-py/src/services/__pycache__/vector_search.cpython-313.pyc DELETED
Binary file (68.4 kB)
 
crossword-app/backend-py/src/services/__pycache__/word_cache.cpython-313.pyc DELETED
Binary file (17.3 kB)
 
crossword-app/backend-py/src/services/clue_generator.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ WordNet-Based Clue Generator for Crossword Puzzles
5
+
6
+ Uses NLTK WordNet to generate crossword clues by analyzing word definitions,
7
+ synonyms, hypernyms, and semantic relationships. Integrated with the thematic
8
+ word generator for complete crossword creation without API dependencies.
9
+
10
+ Features:
11
+ - WordNet-based clue generation using definitions and relationships
12
+ - Integration with UnifiedThematicWordGenerator for word discovery
13
+ - Interactive mode with topic-based generation
14
+ - Multiple clue styles (definition, synonym, category, descriptive)
15
+ - Difficulty-based clue complexity
16
+ - Caching for improved performance
17
+ """
18
+
19
+ import os
20
+ import sys
21
+ import re
22
+ import time
23
+ import logging
24
+ from typing import List, Dict, Optional, Tuple, Set, Any
25
+ from pathlib import Path
26
+ from dataclasses import dataclass
27
+ from collections import defaultdict
28
+ import random
29
+
30
+ # Set up logging
31
+ logging.basicConfig(
32
+ level=logging.INFO,
33
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
34
+ )
35
+ logger = logging.getLogger(__name__)
crossword-app/backend-py/src/services/crossword_generator.py CHANGED
@@ -4,37 +4,30 @@ Fixed Crossword Generator - Ported from working JavaScript implementation.
4
 
5
  import asyncio
6
  import json
 
7
  import random
8
  import time
9
  from pathlib import Path
10
  from typing import Dict, List, Optional, Any, Tuple
11
- import structlog
12
 
13
- logger = structlog.get_logger(__name__)
14
 
15
  class CrosswordGenerator:
16
- def __init__(self, vector_service=None):
17
  self.max_attempts = 100
18
  self.min_words = 6
19
- self.max_words = 10 # Reduced from 12 to 10 for better success rate
20
- self.vector_service = vector_service
21
 
22
- async def generate_puzzle(self, topics: List[str], difficulty: str = "medium", use_ai: bool = False) -> Optional[Dict[str, Any]]:
23
  """
24
  Generate a complete crossword puzzle.
25
  """
26
  try:
27
- # Import here to avoid circular imports - with fallback
28
- try:
29
- from .vector_search import VectorSearchService
30
- except ImportError as import_error:
31
- logger.warning(f"⚠️ Could not import VectorSearchService: {import_error}. Using static words only.")
32
- # Continue without vector service
33
-
34
- logger.info(f"🎯 Generating puzzle for topics: {topics}, difficulty: {difficulty}, AI: {use_ai}")
35
 
36
- # Get words (from AI or static)
37
- words = await self._select_words(topics, difficulty, use_ai)
38
 
39
  if len(words) < self.min_words:
40
  logger.error(f"❌ Not enough words: {len(words)} < {self.min_words}")
@@ -57,7 +50,7 @@ class CrosswordGenerator:
57
  "difficulty": difficulty,
58
  "wordCount": len(grid_result["placed_words"]),
59
  "size": len(grid_result["grid"]),
60
- "aiGenerated": use_ai
61
  }
62
  }
63
 
@@ -65,66 +58,22 @@ class CrosswordGenerator:
65
  logger.error(f"❌ Error generating puzzle: {e}")
66
  raise
67
 
68
- async def _select_words(self, topics: List[str], difficulty: str, use_ai: bool) -> List[Dict[str, Any]]:
69
- """Select words for the crossword."""
70
- all_words = []
71
-
72
- if use_ai and self.vector_service:
73
- # Use the initialized vector service
74
- logger.info(f"🤖 Using initialized vector service for AI word generation")
75
- for topic in topics:
76
- ai_words = await self.vector_service.find_similar_words(topic, difficulty, self.max_words // len(topics))
77
- all_words.extend(ai_words)
78
-
79
- if len(all_words) >= self.min_words:
80
- logger.info(f"✅ AI generated {len(all_words)} words")
81
- return self._sort_words_for_crossword(all_words[:self.max_words])
82
- else:
83
- logger.warning(f"⚠️ AI only generated {len(all_words)} words, falling back to static")
84
-
85
- # Fallback to cached words
86
- if self.vector_service:
87
- # Use the cached words from the initialized service
88
- logger.info(f"📦 Using cached words from initialized vector service")
89
- for topic in topics:
90
- cached_words = await self.vector_service._get_cached_fallback(topic, difficulty, self.max_words // len(topics))
91
- all_words.extend(cached_words)
92
- else:
93
- # Last resort: load static words directly
94
- logger.warning(f"⚠️ No vector service available, loading static words directly")
95
- all_words = await self._get_static_words(topics, difficulty)
96
 
97
- return self._sort_words_for_crossword(all_words[:self.max_words])
98
-
99
- async def _get_static_words(self, topics: List[str], difficulty: str) -> List[Dict[str, Any]]:
100
- """Get static words from JSON files."""
101
- all_words = []
102
-
103
- for topic in topics:
104
- # Try multiple case variations
105
- for topic_variation in [topic, topic.capitalize(), topic.lower()]:
106
- word_file = Path(__file__).parent.parent.parent / "data" / "word-lists" / f"{topic_variation.lower()}.json"
107
-
108
- if word_file.exists():
109
- with open(word_file, 'r') as f:
110
- words = json.load(f)
111
- # Filter by difficulty
112
- filtered = self._filter_by_difficulty(words, difficulty)
113
- all_words.extend(filtered)
114
- break
115
-
116
- return all_words
117
-
118
- def _filter_by_difficulty(self, words: List[Dict[str, Any]], difficulty: str) -> List[Dict[str, Any]]:
119
- """Filter words by difficulty (length)."""
120
- difficulty_map = {
121
- "easy": {"min_len": 3, "max_len": 8},
122
- "medium": {"min_len": 4, "max_len": 10},
123
- "hard": {"min_len": 5, "max_len": 15}
124
- }
125
 
126
- criteria = difficulty_map.get(difficulty, difficulty_map["medium"])
127
- return [w for w in words if criteria["min_len"] <= len(w["word"]) <= criteria["max_len"]]
 
 
 
 
128
 
129
  def _sort_words_for_crossword(self, words: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
130
  """Sort words by crossword suitability."""
@@ -271,11 +220,18 @@ class CrosswordGenerator:
271
  logger.info(f"🔧 Backtrack successful, trimming grid...")
272
  trimmed = self._trim_grid(grid, placed_words)
273
  logger.info(f"🔧 Grid trimmed, generating clues...")
274
- clues = self._generate_clues(word_objs, trimmed["placed_words"])
 
 
 
 
 
 
 
275
 
276
  return {
277
  "grid": trimmed["grid"],
278
- "placed_words": trimmed["placed_words"],
279
  "clues": clues
280
  }
281
  else:
@@ -634,6 +590,114 @@ class CrosswordGenerator:
634
 
635
  return {"grid": trimmed_grid, "placed_words": updated_words}
636
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
  def _create_simple_cross(self, word_list: List[str], word_objs: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
638
  """Create simple cross with two words."""
639
  if len(word_list) < 2:
@@ -678,31 +742,29 @@ class CrosswordGenerator:
678
  ]
679
 
680
  trimmed = self._trim_grid(grid, placed_words)
681
- clues = self._generate_clues(word_objs[:2], trimmed["placed_words"])
 
 
 
682
 
683
  return {
684
  "grid": trimmed["grid"],
685
- "placed_words": trimmed["placed_words"],
686
  "clues": clues
687
  }
688
 
689
  def _generate_clues(self, word_objs: List[Dict[str, Any]], placed_words: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
690
- """Generate clues for placed words."""
691
- logger.info(f"🔧 _generate_clues: word_objs={len(word_objs)}, placed_words={len(placed_words)}")
692
  clues = []
693
 
694
  try:
695
- for i, placed_word in enumerate(placed_words):
696
- logger.info(f"🔧 Processing placed word {i}: {placed_word.get('word', 'UNKNOWN')}")
697
-
698
  # Find matching word object
699
  word_obj = next((w for w in word_objs if w["word"].upper() == placed_word["word"]), None)
700
 
701
- if word_obj:
702
- logger.info(f"🔧 Found matching word_obj: {word_obj.get('word', 'UNKNOWN')}")
703
- clue_text = word_obj["clue"] if "clue" in word_obj else f"Clue for {placed_word['word']}"
704
  else:
705
- logger.warning(f"⚠️ No matching word_obj found for {placed_word['word']}")
706
  clue_text = f"Clue for {placed_word['word']}"
707
 
708
  clues.append({
@@ -713,7 +775,6 @@ class CrosswordGenerator:
713
  "position": {"row": placed_word["row"], "col": placed_word["col"]}
714
  })
715
 
716
- logger.info(f"🔧 Generated {len(clues)} clues")
717
  return clues
718
  except Exception as e:
719
  logger.error(f"❌ Error in _generate_clues: {e}")
 
4
 
5
  import asyncio
6
  import json
7
+ import logging
8
  import random
9
  import time
10
  from pathlib import Path
11
  from typing import Dict, List, Optional, Any, Tuple
 
12
 
13
+ logger = logging.getLogger(__name__)
14
 
15
  class CrosswordGenerator:
16
+ def __init__(self, thematic_service=None):
17
  self.max_attempts = 100
18
  self.min_words = 6
19
+ self.thematic_service = thematic_service
 
20
 
21
+ async def generate_puzzle(self, topics: List[str], difficulty: str = "medium", custom_sentence: str = None, multi_theme: bool = True, requested_words: int = 10) -> Optional[Dict[str, Any]]:
22
  """
23
  Generate a complete crossword puzzle.
24
  """
25
  try:
26
+ sentence_info = f", custom sentence: '{custom_sentence}'" if custom_sentence else ""
27
+ logger.info(f"🎯 Generating puzzle for topics: {topics}, difficulty: {difficulty}{sentence_info}, requested words: {requested_words}")
 
 
 
 
 
 
28
 
29
+ # Get words from thematic AI service
30
+ words = await self._select_words(topics, difficulty, custom_sentence, multi_theme, requested_words)
31
 
32
  if len(words) < self.min_words:
33
  logger.error(f"❌ Not enough words: {len(words)} < {self.min_words}")
 
50
  "difficulty": difficulty,
51
  "wordCount": len(grid_result["placed_words"]),
52
  "size": len(grid_result["grid"]),
53
+ "aiGenerated": True
54
  }
55
  }
56
 
 
58
  logger.error(f"❌ Error generating puzzle: {e}")
59
  raise
60
 
61
+ async def _select_words(self, topics: List[str], difficulty: str, custom_sentence: str = None, multi_theme: bool = True, requested_words: int = 10) -> List[Dict[str, Any]]:
62
+ """Select words for the crossword using thematic AI service."""
63
+ if not self.thematic_service:
64
+ raise Exception("Thematic service is required for word generation")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ logger.info(f"🎯 Using thematic AI service for word generation with {requested_words} requested words")
67
+
68
+ # Use the dedicated crossword method for better word selection
69
+ words = await self.thematic_service.find_words_for_crossword(topics, difficulty, requested_words, custom_sentence, multi_theme)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
71
+ if len(words) < self.min_words:
72
+ raise Exception(f"Thematic service generated insufficient words: {len(words)} < {self.min_words}")
73
+
74
+ logger.info(f"✅ Thematic service generated {len(words)} words")
75
+ return self._sort_words_for_crossword(words)
76
+
77
 
78
  def _sort_words_for_crossword(self, words: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
79
  """Sort words by crossword suitability."""
 
220
  logger.info(f"🔧 Backtrack successful, trimming grid...")
221
  trimmed = self._trim_grid(grid, placed_words)
222
  logger.info(f"🔧 Grid trimmed, generating clues...")
223
+
224
+ # Generate clues first so we can display them with positions
225
+ clues_data = self._generate_clues_data(word_objs, trimmed["placed_words"])
226
+
227
+ logger.info(f"🔧 Clues generated, assigning proper crossword numbers...")
228
+
229
+ # Fix numbering based on grid position (reading order) and log with clues
230
+ numbered_words, clues = self._assign_numbers_and_clues(trimmed["placed_words"], clues_data)
231
 
232
  return {
233
  "grid": trimmed["grid"],
234
+ "placed_words": numbered_words,
235
  "clues": clues
236
  }
237
  else:
 
590
 
591
  return {"grid": trimmed_grid, "placed_words": updated_words}
592
 
593
+ def _assign_crossword_numbers(self, placed_words: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
594
+ """
595
+ Assign proper crossword numbers based on grid position (reading order).
596
+
597
+ Crossword numbering rules:
598
+ 1. Numbers are assigned to word starting positions
599
+ 2. Reading order: top-to-bottom, then left-to-right
600
+ 3. A single number can be shared by both across and down words starting at the same cell
601
+ """
602
+ if not placed_words:
603
+ return placed_words
604
+
605
+ # Collect all unique starting positions
606
+ starting_positions = {} # (row, col) -> list of words starting at that position
607
+
608
+ for word in placed_words:
609
+ pos_key = (word["row"], word["col"])
610
+ if pos_key not in starting_positions:
611
+ starting_positions[pos_key] = []
612
+ starting_positions[pos_key].append(word)
613
+
614
+ # Sort positions by reading order (top-to-bottom, left-to-right)
615
+ sorted_positions = sorted(starting_positions.keys(), key=lambda pos: (pos[0], pos[1]))
616
+
617
+ # Assign numbers
618
+ numbered_words = []
619
+ for i, pos in enumerate(sorted_positions):
620
+ number = i + 1 # Crossword numbers start at 1
621
+
622
+ # Assign this number to all words starting at this position
623
+ for word in starting_positions[pos]:
624
+ numbered_word = word.copy()
625
+ numbered_word["number"] = number
626
+ numbered_words.append(numbered_word)
627
+
628
+ logger.info(f"🔢 Assigned crossword numbers: {len(sorted_positions)} unique starting positions (legacy function)")
629
+
630
+ return numbered_words
631
+
632
+ def _generate_clues_data(self, word_objs: List[Dict[str, Any]], placed_words: List[Dict[str, Any]]) -> Dict[str, str]:
633
+ """Generate a mapping of words to their clues."""
634
+ clues_map = {}
635
+
636
+ for placed_word in placed_words:
637
+ # Find matching word object
638
+ word_obj = next((w for w in word_objs if w["word"].upper() == placed_word["word"]), None)
639
+
640
+ if word_obj and "clue" in word_obj:
641
+ clues_map[placed_word["word"]] = word_obj["clue"]
642
+ else:
643
+ clues_map[placed_word["word"]] = f"Clue for {placed_word['word']}"
644
+
645
+ return clues_map
646
+
647
+ def _assign_numbers_and_clues(self, placed_words: List[Dict[str, Any]], clues_data: Dict[str, str]) -> tuple:
648
+ """
649
+ Assign proper crossword numbers based on grid position and create clues with enhanced logging.
650
+
651
+ Returns: (numbered_words, clues_list)
652
+ """
653
+ if not placed_words:
654
+ return placed_words, []
655
+
656
+ # Collect all unique starting positions
657
+ starting_positions = {} # (row, col) -> list of words starting at that position
658
+
659
+ for word in placed_words:
660
+ pos_key = (word["row"], word["col"])
661
+ if pos_key not in starting_positions:
662
+ starting_positions[pos_key] = []
663
+ starting_positions[pos_key].append(word)
664
+
665
+ # Sort positions by reading order (top-to-bottom, left-to-right)
666
+ sorted_positions = sorted(starting_positions.keys(), key=lambda pos: (pos[0], pos[1]))
667
+
668
+ # Assign numbers and create both numbered words and clues
669
+ numbered_words = []
670
+ clues = []
671
+
672
+ logger.info(f"🔢 Assigned crossword numbers: {len(sorted_positions)} unique starting positions")
673
+
674
+ for i, pos in enumerate(sorted_positions):
675
+ number = i + 1 # Crossword numbers start at 1
676
+
677
+ # Process all words starting at this position
678
+ for word in starting_positions[pos]:
679
+ numbered_word = word.copy()
680
+ numbered_word["number"] = number
681
+ numbered_words.append(numbered_word)
682
+
683
+ # Create clue object
684
+ clue_text = clues_data.get(word["word"], f"Clue for {word['word']}")
685
+ direction = "across" if word["direction"] == "horizontal" else "down"
686
+
687
+ clue = {
688
+ "number": number,
689
+ "word": word["word"],
690
+ "text": clue_text,
691
+ "direction": direction,
692
+ "position": {"row": word["row"], "col": word["col"]}
693
+ }
694
+ clues.append(clue)
695
+
696
+ # Enhanced logging with clues
697
+ logger.info(f" {number} {direction}: {word['word']} at ({word['row']}, {word['col']}) - \"{clue_text}\"")
698
+
699
+ return numbered_words, clues
700
+
701
  def _create_simple_cross(self, word_list: List[str], word_objs: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
702
  """Create simple cross with two words."""
703
  if len(word_list) < 2:
 
742
  ]
743
 
744
  trimmed = self._trim_grid(grid, placed_words)
745
+
746
+ # Generate clues first, then assign numbers with enhanced logging
747
+ clues_data = self._generate_clues_data(word_objs[:2], trimmed["placed_words"])
748
+ numbered_words, clues = self._assign_numbers_and_clues(trimmed["placed_words"], clues_data)
749
 
750
  return {
751
  "grid": trimmed["grid"],
752
+ "placed_words": numbered_words,
753
  "clues": clues
754
  }
755
 
756
  def _generate_clues(self, word_objs: List[Dict[str, Any]], placed_words: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
757
+ """Generate clues for placed words (legacy function - use _assign_numbers_and_clues for better logging)."""
 
758
  clues = []
759
 
760
  try:
761
+ for placed_word in placed_words:
 
 
762
  # Find matching word object
763
  word_obj = next((w for w in word_objs if w["word"].upper() == placed_word["word"]), None)
764
 
765
+ if word_obj and "clue" in word_obj:
766
+ clue_text = word_obj["clue"]
 
767
  else:
 
768
  clue_text = f"Clue for {placed_word['word']}"
769
 
770
  clues.append({
 
775
  "position": {"row": placed_word["row"], "col": placed_word["col"]}
776
  })
777
 
 
778
  return clues
779
  except Exception as e:
780
  logger.error(f"❌ Error in _generate_clues: {e}")
crossword-app/backend-py/src/services/crossword_generator_wrapper.py CHANGED
@@ -12,16 +12,18 @@ class CrosswordGenerator:
12
  Wrapper that uses the fixed crossword generator implementation.
13
  """
14
 
15
- def __init__(self, vector_service=None):
16
- self.vector_service = vector_service
17
  self.min_words = 8
18
  self.max_words = 15
19
 
20
  async def generate_puzzle(
21
  self,
22
  topics: List[str],
23
- difficulty: str = "medium",
24
- use_ai: bool = False
 
 
25
  ) -> Dict[str, Any]:
26
  """
27
  Generate a complete crossword puzzle using the fixed generator.
@@ -29,19 +31,21 @@ class CrosswordGenerator:
29
  Args:
30
  topics: List of topic strings
31
  difficulty: "easy", "medium", or "hard"
32
- use_ai: Whether to use vector search for word generation
 
 
33
 
34
  Returns:
35
  Dictionary containing grid, clues, and metadata
36
  """
37
  try:
38
- logger.info(f"🎯 Using fixed crossword generator for topics: {topics}")
39
 
40
- # Use the fixed generator implementation with the initialized vector service
41
  from .crossword_generator import CrosswordGenerator as ActualGenerator
42
- actual_generator = ActualGenerator(vector_service=self.vector_service)
43
 
44
- puzzle = await actual_generator.generate_puzzle(topics, difficulty, use_ai)
45
 
46
  logger.info(f"✅ Generated crossword with fixed algorithm")
47
  return puzzle
@@ -50,9 +54,9 @@ class CrosswordGenerator:
50
  logger.error(f"❌ Failed to generate puzzle: {e}")
51
  raise
52
 
53
- async def generate_words_for_topics(self, topics: List[str], difficulty: str, use_ai: bool) -> List[Dict[str, Any]]:
54
  """Backward compatibility method."""
55
  # This method is kept for compatibility but delegates to the fixed generator
56
  from .crossword_generator import CrosswordGenerator as ActualGenerator
57
- actual_generator = ActualGenerator()
58
- return await actual_generator._select_words(topics, difficulty, use_ai)
 
12
  Wrapper that uses the fixed crossword generator implementation.
13
  """
14
 
15
+ def __init__(self, thematic_service=None):
16
+ self.thematic_service = thematic_service
17
  self.min_words = 8
18
  self.max_words = 15
19
 
20
  async def generate_puzzle(
21
  self,
22
  topics: List[str],
23
+ difficulty: str = "medium",
24
+ custom_sentence: str = None,
25
+ multi_theme: bool = True,
26
+ requested_words: int = 10
27
  ) -> Dict[str, Any]:
28
  """
29
  Generate a complete crossword puzzle using the fixed generator.
 
31
  Args:
32
  topics: List of topic strings
33
  difficulty: "easy", "medium", or "hard"
34
+ custom_sentence: Optional custom sentence to influence word selection
35
+ multi_theme: Whether to use multi-theme processing (True) or single-theme blending (False)
36
+ requested_words: Number of words requested by frontend
37
 
38
  Returns:
39
  Dictionary containing grid, clues, and metadata
40
  """
41
  try:
42
+ logger.info(f"🎯 Using fixed crossword generator for topics: {topics}, requested words: {requested_words}")
43
 
44
+ # Use the fixed generator implementation with the initialized thematic service
45
  from .crossword_generator import CrosswordGenerator as ActualGenerator
46
+ actual_generator = ActualGenerator(thematic_service=self.thematic_service)
47
 
48
+ puzzle = await actual_generator.generate_puzzle(topics, difficulty, custom_sentence, multi_theme, requested_words)
49
 
50
  logger.info(f"✅ Generated crossword with fixed algorithm")
51
  return puzzle
 
54
  logger.error(f"❌ Failed to generate puzzle: {e}")
55
  raise
56
 
57
+ async def generate_words_for_topics(self, topics: List[str], difficulty: str, custom_sentence: str = None) -> List[Dict[str, Any]]:
58
  """Backward compatibility method."""
59
  # This method is kept for compatibility but delegates to the fixed generator
60
  from .crossword_generator import CrosswordGenerator as ActualGenerator
61
+ actual_generator = ActualGenerator(thematic_service=self.thematic_service)
62
+ return await actual_generator._select_words(topics, difficulty, custom_sentence)
crossword-app/backend-py/src/services/thematic_word_service.py ADDED
@@ -0,0 +1,1057 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Unified Thematic Word Generator using WordFreq + SentenceTransformers
4
+
5
+ Eliminates vocabulary redundancy by using WordFreq as the single vocabulary source
6
+ for both word lists and frequency data, with all-mpnet-base-v2 for embeddings.
7
+
8
+ Features:
9
+ - Single vocabulary source (WordFreq 319K words vs previous 3 separate sources)
10
+ - Unified filtering for crossword-suitable words
11
+ - 10-tier frequency classification system
12
+ - Compatible with crossword backend services
13
+ - Comprehensive modern vocabulary with proper frequency data
14
+ - Environment variable configuration for cache paths and settings
15
+
16
+ Environment Variables:
17
+ - CACHE_DIR: Cache directory for all thematic service files (default: ./model_cache)
18
+ - THEMATIC_VOCAB_SIZE_LIMIT: Maximum vocabulary size (default: 100000)
19
+ - MAX_VOCABULARY_SIZE: Fallback vocab size limit (used if THEMATIC_VOCAB_SIZE_LIMIT not set)
20
+ - THEMATIC_MODEL_NAME: Sentence transformer model to use (default: all-mpnet-base-v2)
21
+
22
+ Cache Structure:
23
+ - {cache_dir}/vocabulary_{size}.pkl - Processed vocabulary words
24
+ - {cache_dir}/frequencies_{size}.pkl - Word frequency data
25
+ - {cache_dir}/embeddings_{model}_{size}.npy - Word embeddings
26
+ - {cache_dir}/sentence-transformers/ - Hugging Face model cache
27
+
28
+ Usage:
29
+ # Use environment variables for production
30
+ export CACHE_DIR=/app/cache
31
+ export THEMATIC_VOCAB_SIZE_LIMIT=50000
32
+
33
+ # Or pass directly to constructor for development
34
+ service = ThematicWordService(cache_dir="/custom/path", vocab_size_limit=25000)
35
+ """
36
+
37
+ import os
38
+ import csv
39
+ import pickle
40
+ import numpy as np
41
+ import logging
42
+ import asyncio
43
+ import random
44
+ from typing import List, Tuple, Optional, Dict, Set, Any
45
+ from sentence_transformers import SentenceTransformer
46
+ from sklearn.metrics.pairwise import cosine_similarity
47
+ from sklearn.cluster import KMeans
48
+ from datetime import datetime
49
+ import time
50
+ from collections import Counter
51
+ from pathlib import Path
52
+
53
+ # WordFreq imports (assumed to be available)
54
+ from wordfreq import word_frequency, zipf_frequency, top_n_list
55
+
56
+ # Use backend's logging configuration
57
+ logger = logging.getLogger(__name__)
58
+
59
+ def get_timestamp():
60
+ return datetime.now().strftime("%H:%M:%S")
61
+
62
+ def get_datetimestamp():
63
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
64
+
65
+
66
+ class VocabularyManager:
67
+ """
68
+ Centralized vocabulary management using WordFreq as the single source.
69
+ Handles loading, filtering, caching, and frequency data generation.
70
+ """
71
+
72
+ def __init__(self, cache_dir: Optional[str] = None, vocab_size_limit: Optional[int] = None):
73
+ """Initialize vocabulary manager.
74
+
75
+ Args:
76
+ cache_dir: Directory for caching vocabulary and embeddings
77
+ vocab_size_limit: Maximum vocabulary size (None for full WordFreq vocabulary)
78
+ """
79
+ if cache_dir is None:
80
+ # Check environment variable for cache directory
81
+ cache_dir = os.getenv("CACHE_DIR")
82
+ if cache_dir is None:
83
+ cache_dir = os.path.join(os.path.dirname(__file__), 'model_cache')
84
+
85
+ self.cache_dir = Path(cache_dir)
86
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
87
+
88
+ # Vocabulary size configuration
89
+ self.vocab_size_limit = vocab_size_limit or int(os.getenv("THEMATIC_VOCAB_SIZE_LIMIT",
90
+ os.getenv("MAX_VOCABULARY_SIZE", "100000")))
91
+
92
+ # Cache paths
93
+ self.vocab_cache_path = self.cache_dir / f"vocabulary_{self.vocab_size_limit}.pkl"
94
+ self.frequency_cache_path = self.cache_dir / f"frequencies_{self.vocab_size_limit}.pkl"
95
+
96
+ # Loaded data
97
+ self.vocabulary: List[str] = []
98
+ self.word_frequencies: Counter = Counter()
99
+ self.is_loaded = False
100
+
101
+ def load_vocabulary(self) -> Tuple[List[str], Counter]:
102
+ """Load vocabulary and frequency data, with caching."""
103
+ if self.is_loaded:
104
+ return self.vocabulary, self.word_frequencies
105
+
106
+ # Try loading from cache
107
+ if self._load_from_cache():
108
+ logger.info(f"✅ Loaded vocabulary from cache: {len(self.vocabulary):,} words")
109
+ self.is_loaded = True
110
+ return self.vocabulary, self.word_frequencies
111
+
112
+ # Generate from WordFreq
113
+ logger.info("🔄 Generating vocabulary from WordFreq...")
114
+ self._generate_vocabulary_from_wordfreq()
115
+
116
+ # Save to cache
117
+ self._save_to_cache()
118
+
119
+ self.is_loaded = True
120
+ return self.vocabulary, self.word_frequencies
121
+
122
+ def _load_from_cache(self) -> bool:
123
+ """Load vocabulary and frequencies from cache."""
124
+ try:
125
+ if self.vocab_cache_path.exists() and self.frequency_cache_path.exists():
126
+ logger.info(f"📦 Loading vocabulary from cache...")
127
+ logger.info(f" Vocab cache: {self.vocab_cache_path}")
128
+ logger.info(f" Freq cache: {self.frequency_cache_path}")
129
+
130
+ # Validate cache files are readable
131
+ if not os.access(self.vocab_cache_path, os.R_OK):
132
+ logger.warning(f"⚠️ Vocabulary cache file not readable: {self.vocab_cache_path}")
133
+ return False
134
+
135
+ if not os.access(self.frequency_cache_path, os.R_OK):
136
+ logger.warning(f"⚠️ Frequency cache file not readable: {self.frequency_cache_path}")
137
+ return False
138
+
139
+ with open(self.vocab_cache_path, 'rb') as f:
140
+ self.vocabulary = pickle.load(f)
141
+
142
+ with open(self.frequency_cache_path, 'rb') as f:
143
+ self.word_frequencies = pickle.load(f)
144
+
145
+ # Validate loaded data
146
+ if not self.vocabulary or not self.word_frequencies:
147
+ logger.warning("⚠️ Cache files contain empty data")
148
+ return False
149
+
150
+ logger.info(f"✅ Loaded {len(self.vocabulary):,} words and {len(self.word_frequencies):,} frequencies from cache")
151
+ return True
152
+ else:
153
+ missing = []
154
+ if not self.vocab_cache_path.exists():
155
+ missing.append(f"vocabulary ({self.vocab_cache_path})")
156
+ if not self.frequency_cache_path.exists():
157
+ missing.append(f"frequency ({self.frequency_cache_path})")
158
+ logger.info(f"📂 Cache files missing: {', '.join(missing)}")
159
+ return False
160
+ except Exception as e:
161
+ logger.warning(f"⚠️ Cache loading failed: {e}")
162
+
163
+ return False
164
+
165
+ def _save_to_cache(self):
166
+ """Save vocabulary and frequencies to cache."""
167
+ try:
168
+ logger.info("💾 Saving vocabulary to cache...")
169
+
170
+ with open(self.vocab_cache_path, 'wb') as f:
171
+ pickle.dump(self.vocabulary, f)
172
+
173
+ with open(self.frequency_cache_path, 'wb') as f:
174
+ pickle.dump(self.word_frequencies, f)
175
+
176
+ logger.info("✅ Vocabulary cached successfully")
177
+ except Exception as e:
178
+ logger.warning(f"⚠️ Cache saving failed: {e}")
179
+
180
+ def _generate_vocabulary_from_wordfreq(self):
181
+ """Generate filtered vocabulary from WordFreq database."""
182
+ logger.info(f"📚 Fetching top {self.vocab_size_limit:,} words from WordFreq...")
183
+
184
+ # Get comprehensive word list from WordFreq
185
+ raw_words = top_n_list('en', self.vocab_size_limit * 2, wordlist='large') # Get extra for filtering
186
+ logger.info(f"📥 Retrieved {len(raw_words):,} raw words from WordFreq")
187
+
188
+ # Apply crossword-suitable filtering
189
+ filtered_words = []
190
+ frequency_data = Counter()
191
+
192
+ logger.info("🔍 Applying crossword filtering...")
193
+ for word in raw_words:
194
+ if self._is_crossword_suitable(word):
195
+ filtered_words.append(word.lower())
196
+
197
+ # Get frequency data
198
+ try:
199
+ freq = word_frequency(word, 'en', wordlist='large')
200
+ if freq > 0:
201
+ # Scale frequency to preserve precision
202
+ frequency_data[word.lower()] = int(freq * 1e9)
203
+ except:
204
+ frequency_data[word.lower()] = 1 # Minimal frequency for unknown words
205
+
206
+ if len(filtered_words) >= self.vocab_size_limit:
207
+ break
208
+
209
+ # Remove duplicates and sort
210
+ self.vocabulary = sorted(list(set(filtered_words)))
211
+ self.word_frequencies = frequency_data
212
+
213
+ logger.info(f"✅ Generated filtered vocabulary: {len(self.vocabulary):,} words")
214
+ logger.info(f"📊 Frequency data coverage: {len(self.word_frequencies):,} words")
215
+
216
+ def _is_crossword_suitable(self, word: str) -> bool:
217
+ """Check if word is suitable for crosswords."""
218
+ word = word.lower().strip()
219
+
220
+ # Length check (3-12 characters for crosswords)
221
+ if len(word) < 3 or len(word) > 12:
222
+ return False
223
+
224
+ # Must be alphabetic only
225
+ if not word.isalpha():
226
+ return False
227
+
228
+ # Skip boring/common words
229
+ boring_words = {
230
+ 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'this', 'that',
231
+ 'with', 'from', 'they', 'were', 'been', 'have', 'their', 'said', 'each',
232
+ 'which', 'what', 'there', 'will', 'more', 'when', 'some', 'like', 'into',
233
+ 'time', 'very', 'only', 'has', 'had', 'who', 'its', 'now', 'find', 'long',
234
+ 'down', 'day', 'did', 'get', 'come', 'made', 'may', 'part'
235
+ }
236
+
237
+ if word in boring_words:
238
+ return False
239
+
240
+ # Skip obvious plurals (simple heuristic)
241
+ if len(word) > 4 and word.endswith('s') and not word.endswith(('ss', 'us', 'is')):
242
+ return False
243
+
244
+ # Skip words with repeated characters (often not real words)
245
+ if len(set(word)) < len(word) * 0.6: # Less than 60% unique characters
246
+ return False
247
+
248
+ return True
249
+
250
+
251
+ class ThematicWordService:
252
+ """
253
+ Unified thematic word generator using WordFreq vocabulary and all-mpnet-base-v2 embeddings.
254
+
255
+ Compatible with both hack tools and crossword backend services.
256
+ Eliminates vocabulary redundancy by using single source for everything.
257
+ """
258
+
259
+ def __init__(self, cache_dir: Optional[str] = None, model_name: str = 'all-mpnet-base-v2',
260
+ vocab_size_limit: Optional[int] = None):
261
+ """Initialize the unified thematic word generator.
262
+
263
+ Args:
264
+ cache_dir: Directory to cache model and embeddings
265
+ model_name: Sentence transformer model to use
266
+ vocab_size_limit: Maximum vocabulary size (None for 100K default)
267
+ """
268
+ if cache_dir is None:
269
+ # Check environment variable for cache directory
270
+ cache_dir = os.getenv("CACHE_DIR")
271
+ if cache_dir is None:
272
+ cache_dir = os.path.join(os.path.dirname(__file__), 'model_cache')
273
+
274
+ self.cache_dir = Path(cache_dir)
275
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
276
+
277
+ # Get model name from environment if not specified
278
+ self.model_name = os.getenv("THEMATIC_MODEL_NAME", model_name)
279
+
280
+ # Get vocabulary size limit from environment if not specified
281
+ self.vocab_size_limit = (vocab_size_limit or
282
+ int(os.getenv("THEMATIC_VOCAB_SIZE_LIMIT",
283
+ os.getenv("MAX_VOCABULARY_SIZE", "100000"))))
284
+
285
+ # Core components
286
+ self.vocab_manager = VocabularyManager(str(self.cache_dir), self.vocab_size_limit)
287
+ self.model: Optional[SentenceTransformer] = None
288
+
289
+ # Loaded data
290
+ self.vocabulary: List[str] = []
291
+ self.word_frequencies: Counter = Counter()
292
+ self.vocab_embeddings: Optional[np.ndarray] = None
293
+ self.frequency_tiers: Dict[str, str] = {}
294
+ self.tier_descriptions: Dict[str, str] = {}
295
+
296
+ # Cache paths for embeddings
297
+ vocab_hash = f"{self.model_name.replace('/', '_')}_{self.vocab_size_limit}"
298
+ self.embeddings_cache_path = self.cache_dir / f"embeddings_{vocab_hash}.npy"
299
+
300
+ self.is_initialized = False
301
+
302
+ def initialize(self):
303
+ """Initialize the generator (synchronous version)."""
304
+ if self.is_initialized:
305
+ return
306
+
307
+ start_time = time.time()
308
+ logger.info(f"🚀 Initializing Thematic Word Service...")
309
+ logger.info(f"📁 Cache directory: {self.cache_dir}")
310
+ logger.info(f"🤖 Model: {self.model_name}")
311
+ logger.info(f"📊 Vocabulary size limit: {self.vocab_size_limit:,}")
312
+
313
+ # Check if cache directory exists and is accessible
314
+ if not self.cache_dir.exists():
315
+ logger.warning(f"⚠️ Cache directory does not exist, creating: {self.cache_dir}")
316
+ try:
317
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
318
+ except Exception as e:
319
+ logger.error(f"❌ Failed to create cache directory: {e}")
320
+ raise
321
+
322
+ # Load vocabulary and frequency data
323
+ vocab_start = time.time()
324
+ self.vocabulary, self.word_frequencies = self.vocab_manager.load_vocabulary()
325
+ vocab_time = time.time() - vocab_start
326
+ logger.info(f"✅ Vocabulary loaded in {vocab_time:.2f}s: {len(self.vocabulary):,} words")
327
+
328
+ # Load or create frequency tiers
329
+ self.frequency_tiers = self._create_frequency_tiers()
330
+
331
+ # Load model
332
+ logger.info(f"🤖 Loading embedding model: {self.model_name}")
333
+ model_start = time.time()
334
+ self.model = SentenceTransformer(
335
+ f'sentence-transformers/{self.model_name}',
336
+ cache_folder=str(self.cache_dir)
337
+ )
338
+ model_time = time.time() - model_start
339
+ logger.info(f"✅ Model loaded in {model_time:.2f}s")
340
+
341
+ # Load or create embeddings
342
+ self.vocab_embeddings = self._load_or_create_embeddings()
343
+
344
+ self.is_initialized = True
345
+ total_time = time.time() - start_time
346
+ logger.info(f"🎉 Unified generator initialized in {total_time:.2f}s")
347
+ logger.info(f"📊 Vocabulary: {len(self.vocabulary):,} words")
348
+ logger.info(f"📈 Frequency data: {len(self.word_frequencies):,} words")
349
+
350
+ async def initialize_async(self):
351
+ """Initialize the generator (async version for backend compatibility)."""
352
+ return self.initialize() # For now, same as sync version
353
+
354
+ def _load_or_create_embeddings(self) -> np.ndarray:
355
+ """Load embeddings from cache or create them."""
356
+ # Try loading from cache
357
+ if self.embeddings_cache_path.exists():
358
+ try:
359
+ logger.info(f"📦 Loading embeddings from cache: {self.embeddings_cache_path}")
360
+
361
+ # Validate cache file is readable
362
+ if not os.access(self.embeddings_cache_path, os.R_OK):
363
+ logger.warning(f"⚠️ Embeddings cache file not readable: {self.embeddings_cache_path}")
364
+ return self._create_embeddings_from_scratch()
365
+
366
+ embeddings = np.load(self.embeddings_cache_path)
367
+
368
+ # Validate embeddings shape matches vocabulary size
369
+ expected_shape = (len(self.vocabulary), None) # Second dimension varies by model
370
+ if embeddings.shape[0] != len(self.vocabulary):
371
+ logger.warning(f"⚠️ Embeddings shape mismatch: cache={embeddings.shape[0]}, vocab={len(self.vocabulary)}")
372
+ logger.warning("🔄 Vocabulary size changed, recreating embeddings...")
373
+ return self._create_embeddings_from_scratch()
374
+
375
+ logger.info(f"✅ Loaded embeddings from cache: {embeddings.shape}")
376
+ return embeddings
377
+ except Exception as e:
378
+ logger.warning(f"⚠️ Embeddings cache loading failed: {e}")
379
+ return self._create_embeddings_from_scratch()
380
+ else:
381
+ logger.info(f"📂 Embeddings cache not found: {self.embeddings_cache_path}")
382
+ return self._create_embeddings_from_scratch()
383
+
384
+ def _create_embeddings_from_scratch(self) -> np.ndarray:
385
+
386
+ # Create embeddings
387
+ logger.info("🔄 Creating embeddings for vocabulary...")
388
+ start_time = time.time()
389
+
390
+ # Create embeddings in batches for memory efficiency
391
+ batch_size = 512
392
+ all_embeddings = []
393
+
394
+ for i in range(0, len(self.vocabulary), batch_size):
395
+ batch_words = self.vocabulary[i:i + batch_size]
396
+ batch_embeddings = self.model.encode(
397
+ batch_words,
398
+ convert_to_tensor=False,
399
+ show_progress_bar=i == 0 # Only show progress for first batch
400
+ )
401
+ all_embeddings.append(batch_embeddings)
402
+
403
+ if i % (batch_size * 10) == 0:
404
+ logger.info(f"📊 Embeddings progress: {i:,}/{len(self.vocabulary):,}")
405
+
406
+ embeddings = np.vstack(all_embeddings)
407
+ embedding_time = time.time() - start_time
408
+ logger.info(f"✅ Created embeddings in {embedding_time:.2f}s: {embeddings.shape}")
409
+
410
+ # Save to cache
411
+ try:
412
+ np.save(self.embeddings_cache_path, embeddings)
413
+ logger.info("💾 Embeddings cached successfully")
414
+ except Exception as e:
415
+ logger.warning(f"⚠️ Embeddings cache saving failed: {e}")
416
+
417
+ return embeddings
418
+
419
+ def _create_frequency_tiers(self) -> Dict[str, str]:
420
+ """Create 10-tier frequency classification system."""
421
+ if not self.word_frequencies:
422
+ return {}
423
+
424
+ logger.info("📊 Creating frequency tiers...")
425
+
426
+ tiers = {}
427
+
428
+ # Calculate percentile-based thresholds for even distribution
429
+ all_counts = list(self.word_frequencies.values())
430
+ all_counts.sort(reverse=True)
431
+
432
+ # Define 10 tiers with percentile-based thresholds
433
+ tier_definitions = [
434
+ ("tier_1_ultra_common", 0.999, "Ultra Common (Top 0.1%)"),
435
+ ("tier_2_extremely_common", 0.995, "Extremely Common (Top 0.5%)"),
436
+ ("tier_3_very_common", 0.99, "Very Common (Top 1%)"),
437
+ ("tier_4_highly_common", 0.97, "Highly Common (Top 3%)"),
438
+ ("tier_5_common", 0.92, "Common (Top 8%)"),
439
+ ("tier_6_moderately_common", 0.85, "Moderately Common (Top 15%)"),
440
+ ("tier_7_somewhat_uncommon", 0.70, "Somewhat Uncommon (Top 30%)"),
441
+ ("tier_8_uncommon", 0.50, "Uncommon (Top 50%)"),
442
+ ("tier_9_rare", 0.25, "Rare (Top 75%)"),
443
+ ("tier_10_very_rare", 0.0, "Very Rare (Bottom 25%)")
444
+ ]
445
+
446
+ # Calculate actual thresholds
447
+ thresholds = []
448
+ for tier_name, percentile, description in tier_definitions:
449
+ if percentile > 0:
450
+ idx = int((1 - percentile) * len(all_counts))
451
+ threshold = all_counts[min(idx, len(all_counts) - 1)]
452
+ else:
453
+ threshold = 0
454
+ thresholds.append((tier_name, threshold, description))
455
+
456
+ # Store descriptions
457
+ self.tier_descriptions = {name: desc for name, _, desc in thresholds}
458
+
459
+ # Assign tiers
460
+ for word, count in self.word_frequencies.items():
461
+ assigned = False
462
+ for tier_name, threshold, description in thresholds:
463
+ if count >= threshold:
464
+ tiers[word] = tier_name
465
+ assigned = True
466
+ break
467
+
468
+ if not assigned:
469
+ tiers[word] = "tier_10_very_rare"
470
+
471
+ # Words not in frequency data are very rare
472
+ for word in self.vocabulary:
473
+ if word not in tiers:
474
+ tiers[word] = "tier_10_very_rare"
475
+
476
+ # Log tier distribution
477
+ tier_counts = Counter(tiers.values())
478
+ logger.info(f"✅ Created frequency tiers:")
479
+ for tier_name, count in sorted(tier_counts.items()):
480
+ desc = self.tier_descriptions.get(tier_name, tier_name)
481
+ logger.info(f" {desc}: {count:,} words")
482
+
483
+ return tiers
484
+
485
+ def generate_thematic_words(self,
486
+ inputs,
487
+ num_words: int = 100,
488
+ min_similarity: float = 0.3,
489
+ multi_theme: bool = False,
490
+ difficulty_tier: Optional[str] = None) -> List[Tuple[str, float, str]]:
491
+ """Generate thematically related words from input seeds.
492
+
493
+ Args:
494
+ inputs: Single string, or list of words/sentences as theme seeds
495
+ num_words: Number of words to return
496
+ min_similarity: Minimum similarity threshold
497
+ multi_theme: Whether to detect and use multiple themes
498
+ difficulty_tier: Specific tier to filter by (e.g., "tier_5_common")
499
+
500
+ Returns:
501
+ List of (word, similarity_score, frequency_tier) tuples
502
+ """
503
+ if not self.is_initialized:
504
+ self.initialize()
505
+
506
+ logger.info(f"🎯 Generating {num_words} thematic words")
507
+
508
+ # Handle single string input (convert to list for compatibility)
509
+ if isinstance(inputs, str):
510
+ inputs = [inputs]
511
+
512
+ if not inputs:
513
+ return []
514
+
515
+ # Clean inputs
516
+ clean_inputs = [inp.strip().lower() for inp in inputs if inp.strip()]
517
+ if not clean_inputs:
518
+ return []
519
+
520
+ logger.info(f"📝 Input themes: {clean_inputs}")
521
+ if difficulty_tier:
522
+ logger.info(f"📊 Filtering to tier: {self.tier_descriptions.get(difficulty_tier, difficulty_tier)}")
523
+
524
+ # Get theme vector(s) using original logic
525
+ # Auto-enable multi-theme for 3+ inputs (matching original behavior)
526
+ auto_multi_theme = len(clean_inputs) > 2
527
+ final_multi_theme = multi_theme or auto_multi_theme
528
+
529
+ logger.info(f"🔍 Multi-theme detection: {final_multi_theme} (auto: {auto_multi_theme}, manual: {multi_theme})")
530
+
531
+ if final_multi_theme:
532
+ theme_vectors = self._detect_multiple_themes(clean_inputs)
533
+ logger.info(f"📊 Detected {len(theme_vectors)} themes")
534
+ else:
535
+ theme_vectors = [self._compute_theme_vector(clean_inputs)]
536
+ logger.info("📊 Using single theme vector")
537
+
538
+ # Collect similarities from all themes
539
+ all_similarities = np.zeros(len(self.vocabulary))
540
+
541
+ for theme_vector in theme_vectors:
542
+ # Compute similarities with vocabulary
543
+ similarities = cosine_similarity(theme_vector, self.vocab_embeddings)[0]
544
+ all_similarities += similarities / len(theme_vectors) # Average across themes
545
+
546
+ logger.info("✅ Computed semantic similarities")
547
+
548
+ # Get top candidates sorted by similarity
549
+ # np.argsort() returns indices that would sort array in ascending order
550
+ # [::-1] reverses to get descending order (highest similarity first)
551
+ # top_indices[0] contains the vocabulary index of the word most similar to theme vector
552
+ top_indices = np.argsort(all_similarities)[::-1]
553
+
554
+ # Filter and format results
555
+ results = []
556
+ input_words_set = set(clean_inputs)
557
+ logger.info(f"{clean_inputs=}")
558
+
559
+ # Traverse top_indices from beginning to get most similar words first
560
+ # Each idx is used to lookup the actual word in self.vocabulary[idx]
561
+ for idx in top_indices:
562
+ if len(results) >= num_words * 3: # Get extra candidates for filtering
563
+ break
564
+
565
+ similarity_score = all_similarities[idx]
566
+ word = self.vocabulary[idx] # Get actual word using vocabulary index
567
+
568
+ # Apply filters - use early termination since top_indices is sorted by similarity
569
+ if similarity_score < min_similarity:
570
+ break # All remaining words will also be below threshold since array is sorted
571
+
572
+ # Skip input words themselves
573
+ if word.lower() in input_words_set:
574
+ continue
575
+
576
+ # Get pre-assigned tier for this word
577
+ # Tiers are computed during initialization using WordFreq data
578
+ # Based on percentile thresholds: tier_1 (top 0.1%), tier_5 (top 8%), etc.
579
+ word_tier = self.frequency_tiers.get(word, "tier_10_very_rare")
580
+
581
+ # Filter by difficulty tier if specified
582
+ # If difficulty_tier is specified, only include words from that exact tier
583
+ # If no difficulty_tier specified, include all words (subject to similarity threshold)
584
+ if difficulty_tier and word_tier != difficulty_tier:
585
+ continue
586
+
587
+ results.append((word, similarity_score, word_tier))
588
+
589
+ # Sort by similarity and return top results
590
+ results.sort(key=lambda x: x[1], reverse=True)
591
+ final_results = results[:num_words]
592
+
593
+ logger.info(f"✅ Generated {len(final_results)} thematic words")
594
+ return final_results
595
+
596
+ def _compute_theme_vector(self, inputs: List[str]) -> np.ndarray:
597
+ """Compute semantic centroid from input words/sentences."""
598
+ logger.info(f"🎯 Computing theme vector for {len(inputs)} inputs")
599
+
600
+ # Encode all inputs
601
+ input_embeddings = self.model.encode(inputs, convert_to_tensor=False, show_progress_bar=False)
602
+ logger.info(f"✅ Encoded {len(inputs)} inputs")
603
+
604
+ # Simple approach: average all input embeddings
605
+ theme_vector = np.mean(input_embeddings, axis=0)
606
+
607
+ return theme_vector.reshape(1, -1)
608
+
609
+ def _detect_multiple_themes(self, inputs: List[str], max_themes: int = 3) -> List[np.ndarray]:
610
+ """Detect multiple themes using clustering."""
611
+ if len(inputs) < 2:
612
+ return [self._compute_theme_vector(inputs)]
613
+
614
+ logger.info(f"🔍 Detecting multiple themes from {len(inputs)} inputs")
615
+
616
+ # Encode inputs
617
+ input_embeddings = self.model.encode(inputs, convert_to_tensor=False, show_progress_bar=False)
618
+ logger.info("✅ Encoded inputs for clustering")
619
+
620
+ # Determine optimal number of clusters
621
+ n_clusters = min(max_themes, len(inputs), 3)
622
+ logger.info(f"📊 Using {n_clusters} clusters for theme detection")
623
+
624
+ if n_clusters == 1:
625
+ return [np.mean(input_embeddings, axis=0).reshape(1, -1)]
626
+
627
+ # Perform clustering
628
+ kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
629
+ kmeans.fit(input_embeddings)
630
+
631
+ logger.info(f"✅ Clustered inputs into {n_clusters} themes")
632
+
633
+ # Return cluster centers as theme vectors
634
+ return [center.reshape(1, -1) for center in kmeans.cluster_centers_]
635
+
636
+ def get_tier_words(self, tier: str, limit: int = 1000) -> List[str]:
637
+ """Get all words from a specific frequency tier.
638
+
639
+ Args:
640
+ tier: Frequency tier name (e.g., "tier_5_common")
641
+ limit: Maximum number of words to return
642
+
643
+ Returns:
644
+ List of words in the specified tier
645
+ """
646
+ if not self.is_initialized:
647
+ self.initialize()
648
+
649
+ tier_words = [word for word, word_tier in self.frequency_tiers.items()
650
+ if word_tier == tier]
651
+
652
+ return tier_words[:limit]
653
+
654
+ def get_word_info(self, word: str) -> Dict[str, Any]:
655
+ """Get comprehensive information about a word.
656
+
657
+ Args:
658
+ word: Word to get information for
659
+
660
+ Returns:
661
+ Dictionary with word info including frequency, tier, etc.
662
+ """
663
+ if not self.is_initialized:
664
+ self.initialize()
665
+
666
+ word_lower = word.lower()
667
+
668
+ info = {
669
+ 'word': word,
670
+ 'in_vocabulary': word_lower in self.vocabulary,
671
+ 'frequency': self.word_frequencies.get(word_lower, 0),
672
+ 'tier': self.frequency_tiers.get(word_lower, "tier_10_very_rare"),
673
+ 'tier_description': self.tier_descriptions.get(
674
+ self.frequency_tiers.get(word_lower, "tier_10_very_rare"),
675
+ "Unknown"
676
+ )
677
+ }
678
+
679
+ return info
680
+
681
+ # Backend compatibility methods
682
+ async def find_similar_words(self, topic: str, difficulty: str = "medium", max_words: int = 15) -> List[Dict[str, Any]]:
683
+ """Backend-compatible method for finding similar words.
684
+
685
+ Returns list of word dictionaries compatible with crossword_generator.py
686
+ Expected format: [{"word": str, "clue": str}, ...]
687
+ """
688
+ # Map difficulty to appropriate tier filtering
689
+ difficulty_tier_map = {
690
+ "easy": [ "tier_2_extremely_common", "tier_3_very_common", "tier_4_highly_common"],
691
+ "medium": ["tier_4_highly_common", "tier_5_common", "tier_6_moderately_common", "tier_7_somewhat_uncommon"],
692
+ "hard": ["tier_7_somewhat_uncommon", "tier_8_uncommon", "tier_9_rare"]
693
+ }
694
+
695
+ allowed_tiers = difficulty_tier_map.get(difficulty, difficulty_tier_map["medium"])
696
+
697
+ # Get thematic words
698
+ all_results = self.generate_thematic_words(
699
+ topic,
700
+ num_words=150, # Get extra for filtering
701
+ min_similarity=0.3
702
+ )
703
+
704
+ # Filter by difficulty and format for backend
705
+ backend_words = []
706
+ for word, similarity, tier in all_results:
707
+ # Check difficulty criteria
708
+ if not self._matches_backend_difficulty(word, difficulty):
709
+ continue
710
+
711
+ # Optional tier filtering for more precise difficulty control
712
+ # (Comment out if tier filtering is too restrictive)
713
+ # if tier not in allowed_tiers:
714
+ # continue
715
+
716
+ # Format for backend compatibility
717
+ backend_word = {
718
+ "word": word.upper(), # Backend expects uppercase
719
+ "clue": self._generate_simple_clue(word, topic),
720
+ "similarity": similarity,
721
+ "tier": tier
722
+ }
723
+
724
+ backend_words.append(backend_word)
725
+
726
+ if len(backend_words) >= max_words:
727
+ break
728
+
729
+ logger.info(f"🎯 Generated {len(backend_words)} words for topic '{topic}' (difficulty: {difficulty})")
730
+ return backend_words
731
+
732
+ def _matches_backend_difficulty(self, word: str, difficulty: str) -> bool:
733
+ """Check if word matches backend difficulty criteria."""
734
+ difficulty_map = {
735
+ "easy": {"min_len": 3, "max_len": 8},
736
+ "medium": {"min_len": 4, "max_len": 10},
737
+ "hard": {"min_len": 5, "max_len": 15}
738
+ }
739
+
740
+ criteria = difficulty_map.get(difficulty, difficulty_map["medium"])
741
+ return criteria["min_len"] <= len(word) <= criteria["max_len"]
742
+
743
+ def _generate_simple_clue(self, word: str, topic: str) -> str:
744
+ """Generate a simple clue for the word (backend compatibility)."""
745
+ # Basic clue templates matching backend expectations
746
+ word_lower = word.lower()
747
+ topic_lower = topic.lower()
748
+
749
+ # Topic-specific clue templates
750
+ if "animal" in topic_lower:
751
+ return f"{word_lower} (animal)"
752
+ elif "tech" in topic_lower or "computer" in topic_lower:
753
+ return f"{word_lower} (technology)"
754
+ elif "science" in topic_lower:
755
+ return f"{word_lower} (science)"
756
+ elif "geo" in topic_lower or "place" in topic_lower:
757
+ return f"{word_lower} (geography)"
758
+ elif "food" in topic_lower:
759
+ return f"{word_lower} (food)"
760
+ else:
761
+ return f"{word_lower} (related to {topic_lower})"
762
+
763
+ def get_vocabulary_size(self) -> int:
764
+ """Get the size of the loaded vocabulary."""
765
+ return len(self.vocabulary)
766
+
767
+ def get_tier_distribution(self) -> Dict[str, int]:
768
+ """Get distribution of words across frequency tiers."""
769
+ if not self.frequency_tiers:
770
+ return {}
771
+
772
+ tier_counts = Counter(self.frequency_tiers.values())
773
+ return dict(tier_counts)
774
+
775
+ def get_cache_status(self) -> Dict[str, Any]:
776
+ """Get detailed cache status information."""
777
+ vocab_exists = self.vocab_manager.vocab_cache_path.exists()
778
+ freq_exists = self.vocab_manager.frequency_cache_path.exists()
779
+ embeddings_exists = self.embeddings_cache_path.exists()
780
+
781
+ status = {
782
+ "cache_directory": str(self.cache_dir),
783
+ "vocabulary_cache": {
784
+ "path": str(self.vocab_manager.vocab_cache_path),
785
+ "exists": vocab_exists,
786
+ "readable": vocab_exists and os.access(self.vocab_manager.vocab_cache_path, os.R_OK)
787
+ },
788
+ "frequency_cache": {
789
+ "path": str(self.vocab_manager.frequency_cache_path),
790
+ "exists": freq_exists,
791
+ "readable": freq_exists and os.access(self.vocab_manager.frequency_cache_path, os.R_OK)
792
+ },
793
+ "embeddings_cache": {
794
+ "path": str(self.embeddings_cache_path),
795
+ "exists": embeddings_exists,
796
+ "readable": embeddings_exists and os.access(self.embeddings_cache_path, os.R_OK)
797
+ },
798
+ "complete": vocab_exists and freq_exists and embeddings_exists
799
+ }
800
+
801
+ # Add size information if files exist
802
+ for cache_type in ["vocabulary_cache", "frequency_cache", "embeddings_cache"]:
803
+ cache_info = status[cache_type]
804
+ if cache_info["exists"]:
805
+ try:
806
+ file_path = Path(cache_info["path"])
807
+ cache_info["size_bytes"] = file_path.stat().st_size
808
+ cache_info["size_mb"] = round(cache_info["size_bytes"] / (1024 * 1024), 2)
809
+ except Exception as e:
810
+ cache_info["size_error"] = str(e)
811
+
812
+ return status
813
+
814
+ async def find_words_for_crossword(self, topics: List[str], difficulty: str, requested_words: int = 10, custom_sentence: str = None, multi_theme: bool = True) -> List[Dict[str, Any]]:
815
+ """
816
+ Crossword-specific word finding method with 50% overgeneration and clue quality filtering.
817
+
818
+ Args:
819
+ topics: List of topic strings
820
+ difficulty: "easy", "medium", or "hard"
821
+ requested_words: Number of words requested by frontend
822
+ custom_sentence: Optional custom sentence to influence word selection
823
+ multi_theme: Whether to use multi-theme processing (True) or single-theme blending (False)
824
+
825
+ Returns:
826
+ List of word dictionaries: [{"word": str, "clue": str, "similarity": float, "source": "thematic", "tier": str}]
827
+ """
828
+ if not self.is_initialized:
829
+ await self.initialize_async()
830
+
831
+ sentence_info = f", custom sentence: '{custom_sentence}'" if custom_sentence else ""
832
+ theme_mode = "multi-theme" if multi_theme else "single-theme"
833
+
834
+ # Calculate generation target (3x more for quality filtering - need large pool for clue generation)
835
+ generation_target = int(requested_words * 3)
836
+ logger.info(f"🎯 Finding words for crossword - topics: {topics}, difficulty: {difficulty}{sentence_info}, mode: {theme_mode}")
837
+ logger.info(f"📊 Generating {generation_target} candidates to select best {requested_words} words after clue filtering")
838
+
839
+ # Map difficulty to tier preferences
840
+ difficulty_tier_map = {
841
+ "easy": ["tier_2_extremely_common", "tier_3_very_common", "tier_4_highly_common"],
842
+ "medium": ["tier_4_highly_common", "tier_5_common", "tier_6_moderately_common", "tier_7_somewhat_uncommon"],
843
+ "hard": ["tier_7_somewhat_uncommon", "tier_8_uncommon", "tier_9_rare"]
844
+ }
845
+
846
+ # Map difficulty to similarity thresholds
847
+ difficulty_similarity_map = {
848
+ "easy": 0.4,
849
+ "medium": 0.3,
850
+ "hard": 0.25
851
+ }
852
+
853
+ preferred_tiers = difficulty_tier_map.get(difficulty, difficulty_tier_map["medium"])
854
+ min_similarity = difficulty_similarity_map.get(difficulty, 0.3)
855
+
856
+ # Build input list for thematic word generation
857
+ input_list = topics.copy() # Start with topics: ["Art"]
858
+
859
+ # Add custom sentence as separate input if provided
860
+ if custom_sentence:
861
+ input_list.append(custom_sentence) # Now: ["Art", "i will always love you"]
862
+
863
+ # Determine if multi-theme processing is needed
864
+ is_multi_theme = len(input_list) > 1
865
+
866
+ # Set topic_input for generate_thematic_words
867
+ topic_input = input_list if is_multi_theme else input_list[0]
868
+
869
+ # Get thematic words (get extra for filtering)
870
+ raw_results = self.generate_thematic_words(
871
+ topic_input,
872
+ num_words=150, # Get extra for difficulty filtering
873
+ min_similarity=min_similarity,
874
+ multi_theme=multi_theme
875
+ )
876
+
877
+ # Log generated thematic words sorted by tiers
878
+ if raw_results:
879
+ # Group results by tier for sorted display
880
+ tier_groups = {}
881
+ for word, similarity, tier in raw_results:
882
+ if tier not in tier_groups:
883
+ tier_groups[tier] = []
884
+ tier_groups[tier].append((word, similarity))
885
+
886
+ # Sort tiers from most common to least common
887
+ tier_order = [
888
+ "tier_1_ultra_common",
889
+ "tier_2_extremely_common",
890
+ "tier_3_very_common",
891
+ "tier_4_highly_common",
892
+ "tier_5_common",
893
+ "tier_6_moderately_common",
894
+ "tier_7_somewhat_uncommon",
895
+ "tier_8_uncommon",
896
+ "tier_9_rare",
897
+ "tier_10_very_rare"
898
+ ]
899
+
900
+ # Build single log message with all tier information
901
+ log_lines = [f"📊 Generated {len(raw_results)} thematic words, grouped by tiers:"]
902
+
903
+ for tier in tier_order:
904
+ if tier in tier_groups:
905
+ tier_desc = self.tier_descriptions.get(tier, tier)
906
+ log_lines.append(f" 📊 {tier_desc}:")
907
+ # Sort words within tier alphabetically
908
+ tier_words = sorted(tier_groups[tier], key=lambda x: x[0])
909
+ for word, similarity in tier_words:
910
+ log_lines.append(f" {word:<15} (similarity: {similarity:.3f})")
911
+
912
+ # uncomment this log line if want to print all words returned
913
+ logger.info("\n".join(log_lines))
914
+ else:
915
+ logger.info("📊 No thematic words generated")
916
+
917
+ # Weighted random tier selection for crossword backend
918
+ # Step 1: Group raw_results by tier and filter by difficulty/length
919
+ tier_groups_filtered = {}
920
+ for word, similarity, tier in raw_results:
921
+ # Only consider words from preferred tiers for this difficulty
922
+ if tier in preferred_tiers: # and self._matches_crossword_difficulty(word, difficulty):
923
+ if tier not in tier_groups_filtered:
924
+ tier_groups_filtered[tier] = []
925
+ tier_groups_filtered[tier].append((word, similarity, tier))
926
+
927
+ # Step 2: Calculate word distribution across preferred tiers
928
+ tier_word_counts = {tier: len(words) for tier, words in tier_groups_filtered.items()}
929
+ total_available_words = sum(tier_word_counts.values())
930
+
931
+ logger.info(f"📊 Available words by preferred tier: {tier_word_counts}")
932
+
933
+ if total_available_words == 0:
934
+ logger.info("⚠️ No words found in preferred tiers, returning empty list")
935
+ return []
936
+
937
+ # Step 3: Generate clues for ALL words in preferred tiers (no pre-selection)
938
+ candidate_words = []
939
+
940
+ # Generate clues for all available words in preferred tiers
941
+ # This gives us a large pool to filter by clue quality
942
+ logger.info(f"📊 Generating clues for all {total_available_words} words in preferred tiers")
943
+ for tier, words in tier_groups_filtered.items():
944
+ for word, similarity, tier in words:
945
+ word_data = {
946
+ "word": word.upper(),
947
+ "clue": self._generate_crossword_clue(word, topics),
948
+ "similarity": float(similarity),
949
+ "source": "thematic",
950
+ "tier": tier
951
+ }
952
+ candidate_words.append(word_data)
953
+
954
+ # Step 5: Filter candidates by clue quality and select best words
955
+ logger.info(f"📊 Generated {len(candidate_words)} candidate words, filtering for clue quality")
956
+
957
+ # Separate words by clue quality
958
+ quality_words = [] # Words with proper WordNet-based clues
959
+ fallback_words = [] # Words with generic fallback clues
960
+
961
+ fallback_patterns = ["Related to", "Crossword answer"]
962
+
963
+ for word_data in candidate_words:
964
+ clue = word_data["clue"]
965
+ has_fallback = any(pattern in clue for pattern in fallback_patterns)
966
+
967
+ if has_fallback:
968
+ fallback_words.append(word_data)
969
+ else:
970
+ quality_words.append(word_data)
971
+
972
+ # Prioritize quality words, use fallback only if needed
973
+ final_words = []
974
+
975
+ # First, add quality words up to requested count
976
+ if quality_words:
977
+ random.shuffle(quality_words) # Randomize selection
978
+ final_words.extend(quality_words[:requested_words])
979
+
980
+ # If we don't have enough quality words, add some fallback words
981
+ if len(final_words) < requested_words and fallback_words:
982
+ needed = requested_words - len(final_words)
983
+ random.shuffle(fallback_words)
984
+ final_words.extend(fallback_words[:needed])
985
+
986
+ # Final shuffle to avoid quality-based ordering
987
+ random.shuffle(final_words)
988
+
989
+ logger.info(f"✅ Selected {len(final_words)} words ({len([w for w in final_words if not any(p in w['clue'] for p in fallback_patterns)])} quality, {len([w for w in final_words if any(p in w['clue'] for p in fallback_patterns)])} fallback)")
990
+ logger.info(f"📝 Final words: {[w['word'] for w in final_words]}")
991
+ return final_words
992
+
993
+ def _matches_crossword_difficulty(self, word: str, difficulty: str) -> bool:
994
+ """Check if word matches crossword difficulty criteria."""
995
+ difficulty_criteria = {
996
+ "easy": {"min_len": 3, "max_len": 8},
997
+ "medium": {"min_len": 4, "max_len": 10},
998
+ "hard": {"min_len": 5, "max_len": 12}
999
+ }
1000
+
1001
+ criteria = difficulty_criteria.get(difficulty, difficulty_criteria["medium"])
1002
+ return criteria["min_len"] <= len(word) <= criteria["max_len"]
1003
+
1004
+ def _generate_crossword_clue(self, word: str, topics: List[str]) -> str:
1005
+ """Generate a crossword clue for the word using WordNet."""
1006
+ # Initialize WordNet clue generator if not already done
1007
+ if not hasattr(self, '_wordnet_generator') or self._wordnet_generator is None:
1008
+ try:
1009
+ from .wordnet_clue_generator import WordNetClueGenerator
1010
+ self._wordnet_generator = WordNetClueGenerator(
1011
+ cache_dir=str(self.cache_dir)
1012
+ )
1013
+ self._wordnet_generator.initialize()
1014
+ logger.info("✅ WordNet clue generator initialized on-demand")
1015
+ except Exception as e:
1016
+ logger.warning(f"⚠️ Failed to initialize WordNet clue generator: {e}")
1017
+ self._wordnet_generator = None
1018
+
1019
+ # Use WordNet generator if available
1020
+ if self._wordnet_generator:
1021
+ try:
1022
+ primary_topic = topics[0] if topics else "general"
1023
+ clue = self._wordnet_generator.generate_clue(word, primary_topic)
1024
+ if clue and len(clue.strip()) > 0:
1025
+ return clue
1026
+ except Exception as e:
1027
+ logger.warning(f"⚠️ WordNet clue generation failed for '{word}': {e}")
1028
+
1029
+ # Fallback to simple templates if WordNet fails
1030
+ word_lower = word.lower()
1031
+ primary_topic = topics[0] if topics else "general"
1032
+ topic_lower = primary_topic.lower()
1033
+
1034
+ # Topic-specific clue templates as fallback
1035
+ if any(keyword in topic_lower for keyword in ["animal", "pet", "wildlife"]):
1036
+ return f"{word_lower} (animal)"
1037
+ elif any(keyword in topic_lower for keyword in ["tech", "computer", "software", "digital"]):
1038
+ return f"{word_lower} (technology)"
1039
+ elif any(keyword in topic_lower for keyword in ["science", "biology", "chemistry", "physics"]):
1040
+ return f"{word_lower} (science)"
1041
+ elif any(keyword in topic_lower for keyword in ["geo", "place", "city", "country", "location"]):
1042
+ return f"{word_lower} (geography)"
1043
+ elif any(keyword in topic_lower for keyword in ["food", "cooking", "cuisine", "recipe"]):
1044
+ return f"{word_lower} (food)"
1045
+ elif any(keyword in topic_lower for keyword in ["music", "song", "instrument", "audio"]):
1046
+ return f"{word_lower} (music)"
1047
+ elif any(keyword in topic_lower for keyword in ["sport", "game", "athletic", "exercise"]):
1048
+ return f"{word_lower} (sports)"
1049
+ else:
1050
+ return f"{word_lower} (related to {topic_lower})"
1051
+
1052
+
1053
+ # Backwards compatibility aliases
1054
+ ThematicWordGenerator = ThematicWordService # For existing code
1055
+ UnifiedThematicWordGenerator = ThematicWordService # For existing code
1056
+
1057
+ # Backend service - no interactive demo needed
crossword-app/backend-py/src/services/unified_word_service.py ADDED
@@ -0,0 +1,250 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Integration adapter for Unified Thematic Word Generator
3
+
4
+ This service provides a bridge between the new unified word generator
5
+ and the existing crossword backend, enabling the backend to use the
6
+ comprehensive WordFreq vocabulary instead of the limited model vocabulary.
7
+ """
8
+
9
+ import os
10
+ import sys
11
+ import logging
12
+ from typing import List, Dict, Any, Optional
13
+ from pathlib import Path
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class UnifiedWordService:
18
+ """
19
+ Service adapter for integrating UnifiedThematicWordGenerator with the crossword backend.
20
+
21
+ Provides the same interface as VectorSearchService but uses the comprehensive
22
+ WordFreq vocabulary instead of model-limited vocabulary.
23
+ """
24
+
25
+ def __init__(self, vocab_size_limit: Optional[int] = None):
26
+ """Initialize the unified word service.
27
+
28
+ Args:
29
+ vocab_size_limit: Maximum vocabulary size (None for default 100K)
30
+ """
31
+ self.generator = None
32
+ self.vocab_size_limit = vocab_size_limit or int(os.getenv("MAX_VOCABULARY_SIZE", "100000"))
33
+ self.is_initialized = False
34
+
35
+ # Import the generator from hack directory
36
+ self._import_generator()
37
+
38
+ def _import_generator(self):
39
+ """Import the UnifiedThematicWordGenerator from hack directory."""
40
+ try:
41
+ # Add hack directory to path
42
+ hack_dir = Path(__file__).parent.parent.parent.parent.parent / "hack"
43
+ if hack_dir.exists():
44
+ sys.path.insert(0, str(hack_dir))
45
+ logger.info(f"📁 Added hack directory to path: {hack_dir}")
46
+
47
+ # Import the generator
48
+ from thematic_word_generator import UnifiedThematicWordGenerator
49
+
50
+ # Initialize with appropriate cache directory
51
+ cache_dir = os.path.join(os.path.dirname(__file__), '..', '..', 'cache', 'unified_generator')
52
+
53
+ self.generator = UnifiedThematicWordGenerator(
54
+ cache_dir=cache_dir,
55
+ vocab_size_limit=self.vocab_size_limit
56
+ )
57
+
58
+ logger.info(f"✅ Imported UnifiedThematicWordGenerator with vocab limit: {self.vocab_size_limit:,}")
59
+
60
+ except ImportError as e:
61
+ logger.error(f"❌ Failed to import UnifiedThematicWordGenerator: {e}")
62
+ logger.error(" Make sure the hack directory contains thematic_word_generator.py")
63
+ self.generator = None
64
+ except Exception as e:
65
+ logger.error(f"❌ Error setting up UnifiedThematicWordGenerator: {e}")
66
+ self.generator = None
67
+
68
+ async def initialize(self):
69
+ """Initialize the unified word service."""
70
+ if not self.generator:
71
+ logger.error("❌ Cannot initialize: generator not available")
72
+ return False
73
+
74
+ try:
75
+ logger.info("🚀 Initializing Unified Word Service...")
76
+ start_time = time.time()
77
+
78
+ # Initialize the generator (async compatible)
79
+ await self.generator.initialize_async()
80
+
81
+ self.is_initialized = True
82
+ init_time = time.time() - start_time
83
+
84
+ logger.info(f"✅ Unified Word Service initialized in {init_time:.2f}s")
85
+ logger.info(f"📊 Vocabulary size: {self.generator.get_vocabulary_size():,} words")
86
+ logger.info(f"🎯 Tier distribution: {self.generator.get_tier_distribution()}")
87
+
88
+ return True
89
+
90
+ except Exception as e:
91
+ logger.error(f"❌ Failed to initialize Unified Word Service: {e}")
92
+ self.is_initialized = False
93
+ return False
94
+
95
+ async def find_similar_words(
96
+ self,
97
+ topic: str,
98
+ difficulty: str = "medium",
99
+ max_words: int = 15
100
+ ) -> List[Dict[str, Any]]:
101
+ """
102
+ Find similar words using the unified generator.
103
+
104
+ Compatible with VectorSearchService interface.
105
+
106
+ Args:
107
+ topic: Topic to find words for
108
+ difficulty: Difficulty level (easy/medium/hard)
109
+ max_words: Maximum number of words to return
110
+
111
+ Returns:
112
+ List of word dictionaries: [{"word": str, "clue": str}, ...]
113
+ """
114
+ if not self.is_initialized or not self.generator:
115
+ logger.error("❌ Service not initialized or generator not available")
116
+ return []
117
+
118
+ try:
119
+ # Use the generator's backend-compatible method
120
+ results = await self.generator.find_similar_words(topic, difficulty, max_words)
121
+
122
+ logger.info(f"🎯 Generated {len(results)} words for '{topic}' (difficulty: {difficulty})")
123
+ return results
124
+
125
+ except Exception as e:
126
+ logger.error(f"❌ Error finding similar words for '{topic}': {e}")
127
+ return []
128
+
129
+ async def _get_cached_fallback(self, topic: str, difficulty: str, max_words: int) -> List[Dict[str, Any]]:
130
+ """
131
+ Fallback method for compatibility with existing backend code.
132
+
133
+ Since our unified generator already has comprehensive vocabulary,
134
+ this just calls find_similar_words with relaxed criteria.
135
+ """
136
+ if not self.is_initialized or not self.generator:
137
+ return []
138
+
139
+ try:
140
+ # Try with lower similarity threshold as "fallback"
141
+ results = self.generator.generate_thematic_words(
142
+ topic,
143
+ num_words=max_words,
144
+ min_similarity=0.2 # Lower threshold for fallback
145
+ )
146
+
147
+ # Format for backend compatibility
148
+ backend_words = []
149
+ for word, similarity, tier in results:
150
+ if self.generator._matches_backend_difficulty(word, difficulty):
151
+ backend_word = {
152
+ "word": word.upper(),
153
+ "clue": self.generator._generate_simple_clue(word, topic),
154
+ "similarity": similarity,
155
+ "tier": tier
156
+ }
157
+ backend_words.append(backend_word)
158
+
159
+ logger.info(f"📦 Fallback generated {len(backend_words)} words for '{topic}'")
160
+ return backend_words[:max_words]
161
+
162
+ except Exception as e:
163
+ logger.error(f"❌ Error in cached fallback for '{topic}': {e}")
164
+ return []
165
+
166
+ def get_vocabulary_size(self) -> int:
167
+ """Get the vocabulary size."""
168
+ if self.generator:
169
+ return self.generator.get_vocabulary_size()
170
+ return 0
171
+
172
+ def get_tier_info(self) -> Dict[str, Any]:
173
+ """Get frequency tier information."""
174
+ if not self.generator:
175
+ return {}
176
+
177
+ return {
178
+ "tier_distribution": self.generator.get_tier_distribution(),
179
+ "tier_descriptions": getattr(self.generator, 'tier_descriptions', {}),
180
+ "vocabulary_size": self.generator.get_vocabulary_size()
181
+ }
182
+
183
+
184
+ # Import time for initialization timing
185
+ import time
186
+
187
+ # Factory function for easy backend integration
188
+ async def create_unified_word_service(vocab_size_limit: Optional[int] = None) -> Optional[UnifiedWordService]:
189
+ """
190
+ Factory function to create and initialize a UnifiedWordService.
191
+
192
+ Args:
193
+ vocab_size_limit: Maximum vocabulary size (None for default)
194
+
195
+ Returns:
196
+ Initialized UnifiedWordService or None if initialization failed
197
+ """
198
+ try:
199
+ service = UnifiedWordService(vocab_size_limit)
200
+
201
+ if await service.initialize():
202
+ return service
203
+ else:
204
+ logger.error("❌ Failed to initialize UnifiedWordService")
205
+ return None
206
+
207
+ except Exception as e:
208
+ logger.error(f"❌ Error creating UnifiedWordService: {e}")
209
+ return None
210
+
211
+
212
+ # Example usage for testing
213
+ async def main():
214
+ """Test the unified word service."""
215
+ print("🧪 Testing Unified Word Service")
216
+ print("=" * 50)
217
+
218
+ # Create and initialize service
219
+ service = await create_unified_word_service(vocab_size_limit=50000) # Smaller vocab for testing
220
+
221
+ if not service:
222
+ print("❌ Failed to create service")
223
+ return
224
+
225
+ # Test word generation
226
+ test_topics = ["animal", "science", "technology"]
227
+
228
+ for topic in test_topics:
229
+ print(f"\n🎯 Testing topic: '{topic}'")
230
+ print("-" * 30)
231
+
232
+ for difficulty in ["easy", "medium", "hard"]:
233
+ words = await service.find_similar_words(topic, difficulty, max_words=5)
234
+
235
+ print(f" {difficulty.capitalize()}: {len(words)} words")
236
+ for word_data in words:
237
+ word = word_data['word']
238
+ tier = word_data.get('tier', 'unknown')
239
+ print(f" {word:<12} ({tier})")
240
+
241
+ print(f"\n📊 Service Info:")
242
+ print(f" Vocabulary size: {service.get_vocabulary_size():,}")
243
+ print(f" Tier info: {service.get_tier_info()}")
244
+
245
+ print("\n✅ Test completed!")
246
+
247
+
248
+ if __name__ == "__main__":
249
+ import asyncio
250
+ asyncio.run(main())
crossword-app/backend-py/src/services/vector_search.py CHANGED
@@ -3,6 +3,7 @@ Vector similarity search service using sentence-transformers and FAISS.
3
  This implements true AI word generation via vector space nearest neighbor search.
4
  """
5
 
 
6
  import os
7
  import logging
8
  import asyncio
@@ -21,10 +22,7 @@ from pathlib import Path
21
 
22
  logger = logging.getLogger(__name__)
23
 
24
- def log_with_timestamp(message):
25
- """Helper to log with precise timestamp."""
26
- timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
27
- logger.info(f"[{timestamp}] {message}")
28
 
29
  class VectorSearchService:
30
  """
@@ -70,29 +68,29 @@ class VectorSearchService:
70
  start_time = time.time()
71
 
72
  # Log environment configuration for debugging
73
- log_with_timestamp(f"🔧 Environment Configuration:")
74
- log_with_timestamp(f" 📊 Model: {self.model_name}")
75
- log_with_timestamp(f" 🎯 Base Similarity Threshold: {self.base_similarity_threshold}")
76
- log_with_timestamp(f" 📉 Min Similarity Threshold: {self.min_similarity_threshold}")
77
- log_with_timestamp(f" 📈 Max Results: {self.max_results}")
78
- log_with_timestamp(f" 🌟 Hierarchical Search: {self.use_hierarchical_search}")
79
- log_with_timestamp(f" 🔀 Search Randomness: {os.getenv('SEARCH_RANDOMNESS', '0.02')}")
80
- log_with_timestamp(f" 💾 Cache Dir: {os.getenv('WORD_CACHE_DIR', 'auto-detect')}")
81
 
82
- log_with_timestamp(f"🔧 Loading model: {self.model_name}")
83
 
84
  # Load sentence transformer model
85
  model_start = time.time()
86
  self.model = SentenceTransformer(self.model_name)
87
  model_time = time.time() - model_start
88
- log_with_timestamp(f"✅ Model loaded in {model_time:.2f}s: {self.model_name}")
89
 
90
  # Try to load from cache first
91
  if self._load_cached_index():
92
- log_with_timestamp("🚀 Using cached FAISS index - startup accelerated!")
93
  else:
94
  # Build from scratch
95
- log_with_timestamp("🔨 Building FAISS index from scratch...")
96
 
97
  # Get model vocabulary from tokenizer
98
  vocab_start = time.time()
@@ -102,36 +100,36 @@ class VectorSearchService:
102
  # Filter vocabulary for crossword-suitable words
103
  self.vocab = self._filter_vocabulary(vocab_dict)
104
  vocab_time = time.time() - vocab_start
105
- log_with_timestamp(f"📚 Filtered vocabulary in {vocab_time:.2f}s: {len(self.vocab)} words")
106
 
107
  # Pre-compute embeddings for all vocabulary words
108
  embedding_start = time.time()
109
- log_with_timestamp("🔄 Starting embedding generation...")
110
  await self._build_embeddings_index()
111
  embedding_time = time.time() - embedding_start
112
- log_with_timestamp(f"🔄 Embeddings built in {embedding_time:.2f}s")
113
 
114
  # Save to cache for next time
115
  self._save_index_to_cache()
116
 
117
  # Initialize cache manager
118
  cache_start = time.time()
119
- log_with_timestamp("📦 Initializing word cache manager...")
120
  try:
121
  from .word_cache import WordCacheManager
122
  self.cache_manager = WordCacheManager()
123
  await self.cache_manager.initialize()
124
  cache_time = time.time() - cache_start
125
- log_with_timestamp(f"📦 Cache manager initialized in {cache_time:.2f}s")
126
  except Exception as e:
127
  cache_time = time.time() - cache_start
128
- log_with_timestamp(f"⚠️ Cache manager initialization failed in {cache_time:.2f}s: {e}")
129
- log_with_timestamp("📝 Continuing without persistent caching (in-memory only)")
130
  self.cache_manager = None
131
 
132
  self.is_initialized = True
133
  total_time = time.time() - start_time
134
- log_with_timestamp(f"✅ Vector search service fully initialized in {total_time:.2f}s")
135
 
136
  except Exception as e:
137
  logger.error(f"❌ Failed to initialize vector search: {e}")
@@ -140,7 +138,7 @@ class VectorSearchService:
140
 
141
  def _filter_vocabulary(self, vocab_dict: Dict[str, int]) -> List[str]:
142
  """Filter vocabulary to keep only crossword-suitable words."""
143
- log_with_timestamp(f"📚 Filtering {len(vocab_dict)} vocabulary words...")
144
 
145
  # Pre-compile excluded words set for faster lookup
146
  excluded_words = {
@@ -159,7 +157,7 @@ class VectorSearchService:
159
 
160
  # Progress logging for large vocabularies
161
  if processed % 10000 == 0:
162
- log_with_timestamp(f"📊 Vocabulary filtering progress: {processed}/{len(vocab_dict)}")
163
 
164
  # Clean word (remove special tokens) - optimized
165
  if word.startswith('##'):
@@ -191,7 +189,7 @@ class VectorSearchService:
191
 
192
  # Remove duplicates efficiently and sort
193
  unique_filtered = sorted(list(set(filtered)))
194
- log_with_timestamp(f"📚 Vocabulary filtered: {len(vocab_dict)} → {len(unique_filtered)} words")
195
 
196
  return unique_filtered
197
 
@@ -225,7 +223,7 @@ class VectorSearchService:
225
  cpu_count = os.cpu_count() or 1
226
  # Larger batches for better throughput, smaller for HF Spaces limited memory
227
  batch_size = min(200 if cpu_count > 2 else 100, len(self.vocab) // 4)
228
- log_with_timestamp(f"⚡ Using batch size {batch_size} with {cpu_count} CPUs")
229
 
230
  embeddings_list = []
231
  total_batches = (len(self.vocab) + batch_size - 1) // batch_size
@@ -249,24 +247,24 @@ class VectorSearchService:
249
  # Progress logging - more frequent for slower HF Spaces
250
  if batch_num % max(1, total_batches // 10) == 0:
251
  progress = (batch_num / total_batches) * 100
252
- log_with_timestamp(f"📊 Embedding progress: {progress:.1f}% ({i}/{len(self.vocab)} words)")
253
 
254
  # Combine all embeddings
255
- log_with_timestamp("🔗 Combining embeddings...")
256
  self.word_embeddings = np.vstack(embeddings_list)
257
  logger.info(f"📈 Generated embeddings shape: {self.word_embeddings.shape}")
258
 
259
  # Build FAISS index for fast similarity search
260
- log_with_timestamp("🏗️ Building FAISS index...")
261
  dimension = self.word_embeddings.shape[1]
262
  self.faiss_index = faiss.IndexFlatIP(dimension) # Inner product similarity
263
 
264
  # Normalize embeddings for cosine similarity
265
- log_with_timestamp("📏 Normalizing embeddings for cosine similarity...")
266
  faiss.normalize_L2(self.word_embeddings)
267
 
268
  # Add to FAISS index
269
- log_with_timestamp("📥 Adding embeddings to FAISS index...")
270
  self.faiss_index.add(self.word_embeddings)
271
 
272
  logger.info(f"🔍 FAISS index built with {self.faiss_index.ntotal} vectors")
@@ -316,7 +314,7 @@ class VectorSearchService:
316
 
317
  # Track these words to prevent future repetition
318
  if similar_words:
319
- self._track_used_words(topic, [word['word'] for word in similar_words])
320
 
321
  # Cache successful results for future use
322
  if similar_words:
@@ -338,7 +336,7 @@ class VectorSearchService:
338
 
339
  # Track these words to prevent future repetition
340
  if similar_words:
341
- self._track_used_words(topic, [word['word'] for word in similar_words])
342
 
343
  # If not enough words found, supplement with cached words (more aggressive)
344
  if len(similar_words) < max_words * 0.75: # If less than 75% of target, supplement
@@ -578,7 +576,8 @@ class VectorSearchService:
578
  'urban', 'rural', 'geological', 'topographical', 'cartographic']
579
  }
580
 
581
- for candidate in candidates[:10]: # Only consider top 10 for performance
 
582
  word = candidate['word'].lower()
583
  similarity = candidate['similarity']
584
 
@@ -691,6 +690,8 @@ class VectorSearchService:
691
 
692
  main_topic_candidates.extend(variation_candidates)
693
 
 
 
694
  logger.info(f"🔍 Main topic search found {len(main_topic_candidates)} candidates")
695
 
696
  # Phase 2: Identify subcategories from best candidates
@@ -919,7 +920,18 @@ class VectorSearchService:
919
  max_words: int
920
  ) -> List[Dict[str, Any]]:
921
  """
922
- Ensure diverse representation from different search sources.
 
 
 
 
 
 
 
 
 
 
 
923
  """
924
  if len(candidates) <= max_words:
925
  return candidates
@@ -1047,38 +1059,38 @@ class VectorSearchService:
1047
  """Load FAISS index from cache if available."""
1048
  try:
1049
  if not self._cache_exists():
1050
- log_with_timestamp("📁 No cached index found - will build new index")
1051
  return False
1052
 
1053
- log_with_timestamp("📁 Loading cached FAISS index...")
1054
  cache_start = time.time()
1055
 
1056
  # Load vocabulary
1057
  with open(self.vocab_cache_path, 'rb') as f:
1058
  self.vocab = pickle.load(f)
1059
- log_with_timestamp(f"📚 Loaded {len(self.vocab)} vocabulary words from cache")
1060
 
1061
  # Load embeddings
1062
  self.word_embeddings = np.load(self.embeddings_cache_path)
1063
- log_with_timestamp(f"📈 Loaded embeddings shape: {self.word_embeddings.shape}")
1064
 
1065
  # Load FAISS index
1066
  self.faiss_index = faiss.read_index(self.faiss_cache_path)
1067
- log_with_timestamp(f"🔍 Loaded FAISS index with {self.faiss_index.ntotal} vectors")
1068
 
1069
  cache_time = time.time() - cache_start
1070
- log_with_timestamp(f"✅ Successfully loaded cached index in {cache_time:.2f}s")
1071
  return True
1072
 
1073
  except Exception as e:
1074
- log_with_timestamp(f"❌ Failed to load cached index: {e}")
1075
- log_with_timestamp("🔄 Will rebuild index from scratch")
1076
  return False
1077
 
1078
  def _save_index_to_cache(self):
1079
  """Save the built FAISS index to cache for future use."""
1080
  try:
1081
- log_with_timestamp("💾 Saving FAISS index to cache...")
1082
  save_start = time.time()
1083
 
1084
  # Save vocabulary
@@ -1092,12 +1104,12 @@ class VectorSearchService:
1092
  faiss.write_index(self.faiss_index, self.faiss_cache_path)
1093
 
1094
  save_time = time.time() - save_start
1095
- log_with_timestamp(f"✅ Index cached successfully in {save_time:.2f}s")
1096
- log_with_timestamp(f"📁 Cache location: {self.index_cache_dir}")
1097
 
1098
  except Exception as e:
1099
- log_with_timestamp(f"⚠️ Failed to cache index: {e}")
1100
- log_with_timestamp("📝 Continuing without caching (performance will be slower next startup)")
1101
 
1102
  def _is_topic_relevant(self, word: str, topic: str) -> bool:
1103
  """
@@ -1182,16 +1194,16 @@ class VectorSearchService:
1182
  # Filter by difficulty and quality
1183
  if self._matches_difficulty(word, difficulty):
1184
  difficulty_passed += 1
1185
- if self._is_interesting_word(word, topic) and self._is_topic_relevant(word, topic):
1186
- interesting_passed += 1
1187
- candidates.append({
1188
- "word": word,
1189
- "clue": self._generate_clue(word, topic),
1190
- "similarity": float(score),
1191
- "source": "vector_search"
1192
- })
1193
- else:
1194
- rejected_words.append(f"{word}({score:.3f})")
1195
  else:
1196
  rejected_words.append(f"{word}({score:.3f})")
1197
 
@@ -1341,58 +1353,49 @@ class VectorSearchService:
1341
  "animals": [
1342
  {"word": "DOG", "clue": "Man's best friend"},
1343
  {"word": "CAT", "clue": "Feline pet"},
1344
- {"word": "ELEPHANT", "clue": "Large mammal with trunk"},
1345
- {"word": "TIGER", "clue": "Striped big cat"},
1346
- {"word": "BIRD", "clue": "Flying creature"},
1347
  {"word": "FISH", "clue": "Aquatic animal"},
1348
- {"word": "HORSE", "clue": "Riding animal"},
1349
- {"word": "BEAR", "clue": "Large mammal"},
1350
- {"word": "WHALE", "clue": "Marine mammal"},
1351
- {"word": "LION", "clue": "King of jungle"},
1352
- {"word": "RABBIT", "clue": "Hopping mammal"},
1353
- {"word": "SNAKE", "clue": "Slithering reptile"}
1354
  ],
1355
  "science": [
1356
- {"word": "ATOM", "clue": "Basic unit of matter"},
1357
- {"word": "CELL", "clue": "Basic unit of life"},
1358
- {"word": "DNA", "clue": "Genetic material"},
1359
- {"word": "ENERGY", "clue": "Capacity to do work"},
1360
- {"word": "FORCE", "clue": "Push or pull"},
1361
- {"word": "GRAVITY", "clue": "Force of attraction"},
1362
- {"word": "LIGHT", "clue": "Electromagnetic radiation"},
1363
- {"word": "MATTER", "clue": "Physical substance"},
1364
- {"word": "MOTION", "clue": "Change in position"},
1365
- {"word": "OXYGEN", "clue": "Essential gas"},
1366
- {"word": "PHYSICS", "clue": "Study of matter and energy"},
1367
- {"word": "THEORY", "clue": "Scientific explanation"}
1368
  ],
1369
  "technology": [
1370
- {"word": "COMPUTER", "clue": "Electronic device"},
1371
- {"word": "INTERNET", "clue": "Global network"},
1372
- {"word": "SOFTWARE", "clue": "Computer programs"},
1373
- {"word": "ROBOT", "clue": "Automated machine"},
1374
- {"word": "DATA", "clue": "Information"},
1375
- {"word": "CODE", "clue": "Programming instructions"},
1376
- {"word": "DIGITAL", "clue": "Electronic format"},
1377
- {"word": "NETWORK", "clue": "Connected systems"},
1378
- {"word": "SYSTEM", "clue": "Organized whole"},
1379
- {"word": "DEVICE", "clue": "Technical apparatus"},
1380
- {"word": "MOBILE", "clue": "Portable technology"},
1381
- {"word": "SCREEN", "clue": "Display surface"}
1382
  ],
1383
  "geography": [
1384
- {"word": "MOUNTAIN", "clue": "High landform"},
1385
- {"word": "RIVER", "clue": "Flowing water"},
1386
- {"word": "OCEAN", "clue": "Large body of water"},
1387
- {"word": "DESERT", "clue": "Arid region"},
1388
- {"word": "FOREST", "clue": "Dense trees"},
1389
- {"word": "ISLAND", "clue": "Land surrounded by water"},
1390
- {"word": "VALLEY", "clue": "Low area between hills"},
1391
- {"word": "LAKE", "clue": "Inland water body"},
1392
- {"word": "COAST", "clue": "Land by the sea"},
1393
- {"word": "PLAIN", "clue": "Flat land"},
1394
- {"word": "HILL", "clue": "Small elevation"},
1395
- {"word": "CLIFF", "clue": "Steep rock face"}
1396
  ]
1397
  }
1398
 
@@ -1441,4 +1444,4 @@ class VectorSearchService:
1441
  del self.faiss_index
1442
  if self.cache_manager:
1443
  await self.cache_manager.cleanup_expired_caches()
1444
- self.is_initialized = False
 
3
  This implements true AI word generation via vector space nearest neighbor search.
4
  """
5
 
6
+ from math import log
7
  import os
8
  import logging
9
  import asyncio
 
22
 
23
  logger = logging.getLogger(__name__)
24
 
25
+ # All logging now uses standard logger with filename/line numbers
 
 
 
26
 
27
  class VectorSearchService:
28
  """
 
68
  start_time = time.time()
69
 
70
  # Log environment configuration for debugging
71
+ logger.info(f"🔧 Environment Configuration:")
72
+ logger.info(f" 📊 Model: {self.model_name}")
73
+ logger.info(f" 🎯 Base Similarity Threshold: {self.base_similarity_threshold}")
74
+ logger.info(f" 📉 Min Similarity Threshold: {self.min_similarity_threshold}")
75
+ logger.info(f" 📈 Max Results: {self.max_results}")
76
+ logger.info(f" 🌟 Hierarchical Search: {self.use_hierarchical_search}")
77
+ logger.info(f" 🔀 Search Randomness: {os.getenv('SEARCH_RANDOMNESS', '0.02')}")
78
+ logger.info(f" 💾 Cache Dir: {os.getenv('WORD_CACHE_DIR', 'auto-detect')}")
79
 
80
+ logger.info(f"🔧 Loading model: {self.model_name}")
81
 
82
  # Load sentence transformer model
83
  model_start = time.time()
84
  self.model = SentenceTransformer(self.model_name)
85
  model_time = time.time() - model_start
86
+ logger.info(f"✅ Model loaded in {model_time:.2f}s: {self.model_name}")
87
 
88
  # Try to load from cache first
89
  if self._load_cached_index():
90
+ logger.info("🚀 Using cached FAISS index - startup accelerated!")
91
  else:
92
  # Build from scratch
93
+ logger.info("🔨 Building FAISS index from scratch...")
94
 
95
  # Get model vocabulary from tokenizer
96
  vocab_start = time.time()
 
100
  # Filter vocabulary for crossword-suitable words
101
  self.vocab = self._filter_vocabulary(vocab_dict)
102
  vocab_time = time.time() - vocab_start
103
+ logger.info(f"📚 Filtered vocabulary in {vocab_time:.2f}s: {len(self.vocab)} words")
104
 
105
  # Pre-compute embeddings for all vocabulary words
106
  embedding_start = time.time()
107
+ logger.info("🔄 Starting embedding generation...")
108
  await self._build_embeddings_index()
109
  embedding_time = time.time() - embedding_start
110
+ logger.info(f"🔄 Embeddings built in {embedding_time:.2f}s")
111
 
112
  # Save to cache for next time
113
  self._save_index_to_cache()
114
 
115
  # Initialize cache manager
116
  cache_start = time.time()
117
+ logger.info("📦 Initializing word cache manager...")
118
  try:
119
  from .word_cache import WordCacheManager
120
  self.cache_manager = WordCacheManager()
121
  await self.cache_manager.initialize()
122
  cache_time = time.time() - cache_start
123
+ logger.info(f"📦 Cache manager initialized in {cache_time:.2f}s")
124
  except Exception as e:
125
  cache_time = time.time() - cache_start
126
+ logger.info(f"⚠️ Cache manager initialization failed in {cache_time:.2f}s: {e}")
127
+ logger.info("📝 Continuing without persistent caching (in-memory only)")
128
  self.cache_manager = None
129
 
130
  self.is_initialized = True
131
  total_time = time.time() - start_time
132
+ logger.info(f"✅ Vector search service fully initialized in {total_time:.2f}s")
133
 
134
  except Exception as e:
135
  logger.error(f"❌ Failed to initialize vector search: {e}")
 
138
 
139
  def _filter_vocabulary(self, vocab_dict: Dict[str, int]) -> List[str]:
140
  """Filter vocabulary to keep only crossword-suitable words."""
141
+ logger.info(f"📚 Filtering {len(vocab_dict)} vocabulary words...")
142
 
143
  # Pre-compile excluded words set for faster lookup
144
  excluded_words = {
 
157
 
158
  # Progress logging for large vocabularies
159
  if processed % 10000 == 0:
160
+ logger.info(f"📊 Vocabulary filtering progress: {processed}/{len(vocab_dict)}")
161
 
162
  # Clean word (remove special tokens) - optimized
163
  if word.startswith('##'):
 
189
 
190
  # Remove duplicates efficiently and sort
191
  unique_filtered = sorted(list(set(filtered)))
192
+ logger.info(f"📚 Vocabulary filtered: {len(vocab_dict)} → {len(unique_filtered)} words")
193
 
194
  return unique_filtered
195
 
 
223
  cpu_count = os.cpu_count() or 1
224
  # Larger batches for better throughput, smaller for HF Spaces limited memory
225
  batch_size = min(200 if cpu_count > 2 else 100, len(self.vocab) // 4)
226
+ logger.info(f"⚡ Using batch size {batch_size} with {cpu_count} CPUs")
227
 
228
  embeddings_list = []
229
  total_batches = (len(self.vocab) + batch_size - 1) // batch_size
 
247
  # Progress logging - more frequent for slower HF Spaces
248
  if batch_num % max(1, total_batches // 10) == 0:
249
  progress = (batch_num / total_batches) * 100
250
+ logger.info(f"📊 Embedding progress: {progress:.1f}% ({i}/{len(self.vocab)} words)")
251
 
252
  # Combine all embeddings
253
+ logger.info("🔗 Combining embeddings...")
254
  self.word_embeddings = np.vstack(embeddings_list)
255
  logger.info(f"📈 Generated embeddings shape: {self.word_embeddings.shape}")
256
 
257
  # Build FAISS index for fast similarity search
258
+ logger.info("🏗️ Building FAISS index...")
259
  dimension = self.word_embeddings.shape[1]
260
  self.faiss_index = faiss.IndexFlatIP(dimension) # Inner product similarity
261
 
262
  # Normalize embeddings for cosine similarity
263
+ logger.info("📏 Normalizing embeddings for cosine similarity...")
264
  faiss.normalize_L2(self.word_embeddings)
265
 
266
  # Add to FAISS index
267
+ logger.info("📥 Adding embeddings to FAISS index...")
268
  self.faiss_index.add(self.word_embeddings)
269
 
270
  logger.info(f"🔍 FAISS index built with {self.faiss_index.ntotal} vectors")
 
314
 
315
  # Track these words to prevent future repetition
316
  if similar_words:
317
+ self._track_used_words(topic, similar_words)
318
 
319
  # Cache successful results for future use
320
  if similar_words:
 
336
 
337
  # Track these words to prevent future repetition
338
  if similar_words:
339
+ self._track_used_words(topic, similar_words)
340
 
341
  # If not enough words found, supplement with cached words (more aggressive)
342
  if len(similar_words) < max_words * 0.75: # If less than 75% of target, supplement
 
576
  'urban', 'rural', 'geological', 'topographical', 'cartographic']
577
  }
578
 
579
+ # for candidate in candidates[:10]: # Only consider top 10 for performance
580
+ for candidate in candidates: # Only consider top 10 for performance
581
  word = candidate['word'].lower()
582
  similarity = candidate['similarity']
583
 
 
690
 
691
  main_topic_candidates.extend(variation_candidates)
692
 
693
+ if len(main_topic_candidates) <= 10:
694
+ logger.info(f"🔍 Main topic search found candidates: {main_topic_candidates}")
695
  logger.info(f"🔍 Main topic search found {len(main_topic_candidates)} candidates")
696
 
697
  # Phase 2: Identify subcategories from best candidates
 
920
  max_words: int
921
  ) -> List[Dict[str, Any]]:
922
  """
923
+ Balance word selection across different search sources for optimal variety.
924
+
925
+ Allocates selection quotas to ensure representation from main topic searches
926
+ and subcategory searches, preventing over-concentration from any single source
927
+ while maintaining quality standards.
928
+
929
+ Args:
930
+ candidates: Word candidates with search source metadata
931
+ max_words: Target number of words to select
932
+
933
+ Returns:
934
+ Balanced selection ensuring source diversity
935
  """
936
  if len(candidates) <= max_words:
937
  return candidates
 
1059
  """Load FAISS index from cache if available."""
1060
  try:
1061
  if not self._cache_exists():
1062
+ logger.info("📁 No cached index found - will build new index")
1063
  return False
1064
 
1065
+ logger.info("📁 Loading cached FAISS index...")
1066
  cache_start = time.time()
1067
 
1068
  # Load vocabulary
1069
  with open(self.vocab_cache_path, 'rb') as f:
1070
  self.vocab = pickle.load(f)
1071
+ logger.info(f"📚 Loaded {len(self.vocab)} vocabulary words from cache")
1072
 
1073
  # Load embeddings
1074
  self.word_embeddings = np.load(self.embeddings_cache_path)
1075
+ logger.info(f"📈 Loaded embeddings shape: {self.word_embeddings.shape}")
1076
 
1077
  # Load FAISS index
1078
  self.faiss_index = faiss.read_index(self.faiss_cache_path)
1079
+ logger.info(f"🔍 Loaded FAISS index with {self.faiss_index.ntotal} vectors")
1080
 
1081
  cache_time = time.time() - cache_start
1082
+ logger.info(f"✅ Successfully loaded cached index in {cache_time:.2f}s")
1083
  return True
1084
 
1085
  except Exception as e:
1086
+ logger.info(f"❌ Failed to load cached index: {e}")
1087
+ logger.info("🔄 Will rebuild index from scratch")
1088
  return False
1089
 
1090
  def _save_index_to_cache(self):
1091
  """Save the built FAISS index to cache for future use."""
1092
  try:
1093
+ logger.info("💾 Saving FAISS index to cache...")
1094
  save_start = time.time()
1095
 
1096
  # Save vocabulary
 
1104
  faiss.write_index(self.faiss_index, self.faiss_cache_path)
1105
 
1106
  save_time = time.time() - save_start
1107
+ logger.info(f"✅ Index cached successfully in {save_time:.2f}s")
1108
+ logger.info(f"📁 Cache location: {self.index_cache_dir}")
1109
 
1110
  except Exception as e:
1111
+ logger.info(f"⚠️ Failed to cache index: {e}")
1112
+ logger.info("📝 Continuing without caching (performance will be slower next startup)")
1113
 
1114
  def _is_topic_relevant(self, word: str, topic: str) -> bool:
1115
  """
 
1194
  # Filter by difficulty and quality
1195
  if self._matches_difficulty(word, difficulty):
1196
  difficulty_passed += 1
1197
+ # if self._is_interesting_word(word, topic) and self._is_topic_relevant(word, topic):
1198
+ interesting_passed += 1
1199
+ candidates.append({
1200
+ "word": word,
1201
+ "clue": self._generate_clue(word, topic),
1202
+ "similarity": float(score),
1203
+ "source": "vector_search"
1204
+ })
1205
+ # else:
1206
+ # rejected_words.append(f"{word}({score:.3f})")
1207
  else:
1208
  rejected_words.append(f"{word}({score:.3f})")
1209
 
 
1353
  "animals": [
1354
  {"word": "DOG", "clue": "Man's best friend"},
1355
  {"word": "CAT", "clue": "Feline pet"},
 
 
 
1356
  {"word": "FISH", "clue": "Aquatic animal"},
 
 
 
 
 
 
1357
  ],
1358
  "science": [
1359
+ # {"word": "ATOM", "clue": "Basic unit of matter"},
1360
+ # {"word": "CELL", "clue": "Basic unit of life"},
1361
+ # {"word": "DNA", "clue": "Genetic material"},
1362
+ # {"word": "ENERGY", "clue": "Capacity to do work"},
1363
+ # {"word": "FORCE", "clue": "Push or pull"},
1364
+ # {"word": "GRAVITY", "clue": "Force of attraction"},
1365
+ # {"word": "LIGHT", "clue": "Electromagnetic radiation"},
1366
+ # {"word": "MATTER", "clue": "Physical substance"},
1367
+ # {"word": "MOTION", "clue": "Change in position"},
1368
+ # {"word": "OXYGEN", "clue": "Essential gas"},
1369
+ # {"word": "PHYSICS", "clue": "Study of matter and energy"},
1370
+ # {"word": "THEORY", "clue": "Scientific explanation"}
1371
  ],
1372
  "technology": [
1373
+ # {"word": "COMPUTER", "clue": "Electronic device"},
1374
+ # {"word": "INTERNET", "clue": "Global network"},
1375
+ # {"word": "SOFTWARE", "clue": "Computer programs"},
1376
+ # {"word": "ROBOT", "clue": "Automated machine"},
1377
+ # {"word": "DATA", "clue": "Information"},
1378
+ # {"word": "CODE", "clue": "Programming instructions"},
1379
+ # {"word": "DIGITAL", "clue": "Electronic format"},
1380
+ # {"word": "NETWORK", "clue": "Connected systems"},
1381
+ # {"word": "SYSTEM", "clue": "Organized whole"},
1382
+ # {"word": "DEVICE", "clue": "Technical apparatus"},
1383
+ # {"word": "MOBILE", "clue": "Portable technology"},
1384
+ # {"word": "SCREEN", "clue": "Display surface"}
1385
  ],
1386
  "geography": [
1387
+ # {"word": "MOUNTAIN", "clue": "High landform"},
1388
+ # {"word": "RIVER", "clue": "Flowing water"},
1389
+ # {"word": "OCEAN", "clue": "Large body of water"},
1390
+ # {"word": "DESERT", "clue": "Arid region"},
1391
+ # {"word": "FOREST", "clue": "Dense trees"},
1392
+ # {"word": "ISLAND", "clue": "Land surrounded by water"},
1393
+ # {"word": "VALLEY", "clue": "Low area between hills"},
1394
+ # {"word": "LAKE", "clue": "Inland water body"},
1395
+ # {"word": "COAST", "clue": "Land by the sea"},
1396
+ # {"word": "PLAIN", "clue": "Flat land"},
1397
+ # {"word": "HILL", "clue": "Small elevation"},
1398
+ # {"word": "CLIFF", "clue": "Steep rock face"}
1399
  ]
1400
  }
1401
 
 
1444
  del self.faiss_index
1445
  if self.cache_manager:
1446
  await self.cache_manager.cleanup_expired_caches()
1447
+ self.is_initialized = False
crossword-app/backend-py/src/services/wordnet_clue_generator.py ADDED
@@ -0,0 +1,640 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ WordNet-Based Clue Generator for Crossword Puzzles
4
+
5
+ Uses NLTK WordNet to generate crossword clues by analyzing word definitions,
6
+ synonyms, hypernyms, and semantic relationships. Integrated with the thematic
7
+ word generator for complete crossword creation without API dependencies.
8
+
9
+ Features:
10
+ - WordNet-based clue generation using definitions and relationships
11
+ - Integration with UnifiedThematicWordGenerator for word discovery
12
+ - Interactive mode with topic-based generation
13
+ - Multiple clue styles (definition, synonym, category, descriptive)
14
+ - Difficulty-based clue complexity
15
+ - Caching for improved performance
16
+ """
17
+
18
+ import os
19
+ import sys
20
+ import re
21
+ import time
22
+ import logging
23
+ from typing import List, Dict, Optional, Tuple, Set, Any
24
+ from pathlib import Path
25
+ from dataclasses import dataclass
26
+ from collections import defaultdict
27
+ import random
28
+
29
+ # NLTK imports
30
+ try:
31
+ import nltk
32
+ from nltk.corpus import wordnet as wn
33
+ from nltk.stem import WordNetLemmatizer
34
+ NLTK_AVAILABLE = True
35
+ except ImportError:
36
+ print("❌ NLTK not available. Install with: pip install nltk")
37
+ NLTK_AVAILABLE = False
38
+
39
+ # Add hack directory to path for imports
40
+ sys.path.insert(0, str(Path(__file__).parent))
41
+
42
+ try:
43
+ from .thematic_word_service import ThematicWordService as UnifiedThematicWordGenerator
44
+ THEMATIC_AVAILABLE = True
45
+ except ImportError as e:
46
+ print(f"❌ Thematic generator import error: {e}")
47
+ THEMATIC_AVAILABLE = False
48
+
49
+ # Set up logging
50
+ logging.basicConfig(
51
+ level=logging.INFO,
52
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
53
+ )
54
+ logger = logging.getLogger(__name__)
55
+
56
+
57
+ @dataclass
58
+ class WordNetClueEntry:
59
+ """Complete crossword entry with WordNet-generated clue and metadata."""
60
+ word: str
61
+ clue: str
62
+ topic: str
63
+ similarity_score: float
64
+ frequency_tier: str
65
+ tier_description: str
66
+ clue_type: str # definition, synonym, hypernym, etc.
67
+ synset_info: Optional[str] = None
68
+ definition_source: Optional[str] = None
69
+
70
+
71
+ def ensure_nltk_data(nltk_data_dir: Optional[str] = None):
72
+ """Ensure required NLTK data is downloaded to specified directory.
73
+
74
+ Args:
75
+ nltk_data_dir: Custom directory for NLTK data. If None, uses default.
76
+ """
77
+ if not NLTK_AVAILABLE:
78
+ return False
79
+
80
+ # Set up custom NLTK data directory
81
+ if nltk_data_dir:
82
+ nltk_data_path = Path(nltk_data_dir)
83
+ nltk_data_path.mkdir(parents=True, exist_ok=True)
84
+
85
+ # Add custom path to NLTK search path (at the beginning for priority)
86
+ if str(nltk_data_path) not in nltk.data.path:
87
+ nltk.data.path.insert(0, str(nltk_data_path))
88
+ logger.info(f"📂 Added NLTK data path: {nltk_data_path}")
89
+
90
+ # Map corpus names to their actual directory paths
91
+ corpus_paths = {
92
+ 'wordnet': 'corpora/wordnet',
93
+ 'omw-1.4': 'corpora/omw-1.4',
94
+ 'punkt': 'tokenizers/punkt',
95
+ 'averaged_perceptron_tagger': 'taggers/averaged_perceptron_tagger'
96
+ }
97
+
98
+ required_corpora = ['wordnet', 'punkt', 'averaged_perceptron_tagger', 'omw-1.4']
99
+
100
+ for corpus in required_corpora:
101
+ corpus_path = corpus_paths[corpus]
102
+
103
+ try:
104
+ # Try to find corpus in current search paths
105
+ found_corpus = nltk.data.find(corpus_path)
106
+ logger.info(f"✅ Found {corpus} at: {found_corpus}")
107
+ except LookupError:
108
+ # Check if it exists in our custom directory
109
+ if nltk_data_dir:
110
+ local_corpus_path = Path(nltk_data_dir) / corpus_path
111
+ if local_corpus_path.exists():
112
+ logger.info(f"✅ Found {corpus} locally at: {local_corpus_path}")
113
+ continue
114
+
115
+ # Only download if not found anywhere
116
+ logger.warning(f"❌ {corpus} not found, attempting download...")
117
+ try:
118
+ if nltk_data_dir:
119
+ # Download to custom directory
120
+ logger.info(f"📥 Downloading {corpus} to: {nltk_data_dir}")
121
+ nltk.download(corpus, download_dir=nltk_data_dir, quiet=False)
122
+ logger.info(f"✅ Downloaded {corpus} to: {nltk_data_dir}")
123
+ else:
124
+ # Download to default directory
125
+ logger.info(f"📥 Downloading {corpus} to default location")
126
+ nltk.download(corpus, quiet=False)
127
+ logger.info(f"✅ Downloaded {corpus} to default location")
128
+ except Exception as e:
129
+ logger.warning(f"⚠️ Failed to download {corpus}: {e}")
130
+ return False
131
+
132
+ return True
133
+
134
+
135
+ class WordNetClueGenerator:
136
+ """
137
+ WordNet-based clue generator that creates crossword clues using semantic
138
+ relationships and definitions from the WordNet lexical database.
139
+ """
140
+
141
+ def __init__(self, cache_dir: Optional[str] = None):
142
+ """Initialize WordNet clue generator.
143
+
144
+ Args:
145
+ cache_dir: Directory for caching (used for both model cache and NLTK data)
146
+ """
147
+ self.cache_dir = cache_dir or str(Path(__file__).parent / 'model_cache')
148
+ self.nltk_data_dir = str(Path(self.cache_dir) / 'nltk_data')
149
+ self.lemmatizer = None
150
+ self.clue_cache = {}
151
+ self.is_initialized = False
152
+
153
+ # Simple clue generation using definition concatenation
154
+
155
+ # Words to avoid in clues (common words that don't make good clues)
156
+ self.avoid_words = {
157
+ 'thing', 'stuff', 'item', 'object', 'entity', 'something', 'anything',
158
+ 'person', 'people', 'someone', 'anyone', 'somebody', 'anybody',
159
+ 'place', 'location', 'somewhere', 'anywhere', 'area', 'spot',
160
+ 'time', 'moment', 'period', 'while', 'when', 'then',
161
+ 'way', 'manner', 'method', 'means', 'how', 'what', 'which'
162
+ }
163
+
164
+ def initialize(self):
165
+ """Initialize the WordNet clue generator."""
166
+ if self.is_initialized:
167
+ return True
168
+
169
+ if not NLTK_AVAILABLE:
170
+ logger.error("❌ NLTK not available - cannot initialize WordNet generator")
171
+ return False
172
+
173
+ logger.info("🚀 Initializing WordNet Clue Generator...")
174
+ logger.info(f"📂 Using cache directory: {self.cache_dir}")
175
+ logger.info(f"📂 Using NLTK data directory: {self.nltk_data_dir}")
176
+ start_time = time.time()
177
+
178
+ # Ensure NLTK data is available in cache directory
179
+ if not ensure_nltk_data(self.nltk_data_dir):
180
+ logger.error("❌ Failed to download required NLTK data")
181
+ return False
182
+
183
+ # Initialize lemmatizer
184
+ try:
185
+ self.lemmatizer = WordNetLemmatizer()
186
+ logger.info("✅ WordNet lemmatizer initialized")
187
+ except Exception as e:
188
+ logger.error(f"❌ Failed to initialize lemmatizer: {e}")
189
+ return False
190
+
191
+ self.is_initialized = True
192
+ init_time = time.time() - start_time
193
+ logger.info(f"✅ WordNet clue generator ready in {init_time:.2f}s")
194
+
195
+ return True
196
+
197
+ def generate_clue(self, word: str, topic: str = "", clue_style: str = "auto",
198
+ difficulty: str = "medium") -> str:
199
+ """Generate a crossword clue using WordNet definitions.
200
+
201
+ Args:
202
+ word: Target word for clue generation
203
+ topic: Topic context (for fallback only)
204
+ clue_style: Ignored - kept for compatibility
205
+ difficulty: Ignored - kept for compatibility
206
+
207
+ Returns:
208
+ Generated crossword clue
209
+ """
210
+ if not self.is_initialized:
211
+ if not self.initialize():
212
+ return f"Related to {topic}" if topic else "Crossword answer"
213
+
214
+ word_clean = word.lower().strip()
215
+
216
+ # Get synsets
217
+ synsets = wn.synsets(word_clean)
218
+ if not synsets:
219
+ return f"Related to {topic}" if topic else "Crossword answer"
220
+
221
+ # Limit to max 3 synsets, randomly select if more than 3
222
+ if len(synsets) > 3:
223
+ import random
224
+ synsets = random.sample(synsets, 3)
225
+
226
+ # Get all definitions and filter out those containing the target word
227
+ definitions = []
228
+ word_variants = {
229
+ word_clean,
230
+ word_clean + 's',
231
+ word_clean + 'ing',
232
+ word_clean + 'ed',
233
+ word_clean + 'er',
234
+ word_clean + 'ly'
235
+ }
236
+
237
+ for syn in synsets:
238
+ definition = syn.definition()
239
+ definition_lower = definition.lower()
240
+
241
+ # Check if any variant of the target word appears in the definition
242
+ contains_target = False
243
+ for variant in word_variants:
244
+ if f" {variant} " in f" {definition_lower} " or definition_lower.startswith(variant + " "):
245
+ contains_target = True
246
+ break
247
+
248
+ # Only include definitions that don't contain the target word
249
+ if not contains_target:
250
+ definitions.append(definition)
251
+
252
+ # If no clean definitions found, return fallback
253
+ if not definitions:
254
+ return f"Related to {topic}" if topic else "Crossword answer"
255
+
256
+ # Concatenate clean definitions
257
+ clue = "; ".join(definitions)
258
+
259
+ return clue
260
+
261
+ def _generate_fallback_clue(self, word: str, topic: str) -> str:
262
+ """Generate fallback clue when WordNet fails."""
263
+ if topic:
264
+ return f"Related to {topic}"
265
+ return "Crossword answer"
266
+
267
+
268
+ def get_clue_info(self, word: str) -> Dict[str, Any]:
269
+ """Get detailed information about WordNet data for a word."""
270
+ if not self.is_initialized:
271
+ return {"error": "Generator not initialized"}
272
+
273
+ word_clean = word.lower().strip()
274
+ synsets = self._get_synsets(word_clean)
275
+
276
+ info = {
277
+ "word": word,
278
+ "synsets_count": len(synsets),
279
+ "synsets": []
280
+ }
281
+
282
+ for synset in synsets[:3]: # Top 3 synsets
283
+ synset_info = {
284
+ "name": synset.name(),
285
+ "pos": synset.pos(),
286
+ "definition": synset.definition(),
287
+ "examples": synset.examples()[:2],
288
+ "hypernyms": [h.name() for h in synset.hypernyms()[:2]],
289
+ "synonyms": [l.name().replace('_', ' ') for l in synset.lemmas()[:3]]
290
+ }
291
+ info["synsets"].append(synset_info)
292
+
293
+ return info
294
+
295
+
296
+ class IntegratedWordNetCrosswordGenerator:
297
+ """
298
+ Complete crossword generation system using WordNet clues and thematic word discovery.
299
+ """
300
+
301
+ def __init__(self, vocab_size_limit: Optional[int] = None, cache_dir: Optional[str] = None):
302
+ """Initialize the integrated WordNet crossword generator.
303
+
304
+ Args:
305
+ vocab_size_limit: Maximum vocabulary size for thematic generator
306
+ cache_dir: Cache directory for models and data
307
+ """
308
+ self.cache_dir = cache_dir or str(Path(__file__).parent / 'model_cache')
309
+ self.vocab_size_limit = vocab_size_limit or 50000
310
+
311
+ # Initialize components
312
+ self.thematic_generator = None
313
+ self.clue_generator = None
314
+ self.is_initialized = False
315
+
316
+ # Stats
317
+ self.stats = {
318
+ 'words_discovered': 0,
319
+ 'clues_generated': 0,
320
+ 'cache_hits': 0,
321
+ 'total_time': 0.0
322
+ }
323
+
324
+ def initialize(self):
325
+ """Initialize both generators."""
326
+ if self.is_initialized:
327
+ return True
328
+
329
+ start_time = time.time()
330
+ logger.info("🚀 Initializing Integrated WordNet Crossword Generator...")
331
+
332
+ success = True
333
+
334
+ # Initialize WordNet clue generator with consistent cache directory
335
+ logger.info("🔄 Initializing WordNet clue generator...")
336
+ self.clue_generator = WordNetClueGenerator(self.cache_dir)
337
+ if not self.clue_generator.initialize():
338
+ logger.error("❌ Failed to initialize WordNet clue generator")
339
+ success = False
340
+ else:
341
+ logger.info("✅ WordNet clue generator ready")
342
+ logger.info(f"📂 NLTK data stored in: {self.clue_generator.nltk_data_dir}")
343
+
344
+ # Initialize thematic word generator
345
+ if THEMATIC_AVAILABLE:
346
+ logger.info("🔄 Initializing thematic word generator...")
347
+ try:
348
+ self.thematic_generator = UnifiedThematicWordGenerator(
349
+ cache_dir=self.cache_dir,
350
+ vocab_size_limit=self.vocab_size_limit
351
+ )
352
+ self.thematic_generator.initialize()
353
+ logger.info(f"✅ Thematic generator ready ({self.thematic_generator.get_vocabulary_size():,} words)")
354
+ except Exception as e:
355
+ logger.error(f"❌ Failed to initialize thematic generator: {e}")
356
+ success = False
357
+ else:
358
+ logger.warning("⚠️ Thematic generator not available - limited word discovery")
359
+
360
+ self.is_initialized = success
361
+ init_time = time.time() - start_time
362
+ logger.info(f"{'✅' if success else '❌'} Initialization {'completed' if success else 'failed'} in {init_time:.2f}s")
363
+
364
+ return success
365
+
366
+ def generate_crossword_entries(self, topic: str, num_words: int = 15,
367
+ difficulty: str = "medium", clue_style: str = "auto") -> List[WordNetClueEntry]:
368
+ """Generate complete crossword entries for a topic.
369
+
370
+ Args:
371
+ topic: Topic for word generation
372
+ num_words: Number of entries to generate
373
+ difficulty: Difficulty level ('easy', 'medium', 'hard')
374
+ clue_style: Clue generation style
375
+
376
+ Returns:
377
+ List of WordNetClueEntry objects
378
+ """
379
+ if not self.is_initialized:
380
+ if not self.initialize():
381
+ return []
382
+
383
+ start_time = time.time()
384
+ logger.info(f"🎯 Generating {num_words} crossword entries for '{topic}' (difficulty: {difficulty})")
385
+
386
+ # Generate thematic words
387
+ if self.thematic_generator:
388
+ try:
389
+ # Get more words than needed for better selection
390
+ word_results = self.thematic_generator.generate_thematic_words(
391
+ inputs=topic,
392
+ num_words=num_words * 2,
393
+ min_similarity=0.2
394
+ )
395
+ self.stats['words_discovered'] += len(word_results)
396
+ except Exception as e:
397
+ logger.error(f"❌ Word generation failed: {e}")
398
+ word_results = []
399
+ else:
400
+ # Fallback: use some common words related to topic
401
+ word_results = [(topic.upper(), 0.9, "tier_5_common")]
402
+
403
+ if not word_results:
404
+ logger.warning(f"⚠️ No words found for topic '{topic}'")
405
+ return []
406
+
407
+ # Generate clues for words
408
+ entries = []
409
+ for word, similarity, tier in word_results[:num_words]:
410
+ try:
411
+ clue = self.clue_generator.generate_clue(
412
+ word=word,
413
+ topic=topic,
414
+ clue_style=clue_style,
415
+ difficulty=difficulty
416
+ )
417
+
418
+ if clue:
419
+ tier_desc = self._get_tier_description(tier)
420
+ entry = WordNetClueEntry(
421
+ word=word.upper(),
422
+ clue=clue,
423
+ topic=topic,
424
+ similarity_score=similarity,
425
+ frequency_tier=tier,
426
+ tier_description=tier_desc,
427
+ clue_type=clue_style
428
+ )
429
+ entries.append(entry)
430
+ self.stats['clues_generated'] += 1
431
+
432
+ except Exception as e:
433
+ logger.error(f"❌ Failed to generate clue for '{word}': {e}")
434
+
435
+ # Sort by similarity and limit results
436
+ entries.sort(key=lambda x: x.similarity_score, reverse=True)
437
+ final_entries = entries[:num_words]
438
+
439
+ total_time = time.time() - start_time
440
+ self.stats['total_time'] += total_time
441
+
442
+ logger.info(f"✅ Generated {len(final_entries)} entries in {total_time:.2f}s")
443
+ return final_entries
444
+
445
+ def _get_tier_description(self, tier: str) -> str:
446
+ """Get tier description from thematic generator or provide default."""
447
+ if self.thematic_generator and hasattr(self.thematic_generator, 'tier_descriptions'):
448
+ return self.thematic_generator.tier_descriptions.get(tier, tier)
449
+ return tier.replace('_', ' ').title()
450
+
451
+ def get_stats(self) -> Dict[str, Any]:
452
+ """Get generation statistics."""
453
+ return {
454
+ **self.stats,
455
+ 'thematic_available': self.thematic_generator is not None,
456
+ 'wordnet_available': self.clue_generator is not None and self.clue_generator.is_initialized,
457
+ 'vocab_size': self.thematic_generator.get_vocabulary_size() if self.thematic_generator else 0
458
+ }
459
+
460
+
461
+ def main():
462
+ """Interactive WordNet crossword generator."""
463
+ if not NLTK_AVAILABLE:
464
+ print("❌ NLTK not available. Please install with: pip install nltk")
465
+ return
466
+
467
+ print("🚀 WordNet Crossword Generator")
468
+ print("=" * 60)
469
+ print("Using NLTK WordNet for clue generation + thematic word discovery")
470
+
471
+ # Initialize generator
472
+ cache_dir = str(Path(__file__).parent / 'model_cache')
473
+ generator = IntegratedWordNetCrosswordGenerator(
474
+ vocab_size_limit=50000,
475
+ cache_dir=cache_dir
476
+ )
477
+
478
+ print("\n🔄 Initializing system...")
479
+ if not generator.initialize():
480
+ print("❌ Failed to initialize system")
481
+ return
482
+
483
+ stats = generator.get_stats()
484
+ print(f"\n📊 System Status:")
485
+ print(f" WordNet clues: {'✅' if stats['wordnet_available'] else '❌'}")
486
+ print(f" Thematic words: {'✅' if stats['thematic_available'] else '❌'}")
487
+ if stats['vocab_size'] > 0:
488
+ print(f" Vocabulary: {stats['vocab_size']:,} words")
489
+
490
+ print(f"\n🎮 INTERACTIVE MODE")
491
+ print("=" * 60)
492
+ print("Commands:")
493
+ print(" <topic> - Generate words and clues for topic")
494
+ print(" <topic> <num_words> - Generate specific number of entries")
495
+ print(" <topic> <num_words> <diff> - Set difficulty (easy/medium/hard)")
496
+ print(" <topic> style <style> - Set clue style (definition/synonym/hypernym/category)")
497
+ print(" info <word> - Show WordNet information for word")
498
+ print(" test <word> <topic> - Test clue generation for specific word")
499
+ print(" stats - Show generation statistics")
500
+ print(" help - Show this help")
501
+ print(" quit - Exit")
502
+ print()
503
+ print("Examples:")
504
+ print(" animals - Generate animal-related crossword entries")
505
+ print(" technology 10 hard - 10 hard technology entries")
506
+ print(" music style synonym - Music entries with synonym-style clues")
507
+ print(" info elephant - WordNet info for 'elephant'")
508
+
509
+ while True:
510
+ try:
511
+ user_input = input("\n🎯 Enter command: ").strip()
512
+
513
+ if user_input.lower() in ['quit', 'exit', 'q']:
514
+ break
515
+
516
+ if not user_input:
517
+ continue
518
+
519
+ parts = user_input.split()
520
+
521
+ if user_input.lower() == 'help':
522
+ print("\nCommands:")
523
+ print(" <topic> [num_words] [difficulty] - Generate crossword entries")
524
+ print(" <topic> style <clue_style> - Generate with specific clue style")
525
+ print(" info <word> - Show WordNet info for word")
526
+ print(" test <word> <topic> - Test clue generation")
527
+ print(" stats - Show statistics")
528
+ print(" quit - Exit")
529
+ continue
530
+
531
+ elif user_input.lower() == 'stats':
532
+ stats = generator.get_stats()
533
+ print("\n📊 Generation Statistics:")
534
+ print(f" Words discovered: {stats['words_discovered']}")
535
+ print(f" Clues generated: {stats['clues_generated']}")
536
+ print(f" Total time: {stats['total_time']:.2f}s")
537
+ if stats['clues_generated'] > 0:
538
+ avg_time = stats['total_time'] / stats['clues_generated']
539
+ print(f" Avg time per clue: {avg_time:.2f}s")
540
+ continue
541
+
542
+ elif parts[0].lower() == 'info' and len(parts) > 1:
543
+ word = parts[1]
544
+ print(f"\n📝 WordNet Information: '{word}'")
545
+ info = generator.clue_generator.get_clue_info(word)
546
+
547
+ if 'error' in info:
548
+ print(f" ❌ {info['error']}")
549
+ else:
550
+ print(f" Synsets found: {info['synsets_count']}")
551
+ for i, synset in enumerate(info['synsets'], 1):
552
+ print(f"\n {i}. {synset['name']} ({synset['pos']})")
553
+ print(f" Definition: {synset['definition']}")
554
+ if synset['examples']:
555
+ print(f" Examples: {', '.join(synset['examples'])}")
556
+ if synset['synonyms']:
557
+ print(f" Synonyms: {', '.join(synset['synonyms'])}")
558
+ if synset['hypernyms']:
559
+ print(f" Categories: {', '.join(synset['hypernyms'])}")
560
+ continue
561
+
562
+ elif parts[0].lower() == 'test' and len(parts) >= 3:
563
+ word = parts[1]
564
+ topic = parts[2]
565
+ print(f"\n🧪 Testing clue generation: '{word}' + '{topic}'")
566
+
567
+ styles = ['definition', 'synonym', 'hypernym', 'category', 'descriptive']
568
+ for style in styles:
569
+ clue = generator.clue_generator.generate_clue(word, topic, style, 'medium')
570
+ print(f" {style:12}: {clue if clue else '(no clue generated)'}")
571
+ continue
572
+
573
+ # Parse generation command
574
+ topic = parts[0]
575
+ num_words = 8
576
+ difficulty = 'medium'
577
+ clue_style = 'auto'
578
+
579
+ # Parse additional parameters
580
+ i = 1
581
+ while i < len(parts):
582
+ if parts[i].isdigit():
583
+ num_words = int(parts[i])
584
+ elif parts[i].lower() in ['easy', 'medium', 'hard']:
585
+ difficulty = parts[i].lower()
586
+ elif parts[i].lower() == 'style' and i + 1 < len(parts):
587
+ clue_style = parts[i + 1].lower()
588
+ i += 1
589
+ elif parts[i].lower() in ['definition', 'synonym', 'hypernym', 'category', 'descriptive']:
590
+ clue_style = parts[i].lower()
591
+ i += 1
592
+
593
+ print(f"\n🎯 Generating {num_words} {difficulty} entries for '{topic}'" +
594
+ (f" (style: {clue_style})" if clue_style != 'auto' else ""))
595
+ print("-" * 60)
596
+
597
+ try:
598
+ start_time = time.time()
599
+ entries = generator.generate_crossword_entries(
600
+ topic=topic,
601
+ num_words=num_words,
602
+ difficulty=difficulty,
603
+ clue_style=clue_style
604
+ )
605
+ generation_time = time.time() - start_time
606
+
607
+ if entries:
608
+ print(f"✅ Generated {len(entries)} entries in {generation_time:.2f}s:")
609
+ print()
610
+
611
+ for i, entry in enumerate(entries, 1):
612
+ tier_short = entry.frequency_tier.split('_')[1] if '_' in entry.frequency_tier else 'unk'
613
+ print(f" {i:2}. {entry.word:<12} | {entry.clue}")
614
+ print(f" Similarity: {entry.similarity_score:.3f} | Tier: {tier_short} | Type: {entry.clue_type}")
615
+ print()
616
+ else:
617
+ print("❌ No entries generated. Try a different topic.")
618
+
619
+ except Exception as e:
620
+ print(f"❌ Error: {e}")
621
+
622
+ except KeyboardInterrupt:
623
+ print("\n\n👋 Exiting WordNet crossword generator")
624
+ break
625
+ except Exception as e:
626
+ print(f"❌ Error: {e}")
627
+
628
+ # Show final stats
629
+ final_stats = generator.get_stats()
630
+ if final_stats['clues_generated'] > 0:
631
+ print(f"\n📊 Session Summary:")
632
+ print(f" Entries generated: {final_stats['clues_generated']}")
633
+ print(f" Total time: {final_stats['total_time']:.2f}s")
634
+ print(f" Average per entry: {final_stats['total_time']/final_stats['clues_generated']:.2f}s")
635
+
636
+ print("\n✅ Thanks for using WordNet Crossword Generator!")
637
+
638
+
639
+ if __name__ == "__main__":
640
+ main()
crossword-app/backend-py/test-integration/test_boundary_fix.py DELETED
@@ -1,147 +0,0 @@
1
- #!/usr/bin/env python3
2
-
3
- import sys
4
- import asyncio
5
- from pathlib import Path
6
-
7
- # Add project root to path
8
- project_root = Path(__file__).parent.parent # Go up from test-integration to backend-py
9
- sys.path.insert(0, str(project_root))
10
-
11
- from src.services.crossword_generator import CrosswordGenerator
12
-
13
- async def test_boundary_fix():
14
- """Test that the boundary fix works correctly."""
15
-
16
- # Sample words that are known to cause boundary issues
17
- test_words = [
18
- {"word": "COMPUTER", "clue": "Electronic device"},
19
- {"word": "MACHINE", "clue": "Device with moving parts"},
20
- {"word": "SCIENCE", "clue": "Systematic study"},
21
- {"word": "EXPERT", "clue": "Specialist"},
22
- {"word": "CODE", "clue": "Programming text"},
23
- {"word": "DATA", "clue": "Information"}
24
- ]
25
-
26
- generator = CrosswordGenerator()
27
-
28
- print("🧪 Testing Boundary Fix")
29
- print("=" * 50)
30
-
31
- # Generate a crossword
32
- result = generator._create_grid(test_words)
33
-
34
- if not result:
35
- print("❌ Grid generation failed")
36
- return False
37
-
38
- grid = result["grid"]
39
- placed_words = result["placed_words"]
40
-
41
- print(f"✅ Generated grid with {len(placed_words)} words")
42
- print(f"Grid size: {len(grid)}x{len(grid[0])}")
43
-
44
- # Display the grid
45
- print("\nGenerated Grid:")
46
- for i, row in enumerate(grid):
47
- row_str = " ".join(cell if cell != "." else " " for cell in row)
48
- print(f"{i:2d} | {row_str}")
49
-
50
- print(f"\nPlaced Words:")
51
- for word in placed_words:
52
- print(f" {word['word']} at ({word['row']},{word['col']}) {word['direction']}")
53
-
54
- # Analyze for boundary violations
55
- print(f"\n🔍 Analyzing for boundary violations...")
56
-
57
- violations = []
58
-
59
- # Check horizontal words
60
- for r in range(len(grid)):
61
- current_word = ""
62
- word_start = -1
63
-
64
- for c in range(len(grid[r])):
65
- if grid[r][c] != ".":
66
- if current_word == "":
67
- word_start = c
68
- current_word += grid[r][c]
69
- else:
70
- if current_word:
71
- # Word ended - check if it's a valid placed word
72
- is_valid_word = any(
73
- placed['word'] == current_word and
74
- placed['row'] == r and
75
- placed['col'] == word_start and
76
- placed['direction'] == 'horizontal'
77
- for placed in placed_words
78
- )
79
- if not is_valid_word and len(current_word) > 1:
80
- violations.append(f"Invalid horizontal word '{current_word}' at ({r},{word_start})")
81
- current_word = ""
82
-
83
- # Check word at end of row
84
- if current_word:
85
- is_valid_word = any(
86
- placed['word'] == current_word and
87
- placed['row'] == r and
88
- placed['col'] == word_start and
89
- placed['direction'] == 'horizontal'
90
- for placed in placed_words
91
- )
92
- if not is_valid_word and len(current_word) > 1:
93
- violations.append(f"Invalid horizontal word '{current_word}' at ({r},{word_start})")
94
-
95
- # Check vertical words
96
- for c in range(len(grid[0])):
97
- current_word = ""
98
- word_start = -1
99
-
100
- for r in range(len(grid)):
101
- if grid[r][c] != ".":
102
- if current_word == "":
103
- word_start = r
104
- current_word += grid[r][c]
105
- else:
106
- if current_word:
107
- # Word ended - check if it's a valid placed word
108
- is_valid_word = any(
109
- placed['word'] == current_word and
110
- placed['row'] == word_start and
111
- placed['col'] == c and
112
- placed['direction'] == 'vertical'
113
- for placed in placed_words
114
- )
115
- if not is_valid_word and len(current_word) > 1:
116
- violations.append(f"Invalid vertical word '{current_word}' at ({word_start},{c})")
117
- current_word = ""
118
-
119
- # Check word at end of column
120
- if current_word:
121
- is_valid_word = any(
122
- placed['word'] == current_word and
123
- placed['row'] == word_start and
124
- placed['col'] == c and
125
- placed['direction'] == 'vertical'
126
- for placed in placed_words
127
- )
128
- if not is_valid_word and len(current_word) > 1:
129
- violations.append(f"Invalid vertical word '{current_word}' at ({word_start},{c})")
130
-
131
- # Report results
132
- if violations:
133
- print(f"❌ Found {len(violations)} boundary violations:")
134
- for violation in violations:
135
- print(f" - {violation}")
136
- return False
137
- else:
138
- print(f"✅ No boundary violations found!")
139
- print(f"✅ All words in grid are properly placed and bounded")
140
- return True
141
-
142
- if __name__ == "__main__":
143
- success = asyncio.run(test_boundary_fix())
144
- if success:
145
- print(f"\n🎉 Boundary fix is working correctly!")
146
- else:
147
- print(f"\n💥 Boundary fix needs more work!")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crossword-app/backend-py/test-integration/test_bounds_comprehensive.py DELETED
@@ -1,266 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Comprehensive test for bounds checking fixes in crossword generator.
4
- """
5
-
6
- import asyncio
7
- import sys
8
- import pytest
9
- from pathlib import Path
10
-
11
- # Add project root to path
12
- project_root = Path(__file__).parent.parent # Go up from test-integration to backend-py
13
- sys.path.insert(0, str(project_root))
14
-
15
- from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
16
-
17
- class TestBoundsChecking:
18
- """Test all bounds checking in crossword generator."""
19
-
20
- def setup_method(self):
21
- """Setup test instance."""
22
- self.generator = CrosswordGeneratorFixed(vector_service=None)
23
-
24
- def test_can_place_word_bounds_horizontal(self):
25
- """Test _can_place_word bounds checking for horizontal placement."""
26
- # Create small grid
27
- grid = [["." for _ in range(5)] for _ in range(5)]
28
-
29
- # Test cases that should fail bounds checking
30
- assert not self.generator._can_place_word(grid, "TOOLONG", 2, 1, "horizontal") # Word too long
31
- assert not self.generator._can_place_word(grid, "TEST", -1, 1, "horizontal") # Negative row
32
- assert not self.generator._can_place_word(grid, "TEST", 1, -1, "horizontal") # Negative col
33
- assert not self.generator._can_place_word(grid, "TEST", 5, 1, "horizontal") # Row >= size
34
- assert not self.generator._can_place_word(grid, "TEST", 1, 5, "horizontal") # Col >= size
35
- assert not self.generator._can_place_word(grid, "TEST", 1, 3, "horizontal") # Word extends beyond grid
36
-
37
- # Test cases that should pass
38
- assert self.generator._can_place_word(grid, "TEST", 2, 1, "horizontal") # Valid placement
39
- assert self.generator._can_place_word(grid, "A", 0, 0, "horizontal") # Single letter
40
-
41
- def test_can_place_word_bounds_vertical(self):
42
- """Test _can_place_word bounds checking for vertical placement."""
43
- # Create small grid
44
- grid = [["." for _ in range(5)] for _ in range(5)]
45
-
46
- # Test cases that should fail bounds checking
47
- assert not self.generator._can_place_word(grid, "TOOLONG", 1, 2, "vertical") # Word too long
48
- assert not self.generator._can_place_word(grid, "TEST", -1, 1, "vertical") # Negative row
49
- assert not self.generator._can_place_word(grid, "TEST", 1, -1, "vertical") # Negative col
50
- assert not self.generator._can_place_word(grid, "TEST", 5, 1, "vertical") # Row >= size
51
- assert not self.generator._can_place_word(grid, "TEST", 1, 5, "vertical") # Col >= size
52
- assert not self.generator._can_place_word(grid, "TEST", 3, 1, "vertical") # Word extends beyond grid
53
-
54
- # Test cases that should pass
55
- assert self.generator._can_place_word(grid, "TEST", 1, 2, "vertical") # Valid placement
56
- assert self.generator._can_place_word(grid, "A", 0, 0, "vertical") # Single letter
57
-
58
- def test_place_word_bounds_horizontal(self):
59
- """Test _place_word bounds checking for horizontal placement."""
60
- grid = [["." for _ in range(5)] for _ in range(5)]
61
-
62
- # Valid placement should work
63
- original_state = self.generator._place_word(grid, "TEST", 2, 1, "horizontal")
64
- assert len(original_state) == 4
65
- assert grid[2][1] == "T"
66
- assert grid[2][4] == "T"
67
-
68
- # Test out-of-bounds placement should raise IndexError
69
- with pytest.raises(IndexError):
70
- self.generator._place_word(grid, "TOOLONG", 2, 1, "horizontal")
71
-
72
- with pytest.raises(IndexError):
73
- self.generator._place_word(grid, "TEST", -1, 1, "horizontal")
74
-
75
- with pytest.raises(IndexError):
76
- self.generator._place_word(grid, "TEST", 5, 1, "horizontal")
77
-
78
- with pytest.raises(IndexError):
79
- self.generator._place_word(grid, "TEST", 1, 5, "horizontal")
80
-
81
- def test_place_word_bounds_vertical(self):
82
- """Test _place_word bounds checking for vertical placement."""
83
- grid = [["." for _ in range(5)] for _ in range(5)]
84
-
85
- # Valid placement should work
86
- original_state = self.generator._place_word(grid, "TEST", 1, 2, "vertical")
87
- assert len(original_state) == 4
88
- assert grid[1][2] == "T"
89
- assert grid[4][2] == "T"
90
-
91
- # Test out-of-bounds placement should raise IndexError
92
- with pytest.raises(IndexError):
93
- self.generator._place_word(grid, "TOOLONG", 1, 2, "vertical")
94
-
95
- with pytest.raises(IndexError):
96
- self.generator._place_word(grid, "TEST", -1, 2, "vertical")
97
-
98
- with pytest.raises(IndexError):
99
- self.generator._place_word(grid, "TEST", 5, 2, "vertical")
100
-
101
- with pytest.raises(IndexError):
102
- self.generator._place_word(grid, "TEST", 2, 5, "vertical")
103
-
104
- def test_remove_word_bounds(self):
105
- """Test _remove_word bounds checking."""
106
- grid = [["." for _ in range(5)] for _ in range(5)]
107
-
108
- # Place a word first
109
- original_state = self.generator._place_word(grid, "TEST", 2, 1, "horizontal")
110
-
111
- # Normal removal should work
112
- self.generator._remove_word(grid, original_state)
113
- assert grid[2][1] == "."
114
-
115
- # Test invalid original state should raise IndexError
116
- bad_state = [{"row": -1, "col": 1, "value": "."}]
117
- with pytest.raises(IndexError):
118
- self.generator._remove_word(grid, bad_state)
119
-
120
- bad_state = [{"row": 5, "col": 1, "value": "."}]
121
- with pytest.raises(IndexError):
122
- self.generator._remove_word(grid, bad_state)
123
-
124
- bad_state = [{"row": 1, "col": -1, "value": "."}]
125
- with pytest.raises(IndexError):
126
- self.generator._remove_word(grid, bad_state)
127
-
128
- bad_state = [{"row": 1, "col": 5, "value": "."}]
129
- with pytest.raises(IndexError):
130
- self.generator._remove_word(grid, bad_state)
131
-
132
- def test_create_simple_cross_bounds(self):
133
- """Test _create_simple_cross bounds checking."""
134
- # Test with words that have intersections
135
- word_list = ["CAT", "TOY"] # 'T' intersection
136
- word_objs = [{"word": w, "clue": f"Clue for {w}"} for w in word_list]
137
-
138
- # This should work without bounds errors
139
- result = self.generator._create_simple_cross(word_list, word_objs)
140
- assert result is not None
141
- assert len(result["placed_words"]) == 2
142
-
143
- # Test with words that might cause issues
144
- word_list = ["A", "A"] # Same single letter
145
- word_objs = [{"word": w, "clue": f"Clue for {w}"} for w in word_list]
146
-
147
- # This should not crash with bounds errors
148
- result = self.generator._create_simple_cross(word_list, word_objs)
149
- # May return None due to placement issues, but should not crash
150
-
151
- def test_trim_grid_bounds(self):
152
- """Test _trim_grid bounds checking."""
153
- # Create a grid with words placed
154
- grid = [["." for _ in range(10)] for _ in range(10)]
155
-
156
- # Place some letters
157
- grid[5][3] = "T"
158
- grid[5][4] = "E"
159
- grid[5][5] = "S"
160
- grid[5][6] = "T"
161
-
162
- placed_words = [{
163
- "word": "TEST",
164
- "row": 5,
165
- "col": 3,
166
- "direction": "horizontal",
167
- "number": 1
168
- }]
169
-
170
- # This should work without bounds errors
171
- result = self.generator._trim_grid(grid, placed_words)
172
- assert result is not None
173
- assert "grid" in result
174
- assert "placed_words" in result
175
-
176
- # Test with edge case placements
177
- placed_words = [{
178
- "word": "A",
179
- "row": 0,
180
- "col": 0,
181
- "direction": "horizontal",
182
- "number": 1
183
- }]
184
-
185
- grid[0][0] = "A"
186
- result = self.generator._trim_grid(grid, placed_words)
187
- assert result is not None
188
-
189
- def test_calculation_placement_score_bounds(self):
190
- """Test _calculate_placement_score bounds checking."""
191
- grid = [["." for _ in range(5)] for _ in range(5)]
192
-
193
- # Place some letters for intersection testing
194
- grid[2][2] = "T"
195
- grid[2][3] = "E"
196
-
197
- placement = {"row": 2, "col": 2, "direction": "horizontal"}
198
- placed_words = []
199
-
200
- # This should work without bounds errors
201
- score = self.generator._calculate_placement_score(grid, "TEST", placement, placed_words)
202
- assert isinstance(score, int)
203
-
204
- # Test with out-of-bounds placement (should handle gracefully)
205
- placement = {"row": 4, "col": 3, "direction": "horizontal"} # Would extend beyond grid
206
- score = self.generator._calculate_placement_score(grid, "TEST", placement, placed_words)
207
- assert isinstance(score, int)
208
-
209
- # Test with negative placement (should handle gracefully)
210
- placement = {"row": -1, "col": 0, "direction": "horizontal"}
211
- score = self.generator._calculate_placement_score(grid, "TEST", placement, placed_words)
212
- assert isinstance(score, int)
213
-
214
- async def test_full_generation_stress():
215
- """Stress test full generation to catch index errors."""
216
- generator = CrosswordGeneratorFixed(vector_service=None)
217
-
218
- # Mock word selection to return test words
219
- test_words = [
220
- {"word": "CAT", "clue": "Feline pet"},
221
- {"word": "DOG", "clue": "Man's best friend"},
222
- {"word": "BIRD", "clue": "Flying animal"},
223
- {"word": "FISH", "clue": "Aquatic animal"},
224
- {"word": "ELEPHANT", "clue": "Large mammal"},
225
- {"word": "TIGER", "clue": "Striped cat"},
226
- {"word": "HORSE", "clue": "Riding animal"},
227
- {"word": "BEAR", "clue": "Large carnivore"},
228
- {"word": "WOLF", "clue": "Pack animal"},
229
- {"word": "LION", "clue": "King of jungle"}
230
- ]
231
-
232
- generator._select_words = lambda topics, difficulty, use_ai: test_words
233
-
234
- # Run multiple generation attempts
235
- for i in range(20):
236
- try:
237
- result = await generator.generate_puzzle(["animals"], "medium", use_ai=False)
238
- if result:
239
- print(f"✅ Generation {i+1} succeeded")
240
- else:
241
- print(f"⚠️ Generation {i+1} returned None")
242
- except IndexError as e:
243
- print(f"❌ Index error in generation {i+1}: {e}")
244
- raise
245
- except Exception as e:
246
- print(f"⚠️ Other error in generation {i+1}: {e}")
247
- # Don't raise for other errors, just continue
248
-
249
- print("✅ All stress test generations completed without index errors!")
250
-
251
- if __name__ == "__main__":
252
- # Run tests
253
- print("🧪 Running comprehensive bounds checking tests...")
254
-
255
- # Run pytest on this file
256
- import subprocess
257
- result = subprocess.run([sys.executable, "-m", "pytest", __file__, "-v"],
258
- capture_output=True, text=True)
259
-
260
- print("STDOUT:", result.stdout)
261
- if result.stderr:
262
- print("STDERR:", result.stderr)
263
-
264
- # Run stress test
265
- print("\n🏋️ Running stress test...")
266
- asyncio.run(test_full_generation_stress())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
crossword-app/backend-py/test-integration/test_bounds_fix.py DELETED
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Quick test to verify the bounds checking fix.
4
- """
5
-
6
- import sys
7
- from pathlib import Path
8
-
9
- # Add project root to path
10
- project_root = Path(__file__).parent.parent # Go up from test-integration to backend-py
11
- sys.path.insert(0, str(project_root))
12
-
13
- from src.services.crossword_generator_fixed import CrosswordGeneratorFixed
14
-
15
- def test_bounds_checking():
16
- """Test that placement score calculation doesn't crash with out-of-bounds access."""
17
- print("🧪 Testing bounds checking fix...")
18
-
19
- generator = CrosswordGeneratorFixed()
20
-
21
- # Create a small grid
22
- grid = [["." for _ in range(5)] for _ in range(5)]
23
-
24
- # Test placement that would go out of bounds
25
- placement = {
26
- "row": 3, # Starting at row 3
27
- "col": 2, # Starting at col 2
28
- "direction": "vertical"
29
- }
30
-
31
- # Word that would extend beyond grid (3+8=11 > 5)
32
- word = "ELEPHANT" # 8 letters, would go from row 3 to row 10 (out of bounds)
33
-
34
- try:
35
- # This should NOT crash with bounds checking
36
- score = generator._calculate_placement_score(grid, word, placement, [])
37
- print(f"✅ Success! Placement score calculated: {score}")
38
- print("✅ Bounds checking is working correctly")
39
- return True
40
- except IndexError as e:
41
- print(f"❌ IndexError still occurs: {e}")
42
- return False
43
- except Exception as e:
44
- print(f"❌ Other error: {e}")
45
- return False
46
-
47
- def test_valid_placement():
48
- """Test that valid placements still work correctly."""
49
- print("\n🧪 Testing valid placement scoring...")
50
-
51
- generator = CrosswordGeneratorFixed()
52
-
53
- # Create a grid with some letters
54
- grid = [["." for _ in range(8)] for _ in range(8)]
55
- grid[2][2] = "A" # Place an 'A' at position (2,2)
56
-
57
- # Test placement that intersects properly
58
- placement = {
59
- "row": 2,
60
- "col": 1,
61
- "direction": "horizontal"
62
- }
63
-
64
- word = "CAT" # Should intersect at the 'A'
65
-
66
- try:
67
- score = generator._calculate_placement_score(grid, word, placement, [])
68
- print(f"✅ Valid placement score: {score}")
69
-
70
- # Should have intersection bonus (score > 100)
71
- if score > 300: # Base 100 + intersection 200
72
- print("✅ Intersection detection working")
73
- else:
74
- print(f"⚠️ Expected intersection bonus, got score {score}")
75
-
76
- return True
77
- except Exception as e:
78
- print(f"❌ Error with valid placement: {e}")
79
- return False
80
-
81
- if __name__ == "__main__":
82
- print("🔧 Testing crossword generator bounds fix\n")
83
-
84
- test1_pass = test_bounds_checking()
85
- test2_pass = test_valid_placement()
86
-
87
- if test1_pass and test2_pass:
88
- print("\n✅ All tests passed! The bounds checking fix is working.")
89
- else:
90
- print("\n❌ Some tests failed. More work needed.")