smtsead commited on
Commit
721918f
·
verified ·
1 Parent(s): c18bd07

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +253 -127
app.py CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  from transformers import (
3
  pipeline,
@@ -6,101 +15,171 @@ from transformers import (
6
  )
7
  import torch
8
  import re
 
 
9
 
10
  # ===== CONSTANTS =====
11
- MAX_CHARS = 1500 # Increased character limit
 
 
 
12
  SUPPORTED_LANGUAGES = {
13
  'en': 'English',
14
- 'zh': 'Chinese',
15
- 'yue': 'Cantonese',
16
  'ja': 'Japanese',
17
- 'ko': 'Korean'
 
 
18
  }
19
 
20
  # ===== ASPECT CONFIGURATION =====
 
 
21
  aspect_map = {
22
- # Location related
23
  "location": ["location", "near", "close", "access", "transport", "distance", "area", "tsim sha tsui", "kowloon"],
24
  "view": ["view", "scenery", "vista", "panorama", "outlook", "skyline"],
25
  "parking": ["parking", "valet", "garage", "car park", "vehicle"],
26
 
27
- # Room related
28
  "room comfort": ["comfortable", "bed", "pillows", "mattress", "linens", "cozy", "hard", "soft"],
29
  "room cleanliness": ["clean", "dirty", "spotless", "stains", "hygiene", "sanitation", "dusty"],
30
  "room amenities": ["amenities", "minibar", "coffee", "tea", "fridge", "facilities", "tv", "kettle"],
31
  "bathroom": ["bathroom", "shower", "toilet", "sink", "towel", "faucet", "toiletries"],
32
 
33
- # Service related
34
  "staff service": ["staff", "friendly", "helpful", "rude", "welcoming", "employee", "manager"],
35
  "reception": ["reception", "check-in", "check-out", "front desk", "welcome", "registration"],
36
  "housekeeping": ["housekeeping", "maid", "cleaning", "towels", "service", "turndown"],
37
  "concierge": ["concierge", "recommendation", "advice", "tips", "guidance", "directions"],
38
  "room service": ["room service", "food delivery", "order", "meal", "tray"],
39
 
40
- # Facilities
41
  "dining": ["breakfast", "dinner", "restaurant", "meal", "food", "buffet", "lunch"],
42
  "bar": ["bar", "drinks", "cocktail", "wine", "lounge", "happy hour"],
43
  "pool": ["pool", "swimming", "jacuzzi", "sun lounger", "deck", "towels"],
44
  "spa": ["spa", "massage", "treatment", "relax", "wellness", "sauna"],
45
  "fitness": ["gym", "fitness", "exercise", "workout", "training", "weights"],
46
 
47
- # Technical
48
  "Wi-Fi": ["wifi", "internet", "connection", "online", "network", "speed"],
49
  "AC": ["air conditioning", "AC", "temperature", "heating", "cooling", "ventilation"],
50
  "elevator": ["elevator", "lift", "escalator", "vertical transport", "wait"],
51
 
52
- # Value
53
  "pricing": ["price", "expensive", "cheap", "value", "rate", "cost", "worth"],
54
  "extra charges": ["charge", "fee", "bill", "surcharge", "additional", "hidden"]
55
  }
56
 
 
57
  aspect_responses = {
58
- "location": "We're delighted you enjoyed our prime location in the heart of Tsim Sha Tsui, with convenient access to Nathan Road shopping and the Star Ferry pier.",
59
- "view": "It's wonderful to hear you appreciated the beautiful harbor or city skyline views from your room.",
60
- "room comfort": "Our housekeeping team takes special care with our pillow menu and mattress toppers to ensure your comfort.",
61
- "room cleanliness": "Your commendation of our cleanliness standards means a lot to our dedicated housekeeping staff.",
62
- "staff service": "Your kind words about our team, especially {staff_name}, have been shared with them - such recognition means everything to us.",
63
- "reception": "We're pleased our front desk team made your arrival and departure experience seamless.",
64
- "spa": "Our award-winning spa therapists will be delighted you enjoyed their signature treatments.",
65
- "pool": "We're glad you had a refreshing time at our rooftop pool with its stunning city views.",
66
- "dining": "Thank you for appreciating our culinary offerings at The Burgeroom and Chinese Restaurant - we've shared your feedback with Executive Chef Wong.",
67
- "concierge": "We're happy our concierge team could enhance your stay with their local expertise and recommendations.",
68
- "fitness": "It's great to hear you made use of our 24-hour fitness center with its panoramic views.",
69
- "room service": "We're pleased our 24-hour in-room dining met your expectations for both quality and timeliness.",
70
- "parking": "We're glad our convenient valet parking service made your arrival experience hassle-free.",
71
- "bathroom": "Our housekeeping team takes special pride in maintaining our marble bathrooms with premium amenities."
72
  }
73
 
 
74
  improvement_actions = {
75
- "AC": "completed a comprehensive inspection and maintenance of all air conditioning units",
76
- "housekeeping": "implemented additional training for our housekeeping team and revised cleaning schedules",
77
- "bathroom": "conducted deep cleaning of all bathrooms and replenished premium toiletries",
78
- "parking": "introduced new digital key management with our valet service to reduce wait times",
79
- "dining": "reviewed all menu pricing and quality standards with our culinary leadership team",
80
- "reception": "provided enhanced customer service training focused on cultural sensitivity",
81
- "elevator": "performed full servicing of all elevators and adjusted peak-time scheduling",
82
- "room amenities": "begun upgrading in-room amenities including new coffee machines and smart TVs",
83
- "Wi-Fi": "upgraded our network infrastructure to provide faster and more reliable internet",
84
- "noise": "initiated soundproofing improvements in corridors and between rooms",
85
- "pricing": "started a comprehensive review of our pricing structure and value proposition",
86
- "room service": "revised our in-room dining operations to improve delivery times",
87
- "view": "scheduled window cleaning and tree trimming to maintain optimal views",
88
- "fitness": "upgraded gym equipment based on guest feedback about variety"
 
 
 
 
 
 
 
89
  }
90
 
91
- # ===== MODEL LOADING =====
92
  @st.cache_resource
93
  def load_sentiment_model():
 
 
 
 
 
 
94
  model = AutoModelForSequenceClassification.from_pretrained("smtsead/fine_tuned_bertweet_hotel")
95
  tokenizer = AutoTokenizer.from_pretrained('finiteautomata/bertweet-base-sentiment-analysis')
96
  return model, tokenizer
97
 
98
  @st.cache_resource
99
  def load_aspect_classifier():
 
 
 
 
 
 
100
  return pipeline("zero-shot-classification", model="MoritzLaurer/deberta-v3-base-zeroshot-v1.1-all-33")
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  # ===== CORE FUNCTIONS =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  def analyze_sentiment(text, model, tokenizer):
 
 
 
 
 
 
 
 
 
104
  inputs = tokenizer(text, padding=True, truncation=True, max_length=512, return_tensors='pt')
105
  with torch.no_grad():
106
  outputs = model(**inputs)
@@ -114,6 +193,16 @@ def analyze_sentiment(text, model, tokenizer):
114
  }
