Spaces:
Sleeping
Sleeping
File size: 15,414 Bytes
c27f930 92ca7f3 c18bd07 92ca7f3 c27f930 c18bd07 92ca7f3 c27f930 92ca7f3 c27f930 92ca7f3 c27f930 92ca7f3 c27f930 92ca7f3 c27f930 c18bd07 c27f930 92ca7f3 c27f930 92ca7f3 c18bd07 c27f930 c18bd07 92ca7f3 c18bd07 92ca7f3 c18bd07 c27f930 c18bd07 c27f930 c18bd07 c27f930 c18bd07 c27f930 c18bd07 92ca7f3 c18bd07 c27f930 c18bd07 c27f930 c18bd07 c27f930 c18bd07 c27f930 c18bd07 c27f930 92ca7f3 c27f930 92ca7f3 c27f930 92ca7f3 c18bd07 92ca7f3 c18bd07 92ca7f3 c18bd07 92ca7f3 c27f930 92ca7f3 c18bd07 c27f930 92ca7f3 c27f930 c18bd07 c27f930 92ca7f3 c27f930 c18bd07 92ca7f3 c27f930 92ca7f3 c18bd07 c27f930 92ca7f3 c27f930 92ca7f3 c27f930 c18bd07 c27f930 c18bd07 92ca7f3 c18bd07 92ca7f3 c18bd07 92ca7f3 c18bd07 c27f930 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 |
import streamlit as st
from transformers import (
pipeline,
AutoModelForSequenceClassification,
AutoTokenizer
)
import torch
import re
# ===== CONSTANTS =====
MAX_CHARS = 1500 # Increased character limit
SUPPORTED_LANGUAGES = {
'en': 'English',
'zh': 'Chinese',
'yue': 'Cantonese',
'ja': 'Japanese',
'ko': 'Korean'
}
# ===== ASPECT CONFIGURATION =====
aspect_map = {
# Location related
"location": ["location", "near", "close", "access", "transport", "distance", "area", "tsim sha tsui", "kowloon"],
"view": ["view", "scenery", "vista", "panorama", "outlook", "skyline"],
"parking": ["parking", "valet", "garage", "car park", "vehicle"],
# Room related
"room comfort": ["comfortable", "bed", "pillows", "mattress", "linens", "cozy", "hard", "soft"],
"room cleanliness": ["clean", "dirty", "spotless", "stains", "hygiene", "sanitation", "dusty"],
"room amenities": ["amenities", "minibar", "coffee", "tea", "fridge", "facilities", "tv", "kettle"],
"bathroom": ["bathroom", "shower", "toilet", "sink", "towel", "faucet", "toiletries"],
# Service related
"staff service": ["staff", "friendly", "helpful", "rude", "welcoming", "employee", "manager"],
"reception": ["reception", "check-in", "check-out", "front desk", "welcome", "registration"],
"housekeeping": ["housekeeping", "maid", "cleaning", "towels", "service", "turndown"],
"concierge": ["concierge", "recommendation", "advice", "tips", "guidance", "directions"],
"room service": ["room service", "food delivery", "order", "meal", "tray"],
# Facilities
"dining": ["breakfast", "dinner", "restaurant", "meal", "food", "buffet", "lunch"],
"bar": ["bar", "drinks", "cocktail", "wine", "lounge", "happy hour"],
"pool": ["pool", "swimming", "jacuzzi", "sun lounger", "deck", "towels"],
"spa": ["spa", "massage", "treatment", "relax", "wellness", "sauna"],
"fitness": ["gym", "fitness", "exercise", "workout", "training", "weights"],
# Technical
"Wi-Fi": ["wifi", "internet", "connection", "online", "network", "speed"],
"AC": ["air conditioning", "AC", "temperature", "heating", "cooling", "ventilation"],
"elevator": ["elevator", "lift", "escalator", "vertical transport", "wait"],
# Value
"pricing": ["price", "expensive", "cheap", "value", "rate", "cost", "worth"],
"extra charges": ["charge", "fee", "bill", "surcharge", "additional", "hidden"]
}
aspect_responses = {
"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.",
"view": "It's wonderful to hear you appreciated the beautiful harbor or city skyline views from your room.",
"room comfort": "Our housekeeping team takes special care with our pillow menu and mattress toppers to ensure your comfort.",
"room cleanliness": "Your commendation of our cleanliness standards means a lot to our dedicated housekeeping staff.",
"staff service": "Your kind words about our team, especially {staff_name}, have been shared with them - such recognition means everything to us.",
"reception": "We're pleased our front desk team made your arrival and departure experience seamless.",
"spa": "Our award-winning spa therapists will be delighted you enjoyed their signature treatments.",
"pool": "We're glad you had a refreshing time at our rooftop pool with its stunning city views.",
"dining": "Thank you for appreciating our culinary offerings at The Burgeroom and Chinese Restaurant - we've shared your feedback with Executive Chef Wong.",
"concierge": "We're happy our concierge team could enhance your stay with their local expertise and recommendations.",
"fitness": "It's great to hear you made use of our 24-hour fitness center with its panoramic views.",
"room service": "We're pleased our 24-hour in-room dining met your expectations for both quality and timeliness.",
"parking": "We're glad our convenient valet parking service made your arrival experience hassle-free.",
"bathroom": "Our housekeeping team takes special pride in maintaining our marble bathrooms with premium amenities."
}
improvement_actions = {
"AC": "completed a comprehensive inspection and maintenance of all air conditioning units",
"housekeeping": "implemented additional training for our housekeeping team and revised cleaning schedules",
"bathroom": "conducted deep cleaning of all bathrooms and replenished premium toiletries",
"parking": "introduced new digital key management with our valet service to reduce wait times",
"dining": "reviewed all menu pricing and quality standards with our culinary leadership team",
"reception": "provided enhanced customer service training focused on cultural sensitivity",
"elevator": "performed full servicing of all elevators and adjusted peak-time scheduling",
"room amenities": "begun upgrading in-room amenities including new coffee machines and smart TVs",
"Wi-Fi": "upgraded our network infrastructure to provide faster and more reliable internet",
"noise": "initiated soundproofing improvements in corridors and between rooms",
"pricing": "started a comprehensive review of our pricing structure and value proposition",
"room service": "revised our in-room dining operations to improve delivery times",
"view": "scheduled window cleaning and tree trimming to maintain optimal views",
"fitness": "upgraded gym equipment based on guest feedback about variety"
}
# ===== MODEL LOADING =====
@st.cache_resource
def load_sentiment_model():
model = AutoModelForSequenceClassification.from_pretrained("smtsead/fine_tuned_bertweet_hotel")
tokenizer = AutoTokenizer.from_pretrained('finiteautomata/bertweet-base-sentiment-analysis')
return model, tokenizer
@st.cache_resource
def load_aspect_classifier():
return pipeline("zero-shot-classification", model="MoritzLaurer/deberta-v3-base-zeroshot-v1.1-all-33")
# ===== CORE FUNCTIONS =====
def analyze_sentiment(text, model, tokenizer):
inputs = tokenizer(text, padding=True, truncation=True, max_length=512, return_tensors='pt')
with torch.no_grad():
outputs = model(**inputs)
probs = torch.nn.functional.softmax(outputs.logits, dim=-1)
predicted_label = torch.argmax(probs).item()
confidence = torch.max(probs).item()
return {
'label': predicted_label,
'confidence': f"{confidence:.0%}",
'sentiment': 'POSITIVE' if predicted_label else 'NEGATIVE'
}
def detect_aspects(text, aspect_classifier):
relevant_aspects = []
text_lower = text.lower()
for aspect, keywords in aspect_map.items():
if any(re.search(rf'\b{kw}\b', text_lower) for kw in keywords):
relevant_aspects.append(aspect)
if relevant_aspects:
result = aspect_classifier(
text,
candidate_labels=relevant_aspects,
multi_label=True,
hypothesis_template="This review discusses the hotel's {}."
)
return [(aspect, f"{score:.0%}") for aspect, score in
zip(result['labels'], result['scores']) if score > 0.6]
return []
def generate_response(sentiment, aspects, original_text):
# Personalization
guest_name = ""
name_match = re.search(r"(Mr\.|Ms\.|Mrs\.)\s(\w+)", original_text, re.IGNORECASE)
if name_match:
guest_name = f" {name_match.group(2)}"
# Staff name extraction
staff_name = ""
staff_match = re.search(r"(receptionist|manager|concierge|chef)\s(\w+)", original_text, re.IGNORECASE)
if staff_match:
staff_name = staff_match.group(2)
if sentiment['label'] == 1:
response = f"""Dear{guest_name if guest_name else ' Valued Guest'},
Thank you for choosing The Kimberley Hotel Hong Kong and for sharing your wonderful feedback!"""
# Add relevant aspect responses
added_aspects = set()
for aspect, _ in aspects:
if aspect in aspect_responses:
response_text = aspect_responses[aspect]
if "{staff_name}" in response_text and staff_name:
response_text = response_text.format(staff_name=staff_name)
response += "\n\n" + response_text
added_aspects.add(aspect)
if len(added_aspects) >= 3: # Limit to 3 main points
break
# Special offers
if "room" in added_aspects or "dining" in added_aspects:
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."
response += "\n\nWe look forward to welcoming you back to your home in Hong Kong!\n\nWarm regards,"
else:
response = f"""Dear{guest_name if guest_name else ' Guest'},
Thank you for your valuable feedback - we sincerely apologize that your experience didn't meet our usual high standards."""
# Add improvement actions
added_improvements = set()
for aspect, _ in aspects:
if aspect in improvement_actions:
response += f"\n\nRegarding your comments about the {aspect}, we've {improvement_actions[aspect]}."
added_improvements.add(aspect)
if len(added_improvements) >= 2: # Limit to 2 main improvements
break
# Recovery offer
recovery_offer = "\n\nTo make amends, we'd like to offer you:"
if "room" in added_improvements:
recovery_offer += "\n- One night complimentary room upgrade"
if "dining" in added_improvements:
recovery_offer += "\n- HKD 300 dining credit at our restaurants"
if not ("room" in added_improvements or "dining" in added_improvements):
recovery_offer += "\n- 15% discount on your next stay"
response += recovery_offer
response += "\n\nPlease contact our Guest Relations Manager Ms. Chan directly at [email protected] to arrange this."
response += "\n\nWe hope for another opportunity to provide you with the exceptional experience we're known for.\n\nSincerely,"
return response + "\nMichael Wong\nGuest Experience Manager\nThe Kimberley Hotel Hong Kong\n+852 1234 5678"
# ===== STREAMLIT UI =====
def main():
# Page Config
st.set_page_config(
page_title="Kimberley Review Assistant",
page_icon="🏨",
layout="centered"
)
# Custom CSS
st.markdown("""
<style>
.header {
color: #003366;
font-size: 28px;
font-weight: bold;
margin-bottom: 10px;
}
.subheader {
color: #666666;
font-size: 16px;
margin-bottom: 30px;
}
.badge {
background-color: #e6f2ff;
color: #003366;
padding: 3px 10px;
border-radius: 15px;
font-size: 14px;
display: inline-block;
margin: 0 5px 5px 0;
}
.char-counter {
font-size: 12px;
color: #666;
text-align: right;
margin-top: -15px;
margin-bottom: 15px;
}
.char-counter.warning {
color: #ff6b6b;
}
.result-box {
border-left: 4px solid #003366;
padding: 15px;
background-color: #f9f9f9;
margin: 20px 0;
border-radius: 0 8px 8px 0;
white-space: pre-wrap;
}
.aspect-badge {
background-color: #e6f2ff;
color: #003366;
padding: 2px 8px;
border-radius: 4px;
font-size: 14px;
display: inline-block;
margin: 2px;
}
</style>
""", unsafe_allow_html=True)
# Header
st.markdown('<div class="header">The Kimberley Hotel Hong Kong</div>', unsafe_allow_html=True)
st.markdown('<div class="subheader">Guest Review Analysis System</div>', unsafe_allow_html=True)
# Supported Languages
st.markdown("**Supported Review Languages:**")
lang_cols = st.columns(5)
for i, (code, name) in enumerate(SUPPORTED_LANGUAGES.items()):
lang_cols[i].markdown(f'<div class="badge">{name}</div>', unsafe_allow_html=True)
# Review Input with Character Counter
review = st.text_area("**Paste Guest Review:**",
height=250,
max_chars=MAX_CHARS,
placeholder=f"Enter review in any supported language (max {MAX_CHARS} characters)...",
key="review_input")
char_count = len(st.session_state.review_input) if 'review_input' in st.session_state else 0
char_class = "warning" if char_count > MAX_CHARS else ""
st.markdown(f'<div class="char-counter {char_class}">{char_count}/{MAX_CHARS} characters</div>',
unsafe_allow_html=True)
if st.button("Analyze & Generate Response", type="primary"):
if not review.strip():
st.error("Please enter a review")
return
if char_count > MAX_CHARS:
st.warning(f"Review truncated to {MAX_CHARS} characters for analysis")
review = review[:MAX_CHARS]
with st.spinner("Analyzing feedback..."):
# Load models
sentiment_model, tokenizer = load_sentiment_model()
aspect_classifier = load_aspect_classifier()
# Process review
sentiment = analyze_sentiment(review, sentiment_model, tokenizer)
aspects = detect_aspects(review, aspect_classifier)
response = generate_response(sentiment, aspects, review)
# Display results
st.divider()
# Sentiment and Aspects
col1, col2 = st.columns(2)
with col1:
st.markdown("### Sentiment Analysis")
sentiment_icon = "✅" if sentiment['label'] == 1 else "⚠️"
st.markdown(f"{sentiment_icon} **{sentiment['sentiment']}**")
st.caption(f"Confidence level: {sentiment['confidence']}")
with col2:
st.markdown("### Key Aspects Detected")
if aspects:
for aspect, score in sorted(aspects, key=lambda x: float(x[1][:-1]), reverse=True):
st.markdown(f'<div class="aspect-badge">{aspect} ({score})</div>', unsafe_allow_html=True)
else:
st.markdown("_No specific aspects detected_")
# Generated Response
st.divider()
st.markdown("### Draft Response")
st.markdown(f'<div class="result-box">{response}</div>', unsafe_allow_html=True)
# Copy button
if st.button("Copy Response to Clipboard"):
st.session_state.copied = True
st.rerun()
if st.session_state.get("copied", False):
st.success("Response copied to clipboard!")
st.session_state.copied = False
if __name__ == "__main__":
main() |