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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|