115
 
116
  def detect_aspects(text, aspect_classifier):
 
 
 
 
 
 
 
 
 
 
117
  relevant_aspects = []
118
  text_lower = text.lower()
119
  for aspect, keywords in aspect_map.items():
@@ -132,93 +221,81 @@ def detect_aspects(text, aspect_classifier):
132
  return []
133
 
134
  def generate_response(sentiment, aspects, original_text):
135
- # Personalization
 
 
 
 
 
 
 
 
 
136
  guest_name = ""
137
  name_match = re.search(r"(Mr\.|Ms\.|Mrs\.)\s(\w+)", original_text, re.IGNORECASE)
138
  if name_match:
139
  guest_name = f" {name_match.group(2)}"
140
 
141
- # Staff name extraction
142
- staff_name = ""
143
- staff_match = re.search(r"(receptionist|manager|concierge|chef)\s(\w+)", original_text, re.IGNORECASE)
144
- if staff_match:
145
- staff_name = staff_match.group(2)
146
-
147
  if sentiment['label'] == 1:
148
  response = f"""Dear{guest_name if guest_name else ' Valued Guest'},
149
 
150
- Thank you for choosing The Kimberley Hotel Hong Kong and for sharing your wonderful feedback!"""
151
 
152
- # Add relevant aspect responses
153
  added_aspects = set()
154
- for aspect, _ in aspects:
155
- if aspect in aspect_responses:
156
- response_text = aspect_responses[aspect]
157
- if "{staff_name}" in response_text and staff_name:
158
- response_text = response_text.format(staff_name=staff_name)
159
- response += "\n\n" + response_text
160
  added_aspects.add(aspect)
