minhvtt commited on
Commit
0668f26
·
verified ·
1 Parent(s): 28607ce

Upload 3 files

Browse files
Files changed (3) hide show
  1. README.md +142 -92
  2. app.py +125 -398
  3. requirements.txt +4 -36
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
- title: Aus F
3
- emoji: 👁
4
  colorFrom: indigo
5
  colorTo: pink
6
  sdk: gradio
@@ -9,138 +9,188 @@ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
13
 
14
- # Audience Segmentation AI System
15
 
16
- Hệ thống phân khúc khách hàng và phân tích cảm xúc sử dụng AI cho nền tảng quản lý sự kiện.
17
 
18
- ## Tính năng
 
 
 
19
 
20
- ### 1. Phân khúc khách hàng (Audience Segmentation)
21
- - **Phân cụm tự động** dựa trên hành vi mua vé (RFM Analysis)
22
- - **Phân loại theo sở thích** về danh mục sự kiện
23
- - **Đặt tên tự động** cho từng phân khúc bằng tiếng Việt
24
- - **Tạo nội dung email marketing** tự động cho từng nhóm khách hàng
25
 
26
- ### 2. Phân tích cảm xúc (Sentiment Analysis)
27
- - **Phân loại cảm xúc** của bình luận (Tích cực/Tiêu cực/Trung tính)
28
- - **Sử dụng PhoBERT** - mô hình NLP chuyên biệt cho tiếng Việt
29
- - **Trích xuất từ khóa** tự động từ feedback
30
 
31
- ### 3. Tạo Insight tự động (Generative AI)
32
- - **Top 5 vấn đề** cần cải thiện
33
- - **Gợi ý cải thiện** cho từng vấn đề
34
- - **Dự đoán NPS Score** dựa trên tone của comments
35
- - **Sử dụng Vistral-7B-Chat** - LLM tiên tiến cho tiếng Việt
36
 
37
- ## Cấu trúc thư mục
 
 
38
 
39
- ```
40
- AudienceSegmentation/
41
- ├── models/ # MongoDB data models
42
- │ ├── segmentation_models.py # Audience segment models
43
- │ └── sentiment_models.py # Sentiment analysis models
44
- ├── services/ # Business logic
45
- │ ├── data_aggregation.py # MongoDB aggregation pipelines
46
- │ ├── segmentation_service.py # K-Means clustering
47
- │ ├── sentiment_service.py # PhoBERT sentiment analysis
48
- │ └── genai_service.py # Vistral-7B content generation
49
- ├── config.py # Configuration
50
- ├── database.py # MongoDB connection manager
51
- ├── main.py # Main orchestration script
52
- ├── requirements.txt # Python dependencies
53
- └── .env.example # Environment variables template
54
- ```
55
 
56
- ## Cài đặt
57
 
58
- ### 1. Clone repository
59
- ```bash
60
- cd AudienceSegmentation
61
  ```
62
 
63
- ### 2. Tạo môi trường
64
- ```bash
65
- python -m venv venv
66
- source venv/bin/activate # Linux/Mac
67
- # hoặc
68
- venv\Scripts\activate # Windows
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  ```
70
 
71
- ### 3. Cài đặt dependencies
 
 
72
  ```bash
73
  pip install -r requirements.txt
74
  ```
75
 
76
- ### 4. Download Vistral-7B-Chat
77
  ```bash
78
- # Tải mô hình GGUF từ Hugging Face (CPU nên tải)
79
- mkdir -p models/vistral-7b-chat
80
- # Download từ: https://huggingface.co/Vistral/Vistral-7B-Chat-GGUF
81
  ```
82
 
83
- ### 5. Cấu hình môi trường
84
- ```bash
85
- cp .env.example .env
86
- # Chỉnh sửa .env với thông tin MongoDB của bạn
87
- ```
88
 
89
- ## Sử dụng
90
 
91
- ### Chạy toàn bộ pipeline
92
- ```bash
93
- python main.py --task all
 
 
 
 
94
  ```
95
 
96
- ### Chỉ chạy phân khúc khách hàng
97
- ```bash
98
- python main.py --task segmentation
 
 
 
 
 
99
  ```
100
 
101
- ### Chỉ chạy phân tích cảm xúc
102
  ```bash
103
- python main.py --task sentiment
 
 
 
 
104
  ```
105
 
106
- ### Chỉ tạo nội dung email
 
 
 
 
107
  ```bash
108
- python main.py --task email
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  ```
110
 
111
- ### Tạo insights cho sự kiện cụ thể
112
  ```bash
113
- python main.py --task insights --event-code <event_id>
 
114
  ```
115
 
116
- ## Kiến trúc kỹ thuật
 
 
 
 
 
 
117
 
118
- ### MongoDB Aggregation Framework
119
- Hệ thống tận dụng MongoDB Aggregation để:
120
- - **Tính toán RFM** (Recency, Frequency, Monetary) trực tiếp trên database
121
- - **Đếm danh mục sự kiện** user quan tâm
122
- - **Lọc dữ liệu chưa xử ** để tránh duplicate
123
- - **Giảm thiểu network transfer** - chỉ truyền kết quả cuối cùng
124
 
