Spaces:
Running
Running
| # app.py | |
| import streamlit as st | |
| import json | |
| import os | |
| import requests | |
| import re | |
| import time | |
| from datetime import datetime | |
| from ddgs import DDGS | |
| import isbnlib | |
| from dotenv import load_dotenv | |
| from requests.exceptions import HTTPError | |
| from huggingface_hub import InferenceClient | |
| import languages # استيراد وحدة الترجمة | |
| load_dotenv() | |
| # استيراد الملفات المحلية | |
| try: | |
| from nlm_classifier import AdvancedNLMClassifier | |
| from metadata_fetcher import EnhancedMetadataFetcher | |
| from cover_fetcher import EnhancedCoverFetcher | |
| from marc_generator import ( | |
| build_marc_record, | |
| generate_marc_iso2709, | |
| generate_marcxml | |
| ) | |
| except ImportError as e: | |
| st.error(f"Error importing modules: {e}") | |
| # واجهات افتراضية في حال فشل التحميل | |
| class AdvancedNLMClassifier: | |
| def classify_with_confidence(self, title, summary="", categories=None): | |
| return {'nlm_code': 'W 1', 'confidence_level': 'Low'} | |
| class EnhancedMetadataFetcher: | |
| def fetch_metadata(self, isbn): | |
| return {'title': 'Unknown', 'published_date': '', 'publisher': ''} | |
| class EnhancedCoverFetcher: | |
| def get_cover(self, isbn, title=None): | |
| return {'url': f"https://placehold.co/400x600?text=ISBN:{isbn}", 'status': 'fallback'} | |
| def build_marc_record(data, isbn): | |
| return ["001 ## $a ISBN" + isbn] | |
| def generate_marc_iso2709(marc_fields, isbn): | |
| return {'iso2709': '', 'human_readable': '', 'fields_count': 0, 'record_length': 0} | |
| def generate_marcxml(marc_fields, isbn): | |
| return "<?xml version='1.0'?>" | |
| # --- 1. SETUP & AUTHENTICATION --- | |
| hf_token = os.getenv("HF_TOKEN") | |
| USE_AI = os.getenv("USE_AI", "true").lower() == "true" | |
| st.set_page_config( | |
| page_title="Medical AI Librarian", | |
| layout="wide", | |
| page_icon="🏥", | |
| initial_sidebar_state="collapsed" | |
| ) | |
| # تهيئة حالة اللغة | |
| if 'language' not in st.session_state: | |
| st.session_state.language = 'en' # افتراضي إنجليزي | |
| lang = st.session_state.language | |
| direction = languages.get_language_direction(lang) | |
| # تطبيق اتجاه الصفحة عبر CSS مع تصحيح الأقواس | |
| st.markdown(f""" | |
| <style> | |
| .stApp {{ direction: {direction}; }} | |
| { 'p, div, h1, h2, h3, h4, h5, h6, span {{ text-align: right; }}' if direction == 'rtl' else '' } | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # تهيئة عميل Hugging Face بنموذج مجاني | |
| if hf_token and USE_AI: | |
| try: | |
| client = InferenceClient(model="mistralai/Mistral-7B-Instruct-v0.2", token=hf_token) | |
| st.sidebar.success(languages.get_text("ai_connected", lang)) | |
| except Exception as e: | |
| st.sidebar.warning(languages.get_text("ai_error", lang).format(str(e))) | |
| client = None | |
| else: | |
| if not hf_token: | |
| st.sidebar.info(languages.get_text("no_hf_token", lang)) | |
| client = None | |
| # --- 2. CSS STYLING (باقي الأنماط) --- | |
| st.markdown(f""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Source+Serif+Pro:wght@400;600&display=swap'); | |
| * {{ font-family: 'Inter', sans-serif; }} | |
| .stApp {{ background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); min-height: 100vh; direction: {direction}; }} | |
| .modern-card {{ | |
| background: rgba(255, 255, 255, 0.95); | |
| backdrop-filter: blur(10px); | |
| border-radius: 20px; | |
| padding: 25px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| margin-bottom: 20px; | |
| transition: all 0.3s ease; | |
| }} | |
| .modern-card:hover {{ | |
| transform: translateY(-5px); | |
| box-shadow: 0 15px 40px rgba(0, 0, 0, 0.12); | |
| }} | |
| .main-header {{ | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| padding: 40px; | |
| border-radius: 20px; | |
| color: white; | |
| margin-bottom: 30px; | |
| text-align: center; | |
| box-shadow: 0 10px 30px rgba(102, 126, 234, 0.3); | |
| }} | |
| .main-title {{ | |
| font-size: 2.8rem; | |
| font-weight: 800; | |
| margin-bottom: 10px; | |
| letter-spacing: -0.5px; | |
| }} | |
| .main-subtitle {{ | |
| font-size: 1.1rem; | |
| opacity: 0.9; | |
| font-weight: 300; | |
| }} | |
| .status-pill {{ | |
| display: inline-block; | |
| padding: 6px 16px; | |
| border-radius: 50px; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| margin-right: 10px; | |
| margin-bottom: 10px; | |
| }} | |
| .status-high {{ | |
| background: linear-gradient(135deg, #34D399, #10B981); | |
| color: white; | |
| }} | |
| .status-medium {{ | |
| background: linear-gradient(135deg, #FBBF24, #F59E0B); | |
| color: white; | |
| }} | |
| .status-low {{ | |
| background: linear-gradient(135deg, #F87171, #EF4444); | |
| color: white; | |
| }} | |
| .tag {{ | |
| display: inline-block; | |
| background: linear-gradient(135deg, #E0E7FF, #C7D2FE); | |
| color: #3730A3; | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| margin: 5px 5px 5px 0; | |
| }} | |
| .metric-value {{ | |
| font-size: 2.5rem; | |
| font-weight: 800; | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: 5px; | |
| }} | |
| .metric-label {{ | |
| font-size: 0.9rem; | |
| color: #666; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| }} | |
| .footer {{ | |
| text-align: center; | |
| margin-top: 50px; | |
| padding: 20px; | |
| color: #666; | |
| font-size: 0.9rem; | |
| border-top: 1px solid rgba(0, 0, 0, 0.1); | |
| }} | |
| .nlm-code {{ | |
| font-family: 'Courier New', monospace; | |
| font-size: 1.2rem; | |
| font-weight: bold; | |
| color: #2563eb; | |
| background: #f0f9ff; | |
| padding: 10px 15px; | |
| border-radius: 8px; | |
| border-left: 4px solid #2563eb; | |
| }} | |
| .confidence-high {{ color: #059669; font-weight: 600; }} | |
| .confidence-medium {{ color: #d97706; font-weight: 600; }} | |
| .confidence-low {{ color: #dc2626; font-weight: 600; }} | |
| .book-cover-container {{ | |
| border-radius: 15px; | |
| overflow: hidden; | |
| box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15); | |
| transition: all 0.3s ease; | |
| margin-bottom: 20px; | |
| }} | |
| .book-cover-container:hover {{ | |
| transform: scale(1.02); | |
| }} | |
| .source-badge {{ | |
| display: inline-block; | |
| background: #e0f2fe; | |
| color: #0369a1; | |
| padding: 4px 8px; | |
| border-radius: 12px; | |
| font-size: 0.75rem; | |
| margin-left: 5px; | |
| }} | |
| /* تعديلات RTL */ | |
| {''' | |
| .rtl .stTextInput label, .rtl .stButton button { text-align: right; } | |
| .rtl .metric-value, .rtl .metric-label { text-align: right; } | |
| ''' if direction == 'rtl' else ''} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # --- 3. FUNCTIONS --- | |
| def clean_isbn(isbn_str): | |
| """تنظيف وتصحيح ISBN""" | |
| cleaned = re.sub(r'[^0-9X]', '', isbn_str.upper()) | |
| try: | |
| if len(cleaned) == 10: | |
| if not isbnlib.is_isbn10(cleaned): | |
| return None | |
| elif len(cleaned) == 13: | |
| if not isbnlib.is_isbn13(cleaned): | |
| return None | |
| else: | |
| return None | |
| except: | |
| pass | |
| return cleaned | |
| def search_web_context(isbn, title=None): | |
| """بحث في الويب باستخدام ddgs""" | |
| queries = [] | |
| if title: | |
| queries.extend([ | |
| f"{title} medical book publication date edition", | |
| f"{title} review summary table of contents", | |
| f"{title} medical textbook target audience" | |
| ]) | |
| queries.extend([ | |
| f"ISBN {isbn} publication details", | |
| f"{isbn} medical book specifications", | |
| f"medical library catalog {isbn}" | |
| ]) | |
| all_results = [] | |
| try: | |
| with DDGS() as ddgs: | |
| for query in queries[:4]: | |
| results = list(ddgs.text(query, max_results=3)) | |
| for result in results: | |
| all_results.append({ | |
| 'title': result.get('title', ''), | |
| 'snippet': result.get('body', ''), | |
| 'url': result.get('href', '') | |
| }) | |
| except Exception as e: | |
| st.sidebar.warning(f"Web search limited: {str(e)}") | |
| return all_results if all_results else [] | |
| def enhanced_ai_librarian_analysis(isbn, meta_data, web_context): | |
| """تحليل محسن مع دقة عالية""" | |
| lang = st.session_state.language # استخدام اللغة الحالية | |
| if not client or not USE_AI: | |
| st.info(languages.get_text("local_classification_mode", lang)) | |
| return fallback_local_classification(isbn, meta_data, web_context, lang) | |
| metadata_fetcher = EnhancedMetadataFetcher() | |
| enhanced_meta = metadata_fetcher.fetch_metadata(isbn) | |
| api_title = enhanced_meta.get('title', '') | |
| summary_context = enhanced_meta.get('description', '') | |
| categories = enhanced_meta.get('categories', []) | |
| web_context_text = " ".join([r.get('snippet', '') for r in web_context[:3]]) | |
| nlm_classifier = AdvancedNLMClassifier() | |
| nlm_result = nlm_classifier.classify_with_confidence( | |
| api_title, | |
| f"{summary_context} {web_context_text}", | |
| categories | |
| ) | |
| system_prompt = """You are a Senior Medical Cataloging Expert with specialization in NLM classification. | |
| IMPORTANT GUIDELINES: | |
| 1. Analyze the book's PRIMARY subject focus, not secondary topics | |
| 2. Consider the intended audience and purpose | |
| 3. For medical textbooks, use W 18-W 20 range | |
| 4. For clinical guides, use appropriate WB-WZ codes | |
| 5. For basic sciences, use QS-QZ series | |
| 6. Provide specific, not generic, classifications | |
| 7. Include detailed reasoning for your choice | |
| ALWAYS verify the classification matches the book's main content.""" | |
| user_prompt = f"""Please provide precise cataloging information for this medical book: | |
| ISBN: {isbn} | |
| Title: {api_title} | |
| Publication Year: {enhanced_meta.get('published_date', 'Unknown')} | |
| Publisher: {enhanced_meta.get('publisher', 'Unknown')} | |
| Subjects/Categories: {', '.join(categories) if categories else 'None'} | |
| Description: {summary_context[:300]} | |
| Additional Context: {web_context_text[:300]} | |
| NLM Classification Analysis from our system: | |
| - Suggested Code: {nlm_result['nlm_code']} | |
| - Confidence: {nlm_result['confidence_level']} | |
| - Reason: {nlm_result['confidence_reason']} | |
| Please provide your professional assessment in this JSON format: | |
| {{ | |
| "title": "Complete title", | |
| "sub_title": "Subtitle if available", | |
| "authors": ["Author list"], | |
| "edition": "Edition information", | |
| "publisher": "Publisher name", | |
| "pub_year": "YYYY (extracted accurately)", | |
| "pages": "Number of pages", | |
| "isbn_10": "ISBN-10", | |
| "isbn_13": "ISBN-13", | |
| "summary": "Comprehensive 200-word summary", | |
| "contents_note": "Detailed table of contents", | |
| "audience_category": "Specific audience description", | |
| "audience_reason": "Justification", | |
| "mesh_subjects": ["Relevant MeSH terms"], | |
| "nlm_class": "YOUR PROFESSIONAL NLM CLASSIFICATION", | |
| "nlm_class_reason": "Detailed reasoning based on content analysis", | |
| "nlm_class_confidence": "High/Medium/Low", | |
| "acquisition_decision": "Highly Recommended/Recommended/Optional/Not Recommended", | |
| "acquisition_reason": "Collection development reasoning", | |
| "quality_score": "1-10 based on authority and relevance", | |
| "data_accuracy": "High/Medium/Low based on available information" | |
| }} | |
| CRITICAL: The NLM classification must be accurate and specific to the main subject.""" | |
| try: | |
| messages = [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_prompt} | |
| ] | |
| response = client.chat_completion( | |
| messages, | |
| max_tokens=2500, | |
| temperature=0.1, | |
| top_p=0.9 | |
| ) | |
| content = response.choices[0].message.content | |
| if "```json" in content: | |
| content = content.split("```json")[1].split("```")[0] | |
| elif "```" in content: | |
| content = content.split("```")[1].split("```")[0] | |
| data = json.loads(content.strip()) | |
| # دمج نتائج NLM المحلية | |
| data['nlm_class'] = nlm_result['nlm_code'] | |
| data['nlm_class_ai_reason'] = data.get('nlm_class_reason', '') | |
| data['nlm_class_local_reason'] = nlm_result['confidence_reason'] | |
| data['nlm_class_confidence'] = nlm_result['confidence_level'] | |
| data['nlm_class_score'] = nlm_result['confidence_score'] | |
| data['nlm_alternatives'] = nlm_result.get('alternative_codes', []) | |
| data['metadata_source'] = enhanced_meta.get('source', 'Multiple') | |
| data['metadata_confidence'] = enhanced_meta.get('confidence', 'Unknown') | |
| if enhanced_meta.get('published_date'): | |
| data['pub_year'] = enhanced_meta['published_date'] | |
| data['pub_year_source'] = enhanced_meta.get('source', 'API') | |
| data['pub_year_confidence'] = enhanced_meta.get('confidence', 'Unknown') | |
| if not isinstance(data.get('authors'), list): | |
| if enhanced_meta and enhanced_meta.get('authors'): | |
| data['authors'] = enhanced_meta['authors'] | |
| else: | |
| data['authors'] = [] | |
| data['illustrations_note'] = 'illustrations (chiefly color), portraits' | |
| data['series'] = 'Medical education series' if 'textbook' in data.get('title', '').lower() else '' | |
| data['institution'] = enhanced_meta.get('institution', '') | |
| if not data.get('pages') and enhanced_meta.get('page_count'): | |
| page_count = enhanced_meta['page_count'] | |
| if isinstance(page_count, int): | |
| if page_count > 100: | |
| data['pages'] = f"xvi, {page_count}" | |
| else: | |
| data['pages'] = str(page_count) | |
| return data | |
| except HTTPError as e: | |
| if e.response.status_code == 402: | |
| st.warning(languages.get_text("ai_payment_warning", lang)) | |
| else: | |
| st.error(languages.get_text("ai_unexpected_error", lang).format(e)) | |
| return fallback_local_classification(isbn, enhanced_meta, web_context, lang) | |
| except Exception as e: | |
| st.error(languages.get_text("ai_unexpected_error", lang).format(str(e))) | |
| return fallback_local_classification(isbn, enhanced_meta, web_context, lang) | |
| def fallback_local_classification(isbn, enhanced_meta, web_context, lang='en'): | |
| """تصنيف محلي متقدم مع بيانات افتراضية ذكية وقابلية للترجمة""" | |
| nlm_classifier = AdvancedNLMClassifier() | |
| nlm_result = nlm_classifier.classify_with_confidence( | |
| enhanced_meta.get('title', ''), | |
| enhanced_meta.get('description', ''), | |
| enhanced_meta.get('categories', []) | |
| ) | |
| title = enhanced_meta.get('title', 'Unknown Title') | |
| description = enhanced_meta.get('description', '') | |
| authors = enhanced_meta.get('authors', []) | |
| publisher = enhanced_meta.get('publisher', 'Unknown') | |
| pub_year = enhanced_meta.get('published_date', '') | |
| # توليد ملخص ذكي (يبقى بالإنجليزية) | |
| if description and len(description) > 20: | |
| summary = description[:500] + ('...' if len(description) > 500 else '') | |
| else: | |
| summary = f"This book, '{title}', is a medical publication focusing on {nlm_result['nlm_name']}. " \ | |
| f"It is intended for healthcare professionals and students. " \ | |
| f"Published by {publisher} in {pub_year if pub_year else 'unknown year'}. " \ | |
| f"The work covers key concepts in {nlm_result['nlm_description'].lower() if nlm_result['nlm_description'] else 'medicine'}." | |
| # توليد محتويات افتراضية بناءً على التصنيف (تبقى بالإنجليزية) | |
| main_topic = nlm_result.get('nlm_name', 'Medicine') | |
| contents_templates = { | |
| 'Textbook': [ | |
| "1. Introduction to the field", | |
| "2. Fundamental principles", | |
| "3. Clinical applications", | |
| "4. Diagnostic approaches", | |
| "5. Therapeutic interventions", | |
| "6. Case studies", | |
| "7. Emerging trends", | |
| "8. Review questions" | |
| ], | |
| 'Surgery': [ | |
| "1. Preoperative assessment", | |
| "2. Surgical anatomy", | |
| "3. Operative techniques", | |
| "4. Postoperative care", | |
| "5. Complications and management", | |
| "6. Minimally invasive surgery", | |
| "7. Surgical outcomes" | |
| ], | |
| 'Cardiology': [ | |
| "1. Cardiac anatomy and physiology", | |
| "2. Diagnostic imaging", | |
| "3. Ischemic heart disease", | |
| "4. Heart failure", | |
| "5. Arrhythmias", | |
| "6. Valvular disorders", | |
| "7. Pharmacotherapy", | |
| "8. Interventional cardiology" | |
| ], | |
| 'Neurology': [ | |
| "1. Neuroanatomy", | |
| "2. Neurological examination", | |
| "3. Stroke and cerebrovascular disease", | |
| "4. Epilepsy", | |
| "5. Neurodegenerative disorders", | |
| "6. Headache and pain", | |
| "7. Neuromuscular disorders" | |
| ], | |
| 'Pediatrics': [ | |
| "1. Growth and development", | |
| "2. Neonatal care", | |
| "3. Pediatric infectious diseases", | |
| "4. Childhood immunizations", | |
| "5. Pediatric emergencies", | |
| "6. Adolescent medicine" | |
| ] | |
| } | |
| contents_note = [] | |
| for key, template in contents_templates.items(): | |
| if key.lower() in main_topic.lower(): | |
| contents_note = template | |
| break | |
| if not contents_note: | |
| contents_note = [ | |
| "1. Introduction", | |
| "2. Core concepts", | |
| "3. Clinical relevance", | |
| "4. Diagnostic methods", | |
| "5. Treatment strategies", | |
| "6. Patient management", | |
| "7. Future directions", | |
| "8. Review and self-assessment" | |
| ] | |
| # توليد موضوعات MeSH مع ترجمة إذا وجدت | |
| mesh_mapping = { | |
| 'W 18': [ | |
| languages.get_text("mesh_education_medical", lang), | |
| languages.get_text("mesh_textbooks", lang), | |
| languages.get_text("mesh_curriculum", lang) | |
| ], | |
| 'WB 100': [ | |
| languages.get_text("mesh_clinical_medicine", lang), | |
| languages.get_text("mesh_diagnosis", lang), | |
| languages.get_text("mesh_therapeutics", lang) | |
| ], | |
| 'WB 105': [ | |
| languages.get_text("mesh_emergency_medicine", lang), | |
| languages.get_text("mesh_traumatology", lang), | |
| languages.get_text("mesh_critical_care", lang) | |
| ], | |
| 'WO 100': [ | |
| languages.get_text("mesh_general_surgery", lang), | |
| languages.get_text("mesh_surgical_procedures", lang) | |
| ], | |
| 'WS 1': [ | |
| languages.get_text("mesh_pediatrics", lang), | |
| languages.get_text("mesh_child_development", lang), | |
| languages.get_text("mesh_adolescent_medicine", lang) | |
| ], | |
| 'WG': [ | |
| languages.get_text("mesh_cardiology", lang), | |
| languages.get_text("mesh_cardiovascular_diseases", lang), | |
| languages.get_text("mesh_heart_diseases", lang) | |
| ], | |
| 'WL': [ | |
| languages.get_text("mesh_neurology", lang), | |
| languages.get_text("mesh_nervous_system_diseases", lang), | |
| languages.get_text("mesh_brain", lang) | |
| ], | |
| 'QS 1': [ | |
| languages.get_text("mesh_anatomy", lang), | |
| languages.get_text("mesh_dissection", lang), | |
| languages.get_text("mesh_embryology", lang) | |
| ], | |
| 'QV 1': [ | |
| languages.get_text("mesh_pharmacology", lang), | |
| languages.get_text("mesh_pharmaceutical_preparations", lang), | |
| languages.get_text("mesh_drug_therapy", lang) | |
| ], | |
| 'QZ 4': [ | |
| languages.get_text("mesh_pathology", lang), | |
| languages.get_text("mesh_disease", lang), | |
| languages.get_text("mesh_clinical_pathology", lang) | |
| ], | |
| 'WY 100': [ | |
| languages.get_text("mesh_nursing_care", lang), | |
| languages.get_text("mesh_nursing_process", lang), | |
| languages.get_text("mesh_clinical_nursing_research", lang) | |
| ], | |
| 'WA 1': [ | |
| languages.get_text("mesh_public_health", lang), | |
| languages.get_text("mesh_preventive_medicine", lang), | |
| languages.get_text("mesh_epidemiology", lang) | |
| ], | |
| 'WM 1': [ | |
| languages.get_text("mesh_psychiatry", lang), | |
| languages.get_text("mesh_mental_disorders", lang), | |
| languages.get_text("mesh_psychotherapy", lang) | |
| ] | |
| } | |
| nlm_code = nlm_result['nlm_code'] | |
| mesh_subjects = [] | |
| for code_prefix, subjects in mesh_mapping.items(): | |
| if nlm_code.startswith(code_prefix) or code_prefix in nlm_code: | |
| mesh_subjects = subjects | |
| break | |
| if not mesh_subjects: | |
| mesh_subjects = [ | |
| languages.get_text("mesh_medicine", lang), | |
| languages.get_text("mesh_medical_sciences", lang), | |
| languages.get_text("mesh_health_occupations", lang) | |
| ] | |
| # تحديد الجمهور المستهدف مع الترجمة | |
| if 'Textbook' in nlm_result.get('nlm_name', ''): | |
| audience_category = languages.get_text("audience_undergraduate", lang) | |
| audience_reason = languages.get_text("audience_reason_textbook", lang) | |
| elif 'Education' in nlm_result.get('nlm_name', ''): | |
| audience_category = languages.get_text("audience_educators", lang) | |
| audience_reason = languages.get_text("audience_reason_education", lang) | |
| elif 'Surgery' in nlm_result.get('nlm_name', ''): | |
| audience_category = languages.get_text("audience_surgeons", lang) | |
| audience_reason = languages.get_text("audience_reason_surgery", lang) | |
| else: | |
| audience_category = languages.get_text("audience_medical_students", lang) | |
| audience_reason = languages.get_text("audience_reason_default", lang) | |
| # قرار الشراء مع الترجمة | |
| score = nlm_result['confidence_score'] | |
| if score >= 8: | |
| acquisition_decision = languages.get_text("decision_highly_recommended", lang) | |
| acquisition_reason = languages.get_text("acquisition_reason_high", lang) | |
| elif score >= 4: | |
| acquisition_decision = languages.get_text("decision_recommended", lang) | |
| acquisition_reason = languages.get_text("acquisition_reason_medium", lang) | |
| else: | |
| acquisition_decision = languages.get_text("decision_review_required", lang) | |
| acquisition_reason = languages.get_text("acquisition_reason_low", lang) | |
| return { | |
| 'title': title, | |
| 'sub_title': '', | |
| 'authors': authors, | |
| 'edition': enhanced_meta.get('edition', ''), | |
| 'publisher': publisher, | |
| 'pub_year': pub_year, | |
| 'pages': enhanced_meta.get('page_count', 'xii, 500'), | |
| 'isbn_10': enhanced_meta.get('isbn_10', ''), | |
| 'isbn_13': enhanced_meta.get('isbn_13', isbn), | |
| 'summary': summary, | |
| 'contents_note': contents_note, | |
| 'audience_category': audience_category, | |
| 'audience_reason': audience_reason, | |
| 'mesh_subjects': mesh_subjects, | |
| 'nlm_class': nlm_code, | |
| 'nlm_class_reason': f"Local classification: {nlm_result['confidence_reason']}", | |
| 'nlm_class_confidence': nlm_result['confidence_level'], | |
| 'nlm_class_score': score, | |
| 'acquisition_decision': acquisition_decision, | |
| 'acquisition_reason': acquisition_reason, | |
| 'quality_score': score // 2 if score > 0 else 5, | |
| 'data_accuracy': 'Medium', | |
| 'illustrations_note': 'illustrations', | |
| 'metadata_source': enhanced_meta.get('source', 'Local'), | |
| 'metadata_confidence': enhanced_meta.get('confidence', 'Medium') | |
| } | |
| # --- 4. MAIN UI --- | |
| if 'analysis_data' not in st.session_state: | |
| st.session_state.analysis_data = None | |
| if 'search_history' not in st.session_state: | |
| st.session_state.search_history = [] | |
| # --- HEADER --- | |
| st.markdown(f""" | |
| <div class="main-header"> | |
| <div class="main-title">🏥 {languages.get_text('app_title', lang)}</div> | |
| <div class="main-subtitle">{languages.get_text('app_subtitle', lang)}</div> | |
| </div> | |
| """, unsafe_allow_html=True) | |
| # --- MAIN INPUT AREA --- | |
| col1, col2, col3 = st.columns([2, 1, 1]) | |
| with col1: | |
| st.markdown(languages.get_text("enter_isbn", lang)) | |
| isbn_input = st.text_input( | |
| label="ISBN", | |
| placeholder=languages.get_text("isbn_placeholder", lang), | |
| label_visibility="collapsed", | |
| key="isbn_input" | |
| ) | |
| with col2: | |
| st.markdown("### ") | |
| analyze_btn = st.button( | |
| languages.get_text("analyze_button", lang), | |
| width='stretch', | |
| type="primary", | |
| key="analyze_btn" | |
| ) | |
| with col3: | |
| st.markdown("### ") | |
| if st.button( | |
| languages.get_text("view_stats_button", lang), | |
| width='stretch', | |
| key="stats_btn" | |
| ): | |
| st.switch_page("pages/statistics.py") | |
| # --- PROCESSING --- | |
| if analyze_btn and isbn_input: | |
| isbn_clean = clean_isbn(isbn_input) | |
| if not isbn_clean: | |
| st.error(languages.get_text("invalid_isbn", lang)) | |
| else: | |
| progress_bar = st.progress(0) | |
| status_text = st.empty() | |
| with st.spinner(languages.get_text("initializing", lang)): | |
| status_text.text(languages.get_text("fetching_metadata", lang)) | |
| metadata_fetcher = EnhancedMetadataFetcher() | |
| metadata = metadata_fetcher.fetch_metadata(isbn_clean) | |
| progress_bar.progress(25) | |
| time.sleep(0.5) | |
| status_text.text(languages.get_text("retrieving_cover", lang)) | |
| cover_fetcher = EnhancedCoverFetcher() | |
| cover_result = cover_fetcher.get_cover(isbn_clean, metadata.get('title')) | |
| progress_bar.progress(40) | |
| time.sleep(0.5) | |
| status_text.text(languages.get_text("searching_context", lang)) | |
| web_context = search_web_context(isbn_clean, metadata.get('title')) | |
| progress_bar.progress(60) | |
| time.sleep(0.5) | |
| status_text.text(languages.get_text("ai_analysis", lang)) | |
| ai_result = enhanced_ai_librarian_analysis(isbn_clean, metadata, web_context) | |
| progress_bar.progress(85) | |
| time.sleep(0.5) | |
| if ai_result: | |
| st.session_state.analysis_data = { | |
| 'metadata': metadata, | |
| 'cover_result': cover_result, | |
| 'web_context': web_context, | |
| 'ai_analysis': ai_result, | |
| 'isbn': isbn_clean, | |
| 'timestamp': datetime.now().isoformat() | |
| } | |
| st.session_state.search_history.append({ | |
| 'isbn': isbn_clean, | |
| 'title': ai_result.get('title', 'Unknown'), | |
| 'timestamp': datetime.now().isoformat(), | |
| 'nlm_class': ai_result.get('nlm_class', '') | |
| }) | |
| progress_bar.progress(100) | |
| status_text.text(languages.get_text("analysis_complete", lang)) | |
| time.sleep(1) | |
| st.rerun() | |
| else: | |
| st.error(languages.get_text("analysis_failed", lang)) | |
| progress_bar.empty() | |
| status_text.empty() | |
| # --- DISPLAY RESULTS --- | |
| if st.session_state.analysis_data: | |
| data = st.session_state.analysis_data | |
| if 'progress_bar' in locals(): | |
| progress_bar.empty() | |
| status_text.empty() | |
| tab1, tab2, tab3, tab4 = st.tabs([ | |
| languages.get_text("tab_overview", lang), | |
| languages.get_text("tab_details", lang), | |
| languages.get_text("tab_cataloging", lang), | |
| languages.get_text("tab_ai_insights", lang) | |
| ]) | |
| with tab1: | |
| col1, col2 = st.columns([1, 2]) | |
| with col1: | |
| st.markdown('<div class="book-cover-container">', unsafe_allow_html=True) | |
| st.image(data['cover_result']['url'], width='stretch') | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| if data['cover_result'].get('source'): | |
| st.caption(f"Cover source: {data['cover_result']['source']}") | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown(f"### {languages.get_text('quick_stats', lang)}") | |
| cols = st.columns(3) | |
| with cols[0]: | |
| authors_count = len(data['ai_analysis'].get('authors', [])) | |
| st.markdown(f'<div class="metric-value">{authors_count}</div>', unsafe_allow_html=True) | |
| st.markdown(f'<div class="metric-label">{languages.get_text("authors_label", lang)}</div>', unsafe_allow_html=True) | |
| with cols[1]: | |
| quality = data['ai_analysis'].get('quality_score', 5) | |
| st.markdown(f'<div class="metric-value">{quality}/10</div>', unsafe_allow_html=True) | |
| st.markdown(f'<div class="metric-label">{languages.get_text("quality_label", lang)}</div>', unsafe_allow_html=True) | |
| with cols[2]: | |
| year = data['ai_analysis'].get('pub_year', 'N/A') | |
| st.markdown(f'<div class="metric-value">{year}</div>', unsafe_allow_html=True) | |
| st.markdown(f'<div class="metric-label">{languages.get_text("year_label", lang)}</div>', unsafe_allow_html=True) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| with col2: | |
| ai = data['ai_analysis'] | |
| st.markdown(f"# {ai.get('title', 'Unknown Title')}") | |
| if ai.get('sub_title'): | |
| st.markdown(f"### *{ai['sub_title']}*") | |
| authors = ai.get('authors', []) | |
| if authors: | |
| st.markdown(f"**👥 {languages.get_text('authors_label', lang)}:** {', '.join(authors)}") | |
| pub_info = [] | |
| if ai.get('publisher'): | |
| pub_info.append(ai['publisher']) | |
| if ai.get('pub_year'): | |
| pub_info.append(ai['pub_year']) | |
| if ai.get('pub_year_source'): | |
| pub_info.append(f"({ai['pub_year_source']})") | |
| if ai.get('edition'): | |
| pub_info.append(ai['edition']) | |
| if pub_info: | |
| st.markdown(f"**🏢 Publisher:** {' • '.join(pub_info)}") | |
| isbns = [] | |
| if ai.get('isbn_13'): | |
| isbns.append(f"ISBN-13: `{ai['isbn_13']}`") | |
| if ai.get('isbn_10'): | |
| isbns.append(f"ISBN-10: `{ai['isbn_10']}`") | |
| if isbns: | |
| st.markdown(f"**📋 Identifiers:** {' | '.join(isbns)}") | |
| st.markdown("---") | |
| decision = ai.get('acquisition_decision', languages.get_text("decision_recommended", lang)) | |
| if decision == languages.get_text("decision_highly_recommended", lang): | |
| pill_class = "status-high" | |
| elif decision == languages.get_text("decision_recommended", lang): | |
| pill_class = "status-medium" | |
| elif decision == languages.get_text("decision_review_required", lang) or decision == languages.get_text("decision_optional", lang): | |
| pill_class = "status-low" | |
| else: | |
| pill_class = "status-medium" | |
| st.markdown(f"### {languages.get_text('acquisition_decision', lang)}") | |
| st.markdown(f'<div class="{pill_class} status-pill">{decision}</div>', unsafe_allow_html=True) | |
| st.caption(ai.get('acquisition_reason', '')) | |
| st.markdown(f"### {languages.get_text('target_audience', lang)}") | |
| audience = ai.get('audience_category', languages.get_text("not_specified", lang)) | |
| st.markdown(f'<div class="tag">{audience}</div>', unsafe_allow_html=True) | |
| st.caption(ai.get('audience_reason', '')) | |
| with tab2: | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown(f"### {languages.get_text('summary', lang)}") | |
| st.write(ai.get('summary', languages.get_text('no_summary', lang))) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown(f"### {languages.get_text('table_of_contents', lang)}") | |
| contents = ai.get('contents_note', languages.get_text('not_available', lang)) | |
| if isinstance(contents, list): | |
| for item in contents: | |
| st.write(f"• {item}") | |
| else: | |
| st.write(contents) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| with col2: | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown(f"### {languages.get_text('mesh_subjects', lang)}") | |
| mesh_terms = ai.get('mesh_subjects', []) | |
| if mesh_terms: | |
| for term in mesh_terms[:8]: | |
| st.markdown(f'<div class="tag">{term}</div>', unsafe_allow_html=True) | |
| else: | |
| st.write(languages.get_text('no_mesh', lang)) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown(f"### {languages.get_text('nlm_classification', lang)}") | |
| nlm_code = ai.get('nlm_class', 'W 1') | |
| st.markdown(f'<div class="nlm-code">{nlm_code}</div>', unsafe_allow_html=True) | |
| confidence = ai.get('nlm_class_confidence', 'Medium') | |
| if confidence == 'High': | |
| st.success(f"✅ {languages.get_text('confidence_level', lang)}: {confidence} ({languages.get_text('quality_score', lang)}: {ai.get('nlm_class_score', 0)})") | |
| elif confidence == 'Medium': | |
| st.warning(f"⚠️ {languages.get_text('confidence_level', lang)}: {confidence} ({languages.get_text('quality_score', lang)}: {ai.get('nlm_class_score', 0)})") | |
| else: | |
| st.error(f"❌ {languages.get_text('confidence_level', lang)}: {confidence} ({languages.get_text('quality_score', lang)}: {ai.get('nlm_class_score', 0)})") | |
| if ai.get('nlm_class_explanation'): | |
| st.info(f"💡 {ai['nlm_class_explanation']}") | |
| if ai.get('nlm_class_reason'): | |
| with st.expander(languages.get_text('classification_reasoning', lang)): | |
| st.write(ai['nlm_class_reason']) | |
| if ai.get('nlm_alternatives'): | |
| with st.expander(languages.get_text('alternative_classifications', lang)): | |
| for alt in ai['nlm_alternatives'][:3]: | |
| st.code(alt, language="text") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| with tab3: | |
| ai = data['ai_analysis'] | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown(f"### {languages.get_text('marc_record', lang)}") | |
| marc_lines = build_marc_record(ai, data['isbn']) | |
| marc_text = "\n".join(marc_lines) | |
| iso_record = generate_marc_iso2709(marc_lines, data['isbn']) | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.code(marc_text, language="text") | |
| with col2: | |
| st.code(iso_record['iso2709'], language="text") | |
| st.caption(f"ISO 2709 Record - {iso_record['record_length']} bytes") | |
| marcxml_text = generate_marcxml(marc_lines, data['isbn']) | |
| col1, col2, col3, col4 = st.columns(4) | |
| with col1: | |
| st.download_button( | |
| label=languages.get_text("download_marc", lang), | |
| data=marc_text, | |
| file_name=f"{data['isbn']}.mrc", | |
| mime="text/plain", | |
| width='stretch' | |
| ) | |
| with col2: | |
| st.download_button( | |
| label=languages.get_text("download_iso", lang), | |
| data=iso_record['iso2709'].encode('utf-8'), | |
| file_name=f"{data['isbn']}_iso2709.iso", | |
| mime="application/octet-stream", | |
| width='stretch' | |
| ) | |
| with col3: | |
| st.download_button( | |
| label=languages.get_text("download_json", lang), | |
| data=json.dumps(data, indent=2, ensure_ascii=False), | |
| file_name=f"{data['isbn']}_complete.json", | |
| mime="application/json", | |
| width='stretch' | |
| ) | |
| with col4: | |
| st.download_button( | |
| label=languages.get_text("download_xml", lang), | |
| data=marcxml_text, | |
| file_name=f"{data['isbn']}.xml", | |
| mime="application/xml", | |
| width='stretch' | |
| ) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| with tab4: | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown(f"### {languages.get_text('ai_details', lang)}") | |
| st.metric(languages.get_text("confidence_level", lang), ai.get('confidence_level', 'Medium')) | |
| st.metric(languages.get_text("quality_score", lang), ai.get('quality_score', 'N/A')) | |
| st.metric(languages.get_text("data_sources", lang), len(data['web_context']) + (1 if data['metadata'] else 0)) | |
| if ai.get('metadata_source'): | |
| st.caption(f"{languages.get_text('metadata_source', lang)}: {ai['metadata_source']} ({ai.get('metadata_confidence', 'Unknown')})") | |
| st.markdown("---") | |
| st.markdown(f"#### {languages.get_text('nlm_analysis', lang)}") | |
| classifier = AdvancedNLMClassifier() | |
| nlm_analysis = classifier.classify_with_confidence( | |
| ai.get('title', ''), | |
| ai.get('summary', '') | |
| ) | |
| st.metric(languages.get_text("confidence_level", lang), nlm_analysis['confidence_level']) | |
| st.caption(f"{languages.get_text('code_label', lang)}: {nlm_analysis['nlm_code']}") | |
| st.caption(f"{languages.get_text('reason_label', lang)}: {nlm_analysis['confidence_reason']}") | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| with col2: | |
| st.markdown('<div class="modern-card">', unsafe_allow_html=True) | |
| st.markdown(f"### {languages.get_text('web_context', lang)}") | |
| if data['web_context']: | |
| for i, source in enumerate(data['web_context'][:3], 1): | |
| with st.expander(f"Source {i}: {source.get('title', 'Unknown')}"): | |
| st.write(source.get('snippet', languages.get_text('no_web_context', lang))) | |
| if source.get('url'): | |
| st.caption(f"URL: {source['url']}") | |
| else: | |
| st.info(languages.get_text('no_web_context', lang)) | |
| st.markdown('</div>', unsafe_allow_html=True) | |
| # --- SIDEBAR --- | |
| with st.sidebar: | |
| # إضافة محدد اللغة | |
| lang_options = {'en': 'English', 'ar': 'العربية'} | |
| selected_lang = st.selectbox("Language / اللغة", options=list(lang_options.keys()), format_func=lambda x: lang_options[x], index=0 if lang=='en' else 1) | |
| if selected_lang != lang: | |
| st.session_state.language = selected_lang | |
| st.rerun() | |
| st.markdown(f"### {languages.get_text('recent_searches', lang)}") | |
| if st.session_state.search_history: | |
| for item in reversed(st.session_state.search_history[-5:]): | |
| st.caption(f"• {item['isbn']} - {item['title'][:30]}...") | |
| if item.get('nlm_class'): | |
| st.caption(f" NLM: `{item['nlm_class']}`") | |
| st.markdown("---") | |
| st.markdown(f"### {languages.get_text('settings', lang)}") | |
| if st.button(languages.get_text("clear_history", lang), key="clear_btn"): | |
| st.session_state.search_history = [] | |
| st.session_state.analysis_data = None | |
| st.rerun() | |
| st.markdown("---") | |
| st.markdown(f"### {languages.get_text('nlm_quick_guide', lang)}") | |
| with st.expander(languages.get_text("common_nlm_classifications", lang)): | |
| st.markdown(f""" | |
| {languages.get_text('medical_textbooks', lang)} | |
| {languages.get_text('clinical_medicine', lang)} | |
| {languages.get_text('basic_sciences', lang)} | |
| {languages.get_text('health_professions', lang)} | |
| {languages.get_text('common_examples', lang)} | |
| - {languages.get_text('anatomy_example', lang)} | |
| - {languages.get_text('pharmacology_example', lang)} | |
| - {languages.get_text('surgery_example', lang)} | |
| - {languages.get_text('pediatrics_example', lang)} | |
| - {languages.get_text('radiology_example', lang)} | |
| """) | |
| st.markdown("---") | |
| st.markdown(f"### {languages.get_text('stats', lang)}") | |
| st.metric(languages.get_text("total_analyses", lang), len(st.session_state.search_history)) | |
| if st.session_state.analysis_data: | |
| st.metric(languages.get_text("current_isbn", lang), st.session_state.analysis_data['isbn']) | |
| else: | |
| st.metric(languages.get_text("current_isbn", lang), languages.get_text("none", lang)) | |
| # --- FOOTER --- | |
| st.markdown(f""" | |
| <div class="footer"> | |
| <p>{languages.get_text('footer_text', lang)}</p> | |
| <p>{languages.get_text('footer_powered', lang)}</p> | |
| </div> | |
| """, unsafe_allow_html=True) |