Spaces:
Sleeping
Sleeping
Add all
Browse files- .gitignore +21 -0
- .vscode/settings.json +5 -0
- AppGenerator.py +207 -0
- App_Caller.py +194 -0
- App_Run.py +34 -0
- Assets/ex.exceptions.json +67 -0
- Assets/ex.markers.json +122 -0
- Assets/ex.status.json +20 -0
- Config/APIs.json +11 -0
- Config/Config.json +0 -0
- Config/Configs.py +89 -0
- Config/ModelLoader.py +280 -0
- Database/Categories/Categories_Chunks_Schema.json +8 -0
- Database/Categories/Categories_Chunks_Segment.json +313 -0
- Database/Categories/Categories_Embedding_Index.faiss +3 -0
- Database/Categories/Categories_Embedding_MapChunk.json +175 -0
- Database/Categories/Categories_Embedding_MapData.json +845 -0
- Database/Categories/Categories_Embedding_Mapping.json +177 -0
- Demo/Assets/script.js +167 -0
- Demo/Assets/style.css +279 -0
- Demo/index.html +63 -0
- Dockerfile +39 -0
- Environment/bruh.yml +108 -0
- Environment/env.yml +83 -0
- Libraries/Common_MyUtils.py +273 -0
- Libraries/Common_PdfProcess.py +152 -0
- Libraries/Common_TextProcess.py +125 -0
- Libraries/Faiss_ChunkMapping.py +184 -0
- Libraries/Faiss_Embedding.py +288 -0
- Libraries/Faiss_Searching.py +147 -0
- Libraries/Json_ChunkMaster.py +91 -0
- Libraries/Json_ChunkUnder.py +141 -0
- Libraries/Json_GetStructures.py +223 -0
- Libraries/Json_SchemaExt.py +155 -0
- Libraries/PDF_ExtractData.py +605 -0
- Libraries/PDF_MergeData.py +283 -0
- Libraries/PDF_QualityCheck.py +106 -0
- Libraries/Summarizer_Runner.py +162 -0
- Libraries/Summarizer_Trainer.py +223 -0
- README.md +27 -10
- app.py +144 -0
- requirements.txt +33 -0
- start.sh +6 -0
.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">×</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 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|