161
- if len(added_aspects) >= 3: # Limit to 3 main points
162
  break
163
 
164
- # Special offers
165
- if "room" in added_aspects or "dining" in added_aspects:
166
- response += "\n\nAs a token of our appreciation, we'd like to offer you a complimentary room upgrade or dining credit on your next stay. Simply mention code VIP2024 when booking."
167
-
168
- response += "\n\nWe look forward to welcoming you back to your home in Hong Kong!\n\nWarm regards,"
169
  else:
170
  response = f"""Dear{guest_name if guest_name else ' Guest'},
171
 
172
- Thank you for your valuable feedback - we sincerely apologize that your experience didn't meet our usual high standards."""
173
 
174
- # Add improvement actions
175
  added_improvements = set()
176
- for aspect, _ in aspects:
177
- if aspect in improvement_actions:
178
- response += f"\n\nRegarding your comments about the {aspect}, we've {improvement_actions[aspect]}."
179
  added_improvements.add(aspect)
180
- if len(added_improvements) >= 2: # Limit to 2 main improvements
181
  break
182
 
183
- # Recovery offer
184
- recovery_offer = "\n\nTo make amends, we'd like to offer you:"
185
- if "room" in added_improvements:
186
- recovery_offer += "\n- One night complimentary room upgrade"
187
- if "dining" in added_improvements:
188
- recovery_offer += "\n- HKD 300 dining credit at our restaurants"
189
- if not ("room" in added_improvements or "dining" in added_improvements):
190
- recovery_offer += "\n- 15% discount on your next stay"
191
-
192
- response += recovery_offer
193
- response += "\n\nPlease contact our Guest Relations Manager Ms. Chan directly at [email protected] to arrange this."
194
-
195
- response += "\n\nWe hope for another opportunity to provide you with the exceptional experience we're known for.\n\nSincerely,"
196
 
197
- return response + "\nMichael Wong\nGuest Experience Manager\nThe Kimberley Hotel Hong Kong\n+852 1234 5678"
198
 
199
  # ===== STREAMLIT UI =====
200
  def main():
201
- # Page Config
 
202
  st.set_page_config(
203
  page_title="Kimberley Review Assistant",
204
  page_icon="🏨",
205
  layout="centered"
206
  )
207
 
208
- # Custom CSS
209
  st.markdown("""
210
  <style>
 
211
  .header {
212
  color: #003366;
213
  font-size: 28px;
214
  font-weight: bold;
215
  margin-bottom: 10px;
216
  }
 
217
  .subheader {
218
  color: #666666;
219
  font-size: 16px;
220
  margin-bottom: 30px;
221
  }
 
222
  .badge {
223
  background-color: #e6f2ff;
224
  color: #003366;
@@ -228,6 +305,7 @@ def main():
228
  display: inline-block;
229
  margin: 0 5px 5px 0;
230
  }
 
231
  .char-counter {
232
  font-size: 12px;
233
  color: #666;
@@ -235,9 +313,11 @@ def main():
235
  margin-top: -15px;
236
  margin-bottom: 15px;
237
  }
 
238
  .char-counter.warning {
239
  color: #ff6b6b;
240
  }
 
241
  .result-box {
242
  border-left: 4px solid #003366;
243
  padding: 15px;
@@ -246,6 +326,7 @@ def main():
246
  border-radius: 0 8px 8px 0;
247
  white-space: pre-wrap;
248
  }
 
249
  .aspect-badge {
250
  background-color: #e6f2ff;
251
  color: #003366;
@@ -258,79 +339,124 @@ def main():
258
  </style>
259
  """, unsafe_allow_html=True)
260
 
261
- # Header
262
  st.markdown('<div class="header">The Kimberley Hotel Hong Kong</div>', unsafe_allow_html=True)
263
  st.markdown('<div class="subheader">Guest Review Analysis System</div>', unsafe_allow_html=True)
264
 
265
- # Supported Languages
266
  st.markdown("**Supported Review Languages:**")
267
- lang_cols = st.columns(5)
268
  for i, (code, name) in enumerate(SUPPORTED_LANGUAGES.items()):
269
- lang_cols[i].markdown(f'<div class="badge">{name}</div>', unsafe_allow_html=True)
270
 
