Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -8,6 +8,7 @@ import torch
|
|
8 |
import re
|
9 |
|
10 |
# ===== CONSTANTS =====
|
|
|
11 |
SUPPORTED_LANGUAGES = {
|
12 |
'en': 'English',
|
13 |
'zh': 'Chinese',
|
@@ -16,6 +17,77 @@ SUPPORTED_LANGUAGES = {
|
|
16 |
'ko': 'Korean'
|
17 |
}
|
18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
19 |
# ===== MODEL LOADING =====
|
20 |
@st.cache_resource
|
21 |
def load_sentiment_model():
|
@@ -42,16 +114,11 @@ def analyze_sentiment(text, model, tokenizer):
|
|
42 |
}
|
43 |
|
44 |
def detect_aspects(text, aspect_classifier):
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
"Value": ["price", "expensive", "worth"]
|
51 |
-
}
|
52 |
-
|
53 |
-
relevant_aspects = [aspect for aspect, keywords in aspect_map.items()
|
54 |
-
if any(re.search(rf'\b{kw}\b', text.lower()) for kw in keywords)]
|
55 |
|
56 |
if relevant_aspects:
|
57 |
result = aspect_classifier(
|
@@ -61,50 +128,73 @@ def detect_aspects(text, aspect_classifier):
|
|
61 |
hypothesis_template="This review discusses the hotel's {}."
|
62 |
)
|
63 |
return [(aspect, f"{score:.0%}") for aspect, score in
|
64 |
-
zip(result['labels'], result['scores']) if score > 0.
|
65 |
return []
|
66 |
|
67 |
-
def generate_response(sentiment, aspects):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
if sentiment['label'] == 1:
|
69 |
-
response = """Dear Valued Guest,
|
70 |
|
71 |
-
Thank you for choosing The Kimberley Hotel Hong Kong!"""
|
72 |
-
|
73 |
-
aspect_responses = {
|
74 |
-
"Location": "\nWe're delighted you enjoyed our prime Tsim Sha Tsui location.",
|
75 |
-
"Room Quality": "\nOur team is thrilled you appreciated your room's comfort and cleanliness.",
|
76 |
-
"Staff Service": "\nYour kind words about our staff have been shared with the team.",
|
77 |
-
"Dining": "\nWe're glad you enjoyed our culinary offerings at The Burgeroom.",
|
78 |
-
"Value": "\nWe strive to provide excellent value for our guests."
|
79 |
-
}
|
80 |
|
|
|
|
|
81 |
for aspect, _ in aspects:
|
82 |
if aspect in aspect_responses:
|
83 |
-
|
84 |
-
|
|
|
|
|
|
|
|
|
|
|
85 |
|
86 |
-
|
|
|
|
|
|
|
|
|
87 |
else:
|
88 |
-
response = """Dear Guest,
|
89 |
|
90 |
-
Thank you for your feedback - we sincerely apologize
|
91 |
-
|
92 |
-
improvements = {
|
93 |
-
"Location": "\nWe're enhancing our local area guides to better serve guests.",
|
94 |
-
"Room Quality": "\nWe're currently upgrading our rooms based on guest feedback.",
|
95 |
-
"Staff Service": "\nAdditional training programs are being implemented.",
|
96 |
-
"Dining": "\nOur culinary team is reviewing all menus.",
|
97 |
-
"Value": "\nWe're reassessing our pricing structure."
|
98 |
-
}
|
99 |
|
|
|
|
|
100 |
for aspect, _ in aspects:
|
101 |
-
if aspect in
|
102 |
-
response +=
|
103 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
|
105 |
-
response +=
|
|
|
|
|
|
|
106 |
|
107 |
-
return response + "\
|
108 |
|
109 |
# ===== STREAMLIT UI =====
|
110 |
def main():
|
@@ -138,18 +228,39 @@ def main():
|
|
138 |
display: inline-block;
|
139 |
margin: 0 5px 5px 0;
|
140 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
141 |
.result-box {
|
142 |
border-left: 4px solid #003366;
|
143 |
-
padding:
|
144 |
background-color: #f9f9f9;
|
145 |
-
margin:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
146 |
}
|
147 |
</style>
|
148 |
""", unsafe_allow_html=True)
|
149 |
|
150 |
# Header
|
151 |
st.markdown('<div class="header">The Kimberley Hotel Hong Kong</div>', unsafe_allow_html=True)
|
152 |
-
st.markdown('<div class="subheader">
|
153 |
|
154 |
# Supported Languages
|
155 |
st.markdown("**Supported Review Languages:**")
|
@@ -157,16 +268,28 @@ def main():
|
|
157 |
for i, (code, name) in enumerate(SUPPORTED_LANGUAGES.items()):
|
158 |
lang_cols[i].markdown(f'<div class="badge">{name}</div>', unsafe_allow_html=True)
|
159 |
|
160 |
-
# Review Input
|
161 |
-
review = st.text_area("**Paste Guest Review:**",
|
162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
163 |
|
164 |
if st.button("Analyze & Generate Response", type="primary"):
|
165 |
if not review.strip():
|
166 |
st.error("Please enter a review")
|
167 |
return
|
168 |
|
169 |
-
|
|
|
|
|
|
|
|
|
170 |
# Load models
|
171 |
sentiment_model, tokenizer = load_sentiment_model()
|
172 |
aspect_classifier = load_aspect_classifier()
|
@@ -174,7 +297,7 @@ def main():
|
|
174 |
# Process review
|
175 |
sentiment = analyze_sentiment(review, sentiment_model, tokenizer)
|
176 |
aspects = detect_aspects(review, aspect_classifier)
|
177 |
-
response = generate_response(sentiment, aspects)
|
178 |
|
179 |
# Display results
|
180 |
st.divider()
|
@@ -182,21 +305,32 @@ def main():
|
|
182 |
# Sentiment and Aspects
|
183 |
col1, col2 = st.columns(2)
|
184 |
with col1:
|
185 |
-
st.markdown(
|
186 |
-
|
|
|
|
|
187 |
|
188 |
with col2:
|
|
|
189 |
if aspects:
|
190 |
-
|
191 |
-
|
192 |
-
st.write(f"- {aspect} ({score} confidence)")
|
193 |
else:
|
194 |
-
st.markdown("
|
195 |
|
196 |
# Generated Response
|
197 |
st.divider()
|
198 |
-
st.markdown("
|
199 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
200 |
|
201 |
if __name__ == "__main__":
|
202 |
main()
|
|
|
8 |
import re
|
9 |
|
10 |
# ===== CONSTANTS =====
|
11 |
+
MAX_CHARS = 1500 # Increased character limit
|
12 |
SUPPORTED_LANGUAGES = {
|
13 |
'en': 'English',
|
14 |
'zh': 'Chinese',
|
|
|
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():
|
|
|
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():
|
120 |
+
if any(re.search(rf'\b{kw}\b', text_lower) for kw in keywords):
|
121 |
+
relevant_aspects.append(aspect)
|
|
|
|
|
|
|
|
|
|
|
122 |
|
123 |
if relevant_aspects:
|
124 |
result = aspect_classifier(
|
|
|
128 |
hypothesis_template="This review discusses the hotel's {}."
|
129 |
)
|
130 |
return [(aspect, f"{score:.0%}") for aspect, score in
|
131 |
+
zip(result['labels'], result['scores']) if score > 0.6]
|
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():
|
|
|
228 |
display: inline-block;
|
229 |
margin: 0 5px 5px 0;
|
230 |
}
|
231 |
+
.char-counter {
|
232 |
+
font-size: 12px;
|
233 |
+
color: #666;
|
234 |
+
text-align: right;
|
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;
|
244 |
background-color: #f9f9f9;
|
245 |
+
margin: 20px 0;
|
246 |
+
border-radius: 0 8px 8px 0;
|
247 |
+
white-space: pre-wrap;
|
248 |
+
}
|
249 |
+
.aspect-badge {
|
250 |
+
background-color: #e6f2ff;
|
251 |
+
color: #003366;
|
252 |
+
padding: 2px 8px;
|
253 |
+
border-radius: 4px;
|
254 |
+
font-size: 14px;
|
255 |
+
display: inline-block;
|
256 |
+
margin: 2px;
|
257 |
}
|
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:**")
|
|
|
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()
|
|
|
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()
|
|
|
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()
|