Skip to content

Commit 721bb66

Browse files
authored
Create DocumentClassifier.py
1 parent 5cde219 commit 721bb66

1 file changed

Lines changed: 351 additions & 0 deletions

File tree

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
# ==========================================================
2+
# Document Classifier PRO
3+
# Professional Desktop Tool
4+
# ==========================================================
5+
6+
import os
7+
import sys
8+
import threading
9+
import time
10+
import traceback
11+
from queue import Queue, Empty
12+
from tkinter import filedialog, messagebox
13+
import tkinter as tk
14+
15+
import ttkbootstrap as tb
16+
from ttkbootstrap.constants import *
17+
from tkinterdnd2 import DND_FILES, TkinterDnD
18+
19+
20+
# =================== APP CONFIG ===================
21+
22+
APP_NAME = "Document Classifier"
23+
APP_VERSION = "1.0.0"
24+
25+
26+
# =================== APP ===================
27+
28+
app = TkinterDnD.Tk()
29+
app.title(f"{APP_NAME} {APP_VERSION}")
30+
app.geometry("1120x650")
31+
32+
tb.Style("darkly")
33+
34+
35+
# =================== UTILITY ===================
36+
37+
def resource_path(file_name):
38+
base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__)))
39+
return os.path.join(base_path, file_name)
40+
41+
42+
def log_error():
43+
with open("error.log", "a", encoding="utf-8") as f:
44+
f.write(traceback.format_exc() + "\n")
45+
46+
47+
def show_about():
48+
messagebox.showinfo(
49+
f"About {APP_NAME}",
50+
f"{APP_NAME} v{APP_VERSION}\n\n"
51+
"Professional Document Classification Tool\n\n"
52+
"Features:\n"
53+
"• Drag & Drop files or folders\n"
54+
"• Folder scanning\n"
55+
"• Classify documents by content\n"
56+
"• Output results to folder\n"
57+
"• Pause / Stop processing\n"
58+
"• Live progress tracking\n"
59+
"• Detailed logging\n\n"
60+
"Built with Python + Tkinter + ttkbootstrap\n"
61+
"© 2026 Mate Technologies\n"
62+
"https://matetools.gumroad.com"
63+
)
64+
65+
66+
try:
67+
app.iconbitmap(resource_path("logo.ico"))
68+
except:
69+
pass
70+
71+
72+
# =================== MENU ===================
73+
74+
menubar = tb.Menu(app)
75+
help_menu = tb.Menu(menubar, tearoff=0)
76+
help_menu.add_command(label="About", command=show_about)
77+
menubar.add_cascade(label="Help", menu=help_menu)
78+
app.config(menu=menubar)
79+
80+
81+
# =================== FLAGS ===================
82+
83+
stop_flag = False
84+
pause_flag = False
85+
ui_queue = Queue()
86+
87+
file_list = []
88+
89+
output_path = tb.StringVar()
90+
91+
92+
# =================== TITLE ===================
93+
94+
tb.Label(
95+
app,
96+
text=APP_NAME,
97+
font=("Segoe UI", 24, "bold")
98+
).pack(pady=(10, 2))
99+
100+
tb.Label(
101+
app,
102+
text="Professional Document Classification – AI & Keyword Based",
103+
font=("Segoe UI", 10, "italic"),
104+
foreground="#9ca3af"
105+
).pack(pady=(0, 10))
106+
107+
108+
# =================== FRAME: FILE SELECTION ===================
109+
110+
frame1 = tb.Labelframe(app, text="Files & Folders", padding=10)
111+
frame1.pack(fill="x", padx=10, pady=6)
112+
113+
file_frame = tb.Frame(frame1)
114+
file_frame.pack(fill="x", pady=6)
115+
116+
file_listbox = tk.Listbox(file_frame, height=7, selectmode="extended")
117+
file_listbox.pack(side="left", fill="x", expand=True)
118+
119+
scroll = tb.Scrollbar(file_frame, command=file_listbox.yview)
120+
scroll.pack(side="right", fill="y")
121+
122+
file_listbox.config(yscrollcommand=scroll.set)
123+
124+
125+
# =================== FILE FUNCTIONS ===================
126+
127+
def add_files():
128+
files = filedialog.askopenfilenames(title="Select Documents")
129+
for f in files:
130+
if f not in file_list:
131+
file_list.append(f)
132+
ui_queue.put(("add", f))
133+
134+
135+
def add_folder():
136+
folder = filedialog.askdirectory(title="Select Folder")
137+
if not folder:
138+
return
139+
for root, dirs, files in os.walk(folder):
140+
for name in files:
141+
path = os.path.join(root, name)
142+
if path not in file_list:
143+
file_list.append(path)
144+
ui_queue.put(("add", path))
145+
146+
147+
def clear_list():
148+
file_list.clear()
149+
ui_queue.put(("clear", None))
150+
151+
152+
def set_output_folder():
153+
folder = filedialog.askdirectory()
154+
if folder:
155+
output_path.set(folder)
156+
157+
158+
# =================== DOCUMENT CLASSIFIER LOGIC ===================
159+
160+
def classify_document(file_path):
161+
"""Simple keyword-based classification placeholder"""
162+
try:
163+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
164+
text = f.read().lower()
165+
if "invoice" in text:
166+
return "Invoices"
167+
elif "report" in text:
168+
return "Reports"
169+
elif "resume" in text or "cv" in text:
170+
return "Resumes"
171+
else:
172+
return "Others"
173+
except Exception:
174+
return "Unknown"
175+
176+
177+
# =================== PROCESSING ===================
178+
179+
def process_documents():
180+
global stop_flag, pause_flag
181+
stop_flag = False
182+
pause_flag = False
183+
184+
classify_btn.config(state="disabled")
185+
pause_btn.config(state="normal")
186+
stop_btn.config(state="normal")
187+
188+
total = len(file_list)
189+
if total == 0:
190+
messagebox.showerror("Error", "No files selected.")
191+
classify_btn.config(state="normal")
192+
pause_btn.config(state="disabled")
193+
stop_btn.config(state="disabled")
194+
return
195+
196+
out_dir = output_path.get() or os.path.dirname(file_list[0])
197+
ui_queue.put(("log", f"Starting classification of {total} files..."))
198+
199+
for idx, file in enumerate(file_list, 1):
200+
if stop_flag:
201+
ui_queue.put(("log", "Process stopped by user."))
202+
break
203+
204+
while pause_flag:
205+
time.sleep(0.2)
206+
207+
try:
208+
category = classify_document(file)
209+
dest_dir = os.path.join(out_dir, category)
210+
os.makedirs(dest_dir, exist_ok=True)
211+
dest = os.path.join(dest_dir, os.path.basename(file))
212+
# Copy file to classified folder
213+
import shutil
214+
shutil.copy2(file, dest)
215+
ui_queue.put(("log", f"✔ {os.path.basename(file)} -> {category}"))
216+
except Exception:
217+
log_error()
218+
ui_queue.put(("log", f"❌ Failed: {file}"))
219+
220+
percent = int((idx / total) * 100)
221+
ui_queue.put(("progress", percent))
222+
223+
ui_queue.put(("complete", "Classification finished."))
224+
225+
226+
# =================== CONTROL BUTTONS ===================
227+
228+
tb.Button(frame1, text="Add Files", command=add_files, bootstyle="success").pack(side="left", padx=4)
229+
tb.Button(frame1, text="Add Folder", command=add_folder, bootstyle="info").pack(side="left", padx=4)
230+
tb.Button(frame1, text="Clear List", command=clear_list, bootstyle="danger-outline").pack(side="left", padx=4)
231+
232+
tb.Label(frame1, text="Output Folder:", width=13).pack(side="left", padx=(12, 0))
233+
tb.Entry(frame1, textvariable=output_path, width=40).pack(side="left", padx=6)
234+
tb.Button(frame1, text="Browse", command=set_output_folder).pack(side="left", padx=4)
235+
236+
classify_btn = tb.Button(frame1, text="📂 Classify", bootstyle="success")
237+
pause_btn = tb.Button(frame1, text="⏸ Pause", bootstyle="warning-outline", state="disabled")
238+
stop_btn = tb.Button(frame1, text="🛑 Stop", bootstyle="danger-outline", state="disabled")
239+
240+
classify_btn.pack(side="left", padx=6)
241+
pause_btn.pack(side="left", padx=4)
242+
stop_btn.pack(side="left", padx=4)
243+
244+
245+
# =================== PROGRESS ===================
246+
247+
frame2 = tb.Labelframe(app, text="Progress", padding=8)
248+
frame2.pack(fill="x", padx=10)
249+
250+
progress_var = tb.IntVar()
251+
252+
tb.Progressbar(
253+
frame2,
254+
variable=progress_var,
255+
maximum=100,
256+
length=500
257+
).pack(side="left", padx=10)
258+
259+
status_lbl = tb.Label(frame2, text="Status: Ready")
260+
status_lbl.pack(side="left", padx=10)
261+
262+
263+
# =================== LOG ===================
264+
265+
frame3 = tb.Labelframe(app, text="Processing Log", padding=8)
266+
frame3.pack(fill="both", expand=True, padx=10, pady=6)
267+
268+
log_text = tk.Text(frame3, height=10)
269+
log_text.pack(side="left", fill="both", expand=True)
270+
271+
log_scroll = tk.Scrollbar(frame3, command=log_text.yview)
272+
log_scroll.pack(side="right", fill="y")
273+
274+
log_text.config(yscrollcommand=log_scroll.set, state="disabled")
275+
276+
277+
# =================== UI QUEUE ===================
278+
279+
def process_ui_queue():
280+
try:
281+
while True:
282+
cmd, data = ui_queue.get_nowait()
283+
if cmd == "add":
284+
file_listbox.insert("end", data)
285+
elif cmd == "clear":
286+
file_listbox.delete(0, "end")
287+
elif cmd == "progress":
288+
progress_var.set(data)
289+
elif cmd == "log":
290+
log_text.config(state="normal")
291+
log_text.insert("end", data + "\n")
292+
log_text.see("end")
293+
log_text.config(state="disabled")
294+
elif cmd == "complete":
295+
progress_var.set(100)
296+
status_lbl.config(text=f"Status: {data}")
297+
classify_btn.config(state="normal")
298+
pause_btn.config(state="disabled")
299+
stop_btn.config(state="disabled")
300+
except Empty:
301+
pass
302+
app.after(100, process_ui_queue)
303+
304+
305+
# =================== BUTTON COMMANDS ===================
306+
307+
def toggle_pause():
308+
global pause_flag
309+
pause_flag = not pause_flag
310+
pause_btn.config(text="▶ Resume" if pause_flag else "⏸ Pause")
311+
312+
313+
def stop_process():
314+
global stop_flag
315+
stop_flag = True
316+
status_lbl.config(text="Status: Stopping...")
317+
318+
319+
classify_btn.config(
320+
command=lambda: threading.Thread(target=process_documents, daemon=True).start()
321+
)
322+
pause_btn.config(command=toggle_pause)
323+
stop_btn.config(command=stop_process)
324+
325+
326+
# =================== DRAG & DROP ===================
327+
328+
def drop(event):
329+
files = app.tk.splitlist(event.data)
330+
for f in files:
331+
if os.path.isfile(f):
332+
if f not in file_list:
333+
file_list.append(f)
334+
ui_queue.put(("add", f))
335+
elif os.path.isdir(f):
336+
for root, dirs, names in os.walk(f):
337+
for name in names:
338+
path = os.path.join(root, name)
339+
if path not in file_list:
340+
file_list.append(path)
341+
ui_queue.put(("add", path))
342+
343+
344+
file_listbox.drop_target_register(DND_FILES)
345+
file_listbox.dnd_bind("<<Drop>>", drop)
346+
347+
348+
# =================== START UI ===================
349+
350+
app.after(100, process_ui_queue)
351+
app.mainloop()

0 commit comments

Comments
 (0)