Initial commit: Organize project structure

- Create organized directory structure (src/, docs/, data/, static/, templates/)
- Add comprehensive .gitignore for Python projects
- Move Python source files to src/
- Move documentation files to docs/ with project/ and user/ subdirectories
- Move database files to data/
- Update all database path references in Python code
- Maintain Flask static/ and templates/ directories

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-10 00:40:39 +03:00
commit fd87ebca03
23 changed files with 7317 additions and 0 deletions

285
src/app.py Normal file
View File

@@ -0,0 +1,285 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
FLASK WEB APPLICATION - INDEX-SISTEM-JOCURI
Author: Claude AI Assistant
Date: 2025-09-09
Purpose: Web interface for searching educational activities
Features:
- Search interface matching interfata-web.jpg mockup exactly
- 9 filter dropdowns as specified in PRD
- Full-text search functionality
- Results display in table format
- Links to source files
- Activity sheet generation
PRD Requirements:
- RF6: Layout identical to mockup
- RF7: Search box for free text search
- RF8: 9 dropdown filters
- RF9: Apply and Reset buttons
- RF10: Results table display
- RF11: Links to source files
"""
from flask import Flask, request, render_template, jsonify, redirect, url_for
from database import DatabaseManager
import os
from pathlib import Path
import json
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key-here'
# Initialize database manager
db = DatabaseManager("../data/activities.db")
# Filter options for dropdowns (based on PRD RF8)
FILTER_OPTIONS = {
'valori': [
'Viziune și perspectivă',
'Recunoștință',
'Altele',
'Management timpul',
'Identitate personală'
],
'durata': [
'5-15min',
'15-30min',
'30+min'
],
'tematica': [
'cercetășesc',
'team building',
'educativ'
],
'domeniu': [
'sport',
'artă',
'știință'
],
'metoda': [
'joc',
'poveste',
'atelier'
],
'materiale': [
'fără',
'simple',
'complexe'
],
'competente_fizice': [
'fizice',
'mentale',
'sociale'
],
'competente_impactate': [
'fizice',
'mentale',
'sociale'
],
'participanti': [
'2-5',
'5-10',
'10-30',
'30+'
],
'varsta': [
'5-8',
'8-12',
'12-16',
'16+'
]
}
def get_dynamic_filter_options():
"""Get dynamic filter options from database"""
try:
return db.get_filter_options()
except:
return {}
@app.route('/')
def index():
"""Main search page"""
# Get dynamic filter options from database
dynamic_filters = get_dynamic_filter_options()
# Merge with static options
all_filters = FILTER_OPTIONS.copy()
all_filters.update(dynamic_filters)
return render_template('index.html', filters=all_filters)
@app.route('/search', methods=['GET', 'POST'])
def search():
"""Search activities based on filters and query"""
# Get search parameters
search_query = request.form.get('search_query', '').strip() or request.args.get('q', '').strip()
# Get filter values
filters = {}
for filter_name in FILTER_OPTIONS.keys():
value = request.form.get(filter_name) or request.args.get(filter_name)
if value and value != '':
filters[filter_name] = value
# Map filter names to database fields
db_filters = {}
if 'tematica' in filters:
db_filters['category'] = filters['tematica']
if 'varsta' in filters:
db_filters['age_group'] = filters['varsta'] + ' ani'
if 'participanti' in filters:
db_filters['participants'] = filters['participanti'] + ' persoane'
if 'durata' in filters:
db_filters['duration'] = filters['durata']
if 'materiale' in filters:
material_map = {'fără': 'Fără materiale', 'simple': 'Materiale simple', 'complexe': 'Materiale complexe'}
db_filters['materials'] = material_map.get(filters['materiale'], filters['materiale'])
# Search in database
try:
results = db.search_activities(
search_text=search_query if search_query else None,
**db_filters,
limit=100
)
# Convert results to list of dicts for template
activities = []
for result in results:
activities.append({
'id': result['id'],
'title': result['title'],
'description': result['description'][:200] + '...' if len(result['description']) > 200 else result['description'],
'category': result['category'],
'age_group': result['age_group'],
'participants': result['participants'],
'duration': result['duration'],
'materials': result['materials'],
'file_path': result['file_path'],
'tags': json.loads(result['tags']) if result['tags'] else []
})
except Exception as e:
print(f"Search error: {e}")
activities = []
# Get dynamic filter options for the form
dynamic_filters = get_dynamic_filter_options()
all_filters = FILTER_OPTIONS.copy()
all_filters.update(dynamic_filters)
return render_template('results.html',
activities=activities,
search_query=search_query,
applied_filters=filters,
filters=all_filters,
results_count=len(activities))
@app.route('/generate_sheet/<int:activity_id>')
def generate_sheet(activity_id):
"""Generate activity sheet for specific activity"""
try:
# Get activity from database
results = db.search_activities(limit=1000) # Get all to find by ID
activity_data = None
for result in results:
if result['id'] == activity_id:
activity_data = result
break
if not activity_data:
return "Activity not found", 404
# Get similar activities for recommendations
similar_activities = db.search_activities(
category=activity_data['category'],
limit=5
)
# Filter out current activity and limit to 3
recommendations = [act for act in similar_activities if act['id'] != activity_id][:3]
return render_template('fisa.html',
activity=activity_data,
recommendations=recommendations)
except Exception as e:
print(f"Sheet generation error: {e}")
return f"Error generating sheet: {e}", 500
@app.route('/file/<path:filename>')
def view_file(filename):
"""Serve activity files (PDFs, docs, etc.)"""
# Security: only serve files from the base directory
base_path = Path("/mnt/d/GoogleDrive/Cercetasi/carti-camp-jocuri")
file_path = base_path / filename
try:
if file_path.exists() and file_path.is_file():
# For now, just return file info - in production you'd serve the actual file
return f"File: {filename}<br>Path: {file_path}<br>Size: {file_path.stat().st_size} bytes"
else:
return "File not found", 404
except Exception as e:
return f"Error accessing file: {e}", 500
@app.route('/api/statistics')
def api_statistics():
"""API endpoint for database statistics"""
try:
stats = db.get_statistics()
return jsonify(stats)
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/reset_filters')
def reset_filters():
"""Reset all filters and redirect to main page"""
return redirect(url_for('index'))
@app.errorhandler(404)
def not_found(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
return render_template('500.html'), 500
def init_app():
"""Initialize application"""
print("🚀 Starting INDEX-SISTEM-JOCURI Flask Application")
print("=" * 50)
# Check if database exists and has data
try:
stats = db.get_statistics()
print(f"✅ Database connected: {stats['total_activities']} activities loaded")
if stats['total_activities'] == 0:
print("⚠️ Warning: No activities found in database!")
print(" Run: python indexer.py --clear-db to index files first")
except Exception as e:
print(f"❌ Database error: {e}")
print(f"🌐 Web interface will be available at: http://localhost:5000")
print("📱 Interface matches: interfata-web.jpg")
print("=" * 50)
if __name__ == '__main__':
init_app()
# Run Flask app
app.run(
host='0.0.0.0', # Accept connections from any IP
port=5000,
debug=True, # Enable debug mode for development
threaded=True # Handle multiple requests
)

329
src/database.py Normal file
View File

@@ -0,0 +1,329 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
DATABASE HELPER - SQLite database management for INDEX-SISTEM-JOCURI
Author: Claude AI Assistant
Date: 2025-09-09
Purpose: Database management according to PRD specifications
Schema based on PRD.md section 5.3
"""
import sqlite3
import json
from pathlib import Path
from typing import Dict, List, Any, Optional
from datetime import datetime
class DatabaseManager:
"""Manager for SQLite database operations"""
def __init__(self, db_path: str = "../data/activities.db"):
self.db_path = Path(db_path)
self.init_database()
def init_database(self):
"""Initialize database with PRD-compliant schema"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Main activities table (PRD Section 5.3)
cursor.execute('''
CREATE TABLE IF NOT EXISTS activities (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
file_path TEXT NOT NULL,
file_type TEXT,
page_number INTEGER,
tags TEXT,
category TEXT,
age_group TEXT,
participants TEXT,
duration TEXT,
materials TEXT,
difficulty TEXT DEFAULT 'mediu',
source_text TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Full-text search table (PRD Section 5.3)
cursor.execute('''
CREATE VIRTUAL TABLE IF NOT EXISTS activities_fts USING fts5(
title, description, source_text,
content='activities'
)
''')
# Search history table
cursor.execute('''
CREATE TABLE IF NOT EXISTS search_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
query TEXT NOT NULL,
results_count INTEGER,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# File processing log
cursor.execute('''
CREATE TABLE IF NOT EXISTS file_processing_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_path TEXT NOT NULL,
file_type TEXT,
status TEXT,
activities_extracted INTEGER DEFAULT 0,
error_message TEXT,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
print(f"✅ Database initialized: {self.db_path}")
def insert_activity(self, activity_data: Dict) -> int:
"""Insert a new activity into the database"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Convert lists to JSON strings
tags_json = json.dumps(activity_data.get('tags', []), ensure_ascii=False)
cursor.execute('''
INSERT INTO activities
(title, description, file_path, file_type, page_number, tags,
category, age_group, participants, duration, materials,
difficulty, source_text)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
activity_data['title'],
activity_data.get('description', ''),
activity_data['file_path'],
activity_data.get('file_type', ''),
activity_data.get('page_number'),
tags_json,
activity_data.get('category', ''),
activity_data.get('age_group', ''),
activity_data.get('participants', ''),
activity_data.get('duration', ''),
activity_data.get('materials', ''),
activity_data.get('difficulty', 'mediu'),
activity_data.get('source_text', '')
))
activity_id = cursor.lastrowid
# Update FTS index
cursor.execute('''
INSERT INTO activities_fts (rowid, title, description, source_text)
VALUES (?, ?, ?, ?)
''', (
activity_id,
activity_data['title'],
activity_data.get('description', ''),
activity_data.get('source_text', '')
))
conn.commit()
conn.close()
return activity_id
def search_activities(self,
search_text: str = None,
category: str = None,
age_group: str = None,
participants: str = None,
duration: str = None,
materials: str = None,
difficulty: str = None,
limit: int = 50) -> List[Dict]:
"""Search activities with multiple filters"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Build the query
if search_text:
# Use FTS for text search
query = '''
SELECT a.* FROM activities a
JOIN activities_fts fts ON a.id = fts.rowid
WHERE activities_fts MATCH ?
'''
params = [search_text]
else:
query = 'SELECT * FROM activities WHERE 1=1'
params = []
# Add filters
if category:
query += ' AND category = ?'
params.append(category)
if age_group:
query += ' AND age_group = ?'
params.append(age_group)
if participants:
query += ' AND participants = ?'
params.append(participants)
if duration:
query += ' AND duration = ?'
params.append(duration)
if materials:
query += ' AND materials = ?'
params.append(materials)
if difficulty:
query += ' AND difficulty = ?'
params.append(difficulty)
query += f' LIMIT {limit}'
cursor.execute(query, params)
columns = [desc[0] for desc in cursor.description]
results = [dict(zip(columns, row)) for row in cursor.fetchall()]
conn.close()
return results
def get_filter_options(self) -> Dict[str, List[str]]:
"""Get all unique values for filter dropdowns"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
filters = {}
# Get unique values for each filter field
fields = ['category', 'age_group', 'participants', 'duration',
'materials', 'difficulty']
for field in fields:
cursor.execute(f'''
SELECT DISTINCT {field}
FROM activities
WHERE {field} IS NOT NULL AND {field} != ''
ORDER BY {field}
''')
filters[field] = [row[0] for row in cursor.fetchall()]
conn.close()
return filters
def log_file_processing(self, file_path: str, file_type: str,
status: str, activities_count: int = 0,
error_message: str = None):
"""Log file processing results"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
INSERT INTO file_processing_log
(file_path, file_type, status, activities_extracted, error_message)
VALUES (?, ?, ?, ?, ?)
''', (file_path, file_type, status, activities_count, error_message))
conn.commit()
conn.close()
def get_statistics(self) -> Dict[str, Any]:
"""Get database statistics"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# Total activities
cursor.execute('SELECT COUNT(*) FROM activities')
total_activities = cursor.fetchone()[0]
# Activities by category
cursor.execute('''
SELECT category, COUNT(*)
FROM activities
WHERE category IS NOT NULL AND category != ''
GROUP BY category
ORDER BY COUNT(*) DESC
''')
categories = dict(cursor.fetchall())
# Activities by age group
cursor.execute('''
SELECT age_group, COUNT(*)
FROM activities
WHERE age_group IS NOT NULL AND age_group != ''
GROUP BY age_group
ORDER BY age_group
''')
age_groups = dict(cursor.fetchall())
# Recent file processing
cursor.execute('''
SELECT file_type, COUNT(*) as processed_files,
SUM(activities_extracted) as total_activities
FROM file_processing_log
GROUP BY file_type
ORDER BY COUNT(*) DESC
''')
file_stats = [dict(zip(['file_type', 'files_processed', 'activities_extracted'], row))
for row in cursor.fetchall()]
conn.close()
return {
'total_activities': total_activities,
'categories': categories,
'age_groups': age_groups,
'file_statistics': file_stats,
'last_updated': datetime.now().isoformat()
}
def clear_database(self):
"""Clear all activities (for re-indexing)"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('DELETE FROM activities')
cursor.execute('DELETE FROM activities_fts')
cursor.execute('DELETE FROM file_processing_log')
conn.commit()
conn.close()
print("✅ Database cleared for re-indexing")
def test_database():
"""Test database functionality"""
db = DatabaseManager("../data/test_activities.db")
# Test insert
test_activity = {
'title': 'Test Activity',
'description': 'A test activity for validation',
'file_path': '/path/to/test.pdf',
'file_type': 'pdf',
'page_number': 1,
'tags': ['test', 'example'],
'category': 'Test Category',
'age_group': '8-12 ani',
'participants': '5-10 persoane',
'duration': '15-30 minute',
'materials': 'Fără materiale',
'source_text': 'Test activity for validation purposes'
}
activity_id = db.insert_activity(test_activity)
print(f"✅ Inserted test activity with ID: {activity_id}")
# Test search
results = db.search_activities(search_text="test")
print(f"✅ Search found {len(results)} activities")
# Test filters
filters = db.get_filter_options()
print(f"✅ Available filters: {list(filters.keys())}")
# Test statistics
stats = db.get_statistics()
print(f"✅ Statistics: {stats['total_activities']} total activities")
print("🎯 Database testing completed successfully!")
if __name__ == "__main__":
test_database()

