Spaces:
Runtime error
Runtime error
Fix font
Browse files- app.tex +516 -0
- app/database/rating_prediction.db +0 -0
- app/services/__pycache__/ml_service.cpython-313.pyc +0 -0
- app/services/report_service.py +80 -10
app.tex
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
% ============================================
|
| 2 |
+
% CHƯƠNG: XÂY DỰNG VÀ TRIỂN KHAI ỨNG DỤNG
|
| 3 |
+
% ============================================
|
| 4 |
+
\setlength{\parindent}{0pt}
|
| 5 |
+
|
| 6 |
+
\section{Xây dựng và Triển khai Ứng dụng}
|
| 7 |
+
|
| 8 |
+
Chương này trình bày chi tiết về kiến trúc hệ thống, giao diện người dùng và quy trình triển khai ứng dụng dự đoán đánh giá sản phẩm từ bình luận tiếng Việt.
|
| 9 |
+
|
| 10 |
+
% ============================================
|
| 11 |
+
% PHẦN 1: KIẾN TRÚC HỆ THỐNG
|
| 12 |
+
% ============================================
|
| 13 |
+
\subsection{Kiến trúc Hệ thống}
|
| 14 |
+
|
| 15 |
+
Hệ thống được thiết kế theo mô hình \textbf{Client-Server} với kiến trúc phân lớp (Layered Architecture), đảm bảo tính module hóa và dễ dàng bảo trì.
|
| 16 |
+
|
| 17 |
+
\subsubsection{Tổng quan Kiến trúc}
|
| 18 |
+
|
| 19 |
+
Hệ thống bao gồm 4 lớp chính:
|
| 20 |
+
|
| 21 |
+
\begin{itemize}
|
| 22 |
+
\item \textbf{Frontend Layer}: Giao diện người dùng được xây dựng bằng HTML/CSS (TailwindCSS) và JavaScript, sử dụng Jinja2 Template Engine để render động.
|
| 23 |
+
\item \textbf{API Layer}: FastAPI Backend xử lý các HTTP request thông qua RESTful API endpoints.
|
| 24 |
+
\item \textbf{Service Layer}: Các service xử lý business logic bao gồm Authentication Service, ML Prediction Service và Visualization Service.
|
| 25 |
+
\item \textbf{Data Layer}: SQLAlchemy ORM kết nối với cơ sở dữ liệu SQLite (development) hoặc PostgreSQL (production).
|
| 26 |
+
\end{itemize}
|
| 27 |
+
|
| 28 |
+
\begin{figure}[H]
|
| 29 |
+
\centering
|
| 30 |
+
\includegraphics[width=0.9\textwidth]{images/kien_truc_he_thong.png}
|
| 31 |
+
\caption{Sơ đồ kiến trúc tổng quan của hệ thống}
|
| 32 |
+
\label{fig:kien_truc_he_thong}
|
| 33 |
+
\end{figure}
|
| 34 |
+
|
| 35 |
+
\subsubsection{Frontend - Giao diện Người dùng}
|
| 36 |
+
|
| 37 |
+
Frontend được xây dựng với các công nghệ:
|
| 38 |
+
|
| 39 |
+
\begin{itemize}
|
| 40 |
+
\item \textbf{Jinja2 Templates}: Engine template của Python, tích hợp chặt chẽ với FastAPI để render các trang HTML động.
|
| 41 |
+
\item \textbf{TailwindCSS}: Framework CSS utility-first giúp xây dựng giao diện responsive và hiện đại.
|
| 42 |
+
\item \textbf{JavaScript (Fetch API)}: Xử lý các AJAX request để giao tiếp bất đồng bộ với Backend.
|
| 43 |
+
\item \textbf{Chart.js}: Thư viện visualize để hiển thị biểu đồ phân bố rating.
|
| 44 |
+
\end{itemize}
|
| 45 |
+
|
| 46 |
+
Khi người dùng thực hiện thao tác (ví dụ: nhập comment và nhấn "Predict"), JavaScript sẽ gửi HTTP POST request đến Backend API, nhận response JSON và cập nhật giao diện mà không cần reload trang.
|
| 47 |
+
|
| 48 |
+
\subsubsection{Backend - FastAPI Server}
|
| 49 |
+
|
| 50 |
+
Backend được xây dựng trên framework \textbf{FastAPI} với Python, có các đặc điểm nổi bật:
|
| 51 |
+
|
| 52 |
+
\begin{itemize}
|
| 53 |
+
\item \textbf{High Performance}: Xây dựng trên Starlette và Pydantic, FastAPI là một trong những framework Python nhanh nhất.
|
| 54 |
+
\item \textbf{Auto Documentation}: Tự động sinh Swagger UI (/docs) và ReDoc (/redoc) cho API documentation.
|
| 55 |
+
\item \textbf{Type Hints}: Sử dụng Python type hints để validation và serialization tự động.
|
| 56 |
+
\end{itemize}
|
| 57 |
+
|
| 58 |
+
Cấu trúc Router của Backend:
|
| 59 |
+
|
| 60 |
+
\vietnameselst
|
| 61 |
+
\begin{lstlisting}[language=Python]
|
| 62 |
+
# main.py - Khoi tao FastAPI Application
|
| 63 |
+
from fastapi import FastAPI
|
| 64 |
+
from app.routers import auth, prediction, dashboard
|
| 65 |
+
|
| 66 |
+
app = FastAPI(
|
| 67 |
+
title="Vietnamese Product Rating Prediction API",
|
| 68 |
+
description="ML-powered sentiment analysis for Vietnamese reviews",
|
| 69 |
+
version="1.0.0"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# Dang ky cac Router
|
| 73 |
+
app.include_router(auth.router, prefix="/api/auth", tags=["Authentication"])
|
| 74 |
+
app.include_router(prediction.router, prefix="/api/predict", tags=["Prediction"])
|
| 75 |
+
app.include_router(dashboard.router, tags=["Dashboard"])
|
| 76 |
+
\end{lstlisting}
|
| 77 |
+
|
| 78 |
+
\subsubsection{Cơ chế Lazy Loading của Model}
|
| 79 |
+
|
| 80 |
+
Một trong những tối ưu quan trọng nhất của hệ thống là cơ chế \textbf{Lazy Loading} cho ML Model. Thay vì load model ngay khi khởi động server (có thể mất 30-60 giây với model PhoBERT ~500MB), model chỉ được load vào RAM khi có request đầu tiên.
|
| 81 |
+
|
| 82 |
+
\vietnameselst
|
| 83 |
+
\begin{lstlisting}[language=Python]
|
| 84 |
+
class MLPredictionService:
|
| 85 |
+
"""ML Service voi co che Lazy Loading"""
|
| 86 |
+
|
| 87 |
+
def __init__(self):
|
| 88 |
+
# Chi khoi tao cac bien, KHONG load model
|
| 89 |
+
self.model = None
|
| 90 |
+
self.tokenizer = None
|
| 91 |
+
self.model_loaded = False
|
| 92 |
+
|
| 93 |
+
# Dinh nghia Repo ID chua model tren Hugging Face
|
| 94 |
+
self.MODEL_REPO_ID = "vtdung23/my-phobert-models"
|
| 95 |
+
self.MODEL_FILENAME = "best_phoBER.pth"
|
| 96 |
+
|
| 97 |
+
print("ML Service initialized (Model se load khi co request dau tien)")
|
| 98 |
+
|
| 99 |
+
def _load_model(self):
|
| 100 |
+
"""Load model chi khi can thiet (lazy loading)"""
|
| 101 |
+
if self.model_loaded:
|
| 102 |
+
return # Da load roi, khong can load lai
|
| 103 |
+
|
| 104 |
+
print("Dang load ML model (first request)...")
|
| 105 |
+
|
| 106 |
+
# Import cac thu vien nang chi khi can
|
| 107 |
+
import torch
|
| 108 |
+
from transformers import AutoTokenizer, RobertaForSequenceClassification
|
| 109 |
+
from huggingface_hub import hf_hub_download
|
| 110 |
+
|
| 111 |
+
# Tai file weights tu Hugging Face Hub
|
| 112 |
+
model_path = hf_hub_download(
|
| 113 |
+
repo_id=self.MODEL_REPO_ID,
|
| 114 |
+
filename=self.MODEL_FILENAME,
|
| 115 |
+
repo_type="model"
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# Load tokenizer va model
|
| 119 |
+
self.tokenizer = AutoTokenizer.from_pretrained("vinai/phobert-base")
|
| 120 |
+
self.model = RobertaForSequenceClassification.from_pretrained(
|
| 121 |
+
"vinai/phobert-base",
|
| 122 |
+
num_labels=5
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
# Load trained weights
|
| 126 |
+
state_dict = torch.load(model_path, map_location="cpu")
|
| 127 |
+
self.model.load_state_dict(state_dict)
|
| 128 |
+
self.model.eval()
|
| 129 |
+
|
| 130 |
+
self.model_loaded = True
|
| 131 |
+
print("Model loaded thanh cong!")
|
| 132 |
+
|
| 133 |
+
def predict_single(self, text: str):
|
| 134 |
+
"""Du doan rating cho 1 comment"""
|
| 135 |
+
self._load_model() # Dam bao model da duoc load
|
| 136 |
+
# ... logic du doan ...
|
| 137 |
+
\end{lstlisting}
|
| 138 |
+
|
| 139 |
+
\textbf{Lợi ích của Lazy Loading:}
|
| 140 |
+
\begin{itemize}
|
| 141 |
+
\item \textbf{Khởi động nhanh}: Server start trong vài giây thay vì phải chờ load model.
|
| 142 |
+
\item \textbf{Tiết kiệm RAM}: Trên các nền tảng miễn phí (Hugging Face Spaces, Render), RAM bị giới hạn. Model chỉ chiếm RAM khi thực sự cần thiết.
|
| 143 |
+
\item \textbf{Cold Start Optimization}: Phù hợp với serverless hoặc container-based deployment.
|
| 144 |
+
\end{itemize}
|
| 145 |
+
|
| 146 |
+
\subsubsection{Luồng Dữ liệu (Data Flow)}
|
| 147 |
+
|
| 148 |
+
Sơ đồ dưới đây mô tả luồng đi của dữ liệu khi người dùng thực hiện dự đoán:
|
| 149 |
+
|
| 150 |
+
\begin{figure}[H]
|
| 151 |
+
\centering
|
| 152 |
+
\includegraphics[width=0.95\textwidth]{images/data_flow.png}
|
| 153 |
+
\caption{Sơ đồ luồng dữ liệu của hệ thống dự đoán}
|
| 154 |
+
\label{fig:data_flow}
|
| 155 |
+
\end{figure}
|
| 156 |
+
|
| 157 |
+
\textbf{Mô tả luồng dữ liệu:}
|
| 158 |
+
|
| 159 |
+
\begin{enumerate}
|
| 160 |
+
\item \textbf{User Input}: Người dùng nhập comment tiếng Việt vào form trên Dashboard.
|
| 161 |
+
\item \textbf{HTTP Request}: JavaScript gửi POST request đến endpoint \texttt{/api/predict/single} với JSON body chứa comment.
|
| 162 |
+
\item \textbf{Authentication}: Middleware kiểm tra JWT token trong header để xác thực người dùng.
|
| 163 |
+
\item \textbf{Prediction Router}: Router nhận request, validate input bằng Pydantic schema.
|
| 164 |
+
\item \textbf{ML Service}:
|
| 165 |
+
\begin{itemize}
|
| 166 |
+
\item Lazy load model nếu chưa được load.
|
| 167 |
+
\item Tiền xử lý văn bản (word tokenization với Underthesea).
|
| 168 |
+
\item Tokenize với PhoBERT tokenizer.
|
| 169 |
+
\item Inference với model PhoBERT fine-tuned.
|
| 170 |
+
\item Trả về rating (1-5 sao) và confidence score.
|
| 171 |
+
\end{itemize}
|
| 172 |
+
\item \textbf{Save History}: Lưu kết quả dự đoán vào database để theo dõi lịch sử.
|
| 173 |
+
\item \textbf{Response}: Trả về JSON response với rating, confidence, highlighted keywords.
|
| 174 |
+
\item \textbf{UI Update}: JavaScript nhận response và cập nhật giao diện hiển thị kết quả.
|
| 175 |
+
\end{enumerate}
|
| 176 |
+
|
| 177 |
+
% ============================================
|
| 178 |
+
% PHẦN 2: GIAO DIỆN VÀ CHỨC NĂNG
|
| 179 |
+
% ============================================
|
| 180 |
+
\subsection{Giao diện và Chức năng}
|
| 181 |
+
|
| 182 |
+
\subsubsection{Màn hình Đăng nhập và Đăng ký}
|
| 183 |
+
|
| 184 |
+
Hệ thống yêu cầu người dùng đăng nhập trước khi sử dụng các tính năng dự đoán. Việc này giúp:
|
| 185 |
+
\begin{itemize}
|
| 186 |
+
\item Theo dõi lịch sử dự đoán của từng người dùng.
|
| 187 |
+
\item Bảo vệ API endpoints khỏi các truy cập trái phép.
|
| 188 |
+
\item Phân quyền và quản lý người dùng trong tương lai.
|
| 189 |
+
\end{itemize}
|
| 190 |
+
|
| 191 |
+
Xác thực được thực hiện bằng \textbf{JWT (JSON Web Token)} với thuật toán HS256 và mật khẩu được hash bằng \textbf{bcrypt}.
|
| 192 |
+
|
| 193 |
+
\subsubsection{Màn hình Dashboard - Giao diện Chính}
|
| 194 |
+
|
| 195 |
+
Dashboard là màn hình chính của ứng dụng, nơi người dùng thực hiện các thao tác dự đoán. Giao diện được thiết kế theo phong cách hiện đại với TailwindCSS, hỗ trợ cả Light Mode và Dark Mode.
|
| 196 |
+
|
| 197 |
+
\begin{figure}[H]
|
| 198 |
+
\centering
|
| 199 |
+
\includegraphics[width=0.9\textwidth]{images/giao_dien_chinh.png}
|
| 200 |
+
\caption{Màn hình Dashboard - Giao diện chính của ứng dụng}
|
| 201 |
+
\label{fig:giao_dien_chinh}
|
| 202 |
+
\end{figure}
|
| 203 |
+
|
| 204 |
+
\textbf{Các thành phần chính của Dashboard:}
|
| 205 |
+
|
| 206 |
+
\begin{itemize}
|
| 207 |
+
\item \textbf{Navigation Bar}: Hiển thị tên người dùng và nút Logout.
|
| 208 |
+
\item \textbf{Welcome Section}: Giới thiệu các tính năng chính của ứng dụng.
|
| 209 |
+
\item \textbf{Tab Input Mode}: Cho phép chuyển đổi giữa "Single Comment" và "Upload CSV".
|
| 210 |
+
\item \textbf{Input Area}: Vùng nhập liệu comment hoặc upload file CSV.
|
| 211 |
+
\item \textbf{Result Section}: Hiển thị kết quả dự đoán với các visualization.
|
| 212 |
+
\end{itemize}
|
| 213 |
+
|
| 214 |
+
\subsubsection{Chức năng Dự đoán Đơn lẻ (Single Prediction)}
|
| 215 |
+
|
| 216 |
+
Đây là chức năng cơ bản nhất, cho phép người dùng nhập một comment tiếng Việt và nhận kết quả dự đoán.
|
| 217 |
+
|
| 218 |
+
\textbf{Quy trình sử dụng:}
|
| 219 |
+
\begin{enumerate}
|
| 220 |
+
\item Chọn tab "Single Comment".
|
| 221 |
+
\item Nhập comment tiếng Việt vào textarea.
|
| 222 |
+
\item (Tùy chọn) Bật checkbox "Bao gồm giải thích AI" để nhận word importance.
|
| 223 |
+
\item Nhấn nút "Predict Rating".
|
| 224 |
+
\item Xem kết quả hiển thị bên dưới.
|
| 225 |
+
\end{enumerate}
|
| 226 |
+
|
| 227 |
+
\begin{figure}[H]
|
| 228 |
+
\centering
|
| 229 |
+
\includegraphics[width=0.85\textwidth]{images/ket_qua_du_doan.png}
|
| 230 |
+
\caption{Kết quả dự đoán với Rating, Confidence và Keyword Highlighting}
|
| 231 |
+
\label{fig:ket_qua_du_doan}
|
| 232 |
+
\end{figure}
|
| 233 |
+
|
| 234 |
+
\textbf{Thông tin hiển thị trong kết quả:}
|
| 235 |
+
\begin{itemize}
|
| 236 |
+
\item \textbf{Predicted Rating}: Số sao dự đoán từ 1-5, hiển thị dạng số và icon sao.
|
| 237 |
+
\item \textbf{Confidence Score}: Độ tin cậy của dự đoán (0-100\%).
|
| 238 |
+
\item \textbf{Highlighted Comment}: Comment gốc với các keyword tích cực (xanh) và tiêu cực (đỏ) được highlight.
|
| 239 |
+
\item \textbf{Keywords Found}: Danh sách các keyword tích cực/tiêu cực được phát hiện.
|
| 240 |
+
\item \textbf{AI Explanation} (nếu bật): Biểu đồ word importance thể hiện ảnh hưởng của từng từ đến kết quả.
|
| 241 |
+
\end{itemize}
|
| 242 |
+
|
| 243 |
+
\subsubsection{Chức năng Dự đoán Hàng loạt (Batch Prediction)}
|
| 244 |
+
|
| 245 |
+
Tính năng này cho phép upload file CSV chứa nhiều comment để dự đoán đồng thời, phù hợp cho việc phân tích dữ liệu lớn.
|
| 246 |
+
|
| 247 |
+
\textbf{Yêu cầu file CSV:}
|
| 248 |
+
\begin{lstlisting}[language=Python]
|
| 249 |
+
Comment
|
| 250 |
+
"San pham rat tot, dong goi can than"
|
| 251 |
+
"Chat luong kem, khong nhu mo ta"
|
| 252 |
+
"Giao hang nhanh, san pham on"
|
| 253 |
+
"Rat hai long voi san pham nay"
|
| 254 |
+
\end{lstlisting}
|
| 255 |
+
|
| 256 |
+
\textbf{Kết quả Batch Prediction bao gồm:}
|
| 257 |
+
|
| 258 |
+
\begin{itemize}
|
| 259 |
+
\item \textbf{Rating Distribution Chart}: Biểu đồ tròn/cột thể hiện phân bố số lượng comment theo từng mức rating.
|
| 260 |
+
\item \textbf{Word Cloud}: Đám mây từ khóa phổ biến trong các comment, kích thước từ tỷ lệ với tần suất xuất hiện.
|
| 261 |
+
\item \textbf{N-gram Analysis}: Phân tích các cụm từ phổ biến (unigrams, bigrams, trigrams).
|
| 262 |
+
\item \textbf{Keyword Frequency}: Thống kê tần suất xuất hiện của các keyword tích cực và tiêu cực.
|
| 263 |
+
\item \textbf{Results Table}: Bảng chi tiết kết quả dự đoán cho từng comment.
|
| 264 |
+
\item \textbf{Export Options}: Xuất kết quả ra file CSV hoặc PDF report.
|
| 265 |
+
\end{itemize}
|
| 266 |
+
|
| 267 |
+
\begin{figure}[H]
|
| 268 |
+
\centering
|
| 269 |
+
\includegraphics[width=0.9\textwidth]{images/batch_result.png}
|
| 270 |
+
\caption{Kết quả Batch Prediction với biểu đồ và Word Cloud}
|
| 271 |
+
\label{fig:batch_result}
|
| 272 |
+
\end{figure}
|
| 273 |
+
|
| 274 |
+
\subsubsection{Các Tính năng Nâng cao}
|
| 275 |
+
|
| 276 |
+
\textbf{1. Keyword Highlighting:}
|
| 277 |
+
|
| 278 |
+
Hệ thống tự động nhận diện và highlight các từ khóa tích cực/tiêu cực trong comment:
|
| 279 |
+
|
| 280 |
+
\vietnameselst
|
| 281 |
+
\begin{lstlisting}[language=Python]
|
| 282 |
+
class KeywordAnalyzer:
|
| 283 |
+
def __init__(self):
|
| 284 |
+
# Tu khoa tich cuc
|
| 285 |
+
self.positive_words = [
|
| 286 |
+
'tot', 'dep', 'tuyet voi', 'xuat sac', 'hoan hao',
|
| 287 |
+
'chat luong', 'nhanh', 'hai long', 'ung', 'de thuong'
|
| 288 |
+
]
|
| 289 |
+
# Tu khoa tieu cuc
|
| 290 |
+
self.negative_words = [
|
| 291 |
+
'te', 'xau', 'kem', 'that vong', 'loi', 'hong',
|
| 292 |
+
'cham', 'gia', 'dat', 'khong dang'
|
| 293 |
+
]
|
| 294 |
+
|
| 295 |
+
def analyze(self, text: str) -> Dict:
|
| 296 |
+
"""Phan tich text va tra ve cac keyword tim thay"""
|
| 297 |
+
found_positive = [w for w in self.positive_words if w in text.lower()]
|
| 298 |
+
found_negative = [w for w in self.negative_words if w in text.lower()]
|
| 299 |
+
return {
|
| 300 |
+
'positive_keywords': found_positive,
|
| 301 |
+
'negative_keywords': found_negative
|
| 302 |
+
}
|
| 303 |
+
\end{lstlisting}
|
| 304 |
+
|
| 305 |
+
\textbf{2. N-gram Analysis:}
|
| 306 |
+
|
| 307 |
+
Phân tích các cụm từ phổ biến giúp hiểu rõ hơn về nội dung các review:
|
| 308 |
+
\begin{itemize}
|
| 309 |
+
\item \textbf{Unigrams} (1 từ): "tốt", "đẹp", "nhanh"
|
| 310 |
+
\item \textbf{Bigrams} (2 từ): "giao hàng", "chất lượng", "đóng gói"
|
| 311 |
+
\item \textbf{Trigrams} (3 từ): "rất hài lòng", "giao hàng nhanh", "đúng như mô tả"
|
| 312 |
+
\end{itemize}
|
| 313 |
+
|
| 314 |
+
\textbf{3. Word Importance Explanation:}
|
| 315 |
+
|
| 316 |
+
Khi bật tùy chọn "AI Explanation", hệ thống sẽ hiển thị mức độ ảnh hưởng của từng từ đến kết quả dự đoán, giúp người dùng hiểu tại sao model đưa ra kết quả như vậy.
|
| 317 |
+
|
| 318 |
+
\subsubsection{Trải nghiệm Người dùng (UX)}
|
| 319 |
+
|
| 320 |
+
Giao diện được thiết kế với các nguyên tắc UX hiện đại:
|
| 321 |
+
|
| 322 |
+
\begin{itemize}
|
| 323 |
+
\item \textbf{Responsive Design}: Tự động điều chỉnh layout trên mọi kích thước màn hình (desktop, tablet, mobile).
|
| 324 |
+
\item \textbf{Loading States}: Hiển thị skeleton loading khi đang xử lý request, giúp người dùng biết hệ thống đang hoạt động.
|
| 325 |
+
\item \textbf{Real-time Feedback}: Kết quả hiển thị ngay trên trang mà không cần reload.
|
| 326 |
+
\item \textbf{Dark Mode Support}: Hỗ trợ chế độ tối để giảm mỏi mắt khi làm việc lâu.
|
| 327 |
+
\item \textbf{Error Handling}: Thông báo lỗi rõ ràng khi có vấn đề xảy ra.
|
| 328 |
+
\end{itemize}
|
| 329 |
+
|
| 330 |
+
% ============================================
|
| 331 |
+
% PHẦN 3: TRIỂN KHAI (DEPLOYMENT)
|
| 332 |
+
% ============================================
|
| 333 |
+
\subsection{Triển khai Ứng dụng (Deployment)}
|
| 334 |
+
|
| 335 |
+
Ứng dụng được thiết kế để có thể triển khai trên nhiều môi trường khác nhau, từ local development đến các nền tảng cloud.
|
| 336 |
+
|
| 337 |
+
\subsubsection{Triển khai trên Cloud - Hugging Face Spaces}
|
| 338 |
+
|
| 339 |
+
\textbf{Hugging Face Spaces} là nền tảng hosting miễn phí cho các ứng dụng Machine Learning, được chọn vì các lý do:
|
| 340 |
+
|
| 341 |
+
\begin{itemize}
|
| 342 |
+
\item \textbf{Miễn phí}: Cung cấp CPU với 16GB RAM miễn phí, đủ để chạy model PhoBERT.
|
| 343 |
+
\item \textbf{Docker Support}: Hỗ trợ Docker SDK, cho phép deploy các ứng dụng phức tạp.
|
| 344 |
+
\item \textbf{Git-based Deployment}: Deploy bằng git push, dễ dàng cập nhật.
|
| 345 |
+
\item \textbf{Integrated với Hugging Face Hub}: Dễ dàng tải model từ Hub.
|
| 346 |
+
\end{itemize}
|
| 347 |
+
|
| 348 |
+
\textbf{Cấu hình Dockerfile cho Hugging Face Spaces:}
|
| 349 |
+
|
| 350 |
+
\begin{lstlisting}[language=Python]
|
| 351 |
+
# Dockerfile for Hugging Face Spaces
|
| 352 |
+
FROM python:3.10-slim
|
| 353 |
+
|
| 354 |
+
# Cai dat font cho WordCloud
|
| 355 |
+
RUN apt-get update && apt-get install -y \
|
| 356 |
+
fonts-dejavu fonts-dejavu-core build-essential gcc \
|
| 357 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 358 |
+
|
| 359 |
+
# Tao non-root user (bat buoc tren HF Spaces)
|
| 360 |
+
RUN useradd -m -u 1000 user
|
| 361 |
+
WORKDIR /app
|
| 362 |
+
|
| 363 |
+
# Cai dat dependencies
|
| 364 |
+
COPY --chown=user:user requirements.txt .
|
| 365 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 366 |
+
|
| 367 |
+
# Copy source code
|
| 368 |
+
COPY --chown=user:user . .
|
| 369 |
+
|
| 370 |
+
# Tao thu muc can thiet
|
| 371 |
+
RUN mkdir -p /app/app/static/uploads/wordclouds && \
|
| 372 |
+
mkdir -p /app/app/database && \
|
| 373 |
+
chmod -R 777 /app/app/static/uploads
|
| 374 |
+
|
| 375 |
+
USER user
|
| 376 |
+
EXPOSE 7860
|
| 377 |
+
|
| 378 |
+
# Start FastAPI (port 7860 bat buoc tren HF Spaces)
|
| 379 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
| 380 |
+
\end{lstlisting}
|
| 381 |
+
|
| 382 |
+
\textbf{Biến môi trường cần thiết:}
|
| 383 |
+
\begin{itemize}
|
| 384 |
+
\item \texttt{DATABASE\_URL}: Connection string đến PostgreSQL database.
|
| 385 |
+
\item \texttt{SECRET\_KEY}: Key bí mật để mã hóa JWT token.
|
| 386 |
+
\end{itemize}
|
| 387 |
+
|
| 388 |
+
\textbf{Link Demo ứng dụng:}
|
| 389 |
+
|
| 390 |
+
Ứng dụng hiện đang hoạt động tại: \url{https://huggingface.co/spaces/vtdung23/RatingPrediction}
|
| 391 |
+
|
| 392 |
+
\subsubsection{Triển khai Local - Cài đặt trên Máy Cá nhân}
|
| 393 |
+
|
| 394 |
+
Hướng dẫn từng bước để chạy ứng dụng trên máy tính cá nhân:
|
| 395 |
+
|
| 396 |
+
\textbf{Yêu cầu hệ thống:}
|
| 397 |
+
\begin{itemize}
|
| 398 |
+
\item Python 3.10 trở lên
|
| 399 |
+
\item Git (để clone repository)
|
| 400 |
+
\item RAM tối thiểu 8GB (khuyến nghị 16GB cho model PhoBERT)
|
| 401 |
+
\item Dung lượng ổ cứng trống: 3GB (cho dependencies và model)
|
| 402 |
+
\end{itemize}
|
| 403 |
+
|
| 404 |
+
\textbf{Bước 1: Clone Project từ Git}
|
| 405 |
+
|
| 406 |
+
\begin{lstlisting}[language=bash]
|
| 407 |
+
# Clone repository
|
| 408 |
+
git clone https://github.com/your-username/rating-prediction.git
|
| 409 |
+
|
| 410 |
+
# Di chuyen vao thu muc project
|
| 411 |
+
cd rating-prediction
|
| 412 |
+
\end{lstlisting}
|
| 413 |
+
|
| 414 |
+
\textbf{Bước 2: Tạo và Kích hoạt Môi trường ảo}
|
| 415 |
+
|
| 416 |
+
\vietnameselst
|
| 417 |
+
\begin{lstlisting}[language=bash]
|
| 418 |
+
# Option A: Su dung Conda (khuyen nghi)
|
| 419 |
+
conda create -p ./env python=3.10 -y
|
| 420 |
+
conda activate ./env
|
| 421 |
+
|
| 422 |
+
# Option B: Su dung venv
|
| 423 |
+
python -m venv env
|
| 424 |
+
|
| 425 |
+
# Kich hoat tren Windows:
|
| 426 |
+
env\Scripts\activate
|
| 427 |
+
|
| 428 |
+
# Kich hoat tren Linux/Mac:
|
| 429 |
+
source env/bin/activate
|
| 430 |
+
\end{lstlisting}
|
| 431 |
+
|
| 432 |
+
\textbf{Bước 3: Cài đặt Thư viện}
|
| 433 |
+
|
| 434 |
+
\begin{lstlisting}[language=bash]
|
| 435 |
+
# Cai dat tat ca dependencies tu requirements.txt
|
| 436 |
+
pip install -r requirements.txt
|
| 437 |
+
|
| 438 |
+
# Qua trinh nay co the mat 5-10 phut
|
| 439 |
+
# tuy thuoc vao toc do mang (can tai PyTorch, Transformers)
|
| 440 |
+
\end{lstlisting}
|
| 441 |
+
|
| 442 |
+
\textbf{Bước 4: Chạy Ứng dụng}
|
| 443 |
+
|
| 444 |
+
\begin{lstlisting}[language=bash]
|
| 445 |
+
# Chay server development
|
| 446 |
+
python main.py
|
| 447 |
+
|
| 448 |
+
# Hoac su dung uvicorn truc tiep
|
| 449 |
+
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
| 450 |
+
\end{lstlisting}
|
| 451 |
+
|
| 452 |
+
\textbf{Bước 5: Truy cập Ứng dụng}
|
| 453 |
+
|
| 454 |
+
Sau khi server khởi động thành công, mở trình duyệt và truy cập:
|
| 455 |
+
|
| 456 |
+
\begin{itemize}
|
| 457 |
+
\item \textbf{Dashboard}: \url{http://localhost:8000}
|
| 458 |
+
\item \textbf{Swagger API Docs}: \url{http://localhost:8000/docs}
|
| 459 |
+
\item \textbf{ReDoc}: \url{http://localhost:8000/redoc}
|
| 460 |
+
\end{itemize}
|
| 461 |
+
|
| 462 |
+
\textbf{Lưu ý quan trọng:}
|
| 463 |
+
\begin{itemize}
|
| 464 |
+
\item Lần đầu tiên thực hiện dự đoán, hệ thống sẽ tự động tải model PhoBERT từ Hugging Face Hub (~500MB). Quá trình này có thể mất vài phút tùy tốc độ mạng.
|
| 465 |
+
\item Đảm bảo máy tính có đủ RAM trống (ít nhất 4GB) khi model được load.
|
| 466 |
+
\item Trên Windows, nếu gặp lỗi với thư viện \texttt{underthesea}, cần cài đặt Visual C++ Build Tools.
|
| 467 |
+
\end{itemize}
|
| 468 |
+
|
| 469 |
+
\subsubsection{So sánh các Phương pháp Triển khai}
|
| 470 |
+
|
| 471 |
+
\begin{table}[H]
|
| 472 |
+
\centering
|
| 473 |
+
\begin{tabular}{|l|c|c|}
|
| 474 |
+
\hline
|
| 475 |
+
\textbf{Tiêu chí} & \textbf{Hugging Face Spaces} & \textbf{Local} \\
|
| 476 |
+
\hline
|
| 477 |
+
Chi phí & Miễn phí (CPU 16GB) & Miễn phí \\
|
| 478 |
+
\hline
|
| 479 |
+
Cấu hình & Docker required & Chỉ cần Python \\
|
| 480 |
+
\hline
|
| 481 |
+
Truy cập & Public URL & localhost only \\
|
| 482 |
+
\hline
|
| 483 |
+
Uptime & 24/7 (auto sleep) & Khi máy bật \\
|
| 484 |
+
\hline
|
| 485 |
+
Phù hợp & Demo, Production & Development, Testing \\
|
| 486 |
+
\hline
|
| 487 |
+
\end{tabular}
|
| 488 |
+
\caption{So sánh các phương pháp triển khai}
|
| 489 |
+
\label{tab:deploy_comparison}
|
| 490 |
+
\end{table}
|
| 491 |
+
|
| 492 |
+
\subsubsection{Cấu trúc Thư mục Project}
|
| 493 |
+
|
| 494 |
+
\begin{lstlisting}[language=bash]
|
| 495 |
+
RatingPrediction/
|
| 496 |
+
|-- main.py # Entry point FastAPI
|
| 497 |
+
|-- requirements.txt # Python dependencies
|
| 498 |
+
|-- Dockerfile # Docker configuration
|
| 499 |
+
|-- app/
|
| 500 |
+
| |-- config.py # Cau hinh ung dung
|
| 501 |
+
| |-- database.py # Database connection
|
| 502 |
+
| |-- models.py # SQLAlchemy models
|
| 503 |
+
| |-- schemas.py # Pydantic schemas
|
| 504 |
+
| |-- routers/
|
| 505 |
+
| | |-- auth.py # Authentication endpoints
|
| 506 |
+
| | |-- prediction.py # Prediction endpoints
|
| 507 |
+
| | |-- dashboard.py # Dashboard pages
|
| 508 |
+
| |-- services/
|
| 509 |
+
| | |-- auth_service.py # JWT & password handling
|
| 510 |
+
| | |-- ml_service.py # ML prediction logic
|
| 511 |
+
| | |-- visualization_service.py # Charts, WordCloud
|
| 512 |
+
| |-- templates/ # Jinja2 HTML templates
|
| 513 |
+
| |-- static/ # CSS, JS, uploaded files
|
| 514 |
+
\end{lstlisting}
|
| 515 |
+
|
| 516 |
+
Với cấu trúc module hóa này, việc bảo trì và mở rộng ứng dụng trở nên dễ dàng. Mỗi thành phần có trách nhiệm riêng biệt, tuân theo nguyên tắc Single Responsibility Principle.
|
app/database/rating_prediction.db
CHANGED
|
Binary files a/app/database/rating_prediction.db and b/app/database/rating_prediction.db differ
|
|
|
app/services/__pycache__/ml_service.cpython-313.pyc
CHANGED
|
Binary files a/app/services/__pycache__/ml_service.cpython-313.pyc and b/app/services/__pycache__/ml_service.cpython-313.pyc differ
|
|
|
app/services/report_service.py
CHANGED
|
@@ -31,25 +31,95 @@ class ReportService:
|
|
| 31 |
|
| 32 |
def __init__(self):
|
| 33 |
self.styles = getSampleStyleSheet()
|
| 34 |
-
self._setup_custom_styles()
|
| 35 |
self._setup_fonts()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
def _setup_fonts(self):
|
| 38 |
"""Setup fonts for Vietnamese character support"""
|
|
|
|
|
|
|
|
|
|
| 39 |
try:
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
except Exception as e:
|
| 45 |
-
# If fonts not found,
|
| 46 |
-
print(f"
|
|
|
|
|
|
|
| 47 |
|
| 48 |
def _setup_custom_styles(self):
|
| 49 |
"""Setup custom paragraph styles"""
|
| 50 |
-
# Use
|
| 51 |
-
font_name =
|
| 52 |
-
font_name_bold =
|
| 53 |
|
| 54 |
self.styles.add(ParagraphStyle(
|
| 55 |
name='CustomTitle',
|
|
|
|
| 31 |
|
| 32 |
def __init__(self):
|
| 33 |
self.styles = getSampleStyleSheet()
|
|
|
|
| 34 |
self._setup_fonts()
|
| 35 |
+
self._setup_custom_styles()
|
| 36 |
+
|
| 37 |
+
def _get_font_path(self):
|
| 38 |
+
"""Get font path based on OS"""
|
| 39 |
+
import platform
|
| 40 |
+
import os
|
| 41 |
+
|
| 42 |
+
system = platform.system()
|
| 43 |
+
|
| 44 |
+
# Define possible font paths for different OS
|
| 45 |
+
font_paths = {
|
| 46 |
+
'Linux': [
|
| 47 |
+
'/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
|
| 48 |
+
'/usr/share/fonts/TTF/DejaVuSans.ttf',
|
| 49 |
+
],
|
| 50 |
+
'Windows': [
|
| 51 |
+
'C:/Windows/Fonts/arial.ttf',
|
| 52 |
+
'C:/Windows/Fonts/segoeui.ttf',
|
| 53 |
+
'C:/Windows/Fonts/tahoma.ttf',
|
| 54 |
+
],
|
| 55 |
+
'Darwin': [ # macOS
|
| 56 |
+
'/Library/Fonts/Arial.ttf',
|
| 57 |
+
'/System/Library/Fonts/Helvetica.ttc',
|
| 58 |
+
]
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
font_bold_paths = {
|
| 62 |
+
'Linux': [
|
| 63 |
+
'/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
|
| 64 |
+
'/usr/share/fonts/TTF/DejaVuSans-Bold.ttf',
|
| 65 |
+
],
|
| 66 |
+
'Windows': [
|
| 67 |
+
'C:/Windows/Fonts/arialbd.ttf',
|
| 68 |
+
'C:/Windows/Fonts/segoeuib.ttf',
|
| 69 |
+
'C:/Windows/Fonts/tahomabd.ttf',
|
| 70 |
+
],
|
| 71 |
+
'Darwin': [
|
| 72 |
+
'/Library/Fonts/Arial Bold.ttf',
|
| 73 |
+
]
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
paths = font_paths.get(system, font_paths['Linux'])
|
| 77 |
+
bold_paths = font_bold_paths.get(system, font_bold_paths['Linux'])
|
| 78 |
+
|
| 79 |
+
font_path = None
|
| 80 |
+
font_bold_path = None
|
| 81 |
+
|
| 82 |
+
for path in paths:
|
| 83 |
+
if os.path.exists(path):
|
| 84 |
+
font_path = path
|
| 85 |
+
break
|
| 86 |
+
|
| 87 |
+
for path in bold_paths:
|
| 88 |
+
if os.path.exists(path):
|
| 89 |
+
font_bold_path = path
|
| 90 |
+
break
|
| 91 |
+
|
| 92 |
+
return font_path, font_bold_path
|
| 93 |
|
| 94 |
def _setup_fonts(self):
|
| 95 |
"""Setup fonts for Vietnamese character support"""
|
| 96 |
+
self.font_name = 'Helvetica'
|
| 97 |
+
self.font_name_bold = 'Helvetica-Bold'
|
| 98 |
+
|
| 99 |
try:
|
| 100 |
+
font_path, font_bold_path = self._get_font_path()
|
| 101 |
+
|
| 102 |
+
if font_path:
|
| 103 |
+
pdfmetrics.registerFont(TTFont('CustomFont', font_path))
|
| 104 |
+
self.font_name = 'CustomFont'
|
| 105 |
+
print(f"✅ Loaded font: {font_path}")
|
| 106 |
+
|
| 107 |
+
if font_bold_path:
|
| 108 |
+
pdfmetrics.registerFont(TTFont('CustomFontBold', font_bold_path))
|
| 109 |
+
self.font_name_bold = 'CustomFontBold'
|
| 110 |
+
print(f"✅ Loaded bold font: {font_bold_path}")
|
| 111 |
+
|
| 112 |
except Exception as e:
|
| 113 |
+
# If fonts not found, use default Helvetica
|
| 114 |
+
print(f"⚠️ Using default fonts (Helvetica): {e}")
|
| 115 |
+
self.font_name = 'Helvetica'
|
| 116 |
+
self.font_name_bold = 'Helvetica-Bold'
|
| 117 |
|
| 118 |
def _setup_custom_styles(self):
|
| 119 |
"""Setup custom paragraph styles"""
|
| 120 |
+
# Use dynamically loaded fonts
|
| 121 |
+
font_name = self.font_name
|
| 122 |
+
font_name_bold = self.font_name_bold
|
| 123 |
|
| 124 |
self.styles.add(ParagraphStyle(
|
| 125 |
name='CustomTitle',
|