feat(crossword): generated crosswords with clues
Browse filesSigned-off-by: Vimal Kumar <[email protected]>
This view is limited to 50 files because it contains too many changes.
See raw diff
- .gitignore +18 -1
- CLAUDE.md +3 -1
- Dockerfile +5 -5
- VOCABULARY_OPTIMIZATION.md +164 -0
- crossword-app/backend-py/CROSSWORD_GENERATION_WALKTHROUGH.md +434 -0
- crossword-app/backend-py/README.md +224 -49
- crossword-app/backend-py/all-packages.txt +69 -0
- crossword-app/backend-py/app.py +80 -24
- crossword-app/backend-py/data/data +0 -1
- crossword-app/backend-py/data/word-lists/animals.json +0 -165
- crossword-app/backend-py/data/word-lists/geography.json +0 -161
- crossword-app/backend-py/data/word-lists/science.json +0 -170
- crossword-app/backend-py/data/word-lists/technology.json +0 -221
- crossword-app/backend-py/debug_full_generation.py +0 -316
- crossword-app/backend-py/debug_grid_direct.py +0 -293
- crossword-app/backend-py/debug_index_error.py +0 -307
- crossword-app/backend-py/debug_simple.py +0 -142
- crossword-app/backend-py/public/assets/index-2XJqMaqu.js +10 -0
- crossword-app/backend-py/public/assets/index-2XJqMaqu.js.map +1 -0
- crossword-app/backend-py/public/assets/index-7dkEH9uQ.js +10 -0
- crossword-app/backend-py/public/assets/index-7dkEH9uQ.js.map +1 -0
- crossword-app/backend-py/public/assets/index-CWqdoNhy.css +1 -0
- crossword-app/backend-py/public/assets/index-DyT-gQda.css +1 -0
- crossword-app/backend-py/public/assets/index-V4v18wFW.css +1 -0
- crossword-app/backend-py/public/assets/index-uK3VdD5a.js +10 -0
- crossword-app/backend-py/public/assets/index-uK3VdD5a.js.map +1 -0
- crossword-app/backend-py/public/assets/vendor-nf7bT_Uh.js +0 -0
- crossword-app/backend-py/public/assets/vendor-nf7bT_Uh.js.map +0 -0
- crossword-app/backend-py/public/index.html +16 -0
- crossword-app/backend-py/requirements.txt +13 -10
- crossword-app/backend-py/src/routes/api.py +144 -27
- crossword-app/backend-py/src/services/// +12 -0
- crossword-app/backend-py/src/services/__pycache__/__init__.cpython-310.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/__init__.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/crossword_generator.cpython-310.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/crossword_generator.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/crossword_generator_fixed.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/crossword_generator_wrapper.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/vector_search.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/__pycache__/word_cache.cpython-313.pyc +0 -0
- crossword-app/backend-py/src/services/clue_generator.py +35 -0
- crossword-app/backend-py/src/services/crossword_generator.py +150 -89
- crossword-app/backend-py/src/services/crossword_generator_wrapper.py +16 -12
- crossword-app/backend-py/src/services/thematic_word_service.py +1057 -0
- crossword-app/backend-py/src/services/unified_word_service.py +250 -0
- crossword-app/backend-py/src/services/vector_search.py +109 -106
- crossword-app/backend-py/src/services/wordnet_clue_generator.py +640 -0
- crossword-app/backend-py/test-integration/test_boundary_fix.py +0 -147
- crossword-app/backend-py/test-integration/test_bounds_comprehensive.py +0 -266
- 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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 |
-
|
| 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
|
| 2 |
|
| 3 |
-
This is the Python implementation of the crossword generator backend, featuring
|
| 4 |
|
| 5 |
## 🚀 Features
|
| 6 |
|
| 7 |
-
- **
|
| 8 |
-
- **
|
|
|
|
|
|
|
| 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** |
|
| 18 |
-
| **Vocabulary Size** | ~100 words per topic |
|
| 19 |
-
| **AI Approach** |
|
| 20 |
-
| **Performance** | Fast but limited | Slower startup,
|
| 21 |
-
| **Dependencies** | Node.js +
|
| 22 |
|
| 23 |
## 🛠️ Setup & Installation
|
| 24 |
|
|
@@ -70,10 +71,11 @@ backend-py/
|
|
| 70 |
├── requirements-dev.txt # Full development dependencies
|
| 71 |
├── src/
|
| 72 |
│ ├── services/
|
| 73 |
-
│ │ ├──
|
| 74 |
-
│ │
|
|
|
|
| 75 |
│ └── routes/
|
| 76 |
-
│ └── api.py
|
| 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 |
-
- `
|
| 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
|
|
|
|
|
|
|
| 207 |
|
| 208 |
```bash
|
| 209 |
-
#
|
| 210 |
-
|
| 211 |
-
|
|
|
|
| 212 |
|
| 213 |
-
#
|
| 214 |
-
|
| 215 |
-
|
| 216 |
|
| 217 |
# Optional
|
| 218 |
-
LOG_LEVEL=INFO
|
| 219 |
```
|
| 220 |
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
1. **Initialization**:
|
|
|
|
|
|
|
| 224 |
- Load sentence-transformers model locally
|
| 225 |
-
-
|
| 226 |
-
-
|
| 227 |
-
- Build FAISS index for fast similarity search
|
| 228 |
|
| 229 |
2. **Word Generation**:
|
| 230 |
- Get topic embedding: `"Animals" → [768-dim vector]`
|
| 231 |
-
-
|
| 232 |
-
- Filter by similarity threshold
|
| 233 |
-
- Filter by
|
| 234 |
- Return top matches with generated clues
|
| 235 |
|
| 236 |
-
3. **
|
| 237 |
-
-
|
| 238 |
-
-
|
|
|
|
| 239 |
|
| 240 |
## 🧪 Testing
|
| 241 |
|
|
@@ -248,16 +268,163 @@ python test_local.py
|
|
| 248 |
python app.py
|
| 249 |
```
|
| 250 |
|
| 251 |
-
## 🐳
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
-
|
| 254 |
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
#
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 +
|
|
|
|
| 305 |
|
| 306 |
**Word Quality**:
|
| 307 |
-
- JavaScript: Limited by static word lists
|
| 308 |
-
- Python:
|
| 309 |
|
| 310 |
**Memory Usage**:
|
| 311 |
- JavaScript: ~100MB
|
| 312 |
-
- Python: ~500MB-1GB (model + embeddings
|
|
|
|
| 313 |
|
| 314 |
**API Response Time**:
|
| 315 |
-
- JavaScript: ~100ms (
|
| 316 |
-
- Python: ~200-500ms (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 317 |
|
| 318 |
## 🔄 Migration Strategy
|
| 319 |
|
|
@@ -325,8 +498,10 @@ pytest tests/ --cov=src --cov-report=html
|
|
| 325 |
|
| 326 |
## 🎯 Next Steps
|
| 327 |
|
| 328 |
-
- [
|
| 329 |
-
- [
|
|
|
|
|
|
|
| 330 |
- [ ] Add more sophisticated crossword grid generation
|
| 331 |
- [ ] Implement LLM-based clue generation
|
| 332 |
-
- [ ] Add
|
|
|
|
| 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.
|
| 21 |
|
| 22 |
# Load environment variables
|
| 23 |
load_dotenv()
|
| 24 |
|
| 25 |
-
# Set up logging
|
| 26 |
-
logging.basicConfig(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
logger = logging.getLogger(__name__)
|
| 28 |
|
| 29 |
-
|
| 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
|
| 35 |
-
|
| 36 |
|
| 37 |
@asynccontextmanager
|
| 38 |
async def lifespan(app: FastAPI):
|
| 39 |
"""Initialize and cleanup application resources."""
|
| 40 |
-
global
|
| 41 |
|
| 42 |
# Startup
|
| 43 |
startup_time = time.time()
|
| 44 |
-
|
| 45 |
|
| 46 |
-
# Initialize
|
| 47 |
try:
|
| 48 |
service_start = time.time()
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
init_time = time.time() - service_start
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
except Exception as e:
|
| 58 |
-
logger.error(f"❌ Failed to initialize
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
# Make
|
| 62 |
-
app.state.
|
| 63 |
|
| 64 |
yield
|
| 65 |
|
| 66 |
# Shutdown
|
| 67 |
logger.info("🛑 Shutting down Python backend...")
|
| 68 |
-
if
|
| 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
|
| 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":";;;;;;;;GASa,IAAIA,EAAEC,EAAiBC,EAAE,OAAO,IAAI,eAAe,EAAEC,EAAE,OAAO,IAAI,gBAAgB,EAAEC,EAAE,OAAO,UAAU,eAAeC,EAAEL,EAAE,mDAAmD,kBAAkBM,EAAE,CAAC,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,EAAE,EAClP,SAASC,EAAEC,EAAE,EAAEC,EAAE,CAAC,IAAIC,EAAEC,EAAE,GAAGC,EAAE,KAAKC,EAAE,KAAcJ,IAAT,SAAaG,EAAE,GAAGH,GAAY,EAAE,MAAX,SAAiBG,EAAE,GAAG,EAAE,KAAc,EAAE,MAAX,SAAiBC,EAAE,EAAE,KAAK,IAAIH,KAAK,EAAEN,EAAE,KAAK,EAAEM,CAAC,GAAG,CAACJ,EAAE,eAAeI,CAAC,IAAIC,EAAED,CAAC,EAAE,EAAEA,CAAC,GAAG,GAAGF,GAAGA,EAAE,aAAa,IAAIE,KAAK,EAAEF,EAAE,aAAa,EAAWG,EAAED,CAAC,aAAIC,EAAED,CAAC,EAAE,EAAEA,CAAC,GAAG,MAAM,CAAC,SAASR,EAAE,KAAKM,EAAE,IAAII,EAAE,IAAIC,EAAE,MAAMF,EAAE,OAAON,EAAE,OAAO,CAAC,YAAkBF,EAAEW,EAAA,IAAYP,EAAEO,EAAA,KAAaP,ECPxWQ,EAAA,QAAiBd,uBCDfG,EAAIH,EAENe,EAAA,WAAqBZ,EAAE,WACvBY,EAAA,YAAsBZ,EAAE,YCH1B,MAAMa,EAAgB,CAAC,CACrB,eAAAC,EACA,gBAAAC,EAAkB,CAAA,EAClB,eAAAC,EAAiB,CAAA,CACnB,IAAM,CACJ,MAAMC,EAAqBC,GAAU,CACnC,MAAMC,EAAoBH,EAAe,SAASE,CAAK,EACnDF,EAAe,OAAOI,GAAKA,IAAMF,CAAK,EACtC,CAAC,GAAGF,EAAgBE,CAAK,EAE7BJ,EAAeK,CAAiB,CAClC,EAEA,OACEE,EAAAA,KAAC,MAAA,CAAI,UAAU,iBACb,SAAA,CAAAC,EAAAA,IAAC,MAAG,SAAA,eAAA,CAAa,QAChB,MAAA,CAAI,UAAU,gBACZ,SAAAP,EAAgB,IAAIG,GACnBI,EAAAA,IAAC,SAAA,CAEC,UAAW,aAAaN,EAAe,SAASE,EAAM,IAAI,EAAI,WAAa,EAAE,GAC7E,QAAS,IAAMD,EAAkBC,EAAM,IAAI,EAE1C,SAAAA,EAAM,IAAA,EAJFA,EAAM,EAAA,CAMd,EACH,EAEAG,EAAAA,KAAC,IAAA,CAAE,UAAU,iBACV,SAAA,CAAAL,EAAe,OAAO,SAAOA,EAAe,SAAW,EAAI,IAAM,GAAG,WAAA,CAAA,CACvE,CAAA,EACF,CAEJ,ECjCMO,EAAa,CAAC,CAAE,KAAAC,EAAM,MAAAC,EAAO,aAAAC,EAAc,aAAAC,KAAmB,CAClE,KAAM,CAACC,EAAaC,CAAc,EAAIC,EAAAA,SAAS,CAAA,CAAE,EAE3CC,EAAkB,CAACC,EAAKC,EAAKC,IAAU,CAC3C,MAAMC,EAAM,GAAGH,CAAG,IAAIC,CAAG,GACnBG,EAAa,CAAE,GAAGR,EAAa,CAACO,CAAG,EAAGD,EAAM,aAAY,EAC9DL,EAAeO,CAAU,EACzBT,GAAgBA,EAAaK,EAAKC,EAAKC,CAAK,CAC9C,EAEMG,EAAe,CAACL,EAAKC,IAAQ,CACjC,GAAIP,GAAgB,CAACY,EAAYN,EAAKC,CAAG,EACvC,OAAOT,EAAKQ,CAAG,EAAEC,CAAG,EAEtB,MAAME,EAAM,GAAGH,CAAG,IAAIC,CAAG,GACzB,OAAOL,EAAYO,CAAG,GAAK,EAC7B,EAEMG,EAAc,CAACN,EAAKC,IACjBT,EAAKQ,CAAG,EAAEC,CAAG,IAAM,IAGtBM,EAAgB,CAACP,EAAKC,IAAQ,CAClC,GAAI,CAACR,EAAO,OAAO,KACnB,MAAMe,EAAOf,EAAM,KAAKrB,GAAKA,EAAE,SAAS,MAAQ4B,GAAO5B,EAAE,SAAS,MAAQ6B,CAAG,EAC7E,OAAOO,EAAOA,EAAK,OAAS,IAC9B,EAEA,GAAI,CAAChB,GAAQA,EAAK,SAAW,EAC3B,OAAOF,EAAAA,IAAC,MAAA,CAAI,UAAU,cAAc,SAAA,mBAAgB,EAGtD,MAAMmB,EAAWjB,EAAK,OAChBkB,EAAWlB,EAAK,CAAC,EAAIA,EAAK,CAAC,EAAE,OAAS,EAE5C,OACEF,EAAAA,IAAC,MAAA,CAAI,UAAU,mBACb,SAAAA,EAAAA,IAAC,MAAA,CACC,UAAU,cACV,MAAO,CACL,oBAAqB,UAAUoB,CAAQ,UACvC,iBAAkB,UAAUD,CAAQ,SAAA,EAGrC,SAAAjB,EAAK,IAAI,CAACQ,EAAKW,IACdX,EAAI,IAAI,CAACY,EAAMC,IAAa,CAC1B,MAAMC,EAAaP,EAAcI,EAAUE,CAAQ,EAInD,OAHgBP,EAAYK,EAAUE,CAAQ,EAK1CvB,EAAAA,IAAC,MAAA,CAEC,UAAU,uBACV,MAAO,CAAE,WAAY,QAAA,CAAS,EAFzB,GAAGqB,CAAQ,IAAIE,CAAQ,EAAA,EAShCxB,EAAAA,KAAC,MAAA,CAEC,UAAU,uBAET,SAAA,CAAAyB,GAAcxB,EAAAA,IAAC,OAAA,CAAK,UAAU,cAAe,SAAAwB,EAAW,EACzDxB,EAAAA,IAAC,QAAA,CACC,KAAK,OACL,UAAU,IACV,MAAOe,EAAaM,EAAUE,CAAQ,EACtC,SAAWrC,GAAMuB,EAAgBY,EAAUE,EAAUrC,EAAE,OAAO,KAAK,EACnE,UAAW,cAAckB,EAAe,gBAAkB,EAAE,GAC5D,SAAUA,CAAA,CAAA,CACZ,CAAA,EAXK,GAAGiB,CAAQ,IAAIE,CAAQ,EAAA,CAclC,CAAC,CAAA,CACH,CAAA,EAEJ,CAEJ,EClFME,EAAW,CAAC,CAAE,MAAAtB,EAAQ,CAAA,KAAS,CACnC,MAAMuB,EAAcvB,EAAM,OAAOe,GAAQA,EAAK,YAAc,QAAQ,EAC9DS,EAAYxB,EAAM,OAAOe,GAAQA,EAAK,YAAc,MAAM,EAE1DU,EAAc,CAAC,CAAE,MAAAC,EAAO,SAAAC,KAC5B/B,OAAC,MAAA,CAAI,UAAU,eACb,SAAA,CAAAC,EAAAA,IAAC,MAAI,SAAA6B,CAAA,CAAM,EACX7B,EAAAA,IAAC,MACE,SAAA8B,EAAS,OACR/B,EAAAA,KAAC,KAAA,CAA4C,UAAU,YACrD,SAAA,CAAAC,EAAAA,IAAC,OAAA,CAAK,UAAU,cAAe,SAAAkB,EAAK,OAAO,EAC3ClB,EAAAA,IAAC,OAAA,CAAK,UAAU,YAAa,WAAK,IAAA,CAAK,CAAA,GAFhC,GAAGkB,EAAK,MAAM,IAAIA,EAAK,SAAS,EAGzC,CACD,CAAA,CACH,CAAA,EACF,EAGF,OACEnB,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAC,EAAAA,IAAC4B,EAAA,CAAY,MAAM,SAAS,SAAUF,EAAa,EACnD1B,EAAAA,IAAC4B,EAAA,CAAY,MAAM,OAAO,SAAUD,CAAA,CAAW,CAAA,EACjD,CAEJ,ECxBMI,EAAiB,CAAC,CAAE,QAAAC,EAAU,0BAEhCjC,EAAAA,KAAC,MAAA,CAAI,UAAU,kBACb,SAAA,CAAAC,EAAAA,IAAC,MAAA,CAAI,UAAU,SAAA,CAAU,EACzBA,EAAAA,IAAC,IAAA,CAAE,UAAU,kBAAmB,SAAAgC,CAAA,CAAQ,CAAA,EAC1C,ECLEC,EAAe,IAAM,CACzB,KAAM,CAACC,EAAQC,CAAS,EAAI3B,EAAAA,SAAS,IAAI,EACnC,CAAC4B,EAASC,CAAU,EAAI7B,EAAAA,SAAS,EAAK,EACtC,CAAC8B,EAAOC,CAAQ,EAAI/B,EAAAA,SAAS,IAAI,EACjC,CAACgC,EAAQC,CAAS,EAAIjC,EAAAA,SAAS,CAAA,CAAE,EAEjCkC,EAA4E,GAE5EC,EAAcC,EAAAA,YAAY,SAAY,CAC1C,GAAI,CACFP,EAAW,EAAI,EACf,MAAMQ,EAAW,MAAM,MAAM,GAAGH,CAAY,aAAa,EACzD,GAAI,CAACG,EAAS,GAAI,MAAM,IAAI,MAAM,wBAAwB,EAC1D,MAAMC,EAAO,MAAMD,EAAS,KAAA,EAC5BJ,EAAUK,CAAI,CAChB,OAASC,EAAK,CACZR,EAASQ,EAAI,OAAO,CACtB,QAAA,CACEV,EAAW,EAAK,CAClB,CACF,EAAG,CAACK,CAAY,CAAC,EAEXM,EAAiBJ,EAAAA,YAAY,MAAOlD,EAAgBuD,EAAa,SAAUC,EAAQ,KAAU,CACjG,GAAI,CACFb,EAAW,EAAI,EACfE,EAAS,IAAI,EAEb,MAAMM,EAAW,MAAM,MAAM,GAAGH,CAAY,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CACP,eAAgB,kBAAA,EAElB,KAAM,KAAK,UAAU,CACnB,OAAQhD,EACR,WAAAuD,EACA,MAAAC,CAAA,CACD,CAAA,CACF,EAED,GAAI,CAACL,EAAS,GAAI,CAChB,MAAMM,EAAY,MAAMN,EAAS,KAAA,EAAO,MAAM,KAAO,CAAA,EAAG,EACxD,MAAM,IAAI,MAAMM,EAAU,SAAW,2BAA2B,CAClE,CAEA,MAAMC,EAAa,MAAMP,EAAS,KAAA,EAClC,OAAAV,EAAUiB,CAAU,EACbA,CACT,OAASL,EAAK,CACZ,OAAAR,EAASQ,EAAI,OAAO,EACb,IACT,QAAA,CACEV,EAAW,EAAK,CAClB,CACF,EAAG,CAACK,CAAY,CAAC,EAEXW,EAAkBT,cAAY,MAAOtC,GAAgB,CACzD,GAAI,CACF,MAAMuC,EAAW,MAAM,MAAM,GAAGH,CAAY,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CACP,eAAgB,kBAAA,EAElB,KAAM,KAAK,UAAU,CACnB,OAAAR,EACA,QAAS5B,CAAA,CACV,CAAA,CACF,EAED,GAAI,CAACuC,EAAS,GAAI,MAAM,IAAI,MAAM,4BAA4B,EAE9D,OAAO,MAAMA,EAAS,KAAA,CACxB,OAASE,EAAK,CACZ,OAAAR,EAASQ,EAAI,OAAO,EACb,IACT,CACF,EAAG,CAACL,EAAcR,CAAM,CAAC,EAEnBoB,EAAcV,EAAAA,YAAY,IAAM,CACpCT,EAAU,IAAI,EACdI,EAAS,IAAI,CACf,EAAG,CAAA,CAAE,EAEL,MAAO,CACL,OAAAL,EACA,QAAAE,EACA,MAAAE,EACA,OAAAE,EACA,YAAAG,EACA,eAAAK,EACA,gBAAAK,EACA,YAAAC,CAAA,CAEJ,ECtFA,SAASC,GAAM,CACb,KAAM,CAAC7D,EAAgB8D,CAAiB,EAAIhD,EAAAA,SAAS,CAAA,CAAE,EACjD,CAACyC,EAAYQ,CAAa,EAAIjD,EAAAA,SAAS,QAAQ,EAC/C,CAACJ,EAAcsD,CAAe,EAAIlD,EAAAA,SAAS,EAAK,EAEhD,CACJ,OAAA0B,EACA,QAAAE,EACA,MAAAE,EACA,OAAAE,EACA,YAAAG,EACA,eAAAK,EACA,YAAAM,CAAA,EACErB,EAAA,EAEJ0B,EAAAA,UAAU,IAAM,CACdhB,EAAA,CACF,EAAG,CAACA,CAAW,CAAC,EAEhB,MAAMiB,EAAuB,SAAY,CACvC,GAAIlE,EAAe,SAAW,EAAG,CAC/B,MAAM,kCAAkC,EACxC,MACF,CAEA,MAAMsD,EAAetD,EAAgBuD,EAAY,EAAK,CACxD,EAEMY,EAAsBrB,GAAW,CACrCgB,EAAkBhB,CAAM,CAC1B,EAGMsB,EAAc,IAAM,CACxBR,EAAA,EACAE,EAAkB,CAAA,CAAE,EACpBE,EAAgB,EAAK,EACrBD,EAAc,QAAQ,CACxB,EAEMM,EAAuB,IAAM,CACjCL,EAAgB,EAAI,CACtB,EAEA,OACE3D,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CAAO,UAAU,aAChB,SAAA,CAAAC,EAAAA,IAAC,KAAA,CAAG,UAAU,YAAY,SAAA,6BAA0B,EACpDA,EAAAA,IAAC,KAAE,SAAA,0DAAA,CAAwD,CAAA,EAC7D,EAEAA,EAAAA,IAACT,EAAA,CACC,eAAgBsE,EAChB,gBAAiBrB,EACjB,eAAA9C,CAAA,CAAA,EAGFK,EAAAA,KAAC,MAAA,CAAI,UAAU,kBACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CACC,MAAOkD,EACP,SAAW/D,GAAMuE,EAAcvE,EAAE,OAAO,KAAK,EAC7C,UAAU,cAEV,SAAA,CAAAc,EAAAA,IAAC,SAAA,CAAO,MAAM,OAAO,SAAA,OAAI,EACzBA,EAAAA,IAAC,SAAA,CAAO,MAAM,SAAS,SAAA,SAAM,EAC7BA,EAAAA,IAAC,SAAA,CAAO,MAAM,OAAO,SAAA,MAAA,CAAI,CAAA,CAAA,CAAA,EAG3BA,EAAAA,IAAC,SAAA,CACC,QAAS4D,EACT,SAAUxB,GAAW1C,EAAe,SAAW,EAC/C,UAAU,2BAET,WAAU,gBAAkB,iBAAA,CAAA,EAG/BM,EAAAA,IAAC,SAAA,CACC,QAAS8D,EACT,UAAU,wBACX,SAAA,OAAA,CAAA,EAIA5B,GAAU,CAAC9B,GACVJ,EAAAA,IAAC,SAAA,CACC,QAAS+D,EACT,UAAU,yBACX,SAAA,iBAAA,CAAA,CAED,EAEJ,EAECzB,GACCvC,EAAAA,KAAC,MAAA,CAAI,UAAU,gBAAgB,SAAA,CAAA,UACrBuC,CAAA,EACV,EAGDF,SAAYL,EAAA,EAAe,EAE3BG,GAAU,CAACE,GACVrC,EAAAA,KAAAiE,EAAAA,SAAA,CACE,SAAA,CAAAhE,EAAAA,IAAC,OAAI,UAAU,cACb,SAAAD,EAAAA,KAAC,OAAA,CAAK,UAAU,eACb,SAAA,CAAAmC,EAAO,SAAS,UAAU,YAAUA,EAAO,SAAS,KAAK,IAAEA,EAAO,SAAS,KAAK,OAAA,CAAA,CACnF,CAAA,CACF,EACAnC,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAC,EAAAA,IAACC,EAAA,CACC,KAAMiC,EAAO,KACb,MAAOA,EAAO,MACd,aAAA9B,CAAA,CAAA,EAEFJ,EAAAA,IAACyB,EAAA,CAAS,MAAOS,EAAO,KAAA,CAAO,CAAA,CAAA,CACjC,CAAA,EACF,EAGD,CAACA,GAAU,CAACE,GAAW,CAACE,GACvBtC,EAAAA,IAAC,MAAA,CAAI,MAAO,CAAE,UAAW,SAAU,QAAS,OAAQ,MAAO,SAAA,EAAa,SAAA,qDAAA,CAExE,CAAA,EAEJ,CAEJ,CClIAiE,EAAS,WAAW,SAAS,eAAe,MAAM,CAAC,EAAE,aAClDC,EAAM,WAAN,CACC,SAAAlE,MAACuD,IAAI,CAAA,CACP,CACF","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":";;;;;;;;GASa,IAAIA,EAAEC,EAAiBC,EAAE,OAAO,IAAI,eAAe,EAAEC,EAAE,OAAO,IAAI,gBAAgB,EAAEC,EAAE,OAAO,UAAU,eAAeC,EAAEL,EAAE,mDAAmD,kBAAkBM,EAAE,CAAC,IAAI,GAAG,IAAI,GAAG,OAAO,GAAG,SAAS,EAAE,EAClP,SAASC,EAAEC,EAAEC,EAAEC,EAAE,CAAC,IAAIC,EAAEC,EAAE,GAAGC,EAAE,KAAKC,EAAE,KAAcJ,IAAT,SAAaG,EAAE,GAAGH,GAAYD,EAAE,MAAX,SAAiBI,EAAE,GAAGJ,EAAE,KAAcA,EAAE,MAAX,SAAiBK,EAAEL,EAAE,KAAK,IAAIE,KAAKF,EAAEL,EAAE,KAAKK,EAAEE,CAAC,GAAG,CAACL,EAAE,eAAeK,CAAC,IAAIC,EAAED,CAAC,EAAEF,EAAEE,CAAC,GAAG,GAAGH,GAAGA,EAAE,aAAa,IAAIG,KAAKF,EAAED,EAAE,aAAaC,EAAWG,EAAED,CAAC,aAAIC,EAAED,CAAC,EAAEF,EAAEE,CAAC,GAAG,MAAM,CAAC,SAAST,EAAE,KAAKM,EAAE,IAAIK,EAAE,IAAIC,EAAE,MAAMF,EAAE,OAAOP,EAAE,OAAO,CAAC,YAAkBF,EAAEY,EAAA,IAAYR,EAAEQ,EAAA,KAAaR,ECPxWS,EAAA,QAAiBf,uBCDfG,EAAIH,EAENgB,EAAA,WAAqBb,EAAE,WACvBa,EAAA,YAAsBb,EAAE,YCH1B,MAAMc,EAAgB,CAAC,CACrB,eAAAC,EACA,gBAAAC,EAAkB,CAAA,EAClB,eAAAC,EAAiB,CAAA,EACjB,eAAAC,EAAiB,GACjB,iBAAAC,CACF,IAAM,CACJ,MAAMC,EAAqBC,GAAU,CACnC,MAAMC,EAAoBL,EAAe,SAASI,CAAK,EACnDJ,EAAe,OAAOM,GAAKA,IAAMF,CAAK,EACtC,CAAC,GAAGJ,EAAgBI,CAAK,EAE7BN,EAAeO,CAAiB,CAClC,EAEA,OACEE,EAAAA,KAAC,MAAA,CAAI,UAAU,iBACb,SAAA,CAAAC,EAAAA,IAAC,MAAG,SAAA,eAAA,CAAa,QAChB,MAAA,CAAI,UAAU,gBACZ,SAAAT,EAAgB,IAAIK,GACnBI,EAAAA,IAAC,SAAA,CAEC,UAAW,aAAaR,EAAe,SAASI,EAAM,IAAI,EAAI,WAAa,EAAE,GAC7E,QAAS,IAAMD,EAAkBC,EAAM,IAAI,EAE1C,SAAAA,EAAM,IAAA,EAJFA,EAAM,EAAA,CAMd,EACH,EAEAG,EAAAA,KAAC,MAAA,CAAI,UAAU,2BACb,SAAA,CAAAC,MAAC,QAAA,CAAM,QAAQ,kBAAkB,UAAU,iBAAiB,SAAA,6BAE5D,EACAA,EAAAA,IAAC,WAAA,CACC,GAAG,kBACH,UAAU,iBACV,MAAOP,EACP,SAAWT,GAAMU,GAAoBA,EAAiBV,EAAE,OAAO,KAAK,EACpE,YAAY,kDACZ,KAAK,IACL,UAAU,KAAA,CAAA,EAEZe,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAA,EAAAA,KAAC,OAAA,CAAK,UAAU,aAAc,SAAA,CAAAN,EAAe,OAAO,iBAAA,EAAe,EAClEA,GACCO,EAAAA,IAAC,SAAA,CACC,KAAK,SACL,UAAU,qBACV,QAAS,IAAMN,GAAoBA,EAAiB,EAAE,EACtD,MAAM,iBACP,SAAA,OAAA,CAAA,CAED,CAAA,CAEJ,CAAA,EACF,EAEAK,EAAAA,KAAC,IAAA,CAAE,UAAU,iBACV,SAAA,CAAAP,EAAe,OAAO,SAAOA,EAAe,SAAW,EAAI,IAAM,GAAG,WAAA,CAAA,CACvE,CAAA,EACF,CAEJ,EC/DMS,EAAa,CAAC,CAAE,KAAAC,EAAM,MAAAC,EAAO,aAAAC,EAAc,aAAAC,KAAmB,CAClE,KAAM,CAACC,EAAaC,CAAc,EAAIC,EAAAA,SAAS,CAAA,CAAE,EAE3CC,EAAkB,CAACC,EAAKC,EAAKC,IAAU,CAC3C,MAAMC,EAAM,GAAGH,CAAG,IAAIC,CAAG,GACnBG,EAAa,CAAE,GAAGR,EAAa,CAACO,CAAG,EAAGD,EAAM,aAAY,EAC9DL,EAAeO,CAAU,EACzBT,GAAgBA,EAAaK,EAAKC,EAAKC,CAAK,CAC9C,EAEMG,EAAe,CAACL,EAAKC,IAAQ,CACjC,GAAIP,GAAgB,CAACY,EAAYN,EAAKC,CAAG,EACvC,OAAOT,EAAKQ,CAAG,EAAEC,CAAG,EAEtB,MAAME,EAAM,GAAGH,CAAG,IAAIC,CAAG,GACzB,OAAOL,EAAYO,CAAG,GAAK,EAC7B,EAEMG,EAAc,CAACN,EAAKC,IACjBT,EAAKQ,CAAG,EAAEC,CAAG,IAAM,IAGtBM,EAAgB,CAACP,EAAKC,IAAQ,CAClC,GAAI,CAACR,EAAO,OAAO,KACnB,MAAMe,EAAOf,EAAM,KAAKxB,GAAKA,EAAE,SAAS,MAAQ+B,GAAO/B,EAAE,SAAS,MAAQgC,CAAG,EAC7E,OAAOO,EAAOA,EAAK,OAAS,IAC9B,EAEA,GAAI,CAAChB,GAAQA,EAAK,SAAW,EAC3B,OAAOF,EAAAA,IAAC,MAAA,CAAI,UAAU,cAAc,SAAA,mBAAgB,EAGtD,MAAMmB,EAAWjB,EAAK,OAChBkB,EAAWlB,EAAK,CAAC,EAAIA,EAAK,CAAC,EAAE,OAAS,EAE5C,OACEF,EAAAA,IAAC,MAAA,CAAI,UAAU,mBACb,SAAAA,EAAAA,IAAC,MAAA,CACC,UAAU,cACV,MAAO,CACL,oBAAqB,UAAUoB,CAAQ,UACvC,iBAAkB,UAAUD,CAAQ,SAAA,EAGrC,SAAAjB,EAAK,IAAI,CAACQ,EAAKW,IACdX,EAAI,IAAI,CAACY,EAAMC,IAAa,CAC1B,MAAMC,EAAaP,EAAcI,EAAUE,CAAQ,EAInD,OAHgBP,EAAYK,EAAUE,CAAQ,EAK1CvB,EAAAA,IAAC,MAAA,CAEC,UAAU,uBACV,MAAO,CAAE,WAAY,QAAA,CAAS,EAFzB,GAAGqB,CAAQ,IAAIE,CAAQ,EAAA,EAShCxB,EAAAA,KAAC,MAAA,CAEC,UAAU,uBAET,SAAA,CAAAyB,GAAcxB,EAAAA,IAAC,OAAA,CAAK,UAAU,cAAe,SAAAwB,EAAW,EACzDxB,EAAAA,IAAC,QAAA,CACC,KAAK,OACL,UAAU,IACV,MAAOe,EAAaM,EAAUE,CAAQ,EACtC,SAAWvC,GAAMyB,EAAgBY,EAAUE,EAAUvC,EAAE,OAAO,KAAK,EACnE,UAAW,cAAcoB,EAAe,gBAAkB,EAAE,GAC5D,SAAUA,CAAA,CAAA,CACZ,CAAA,EAXK,GAAGiB,CAAQ,IAAIE,CAAQ,EAAA,CAclC,CAAC,CAAA,CACH,CAAA,EAEJ,CAEJ,EClFME,EAAW,CAAC,CAAE,MAAAtB,EAAQ,CAAA,KAAS,CACnC,MAAMuB,EAAcvB,EAAM,OAAOe,GAAQA,EAAK,YAAc,QAAQ,EAC9DS,EAAYxB,EAAM,OAAOe,GAAQA,EAAK,YAAc,MAAM,EAE1DU,EAAc,CAAC,CAAE,MAAAC,EAAO,SAAAC,KAC5B/B,OAAC,MAAA,CAAI,UAAU,eACb,SAAA,CAAAC,EAAAA,IAAC,MAAI,SAAA6B,CAAA,CAAM,EACX7B,EAAAA,IAAC,MACE,SAAA8B,EAAS,OACR/B,EAAAA,KAAC,KAAA,CAA4C,UAAU,YACrD,SAAA,CAAAC,EAAAA,IAAC,OAAA,CAAK,UAAU,cAAe,SAAAkB,EAAK,OAAO,EAC3ClB,EAAAA,IAAC,OAAA,CAAK,UAAU,YAAa,WAAK,IAAA,CAAK,CAAA,GAFhC,GAAGkB,EAAK,MAAM,IAAIA,EAAK,SAAS,EAGzC,CACD,CAAA,CACH,CAAA,EACF,EAGF,OACEnB,EAAAA,KAAC,MAAA,CAAI,UAAU,YACb,SAAA,CAAAC,EAAAA,IAAC4B,EAAA,CAAY,MAAM,SAAS,SAAUF,EAAa,EACnD1B,EAAAA,IAAC4B,EAAA,CAAY,MAAM,OAAO,SAAUD,CAAA,CAAW,CAAA,EACjD,CAEJ,ECxBMI,EAAiB,CAAC,CAAE,QAAAC,EAAU,0BAEhCjC,EAAAA,KAAC,MAAA,CAAI,UAAU,kBACb,SAAA,CAAAC,EAAAA,IAAC,MAAA,CAAI,UAAU,SAAA,CAAU,EACzBA,EAAAA,IAAC,IAAA,CAAE,UAAU,kBAAmB,SAAAgC,CAAA,CAAQ,CAAA,EAC1C,ECLEC,EAAe,IAAM,CACzB,KAAM,CAACC,EAAQC,CAAS,EAAI3B,EAAAA,SAAS,IAAI,EACnC,CAAC4B,EAASC,CAAU,EAAI7B,EAAAA,SAAS,EAAK,EACtC,CAAC8B,EAAOC,CAAQ,EAAI/B,EAAAA,SAAS,IAAI,EACjC,CAACgC,EAAQC,CAAS,EAAIjC,EAAAA,SAAS,CAAA,CAAE,EAEjCkC,EAA4E,GAE5EC,EAAcC,EAAAA,YAAY,SAAY,CAC1C,GAAI,CACFP,EAAW,EAAI,EACf,MAAMQ,EAAW,MAAM,MAAM,GAAGH,CAAY,aAAa,EACzD,GAAI,CAACG,EAAS,GAAI,MAAM,IAAI,MAAM,wBAAwB,EAC1D,MAAMC,EAAO,MAAMD,EAAS,KAAA,EAC5BJ,EAAUK,CAAI,CAChB,OAASC,EAAK,CACZR,EAASQ,EAAI,OAAO,CACtB,QAAA,CACEV,EAAW,EAAK,CAClB,CACF,EAAG,CAACK,CAAY,CAAC,EAEXM,EAAiBJ,cAAY,MAAOpD,EAAgByD,EAAa,SAAUC,EAAQ,GAAOzD,EAAiB,KAAO,CACtH,GAAI,CACF4C,EAAW,EAAI,EACfE,EAAS,IAAI,EAEb,MAAMM,EAAW,MAAM,MAAM,GAAGH,CAAY,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CACP,eAAgB,kBAAA,EAElB,KAAM,KAAK,UAAU,CACnB,OAAQlD,EACR,WAAAyD,EACA,MAAAC,EACA,GAAIzD,GAAkB,CAAE,eAAAA,CAAA,CAAe,CACxC,CAAA,CACF,EAED,GAAI,CAACoD,EAAS,GAAI,CAChB,MAAMM,EAAY,MAAMN,EAAS,KAAA,EAAO,MAAM,KAAO,CAAA,EAAG,EACxD,MAAM,IAAI,MAAMM,EAAU,SAAW,2BAA2B,CAClE,CAEA,MAAMC,EAAa,MAAMP,EAAS,KAAA,EAClC,OAAAV,EAAUiB,CAAU,EACbA,CACT,OAASL,EAAK,CACZ,OAAAR,EAASQ,EAAI,OAAO,EACb,IACT,QAAA,CACEV,EAAW,EAAK,CAClB,CACF,EAAG,CAACK,CAAY,CAAC,EAEXW,EAAkBT,cAAY,MAAOtC,GAAgB,CACzD,GAAI,CACF,MAAMuC,EAAW,MAAM,MAAM,GAAGH,CAAY,gBAAiB,CAC3D,OAAQ,OACR,QAAS,CACP,eAAgB,kBAAA,EAElB,KAAM,KAAK,UAAU,CACnB,OAAAR,EACA,QAAS5B,CAAA,CACV,CAAA,CACF,EAED,GAAI,CAACuC,EAAS,GAAI,MAAM,IAAI,MAAM,4BAA4B,EAE9D,OAAO,MAAMA,EAAS,KAAA,CACxB,OAASE,EAAK,CACZ,OAAAR,EAASQ,EAAI,OAAO,EACb,IACT,CACF,EAAG,CAACL,EAAcR,CAAM,CAAC,EAEnBoB,EAAcV,EAAAA,YAAY,IAAM,CACpCT,EAAU,IAAI,EACdI,EAAS,IAAI,CACf,EAAG,CAAA,CAAE,EAEL,MAAO,CACL,OAAAL,EACA,QAAAE,EACA,MAAAE,EACA,OAAAE,EACA,YAAAG,EACA,eAAAK,EACA,gBAAAK,EACA,YAAAC,CAAA,CAEJ,ECvFA,SAASC,GAAM,CACb,KAAM,CAAC/D,EAAgBgE,CAAiB,EAAIhD,EAAAA,SAAS,CAAA,CAAE,EACjD,CAACyC,EAAYQ,CAAa,EAAIjD,EAAAA,SAAS,QAAQ,EAC/C,CAACJ,EAAcsD,CAAe,EAAIlD,EAAAA,SAAS,EAAK,EAChD,CAACf,EAAgBkE,CAAiB,EAAInD,EAAAA,SAAS,EAAE,EAEjD,CACJ,OAAA0B,EACA,QAAAE,EACA,MAAAE,EACA,OAAAE,EACA,YAAAG,EACA,eAAAK,EACA,YAAAM,CAAA,EACErB,EAAA,EAEJ2B,EAAAA,UAAU,IAAM,CACdjB,EAAA,CACF,EAAG,CAACA,CAAW,CAAC,EAEhB,MAAMkB,EAAuB,SAAY,CACvC,GAAIrE,EAAe,SAAW,EAAG,CAC/B,MAAM,kCAAkC,EACxC,MACF,CAEA,MAAMwD,EAAexD,EAAgByD,EAAY,GAAOxD,CAAc,CACxE,EAEMqE,EAAsBtB,GAAW,CACrCgB,EAAkBhB,CAAM,CAC1B,EAEMuB,EAAwBC,GAAa,CACzCL,EAAkBK,CAAQ,CAC5B,EAGMC,EAAc,IAAM,CACxBX,EAAA,EACAE,EAAkB,CAAA,CAAE,EACpBE,EAAgB,EAAK,EACrBD,EAAc,QAAQ,EACtBE,EAAkB,EAAE,CACtB,EAEMO,EAAuB,IAAM,CACjCR,EAAgB,EAAI,CACtB,EAEA,OACE3D,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CAAO,UAAU,aAChB,SAAA,CAAAC,EAAAA,IAAC,KAAA,CAAG,UAAU,YAAY,SAAA,6BAA0B,EACpDA,EAAAA,IAAC,KAAE,SAAA,0DAAA,CAAwD,CAAA,EAC7D,EAEAA,EAAAA,IAACX,EAAA,CACC,eAAgByE,EAChB,gBAAiBtB,EACjB,eAAAhD,EACA,eAAAC,EACA,iBAAkBsE,CAAA,CAAA,EAGpBhE,EAAAA,KAAC,MAAA,CAAI,UAAU,kBACb,SAAA,CAAAA,EAAAA,KAAC,SAAA,CACC,MAAOkD,EACP,SAAWjE,GAAMyE,EAAczE,EAAE,OAAO,KAAK,EAC7C,UAAU,cAEV,SAAA,CAAAgB,EAAAA,IAAC,SAAA,CAAO,MAAM,OAAO,SAAA,OAAI,EACzBA,EAAAA,IAAC,SAAA,CAAO,MAAM,SAAS,SAAA,SAAM,EAC7BA,EAAAA,IAAC,SAAA,CAAO,MAAM,OAAO,SAAA,MAAA,CAAI,CAAA,CAAA,CAAA,EAG3BA,EAAAA,IAAC,SAAA,CACC,QAAS6D,EACT,SAAUzB,GAAW5C,EAAe,SAAW,EAC/C,UAAU,2BAET,WAAU,gBAAkB,iBAAA,CAAA,EAG/BQ,EAAAA,IAAC,SAAA,CACC,QAASiE,EACT,UAAU,wBACX,SAAA,OAAA,CAAA,EAIA/B,GAAU,CAAC9B,GACVJ,EAAAA,IAAC,SAAA,CACC,QAASkE,EACT,UAAU,yBACX,SAAA,iBAAA,CAAA,CAED,EAEJ,EAEC5B,GACCvC,EAAAA,KAAC,MAAA,CAAI,UAAU,gBAAgB,SAAA,CAAA,UACrBuC,CAAA,EACV,EAGDF,SAAYL,EAAA,EAAe,EAE3BG,GAAU,CAACE,GACVrC,EAAAA,KAAAoE,EAAAA,SAAA,CACE,SAAA,CAAAnE,EAAAA,IAAC,OAAI,UAAU,cACb,SAAAD,EAAAA,KAAC,OAAA,CAAK,UAAU,eACb,SAAA,CAAAmC,EAAO,SAAS,UAAU,YAAUA,EAAO,SAAS,KAAK,IAAEA,EAAO,SAAS,KAAK,OAAA,CAAA,CACnF,CAAA,CACF,EACAnC,EAAAA,KAAC,MAAA,CAAI,UAAU,gBACb,SAAA,CAAAC,EAAAA,IAACC,EAAA,CACC,KAAMiC,EAAO,KACb,MAAOA,EAAO,MACd,aAAA9B,CAAA,CAAA,EAEFJ,EAAAA,IAACyB,EAAA,CAAS,MAAOS,EAAO,KAAA,CAAO,CAAA,CAAA,CACjC,CAAA,EACF,EAGD,CAACA,GAAU,CAACE,GAAW,CAACE,GACvBtC,EAAAA,IAAC,MAAA,CAAI,MAAO,CAAE,UAAW,SAAU,QAAS,OAAQ,MAAO,SAAA,EAAa,SAAA,qDAAA,CAExE,CAAA,EAEJ,CAEJ,CC1IAoE,EAAS,WAAW,SAAS,eAAe,MAAM,CAAC,EAAE,aAClDC,EAAM,WAAN,CACC,SAAArE,MAACuD,IAAI,CAAA,CACP,CACF","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 |
-
|
|
|
|
| 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
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 59 |
global generator
|
| 60 |
if generator is None:
|
| 61 |
-
|
| 62 |
-
generator = CrosswordGenerator(
|
| 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
|
| 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
|
| 84 |
|
| 85 |
This endpoint matches the JavaScript API exactly for frontend compatibility.
|
| 86 |
"""
|
| 87 |
try:
|
| 88 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 153 |
"status": "healthy",
|
| 154 |
"timestamp": datetime.utcnow().isoformat(),
|
| 155 |
"backend": "python",
|
| 156 |
-
"version": "2.0.0"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
@router.get("/debug/
|
| 160 |
-
async def
|
| 161 |
topic: str,
|
| 162 |
difficulty: str = "medium",
|
| 163 |
max_words: int = 10,
|
| 164 |
request: Request = None
|
| 165 |
):
|
| 166 |
"""
|
| 167 |
-
Debug endpoint to test
|
| 168 |
"""
|
| 169 |
try:
|
| 170 |
-
|
| 171 |
-
if not
|
| 172 |
-
raise HTTPException(status_code=503, detail="
|
| 173 |
|
| 174 |
-
words = await
|
| 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"❌
|
| 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 =
|
| 14 |
|
| 15 |
class CrosswordGenerator:
|
| 16 |
-
def __init__(self,
|
| 17 |
self.max_attempts = 100
|
| 18 |
self.min_words = 6
|
| 19 |
-
self.
|
| 20 |
-
self.vector_service = vector_service
|
| 21 |
|
| 22 |
-
async def generate_puzzle(self, topics: List[str], difficulty: str = "medium",
|
| 23 |
"""
|
| 24 |
Generate a complete crossword puzzle.
|
| 25 |
"""
|
| 26 |
try:
|
| 27 |
-
|
| 28 |
-
|
| 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
|
| 37 |
-
words = await self._select_words(topics, difficulty,
|
| 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":
|
| 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,
|
| 69 |
-
"""Select words for the crossword."""
|
| 70 |
-
|
| 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 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 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 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
return {
|
| 277 |
"grid": trimmed["grid"],
|
| 278 |
-
"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 |
-
|
|
|
|
|
|
|
|
|
|
| 682 |
|
| 683 |
return {
|
| 684 |
"grid": trimmed["grid"],
|
| 685 |
-
"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
|
| 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 |
-
|
| 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,
|
| 16 |
-
self.
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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
|
| 41 |
from .crossword_generator import CrosswordGenerator as ActualGenerator
|
| 42 |
-
actual_generator = ActualGenerator(
|
| 43 |
|
| 44 |
-
puzzle = await actual_generator.generate_puzzle(topics, difficulty,
|
| 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,
|
| 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,
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
|
| 82 |
-
|
| 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 |
-
|
| 89 |
|
| 90 |
# Try to load from cache first
|
| 91 |
if self._load_cached_index():
|
| 92 |
-
|
| 93 |
else:
|
| 94 |
# Build from scratch
|
| 95 |
-
|
| 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 |
-
|
| 106 |
|
| 107 |
# Pre-compute embeddings for all vocabulary words
|
| 108 |
embedding_start = time.time()
|
| 109 |
-
|
| 110 |
await self._build_embeddings_index()
|
| 111 |
embedding_time = time.time() - embedding_start
|
| 112 |
-
|
| 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 |
-
|
| 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 |
-
|
| 126 |
except Exception as e:
|
| 127 |
cache_time = time.time() - cache_start
|
| 128 |
-
|
| 129 |
-
|
| 130 |
self.cache_manager = None
|
| 131 |
|
| 132 |
self.is_initialized = True
|
| 133 |
total_time = time.time() - start_time
|
| 134 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 253 |
|
| 254 |
# Combine all embeddings
|
| 255 |
-
|
| 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 |
-
|
| 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 |
-
|
| 266 |
faiss.normalize_L2(self.word_embeddings)
|
| 267 |
|
| 268 |
# Add to FAISS index
|
| 269 |
-
|
| 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,
|
| 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,
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1051 |
return False
|
| 1052 |
|
| 1053 |
-
|
| 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 |
-
|
| 1060 |
|
| 1061 |
# Load embeddings
|
| 1062 |
self.word_embeddings = np.load(self.embeddings_cache_path)
|
| 1063 |
-
|
| 1064 |
|
| 1065 |
# Load FAISS index
|
| 1066 |
self.faiss_index = faiss.read_index(self.faiss_cache_path)
|
| 1067 |
-
|
| 1068 |
|
| 1069 |
cache_time = time.time() - cache_start
|
| 1070 |
-
|
| 1071 |
return True
|
| 1072 |
|
| 1073 |
except Exception as e:
|
| 1074 |
-
|
| 1075 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1096 |
-
|
| 1097 |
|
| 1098 |
except Exception as e:
|
| 1099 |
-
|
| 1100 |
-
|
| 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 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
else:
|
| 1194 |
-
|
| 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.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|