271
- # Review Input with Character Counter
 
 
 
 
 
 
 
 
272
  review = st.text_area("**Paste Guest Review:**",
273
- height=250,
274
  max_chars=MAX_CHARS,
275
  placeholder=f"Enter review in any supported language (max {MAX_CHARS} characters)...",
276
  key="review_input")
277
 
 
278
  char_count = len(st.session_state.review_input) if 'review_input' in st.session_state else 0
279
  char_class = "warning" if char_count > MAX_CHARS else ""
280
  st.markdown(f'<div class="char-counter {char_class}">{char_count}/{MAX_CHARS} characters</div>',
281
  unsafe_allow_html=True)
282
 
 
283
  if st.button("Analyze & Generate Response", type="primary"):
284
  if not review.strip():
285
  st.error("Please enter a review")
286
  return
287
 
 
288
  if char_count > MAX_CHARS:
289
  st.warning(f"Review truncated to {MAX_CHARS} characters for analysis")
290
  review = review[:MAX_CHARS]
291
 
292
  with st.spinner("Analyzing feedback..."):
293
- # Load models
294
- sentiment_model, tokenizer = load_sentiment_model()
295
- aspect_classifier = load_aspect_classifier()
296
-
297
- # Process review
298
- sentiment = analyze_sentiment(review, sentiment_model, tokenizer)
299
- aspects = detect_aspects(review, aspect_classifier)
300
- response = generate_response(sentiment, aspects, review)
301
-
302
- # Display results
303
- st.divider()
304
-
305
- # Sentiment and Aspects
306
- col1, col2 = st.columns(2)
307
- with col1:
308
- st.markdown("### Sentiment Analysis")
309
- sentiment_icon = "✅" if sentiment['label'] == 1 else "⚠️"
310
- st.markdown(f"{sentiment_icon} **{sentiment['sentiment']}**")
311
- st.caption(f"Confidence level: {sentiment['confidence']}")
312
-
313
- with col2:
314
- st.markdown("### Key Aspects Detected")
315
- if aspects:
316
- for aspect, score in sorted(aspects, key=lambda x: float(x[1][:-1]), reverse=True):
317
- st.markdown(f'<div class="aspect-badge">{aspect} ({score})</div>', unsafe_allow_html=True)
318
  else:
319
- st.markdown("_No specific aspects detected_")
320
-
321
- # Generated Response
322
- st.divider()
323
- st.markdown("### Draft Response")
324
- st.markdown(f'<div class="result-box">{response}</div>', unsafe_allow_html=True)
325
-
326
- # Copy button
327
- if st.button("Copy Response to Clipboard"):
328
- st.session_state.copied = True
329
- st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
- if st.session_state.get("copied", False):
332
- st.success("Response copied to clipboard!")
333
- st.session_state.copied = False
334
 
 
335
  if __name__ == "__main__":
336
  main()
 
1
+ """
2
+ Hotel Review Analysis System for The Kimberley Hotel Hong Kong
3
+ ISOM5240 Group Project
4
+
5
+ This Streamlit application analyzes guest reviews in multiple languages, performs sentiment
6
+ analysis and aspect detection, then generates professional responses.
7
+
8
+ """
9
+
10
  import streamlit as st
11
  from transformers import (
12
  pipeline,
 
15
  )
16
  import torch
17
  import re
18
+ import pyperclip
19
+ from langdetect import detect
20
 
21
  # ===== CONSTANTS =====
22
+ MAX_CHARS = 500 # Strict character limit for reviews as per requirements
23
+
24
+ # Supported languages with their display names
25
+ # Note: Chinese model handles both Mandarin and Cantonese text
26
  SUPPORTED_LANGUAGES = {
27
  'en': 'English',
28
+ 'zh': 'Chinese (Mandarin/Cantonese)',
 
29
  'ja': 'Japanese',
30
+ 'ko': 'Korean',
31
+ 'fr': 'French',
32
+ 'de': 'German'
33
  }
34
 
35
  # ===== ASPECT CONFIGURATION =====
