Spaces:
Running on Zero
Running on Zero
Commit ·
6b161eb
1
Parent(s): 3998cca
kind fixes
Browse files- app/bot/handlers/admin.py +13 -1
- app/services/classifier.py +9 -6
- tests/test_logic.py +6 -1
app/bot/handlers/admin.py
CHANGED
|
@@ -53,7 +53,7 @@ HELP = (
|
|
| 53 |
"<b>Smart upload (default):</b> just send or FORWARD files. The bot finds the course, type, "
|
| 54 |
"title and skips duplicates. If unsure it asks you (tap a button).\n"
|
| 55 |
"/recent : last 15 files · /review : files waiting for you\n"
|
| 56 |
-
"/move <id> <course_id> [kind] · /rename <id> <title> · /del <id>\n"
|
| 57 |
"/mergecourses : merge courses shared by both streams (run once, then /seedcourses)\n\n"
|
| 58 |
"<b>Force a course:</b> <code>/upload 3 exam</code>, then send files, /done when finished.\n"
|
| 59 |
"<b>Or</b> send a file with caption\n"
|
|
@@ -398,6 +398,18 @@ async def move(message: Message, command: CommandObject, session: AsyncSession)
|
|
| 398 |
await message.answer(f"✅ #{material.id} → {course.name} · {material.kind}")
|
| 399 |
|
| 400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
@router.message(Command("rename"))
|
| 402 |
async def rename(message: Message, command: CommandObject, session: AsyncSession) -> None:
|
| 403 |
parts = (command.args or "").split(maxsplit=1)
|
|
|
|
| 53 |
"<b>Smart upload (default):</b> just send or FORWARD files. The bot finds the course, type, "
|
| 54 |
"title and skips duplicates. If unsure it asks you (tap a button).\n"
|
| 55 |
"/recent : last 15 files · /review : files waiting for you\n"
|
| 56 |
+
"/move <id> <course_id> [kind] · /kind <id> <kind> · /rename <id> <title> · /del <id>\n"
|
| 57 |
"/mergecourses : merge courses shared by both streams (run once, then /seedcourses)\n\n"
|
| 58 |
"<b>Force a course:</b> <code>/upload 3 exam</code>, then send files, /done when finished.\n"
|
| 59 |
"<b>Or</b> send a file with caption\n"
|
|
|
|
| 398 |
await message.answer(f"✅ #{material.id} → {course.name} · {material.kind}")
|
| 399 |
|
| 400 |
|
| 401 |
+
@router.message(Command("kind"))
|
| 402 |
+
async def set_kind(message: Message, command: CommandObject, session: AsyncSession) -> None:
|
| 403 |
+
parts = (command.args or "").split()
|
| 404 |
+
kind = KIND_ALIASES.get(parts[1].lower()) if len(parts) == 2 else None
|
| 405 |
+
material = await repo.get_material(session, int(parts[0])) if parts and parts[0].isdigit() else None
|
| 406 |
+
if not material or not kind:
|
| 407 |
+
await message.answer("Usage: <code>/kind <file_id> <slide|exam|note|book></code>")
|
| 408 |
+
return
|
| 409 |
+
material.kind = kind
|
| 410 |
+
await message.answer(f"✅ #{material.id} is now <b>{kind}</b>")
|
| 411 |
+
|
| 412 |
+
|
| 413 |
@router.message(Command("rename"))
|
| 414 |
async def rename(message: Message, command: CommandObject, session: AsyncSession) -> None:
|
| 415 |
parts = (command.args or "").split(maxsplit=1)
|
app/services/classifier.py
CHANGED
|
@@ -133,7 +133,8 @@ def decide(ranked: Sequence[tuple[float, object]]) -> tuple[Optional[object], bo
|
|
| 133 |
|
| 134 |
|
| 135 |
# ---------------------------------------------------------------- kind
|
| 136 |
-
|
|
|
|
| 137 |
_SLIDE = ("slide", "lecture", "ppt", "presentation", "chapter")
|
| 138 |
_NOTE = ("note", "handout", "summary", "worksheet", "assignment", "tutorial", "module", "ማስታወሻ")
|
| 139 |
_BOOK = ("textbook", "book", "edition", "isbn", "volume")
|
|
@@ -144,19 +145,21 @@ def detect_kind(name: str, caption: str, text: str, pages: Optional[int], size:
|
|
| 144 |
label = f" {(name or '').lower()} {(caption or '').lower()} ".replace("_", " ").replace("-", " ")
|
| 145 |
if ext in (".ppt", ".pptx"):
|
| 146 |
return "slide", "presentation file"
|
| 147 |
-
if any(k in label for k in
|
| 148 |
return "exam", "name says exam"
|
| 149 |
if any(k in label for k in _SLIDE):
|
| 150 |
return "slide", "name says slides/chapter"
|
| 151 |
if any(k in label for k in _BOOK):
|
| 152 |
return "book", "name says book"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
if sum(1 for k in _EXAM_TEXT if k in (text or "").lower()) >= 2:
|
| 154 |
return "exam", "content looks like an exam"
|
| 155 |
-
if
|
| 156 |
-
return "note" if not pages or pages < 150 else "book", "name says module/notes"
|
| 157 |
-
if pages and pages >= 200:
|
| 158 |
return "book", f"{pages} pages"
|
| 159 |
-
if size and size >
|
| 160 |
return "book", "very large file"
|
| 161 |
return "note", "default"
|
| 162 |
|
|
|
|
| 133 |
|
| 134 |
|
| 135 |
# ---------------------------------------------------------------- kind
|
| 136 |
+
_EXAM_STRONG = ("exam", "midterm", "mid exam", "quiz", "past paper", "question paper", "ፈተና")
|
| 137 |
+
_EXAM_WEAK = ("final", "test", "question", "previous") # "Geography Module Final" is NOT an exam
|
| 138 |
_SLIDE = ("slide", "lecture", "ppt", "presentation", "chapter")
|
| 139 |
_NOTE = ("note", "handout", "summary", "worksheet", "assignment", "tutorial", "module", "ማስታወሻ")
|
| 140 |
_BOOK = ("textbook", "book", "edition", "isbn", "volume")
|
|
|
|
| 145 |
label = f" {(name or '').lower()} {(caption or '').lower()} ".replace("_", " ").replace("-", " ")
|
| 146 |
if ext in (".ppt", ".pptx"):
|
| 147 |
return "slide", "presentation file"
|
| 148 |
+
if any(k in label for k in _EXAM_STRONG):
|
| 149 |
return "exam", "name says exam"
|
| 150 |
if any(k in label for k in _SLIDE):
|
| 151 |
return "slide", "name says slides/chapter"
|
| 152 |
if any(k in label for k in _BOOK):
|
| 153 |
return "book", "name says book"
|
| 154 |
+
if any(k in label for k in _NOTE):
|
| 155 |
+
return "note", "name says module/notes" # module/notes beat the ambiguous word "final"
|
| 156 |
+
if any(k in label for k in _EXAM_WEAK) or re.search(r"\bmid\b", label):
|
| 157 |
+
return "exam", "name hints at an exam"
|
| 158 |
if sum(1 for k in _EXAM_TEXT if k in (text or "").lower()) >= 2:
|
| 159 |
return "exam", "content looks like an exam"
|
| 160 |
+
if pages and pages >= 350: # national modules are 150-300 pages: those stay "note"
|
|
|
|
|
|
|
| 161 |
return "book", f"{pages} pages"
|
| 162 |
+
if size and size > 40 * 1024 * 1024:
|
| 163 |
return "book", "very large file"
|
| 164 |
return "note", "default"
|
| 165 |
|
tests/test_logic.py
CHANGED
|
@@ -193,7 +193,12 @@ def test_kind_detection():
|
|
| 193 |
assert C.detect_kind("Phys final exam.pdf", "", "", None, None, ".pdf")[0] == "exam"
|
| 194 |
assert C.detect_kind("lecture1.pptx", "", "", None, None, ".pptx")[0] == "slide"
|
| 195 |
assert C.detect_kind("Math module.pdf", "", "", 120, None, ".pdf")[0] == "note"
|
| 196 |
-
assert C.detect_kind("Math module.pdf", "", "", 260, None, ".pdf")[0] == "
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
assert C.detect_kind("scan.pdf", "", "Time allowed: 2 hours. Answer all questions", None, None, ".pdf")[0] == "exam"
|
| 198 |
assert C.detect_kind("whatever.pdf", "", "", None, None, ".pdf")[0] == "note"
|
| 199 |
|
|
|
|
| 193 |
assert C.detect_kind("Phys final exam.pdf", "", "", None, None, ".pdf")[0] == "exam"
|
| 194 |
assert C.detect_kind("lecture1.pptx", "", "", None, None, ".pptx")[0] == "slide"
|
| 195 |
assert C.detect_kind("Math module.pdf", "", "", 120, None, ".pdf")[0] == "note"
|
| 196 |
+
assert C.detect_kind("Math module.pdf", "", "", 260, None, ".pdf")[0] == "note"
|
| 197 |
+
assert C.detect_kind("calculus.pdf", "", "", 400, None, ".pdf")[0] == "book"
|
| 198 |
+
assert C.detect_kind("Mathematics for Social Sciences.pdf", "", "", 280, None, ".pdf")[0] == "note" # was #6
|
| 199 |
+
assert C.detect_kind("Geography Module Final.pdf", "", "", 150, None, ".pdf")[0] == "note" # was #3
|
| 200 |
+
assert C.detect_kind("Physics final.pdf", "", "", None, None, ".pdf")[0] == "exam"
|
| 201 |
+
assert C.detect_kind("Phys mid.pdf", "", "", None, None, ".pdf")[0] == "exam"
|
| 202 |
assert C.detect_kind("scan.pdf", "", "Time allowed: 2 hours. Answer all questions", None, None, ".pdf")[0] == "exam"
|
| 203 |
assert C.detect_kind("whatever.pdf", "", "", None, None, ".pdf")[0] == "note"
|
| 204 |
|