125
- ### AI Models
126
 
127
- #### 1. Segmentation: scikit-learn K-Means
128
- - **Input**: Feature vector [R, F, M, Category1, Category2, ...]
129
- - **Output**: Cluster labels + Confidence scores
130
- - **Số cụm**: 5 (có thể cấu hình)
131
 
132
- #### 2. Sentiment: wonrax/phobert-base-vietnamese-sentiment
133
- - **Model**: PhoBERT fine-tuned cho Vietnamese
134
- - **Output**: Positive/Negative/Neutral + Confidence
135
- - **Batch size**: 32
136
 
 
 
 
 
 
 
137
 
138
- ## Collections MongoDB
139
 
 
140
 
141
- ### Output Collections (New)
142
- - `AudienceSegment` - Các phân khúc khách hàng
143
- - `UserSegmentAssignment` - Gán user vào segment
144
- - `SentimentAnalysisResult` - Kết quả phân tích cảm xúc
145
- - `EventInsightReport` - Báo cáo insight cho sự kiện
146
 
 
 
1
  ---
2
+ title: Real Estate Formatter
3
+ emoji: 🏠
4
  colorFrom: indigo
5
  colorTo: pink
6
  sdk: gradio
 
9
  pinned: false
10
  ---
11
 
12
+ # 🏠 Real Estate Description Formatter
13
 
14
+ API sử dụng AI models nhẹ từ HuggingFace để format mô tả bất động sản từ dạng "xấu" (không cấu trúc) sang dạng HTML đẹp mắt với CSS inline.
15
 
16
+ ## 🎯 Tính năng
17
 
18
+ - **Format tự động**: Chuyển đổi mô tả BDS xấu thành HTML được styling đẹp
19
+ - **Giữ nguyên nội dung**: Không thay đổi text gốc, chỉ thêm HTML/CSS
20
+ - **AI nhẹ & nhanh**: Sử dụng Small Language Models (1.7B params)
21
+ - **Free & Open-source**: Hoàn toàn miễn phí sử dụng
22
 
23
+ ## 🤖 AI Models được sử dụng
 
 
 
 
24
 
25
+ Dự án hỗ trợ các model mạnh và miễn phí từ HuggingFace:
 
 
 
26
 
27
+ - **Qwen2.5-7B-Instruct** (mặc định) - 7B params, hỗ trợ tiếng Việt, instruction-tuned
28
+ - **Mistral-7B-Instruct-v0.3** - 7B params, outperforms Llama 2 13B
29
+ - **Gemma-2-7B-IT** - 7B params, Google, chat-optimized
 
 
30
 
31
+ ### Models nhẹ hơn (nếu cần tiết kiệm tài nguyên):
32
+ - **Qwen2.5-3B-Instruct** - 3B params, cân bằng tốt
33
+ - **Phi-4-mini-instruct** - 3.8B params, Microsoft, reasoning tốt
34
 
35
+ Bạn có thể thay đổi model bằng biến môi trường `MODEL_NAME`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
+ ## 📋 Ví dụ
38
 
39
+ **Input (form xấu):**
40
+ ```
41
+ NHÀ 2 TẦNG HẺM OTO LIÊN HOA VĨNH NGỌC - TÂY NHA TRANG- Diện tích 92m² full ONT- Hướng Đông Bắc- Pháp lý sổ hồng hoàn công. Hẻm betong rộng 3m- Kết cấu 1 trệt 1 lầu với 4Pn, 2wc, bếp, p.khách, p.thờ, sân để xe ô tô... Full nội thất như hình..- Khu dân cư đông đúc, cách Bệnh viện Sài Gòn Nha Trang 450m, gần siêu thị Big C Go 700m.G.iá bán 4tỷ5 ( thương lượng )0905 124 ***
42
  ```
43
 