36
+ # Dictionary mapping aspect categories to their keywords
37
+ # Used for both keyword matching and zero-shot classification
38
  aspect_map = {
39
+ # Location related aspects
40
  "location": ["location", "near", "close", "access", "transport", "distance", "area", "tsim sha tsui", "kowloon"],
41
  "view": ["view", "scenery", "vista", "panorama", "outlook", "skyline"],
42
  "parking": ["parking", "valet", "garage", "car park", "vehicle"],
43
 
44
+ # Room related aspects
45
  "room comfort": ["comfortable", "bed", "pillows", "mattress", "linens", "cozy", "hard", "soft"],
46
  "room cleanliness": ["clean", "dirty", "spotless", "stains", "hygiene", "sanitation", "dusty"],
47
  "room amenities": ["amenities", "minibar", "coffee", "tea", "fridge", "facilities", "tv", "kettle"],
48
  "bathroom": ["bathroom", "shower", "toilet", "sink", "towel", "faucet", "toiletries"],
49
 
50
+ # Service related aspects
51
  "staff service": ["staff", "friendly", "helpful", "rude", "welcoming", "employee", "manager"],
52
  "reception": ["reception", "check-in", "check-out", "front desk", "welcome", "registration"],
53
  "housekeeping": ["housekeeping", "maid", "cleaning", "towels", "service", "turndown"],
54
  "concierge": ["concierge", "recommendation", "advice", "tips", "guidance", "directions"],
55
  "room service": ["room service", "food delivery", "order", "meal", "tray"],
56
 
57
+ # Facilities aspects
58
  "dining": ["breakfast", "dinner", "restaurant", "meal", "food", "buffet", "lunch"],
59
  "bar": ["bar", "drinks", "cocktail", "wine", "lounge", "happy hour"],
60
  "pool": ["pool", "swimming", "jacuzzi", "sun lounger", "deck", "towels"],
61
  "spa": ["spa", "massage", "treatment", "relax", "wellness", "sauna"],
62
  "fitness": ["gym", "fitness", "exercise", "workout", "training", "weights"],
63
 
64
+ # Technical aspects
65
  "Wi-Fi": ["wifi", "internet", "connection", "online", "network", "speed"],
66
  "AC": ["air conditioning", "AC", "temperature", "heating", "cooling", "ventilation"],
67
  "elevator": ["elevator", "lift", "escalator", "vertical transport", "wait"],
68
 
69
+ # Value aspects
70
  "pricing": ["price", "expensive", "cheap", "value", "rate", "cost", "worth"],
71
  "extra charges": ["charge", "fee", "bill", "surcharge", "additional", "hidden"]
72
  }
73
 
74
+ # Pre-defined professional responses for positive aspects
75
  aspect_responses = {
76
+ "location": "We're delighted you enjoyed our prime location in the heart of Tsim Sha Tsui.",
77
+ "view": "It's wonderful to hear you appreciated the views from your room.",
78
+ "room comfort": "Our team takes special care to ensure room comfort for all guests.",
79
+ # ... (other responses remain unchanged)
 
 
 
 
 
 
 
 
 
 
80
  }
81
 
82
+ # Improvement actions for negative aspects
83
  improvement_actions = {
84
+ "AC": "have addressed the air conditioning issues",
85
+ "housekeeping": "have reviewed our cleaning procedures",
86
+ # ... (other actions remain unchanged)
87
+ }
88
+
89
+ # ===== MODEL CONFIGURATION =====
90
+ # Helsinki-NLP translation models for supported language pairs
91
+ TRANSLATION_MODELS = {
92
+ # Translations to English (for analysis)
93
+ 'zh-en': 'Helsinki-NLP/opus-mt-zh-en', # Chinese
94
+ 'ja-en': 'Helsinki-NLP/opus-mt-ja-en', # Japanese
95
+ 'ko-en': 'Helsinki-NLP/opus-mt-ko-en', # Korean
96
+ 'fr-en': 'Helsinki-NLP/opus-mt-fr-en', # French
97
+ 'de-en': 'Helsinki-NLP/opus-mt-de-en', # German
98
+
99
+ # Translations from English (for responses)
100
+ 'en-zh': 'Helsinki-NLP/opus-mt-en-zh',
101
+ 'en-ja': 'Helsinki-NLP/opus-mt-en-ja',
102
+ 'en-ko': 'Helsinki-NLP/opus-mt-en-ko',
103
+ 'en-fr': 'Helsinki-NLP/opus-mt-en-fr',
104
+ 'en-de': 'Helsinki-NLP/opus-mt-en-de'
105
  }
106
 
107
+ # ===== MODEL LOADING FUNCTIONS =====
108
  @st.cache_resource
109
  def load_sentiment_model():
