corpus-engine-tanjha 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tanush Ojha
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,34 @@
1
+ Metadata-Version: 2.4
2
+ Name: corpus-engine_tanjha
3
+ Version: 0.1.0
4
+ Summary: A CLI tool meant for directory text querying VIA text embedding
5
+ Author: Tanush Ojha
6
+ Author-email: Tanush Ojha <tanushojha057@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: einops>=0.8.2
10
+ Requires-Dist: langchain-text-splitters>=1.1.2
11
+ Requires-Dist: numpy>=2.4.6
12
+ Requires-Dist: scikit-learn>=1.9.0
13
+ Requires-Dist: sentence-transformers>=5.5.1
14
+ Requires-Dist: typer>=0.26.5
15
+ Requires-Python: >=3.11.9
16
+ Project-URL: Homepage, https://github.com/tanjha/corpus-engine
17
+ Project-URL: Issues, https://github.com/tanjha/corpus-engine/issues
18
+ Description-Content-Type: text/markdown
19
+
20
+ # Corpus Engine
21
+ Vector Embedding Powered Corpus Search Engine
22
+
23
+
24
+ # Build
25
+
26
+ Build in project root using:
27
+ ```
28
+ uv tool install .
29
+ ```
30
+
31
+ Dev build using:
32
+ ```
33
+ uv tool install -e .
34
+ ```
@@ -0,0 +1,15 @@
1
+ # Corpus Engine
2
+ Vector Embedding Powered Corpus Search Engine
3
+
4
+
5
+ # Build
6
+
7
+ Build in project root using:
8
+ ```
9
+ uv tool install .
10
+ ```
11
+
12
+ Dev build using:
13
+ ```
14
+ uv tool install -e .
15
+ ```
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "corpus-engine_tanjha"
3
+ version = "0.1.0"
4
+ description = "A CLI tool meant for directory text querying VIA text embedding"
5
+ authors = [{name="Tanush Ojha",email="tanushojha057@gmail.com"}]
6
+ readme = "README.md"
7
+ requires-python = ">=3.11.9"
8
+ dependencies = [
9
+ "einops>=0.8.2",
10
+ "langchain-text-splitters>=1.1.2",
11
+ "numpy>=2.4.6",
12
+ "scikit-learn>=1.9.0",
13
+ "sentence-transformers>=5.5.1",
14
+ "typer>=0.26.5",
15
+ ]
16
+ license= "MIT"
17
+ license-files=["LICEN[CS]E*"]
18
+
19
+ [project.scripts]
20
+ engine = "corpusEngine.engine:app"
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/tanjha/corpus-engine"
24
+ Issues = "https://github.com/tanjha/corpus-engine/issues"
25
+
26
+
27
+ [build-system]
28
+ requires = ["uv_build >= 0.11.23, <0.12.0"]
29
+ build-backend = "uv_build"
30
+
31
+ [tool.uv.build-backend]
32
+ module-name = "corpusEngine"
@@ -0,0 +1,4 @@
1
+ if __name__ == "__main__":
2
+ from corpusEngine.cli import app
3
+
4
+ app()
@@ -0,0 +1,134 @@
1
+ import sqlite3
2
+ from pathlib import Path
3
+ import numpy as np
4
+ from .entryObj import entry
5
+
6
+
7
+ class dbManager:
8
+ def __init__(self, db: sqlite3.Connection):
9
+ self.db = db
10
+
11
+ def initSQL(self):
12
+ self.db.execute("""
13
+ CREATE TABLE IF NOT EXISTS files (
14
+ id INTEGER PRIMARY KEY,
15
+ path TEXT UNIQUE NOT NULL,
16
+ time_updated TEXT NOT NULL
17
+ );
18
+ """)
19
+ self.db.execute("""
20
+ CREATE TABLE IF NOT EXISTS chunks (
21
+ id INTEGER PRIMARY KEY,
22
+ file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
23
+ start_line INTEGER,
24
+ end_line INTEGER,
25
+ chunk_num INTEGER,
26
+ embedding BLOB NOT NULL
27
+ );
28
+ """)
29
+ self.db.execute(
30
+ "CREATE INDEX IF NOT EXISTS idx_chunks_file_id ON chunks(file_id);"
31
+ )
32
+ self.db.commit()
33
+
34
+ def getEmbeddings(self):
35
+ rows = self.db.execute("""
36
+ SELECT c.id, c.start_line, c.end_line, f.path, c.embedding, c.chunk_num
37
+ FROM chunks c
38
+ JOIN files f ON c.file_id = f.id
39
+ """)
40
+ return [
41
+ {
42
+ "id": row[0],
43
+ "start_line": row[1],
44
+ "end_line": row[2],
45
+ "path": Path(row[3]),
46
+ "embedding": np.frombuffer(row[4], dtype=np.float32),
47
+ "chunk_num": int(row[5]),
48
+ }
49
+ for row in rows
50
+ ]
51
+
52
+ def getDB(self) -> sqlite3.Connection:
53
+ return self.db
54
+
55
+ def insertFull(self, entries: list[entry]):
56
+ if entries:
57
+ self._insertFile(str(entries[0].path.absolute()), entries[0].time)
58
+ print(f"inserting {str(entries[0].path.absolute())}")
59
+ self.db.commit()
60
+ for ent in entries:
61
+ file_id = self._getFileId(str(ent.path.absolute()))
62
+ self._insertChunk(
63
+ file_id,
64
+ ent.start_line,
65
+ ent.end_line,
66
+ ent.chunk_num,
67
+ ent.embedding,
68
+ )
69
+ self.db.commit()
70
+
71
+ def removeAll(self, paths: list[Path]):
72
+ for path in paths:
73
+ p = str(path)
74
+ fileId = self._getFileId(path)
75
+ if fileId is not None:
76
+ self.db.execute("DELETE FROM files where path = ?", (p,))
77
+ self.db.commit()
78
+
79
+ def removeFile(self, path: Path):
80
+ p = str(path)
81
+ fileId = self._getFileId(path)
82
+ if fileId is not None:
83
+ self.db.execute("DELETE FROM files where path = ?", (p,))
84
+ self.db.commit()
85
+
86
+ def _insertFile(
87
+ self,
88
+ path: Path,
89
+ time: str,
90
+ ):
91
+ self.db.execute(
92
+ """INSERT INTO files (path, time_updated)
93
+ VALUES (?, ?)""",
94
+ (
95
+ path,
96
+ time,
97
+ ),
98
+ )
99
+
100
+ def _getFileId(self, path: Path) -> int:
101
+ p = str(path)
102
+ row = self.db.execute("SELECT id FROM files WHERE path = ?", (p,)).fetchone()
103
+ if row is None:
104
+ return row
105
+ return row[0]
106
+
107
+ def getFileTime(self, path: Path):
108
+ p = str(path)
109
+ row = self.db.execute(
110
+ "SELECT time_updated FROM files where path = ?", (p,)
111
+ ).fetchone()
112
+ if row is None:
113
+ return row
114
+ return row[0]
115
+
116
+ def _insertChunk(
117
+ self,
118
+ file_id: int,
119
+ start_line: int,
120
+ end_line: int,
121
+ chunk_num: int,
122
+ embedding: np.ndarray,
123
+ ):
124
+ self.db.execute(
125
+ """INSERT INTO chunks (file_id, start_line, end_line, chunk_num, embedding)
126
+ VALUES (?, ?, ?, ?, ?)""",
127
+ (
128
+ file_id,
129
+ start_line,
130
+ end_line,
131
+ chunk_num,
132
+ embedding.astype(np.float32).tobytes(),
133
+ ),
134
+ )
@@ -0,0 +1,42 @@
1
+ from sentence_transformers import SentenceTransformer, util
2
+
3
+
4
+ class embed_engine:
5
+ def __init__(self, embeddings):
6
+ self.model = SentenceTransformer(
7
+ "nomic-ai/nomic-embed-text-v1", trust_remote_code=True
8
+ )
9
+ self.embeddings = embeddings
10
+
11
+ def embedDoc(self, raw_text):
12
+ embedding = self.model.encode(
13
+ f"search_document: {raw_text}", normalize_embeddings=True
14
+ )
15
+ return embedding
16
+
17
+ def embedAll(self, texts: list[str]):
18
+ prefixed = [f"search_document: {text}" for text in texts]
19
+ embedding = self.model.encode(
20
+ prefixed, normalize_embeddings=True, batch_size=32, show_progress_bar=True
21
+ )
22
+ return embedding
23
+
24
+ def _embedQuery(self, raw_text):
25
+ embedding = self.model.encode(
26
+ f"search_query: {raw_text}", normalize_embeddings=True
27
+ )
28
+ return embedding
29
+
30
+ def similarityFull(self, query, embeddings):
31
+ for embed in embeddings:
32
+ embed["similarity"] = self._similarity(query, embed["embedding"])
33
+
34
+ return embeddings
35
+
36
+ def _similarity(self, query, embed):
37
+ similarity = util.cos_sim(query, embed)
38
+ return similarity
39
+
40
+ def setEmbed(self, embeddings):
41
+ self.embeddings = embeddings
42
+ return
@@ -0,0 +1,81 @@
1
+ import typer
2
+ from pathlib import Path
3
+ from typing import Annotated
4
+ import os
5
+
6
+ app = typer.Typer(no_args_is_help=True)
7
+
8
+
9
+ @app.command()
10
+ def init(debug: Annotated[bool, typer.Option(help="Print debug logs")] = False):
11
+ """Initialize engine into current directory."""
12
+ doInit(debug)
13
+
14
+
15
+ def doInit(debug: bool = False):
16
+ initialized = False
17
+ cwd = Path.cwd()
18
+ # print(cwd)
19
+ for item in cwd.iterdir():
20
+ if item.name == "corpusEngine.db":
21
+ initialized = True
22
+ print("[INFO] Corpus Engine already initialized")
23
+ if not initialized:
24
+ from .worker import setup
25
+
26
+ setup(cwd, debug)
27
+
28
+
29
+ @app.command()
30
+ def query(debug: Annotated[bool, typer.Option(help="Print debug logs")] = False):
31
+ """Query directory using input - returns top 5 search results"""
32
+ initialized = False
33
+ cwd = Path.cwd()
34
+ # print(cwd)
35
+ for item in cwd.iterdir():
36
+ if item.name == "corpusEngine.db":
37
+ initialized = True
38
+
39
+ if initialized:
40
+ from .worker import query
41
+
42
+ query(debug)
43
+ else:
44
+ print("[INFO] Corpus Engine not initialized")
45
+
46
+
47
+ @app.command()
48
+ def update():
49
+ initialized = False
50
+ cwd = Path.cwd()
51
+ # print(cwd)
52
+ for item in cwd.iterdir():
53
+ if item.name == "corpusEngine.db":
54
+ initialized = True
55
+
56
+ if initialized:
57
+ from .worker import updateEmbed
58
+
59
+ updateEmbed(cwd)
60
+ else:
61
+ print("[INFO] Corpus Engine not initialized")
62
+
63
+
64
+ @app.command()
65
+ def reset(debug: Annotated[bool, typer.Option(help="Print debug logs")] = False):
66
+ """Remove Corpus Engine and reinitialize it."""
67
+ doRemove()
68
+ doInit(debug)
69
+
70
+
71
+ @app.command()
72
+ def remove():
73
+ doRemove()
74
+
75
+
76
+ def doRemove():
77
+ if os.path.exists("corpusEngine.db"):
78
+ os.remove("corpusEngine.db")
79
+ print("[INFO] Removed Corpus Engine")
80
+ else:
81
+ print("[INFO] Corpus Engine not initialized")
@@ -0,0 +1,35 @@
1
+ from pathlib import Path
2
+ import numpy as np
3
+
4
+
5
+ class entry:
6
+ path: Path
7
+ start_line: int
8
+ end_line: int
9
+ time: str
10
+ chunk_num: int
11
+ embedding: np.ndarray
12
+
13
+ def __init__(
14
+ self,
15
+ path: Path,
16
+ start_line: int,
17
+ end_line: int,
18
+ time: str,
19
+ chunk_num: int,
20
+ ):
21
+ self.path = path
22
+ self.start_line = start_line
23
+ self.end_line = end_line
24
+ self.time = time
25
+ self.chunk_num = chunk_num
26
+ self.embedding = None
27
+
28
+ def set_embed(self, embed: np.ndarray):
29
+ self.embedding = embed
30
+ return
31
+
32
+ def printAll(self):
33
+ print(
34
+ f"Path: {self.path} Start: {self.start_line} End: {self.end_line} Time {self.time} Raw: {self.chunk_num}"
35
+ )
@@ -0,0 +1,464 @@
1
+ """
2
+ vectorizable_extensions.py
3
+ ==========================
4
+
5
+ A comprehensive catalog of file-type suffixes whose contents can be turned into
6
+ human-readable plaintext, and therefore chunked + embedded ("vectorized") for
7
+ search / RAG pipelines.
8
+
9
+ Two practical buckets:
10
+
11
+ 1. PLAINTEXT_NATIVE -> the bytes on disk are already text; just decode them.
12
+ 2. NEEDS_EXTRACTION -> binary/container formats that yield text only after a
13
+ parser/library extracts it (PDF, DOCX, EPUB, ...).
14
+
15
+ Use `VECTORIZABLE_EXTENSIONS` for a single flat membership check, or the
16
+ category dicts below if you want to route each file to the right loader.
17
+
18
+ All suffixes are lowercase and include the leading dot.
19
+
20
+ Optional buckets (images via OCR, audio/video via transcription) are provided
21
+ separately at the bottom and are NOT included in the defaults, because their
22
+ "text" is derived rather than contained in the file.
23
+ """
24
+
25
+ from pathlib import Path
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # 1. PLAINTEXT-NATIVE (read directly, e.g. open(path, encoding="utf-8"))
29
+ # ---------------------------------------------------------------------------
30
+
31
+ PLAIN_TEXT = {
32
+ ".txt",
33
+ ".text",
34
+ ".log",
35
+ ".nfo",
36
+ ".me",
37
+ ".readme",
38
+ ".asc",
39
+ ".diff",
40
+ ".patch",
41
+ }
42
+
43
+ MARKUP_AND_DOCS_TEXT = {
44
+ ".md",
45
+ ".markdown",
46
+ ".mdown",
47
+ ".mkd",
48
+ ".mdx",
49
+ ".rst",
50
+ ".adoc",
51
+ ".asciidoc",
52
+ ".org",
53
+ ".textile",
54
+ ".creole",
55
+ ".wiki",
56
+ ".tex",
57
+ ".latex",
58
+ ".ltx",
59
+ ".bib",
60
+ ".sty",
61
+ ".cls",
62
+ ".man",
63
+ ".roff",
64
+ ".1",
65
+ ".2",
66
+ ".3",
67
+ }
68
+
69
+ WEB_AND_TEMPLATES = {
70
+ ".html",
71
+ ".htm",
72
+ ".xhtml",
73
+ ".shtml",
74
+ ".css",
75
+ ".scss",
76
+ ".sass",
77
+ ".less",
78
+ ".styl",
79
+ ".vue",
80
+ ".svelte",
81
+ ".astro",
82
+ ".jsx",
83
+ ".tsx",
84
+ ".haml",
85
+ ".pug",
86
+ ".jade",
87
+ ".ejs",
88
+ ".erb",
89
+ ".hbs",
90
+ ".handlebars",
91
+ ".mustache",
92
+ ".liquid",
93
+ ".twig",
94
+ ".njk",
95
+ ".j2",
96
+ ".jinja",
97
+ ".jinja2",
98
+ }
99
+
100
+ DATA_AND_CONFIG = {
101
+ ".json",
102
+ ".jsonl",
103
+ ".ndjson",
104
+ ".json5",
105
+ ".jsonc",
106
+ ".geojson",
107
+ ".yaml",
108
+ ".yml",
109
+ ".toml",
110
+ ".ini",
111
+ ".cfg",
112
+ ".conf",
113
+ ".config",
114
+ ".properties",
115
+ ".env",
116
+ ".csv",
117
+ ".tsv",
118
+ ".psv",
119
+ ".tab",
120
+ ".xml",
121
+ ".xsd",
122
+ ".xsl",
123
+ ".xslt",
124
+ ".dtd",
125
+ ".rss",
126
+ ".atom",
127
+ ".plist",
128
+ ".svg",
129
+ ".graphql",
130
+ ".gql",
131
+ ".proto",
132
+ ".thrift",
133
+ ".avsc",
134
+ ".prisma",
135
+ ".ron",
136
+ ".hcl",
137
+ ".editorconfig",
138
+ ".gitignore",
139
+ ".gitattributes",
140
+ ".dockerignore",
141
+ ".npmrc",
142
+ ".yarnrc",
143
+ }
144
+
145
+ SOURCE_CODE = {
146
+ # Python
147
+ ".py",
148
+ ".pyw",
149
+ ".pyi",
150
+ ".pyx",
151
+ ".pxd",
152
+ ".rpy",
153
+ # JavaScript / TypeScript
154
+ ".js",
155
+ ".mjs",
156
+ ".cjs",
157
+ ".ts",
158
+ ".mts",
159
+ ".cts",
160
+ ".coffee",
161
+ # C / C++
162
+ ".c",
163
+ ".h",
164
+ ".cpp",
165
+ ".cc",
166
+ ".cxx",
167
+ ".c++",
168
+ ".hpp",
169
+ ".hh",
170
+ ".hxx",
171
+ ".h++",
172
+ ".ino",
173
+ # C# / .NET
174
+ ".cs",
175
+ ".csx",
176
+ ".vb",
177
+ ".fs",
178
+ ".fsx",
179
+ ".fsi",
180
+ # JVM languages
181
+ ".java",
182
+ ".kt",
183
+ ".kts",
184
+ ".scala",
185
+ ".sc",
186
+ ".groovy",
187
+ ".gradle",
188
+ ".clj",
189
+ ".cljs",
190
+ ".cljc",
191
+ ".edn",
192
+ # Go / Rust / Zig / Nim / D / Crystal
193
+ ".go",
194
+ ".rs",
195
+ ".zig",
196
+ ".nim",
197
+ ".nims",
198
+ ".d",
199
+ ".cr",
200
+ # Ruby
201
+ ".rb",
202
+ ".rake",
203
+ ".gemspec",
204
+ ".ru",
205
+ # PHP / Perl
206
+ ".php",
207
+ ".phtml",
208
+ ".php3",
209
+ ".php4",
210
+ ".php5",
211
+ ".pl",
212
+ ".pm",
213
+ ".pod",
214
+ ".t",
215
+ # Swift / Objective-C
216
+ ".swift",
217
+ ".m",
218
+ ".mm",
219
+ # Shell / scripting
220
+ ".sh",
221
+ ".bash",
222
+ ".zsh",
223
+ ".fish",
224
+ ".ksh",
225
+ ".csh",
226
+ ".tcsh",
227
+ ".ps1",
228
+ ".psm1",
229
+ ".psd1",
230
+ ".bat",
231
+ ".cmd",
232
+ # Lua / Tcl / Dart / Julia / R
233
+ ".lua",
234
+ ".tcl",
235
+ ".dart",
236
+ ".jl",
237
+ ".r",
238
+ ".rmd",
239
+ ".rnw",
240
+ # Functional / academic
241
+ ".ex",
242
+ ".exs",
243
+ ".erl",
244
+ ".hrl",
245
+ ".hs",
246
+ ".lhs",
247
+ ".ml",
248
+ ".mli",
249
+ ".elm",
250
+ ".scm",
251
+ ".ss",
252
+ ".lisp",
253
+ ".lsp",
254
+ ".cl",
255
+ ".el",
256
+ ".rkt",
257
+ # Systems / legacy
258
+ ".pas",
259
+ ".pp",
260
+ ".f",
261
+ ".for",
262
+ ".f90",
263
+ ".f95",
264
+ ".f03",
265
+ ".f08",
266
+ ".cob",
267
+ ".cbl",
268
+ ".asm",
269
+ ".s",
270
+ ".vhd",
271
+ ".vhdl",
272
+ ".v",
273
+ ".sv",
274
+ ".vbs",
275
+ # Other
276
+ ".sql",
277
+ ".sol",
278
+ ".gd",
279
+ ".gdscript",
280
+ ".applescript",
281
+ ".scpt",
282
+ ".vala",
283
+ ".hx",
284
+ ".moon",
285
+ ".p",
286
+ ".ada",
287
+ ".adb",
288
+ ".ads",
289
+ }
290
+
291
+ BUILD_AND_INFRA = {
292
+ ".dockerfile",
293
+ ".containerfile",
294
+ ".mk",
295
+ ".cmake",
296
+ ".make",
297
+ ".bazel",
298
+ ".bzl",
299
+ ".buck",
300
+ ".starlark",
301
+ ".star",
302
+ ".tf",
303
+ ".tfvars",
304
+ ".nomad",
305
+ ".jenkinsfile",
306
+ ".cabal",
307
+ ".lock",
308
+ }
309
+
310
+ NOTEBOOKS_AND_LITERATE = {
311
+ ".ipynb", # JSON container; cell text is readable after parsing
312
+ ".rmd", # already above, kept for clarity
313
+ }
314
+
315
+ SUBTITLES = {
316
+ ".srt",
317
+ ".vtt",
318
+ ".sub",
319
+ ".ass",
320
+ ".ssa",
321
+ ".sbv",
322
+ ".lrc",
323
+ }
324
+
325
+ EMAIL_TEXT = {
326
+ ".eml",
327
+ ".mbox",
328
+ ".mbx",
329
+ }
330
+
331
+ # Aggregate of everything that is already plaintext on disk
332
+ PLAINTEXT_NATIVE = (
333
+ PLAIN_TEXT
334
+ | MARKUP_AND_DOCS_TEXT
335
+ | WEB_AND_TEMPLATES
336
+ | DATA_AND_CONFIG
337
+ | SOURCE_CODE
338
+ | BUILD_AND_INFRA
339
+ | NOTEBOOKS_AND_LITERATE
340
+ | SUBTITLES
341
+ | EMAIL_TEXT
342
+ )
343
+
344
+ # ---------------------------------------------------------------------------
345
+ # 2. NEEDS-EXTRACTION (binary/container; text via a parser library)
346
+ # Suggested library noted per format.
347
+ # ---------------------------------------------------------------------------
348
+
349
+ NEEDS_EXTRACTION = {
350
+ # Documents
351
+ ".pdf", # pdfplumber / pypdf / PyMuPDF (fitz) / unstructured
352
+ ".doc", # antiword / textract / LibreOffice
353
+ ".docx", # python-docx / docx2txt / unstructured
354
+ ".odt", # odfpy
355
+ ".rtf", # striprtf
356
+ ".wpd", # libwpd
357
+ ".epub", # ebooklib / unstructured
358
+ ".mobi", # mobi / kindleunpack
359
+ ".azw",
360
+ ".azw3",
361
+ ".fb2", # ebook-convert (Calibre)
362
+ ".pages", # Apple Pages (zip container; LibreOffice / textutil)
363
+ # Spreadsheets
364
+ ".xls", # xlrd
365
+ ".xlsx",
366
+ ".xlsm", # openpyxl / pandas
367
+ ".ods", # odfpy / pandas
368
+ ".numbers", # numbers-parser
369
+ # Presentations
370
+ ".ppt", # LibreOffice / textract
371
+ ".pptx", # python-pptx
372
+ ".odp", # odfpy
373
+ ".key", # Apple Keynote (LibreOffice)
374
+ # Email containers
375
+ ".msg", # extract-msg
376
+ ".pst",
377
+ ".ost", # libpff / readpst
378
+ }
379
+
380
+ # ---------------------------------------------------------------------------
381
+ # 3. FLAT SET — use this for a quick "is this file vectorizable?" check
382
+ # ---------------------------------------------------------------------------
383
+
384
+ VECTORIZABLE_EXTENSIONS = frozenset(PLAINTEXT_NATIVE | NEEDS_EXTRACTION)
385
+
386
+ # Sorted list form, if you prefer a list literal:
387
+ VECTORIZABLE_EXTENSIONS_LIST = sorted(VECTORIZABLE_EXTENSIONS)
388
+
389
+ # ---------------------------------------------------------------------------
390
+ # 4. OPTIONAL — derived text, NOT included by default
391
+ # (the file does not *contain* text; text is produced from it)
392
+ # ---------------------------------------------------------------------------
393
+
394
+ OCR_IMAGE_EXTENSIONS = {
395
+ ".png",
396
+ ".jpg",
397
+ ".jpeg",
398
+ ".tif",
399
+ ".tiff",
400
+ ".bmp",
401
+ ".gif",
402
+ ".webp",
403
+ # via pytesseract / easyocr / unstructured
404
+ }
405
+
406
+ TRANSCRIBABLE_AV_EXTENSIONS = {
407
+ ".mp3",
408
+ ".wav",
409
+ ".m4a",
410
+ ".flac",
411
+ ".ogg",
412
+ ".aac",
413
+ ".mp4",
414
+ ".mov",
415
+ ".mkv",
416
+ ".avi",
417
+ ".webm",
418
+ ".m4v",
419
+ # via whisper / faster-whisper
420
+ }
421
+
422
+ # ---------------------------------------------------------------------------
423
+ # 5. Helpers
424
+ # ---------------------------------------------------------------------------
425
+
426
+
427
+ def is_vectorizable(path, include_ocr=False, include_av=False) -> bool:
428
+ """Return True if `path`'s suffix can be turned into text."""
429
+ ext = Path(path).suffix.lower()
430
+ if ext in VECTORIZABLE_EXTENSIONS:
431
+ return True
432
+ if include_ocr and ext in OCR_IMAGE_EXTENSIONS:
433
+ return True
434
+ if include_av and ext in TRANSCRIBABLE_AV_EXTENSIONS:
435
+ return True
436
+ # Common extension-less text files (Dockerfile, Makefile, etc.)
437
+ if ext == "" and Path(path).name.lower() in {
438
+ "dockerfile",
439
+ "makefile",
440
+ "rakefile",
441
+ "gemfile",
442
+ "procfile",
443
+ "jenkinsfile",
444
+ "vagrantfile",
445
+ "license",
446
+ "readme",
447
+ "authors",
448
+ }:
449
+ return True
450
+ return False
451
+
452
+
453
+ def needs_extraction_library(path) -> bool:
454
+ """True if the file is binary and requires a parser to read its text."""
455
+ return Path(path).suffix.lower() in NEEDS_EXTRACTION
456
+
457
+
458
+ if __name__ == "__main__":
459
+ # print(f"Plaintext-native suffixes : {len(PLAINTEXT_NATIVE)}")
460
+ # print(f"Needs-extraction suffixes : {len(NEEDS_EXTRACTION)}")
461
+ # print(f"Total vectorizable : {len(VECTORIZABLE_EXTENSIONS)}")
462
+ # print()
463
+ # print(VECTORIZABLE_EXTENSIONS_LIST)
464
+ pass
@@ -0,0 +1,182 @@
1
+ from langchain_text_splitters import RecursiveCharacterTextSplitter
2
+ import sqlite3
3
+ from pathlib import Path
4
+ from datetime import datetime
5
+ from .embed import embed_engine
6
+ from .entryObj import entry
7
+ from .dbManager import dbManager
8
+ from .vectorCheck import is_vectorizable
9
+
10
+ scale = 2
11
+ config = {"chunk_size": 256 * scale, "overlap": 26 * scale}
12
+ debug = False
13
+
14
+
15
+ def setup(cwd: Path, dbg: bool = False):
16
+ # debug = dbg
17
+ print("[INFO] Initializing Corpus Engine...")
18
+ dbM = _initDB("corpusEngine.db")
19
+ dbM.initSQL()
20
+
21
+ splitter = RecursiveCharacterTextSplitter(
22
+ chunk_size=config["chunk_size"],
23
+ chunk_overlap=config["overlap"],
24
+ separators=[""],
25
+ strip_whitespace=False,
26
+ )
27
+ embedder = embed_engine([])
28
+
29
+ _lookDir(splitter=splitter, path=cwd, dbM=dbM, embedder=embedder)
30
+ print("[INFO] Finished initialization.")
31
+
32
+
33
+ def updateEmbed(cwd: Path, dbg: bool = False):
34
+ dbM = _initDB("corpusEngine.db")
35
+ embedder = embed_engine([])
36
+ splitter = RecursiveCharacterTextSplitter(
37
+ chunk_size=config["chunk_size"],
38
+ chunk_overlap=config["overlap"],
39
+ separators=[""],
40
+ strip_whitespace=False,
41
+ )
42
+ walked_dir = _listAll(cwd)
43
+ paths = {str(p.absolute()) for p in walked_dir}
44
+ db_rows = {
45
+ row[0]: row[1]
46
+ for row in dbM.getDB().execute("SELECT path, time_updated FROM files")
47
+ }
48
+ for path in paths:
49
+ time = datetime.fromtimestamp(Path(path).stat().st_mtime)
50
+ if path not in db_rows:
51
+ _splitFile(file=Path(path), splitter=splitter, dbM=dbM, embedder=embedder)
52
+ elif time != db_rows[path]:
53
+ print("[INFO] Updating file")
54
+ dbM.removeFile(Path(path))
55
+ _splitFile(file=Path(path), splitter=splitter, dbM=dbM, embedder=embedder)
56
+ for removed in db_rows.keys() - paths:
57
+ dbM.removeFile(Path(removed))
58
+
59
+
60
+ def query(debug: bool = False):
61
+ print("Querying")
62
+ dbM = _initDB("corpusEngine.db")
63
+ embeddings = dbM.getEmbeddings()
64
+ embedder = embed_engine(embeddings)
65
+ query = _getQuery(embedder)
66
+ sim_embeddings = embedder.similarityFull(query, embeddings)
67
+ similarities = sorted(sim_embeddings, key=lambda x: x["similarity"], reverse=True)[
68
+ :5
69
+ ]
70
+ splitter = RecursiveCharacterTextSplitter(
71
+ chunk_size=config["chunk_size"],
72
+ chunk_overlap=config["overlap"],
73
+ separators=[""],
74
+ strip_whitespace=False,
75
+ )
76
+
77
+ for sim in similarities:
78
+ similarity = sim["similarity"]
79
+ path = sim["path"]
80
+ start_line = sim["start_line"]
81
+ end_line = sim["end_line"]
82
+
83
+ chunk_num = sim["chunk_num"]
84
+ with open(Path(path), "r") as f:
85
+ raw_full = f.read()
86
+ chunks = splitter.split_text(raw_full)
87
+ text = chunks[chunk_num]
88
+ print(f"Similarity: {similarity.item():.4f}")
89
+ print(f"File: {path}")
90
+ if int(start_line) == int(end_line):
91
+ print(f"Line: {start_line}")
92
+ else:
93
+ print(f"Lines: {start_line}-{end_line}")
94
+ print(f"Text: {text}")
95
+ print("-------------------------------------------------------------")
96
+
97
+
98
+ def _getQuery(embedder: embed_engine) -> str:
99
+ print("Query:")
100
+ raw = input("> ")
101
+ return embedder._embedQuery(raw)
102
+
103
+
104
+ def _lookDir(
105
+ splitter: RecursiveCharacterTextSplitter,
106
+ path: Path,
107
+ dbM: dbManager,
108
+ embedder: embed_engine,
109
+ _seen=None,
110
+ ):
111
+ _seen = _seen if _seen is not None else set()
112
+ real = path.resolve()
113
+ if real in _seen:
114
+ return
115
+ _seen.add(real)
116
+ pathlist = []
117
+ for item in path.iterdir():
118
+ pathlist.append(item)
119
+ if item.is_dir():
120
+ _lookDir(splitter, item, dbM, embedder, _seen)
121
+ elif is_vectorizable(item):
122
+ _splitFile(item, splitter, dbM, embedder)
123
+
124
+
125
+ def _listAll(
126
+ path: Path,
127
+ _seen=None,
128
+ ) -> list[Path]:
129
+ _seen = _seen if _seen is not None else set()
130
+ real = path.resolve()
131
+ if real in _seen:
132
+ return []
133
+ _seen.add(real)
134
+
135
+ paths = []
136
+ for item in path.iterdir():
137
+ if item.is_dir():
138
+ paths += _listAll(item, _seen)
139
+ elif is_vectorizable(item):
140
+ paths.append(item)
141
+ return paths
142
+
143
+
144
+ def _initDB(connection: str) -> dbManager:
145
+ db = sqlite3.connect(connection)
146
+ db.execute("PRAGMA foreign_keys = ON")
147
+ return dbManager(db)
148
+
149
+
150
+ def _splitFile(
151
+ file: Path,
152
+ splitter: RecursiveCharacterTextSplitter,
153
+ dbM: dbManager,
154
+ embedder: embed_engine,
155
+ ):
156
+ time = datetime.fromtimestamp(file.stat().st_mtime)
157
+ with open(file, "r") as f:
158
+ raw_full = f.read()
159
+ chunks = splitter.split_text(raw_full)
160
+ to_embed = []
161
+ entries = []
162
+ line = 0
163
+ for i, chunk in enumerate(chunks):
164
+ start_line = line
165
+ line += chunk.count("\n")
166
+ end_line = line
167
+
168
+ to_embed.append(chunk)
169
+ ent = entry(
170
+ path=file,
171
+ start_line=start_line,
172
+ end_line=end_line,
173
+ time=time,
174
+ chunk_num=i,
175
+ )
176
+ # ent.printAll()
177
+ entries.append(ent)
178
+ embedded = embedder.embedAll(to_embed)
179
+ for chunk, ent in zip(embedded, entries):
180
+ ent.embedding = chunk
181
+ dbM.insertFull(entries)
182
+ print(f"[DEBUG] inserted all chunks of {file.name}")