Spaces:
Sleeping
Sleeping
| """ | |
| Hotel Review Analysis and Response System | |
| ISOM5240 Group Project | |
| Automatically analyzes hotel guest reviews in multiple languages, performs sentiment | |
| analysis and aspect detection, then generates professional responses. | |
| """ | |
| import streamlit as st | |
| from transformers import ( | |
| pipeline, | |
| AutoModelForSequenceClassification, | |
| AutoTokenizer | |
| ) | |
| import torch | |
| import re | |
| from langdetect import detect | |
| # ===== CONSTANTS ===== | |
| MAX_CHARS = 1000 # Character limit for reviews | |
| # Supported languages with their display names | |
| SUPPORTED_LANGUAGES = { | |
| 'en': 'English', | |
| 'zh': 'Chinese', | |
| 'ja': 'Japanese', | |
| 'ko': 'Korean', | |
| 'fr': 'French', | |
| 'de': 'German' | |
| } | |
| # ===== 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 major attractions.", | |
| "view": "It's wonderful to hear you appreciated the stunning views of Victoria Harbour from your room.", | |
| "room comfort": "Our team is thrilled you found your room comfortable and well-appointed for your needs.", | |
| "room cleanliness": "Your commendation of our cleanliness standards means a great deal to our housekeeping team who work diligently to maintain our high standards.", | |
| "staff service": "Your kind words about our team have been shared with them and are greatly appreciated.", | |
| "reception": "We're pleased our front desk team made your arrival and departure experience seamless and welcoming.", | |
| "spa": "Our spa practitioners will be delighted you enjoyed their treatments and the relaxing ambiance of our wellness center.", | |
| "pool": "We're glad you had a refreshing time at our rooftop pool with its panoramic city views.", | |
| "dining": "Thank you for appreciating our culinary offerings - we've shared your compliments with our executive chef and culinary team.", | |
| "concierge": "We're happy our concierge could enhance your stay with their local knowledge and personalized recommendations.", | |
| "fitness": "It's great to hear you made use of our 24-hour fitness center with its modern equipment.", | |
| "room service": "We're pleased our in-room dining met your expectations for both quality and timely service.", | |
| "parking": "We're glad our valet parking service provided convenience during your stay with us.", | |
| "bathroom": "We appreciate your feedback about our bathroom amenities and the cleanliness of your facilities.", | |
| "bar": "Thank you for your comments about our bar service and the selection of beverages available in our lounge.", | |
| "housekeeping": "Your positive feedback about our housekeeping service has been shared with the entire team.", | |
| "Wi-Fi": "We're pleased our high-speed internet service met your connectivity needs throughout the property.", | |
| "elevator": "We're glad our elevator service provided convenient access to all areas of the hotel during your stay." | |
| } | |
| improvement_actions = { | |
| "AC": "completed a comprehensive inspection and maintenance of all air conditioning units", | |
| "housekeeping": "conducted additional training for our housekeeping team and adjusted cleaning schedules", | |
| "bathroom": "performed deep cleaning and maintenance on all bathroom facilities", | |
| "parking": "implemented enhanced key management protocols with our valet service team", | |
| "dining": "reviewed our menu pricing and quality standards with the culinary leadership team", | |
| "reception": "provided additional customer service training to our front desk associates", | |
| "elevator": "completed full servicing and testing of all elevator systems", | |
| "room amenities": "begun upgrading in-room amenities based on recent guest feedback", | |
| "noise": "initiated soundproofing improvements in identified high-traffic areas", | |
| "pricing": "commenced a comprehensive review of our pricing structure and value proposition", | |
| "Wi-Fi": "begun upgrading our network infrastructure to enhance connectivity", | |
| "bar": "reviewed our beverage service procedures and inventory management", | |
| "staff service": "implemented additional staff training programs focusing on guest interactions", | |
| "room service": "optimized our food delivery processes to improve efficiency", | |
| "fitness": "scheduled upgrades to our gym equipment based on guest preferences" | |
| } | |
| # ===== MODEL CONFIGURATION ===== | |
| TRANSLATION_MODELS = { | |
| # Translations to English | |
| 'zh-en': 'Helsinki-NLP/opus-mt-zh-en', | |
| 'ja-en': 'Helsinki-NLP/opus-mt-ja-en', | |
| 'ko-en': 'Helsinki-NLP/opus-mt-ko-en', | |
| 'fr-en': 'Helsinki-NLP/opus-mt-fr-en', | |
| 'de-en': 'Helsinki-NLP/opus-mt-de-en', | |
| # Translations from English | |
| 'en-zh': 'Helsinki-NLP/opus-mt-en-zh', | |
| 'en-ja': 'Helsinki-NLP/opus-mt-en-ja', | |
| 'en-ko': 'Helsinki-NLP/opus-mt-en-ko', | |
| 'en-fr': 'Helsinki-NLP/opus-mt-en-fr', | |
| 'en-de': 'Helsinki-NLP/opus-mt-en-de' | |
| } | |
| # ===== MODEL LOADING ===== | |
| 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 | |
| def load_aspect_classifier(): | |
| return pipeline("zero-shot-classification", model="MoritzLaurer/deberta-v3-base-zeroshot-v1.1-all-33") | |
| def load_translation_model(src_lang, target_lang='en'): | |
| model_key = f"{src_lang}-{target_lang}" | |
| if model_key not in TRANSLATION_MODELS: | |
| raise ValueError(f"Unsupported translation: {src_lang}→{target_lang}") | |
| return pipeline("translation", model=TRANSLATION_MODELS[model_key]) | |
| # ===== CORE FUNCTIONS ===== | |
| def detect_language(text): | |
| try: | |
| lang = detect(text) | |
| return 'zh' if lang in ['zh', 'yue'] else lang if lang in SUPPORTED_LANGUAGES else 'en' | |
| except: | |
| return 'en' | |
| def translate_text(text, src_lang, target_lang='en'): | |
| try: | |
| if src_lang == target_lang: | |
| return {'translation': text, 'source_lang': src_lang} | |
| translator = load_translation_model(src_lang, target_lang) | |
| result = translator(text)[0]['translation_text'] | |
| return {'translation': result, 'source_lang': src_lang} | |
| except Exception as e: | |
| return {'error': str(e)} | |
| 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 - only extract guest name | |
| 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)}" | |
| if sentiment['label'] == 1: | |
| response = f"""Dear{guest_name if guest_name else ' Valued Guest'}, | |
| Thank you for choosing our hotel and for sharing your kind feedback with us.""" | |
| # Add relevant aspect responses | |
| added_aspects = set() | |
| for aspect, _ in sorted(aspects, key=lambda x: float(x[1][:-1]), reverse=True): | |
| if aspect in aspect_responses: | |
| response_text = aspect_responses[aspect] | |
| response += "\n\n" + response_text | |
| added_aspects.add(aspect) | |
| if len(added_aspects) >= 3: | |
| break | |
| response += "\n\nWe look forward to welcoming you back for another memorable stay." | |
| else: | |
| response = f"""Dear{guest_name if guest_name else ' Guest'}, | |
| Thank you for taking the time to share your feedback with us. We sincerely regret that your experience did not meet your expectations.""" | |
| # Add improvement actions | |
| added_improvements = set() | |
| improvement_text = "" | |
| for aspect, _ in sorted(aspects, key=lambda x: float(x[1][:-1]), reverse=True): | |
| if aspect in improvement_actions: | |
| improvement_text += f"\n- Regarding the {aspect}, we have {improvement_actions[aspect]}" | |
| added_improvements.add(aspect) | |
| if len(added_improvements) >= 2: | |
| break | |
| if improvement_text: | |
| response += "\n\nTo address your concerns:" + improvement_text | |
| response += "\n\nYour feedback is invaluable to us as we strive to improve our services." | |
| # Common closing | |
| response += """ | |
| Should you require any further assistance, please don't hesitate to contact our Guest Relations team. | |
| Sincerely yours, | |
| Guest Relations Team | |
| The Mira Hong Kong | |
| +852 1234 5678 | [email protected]""" | |
| return response | |
| # ===== STREAMLIT UI ===== | |
| def main(): | |
| st.set_page_config( | |
| page_title="Hotel Review Analysis and Response System", | |
| page_icon="🏨", | |
| layout="centered" | |
| ) | |
| st.markdown(""" | |
| <style> | |
| .header { color: #003366; font-size: 28px; font-weight: bold; margin-bottom: 10px; } | |
| .subheader { color: #666666; font-size: 16px; margin-bottom: 30px; } | |
| .char-counter { font-size: 12px; color: #666; text-align: right; margin-top: -15px; } | |
| .char-counter.warning { color: #ff6b6b; } | |
| .result-box { border-left: 4px solid #003366; padding: 15px; background-color: #f9f9f9; margin: 20px 0; } | |
| .aspect-badge { background-color: #e6f2ff; padding: 2px 8px; border-radius: 4px; display: inline-block; margin: 2px; } | |
| .response-box { white-space: pre-wrap; font-family: monospace; } | |
| .english-response { color: #555555; font-size: 14px; } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| st.markdown('<div class="header">Hotel Review Analysis and Response System</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="subheader">The Mira Hong Kong</div>', unsafe_allow_html=True) | |
| review = st.text_area("**Paste Guest Review:**", | |
| height=200, | |
| max_chars=MAX_CHARS, | |
| placeholder=f"Enter review (max {MAX_CHARS} characters)...", | |
| key="review_input") | |
| char_count = len(st.session_state.review_input) if 'review_input' in st.session_state else 0 | |
| st.markdown(f'<div class="char-counter{" warning" if char_count > MAX_CHARS else ""}">{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") | |
| review = review[:MAX_CHARS] | |
| with st.spinner("Analyzing feedback..."): | |
| try: | |
| # Auto-detect language | |
| review_lang = detect_language(review) | |
| st.info(f"Detected language: {SUPPORTED_LANGUAGES.get(review_lang, 'English')}") | |
| # Translate if not English | |
| if review_lang != 'en': | |
| translation = translate_text(review, review_lang, 'en') | |
| if 'error' in translation: | |
| st.error(f"Translation error: {translation['error']}") | |
| return | |
| analysis_text = translation['translation'] | |
| with st.expander("View Translation"): | |
| st.write("**Original Review:**") | |
| st.write(review) | |
| st.write("**English Translation:**") | |
| st.write(translation['translation']) | |
| else: | |
| analysis_text = review | |
| # Analyze text | |
| sentiment_model, tokenizer = load_sentiment_model() | |
| aspect_classifier = load_aspect_classifier() | |
| sentiment = analyze_sentiment(analysis_text, sentiment_model, tokenizer) | |
| aspects = detect_aspects(analysis_text, aspect_classifier) | |
| response = generate_response(sentiment, aspects, review) | |
| # Translate response back if needed | |
| if review_lang != 'en': | |
| translation_back = translate_text(response, 'en', review_lang) | |
| final_response = translation_back['translation'] if 'error' not in translation_back else response | |
| else: | |
| final_response = response | |
| # Display results | |
| st.divider() | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown("### Sentiment Analysis") | |
| st.markdown(f"{'✅' if sentiment['label'] == 1 else '⚠️'} **{sentiment['sentiment']}**") | |
| st.caption(f"Confidence: {sentiment['confidence']}") | |
| with col2: | |
| st.markdown("### Key Aspects") | |
| 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_") | |
| st.divider() | |
| st.markdown("### Draft Response") | |
| # Show English response if original language wasn't English | |
| if review_lang != 'en': | |
| st.markdown('<div class="english-response">English version:</div>', unsafe_allow_html=True) | |
| st.markdown(f'<div class="result-box"><div class="response-box">{response}</div></div>', | |
| unsafe_allow_html=True) | |
| st.markdown('<div class="english-response">Translated version:</div>', unsafe_allow_html=True) | |
| # Show final response (translated if needed) | |
| st.markdown(f'<div class="result-box"><div class="response-box">{final_response}</div></div>', | |
| unsafe_allow_html=True) | |
| except Exception as e: | |
| st.error(f"An error occurred: {str(e)}") | |
| if __name__ == "__main__": | |
| main() |