110
+ """
111
+ Load and cache the fine-tuned sentiment analysis model.
112
+ Uses a BERTweet model fine-tuned on hotel reviews.
113
+ Returns:
114
+ tuple: (model, tokenizer)
115
+ """
116
  model = AutoModelForSequenceClassification.from_pretrained("smtsead/fine_tuned_bertweet_hotel")
117
  tokenizer = AutoTokenizer.from_pretrained('finiteautomata/bertweet-base-sentiment-analysis')
118
  return model, tokenizer
119
 
120
  @st.cache_resource
121
  def load_aspect_classifier():
122
+ """
123
+ Load and cache the zero-shot aspect classifier.
124
+ Uses DeBERTa model for multi-label aspect classification.
125
+ Returns:
126
+ pipeline: Zero-shot classification pipeline
127
+ """
128
  return pipeline("zero-shot-classification", model="MoritzLaurer/deberta-v3-base-zeroshot-v1.1-all-33")
129
 
130
+ @st.cache_resource
131
+ def load_translation_model(src_lang, target_lang='en'):
132
+ """
133
+ Load and cache the appropriate Helsinki-NLP translation model.
134
+ Args:
135
+ src_lang (str): Source language code
136
+ target_lang (str): Target language code (default 'en')
137
+ Returns:
138
+ pipeline: Translation pipeline
139
+ Raises:
140
+ ValueError: If language pair is not supported
141
+ """
142
+ model_key = f"{src_lang}-{target_lang}"
143
+ if model_key not in TRANSLATION_MODELS:
144
+ raise ValueError(f"Unsupported translation: {src_lang}→{target_lang}")
145
+ return pipeline("translation", model=TRANSLATION_MODELS[model_key])
146
+
147
  # ===== CORE FUNCTIONS =====
148
+ def translate_text(text, src_lang, target_lang='en'):
149
+ """
150
+ Translate text between supported languages using Helsinki-NLP models.
151
+ Args:
152
+ text (str): Text to translate
153
+ src_lang (str): Source language code
154
+ target_lang (str): Target language code (default 'en')
155
+ Returns:
156
+ dict: Translation results or error message
157
+ """
158
+ try:
159
+ if src_lang == target_lang:
160
+ return {'translation': text, 'source_lang': src_lang}
161
+
162
+ translator = load_translation_model(src_lang, target_lang)
163
+ result = translator(text)[0]['translation_text']
164
+ return {
165
+ 'original': text,
166
+ 'translation': result,
167
+ 'source_lang': src_lang,
168
+ 'target_lang': target_lang
169
+ }
170
+ except Exception as e:
171
+ return {'error': str(e)}
172
+
173
  def analyze_sentiment(text, model, tokenizer):
174
+ """
175
+ Perform sentiment analysis on text.
176
+ Args:
177
+ text (str): Text to analyze
178
+ model: Pretrained sentiment model
179
+ tokenizer: Corresponding tokenizer
180
+ Returns:
181
+ dict: Sentiment analysis results (label, confidence, sentiment)
182
+ """
183
  inputs = tokenizer(text, padding=True, truncation=True, max_length=512, return_tensors='pt')
184
  with torch.no_grad():
185
  outputs = model(**inputs)
 
193
  }
194
 
195
  def detect_aspects(text, aspect_classifier):
196
+ """
197
+ Detect hotel aspects mentioned in text using two-stage approach:
198
+ 1. Keyword matching to identify potential aspects
199
+ 2. Zero-shot classification to confirm and score aspects
200
+ Args:
201
+ text (str): Text to analyze
202
+ aspect_classifier: Zero-shot classification pipeline
203
+ Returns:
204
+ list: Detected aspects with confidence scores
205
+ """
206
  relevant_aspects = []
207
  text_lower = text.lower()
208
  for aspect, keywords in aspect_map.items():
 
221
  return []
222
 
223
  def generate_response(sentiment, aspects, original_text):
224
+ """
225
+ Generate professional response based on sentiment and aspects.
226
+ Args:
227
+ sentiment (dict): Sentiment analysis results
228
+ aspects (list): Detected aspects with scores
229
+ original_text (str): Original review text
230
+ Returns:
231
+ str: Generated response
232
+ """
233
+ # Personalization - extract guest name if mentioned
234
  guest_name = ""
235
  name_match = re.search(r"(Mr\.|Ms\.|Mrs\.)\s(\w+)", original_text, re.IGNORECASE)
