Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -1,845 +0,0 @@
|
|
| 1 |
-
from flask import Flask, render_template, request, jsonify, session, Response
|
| 2 |
-
import sys
|
| 3 |
-
import pickle
|
| 4 |
-
import json
|
| 5 |
-
import gc
|
| 6 |
-
import weakref
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
from utils import *
|
| 9 |
-
from options import args
|
| 10 |
-
from models import model_factory
|
| 11 |
-
from flask_socketio import SocketIO, emit
|
| 12 |
-
from datetime import datetime
|
| 13 |
-
import random
|
| 14 |
-
import re
|
| 15 |
-
import xml.etree.ElementTree as ET
|
| 16 |
-
|
| 17 |
-
app = Flask(__name__)
|
| 18 |
-
app.secret_key = '1903bjk'
|
| 19 |
-
socketio = SocketIO(app, cors_allowed_origins="*")
|
| 20 |
-
|
| 21 |
-
# Memory-efficient chat system
|
| 22 |
-
class ChatManager:
|
| 23 |
-
def __init__(self, max_messages=100): # Reduced from 300
|
| 24 |
-
self.messages = []
|
| 25 |
-
self.active_users = {}
|
| 26 |
-
self.max_messages = max_messages
|
| 27 |
-
|
| 28 |
-
def add_message(self, message):
|
| 29 |
-
self.messages.append(message)
|
| 30 |
-
if len(self.messages) > self.max_messages:
|
| 31 |
-
self.messages.pop(0)
|
| 32 |
-
|
| 33 |
-
def get_messages(self):
|
| 34 |
-
return self.messages
|
| 35 |
-
|
| 36 |
-
def add_user(self, sid, username):
|
| 37 |
-
self.active_users[sid] = {
|
| 38 |
-
'username': username,
|
| 39 |
-
'connected_at': datetime.now()
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
def remove_user(self, sid):
|
| 43 |
-
return self.active_users.pop(sid, None)
|
| 44 |
-
|
| 45 |
-
def get_user_count(self):
|
| 46 |
-
return len(self.active_users)
|
| 47 |
-
|
| 48 |
-
def get_username(self, sid):
|
| 49 |
-
user = self.active_users.get(sid)
|
| 50 |
-
return user['username'] if user else None
|
| 51 |
-
|
| 52 |
-
def update_username(self, sid, new_username):
|
| 53 |
-
if sid in self.active_users:
|
| 54 |
-
self.active_users[sid]['username'] = new_username
|
| 55 |
-
|
| 56 |
-
chat_manager = ChatManager()
|
| 57 |
-
|
| 58 |
-
def generate_username():
|
| 59 |
-
adjectives = ['Cool', 'Awesome', 'Swift', 'Bright', 'Happy', 'Smart', 'Kind', 'Brave', 'Calm', 'Epic', "Black"]
|
| 60 |
-
nouns = ['Otaku', 'Ninja', 'Samurai', 'Dragon', 'Phoenix', 'Tiger', 'Wolf', 'Eagle', 'Fox', 'Bear']
|
| 61 |
-
return f"{random.choice(adjectives)}{random.choice(nouns)}{random.randint(100, 999)}"
|
| 62 |
-
|
| 63 |
-
def clean_message(message):
|
| 64 |
-
# HTML tag'leri temizle
|
| 65 |
-
message = re.sub(r'<[^>]*>', '', message)
|
| 66 |
-
# Uzunluk kontrolü
|
| 67 |
-
if len(message) > 500:
|
| 68 |
-
message = message[:500]
|
| 69 |
-
return message.strip()
|
| 70 |
-
|
| 71 |
-
# Lazy loading için wrapper class
|
| 72 |
-
class LazyDict:
|
| 73 |
-
def __init__(self, file_path):
|
| 74 |
-
self.file_path = file_path
|
| 75 |
-
self._data = None
|
| 76 |
-
self._loaded = False
|
| 77 |
-
|
| 78 |
-
def _load_data(self):
|
| 79 |
-
if not self._loaded:
|
| 80 |
-
try:
|
| 81 |
-
with open(self.file_path, "r", encoding="utf-8") as file:
|
| 82 |
-
self._data = json.load(file)
|
| 83 |
-
self._loaded = True
|
| 84 |
-
except Exception as e:
|
| 85 |
-
print(f"Warning: Could not load {self.file_path}: {str(e)}")
|
| 86 |
-
self._data = {}
|
| 87 |
-
self._loaded = True
|
| 88 |
-
|
| 89 |
-
def get(self, key, default=None):
|
| 90 |
-
self._load_data()
|
| 91 |
-
return self._data.get(key, default)
|
| 92 |
-
|
| 93 |
-
def __contains__(self, key):
|
| 94 |
-
self._load_data()
|
| 95 |
-
return key in self._data
|
| 96 |
-
|
| 97 |
-
def items(self):
|
| 98 |
-
self._load_data()
|
| 99 |
-
return self._data.items()
|
| 100 |
-
|
| 101 |
-
def keys(self):
|
| 102 |
-
self._load_data()
|
| 103 |
-
return self._data.keys()
|
| 104 |
-
|
| 105 |
-
def __len__(self):
|
| 106 |
-
self._load_data()
|
| 107 |
-
return len(self._data)
|
| 108 |
-
|
| 109 |
-
# Sitemap route'ları
|
| 110 |
-
@app.route('/sitemap.xml')
|
| 111 |
-
def sitemap():
|
| 112 |
-
"""Dinamik sitemap.xml oluşturur"""
|
| 113 |
-
try:
|
| 114 |
-
# XML root element
|
| 115 |
-
urlset = ET.Element('urlset')
|
| 116 |
-
urlset.set('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9')
|
| 117 |
-
urlset.set('xmlns:image', 'http://www.google.com/schemas/sitemap-image/1.1')
|
| 118 |
-
|
| 119 |
-
# Base URL
|
| 120 |
-
base_url = request.url_root.rstrip('/')
|
| 121 |
-
current_date = datetime.now().strftime('%Y-%m-%d')
|
| 122 |
-
|
| 123 |
-
# Ana sayfa
|
| 124 |
-
url = ET.SubElement(urlset, 'url')
|
| 125 |
-
ET.SubElement(url, 'loc').text = f'{base_url}/'
|
| 126 |
-
ET.SubElement(url, 'lastmod').text = current_date
|
| 127 |
-
ET.SubElement(url, 'changefreq').text = 'daily'
|
| 128 |
-
ET.SubElement(url, 'priority').text = '1.0'
|
| 129 |
-
|
| 130 |
-
# Chat sayfası
|
| 131 |
-
url = ET.SubElement(urlset, 'url')
|
| 132 |
-
ET.SubElement(url, 'loc').text = f'{base_url}/chat'
|
| 133 |
-
ET.SubElement(url, 'lastmod').text = current_date
|
| 134 |
-
ET.SubElement(url, 'changefreq').text = 'hourly'
|
| 135 |
-
ET.SubElement(url, 'priority').text = '0.8'
|
| 136 |
-
|
| 137 |
-
# Anime sayfaları (sadece ilk 50 anime - SEO için)
|
| 138 |
-
if recommendation_system and recommendation_system.id_to_anime:
|
| 139 |
-
anime_count = 0
|
| 140 |
-
for anime_id, anime_data in recommendation_system.id_to_anime.items():
|
| 141 |
-
if anime_count >= 50: # Reduced from 100
|
| 142 |
-
break
|
| 143 |
-
|
| 144 |
-
try:
|
| 145 |
-
anime_name = anime_data[0] if isinstance(anime_data, list) and len(anime_data) > 0 else str(anime_data)
|
| 146 |
-
safe_name = anime_name.replace(' ', '-').replace('/', '-').replace('?', '').replace('&', 'and')
|
| 147 |
-
safe_name = re.sub(r'[^\w\-]', '', safe_name)
|
| 148 |
-
|
| 149 |
-
url = ET.SubElement(urlset, 'url')
|
| 150 |
-
ET.SubElement(url, 'loc').text = f'{base_url}/anime/{anime_id}/{safe_name}'
|
| 151 |
-
ET.SubElement(url, 'lastmod').text = current_date
|
| 152 |
-
ET.SubElement(url, 'changefreq').text = 'weekly'
|
| 153 |
-
ET.SubElement(url, 'priority').text = '0.6'
|
| 154 |
-
|
| 155 |
-
# Sadece gerekli durumlarda resim URL'si ekle
|
| 156 |
-
if anime_count < 20: # Sadece ilk 20 anime için resim
|
| 157 |
-
image_url = recommendation_system.get_anime_image_url(int(anime_id))
|
| 158 |
-
if image_url:
|
| 159 |
-
image_elem = ET.SubElement(url, 'image:image')
|
| 160 |
-
ET.SubElement(image_elem, 'image:loc').text = image_url
|
| 161 |
-
ET.SubElement(image_elem, 'image:title').text = anime_name
|
| 162 |
-
ET.SubElement(image_elem, 'image:caption').text = f'Poster image for {anime_name}'
|
| 163 |
-
|
| 164 |
-
anime_count += 1
|
| 165 |
-
except Exception as e:
|
| 166 |
-
print(f"Error processing anime {anime_id}: {e}")
|
| 167 |
-
continue
|
| 168 |
-
|
| 169 |
-
# XML'i string'e çevir
|
| 170 |
-
xml_str = ET.tostring(urlset, encoding='unicode')
|
| 171 |
-
xml_declaration = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
| 172 |
-
full_xml = xml_declaration + xml_str
|
| 173 |
-
|
| 174 |
-
return Response(full_xml, mimetype='application/xml')
|
| 175 |
-
|
| 176 |
-
except Exception as e:
|
| 177 |
-
print(f"Sitemap generation error: {e}")
|
| 178 |
-
return Response(
|
| 179 |
-
'<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></urlset>',
|
| 180 |
-
mimetype='application/xml')
|
| 181 |
-
|
| 182 |
-
@app.route('/robots.txt')
|
| 183 |
-
def robots_txt():
|
| 184 |
-
"""Robots.txt dosyası"""
|
| 185 |
-
robots_content = f"""User-agent: *
|
| 186 |
-
Allow: /
|
| 187 |
-
Allow: /chat
|
| 188 |
-
|
| 189 |
-
Sitemap: {request.url_root.rstrip('/')}/sitemap.xml
|
| 190 |
-
"""
|
| 191 |
-
return Response(robots_content, mimetype='text/plain')
|
| 192 |
-
|
| 193 |
-
@app.route('/anime/<int:anime_id>/<path:anime_name>')
|
| 194 |
-
def anime_detail(anime_id, anime_name):
|
| 195 |
-
"""Anime detay sayfası (SEO için)"""
|
| 196 |
-
if not recommendation_system or str(anime_id) not in recommendation_system.id_to_anime:
|
| 197 |
-
return render_template('error.html', error="Anime not found"), 404
|
| 198 |
-
|
| 199 |
-
anime_data = recommendation_system.id_to_anime.get(str(anime_id))
|
| 200 |
-
anime_name = anime_data[0] if isinstance(anime_data, list) and len(anime_data) > 0 else str(anime_data)
|
| 201 |
-
|
| 202 |
-
# Anime bilgilerini lazy loading ile al
|
| 203 |
-
image_url = recommendation_system.get_anime_image_url(anime_id)
|
| 204 |
-
mal_url = recommendation_system.get_anime_mal_url(anime_id)
|
| 205 |
-
genres = recommendation_system.get_anime_genres(anime_id)
|
| 206 |
-
anime_type = recommendation_system._get_type(anime_id)
|
| 207 |
-
|
| 208 |
-
# Benzer animeler öner (sadece 5 tane)
|
| 209 |
-
similar_animes = []
|
| 210 |
-
try:
|
| 211 |
-
recommendations, _, _ = recommendation_system.get_recommendations([anime_id], num_recommendations=5)
|
| 212 |
-
similar_animes = recommendations
|
| 213 |
-
except:
|
| 214 |
-
pass
|
| 215 |
-
|
| 216 |
-
anime_info = {
|
| 217 |
-
'id': anime_id,
|
| 218 |
-
'name': anime_name,
|
| 219 |
-
'image_url': image_url,
|
| 220 |
-
'mal_url': mal_url,
|
| 221 |
-
'genres': genres,
|
| 222 |
-
'similar_animes': similar_animes,
|
| 223 |
-
'type': anime_type
|
| 224 |
-
}
|
| 225 |
-
|
| 226 |
-
# JSON-LD structured data oluştur
|
| 227 |
-
structured_data = generate_anime_structured_data(anime_info)
|
| 228 |
-
|
| 229 |
-
return render_template('anime_detail.html', anime=anime_info, structured_data=json.dumps(structured_data))
|
| 230 |
-
|
| 231 |
-
def generate_anime_structured_data(anime_info):
|
| 232 |
-
"""Anime için JSON-LD structured data oluşturur"""
|
| 233 |
-
structured_data = {
|
| 234 |
-
"@context": "https://schema.org",
|
| 235 |
-
"@type": anime_info["type"],
|
| 236 |
-
"name": anime_info['name'],
|
| 237 |
-
"url": f"{request.url_root.rstrip('/')}/anime/{anime_info['id']}/{anime_info['name'].replace(' ', '-')}"
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
if anime_info['genres']:
|
| 241 |
-
structured_data["genre"] = anime_info['genres']
|
| 242 |
-
|
| 243 |
-
if anime_info['image_url']:
|
| 244 |
-
structured_data["image"] = anime_info['image_url']
|
| 245 |
-
|
| 246 |
-
if anime_info['mal_url']:
|
| 247 |
-
structured_data["sameAs"] = anime_info['mal_url']
|
| 248 |
-
|
| 249 |
-
return structured_data
|
| 250 |
-
|
| 251 |
-
@app.route('/sitemap-index.xml')
|
| 252 |
-
def sitemap_index():
|
| 253 |
-
"""Sitemap index dosyası"""
|
| 254 |
-
try:
|
| 255 |
-
sitemapindex = ET.Element('sitemapindex')
|
| 256 |
-
sitemapindex.set('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9')
|
| 257 |
-
|
| 258 |
-
base_url = request.url_root.rstrip('/')
|
| 259 |
-
current_date = datetime.now().strftime('%Y-%m-%d')
|
| 260 |
-
|
| 261 |
-
# Ana sitemap
|
| 262 |
-
sitemap = ET.SubElement(sitemapindex, 'sitemap')
|
| 263 |
-
ET.SubElement(sitemap, 'loc').text = f'{base_url}/sitemap.xml'
|
| 264 |
-
ET.SubElement(sitemap, 'lastmod').text = current_date
|
| 265 |
-
|
| 266 |
-
xml_str = ET.tostring(sitemapindex, encoding='unicode')
|
| 267 |
-
xml_declaration = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
| 268 |
-
full_xml = xml_declaration + xml_str
|
| 269 |
-
|
| 270 |
-
return Response(full_xml, mimetype='application/xml')
|
| 271 |
-
|
| 272 |
-
except Exception as e:
|
| 273 |
-
print(f"Sitemap index generation error: {e}")
|
| 274 |
-
return Response(
|
| 275 |
-
'<?xml version="1.0" encoding="UTF-8"?><sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"></sitemapindex>',
|
| 276 |
-
mimetype='application/xml')
|
| 277 |
-
|
| 278 |
-
@app.route('/chat')
|
| 279 |
-
def chat():
|
| 280 |
-
return render_template('chat.html')
|
| 281 |
-
|
| 282 |
-
# SocketIO event'leri
|
| 283 |
-
@socketio.on('connect')
|
| 284 |
-
def on_connect():
|
| 285 |
-
username = generate_username()
|
| 286 |
-
chat_manager.add_user(request.sid, username)
|
| 287 |
-
|
| 288 |
-
# Kullanıcıya mevcut mesajları gönder
|
| 289 |
-
emit('chat_history', chat_manager.get_messages())
|
| 290 |
-
|
| 291 |
-
# Kullanıcı katıldı mesajı
|
| 292 |
-
join_message = {
|
| 293 |
-
'username': 'System',
|
| 294 |
-
'message': f'{username} joined the chat',
|
| 295 |
-
'timestamp': datetime.now().strftime('%H:%M'),
|
| 296 |
-
'type': 'system'
|
| 297 |
-
}
|
| 298 |
-
|
| 299 |
-
chat_manager.add_message(join_message)
|
| 300 |
-
emit('new_message', join_message, broadcast=True)
|
| 301 |
-
emit('user_count', chat_manager.get_user_count(), broadcast=True)
|
| 302 |
-
|
| 303 |
-
@socketio.on('disconnect')
|
| 304 |
-
def on_disconnect():
|
| 305 |
-
user = chat_manager.remove_user(request.sid)
|
| 306 |
-
if user:
|
| 307 |
-
username = user['username']
|
| 308 |
-
leave_message = {
|
| 309 |
-
'username': 'System',
|
| 310 |
-
'message': f'{username} left the chat',
|
| 311 |
-
'timestamp': datetime.now().strftime('%H:%M'),
|
| 312 |
-
'type': 'system'
|
| 313 |
-
}
|
| 314 |
-
|
| 315 |
-
chat_manager.add_message(leave_message)
|
| 316 |
-
emit('new_message', leave_message, broadcast=True)
|
| 317 |
-
emit('user_count', chat_manager.get_user_count(), broadcast=True)
|
| 318 |
-
|
| 319 |
-
@socketio.on('send_message')
|
| 320 |
-
def handle_message(data):
|
| 321 |
-
username = chat_manager.get_username(request.sid)
|
| 322 |
-
if not username:
|
| 323 |
-
return
|
| 324 |
-
|
| 325 |
-
message = clean_message(data.get('message', ''))
|
| 326 |
-
if not message:
|
| 327 |
-
return
|
| 328 |
-
|
| 329 |
-
message_obj = {
|
| 330 |
-
'username': username,
|
| 331 |
-
'message': message,
|
| 332 |
-
'timestamp': datetime.now().strftime('%H:%M'),
|
| 333 |
-
'type': 'user'
|
| 334 |
-
}
|
| 335 |
-
|
| 336 |
-
chat_manager.add_message(message_obj)
|
| 337 |
-
emit('new_message', message_obj, broadcast=True)
|
| 338 |
-
|
| 339 |
-
@socketio.on('change_username')
|
| 340 |
-
def handle_username_change(data):
|
| 341 |
-
old_username = chat_manager.get_username(request.sid)
|
| 342 |
-
if not old_username:
|
| 343 |
-
return
|
| 344 |
-
|
| 345 |
-
new_username = clean_message(data.get('username', ''))
|
| 346 |
-
if not new_username or len(new_username) < 2:
|
| 347 |
-
return
|
| 348 |
-
|
| 349 |
-
chat_manager.update_username(request.sid, new_username)
|
| 350 |
-
|
| 351 |
-
change_message = {
|
| 352 |
-
'username': 'System',
|
| 353 |
-
'message': f'{old_username} changed name to {new_username}',
|
| 354 |
-
'timestamp': datetime.now().strftime('%H:%M'),
|
| 355 |
-
'type': 'system'
|
| 356 |
-
}
|
| 357 |
-
|
| 358 |
-
chat_manager.add_message(change_message)
|
| 359 |
-
emit('new_message', change_message, broadcast=True)
|
| 360 |
-
emit('username_changed', {'username': new_username})
|
| 361 |
-
|
| 362 |
-
class AnimeRecommendationSystem:
|
| 363 |
-
def __init__(self, checkpoint_path, dataset_path, animes_path, images_path, mal_urls_path, type_seq_path, genres_path):
|
| 364 |
-
self.model = None
|
| 365 |
-
self.dataset = None
|
| 366 |
-
self.checkpoint_path = checkpoint_path
|
| 367 |
-
self.dataset_path = dataset_path
|
| 368 |
-
self.animes_path = animes_path
|
| 369 |
-
|
| 370 |
-
# Lazy loading ile memory optimization
|
| 371 |
-
self.id_to_anime = LazyDict(animes_path)
|
| 372 |
-
self.id_to_url = LazyDict(images_path)
|
| 373 |
-
self.id_to_mal_url = LazyDict(mal_urls_path)
|
| 374 |
-
self.id_to_type_seq = LazyDict(type_seq_path)
|
| 375 |
-
self.id_to_genres = LazyDict(genres_path)
|
| 376 |
-
|
| 377 |
-
# Cache için weak reference kullan
|
| 378 |
-
self._cache = {}
|
| 379 |
-
|
| 380 |
-
self.load_model_and_data()
|
| 381 |
-
|
| 382 |
-
def load_model_and_data(self):
|
| 383 |
-
try:
|
| 384 |
-
print("Loading model and data...")
|
| 385 |
-
args.bert_max_len = 128
|
| 386 |
-
|
| 387 |
-
# Dataset'i yükle
|
| 388 |
-
dataset_path = Path(self.dataset_path)
|
| 389 |
-
with dataset_path.open('rb') as f:
|
| 390 |
-
self.dataset = pickle.load(f)
|
| 391 |
-
|
| 392 |
-
# Model'i yükle
|
| 393 |
-
self.model = model_factory(args)
|
| 394 |
-
self.load_checkpoint()
|
| 395 |
-
|
| 396 |
-
# Garbage collection
|
| 397 |
-
gc.collect()
|
| 398 |
-
print("Model loaded successfully!")
|
| 399 |
-
|
| 400 |
-
except Exception as e:
|
| 401 |
-
print(f"Error loading model: {str(e)}")
|
| 402 |
-
raise e
|
| 403 |
-
|
| 404 |
-
def load_checkpoint(self):
|
| 405 |
-
try:
|
| 406 |
-
with open(self.checkpoint_path, 'rb') as f:
|
| 407 |
-
checkpoint = torch.load(f, map_location='cpu', weights_only=False)
|
| 408 |
-
self.model.load_state_dict(checkpoint['model_state_dict'])
|
| 409 |
-
self.model.eval()
|
| 410 |
-
|
| 411 |
-
# Checkpoint'i bellekten temizle
|
| 412 |
-
del checkpoint
|
| 413 |
-
gc.collect()
|
| 414 |
-
|
| 415 |
-
except Exception as e:
|
| 416 |
-
raise Exception(f"Failed to load checkpoint from {self.checkpoint_path}: {str(e)}")
|
| 417 |
-
|
| 418 |
-
def get_anime_genres(self, anime_id):
|
| 419 |
-
genres = self.id_to_genres.get(str(anime_id), [])
|
| 420 |
-
return [genre.title() for genre in genres] if genres else []
|
| 421 |
-
|
| 422 |
-
def get_all_animes(self):
|
| 423 |
-
"""Tüm anime listesini döndürür - cache kullanır"""
|
| 424 |
-
cache_key = 'all_animes'
|
| 425 |
-
if cache_key in self._cache:
|
| 426 |
-
return self._cache[cache_key]
|
| 427 |
-
|
| 428 |
-
animes = []
|
| 429 |
-
# Sadece gerekli durumlarda yükle
|
| 430 |
-
for k, v in list(self.id_to_anime.items())[:1000]: # İlk 1000 anime
|
| 431 |
-
anime_name = v[0] if isinstance(v, list) and len(v) > 0 else str(v)
|
| 432 |
-
animes.append((int(k), anime_name))
|
| 433 |
-
|
| 434 |
-
animes.sort(key=lambda x: x[1])
|
| 435 |
-
self._cache[cache_key] = animes
|
| 436 |
-
return animes
|
| 437 |
-
|
| 438 |
-
def get_anime_image_url(self, anime_id):
|
| 439 |
-
return self.id_to_url.get(str(anime_id), None)
|
| 440 |
-
|
| 441 |
-
def get_anime_mal_url(self, anime_id):
|
| 442 |
-
return self.id_to_mal_url.get(str(anime_id), None)
|
| 443 |
-
|
| 444 |
-
def get_filtered_anime_pool(self, filters):
|
| 445 |
-
"""Filtrelere göre anime havuzunu önceden filtreler"""
|
| 446 |
-
if not filters:
|
| 447 |
-
return None
|
| 448 |
-
|
| 449 |
-
if filters.get('show_hentai') and len([k for k, v in filters.items() if v]) == 1:
|
| 450 |
-
hentai_animes = []
|
| 451 |
-
# Sadece gerekli verileri kontrol et
|
| 452 |
-
for anime_id_str in list(self.id_to_anime.keys())[:500]: # Limit
|
| 453 |
-
anime_id = int(anime_id_str)
|
| 454 |
-
if self._is_hentai(anime_id):
|
| 455 |
-
hentai_animes.append(anime_id)
|
| 456 |
-
return hentai_animes
|
| 457 |
-
|
| 458 |
-
return None
|
| 459 |
-
|
| 460 |
-
def _is_hentai(self, anime_id):
|
| 461 |
-
"""Anime'nin hentai olup olmadığını kontrol eder"""
|
| 462 |
-
type_seq_info = self.id_to_type_seq.get(str(anime_id))
|
| 463 |
-
if not type_seq_info or len(type_seq_info) < 3:
|
| 464 |
-
return False
|
| 465 |
-
return type_seq_info[2]
|
| 466 |
-
|
| 467 |
-
def _get_type(self, anime_id):
|
| 468 |
-
"""Anime tipini döndürür"""
|
| 469 |
-
type_seq_info = self.id_to_type_seq.get(str(anime_id))
|
| 470 |
-
if not type_seq_info or len(type_seq_info) < 2:
|
| 471 |
-
return "Unknown"
|
| 472 |
-
return type_seq_info[1]
|
| 473 |
-
|
| 474 |
-
def get_recommendations(self, favorite_anime_ids, num_recommendations=20, filters=None): # Reduced from 40
|
| 475 |
-
try:
|
| 476 |
-
if not favorite_anime_ids:
|
| 477 |
-
return [], [], "Please add some favorite animes first!"
|
| 478 |
-
|
| 479 |
-
smap = self.dataset
|
| 480 |
-
inverted_smap = {v: k for k, v in smap.items()}
|
| 481 |
-
|
| 482 |
-
converted_ids = []
|
| 483 |
-
for anime_id in favorite_anime_ids:
|
| 484 |
-
if anime_id in smap:
|
| 485 |
-
converted_ids.append(smap[anime_id])
|
| 486 |
-
|
| 487 |
-
if not converted_ids:
|
| 488 |
-
return [], [], "None of the selected animes are in the model vocabulary!"
|
| 489 |
-
|
| 490 |
-
# Hentai filtresi özel durumu
|
| 491 |
-
filtered_pool = self.get_filtered_anime_pool(filters)
|
| 492 |
-
if filtered_pool is not None:
|
| 493 |
-
return self._get_recommendations_from_pool(favorite_anime_ids, filtered_pool, num_recommendations, filters)
|
| 494 |
-
|
| 495 |
-
# Normal öneriler
|
| 496 |
-
target_len = 128
|
| 497 |
-
padded = converted_ids + [0] * (target_len - len(converted_ids))
|
| 498 |
-
input_tensor = torch.tensor(padded, dtype=torch.long).unsqueeze(0)
|
| 499 |
-
|
| 500 |
-
max_predictions = min(75, len(inverted_smap)) # Reduced from 125
|
| 501 |
-
|
| 502 |
-
with torch.no_grad():
|
| 503 |
-
logits = self.model(input_tensor)
|
| 504 |
-
last_logits = logits[:, -1, :]
|
| 505 |
-
top_scores, top_indices = torch.topk(last_logits, k=max_predictions, dim=1)
|
| 506 |
-
|
| 507 |
-
recommendations = []
|
| 508 |
-
scores = []
|
| 509 |
-
|
| 510 |
-
for idx, score in zip(top_indices.numpy()[0], top_scores.detach().numpy()[0]):
|
| 511 |
-
if idx in inverted_smap:
|
| 512 |
-
anime_id = inverted_smap[idx]
|
| 513 |
-
|
| 514 |
-
if anime_id in favorite_anime_ids:
|
| 515 |
-
continue
|
| 516 |
-
|
| 517 |
-
if str(anime_id) in self.id_to_anime:
|
| 518 |
-
# Filtreleme kontrolü
|
| 519 |
-
if filters and not self._should_include_anime(anime_id, filters):
|
| 520 |
-
continue
|
| 521 |
-
|
| 522 |
-
anime_data = self.id_to_anime.get(str(anime_id))
|
| 523 |
-
anime_name = anime_data[0] if isinstance(anime_data, list) and len(anime_data) > 0 else str(anime_data)
|
| 524 |
-
|
| 525 |
-
# Lazy loading ile image ve mal url al
|
| 526 |
-
image_url = self.get_anime_image_url(anime_id)
|
| 527 |
-
mal_url = self.get_anime_mal_url(anime_id)
|
| 528 |
-
|
| 529 |
-
recommendations.append({
|
| 530 |
-
'id': anime_id,
|
| 531 |
-
'name': anime_name,
|
| 532 |
-
'score': float(score),
|
| 533 |
-
'image_url': image_url,
|
| 534 |
-
'mal_url': mal_url,
|
| 535 |
-
'genres': self.get_anime_genres(anime_id)
|
| 536 |
-
})
|
| 537 |
-
scores.append(float(score))
|
| 538 |
-
|
| 539 |
-
if len(recommendations) >= num_recommendations:
|
| 540 |
-
break
|
| 541 |
-
|
| 542 |
-
# Memory cleanup
|
| 543 |
-
del logits, last_logits, top_scores, top_indices
|
| 544 |
-
gc.collect()
|
| 545 |
-
|
| 546 |
-
return recommendations, scores, f"Found {len(recommendations)} recommendations!"
|
| 547 |
-
|
| 548 |
-
except Exception as e:
|
| 549 |
-
return [], [], f"Error during prediction: {str(e)}"
|
| 550 |
-
|
| 551 |
-
def _get_recommendations_from_pool(self, favorite_anime_ids, anime_pool, num_recommendations, filters):
|
| 552 |
-
"""Önceden filtrelenmiş anime havuzundan öneriler alır"""
|
| 553 |
-
try:
|
| 554 |
-
smap = self.dataset
|
| 555 |
-
converted_ids = []
|
| 556 |
-
for anime_id in favorite_anime_ids:
|
| 557 |
-
if anime_id in smap:
|
| 558 |
-
converted_ids.append(smap[anime_id])
|
| 559 |
-
|
| 560 |
-
if not converted_ids:
|
| 561 |
-
return [], [], "None of the selected animes are in the model vocabulary!"
|
| 562 |
-
|
| 563 |
-
target_len = 128
|
| 564 |
-
padded = converted_ids + [0] * (target_len - len(converted_ids))
|
| 565 |
-
input_tensor = torch.tensor(padded, dtype=torch.long).unsqueeze(0)
|
| 566 |
-
|
| 567 |
-
with torch.no_grad():
|
| 568 |
-
logits = self.model(input_tensor)
|
| 569 |
-
last_logits = logits[:, -1, :]
|
| 570 |
-
|
| 571 |
-
# Anime havuzundaki her anime için skor hesapla
|
| 572 |
-
anime_scores = []
|
| 573 |
-
for anime_id in anime_pool:
|
| 574 |
-
if anime_id in favorite_anime_ids:
|
| 575 |
-
continue
|
| 576 |
-
|
| 577 |
-
if anime_id in smap:
|
| 578 |
-
model_id = smap[anime_id]
|
| 579 |
-
if model_id < last_logits.shape[1]:
|
| 580 |
-
score = last_logits[0, model_id].item()
|
| 581 |
-
anime_scores.append((anime_id, score))
|
| 582 |
-
|
| 583 |
-
# Skorlara göre sırala
|
| 584 |
-
anime_scores.sort(key=lambda x: x[1], reverse=True)
|
| 585 |
-
|
| 586 |
-
recommendations = []
|
| 587 |
-
for anime_id, score in anime_scores[:num_recommendations]:
|
| 588 |
-
if str(anime_id) in self.id_to_anime:
|
| 589 |
-
anime_data = self.id_to_anime.get(str(anime_id))
|
| 590 |
-
anime_name = anime_data[0] if isinstance(anime_data, list) and len(anime_data) > 0 else str(anime_data)
|
| 591 |
-
|
| 592 |
-
recommendations.append({
|
| 593 |
-
'id': anime_id,
|
| 594 |
-
'name': anime_name,
|
| 595 |
-
'score': float(score),
|
| 596 |
-
'image_url': self.get_anime_image_url(anime_id),
|
| 597 |
-
'mal_url': self.get_anime_mal_url(anime_id),
|
| 598 |
-
'genres': self.get_anime_genres(anime_id)
|
| 599 |
-
})
|
| 600 |
-
|
| 601 |
-
# Memory cleanup
|
| 602 |
-
del logits, last_logits
|
| 603 |
-
gc.collect()
|
| 604 |
-
|
| 605 |
-
return recommendations, [r['score'] for r in recommendations], f"Found {len(recommendations)} filtered recommendations!"
|
| 606 |
-
|
| 607 |
-
except Exception as e:
|
| 608 |
-
return [], [], f"Error during filtered prediction: {str(e)}"
|
| 609 |
-
|
| 610 |
-
def _should_include_anime(self, anime_id, filters):
|
| 611 |
-
"""Filtrelere göre anime'nin dahil edilip edilmeyeceğini kontrol eder"""
|
| 612 |
-
if 'blacklisted_animes' in filters:
|
| 613 |
-
if anime_id in filters['blacklisted_animes']:
|
| 614 |
-
return False
|
| 615 |
-
|
| 616 |
-
type_seq_info = self.id_to_type_seq.get(str(anime_id))
|
| 617 |
-
if not type_seq_info or len(type_seq_info) < 2:
|
| 618 |
-
return True
|
| 619 |
-
|
| 620 |
-
anime_type = type_seq_info[0]
|
| 621 |
-
is_sequel = type_seq_info[1]
|
| 622 |
-
is_hentai = type_seq_info[2]
|
| 623 |
-
|
| 624 |
-
# Sequel filtresi
|
| 625 |
-
if 'show_sequels' in filters:
|
| 626 |
-
if not filters['show_sequels'] and is_sequel:
|
| 627 |
-
return False
|
| 628 |
-
|
| 629 |
-
# Hentai filtresi
|
| 630 |
-
if 'show_hentai' in filters:
|
| 631 |
-
if filters['show_hentai']:
|
| 632 |
-
if not is_hentai:
|
| 633 |
-
return False
|
| 634 |
-
else:
|
| 635 |
-
if is_hentai:
|
| 636 |
-
return False
|
| 637 |
-
|
| 638 |
-
# Tür filtreleri
|
| 639 |
-
if 'show_movies' in filters:
|
| 640 |
-
if not filters['show_movies'] and anime_type == 'MOVIE':
|
| 641 |
-
return False
|
| 642 |
-
|
| 643 |
-
if 'show_tv' in filters:
|
| 644 |
-
if not filters['show_tv'] and anime_type == 'TV':
|
| 645 |
-
return False
|
| 646 |
-
|
| 647 |
-
if 'show_ova' in filters:
|
| 648 |
-
if not filters['show_ova'] and anime_type in ['ONA', 'OVA', 'SPECIAL']:
|
| 649 |
-
return False
|
| 650 |
-
|
| 651 |
-
return True
|
| 652 |
-
|
| 653 |
-
recommendation_system = None
|
| 654 |
-
|
| 655 |
-
@app.route('/')
|
| 656 |
-
def index():
|
| 657 |
-
if recommendation_system is None:
|
| 658 |
-
return render_template('error.html', error="Recommendation system not initialized. Please check server logs.")
|
| 659 |
-
|
| 660 |
-
animes = recommendation_system.get_all_animes()
|
| 661 |
-
return render_template('index.html', animes=animes)
|
| 662 |
-
|
| 663 |
-
@app.route('/api/search_animes')
|
| 664 |
-
def search_animes():
|
| 665 |
-
query = request.args.get('q', '').lower()
|
| 666 |
-
animes = []
|
| 667 |
-
|
| 668 |
-
# Sadece ilk 200 anime'yi arama - performance için
|
| 669 |
-
count = 0
|
| 670 |
-
for k, v in recommendation_system.id_to_anime.items():
|
| 671 |
-
if count >= 200:
|
| 672 |
-
break
|
| 673 |
-
|
| 674 |
-
anime_names = v if isinstance(v, list) else [v]
|
| 675 |
-
match_found = False
|
| 676 |
-
|
| 677 |
-
for name in anime_names:
|
| 678 |
-
if query in name.lower():
|
| 679 |
-
match_found = True
|
| 680 |
-
break
|
| 681 |
-
|
| 682 |
-
if not query or match_found:
|
| 683 |
-
main_name = anime_names[0] if anime_names else "Unknown"
|
| 684 |
-
animes.append((int(k), main_name))
|
| 685 |
-
count += 1
|
| 686 |
-
|
| 687 |
-
animes.sort(key=lambda x: x[1])
|
| 688 |
-
return jsonify(animes)
|
| 689 |
-
|
| 690 |
-
@app.route('/api/add_favorite', methods=['POST'])
|
| 691 |
-
def add_favorite():
|
| 692 |
-
if 'favorites' not in session:
|
| 693 |
-
session['favorites'] = []
|
| 694 |
-
|
| 695 |
-
data = request.get_json()
|
| 696 |
-
anime_id = int(data['anime_id'])
|
| 697 |
-
|
| 698 |
-
if anime_id not in session['favorites']:
|
| 699 |
-
# Maksimum 20 favori anime (memory için)
|
| 700 |
-
if len(session['favorites']) >= 20:
|
| 701 |
-
return jsonify({'success': False, 'message': 'Maximum 20 favorite animes allowed'})
|
| 702 |
-
|
| 703 |
-
session['favorites'].append(anime_id)
|
| 704 |
-
session.modified = True
|
| 705 |
-
return jsonify({'success': True})
|
| 706 |
-
else:
|
| 707 |
-
return jsonify({'success': False})
|
| 708 |
-
|
| 709 |
-
@app.route('/api/remove_favorite', methods=['POST'])
|
| 710 |
-
def remove_favorite():
|
| 711 |
-
if 'favorites' not in session:
|
| 712 |
-
session['favorites'] = []
|
| 713 |
-
|
| 714 |
-
data = request.get_json()
|
| 715 |
-
anime_id = int(data['anime_id'])
|
| 716 |
-
|
| 717 |
-
if anime_id in session['favorites']:
|
| 718 |
-
session['favorites'].remove(anime_id)
|
| 719 |
-
session.modified = True
|
| 720 |
-
return jsonify({'success': True})
|
| 721 |
-
else:
|
| 722 |
-
return jsonify({'success': False})
|
| 723 |
-
|
| 724 |
-
@app.route('/api/clear_favorites', methods=['POST'])
|
| 725 |
-
def clear_favorites():
|
| 726 |
-
session['favorites'] = []
|
| 727 |
-
session.modified = True
|
| 728 |
-
return jsonify({'success': True})
|
| 729 |
-
|
| 730 |
-
@app.route('/api/get_favorites')
|
| 731 |
-
def get_favorites():
|
| 732 |
-
if 'favorites' not in session:
|
| 733 |
-
session['favorites'] = []
|
| 734 |
-
|
| 735 |
-
favorite_animes = []
|
| 736 |
-
for anime_id in session['favorites']:
|
| 737 |
-
if str(anime_id) in recommendation_system.id_to_anime:
|
| 738 |
-
anime_data = recommendation_system.id_to_anime.get(str(anime_id))
|
| 739 |
-
anime_name = anime_data[0] if isinstance(anime_data, list) and len(anime_data) > 0 else str(anime_data)
|
| 740 |
-
favorite_animes.append({'id': anime_id, 'name': anime_name})
|
| 741 |
-
|
| 742 |
-
return jsonify(favorite_animes)
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
@app.route('/api/get_recommendations', methods=['POST'])
|
| 746 |
-
def get_recommendations():
|
| 747 |
-
if 'favorites' not in session or not session['favorites']:
|
| 748 |
-
return jsonify({'success': False, 'message': 'Please add some favorite animes first!'})
|
| 749 |
-
|
| 750 |
-
data = request.get_json() or {}
|
| 751 |
-
filters = data.get('filters', {})
|
| 752 |
-
|
| 753 |
-
# Blacklist bilgisini ekle
|
| 754 |
-
blacklisted_animes = data.get('blacklisted_animes', [])
|
| 755 |
-
if blacklisted_animes:
|
| 756 |
-
filters['blacklisted_animes'] = blacklisted_animes
|
| 757 |
-
|
| 758 |
-
recommendations, scores, message = recommendation_system.get_recommendations(
|
| 759 |
-
session['favorites'],
|
| 760 |
-
filters=filters
|
| 761 |
-
)
|
| 762 |
-
|
| 763 |
-
if recommendations:
|
| 764 |
-
return jsonify({
|
| 765 |
-
'success': True,
|
| 766 |
-
'recommendations': recommendations,
|
| 767 |
-
'message': message
|
| 768 |
-
})
|
| 769 |
-
else:
|
| 770 |
-
return jsonify({'success': False, 'message': message})
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
@app.route('/api/mal_logo')
|
| 774 |
-
def get_mal_logo():
|
| 775 |
-
# MyAnimeList logo URL'ini döndür
|
| 776 |
-
return jsonify({
|
| 777 |
-
'success': True,
|
| 778 |
-
'logo_url': 'https://cdn.myanimelist.net/img/sp/icon/apple-touch-icon-256.png'
|
| 779 |
-
})
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
def main():
|
| 783 |
-
global recommendation_system
|
| 784 |
-
|
| 785 |
-
args.num_items = 15687
|
| 786 |
-
|
| 787 |
-
import gdown
|
| 788 |
-
import os
|
| 789 |
-
|
| 790 |
-
file_ids = {
|
| 791 |
-
"1C6mdjblhiWGhRgbIk5DP2XCc4ElS9x8p": "pretrained_bert.pth",
|
| 792 |
-
"1J1RmuJE5OjZUO0z1irVb2M-xnvuVvvHR": "animes.json",
|
| 793 |
-
"1xGxUCbCDUnbdnJa6Ab8wgM9cpInpeQnN": "dataset.pkl",
|
| 794 |
-
"1PtB6o_91tNWAb4zN0xj-Kf8SKvVAJp1c": "id_to_url.json",
|
| 795 |
-
"1xVfTB_CmeYEqq6-l_BkQXo-QAUEyBfbW": "anime_to_malurl.json",
|
| 796 |
-
"1zMbL9TpCbODKfVT5ahiaYILlnwBZNJc1": "anime_to_typenseq.json",
|
| 797 |
-
"1LLMRhYyw82GOz3d8SUDZF9YRJdybgAFA": "id_to_genres.json"
|
| 798 |
-
}
|
| 799 |
-
|
| 800 |
-
def download_from_gdrive(file_id, output_path):
|
| 801 |
-
url = f"https://drive.google.com/uc?id={file_id}"
|
| 802 |
-
try:
|
| 803 |
-
print(f"Downloading: {file_id}")
|
| 804 |
-
gdown.download(url, output_path, quiet=False)
|
| 805 |
-
print(f"Downloaded: {output_path}")
|
| 806 |
-
return True
|
| 807 |
-
except Exception as e:
|
| 808 |
-
print(f"Error: {e}")
|
| 809 |
-
return False
|
| 810 |
-
|
| 811 |
-
for key, value in file_ids.items():
|
| 812 |
-
if os.path.isfile(value):
|
| 813 |
-
continue
|
| 814 |
-
download_from_gdrive(key, value)
|
| 815 |
-
|
| 816 |
-
try:
|
| 817 |
-
images_path = "id_to_url.json"
|
| 818 |
-
mal_urls_path = "anime_to_malurl.json"
|
| 819 |
-
type_seq_path = "anime_to_typenseq.json"
|
| 820 |
-
|
| 821 |
-
if not os.path.exists(images_path):
|
| 822 |
-
print(f"Warning: {images_path} not found. Images will not be displayed.")
|
| 823 |
-
|
| 824 |
-
if not os.path.exists(mal_urls_path):
|
| 825 |
-
print(f"Warning: {mal_urls_path} not found. MAL links will not be available.")
|
| 826 |
-
|
| 827 |
-
recommendation_system = AnimeRecommendationSystem(
|
| 828 |
-
"pretrained_bert.pth",
|
| 829 |
-
"dataset.pkl",
|
| 830 |
-
"animes.json",
|
| 831 |
-
images_path,
|
| 832 |
-
mal_urls_path,
|
| 833 |
-
type_seq_path,
|
| 834 |
-
"id_to_genres.json"
|
| 835 |
-
)
|
| 836 |
-
print("Recommendation system initialized successfully!")
|
| 837 |
-
except Exception as e:
|
| 838 |
-
print(f"Failed to initialize recommendation system: {e}")
|
| 839 |
-
sys.exit(1)
|
| 840 |
-
|
| 841 |
-
app.run(debug=False, host='0.0.0.0', port=5000)
|
| 842 |
-
|
| 843 |
-
|
| 844 |
-
if __name__ == "__main__":
|
| 845 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|