LongK171 commited on
Commit
dbe2c62
·
1 Parent(s): 7a5cceb
.gitignore ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.so
5
+ *.pyd
6
+ *.egg-info/
7
+ .venv/
8
+ venv/
9
+ .env
10
+
11
+ # Cache / data
12
+ .cache/
13
+ **/Models/
14
+ **/outputs/
15
+ **/checkpoints/
16
+
17
+ # OS junk
18
+ .DS_Store
19
+
20
+ # Hugging Face
21
+ *.bak
.vscode/settings.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "python-envs.defaultEnvManager": "ms-python.python:conda",
3
+ "python-envs.defaultPackageManager": "ms-python.python:conda",
4
+ "python-envs.pythonProjects": []
5
+ }
AppGenerator.py ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fitz
2
+
3
+ from Config import Configs
4
+ from Config import ModelLoader as ML
5
+
6
+ from Libraries import Common_MyUtils as MU
7
+ from Libraries import PDF_ExtractData as ExtractData, PDF_MergeData as MergeData, PDF_QualityCheck as QualityCheck
8
+ from Libraries import Json_GetStructures as GetStructures, Json_ChunkMaster as ChunkMaster, Json_SchemaExt as SchemaExt
9
+ from Libraries import Faiss_Embedding as F_Embedding
10
+
11
+ Checkpoint = "vinai/bartpho-syllable"
12
+ service = "Categories"
13
+ inputs = "Categories.json"
14
+ JsonKey = "paragraphs"
15
+ JsonField = "Text"
16
+
17
+ config = Configs.ConfigValues(service=service, inputs=inputs)
18
+ inputPath = config["inputPath"]
19
+ PdfPath = config["PdfPath"]
20
+ DocPath = config["DocPath"]
21
+ exceptPath = config["exceptPath"]
22
+ markerPath = config["markerPath"]
23
+ statusPath = config["statusPath"]
24
+ RawDataPath = config["RawDataPath"]
25
+ RawLvlsPath = config["RawLvlsPath"]
26
+ StructsPath = config["StructsPath"]
27
+ SegmentPath = config["SegmentPath"]
28
+ SchemaPath = config["SchemaPath"]
29
+ FaissPath = config["FaissPath"]
30
+ MappingPath = config["MappingPath"]
31
+ MapDataPath = config["MapDataPath"]
32
+ MapChunkPath = config["MapChunkPath"]
33
+ MetaPath = config["MetaPath"]
34
+ DATA_KEY = config["DATA_KEY"]
35
+ EMBE_KEY = config["EMBE_KEY"]
36
+ SEARCH_EGINE = config["SEARCH_EGINE"]
37
+ RERANK_MODEL = config["RERANK_MODEL"]
38
+ RESPON_MODEL = config["RESPON_MODEL"]
39
+ EMBEDD_MODEL = config["EMBEDD_MODEL"]
40
+ CHUNKS_MODEL = config["CHUNKS_MODEL"]
41
+ SUMARY_MODEL = config["SUMARY_MODEL"]
42
+ WORD_LIMIT = config["WORD_LIMIT"]
43
+
44
+ MODEL_DIR = "Models"
45
+ MODEL_ENCODE = "Sentence_Transformer"
46
+ MODEL_SUMARY = "Summarizer"
47
+ EMBEDD_CACHED_MODEL = f"{MODEL_DIR}/{MODEL_ENCODE}/{EMBEDD_MODEL}"
48
+ CHUNKS_CACHED_MODEL = F"{MODEL_DIR}/{MODEL_ENCODE}/{CHUNKS_MODEL}"
49
+ SUMARY_CACHED_MODEL = f"{MODEL_DIR}/{MODEL_SUMARY}/{SUMARY_MODEL}"
50
+
51
+ MAX_INPUT = 1024
52
+ MAX_TARGET = 256
53
+ MIN_TARGET = 64
54
+ TRAIN_EPOCHS = 3
55
+ LEARNING_RATE = 3e-5
56
+ WEIGHT_DECAY = 0.01
57
+ BATCH_SIZE = 4
58
+
59
+ def loadHardcodes(file_path, wanted=None):
60
+ data = MU.read_json(file_path)
61
+ if "items" not in data:
62
+ return
63
+ result = {}
64
+ for item in data["items"]:
65
+ key = item["key"]
66
+ if (not wanted) or (key in wanted):
67
+ result[key] = item["values"]
68
+ return result
69
+
70
+ exceptData = loadHardcodes(exceptPath, wanted=["common_words", "proper_names", "abbreviations"])
71
+ markerData = loadHardcodes(markerPath, wanted=["keywords", "markers"])
72
+ statusData = loadHardcodes(statusPath, wanted=["brackets", "sentence_ends"])
73
+
74
+ Loader = ML.ModelLoader()
75
+ indexer, embeddDevice = Loader.load_encoder(EMBEDD_MODEL, EMBEDD_CACHED_MODEL)
76
+ chunker, chunksDevice = Loader.load_encoder(CHUNKS_MODEL, CHUNKS_CACHED_MODEL)
77
+
78
+ dataExtractor = ExtractData.B1Extractor(
79
+ exceptData,
80
+ markerData,
81
+ statusData,
82
+ proper_name_min_count=10
83
+ )
84
+
85
+ structAnalyzer = GetStructures.StructureAnalyzer(
86
+ verbose=True
87
+ )
88
+
89
+ chunkBuilder = ChunkMaster.ChunkBuilder()
90
+
91
+ schemaExt = SchemaExt.JSONSchemaExtractor(
92
+ list_policy="first",
93
+ verbose=True
94
+ )
95
+
96
+ faissIndexer = F_Embedding.DirectFaissIndexer(
97
+ indexer=indexer,
98
+ device=str(embeddDevice),
99
+ batch_size=32,
100
+ show_progress=True,
101
+ flatten_mode="split",
102
+ join_sep="\n",
103
+ allowed_schema_types=("string", "array", "dict"),
104
+ max_chars_per_text=2000,
105
+ normalize=True,
106
+ verbose=False
107
+ )
108
+
109
+ def extractRun(pdf_doc):
110
+ extractedData = dataExtractor.extract(pdf_doc)
111
+ RawDataDict = MergeData.mergeLinesToParagraphs(extractedData)
112
+ return RawDataDict
113
+
114
+ def structRun(RawDataDict):
115
+ markers = structAnalyzer.extract_markers(RawDataDict)
116
+ structures = structAnalyzer.build_structures(markers)
117
+ dedup = structAnalyzer.deduplicate(structures)
118
+ top = structAnalyzer.select_top(dedup)
119
+ RawLvlsDict = structAnalyzer.extend_top(top, dedup)
120
+ print(MU.json_convert(RawLvlsDict, pretty=True))
121
+ return RawLvlsDict
122
+
123
+ def chunkRun(RawLvlsDict=None, RawDataDict=None):
124
+ StructsDict = chunkBuilder.build(RawLvlsDict, RawDataDict)
125
+ return StructsDict
126
+
127
+ def SegmentRun(StructsDict, RawLvlsDict):
128
+ first_key = list(RawLvlsDict[0].keys())[0]
129
+
130
+ SegmentDict = []
131
+ for item in StructsDict:
132
+ value = item.get(first_key)
133
+ if not value: continue
134
+
135
+ if isinstance(value, list):
136
+ value = " ".join(v.strip() for v in value if isinstance(v, str) and v.strip().lower() != "none")
137
+ if value.strip():
138
+ SegmentDict.append(item)
139
+
140
+ for i, item in enumerate(SegmentDict, start=1):
141
+ item["Index"] = i
142
+
143
+ return SegmentDict
144
+
145
+ def schemaRun(SegmentDict):
146
+ SchemaDict = schemaExt.schemaRun(SegmentDict=SegmentDict)
147
+ print(SchemaDict)
148
+ return SchemaDict
149
+
150
+ def Indexing(SchemaDict):
151
+ Mapping, MapData = faissIndexer.build_from_json(
152
+ SegmentPath=SegmentPath,
153
+ SchemaDict=SchemaDict,
154
+ FaissPath=FaissPath,
155
+ MapDataPath=MapDataPath,
156
+ MappingPath=MappingPath,
157
+ MapChunkPath=MapChunkPath
158
+ )
159
+ return Mapping, MapData
160
+
161
+ mode = "json"
162
+
163
+ def Prepare():
164
+ if mode == "pdf":
165
+ print("\nLoading File...")
166
+ pdf_doc = fitz.open(PdfPath)
167
+ checker = QualityCheck.PDFQualityChecker()
168
+ is_good, info = checker.evaluate(pdf_doc)
169
+ print(info["status"])
170
+ if not is_good:
171
+ print("⚠️ Bỏ qua file này.")
172
+ return None, None, None, None
173
+ else:
174
+ print("✅ Tiếp tục xử lý.")
175
+
176
+ print("\nExtracting...")
177
+ RawDataDict = extractRun(pdf_doc)
178
+ MU.write_json(RawDataDict, RawDataPath, indent=1)
179
+ pdf_doc.close()
180
+
181
+ print("\nGetting Struct...")
182
+ RawLvlsDict = structRun(RawDataDict)
183
+ MU.write_json(RawLvlsDict, RawLvlsPath, indent=2)
184
+
185
+ print("\nChunking...")
186
+ StructsDict = chunkRun(RawLvlsDict, RawDataDict)
187
+ MU.write_json(StructsDict, StructsPath, indent=2)
188
+
189
+ print("\nSegmenting...")
190
+ SegmentDict = SegmentRun(StructsDict, RawLvlsDict)
191
+ MU.write_json(SegmentDict, SegmentPath, indent=2)
192
+ else:
193
+ SegmentDict = MU.read_json(SegmentPath)
194
+ print("\nCreating Schema...")
195
+ SchemaDict = schemaRun(SegmentDict)
196
+ MU.write_json(SchemaDict, SchemaPath, indent=2)
197
+
198
+ print("\nEmbedding...")
199
+ Mapping, MapData = Indexing(SchemaDict)
200
+ MU.write_json(Mapping, MappingPath, indent=2)
201
+ MU.write_json(MapData, MapDataPath, indent=2)
202
+
203
+ print("\nCompleted!")
204
+
205
+ return SegmentDict, SchemaDict, Mapping, MapData
206
+
207
+ SegmentDict, SchemaDict, Mapping, MapData = Prepare()
App_Caller.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import faiss
2
+ import fitz
3
+
4
+ from sentence_transformers import CrossEncoder
5
+
6
+ from Config import Configs
7
+ from Config import ModelLoader as ML
8
+ from Libraries import Common_MyUtils as MU, Common_TextProcess as TP
9
+ from Libraries import PDF_ExtractData as ExtractData, PDF_MergeData as MergeData, PDF_QualityCheck as QualityCheck
10
+ from Libraries import Json_ChunkUnder as ChunkUnder
11
+ from Libraries import Faiss_Searching as F_Searching, Faiss_ChunkMapping as ChunkMapper
12
+ from Libraries import Summarizer_Runner as SummaryRun
13
+
14
+ Checkpoint = "vinai/bartpho-syllable"
15
+ service = "Categories"
16
+ inputs = "BAD.pdf"
17
+ JsonKey = "paragraphs"
18
+ JsonField = "Text"
19
+
20
+ config = Configs.ConfigValues(service=service, inputs=inputs)
21
+ inputPath = config["inputPath"]
22
+ PdfPath = config["PdfPath"]
23
+ DocPath = config["DocPath"]
24
+ exceptPath = config["exceptPath"]
25
+ markerPath = config["markerPath"]
26
+ statusPath = config["statusPath"]
27
+ RawDataPath = config["RawDataPath"]
28
+ RawLvlsPath = config["RawLvlsPath"]
29
+ StructsPath = config["StructsPath"]
30
+ SegmentPath = config["SegmentPath"]
31
+ SchemaPath = config["SchemaPath"]
32
+ FaissPath = config["FaissPath"]
33
+ MappingPath = config["MappingPath"]
34
+ MapDataPath = config["MapDataPath"]
35
+ MapChunkPath = config["MapChunkPath"]
36
+ MetaPath = config["MetaPath"]
37
+ DATA_KEY = config["DATA_KEY"]
38
+ EMBE_KEY = config["EMBE_KEY"]
39
+ SEARCH_EGINE = config["SEARCH_EGINE"]
40
+ RERANK_MODEL = config["RERANK_MODEL"]
41
+ RESPON_MODEL = config["RESPON_MODEL"]
42
+ EMBEDD_MODEL = config["EMBEDD_MODEL"]
43
+ CHUNKS_MODEL = config["CHUNKS_MODEL"]
44
+ SUMARY_MODEL = config["SUMARY_MODEL"]
45
+ WORD_LIMIT = config["WORD_LIMIT"]
46
+
47
+ MODEL_DIR = "Models"
48
+ MODEL_ENCODE = "Sentence_Transformer"
49
+ MODEL_SUMARY = "Summarizer"
50
+ EMBEDD_CACHED_MODEL = f"{MODEL_DIR}/{MODEL_ENCODE}/{EMBEDD_MODEL}"
51
+ CHUNKS_CACHED_MODEL = F"{MODEL_DIR}/{MODEL_ENCODE}/{CHUNKS_MODEL}"
52
+ SUMARY_CACHED_MODEL = f"{MODEL_DIR}/{MODEL_SUMARY}/{SUMARY_MODEL}"
53
+
54
+ MAX_INPUT = 1024
55
+ MAX_TARGET = 256
56
+ MIN_TARGET = 64
57
+ TRAIN_EPOCHS = 3
58
+ LEARNING_RATE = 3e-5
59
+ WEIGHT_DECAY = 0.01
60
+ BATCH_SIZE = 4
61
+
62
+ def loadHardcodes(file_path, wanted=None):
63
+ data = MU.read_json(file_path)
64
+ if "items" not in data:
65
+ return
66
+ result = {}
67
+ for item in data["items"]:
68
+ key = item["key"]
69
+ if (not wanted) or (key in wanted):
70
+ result[key] = item["values"]
71
+ return result
72
+
73
+ exceptData = loadHardcodes(exceptPath, wanted=["common_words", "proper_names", "abbreviations"])
74
+ markerData = loadHardcodes(markerPath, wanted=["keywords", "markers"])
75
+ statusData = loadHardcodes(statusPath, wanted=["brackets", "sentence_ends"])
76
+
77
+ Loader = ML.ModelLoader()
78
+ indexer, embeddDevice = Loader.load_encoder(EMBEDD_MODEL, EMBEDD_CACHED_MODEL)
79
+ chunker, chunksDevice = Loader.load_encoder(CHUNKS_MODEL, CHUNKS_CACHED_MODEL)
80
+
81
+ tokenizer, summarizer, summaryDevice = Loader.load_summarizer(SUMARY_MODEL, SUMARY_CACHED_MODEL)
82
+
83
+ def runPrepareData():
84
+ SegmentDict = MU.read_json(SegmentPath)
85
+ Mapping = MU.read_json(MappingPath)
86
+ MapData = MU.read_json(MapDataPath)
87
+
88
+ MapChunk = MU.read_json(MapChunkPath)
89
+ faissIndex = faiss.read_index(FaissPath)
90
+ return SegmentDict, Mapping, MapData, MapChunk, faissIndex
91
+
92
+ SegmentDict, Mapping, MapData, MapChunk, faissIndex = runPrepareData()
93
+
94
+ dataExtractor = ExtractData.B1Extractor(
95
+ exceptData,
96
+ markerData,
97
+ statusData,
98
+ proper_name_min_count=10
99
+ )
100
+
101
+ chunkUnder = ChunkUnder.ChunkUndertheseaBuilder(
102
+ embedder=indexer,
103
+ device=embeddDevice,
104
+ min_words=256,
105
+ max_words=768,
106
+ sim_threshold=0.7,
107
+ key_sent_ratio=0.4
108
+ )
109
+
110
+ summarizer_engine = SummaryRun.RecursiveSummarizer(
111
+ tokenizer=tokenizer,
112
+ summarizer=summarizer,
113
+ sum_device=summaryDevice,
114
+ chunk_builder=chunkUnder,
115
+ max_length=200,
116
+ min_length=100,
117
+ max_depth=4
118
+ )
119
+
120
+ reranker = CrossEncoder(RERANK_MODEL, device=str(embeddDevice))
121
+ searchEngine = F_Searching.SemanticSearchEngine(
122
+ indexer=indexer,
123
+ reranker=reranker,
124
+ device=str(embeddDevice),
125
+ normalize=True,
126
+ top_k=20,
127
+ rerank_k=10,
128
+ rerank_batch_size=16
129
+ )
130
+
131
+ def extractRun(pdf_doc):
132
+ extractedData = dataExtractor.extract(pdf_doc)
133
+ RawDataDict = MergeData.mergeLinesToParagraphs(extractedData)
134
+ return RawDataDict
135
+
136
+ def runSearch(query):
137
+ results = searchEngine.search(
138
+ query=query,
139
+ faissIndex=faissIndex,
140
+ Mapping=Mapping,
141
+ MapData=MapData,
142
+ MapChunk=MapChunk,
143
+ top_k=20
144
+ )
145
+ return results
146
+
147
+ def runRerank(query, results):
148
+ reranked = searchEngine.rerank(
149
+ query=query,
150
+ results=results,
151
+ top_k=10
152
+ )
153
+ return reranked
154
+
155
+ def fileProcess(pdf_bytes):
156
+ """Nhận file PDF bytes, thực hiện pipeline chính."""
157
+ pdf_doc = fitz.open(stream=pdf_bytes, filetype="pdf")
158
+ checker = QualityCheck.PDFQualityChecker()
159
+ is_good, metrics = checker.evaluate(pdf_doc)
160
+ print(metrics)
161
+
162
+ if not is_good:
163
+ print("⚠️ Bỏ qua file này.")
164
+ check_status = 0
165
+ summaryText = metrics["check_mess"]
166
+ bestArticle = ""
167
+ reranked = ""
168
+ else:
169
+ print("✅ Tiếp tục xử lý.")
170
+ check_status = 1,
171
+ RawDataDict = extractRun(pdf_doc)
172
+ full_text = TP.merge_txt(RawDataDict, JsonKey, JsonField)
173
+ summarized = summarizer_engine.summarize(full_text, minInput = 256, maxInput = 1024)
174
+ summaryText = summarized["summary_text"]
175
+ resuls = runSearch(summaryText)
176
+ reranked = runRerank(summaryText, resuls)
177
+ chunkReturn = ChunkMapper.process_chunks_pipeline(
178
+ reranked_results=reranked,
179
+ SegmentDict=SegmentDict,
180
+ drop_fields=["Index"],
181
+ fields=["Article"],
182
+ n_chunks=1,
183
+ )
184
+ bestArticles = [item["fields"].get("Article") for item in chunkReturn["extracted_fields"]]
185
+ bestArticle = bestArticles[0] if len(bestArticles) == 1 else ", ".join(bestArticles)
186
+
187
+ pdf_doc.close()
188
+ return {
189
+ "checkstatus": check_status,
190
+ "metrics": metrics,
191
+ "summary": summaryText,
192
+ "category": bestArticle,
193
+ "reranked": reranked[:5] if reranked else []
194
+ }
App_Run.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ from flask_cors import CORS
3
+
4
+ import App_Caller
5
+
6
+ app = Flask(__name__)
7
+ CORS(app)
8
+
9
+ @app.route("/process_pdf", methods=["POST"])
10
+ def process_pdf():
11
+ """API nhận file PDF và trả về summary + category."""
12
+ if "file" not in request.files:
13
+ return jsonify({"error": "Thiếu file PDF"}), 400
14
+
15
+ pdf_file = request.files["file"]
16
+ if not pdf_file.filename.endswith(".pdf"):
17
+ return jsonify({"error": "File không hợp lệ"}), 400
18
+
19
+ try:
20
+ pdf_bytes = pdf_file.read()
21
+ result = App_Caller.fileProcess(pdf_bytes)
22
+ return jsonify({
23
+ "status": "success",
24
+ "checkstatus": result["checkstatus"],
25
+ "metrics": result["metrics"],
26
+ "summary": result["summary"],
27
+ "category": result["category"],
28
+ "top_candidates": result["reranked"]
29
+ })
30
+ except Exception as e:
31
+ return jsonify({"status": "error", "message": str(e)}), 500
32
+
33
+ if __name__ == "__main__":
34
+ app.run(host="0.0.0.0", port=8000)
Assets/ex.exceptions.json ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "exceptions",
3
+ "items": [
4
+ {
5
+ "key": "common_words",
6
+ "values": [
7
+ "a",
8
+ "an",
9
+ "the",
10
+ "and",
11
+ "but",
12
+ "or",
13
+ "nor",
14
+ "for",
15
+ "so",
16
+ "yet",
17
+ "at",
18
+ "by",
19
+ "in",
20
+ "of",
21
+ "on",
22
+ "to",
23
+ "from",
24
+ "with",
25
+ "as",
26
+ "into",
27
+ "like",
28
+ "over",
29
+ "under",
30
+ "up",
31
+ "down",
32
+ "out",
33
+ "upon",
34
+ "onto",
35
+ "amid",
36
+ "among",
37
+ "between",
38
+ "before",
39
+ "after",
40
+ "against"
41
+ ]
42
+ },
43
+ {
44
+ "key": "proper_names",
45
+ "values": [
46
+ { "text": "HCM", "case_style": "upper" },
47
+ { "text": "ASEAN", "case_style": "upper" },
48
+ { "text": "UNESCO", "case_style": "upper" }
49
+ ]
50
+ },
51
+ {
52
+ "key": "abbreviations",
53
+ "values": [
54
+ { "text": "VN", "case_style": "upper" },
55
+ { "text": "TP.HCM", "case_style": "title" },
56
+ { "text": "ĐH", "case_style": "upper" },
57
+ { "text": "THPT", "case_style": "upper" },
58
+ { "text": "UBND", "case_style": "upper" },
59
+ { "text": "KT-XH", "case_style": "upper" },
60
+ { "text": "BĐS", "case_style": "upper" },
61
+ { "text": "QH", "case_style": "upper" },
62
+ { "text": "NN", "case_style": "upper" },
63
+ { "text": "XD", "case_style": "upper" }
64
+ ]
65
+ }
66
+ ]
67
+ }
Assets/ex.markers.json ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "markers",
3
+ "items": [
4
+ {
5
+ "key": "keywords",
6
+ "values": [
7
+ "điều",
8
+ "khoản",
9
+ "mục",
10
+ "điểm",
11
+ "tiểu mục",
12
+ "chương",
13
+ "phần",
14
+ "chapter",
15
+ "section",
16
+ "article",
17
+ "part",
18
+ "clause",
19
+ "paragraph",
20
+ "item",
21
+ "point"
22
+ ]
23
+ },
24
+ {
25
+ "key": "markers",
26
+ "values": [
27
+ {
28
+ "pattern": "^([-+*•●◦○])\\s+",
29
+ "description": "Dấu đầu dòng (ví dụ: -, *, •, v.v.) theo sau bởi khoảng trắng",
30
+ "type": ""
31
+ },
32
+
33
+ {
34
+ "pattern": "^(?:{keywords})([0-9]+|[IVX]+)([.\\)\\]:,;-])\\s+",
35
+ "description": "Từ khóa dính liền số hoặc số La Mã, theo sau bởi khoảng trắng",
36
+ "type": ""
37
+ },
38
+ {
39
+ "pattern": "^(?:{keywords})([0-9]+|[IVX]+)([.\\)\\]:,;-]?)$",
40
+ "description": "Từ khóa dính liền số hoặc số La Mã, không có ký tự nào sau marker",
41
+ "type": ""
42
+ },
43
+ {
44
+ "pattern": "^(?:{keywords})\\s+([0-9]+|[a-z]|[A-Z]|[IVX]+)([.\\)\\]:,;-])\\s+",
45
+ "description": "Từ khóa theo sau bởi số, chữ cái (1 ký tự), hoặc số La Mã, với dấu kết thúc, sau đó là khoảng trắng",
46
+ "type": ""
47
+ },
48
+ {
49
+ "pattern": "^(?:{keywords})\\s+([0-9]+|[a-z]|[A-Z]|[IVX]+)([.\\)\\]:,;-]?)$",
50
+ "description": "Từ khóa theo sau bởi số, chữ cái (1 ký tự), hoặc số La Mã, với dấu kết thúc tùy chọn, không có ký tự nào sau marker",
51
+ "type": ""
52
+ },
53
+ {
54
+ "pattern": "^(?:{keywords})\\s+([0-9]+(?:\\.[0-9]+)*|[a-z](?:\\.[a-z])*|[A-Z](?:\\.[A-Z])*|[IVX]+(?:\\.[IVX]+)*)([.\\)\\]:,;-])\\s+",
55
+ "description": "Từ khóa theo sau bởi số, chữ cái (1 ký tự), hoặc số La Mã dạng phân cấp, với dấu kết thúc, sau đó là khoảng trắng",
56
+ "type": ""
57
+ },
58
+ {
59
+ "pattern": "^(?:{keywords})\\s+([0-9]+(?:\\.[0-9]+)*|[a-z](?:\\.[a-z])*|[A-Z](?:\\.[A-Z])*|[IVX]+(?:\\.[IVX]+)*)([.\\)\\]:,;-]?)$",
60
+ "description": "Từ khóa theo sau bởi số, chữ cái (1 ký tự), hoặc số La Mã dạng phân cấp, với dấu kết thúc tùy chọn, không có ký tự nào sau marker",
61
+ "type": ""
62
+ },
63
+
64
+ {
65
+ "pattern": "^([0-9]+|[a-z]|[A-Z])([.\\)\\]:,;-])\\s+",
66
+ "description": "Số hoặc chữ cái (1 ký tự) với dấu kết thúc, theo sau bởi khoảng trắng",
67
+ "type": ""
68
+ },
69
+ {
70
+ "pattern": "^(?:\\(([0-9]+|[a-z]|[A-Z]|[IVX]+)\\)|\"([0-9]+)\"|'([a-z])'|\\{([IVX]+)\\})\\s+",
71
+ "description": "Nhóm 'trong ngoặc' (tròn: số/chữ/La Mã; kép: số; đơn: chữ thường; nhọn: La Mã), theo sau bởi khoảng trắng",
72
+ "type": ""
73
+ },
74
+ {
75
+ "pattern": "^([0-9]+(?:\\.[0-9]+)*|[a-z](?:\\.[a-z])*|[A-Z](?:\\.[A-Z])*|[IVX]+(?:\\.[IVX]+)*)([.\\)\\]:,;-])\\s+",
76
+ "description": "Số/chữ/La Mã dạng phân cấp có dấu kết thúc, theo sau bởi khoảng trắng",
77
+ "type": ""
78
+ },
79
+ {
80
+ "pattern": "^([0-9]+)\\s*-\\s*([0-9]+)\\s+",
81
+ "description": "Khoảng số (ví dụ: 1 - 2), theo sau bởi khoảng trắng",
82
+ "type": ""
83
+ },
84
+ {
85
+ "pattern": "^([A-Z]\\+|[IVX]+\\+|[0-9]+\\+)([.\\)\\]:,;-])\\s+",
86
+ "description": "Chữ hoa, La Mã hoặc số kèm dấu +, bắt buộc có dấu kết thúc, theo sau bởi khoảng trắng",
87
+ "type": ""
88
+ },
89
+
90
+ {
91
+ "pattern": "^([đêôơưĐÊÔƠƯ])([.\\)\\]:,;-])\\s+",
92
+ "description": " == CHỮ CÁI TIẾNG VIỆT == với dấu kết thúc, theo sau bởi khoảng trắng",
93
+ "type": ""
94
+ },
95
+ {
96
+ "pattern": "^\\(([đêôơưĐÊÔƠƯ])\\)\\s+",
97
+ "description": " == CHỮ CÁI TIẾNG VIỆT == trong ngoặc tròn, theo sau bởi khoảng trắng",
98
+ "type": ""
99
+ },
100
+ {
101
+ "pattern": "^\"([đêôơưĐÊÔƠƯ])\"\\s+",
102
+ "description": " == CHỮ CÁI TIẾNG VIỆT == trong ngoặc kép, theo sau bởi khoảng trắng",
103
+ "type": ""
104
+ },
105
+ {
106
+ "pattern": "^'([đêôơưĐÊÔƠƯ])'\\s+",
107
+ "description": " == CHỮ CÁI TIẾNG VIỆT == trong ngoặc đơn, theo sau bởi khoảng trắng",
108
+ "type": ""
109
+ },
110
+ {
111
+ "pattern": "^\\{([đêôơưĐÊÔƠƯ])\\}\\s+",
112
+ "description": " == CHỮ CÁI TIẾNG VIỆT == trong ngoặc nhọn, theo sau bởi khoảng trắng",
113
+ "type": ""
114
+ }
115
+ ]
116
+ },
117
+ {
118
+ "key": "notMakers",
119
+ "values": []
120
+ }
121
+ ]
122
+ }
Assets/ex.status.json ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "status",
3
+ "items": [
4
+ {
5
+ "key": "brackets",
6
+ "values": {
7
+ "open": "[\\(\\[\\{«“‘]",
8
+ "close": "[\\)\\]\\}»”’]",
9
+ "pairs": ["()", "[]", "{}", "«»", "“”", "‘’"]
10
+ }
11
+ },
12
+ {
13
+ "key": "sentence_ends",
14
+ "values": {
15
+ "punctuation": "[.!?:;]",
16
+ "valid_brackets": ["()", "[]", "{}"]
17
+ }
18
+ }
19
+ ]
20
+ }
Config/APIs.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "APIs": [
3
+ "AIzaSyDaHS-8h6GJkyVPhoX4svvYeBTTVLNO-2w",
4
+ "AIzaSyD81vpriaNcvCyGOxy3TRR0w_njxgPJYfE",
5
+ "AIzaSyCsQo1gnYSLELV9flyPkYgHBdEvz7lqPjk",
6
+ "AIzaSyAJ7QFBJtozfyooguHAqsJsLO0a2L--tKo",
7
+ "AIzaSyBPjyMfHkS9OW3h7G0kmLSQkWQMfqfX5v0",
8
+ "AIzaSyA4HvCdIc4gGK4YCBlWS3vfXGjY3y9Zadg",
9
+ "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"
10
+ ]
11
+ }
Config/Config.json ADDED
File without changes
Config/Configs.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import faiss
4
+
5
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
6
+ os.environ["HF_HUB_DISABLE_SYMLINKS_WARNING"] = "1"
7
+ os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
8
+ os.environ["TORCH_USE_CUDA_DSA"] = "1"
9
+
10
+ def ConfigValues(service="Search", inputs="file.pdf"):
11
+
12
+ # Inputs
13
+ inputFolder = f"./Private/Tests"
14
+ inputPath = f"{inputFolder}/{inputs}"
15
+
16
+ # Assets
17
+ assetsFolder = f"./Assets"
18
+ exceptPath = f"{assetsFolder}/ex.exceptions.json"
19
+ markerPath = f"{assetsFolder}/ex.markers.json"
20
+ statusPath = f"{assetsFolder}/ex.status.json"
21
+
22
+ # Documents
23
+ DocFolder = "./Documents"
24
+ DocPath = f"{DocFolder}/{service}"
25
+ PdfPath = f"{DocPath}.pdf"
26
+ DocPath = f"{DocPath}.docx"
27
+
28
+ # Database
29
+ DBFolder = "./Database"
30
+ DBPath = f"{DBFolder}/{service}/{service}"
31
+
32
+ RawExtractPath = f"{DBPath}_Extract"
33
+ ChunksPath = f"{DBPath}_Chunks"
34
+ EmbeddingPath = f"{DBPath}_Embedding"
35
+
36
+ RawDataPath = f"{RawExtractPath}_Raw.json"
37
+ RawLvlsPath = f"{RawExtractPath}_Levels.json"
38
+
39
+ StructsPath = f"{ChunksPath}_Struct.json"
40
+ SegmentPath = f"{ChunksPath}_Segment.json"
41
+ SchemaPath = f"{ChunksPath}_Schema.json"
42
+
43
+ FaissPath = f"{EmbeddingPath}_Index.faiss"
44
+ MappingPath = f"{EmbeddingPath}_Mapping.json"
45
+ MapDataPath = f"{EmbeddingPath}_MapData.json"
46
+ MapChunkPath = f"{EmbeddingPath}_MapChunk.json"
47
+ MetaPath = f"{EmbeddingPath}_Meta.json"
48
+
49
+ # Keys
50
+ DATA_KEY = "contents"
51
+ EMBE_KEY = "embeddings"
52
+
53
+ # Models
54
+ SEARCH_EGINE = faiss.IndexFlatIP
55
+ RERANK_MODEL = "BAAI/bge-reranker-base"
56
+ CHUNKS_MODEL = "paraphrase-multilingual-MiniLM-L12-v2"
57
+ EMBEDD_MODEL = "VoVanPhuc/sup-SimCSE-VietNamese-phobert-base"
58
+ RESPON_MODEL = "gpt-3.5-turbo"
59
+ SUMARY_MODEL = "vinai/bartpho-syllable"
60
+
61
+ WORD_LIMIT = 1000
62
+
63
+ return {
64
+ "inputPath": inputPath,
65
+ "PdfPath": PdfPath,
66
+ "DocPath": DocPath,
67
+ "exceptPath": exceptPath,
68
+ "markerPath": markerPath,
69
+ "statusPath": statusPath,
70
+ "RawDataPath": RawDataPath,
71
+ "RawLvlsPath": RawLvlsPath,
72
+ "StructsPath": StructsPath,
73
+ "SegmentPath": SegmentPath,
74
+ "SchemaPath": SchemaPath,
75
+ "FaissPath": FaissPath,
76
+ "MappingPath": MappingPath,
77
+ "MapDataPath": MapDataPath,
78
+ "MapChunkPath": MapChunkPath,
79
+ "MetaPath": MetaPath,
80
+ "DATA_KEY": DATA_KEY,
81
+ "EMBE_KEY": EMBE_KEY,
82
+ "SEARCH_EGINE": SEARCH_EGINE,
83
+ "RERANK_MODEL": RERANK_MODEL,
84
+ "RESPON_MODEL": RESPON_MODEL,
85
+ "CHUNKS_MODEL": CHUNKS_MODEL,
86
+ "EMBEDD_MODEL": EMBEDD_MODEL,
87
+ "SUMARY_MODEL": SUMARY_MODEL,
88
+ "WORD_LIMIT": WORD_LIMIT
89
+ }
Config/ModelLoader.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # Config/ModelLoader.py — Official, unified, complete
3
+ # - Manage Encoder/Chunker (SentenceTransformer) and Summarizer (Seq2Seq)
4
+ # - Auto-download to local cache when missing
5
+ # - GPU/CPU selection with CUDA checks
6
+ # - Consistent class-based API
7
+ # ============================================================
8
+
9
+ import os
10
+ import torch
11
+ from typing import List, Tuple, Optional, Dict, Any
12
+
13
+ from sentence_transformers import SentenceTransformer
14
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
15
+
16
+
17
+ class ModelLoader:
18
+ """
19
+ Unified model manager:
20
+ - Encoder (SentenceTransformer)
21
+ - Chunker (SentenceTransformer)
22
+ - Summarizer (Seq2Seq: T5/BART/vit5)
23
+ Provides:
24
+ - load_encoder(name, cache)
25
+ - load_chunker(name, cache)
26
+ - load_summarizer(name, cache)
27
+ - summarize(text, max_len, min_len)
28
+ - summarize_batch(texts, max_len, min_len)
29
+ - print_devices()
30
+ """
31
+
32
+ # -----------------------------
33
+ # Construction / State
34
+ # -----------------------------
35
+ def __init__(self, prefer_cuda: bool = True) -> None:
36
+ self.models: Dict[str, Any] = {}
37
+ self.tokenizers: Dict[str, Any] = {}
38
+ self.devices: Dict[str, torch.device] = {}
39
+ self.prefer_cuda = prefer_cuda
40
+
41
+ # -----------------------------
42
+ # Device helpers
43
+ # -----------------------------
44
+ @staticmethod
45
+ def _cuda_check() -> None:
46
+ print("CUDA supported:", torch.cuda.is_available())
47
+ print("Number of GPUs:", torch.cuda.device_count())
48
+ if torch.cuda.is_available():
49
+ print("Current GPU:", torch.cuda.get_device_name(0))
50
+ print("Capability:", torch.cuda.get_device_capability(0))
51
+ print("CUDA version (PyTorch):", torch.version.cuda)
52
+ print("cuDNN version:", torch.backends.cudnn.version())
53
+ else:
54
+ print("⚠️ CUDA not available, using CPU.")
55
+
56
+ def _get_device(self) -> torch.device:
57
+ if self.prefer_cuda and torch.cuda.is_available():
58
+ return torch.device("cuda")
59
+ return torch.device("cpu")
60
+
61
+ @staticmethod
62
+ def _ensure_dir(path: Optional[str]) -> None:
63
+ if path:
64
+ os.makedirs(path, exist_ok=True)
65
+
66
+ # -----------------------------
67
+ # SentenceTransformer (Encoder/Chunker)
68
+ # -----------------------------
69
+ @staticmethod
70
+ def _ensure_cached_sentence_model(model_name: str, cache_path: str) -> str:
71
+ """
72
+ Ensure SentenceTransformer exists under cache_path.
73
+ Rebuild structure if config missing.
74
+ """
75
+ if not os.path.exists(cache_path):
76
+ print(f"📥 Downloading SentenceTransformer to: {cache_path}")
77
+ model = SentenceTransformer(model_name)
78
+ model.save(cache_path)
79
+ print("✅ Cached SentenceTransformer successfully.")
80
+ else:
81
+ cfg = os.path.join(cache_path, "config_sentence_transformers.json")
82
+ if not os.path.exists(cfg):
83
+ print("⚙️ Rebuilding SentenceTransformer cache structure...")
84
+ tmp = SentenceTransformer(model_name)
85
+ tmp.save(cache_path)
86
+ return cache_path
87
+
88
+ def _load_sentence_model(self, model_name: str, cache_path: Optional[str]) -> Tuple[SentenceTransformer, torch.device]:
89
+ device = self._get_device()
90
+ print(f"\n🔍 Loading SentenceTransformer ({model_name}) on {device} ...")
91
+ self._cuda_check()
92
+
93
+ if cache_path:
94
+ self._ensure_dir(cache_path)
95
+ self._ensure_cached_sentence_model(model_name, cache_path)
96
+ model = SentenceTransformer(cache_path, device=str(device))
97
+ print(f"📂 Loaded from cache: {cache_path}")
98
+ else:
99
+ model = SentenceTransformer(model_name, device=str(device))
100
+
101
+ print("✅ SentenceTransformer ready.")
102
+ return model, device
103
+
104
+ # Public APIs for SentenceTransformer
105
+ def load_encoder(self, name: str, cache: Optional[str] = None) -> Tuple[SentenceTransformer, torch.device]:
106
+ model, device = self._load_sentence_model(name, cache)
107
+ self.models["encoder"] = model
108
+ self.devices["encoder"] = device
109
+ return model, device
110
+
111
+ def load_chunker(self, name: str, cache: Optional[str] = None) -> Tuple[SentenceTransformer, torch.device]:
112
+ model, device = self._load_sentence_model(name, cache)
113
+ self.models["chunker"] = model
114
+ self.devices["chunker"] = device
115
+ return model, device
116
+
117
+ # -----------------------------
118
+ # Summarizer (Seq2Seq: T5/BART/vit5)
119
+ # -----------------------------
120
+ @staticmethod
121
+ def _has_hf_config(cache_dir: str) -> bool:
122
+ return os.path.exists(os.path.join(cache_dir, "config.json"))
123
+
124
+ @staticmethod
125
+ def _download_and_cache_summarizer(model_name: str, cache_dir: str) -> None:
126
+ """
127
+ Download HF model + tokenizer and save_pretrained to cache_dir.
128
+ """
129
+ print("⚙️ Cache missing — downloading model from Hugging Face...")
130
+ tokenizer = AutoTokenizer.from_pretrained(model_name)
131
+ model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
132
+ os.makedirs(cache_dir, exist_ok=True)
133
+ tokenizer.save_pretrained(cache_dir)
134
+ model.save_pretrained(cache_dir)
135
+ print(f"✅ Summarizer cached at: {cache_dir}")
136
+
137
+ def _load_summarizer_core(self, model_or_dir: str, device: torch.device) -> Tuple[AutoTokenizer, AutoModelForSeq2SeqLM]:
138
+ tokenizer = AutoTokenizer.from_pretrained(model_or_dir)
139
+ model = AutoModelForSeq2SeqLM.from_pretrained(model_or_dir).to(device)
140
+ return tokenizer, model
141
+
142
+ def load_summarizer(self, name: str, cache: Optional[str] = None) -> Tuple[AutoTokenizer, AutoModelForSeq2SeqLM, torch.device]:
143
+ """
144
+ Load Seq2Seq model; auto-download if cache dir missing or invalid.
145
+ """
146
+ device = self._get_device()
147
+ print(f"\n🔍 Initializing summarizer ({name}) on {device} ...")
148
+ self._cuda_check()
149
+
150
+ if cache:
151
+ self._ensure_dir(cache)
152
+ if not self._has_hf_config(cache):
153
+ self._download_and_cache_summarizer(name, cache)
154
+ print("📂 Loading summarizer from local cache...")
155
+ tok, mdl = self._load_summarizer_core(cache, device)
156
+ else:
157
+ print("🌐 Loading summarizer directly from Hugging Face (no cache dir provided)...")
158
+ tok, mdl = self._load_summarizer_core(name, device)
159
+
160
+ self.tokenizers["summarizer"] = tok
161
+ self.models["summarizer"] = mdl
162
+ self.devices["summarizer"] = device
163
+
164
+ print(f"✅ Summarizer ready on {device}")
165
+ return tok, mdl, device
166
+
167
+ # -----------------------------
168
+ # Summarization helpers
169
+ # -----------------------------
170
+ @staticmethod
171
+ def _apply_vietnews_prefix(text: str, prefix: str, suffix: str) -> str:
172
+ """
173
+ For VietAI/vit5-vietnews: prefix 'vietnews: ' and suffix ' </s>'
174
+ Safe for general T5-family; harmless for BART-family.
175
+ """
176
+ t = (text or "").strip()
177
+ if not t:
178
+ return ""
179
+ return f"{prefix}{t}{suffix}"
180
+
181
+ def summarize(self,
182
+ text: str,
183
+ max_len: int = 256,
184
+ min_len: int = 64,
185
+ prefix: str = "vietnews: ",
186
+ suffix: str = " </s>") -> str:
187
+ """
188
+ Summarize a single text with loaded summarizer.
189
+ Raises RuntimeError if summarizer not loaded.
190
+ """
191
+ if "summarizer" not in self.models or "summarizer" not in self.tokenizers:
192
+ raise RuntimeError("❌ Summarizer not loaded. Call load_summarizer() first.")
193
+
194
+ model: AutoModelForSeq2SeqLM = self.models["summarizer"]
195
+ tokenizer: AutoTokenizer = self.tokenizers["summarizer"]
196
+ device: torch.device = self.devices["summarizer"]
197
+
198
+ prepared = self._apply_vietnews_prefix(text, prefix, suffix)
199
+ if not prepared:
200
+ return ""
201
+
202
+ encoding = tokenizer(
203
+ prepared,
204
+ return_tensors="pt",
205
+ truncation=True,
206
+ max_length=1024
207
+ ).to(device)
208
+
209
+ with torch.no_grad():
210
+ outputs = model.generate(
211
+ **encoding,
212
+ max_length=max_len,
213
+ min_length=min_len,
214
+ num_beams=4,
215
+ no_repeat_ngram_size=3,
216
+ early_stopping=True
217
+ )
218
+
219
+ summary = tokenizer.decode(
220
+ outputs[0],
221
+ skip_special_tokens=True,
222
+ clean_up_tokenization_spaces=True
223
+ )
224
+ return summary
225
+
226
+ def summarize_batch(self,
227
+ texts: List[str],
228
+ max_len: int = 256,
229
+ min_len: int = 64,
230
+ prefix: str = "vietnews: ",
231
+ suffix: str = " </s>") -> List[str]:
232
+ """
233
+ Batch summarization. Processes in a single forward pass when possible.
234
+ """
235
+ if "summarizer" not in self.models or "summarizer" not in self.tokenizers:
236
+ raise RuntimeError("❌ Summarizer not loaded. Call load_summarizer() first.")
237
+
238
+ model: AutoModelForSeq2SeqLM = self.models["summarizer"]
239
+ tokenizer: AutoTokenizer = self.tokenizers["summarizer"]
240
+ device: torch.device = self.devices["summarizer"]
241
+
242
+ batch = [self._apply_vietnews_prefix(t, prefix, suffix) for t in texts]
243
+ batch = [b for b in batch if b] # drop empties
244
+ if not batch:
245
+ return []
246
+
247
+ encoding = tokenizer(
248
+ batch,
249
+ return_tensors="pt",
250
+ truncation=True,
251
+ max_length=1024,
252
+ padding=True
253
+ ).to(device)
254
+
255
+ summaries: List[str] = []
256
+ with torch.no_grad():
257
+ outputs = model.generate(
258
+ **encoding,
259
+ max_length=max_len,
260
+ min_length=min_len,
261
+ num_beams=4,
262
+ no_repeat_ngram_size=3,
263
+ early_stopping=True
264
+ )
265
+ for i in range(outputs.shape[0]):
266
+ dec = tokenizer.decode(
267
+ outputs[i],
268
+ skip_special_tokens=True,
269
+ clean_up_tokenization_spaces=True
270
+ )
271
+ summaries.append(dec)
272
+ return summaries
273
+
274
+ # -----------------------------
275
+ # Diagnostics
276
+ # -----------------------------
277
+ def print_devices(self) -> None:
278
+ print("\n📊 Device summary:")
279
+ for key, dev in self.devices.items():
280
+ print(f" - {key}: {dev}")
Database/Categories/Categories_Chunks_Schema.json ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "Article": "string",
3
+ "Content": "object",
4
+ "Content.SubCategory": "object",
5
+ "Content.SubCategory.Core": "array",
6
+ "Content.SubCategory.Applied": "array",
7
+ "Content.SubCategory.Interdisciplinary": "array"
8
+ }
Database/Categories/Categories_Chunks_Segment.json ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "Article": "Toán Học",
4
+ "Content": {
5
+ "SubCategory": {
6
+ "Core": [
7
+ "Đại số tuyến tính",
8
+ "Giải tích vi phân - tích phân",
9
+ "Hình học phẳng và không gian",
10
+ "Tổ hợp và xác suất",
11
+ "Logic toán học"
12
+ ],
13
+ "Applied": [
14
+ "Toán rời rạc",
15
+ "Thống kê ứng dụng",
16
+ "Tối ưu hóa",
17
+ "Toán mô phỏng"
18
+ ],
19
+ "Interdisciplinary": [
20
+ "Toán học trong trí tuệ nhân tạo",
21
+ "Toán tài chính",
22
+ "Phân tích dữ liệu định lượng"
23
+ ]
24
+ }
25
+ }
26
+ },
27
+ {
28
+ "Article": "Tin Học",
29
+ "Content": {
30
+ "SubCategory": {
31
+ "Core": [
32
+ "Thuật toán và cấu trúc dữ liệu",
33
+ "Ngôn ngữ lập trình",
34
+ "Hệ điều hành",
35
+ "Cơ sở dữ liệu",
36
+ "Mạng máy tính"
37
+ ],
38
+ "Applied": [
39
+ "Phát triển phần mềm",
40
+ "Phân tích dữ liệu",
41
+ "Trí tuệ nhân tạo",
42
+ "Bảo mật thông tin"
43
+ ],
44
+ "Interdisciplinary": [
45
+ "Khoa học dữ liệu",
46
+ "Học máy và học sâu",
47
+ "Tin học y sinh",
48
+ "Thị giác máy tính"
49
+ ]
50
+ }
51
+ }
52
+ },
53
+ {
54
+ "Article": "Vật Lý",
55
+ "Content": {
56
+ "SubCategory": {
57
+ "Core": [
58
+ "Cơ học cổ điển",
59
+ "Điện học và từ học",
60
+ "Quang học",
61
+ "Nhiệt học",
62
+ "Dao động và sóng"
63
+ ],
64
+ "Applied": [
65
+ "Điện tử học cơ bản",
66
+ "Vật lý hạt nhân",
67
+ "Vật lý chất rắn",
68
+ "Vật lý thiên văn"
69
+ ],
70
+ "Interdisciplinary": [
71
+ "Vật lý lượng tử",
72
+ "Vật lý vật liệu",
73
+ "Khoa học năng lượng",
74
+ "Vật lý tính toán"
75
+ ]
76
+ }
77
+ }
78
+ },
79
+ {
80
+ "Article": "Hóa Học",
81
+ "Content": {
82
+ "SubCategory": {
83
+ "Core": [
84
+ "Hóa vô cơ",
85
+ "Hóa hữu cơ",
86
+ "Hóa lý",
87
+ "Hóa phân tích",
88
+ "Liên kết hóa học"
89
+ ],
90
+ "Applied": [
91
+ "Hóa học môi trường",
92
+ "Hóa học vật liệu",
93
+ "Hóa dược",
94
+ "Công nghệ hóa học"
95
+ ],
96
+ "Interdisciplinary": [
97
+ "Hóa sinh học",
98
+ "Hóa học tính toán",
99
+ "Hóa học năng lượng",
100
+ "Hóa học nano"
101
+ ]
102
+ }
103
+ }
104
+ },
105
+ {
106
+ "Article": "Công Nghệ",
107
+ "Content": {
108
+ "SubCategory": {
109
+ "Core": [
110
+ "Cơ khí chế tạo",
111
+ "Điện - Điện tử cơ bản",
112
+ "Tự động hóa",
113
+ "Kỹ thuật vật liệu",
114
+ "An toàn kỹ thuật"
115
+ ],
116
+ "Applied": [
117
+ "Robot và IoT",
118
+ "Công nghệ năng lượng tái tạo",
119
+ "Công nghệ môi trường",
120
+ "Công nghệ sinh học ứng dụng"
121
+ ],
122
+ "Interdisciplinary": [
123
+ "Công nghệ thực phẩm",
124
+ "Công nghệ nano",
125
+ "Kỹ thuật sản xuất thông minh",
126
+ "Công nghệ xanh và bền vững"
127
+ ]
128
+ }
129
+ }
130
+ },
131
+ {
132
+ "Article": "Sinh Học",
133
+ "Content": {
134
+ "SubCategory": {
135
+ "Core": [
136
+ "Tế bào học",
137
+ "Di truyền học",
138
+ "Sinh lý học",
139
+ "Sinh thái học",
140
+ "Tiến hóa học"
141
+ ],
142
+ "Applied": [
143
+ "Vi sinh vật học",
144
+ "Công nghệ sinh học",
145
+ "Sinh học phân tử",
146
+ "Sinh học phát triển"
147
+ ],
148
+ "Interdisciplinary": [
149
+ "Sinh học tính toán",
150
+ "Sinh học y học",
151
+ "Sinh học môi trường",
152
+ "Hệ gen học và tin sinh học"
153
+ ]
154
+ }
155
+ }
156
+ },
157
+ {
158
+ "Article": "Văn Học",
159
+ "Content": {
160
+ "SubCategory": {
161
+ "Core": [
162
+ "Lý luận văn học",
163
+ "Văn học Việt Nam",
164
+ "Văn học nước ngoài",
165
+ "Ngữ pháp và ngôn ngữ",
166
+ "Thể loại văn học"
167
+ ],
168
+ "Applied": [
169
+ "Phân tích và bình giảng tác phẩm",
170
+ "Làm văn - sáng tác",
171
+ "Phong cách học",
172
+ "Phê bình văn học"
173
+ ],
174
+ "Interdisciplinary": [
175
+ "Văn học so sánh",
176
+ "Văn hóa học",
177
+ "Ngôn ngữ học ứng dụng trong văn học",
178
+ "Tư duy phản biện văn học"
179
+ ]
180
+ }
181
+ }
182
+ },
183
+ {
184
+ "Article": "Địa Lý",
185
+ "Content": {
186
+ "SubCategory": {
187
+ "Core": [
188
+ "Địa lý tự nhiên",
189
+ "Địa lý kinh tế",
190
+ "Địa lý dân cư",
191
+ "Khí hậu học",
192
+ "Địa mạo học"
193
+ ],
194
+ "Applied": [
195
+ "Bản đồ học",
196
+ "Quản lý tài nguyên",
197
+ "Quy hoạch vùng và đô thị",
198
+ "Địa lý Việt Nam"
199
+ ],
200
+ "Interdisciplinary": [
201
+ "Địa lý môi trường",
202
+ "Địa lý chính trị",
203
+ "GIS và viễn thám",
204
+ "Địa lý phát triển bền vững"
205
+ ]
206
+ }
207
+ }
208
+ },
209
+ {
210
+ "Article": "Lịch Sử",
211
+ "Content": {
212
+ "SubCategory": {
213
+ "Core": [
214
+ "Lịch sử Việt Nam cổ đại",
215
+ "Lịch sử Việt Nam cận đại",
216
+ "Lịch sử thế giới cổ đại",
217
+ "Lịch sử thế giới hiện đại",
218
+ "Phương pháp sử học"
219
+ ],
220
+ "Applied": [
221
+ "Lịch sử chiến tranh và cách mạng",
222
+ "Văn hóa và xã hội qua các thời kỳ",
223
+ "Nhân vật lịch sử tiêu biểu",
224
+ "Nghiên cứu di sản"
225
+ ],
226
+ "Interdisciplinary": [
227
+ "Lịch sử tư tưởng - triết học",
228
+ "Lịch sử tôn giáo",
229
+ "Lịch sử khu vực (châu Á, Âu, Mỹ)",
230
+ "Lịch sử nghệ thuật"
231
+ ]
232
+ }
233
+ }
234
+ },
235
+ {
236
+ "Article": "Kinh Tế",
237
+ "Content": {
238
+ "SubCategory": {
239
+ "Core": [
240
+ "Kinh tế vi mô",
241
+ "Kinh tế vĩ mô",
242
+ "Thống kê kinh tế",
243
+ "Kinh tế lượng",
244
+ "Lý thuyết trò chơi"
245
+ ],
246
+ "Applied": [
247
+ "Tài chính - Ngân hàng",
248
+ "Kế toán - Kiểm toán",
249
+ "Marketing",
250
+ "Quản trị kinh doanh"
251
+ ],
252
+ "Interdisciplinary": [
253
+ "Kinh tế quốc tế",
254
+ "Kinh tế học hành vi",
255
+ "Kinh tế phát triển",
256
+ "Thương mại điện tử"
257
+ ]
258
+ }
259
+ }
260
+ },
261
+ {
262
+ "Article": "Chính Trị",
263
+ "Content": {
264
+ "SubCategory": {
265
+ "Core": [
266
+ "Chủ nghĩa Mác - Lênin",
267
+ "Tư tưởng Hồ Chí Minh",
268
+ "Hệ thống chính trị Việt Nam",
269
+ "Triết học chính trị",
270
+ "Lý luận nhà nước và pháp luật"
271
+ ],
272
+ "Applied": [
273
+ "Quan hệ quốc tế",
274
+ "Công tác Đảng và chính quyền",
275
+ "Chính sách công",
276
+ "Quản trị nhà nước"
277
+ ],
278
+ "Interdisciplinary": [
279
+ "Chính trị học so sánh",
280
+ "Kinh tế chính trị",
281
+ "Xã hội học chính trị",
282
+ "Truyền thông chính trị"
283
+ ]
284
+ }
285
+ }
286
+ },
287
+ {
288
+ "Article": "Ngoại Ngữ",
289
+ "Content": {
290
+ "SubCategory": {
291
+ "Core": [
292
+ "Ngữ pháp cơ bản",
293
+ "Từ vựng và ngữ nghĩa",
294
+ "Ngữ âm - Phát âm",
295
+ "Cấu trúc câu",
296
+ "Ngữ dụng học"
297
+ ],
298
+ "Applied": [
299
+ "Kỹ năng nghe",
300
+ "Kỹ năng nói",
301
+ "Kỹ năng đọc hiểu",
302
+ "Kỹ năng viết"
303
+ ],
304
+ "Interdisciplinary": [
305
+ "Biên - phiên dịch học",
306
+ "Ngôn ngữ học so sánh",
307
+ "Văn hóa và giao tiếp liên văn hóa",
308
+ "Ngôn ngữ học ứng dụng trong AI"
309
+ ]
310
+ }
311
+ }
312
+ }
313
+ ]
Database/Categories/Categories_Embedding_Index.faiss ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eccb9e6695079ec697e9570526d9a44a34efb376ecd159f66d4448d61f9b49e0
3
+ size 513069
Database/Categories/Categories_Embedding_MapChunk.json ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "index_to_chunk": {
3
+ "0": [1],
4
+ "1": [1],
5
+ "2": [1],
6
+ "3": [1],
7
+ "4": [1],
8
+ "5": [1],
9
+ "6": [1],
10
+ "7": [1],
11
+ "8": [1],
12
+ "9": [1],
13
+ "10": [1],
14
+ "11": [1],
15
+ "12": [1],
16
+ "13": [2],
17
+ "14": [2],
18
+ "15": [2],
19
+ "16": [2],
20
+ "17": [2],
21
+ "18": [2],
22
+ "19": [2],
23
+ "20": [2],
24
+ "21": [2],
25
+ "22": [2],
26
+ "23": [2],
27
+ "24": [2],
28
+ "25": [2],
29
+ "26": [2],
30
+ "27": [3],
31
+ "28": [3],
32
+ "29": [3],
33
+ "30": [3],
34
+ "31": [3],
35
+ "32": [3],
36
+ "33": [3],
37
+ "34": [3],
38
+ "35": [3],
39
+ "36": [3],
40
+ "37": [3],
41
+ "38": [3],
42
+ "39": [3],
43
+ "40": [3],
44
+ "41": [4],
45
+ "42": [4],
46
+ "43": [4],
47
+ "44": [4],
48
+ "45": [4],
49
+ "46": [4],
50
+ "47": [4],
51
+ "48": [4],
52
+ "49": [4],
53
+ "50": [4],
54
+ "51": [4],
55
+ "52": [4],
56
+ "53": [4],
57
+ "54": [4],
58
+ "55": [5],
59
+ "56": [5],
60
+ "57": [5],
61
+ "58": [5],
62
+ "59": [5],
63
+ "60": [5],
64
+ "61": [5],
65
+ "62": [5],
66
+ "63": [5],
67
+ "64": [5],
68
+ "65": [5],
69
+ "66": [5],
70
+ "67": [5],
71
+ "68": [5],
72
+ "69": [6],
73
+ "70": [6],
74
+ "71": [6],
75
+ "72": [6],
76
+ "73": [6],
77
+ "74": [6],
78
+ "75": [6],
79
+ "76": [6],
80
+ "77": [6],
81
+ "78": [6],
82
+ "79": [6],
83
+ "80": [6],
84
+ "81": [6],
85
+ "82": [6],
86
+ "83": [7],
87
+ "84": [7],
88
+ "85": [7],
89
+ "86": [7],
90
+ "87": [7],
91
+ "88": [7],
92
+ "89": [7],
93
+ "90": [7],
94
+ "91": [7],
95
+ "92": [7],
96
+ "93": [7],
97
+ "94": [7],
98
+ "95": [7],
99
+ "96": [7],
100
+ "97": [8],
101
+ "98": [8],
102
+ "99": [8],
103
+ "100": [8],
104
+ "101": [8],
105
+ "102": [8],
106
+ "103": [8],
107
+ "104": [8],
108
+ "105": [8],
109
+ "106": [8],
110
+ "107": [8],
111
+ "108": [8],
112
+ "109": [8],
113
+ "110": [8],
114
+ "111": [9],
115
+ "112": [9],
116
+ "113": [9],
117
+ "114": [9],
118
+ "115": [9],
119
+ "116": [9],
120
+ "117": [9],
121
+ "118": [9],
122
+ "119": [9],
123
+ "120": [9],
124
+ "121": [9],
125
+ "122": [9],
126
+ "123": [9],
127
+ "124": [9],
128
+ "125": [10],
129
+ "126": [10],
130
+ "127": [10],
131
+ "128": [10],
132
+ "129": [10],
133
+ "130": [10],
134
+ "131": [10],
135
+ "132": [10],
136
+ "133": [10],
137
+ "134": [10],
138
+ "135": [10],
139
+ "136": [10],
140
+ "137": [10],
141
+ "138": [10],
142
+ "139": [11],
143
+ "140": [11],
144
+ "141": [11],
145
+ "142": [11],
146
+ "143": [11],
147
+ "144": [11],
148
+ "145": [11],
149
+ "146": [11],
150
+ "147": [11],
151
+ "148": [11],
152
+ "149": [11],
153
+ "150": [11],
154
+ "151": [11],
155
+ "152": [11],
156
+ "153": [12],
157
+ "154": [12],
158
+ "155": [12],
159
+ "156": [12],
160
+ "157": [12],
161
+ "158": [12],
162
+ "159": [12],
163
+ "160": [12],
164
+ "161": [12],
165
+ "162": [12],
166
+ "163": [12],
167
+ "164": [12],
168
+ "165": [12],
169
+ "166": [12]
170
+ },
171
+ "meta": {
172
+ "count": 167,
173
+ "source": "Categories_Chunks_Segment.json"
174
+ }
175
+ }
Database/Categories/Categories_Embedding_MapData.json ADDED
@@ -0,0 +1,845 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "items": [
3
+ {
4
+ "index": 0,
5
+ "key": "Article",
6
+ "text": "Toán Học"
7
+ },
8
+ {
9
+ "index": 1,
10
+ "key": "Content.SubCategory.Core[0]",
11
+ "text": "Đại số tuyến tính"
12
+ },
13
+ {
14
+ "index": 2,
15
+ "key": "Content.SubCategory.Core[1]",
16
+ "text": "Giải tích vi phân - tích phân"
17
+ },
18
+ {
19
+ "index": 3,
20
+ "key": "Content.SubCategory.Core[2]",
21
+ "text": "Hình học phẳng và không gian"
22
+ },
23
+ {
24
+ "index": 4,
25
+ "key": "Content.SubCategory.Core[3]",
26
+ "text": "Tổ hợp và xác suất"
27
+ },
28
+ {
29
+ "index": 5,
30
+ "key": "Content.SubCategory.Core[4]",
31
+ "text": "Logic toán học"
32
+ },
33
+ {
34
+ "index": 6,
35
+ "key": "Content.SubCategory.Applied[0]",
36
+ "text": "Toán rời rạc"
37
+ },
38
+ {
39
+ "index": 7,
40
+ "key": "Content.SubCategory.Applied[1]",
41
+ "text": "Thống kê ứng dụng"
42
+ },
43
+ {
44
+ "index": 8,
45
+ "key": "Content.SubCategory.Applied[2]",
46
+ "text": "Tối ưu hóa"
47
+ },
48
+ {
49
+ "index": 9,
50
+ "key": "Content.SubCategory.Applied[3]",
51
+ "text": "Toán mô phỏng"
52
+ },
53
+ {
54
+ "index": 10,
55
+ "key": "Content.SubCategory.Interdisciplinary[0]",
56
+ "text": "Toán học trong trí tuệ nhân tạo"
57
+ },
58
+ {
59
+ "index": 11,
60
+ "key": "Content.SubCategory.Interdisciplinary[1]",
61
+ "text": "Toán tài chính"
62
+ },
63
+ {
64
+ "index": 12,
65
+ "key": "Content.SubCategory.Interdisciplinary[2]",
66
+ "text": "Phân tích dữ liệu định lượng"
67
+ },
68
+ {
69
+ "index": 13,
70
+ "key": "Article",
71
+ "text": "Tin Học"
72
+ },
73
+ {
74
+ "index": 14,
75
+ "key": "Content.SubCategory.Core[0]",
76
+ "text": "Thuật toán và cấu trúc dữ liệu"
77
+ },
78
+ {
79
+ "index": 15,
80
+ "key": "Content.SubCategory.Core[1]",
81
+ "text": "Ngôn ngữ lập trình"
82
+ },
83
+ {
84
+ "index": 16,
85
+ "key": "Content.SubCategory.Core[2]",
86
+ "text": "Hệ điều hành"
87
+ },
88
+ {
89
+ "index": 17,
90
+ "key": "Content.SubCategory.Core[3]",
91
+ "text": "Cơ sở dữ liệu"
92
+ },
93
+ {
94
+ "index": 18,
95
+ "key": "Content.SubCategory.Core[4]",
96
+ "text": "Mạng máy tính"
97
+ },
98
+ {
99
+ "index": 19,
100
+ "key": "Content.SubCategory.Applied[0]",
101
+ "text": "Phát triển phần mềm"
102
+ },
103
+ {
104
+ "index": 20,
105
+ "key": "Content.SubCategory.Applied[1]",
106
+ "text": "Phân tích dữ liệu"
107
+ },
108
+ {
109
+ "index": 21,
110
+ "key": "Content.SubCategory.Applied[2]",
111
+ "text": "Trí tuệ nhân tạo"
112
+ },
113
+ {
114
+ "index": 22,
115
+ "key": "Content.SubCategory.Applied[3]",
116
+ "text": "Bảo mật thông tin"
117
+ },
118
+ {
119
+ "index": 23,
120
+ "key": "Content.SubCategory.Interdisciplinary[0]",
121
+ "text": "Khoa học dữ liệu"
122
+ },
123
+ {
124
+ "index": 24,
125
+ "key": "Content.SubCategory.Interdisciplinary[1]",
126
+ "text": "Học máy và học sâu"
127
+ },
128
+ {
129
+ "index": 25,
130
+ "key": "Content.SubCategory.Interdisciplinary[2]",
131
+ "text": "Tin học y sinh"
132
+ },
133
+ {
134
+ "index": 26,
135
+ "key": "Content.SubCategory.Interdisciplinary[3]",
136
+ "text": "Thị giác máy tính"
137
+ },
138
+ {
139
+ "index": 27,
140
+ "key": "Article",
141
+ "text": "Vật Lý"
142
+ },
143
+ {
144
+ "index": 28,
145
+ "key": "Content.SubCategory.Core[0]",
146
+ "text": "Cơ học cổ điển"
147
+ },
148
+ {
149
+ "index": 29,
150
+ "key": "Content.SubCategory.Core[1]",
151
+ "text": "Điện học và từ học"
152
+ },
153
+ {
154
+ "index": 30,
155
+ "key": "Content.SubCategory.Core[2]",
156
+ "text": "Quang học"
157
+ },
158
+ {
159
+ "index": 31,
160
+ "key": "Content.SubCategory.Core[3]",
161
+ "text": "Nhiệt học"
162
+ },
163
+ {
164
+ "index": 32,
165
+ "key": "Content.SubCategory.Core[4]",
166
+ "text": "Dao động và sóng"
167
+ },
168
+ {
169
+ "index": 33,
170
+ "key": "Content.SubCategory.Applied[0]",
171
+ "text": "Điện tử học cơ bản"
172
+ },
173
+ {
174
+ "index": 34,
175
+ "key": "Content.SubCategory.Applied[1]",
176
+ "text": "Vật lý hạt nhân"
177
+ },
178
+ {
179
+ "index": 35,
180
+ "key": "Content.SubCategory.Applied[2]",
181
+ "text": "Vật lý chất rắn"
182
+ },
183
+ {
184
+ "index": 36,
185
+ "key": "Content.SubCategory.Applied[3]",
186
+ "text": "Vật lý thiên văn"
187
+ },
188
+ {
189
+ "index": 37,
190
+ "key": "Content.SubCategory.Interdisciplinary[0]",
191
+ "text": "Vật lý lượng tử"
192
+ },
193
+ {
194
+ "index": 38,
195
+ "key": "Content.SubCategory.Interdisciplinary[1]",
196
+ "text": "Vật lý vật liệu"
197
+ },
198
+ {
199
+ "index": 39,
200
+ "key": "Content.SubCategory.Interdisciplinary[2]",
201
+ "text": "Khoa học năng lượng"
202
+ },
203
+ {
204
+ "index": 40,
205
+ "key": "Content.SubCategory.Interdisciplinary[3]",
206
+ "text": "Vật lý tính toán"
207
+ },
208
+ {
209
+ "index": 41,
210
+ "key": "Article",
211
+ "text": "Hóa Học"
212
+ },
213
+ {
214
+ "index": 42,
215
+ "key": "Content.SubCategory.Core[0]",
216
+ "text": "Hóa vô cơ"
217
+ },
218
+ {
219
+ "index": 43,
220
+ "key": "Content.SubCategory.Core[1]",
221
+ "text": "Hóa hữu cơ"
222
+ },
223
+ {
224
+ "index": 44,
225
+ "key": "Content.SubCategory.Core[2]",
226
+ "text": "Hóa lý"
227
+ },
228
+ {
229
+ "index": 45,
230
+ "key": "Content.SubCategory.Core[3]",
231
+ "text": "Hóa phân tích"
232
+ },
233
+ {
234
+ "index": 46,
235
+ "key": "Content.SubCategory.Core[4]",
236
+ "text": "Liên kết hóa học"
237
+ },
238
+ {
239
+ "index": 47,
240
+ "key": "Content.SubCategory.Applied[0]",
241
+ "text": "Hóa học môi trường"
242
+ },
243
+ {
244
+ "index": 48,
245
+ "key": "Content.SubCategory.Applied[1]",
246
+ "text": "Hóa học vật liệu"
247
+ },
248
+ {
249
+ "index": 49,
250
+ "key": "Content.SubCategory.Applied[2]",
251
+ "text": "Hóa dược"
252
+ },
253
+ {
254
+ "index": 50,
255
+ "key": "Content.SubCategory.Applied[3]",
256
+ "text": "Công nghệ hóa học"
257
+ },
258
+ {
259
+ "index": 51,
260
+ "key": "Content.SubCategory.Interdisciplinary[0]",
261
+ "text": "Hóa sinh học"
262
+ },
263
+ {
264
+ "index": 52,
265
+ "key": "Content.SubCategory.Interdisciplinary[1]",
266
+ "text": "Hóa học tính toán"
267
+ },
268
+ {
269
+ "index": 53,
270
+ "key": "Content.SubCategory.Interdisciplinary[2]",
271
+ "text": "Hóa học năng lượng"
272
+ },
273
+ {
274
+ "index": 54,
275
+ "key": "Content.SubCategory.Interdisciplinary[3]",
276
+ "text": "Hóa học nano"
277
+ },
278
+ {
279
+ "index": 55,
280
+ "key": "Article",
281
+ "text": "Công Nghệ"
282
+ },
283
+ {
284
+ "index": 56,
285
+ "key": "Content.SubCategory.Core[0]",
286
+ "text": "Cơ khí chế tạo"
287
+ },
288
+ {
289
+ "index": 57,
290
+ "key": "Content.SubCategory.Core[1]",
291
+ "text": "Điện - Điện tử cơ bản"
292
+ },
293
+ {
294
+ "index": 58,
295
+ "key": "Content.SubCategory.Core[2]",
296
+ "text": "Tự động hóa"
297
+ },
298
+ {
299
+ "index": 59,
300
+ "key": "Content.SubCategory.Core[3]",
301
+ "text": "Kỹ thuật vật liệu"
302
+ },
303
+ {
304
+ "index": 60,
305
+ "key": "Content.SubCategory.Core[4]",
306
+ "text": "An toàn kỹ thuật"
307
+ },
308
+ {
309
+ "index": 61,
310
+ "key": "Content.SubCategory.Applied[0]",
311
+ "text": "Robot và IoT"
312
+ },
313
+ {
314
+ "index": 62,
315
+ "key": "Content.SubCategory.Applied[1]",
316
+ "text": "Công nghệ năng lượng tái tạo"
317
+ },
318
+ {
319
+ "index": 63,
320
+ "key": "Content.SubCategory.Applied[2]",
321
+ "text": "Công nghệ môi trường"
322
+ },
323
+ {
324
+ "index": 64,
325
+ "key": "Content.SubCategory.Applied[3]",
326
+ "text": "Công nghệ sinh học ứng dụng"
327
+ },
328
+ {
329
+ "index": 65,
330
+ "key": "Content.SubCategory.Interdisciplinary[0]",
331
+ "text": "Công nghệ thực phẩm"
332
+ },
333
+ {
334
+ "index": 66,
335
+ "key": "Content.SubCategory.Interdisciplinary[1]",
336
+ "text": "Công nghệ nano"
337
+ },
338
+ {
339
+ "index": 67,
340
+ "key": "Content.SubCategory.Interdisciplinary[2]",
341
+ "text": "Kỹ thuật sản xuất thông minh"
342
+ },
343
+ {
344
+ "index": 68,
345
+ "key": "Content.SubCategory.Interdisciplinary[3]",
346
+ "text": "Công nghệ xanh và bền vững"
347
+ },
348
+ {
349
+ "index": 69,
350
+ "key": "Article",
351
+ "text": "Sinh Học"
352
+ },
353
+ {
354
+ "index": 70,
355
+ "key": "Content.SubCategory.Core[0]",
356
+ "text": "Tế bào học"
357
+ },
358
+ {
359
+ "index": 71,
360
+ "key": "Content.SubCategory.Core[1]",
361
+ "text": "Di truyền học"
362
+ },
363
+ {
364
+ "index": 72,
365
+ "key": "Content.SubCategory.Core[2]",
366
+ "text": "Sinh lý học"
367
+ },
368
+ {
369
+ "index": 73,
370
+ "key": "Content.SubCategory.Core[3]",
371
+ "text": "Sinh thái học"
372
+ },
373
+ {
374
+ "index": 74,
375
+ "key": "Content.SubCategory.Core[4]",
376
+ "text": "Tiến hóa học"
377
+ },
378
+ {
379
+ "index": 75,
380
+ "key": "Content.SubCategory.Applied[0]",
381
+ "text": "Vi sinh vật học"
382
+ },
383
+ {
384
+ "index": 76,
385
+ "key": "Content.SubCategory.Applied[1]",
386
+ "text": "Công nghệ sinh học"
387
+ },
388
+ {
389
+ "index": 77,
390
+ "key": "Content.SubCategory.Applied[2]",
391
+ "text": "Sinh học phân tử"
392
+ },
393
+ {
394
+ "index": 78,
395
+ "key": "Content.SubCategory.Applied[3]",
396
+ "text": "Sinh học phát triển"
397
+ },
398
+ {
399
+ "index": 79,
400
+ "key": "Content.SubCategory.Interdisciplinary[0]",
401
+ "text": "Sinh học tính toán"
402
+ },
403
+ {
404
+ "index": 80,
405
+ "key": "Content.SubCategory.Interdisciplinary[1]",
406
+ "text": "Sinh học y học"
407
+ },
408
+ {
409
+ "index": 81,
410
+ "key": "Content.SubCategory.Interdisciplinary[2]",
411
+ "text": "Sinh học môi trường"
412
+ },
413
+ {
414
+ "index": 82,
415
+ "key": "Content.SubCategory.Interdisciplinary[3]",
416
+ "text": "Hệ gen học và tin sinh học"
417
+ },
418
+ {
419
+ "index": 83,
420
+ "key": "Article",
421
+ "text": "Văn Học"
422
+ },
423
+ {
424
+ "index": 84,
425
+ "key": "Content.SubCategory.Core[0]",
426
+ "text": "Lý luận văn học"
427
+ },
428
+ {
429
+ "index": 85,
430
+ "key": "Content.SubCategory.Core[1]",
431
+ "text": "Văn học Việt Nam"
432
+ },
433
+ {
434
+ "index": 86,
435
+ "key": "Content.SubCategory.Core[2]",
436
+ "text": "Văn học nước ngoài"
437
+ },
438
+ {
439
+ "index": 87,
440
+ "key": "Content.SubCategory.Core[3]",
441
+ "text": "Ngữ pháp và ngôn ngữ"
442
+ },
443
+ {
444
+ "index": 88,
445
+ "key": "Content.SubCategory.Core[4]",
446
+ "text": "Thể loại văn học"
447
+ },
448
+ {
449
+ "index": 89,
450
+ "key": "Content.SubCategory.Applied[0]",
451
+ "text": "Phân tích và bình giảng tác phẩm"
452
+ },
453
+ {
454
+ "index": 90,
455
+ "key": "Content.SubCategory.Applied[1]",
456
+ "text": "Làm văn - sáng tác"
457
+ },
458
+ {
459
+ "index": 91,
460
+ "key": "Content.SubCategory.Applied[2]",
461
+ "text": "Phong cách học"
462
+ },
463
+ {
464
+ "index": 92,
465
+ "key": "Content.SubCategory.Applied[3]",
466
+ "text": "Phê bình văn học"
467
+ },
468
+ {
469
+ "index": 93,
470
+ "key": "Content.SubCategory.Interdisciplinary[0]",
471
+ "text": "Văn học so sánh"
472
+ },
473
+ {
474
+ "index": 94,
475
+ "key": "Content.SubCategory.Interdisciplinary[1]",
476
+ "text": "Văn hóa học"
477
+ },
478
+ {
479
+ "index": 95,
480
+ "key": "Content.SubCategory.Interdisciplinary[2]",
481
+ "text": "Ngôn ngữ học ứng dụng trong văn học"
482
+ },
483
+ {
484
+ "index": 96,
485
+ "key": "Content.SubCategory.Interdisciplinary[3]",
486
+ "text": "Tư duy phản biện văn học"
487
+ },
488
+ {
489
+ "index": 97,
490
+ "key": "Article",
491
+ "text": "Địa Lý"
492
+ },
493
+ {
494
+ "index": 98,
495
+ "key": "Content.SubCategory.Core[0]",
496
+ "text": "Địa lý tự nhiên"
497
+ },
498
+ {
499
+ "index": 99,
500
+ "key": "Content.SubCategory.Core[1]",
501
+ "text": "Địa lý kinh tế"
502
+ },
503
+ {
504
+ "index": 100,
505
+ "key": "Content.SubCategory.Core[2]",
506
+ "text": "Địa lý dân cư"
507
+ },
508
+ {
509
+ "index": 101,
510
+ "key": "Content.SubCategory.Core[3]",
511
+ "text": "Khí hậu học"
512
+ },
513
+ {
514
+ "index": 102,
515
+ "key": "Content.SubCategory.Core[4]",
516
+ "text": "Địa mạo học"
517
+ },
518
+ {
519
+ "index": 103,
520
+ "key": "Content.SubCategory.Applied[0]",
521
+ "text": "Bản đồ học"
522
+ },
523
+ {
524
+ "index": 104,
525
+ "key": "Content.SubCategory.Applied[1]",
526
+ "text": "Quản lý tài nguyên"
527
+ },
528
+ {
529
+ "index": 105,
530
+ "key": "Content.SubCategory.Applied[2]",
531
+ "text": "Quy hoạch vùng và đô thị"
532
+ },
533
+ {
534
+ "index": 106,
535
+ "key": "Content.SubCategory.Applied[3]",
536
+ "text": "Địa lý Việt Nam"
537
+ },
538
+ {
539
+ "index": 107,
540
+ "key": "Content.SubCategory.Interdisciplinary[0]",
541
+ "text": "Địa lý môi trường"
542
+ },
543
+ {
544
+ "index": 108,
545
+ "key": "Content.SubCategory.Interdisciplinary[1]",
546
+ "text": "Địa lý chính trị"
547
+ },
548
+ {
549
+ "index": 109,
550
+ "key": "Content.SubCategory.Interdisciplinary[2]",
551
+ "text": "GIS và viễn thám"
552
+ },
553
+ {
554
+ "index": 110,
555
+ "key": "Content.SubCategory.Interdisciplinary[3]",
556
+ "text": "Địa lý phát triển bền vững"
557
+ },
558
+ {
559
+ "index": 111,
560
+ "key": "Article",
561
+ "text": "Lịch Sử"
562
+ },
563
+ {
564
+ "index": 112,
565
+ "key": "Content.SubCategory.Core[0]",
566
+ "text": "Lịch sử Việt Nam cổ đại"
567
+ },
568
+ {
569
+ "index": 113,
570
+ "key": "Content.SubCategory.Core[1]",
571
+ "text": "Lịch sử Việt Nam cận đại"
572
+ },
573
+ {
574
+ "index": 114,
575
+ "key": "Content.SubCategory.Core[2]",
576
+ "text": "Lịch sử thế giới cổ đại"
577
+ },
578
+ {
579
+ "index": 115,
580
+ "key": "Content.SubCategory.Core[3]",
581
+ "text": "Lịch sử thế giới hiện đại"
582
+ },
583
+ {
584
+ "index": 116,
585
+ "key": "Content.SubCategory.Core[4]",
586
+ "text": "Phương pháp sử học"
587
+ },
588
+ {
589
+ "index": 117,
590
+ "key": "Content.SubCategory.Applied[0]",
591
+ "text": "Lịch sử chiến tranh và cách mạng"
592
+ },
593
+ {
594
+ "index": 118,
595
+ "key": "Content.SubCategory.Applied[1]",
596
+ "text": "Văn hóa và xã hội qua các thời kỳ"
597
+ },
598
+ {
599
+ "index": 119,
600
+ "key": "Content.SubCategory.Applied[2]",
601
+ "text": "Nhân vật lịch sử tiêu biểu"
602
+ },
603
+ {
604
+ "index": 120,
605
+ "key": "Content.SubCategory.Applied[3]",
606
+ "text": "Nghiên cứu di sản"
607
+ },
608
+ {
609
+ "index": 121,
610
+ "key": "Content.SubCategory.Interdisciplinary[0]",
611
+ "text": "Lịch sử tư tưởng - triết học"
612
+ },
613
+ {
614
+ "index": 122,
615
+ "key": "Content.SubCategory.Interdisciplinary[1]",
616
+ "text": "Lịch sử tôn giáo"
617
+ },
618
+ {
619
+ "index": 123,
620
+ "key": "Content.SubCategory.Interdisciplinary[2]",
621
+ "text": "Lịch sử khu vực (châu Á, Âu, Mỹ)"
622
+ },
623
+ {
624
+ "index": 124,
625
+ "key": "Content.SubCategory.Interdisciplinary[3]",
626
+ "text": "Lịch sử nghệ thuật"
627
+ },
628
+ {
629
+ "index": 125,
630
+ "key": "Article",
631
+ "text": "Kinh Tế"
632
+ },
633
+ {
634
+ "index": 126,
635
+ "key": "Content.SubCategory.Core[0]",
636
+ "text": "Kinh tế vi mô"
637
+ },
638
+ {
639
+ "index": 127,
640
+ "key": "Content.SubCategory.Core[1]",
641
+ "text": "Kinh tế vĩ mô"
642
+ },
643
+ {
644
+ "index": 128,
645
+ "key": "Content.SubCategory.Core[2]",
646
+ "text": "Thống kê kinh tế"
647
+ },
648
+ {
649
+ "index": 129,
650
+ "key": "Content.SubCategory.Core[3]",
651
+ "text": "Kinh tế lượng"
652
+ },
653
+ {
654
+ "index": 130,
655
+ "key": "Content.SubCategory.Core[4]",
656
+ "text": "Lý thuyết trò chơi"
657
+ },
658
+ {
659
+ "index": 131,
660
+ "key": "Content.SubCategory.Applied[0]",
661
+ "text": "Tài chính - Ngân hàng"
662
+ },
663
+ {
664
+ "index": 132,
665
+ "key": "Content.SubCategory.Applied[1]",
666
+ "text": "Kế toán - Kiểm toán"
667
+ },
668
+ {
669
+ "index": 133,
670
+ "key": "Content.SubCategory.Applied[2]",
671
+ "text": "Marketing"
672
+ },
673
+ {
674
+ "index": 134,
675
+ "key": "Content.SubCategory.Applied[3]",
676
+ "text": "Quản trị kinh doanh"
677
+ },
678
+ {
679
+ "index": 135,
680
+ "key": "Content.SubCategory.Interdisciplinary[0]",
681
+ "text": "Kinh tế quốc tế"
682
+ },
683
+ {
684
+ "index": 136,
685
+ "key": "Content.SubCategory.Interdisciplinary[1]",
686
+ "text": "Kinh tế học hành vi"
687
+ },
688
+ {
689
+ "index": 137,
690
+ "key": "Content.SubCategory.Interdisciplinary[2]",
691
+ "text": "Kinh tế phát triển"
692
+ },
693
+ {
694
+ "index": 138,
695
+ "key": "Content.SubCategory.Interdisciplinary[3]",
696
+ "text": "Thương mại điện tử"
697
+ },
698
+ {
699
+ "index": 139,
700
+ "key": "Article",
701
+ "text": "Chính Trị"
702
+ },
703
+ {
704
+ "index": 140,
705
+ "key": "Content.SubCategory.Core[0]",
706
+ "text": "Chủ nghĩa Mác - Lênin"
707
+ },
708
+ {
709
+ "index": 141,
710
+ "key": "Content.SubCategory.Core[1]",
711
+ "text": "Tư tưởng Hồ Chí Minh"
712
+ },
713
+ {
714
+ "index": 142,
715
+ "key": "Content.SubCategory.Core[2]",
716
+ "text": "Hệ thống chính trị Việt Nam"
717
+ },
718
+ {
719
+ "index": 143,
720
+ "key": "Content.SubCategory.Core[3]",
721
+ "text": "Triết học chính trị"
722
+ },
723
+ {
724
+ "index": 144,
725
+ "key": "Content.SubCategory.Core[4]",
726
+ "text": "Lý luận nhà nước và pháp luật"
727
+ },
728
+ {
729
+ "index": 145,
730
+ "key": "Content.SubCategory.Applied[0]",
731
+ "text": "Quan hệ quốc tế"
732
+ },
733
+ {
734
+ "index": 146,
735
+ "key": "Content.SubCategory.Applied[1]",
736
+ "text": "Công tác Đảng và chính quyền"
737
+ },
738
+ {
739
+ "index": 147,
740
+ "key": "Content.SubCategory.Applied[2]",
741
+ "text": "Chính sách công"
742
+ },
743
+ {
744
+ "index": 148,
745
+ "key": "Content.SubCategory.Applied[3]",
746
+ "text": "Quản trị nhà nước"
747
+ },
748
+ {
749
+ "index": 149,
750
+ "key": "Content.SubCategory.Interdisciplinary[0]",
751
+ "text": "Chính trị học so sánh"
752
+ },
753
+ {
754
+ "index": 150,
755
+ "key": "Content.SubCategory.Interdisciplinary[1]",
756
+ "text": "Kinh tế chính trị"
757
+ },
758
+ {
759
+ "index": 151,
760
+ "key": "Content.SubCategory.Interdisciplinary[2]",
761
+ "text": "Xã hội học chính trị"
762
+ },
763
+ {
764
+ "index": 152,
765
+ "key": "Content.SubCategory.Interdisciplinary[3]",
766
+ "text": "Truyền thông chính trị"
767
+ },
768
+ {
769
+ "index": 153,
770
+ "key": "Article",
771
+ "text": "Ngoại Ngữ"
772
+ },
773
+ {
774
+ "index": 154,
775
+ "key": "Content.SubCategory.Core[0]",
776
+ "text": "Ngữ pháp cơ bản"
777
+ },
778
+ {
779
+ "index": 155,
780
+ "key": "Content.SubCategory.Core[1]",
781
+ "text": "Từ vựng và ngữ nghĩa"
782
+ },
783
+ {
784
+ "index": 156,
785
+ "key": "Content.SubCategory.Core[2]",
786
+ "text": "Ngữ âm - Phát âm"
787
+ },
788
+ {
789
+ "index": 157,
790
+ "key": "Content.SubCategory.Core[3]",
791
+ "text": "Cấu trúc câu"
792
+ },
793
+ {
794
+ "index": 158,
795
+ "key": "Content.SubCategory.Core[4]",
796
+ "text": "Ngữ dụng học"
797
+ },
798
+ {
799
+ "index": 159,
800
+ "key": "Content.SubCategory.Applied[0]",
801
+ "text": "Kỹ năng nghe"
802
+ },
803
+ {
804
+ "index": 160,
805
+ "key": "Content.SubCategory.Applied[1]",
806
+ "text": "Kỹ năng nói"
807
+ },
808
+ {
809
+ "index": 161,
810
+ "key": "Content.SubCategory.Applied[2]",
811
+ "text": "Kỹ năng đọc hiểu"
812
+ },
813
+ {
814
+ "index": 162,
815
+ "key": "Content.SubCategory.Applied[3]",
816
+ "text": "Kỹ năng viết"
817
+ },
818
+ {
819
+ "index": 163,
820
+ "key": "Content.SubCategory.Interdisciplinary[0]",
821
+ "text": "Biên - phiên dịch học"
822
+ },
823
+ {
824
+ "index": 164,
825
+ "key": "Content.SubCategory.Interdisciplinary[1]",
826
+ "text": "Ngôn ngữ học so sánh"
827
+ },
828
+ {
829
+ "index": 165,
830
+ "key": "Content.SubCategory.Interdisciplinary[2]",
831
+ "text": "Văn hóa và giao tiếp liên văn hóa"
832
+ },
833
+ {
834
+ "index": 166,
835
+ "key": "Content.SubCategory.Interdisciplinary[3]",
836
+ "text": "Ngôn ngữ học ứng dụng trong AI"
837
+ }
838
+ ],
839
+ "meta": {
840
+ "count": 167,
841
+ "flatten_mode": "split",
842
+ "schema_used": true,
843
+ "list_policy": "split"
844
+ }
845
+ }
Database/Categories/Categories_Embedding_Mapping.json ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "meta": {
3
+ "count": 167,
4
+ "dim": 768,
5
+ "metric": "ip",
6
+ "normalized": true
7
+ },
8
+ "index_to_key": {
9
+ "0": "Article",
10
+ "1": "Content.SubCategory.Core[0]",
11
+ "2": "Content.SubCategory.Core[1]",
12
+ "3": "Content.SubCategory.Core[2]",
13
+ "4": "Content.SubCategory.Core[3]",
14
+ "5": "Content.SubCategory.Core[4]",
15
+ "6": "Content.SubCategory.Applied[0]",
16
+ "7": "Content.SubCategory.Applied[1]",
17
+ "8": "Content.SubCategory.Applied[2]",
18
+ "9": "Content.SubCategory.Applied[3]",
19
+ "10": "Content.SubCategory.Interdisciplinary[0]",
20
+ "11": "Content.SubCategory.Interdisciplinary[1]",
21
+ "12": "Content.SubCategory.Interdisciplinary[2]",
22
+ "13": "Article",
23
+ "14": "Content.SubCategory.Core[0]",
24
+ "15": "Content.SubCategory.Core[1]",
25
+ "16": "Content.SubCategory.Core[2]",
26
+ "17": "Content.SubCategory.Core[3]",
27
+ "18": "Content.SubCategory.Core[4]",
28
+ "19": "Content.SubCategory.Applied[0]",
29
+ "20": "Content.SubCategory.Applied[1]",
30
+ "21": "Content.SubCategory.Applied[2]",
31
+ "22": "Content.SubCategory.Applied[3]",
32
+ "23": "Content.SubCategory.Interdisciplinary[0]",
33
+ "24": "Content.SubCategory.Interdisciplinary[1]",
34
+ "25": "Content.SubCategory.Interdisciplinary[2]",
35
+ "26": "Content.SubCategory.Interdisciplinary[3]",
36
+ "27": "Article",
37
+ "28": "Content.SubCategory.Core[0]",
38
+ "29": "Content.SubCategory.Core[1]",
39
+ "30": "Content.SubCategory.Core[2]",
40
+ "31": "Content.SubCategory.Core[3]",
41
+ "32": "Content.SubCategory.Core[4]",
42
+ "33": "Content.SubCategory.Applied[0]",
43
+ "34": "Content.SubCategory.Applied[1]",
44
+ "35": "Content.SubCategory.Applied[2]",
45
+ "36": "Content.SubCategory.Applied[3]",
46
+ "37": "Content.SubCategory.Interdisciplinary[0]",
47
+ "38": "Content.SubCategory.Interdisciplinary[1]",
48
+ "39": "Content.SubCategory.Interdisciplinary[2]",
49
+ "40": "Content.SubCategory.Interdisciplinary[3]",
50
+ "41": "Article",
51
+ "42": "Content.SubCategory.Core[0]",
52
+ "43": "Content.SubCategory.Core[1]",
53
+ "44": "Content.SubCategory.Core[2]",
54
+ "45": "Content.SubCategory.Core[3]",
55
+ "46": "Content.SubCategory.Core[4]",
56
+ "47": "Content.SubCategory.Applied[0]",
57
+ "48": "Content.SubCategory.Applied[1]",
58
+ "49": "Content.SubCategory.Applied[2]",
59
+ "50": "Content.SubCategory.Applied[3]",
60
+ "51": "Content.SubCategory.Interdisciplinary[0]",
61
+ "52": "Content.SubCategory.Interdisciplinary[1]",
62
+ "53": "Content.SubCategory.Interdisciplinary[2]",
63
+ "54": "Content.SubCategory.Interdisciplinary[3]",
64
+ "55": "Article",
65
+ "56": "Content.SubCategory.Core[0]",
66
+ "57": "Content.SubCategory.Core[1]",
67
+ "58": "Content.SubCategory.Core[2]",
68
+ "59": "Content.SubCategory.Core[3]",
69
+ "60": "Content.SubCategory.Core[4]",
70
+ "61": "Content.SubCategory.Applied[0]",
71
+ "62": "Content.SubCategory.Applied[1]",
72
+ "63": "Content.SubCategory.Applied[2]",
73
+ "64": "Content.SubCategory.Applied[3]",
74
+ "65": "Content.SubCategory.Interdisciplinary[0]",
75
+ "66": "Content.SubCategory.Interdisciplinary[1]",
76
+ "67": "Content.SubCategory.Interdisciplinary[2]",
77
+ "68": "Content.SubCategory.Interdisciplinary[3]",
78
+ "69": "Article",
79
+ "70": "Content.SubCategory.Core[0]",
80
+ "71": "Content.SubCategory.Core[1]",
81
+ "72": "Content.SubCategory.Core[2]",
82
+ "73": "Content.SubCategory.Core[3]",
83
+ "74": "Content.SubCategory.Core[4]",
84
+ "75": "Content.SubCategory.Applied[0]",
85
+ "76": "Content.SubCategory.Applied[1]",
86
+ "77": "Content.SubCategory.Applied[2]",
87
+ "78": "Content.SubCategory.Applied[3]",
88
+ "79": "Content.SubCategory.Interdisciplinary[0]",
89
+ "80": "Content.SubCategory.Interdisciplinary[1]",
90
+ "81": "Content.SubCategory.Interdisciplinary[2]",
91
+ "82": "Content.SubCategory.Interdisciplinary[3]",
92
+ "83": "Article",
93
+ "84": "Content.SubCategory.Core[0]",
94
+ "85": "Content.SubCategory.Core[1]",
95
+ "86": "Content.SubCategory.Core[2]",
96
+ "87": "Content.SubCategory.Core[3]",
97
+ "88": "Content.SubCategory.Core[4]",
98
+ "89": "Content.SubCategory.Applied[0]",
99
+ "90": "Content.SubCategory.Applied[1]",
100
+ "91": "Content.SubCategory.Applied[2]",
101
+ "92": "Content.SubCategory.Applied[3]",
102
+ "93": "Content.SubCategory.Interdisciplinary[0]",
103
+ "94": "Content.SubCategory.Interdisciplinary[1]",
104
+ "95": "Content.SubCategory.Interdisciplinary[2]",
105
+ "96": "Content.SubCategory.Interdisciplinary[3]",
106
+ "97": "Article",
107
+ "98": "Content.SubCategory.Core[0]",
108
+ "99": "Content.SubCategory.Core[1]",
109
+ "100": "Content.SubCategory.Core[2]",
110
+ "101": "Content.SubCategory.Core[3]",
111
+ "102": "Content.SubCategory.Core[4]",
112
+ "103": "Content.SubCategory.Applied[0]",
113
+ "104": "Content.SubCategory.Applied[1]",
114
+ "105": "Content.SubCategory.Applied[2]",
115
+ "106": "Content.SubCategory.Applied[3]",
116
+ "107": "Content.SubCategory.Interdisciplinary[0]",
117
+ "108": "Content.SubCategory.Interdisciplinary[1]",
118
+ "109": "Content.SubCategory.Interdisciplinary[2]",
119
+ "110": "Content.SubCategory.Interdisciplinary[3]",
120
+ "111": "Article",
121
+ "112": "Content.SubCategory.Core[0]",
122
+ "113": "Content.SubCategory.Core[1]",
123
+ "114": "Content.SubCategory.Core[2]",
124
+ "115": "Content.SubCategory.Core[3]",
125
+ "116": "Content.SubCategory.Core[4]",
126
+ "117": "Content.SubCategory.Applied[0]",
127
+ "118": "Content.SubCategory.Applied[1]",
128
+ "119": "Content.SubCategory.Applied[2]",
129
+ "120": "Content.SubCategory.Applied[3]",
130
+ "121": "Content.SubCategory.Interdisciplinary[0]",
131
+ "122": "Content.SubCategory.Interdisciplinary[1]",
132
+ "123": "Content.SubCategory.Interdisciplinary[2]",
133
+ "124": "Content.SubCategory.Interdisciplinary[3]",
134
+ "125": "Article",
135
+ "126": "Content.SubCategory.Core[0]",
136
+ "127": "Content.SubCategory.Core[1]",
137
+ "128": "Content.SubCategory.Core[2]",
138
+ "129": "Content.SubCategory.Core[3]",
139
+ "130": "Content.SubCategory.Core[4]",
140
+ "131": "Content.SubCategory.Applied[0]",
141
+ "132": "Content.SubCategory.Applied[1]",
142
+ "133": "Content.SubCategory.Applied[2]",
143
+ "134": "Content.SubCategory.Applied[3]",
144
+ "135": "Content.SubCategory.Interdisciplinary[0]",
145
+ "136": "Content.SubCategory.Interdisciplinary[1]",
146
+ "137": "Content.SubCategory.Interdisciplinary[2]",
147
+ "138": "Content.SubCategory.Interdisciplinary[3]",
148
+ "139": "Article",
149
+ "140": "Content.SubCategory.Core[0]",
150
+ "141": "Content.SubCategory.Core[1]",
151
+ "142": "Content.SubCategory.Core[2]",
152
+ "143": "Content.SubCategory.Core[3]",
153
+ "144": "Content.SubCategory.Core[4]",
154
+ "145": "Content.SubCategory.Applied[0]",
155
+ "146": "Content.SubCategory.Applied[1]",
156
+ "147": "Content.SubCategory.Applied[2]",
157
+ "148": "Content.SubCategory.Applied[3]",
158
+ "149": "Content.SubCategory.Interdisciplinary[0]",
159
+ "150": "Content.SubCategory.Interdisciplinary[1]",
160
+ "151": "Content.SubCategory.Interdisciplinary[2]",
161
+ "152": "Content.SubCategory.Interdisciplinary[3]",
162
+ "153": "Article",
163
+ "154": "Content.SubCategory.Core[0]",
164
+ "155": "Content.SubCategory.Core[1]",
165
+ "156": "Content.SubCategory.Core[2]",
166
+ "157": "Content.SubCategory.Core[3]",
167
+ "158": "Content.SubCategory.Core[4]",
168
+ "159": "Content.SubCategory.Applied[0]",
169
+ "160": "Content.SubCategory.Applied[1]",
170
+ "161": "Content.SubCategory.Applied[2]",
171
+ "162": "Content.SubCategory.Applied[3]",
172
+ "163": "Content.SubCategory.Interdisciplinary[0]",
173
+ "164": "Content.SubCategory.Interdisciplinary[1]",
174
+ "165": "Content.SubCategory.Interdisciplinary[2]",
175
+ "166": "Content.SubCategory.Interdisciplinary[3]"
176
+ }
177
+ }
Demo/Assets/script.js ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const chatBody = document.getElementById("chatBody");
3
+ const chatForm = document.getElementById("chatForm");
4
+ const fileInput = document.getElementById("fileUpload");
5
+ const fileCard = document.getElementById("fileCard");
6
+ const fileNameSpan = document.getElementById("fileName");
7
+ const removeFileBtn = document.getElementById("removeFile");
8
+
9
+ /**
10
+ * Gõ từng ký tự của một chuỗi văn bản vào một phần tử.
11
+ * @param {HTMLElement} element - Phần tử để gõ chữ vào.
12
+ * @param {string} text - Nội dung văn bản.
13
+ * @param {number} delay - Thời gian trễ giữa các ký tự (ms).
14
+ */
15
+ function typeWriter(element, text, delay = 10) {
16
+ let i = 0;
17
+ element.innerHTML = ""; // Xóa nội dung cũ
18
+
19
+ function typing() {
20
+ if (i < text.length) {
21
+ // Giữ lại các thẻ HTML như **, \n
22
+ if (text.substring(i, i + 2) === "**") {
23
+ let boldEnd = text.indexOf("**", i + 2);
24
+ if (boldEnd !== -1) {
25
+ element.innerHTML += `<strong>${text.substring(i + 2, boldEnd)}</strong>`;
26
+ i = boldEnd + 2;
27
+ }
28
+ } else if (text.substring(i, i + 1) === "\n") {
29
+ element.innerHTML += "<br>";
30
+ i++;
31
+ }
32
+ else {
33
+ element.innerHTML += text.charAt(i);
34
+ i++;
35
+ }
36
+ chatBody.scrollTop = chatBody.scrollHeight; // Cuộn xuống khi gõ
37
+ setTimeout(typing, delay);
38
+ }
39
+ }
40
+ typing();
41
+ }
42
+
43
+
44
+ /**
45
+ * Tạo và nối một hàng tin nhắn mới vào thân chat.
46
+ * @param {string} sender - "user" hoặc "bot".
47
+ * @param {string} text - Nội dung văn bản của tin nhắn.
48
+ * @param {boolean} useTypewriter - Kích hoạt hiệu ứng gõ chữ cho bot.
49
+ */
50
+ function appendMessage(sender, text, useTypewriter = false) {
51
+ const messageRow = document.createElement("div");
52
+ const avatar = document.createElement("div");
53
+ const messageBubble = document.createElement("div")
54
+
55
+ messageRow.classList.add("message-row", `${sender}-row`);
56
+ avatar.classList.add("avatar");
57
+ avatar.textContent = "";
58
+ if (sender === 'bot'){
59
+ messageBubble.classList.add("bot-msg");
60
+ } else {
61
+ messageBubble.classList.add("user-msg");
62
+ }
63
+
64
+ if (sender === 'bot' && useTypewriter) {
65
+ typeWriter(messageBubble, text, 5);
66
+ } else {
67
+ // Thay thế markdown đơn giản cho hiển thị
68
+ const formattedText = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>').replace(/\n/g, '<br>');
69
+ messageBubble.innerHTML = formattedText;
70
+ }
71
+
72
+ messageRow.appendChild(avatar);
73
+ messageRow.appendChild(messageBubble);
74
+ chatBody.appendChild(messageRow);
75
+ chatBody.scrollTop = chatBody.scrollHeight;
76
+ }
77
+
78
+
79
+ /**
80
+ * Hiển thị chỉ báo đang gõ của bot.
81
+ */
82
+ function appendTyping() {
83
+ const typing = document.createElement("div");
84
+ typing.classList.add("typing");
85
+ typing.textContent = "AI đang xử lý...";
86
+ chatBody.appendChild(typing);
87
+ chatBody.scrollTop = chatBody.scrollHeight;
88
+ return typing;
89
+ }
90
+
91
+ // Chào mừng ban đầu
92
+ setTimeout(() => {
93
+ appendMessage("bot", "Xin chào! Hãy tải lên file PDF để tôi tóm tắt và phân loại cho bạn.", true);
94
+ }, 500);
95
+
96
+
97
+ // Khi chọn file, hiển thị card tên
98
+ fileInput.addEventListener("change", () => {
99
+ const file = fileInput.files[0];
100
+ if (file) {
101
+ fileCard.style.display = "flex";
102
+ fileNameSpan.textContent = file.name;
103
+ } else {
104
+ fileCard.style.display = "none";
105
+ }
106
+ });
107
+
108
+ // Nút xóa file
109
+ removeFileBtn.addEventListener("click", () => {
110
+ fileInput.value = "";
111
+ fileCard.style.display = "none";
112
+ });
113
+
114
+ /**
115
+ * Gửi file đến backend API để xử lý.
116
+ * @param {File} file - File PDF cần gửi.
117
+ */
118
+ async function sendFile(file) {
119
+ appendMessage("user", `Attached: ${file.name}`);
120
+
121
+ const typing = appendTyping();
122
+ const formData = new FormData();
123
+ formData.append("file", file);
124
+
125
+ try {
126
+ const response = await fetch("http://127.0.0.1:8000/process_pdf", {
127
+ method: "POST",
128
+ body: formData
129
+ });
130
+ const data = await response.json();
131
+ typing.remove();
132
+
133
+ if (response.ok && data.status == "success") {
134
+
135
+ if (data.checkstatus == 1) {
136
+ appendMessage("bot", `✨**Chatbot**\n Đây là một văn bản về chủ đề **${data.category}** với nội dung được tóm tắt như sau: \n${data.summary}`, true);
137
+ } else {
138
+ appendMessage(
139
+ "bot",
140
+ `✨**Chatbot**\n Văn bản không được chấp nhận:\n${data.summary}\n` +
141
+ `Checkstatus: ${data.checkstatus}\n` +
142
+ `Metrics:\n${JSON.stringify(data.metrics, null, 2)}`,
143
+ true
144
+ );
145
+ }
146
+ } else {
147
+ appendMessage("bot", `❌ **Lỗi:** ${data.message || "Không rõ"}`, true);
148
+ }
149
+ } catch (err) {
150
+ typing.remove();
151
+ appendMessage("bot", "⚠️ **Lỗi kết nối:** Không thể kết nối tới API!", true);
152
+ }
153
+ }
154
+
155
+ // Xử lý việc gửi form
156
+ chatForm.addEventListener("submit", async (e) => {
157
+ e.preventDefault();
158
+ const file = fileInput.files[0];
159
+ if (!file) {
160
+ appendMessage("bot", "⚠️ Vui lòng chọn một file PDF để xử lý.", true);
161
+ return;
162
+ }
163
+ await sendFile(file);
164
+ fileInput.value = "";
165
+ fileCard.style.display = "none";
166
+ });
167
+ });
Demo/Assets/style.css ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
2
+
3
+ :root {
4
+ --bg-primary: #0d1117;
5
+ --bg-secondary: #161b22;
6
+ --bg-tertiary: #21262d;
7
+ --border-color: #30363d;
8
+ --text-primary: #c9d1d9;
9
+ --text-secondary: #8b949e;
10
+ --accent-color: #58a6ff;
11
+ --user-msg-bg: #21262d;
12
+ --bot-msg-bg: #161b22;
13
+ --danger-color: #f85149;
14
+ }
15
+
16
+ * {
17
+ margin: 0;
18
+ padding: 0;
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ body {
23
+ font-family: 'Inter', sans-serif;
24
+ background-color: var(--bg-primary);
25
+ color: var(--text-primary);
26
+ overflow: hidden;
27
+ }
28
+
29
+ .app-container {
30
+ display: grid;
31
+ grid-template-columns: 240px 1fr; /* Bố cục 5 cột có thể quá phức tạp, 2 cột (1 cho menu, 1 cho chat) hiệu quả hơn */
32
+ height: 100vh;
33
+ }
34
+
35
+ /* --- Sidebar (Menu) --- */
36
+ .sidebar {
37
+ background-color: var(--bg-secondary);
38
+ border-right: 1px solid var(--border-color);
39
+ padding: 20px;
40
+ }
41
+
42
+ .sidebar-header h3 {
43
+ font-size: 1.2rem;
44
+ margin-bottom: 24px;
45
+ }
46
+
47
+ .menu ul {
48
+ list-style-type: none;
49
+ }
50
+
51
+ .menu li a {
52
+ display: block;
53
+ padding: 10px 15px;
54
+ text-decoration: none;
55
+ color: var(--text-secondary);
56
+ border-radius: 6px;
57
+ transition: background-color 0.2s, color 0.2s;
58
+ }
59
+
60
+ .menu li a:hover {
61
+ background-color: var(--bg-tertiary);
62
+ color: var(--text-primary);
63
+ }
64
+
65
+ .menu li.active a {
66
+ background-color: rgba(88, 166, 255, 0.1);
67
+ color: var(--accent-color);
68
+ font-weight: 500;
69
+ }
70
+
71
+ /* --- Chat Container --- */
72
+ .chat-container {
73
+ display: flex;
74
+ flex-direction: column;
75
+ background-color: var(--bg-primary);
76
+ }
77
+
78
+ .chat-header {
79
+ padding: 20px 30px;
80
+ border-bottom: 1px solid var(--border-color);
81
+ background-color: var(--bg-secondary);
82
+ }
83
+
84
+ .chat-header h2 {
85
+ font-size: 1.25rem;
86
+ }
87
+
88
+ .chat-header p {
89
+ color: var(--text-secondary);
90
+ font-size: 0.9rem;
91
+ }
92
+
93
+ .chat-body {
94
+ flex-grow: 1;
95
+ overflow-y: auto;
96
+ padding: 20px 30px;
97
+ }
98
+
99
+ /* --- Tin nhắn --- */
100
+ .message-row {
101
+ display: flex;
102
+ align-items: flex-start;
103
+ gap: 15px;
104
+ margin-bottom: 20px;
105
+ max-width: 80%;
106
+ }
107
+
108
+ .bot-row {
109
+ justify-content: flex-start;
110
+ }
111
+
112
+ .user-row {
113
+ justify-content: flex-end;
114
+ margin-left: auto; /* Đẩy tin nhắn người dùng sang phải */
115
+ }
116
+
117
+ .avatar {
118
+ width: 36px;
119
+ height: 36px;
120
+ border-radius: 50%;
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ font-size: 1.2rem;
125
+ background-color: var(--bg-tertiary);
126
+ }
127
+
128
+ .bot-msg, .user-msg {
129
+ padding: 12px 16px;
130
+ border-radius: 18px;
131
+ line-height: 1.6;
132
+ border: 1px solid var(--border-color);
133
+ }
134
+
135
+ .bot-msg {
136
+ background-color: var(--bot-msg-bg);
137
+ border-top-left-radius: 4px;
138
+ }
139
+
140
+ .user-msg {
141
+ background-color: var(--user-msg-bg);
142
+ border-top-right-radius: 4px;
143
+ }
144
+
145
+ /* --- Hiệu ứng đang gõ --- */
146
+ .typing {
147
+ color: var(--text-secondary);
148
+ padding: 10px 30px;
149
+ font-style: italic;
150
+ animation: pulse 1.5s infinite ease-in-out;
151
+ }
152
+
153
+ @keyframes pulse {
154
+ 0% { opacity: 0.5; }
155
+ 50% { opacity: 1; }
156
+ 100% { opacity: 0.5; }
157
+ }
158
+
159
+ /* --- Chat Footer & Form --- */
160
+ .chat-footer {
161
+ padding: 15px 30px;
162
+ border-top: 1px solid var(--border-color);
163
+ background-color: var(--bg-secondary);
164
+ }
165
+
166
+ .chat-form {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 10px;
170
+ background-color: var(--bg-primary);
171
+ border-radius: 8px;
172
+ padding: 5px;
173
+ border: 1px solid var(--border-color);
174
+ }
175
+
176
+ .input-wrapper {
177
+ flex-grow: 1;
178
+ position: relative;
179
+ display: flex;
180
+ align-items: center;
181
+ }
182
+
183
+ .input-wrapper input[type="text"] {
184
+ width: 100%;
185
+ background: none;
186
+ border: none;
187
+ outline: none;
188
+ color: var(--text-primary);
189
+ font-size: 1rem;
190
+ padding: 10px;
191
+ }
192
+
193
+ .input-wrapper input[type="text"]:disabled {
194
+ cursor: not-allowed;
195
+ color: var(--text-secondary);
196
+ }
197
+
198
+ .icon-btn, .send-btn {
199
+ background: none;
200
+ border: none;
201
+ color: var(--text-secondary);
202
+ cursor: pointer;
203
+ padding: 8px;
204
+ border-radius: 6px;
205
+ display: flex;
206
+ align-items: center;
207
+ justify-content: center;
208
+ transition: background-color 0.2s, color 0.2s;
209
+ }
210
+
211
+ .icon-btn:hover {
212
+ background-color: var(--bg-tertiary);
213
+ color: var(--text-primary);
214
+ }
215
+
216
+ .send-btn {
217
+ background-color: var(--accent-color);
218
+ color: var(--bg-primary);
219
+ border-radius: 50%;
220
+ width: 38px;
221
+ height: 38px;
222
+ transition: background-color 0.2s;
223
+ }
224
+
225
+ .send-btn:hover {
226
+ background-color: #79c0ff;
227
+ }
228
+
229
+ .send-btn svg {
230
+ width: 20px;
231
+ height: 20px;
232
+ transform: translateX(1px);
233
+ }
234
+
235
+ /* --- File Card --- */
236
+ #fileCard {
237
+ display: none; /* Ẩn mặc định */
238
+ align-items: center;
239
+ gap: 8px;
240
+ background-color: var(--bg-tertiary);
241
+ padding: 5px 10px;
242
+ border-radius: 6px;
243
+ margin-left: 10px;
244
+ font-size: 0.9rem;
245
+ position: absolute;
246
+ }
247
+
248
+ #fileName {
249
+ max-width: 200px;
250
+ white-space: nowrap;
251
+ overflow: hidden;
252
+ text-overflow: ellipsis;
253
+ }
254
+
255
+ #removeFile {
256
+ background: none;
257
+ border: none;
258
+ color: var(--text-secondary);
259
+ cursor: pointer;
260
+ font-size: 1.2rem;
261
+ line-height: 1;
262
+ }
263
+
264
+ #removeFile:hover {
265
+ color: var(--danger-color);
266
+ }
267
+
268
+ /* --- Scrollbar --- */
269
+ .chat-body::-webkit-scrollbar {
270
+ width: 8px;
271
+ }
272
+ .chat-body::-webkit-scrollbar-track {
273
+ background: var(--bg-secondary);
274
+ }
275
+ .chat-body::-webkit-scrollbar-thumb {
276
+ background-color: var(--bg-tertiary);
277
+ border-radius: 10px;
278
+ border: 2px solid var(--bg-secondary);
279
+ }
Demo/index.html ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>PDF Chatbot</title>
7
+ <link rel="stylesheet" href="Assets/style.css">
8
+ <script src="https://kit.fontawesome.com/a076d05399.js" crossorigin="anonymous"></script> </head>
9
+ <body>
10
+ <div class="app-container">
11
+ <aside class="sidebar">
12
+ <div class="sidebar-header">
13
+ <h3>Menu</h3>
14
+ </div>
15
+ <nav class="menu">
16
+ <ul>
17
+ <li class="active"><a href="#">PDF Assistant</a></li>
18
+ <li><a href="#">⚙️ Cài đặt</a></li>
19
+ <li><a href="#">❓ Trợ giúp</a></li>
20
+ </ul>
21
+ </nav>
22
+ </aside>
23
+
24
+ <main class="chat-container">
25
+ <header class="chat-header">
26
+ <h2>PDF Assistant</h2>
27
+ <p>Tải lên tài liệu của bạn để tóm tắt và phân loại.</p>
28
+ </header>
29
+
30
+ <div id="chatBody" class="chat-body">
31
+ </div>
32
+
33
+ <footer class="chat-footer">
34
+ <form id="chatForm" class="chat-form">
35
+ <label for="fileUpload" class="icon-btn" title="Đính kèm file PDF">
36
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
37
+ </label>
38
+ <input type="file" id="fileUpload" accept=".pdf" hidden>
39
+
40
+ <button type="button" class="icon-btn" title="Công cụ (Tóm tắt, Hỏi đáp)">
41
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>
42
+ </button>
43
+
44
+ <div class="input-wrapper">
45
+ <div id="fileCard" class="file-card">
46
+ <span class="file-icon">📄</span>
47
+ <span id="fileName"></span>
48
+ <button type="button" id="removeFile" class="remove-file-btn">&times;</button>
49
+ </div>
50
+ <input type="text" placeholder="" disabled>
51
+ </div>
52
+
53
+ <button type="submit" class="send-btn" title="Gửi">
54
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z"></path></svg>
55
+ </button>
56
+ </form>
57
+ </footer>
58
+ </main>
59
+ </div>
60
+
61
+ <script src="Assets/script.js"></script>
62
+ </body>
63
+ </html>
Dockerfile ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ---- Base image (CUDA-enabled; works on GPU runners). For CPU, Hugging Face will still run it. ----
2
+ FROM pytorch/pytorch:2.3.1-cuda11.8-cudnn8-runtime
3
+
4
+ # Avoid interactive tzdata prompts
5
+ ENV DEBIAN_FRONTEND=noninteractive \
6
+ PIP_DISABLE_PIP_VERSION_CHECK=1 \
7
+ PYTHONDONTWRITEBYTECODE=1 \
8
+ PYTHONUNBUFFERED=1
9
+
10
+ # System deps (faiss-cpu works; for faiss-gpu you may switch below)
11
+ RUN apt-get update && apt-get install -y --no-install-recommends \
12
+ git git-lfs build-essential poppler-utils \
13
+ && rm -rf /var/lib/apt/lists/*
14
+
15
+ # Enable Git LFS (Spaces uses it automatically, but good to ensure)
16
+ RUN git lfs install
17
+
18
+ # Workdir
19
+ WORKDIR /app
20
+
21
+ # Copy only requirement files first to leverage Docker layer caching
22
+ COPY requirements.txt ./
23
+
24
+ # Install Python deps
25
+ RUN pip install --no-cache-dir -r requirements.txt
26
+
27
+ # Copy the rest
28
+ COPY . .
29
+
30
+ # Expose the port set by Spaces via $PORT
31
+ ENV HOST=0.0.0.0
32
+ ENV PORT=7860
33
+
34
+ # Optional envs (override in Space Secrets)
35
+ ENV HF_TOKEN=""
36
+ ENV API_SECRET=""
37
+
38
+ # Start the server
39
+ CMD ["/bin/bash", "start.sh"]
Environment/bruh.yml ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ======================================
2
+ # Conda Environment for Document AI API
3
+ # ======================================
4
+ name: bruh
5
+
6
+ channels:
7
+ - nvidia
8
+ - pytorch
9
+ - conda-forge
10
+ - defaults
11
+
12
+ dependencies:
13
+ # --- Python base ---
14
+ - python=3.11
15
+ - pip
16
+ - setuptools
17
+ - wheel
18
+
19
+ # --- Core DS stack ---
20
+ - numpy
21
+ - pandas
22
+ - scipy
23
+ - scikit-learn
24
+ - matplotlib
25
+ - seaborn
26
+ - networkx
27
+ - sympy
28
+ - pillow
29
+ - statsmodels
30
+ - joblib
31
+
32
+ # --- GPU / Deep Learning ---
33
+ - pytorch=2.3.1
34
+ - torchvision=0.18.1
35
+ - torchaudio=2.3.1
36
+ - pytorch-cuda=12.1 # GPU CUDA 12.1 runtime (NVIDIA driver >= 530)
37
+
38
+ # --- System & Utility ---
39
+ - psutil
40
+ - pyyaml
41
+ - requests
42
+ - filelock
43
+
44
+ # --- Jupyter (tùy chọn, có thể bỏ nếu deploy server) ---
45
+ - jupyterlab
46
+ - ipython
47
+ - ipykernel
48
+ - ipywidgets
49
+
50
+ # --- NLP, Transformers, Search, Web ---
51
+ - pip:
52
+ # ===============================
53
+ # Transformers Ecosystem
54
+ # ===============================
55
+ - torch==2.3.1
56
+ - torchvision==0.18.1
57
+ - torchaudio==2.3.1
58
+ - transformers==4.44.2
59
+ - sentence-transformers==3.0.1
60
+ - tokenizers>=0.19.1
61
+ - huggingface-hub>=0.23.4
62
+ - safetensors>=0.4.3
63
+ - accelerate==0.31.0
64
+ - datasets>=2.19.0
65
+ - evaluate>=0.4.2
66
+ - sentencepiece>=0.2.0
67
+ - protobuf>=4.25.2
68
+ - nltk>=3.9
69
+ - rouge-score>=0.1.2
70
+
71
+ # ===============================
72
+ # Semantic Search / FAISS
73
+ # ===============================
74
+ - faiss-gpu==1.8.0
75
+
76
+ # ===============================
77
+ # PDF / Text Processing
78
+ # ===============================
79
+ - PyMuPDF==1.24.9
80
+ - underthesea==6.8.4
81
+ - regex>=2024.5.15
82
+ - tqdm
83
+ - openpyxl
84
+ - lxml
85
+ - beautifulsoup4
86
+
87
+ # ===============================
88
+ # Web Framework / API
89
+ # ===============================
90
+ - fastapi==0.115.0
91
+ - uvicorn[standard]==0.30.6
92
+ - starlette>=0.37.2
93
+ - flask
94
+ - flask-cors
95
+
96
+ # ===============================
97
+ # Hugging Face / Google / Utils
98
+ # ===============================
99
+ - google-generativeai
100
+ - fsspec
101
+ - cloudpathlib
102
+ - multiprocess
103
+ - dill
104
+ - typer
105
+ - rich
106
+
107
+ # --- Vietnamese NLP ---
108
+ - underthesea==6.8.4
Environment/env.yml ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # conda env create -f env.yml
2
+ name: bruh
3
+
4
+ channels:
5
+ - conda-forge
6
+ - pytorch
7
+ - nvidia
8
+ - defaults
9
+
10
+ dependencies:
11
+ # --- Nền tảng & DS ---
12
+ - python=3.11
13
+ - pip
14
+ - numpy
15
+ - pandas
16
+ - scipy
17
+ - matplotlib
18
+ - seaborn
19
+ - scikit-learn
20
+ - statsmodels
21
+ - joblib
22
+ - networkx
23
+ - sympy
24
+ - pillow
25
+
26
+ # --- Deep Learning / GPU ---
27
+ - pytorch
28
+ - torchvision
29
+ - torchaudio
30
+ - pytorch-cuda=12.1
31
+
32
+ # --- Notebook & Jupyter ---
33
+ - jupyterlab
34
+ - ipython
35
+ - ipykernel
36
+ - ipywidgets
37
+
38
+ # --- Tiện ích hệ thống ---
39
+ - psutil
40
+ - pyyaml
41
+ - filelock
42
+ - requests
43
+
44
+ # --- Các gói bổ sung cài đặt bằng Pip ---
45
+ - pip:
46
+ - transformers==4.41.2
47
+ - datasets
48
+ - evaluate
49
+ - accelerate==0.31.0
50
+ - sentence-transformers
51
+ - spacy
52
+ - faiss-cpu
53
+ - google-generativeai
54
+ - beautifulsoup4
55
+ - tqdm
56
+ - openpyxl
57
+ - requests
58
+ - lxml
59
+ - rouge-score
60
+ - sentencepiece
61
+ - protobuf
62
+ - ipywidgets
63
+ - PyMuPDF
64
+ - fastapi
65
+ - uvicorn[standard]
66
+
67
+ # --- NLP & HuggingFace ecosystem ---
68
+ - huggingface-hub
69
+ - safetensors
70
+ - tokenizers
71
+ - nltk
72
+
73
+ # --- Web / API phụ trợ ---
74
+ - starlette
75
+ - typer
76
+ - rich
77
+
78
+ # --- Tiện ích khác ---
79
+ - regex
80
+ - multiprocess
81
+ - dill
82
+ - fsspec
83
+ - cloudpathlib
Libraries/Common_MyUtils.py ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import re, os
3
+ import pandas as pd
4
+ import json, csv, openpyxl
5
+
6
+ from typing import Dict, List, Any, Tuple
7
+ from collections import Counter
8
+
9
+
10
+ # ===============================
11
+ # 0. ERROR CATCHER
12
+ # ===============================
13
+ def exc(func, fallback=None):
14
+ """
15
+ Thực thi func() an toàn.
16
+ Nếu lỗi → log exception (e) và trả về fallback.
17
+ """
18
+ try:
19
+ return func()
20
+ except Exception as e:
21
+ logging.warning(e)
22
+ return fallback
23
+
24
+ # ===============================
25
+ # 1. JSON
26
+ # ===============================
27
+ def read_json(path: str) -> Any:
28
+ if not os.path.exists(path):
29
+ return []
30
+ with open(path, "r", encoding="utf-8") as f:
31
+ return json.load(f)
32
+
33
+ def write_json(data: Any, path: str, indent: int = 2) -> None:
34
+ dir_path = os.path.dirname(path)
35
+ if dir_path: os.makedirs(dir_path, exist_ok=True)
36
+ with open(path, "w", encoding="utf-8") as f:
37
+ json.dump(data, f, indent=indent, ensure_ascii=False)
38
+
39
+ def insert_json(data: Any, path: str, indent: int = 2):
40
+ dir_path = os.path.dirname(path)
41
+ if dir_path: os.makedirs(dir_path, exist_ok=True)
42
+ with open(path, 'a', encoding='utf-8') as f:
43
+ json.dump(data, f, indent=indent, ensure_ascii=False)
44
+
45
+
46
+ # ===============================
47
+ # 2. JSONL
48
+ # ===============================
49
+ def read_jsonl(path: str) -> List[dict]:
50
+ with open(path, "r", encoding="utf-8") as f:
51
+ return [json.loads(line) for line in f]
52
+
53
+ def write_jsonl(data: List[dict], path: str) -> None:
54
+ dir_path = os.path.dirname(path)
55
+ if dir_path: os.makedirs(dir_path, exist_ok=True)
56
+ with open(path, "w", encoding="utf-8") as f:
57
+ for item in data:
58
+ f.write(json.dumps(item, ensure_ascii=False) + "\n")
59
+
60
+ def insert_jsonl(data: List[dict], path: str):
61
+ dir_path = os.path.dirname(path)
62
+ if dir_path: os.makedirs(dir_path, exist_ok=True)
63
+ with open(path, 'a', encoding='utf-8') as f:
64
+ for item in data:
65
+ f.write(json.dumps(item, ensure_ascii=False) + '\n')
66
+
67
+
68
+ # ===============================
69
+ # 3. CSV
70
+ # ===============================
71
+ def read_csv(path: str) -> List[dict]:
72
+ with open(path, "r", encoding="utf-8", newline="") as f:
73
+ return list(csv.DictReader(f))
74
+
75
+ def write_csv(data: List[dict], path: str) -> None:
76
+ dir_path = os.path.dirname(path)
77
+ if dir_path: os.makedirs(dir_path, exist_ok=True)
78
+ if not data:
79
+ return
80
+ with open(path, "w", encoding="utf-8", newline="") as f:
81
+ writer = csv.DictWriter(f, fieldnames=data[0].keys())
82
+ writer.writeheader()
83
+ writer.writerows(data)
84
+
85
+
86
+ # ===============================
87
+ # 4.XLSX
88
+ # ===============================
89
+ def read_xlsx(path: str, sheet_name: str = None) -> List[dict]:
90
+ wb = openpyxl.load_workbook(path)
91
+ sheet = wb[sheet_name] if sheet_name else wb.active
92
+ rows = list(sheet.values)
93
+ headers = rows[0]
94
+ return [dict(zip(headers, row)) for row in rows[1:]]
95
+
96
+ def write_xlsx(data: List[dict], path: str, sheet_name: str = "Sheet1") -> None:
97
+ dir_path = os.path.dirname(path)
98
+ if dir_path: os.makedirs(dir_path, exist_ok=True)
99
+ wb = openpyxl.Workbook()
100
+ ws = wb.active
101
+ ws.title = sheet_name
102
+ if not data:
103
+ wb.save(path)
104
+ return
105
+ ws.append(list(data[0].keys()))
106
+ for row in data:
107
+ ws.append(list(row.values()))
108
+ wb.save(path)
109
+
110
+ def convert_to_xlsx(json_path, xlsx_path):
111
+ os.makedirs(os.path.dirname(xlsx_path), exist_ok=True)
112
+ """Chuyển file JSON (dạng list các object) hoặc JSONL sang XLSX."""
113
+ try:
114
+ if json_path.endswith('.jsonl'):
115
+ df = pd.read_json(json_path, lines=True)
116
+ else:
117
+ df = pd.read_json(json_path)
118
+
119
+ column_order = ["category", "sub_category", "url", "title", "description", "content", "date", "words"]
120
+ df = df[[col for col in column_order if col in df.columns]]
121
+ df.to_excel(xlsx_path, index=False, engine='openpyxl')
122
+ print(f"-> Đã xuất thành công file Excel tại {xlsx_path}")
123
+ except (FileNotFoundError, ValueError) as e:
124
+ print(f"-> Không có dữ liệu hoặc lỗi khi chuyển sang Excel: {e}")
125
+
126
+
127
+ # ===============================
128
+ # 5. Convert
129
+ # ===============================
130
+ def json_convert(data: Any, pretty: bool = True) -> str:
131
+ return json.dumps(data, ensure_ascii=False, indent=2 if pretty else None)
132
+
133
+ def jsonl_convert(data: List[dict]) -> str:
134
+ return "\n".join(json.dumps(item, ensure_ascii=False) for item in data)
135
+
136
+
137
+ # ===============================
138
+ # 6. Sort
139
+ # ===============================
140
+ def sort_records(data: List[dict], keys: List[str]) -> List[dict]:
141
+ """Sắp xếp theo nhiều keys với ưu tiên từ trái sang phải"""
142
+ return sorted(data, key=lambda x: tuple(x.get(k) for k in keys))
143
+
144
+
145
+ # ===============================
146
+ # 7. Most Common
147
+ # ===============================
148
+ def most_common(values):
149
+ if not values:
150
+ return None
151
+ return Counter(values).most_common(1)[0][0]
152
+
153
+ DEFAULT_NON_KEEP_PATTERN = re.compile(r"[^\w\s\(\)\.\,\;\:\-–]", flags=re.UNICODE)
154
+
155
+ def preprocess_text(
156
+ text: Any,
157
+ non_keep_pattern: re.Pattern = DEFAULT_NON_KEEP_PATTERN,
158
+ max_chars_per_text: int | None = None,
159
+ ) -> Any:
160
+ """
161
+ Làm sạch chuỗi: strip, bỏ ký tự không mong muốn, rút gọn khoảng trắng.
162
+ Vẫn cho phép list/dict đi qua để hàm preprocess_data xử lý đệ quy.
163
+ """
164
+ if isinstance(text, list):
165
+ # Truyền tiếp đủ tham số khi gọi đệ quy
166
+ return [preprocess_text(t, non_keep_pattern=non_keep_pattern, max_chars_per_text=max_chars_per_text) for t in text]
167
+ if isinstance(text, str):
168
+ s = text.strip() # <-- sửa từ s = strip()
169
+ s = non_keep_pattern.sub("", s)
170
+ s = re.sub(r"[ ]{2,}", " ", s)
171
+ if max_chars_per_text is not None and len(s) > max_chars_per_text:
172
+ s = s[: max_chars_per_text]
173
+ return s
174
+ return text
175
+
176
+ def preprocess_data(
177
+ data: Any,
178
+ non_keep_pattern: re.Pattern = DEFAULT_NON_KEEP_PATTERN,
179
+ max_chars_per_text: int | None = None,
180
+ ) -> Any:
181
+ """Đệ quy tiền xử lý lên toàn bộ JSON."""
182
+ if isinstance(data, dict):
183
+ return {
184
+ k: preprocess_data(v, non_keep_pattern=non_keep_pattern, max_chars_per_text=max_chars_per_text)
185
+ for k, v in data.items()
186
+ }
187
+ if isinstance(data, list):
188
+ return [
189
+ preprocess_data(x, non_keep_pattern=non_keep_pattern, max_chars_per_text=max_chars_per_text)
190
+ for x in data
191
+ ]
192
+ return preprocess_text(data, non_keep_pattern=non_keep_pattern, max_chars_per_text=max_chars_per_text)
193
+
194
+
195
+ # ===============================
196
+ # 9. Json
197
+ # ===============================
198
+ def flatten_json(
199
+ data: Any,
200
+ prefix: str = "",
201
+ flatten_mode: str = "split", # mặc định: tách từng phần tử list
202
+ join_sep: str = "\n", # mặc định: xuống dòng khi join list
203
+ ) -> Dict[str, Any]:
204
+ """
205
+ Làm phẳng JSON với xử lý list theo flatten_mode.
206
+
207
+ - "split": mỗi phần tử list tạo key riêng: a.b[0], a.b[1], ...
208
+ Nếu phần tử là dict/list → tiếp tục flatten (được lồng chỉ số).
209
+ - "join": join list về 1 chuỗi (join_sep). (Phần tử không phải str sẽ str())
210
+ - "keep": giữ nguyên list (chỉ gán 1 key cho toàn list).
211
+
212
+ Trả về: dict key->giá trị (lá).
213
+ """
214
+ flat: Dict[str, Any] = {}
215
+
216
+ def _recur(node: Any, pfx: str) -> None:
217
+ if isinstance(node, dict):
218
+ for k, v in node.items():
219
+ new_pfx = f"{pfx}{k}" if not pfx else f"{pfx}.{k}"
220
+ _recur(v, new_pfx)
221
+ return
222
+
223
+ if isinstance(node, list):
224
+ if flatten_mode == "split":
225
+ for i, item in enumerate(node):
226
+ idx_key = f"{pfx}[{i}]"
227
+ _recur(item, idx_key)
228
+ elif flatten_mode == "join":
229
+ joined = join_sep.join(str(x).strip() for x in node if str(x).strip())
230
+ flat[pfx] = joined
231
+ else: # "keep"
232
+ flat[pfx] = node
233
+ return
234
+
235
+ # lá: số/chuỗi/None/...
236
+ flat[pfx] = node
237
+
238
+ _recur(data, prefix.rstrip("."))
239
+ return flat
240
+
241
+
242
+ def deduplicates_by_key(pairs: List[Tuple[str, str]]) -> List[Tuple[str, str]]:
243
+ """
244
+ Lọc trùng theo value trong cùng key (hoặc base_key).
245
+
246
+ Giữ lại **lần xuất hiện đầu tiên** của mỗi (key, text),
247
+ loại bỏ những dòng có cùng key và cùng text lặp lại sau đó.
248
+
249
+ Args:
250
+ pairs: Danh sách (key, text) sau khi flatten.
251
+
252
+ Returns:
253
+ Danh sách (key, text) đã loại bỏ trùng lặp.
254
+ """
255
+ seen_per_key: Dict[str, set] = {}
256
+ filtered: List[Tuple[str, str]] = []
257
+
258
+ for key, text in pairs:
259
+ text_norm = text.strip()
260
+ if not text_norm:
261
+ continue
262
+
263
+ base_key = re.sub(r"\[\d+\]", "", key)
264
+ if base_key not in seen_per_key:
265
+ seen_per_key[base_key] = set()
266
+
267
+ if text_norm in seen_per_key[base_key]:
268
+ continue
269
+
270
+ seen_per_key[base_key].add(text_norm)
271
+ filtered.append((key, text_norm))
272
+
273
+ return filtered
Libraries/Common_PdfProcess.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from . import Common_MyUtils as MyUtils
2
+
3
+ # ===============================
4
+ # 1. General
5
+ # ===============================
6
+
7
+ def fontFlags(span):
8
+ """Trả về tuple booleans (bold, italic, underline) từ span.flags"""
9
+ flags = span.get("flags", 0)
10
+ b = bool(flags & 16)
11
+ i = bool(flags & 2)
12
+ u = bool(flags & 8)
13
+ return b, i, u
14
+
15
+ def setAlign(position, regionWidth):
16
+ mid = abs(position["Mid"])
17
+ left = position["Left"]
18
+ if mid <= 0.01 * regionWidth:
19
+ if left > 0.01 * regionWidth:
20
+ return "Center"
21
+ else:
22
+ return "Justify"
23
+ elif position["Mid"] > 0.01 * regionWidth:
24
+ return "Right"
25
+ else:
26
+ return "Left"
27
+
28
+ def setPosition(line, prev_line, next_line, xStart, xEnd, xMid):
29
+ left = round(line["Coords"]["X0"] - xStart, 1)
30
+ right = round(xEnd - line["Coords"]["X1"], 1)
31
+ mid = round(line["Coords"]["XM"] - xMid, 1)
32
+ top = round(line["Coords"]["Y1"] - prev_line["Coords"]["Y1"], 1) if prev_line else 0
33
+ bot = round(next_line["Coords"]["Y1"] - line["Coords"]["Y1"], 1) if next_line else 0
34
+ return (left, right, mid, top, bot)
35
+
36
+
37
+ # ===============================
38
+ # 2. Words
39
+ # ===============================
40
+
41
+ def extractWords(line):
42
+ """Trả về list [(word, span)] theo thứ tự trong line; giữ nguyên dấu câu."""
43
+ spans = line.get("spans", [])
44
+ full_text = line.get("text", "")
45
+ if not spans or not full_text.strip():
46
+ return []
47
+
48
+ # chỉ giữ spans có chữ thật
49
+ valid_spans = [s for s in spans if s.get("text", "").strip()]
50
+ if not valid_spans:
51
+ valid_spans = spans
52
+
53
+ words = []
54
+ for s in valid_spans:
55
+ for raw in s.get("text", "").split():
56
+ words.append((raw, s))
57
+ return words
58
+
59
+ def getWordText(line, index: int):
60
+ """Lấy Text của từ tại vị trí index (hỗ trợ index âm)."""
61
+ words = extractWords(line)
62
+ if -len(words) <= index < len(words):
63
+ return words[index][0]
64
+ return ""
65
+
66
+ def getWordFontSize(line, index: int):
67
+ """Lấy FontSize của từ tại vị trí index."""
68
+ words = extractWords(line)
69
+ if -len(words) <= index < len(words):
70
+ _, span = words[index]
71
+ return round(span.get("size", 12.0), 1)
72
+ return 0.0
73
+
74
+ def getWordCoord(line, index: int):
75
+ """Lấy tọa độ (x0, x1, xm, y0, y1) của từ tại vị trí index (dựa bbox của span chứa từ)."""
76
+ words = extractWords(line)
77
+ if -len(words) <= index < len(words):
78
+ _, span = words[index]
79
+ x0, y0, x1, y1 = span["bbox"]
80
+ x0, y0, x1, y1 = round(x0, 1), round(y0, 1), round(x1, 1), round(y1, 1)
81
+ return (x0, x1, y0, y1)
82
+ return (0, 0, 0, 0)
83
+
84
+
85
+ # ===============================
86
+ # 3. Lines
87
+ # ===============================
88
+
89
+ def getLineFontSize(line):
90
+ """FontSize của line = mean FontSize các từ (làm tròn 0.5)."""
91
+ words = extractWords(line)
92
+ if not words:
93
+ return 12.0
94
+ sizes = [span.get("size", 12.0) for _, span in words]
95
+ avg = sum(sizes) / len(sizes)
96
+ return round(avg * 2) / 2
97
+
98
+ def getLineCoord(line):
99
+ """
100
+ Coord của line:
101
+ - x0 = x0 của từ đầu tiên
102
+ - x1 = x1 của từ cuối cùng
103
+ - y0 = min(y0) các từ
104
+ - y1 = max(y1) các từ
105
+ - xm = (x0 + x1) / 2
106
+ """
107
+ words = extractWords(line)
108
+ if not words:
109
+ return (0, 0, 0, 0, 0)
110
+
111
+ coords = []
112
+ for _, span in words:
113
+ x0, y0, x1, y1 = span["bbox"]
114
+ coords.append((round(x0, 1), round(y0, 1), round(x1, 1), round(y1, 1)))
115
+
116
+ x0 = coords[0][0]
117
+ x1 = coords[-1][2]
118
+ y0 = min(c[1] for c in coords)
119
+ y1 = max(c[3] for c in coords)
120
+ xm = round((x0 + x1) / 2, 1)
121
+ return (x0, x1, xm, y0, y1)
122
+
123
+ def setLineSize(line):
124
+ x0, x1, y0, y1 = line["Coords"]["X0"], line["Coords"]["X1"], line["Coords"]["Y0"], line["Coords"]["Y1"]
125
+ return (round(x1 - x0, 1), round(y1 - y0, 1))
126
+
127
+
128
+ # ===============================
129
+ # 4. Page
130
+ # ===============================
131
+
132
+ def setPageCoords(lines, pageGeneralSize):
133
+ x0s = [round(l["Coords"]["X0"], 1) for l in lines]
134
+ x1s = [round(l["Coords"]["X1"], 1) for l in lines]
135
+ y0s = [round(l["Coords"]["Y0"], 1) for l in lines]
136
+ y1s = [round(l["Coords"]["Y1"], 1) for l in lines]
137
+
138
+ xStart = MyUtils.most_common(x0s)
139
+ page_width = pageGeneralSize[1]
140
+ threshold = page_width * 0.75
141
+ x1_candidates = [x for x in x1s if x >= threshold]
142
+ xEnd = MyUtils.most_common(x1_candidates) if x1_candidates else max(x1s)
143
+
144
+ yStart = min(y0s)
145
+ yEnd = max(y1s)
146
+ xMid = round((xStart + xEnd) / 2, 1)
147
+ yMid = round((yStart + yEnd) / 2, 1)
148
+
149
+ return (xStart, yStart, xEnd, yEnd, xMid, yMid)
150
+
151
+ def setPageRegionSize(xStart, yStart, xEnd, yEnd):
152
+ return (round(xEnd - xStart, 1), round(yEnd - yStart, 1))
Libraries/Common_TextProcess.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ from difflib import SequenceMatcher
4
+
5
+ from . import Common_MyUtils as MyUtils
6
+
7
+ ex = MyUtils.exc
8
+
9
+ # ===============================
10
+ # 1. Abbreviation
11
+ # ===============================
12
+
13
+ # Phụ âm đầu
14
+ VALID_ONSETS = [
15
+ "b", "c", "ch", "d", "đ", "g", "gh", "gi",
16
+ "h", "k", "kh", "l", "m", "n", "ng", "ngh",
17
+ "nh", "p", "ph", "q", "r", "s", "t", "th",
18
+ "tr", "v", "x"
19
+ ]
20
+
21
+ # Nguyên âm
22
+ VALID_NUCLEI = [
23
+ "a", "ă", "â", "e", "ê", "i", "o", "ô", "ơ", "u", "ư", "y",
24
+
25
+ "ia", "iê", "ya", "ya", "ua", "uô", "ưa", "ươ",
26
+ "ai", "ao", "au", "ay", "âu", "ây",
27
+ "eo", "êu",
28
+ "ia", "iê", "yê",
29
+ "oi", "ôi", "ơi",
30
+ "ua", "uô", "ươ", "ưu", "uy", "uya"
31
+ ]
32
+
33
+ # Phụ âm cuối
34
+ VALID_CODAS = ["c", "ch", "m", "n", "ng", "nh", "p", "t"]
35
+
36
+ # ===== Hàm kiểm tra viết tắt =====
37
+ def is_abbreviation(word: str) -> bool:
38
+ """
39
+ Trả về True nếu từ KHÔNG phải âm tiết tiếng Việt chuẩn,
40
+ tức là có khả năng là viết tắt.
41
+ Quy tắc:
42
+ 1. Không có nguyên âm hoặc nguyên âm không hợp lệ -> viết tắt
43
+ 2. Phụ âm đầu không hợp lệ -> viết tắt
44
+ 3. Phụ âm cuối không hợp lệ -> viết tắt
45
+ 4. Nhiều hơn 3 phần (đầu - nguyên âm - cuối) -> viết tắt
46
+ """
47
+ w = word.lower()
48
+ w = re.sub(r'[^a-zăâêôơưđ]', '', w)
49
+
50
+ if not w:
51
+ return True
52
+
53
+ # 1. Tìm phụ âm đầu
54
+ onset = None
55
+ for o in sorted(VALID_ONSETS, key=len, reverse=True):
56
+ if w.startswith(o):
57
+ onset = o
58
+ break
59
+
60
+ rest = w[len(onset):] if onset else w
61
+ if onset is None and rest and rest[0] not in "aeiouyăâêôơư":
62
+ return True # phụ âm đầu không hợp lệ
63
+
64
+ # 2. Tìm phụ âm cuối
65
+ coda = None
66
+ for c in sorted(VALID_CODAS, key=len, reverse=True):
67
+ if rest.endswith(c):
68
+ coda = c
69
+ break
70
+
71
+ nucleus = rest[:-len(coda)] if coda else rest
72
+
73
+ # 3. Kiểm tra nguyên âm
74
+ if not nucleus:
75
+ return True
76
+ if nucleus not in VALID_NUCLEI:
77
+ return True
78
+
79
+ # 4. Kiểm tra số phần
80
+ parts = [p for p in [onset, nucleus, coda] if p]
81
+ if len(parts) > 3:
82
+ return True
83
+
84
+ return False
85
+
86
+ # ===============================
87
+ # 2. Words
88
+ # ===============================
89
+
90
+ # ===== Hàm chuẩn hóa từ ======================
91
+ def normalize_word(w: str) -> str:
92
+ return re.sub(r'[^A-Za-zÀ-ỹĐđ0-9]', '', w)
93
+
94
+ # ===== Hàm so sánh độ tương đồng =============
95
+ def similar(a, b):
96
+ return SequenceMatcher(None, a, b).ratio()
97
+
98
+ # ===== Hàm chuyển số La Mã ===================
99
+ def is_roman(s):
100
+ return bool(re.fullmatch(r'[IVXLC]+', s))
101
+
102
+ # ===== Chuyển số La Mã sang số Ả Rập =========
103
+ def roman_to_int(s):
104
+ roman_numerals = {'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100}
105
+ result, prev = 0, 0
106
+ for c in reversed(s):
107
+ val = roman_numerals.get(c, 0)
108
+ if val < prev:
109
+ result -= val
110
+ else:
111
+ result += val
112
+ prev = val
113
+ return result
114
+
115
+ # ===== Hàm loại bỏ khoảng trắng thừa =========
116
+ def strip_extra_spaces(s: str) -> str:
117
+ if not isinstance(s, str):
118
+ return s
119
+ return re.sub(r'\s+', ' ', s).strip()
120
+
121
+ def merge_txt(RawDataDict, JsonKey, JsonField):
122
+ paragraphs = RawDataDict.get(JsonKey, [])
123
+ merged = "\n".join(p.get(JsonField, "").strip() for p in paragraphs if p.get(JsonField))
124
+ merged = re.sub(r"\n{2,}", "\n", merged.strip())
125
+ return merged
Libraries/Faiss_ChunkMapping.py ADDED
@@ -0,0 +1,184 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Any, Optional, Iterable
2
+
3
+ # --------- A. Tiện ích cơ bản ---------
4
+
5
+ def _ordered_unique_chunk_ids(reranked: List[Dict[str, Any]]) -> List[int]:
6
+ seen, ordered = set(), []
7
+ for r in reranked:
8
+ for cid in r.get("chunk_ids", []):
9
+ if isinstance(cid, (int, str)) and str(cid).isdigit():
10
+ cid = int(cid)
11
+ if cid not in seen:
12
+ seen.add(cid)
13
+ ordered.append(cid)
14
+ return ordered
15
+
16
+
17
+ def _filter_fields_recursive(obj: Any, drop_lower: set) -> Any:
18
+ """Loại bỏ các field có tên xuất hiện trong drop_lower (case-insensitive) trên toàn cấu trúc."""
19
+ if isinstance(obj, dict):
20
+ return {
21
+ k: _filter_fields_recursive(v, drop_lower)
22
+ for k, v in obj.items()
23
+ if k.lower() not in drop_lower
24
+ }
25
+ if isinstance(obj, list):
26
+ return [_filter_fields_recursive(x, drop_lower) for x in obj]
27
+ return obj
28
+
29
+
30
+ def _iter_values_no_keys(obj: Any) -> Iterable[str]:
31
+ """Duyệt đệ quy, chỉ yield GIÁ TRỊ (bỏ key), split theo '\n' nếu là chuỗi."""
32
+ if isinstance(obj, dict):
33
+ for v in obj.values():
34
+ yield from _iter_values_no_keys(v)
35
+ elif isinstance(obj, list):
36
+ for item in obj:
37
+ yield from _iter_values_no_keys(item)
38
+ elif isinstance(obj, str):
39
+ for line in obj.splitlines():
40
+ yield line
41
+ else:
42
+ yield str(obj)
43
+
44
+
45
+ def _get_by_path(obj: Any, path: str) -> Any:
46
+ """
47
+ Lấy giá trị theo path kiểu 'A.B.C'.
48
+ - Nếu gặp list trong quá trình đi xuống → thu thập giá trị từ từng phần tử (map-collect).
49
+ - Nếu path không tồn tại → trả về None.
50
+ """
51
+ parts = path.split(".")
52
+ def _step(o, idx=0):
53
+ if idx == len(parts):
54
+ return o
55
+ key = parts[idx]
56
+ if isinstance(o, dict):
57
+ if key not in o:
58
+ return None
59
+ return _step(o[key], idx + 1)
60
+ if isinstance(o, list):
61
+ collected = []
62
+ for it in o:
63
+ collected.append(_step(it, idx))
64
+ # gộp phẳng các None
65
+ flat = []
66
+ for v in collected:
67
+ if v is None:
68
+ continue
69
+ if isinstance(v, list):
70
+ flat.extend(v)
71
+ else:
72
+ flat.append(v)
73
+ return flat
74
+ return None
75
+ return _step(obj, 0)
76
+
77
+
78
+ # --------- B. Các hàm chính ---------
79
+
80
+ def extract_chunks_from_rerank_flexible(
81
+ reranked_results: List[Dict[str, Any]],
82
+ SegmentDict: List[Dict[str, Any]],
83
+ n_chunks: Optional[int] = None,
84
+ drop_fields: Optional[List[str]] = None,
85
+ ) -> List[Dict[str, Any]]:
86
+ """
87
+ - Lấy chunk theo thứ tự từ reranked.
88
+ - Giới hạn số lượng chunk gốc trả về bằng n_chunks (nếu có).
89
+ - Áp dụng bỏ trường theo drop_fields (toàn bộ cấu trúc).
90
+ - Kết quả: [{"chunk_id": int, "data": <json đã lọc>}]
91
+ """
92
+ if not reranked_results:
93
+ return []
94
+
95
+ ordered_ids = _ordered_unique_chunk_ids(reranked_results)
96
+ if n_chunks is not None:
97
+ ordered_ids = ordered_ids[:int(n_chunks)]
98
+
99
+ drop_lower = set(x.lower() for x in (drop_fields or []))
100
+
101
+ out = []
102
+ seen = set()
103
+ for cid in ordered_ids:
104
+ if cid in seen:
105
+ continue
106
+ seen.add(cid)
107
+ if 1 <= cid <= len(SegmentDict):
108
+ data = SegmentDict[cid - 1]
109
+ filtered = _filter_fields_recursive(data, drop_lower) if drop_lower else data
110
+ out.append({"chunk_id": cid, "data": filtered})
111
+ return out
112
+
113
+
114
+ def collect_chunk_text(chunks: List[Dict[str, Any]]) -> str:
115
+ """Biến toàn bộ danh sách chunk thành text (bỏ key, split dòng)."""
116
+ if not chunks:
117
+ return "(Không có chunk nào)"
118
+
119
+ lines: List[str] = []
120
+ for ch in chunks:
121
+ for line in _iter_values_no_keys(ch["data"]):
122
+ lines.append(line)
123
+ lines.append("")
124
+ return "\n".join(lines).strip()
125
+
126
+
127
+ def extract_fields_for_each_chunk(
128
+ chunks: List[Dict[str, Any]],
129
+ fields: Optional[List[str]] = None,
130
+ ) -> List[Dict[str, Any]]:
131
+ """
132
+ - Với mỗi chunk gốc, lấy những TRƯỜNG được truyền vào (hỗ trợ path 'A.B.C').
133
+ - Nếu fields=None → lấy TẤT CẢ top-level fields còn lại trong chunk['data'].
134
+ - Trả về list theo từng chunk: {"chunk_id": ..., "fields": {...}}
135
+ """
136
+ results = []
137
+ for ch in chunks:
138
+ data = ch["data"]
139
+ if not isinstance(data, dict):
140
+ results.append({"chunk_id": ch["chunk_id"], "fields": data})
141
+ continue
142
+
143
+ if fields is None:
144
+ payload = {k: v for k, v in data.items()}
145
+ else:
146
+ payload = {}
147
+ for f in fields:
148
+ payload[f] = _get_by_path(data, f)
149
+ results.append({"chunk_id": ch["chunk_id"], "fields": payload})
150
+ return results
151
+
152
+
153
+ def process_chunks_pipeline(
154
+ reranked_results: List[Dict[str, Any]],
155
+ SegmentDict: List[Dict[str, Any]],
156
+ drop_fields: Optional[List[str]] = None, # Trường bị bỏ qua (áp dụng toàn bộ)
157
+ fields: Optional[List[str]] = None, # Trường muốn trích xuất (None → tất cả top-level)
158
+ n_chunks: Optional[int] = None # Số lượng chunk gốc & text (nếu None → tất cả)
159
+ ) -> Dict[str, Any]:
160
+ """
161
+ Trả về:
162
+ - chunks_json: đúng số lượng chunk gốc (đã drop_fields)
163
+ - chunks_text: text từ cùng số lượng chunk (bỏ key, split dòng)
164
+ - extracted_fields: các trường được chỉ định cho mỗi chunk
165
+ """
166
+ # 1️⃣ Lấy chunk gốc (JSON)
167
+ chunks_json = extract_chunks_from_rerank_flexible(
168
+ reranked_results=reranked_results,
169
+ SegmentDict=SegmentDict,
170
+ n_chunks=n_chunks,
171
+ drop_fields=drop_fields,
172
+ )
173
+
174
+ # 2️⃣ Biến thành text (cùng số lượng chunk)
175
+ chunks_text = collect_chunk_text(chunks_json)
176
+
177
+ # 3️⃣ Lấy các trường cụ thể
178
+ extracted_fields = extract_fields_for_each_chunk(chunks_json, fields=fields)
179
+
180
+ return {
181
+ "chunks_json": chunks_json, # JSON chuẩn
182
+ "chunks_text": chunks_text, # text của cùng số lượng chunk
183
+ "extracted_fields": extracted_fields # field được chọn
184
+ }
Libraries/Faiss_Embedding.py ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import re, os
3
+ import torch
4
+ import faiss
5
+ import numpy as np
6
+
7
+ from typing import Dict, List, Any, Tuple, Optional
8
+
9
+ from . import Common_MyUtils as MyUtils
10
+
11
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
12
+
13
+ class DirectFaissIndexer:
14
+ """
15
+ 1) FaissPath (.faiss): chỉ chứa vectors,
16
+ 2) MapDataPath (.json): content + index,
17
+ 3) MappingPath (.json): ánh xạ key <-> index.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ indexer: Any,
23
+ device: str = "cpu",
24
+ batch_size: int = 32,
25
+ show_progress: bool = False,
26
+ flatten_mode: str = "split",
27
+ join_sep: str = "\n",
28
+ allowed_schema_types: Tuple[str, ...] = ("string", "array", "dict"),
29
+ max_chars_per_text: Optional[int] = None,
30
+ normalize: bool = True,
31
+ verbose: bool = False,
32
+ list_policy: str = "split", # "merge" | "split"
33
+ ):
34
+ self.indexer = indexer
35
+ self.device = device
36
+ self.batch_size = batch_size
37
+ self.show_progress = show_progress
38
+ self.flatten_mode = flatten_mode
39
+ self.join_sep = join_sep
40
+ self.allowed_schema_types = allowed_schema_types
41
+ self.max_chars_per_text = max_chars_per_text
42
+ self.normalize = normalize
43
+ self.verbose = verbose
44
+ self.list_policy = list_policy
45
+
46
+ self._non_keep_pattern = re.compile(r"[^\w\s\(\)\.\,\;\:\-–]", flags=re.UNICODE)
47
+
48
+ # ---------- Schema & chọn trường ----------
49
+
50
+ @staticmethod
51
+ def _base_key_for_schema(key: str) -> str:
52
+
53
+ return re.sub(r"\[\d+\]", "", key)
54
+
55
+ def _eligible_by_schema(self, key: str, schema: Optional[Dict[str, str]]) -> bool:
56
+ if schema is None:
57
+ return True
58
+ base_key = self._base_key_for_schema(key)
59
+ typ = schema.get(base_key)
60
+ return (typ in self.allowed_schema_types) if typ is not None else False
61
+
62
+ # ---------- Tiền xử lý & flatten ----------
63
+ def _preprocess_data(self, data: Any) -> Any:
64
+
65
+ if MyUtils and hasattr(MyUtils, "preprocess_data"):
66
+ return MyUtils.preprocess_data(
67
+ data,
68
+ non_keep_pattern=self._non_keep_pattern,
69
+ max_chars_per_text=self.max_chars_per_text
70
+ )
71
+
72
+ def _flatten_json(self, data: Any) -> Dict[str, Any]:
73
+ """
74
+ Flatten JSON theo list_policy:
75
+ - merge: gộp list/dict chứa chuỗi thành 1 đoạn text duy nhất
76
+ - split: tách từng phần tử
77
+ """
78
+ # Nếu merge, xử lý JSON trước khi flatten
79
+ if self.list_policy == "merge":
80
+ def _merge_lists(obj):
81
+ if isinstance(obj, dict):
82
+ return {k: _merge_lists(v) for k, v in obj.items()}
83
+ elif isinstance(obj, list):
84
+ # Nếu list chỉ chứa chuỗi / số, gộp lại
85
+ if all(isinstance(i, (str, int, float)) for i in obj):
86
+ return self.join_sep.join(map(str, obj))
87
+ # Nếu list chứa dict hoặc list lồng, đệ quy
88
+ return [_merge_lists(v) for v in obj]
89
+ else:
90
+ return obj
91
+
92
+ data = _merge_lists(data)
93
+
94
+ # Sau đó gọi MyUtils.flatten_json như cũ
95
+ return MyUtils.flatten_json(
96
+ data,
97
+ prefix="",
98
+ flatten_mode=self.flatten_mode,
99
+ join_sep=self.join_sep
100
+ )
101
+
102
+ # ---------- Encode (batch) với fallback OOM CPU ----------
103
+ def _encode_texts(self, texts: List[str]) -> torch.Tensor:
104
+ try:
105
+ embs = self.indexer.encode(
106
+ sentences=texts,
107
+ batch_size=self.batch_size,
108
+ convert_to_tensor=True,
109
+ device=self.device,
110
+ show_progress_bar=self.show_progress,
111
+ )
112
+ return embs
113
+ except RuntimeError as e:
114
+ if "CUDA out of memory" in str(e):
115
+ print("⚠️ CUDA OOM → fallback CPU.")
116
+ try:
117
+ self.indexer.to("cpu")
118
+ except Exception:
119
+ pass
120
+ embs = self.indexer.encode(
121
+ sentences=texts,
122
+ batch_size=self.batch_size,
123
+ convert_to_tensor=True,
124
+ device="cpu",
125
+ show_progress_bar=self.show_progress,
126
+ )
127
+ return embs
128
+ raise
129
+
130
+ # ---------- Build FAISS ----------
131
+ @staticmethod
132
+ def _l2_normalize(mat: np.ndarray) -> np.ndarray:
133
+ norms = np.linalg.norm(mat, axis=1, keepdims=True)
134
+ norms[norms == 0.0] = 1.0
135
+ return mat / norms
136
+
137
+ def _create_faiss_index(self, matrix: np.ndarray) -> faiss.Index:
138
+ dim = int(matrix.shape[1])
139
+ index = faiss.IndexFlatIP(dim)
140
+ index.add(matrix.astype("float32"))
141
+ return index
142
+
143
+
144
+ # ================================================================
145
+ # Hàm lọc trùng nhưng vẫn gom nhóm chunk tương ứng
146
+ # ================================================================
147
+ def deduplicates_with_mask(
148
+ self,
149
+ pairs: List[Tuple[str, str]],
150
+ chunk_map: List[int]
151
+ ) -> Tuple[List[Tuple[str, str]], List[List[int]]]:
152
+
153
+ assert len(pairs) == len(chunk_map), "pairs và chunk_map phải đồng dài"
154
+
155
+ seen_per_key: Dict[str, Dict[str, int]] = {}
156
+ # base_key -> text_norm -> index trong filtered_pairs
157
+
158
+ filtered_pairs: List[Tuple[str, str]] = []
159
+ chunk_groups: List[List[int]] = [] # song song với filtered_pairs
160
+
161
+ for (key, text), c in zip(pairs, chunk_map):
162
+ text_norm = text.strip()
163
+ if not text_norm:
164
+ continue
165
+
166
+ base_key = re.sub(r"\[\d+\]", "", key)
167
+ if base_key not in seen_per_key:
168
+ seen_per_key[base_key] = {}
169
+
170
+ # Nếu text đã xuất hiện → thêm chunk vào nhóm cũ
171
+ if text_norm in seen_per_key[base_key]:
172
+ idx = seen_per_key[base_key][text_norm]
173
+ if c not in chunk_groups[idx]:
174
+ chunk_groups[idx].append(c)
175
+ continue
176
+
177
+ # Nếu chưa có → tạo mới
178
+ seen_per_key[base_key][text_norm] = len(filtered_pairs)
179
+ filtered_pairs.append((key, text_norm))
180
+ chunk_groups.append([c])
181
+
182
+ return filtered_pairs, chunk_groups
183
+
184
+ # ================================================================
185
+ # Ghi ChunkMapping
186
+ # ================================================================
187
+ def write_chunk_mapping(self, MapChunkPath: str, SegmentPath: str, chunk_groups: List[List[int]]) -> None:
188
+ # Ghi chunk mapping dạng gọn: mỗi index một dòng
189
+ with open(MapChunkPath, "w", encoding="utf-8") as f:
190
+ f.write('{\n')
191
+ f.write(' "index_to_chunk": {\n')
192
+
193
+ items = list(enumerate(chunk_groups))
194
+ for i, (idx, group) in enumerate(items):
195
+ group_str = "[" + ", ".join(map(str, group)) + "]"
196
+ comma = "," if i < len(items) - 1 else ""
197
+ f.write(f' "{idx}": {group_str}{comma}\n')
198
+
199
+ f.write(' },\n')
200
+ f.write(' "meta": {\n')
201
+ f.write(f' "count": {len(chunk_groups)},\n')
202
+ f.write(f' "source": "{os.path.basename(SegmentPath)}"\n')
203
+ f.write(' }\n')
204
+ f.write('}\n')
205
+
206
+ # ================================================================
207
+ # Hàm build_from_json
208
+ # ================================================================
209
+ def build_from_json(
210
+ self,
211
+ SegmentPath: str,
212
+ SchemaDict: Optional[str],
213
+ FaissPath: str,
214
+ MapDataPath: str,
215
+ MappingPath: str,
216
+ MapChunkPath: Optional[str] = None,
217
+ ) -> None:
218
+ assert os.path.exists(SegmentPath), f"Không thấy file JSON: {SegmentPath}"
219
+
220
+ os.makedirs(os.path.dirname(FaissPath), exist_ok=True)
221
+ os.makedirs(os.path.dirname(MapDataPath), exist_ok=True)
222
+ os.makedirs(os.path.dirname(MappingPath), exist_ok=True)
223
+ if MapChunkPath:
224
+ os.makedirs(os.path.dirname(MapChunkPath), exist_ok=True)
225
+
226
+ schema = SchemaDict
227
+
228
+ # 1️⃣ Read JSON
229
+ data_obj = MyUtils.read_json(SegmentPath)
230
+ data_list = data_obj if isinstance(data_obj, list) else [data_obj]
231
+
232
+ # 2️⃣ Flatten + lưu chunk_id
233
+ pair_list: List[Tuple[str, str]] = []
234
+ chunk_map: List[int] = []
235
+ for chunk_id, item in enumerate(data_list, start=1):
236
+ processed = self._preprocess_data(item)
237
+ flat = self._flatten_json(processed)
238
+ for k, v in flat.items():
239
+ if not self._eligible_by_schema(k, schema):
240
+ continue
241
+ if isinstance(v, str) and v.strip():
242
+ pair_list.append((k, v.strip()))
243
+ chunk_map.append(chunk_id)
244
+
245
+ if not pair_list:
246
+ raise ValueError("Không tìm thấy nội dung văn bản hợp lệ để encode.")
247
+
248
+ # 3️⃣ Loại trùng nhưng gom nhóm chunk
249
+ pair_list, chunk_groups = self.deduplicates_with_mask(pair_list, chunk_map)
250
+
251
+ # 4️⃣ Encode
252
+ keys = [k for k, _ in pair_list]
253
+ texts = [t for _, t in pair_list]
254
+ embs_t = self._encode_texts(texts)
255
+ embs = embs_t.detach().cpu().numpy()
256
+ if self.normalize:
257
+ embs = self._l2_normalize(embs)
258
+
259
+ # 5️⃣ FAISS
260
+ index = self._create_faiss_index(embs)
261
+ faiss.write_index(index, FaissPath)
262
+ logging.info(f"✅ Đã xây FAISS: {FaissPath}")
263
+
264
+ # 6️⃣ Mapping + MapData
265
+
266
+ index_to_key = {str(i): k for i, k in enumerate(keys)}
267
+ Mapping = {
268
+ "meta": {
269
+ "count": len(keys),
270
+ "dim": int(embs.shape[1]),
271
+ "metric": "ip",
272
+ "normalized": bool(self.normalize),
273
+ },
274
+
275
+ "index_to_key": index_to_key,
276
+ }
277
+ MapData = {
278
+ "items": [{"index": i, "key": k, "text": t} for i, (k, t) in enumerate(pair_list)],
279
+ "meta": {
280
+ "count": len(keys),
281
+ "flatten_mode": self.flatten_mode,
282
+ "schema_used": schema is not None,
283
+ "list_policy": self.list_policy
284
+ }
285
+ }
286
+
287
+ self.write_chunk_mapping(MapChunkPath, SegmentPath, chunk_groups)
288
+ return Mapping, MapData
Libraries/Faiss_Searching.py ADDED
@@ -0,0 +1,147 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import faiss
2
+ import numpy as np
3
+
4
+ from typing import Dict, List, Any, Optional
5
+ from sentence_transformers import SentenceTransformer, CrossEncoder
6
+
7
+
8
+ class SemanticSearchEngine:
9
+
10
+ def __init__(
11
+ self,
12
+ indexer: SentenceTransformer,
13
+ reranker: Optional[CrossEncoder] = None,
14
+ device: str = "cuda",
15
+ normalize: bool = True,
16
+ top_k: int = 20,
17
+ rerank_k: int = 10,
18
+ rerank_batch_size: int = 16,
19
+ ):
20
+ self.device = device
21
+ self.normalize = normalize
22
+ self.top_k = int(top_k)
23
+ self.rerank_k = int(rerank_k)
24
+ self.rerank_batch_size = int(rerank_batch_size)
25
+
26
+ # ✅ Nhận trực tiếp model đã load
27
+ if not isinstance(indexer, SentenceTransformer):
28
+ raise TypeError("indexer phải là SentenceTransformer đã load sẵn.")
29
+ self._indexer = indexer
30
+
31
+ # Reranker là tùy chọn
32
+ if reranker and not isinstance(reranker, CrossEncoder):
33
+ raise TypeError("reranker phải là CrossEncoder hoặc None.")
34
+ self.reranker = reranker
35
+
36
+ # ---------------------------
37
+ # Tiện ích nội bộ
38
+ # ---------------------------
39
+ @staticmethod
40
+ def _l2_normalize(x: np.ndarray, axis: int = 1, eps: float = 1e-12) -> np.ndarray:
41
+ denom = np.linalg.norm(x, axis=axis, keepdims=True)
42
+ denom = np.maximum(denom, eps)
43
+ return x / denom
44
+
45
+ @staticmethod
46
+ def _build_idx_maps(Mapping: Dict[str, Any], MapData: Dict[str, Any]):
47
+ """Tạo ánh xạ index→text và index→key"""
48
+ items = MapData.get("items", [])
49
+ idx2text = {int(item["index"]): item.get("text", None) for item in items}
50
+ raw_i2k = Mapping.get("index_to_key", {})
51
+ idx2key = {int(i): k for i, k in raw_i2k.items()}
52
+ return idx2text, idx2key
53
+
54
+ # ---------------------------
55
+ # 1️⃣ SEARCH: FAISS vector search
56
+ # ---------------------------
57
+ def search(
58
+ self,
59
+ query: str,
60
+ faissIndex: "faiss.Index", # type: ignore
61
+ Mapping: Dict[str, Any],
62
+ MapData: Dict[str, Any],
63
+ MapChunk: Optional[Dict[str, Any]] = None,
64
+ top_k: Optional[int] = None,
65
+ query_embedding: Optional[np.ndarray] = None,
66
+ ) -> List[Dict[str, Any]]:
67
+ """
68
+ Trả về:
69
+ [{"index":..., "key":..., "text":..., "faiss_score":...}, ...]
70
+ """
71
+ k = int(top_k or self.top_k)
72
+
73
+ # 1. Encode truy vấn (hoặc dùng sẵn embedding)
74
+ if query_embedding is None:
75
+ q = self._indexer.encode(
76
+ [query], convert_to_tensor=True, device=str(self.device)
77
+ )
78
+ q = q.detach().cpu().numpy().astype("float32")
79
+ else:
80
+ q = np.asarray(query_embedding, dtype="float32")
81
+ if q.ndim == 1:
82
+ q = q[None, :]
83
+
84
+ # 2. Normalize nếu dùng cosine
85
+ if self.normalize:
86
+ q = self._l2_normalize(q)
87
+
88
+ # 3. Search FAISS
89
+ scores, ids = faissIndex.search(q, k)
90
+ idx2text, idx2key = self._build_idx_maps(Mapping, MapData)
91
+
92
+ # 4. Mapping kết quả
93
+ chunk_map = MapChunk.get("index_to_chunk", {}) if MapChunk else {}
94
+ results = []
95
+ for score, idx in zip(scores[0].tolist(), ids[0].tolist()):
96
+ chunk_ids = chunk_map.get(str(idx), [])
97
+ results.append({
98
+ "index": int(idx),
99
+ "key": idx2key.get(int(idx)),
100
+ "text": idx2text.get(int(idx)),
101
+ "faiss_score": float(score),
102
+ "chunk_ids": chunk_ids,
103
+ })
104
+ return results
105
+
106
+ # ---------------------------
107
+ # 2️⃣ RERANK: CrossEncoder rerank
108
+ # ---------------------------
109
+ def rerank(
110
+ self,
111
+ query: str,
112
+ results: List[Dict[str, Any]],
113
+ top_k: Optional[int] = None,
114
+ show_progress: bool = False,
115
+ ) -> List[Dict[str, Any]]:
116
+ """
117
+ Xếp hạng lại kết quả bằng CrossEncoder (nếu có).
118
+ Trả về danh sách top_k kết quả đã rerank.
119
+ """
120
+ if not results:
121
+ return []
122
+ if self.reranker is None:
123
+ raise ValueError("⚠️ Không có reranker được cung cấp khi khởi tạo.")
124
+
125
+ k = int(top_k or self.rerank_k)
126
+
127
+ pairs = []
128
+ valid_indices = []
129
+ for i, r in enumerate(results):
130
+ text = r.get("text")
131
+ if isinstance(text, str) and text.strip():
132
+ pairs.append([query, text])
133
+ valid_indices.append(i)
134
+
135
+ if not pairs:
136
+ return []
137
+
138
+ scores = self.reranker.predict(
139
+ pairs, batch_size=self.rerank_batch_size, show_progress_bar=show_progress
140
+ )
141
+
142
+ for i, s in zip(valid_indices, scores):
143
+ results[i]["rerank_score"] = float(s)
144
+
145
+ reranked = [r for r in results if "rerank_score" in r]
146
+ reranked.sort(key=lambda x: x["rerank_score"], reverse=True)
147
+ return reranked[:k]
Libraries/Json_ChunkMaster.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import OrderedDict
2
+ from copy import deepcopy
3
+
4
+ class ChunkBuilder:
5
+ def readInput(self, RawLvlsDict=None, RawDataDict=None):
6
+ # Đọc dữ liệu
7
+
8
+ self.struct_spec = RawLvlsDict[0]
9
+ self.paragraphs = sorted(
10
+ RawDataDict.get("paragraphs", []),
11
+ key=lambda x: x.get("Paragraph", 0)
12
+ )
13
+
14
+ # Chuẩn bị cấu trúc
15
+ self.ordered_fields = list(self.struct_spec.keys())
16
+ self.last_field = self.ordered_fields[-1]
17
+ self.level_fields = self.ordered_fields[:-1]
18
+
19
+ # Tập marker cho từng field
20
+ self.marker_dict = {}
21
+ for fld in self.ordered_fields:
22
+ vals = self.struct_spec.get(fld, [])
23
+ self.marker_dict[fld] = set(vals) if isinstance(vals, list) else set()
24
+
25
+ # Biến tạm
26
+ self.StructDict = []
27
+ self.index_counter = 1
28
+
29
+ # ===== Các hàm tiện ích =====
30
+ def _new_temp(self):
31
+ return {fld: "" for fld in self.level_fields} | {self.last_field: []}
32
+
33
+ def _temp_has_data(self, temp):
34
+ return any(temp[f].strip() for f in self.level_fields) or bool(temp[self.last_field])
35
+
36
+ def _reset_deeper(self, temp, touched_field):
37
+ idx = self.level_fields.index(touched_field)
38
+ for f in self.level_fields[idx+1:]:
39
+ temp[f] = ""
40
+ temp[self.last_field] = []
41
+
42
+ def _has_data_from_level(self, temp, fld):
43
+ """Kiểm tra từ level fld trở xuống có dữ liệu không"""
44
+ if fld not in self.level_fields:
45
+ return False
46
+ idx = self.level_fields.index(fld)
47
+ for f in self.level_fields[idx:]:
48
+ if temp[f].strip():
49
+ return True
50
+ if temp[self.last_field]:
51
+ return True
52
+ return False
53
+
54
+ def _with_index(self, temp, idx):
55
+ """Tạo OrderedDict với Index đứng đầu"""
56
+ od = OrderedDict()
57
+ od["Index"] = idx
58
+ for f in self.level_fields:
59
+ od[f] = temp[f]
60
+ od[self.last_field] = temp[self.last_field]
61
+ return od
62
+
63
+ # ===== Hàm chính =====
64
+ def build(self, RawLvlsDict=None, RawDataDict=None):
65
+ self.readInput(RawLvlsDict, RawDataDict)
66
+ temp = self._new_temp()
67
+ for p in self.paragraphs:
68
+ text = p.get("Text") or ""
69
+ marker = p.get("MarkerType", None) or "none"
70
+
71
+ matched_field = None
72
+ for fld in self.level_fields:
73
+ if marker in self.marker_dict.get(fld, set()):
74
+ matched_field = fld
75
+ break
76
+
77
+ if matched_field is not None:
78
+ if self._has_data_from_level(temp, matched_field):
79
+ self.StructDict.append(self._with_index(deepcopy(temp), self.index_counter))
80
+ self.index_counter += 1
81
+
82
+ temp[matched_field] = text
83
+ self._reset_deeper(temp, matched_field)
84
+ else:
85
+ temp[self.last_field].append(text)
86
+
87
+ if self._temp_has_data(temp):
88
+ self.StructDict.append(self._with_index(deepcopy(temp), self.index_counter))
89
+ self.index_counter += 1
90
+
91
+ return self.StructDict
Libraries/Json_ChunkUnder.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import numpy as np
3
+
4
+ from underthesea import sent_tokenize
5
+
6
+ class ChunkUndertheseaBuilder:
7
+ """
8
+ Bộ tách văn bản tiếng Việt thông minh:
9
+ 1️⃣ Lọc trước (Extractive): chỉ giữ các câu có ý chính
10
+ 2️⃣ Gộp sau (Semantic): nhóm các câu trọng tâm theo ngữ nghĩa
11
+ """
12
+
13
+ def __init__(self,
14
+ embedder,
15
+ device: str = "cpu",
16
+ min_words: int = 256,
17
+ max_words: int = 768,
18
+ sim_threshold: float = 0.7,
19
+ key_sent_ratio: float = 0.4):
20
+ if embedder is None:
21
+ raise ValueError("❌ Cần truyền mô hình embedder đã load sẵn.")
22
+ self.embedder = embedder
23
+ self.device = device
24
+ self.min_words = min_words
25
+ self.max_words = max_words
26
+ self.sim_threshold = sim_threshold
27
+ self.key_sent_ratio = key_sent_ratio
28
+
29
+ # ============================================================
30
+ # 1️⃣ Tách câu
31
+ # ============================================================
32
+ def _split_sentences(self, text: str):
33
+ """Tách câu tiếng Việt (fallback nếu underthesea lỗi)."""
34
+ text = re.sub(r"[\x00-\x1f]+", " ", text)
35
+ try:
36
+ sents = sent_tokenize(text)
37
+ except Exception:
38
+ sents = re.split(r"(?<=[.!?])\s+", text)
39
+ return [s.strip() for s in sents if len(s.strip()) > 2]
40
+
41
+ # ============================================================
42
+ # 2️⃣ Encode an toàn (GPU/CPU fallback)
43
+ # ============================================================
44
+ def _encode(self, sentences):
45
+ try:
46
+ return self.embedder.encode(
47
+ sentences,
48
+ convert_to_numpy=True,
49
+ show_progress_bar=False,
50
+ device=str(self.device)
51
+ )
52
+ except TypeError:
53
+ return self.embedder.encode(sentences, convert_to_numpy=True, show_progress_bar=False)
54
+ except RuntimeError as e:
55
+ if "CUDA" in str(e):
56
+ print("⚠️ GPU OOM, fallback sang CPU.")
57
+ return self.embedder.encode(
58
+ sentences, convert_to_numpy=True, show_progress_bar=False, device="cpu"
59
+ )
60
+ raise e
61
+
62
+ # ============================================================
63
+ # 3️⃣ Lọc ý chính trước (EXTRACTIVE)
64
+ # ============================================================
65
+ def _extractive_filter(self, sentences):
66
+ """Chọn ra top-k câu đại diện nội dung nhất."""
67
+ if len(sentences) <= 3:
68
+ return sentences
69
+
70
+ embeddings = self._encode(sentences)
71
+ mean_vec = np.mean(embeddings, axis=0)
72
+ sims = np.dot(embeddings, mean_vec) / (
73
+ np.linalg.norm(embeddings, axis=1) * np.linalg.norm(mean_vec)
74
+ )
75
+
76
+ # Chọn top-k câu có similarity cao nhất
77
+ k = max(1, int(len(sentences) * self.key_sent_ratio))
78
+ idx = np.argsort(-sims)[:k]
79
+ idx.sort() # giữ thứ tự gốc
80
+ selected = [sentences[i] for i in idx]
81
+ return selected
82
+
83
+ # ============================================================
84
+ # 4️⃣ Gộp các câu trọng tâm theo ngữ nghĩa
85
+ # ============================================================
86
+ def _semantic_group(self, sentences):
87
+ """Gộp các câu đã lọc theo mức tương đồng ngữ nghĩa."""
88
+ if not sentences:
89
+ return []
90
+
91
+ embeddings = self._encode(sentences)
92
+ embeddings = embeddings / np.linalg.norm(embeddings, axis=1, keepdims=True)
93
+
94
+ chunks, cur_chunk, cur_len = [], [], 0
95
+ for i, sent in enumerate(sentences):
96
+ wc = len(sent.split())
97
+ if not cur_chunk:
98
+ cur_chunk.append(sent)
99
+ cur_len = wc
100
+ continue
101
+
102
+ sim = np.dot(embeddings[i - 1], embeddings[i])
103
+ too_long = cur_len + wc > self.max_words
104
+ too_short = cur_len < self.min_words
105
+ topic_changed = sim < self.sim_threshold
106
+
107
+ if too_long or (not too_short and topic_changed):
108
+ chunks.append(" ".join(cur_chunk))
109
+ cur_chunk = [sent]
110
+ cur_len = wc
111
+ else:
112
+ cur_chunk.append(sent)
113
+ cur_len += wc
114
+
115
+ if cur_chunk:
116
+ chunks.append(" ".join(cur_chunk))
117
+ return chunks
118
+
119
+ # ============================================================
120
+ # 5️⃣ Hàm chính build()
121
+ # ============================================================
122
+ def build(self, full_text: str):
123
+ """
124
+ Trả về list chứa {Index, Content} cho từng chunk.
125
+ Quy trình:
126
+ - Lọc câu trọng tâm trước
127
+ - Gộp các câu đã lọc theo ngữ nghĩa
128
+ """
129
+ all_sentences = self._split_sentences(full_text)
130
+ print(f"📄 Tổng số câu: {len(all_sentences)}")
131
+
132
+ # --- Bước 1: lọc ý chính ---
133
+ filtered = self._extractive_filter(all_sentences)
134
+ print(f"✨ Giữ lại {len(filtered)} câu (~{len(filtered)/len(all_sentences):.0%}) sau extractive filter")
135
+
136
+ # --- Bước 2: gộp thành các đoạn ngữ nghĩa ---
137
+ chunks = self._semantic_group(filtered)
138
+ results = [{"Index": i, "Content": chunk} for i, chunk in enumerate(chunks, start=1)]
139
+
140
+ print(f"🔹 Tạo {len(results)} chunk ngữ nghĩa từ {len(filtered)} câu trọng tâm.")
141
+ return results
Libraries/Json_GetStructures.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ from typing import Dict, List, Any
4
+ from collections import Counter, defaultdict
5
+
6
+ class StructureAnalyzer:
7
+ def __init__(self, verbose: bool = False):
8
+ self.verbose = verbose
9
+
10
+ # ---------------- B1 ---------------- #
11
+ def extract_markers(self, RawDataDict) -> List[str]:
12
+ bullet_pattern = re.compile(r"^\s*[-•●♦▪‣–—]+\s*$")
13
+
14
+ paragraphs = RawDataDict.get("paragraphs", [])
15
+ common_markers = set(RawDataDict.get("general", {}).get("commonMarkers", []))
16
+
17
+ raw_markers: List[Any] = []
18
+ for p in paragraphs:
19
+ mt = p.get("MarkerText")
20
+ mtype = p.get("MarkerType")
21
+
22
+ # Bỏ bullet
23
+ if bullet_pattern.match(mt or "") or bullet_pattern.match(mtype or ""):
24
+ continue
25
+
26
+ # Giữ nếu thuộc common hoặc là None
27
+ if mtype in common_markers or mtype is None:
28
+ raw_markers.append(mtype)
29
+
30
+ # Loại bỏ trùng kề nhau và chuẩn hóa None -> "none"
31
+ cleaned: List[str] = []
32
+ prev = object()
33
+ for m in raw_markers:
34
+ val = str(m) if m is not None else "none"
35
+ if val != prev:
36
+ cleaned.append(val)
37
+ prev = val
38
+
39
+ return cleaned
40
+
41
+ # ---------------- B2 ---------------- #
42
+ def build_structures(self, markers: List[str]) -> List[Dict[str, Any]]:
43
+ unique_markers = list(dict.fromkeys(markers))
44
+ counter1 = Counter(markers)
45
+ results = [{"Depth": 1, "Structure": [m], "Count": counter1[m]} for m in unique_markers]
46
+
47
+ max_depth = len(unique_markers)
48
+ prev_structures = set((m,) for m in unique_markers)
49
+
50
+ for i in range(2, max_depth + 1):
51
+ counter = Counter()
52
+ for j in range(len(markers) - i + 1):
53
+ seq_raw = tuple(markers[j:j+i])
54
+ prefix = seq_raw[:-1]
55
+
56
+ # Điều kiện 1: phải có cha
57
+ if prefix not in prev_structures:
58
+ continue
59
+ # Điều kiện 2: không trùng MarkerType trong cùng cấu trúc
60
+ if len(seq_raw) != len(set(seq_raw)):
61
+ continue
62
+ # Điều kiện 3: chỉ chấp nhận nếu "none" không có, hoặc nằm ở cuối
63
+ if "none" in seq_raw and seq_raw[-1] != "none":
64
+ continue
65
+
66
+ counter[seq_raw] += 1
67
+
68
+ if not counter:
69
+ break
70
+
71
+ min_count = min(counter.values())
72
+ max_count = max(counter.values())
73
+ filtered = {s: f for s, f in counter.items() if not (f == min_count and f != max_count)}
74
+ sorted_structs = sorted(filtered.items(), key=lambda x: x[1], reverse=True)
75
+
76
+ depth_lines = [{"Depth": i, "Structure": list(s), "Count": f} for s, f in sorted_structs]
77
+ results.extend(depth_lines)
78
+
79
+ prev_structures = set(s for s, _ in sorted_structs)
80
+
81
+ return results
82
+
83
+ # ---------------- B3 ---------------- #
84
+ def deduplicate(self, structures: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
85
+ grouped = defaultdict(list)
86
+ for item in structures:
87
+ depth = item["Depth"]
88
+ key = (depth, tuple(sorted(item["Structure"])))
89
+ grouped[key].append(item)
90
+
91
+ filtered = []
92
+ for _, group in grouped.items():
93
+ best = max(group, key=lambda x: x["Count"])
94
+ filtered.append(best)
95
+
96
+ filtered.sort(key=lambda x: (x["Depth"], -x["Count"], x["Structure"]))
97
+
98
+ return filtered
99
+
100
+ # ---------------- B4 ---------------- #
101
+ def select_top(self, dedup: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
102
+ if not dedup:
103
+ return []
104
+
105
+ max_depth = max(item["Depth"] for item in dedup)
106
+ at_max = [x for x in dedup if x["Depth"] == max_depth]
107
+ max_count = max(x["Count"] for x in at_max)
108
+ top = [x for x in at_max if x["Count"] == max_count]
109
+
110
+ result = []
111
+ for t in top:
112
+ level_dict = {}
113
+ for i, marker in enumerate(t["Structure"]):
114
+ if i == len(t["Structure"]) - 1:
115
+ # phần tử cuối cùng
116
+ level_dict["Contents"] = marker
117
+ else:
118
+ level_dict[f"Level {i+1}"] = marker
119
+ result.append(level_dict)
120
+
121
+ return result
122
+
123
+ def level_rank(level: str) -> int:
124
+ """Quy đổi level thành số để so sánh"""
125
+ if level == "Contents":
126
+ return 9999 # Contents coi như cao nhất
127
+ if level.startswith("Level "):
128
+ try:
129
+ return int(level.split()[1])
130
+ except Exception:
131
+ return 0
132
+ return 0
133
+
134
+ def extend_top(self, top: List[Dict[str, Any]], dedup: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
135
+ """
136
+ Mở rộng top bằng cách thêm tail từ dedup:
137
+ - Nếu Contents: chỉ giữ tail == ['none']
138
+ - Các level khác: thêm tail vào các level tiếp theo
139
+ - Nếu level đã có -> gộp vào list
140
+ - Luôn chuẩn hóa: mọi giá trị là list
141
+ """
142
+ if not top:
143
+ return []
144
+
145
+ RawLvlsDict = dict(top[0]) # copy để tránh sửa trực tiếp
146
+ all_markers = set(v for val in RawLvlsDict.values() for v in (val if isinstance(val, list) else [val]))
147
+ seen_tails = set()
148
+
149
+ # snapshot tránh lỗi "dict changed size"
150
+ snapshot_items = list(RawLvlsDict.items())
151
+
152
+ for level, marker_values in reversed(snapshot_items):
153
+ if level == "Level 1":
154
+ continue
155
+
156
+ # chuẩn hóa về list để dễ xử lý
157
+ markers = marker_values if isinstance(marker_values, list) else [marker_values]
158
+
159
+ for marker in markers:
160
+ for d in dedup:
161
+ struct = d["Structure"]
162
+ if d["Depth"] < 2:
163
+ continue
164
+
165
+ if struct and struct[0] == marker:
166
+ if not (set(struct) & (all_markers - {marker})):
167
+ tail = tuple(struct[1:])
168
+
169
+ # xử lý riêng cho Contents
170
+ if level == "Contents" and tail != ("none",):
171
+ continue
172
+ if tail in seen_tails:
173
+ continue
174
+ seen_tails.add(tail)
175
+
176
+ # xác định base level
177
+ if level.startswith("Level "):
178
+ base_level_num = int(level.split()[1])
179
+ elif level == "Contents":
180
+ base_level_num = max(
181
+ int(l.split()[1]) for l in RawLvlsDict if l.startswith("Level ")
182
+ )
183
+ else:
184
+ base_level_num = 0
185
+
186
+ # thêm từng phần tử tail vào level tiếp theo
187
+ for i, t in enumerate(tail, start=1):
188
+ next_level = f"Level {base_level_num+i}"
189
+ if next_level not in RawLvlsDict:
190
+ RawLvlsDict[next_level] = []
191
+ if not isinstance(RawLvlsDict[next_level], list):
192
+ RawLvlsDict[next_level] = [RawLvlsDict[next_level]]
193
+ if t not in RawLvlsDict[next_level]:
194
+ RawLvlsDict[next_level].append(t)
195
+
196
+ # đổi level cao nhất thành Contents (và gộp nếu đã có)
197
+ level_nums = [int(l.split()[1]) for l in RawLvlsDict if l.startswith("Level ")]
198
+ if level_nums:
199
+ max_level = f"Level {max(level_nums)}"
200
+ new_contents = RawLvlsDict.pop(max_level)
201
+
202
+ if "Contents" not in RawLvlsDict:
203
+ RawLvlsDict["Contents"] = []
204
+ if not isinstance(RawLvlsDict["Contents"], list):
205
+ RawLvlsDict["Contents"] = [RawLvlsDict["Contents"]]
206
+
207
+ for v in (new_contents if isinstance(new_contents, list) else [new_contents]):
208
+ if v not in RawLvlsDict["Contents"]:
209
+ RawLvlsDict["Contents"].append(v)
210
+
211
+ # --- 🔹 Đổi nhãn ngay trước khi trả kết quả --- #
212
+ keys = list(RawLvlsDict.keys())
213
+ if len(keys) > 1 and keys[-2].startswith("Level "):
214
+ RawLvlsDict["Article"] = RawLvlsDict.pop(keys[-2])
215
+ if "Contents" in RawLvlsDict:
216
+ RawLvlsDict["Content"] = RawLvlsDict.pop("Contents")
217
+
218
+ # chuẩn hóa tất cả value thành list
219
+ for k, v in RawLvlsDict.items():
220
+ if not isinstance(v, list):
221
+ RawLvlsDict[k] = [v]
222
+
223
+ return [RawLvlsDict]
Libraries/Json_SchemaExt.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, List, Any
2
+
3
+ class JSONSchemaExtractor:
4
+
5
+ def __init__(self, list_policy: str = "first", verbose: bool = True) -> None:
6
+ """
7
+ :param list_policy: "first" | "union"
8
+ - "first": nếu gặp list các object, lấy schema theo PHẦN TỬ ĐẦU (như bản gốc).
9
+ - "union": duyệt mọi phần tử, hợp nhất các field/type.
10
+ """
11
+ assert list_policy in ("first", "union"), "list_policy must be 'first' or 'union'"
12
+ self.list_policy = list_policy
13
+ self.verbose = verbose
14
+
15
+ self._processed_fields: set[str] = set()
16
+ self._full_schema: Dict[str, str] = {}
17
+
18
+ # =====================================
19
+ # 1) Chuẩn hóa kiểu dữ liệu
20
+ # =====================================
21
+ @staticmethod
22
+ def get_standard_type(value: Any) -> str:
23
+
24
+ if isinstance(value, bool):
25
+ return "boolean"
26
+ elif isinstance(value, int):
27
+ return "number"
28
+ elif isinstance(value, float):
29
+ return "number"
30
+ elif isinstance(value, str):
31
+ return "string"
32
+ elif isinstance(value, list):
33
+ return "array"
34
+ elif isinstance(value, dict):
35
+ return "object"
36
+ elif value is None:
37
+ return "null"
38
+ return "unknown"
39
+
40
+ # =====================================
41
+ # 2) Hợp nhất kiểu (null / mixed)
42
+ # =====================================
43
+ def _merge_type(self, key: str, new_type: str, item_index: int) -> None:
44
+ """
45
+ Cập nhật self._full_schema[key] theo quy tắc:
46
+ - Nếu chưa có: đặt = new_type và log "New: ..."
47
+ - Nếu khác:
48
+ + Nếu new_type == "null": giữ kiểu cũ.
49
+ + Nếu kiểu cũ == "null": cập nhật = new_type.
50
+ + Ngược lại: nếu khác nhau và chưa "mixed" => set "mixed" và cảnh báo.
51
+ """
52
+ if key not in self._full_schema:
53
+ self._full_schema[key] = new_type
54
+ self._processed_fields.add(key)
55
+ return
56
+
57
+ old_type = self._full_schema[key]
58
+ if old_type == new_type:
59
+ return
60
+
61
+ if new_type == "null":
62
+ return
63
+
64
+ if old_type == "null":
65
+ self._full_schema[key] = new_type
66
+ return
67
+
68
+ if old_type != "mixed":
69
+ self._full_schema[key] = "mixed"
70
+
71
+ # =====================================
72
+ # 3) Đệ quy trích xuất schema
73
+ # =====================================
74
+ def _extract_schema_from_obj(self, data: Dict[str, Any], prefix: str, item_index: int) -> None:
75
+ """
76
+ Duyệt dict hiện tại, cập nhật _full_schema với kiểu tại key (phẳng),
77
+ và nếu là object/array lồng thì đệ quy theo quy tắc gốc.
78
+ """
79
+ for key, value in data.items():
80
+ new_prefix = f"{prefix}{key}" if prefix else key
81
+
82
+ vtype = self.get_standard_type(value)
83
+ self._merge_type(new_prefix, vtype, item_index)
84
+
85
+ if isinstance(value, dict):
86
+ self._extract_schema_from_obj(value, f"{new_prefix}.", item_index)
87
+
88
+ elif isinstance(value, list) and value:
89
+ first = value[0]
90
+ if isinstance(first, dict):
91
+ if self.list_policy == "first":
92
+ self._extract_schema_from_obj(first, f"{new_prefix}.", item_index)
93
+ else: # union
94
+ for elem in value:
95
+ if isinstance(elem, dict):
96
+ self._extract_schema_from_obj(elem, f"{new_prefix}.", item_index)
97
+ elif isinstance(first, list):
98
+ if self.list_policy == "first":
99
+ self._extract_schema_from_list(first, f"{new_prefix}.", item_index)
100
+ else:
101
+ for elem in value:
102
+ if isinstance(elem, list):
103
+ self._extract_schema_from_list(elem, f"{new_prefix}.", item_index)
104
+
105
+ def _extract_schema_from_list(self, data_list: List[Any], prefix: str, item_index: int) -> None:
106
+ """
107
+ Hỗ trợ cho trường hợp list lồng list (ít gặp). Duyệt tương tự _extract_schema_from_obj.
108
+ """
109
+ if not data_list:
110
+ return
111
+
112
+ first = data_list[0]
113
+ if isinstance(first, dict):
114
+ if self.list_policy == "first":
115
+ self._extract_schema_from_obj(first, prefix, item_index)
116
+ else:
117
+ for elem in data_list:
118
+ if isinstance(elem, dict):
119
+ self._extract_schema_from_obj(elem, prefix, item_index)
120
+ elif isinstance(first, list):
121
+ if self.list_policy == "first":
122
+ self._extract_schema_from_list(first, prefix, item_index)
123
+ else:
124
+ for elem in data_list:
125
+ if isinstance(elem, list):
126
+ self._extract_schema_from_list(elem, prefix, item_index)
127
+
128
+ # =====================================
129
+ # 4) API chính (data/file)
130
+ # =====================================
131
+ def create_schema_from_data(self, data: Any) -> Dict[str, str]:
132
+ """
133
+ Tạo schema từ biến Python (list | dict).
134
+ Giữ log giống bản gốc.
135
+ """
136
+
137
+ self._processed_fields.clear()
138
+ self._full_schema.clear()
139
+
140
+ data_list = data if isinstance(data, list) else [data]
141
+
142
+ if not data_list:
143
+ raise ValueError("JSON data is empty")
144
+
145
+ for i, item in enumerate(data_list, 1):
146
+ if not isinstance(item, dict):
147
+ continue
148
+
149
+ self._extract_schema_from_obj(item, prefix="", item_index=i)
150
+
151
+ return dict(self._full_schema)
152
+
153
+ def schemaRun(self, SegmentDict: str) -> Dict[str, str]:
154
+ SchemaDict = self.create_schema_from_data(SegmentDict)
155
+ return SchemaDict
Libraries/PDF_ExtractData.py ADDED
@@ -0,0 +1,605 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+
3
+ from typing import Dict, Any
4
+ from collections import Counter, defaultdict
5
+
6
+ from . import Common_TextProcess as TextProcess
7
+ from . import Common_PdfProcess as PdfProcess
8
+
9
+ # ===============================
10
+ # 1. Utils -> class U1_Utils
11
+ # ===============================
12
+ class U1_Utils:
13
+
14
+ # ===== Hàm tự động thu thập tên riêng =====
15
+ @staticmethod
16
+ def collect_proper_names(lines, min_count=10):
17
+ title_words = []
18
+
19
+ for line in lines:
20
+ text = line.get("Text", "")
21
+ words = re.findall(r"[A-Za-zÀ-ỹĐđ0-9]+", text)
22
+ if not words:
23
+ continue
24
+
25
+ # Bỏ qua từ đầu tiên
26
+ for w in words[1:]:
27
+ if w.istitle():
28
+ clean_w = TextProcess.normalize_word(w)
29
+ if clean_w:
30
+ title_words.append(clean_w)
31
+
32
+ counter = Counter(title_words)
33
+ proper_names = {TextProcess.normalize_word(w) for w, cnt in counter.items() if cnt >= min_count}
34
+ return proper_names
35
+
36
+ @staticmethod
37
+ def extract_marker(text, patterns):
38
+ for pattern_info in patterns["markers"]:
39
+ match = pattern_info["pattern"].match(text)
40
+ if match:
41
+ marker_text = re.sub(r'^\s+', '', match.group(0))
42
+ marker_text = re.sub(r'\s+$', ' ', marker_text)
43
+ return {"marker_text": marker_text}
44
+ return {"marker_text": None}
45
+
46
+ @staticmethod
47
+ def format_marker(marker_text, patterns):
48
+ """
49
+ Chuẩn hoá MarkerText
50
+ """
51
+ if not marker_text:
52
+ return None
53
+
54
+ formatted = marker_text
55
+ formatted = re.sub(r'\b[0-9]+\b', '123', formatted)
56
+ formatted = re.sub(r'\b[IVXLC]+\b', 'XVI', formatted)
57
+
58
+ parts = re.split(r'(\W+)', formatted)
59
+ formatted_parts = []
60
+ for part in parts:
61
+ if re.match(r'(\W+)', part):
62
+ formatted_parts.append(part)
63
+ continue
64
+ if part.lower() in patterns["keywords_set"]:
65
+ formatted_parts.append(part)
66
+ elif re.match(r'^[a-z]$', part) or re.match(r'^[a-zđêôơư]$', part):
67
+ formatted_parts.append('abc')
68
+ elif re.match(r'^[A-Z]$', part) or re.match(r'^[A-ZĐÊÔƠƯ]$', part):
69
+ formatted_parts.append('ABC')
70
+ else:
71
+ formatted_parts.append(part)
72
+ return ''.join(formatted_parts)
73
+
74
+ # ===== Hàm chuẩn hoá số La Mã =====
75
+ @staticmethod
76
+ def normalizeRomans(lines, mode="marker", replace_with="ABC"):
77
+ format_groups = defaultdict(list)
78
+ for idx, line in enumerate(lines):
79
+ fmt = line.get("MarkerType")
80
+ marker = line.get("MarkerText")
81
+ if fmt and marker:
82
+ format_groups[fmt].append((idx, marker))
83
+
84
+ # --- kiểm tra MarkerType ---
85
+ if mode == "marker":
86
+ for fmt, group in format_groups.items():
87
+ roman_markers = []
88
+ for idx, marker in group:
89
+ m = re.search(r'\b([IVXLC]+)\b', marker)
90
+ if m and TextProcess.is_roman(m.group(1)):
91
+ roman_markers.append((idx, m.group(1)))
92
+ else:
93
+ break
94
+
95
+ if roman_markers:
96
+ roman_numbers = [TextProcess.roman_to_int(rm[1]) for rm in roman_markers]
97
+ expected = list(range(min(roman_numbers), max(roman_numbers) + 1))
98
+ if sorted(roman_numbers) != expected:
99
+ for idx, _ in roman_markers:
100
+ lines[idx]["MarkerType"] = re.sub(r'\b[IVXLC]+\b', replace_with, lines[idx]["MarkerType"])
101
+
102
+ # --- Chuẩn hoá toàn bộ Text/MarkerText ---
103
+ elif mode == "text":
104
+ for line in lines:
105
+ for key in ["Text", "MarkerText", "MarkerType"]:
106
+ if line.get(key):
107
+ line[key] = re.sub(r'\b[IVXLC]+\b', replace_with, line[key])
108
+
109
+ return lines
110
+
111
+
112
+ # ===============================
113
+ # 2. Word-level functions (mới) -> class U2_Word
114
+ # ===============================
115
+ class U2_Word:
116
+ @staticmethod
117
+ def caseStyle(word_text: str) -> int:
118
+ """CaseStyle cho từ: 3000 (UPPER), 2000 (Title), 1000 (khác)"""
119
+ clean = re.sub(r'[^A-Za-zÀ-ỹà-ỹ0-9]', '', word_text)
120
+ if clean and clean.isupper():
121
+ return 3000
122
+ if clean and clean.istitle():
123
+ return 2000
124
+ return 1000
125
+
126
+ @staticmethod
127
+ def buildStyle(word_text, span):
128
+ """Style gộp = CaseStyle + FontStyle (100,10,1)"""
129
+ cs = U2_Word.caseStyle(word_text)
130
+ b, i, u = PdfProcess.fontFlags(span)
131
+ fs = (100 if b else 0) + (10 if i else 0) + (1 if u else 0)
132
+ return cs + fs
133
+
134
+ @staticmethod
135
+ def getWordStyle(line, index: int):
136
+ """Lấy Style của từ tại vị trí index."""
137
+ words = PdfProcess.extractWords(line)
138
+ if -len(words) <= index < len(words):
139
+ word, span = words[index]
140
+ return U2_Word.buildStyle(word, span)
141
+ return 0
142
+
143
+
144
+ # ===============================
145
+ # 3. Line-level functions (mới) -> class U3_Line
146
+ # ===============================
147
+ class U3_Line:
148
+ @staticmethod
149
+ def getPageGeneralSize(page):
150
+ """[height, width] của trang"""
151
+ return [round(page.rect.height, 1), round(page.rect.width, 1)]
152
+
153
+ @staticmethod
154
+ def getLineText(line):
155
+ """Text đầy đủ của line"""
156
+ return line.get("text", "")
157
+
158
+ @staticmethod
159
+ def getLineStyle(line, exceptions=None):
160
+ """
161
+ Style của line = CaseStyle (min trên từ hợp lệ) + FontStyle (AND spans).
162
+ """
163
+ words = line.get("words", [])
164
+ spans = line.get("spans", [])
165
+
166
+ # Gom exceptions
167
+ exception_texts = set()
168
+ if exceptions:
169
+ exception_texts = (
170
+ set(exceptions.get("common_words", [])) |
171
+ set(exceptions.get("proper_names", [])) |
172
+ set(exceptions.get("abbreviations", []))
173
+ )
174
+
175
+ # ===== CaseStyle =====
176
+ cs_values = []
177
+ for w, _ in words:
178
+ clean_w = TextProcess.normalize_word(w)
179
+ if not clean_w:
180
+ continue
181
+ if clean_w in exception_texts or TextProcess.is_abbreviation(clean_w):
182
+ continue
183
+ cs_values.append(U2_Word.caseStyle(clean_w))
184
+
185
+ cs_line = min(cs_values) if cs_values else 1000
186
+
187
+ # ===== FontStyle =====
188
+ if spans:
189
+ bold_all = italic_all = underline_all = True
190
+ for s in spans:
191
+ b, i, u = PdfProcess.fontFlags(s)
192
+ bold_all &= b
193
+ italic_all &= i
194
+ underline_all &= u
195
+ fs_line = (100 if bold_all else 0) + (10 if italic_all else 0) + (1 if underline_all else 0)
196
+ else:
197
+ fs_line = 0
198
+
199
+ return cs_line + fs_line
200
+
201
+
202
+ # ===============================
203
+ # 4. Compatibility wrappers -> class U4_Compat
204
+ # ===============================
205
+ class U4_Compat:
206
+ @staticmethod
207
+ def getText(line):
208
+ """Alias cũ: Text của line"""
209
+ return U3_Line.getLineText(line)
210
+
211
+ @staticmethod
212
+ def getCoords(line):
213
+ """Alias cũ: Coord của line, giữ tuple (x0, x1, xm, y0, y1)"""
214
+ return PdfProcess.getLineCoord(line)
215
+
216
+ @staticmethod
217
+ def getFirstWord(line):
218
+ """Giữ API cũ: trả {Text, Style, FontSize} của từ đầu"""
219
+ return {
220
+ "Text": PdfProcess.getWordText(line, 0),
221
+ "Style": U2_Word.getWordStyle(line, 0),
222
+ "FontSize": PdfProcess.getWordFontSize(line, 0),
223
+ }
224
+
225
+ @staticmethod
226
+ def getLastWord(line):
227
+ """Giữ API cũ: trả {Text, Style, FontSize} của từ cuối"""
228
+ return {
229
+ "Text": PdfProcess.getWordText(line, -1),
230
+ "Style": U2_Word.getWordStyle(line, -1),
231
+ "FontSize": PdfProcess.getWordFontSize(line, -1),
232
+ }
233
+
234
+
235
+ # ===============================
236
+ # 5. Marker / Style (line-level) -> class U5_MarkerStyle
237
+ # ===============================
238
+ class U5_MarkerStyle:
239
+ @staticmethod
240
+ def getMarker(text, patterns):
241
+ info = U1_Utils.extract_marker(text, patterns)
242
+ marker_text = info.get("marker_text")
243
+ marker_type = None
244
+ if marker_text:
245
+ # Giữ sửa lỗi xử lý dấu '+'
246
+ marker_text_cleaned = re.sub(r'([A-Za-z0-9ĐÊÔƠƯđêôơư])\+(?=\W|$)', r'\1', marker_text)
247
+ marker_type = U1_Utils.format_marker(marker_text_cleaned, patterns)
248
+ return marker_text, marker_type
249
+
250
+ @staticmethod
251
+ def getFontSize(line):
252
+ """
253
+ Mean FontSize trên spans (logic cũ) — vẫn giữ cho compatibility nếu còn chỗ gọi.
254
+ """
255
+ spans = line.get("spans", [])
256
+ if spans:
257
+ valid_spans = [s for s in spans if s.get("text", "").strip()]
258
+ if valid_spans:
259
+ sizes = [s.get("size", 12.0) for s in valid_spans]
260
+ else:
261
+ sizes = [s.get("size", 12.0) for s in spans]
262
+ avg = sum(sizes) / len(sizes)
263
+ return round(avg * 2) / 2
264
+ return 12.0
265
+
266
+
267
+ # ===============================
268
+ # 6. Tổng hợp toàn văn bản -> class U6_Document
269
+ # ===============================
270
+ class U6_Document:
271
+ @staticmethod
272
+ def getTextStatus(pdf_doc, exceptions, patterns):
273
+ doc = pdf_doc
274
+ general = {"pageGeneralSize": U3_Line.getPageGeneralSize(doc[0])}
275
+ lines = []
276
+ for i, page in enumerate(doc):
277
+ text_dict = page.get_text("dict")
278
+ for block in text_dict["blocks"]:
279
+ if "lines" in block:
280
+ for l in block["lines"]:
281
+ text = "".join(span["text"] for span in l["spans"]).strip()
282
+ if not text:
283
+ continue
284
+
285
+ # Marker
286
+ marker_text, marker_type = U5_MarkerStyle.getMarker(text, patterns)
287
+
288
+ # Style/FontSize/Coord
289
+ line_obj = {"text": text, "spans": l["spans"]}
290
+ style = U3_Line.getLineStyle(line_obj)
291
+ fontsize = PdfProcess.getLineFontSize(line_obj)
292
+ x0, x1, xm, y0, y1 = PdfProcess.getLineCoord(line_obj)
293
+
294
+ # Words
295
+ words_obj = {
296
+ "First": U4_Compat.getFirstWord(line_obj),
297
+ "Last": U4_Compat.getLastWord(line_obj)
298
+ }
299
+
300
+ line_dict = {
301
+ "Line": len(lines) + 1,
302
+ "Text": text,
303
+ "MarkerText": marker_text,
304
+ "MarkerType": marker_type,
305
+ "Style": style,
306
+ "FontSize": fontsize,
307
+ "Words": words_obj,
308
+ "Coords": {"X0": x0, "X1": x1, "XM": xm, "Y0": y0, "Y1": y1}
309
+ }
310
+ lines.append(line_dict)
311
+ return {"general": general, "lines": lines}
312
+
313
+
314
+ # ===============================
315
+ # 7. Các hàm set* -> class U7_Setters
316
+ # ===============================
317
+ class U7_Setters:
318
+ @staticmethod
319
+ def setCommonStatus(lines, attr, rank=1):
320
+ values = [l[attr] for l in lines if l.get(attr) is not None]
321
+ counter = Counter(values)
322
+ return counter.most_common(rank)
323
+
324
+ @staticmethod
325
+ def setCommonFontSize(lines):
326
+ fs, _ = U7_Setters.setCommonStatus(lines, "FontSize", 1)[0]
327
+ return round(fs, 1)
328
+
329
+ @staticmethod
330
+ def setCommonFontSizes(lines):
331
+ """
332
+ Trả về tất cả FontSize và số lượng của chúng, sắp xếp theo tần suất giảm dần.
333
+ """
334
+ values = [l["FontSize"] for l in lines if l.get("FontSize") is not None]
335
+ counter = Counter(values)
336
+ results = []
337
+ for fs, count in counter.most_common(): # trả về tất cả
338
+ results.append({"FontSize": round(fs, 1), "Count": count})
339
+ return results
340
+
341
+ @staticmethod
342
+ def setCommonMarkers(lines):
343
+ total = len(lines)
344
+ counter = Counter([l["MarkerType"] for l in lines if l["MarkerType"]])
345
+ results = []
346
+ for marker, count in counter.most_common(10):
347
+ if count >= total * 0.005:
348
+ results.append(marker)
349
+ else:
350
+ break
351
+ return results
352
+
353
+ @staticmethod
354
+ def setTextStatus(baseJson):
355
+ lines = baseJson["lines"]
356
+ pageGeneralSize = baseJson["general"]["pageGeneralSize"]
357
+ xStart, yStart, xEnd, yEnd, xMid, yMid = PdfProcess.setPageCoords(lines, pageGeneralSize)
358
+ regionWidth, regionHeight = PdfProcess.setPageRegionSize(xStart, yStart, xEnd, yEnd)
359
+ commonFontSizes = U7_Setters.setCommonFontSizes(lines)
360
+ commonFontSize = U7_Setters.setCommonFontSize(lines)
361
+ commonMarkers = U7_Setters.setCommonMarkers(lines)
362
+
363
+ new_general = {
364
+ "pageGeneralSize": baseJson["general"]["pageGeneralSize"],
365
+ "pageCoords": {"xStart": xStart, "yStart": yStart, "xEnd": xEnd, "yEnd": yEnd, "xMid": xMid, "yMid": yMid},
366
+ "pageRegionWidth": regionWidth,
367
+ "pageRegionHeight": regionHeight,
368
+ "commonFontSize": commonFontSize,
369
+ "commonFontSizes": commonFontSizes,
370
+ "commonMarkers": commonMarkers
371
+ }
372
+
373
+ new_lines = []
374
+ for i, line in enumerate(lines):
375
+ lineWidth, lineHeight = PdfProcess.setLineSize(line)
376
+ pos = PdfProcess.setPosition(line, lines[i - 1] if i > 0 else None,
377
+ lines[i + 1] if i < len(lines) - 1 else None,
378
+ xStart, xEnd, xMid)
379
+ pos_dict = {"Left": pos[0], "Right": pos[1], "Mid": pos[2], "Top": pos[3], "Bot": pos[4]}
380
+
381
+ line_dict = {
382
+ **line,
383
+ "LineWidth": lineWidth,
384
+ "LineHeight": lineHeight,
385
+ "Position": pos_dict,
386
+ "Align": PdfProcess.setAlign(pos_dict, regionWidth)
387
+ }
388
+ new_lines.append(line_dict)
389
+
390
+ return {"general": new_general, "lines": new_lines}
391
+
392
+
393
+ # ===============================
394
+ # 8. Các hàm del/reset -> class U8_Cleanup
395
+ # ===============================
396
+ class U8_Cleanup:
397
+ @staticmethod
398
+ def delStatus(jsonDict, deleteList):
399
+ for line in jsonDict["lines"]:
400
+ for attr in deleteList:
401
+ if attr in line:
402
+ del line[attr]
403
+ return jsonDict
404
+
405
+ @staticmethod
406
+ def resetPosition(jsonDict):
407
+ lines = jsonDict.get("lines", [])
408
+ for i, line in enumerate(lines):
409
+ pos = line.get("Position", {})
410
+
411
+ if "Top" in pos and pos["Top"] < 0:
412
+ top_candidates = []
413
+ if i > 0:
414
+ prev_top = lines[i - 1].get("Position", {}).get("Top")
415
+ if prev_top is not None:
416
+ top_candidates.append(prev_top)
417
+ if i < len(lines) - 1:
418
+ next_top = lines[i + 1].get("Position", {}).get("Top")
419
+ if next_top is not None:
420
+ top_candidates.append(next_top)
421
+ if top_candidates:
422
+ pos["Top"] = min(top_candidates)
423
+
424
+ if "Bot" in pos and pos["Bot"] < 0:
425
+ bot_candidates = []
426
+ if i > 0:
427
+ prev_bot = lines[i - 1].get("Position", {}).get("Bot")
428
+ if prev_bot is not None:
429
+ bot_candidates.append(prev_bot)
430
+ if i < len(lines) - 1:
431
+ next_bot = lines[i + 1].get("Position", {}).get("Bot")
432
+ if next_bot is not None:
433
+ bot_candidates.append(next_bot)
434
+ if bot_candidates:
435
+ pos["Bot"] = min(bot_candidates)
436
+ line["Position"] = pos
437
+ return jsonDict
438
+
439
+ @staticmethod
440
+ def normalizeFinal(jsonDict):
441
+ for line in jsonDict.get("lines", []):
442
+ # xử lý Text và MarkerText
443
+ if "Text" in line:
444
+ line["Text"] = TextProcess.strip_extra_spaces(line["Text"])
445
+ if "MarkerText" in line and line["MarkerText"]:
446
+ line["MarkerText"] = TextProcess.strip_extra_spaces(line["MarkerText"])
447
+
448
+ # xử lý word-level
449
+ words = line.get("Words", {})
450
+ for key in ["First", "Last"]:
451
+ if key in words and "Text" in words[key]:
452
+ words[key]["Text"] = TextProcess.strip_extra_spaces(words[key]["Text"])
453
+ return jsonDict
454
+
455
+
456
+ # ===============================
457
+ # 9. Hàm chính extractData (giữ API cũ)
458
+ # ===============================
459
+ def extractData(pdf_doc, exceptData, markerData, statusData):
460
+
461
+ # ===== 1. Load JSON theo format đồng bộ =====
462
+ exceptions = dict(exceptData)
463
+ markers = dict(markerData)
464
+ status = dict(statusData)
465
+
466
+ # ===== 2. Biên dịch markers =====
467
+ keywords = markers.get("keywords", [])
468
+ title_keywords = '|'.join(re.escape(k[0].upper() + k[1:].lower()) for k in keywords)
469
+ upper_keywords = '|'.join(re.escape(k.upper()) for k in keywords)
470
+ all_keywords = f"{title_keywords}|{upper_keywords}"
471
+
472
+ compiled_markers = []
473
+ for item in markers.get("markers", []):
474
+ pattern_str = item["pattern"].replace("{keywords}", all_keywords)
475
+ try:
476
+ compiled_pattern = re.compile(pattern_str)
477
+ except re.error:
478
+ continue
479
+ compiled_markers.append({
480
+ "pattern": compiled_pattern,
481
+ "description": item.get("description", ""),
482
+ "type": item.get("type", "")
483
+ })
484
+
485
+ patterns = {
486
+ "markers": compiled_markers,
487
+ "keywords_set": set(k.lower() for k in keywords)
488
+ }
489
+
490
+ # ===== 3. Xử lý PDF =====
491
+ baseJson = U6_Document.getTextStatus(pdf_doc, exceptions, patterns)
492
+ baseJson["lines"] = U1_Utils.normalizeRomans(baseJson["lines"])
493
+
494
+ modifiedJson = U7_Setters.setTextStatus(baseJson)
495
+ cleanJson = U8_Cleanup.resetPosition(modifiedJson)
496
+ extractedData = U8_Cleanup.delStatus(cleanJson, ["Coords"])
497
+ extractedData = U8_Cleanup.normalizeFinal(extractedData)
498
+
499
+ # ===== 4. Bổ sung tên riêng động =====
500
+ proper_names_auto = U1_Utils.collect_proper_names(extractedData["lines"], min_count=10)
501
+
502
+ proper_names_existing = [p["text"] if isinstance(p, dict) else str(p)
503
+ for p in exceptions.get("proper_names", [])]
504
+
505
+ exceptions["proper_names"] = list(set(proper_names_existing) | proper_names_auto)
506
+
507
+ return extractedData
508
+
509
+
510
+ class B1Extractor:
511
+ """
512
+ Orchestrator theo instance:
513
+ - Giữ nguyên quy tắc/thuật toán của extractData cũ.
514
+ - exceptions/markers/status và regex markers được nạp/biên dịch 1 lần.
515
+ """
516
+
517
+ def __init__(
518
+ self,
519
+ exceptData: Any,
520
+ markerData: Any,
521
+ statusData: Any,
522
+ proper_name_min_count: int = 10,
523
+ ) -> None:
524
+ """
525
+ exceptData / markerData / statusData:
526
+ - str: đường dẫn tới JSON theo format đồng bộ (U1_Utils.loadHardcodes)
527
+ - dict: dữ liệu đã load sẵn (bỏ qua loadHardcodes)
528
+ proper_name_min_count:
529
+ - Ngưỡng đếm tên riêng động.
530
+ """
531
+ # ---- 1) Nạp exceptions/markers/status (không đổi format) ----
532
+ def _ensure_dict(src, wanted=None):
533
+ if isinstance(src, dict):
534
+ return dict(src)
535
+ raise ValueError("Vui lòng truyền dict đã load sẵn thay vì đường dẫn file.")
536
+
537
+ self.exceptions: Dict[str, Any] = _ensure_dict(
538
+ exceptData, wanted=["common_words", "proper_names", "abbreviations"]
539
+ )
540
+ self.markers: Dict[str, Any] = _ensure_dict(
541
+ markerData, wanted=["keywords", "markers"]
542
+ )
543
+ self.status: Dict[str, Any] = _ensure_dict(statusData)
544
+
545
+ self.proper_name_min_count = proper_name_min_count
546
+
547
+ # ---- 2) Biên dịch markers (y như logic cũ) ----
548
+ keywords = self.markers.get("keywords", [])
549
+ title_keywords = "|".join(re.escape(k[0].upper() + k[1:].lower()) for k in keywords)
550
+ upper_keywords = "|".join(re.escape(k.upper()) for k in keywords)
551
+ all_keywords = f"{title_keywords}|{upper_keywords}" if keywords else ""
552
+
553
+ compiled_markers = []
554
+ for item in self.markers.get("markers", []):
555
+ pattern_str = item.get("pattern", "")
556
+ if all_keywords:
557
+ pattern_str = pattern_str.replace("{keywords}", all_keywords)
558
+ try:
559
+ compiled = re.compile(pattern_str)
560
+ except re.error:
561
+ continue
562
+ compiled_markers.append(
563
+ {
564
+ "pattern": compiled,
565
+ "description": item.get("description", ""),
566
+ "type": item.get("type", ""),
567
+ }
568
+ )
569
+
570
+ self.patterns = {
571
+ "markers": compiled_markers,
572
+ "keywords_set": set(k.lower() for k in keywords),
573
+ }
574
+
575
+ # ---------- Public API ----------
576
+ def extract(self, pdf_doc) -> Dict[str, Any]:
577
+ """
578
+ Chạy pipeline extractData cũ cho 1 file PDF.
579
+ Trả về extractedData (như trước).
580
+ """
581
+
582
+ # ===== 3) Trích xuất text & thuộc tính dòng từ PDF =====
583
+ baseJson = U6_Document.getTextStatus(pdf_doc, self.exceptions, self.patterns)
584
+
585
+ # Chuẩn hoá số La Mã (giữ nguyên quy tắc)
586
+ baseJson["lines"] = U1_Utils.normalizeRomans(baseJson["lines"])
587
+
588
+ # ===== 4) Tính toán status/position/align (giữ nguyên) =====
589
+ modifiedJson = U7_Setters.setTextStatus(baseJson)
590
+ cleanJson = U8_Cleanup.resetPosition(modifiedJson)
591
+ extractedData = U8_Cleanup.delStatus(cleanJson, ["Coords"])
592
+ extractedData = U8_Cleanup.normalizeFinal(extractedData)
593
+
594
+ # ===== 5) Bổ sung proper_names động (giữ nguyên tinh thần) =====
595
+ proper_names_auto = U1_Utils.collect_proper_names(
596
+ extractedData["lines"], min_count=self.proper_name_min_count
597
+ )
598
+ proper_names_existing = [
599
+ p["text"] if isinstance(p, dict) else str(p)
600
+ for p in self.exceptions.get("proper_names", [])
601
+ ]
602
+ # Cập nhật vào trạng thái của instance (để chạy nhiều file liên tiếp vẫn tích lũy)
603
+ self.exceptions["proper_names"] = list(set(proper_names_existing) | proper_names_auto)
604
+
605
+ return extractedData
Libraries/PDF_MergeData.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections import Counter
2
+ from statistics import mean, multimode
3
+
4
+ # ===============================
5
+ # HÀM CHÍNH
6
+ # ===============================
7
+ def mergeLinesToParagraphs(baseJson):
8
+ """
9
+ Nhận vào JSON sau extractData (lines-level)
10
+ Trả về JSON mới (paragraph-level)
11
+ """
12
+ general = baseJson["general"]
13
+ lines = baseJson["lines"]
14
+
15
+ paragraphs = []
16
+ buffer = []
17
+
18
+ for i, curr in enumerate(lines):
19
+ if not buffer:
20
+ buffer.append(curr)
21
+ continue
22
+
23
+ prev = lines[i-1]
24
+
25
+ if canMerge(prev, curr, i-1, i):
26
+ buffer.append(curr)
27
+
28
+ else:
29
+ paragraphs.append(buildParagraph(buffer, len(paragraphs)+1, general))
30
+ buffer = [curr]
31
+
32
+ if buffer:
33
+ paragraphs.append(buildParagraph(buffer, len(paragraphs)+1, general))
34
+
35
+ merged = {"general": general, "paragraphs": paragraphs}
36
+ # >>> TÍNH LẠI 'common' TRONG GENERAL DỰA TRÊN PARAGRAPHS
37
+ merged = recomputeCommonsInGeneralAfterMerge(merged)
38
+
39
+ return {"general": general, "paragraphs": paragraphs}
40
+
41
+
42
+
43
+ # ===============================
44
+ # CÁC HÀM ĐIỀU KIỆN MERGE
45
+ # ===============================
46
+
47
+ def canMerge(prev, curr, idx_prev=None, idx_curr=None):
48
+ """
49
+ Kiểm tra line curr có thể merge vào prev không
50
+ Ghi log lý do True/False
51
+ """
52
+ pair = f"[{idx_prev+1}->{idx_curr+1}]" if idx_prev is not None else ""
53
+
54
+ if isNewPara(curr):
55
+ return False
56
+
57
+ if not isSameFontSize(prev, curr):
58
+ return False
59
+
60
+ if not isSameStyle(prev, curr):
61
+ return False
62
+
63
+ if not isNear(prev, curr):
64
+ return False
65
+
66
+ if isSameAlign(prev, curr):
67
+ return True
68
+
69
+ if isBadAlign(prev, curr):
70
+ return False
71
+
72
+ if canMergeWithAlign(prev) or canMergeWithLeft(prev, curr):
73
+ return True
74
+
75
+ print(f"{pair} Merge=False | Reason: Fallback")
76
+ return False
77
+
78
+
79
+ # Check MarkerText
80
+ def isNewPara(line):
81
+ return line.get("MarkerText") not in (None, "", " ")
82
+
83
+ # Check FontSize
84
+ def isSameFontSize(prev, curr):
85
+ return abs(prev["FontSize"] - curr["FontSize"]) <= 0.7
86
+
87
+
88
+ # Check Style
89
+ def isSameStyle(prev, curr):
90
+ return isSameLineStyle(prev, curr) or isSameFirstStyle(prev, curr) or isSameLastStyle(prev, curr) or isSameWordStyle(prev, curr)
91
+
92
+ def isSameFStyle(prev, curr):
93
+ return isSameLineFStyle(prev, curr) or isSameFirstFStyle(prev, curr) or isSameLastFStyle(prev, curr) or isSameWordFStyle(prev, curr)
94
+
95
+ def isSameCase(prev, curr):
96
+ return isSameLineCase(prev, curr) or isSameFirstCase(prev, curr) or isSameLastCase(prev, curr) or isSameWordCase(prev, curr)
97
+
98
+ # Line - Line
99
+ def isSameLineStyle(prev, curr):
100
+ return prev["Style"] == curr["Style"]
101
+
102
+ def isSameLineFStyle(prev, curr):
103
+ return prev["Style"] %1000 == curr["Style"] %1000
104
+
105
+ def isSameLineCase(prev, curr):
106
+ return prev["Style"] /1000 == curr["Style"] /1000
107
+
108
+ # First - Line
109
+ def isSameFirstStyle(prev, curr):
110
+ return prev["Style"] == curr["Words"]["First"]["Style"]
111
+
112
+ def isSameFirstFStyle(prev, curr):
113
+ return prev["Style"] %1000 == curr["Words"]["First"]["Style"] %1000
114
+
115
+ def isSameFirstCase(prev, curr):
116
+ return prev["Style"] /1000 == curr["Words"]["First"]["Style"] /1000
117
+
118
+ # Last - Line
119
+ def isSameLastStyle(prev, curr):
120
+ return prev["Words"]["Last"]["Style"] == curr["Style"]
121
+
122
+ def isSameLastFStyle(prev, curr):
123
+ return prev["Words"]["Last"]["Style"] %1000 == curr["Style"] %1000
124
+
125
+ def isSameLastCase(prev, curr):
126
+ return prev["Words"]["Last"]["Style"] /1000 == curr["Style"] /1000
127
+
128
+ # Last - First
129
+ def isSameWordStyle(prev, curr):
130
+ return prev["Words"]["Last"]["Style"] == curr["Words"]["First"]["Style"]
131
+
132
+ def isSameWordFStyle(prev, curr):
133
+ return prev["Words"]["Last"]["Style"] %1000 == curr["Words"]["First"]["Style"] %1000
134
+
135
+ def isSameWordCase(prev, curr):
136
+ return prev["Words"]["Last"]["Style"] /1000 == curr["Words"]["First"]["Style"] /1000
137
+
138
+
139
+ # Linespace
140
+ def isNear(prev, curr):
141
+ if "Position" not in prev or "Position" not in curr:
142
+ return False
143
+ if "LineHeight" not in curr:
144
+ return False
145
+
146
+ hig_curr = curr["LineHeight"]
147
+ top_prev = prev["Position"]["Top"]
148
+ top_curr = curr["Position"]["Top"]
149
+ bot_curr = curr["Position"]["Bot"]
150
+
151
+ return (top_curr < top_prev * 2) and ((top_curr < bot_curr * 2) or bot_curr <= 3.0) and (top_curr < hig_curr * 5)
152
+
153
+
154
+ def isSameAlign(prev, curr):
155
+ return prev.get("Align") == curr.get("Align")
156
+
157
+ def isBadAlign(prev, curr):
158
+ return (prev.get("Align") != "right" and curr.get("Align") == "right")
159
+
160
+ def isNoSameAlign0(prev):
161
+ return prev.get("Align") == "Justify"
162
+
163
+ def isNoSameAlignC(prev):
164
+ return prev.get("Align") == "Center"
165
+
166
+ def isNoSameAlignR(prev):
167
+ return prev.get("Align") == "Right"
168
+
169
+ def isNoSameAlignL(prev, curr):
170
+ return prev.get("Align") == "Left" and curr.get("Align") == "Justify"
171
+
172
+ def canMergeWithAlign(prev):
173
+ return isNoSameAlign0(prev) or isNoSameAlignC(prev) or isNoSameAlignR(prev)
174
+
175
+ def canMergeWithLeft(prev, curr):
176
+ return isNoSameAlignL(prev, curr)
177
+
178
+
179
+ # ===============================
180
+ # HÀM BUILD PARAGRAPH
181
+ # ===============================
182
+
183
+ def buildParagraph(lines, para_id, general=None):
184
+ """
185
+ Tạo dict Paragraph từ list lines đã merge
186
+ """
187
+ text = " ".join([ln["Text"] for ln in lines])
188
+ marker_text = lines[0]["MarkerText"]
189
+ marker_type = lines[0]["MarkerType"]
190
+
191
+ # Style: lấy min theo từng chữ số
192
+ style = mergeStyle([ln["Style"] for ln in lines])
193
+
194
+ # first_word = lines[0]["Words"]["First"]
195
+ # last_word = lines[-1]["Words"]["Last"]
196
+
197
+ fs_values = [ln["FontSize"] for ln in lines if ln.get("FontSize") is not None]
198
+
199
+ if fs_values:
200
+ modes = multimode(fs_values) # trả về list tất cả các mode
201
+ if len(modes) == 1:
202
+ font_size = modes[0]
203
+ else:
204
+ # có nhiều mode → chọn gần với commonFontSize trong general
205
+ if general and general.get("commonFontSize") is not None:
206
+ target = general["commonFontSize"]
207
+ font_size = min(modes, key=lambda x: abs(x - target))
208
+ else:
209
+ font_size = mean(fs_values)
210
+ font_size = round(font_size, 1)
211
+ else:
212
+ font_size = 12.0
213
+ align = mostCommon([ln["Align"] for ln in lines]) or lines[-1]["Align"]
214
+
215
+ return {
216
+ "Paragraph": para_id,
217
+ "Text": text,
218
+ "MarkerText": marker_text,
219
+ "MarkerType": marker_type,
220
+ "Style": style,
221
+ "FontSize": font_size,
222
+ "Align": align,
223
+ }
224
+
225
+
226
+ # ===============================
227
+ # HELPERS
228
+ # ===============================
229
+
230
+ def mergeStyle(styles):
231
+ """
232
+ styles: list số 4 chữ số (CaseStyle*1000 + FontStyle)
233
+ - Lấy min của từng chữ số
234
+ """
235
+ digits = [list(str(s).zfill(4)) for s in styles]
236
+ min_digits = [min(int(d[i]) for d in digits) for i in range(4)]
237
+ return int("".join(str(d) for d in min_digits))
238
+
239
+
240
+ def mostCommon(values):
241
+ if not values:
242
+ return None
243
+ count = Counter(values)
244
+ most = count.most_common(1)
245
+ return most[0][0] if most else None
246
+
247
+
248
+ # ===============================
249
+ # RESOLVE COMMONS
250
+ # ===============================
251
+
252
+ def recomputeCommonsInGeneralAfterMerge(mergedJson):
253
+ """
254
+ Cập nhật lại các 'common' trong mergedJson['general'] dựa trên danh sách paragraphs.
255
+ Các field cập nhật:
256
+ - commonFontSize
257
+ - commonFontSizes: [{FontSize, Count}, ...] (giảm dần theo Count)
258
+ - commonMarkers: top marker thỏa ngưỡng >= 0.5% tổng số paragraph, tối đa 10 mục
259
+ """
260
+ paragraphs = mergedJson.get("paragraphs", [])
261
+ total = len(paragraphs)
262
+
263
+ # --- Font sizes ---
264
+ fs_values = [p["FontSize"] for p in paragraphs if p.get("FontSize") is not None]
265
+ fs_counter = Counter(fs_values)
266
+
267
+ commonFontSizes = [{"FontSize": round(fs, 1), "Count": cnt}
268
+ for fs, cnt in fs_counter.most_common()]
269
+ commonFontSize = commonFontSizes[0]["FontSize"] if commonFontSizes else None
270
+
271
+ # --- Markers ---
272
+ mk_values = [p["MarkerType"] for p in paragraphs if p.get("MarkerType")]
273
+ mk_counter = Counter(mk_values)
274
+ threshold = max(1, int(total * 0.005))
275
+ commonMarkers = [m for m, c in mk_counter.most_common(10) if c >= threshold]
276
+
277
+ # --- Ghi đè vào general ---
278
+ mergedJson["general"].update({
279
+ "commonFontSize": commonFontSize,
280
+ "commonFontSizes": commonFontSizes,
281
+ "commonMarkers": commonMarkers
282
+ })
283
+ return mergedJson
Libraries/PDF_QualityCheck.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import fitz
3
+
4
+ from typing import Dict, Tuple, Union
5
+
6
+ class PDFQualityChecker:
7
+ """
8
+ Bộ lọc chất lượng PDF cơ bản trước khi xử lý.
9
+ Đánh giá lỗi font, lỗi encode, ký tự hỏng, OCR kém, v.v.
10
+ """
11
+
12
+ def __init__(self,
13
+ max_invalid_ratio: float = 0.2,
14
+ max_whitespace_ratio: float = 0.2,
15
+ max_short_line_ratio: float = 0.3,
16
+ min_total_chars: int = 300):
17
+ self.max_invalid_ratio = max_invalid_ratio
18
+ self.max_whitespace_ratio = max_whitespace_ratio
19
+ self.max_short_line_ratio = max_short_line_ratio
20
+ self.min_total_chars = min_total_chars
21
+
22
+ # Regex nhận diện ký tự hợp lệ (chữ, số, dấu tiếng Việt, ký hiệu cơ bản)
23
+ self.valid_char_pattern = re.compile(r"[A-Za-zÀ-ỹĐđ0-9.,:;!?()\"'’”“–\-_\s]")
24
+
25
+ # ============================================================
26
+ # 1️⃣ HÀM CHÍNH
27
+ # ============================================================
28
+ def evaluate(self, pdf: Union[str, fitz.Document]) -> Tuple[bool, Dict]:
29
+ """
30
+ Đánh giá chất lượng PDF.
31
+ - pdf: đường dẫn (str) hoặc fitz.Document đã mở
32
+ - trả (is_good, metrics)
33
+ """
34
+ # ---- Chuẩn hóa input ----
35
+ if isinstance(pdf, str):
36
+ try:
37
+ doc = fitz.open(pdf)
38
+ except Exception as e:
39
+ return False, {"check_mess": f"❌ Không mở được file: {e}"}
40
+ elif isinstance(pdf, fitz.Document):
41
+ doc = pdf
42
+ else:
43
+ raise TypeError("pdf phải là str hoặc fitz.Document")
44
+
45
+ # ---- Bắt đầu thống kê ----
46
+ text_all = ""
47
+ short_lines = 0
48
+ all_lines = 0
49
+
50
+ for page in doc:
51
+ text = page.get_text("text") or ""
52
+ if not text.strip():
53
+ continue
54
+ lines = text.splitlines()
55
+ for line in lines:
56
+ if not line.strip():
57
+ continue
58
+ all_lines += 1
59
+ if len(line.strip()) < 10:
60
+ short_lines += 1
61
+ text_all += text + "\n"
62
+
63
+ total_chars = len(text_all)
64
+ if total_chars < self.min_total_chars:
65
+ return False, {
66
+ "check_mess": "❌ File quá ngắn hoặc không có text layer",
67
+ "total_chars": total_chars,
68
+ }
69
+
70
+ # ---- Tính tỷ lệ lỗi ----
71
+ valid_chars = sum(1 for ch in text_all if self.valid_char_pattern.match(ch))
72
+ invalid_chars = total_chars - valid_chars
73
+ invalid_ratio = invalid_chars / total_chars
74
+
75
+ whitespace_excess = len(re.findall(r" {3,}", text_all))
76
+ whitespace_ratio = whitespace_excess / total_chars
77
+
78
+ short_line_ratio = short_lines / max(all_lines, 1)
79
+
80
+ # ---- Đưa ra kết luận ----
81
+ is_good = (
82
+ invalid_ratio <= self.max_invalid_ratio
83
+ and whitespace_ratio <= self.max_whitespace_ratio
84
+ and short_line_ratio < 1
85
+ )
86
+
87
+ if not is_good:
88
+ if invalid_ratio > self.max_invalid_ratio:
89
+ check_mess = "❌ Nhiều ký tự lỗi / encode sai"
90
+ elif whitespace_ratio > self.max_whitespace_ratio:
91
+ check_mess = "❌ Nhiều khoảng trắng thừa"
92
+ elif short_line_ratio >= 1:
93
+ check_mess = "⚠️ OCR hoặc mất ký tự"
94
+ else:
95
+ check_mess = "❌ Văn bản lỗi nặng"
96
+ else:
97
+ check_mess = "✅ Đạt yêu cầu"
98
+
99
+ metrics = {
100
+ "check_mess": check_mess,
101
+ "total_chars": total_chars,
102
+ "invalid_ratio": round(invalid_ratio, 3),
103
+ "whitespace_ratio": round(whitespace_ratio, 3),
104
+ "short_line_ratio": round(short_line_ratio, 3),
105
+ }
106
+ return is_good, metrics
Libraries/Summarizer_Runner.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+
3
+ from typing import Dict
4
+
5
+ from . import Json_ChunkUnder
6
+
7
+
8
+ class RecursiveSummarizer:
9
+ """
10
+ Bộ tóm tắt học thuật tiếng Việt theo hướng:
11
+ Extractive (chunk semantic) + Abstractive (recursive summarization)
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ tokenizer,
17
+ summarizer,
18
+ sum_device: str,
19
+ chunk_builder: Json_ChunkUnder.ChunkUndertheseaBuilder,
20
+ max_length: int = 256,
21
+ min_length: int = 64,
22
+ max_depth: int = 5
23
+ ):
24
+ """
25
+ tokenizer: AutoTokenizer đã load sẵn.
26
+ summarizer: AutoModelForSeq2SeqLM (ViT5 / BartPho / mT5)
27
+ sum_device: 'cuda' hoặc 'cpu'
28
+ chunk_builder: ChunkUndertheseaBuilder instance.
29
+ """
30
+ self.tokenizer = tokenizer
31
+ self.model = summarizer
32
+ self.device = sum_device
33
+ self.chunk_builder = chunk_builder
34
+ self.max_length = max_length
35
+ self.min_length = min_length
36
+ self.max_depth = max_depth
37
+
38
+ # ============================================================
39
+ # 1️⃣ Hàm tóm tắt 1 đoạn
40
+ # ============================================================
41
+ def summarize_single(self, text: str) -> str:
42
+ """
43
+ Tóm tắt 1 đoạn đơn bằng mô hình abstractive (ViT5/BartPho).
44
+ """
45
+ if not text or len(text.strip()) == 0:
46
+ return ""
47
+
48
+ if "vit5" in str(self.model.__class__).lower():
49
+ input_text = f"vietnews: {text.strip()} </s>"
50
+ else:
51
+ input_text = text.strip()
52
+
53
+ try:
54
+ inputs = self.tokenizer(
55
+ input_text,
56
+ return_tensors="pt",
57
+ truncation=True,
58
+ max_length=1024
59
+ ).to(self.device)
60
+
61
+ with torch.no_grad():
62
+ summary_ids = self.model.generate(
63
+ **inputs,
64
+ max_length=self.max_length,
65
+ min_length=self.min_length,
66
+ num_beams=4,
67
+ no_repeat_ngram_size=3,
68
+ early_stopping=True
69
+ )
70
+
71
+ summary = self.tokenizer.decode(summary_ids[0], skip_special_tokens=True)
72
+ return summary.strip()
73
+
74
+ except torch.cuda.OutOfMemoryError:
75
+ print("⚠️ GPU OOM – fallback sang CPU.")
76
+ self.model = self.model.to("cpu")
77
+ inputs = inputs.to("cpu")
78
+
79
+ with torch.no_grad():
80
+ summary_ids = self.model.generate(
81
+ **inputs,
82
+ max_length=self.max_length,
83
+ min_length=self.min_length,
84
+ num_beams=4
85
+ )
86
+
87
+ return self.tokenizer.decode(summary_ids[0], skip_special_tokens=True).strip()
88
+
89
+ except Exception as e:
90
+ print(f"❌ Lỗi khi tóm tắt đoạn: {e}")
91
+ return ""
92
+
93
+ # ============================================================
94
+ # 2️⃣ Đệ quy tóm tắt văn bản dài
95
+ # ============================================================
96
+ def summarize_recursive(self, text: str, depth: int = 0, minInput: int = 256, maxInput: int = 1024) -> str:
97
+ """
98
+ Đệ quy tóm tắt văn bản dài:
99
+ - <256 từ: giữ nguyên
100
+ - <1024 từ: tóm tắt trực tiếp
101
+ - >=1024 từ: chia chunk + tóm tắt từng phần → gộp → đệ quy
102
+ """
103
+ word_count = len(text.split())
104
+ indent = " " * depth
105
+ print(f"{indent}🔹 Level {depth}: {word_count} từ")
106
+
107
+ # 1️⃣ Văn bản ngắn
108
+ if word_count < minInput:
109
+ return self.summarize_single(text)
110
+
111
+ else:
112
+ chunks = self.chunk_builder.build(text)
113
+ summaries = []
114
+
115
+ for item in chunks:
116
+ content = item.get("Content", "")
117
+ print(content)
118
+ idx = item.get("Index", "?")
119
+ wc = len(content.split())
120
+
121
+ if wc < 20:
122
+ print(f"{indent}⚠️ Bỏ qua chunk {idx} (quá ngắn)")
123
+ continue
124
+
125
+ print(f"{indent}🔸 Chunk {idx}: {wc} từ")
126
+ sub_summary = self.summarize_single(content)
127
+ if sub_summary:
128
+ summaries.append(sub_summary)
129
+
130
+ merged_summary = "\n".join(summaries)
131
+ merged_len = len(merged_summary.split())
132
+ print(f"{indent}🔁 Gộp {len(summaries)} summary → {merged_len} từ")
133
+
134
+ # Đệ quy nếu vẫn dài
135
+ if merged_len > 1024 and depth < self.max_depth:
136
+ return self.summarize_recursive(merged_summary, depth + 1)
137
+ else:
138
+ return merged_summary
139
+
140
+ # ============================================================
141
+ # 3️⃣ Hàm chính cho người dùng
142
+ # ============================================================
143
+ def summarize(self, full_text: str, minInput: int = 256, maxInput: int = 1024) -> Dict[str, str]:
144
+ """
145
+ Giao diện chính:
146
+ - Nhận text dài
147
+ - Tự động chia chunk, tóm tắt, gộp
148
+ - Trả về dict gồm summary và thống kê
149
+ """
150
+ original_len = len(full_text.split())
151
+ summary = self.summarize_recursive(full_text, depth = 0, minInput = minInput, maxInput = maxInput)
152
+
153
+ summary_len = len(summary.split())
154
+ ratio = round(summary_len / original_len, 3) if original_len else 0
155
+
156
+ print(f"\n✨ FINAL SUMMARY ({summary_len}/{original_len} từ, r={ratio}) ✨")
157
+ return {
158
+ "summary_text": summary,
159
+ "original_words": original_len,
160
+ "summary_words": summary_len,
161
+ "compression_ratio": ratio
162
+ }
Libraries/Summarizer_Trainer.py ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ import json
5
+
6
+ from typing import Optional, Union
7
+
8
+ import evaluate
9
+ from datasets import Dataset, DatasetDict, load_from_disk
10
+
11
+ from transformers import (
12
+ AutoTokenizer,
13
+ AutoModelForSeq2SeqLM,
14
+ DataCollatorForSeq2Seq,
15
+ Seq2SeqTrainer,
16
+ Seq2SeqTrainingArguments,
17
+ EarlyStoppingCallback,
18
+ set_seed,
19
+ )
20
+
21
+
22
+ class SummarizationTrainer:
23
+ """
24
+ Fine-tune mô hình tóm tắt (Seq2Seq) đa dụng — thống nhất interface:
25
+ run(Checkpoint, ModelPath, DataPath | dataset, tokenizer)
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ Max_Input_Length: int = 1024,
31
+ Max_Target_Length: int = 256,
32
+ prefix: str = "",
33
+ input_column: str = "article",
34
+ target_column: str = "summary",
35
+ Learning_Rate: float = 3e-5,
36
+ Weight_Decay: float = 0.01,
37
+ Batch_Size: int = 8,
38
+ Num_Train_Epochs: int = 3,
39
+ gradient_accumulation_steps: int = 1,
40
+ warmup_ratio: float = 0.05,
41
+ lr_scheduler_type: str = "linear",
42
+ seed: int = 42,
43
+ num_beams: int = 4,
44
+ generation_max_length: Optional[int] = None,
45
+ fp16: bool = True,
46
+ early_stopping_patience: int = 2,
47
+ logging_steps: int = 200,
48
+ report_to: str = "none",
49
+ ):
50
+ # Hyperparams
51
+ self.Max_Input_Length = Max_Input_Length
52
+ self.Max_Target_Length = Max_Target_Length
53
+ self.prefix = prefix
54
+ self.input_column = input_column
55
+ self.target_column = target_column
56
+
57
+ self.Learning_Rate = Learning_Rate
58
+ self.Weight_Decay = Weight_Decay
59
+ self.Batch_Size = Batch_Size
60
+ self.Num_Train_Epochs = Num_Train_Epochs
61
+ self.gradient_accumulation_steps = gradient_accumulation_steps
62
+ self.warmup_ratio = warmup_ratio
63
+ self.lr_scheduler_type = lr_scheduler_type
64
+ self.seed = seed
65
+
66
+ self.num_beams = num_beams
67
+ self.generation_max_length = generation_max_length
68
+ self.fp16 = fp16
69
+ self.early_stopping_patience = early_stopping_patience
70
+ self.logging_steps = logging_steps
71
+ self.report_to = report_to
72
+
73
+ self._rouge = evaluate.load("rouge")
74
+ self._tokenizer = None
75
+ self._model = None
76
+
77
+ # =========================================================
78
+ # 1️⃣ Đọc dữ liệu JSONL hoặc Arrow
79
+ # =========================================================
80
+ def _load_jsonl_to_datasetdict(self, DataPath: str) -> DatasetDict:
81
+ print(f"Đang tải dữ liệu từ {DataPath} ...")
82
+ data_list = []
83
+ with open(DataPath, "r", encoding="utf-8") as f:
84
+ for line in f:
85
+ if not line.strip():
86
+ continue
87
+ try:
88
+ data_list.append(json.loads(line))
89
+ except json.JSONDecodeError:
90
+ continue
91
+
92
+ df = pd.DataFrame(data_list)
93
+ if self.input_column not in df or self.target_column not in df:
94
+ raise ValueError(f"File {DataPath} thiếu cột {self.input_column}/{self.target_column}")
95
+ df = df[[self.input_column, self.target_column]].dropna()
96
+
97
+ dataset = Dataset.from_pandas(df, preserve_index=False)
98
+ split = dataset.train_test_split(test_size=0.1, seed=self.seed)
99
+ print(f"✔ Dữ liệu chia: {len(split['train'])} train / {len(split['test'])} validation")
100
+ return DatasetDict({"train": split["train"], "validation": split["test"]})
101
+
102
+ def _ensure_datasetdict(self, dataset: Optional[Union[Dataset, DatasetDict]], DataPath: Optional[str]) -> DatasetDict:
103
+ if dataset is not None:
104
+ if isinstance(dataset, DatasetDict):
105
+ return dataset
106
+ if isinstance(dataset, Dataset):
107
+ split = dataset.train_test_split(test_size=0.1, seed=self.seed)
108
+ return DatasetDict({"train": split["train"], "validation": split["test"]})
109
+ raise TypeError("dataset phải là datasets.Dataset hoặc datasets.DatasetDict.")
110
+ if DataPath:
111
+ if os.path.isdir(DataPath):
112
+ print(f"Load DatasetDict từ thư mục Arrow: {DataPath}")
113
+ return load_from_disk(DataPath)
114
+ return self._load_jsonl_to_datasetdict(DataPath)
115
+ raise ValueError("Cần truyền dataset hoặc DataPath")
116
+
117
+ # =========================================================
118
+ # 2️⃣ Token hóa
119
+ # =========================================================
120
+ def _preprocess_function(self, examples):
121
+ inputs = examples[self.input_column]
122
+ if self.prefix:
123
+ inputs = [self.prefix + x for x in inputs]
124
+ model_inputs = self._tokenizer(inputs, max_length=self.Max_Input_Length, truncation=True)
125
+ with self._tokenizer.as_target_tokenizer():
126
+ labels = self._tokenizer(examples[self.target_column], max_length=self.Max_Target_Length, truncation=True)
127
+ model_inputs["labels"] = labels["input_ids"]
128
+ return model_inputs
129
+
130
+ # =========================================================
131
+ # 3️⃣ Tính điểm ROUGE
132
+ # =========================================================
133
+ def _compute_metrics(self, eval_pred):
134
+ preds, labels = eval_pred
135
+ decoded_preds = self._tokenizer.batch_decode(preds, skip_special_tokens=True)
136
+ labels = np.where(labels != -100, labels, self._tokenizer.pad_token_id)
137
+ decoded_labels = self._tokenizer.batch_decode(labels, skip_special_tokens=True)
138
+ result = self._rouge.compute(predictions=decoded_preds, references=decoded_labels, use_stemmer=True)
139
+ return {k: round(v * 100, 4) for k, v in result.items()}
140
+
141
+ # =========================================================
142
+ # 4️⃣ Chạy huấn luyện
143
+ # =========================================================
144
+ def run(
145
+ self,
146
+ Checkpoint: str,
147
+ ModelPath: str,
148
+ DataPath: Optional[str] = None,
149
+ dataset: Optional[Union[Dataset, DatasetDict]] = None,
150
+ tokenizer: Optional[AutoTokenizer] = None,
151
+ ):
152
+ set_seed(self.seed)
153
+ ds = self._ensure_datasetdict(dataset, DataPath)
154
+ self._tokenizer = tokenizer or AutoTokenizer.from_pretrained(Checkpoint)
155
+ print(f"Tải model checkpoint: {Checkpoint}")
156
+ self._model = AutoModelForSeq2SeqLM.from_pretrained(Checkpoint)
157
+
158
+ print("Tokenizing dữ liệu ...")
159
+ tokenized = ds.map(self._preprocess_function, batched=True)
160
+ data_collator = DataCollatorForSeq2Seq(tokenizer=self._tokenizer, model=self._model)
161
+ gen_max_len = self.generation_max_length or self.Max_Target_Length
162
+
163
+ training_args = Seq2SeqTrainingArguments(
164
+ output_dir=ModelPath,
165
+ evaluation_strategy="epoch",
166
+ save_strategy="epoch",
167
+ learning_rate=self.Learning_Rate,
168
+ per_device_train_batch_size=self.Batch_Size,
169
+ per_device_eval_batch_size=self.Batch_Size,
170
+ weight_decay=self.Weight_Decay,
171
+ num_train_epochs=self.Num_Train_Epochs,
172
+ predict_with_generate=True,
173
+ generation_max_length=gen_max_len,
174
+ generation_num_beams=self.num_beams,
175
+ fp16=self.fp16,
176
+ gradient_accumulation_steps=self.gradient_accumulation_steps,
177
+ warmup_ratio=self.warmup_ratio,
178
+ lr_scheduler_type=self.lr_scheduler_type,
179
+ logging_steps=self.logging_steps,
180
+ load_best_model_at_end=True,
181
+ metric_for_best_model="rougeL",
182
+ greater_is_better=True,
183
+ save_total_limit=3,
184
+ report_to=self.report_to,
185
+ )
186
+
187
+ trainer = Seq2SeqTrainer(
188
+ model=self._model,
189
+ args=training_args,
190
+ train_dataset=tokenized["train"],
191
+ eval_dataset=tokenized["validation"],
192
+ tokenizer=self._tokenizer,
193
+ data_collator=data_collator,
194
+ compute_metrics=self._compute_metrics,
195
+ callbacks=[EarlyStoppingCallback(early_stopping_patience=self.early_stopping_patience)],
196
+ )
197
+
198
+ print("\n🚀 BẮT ĐẦU HUẤN LUYỆN ...")
199
+ trainer.train()
200
+ print("✅ HUẤN LUYỆN HOÀN TẤT.")
201
+ trainer.save_model(ModelPath)
202
+ self._tokenizer.save_pretrained(ModelPath)
203
+ print(f"💾 Đã lưu model & tokenizer tại: {ModelPath}")
204
+ return trainer
205
+
206
+ # =========================================================
207
+ # 5️⃣ Sinh tóm tắt
208
+ # =========================================================
209
+ def generate(self, text: str, max_new_tokens: Optional[int] = None) -> str:
210
+ if self._model is None or self._tokenizer is None:
211
+ raise RuntimeError("Model/tokenizer chưa khởi tạo, hãy gọi run() trước.")
212
+ prompt = (self.prefix + text) if self.prefix else text
213
+ inputs = self._tokenizer(prompt, return_tensors="pt", truncation=True, max_length=self.Max_Input_Length)
214
+ gen_len = max_new_tokens or self.Max_Target_Length
215
+ outputs = self._model.generate(**inputs, max_new_tokens=gen_len, num_beams=self.num_beams)
216
+ return self._tokenizer.decode(outputs[0], skip_special_tokens=True)
217
+
218
+ # =========================================================
219
+ # 6️⃣ Load lại Dataset Arrow
220
+ # =========================================================
221
+ @staticmethod
222
+ def load_local_dataset(DataPath: str) -> DatasetDict:
223
+ return load_from_disk(DataPath)
README.md CHANGED
@@ -1,10 +1,27 @@
1
- ---
2
- title: Doc Ai Api
3
- emoji: 📊
4
- colorFrom: yellow
5
- colorTo: red
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HF_Space_API (FastAPI + Docker)
2
+
3
+ This Space exposes a **REST API** for document processing (summarize, embed, search, process PDFs).
4
+ It is designed to **wrap your existing project** with minimal changes.
5
+
6
+ ## How to use
7
+
8
+ 1. Create a new **Hugging Face Space** → **Docker** template.
9
+ 2. Push this folder to that Space (or upload as a ZIP and use the Space UI).
10
+ 3. Set Space **Hardware** (e.g., T4 or A10G for GPU) and add **Space secrets** if needed:
11
+ - `HF_TOKEN` (optional, for private models)
12
+ - `API_SECRET` (optional, simple bearer auth)
13
+
14
+ 4. Wire your own modules inside `app.py` (see TODO markers).
15
+
16
+ ## Endpoints
17
+
18
+ - `GET /health`
19
+ - `POST /embed` — JSON: `{ "texts": ["..."] }`
20
+ - `POST /summarize` — JSON: `{ "text": "..." }`
21
+ - `POST /search` — JSON: `{ "query": "...", "k": 5 }` (dummy store until you wire your FAISS index)
22
+ - `POST /process_pdf` — multipart/form-data: `[email protected]`
23
+
24
+ ## Notes
25
+
26
+ - Default uses `faiss-cpu`. On GPU Space you may switch to `faiss-gpu` in `requirements.txt`.
27
+ - Avoid committing local cached models into git. Publish models to the Hub and **download at startup**.
app.py ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ FastAPI gateway for your App_Caller pipeline.
3
+
4
+ ✅ Giữ nguyên pipeline gốc (App_Caller.py)
5
+ ✅ Tương thích Hugging Face Spaces (Docker)
6
+ ✅ Có Bearer token, Swagger UI (/docs)
7
+ ✅ Endpoint: /health, /process_pdf, /search, /summarize
8
+ """
9
+
10
+ import os
11
+ import time
12
+ from typing import Optional
13
+
14
+ from fastapi import FastAPI, UploadFile, File, HTTPException, Depends, Header
15
+ from fastapi.responses import JSONResponse
16
+ from pydantic import BaseModel
17
+
18
+ # -------------------------
19
+ # 🔒 Bearer token (optional)
20
+ # -------------------------
21
+ API_SECRET = os.getenv("API_SECRET", "").strip()
22
+
23
+ def require_bearer(authorization: Optional[str] = Header(None)):
24
+ """Kiểm tra Bearer token nếu bật API_SECRET."""
25
+ if not API_SECRET:
26
+ return # Không bật xác thực
27
+ if not authorization or not authorization.startswith("Bearer "):
28
+ raise HTTPException(status_code=401, detail="Missing Bearer token")
29
+ token = authorization.split(" ", 1)[1].strip()
30
+ if token != API_SECRET:
31
+ raise HTTPException(status_code=403, detail="Invalid token")
32
+
33
+ # -------------------------
34
+ # 🧩 Import project modules
35
+ # -------------------------
36
+ try:
37
+ import App_Caller as APP_CALLER
38
+ print("✅ Đã load App_Caller.")
39
+ except Exception as e:
40
+ APP_CALLER = None
41
+ print(f"⚠️ Không thể import App_Caller: {e}")
42
+
43
+ # -------------------------
44
+ # 🚀 Init FastAPI
45
+ # -------------------------
46
+ app = FastAPI(
47
+ title="Document AI API (FastAPI)",
48
+ version="2.0.0",
49
+ description="API xử lý PDF: trích xuất, tóm tắt, tìm kiếm, phân loại.",
50
+ )
51
+
52
+ # -------------------------
53
+ # 🩺 /health
54
+ # -------------------------
55
+ @app.get("/health")
56
+ def health(_=Depends(require_bearer)):
57
+ """Kiểm tra trạng thái hoạt động."""
58
+ return {
59
+ "status": "ok",
60
+ "time": time.time(),
61
+ "App_Caller": bool(APP_CALLER),
62
+ "has_fileProcess": hasattr(APP_CALLER, "fileProcess") if APP_CALLER else False,
63
+ }
64
+
65
+ # -------------------------
66
+ # 📘 /process_pdf
67
+ # -------------------------
68
+ @app.post("/process_pdf")
69
+ async def process_pdf(file: UploadFile = File(...), _=Depends(require_bearer)):
70
+ """Nhận file PDF → chạy App_Caller.fileProcess → trả về summary + category."""
71
+ if not file.filename.lower().endswith(".pdf"):
72
+ raise HTTPException(status_code=400, detail="Chỉ chấp nhận file PDF.")
73
+
74
+ pdf_bytes = await file.read()
75
+
76
+ if not APP_CALLER or not hasattr(APP_CALLER, "fileProcess"):
77
+ raise HTTPException(status_code=500, detail="Không tìm thấy App_Caller.fileProcess().")
78
+
79
+ try:
80
+ result = APP_CALLER.fileProcess(pdf_bytes)
81
+ return {
82
+ "status": "success",
83
+ "checkstatus": result.get("checkstatus"),
84
+ "summary": result.get("summary"),
85
+ "category": result.get("category"),
86
+ "top_candidates": result.get("reranked", []),
87
+ }
88
+ except Exception as e:
89
+ raise HTTPException(status_code=500, detail=f"Lỗi xử lý PDF: {str(e)}")
90
+
91
+ # -------------------------
92
+ # 🔍 /search
93
+ # -------------------------
94
+ class SearchIn(BaseModel):
95
+ query: str
96
+ k: int = 10
97
+
98
+ @app.post("/search")
99
+ def search(body: SearchIn, _=Depends(require_bearer)):
100
+ """Tìm kiếm bằng FAISS + Rerank từ App_Caller.runSearch()."""
101
+ q = (body.query or "").strip()
102
+ if not q:
103
+ raise HTTPException(status_code=400, detail="query không được để trống")
104
+
105
+ if not APP_CALLER or not hasattr(APP_CALLER, "runSearch"):
106
+ raise HTTPException(status_code=500, detail="Không tìm thấy App_Caller.runSearch().")
107
+
108
+ try:
109
+ results = APP_CALLER.runSearch(q)
110
+ if isinstance(results, list):
111
+ formatted = results[:body.k]
112
+ elif isinstance(results, dict) and "results" in results:
113
+ formatted = results["results"][:body.k]
114
+ else:
115
+ formatted = [str(results)]
116
+ return {"status": "success", "results": formatted}
117
+ except Exception as e:
118
+ raise HTTPException(status_code=500, detail=f"Lỗi tìm kiếm: {str(e)}")
119
+
120
+ # -------------------------
121
+ # 🧠 /summarize
122
+ # -------------------------
123
+ class SummIn(BaseModel):
124
+ text: str
125
+ minInput: int = 256
126
+ maxInput: int = 1024
127
+
128
+ @app.post("/summarize")
129
+ def summarize_text(body: SummIn, _=Depends(require_bearer)):
130
+ """Tóm tắt văn bản bằng App_Caller.summarizer_engine."""
131
+ text = (body.text or "").strip()
132
+ if not text:
133
+ raise HTTPException(status_code=400, detail="text không được để trống")
134
+
135
+ if not APP_CALLER or not hasattr(APP_CALLER, "summarizer_engine"):
136
+ raise HTTPException(status_code=500, detail="Không tìm thấy App_Caller.summarizer_engine.")
137
+
138
+ try:
139
+ summarized = APP_CALLER.summarizer_engine.summarize(
140
+ text, minInput=body.minInput, maxInput=body.maxInput
141
+ )
142
+ return {"status": "success", "summary": summarized.get("summary_text", "")}
143
+ except Exception as e:
144
+ raise HTTPException(status_code=500, detail=f"Lỗi tóm tắt: {str(e)}")
requirements.txt ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ===============================
2
+ # Core Python / AI Environment
3
+ # ===============================
4
+ torch==2.3.1
5
+ torchvision==0.18.1
6
+ torchaudio==2.3.1
7
+ pytorch-cuda==12.1 # nếu dùng conda, pip sẽ bỏ qua
8
+
9
+ # ===============================
10
+ # Transformers Ecosystem
11
+ # ===============================
12
+ transformers==4.44.2
13
+ sentence-transformers==3.0.1
14
+ tokenizers>=0.19.1
15
+ huggingface-hub>=0.23.4
16
+ safetensors>=0.4.3
17
+ accelerate==0.31.0
18
+ datasets>=2.19.0
19
+ evaluate>=0.4.2
20
+ sentencepiece>=0.2.0
21
+ protobuf>=4.25.2
22
+ nltk>=3.9
23
+ rouge-score>=0.1.2
24
+
25
+ # ===============================
26
+ # Semantic Search / FAISS
27
+ # ===============================
28
+ faiss-gpu==1.8.0
29
+
30
+ # ===============================
31
+ # PDF / Text Processing
32
+ # ===============================
33
+ PyMuPDF==1
start.sh ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ # Uvicorn picks PORT from env (Spaces sets it). HOST is 0.0.0.0 for external access.
5
+ # Workers=1 to keep memory predictable on small machines; tune up if needed.
6
+ exec uvicorn app:app --host "${HOST:-0.0.0.0}" --port "${PORT:-7860}" --workers 1