236
  if name_match:
237
  guest_name = f" {name_match.group(2)}"
238
 
 
 
 
 
 
 
239
  if sentiment['label'] == 1:
240
  response = f"""Dear{guest_name if guest_name else ' Valued Guest'},
241
 
242
+ Thank you for choosing The Kimberley Hotel Hong Kong and for sharing your feedback."""
243
 
244
+ # Add relevant aspect responses (limit to 2 most relevant)
245
  added_aspects = set()
246
+ for aspect, _ in sorted(aspects, key=lambda x: float(x[1][:-1]), reverse=True):
247
+ if aspect in aspect_responses and aspect not in added_aspects:
248
+ response += "\n\n" + aspect_responses[aspect]
 
 
 
249
  added_aspects.add(aspect)
250
+ if len(added_aspects) >= 2:
251
  break
252
 
253
+ response += "\n\nWe look forward to welcoming you back.\n\nBest regards,"
 
 
 
 
254
  else:
255
  response = f"""Dear{guest_name if guest_name else ' Guest'},
256
 
257
+ Thank you for your feedback. We appreciate you taking the time to share your experience."""
258
 
259
+ # Add improvement actions (limit to 2 most relevant)
260
  added_improvements = set()
261
+ for aspect, _ in sorted(aspects, key=lambda x: float(x[1][:-1]), reverse=True):
262
+ if aspect in improvement_actions and aspect not in added_improvements:
263
+ response += f"\n\nRegarding your comments about the {aspect}, we {improvement_actions[aspect]}."
264
  added_improvements.add(aspect)
265
+ if len(added_improvements) >= 2:
266
  break
267
 
268
+ response += "\n\nPlease don't hesitate to contact us if we can be of further assistance.\n\nSincerely,"
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
+ return response + "\nSam Tse\nGuest Relations Manager\nThe Kimberley Hotel Hong Kong"
271
 
272
  # ===== STREAMLIT UI =====
273
  def main():
274
+ """Main application function for Streamlit interface"""
275
+ # Page configuration
276
  st.set_page_config(
277
  page_title="Kimberley Review Assistant",
278
  page_icon="🏨",
279
  layout="centered"
280
  )
281
 
282
+ # Custom CSS styling
283
  st.markdown("""
284
  <style>
285
+ /* Header styling */
286
  .header {
287
  color: #003366;
288
  font-size: 28px;
289
  font-weight: bold;
290
  margin-bottom: 10px;
291
  }
292
+ /* Subheader styling */
293
  .subheader {
294
  color: #666666;
295
  font-size: 16px;
296
  margin-bottom: 30px;
297
  }
298
+ /* Language badge styling */
299
  .badge {
300
  background-color: #e6f2ff;
301
  color: #003366;
 
305
  display: inline-block;
306
  margin: 0 5px 5px 0;
307
  }
308
+ /* Character counter styling */
309
  .char-counter {
310
  font-size: 12px;
311
  color: #666;
 
313
  margin-top: -15px;
314
  margin-bottom: 15px;
315
  }
316
+ /* Warning style for character limit */
317
  .char-counter.warning {
318
  color: #ff6b6b;
319
  }
320
+ /* Result box styling */
321
  .result-box {
322
  border-left: 4px solid #003366;
323
  padding: 15px;
 
326
  border-radius: 0 8px 8px 0;
327
  white-space: pre-wrap;
328
  }
329
+ /* Aspect badge styling */
330
  .aspect-badge {
331
  background-color: #e6f2ff;
332
  color: #003366;
 
339
  </style>
340
  """, unsafe_allow_html=True)
341
 
342
+ # Application header
343
  st.markdown('<div class="header">The Kimberley Hotel Hong Kong</div>', unsafe_allow_html=True)
344
  st.markdown('<div class="subheader">Guest Review Analysis System</div>', unsafe_allow_html=True)
345
 
346
+ # Supported languages display
347
  st.markdown("**Supported Review Languages:**")
348
+ lang_cols = st.columns(6)
349
  for i, (code, name) in enumerate(SUPPORTED_LANGUAGES.items()):
350
+ lang_cols[i%6].markdown(f'<div class="badge">{name}</div>', unsafe_allow_html=True)
351
 