44
+ **Output (HTML đẹp):**
45
+ ```html
46
+ <div class="property-card" style="background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border-radius: 12px; padding: 24px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
47
+ <h1 class="title" style="color: #2c3e50; font-size: 24px; text-transform: uppercase; margin-bottom: 16px;">
48
+ NHÀ 2 TẦNG HẺM OTO LIÊN HOA VĨNH NGỌC - TÂY NHA TRANG
49
+ </h1>
50
+
51
+ <div class="specs" style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px;">
52
+ <span style="background: #3498db; color: white; padding: 6px 12px; border-radius: 20px; font-size: 14px;">
53
+ 92m²
54
+ </span>
55
+ <span style="background: #2ecc71; color: white; padding: 6px 12px; border-radius: 20px; font-size: 14px;">
56
+ Đông Bắc
57
+ </span>
58
+ <span style="background: #9b59b6; color: white; padding: 6px 12px; border-radius: 20px; font-size: 14px;">
59
+ Sổ hồng hoàn công
60
+ </span>
61
+ </div>
62
+
63
+ <div class="description" style="background: white; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
64
+ <p style="margin-bottom: 8px;">Hẻm betong rộng 3m</p>
65
+ <p style="margin-bottom: 8px;">Kết cấu: 1 trệt 1 lầu</p>
66
+ <p style="margin-bottom: 8px;">4 <span title="Phòng ngủ">Pn</span>, 2 <span title="WC">wc</span>, bếp, phòng khách, phòng thờ, sân để xe ô tô</p>
67
+ <p>Full nội thất</p>
68
+ </div>
69
+
70
+ <div class="location" style="background: #ecf0f1; padding: 16px; border-radius: 8px; margin-bottom: 16px;">
71
+ <p style="margin-bottom: 8px;"><strong>Khu dân cư đông đúc</strong></p>
72
+ <p style="margin-bottom: 8px;">Cách Bệnh viện Sài Gòn Nha Trang 450m</p>
73
+ <p>Gần siêu thị Big C Go 700m</p>
74
+ </div>
75
+
76
+ <div class="price" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 16px; border-radius: 8px; text-align: center; margin-bottom: 16px;">
77
+ <p style="font-size: 28px; font-weight: bold; margin: 0;">4 tỷ 500 triệu</p>
78
+ <p style="font-size: 14px; margin: 4px 0 0 0;">(Thương lượng)</p>
79
+ </div>
80
+
81
+ <div class="contact" style="text-align: center;">
82
+ <p style="font-size: 18px; font-weight: bold; color: #2c3e50;">0905 124 ***</p>
83
+ </div>
84
+ </div>
85
  ```
86
 
87
+ ## 🚀 Cài đặt & Chạy
88
+
89
+ ### 1. Cài đặt dependencies
90
  ```bash
91
  pip install -r requirements.txt
92
  ```
93
 
94
+ ### 2. Chạy server
95
  ```bash
96
+ python app.py
 
 
97
  ```
98
 
99
+ Server sẽ chạy tại: `http://0.0.0.0:7860`
 
 
 
 
100
 
101
+ ### 3. Sử dụng API
102
 
103
+ **Endpoint:** `POST /format`
104
+
105
+ **Request:**
106
+ ```json
107
+ {
108
+ "description": "NHÀ 2 TẦNG HẺM OTO LIÊN HOA VĨNH NGỌC..."
109
+ }
110
  ```
111
 
112
+ **Response:**
113
+ ```json
114
+ {
115
+ "original": "NHÀ 2 TẦNG HẺM OTO...",
116
+ "formatted_html": "<div class=\"property-card\">...</div>",
117
+ "success": true,
118
+ "error": null
119
+ }
120
  ```
121
 
122
+ ### 4. Test với cURL
123
  ```bash
124
+ curl -X POST "http://localhost:7860/format" \
125
+ -H "Content-Type: application/json" \
126
+ -d '{
127
+ "description": "NHÀ 2 TẦNG HẺM OTO LIÊN HOA VĨNH NGỌC - TÂY NHA TRANG- Diện tích 92m² full ONT- Hướng Đông Bắc- Pháp lý sổ hồng hoàn công..."
128
+ }'
129
  ```
130
 
131
+ ## ⚙️ Cấu hình
132
+
133
+ ### Thay đổi model AI
134
+ Sử dụng biến môi trường `MODEL_NAME`:
135
+
136
  ```bash
137
+ # Model mặc định - Qwen2.5-7B-Instruct (khuyên dùng cho tiếng Việt)
138
+ python app.py
139
+
140
+ # Sử dụng Mistral-7B (mạnh, nhanh)
141
+ export MODEL_NAME="mistralai/Mistral-7B-Instruct-v0.3"
142
+ python app.py
143
+
144
+ # Sử dụng Gemma-2-7B (Google)
145
+ export MODEL_NAME="google/gemma-2-7b-it"
146
+ python app.py
147
+
148
+ # Sử dụng model nhẹ hơn nếu cần (3B params)
149
+ export MODEL_NAME="Qwen/Qwen2.5-3B-Instruct"
150
+ python app.py
151
  ```
152
 
153
+ ### Sử dụng HuggingFace Token (cho private models)
154
  ```bash
155
+ export HF_TOKEN="your_huggingface_token"
156
+ python app.py
157
  ```
158
 
159
+ ## 📚 API Documentation
160
+
161
+ Sau khi chạy server, truy cập:
162
+ - Swagger UI: `http://localhost:7860/docs`
163
+ - ReDoc: `http://localhost:7860/redoc`
164
+
165
+ ## 🎨 Prompt Engineering
166
 
167
+ Prompt được thiết kế phức tạp để:
168
+ - Giữ nguyên nội dung text gốc
169
+ - Phân tích và nhận diện các thành phần (tiêu đề, specs, giá, liên hệ...)
170
+ - Thêm CSS inline với màu sắc hiện đại
171
+ - Xửviết tắt với tooltip
172
+ - Output HTML thuần, không kèm markdown
173
 