502
src/game_library_manager.py Normal file
View File

@@ -0,0 +1,502 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GAME LIBRARY MANAGER - Manager pentru Colecția de Jocuri și Activități
Autor: Claude AI Assistant
Data: 2025-09-09
Scopul: Automatizarea căutărilor și generarea de fișe de activități din colecția catalogată
Funcționalități:
- Căutare activități după criterii multiple
- Generare fișe de activități personalizate
- Export în format PDF/HTML/Markdown
- Statistici și rapoarte
- Administrare index
"""
import json
import os
import sqlite3
import re
from datetime import datetime
from typing import Dict, List, Tuple, Optional, Union
from dataclasses import dataclass, asdict
from pathlib import Path
@dataclass
class Activity:
"""Clasă pentru stocarea informațiilor despre o activitate"""
id: str
title: str
file_path: str
category: str
subcategory: str
age_group: str
participants: str
duration: str
materials: str
description: str
examples: List[str]
keywords: List[str]
difficulty: str = "mediu"
language: str = "ro"
def to_dict(self) -> Dict:
return asdict(self)
def matches_criteria(self, criteria: Dict) -> bool:
"""Verifică dacă activitatea se potrivește cu criteriile de căutare"""
for key, value in criteria.items():
if key == 'age_min' and value:
# Extrage vârsta minimă din age_group
age_match = re.search(r'(\d+)', self.age_group)
if age_match and int(age_match.group(1)) < value:
return False
elif key == 'keywords' and value:
# Caută cuvinte cheie în toate câmpurile text
search_text = f"{self.title} {self.description} {' '.join(self.keywords)}".lower()
if not any(keyword.lower() in search_text for keyword in value):
return False
elif key in ['category', 'difficulty', 'language'] and value:
if getattr(self, key).lower() != value.lower():
return False
return True
class GameLibraryManager:
"""Manager principal pentru colecția de jocuri"""
def __init__(self, base_path: str = "/mnt/d/GoogleDrive/Cercetasi/carti-camp-jocuri"):
self.base_path = Path(base_path)
self.index_path = self.base_path / "INDEX-SISTEM-JOCURI"
self.db_path = self.index_path / "game_library.db"
self.activities: List[Activity] = []
self.init_database()
self.load_activities_from_index()
def init_database(self):
"""Inițializează baza de date SQLite"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS activities (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
file_path TEXT NOT NULL,
category TEXT NOT NULL,
subcategory TEXT,
age_group TEXT,
participants TEXT,
duration TEXT,
materials TEXT,
description TEXT,
examples TEXT,
keywords TEXT,
difficulty TEXT DEFAULT 'mediu',
language TEXT DEFAULT 'ro',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS search_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
query TEXT NOT NULL,
results_count INTEGER,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
def load_activities_from_index(self):
"""Încarcă activitățile din indexul principal"""
# Date structurate din INDEX_MASTER_JOCURI_ACTIVITATI.md
sample_activities = [
Activity(
id="cubs_acting_01",
title="Animal Mimes",
file_path="./Activities and Games Scouts NZ/Cubs Acting Games.pdf",
category="Jocuri Cercetășești",
subcategory="Acting Games",
age_group="8-11 ani",
participants="8-30 copii",
duration="5-10 minute",
materials="Fără materiale",
description="Joc de imitare animale prin mimică, dezvoltă creativitatea și expresia corporală",
examples=["Imitarea unui leu", "Mișcarea unei broaște", "Zborul unei păsări"],
keywords=["acting", "mimică", "animale", "creativitate", "expresie"]
),
Activity(
id="cubs_team_01",
title="Relay Races",
file_path="./Activities and Games Scouts NZ/Cubs Team Games.pdf",
category="Jocuri Cercetășești",
subcategory="Team Games",
age_group="8-11 ani",
participants="12-30 copii",
duration="15-25 minute",
materials="Echipament sportiv de bază",
description="Curse de ștafetă variate pentru dezvoltarea spiritului de echipă",
examples=["Ștafeta cu mingea", "Ștafeta cu obstacole", "Ștafeta cu sacii"],
keywords=["ștafetă", "echipă", "alergare", "competiție", "sport"]
),
Activity(
id="teambuilding_01",
title="Cercul Încrederii",
file_path="./160-de-activitati-dinamice-jocuri-pentru-team-building-.pdf",
category="Team Building",
subcategory="Exerciții de Încredere",
age_group="12+ ani",
participants="8-15 persoane",
duration="15-20 minute",
materials="Niciuna",
description="Participanții stau în cerc, unul în mijloc se lasă să cadă încredințându-se în ceilalți",
examples=["Căderea încrederii", "Sprijinirea în grup", "Construirea încrederii"],
keywords=["încredere", "echipă", "cooperare", "siguranță", "grup"]
),
Activity(
id="escape_room_01",
title="Puzzle cu Puncte și Coduri",
file_path="./escape-room/101 Puzzles for Low Cost Escape Rooms.pdf",
category="Escape Room",
subcategory="Coduri și Cifre",
age_group="10+ ani",
participants="3-8 persoane",
duration="10-30 minute",
materials="Hârtie, creioane, obiecte cu puncte",
description="Puzzle-uri cu puncte care formează coduri numerice sau literale",
examples=["Conectarea punctelor pentru cifre", "Coduri Morse cu puncte", "Desene cu sens ascuns"],
keywords=["puzzle", "coduri", "logică", "rezolvare probleme", "mister"]
),
Activity(
id="orienteering_01",
title="Compass Game cu 8 Posturi",
file_path="./Compass Game Beginner.pdf",
category="Orientare",
subcategory="Jocuri cu Busola",
age_group="10+ ani",
participants="6-20 persoane",
duration="45-90 minute",
materials="Busole, conuri colorate, carduri cu provocări",
description="Joc cu 8 posturi și 90 de provocări diferite pentru învățarea orientării",
examples=["Găsirea azimutului", "Calcularea distanței", "Identificarea pe hartă"],
keywords=["orientare", "busola", "azimut", "hartă", "navigare"]
),
Activity(
id="first_aid_01",
title="RCP - Resuscitare Cardio-Pulmonară",
file_path="./prim-ajutor/RCP_demonstration.jpg",
category="Primul Ajutor",
subcategory="Tehnici de Salvare",
age_group="14+ ani",
participants="5-15 persoane",
duration="30-45 minute",
materials="Manechin RCP, kit primul ajutor",
description="Învățarea tehnicilor de RCP pentru situații de urgență",
examples=["Compresia toracică", "Ventilația artificială", "Verificarea pulsului"],
keywords=["RCP", "primul ajutor", "urgență", "salvare", "resuscitare"],
difficulty="avansat"
),
Activity(
id="science_biology_01",
title="Leaf Collection & Identification",
file_path="./dragon.sleepdeprived.ca/program/science/biology.html",
category="Activități Educaționale",
subcategory="Biologie",
age_group="8+ ani",
participants="5-25 persoane",
duration="60-120 minute",
materials="Pungi pentru colectat, lupă, ghid identificare",
description="Colectarea și identificarea frunzelor pentru crearea unui ierbar",
examples=["Colectarea frunzelor", "Identificarea speciilor", "Crearea ierbarului"],
keywords=["biologie", "natură", "frunze", "identificare", "ierbar"]
),
Activity(
id="songs_welcome_01",
title="Welcome Circle Song",
file_path="./dragon.sleepdeprived.ca/songbook/songs1/welcome.html",
category="Resurse Speciale",
subcategory="Cântece de Bun Venit",
age_group="5+ ani",
participants="8-50 persoane",
duration="3-5 minute",
materials="Niciuna",
description="Cântec simplu în cerc pentru întâmpinarea participanților noi",
examples=["Cântecul de bun venit", "Prezentarea numelor", "Formarea cercului"],
keywords=["cântec", "bun venit", "cerc", "prezentare", "început"]
)
]
self.activities = sample_activities
self.save_activities_to_db()
def save_activities_to_db(self):
"""Salvează activitățile în baza de date"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
for activity in self.activities:
cursor.execute('''
INSERT OR REPLACE INTO activities
(id, title, file_path, category, subcategory, age_group, participants,
duration, materials, description, examples, keywords, difficulty, language)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
activity.id, activity.title, activity.file_path, activity.category,
activity.subcategory, activity.age_group, activity.participants,
activity.duration, activity.materials, activity.description,
json.dumps(activity.examples, ensure_ascii=False),
json.dumps(activity.keywords, ensure_ascii=False),
activity.difficulty, activity.language
))
conn.commit()
conn.close()
def search_activities(self, **criteria) -> List[Activity]:
"""
Caută activități după criterii
Criterii disponibile:
- category: categoria principală
- age_min: vârsta minimă
- participants_max: numărul maxim de participanți
- duration_max: durata maximă în minute
- materials: tipul de materiale
- keywords: lista de cuvinte cheie
- difficulty: nivelul de dificultate
"""
results = []
for activity in self.activities:
if activity.matches_criteria(criteria):
results.append(activity)
# Salvează căutarea în istoric
self.save_search_to_history(str(criteria), len(results))
return results
def save_search_to_history(self, query: str, results_count: int):
"""Salvează căutarea în istoricul de căutări"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute(
'INSERT INTO search_history (query, results_count) VALUES (?, ?)',
(query, results_count)
)
conn.commit()
conn.close()
def generate_activity_sheet(self, activity: Activity, format: str = "markdown") -> str:
"""Generează o fișă de activitate în formatul specificat"""
if format == "markdown":
return self._generate_markdown_sheet(activity)
elif format == "html":
return self._generate_html_sheet(activity)
else:
raise ValueError(f"Format nepermis: {format}")
def _generate_markdown_sheet(self, activity: Activity) -> str:
"""Generează fișa în format Markdown"""
examples_text = "\n".join(f"- {ex}" for ex in activity.examples)
keywords_text = ", ".join(activity.keywords)
sheet = f"""# FIȘA ACTIVITĂȚII: {activity.title}
## 📋 INFORMAȚII GENERALE
- **Categorie:** {activity.category}{activity.subcategory}
- **Grupa de vârstă:** {activity.age_group}
- **Numărul participanților:** {activity.participants}
- **Durata estimată:** {activity.duration}
- **Nivel de dificultate:** {activity.difficulty.capitalize()}
## 🎯 DESCRIEREA ACTIVITĂȚII
{activity.description}
## 🧰 MATERIALE NECESARE
{activity.materials}
## 💡 EXEMPLE DE APLICARE
{examples_text}
## 🔗 SURSA
**Fișier:** `{activity.file_path}`
## 🏷️ CUVINTE CHEIE
{keywords_text}
---
**Generat automat:** {datetime.now().strftime('%Y-%m-%d %H:%M')}
**ID Activitate:** {activity.id}
"""
return sheet
def _generate_html_sheet(self, activity: Activity) -> str:
"""Generează fișa în format HTML"""
examples_html = "".join(f"<li>{ex}</li>" for ex in activity.examples)
keywords_html = ", ".join(f'<span class="keyword">{kw}</span>' for kw in activity.keywords)
sheet = f"""<!DOCTYPE html>
<html lang="ro">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Fișa Activității: {activity.title}</title>
<style>
body {{ font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }}
.header {{ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 10px; }}
.info-grid {{ display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin: 20px 0; }}
.info-item {{ background: #f8f9fa; padding: 15px; border-radius: 8px; border-left: 4px solid #667eea; }}
.examples {{ background: #e8f5e8; padding: 15px; border-radius: 8px; }}
.keyword {{ background: #667eea; color: white; padding: 3px 8px; border-radius: 15px; font-size: 0.9em; }}
.footer {{ margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666; }}
</style>
</head>
<body>
<div class="header">
<h1>🎮 {activity.title}</h1>
<p><strong>{activity.category}</strong> → {activity.subcategory}</p>
</div>
<div class="info-grid">
<div class="info-item">
<strong>👥 Participanți:</strong><br>{activity.participants}
</div>
<div class="info-item">
<strong>⏰ Durata:</strong><br>{activity.duration}
</div>
<div class="info-item">
<strong>🎂 Vârsta:</strong><br>{activity.age_group}
</div>
<div class="info-item">
<strong>📊 Dificultate:</strong><br>{activity.difficulty.capitalize()}
</div>
</div>
<div class="info-item">
<h3>🎯 Descrierea Activității</h3>
<p>{activity.description}</p>
</div>
<div class="info-item">
<h3>🧰 Materiale Necesare</h3>
<p>{activity.materials}</p>
</div>
<div class="examples">
<h3>💡 Exemple de Aplicare</h3>
<ul>{examples_html}</ul>
</div>
<div class="info-item">
<h3>🏷️ Cuvinte Cheie</h3>
<p>{keywords_html}</p>
</div>
<div class="footer">
<p><strong>Sursa:</strong> <code>{activity.file_path}</code></p>
<p><strong>ID:</strong> {activity.id} | <strong>Generat:</strong> {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
</div>
</body>
</html>"""
return sheet
def export_search_results(self, activities: List[Activity], filename: str, format: str = "markdown"):
"""Exportă rezultatele căutării într-un fișier"""
output_path = self.index_path / f"{filename}.{format}"
if format == "markdown":
content = f"# REZULTATE CĂUTARE - {datetime.now().strftime('%Y-%m-%d %H:%M')}\n\n"
content += f"**Numărul de activități găsite:** {len(activities)}\n\n---\n\n"
for i, activity in enumerate(activities, 1):
content += f"## {i}. {activity.title}\n\n"
content += f"**Categorie:** {activity.category}{activity.subcategory} \n"
content += f"**Vârsta:** {activity.age_group} | **Participanți:** {activity.participants} \n"
content += f"**Durata:** {activity.duration} | **Materiale:** {activity.materials} \n\n"
content += f"{activity.description}\n\n"
content += f"**Fișier:** `{activity.file_path}`\n\n---\n\n"
with open(output_path, 'w', encoding='utf-8') as f:
f.write(content)
return output_path
def get_statistics(self) -> Dict:
"""Returnează statistici despre colecție"""
total_activities = len(self.activities)
# Grupare pe categorii
categories = {}
age_groups = {}
difficulties = {}
for activity in self.activities:
categories[activity.category] = categories.get(activity.category, 0) + 1
age_groups[activity.age_group] = age_groups.get(activity.age_group, 0) + 1
difficulties[activity.difficulty] = difficulties.get(activity.difficulty, 0) + 1
return {
'total_activities': total_activities,
'categories': categories,
'age_groups': age_groups,
'difficulties': difficulties,
'last_updated': datetime.now().strftime('%Y-%m-%d %H:%M')
}
def main():
"""Funcție principală pentru testarea sistemului"""
print("🎮 GAME LIBRARY MANAGER - Inițializare...")
# Inițializare manager
manager = GameLibraryManager()
print(f"✅ Încărcate {len(manager.activities)} activități")
# Exemplu căutări
print("\n🔍 EXEMPLE DE CĂUTĂRI:")
# Căutare 1: Activități pentru copii mici
print("\n1. Activități pentru copii 5-8 ani:")
young_activities = manager.search_activities(age_min=5, keywords=["simplu"])
for activity in young_activities[:3]: # Prima 3
print(f" - {activity.title} ({activity.category})")
# Căutare 2: Team building
print("\n2. Activități de team building:")
team_activities = manager.search_activities(category="Team Building")
for activity in team_activities:
print(f" - {activity.title} ({activity.duration})")
# Căutare 3: Activități cu materiale minime
print("\n3. Activități fără materiale:")
no_materials = manager.search_activities(keywords=["fără materiale", "niciuna"])
for activity in no_materials[:3]:
print(f" - {activity.title} ({activity.materials})")
# Generare fișă exemplu
print("\n📄 GENERARE FIȘĂ EXEMPLU:")
if manager.activities:
sample_activity = manager.activities[0]
sheet = manager.generate_activity_sheet(sample_activity, "markdown")
sheet_path = manager.index_path / f"FISA_EXEMPLU_{sample_activity.id}.md"
with open(sheet_path, 'w', encoding='utf-8') as f:
f.write(sheet)
print(f" Fișă generată: {sheet_path}")
# Statistici
print("\n📊 STATISTICI COLECȚIE:")
stats = manager.get_statistics()
print(f" Total activități: {stats['total_activities']}")
print(f" Categorii: {list(stats['categories'].keys())}")
print(f" Ultimul update: {stats['last_updated']}")
print("\n🎯 SISTEM INIȚIALIZAT CU SUCCES!")
print("💡 Pentru utilizare interactivă, rulați: python -c \"from game_library_manager import GameLibraryManager; manager = GameLibraryManager(); print('Manager inițializat!')\"")
if __name__ == "__main__":
main()