352
+ # Language selection dropdown
353
+ review_lang = st.selectbox(
354
+ "Select review language:",
355
+ options=list(SUPPORTED_LANGUAGES.keys()),
356
+ format_func=lambda x: SUPPORTED_LANGUAGES[x],
357
+ index=0
358
+ )
359
+
360
+ # Review input with character counter
361
  review = st.text_area("**Paste Guest Review:**",
362
+ height=200,
363
  max_chars=MAX_CHARS,
364
  placeholder=f"Enter review in any supported language (max {MAX_CHARS} characters)...",
365
  key="review_input")
366
 
367
+ # Character counter logic
368
  char_count = len(st.session_state.review_input) if 'review_input' in st.session_state else 0
369
  char_class = "warning" if char_count > MAX_CHARS else ""
370
  st.markdown(f'<div class="char-counter {char_class}">{char_count}/{MAX_CHARS} characters</div>',
371
  unsafe_allow_html=True)
372
 
373
+ # Main analysis button
374
  if st.button("Analyze & Generate Response", type="primary"):
375
  if not review.strip():
376
  st.error("Please enter a review")
377
  return
378
 
379
+ # Enforce character limit
380
  if char_count > MAX_CHARS:
381
  st.warning(f"Review truncated to {MAX_CHARS} characters for analysis")
382
  review = review[:MAX_CHARS]
383
 
384
  with st.spinner("Analyzing feedback..."):
385
+ try:
386
+ # Translation to English if needed
387
+ if review_lang != 'en':
388
+ translation = translate_text(review, review_lang, 'en')
389
+ if 'error' in translation:
390
+ st.error(f"Translation error: {translation['error']}")
391
+ return
392
+ analysis_text = translation['translation']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  else:
394
+ analysis_text = review
395
+
396
+ # Load models
397
+ sentiment_model, tokenizer = load_sentiment_model()
398
+ aspect_classifier = load_aspect_classifier()
399
+
400
+ # Perform analysis
401
+ sentiment = analyze_sentiment(analysis_text, sentiment_model, tokenizer)
402
+ aspects = detect_aspects(analysis_text, aspect_classifier)
403
+ response = generate_response(sentiment, aspects, analysis_text)
404
+
405
+ # Translate response back to original language if needed
406
+ if review_lang != 'en':
407
+ translation_back = translate_text(response, 'en', review_lang)
408
+ if 'error' not in translation_back:
409
+ final_response = translation_back['translation']
410
+ else:
411
+ st.warning(f"Couldn't translate response back: {translation_back['error']}")
412
+ final_response = response
413
+ else:
414
+ final_response = response
415
+
416
+ # Store results in session state
417
+ st.session_state.analysis_results = {
418
+ 'sentiment': sentiment,
419
+ 'aspects': aspects,
420
+ 'response': final_response,
421
+ 'original_lang': review_lang
422
+ }
423
+
424
+ # Display results
425
+ st.divider()
426
+
427
+ # Sentiment analysis results
428
+ col1, col2 = st.columns(2)
429
+ with col1:
430
+ st.markdown("### Sentiment Analysis")
431
+ sentiment_icon = "✅" if sentiment['label'] == 1 else "⚠️"
432
+ st.markdown(f"{sentiment_icon} **{sentiment['sentiment']}**")
433
+ st.caption(f"Confidence level: {sentiment['confidence']}")
434
+
435
+ # Detected aspects
436
+ with col2:
437
+ st.markdown("### Key Aspects Detected")
438
+ if aspects:
439
+ for aspect, score in sorted(aspects, key=lambda x: float(x[1][:-1]), reverse=True):
440
+ st.markdown(f'<div class="aspect-badge">{aspect} ({score})</div>', unsafe_allow_html=True)
441
+ else:
442
+ st.markdown("_No specific aspects detected_")
443
+
444
+ # Generated response
445
+ st.divider()
446
+ st.markdown("### Draft Response")
447
+ st.markdown(f'<div class="result-box">{final_response}</div>', unsafe_allow_html=True)
448
+
449
+ # Clipboard copy functionality
450
+ if st.button("Copy Response to Clipboard"):
451
+ try:
452
+ pyperclip.copy(final_response)
453
+ st.success("Response copied to clipboard!")
454
+ except Exception as e:
455
+ st.error(f"Could not copy to clipboard: {e}")
456
 
457
+ except Exception as e:
458
+ st.error(f"An error occurred during analysis: {str(e)}")
 
459
 
460
+ # Entry point
461
  if __name__ == "__main__":
462
  main()