Spaces:
Sleeping
Sleeping
quocdat1964 commited on
Commit ·
bddeee9
1
Parent(s): 7b60154
update
Browse files- .env.example +0 -31
- plan.txt +0 -9
- test_api.py +0 -267
.env.example
DELETED
|
@@ -1,31 +0,0 @@
|
|
| 1 |
-
# ============================
|
| 2 |
-
# HuggingFace Configuration
|
| 3 |
-
# ============================
|
| 4 |
-
|
| 5 |
-
# Tên repository model trên HuggingFace (BẮT BUỘC - thay bằng repo của bạn)
|
| 6 |
-
MODEL_NAME=DeeAteTee/gemma-300m-it-matching-vn
|
| 7 |
-
# Token xác thực HuggingFace (chỉ cần nếu repo là PRIVATE)
|
| 8 |
-
|
| 9 |
-
# ============================
|
| 10 |
-
# Model Configuration
|
| 11 |
-
# ============================
|
| 12 |
-
|
| 13 |
-
# HuggingFace token để tải model private
|
| 14 |
-
HF_TOKEN=your_hf_token_here
|
| 15 |
-
|
| 16 |
-
# Google API Key cho Gemini / Google AI (Bắt buộc để trích xuất CV)
|
| 17 |
-
GOOGLE_API_KEY=your_google_api_key_here
|
| 18 |
-
|
| 19 |
-
# Cấu hình thiết bị chạy: "cpu" hoặc "cuda"
|
| 20 |
-
DEVICE=cpu
|
| 21 |
-
|
| 22 |
-
# Giới hạn độ dài tối đa input text (tính theo tokens)
|
| 23 |
-
MAX_LENGTH=512
|
| 24 |
-
|
| 25 |
-
# ============================
|
| 26 |
-
# MiniLM Configuration
|
| 27 |
-
# ============================
|
| 28 |
-
|
| 29 |
-
# Model MiniLM cho behavior-based matching (public, khong can token)
|
| 30 |
-
MODEL_NAME_MINILM=sentence-transformers/all-MiniLM-L6-v2
|
| 31 |
-
MAX_LENGTH_MINILM=256
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
plan.txt
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
Luồng Triển Khai Tự Động Hóa AI API (FastAPI -> Docker -> AWS)
|
| 2 |
-
1. Xây dựng Service với FastAPI (Tải Model 1 Lần)
|
| 3 |
-
Bắt đầu bằng việc tạo một ứng dụng FastAPI để làm "lớp vỏ giao tiếp" giúp các ứng dụng khác gửi văn bản và nhận lại vector nhúng. Trong tệp mã nguồn main.py, bạn tiến hành khởi tạo ứng dụng FastAPI và thiết lập đoạn code để tải mô hình trực tiếp từ Hugging Face. Điểm mấu chốt ở đây là phải cấu hình để tải mô hình duy nhất một lần dưới dạng biến toàn cục (global variable) ngay khi ứng dụng khởi động. Việc này đảm bảo mô hình nằm sẵn trong RAM, tránh việc phải tải lại từ đầu mỗi khi có request mới. Kế tiếp, bạn cần định nghĩa hai endpoint chính: một endpoint GET (Health Check) trả về trạng thái hoạt động để xác nhận API đang chạy , và một endpoint POST xử lý Embedding. Endpoint này sẽ nhận văn bản đầu vào, truyền qua mô hình đã được load sẵn để trích xuất vector, và trả toàn bộ kết quả về dưới dạng JSON. Trước khi sang bước sau, hãy chạy thử cục bộ bằng lệnh fastapi dev main.py để đảm bảo API hoạt động trơn tru trên port 8000.
|
| 4 |
-
|
| 5 |
-
2. Đóng gói Hệ Thống với Docker
|
| 6 |
-
Để ứng dụng có thể chạy ổn định trên mọi máy chủ mà không lo lỗi môi trường, hãy đóng gói tất cả vào một Docker container. Tạo một tệp requirements.txt chứa danh sách các thư viện Python đã sử dụng (như fastapi, torch, transformers). Tiếp theo, thiết lập tệp Dockerfile với các bước tuần tự: chọn base image là Python chính thức (ví dụ: python:3.10) , tạo thư mục làm việc /code , copy tệp requirements.txt vào container và thực thi lệnh cài đặt. Sau khi cài xong thư viện, copy toàn bộ mã nguồn của bạn vào thư mục /code , và thiết lập lệnh (Command) để tự động khởi chạy server uvicorn/fastapi mỗi khi container được bật. Hoàn thiện việc đóng gói bằng cách chạy lệnh docker build để tạo image. Cuối cùng, tạo một repository trên Docker Hub , đổi tên (tag) cho image local khớp với repository này , và thực thi docker push để đưa image lên kho lưu trữ đám mây, sẵn sàng cho việc triển khai.
|
| 7 |
-
|
| 8 |
-
3. Triển khai Vận hành trên AWS ECS (Fargate)
|
| 9 |
-
Khi image đã sẵn sàng trên Docker Hub, bước cuối là đưa nó lên môi trường Internet thông qua Amazon Elastic Container Service (ECS). Truy cập AWS, tạo một Task Definition mới sử dụng hạ tầng AWS Fargate. Fargate sẽ tự động hóa việc quản lý máy chủ vật lý cho bạn. Tại đây, bạn cần chỉ định đủ tài nguyên RAM và CPU cho container, vì mô hình 300 triệu tham số sẽ cần một không gian nhớ nhất định để load thành công. Dán URL của Docker image vào cấu hình và mở port tương ứng (ví dụ 80 hoặc 8000). Tiếp đó, tạo một Cluster dùng Fargate và khởi tạo một Service bên trong Cluster này để đảm bảo API của bạn được duy trì chạy liên tục 24/7. Một thao tác bắt buộc để API có thể nhận được request từ bên ngoài là cấu hình Security Groups: bạn phải thêm Inbound Rules cho phép lưu lượng truy cập mạng (ví dụ 0.0.0.0/0) đi vào đúng port mà ứng dụng đang mở. Hoàn thành luồng bằng cách lấy Public IP được cấp và gọi thử một API request để nghiệm thu hệ thống.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
test_api.py
DELETED
|
@@ -1,267 +0,0 @@
|
|
| 1 |
-
"""Script test nhanh cho Embedding API"""
|
| 2 |
-
from urllib.request import urlopen, Request
|
| 3 |
-
from urllib.error import HTTPError
|
| 4 |
-
import json
|
| 5 |
-
|
| 6 |
-
BASE_URL = "http://localhost:8000"
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
def test_health_check():
|
| 10 |
-
print("=" * 50)
|
| 11 |
-
print("TEST 1: Health Check (GET /health)")
|
| 12 |
-
print("=" * 50)
|
| 13 |
-
r = urlopen(f"{BASE_URL}/health")
|
| 14 |
-
data = json.loads(r.read())
|
| 15 |
-
print(f" Status: {r.status}")
|
| 16 |
-
print(f" Response: {data}")
|
| 17 |
-
assert data["status"] == "ok", "Health check failed!"
|
| 18 |
-
print(" => PASSED\n")
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def test_single_text():
|
| 22 |
-
print("=" * 50)
|
| 23 |
-
print("TEST 2: Single Text Embedding (POST /embed)")
|
| 24 |
-
print("=" * 50)
|
| 25 |
-
payload = {"text": "hello world"}
|
| 26 |
-
req = Request(
|
| 27 |
-
f"{BASE_URL}/embed",
|
| 28 |
-
data=json.dumps(payload).encode(),
|
| 29 |
-
headers={"Content-Type": "application/json"},
|
| 30 |
-
)
|
| 31 |
-
try:
|
| 32 |
-
r = urlopen(req)
|
| 33 |
-
data = json.loads(r.read())
|
| 34 |
-
print(f" Status: {r.status}")
|
| 35 |
-
print(f" Dimension: {data['dimension']}")
|
| 36 |
-
print(f" Num texts: {data['num_texts']}")
|
| 37 |
-
print(f" Vector (first 5 values): {data['embeddings'][0][:5]}")
|
| 38 |
-
assert data["num_texts"] == 1, "Expected 1 text"
|
| 39 |
-
assert data["dimension"] > 0, "Dimension should be > 0"
|
| 40 |
-
print(" => PASSED\n")
|
| 41 |
-
except HTTPError as e:
|
| 42 |
-
error_body = e.read().decode('utf-8', errors='replace')
|
| 43 |
-
print(f" ERROR {e.code}: {error_body.encode('ascii', 'backslashreplace').decode('ascii')}\n")
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def test_batch_texts():
|
| 47 |
-
print("=" * 50)
|
| 48 |
-
print("TEST 3: Batch Texts Embedding (POST /embed)")
|
| 49 |
-
print("=" * 50)
|
| 50 |
-
payload = {"texts": ["Python developer", "Data analyst", "Machine learning engineer"]}
|
| 51 |
-
req = Request(
|
| 52 |
-
f"{BASE_URL}/embed",
|
| 53 |
-
data=json.dumps(payload).encode(),
|
| 54 |
-
headers={"Content-Type": "application/json"},
|
| 55 |
-
)
|
| 56 |
-
try:
|
| 57 |
-
r = urlopen(req)
|
| 58 |
-
data = json.loads(r.read())
|
| 59 |
-
print(f" Status: {r.status}")
|
| 60 |
-
print(f" Dimension: {data['dimension']}")
|
| 61 |
-
print(f" Num texts: {data['num_texts']}")
|
| 62 |
-
print(f" Num vectors: {len(data['embeddings'])}")
|
| 63 |
-
assert data["num_texts"] == 3, "Expected 3 texts"
|
| 64 |
-
assert len(data["embeddings"]) == 3, "Expected 3 vectors"
|
| 65 |
-
print(" => PASSED\n")
|
| 66 |
-
except HTTPError as e:
|
| 67 |
-
print(f" ERROR {e.code}: {e.read().decode()}\n")
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def test_empty_input():
|
| 71 |
-
print("=" * 50)
|
| 72 |
-
print("TEST 4: Empty Input (POST /embed) - Should return 400")
|
| 73 |
-
print("=" * 50)
|
| 74 |
-
payload = {}
|
| 75 |
-
req = Request(
|
| 76 |
-
f"{BASE_URL}/embed",
|
| 77 |
-
data=json.dumps(payload).encode(),
|
| 78 |
-
headers={"Content-Type": "application/json"},
|
| 79 |
-
)
|
| 80 |
-
try:
|
| 81 |
-
r = urlopen(req)
|
| 82 |
-
print(f" UNEXPECTED: Got status {r.status} (expected 400)\n")
|
| 83 |
-
except HTTPError as e:
|
| 84 |
-
body = json.loads(e.read().decode())
|
| 85 |
-
print(f" Status: {e.code}")
|
| 86 |
-
detail_msg = body.get('detail', 'N/A')
|
| 87 |
-
print(f" Detail: {detail_msg.encode('ascii', 'backslashreplace').decode('ascii')}")
|
| 88 |
-
if e.code == 400:
|
| 89 |
-
print(" => PASSED (correctly rejected empty input)\n")
|
| 90 |
-
else:
|
| 91 |
-
print(f" => FAILED (expected 400, got {e.code})\n")
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def test_similarity_text():
|
| 95 |
-
print("=" * 50)
|
| 96 |
-
print("TEST 5: Text Similarity (POST /similarity)")
|
| 97 |
-
print("=" * 50)
|
| 98 |
-
payload = {
|
| 99 |
-
"text1": "Python developer with Django experience",
|
| 100 |
-
"text2": "Looking for Python programmer who knows Django framework"
|
| 101 |
-
}
|
| 102 |
-
req = Request(
|
| 103 |
-
f"{BASE_URL}/similarity",
|
| 104 |
-
data=json.dumps(payload).encode(),
|
| 105 |
-
headers={"Content-Type": "application/json"},
|
| 106 |
-
)
|
| 107 |
-
try:
|
| 108 |
-
r = urlopen(req)
|
| 109 |
-
data = json.loads(r.read())
|
| 110 |
-
print(f" Status: {r.status}")
|
| 111 |
-
print(f" Similarity score: {data['similarity_score']}")
|
| 112 |
-
print(f" Percentage: {data['percentage']}%")
|
| 113 |
-
assert 0 <= data["similarity_score"] <= 1, "Score should be between 0 and 1"
|
| 114 |
-
assert data["percentage"] > 0, "Percentage should be > 0"
|
| 115 |
-
print(" => PASSED\n")
|
| 116 |
-
except HTTPError as e:
|
| 117 |
-
error_body = e.read().decode('utf-8', errors='replace')
|
| 118 |
-
print(f" ERROR {e.code}: {error_body.encode('ascii', 'backslashreplace').decode('ascii')}\n")
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
def test_similarity_vectors():
|
| 122 |
-
print("=" * 50)
|
| 123 |
-
print("TEST 6: Vector Similarity (POST /similarity/vectors)")
|
| 124 |
-
print("=" * 50)
|
| 125 |
-
# First, get 2 real vectors from /embed
|
| 126 |
-
texts = ["Software engineer", "Truck driver"]
|
| 127 |
-
req1 = Request(
|
| 128 |
-
f"{BASE_URL}/embed",
|
| 129 |
-
data=json.dumps({"texts": texts}).encode(),
|
| 130 |
-
headers={"Content-Type": "application/json"},
|
| 131 |
-
)
|
| 132 |
-
try:
|
| 133 |
-
r1 = urlopen(req1)
|
| 134 |
-
embed_data = json.loads(r1.read())
|
| 135 |
-
vec1 = embed_data["embeddings"][0]
|
| 136 |
-
vec2 = embed_data["embeddings"][1]
|
| 137 |
-
|
| 138 |
-
# Now compute similarity from vectors
|
| 139 |
-
payload = {"vector1": vec1, "vector2": vec2}
|
| 140 |
-
req2 = Request(
|
| 141 |
-
f"{BASE_URL}/similarity/vectors",
|
| 142 |
-
data=json.dumps(payload).encode(),
|
| 143 |
-
headers={"Content-Type": "application/json"},
|
| 144 |
-
)
|
| 145 |
-
r2 = urlopen(req2)
|
| 146 |
-
data = json.loads(r2.read())
|
| 147 |
-
print(f" Status: {r2.status}")
|
| 148 |
-
print(f" Texts compared: '{texts[0]}' vs '{texts[1]}'")
|
| 149 |
-
print(f" Similarity score: {data['similarity_score']}")
|
| 150 |
-
print(f" Percentage: {data['percentage']}%")
|
| 151 |
-
assert -1 <= data["similarity_score"] <= 1, "Score should be between -1 and 1"
|
| 152 |
-
print(" => PASSED\n")
|
| 153 |
-
except HTTPError as e:
|
| 154 |
-
error_body = e.read().decode('utf-8', errors='replace')
|
| 155 |
-
print(f" ERROR {e.code}: {error_body.encode('ascii', 'backslashreplace').decode('ascii')}\n")
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
def test_similarity_mismatched_vectors():
|
| 159 |
-
print("=" * 50)
|
| 160 |
-
print("TEST 7: Mismatched Vectors (POST /similarity/vectors) - Should return 400")
|
| 161 |
-
print("=" * 50)
|
| 162 |
-
payload = {
|
| 163 |
-
"vector1": [0.1, 0.2, 0.3],
|
| 164 |
-
"vector2": [0.1, 0.2],
|
| 165 |
-
}
|
| 166 |
-
req = Request(
|
| 167 |
-
f"{BASE_URL}/similarity/vectors",
|
| 168 |
-
data=json.dumps(payload).encode(),
|
| 169 |
-
headers={"Content-Type": "application/json"},
|
| 170 |
-
)
|
| 171 |
-
try:
|
| 172 |
-
r = urlopen(req)
|
| 173 |
-
print(f" UNEXPECTED: Got status {r.status} (expected 400)\n")
|
| 174 |
-
except HTTPError as e:
|
| 175 |
-
print(f" Status: {e.code}")
|
| 176 |
-
if e.code == 400:
|
| 177 |
-
print(" => PASSED (correctly rejected mismatched vectors)\n")
|
| 178 |
-
else:
|
| 179 |
-
print(f" => FAILED (expected 400, got {e.code})\n")
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
def test_minilm_single_text():
|
| 183 |
-
print("=" * 50)
|
| 184 |
-
print("TEST 8: MiniLM Single Text (POST /embed/minilm)")
|
| 185 |
-
print("=" * 50)
|
| 186 |
-
payload = {"text": "Python remote HCM"}
|
| 187 |
-
req = Request(
|
| 188 |
-
f"{BASE_URL}/embed/minilm",
|
| 189 |
-
data=json.dumps(payload).encode(),
|
| 190 |
-
headers={"Content-Type": "application/json"},
|
| 191 |
-
)
|
| 192 |
-
try:
|
| 193 |
-
r = urlopen(req)
|
| 194 |
-
data = json.loads(r.read())
|
| 195 |
-
print(f" Status: {r.status}")
|
| 196 |
-
print(f" Dimension: {data['dimension']}")
|
| 197 |
-
print(f" Num texts: {data['num_texts']}")
|
| 198 |
-
assert data["dimension"] == 384, "MiniLM should return 384 dimensions"
|
| 199 |
-
print(" => PASSED\n")
|
| 200 |
-
except HTTPError as e:
|
| 201 |
-
error_body = e.read().decode('utf-8', errors='replace')
|
| 202 |
-
print(f" ERROR {e.code}: {error_body.encode('ascii', 'backslashreplace').decode('ascii')}\n")
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
def test_minilm_batch_texts():
|
| 206 |
-
print("=" * 50)
|
| 207 |
-
print("TEST 9: MiniLM Batch Texts (POST /embed/minilm)")
|
| 208 |
-
print("=" * 50)
|
| 209 |
-
payload = {"texts": ["Python remote HCM", "Product Manager", "BA"]}
|
| 210 |
-
req = Request(
|
| 211 |
-
f"{BASE_URL}/embed/minilm",
|
| 212 |
-
data=json.dumps(payload).encode(),
|
| 213 |
-
headers={"Content-Type": "application/json"},
|
| 214 |
-
)
|
| 215 |
-
try:
|
| 216 |
-
r = urlopen(req)
|
| 217 |
-
data = json.loads(r.read())
|
| 218 |
-
print(f" Status: {r.status}")
|
| 219 |
-
print(f" Dimension: {data['dimension']}")
|
| 220 |
-
print(f" Num texts: {data['num_texts']}")
|
| 221 |
-
assert data["num_texts"] == 3, "Expected 3 texts"
|
| 222 |
-
assert data["dimension"] == 384, "MiniLM should return 384 dimensions"
|
| 223 |
-
print(" => PASSED\n")
|
| 224 |
-
except HTTPError as e:
|
| 225 |
-
error_body = e.read().decode('utf-8', errors='replace')
|
| 226 |
-
print(f" ERROR {e.code}: {error_body.encode('ascii', 'backslashreplace').decode('ascii')}\n")
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
def test_minilm_similarity_text():
|
| 230 |
-
print("=" * 50)
|
| 231 |
-
print("TEST 10: MiniLM Text Similarity (POST /similarity/minilm)")
|
| 232 |
-
print("=" * 50)
|
| 233 |
-
payload = {
|
| 234 |
-
"text1": "Python remote HCM",
|
| 235 |
-
"text2": "Looking for Python developer, remote working"
|
| 236 |
-
}
|
| 237 |
-
req = Request(
|
| 238 |
-
f"{BASE_URL}/similarity/minilm",
|
| 239 |
-
data=json.dumps(payload).encode(),
|
| 240 |
-
headers={"Content-Type": "application/json"},
|
| 241 |
-
)
|
| 242 |
-
try:
|
| 243 |
-
r = urlopen(req)
|
| 244 |
-
data = json.loads(r.read())
|
| 245 |
-
print(f" Status: {r.status}")
|
| 246 |
-
print(f" Similarity score: {data['similarity_score']}")
|
| 247 |
-
print(f" Percentage: {data['percentage']}%")
|
| 248 |
-
assert 0 <= data["similarity_score"] <= 1, "Score should be between 0 and 1"
|
| 249 |
-
print(" => PASSED\n")
|
| 250 |
-
except HTTPError as e:
|
| 251 |
-
error_body = e.read().decode('utf-8', errors='replace')
|
| 252 |
-
print(f" ERROR {e.code}: {error_body.encode('ascii', 'backslashreplace').decode('ascii')}\n")
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
if __name__ == "__main__":
|
| 256 |
-
print("\n*** EMBEDDING API TEST SUITE ***\n")
|
| 257 |
-
test_health_check()
|
| 258 |
-
test_single_text()
|
| 259 |
-
test_batch_texts()
|
| 260 |
-
test_empty_input()
|
| 261 |
-
test_similarity_text()
|
| 262 |
-
test_similarity_vectors()
|
| 263 |
-
test_similarity_mismatched_vectors()
|
| 264 |
-
test_minilm_single_text()
|
| 265 |
-
test_minilm_batch_texts()
|
| 266 |
-
test_minilm_similarity_text()
|
| 267 |
-
print("*** ALL TESTS COMPLETE ***")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|