579
src/indexer.py Normal file
View File

@@ -0,0 +1,579 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
MULTI-FORMAT INDEXER - Automated activity extraction from various file types
Author: Claude AI Assistant
Date: 2025-09-09
Purpose: Extract educational activities from PDF, DOC, HTML, MD, TXT files
Supported formats:
- PDF: PyPDF2 + pdfplumber (backup)
- DOC/DOCX: python-docx
- HTML: BeautifulSoup4
- MD: markdown
- TXT: direct text processing
Requirements from PRD:
- RF1: Extract activities from all supported formats
- RF2: Auto-detect parameters (title, description, age, duration, materials)
- RF3: Batch processing for existing files
- RF4: Incremental processing for new files
- RF5: Progress tracking
"""
import os
import re
import json
import argparse
from pathlib import Path
from typing import List, Dict, Optional, Tuple
from datetime import datetime
import hashlib
# File processing imports
try:
import PyPDF2
PDF_AVAILABLE = True
except ImportError:
PDF_AVAILABLE = False
try:
import pdfplumber
PDFPLUMBER_AVAILABLE = True
except ImportError:
PDFPLUMBER_AVAILABLE = False
try:
from docx import Document as DocxDocument
DOCX_AVAILABLE = True
except ImportError:
DOCX_AVAILABLE = False
try:
from bs4 import BeautifulSoup
HTML_AVAILABLE = True
except ImportError:
HTML_AVAILABLE = False
try:
import markdown
MARKDOWN_AVAILABLE = True
except ImportError:
MARKDOWN_AVAILABLE = False
from database import DatabaseManager
class ActivityExtractor:
"""Base class for activity extraction"""
# Pattern definitions for auto-detection
TITLE_PATTERNS = [
r'^#\s+(.+)$', # Markdown header
r'^##\s+(.+)$', # Markdown subheader
r'^\*\*([^*]+)\*\*', # Bold text
r'^([A-Z][^.!?]*[.!?])$', # Capitalized sentence
r'^(\d+[\.\)]\s*[A-Z][^.!?]*[.!?])$', # Numbered item
]
AGE_PATTERNS = [
r'(\d+)[-](\d+)\s*ani', # "8-12 ani"
r'(\d+)\+\s*ani', # "12+ ani"
r'varsta\s*:?\s*(\d+)[-](\d+)', # "Varsta: 8-12"
r'age\s*:?\s*(\d+)[-](\d+)', # "Age: 8-12"
]
DURATION_PATTERNS = [
r'(\d+)[-](\d+)\s*min', # "15-30 min"
r'(\d+)\s*minute', # "15 minute"
r'durata\s*:?\s*(\d+)[-](\d+)', # "Durata: 15-30"
r'duration\s*:?\s*(\d+)[-](\d+)', # "Duration: 15-30"
]
PARTICIPANTS_PATTERNS = [
r'(\d+)[-](\d+)\s*(copii|persoane|participanti)', # "8-15 copii"
r'(\d+)\+\s*(copii|persoane|participanti)', # "10+ copii"
r'participanti\s*:?\s*(\d+)[-](\d+)', # "Participanti: 5-10"
r'players\s*:?\s*(\d+)[-](\d+)', # "Players: 5-10"
]
MATERIALS_PATTERNS = [
r'materiale\s*:?\s*([^.\n]+)', # "Materiale: ..."
r'materials\s*:?\s*([^.\n]+)', # "Materials: ..."
r'echipament\s*:?\s*([^.\n]+)', # "Echipament: ..."
r'equipment\s*:?\s*([^.\n]+)', # "Equipment: ..."
r'fara\s+materiale', # "fara materiale"
r'no\s+materials', # "no materials"
]
def extract_parameter(self, text: str, patterns: List[str]) -> Optional[str]:
"""Extract parameter using regex patterns"""
text_lower = text.lower()
for pattern in patterns:
match = re.search(pattern, text_lower, re.IGNORECASE | re.MULTILINE)
if match:
if len(match.groups()) == 1:
return match.group(1).strip()
elif len(match.groups()) == 2:
return f"{match.group(1)}-{match.group(2)}"
else:
return match.group(0).strip()
return None
def detect_activity_boundaries(self, text: str) -> List[Tuple[int, int]]:
"""Detect where activities start and end in text"""
# Simple heuristic: activities are separated by blank lines or headers
lines = text.split('\n')
boundaries = []
start_idx = 0
for i, line in enumerate(lines):
if (line.strip() == '' and i > start_idx + 3) or \
(re.match(r'^#{1,3}\s+', line) and i > start_idx):
if i > start_idx:
boundaries.append((start_idx, i))
start_idx = i
# Add the last section
if start_idx < len(lines) - 1:
boundaries.append((start_idx, len(lines)))
return boundaries
def extract_activities_from_text(self, text: str, file_path: str, file_type: str) -> List[Dict]:
"""Extract activities from plain text"""
activities = []
boundaries = self.detect_activity_boundaries(text)
for i, (start, end) in enumerate(boundaries):
lines = text.split('\n')[start:end]
section_text = '\n'.join(lines).strip()
if len(section_text) < 50: # Skip very short sections
continue
# Extract title (first meaningful line)
title = None
for line in lines:
line = line.strip()
if line and not line.startswith('#'):
title = line[:100] # Limit title length
break
if not title:
title = f"Activity {i+1}"
# Extract parameters
age_group = self.extract_parameter(section_text, self.AGE_PATTERNS)
duration = self.extract_parameter(section_text, self.DURATION_PATTERNS)
participants = self.extract_parameter(section_text, self.PARTICIPANTS_PATTERNS)
materials = self.extract_parameter(section_text, self.MATERIALS_PATTERNS)
# Create activity record
activity = {
'title': title,
'description': section_text[:500], # First 500 chars as description
'file_path': str(file_path),
'file_type': file_type,
'page_number': None,
'tags': self._extract_keywords(section_text),
'category': self._guess_category(section_text),
'age_group': age_group or '',
'participants': participants or '',
'duration': duration or '',
'materials': materials or '',
'difficulty': 'mediu',
'source_text': section_text
}
activities.append(activity)
return activities
def _extract_keywords(self, text: str, max_keywords: int = 10) -> List[str]:
"""Extract keywords from text"""
# Simple keyword extraction based on common activity terms
common_keywords = [
'joc', 'game', 'echipa', 'team', 'copii', 'children', 'grupa', 'group',
'activitate', 'activity', 'exercitiu', 'exercise', 'cooperare', 'cooperation',
'creativitate', 'creativity', 'sport', 'concurs', 'competition', 'energie',
'comunicare', 'communication', 'leadership', 'incredere', 'trust'
]
text_lower = text.lower()
found_keywords = []
for keyword in common_keywords:
if keyword in text_lower and keyword not in found_keywords:
found_keywords.append(keyword)
if len(found_keywords) >= max_keywords:
break
return found_keywords
def _guess_category(self, text: str) -> str:
"""Guess activity category from text content"""
text_lower = text.lower()
# Category mapping based on keywords
categories = {
'team building': ['team', 'echipa', 'cooperare', 'incredere', 'comunicare'],
'jocuri cercetășești': ['scout', 'cercetasi', 'baden', 'uniform', 'patrol'],
'activități educaționale': ['învățare', 'educativ', 'știință', 'biologie'],
'orientare': ['busola', 'compass', 'hartă', 'orientare', 'azimut'],
'primul ajutor': ['primul ajutor', 'first aid', 'medical', 'urgenta'],
'escape room': ['puzzle', 'enigma', 'cod', 'mister', 'escape'],
'camping & exterior': ['camping', 'natura', 'exterior', 'tabara', 'survival']
}
for category, keywords in categories.items():
if any(keyword in text_lower for keyword in keywords):
return category
return 'diverse'
class PDFExtractor(ActivityExtractor):
"""PDF file processor"""
def extract(self, file_path: Path) -> List[Dict]:
"""Extract activities from PDF file"""
activities = []
if not PDF_AVAILABLE and not PDFPLUMBER_AVAILABLE:
raise ImportError("Neither PyPDF2 nor pdfplumber is available")
try:
# Try pdfplumber first (better text extraction)
if PDFPLUMBER_AVAILABLE:
with pdfplumber.open(file_path) as pdf:
full_text = ""
for page in pdf.pages:
text = page.extract_text()
if text:
full_text += text + "\n"
if full_text.strip():
activities = self.extract_activities_from_text(
full_text, file_path, 'pdf'
)
# Fallback to PyPDF2
elif PDF_AVAILABLE and not activities:
with open(file_path, 'rb') as file:
pdf_reader = PyPDF2.PdfReader(file)
full_text = ""
for page in pdf_reader.pages:
text = page.extract_text()
if text:
full_text += text + "\n"
if full_text.strip():
activities = self.extract_activities_from_text(
full_text, file_path, 'pdf'
)
except Exception as e:
print(f"❌ Error processing PDF {file_path}: {e}")
return []
return activities
class DOCExtractor(ActivityExtractor):
"""DOC/DOCX file processor"""
def extract(self, file_path: Path) -> List[Dict]:
"""Extract activities from DOC/DOCX file"""
if not DOCX_AVAILABLE:
raise ImportError("python-docx not available")
try:
doc = DocxDocument(file_path)
full_text = ""
for paragraph in doc.paragraphs:
if paragraph.text.strip():
full_text += paragraph.text + "\n"
if full_text.strip():
return self.extract_activities_from_text(full_text, file_path, 'docx')
except Exception as e:
print(f"❌ Error processing DOCX {file_path}: {e}")
return []
class HTMLExtractor(ActivityExtractor):
"""HTML file processor"""
def extract(self, file_path: Path) -> List[Dict]:
"""Extract activities from HTML file"""
if not HTML_AVAILABLE:
raise ImportError("BeautifulSoup4 not available")
try:
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
soup = BeautifulSoup(content, 'html.parser')
# Remove script and style elements
for script in soup(["script", "style"]):
script.decompose()
# Extract text
text = soup.get_text()
# Clean up text
lines = (line.strip() for line in text.splitlines())
text = '\n'.join(line for line in lines if line)
if text.strip():
return self.extract_activities_from_text(text, file_path, 'html')
except Exception as e:
print(f"❌ Error processing HTML {file_path}: {e}")
return []
class MarkdownExtractor(ActivityExtractor):
"""Markdown file processor"""
def extract(self, file_path: Path) -> List[Dict]:
"""Extract activities from Markdown file"""
try:
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# Convert to HTML first if markdown lib available, otherwise use raw text
if MARKDOWN_AVAILABLE:
html = markdown.markdown(content)
soup = BeautifulSoup(html, 'html.parser')
text = soup.get_text()
else:
text = content
if text.strip():
return self.extract_activities_from_text(text, file_path, 'md')
except Exception as e:
print(f"❌ Error processing Markdown {file_path}: {e}")
return []
class TextExtractor(ActivityExtractor):
"""Plain text file processor"""
def extract(self, file_path: Path) -> List[Dict]:
"""Extract activities from plain text file"""
try:
with open(file_path, 'r', encoding='utf-8') as file:
text = file.read()
if text.strip():
return self.extract_activities_from_text(text, file_path, 'txt')
except Exception as e:
print(f"❌ Error processing text {file_path}: {e}")
return []
class MultiFormatIndexer:
"""Main indexer class for processing multiple file formats"""
def __init__(self, base_path: str, db_path: str = "../data/activities.db"):
self.base_path = Path(base_path)
self.db = DatabaseManager(db_path)
# Initialize extractors
self.extractors = {
'.pdf': PDFExtractor(),
'.doc': DOCExtractor(),
'.docx': DOCExtractor(),
'.html': HTMLExtractor(),
'.htm': HTMLExtractor(),
'.md': MarkdownExtractor(),
'.txt': TextExtractor()
}
self.processed_files = set()
self.stats = {
'total_files_found': 0,
'total_files_processed': 0,
'total_activities_extracted': 0,
'files_failed': 0,
'by_type': {}
}
def get_supported_files(self) -> List[Path]:
"""Get all supported files from base directory"""
supported_files = []
for ext in self.extractors.keys():
pattern = f"**/*{ext}"
files = list(self.base_path.glob(pattern))
supported_files.extend(files)
# Update stats
if ext not in self.stats['by_type']:
self.stats['by_type'][ext] = {'found': 0, 'processed': 0, 'activities': 0}
self.stats['by_type'][ext]['found'] = len(files)
self.stats['total_files_found'] = len(supported_files)
return supported_files
def process_file(self, file_path: Path) -> Tuple[int, str]:
"""Process a single file and return (activities_count, status)"""
file_ext = file_path.suffix.lower()
if file_ext not in self.extractors:
return 0, f"Unsupported file type: {file_ext}"
extractor = self.extractors[file_ext]
try:
print(f"📄 Processing: {file_path.name}")
activities = extractor.extract(file_path)
# Insert activities into database
inserted_count = 0
for activity in activities:
try:
self.db.insert_activity(activity)
inserted_count += 1
except Exception as e:
print(f" ⚠️ Failed to insert activity: {e}")
# Log processing result
self.db.log_file_processing(
str(file_path), file_ext[1:], 'success', inserted_count
)
# Update stats
self.stats['total_files_processed'] += 1
self.stats['total_activities_extracted'] += inserted_count
self.stats['by_type'][file_ext]['processed'] += 1
self.stats['by_type'][file_ext]['activities'] += inserted_count
print(f" ✅ Extracted {inserted_count} activities")
return inserted_count, 'success'
except Exception as e:
error_msg = f"Processing failed: {e}"
print(f"{error_msg}")
# Log error
self.db.log_file_processing(str(file_path), file_ext[1:], 'error', 0, error_msg)
self.stats['files_failed'] += 1
return 0, error_msg
def run_batch_indexing(self, clear_db: bool = False, max_files: int = None):
"""Run batch indexing of all supported files"""
print("🚀 MULTI-FORMAT INDEXER - Starting batch processing")
print("=" * 60)
if clear_db:
print("🗑️ Clearing existing database...")
self.db.clear_database()
# Get files to process
files_to_process = self.get_supported_files()
if max_files:
files_to_process = files_to_process[:max_files]
print(f"📁 Found {len(files_to_process)} supported files")
print(f"📊 File types: {list(self.stats['by_type'].keys())}")
# Check dependencies
self._check_dependencies()
# Process files
print("\n📄 PROCESSING FILES:")
print("-" * 40)
start_time = datetime.now()
for i, file_path in enumerate(files_to_process, 1):
print(f"\n[{i}/{len(files_to_process)}] ", end="")
self.process_file(file_path)
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
# Print summary
print("\n" + "=" * 60)
print("📊 INDEXING SUMMARY")
print("=" * 60)
print(f"⏱️ Total time: {duration:.2f} seconds")
print(f"📁 Files found: {self.stats['total_files_found']}")
print(f"✅ Files processed: {self.stats['total_files_processed']}")
print(f"❌ Files failed: {self.stats['files_failed']}")
print(f"🎮 Total activities extracted: {self.stats['total_activities_extracted']}")
print("\n📂 BY FILE TYPE:")
for ext, stats in self.stats['by_type'].items():
if stats['found'] > 0:
success_rate = (stats['processed'] / stats['found']) * 100
print(f" {ext}: {stats['processed']}/{stats['found']} files ({success_rate:.1f}%) → {stats['activities']} activities")
print(f"\n🎯 Average: {self.stats['total_activities_extracted'] / max(1, self.stats['total_files_processed']):.1f} activities per file")
print("=" * 60)
def _check_dependencies(self):
"""Check availability of required libraries"""
print("\n🔧 CHECKING DEPENDENCIES:")
deps = [
("PyPDF2", PDF_AVAILABLE, "PDF processing"),
("pdfplumber", PDFPLUMBER_AVAILABLE, "Enhanced PDF processing"),
("python-docx", DOCX_AVAILABLE, "DOC/DOCX processing"),
("BeautifulSoup4", HTML_AVAILABLE, "HTML processing"),
("markdown", MARKDOWN_AVAILABLE, "Markdown processing")
]
for name, available, purpose in deps:
status = "" if available else ""
print(f" {status} {name}: {purpose}")
# Check if we can process any files
if not any([PDF_AVAILABLE, PDFPLUMBER_AVAILABLE, DOCX_AVAILABLE, HTML_AVAILABLE]):
print("\n⚠️ WARNING: No file processing libraries available!")
print(" Install with: pip install PyPDF2 python-docx beautifulsoup4 markdown pdfplumber")
def main():
"""Command line interface"""
parser = argparse.ArgumentParser(description="Multi-format activity indexer")
parser.add_argument('--base-path', '-p',
default='/mnt/d/GoogleDrive/Cercetasi/carti-camp-jocuri',
help='Base directory to scan for files')
parser.add_argument('--db-path', '-d',
default='../data/activities.db',
help='Database file path')
parser.add_argument('--clear-db', '-c', action='store_true',
help='Clear database before indexing')
parser.add_argument('--max-files', '-m', type=int,
help='Maximum number of files to process (for testing)')
parser.add_argument('--test-mode', '-t', action='store_true',
help='Run in test mode with limited files')
args = parser.parse_args()
if args.test_mode:
args.max_files = 5
print("🧪 Running in TEST MODE (max 5 files)")
# Initialize and run indexer
indexer = MultiFormatIndexer(args.base_path, args.db_path)
indexer.run_batch_indexing(
clear_db=args.clear_db,
max_files=args.max_files
)
if __name__ == "__main__":
main()

173
src/search_games.py Normal file
View File

@@ -0,0 +1,173 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CĂUTARE INTERACTIVĂ JOCURI - Script simplu pentru căutări rapide
Folosire:
python search_games.py
sau
python search_games.py --category "Team Building"
python search_games.py --age 8 --duration 30
"""
import argparse
import sys
from game_library_manager import GameLibraryManager
def interactive_search():
"""Mod interactiv pentru căutări"""
print("🎮 CĂUTARE INTERACTIVĂ ACTIVITĂȚI")
print("=" * 50)
manager = GameLibraryManager()
print(f"📚 Colecție încărcată: {len(manager.activities)} activități\n")
while True:
print("\n🔍 CRITERII DE CĂUTARE (Enter pentru a sări):")
# Colectare criterii
criteria = {}
category = input("Categoria (Team Building/Jocuri Cercetășești/etc.): ").strip()
if category:
criteria['category'] = category
age = input("Vârsta minimă (ex: 8): ").strip()
if age and age.isdigit():
criteria['age_min'] = int(age)
keywords_input = input("Cuvinte cheie (separate prin virgulă): ").strip()
if keywords_input:
criteria['keywords'] = [kw.strip() for kw in keywords_input.split(',')]
difficulty = input("Dificultatea (ușor/mediu/avansat): ").strip()
if difficulty:
criteria['difficulty'] = difficulty
# Căutare
if criteria:
results = manager.search_activities(**criteria)
print(f"\n🎯 REZULTATE: {len(results)} activități găsite")
print("-" * 50)
for i, activity in enumerate(results, 1):
print(f"{i}. **{activity.title}**")
print(f" 📂 {activity.category}{activity.subcategory}")
print(f" 👥 {activity.participants} | ⏰ {activity.duration} | 🎂 {activity.age_group}")
print(f" 💡 {activity.description[:80]}...")
print(f" 📁 {activity.file_path}")
print()
# Opțiuni post-căutare
if results:
choice = input("\n📝 Generam fișe pentru activități? (da/nu/număr): ").strip().lower()
if choice == 'da':
# Generează fișe pentru toate
filename = f"cautare_rezultate_{len(results)}_activitati"
export_path = manager.export_search_results(results, filename)
print(f"✅ Exportat în: {export_path}")
elif choice.isdigit():
# Generează fișă pentru activitate specifică
idx = int(choice) - 1
if 0 <= idx < len(results):
activity = results[idx]
sheet = manager.generate_activity_sheet(activity, "markdown")
filename = f"FISA_{activity.id}_{activity.title.replace(' ', '_')}.md"
sheet_path = manager.index_path / filename
with open(sheet_path, 'w', encoding='utf-8') as f:
f.write(sheet)
print(f"✅ Fișă generată: {sheet_path}")
else:
print("❌ Nu ați specificat criterii de căutare")
# Continuă?
continue_search = input("\n🔄 Altă căutare? (da/nu): ").strip().lower()
if continue_search != 'da':
break
print("\n👋 La revedere!")
def command_line_search(args):
"""Căutare din linia de comandă"""
manager = GameLibraryManager()
criteria = {}
if args.category:
criteria['category'] = args.category
if args.age:
criteria['age_min'] = args.age
if args.keywords:
criteria['keywords'] = args.keywords.split(',')
if args.difficulty:
criteria['difficulty'] = args.difficulty
results = manager.search_activities(**criteria)
print(f"🎯 Găsite {len(results)} activități:")
for i, activity in enumerate(results, 1):
print(f"{i}. {activity.title} ({activity.category})")
print(f" {activity.age_group} | {activity.duration} | {activity.file_path}")
def show_categories():
"""Afișează categoriile disponibile"""
manager = GameLibraryManager()
stats = manager.get_statistics()
print("📂 CATEGORII DISPONIBILE:")
for category, count in stats['categories'].items():
print(f" - {category} ({count} activități)")
def show_statistics():
"""Afișează statistici complete"""
manager = GameLibraryManager()
stats = manager.get_statistics()
print("📊 STATISTICI COLECȚIE:")
print(f" Total activități: {stats['total_activities']}")
print(f" Ultimul update: {stats['last_updated']}")
print("\n📂 Distribuție pe categorii:")
for category, count in stats['categories'].items():
percentage = (count / stats['total_activities']) * 100
print(f" - {category}: {count} ({percentage:.1f}%)")
print("\n🎂 Distribuție pe grupe de vârstă:")
for age_group, count in stats['age_groups'].items():
print(f" - {age_group}: {count}")
print("\n📊 Distribuție pe dificultate:")
for difficulty, count in stats['difficulties'].items():
print(f" - {difficulty}: {count}")
def main():
parser = argparse.ArgumentParser(description="Căutare în colecția de jocuri și activități")
# Opțiuni de căutare
parser.add_argument('--category', '-c', help='Categoria activității')
parser.add_argument('--age', '-a', type=int, help='Vârsta minimă')
parser.add_argument('--keywords', '-k', help='Cuvinte cheie (separate prin virgulă)')
parser.add_argument('--difficulty', '-d', help='Nivelul de dificultate')
# Opțiuni informaționale
parser.add_argument('--categories', action='store_true', help='Afișează categoriile disponibile')
parser.add_argument('--stats', action='store_true', help='Afișează statistici complete')
parser.add_argument('--interactive', '-i', action='store_true', help='Mod interactiv')
args = parser.parse_args()
# Verifică dacă nu sunt argumente - pornește modul interactiv
if len(sys.argv) == 1:
interactive_search()
elif args.categories:
show_categories()
elif args.stats:
show_statistics()
elif args.interactive:
interactive_search()
else:
command_line_search(args)
if __name__ == "__main__":
main()