Spaces:
Sleeping
Sleeping
Commit
·
e04900a
1
Parent(s):
63b1daf
Fix: Resolve merge markers and ensure correct v2.0 state
Browse files- requirements.txt +0 -14
- scripts/app.py +0 -163
- scripts/main.py +0 -112
requirements.txt
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
<<<<<<< HEAD
|
| 2 |
langchain==0.3.27
|
| 3 |
langchain-community==0.3.31
|
| 4 |
gradio==5.49.1
|
|
@@ -15,16 +14,3 @@ numpy==2.0.2
|
|
| 15 |
accelerate==1.11.0
|
| 16 |
aiohttp==3.13.1
|
| 17 |
huggingface-hub==0.35.3
|
| 18 |
-
=======
|
| 19 |
-
torch==2.8.0
|
| 20 |
-
transformers==4.56.1
|
| 21 |
-
pytorch-lightning==2.5.5
|
| 22 |
-
torchmetrics==1.8.2
|
| 23 |
-
sentencepiece==0.2.1
|
| 24 |
-
pandas==2.2.2
|
| 25 |
-
scikit-learn==1.6.1
|
| 26 |
-
gradio==5.44.1
|
| 27 |
-
matplotlib==3.10.0
|
| 28 |
-
seaborn==0.13.2
|
| 29 |
-
wordcloud==1.9.4
|
| 30 |
-
>>>>>>> e6de3c4338f79386345fa6e4bba5b0666ad808da
|
|
|
|
|
|
|
| 1 |
langchain==0.3.27
|
| 2 |
langchain-community==0.3.31
|
| 3 |
gradio==5.49.1
|
|
|
|
| 14 |
accelerate==1.11.0
|
| 15 |
aiohttp==3.13.1
|
| 16 |
huggingface-hub==0.35.3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/app.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
<<<<<<< HEAD
|
| 2 |
# app.py
|
| 3 |
|
| 4 |
import gradio as gr
|
|
@@ -279,165 +278,3 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
|
| 279 |
if __name__ == "__main__":
|
| 280 |
chat_memory.clear() # Clear memory each time app starts
|
| 281 |
demo.launch(debug=True)
|
| 282 |
-
=======
|
| 283 |
-
import gradio as gr
|
| 284 |
-
import os
|
| 285 |
-
import torch
|
| 286 |
-
import pandas as pd
|
| 287 |
-
import re
|
| 288 |
-
|
| 289 |
-
# --- IMPORTANT ---
|
| 290 |
-
# This script assumes you have a 'models.py' file in the same directory
|
| 291 |
-
# containing the definitions for all model and inference classes.
|
| 292 |
-
try:
|
| 293 |
-
from models import (
|
| 294 |
-
ReviewSummarizer,
|
| 295 |
-
AspectAnalyzer,
|
| 296 |
-
AspectExtractor,
|
| 297 |
-
FineTunedSentimentClassifier
|
| 298 |
-
)
|
| 299 |
-
except ImportError:
|
| 300 |
-
print("CRITICAL ERROR: Make sure 'models.py' exists and contains the required classes.")
|
| 301 |
-
# Define dummy classes if imports fail, so Gradio can at least launch with an error message.
|
| 302 |
-
class ReviewSummarizer: pass
|
| 303 |
-
class AspectAnalyzer: pass
|
| 304 |
-
class AspectExtractor: pass
|
| 305 |
-
class FineTunedSentimentClassifier: pass
|
| 306 |
-
|
| 307 |
-
# --- Configuration ---
|
| 308 |
-
# --- IMPORTANT: UPDATE THIS PATH ---
|
| 309 |
-
# You need to provide the path to the best checkpoint file that was saved
|
| 310 |
-
# during the training of your sentiment model.
|
| 311 |
-
SENTIMENT_CHECKPOINT_PATH = "checkpoints/sentiment-binary-best-checkpoint.ckpt" # <-- CHANGE THIS
|
| 312 |
-
|
| 313 |
-
# --- Pre-defined Aspect Dictionaries for Different Product Categories ---
|
| 314 |
-
ASPECT_DICTIONARIES = {
|
| 315 |
-
"Phone": ['camera', 'battery', 'battery life', 'screen', 'performance', 'price', 'design'],
|
| 316 |
-
"Coffee Maker": ['ease of use', 'design', 'noise level', 'coffee quality', 'brew time', 'cleaning'],
|
| 317 |
-
"Book": ['plot', 'characters', 'writing style', 'pacing', 'ending'],
|
| 318 |
-
"Default": ['quality', 'price', 'service', 'design', 'features'] # A fallback list
|
| 319 |
-
}
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
# --- 1. Load All Models (Global Objects) ---
|
| 323 |
-
print("--- Initializing all models for the Gradio App ---")
|
| 324 |
-
sentiment_classifier, summarizer, aspect_analyzer, aspect_extractor = None, None, None, None
|
| 325 |
-
try:
|
| 326 |
-
summarizer = ReviewSummarizer(force_cpu=True)
|
| 327 |
-
aspect_analyzer = AspectAnalyzer(force_cpu=True)
|
| 328 |
-
aspect_extractor = AspectExtractor(force_cpu=True)
|
| 329 |
-
|
| 330 |
-
if not os.path.exists(SENTIMENT_CHECKPOINT_PATH):
|
| 331 |
-
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
| 332 |
-
print("!!! WARNING: Sentiment checkpoint path not found or not set. !!!")
|
| 333 |
-
print(f"!!! Please update the 'SENTIMENT_CHECKPOINT_PATH' variable in app.py")
|
| 334 |
-
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
| 335 |
-
else:
|
| 336 |
-
sentiment_classifier = FineTunedSentimentClassifier(
|
| 337 |
-
checkpoint_path=SENTIMENT_CHECKPOINT_PATH, force_cpu=True
|
| 338 |
-
)
|
| 339 |
-
print("\n--- All models loaded successfully ---\n")
|
| 340 |
-
except Exception as e:
|
| 341 |
-
print(f"An error occurred during model initialization: {e}")
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
# --- 2. Define the Core Analysis Function ---
|
| 345 |
-
def analyze_review(review_text, product_category):
|
| 346 |
-
if not review_text:
|
| 347 |
-
return {"ERROR": "Please enter a review."}, "", None
|
| 348 |
-
|
| 349 |
-
# --- a. Overall Sentiment Analysis ---
|
| 350 |
-
if sentiment_classifier:
|
| 351 |
-
sentiment_result = sentiment_classifier.classify(review_text)
|
| 352 |
-
sentiment_output = {
|
| 353 |
-
sentiment_result['label']: f"{sentiment_result['score']:.2f}"
|
| 354 |
-
}
|
| 355 |
-
else:
|
| 356 |
-
sentiment_output = {"ERROR": "Fine-tuned model not loaded. Check path."}
|
| 357 |
-
|
| 358 |
-
# --- b. Review Summarization ---
|
| 359 |
-
if summarizer:
|
| 360 |
-
summary_output = summarizer.summarize(review_text)
|
| 361 |
-
else:
|
| 362 |
-
summary_output = "ERROR: Summarizer model not loaded."
|
| 363 |
-
|
| 364 |
-
# --- c. Dynamic Aspect Extraction & Analysis ---
|
| 365 |
-
aspect_df = None
|
| 366 |
-
if aspect_extractor and aspect_analyzer:
|
| 367 |
-
aspect_dictionary = ASPECT_DICTIONARIES.get(product_category, ASPECT_DICTIONARIES["Default"])
|
| 368 |
-
extracted_aspects = aspect_extractor.extract(review_text, aspect_dictionary=aspect_dictionary)
|
| 369 |
-
|
| 370 |
-
if extracted_aspects:
|
| 371 |
-
aspect_results = aspect_analyzer.analyze(review_text, extracted_aspects)
|
| 372 |
-
aspect_df = pd.DataFrame([
|
| 373 |
-
{'Aspect': aspect, 'Sentiment': result['sentiment'], 'Score': f"{result['score']:.2f}"}
|
| 374 |
-
for aspect, result in aspect_results.items()
|
| 375 |
-
])
|
| 376 |
-
|
| 377 |
-
return sentiment_output, summary_output, aspect_df
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
# --- 3. Build the Gradio Interface ---
|
| 381 |
-
with gr.Blocks(theme=gr.themes.Soft()) as demo:
|
| 382 |
-
gr.Markdown("# 🛍️ ReviewSense: Product Review Analysis Engine")
|
| 383 |
-
gr.Markdown(
|
| 384 |
-
"Enter a product review and select the product category. The tool will automatically "
|
| 385 |
-
"detect relevant features and provide an overall sentiment score, a summary, and a "
|
| 386 |
-
"breakdown of sentiment towards each feature."
|
| 387 |
-
)
|
| 388 |
-
|
| 389 |
-
with gr.Row():
|
| 390 |
-
with gr.Column(scale=2):
|
| 391 |
-
review_input = gr.Textbox(
|
| 392 |
-
lines=10,
|
| 393 |
-
label="Enter Product Review Here",
|
| 394 |
-
placeholder="e.g., The camera is amazing, but the battery life is terrible..."
|
| 395 |
-
)
|
| 396 |
-
category_input = gr.Dropdown(
|
| 397 |
-
choices=list(ASPECT_DICTIONARIES.keys()),
|
| 398 |
-
label="Select Product Category",
|
| 399 |
-
value="Phone"
|
| 400 |
-
)
|
| 401 |
-
analyze_button = gr.Button("Analyze Review", variant="primary")
|
| 402 |
-
|
| 403 |
-
with gr.Column(scale=1):
|
| 404 |
-
gr.Markdown("### Overall Sentiment")
|
| 405 |
-
sentiment_output = gr.Label()
|
| 406 |
-
|
| 407 |
-
gr.Markdown("### Generated Summary")
|
| 408 |
-
summary_output = gr.Textbox(lines=5, label="Summary", interactive=False)
|
| 409 |
-
|
| 410 |
-
gr.Markdown("### Detected Aspect Sentiments")
|
| 411 |
-
aspect_output = gr.DataFrame(headers=["Aspect", "Sentiment", "Score"], label="Aspects", interactive=False)
|
| 412 |
-
|
| 413 |
-
# Connect the button to the function
|
| 414 |
-
analyze_button.click(
|
| 415 |
-
fn=analyze_review,
|
| 416 |
-
inputs=[review_input, category_input],
|
| 417 |
-
outputs=[sentiment_output, summary_output, aspect_output]
|
| 418 |
-
)
|
| 419 |
-
|
| 420 |
-
gr.Examples(
|
| 421 |
-
examples=[
|
| 422 |
-
[
|
| 423 |
-
"The camera on this phone is incredible, the pictures are professional quality. However, the battery life is a total disaster, it barely lasts half a day with light use. The screen is bright and responsive, which I love.",
|
| 424 |
-
"Phone"
|
| 425 |
-
],
|
| 426 |
-
[
|
| 427 |
-
"I am absolutely in love with this coffee maker! It's incredibly easy to use, brews a perfect cup every single time, and the design looks fantastic on my countertop. It's also surprisingly quiet.",
|
| 428 |
-
"Coffee Maker"
|
| 429 |
-
],
|
| 430 |
-
[
|
| 431 |
-
"An amazing story with characters that felt so real. The plot had me hooked from the first page, though I felt the ending was a bit rushed.",
|
| 432 |
-
"Book"
|
| 433 |
-
]
|
| 434 |
-
],
|
| 435 |
-
inputs=[review_input, category_input]
|
| 436 |
-
)
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
# --- 4. Launch the App ---
|
| 440 |
-
if __name__ == "__main__":
|
| 441 |
-
print("Launching Gradio App...")
|
| 442 |
-
demo.launch()
|
| 443 |
-
>>>>>>> e6de3c4338f79386345fa6e4bba5b0666ad808da
|
|
|
|
|
|
|
| 1 |
# app.py
|
| 2 |
|
| 3 |
import gradio as gr
|
|
|
|
| 278 |
if __name__ == "__main__":
|
| 279 |
chat_memory.clear() # Clear memory each time app starts
|
| 280 |
demo.launch(debug=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/main.py
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
<<<<<<< HEAD
|
| 2 |
# main.py
|
| 3 |
|
| 4 |
import torch
|
|
@@ -211,114 +210,3 @@ if __name__ == "__main__":
|
|
| 211 |
print("\n--- Chat session ended. ---")
|
| 212 |
|
| 213 |
print("\n--- Local Execution Finished ---")
|
| 214 |
-
=======
|
| 215 |
-
import os
|
| 216 |
-
import torch
|
| 217 |
-
import pandas as pd
|
| 218 |
-
|
| 219 |
-
try:
|
| 220 |
-
from data_prepare import ReviewDataset, ReviewDataModule
|
| 221 |
-
from models import SentimentClassifier, ReviewSummarizer, AspectAnalyzer, FineTunedSentimentClassifier, AspectExtractor
|
| 222 |
-
except ImportError:
|
| 223 |
-
print("CRITICAL ERROR: Make sure 'review_summarizer.py', 'aspect_extractor.py', and 'sentiment_classifier_model.py' are in the same directory.")
|
| 224 |
-
exit()
|
| 225 |
-
|
| 226 |
-
# --- Configuration ---
|
| 227 |
-
# --- IMPORTANT: UPDATE THIS PATH ---
|
| 228 |
-
# You need to provide the path to the best checkpoint file that was saved
|
| 229 |
-
# during the training of your sentiment model.
|
| 230 |
-
SENTIMENT_CHECKPOINT_PATH = "checkpoints/sentiment-binary-best-checkpoint.ckpt"
|
| 231 |
-
|
| 232 |
-
# --- Pre-defined Aspect Dictionaries for Different Product Categories ---
|
| 233 |
-
ASPECT_DICTIONARIES = {
|
| 234 |
-
"Phone": ['camera', 'battery', 'battery life', 'screen', 'performance', 'price', 'design'],
|
| 235 |
-
"Coffee Maker": ['ease of use', 'design', 'noise level', 'coffee quality', 'brew time', 'cleaning'],
|
| 236 |
-
"Book": ['plot', 'characters', 'writing style', 'pacing', 'ending'],
|
| 237 |
-
"Default": ['quality', 'price', 'service', 'design', 'features'] # A fallback list
|
| 238 |
-
}
|
| 239 |
-
|
| 240 |
-
def main():
|
| 241 |
-
"""
|
| 242 |
-
Main function to run the command-line review analysis tool.
|
| 243 |
-
"""
|
| 244 |
-
# --- 1. Load All Models ---
|
| 245 |
-
print("--- Initializing all models ---")
|
| 246 |
-
sentiment_classifier, summarizer, aspect_analyzer, aspect_extractor = None, None, None, None
|
| 247 |
-
try:
|
| 248 |
-
summarizer = ReviewSummarizer(force_cpu=True)
|
| 249 |
-
aspect_analyzer = AspectAnalyzer(force_cpu=True)
|
| 250 |
-
aspect_extractor = AspectExtractor(force_cpu=True)
|
| 251 |
-
|
| 252 |
-
if not os.path.exists(SENTIMENT_CHECKPOINT_PATH):
|
| 253 |
-
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
| 254 |
-
print("!!! WARNING: Sentiment checkpoint path not found or not set. !!!")
|
| 255 |
-
print(f"!!! Please update the 'SENTIMENT_CHECKPOINT_PATH' variable in main.py")
|
| 256 |
-
print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
|
| 257 |
-
else:
|
| 258 |
-
sentiment_classifier = FineTunedSentimentClassifier(
|
| 259 |
-
checkpoint_path=SENTIMENT_CHECKPOINT_PATH, force_cpu=True
|
| 260 |
-
)
|
| 261 |
-
print("\n--- All models loaded successfully ---\n")
|
| 262 |
-
except Exception as e:
|
| 263 |
-
print(f"An error occurred during model initialization: {e}")
|
| 264 |
-
return
|
| 265 |
-
|
| 266 |
-
# --- 2. Interactive Loop ---
|
| 267 |
-
while True:
|
| 268 |
-
print("\n==================================================")
|
| 269 |
-
print(" Product Review Analysis Tool ")
|
| 270 |
-
print("==================================================")
|
| 271 |
-
|
| 272 |
-
# Get user input
|
| 273 |
-
review_text = input("Enter the product review text (or type 'quit' to exit):\n> ")
|
| 274 |
-
if review_text.lower() == 'quit':
|
| 275 |
-
break
|
| 276 |
-
|
| 277 |
-
print("\nAvailable Product Categories:")
|
| 278 |
-
for i, category in enumerate(ASPECT_DICTIONARIES.keys(), 1):
|
| 279 |
-
print(f"{i}. {category}")
|
| 280 |
-
|
| 281 |
-
category_choice = input(f"Select a product category (1-{len(ASPECT_DICTIONARIES)}):\n> ")
|
| 282 |
-
try:
|
| 283 |
-
category_idx = int(category_choice) - 1
|
| 284 |
-
product_category = list(ASPECT_DICTIONARIES.keys())[category_idx]
|
| 285 |
-
except (ValueError, IndexError):
|
| 286 |
-
print("Invalid choice. Using 'Default' category.")
|
| 287 |
-
product_category = "Default"
|
| 288 |
-
|
| 289 |
-
# --- 3. Run Analysis ---
|
| 290 |
-
print("\n--- Analyzing Review... ---")
|
| 291 |
-
|
| 292 |
-
# a. Overall Sentiment
|
| 293 |
-
sentiment_result = sentiment_classifier.classify(review_text)
|
| 294 |
-
|
| 295 |
-
# b. Summary
|
| 296 |
-
summary_result = summarizer.summarize(review_text)
|
| 297 |
-
|
| 298 |
-
# c. Aspect Extraction and Analysis
|
| 299 |
-
aspect_dictionary = ASPECT_DICTIONARIES.get(product_category)
|
| 300 |
-
extracted_aspects = aspect_extractor.extract(review_text, aspect_dictionary)
|
| 301 |
-
aspect_results = None
|
| 302 |
-
if extracted_aspects:
|
| 303 |
-
aspect_results = aspect_analyzer.analyze(review_text, extracted_aspects)
|
| 304 |
-
|
| 305 |
-
# --- 4. Display Results ---
|
| 306 |
-
print("\n-------------------- ANALYSIS RESULTS --------------------")
|
| 307 |
-
print(f"\n[ Overall Sentiment ]")
|
| 308 |
-
print(f" - Sentiment: {sentiment_result['label']} (Score: {sentiment_result['score']:.2f})")
|
| 309 |
-
|
| 310 |
-
print(f"\n[ Generated Summary ]")
|
| 311 |
-
print(f" - {summary_result}")
|
| 312 |
-
|
| 313 |
-
print(f"\n[ Detected Aspect Sentiments ]")
|
| 314 |
-
if aspect_results:
|
| 315 |
-
for aspect, result in aspect_results.items():
|
| 316 |
-
print(f" - {aspect.title()}: {result['sentiment']} (Score: {result['score']:.2f})")
|
| 317 |
-
else:
|
| 318 |
-
print(" - No relevant aspects from the dictionary were detected in the review.")
|
| 319 |
-
print("----------------------------------------------------------")
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
if __name__ == "__main__":
|
| 323 |
-
main()
|
| 324 |
-
>>>>>>> e6de3c4338f79386345fa6e4bba5b0666ad808da
|
|
|
|
|
|
|
| 1 |
# main.py
|
| 2 |
|
| 3 |
import torch
|
|
|
|
| 210 |
print("\n--- Chat session ended. ---")
|
| 211 |
|
| 212 |
print("\n--- Local Execution Finished ---")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|