hungnha commited on
Commit
6bd96d9
·
1 Parent(s): 54b2662

Xoa code cu va cap nhat file moi

Browse files
Files changed (2) hide show
  1. .github/prompts/repo.prompt.md +102 -0
  2. colab.ipynb +447 -0
.github/prompts/repo.prompt.md ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Vai trò: Bạn là một Chuyên gia Kỹ sư AI (AI Engineer) và Lập trình viên Full-stack Python.
2
+
3
+ Nhiệm vụ: Hãy viết mã nguồn chi tiết để xây dựng hệ thống "Chatbot tư vấn học tập và quy chế đào tạo sử dụng mô hình RAG (Retrieval-Augmented Generation)".
4
+
5
+ Bối cảnh bài toán: Hệ thống cần hỗ trợ sinh viên Đại học Bách Khoa Hà Nội tra cứu thông tin từ các văn bản quy chế, quy định học phí và hướng dẫn học vụ. Dữ liệu đầu vào là các file PDF/Word (Unstructured text). Hệ thống cần trả lời chính xác, trích dẫn nguồn, và sử dụng tiếng Việt tự nhiên.
6
+
7
+ Tech Stack yêu cầu:
8
+
9
+ Ngôn ngữ: Python
10
+
11
+ Framework LLM: LangChain
12
+
13
+ Vector Database: ChromaDB.
14
+
15
+ Embedding Model: bkai-foundation-models/vietnamese-bi-encoder (hoặc OpenAI text-embedding-3-small nếu muốn đơn giản).
16
+
17
+ Yêu cầu thực hiện chi tiết theo từng bước (Step-by-step):
18
+
19
+ Bước 1: trước khi bạn muốn chạy hay thực hiện code hãy kiểm tra đã có môi trường conda doan hay chưa nếu chưa thì thực hiện chạy lệnh conda activate doan để kích hoạt môi trường conda doan trước để tránh phát sinh lỗi trong quá trình thực hiện code cũng như đảm bảo các thư viện đã được cài đặt đầy đủ không cài các thư viện vào môi trường hệ thống chính.
20
+
21
+ Bước 2:
22
+
23
+ + Parsing sang Markdown (Structured)
24
+ Sử dụng DocumentConverter của thư viện docling.document_converter.
25
+
26
+ Thực hiện convert file PDF.
27
+
28
+ Xuất kết quả ra định dạng Markdown bằng phương thức .export_to_markdown().
29
+
30
+ +sau khi có chuỗi Markdown sạch từ Docling, sử dụng MarkdownHeaderTextSplitter của LangChain.
31
+
32
+ Cấu hình headers_to_split_on:
33
+
34
+ #: "Chương" (Level 1)
35
+
36
+ ##: "Mục/Điều" (Level 2)
37
+
38
+ ###: "Tiểu mục" (Level 3)
39
+
40
+ Mục tiêu: Đảm bảo khi sinh viên hỏi về một điều luật cụ thể, chunk trả về sẽ chứa trọn vẹn điều luật đó kèm theo tiêu đề.
41
+
42
+ Với các "Điều" có nội dung quá dài (vượt quá 2000 ký tự), tiếp tục chia nhỏ bằng RecursiveCharacterTextSplitter (chunk_size=1000, overlap=200).
43
+
44
+ Code phải đảm bảo metadata của mỗi chunk lưu giữ được nguồn gốc phân cấp.
45
+
46
+ Ví dụ: metadata = {"source": "Quy_che_dao_tao.pdf", "header_1": "Chương 2", "header_2": "Điều 5"}.
47
+
48
+
49
+
50
+
51
+
52
+
53
+ Bước 3: Module Vector Database & Embedding
54
+ Mục tiêu: Tạo bộ truy hồi thông tin (Retriever).
55
+
56
+ Khởi tạo Embedding Model: Ưu tiên model hỗ trợ tiếng Việt tốt.
57
+
58
+ Viết hàm create_vector_db(chunks):
59
+
60
+ Nhúng (Embed) các đoạn văn bản (chunks).
61
+
62
+ Lưu vào ChromaDB (chế độ persistent để không phải chạy lại mỗi lần khởi động).
63
+
64
+ Viết hàm get_retriever(): Trả về đối tượng retriever để sử dụng trong RAG chain.
65
+
66
+ Bước 4: Xây dựng RAG Pipeline (Core Logic)
67
+ Mục tiêu: Kết hợp tìm kiếm và sinh câu trả lời.
68
+
69
+ Thiết kế Prompt Template chuyên biệt cho tư vấn học vụ:
70
+
71
+ System Prompt: "Bạn là trợ lý ảo tuyển sinh và đào tạo của ĐH Bách Khoa Hà Nội. Hãy trả lời dựa trên ngữ cảnh được cung cấp (Context). Nếu không có thông tin trong context, hãy nói là không biết, không được bịa đặt."
72
+
73
+ Xây dựng hàm build_rag_chain(retriever, llm):
74
+
75
+ Sử dụng create_retrieval_chain của LangChain.
76
+
77
+ Đầu vào: Câu hỏi sinh viên.
78
+
79
+ Quy trình: Query -> Retriever (tìm top 3-5 đoạn văn bản liên quan) -> Prompt (Câu hỏi + Context) -> LLM -> Câu trả lời.
80
+
81
+ Bước 5: Phân loại ý định (Intent Classification) - Tùy chọn nâng cao
82
+ Viết một hàm classify_intent(query) nhỏ sử dụng LLM để xác định xem câu hỏi là "Greeting" (Chào hỏi xã giao) hay "Query" (Hỏi thông tin).
83
+
84
+ Nếu là Greeting: Trả lời xã giao ngay lập tức (không cần RAG).
85
+
86
+ Nếu là Query: Chạy qua RAG Pipeline ở Bước 4.
87
+
88
+ Bước 6: Giao diện người dùng (User Interface)
89
+ Sử dụng Streamlit để tạo giao diện chat đơn giản (app.py).
90
+
91
+ Có khung chat (Chat input).
92
+
93
+ Hiển thị lịch sử chat (Chat history).
94
+
95
+ Trong phần trả lời của bot, hiển thị thêm phần "Nguồn tham khảo" (Source documents) để sinh viên biết thông tin lấy từ văn bản nào (Page số mấy).
96
+
97
+ Bước 7: Module hỗ trợ Fine-tuning (Data Preparation for Fine-tuning)
98
+ Lưu ý: Chỉ viết code để sinh dữ liệu, không thực hiện training vì cần GPU.
99
+
100
+ Viết hàm generate_qa_pairs(documents): Sử dụng LLM để tự động sinh ra các cặp câu hỏi - câu trả lời (QA pairs) từ các đoạn văn bản đã chunking. Lưu kết quả ra file JSONL để dùng cho việc Fine-tuning sau này.
101
+
102
+ Hãy viết code đầy đủ, comment giải thích rõ ràng từng hàm (function)
colab.ipynb ADDED
@@ -0,0 +1,447 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": 1,
6
+ "id": "287f0df4",
7
+ "metadata": {},
8
+ "outputs": [
9
+ {
10
+ "ename": "KeyboardInterrupt",
11
+ "evalue": "",
12
+ "output_type": "error",
13
+ "traceback": [
14
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
15
+ "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
16
+ "\u001b[0;32m/tmp/ipython-input-3329394316.py\u001b[0m in \u001b[0;36m<cell line: 0>\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[0;32mfrom\u001b[0m \u001b[0mgoogle\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcolab\u001b[0m \u001b[0;32mimport\u001b[0m \u001b[0mdrive\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 2\u001b[0;31m \u001b[0mdrive\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mmount\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'/content/drive'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mforce_remount\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
17
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/colab/drive.py\u001b[0m in \u001b[0;36mmount\u001b[0;34m(mountpoint, force_remount, timeout_ms, readonly)\u001b[0m\n\u001b[1;32m 95\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mmount\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmountpoint\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mforce_remount\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtimeout_ms\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;36m120000\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mreadonly\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mFalse\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 96\u001b[0m \u001b[0;34m\"\"\"Mount your Google Drive at the specified mountpoint path.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 97\u001b[0;31m return _mount(\n\u001b[0m\u001b[1;32m 98\u001b[0m \u001b[0mmountpoint\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 99\u001b[0m \u001b[0mforce_remount\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mforce_remount\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
18
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/colab/drive.py\u001b[0m in \u001b[0;36m_mount\u001b[0;34m(mountpoint, force_remount, timeout_ms, ephemeral, readonly)\u001b[0m\n\u001b[1;32m 132\u001b[0m )\n\u001b[1;32m 133\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mephemeral\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 134\u001b[0;31m _message.blocking_request(\n\u001b[0m\u001b[1;32m 135\u001b[0m \u001b[0;34m'request_auth'\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 136\u001b[0m \u001b[0mrequest\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;34m{\u001b[0m\u001b[0;34m'authType'\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0;34m'dfs_ephemeral'\u001b[0m\u001b[0;34m}\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
19
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/colab/_message.py\u001b[0m in \u001b[0;36mblocking_request\u001b[0;34m(request_type, request, timeout_sec, parent)\u001b[0m\n\u001b[1;32m 174\u001b[0m \u001b[0mrequest_type\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mrequest\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mparent\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0mparent\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mexpect_reply\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mTrue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 175\u001b[0m )\n\u001b[0;32m--> 176\u001b[0;31m \u001b[0;32mreturn\u001b[0m \u001b[0mread_reply_from_input\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrequest_id\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtimeout_sec\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
20
+ "\u001b[0;32m/usr/local/lib/python3.12/dist-packages/google/colab/_message.py\u001b[0m in \u001b[0;36mread_reply_from_input\u001b[0;34m(message_id, timeout_sec)\u001b[0m\n\u001b[1;32m 94\u001b[0m \u001b[0mreply\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0m_read_next_input_message\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 95\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mreply\u001b[0m \u001b[0;34m==\u001b[0m \u001b[0m_NOT_READY\u001b[0m \u001b[0;32mor\u001b[0m \u001b[0;32mnot\u001b[0m \u001b[0misinstance\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mreply\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdict\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 96\u001b[0;31m \u001b[0mtime\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msleep\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m0.025\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 97\u001b[0m \u001b[0;32mcontinue\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 98\u001b[0m if (\n",
21
+ "\u001b[0;31mKeyboardInterrupt\u001b[0m: "
22
+ ]
23
+ }
24
+ ],
25
+ "source": [
26
+ "from google.colab import drive\n",
27
+ "drive.mount('/content/drive', force_remount=True)"
28
+ ]
29
+ },
30
+ {
31
+ "cell_type": "code",
32
+ "execution_count": null,
33
+ "id": "f6891108",
34
+ "metadata": {},
35
+ "outputs": [],
36
+ "source": [
37
+ "# 2. Install dependencies\n",
38
+ "# Cài đặt hệ thống Tesseract, ngôn ngữ Tiếng Việt và các thư viện development cần thiết để build tesserocr\n",
39
+ "!sudo apt-get update > /dev/null\n",
40
+ "!sudo apt-get install -y tesseract-ocr tesseract-ocr-vie libtesseract-dev libleptonica-dev pkg-config > /dev/null\n",
41
+ "\n",
42
+ "# Cài đặt tesserocr (Python wrapper cho Tesseract) và docling\n",
43
+ "# Lưu ý: tesserocr cần được build từ source nên cần các thư viện dev ở trên\n",
44
+ "!pip install tesserocr docling pypdfium2"
45
+ ]
46
+ },
47
+ {
48
+ "cell_type": "code",
49
+ "execution_count": null,
50
+ "id": "ca42bfce",
51
+ "metadata": {},
52
+ "outputs": [],
53
+ "source": [
54
+ "# 3. Extract Data\n",
55
+ "import os\n",
56
+ "import zipfile\n",
57
+ "\n",
58
+ "# Path to your zip file on Drive (Sử dụng cùng file với Marker để so sánh)\n",
59
+ "zip_path = '/content/drive/MyDrive/marker/files.zip' \n",
60
+ "extract_path = '/content/data/files'\n",
61
+ "\n",
62
+ "if not os.path.exists(extract_path):\n",
63
+ " os.makedirs(extract_path, exist_ok=True)\n",
64
+ " print(f\"Extracting {zip_path}...\")\n",
65
+ " try:\n",
66
+ " with zipfile.ZipFile(zip_path, 'r') as zip_ref:\n",
67
+ " zip_ref.extractall(extract_path)\n",
68
+ " print(\"Done extraction!\")\n",
69
+ " except FileNotFoundError:\n",
70
+ " print(f\"❌ File not found: {zip_path}. Please check the path.\")\n",
71
+ "else:\n",
72
+ " print(\"Files already extracted.\")"
73
+ ]
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "execution_count": null,
78
+ "id": "988f7e96",
79
+ "metadata": {},
80
+ "outputs": [],
81
+ "source": [
82
+ "# 4. Define Processor Class (Refactored for High Quality & Performance with Tesseract)\n",
83
+ "import json\n",
84
+ "import os\n",
85
+ "import logging\n",
86
+ "import shutil\n",
87
+ "import re\n",
88
+ "import gc\n",
89
+ "import signal\n",
90
+ "from pathlib import Path\n",
91
+ "from typing import Optional\n",
92
+ "\n",
93
+ "# --- AUTO-CONFIG TESSERACT DATA PATH ---\n",
94
+ "# Fix lỗi \"No language models have been detected\"\n",
95
+ "# Tự động tìm đường dẫn chứa file ngôn ngữ (vie.traineddata) và set biến môi trường\n",
96
+ "def setup_tesseract_path():\n",
97
+ " possible_paths = [\n",
98
+ " \"/usr/share/tesseract-ocr/4.00/tessdata\",\n",
99
+ " \"/usr/share/tesseract-ocr/5/tessdata\",\n",
100
+ " \"/usr/share/tesseract-ocr/tessdata\",\n",
101
+ " \"/usr/local/share/tessdata\"\n",
102
+ " ]\n",
103
+ " \n",
104
+ " found = False\n",
105
+ " for path in possible_paths:\n",
106
+ " if os.path.exists(os.path.join(path, \"vie.traineddata\")):\n",
107
+ " os.environ[\"TESSDATA_PREFIX\"] = path\n",
108
+ " print(f\"✅ Found Tesseract data at: {path}\")\n",
109
+ " print(f\" Set TESSDATA_PREFIX={path}\")\n",
110
+ " found = True\n",
111
+ " break\n",
112
+ " \n",
113
+ " if not found:\n",
114
+ " print(\"⚠️ WARNING: Could not find 'vie.traineddata'. Tesseract might fail.\")\n",
115
+ " print(\" Please run Cell #2 to install tesseract-ocr-vie.\")\n",
116
+ "\n",
117
+ "setup_tesseract_path()\n",
118
+ "\n",
119
+ "# Setup logging\n",
120
+ "logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')\n",
121
+ "logger = logging.getLogger(__name__)\n",
122
+ "\n",
123
+ "# Docling imports\n",
124
+ "from docling.document_converter import DocumentConverter, FormatOption\n",
125
+ "from docling.datamodel.base_models import InputFormat\n",
126
+ "from docling.datamodel.pipeline_options import (\n",
127
+ " PdfPipelineOptions, \n",
128
+ " TableStructureOptions,\n",
129
+ " AcceleratorOptions,\n",
130
+ " AcceleratorDevice,\n",
131
+ " TesseractOcrOptions # SỬ DỤNG TESSERACT CHO ĐỘ CHÍNH XÁC CAO NHẤT\n",
132
+ ")\n",
133
+ "from docling.datamodel.settings import settings\n",
134
+ "from docling.backend.pypdfium2_backend import PyPdfiumDocumentBackend\n",
135
+ "from docling.pipeline.standard_pdf_pipeline import StandardPdfPipeline\n",
136
+ "\n",
137
+ "class ColabDoclingProcessor:\n",
138
+ " def __init__(self, output_dir: str, use_ocr: bool = True, timeout: int = 300):\n",
139
+ " self.output_dir = output_dir\n",
140
+ " self.use_ocr = use_ocr\n",
141
+ " self.timeout = timeout\n",
142
+ " os.makedirs(output_dir, exist_ok=True)\n",
143
+ " \n",
144
+ " # 1. Cấu hình Pipeline Options\n",
145
+ " pipeline_options = PdfPipelineOptions()\n",
146
+ " \n",
147
+ " # --- Cấu hình TableFormer (Ưu tiên số 1) ---\n",
148
+ " # Kích hoạt nhận diện cấu trúc bảng\n",
149
+ " pipeline_options.do_table_structure = True\n",
150
+ " # Sử dụng chế độ ACCURATE để đảm bảo bảng biểu phức tạp (điểm số, học phí) không bị vỡ\n",
151
+ " pipeline_options.table_structure_options = TableStructureOptions(\n",
152
+ " do_cell_matching=True, # Khớp text vào ô chính xác hơn\n",
153
+ " mode=\"accurate\" # Chế độ chính xác cao\n",
154
+ " )\n",
155
+ "\n",
156
+ " # --- FIX LỖI ẢNH MỜ (QUAN TRỌNG) ---\n",
157
+ " # Tăng độ phân giải ảnh lên gấp 3 lần để Tesseract nhìn rõ dấu tiếng Việt\n",
158
+ " # Mặc định là 1.0 (mờ), set lên 3.0 sẽ nét căng.\n",
159
+ " pipeline_options.images_scale = 3.0\n",
160
+ "\n",
161
+ " # --- Chiến lược OCR với Tesseract ---\n",
162
+ " if use_ocr:\n",
163
+ " pipeline_options.do_ocr = True\n",
164
+ " \n",
165
+ " # --- CẤU HÌNH TESSERACT TƯỜNG MINH ---\n",
166
+ " ocr_options = TesseractOcrOptions()\n",
167
+ " \n",
168
+ " # Cấu hình ngôn ngữ tiếng Việt (vie) - Phải khớp với gói tesseract-ocr-vie\n",
169
+ " ocr_options.lang = [\"vie\"] \n",
170
+ " \n",
171
+ " # --- CHẾ ĐỘ HYBRID (THÔNG MINH) ---\n",
172
+ " # Tắt force_full_page_ocr để Docling tự quyết định:\n",
173
+ " # 1. Nếu text layer tốt -> Dùng text layer (Nhanh, nhẹ)\n",
174
+ " # 2. Nếu text layer lỗi hoặc là ảnh -> Dùng OCR\n",
175
+ " ocr_options.force_full_page_ocr = False\n",
176
+ " \n",
177
+ " # Gán options vào pipeline\n",
178
+ " pipeline_options.ocr_options = ocr_options\n",
179
+ " else:\n",
180
+ " pipeline_options.do_ocr = False\n",
181
+ "\n",
182
+ " # --- Tối ưu phần cứng (GPU Acceleration) ---\n",
183
+ " # Tự động phát hiện và sử dụng GPU nếu có (Colab T4/L4)\n",
184
+ " pipeline_options.accelerator_options = AcceleratorOptions(\n",
185
+ " num_threads=8, # Tăng thread cho Tesseract\n",
186
+ " device=AcceleratorDevice.AUTO \n",
187
+ " )\n",
188
+ "\n",
189
+ " # 2. Tạo Format Options\n",
190
+ " format_options = {\n",
191
+ " InputFormat.PDF: FormatOption(\n",
192
+ " backend=PyPdfiumDocumentBackend,\n",
193
+ " pipeline_cls=StandardPdfPipeline,\n",
194
+ " pipeline_options=pipeline_options\n",
195
+ " )\n",
196
+ " }\n",
197
+ " \n",
198
+ " # Khởi tạo Converter\n",
199
+ " self.converter = DocumentConverter(format_options=format_options)\n",
200
+ " print(f\"🚀 Docling Processor Initialized\")\n",
201
+ " print(f\" - OCR Engine: TESSERACT (Vietnamese)\")\n",
202
+ " print(f\" - Mode: HYBRID (Text Layer + OCR fallback)\")\n",
203
+ " print(f\" - Image Scale: 3.0 (High Resolution)\")\n",
204
+ " print(f\" - Table Mode: Accurate\")\n",
205
+ " print(f\" - Device: Auto-detect (GPU/CPU)\")\n",
206
+ " print(f\" - Timeout: {self.timeout}s per file\")\n",
207
+ "\n",
208
+ " def clean_markdown(self, text: str) -> str:\n",
209
+ " \"\"\"Hậu xử lý: Làm sạch Markdown.\"\"\"\n",
210
+ " # 1. Xóa dòng \"Trang x\" (An toàn)\n",
211
+ " text = re.sub(r'\\n\\s*Trang\\s+\\d+\\s*\\n', '\\n', text)\n",
212
+ " \n",
213
+ " # 3. Xóa nhiều dòng trống (An toàn & Cần thiết)\n",
214
+ " text = re.sub(r'\\n{3,}', '\\n\\n', text)\n",
215
+ " return text.strip()\n",
216
+ "\n",
217
+ " def parse_directory(self, source_dir: str):\n",
218
+ " print(f\"📂 Parsing PDFs in: {source_dir}\")\n",
219
+ " source_path = Path(source_dir)\n",
220
+ " pdf_files = list(source_path.rglob(\"*.pdf\"))\n",
221
+ " print(f\" Found {len(pdf_files)} PDF files.\")\n",
222
+ " \n",
223
+ " results = {\"total\": 0, \"parsed\": 0, \"skipped\": 0, \"errors\": 0}\n",
224
+ " \n",
225
+ " # Define timeout handler\n",
226
+ " def timeout_handler(signum, frame):\n",
227
+ " raise TimeoutError(\"Processing timeout\")\n",
228
+ " \n",
229
+ " # Register signal for timeout\n",
230
+ " signal.signal(signal.SIGALRM, timeout_handler)\n",
231
+ " \n",
232
+ " for i, file_path in enumerate(pdf_files):\n",
233
+ " filename = file_path.name\n",
234
+ " \n",
235
+ " # --- GIỮ NGUYÊN CẤU TRÚC THƯ MỤC ---\n",
236
+ " # Tính toán đường dẫn tương đối: data/files/subdir/file.pdf -> subdir/file.pdf\n",
237
+ " try:\n",
238
+ " relative_path = file_path.relative_to(source_path)\n",
239
+ " except ValueError:\n",
240
+ " # Fallback nếu file không nằm trong source_dir (ít khi xảy ra với rglob)\n",
241
+ " relative_path = Path(filename)\n",
242
+ "\n",
243
+ " # Tạo đường dẫn output tương ứng: output_dir/subdir/file.md\n",
244
+ " output_file_path = Path(self.output_dir) / relative_path.with_suffix(\".md\")\n",
245
+ " \n",
246
+ " # Tạo thư mục con nếu chưa tồn tại\n",
247
+ " output_file_path.parent.mkdir(parents=True, exist_ok=True)\n",
248
+ " \n",
249
+ " output_path = str(output_file_path)\n",
250
+ " \n",
251
+ " # --- TỐI ƯU 1: SKIP NẾU ĐÃ CÓ KẾT QUẢ (Checkpoint) ---\n",
252
+ " if os.path.exists(output_path):\n",
253
+ " results[\"skipped\"] += 1\n",
254
+ " if results[\"skipped\"] % 50 == 0:\n",
255
+ " print(f\"⏩ Skipped {results['skipped']} files (already processed)...\")\n",
256
+ " continue\n",
257
+ "\n",
258
+ " try:\n",
259
+ " # Set timeout\n",
260
+ " signal.alarm(self.timeout)\n",
261
+ " \n",
262
+ " # Convert\n",
263
+ " result = self.converter.convert(str(file_path))\n",
264
+ " \n",
265
+ " # Cancel timeout\n",
266
+ " signal.alarm(0)\n",
267
+ " \n",
268
+ " # Export to Markdown (Làm sạch dữ liệu ảnh rác)\n",
269
+ " markdown_content = result.document.export_to_markdown(image_placeholder=\"\")\n",
270
+ " \n",
271
+ " # Post-processing cleaning\n",
272
+ " markdown_content = self.clean_markdown(markdown_content)\n",
273
+ " \n",
274
+ " # Metadata Extraction (Chuẩn bị cho RAG)\n",
275
+ " metadata_header = f\"\"\"---\n",
276
+ "filename: {filename}\n",
277
+ "filepath: {file_path}\n",
278
+ "page_count: {len(result.document.pages)}\n",
279
+ "processed_at: {os.path.getmtime(file_path)}\n",
280
+ "---\n",
281
+ "\n",
282
+ "\"\"\"\n",
283
+ " final_content = metadata_header + markdown_content\n",
284
+ " \n",
285
+ " # Save\n",
286
+ " with open(output_path, 'w', encoding='utf-8') as f:\n",
287
+ " f.write(final_content)\n",
288
+ " \n",
289
+ " results[\"parsed\"] += 1\n",
290
+ " \n",
291
+ " # --- TỐI ƯU 2: GIẢI PHÓNG RAM ---\n",
292
+ " del result\n",
293
+ " del markdown_content\n",
294
+ " \n",
295
+ " if (i+1) % 10 == 0:\n",
296
+ " gc.collect()\n",
297
+ " print(f\"✅ Processed {i+1}/{len(pdf_files)} files (Skipped: {results['skipped']})\")\n",
298
+ " \n",
299
+ " except TimeoutError:\n",
300
+ " print(f\"⏰ Timeout parsing {filename} (>{self.timeout}s)\")\n",
301
+ " results[\"errors\"] += 1\n",
302
+ " except Exception as e:\n",
303
+ " print(f\"❌ Failed to parse {filename}: {e}\")\n",
304
+ " results[\"errors\"] += 1\n",
305
+ " finally:\n",
306
+ " signal.alarm(0) # Ensure alarm is off\n",
307
+ " \n",
308
+ " return results"
309
+ ]
310
+ },
311
+ {
312
+ "cell_type": "code",
313
+ "execution_count": null,
314
+ "id": "0b87fec5",
315
+ "metadata": {},
316
+ "outputs": [],
317
+ "source": [
318
+ "# 5.5. Test Run on Specific File\n",
319
+ "# Chạy cell này để kiểm tra chất lượng trên file cụ thể (giống Marker)\n",
320
+ "import os\n",
321
+ "from pathlib import Path\n",
322
+ "\n",
323
+ "# Setup paths\n",
324
+ "source_dir = '/content/data/files'\n",
325
+ "if os.path.exists(os.path.join(source_dir, 'files')):\n",
326
+ " source_dir = os.path.join(source_dir, 'files')\n",
327
+ "\n",
328
+ "# Tìm file cụ thể\n",
329
+ "target_filename = \"1.1. Kỹ thuật Cơ điện tử.pdf\"\n",
330
+ "target_subdir = \"quy_che\"\n",
331
+ "target_path = Path(source_dir) / target_subdir / target_filename\n",
332
+ "\n",
333
+ "if target_path.exists():\n",
334
+ " print(f\"🧪 Found target file: {target_path.name}\")\n",
335
+ " \n",
336
+ " # Initialize processor for test\n",
337
+ " test_output_dir = '/content/data/test_output'\n",
338
+ " os.makedirs(test_output_dir, exist_ok=True)\n",
339
+ " \n",
340
+ " print(\"🚀 Initializing processor for test run (OCR Enabled - Default)...\")\n",
341
+ " # Use ColabDoclingProcessor defined in previous cell\n",
342
+ " test_processor = ColabDoclingProcessor(\n",
343
+ " output_dir=test_output_dir, \n",
344
+ " use_ocr=True \n",
345
+ " )\n",
346
+ " \n",
347
+ " try:\n",
348
+ " print(f\"⏳ Processing {target_path.name}...\")\n",
349
+ " # Convert\n",
350
+ " result = test_processor.converter.convert(str(target_path))\n",
351
+ " markdown_content = result.document.export_to_markdown()\n",
352
+ " \n",
353
+ " # Save to local output\n",
354
+ " output_file = Path(test_output_dir) / f\"{target_path.stem}.md\"\n",
355
+ " with open(output_file, 'w', encoding='utf-8') as f:\n",
356
+ " f.write(markdown_content)\n",
357
+ " \n",
358
+ " print(f\"💾 Saved local test file: {output_file}\")\n",
359
+ " \n",
360
+ " print(\"\\n\" + \"=\"*50)\n",
361
+ " print(f\"📄 RESULT PREVIEW (First 2000 characters)\")\n",
362
+ " print(\"=\"*50)\n",
363
+ " print(markdown_content[:2000])\n",
364
+ " print(\"\\n\" + \"=\"*50)\n",
365
+ " print(\"✅ Test completed! Hãy chạy cell tiếp theo để lưu kết quả lên Drive.\")\n",
366
+ " \n",
367
+ " except Exception as e:\n",
368
+ " print(f\"❌ Test failed: {e}\")\n",
369
+ "else:\n",
370
+ " print(f\"❌ File not found: {target_path}\")\n",
371
+ " print(f\"Searching in: {source_dir}\")"
372
+ ]
373
+ },
374
+ {
375
+ "cell_type": "code",
376
+ "execution_count": null,
377
+ "id": "a46429ed",
378
+ "metadata": {},
379
+ "outputs": [],
380
+ "source": [
381
+ "# 5.6. Save Test Result to Google Drive\n",
382
+ "import shutil\n",
383
+ "\n",
384
+ "# Cấu hình đường dẫn lưu trên Drive (Lưu vào folder riêng để dễ so sánh với Marker)\n",
385
+ "drive_test_folder = '/content/drive/MyDrive/docling/test_result_docling'\n",
386
+ "\n",
387
+ "# Biến test_output_dir được định nghĩa ở cell 5.5\n",
388
+ "if 'test_output_dir' in locals() and os.path.exists(test_output_dir):\n",
389
+ " # Tạo thư mục cha trên Drive nếu chưa có\n",
390
+ " if not os.path.exists(os.path.dirname(drive_test_folder)):\n",
391
+ " os.makedirs(os.path.dirname(drive_test_folder), exist_ok=True)\n",
392
+ " \n",
393
+ " print(f\"📂 Copying test results to: {drive_test_folder}\")\n",
394
+ " \n",
395
+ " # Sử dụng copytree với dirs_exist_ok=True để copy cả thư mục con và ghi đè nếu cần\n",
396
+ " # Cách này giữ nguyên cấu trúc thư mục (subdir)\n",
397
+ " try:\n",
398
+ " shutil.copytree(test_output_dir, drive_test_folder, dirs_exist_ok=True)\n",
399
+ " print(f\" ✅ Copied entire folder structure successfully!\")\n",
400
+ " except Exception as e:\n",
401
+ " print(f\" ❌ Error copying folder: {e}\")\n",
402
+ " \n",
403
+ " print(f\"\\n🎉 Done! Bạn có thể mở Drive để xem file markdown đầy đủ tại: {drive_test_folder}\")\n",
404
+ "else:\n",
405
+ " print(\"❌ Không tìm thấy thư mục kết quả test hoặc biến 'test_output_dir' chưa được định nghĩa.\")\n",
406
+ " print(\"⚠️ Hãy chạy cell 5.5 (Test Run) trước khi chạy cell này!\")"
407
+ ]
408
+ },
409
+ {
410
+ "cell_type": "code",
411
+ "execution_count": null,
412
+ "id": "8228498a",
413
+ "metadata": {},
414
+ "outputs": [],
415
+ "source": [
416
+ "# 6. Run Processing & Save Results\n",
417
+ "output_dir = '/content/data/docling_output'\n",
418
+ "# Bật OCR mặc định (EasyOCR)\n",
419
+ "processor = ColabDoclingProcessor(output_dir=output_dir, use_ocr=True) \n",
420
+ "\n",
421
+ "# Determine source directory (handle if zip extracted to subfolder)\n",
422
+ "source_dir = '/content/data/files' \n",
423
+ "# Check if files are in a subfolder named 'files' inside the extraction path\n",
424
+ "if os.path.exists(os.path.join(source_dir, 'files')):\n",
425
+ " source_dir = os.path.join(source_dir, 'files')\n",
426
+ "\n",
427
+ "# Run\n",
428
+ "processor.parse_directory(source_dir)\n",
429
+ "\n",
430
+ "# Zip output and save to Drive\n",
431
+ "output_zip_path = '/content/drive/MyDrive/docling/docling_output.zip'\n",
432
+ "print(f\"Zipping output to {output_zip_path}...\")\n",
433
+ "shutil.make_archive(output_zip_path.replace('.zip', ''), 'zip', output_dir)\n",
434
+ "print(\"🎉 Done! Check your Google Drive for docling_output.zip\")"
435
+ ]
436
+ }
437
+ ],
438
+ "metadata": {
439
+ "kernelspec": {
440
+ "display_name": "Python 3 (ipykernel)",
441
+ "language": "python",
442
+ "name": "python3"
443
+ }
444
+ },
445
+ "nbformat": 4,
446
+ "nbformat_minor": 5
447
+ }