174
+ ## 🌐 Deploy lên HuggingFace Spaces
175
 
176
+ 1. Tạo Space mới trên HuggingFace
177
+ 2. Chọn SDK: **Gradio** hoặc **Docker**
178
+ 3. Upload code đợi build
179
+ 4. Truy cập URL của Space
180
 
181
+ ## 📖 Tài liệu tham khảo
 
 
 
182
 
183
+ **AI Models:**
184
+ - [Qwen2.5-7B-Instruct](https://huggingface.co/Qwen/Qwen2.5-7B-Instruct) - Model mặc định, hỗ trợ tiếng Việt
185
+ - [10 Best Open-Source LLM Models 2025](https://huggingface.co/blog/daya-shankar/open-source-llms)
186
+ - [Open LLM Leaderboard](https://huggingface.co/collections/open-llm-leaderboard/open-llm-leaderboard-best-models)
187
+ - [HuggingFace Text Generation Models](https://huggingface.co/models?pipeline_tag=text-generation)
188
+ - [Best Open Source LLMs of 2025](https://klu.ai/blog/open-source-llm-models)
189
 
190
+ ## 📝 License
191
 
192
+ MIT License - Free to use
193
 
194
+ ## 👨‍💻 Author
 
 
 
 
195
 
196
+ Created with ❤️ using Claude Code
app.py CHANGED
@@ -1,32 +1,20 @@
1
  """
2
- FastAPI Application for Event-Centric Audience Segmentation AI
3
- Author: AI Generated
4
- Created: 2025-11-24 (Refactored)
5
- Purpose: REST API with event-based endpoints
6
  """
7
 
8
- from fastapi import FastAPI, HTTPException, BackgroundTasks, status, Query
9
  from fastapi.middleware.cors import CORSMiddleware
10
  from pydantic import BaseModel
11
- from typing import List, Dict, Optional, Any
12
- from datetime import datetime
13
- from bson import ObjectId
14
-
15
- # Import services
16
- from services.segmentation_service import SegmentationService
17
- from services.sentiment_service import SentimentAnalysisService
18
- from services.genai_service import GenerativeAIService
19
- from database import db
20
- from config import settings
21
-
22
 
23
  # FastAPI app
24
  app = FastAPI(
25
- title="Audience Segmentation AI - Event-Centric",
26
- description="REST API for per-event audience analysis",
27
- version="2.0.0",
28
- docs_url="/api/docs",
29
- redoc_url="/api/redoc"
30
  )
31
 
32
  # CORS
@@ -38,402 +26,141 @@ app.add_middleware(
38
  allow_headers=["*"],
39
  )
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
- # Helper
43
- def serialize_doc(doc: Dict) -> Optional[Dict]:
44
- """Convert MongoDB document to JSON-serializable dict"""
45
- if doc is None:
46
- return None
47
- if '_id' in doc:
48
- doc['id'] = str(doc.pop('_id'))
49
-
50
- # Handle nested ObjectIds and lists
51
- for key, value in list(doc.items()):
52
- if isinstance(value, ObjectId):
53
- doc[key] = str(value)
54
- elif isinstance(value, list):
55
- doc[key] = [str(v) if isinstance(v, ObjectId) else v for v in value]
56
- elif isinstance(value, dict):
57
- doc[key] = serialize_doc(value)
58
-
59
- return doc
60
-
61
-
62
- # ===== HEALTH =====
63
- @app.get("/health", tags=["System"])
64
- async def health_check():
65
- """Health check"""
66
- try:
67
- db.client.server_info()
68
- return {
69
- "status": "healthy",
70
- "timestamp": datetime.utcnow(),
71
- "database": "connected"
72
- }
73
- except Exception as e:
74
- raise HTTPException(
75
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
76
- detail=f"Unhealthy: {str(e)}"
77
- )
78
-
79
-
80
- # ===== EVENT ANALYSIS =====
81
- @app.post("/api/events/{event_code}/analyze", tags=["Event Analysis"])
82
- async def analyze_event(event_code: str, background_tasks: BackgroundTasks):
83
- """Run full AI pipeline for an event"""
84
-
85
- def run_pipeline():
86
- # Step 1: Segmentation
87
- seg_service = SegmentationService(event_code)
88
- seg_service.run_segmentation()
89
-
90
- # Step 2: Sentiment
91
- sent_service = SentimentAnalysisService(event_code)
92
- sent_service.analyze_event_comments()
93
-
94
- # Step 3: Email generation
95
- genai_service = GenerativeAIService(event_code)
96
- genai_service.generate_emails_for_all_segments()
97
-
98
- # Step 4: Insights
99
- genai_service.update_sentiment_summary_with_insights()
100
-
101
- background_tasks.add_task(run_pipeline)
102
-
103
- return {
104
- "status": "started",
105
- "message": f"Analysis pipeline started for event {event_code}"
106
- }
107
-
108
-
109
- @app.get("/api/events/{event_code}/dashboard", tags=["Event Analysis"])
110
- async def get_event_dashboard(event_code: str):
111
- """Get complete dashboard for Event Owner"""
112
-
113
- # Get segments
114
- segments = list(db.event_audience_segments.find({"event_code": event_code}))
115
-
116
- # Get sentiment summary
117
- sentiment_summary = db.event_sentiment_summary.find_one({"event_code": event_code})
118
-
119
- return {
120
- "event_code": event_code,
121
- "segments": [serialize_doc(s) for s in segments],
122
- "sentiment_summary": serialize_doc(sentiment_summary) if sentiment_summary else None
123
- }
124
-
125
-
126
- # ===== SEGMENTATION =====
127
- @app.post("/api/events/{event_code}/segmentation/run", tags=["Segmentation"])
128
- async def run_event_segmentation(
129
- event_code: str,
130
- background_tasks: BackgroundTasks,
131
- n_clusters: int = Query(default=5, ge=2, le=10)
132
- ):
133
- """Run segmentation for an event"""
134
-
135
- def run_task():
136
- service = SegmentationService(event_code, n_clusters=n_clusters)
137
- service.run_segmentation()
138
-
139
- background_tasks.add_task(run_task)
140
-
141
- return {
142
- "status": "started",
143
- "message": f"Segmentation started for event {event_code}",
144
- "event_code": event_code
145
- }
146
-
147
-
148
- @app.get("/api/events/{event_code}/segments", tags=["Segmentation"])
149
- async def get_event_segments(
150
- event_code: str,
151
- status_filter: Optional[str] = Query(default=None, description="Filter by Draft, Approved, Sent")
152
- ):
153
- """Get all segments for an event"""
154
-
155
- query = {"event_code": event_code}
156
- if status_filter:
157
- query["marketing_content.status"] = status_filter
158
-
159
- segments = list(db.event_audience_segments.find(query))
160
-
161
- return [serialize_doc(s) for s in segments]
162
-
163
-
164
- @app.get("/api/events/{event_code}/segments/{segment_id}", tags=["Segmentation"])
165
- async def get_segment_detail(event_code: str, segment_id: str):
166
- """Get specific segment details"""
167
-
168
- segment = db.event_audience_segments.find_one({
169
- "_id": ObjectId(segment_id),
170
- "event_code": event_code
171
- })
172
-
173
- if not segment:
174
- raise HTTPException(status_code=404, detail="Segment not found")
175
-
176
- return serialize_doc(segment)
177
-
178
-
179
- @app.get("/api/events/{event_code}/segments/{segment_id}/users", tags=["Segmentation"])
180
- async def get_segment_users(
181
- event_code: str,
182
- segment_id: str,
183
- skip: int = 0,
184
- limit: int = 100
185
- ):
186
- """Get users in a segment with details"""
187
-
188
- segment = db.event_audience_segments.find_one({
189
- "_id": ObjectId(segment_id),
190
- "event_code": event_code
191
- })
192
-
193
- if not segment:
194
- raise HTTPException(status_code=404, detail="Segment not found")
195
-
196
- user_ids = segment.get('user_ids', [])
197
- total_users = len(user_ids)
198
-
199
- # Paginate
200
- paginated_ids = user_ids[skip:skip + limit]
201
-
202
- # Get user details
203
- users = list(db.users.find({
204
- "_id": {"$in": paginated_ids}
205
- }))
206
-
207
- # Enrich with stats (optional)
208
- enriched_users = []
209
- for user in users:
210
- enriched_users.append({
211
- "user_id": str(user['_id']),
212
- "email": user.get('email'),
213
- "full_name": f"{user.get('FirstName', '')} {user.get('LastName', '')}".strip()
214
- })
215
-
216
- return {
217
- "segment_id": segment_id,
218
- "total_users": total_users,
219
- "users": enriched_users
220
- }
221
-
222
-
223
- # ===== APPROVAL WORKFLOW =====
224
- @app.post("/api/events/{event_code}/segments/{segment_id}/approve", tags=["Approval"])
225
- async def approve_segment(
226
- event_code: str,
227
- segment_id: str,
228
- approved_by: Optional[str] = None,
229
- modified_subject: Optional[str] = None,
230
- modified_body: Optional[str] = None
231
- ):
232
- """Event Owner approves marketing content"""
233
-
234
- segment = db.event_audience_segments.find_one({
235
- "_id": ObjectId(segment_id),
236
- "event_code": event_code
237
- })
238
-
239
- if not segment:
240
- raise HTTPException(status_code=404, detail="Segment not found")
241
-
242
- # Update fields
243
- update = {
244
- "marketing_content.status": "Approved",
245
- "marketing_content.approved_at": datetime.utcnow(),
246
- "marketing_content.approved_by": approved_by,
247
- "last_updated": datetime.utcnow()
248
- }
249
-
250
- if modified_subject:
251
- update["marketing_content.email_subject"] = modified_subject
252
- if modified_body:
253
- update["marketing_content.email_body"] = modified_body
254
-
255
- db.event_audience_segments.update_one(
256
- {"_id": ObjectId(segment_id)},
257
- {"$set": update}
258
- )
259
-
260
- updated_segment = db.event_audience_segments.find_one({"_id": ObjectId(segment_id)})
261
-
262
- return {
263
- "status": "success",
264
- "message": "Segment approved",
265
- "segment_id": segment_id,
266
- "marketing_content": updated_segment.get('marketing_content')
267
- }
268
-
269
-
270
- @app.post("/api/events/{event_code}/segments/{segment_id}/send-email", tags=["Approval"])
271
- async def send_segment_email(
272
- event_code: str,
273
- segment_id: str,
274
- send_immediately: bool = True
275
- ):
276
- """Send approved marketing email"""
277
-
278
- segment = db.event_audience_segments.find_one({
279
- "_id": ObjectId(segment_id),
280
- "event_code": event_code
281
- })
282
-
283
- if not segment:
284
- raise HTTPException(status_code=404, detail="Segment not found")
285
-
286
- marketing_content = segment.get('marketing_content', {})
287
- if marketing_content.get('status') != "Approved":
288
- raise HTTPException(status_code=400, detail="Segment not approved yet")
289
-
290
- # TODO: Integrate with email service (SendGrid, AWS SES, etc.)
291
- # For now, just mark as sent
292
-
293
- db.event_audience_segments.update_one(
294
- {"_id": ObjectId(segment_id)},
295
- {"$set": {
296
- "marketing_content.status": "Sent",
297
- "last_updated": datetime.utcnow()
298
- }}
299
- )
300
-
301
- return {
302
- "status": "success",
303
- "message": f"Email sent to {segment.get('user_count', 0)} users",
304
- "segment_id": segment_id,
305
- "emails_sent": segment.get('user_count', 0),
306
- "emails_failed": 0
307
- }
308
-
309
-
310
- # ===== SENTIMENT =====
311
- @app.post("/api/events/{event_code}/sentiment/analyze", tags=["Sentiment"])
312
- async def analyze_event_sentiment(event_code: str, background_tasks: BackgroundTasks):
313
- """Analyze sentiment for event comments"""
314
-
315
- def run_task():
316
- service = SentimentAnalysisService(event_code)
317
- service.analyze_event_comments()
318
-
319
- background_tasks.add_task(run_task)
320
-
321
- return {
322
- "status": "started",
323
- "message": f"Sentiment analysis started for event {event_code}"
324
- }
325
 
 
326
 
327
- @app.get("/api/events/{event_code}/sentiment/summary", tags=["Sentiment"])
328
- async def get_sentiment_summary(event_code: str):
329
- """Get sentiment summary for an event"""
330
-
331
- summary = db.event_sentiment_summary.find_one({"event_code": event_code})
332
-
333
- if not summary:
334
- raise HTTPException(status_code=404, detail="No sentiment data for this event")
335
-
336
- return serialize_doc(summary)
337
 
338
 
339
- @app.get("/api/events/{event_code}/sentiment/results", tags=["Sentiment"])
340
- async def get_sentiment_results(
341
- event_code: str,
342
- sentiment_label: Optional[str] = None,
343
- skip: int = 0,
344
- limit: int = 100
345
- ):
346
- """Get detailed sentiment results"""
347
-
348
- query = {"event_code": event_code}
349
- if sentiment_label:
350
- query["sentiment_label"] = sentiment_label
351
-
352
- total = db.sentiment_results.count_documents(query)
353
- results = list(
354
- db.sentiment_results.find(query)
355
- .sort("analyzed_at", -1)
356
- .skip(skip)
357
- .limit(limit)
358
- )
359
-
360
  return {
361
- "total": total,
362
- "results": [serialize_doc(r) for r in results]
 
 
363
  }
364
 
365
 
366
- # ===== GENAI =====
367
- @app.post("/api/events/{event_code}/genai/generate-emails", tags=["GenAI"])
368
- async def generate_event_emails(event_code: str, background_tasks: BackgroundTasks):
369
- """Generate marketing emails for all segments"""
370
-
371
- def run_task():
372
- service = GenerativeAIService(event_code)
373
- service.generate_emails_for_all_segments()
374
-
375
- background_tasks.add_task(run_task)
376
-
377
- return {
378
- "status": "started",
379
- "message": "Email generation started"
380
- }
381
 
382
-
383
- @app.post("/api/events/{event_code}/genai/generate-insights", tags=["GenAI"])
384
- async def generate_event_insights(event_code: str, background_tasks: BackgroundTasks):
385
- """Generate AI insights from negative feedback"""
386
-
387
- def run_task():
388
- service = GenerativeAIService(event_code)
389
- service.update_sentiment_summary_with_insights()
390
-
391
- background_tasks.add_task(run_task)
392
-
393
- return {
394
- "status": "started",
395
- "message": "Insight generation started"
396
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
 
 
398
 
399
- # ===== MONITORING =====
400
- @app.get("/api/monitoring/pipelines/{pipeline}/metrics", tags=["Monitoring"])
401
- async def get_pipeline_metrics(
402
- pipeline: str,
403
- event_code: Optional[str] = None,
404
- days: int = 7
405
- ):
406
- """Get performance metrics"""
407
- # TODO: Implement based on monitoring.py
408
- return {
409
- "pipeline": pipeline,
410
- "event_code": event_code,
411
- "message": "Metrics endpoint - implement as needed"
412
- }
413
 
 
 
 
 
 
414
 
415
- # ===== ADMIN =====
416
- @app.post("/api/admin/indexes/create", tags=["Admin"])
417
- async def create_indexes():
418
- """Create MongoDB indexes"""
419
- from scripts.create_indexes import create_all_indexes
420
-
421
- try:
422
- create_all_indexes()
423
- return {"status": "success", "message": "Indexes created"}
424
  except Exception as e:
425
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
426
 
427
 
428
- # ===== ROOT =====
429
- @app.get("/")
430
- async def root():
431
- """API root"""
432
  return {
433
- "name": "Audience Segmentation AI - Event-Centric",
434
- "version": "2.0.0",
435
- "docs": "/api/docs",
436
- "health": "/health"
437
  }
438
 
439
 
 
1
  """
2
+ Real Estate Description Formatter API
3
+ Uses lightweight AI models from HuggingFace to format real estate descriptions
 
 
4
  """
5
 
6
+ from fastapi import FastAPI, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
8
  from pydantic import BaseModel
9
+ from typing import Optional
10
+ import os
11
+ from huggingface_hub import InferenceClient
 
 
 
 
 
 
 
 
12
 
13
  # FastAPI app
14
  app = FastAPI(
15
+ title="Real Estate Description Formatter",
16
+ description="API to format real estate descriptions with AI",
17
+ version="1.0.0"
 
 
18
  )
19
 
20
  # CORS
 
26
  allow_headers=["*"],
27
  )
28
 
29
+ # Initialize HuggingFace Inference Client
30
+ # Sử dụng model mạnh: Qwen2.5-7B-Instruct (7B params, hỗ trợ tiếng Việt)
31
+ # Alternatives: mistralai/Mistral-7B-Instruct-v0.3, google/gemma-2-7b-it
32
+ MODEL_NAME = os.getenv("MODEL_NAME", "Qwen/Qwen2.5-7B-Instruct")
33
+ HF_TOKEN = os.getenv("HF_TOKEN", None) # Optional: for private models or faster inference
34
+
35
+ client = InferenceClient(model=MODEL_NAME, token=HF_TOKEN)
36
+
37
+ # Request/Response Models
38
+ class RealEstateInput(BaseModel):
39
+ description: str
40
+
41
+ class RealEstateOutput(BaseModel):
42
+ original: str
43
+ formatted_html: str
44
+ success: bool
45
+ error: Optional[str] = None
46
+
47
+ # Prompt phức tạp để xử lý form description bất động sản
48
+ SYSTEM_PROMPT = """Bạn là một chuyên gia định dạng nội dung bất động sản chuyên nghiệp.
49
+
50
+ NHIỆM VỤ: Chuyển đổi mô tả bất động sản "xấu" (không có cấu trúc, viết tắt, thiếu dấu câu) thành HTML được format đẹp mắt với CSS inline.
51
+
52
+ QUY TẮC QUAN TRỌNG:
53
+ 1. KHÔNG ĐƯỢC thay đổi nội dung text gốc - CHỈ thêm HTML tags và CSS styling
54
+ 2. PHẢI giữ nguyên tất cả thông tin: giá, diện tích, số phòng, địa chỉ, số điện thoại
55
+ 3. PHẢI phân tích và nhận diện các thành phần:
56
+ - Tiêu đề (tên BDS, loại nhà)
57
+ - Thông tin kỹ thuật (diện tích, hướng, pháp lý)
58
+ - Mô tả chi tiết (kết cấu, nội thất)
59
+ - Vị trí (địa chỉ, khu vực, tiện ích xung quanh)
60
+ - Giá bán
61
+ - Liên hệ (SĐT, tên người bán)
62
+ - ... các thành phần khác
63
+
64
+ 4. SỬ DỤNG CẤU TRÚC HTML:
65
+ - <div class="property-card"> cho toàn bộ nội dung
66
+ - <h1 class="title"> cho tiêu đề chính
67
+ - <div class="specs"> cho thông số kỹ thuật (badge style)
68
+ - <div class="description"> cho mô tả chi tiết
69
+ - <div class="location"> cho thông tin vị trí
70
+ - <div class="price"> cho giá (highlight, font lớn)
71
+ - <div class="contact"> cho thông tin liên hệ
72
+
73
+ 5. CSS INLINE STYLING - Màu sắc hiện đại, dễ đọc:
74
+ - Property card: background, border radius, shadow
75
+ - Title: màu đậm, font-size lớn, text-transform uppercase
76
+ - Specs: badge style với background color khác nhau
77
+ - Price: màu phù hợp, không quá chói mắt, cũng không quá trầm, nổi bật, font-weight bold, font-size lớn
78
+ - Icons: không sử dụng icon gì khác.
79
+
80
+ 6. XỬ LÝ VIẾT TẮT:
81
+ - Giữ nguyên viết tắt nhưng thêm tooltip/title
82
+ - Ví dụ: <span title="Phòng ngủ">Pn</span>, <span title="Phòng vệ sinh">wc</span>
83
+
84
+ OUTPUT FORMAT: Chỉ trả về HTML thuần, KHÔNG kèm markdown hoặc giải thích.
85
+ """
86
 
87
+ USER_PROMPT_TEMPLATE = """Hãy format mô tả bất động sản sau thành HTML đẹp với CSS inline:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ {description}
90
 
91
+ Nhớ: GIỮ NGUYÊN nội dung text, CHỈ thêm HTML/CSS để trình bày đẹp hơn."""
 
 
 
 
 
 
 
 
 
92
 
93
 
94
+ @app.get("/")
95
+ async def root():
96
+ """API root"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  return {
98
+ "name": "Real Estate Description Formatter",
99
+ "version": "1.0.0",
100
+ "model": MODEL_NAME,
101
+ "endpoint": "/format"
102
  }
103
 
104
 
105
+ @app.post("/format", response_model=RealEstateOutput)
106
+ async def format_description(input_data: RealEstateInput):
107
+ """
108
+ Format real estate description with AI
 
 
 
 
 
 
 
 
 
 
 
109
 
110
+ Example input:
111
+ {
112
+ "description": "NHÀ 2 TẦNG HẺM OTO LIÊN HOA VĨNH NGỌC - TÂY NHA TRANG- Diện tích 92m² full ONT- Hướng Đông Bắc- Pháp lý sổ hồng hoàn công..."
 
 
 
 
 
 
 
 
 
 
 
113
  }
114
+ """
115
+ try:
116
+ # Prepare messages for chat model
117
+ messages = [
118
+ {
119
+ "role": "system",
120
+ "content": SYSTEM_PROMPT
121
+ },
122
+ {
123
+ "role": "user",
124
+ "content": USER_PROMPT_TEMPLATE.format(description=input_data.description)
125
+ }
126
+ ]
127
+
128
+ # Call HuggingFace Inference API
129
+ response = client.chat_completion(
130
+ messages=messages,
131
+ max_tokens=2000,
132
+ temperature=0.3, # Low temperature for consistent formatting
133
+ )
134
 
135
+ # Extract formatted HTML
136
+ formatted_html = response.choices[0].message.content.strip()
137
 
138
+ # Clean up markdown if model added it
139
+ if formatted_html.startswith("```html"):
140
+ formatted_html = formatted_html.replace("```html", "").replace("```", "").strip()
 
 
 
 
 
 
 
 
 
 
 
141
 
142
+ return RealEstateOutput(
143
+ original=input_data.description,
144
+ formatted_html=formatted_html,
145
+ success=True
146
+ )
147
 
 
 
 
 
 
 
 
 
 
148
  except Exception as e:
149
+ return RealEstateOutput(
150
+ original=input_data.description,
151
+ formatted_html="",
152
+ success=False,
153
+ error=str(e)
154
+ )
155
 
156
 
157
+ @app.get("/health")
158
+ async def health_check():
159
+ """Health check endpoint"""
 
160
  return {
161
+ "status": "healthy",
162
+ "model": MODEL_NAME,
163
+ "service": "Real Estate Formatter"
 
164
  }
165
 
166
 
requirements.txt CHANGED
@@ -1,36 +1,4 @@
1
- # FastAPI Backend Requirements
2
- # Updated for November 2025
3
-
4
- # Web Framework
5
- fastapi==0.121.3
6
- uvicorn[standard]==0.38.0
7
- python-multipart==0.0.20
8
-
9
- # Database
10
- pymongo==4.15.4
11
- motor==3.7.0
12
-
13
- # Data Validation
14
- pydantic==2.10.4
15
- pydantic-settings==2.12.0
16
-
17
- # Data Processing
18
- pandas
19
- numpy
20
- scikit-learn
21
-
22
- # NLP & AI
23
- transformers
24
- torch
25
- tokenizers
26
-
27
- # Vietnamese NLP
28
- pyvi==0.1.1
29
-
30
- # Utilities
31
- python-dotenv==1.0.1
32
- tqdm==4.67.1
33
-
34
- # Security
35
- python-jose[cryptography]==3.4.0
36
- passlib[bcrypt]==1.7.4
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.32.1
3
+ pydantic==2.10.3
4
+ huggingface-hub==0.26.5