From 49c8d29a79f335aaf363f1d5b600b9b4891ada75 Mon Sep 17 00:00:00 2001 From: xxmistacruzxx Date: Thu, 29 Feb 2024 19:01:00 -0500 Subject: [PATCH] HUGE COMMIT. Essentially implemented every necessary route. Integrated database functionality. Setup a .env setup. Updated the openapi.yaml. --- .env | 0 .env.example | 39 ++++ .gitignore | 3 +- .../__pycache__/__init__.cpython-311.pyc | Bin 179 -> 0 bytes .../__pycache__/settings.cpython-311.pyc | Bin 2644 -> 0 bytes .../__pycache__/urls.cpython-311.pyc | Bin 2080 -> 0 bytes .../__pycache__/wsgi.cpython-311.pyc | Bin 715 -> 0 bytes alttextbackend/data/analyze.py | 132 +++++++++++ alttextbackend/data/books.py | 0 alttextbackend/data/images.py | 0 alttextbackend/data/postgres/books.py | 127 +++++++++++ alttextbackend/data/postgres/config.py | 32 +++ alttextbackend/data/postgres/images.py | 208 ++++++++++++++++++ alttextbackend/data/postgres/test.py | 57 +++++ .../views/__pycache__/books.cpython-311.pyc | Bin 5893 -> 7921 bytes .../__pycache__/books_bookid.cpython-311.pyc | Bin 6378 -> 11380 bytes .../books_bookid_export.cpython-311.pyc | Bin 2168 -> 6131 bytes .../books_bookid_image.cpython-311.pyc | Bin 4842 -> 8954 bytes .../books_bookid_images.cpython-311.pyc | Bin 2172 -> 2653 bytes .../books_bookid_src.cpython-311.pyc | Bin 4911 -> 0 bytes .../__pycache__/images_hash.cpython-311.pyc | Bin 2127 -> 2111 bytes alttextbackend/views/books.py | 124 +++++++---- alttextbackend/views/books_bookid.py | 182 +++++++++++---- alttextbackend/views/books_bookid_export.py | 97 +++++++- alttextbackend/views/books_bookid_image.py | 132 +++++++++-- alttextbackend/views/books_bookid_images.py | 33 ++- alttextbackend/views/images_hash.py | 22 +- openapi.yaml | 145 +++++------- 28 files changed, 1113 insertions(+), 220 deletions(-) delete mode 100644 .env create mode 100644 .env.example delete mode 100644 alttextbackend/__pycache__/__init__.cpython-311.pyc delete mode 100644 alttextbackend/__pycache__/settings.cpython-311.pyc delete mode 100644 alttextbackend/__pycache__/urls.cpython-311.pyc delete mode 100644 alttextbackend/__pycache__/wsgi.cpython-311.pyc create mode 100644 alttextbackend/data/analyze.py delete mode 100644 alttextbackend/data/books.py delete mode 100644 alttextbackend/data/images.py create mode 100644 alttextbackend/data/postgres/books.py create mode 100644 alttextbackend/data/postgres/config.py create mode 100644 alttextbackend/data/postgres/images.py create mode 100644 alttextbackend/data/postgres/test.py delete mode 100644 alttextbackend/views/__pycache__/books_bookid_src.cpython-311.pyc diff --git a/.env b/.env deleted file mode 100644 index e69de29..0000000 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7f850e4 --- /dev/null +++ b/.env.example @@ -0,0 +1,39 @@ +# DATABASE CONFIG +DATABASE_NAME=postgres +DATABASE_HOST=127.0.0.1 +DATABASE_USER=postgres +DATABASE_PASSWORD=testpassword +DATABASE_PORT=5432 + +# GENERAL CONFIG +ALT_WITH_CONTEXT=1 +ALT_WITH_HASH=1 +ALT_MULTITHREADED=0 +## ALT_VERSION OPTIONS: 1, 2 +ALT_VERSION=2 + +## DESC_ENGINE OPTIONS: replicateapi, bliplocal, googlevertexapi +DESC_ENGINE=replicateapi +## OCR_ENGINE OPTIONS: tesseract +OCR_ENGINE=tesseract +## LANG_ENGINE OPTIONS: privategpt +LANG_ENGINE=privategpt + +# DESC_ENGINE CONFIG OPTIONS +## REPLICATEAPI +REPLICATE_KEY=example_key +## BLIPLOCAL +BLIPLOCAL_DIR=/path/to/image-captioning +## GOOGLEVERTEXAPI +VERTEX_PROJECT_ID=example-123456 +### VERTEX_LOCATION OPTIONS: https://cloud.google.com/vertex-ai/docs/general/locations +VERTEX_LOCATION=us-central1 +VERTEX_GAC_PATH=/path/to/vertex-key.json + +# OCR_ENGINE CONFIG OPTIONS +## TESSERACT +TESSERACT_PATH=/path/to/tesseract.exe + +# LANG_ENGINE CONFIG OPTIONS +## PRIVATEGPT +PRIVATEGPT_HOST=http://localhost:8001 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2e4a69f..ebd18da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -*/__pycache__/ +**/__pycache__/ +.env /books /covers \ No newline at end of file diff --git a/alttextbackend/__pycache__/__init__.cpython-311.pyc b/alttextbackend/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 897ce98179e92986ffad118c3c6180e0d40979d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 179 zcmZ3^%ge<81h4+n`K#vouox+WO0<#R1&*x8w5#diFSm@k}Jw~OBf(%%}A!rMJ7ja z%#)5S&_i$SA%`M|6h;0_QUs{MlYw5^8-d(%>Z2B0l>^izsK=SN&o}SQ=+{IdO2K2Q z8~9OzqW<(IgD2D_8dO;NAl*L!{m3`WD>moRyC z!q^QChNmMjg#2O{1;j{`633bp3cjGlD11ZkjiIm@M<77~JhXGH6=7fG&v_ z(Pe?kUXkZvInlzdtTzqD!h|S#(_$Ki$;c`p_!-%j>w8#7Ov^NOahdx5>jE`s$kxYi64Gl~vTG?^%d&cI7{6o{*+ z_)|y&M|TKmS@&}}WYjIT7h{k?-OzGa&w;}nF1|0kA-8$*Hq_O>-kJApNs?AS|v6cpXU^|p!U?1Q6u`iki!uYNYLMlFxP0V)Jt&l@j@Bo0p>WR!=7^vReGWB2=Zmw+meb z&B1=N?7>=Td0k))*-iet``EUrT6V7Qj9{&$LIbk>-W)UtTj=&;tuMBmbhj#=5eZtS z4!Ieol?~W8N8Oy&ViDeMH|<;J&@}NrG!n6I_jONd*8}zgo;|?r`4Qz4Fh5{0kYxi- zc!|qD;mZXlP$*UfC&I0*N!?MnvQ{fr%7XpL2yO>fQ(@&+3o!3)s1ruGA-7d>Qt@c} z6h>=bRS2H9qrmKsvpy%pmsg5q-U*bsRo;w4-`kHz^6yqrOSUX%5G29bqM&X#cHP1z zN4#ck6D%m2qRL)#Z1gk?(|&kfPpgWqXl<>8^(NUlwSRP8`(D$m^>aLR@YQ)8l-n9M zmHJr^L9EZUEyurJ%iCeN0ibzu_Q!pjVq@()x;% z;7X;+CSQ;qSA?42B#ULC1}tDel?YCBwOA;W_)V_LJMn6zQj^xJrF^Bl?2Og;)wL2= z;{_+RDXbJd{ZcW{c?IQ+6}TGbg%zA>ZoT$cTH^#^vr;Wc8(gUf!V1)3qQsR~*0~j4 z%2x_JI4rL6l30O~iL67y#k*gWosc)GiI8~>Jma*;0;exu%b z7t8zQ{QWJjcC0NV*Uk1;0b6@;*==#9+7^`foOd(N_1oWL4BgaQgWHg89lnn|0QB1` zerV2uTkoHX#RGNyG)=p{C>?Mqk4#harPtK-@au-CcRrUYqc`fb1^0lYjkaEkZZ)Vq$X&ecL=iz<)-skMhpM`>kU`73Z z$c#Nq^VLh2fZ)eV9kuN#K#w|UJzvb>HzPS^IS zK|Eu^<*Mp=)?6(2`U418Q91$nD{7@px?m*BFwqRzJJVrfNnu3N#P9L9S1DNJHW< zAEWjx-W8fr`aonpEz5t#CJ$I$u#UZTdh=tQL1gl&CP$_}U6|q}!*bT%IxWShI8hgh z*!oVtKFYLOa7S9L(R^|&aGC*g4%iHgXOy2fE2e^mrKS3nyoxd%%Lq+*Leq-7U|C_f z`LUrDcfxX?vd4uo@z#&NnBp((EHBKZ*c^MbL|1H1=rhR|Jjzd8y1&qSu%Qo+x9iOp zoG{+Btv2g7cL+a*r=a=h>GLMMQETFnf@>#VDWRBnw3_0pt@U1i$Zk^K?z?1*EkH=| zE%6^=Kc7()@ZTS)mjM0k>hXgQolLv$;=NJC_V7iow#!;3UcQxDMv8ltuE zBAsg}ojcq4^+s5}{hJ<@Zx71LQF(cwRwA`BRI_J0VeYGNY2(e$(bC3X>3+0yf1o~y z)CY;}PFUFvE8j+y?LnmxRT=|zH&S;mW2}aHBh(*8dSjqJiS#D}buUu)63>Njas7=K eEv^q1H=@OjfqE}e@12)&#nf3|{IM@#&i?^*_hpL! diff --git a/alttextbackend/__pycache__/wsgi.cpython-311.pyc b/alttextbackend/__pycache__/wsgi.cpython-311.pyc deleted file mode 100644 index f57697cd5ef6fa5edeefa275959533eded0544f7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 715 zcmY*X&ubGw6n?X58{OI_nUd&_szWdHaF*jwZH#7{7L|R=ZmpT-A=9s zcJdiGw1E@u2+}5YEC?qqv!XEm+5iv_vUrQz>P_7(6grEZjlKX_BgK42|cyHAC?rQ5rQh1EixkCIs6O zNSUUL#)|I>To$;8g6^?2y4jK}Mozn3mLwtynTeF_b}imJOBSmXX(TOge6vB7wBV7Z zy-46wsuA6RF^OLBeXc_7@ogsiYOE<#v5(S!>Amm=YTqcIVPFC9Yqe^PYBPAhQQ2HyJ9={W5j>5?M6$2T#jF?vR!Iw+M&J(eGn~B^pk(f?S=Ou^N_g;;j{dHQhkoI4J{%E_)0wLtmDUzv6 z$h${C+~fOsxq#=xyY&K`Pwr2c{{i#E1#kGQb_Vrxs1KojQCb|97SBpc=cT3N*}S9= GEckyjyVhX< diff --git a/alttextbackend/data/analyze.py b/alttextbackend/data/analyze.py new file mode 100644 index 0000000..21912a1 --- /dev/null +++ b/alttextbackend/data/analyze.py @@ -0,0 +1,132 @@ +import os +import threading + +import bs4 +from alttext import alttext +from alttext.descengine.bliplocal import BlipLocal +from alttext.descengine.replicateapi import ReplicateAPI +from alttext.langengine.privategpt import PrivateGPT +from alttext.ocrengine.tesseract import Tesseract +from django.core.files.storage import default_storage + +from .postgres import books, images + +# from alttext.descengine.googlevertexapi import GoogleVertexAPI + + +def createAnalyzer(): + descEngine = None + match os.environ["DESC_ENGINE"].lower(): + case "replicateapi": + descEngine = ReplicateAPI(os.environ["REPLICATE_KEY"]) + case "bliplocal": + descEngine = BlipLocal(os.environ["BLIPLOCAL_DIR"]) + # case "googlevertexapi": + # descEngine = GoogleVertexAPI(os.environ["VERTEX_PROJECT_ID"], os.environ["VERTEX_LOCATION"], os.environ["VERTEX_GAC_PATH"]) + case _: + raise ValueError("Invalid description engine") + + ocrEngine = None + match os.environ["OCR_ENGINE"].lower(): + case "tesseract": + ocrEngine = Tesseract() + case _: + raise ValueError("Invalid OCR engine") + + langEngine = None + match os.environ["LANG_ENGINE"].lower(): + case "privategpt": + langEngine = PrivateGPT(os.environ["PRIVATEGPT_HOST"]) + case _: + raise ValueError("Invalid language engine") + + options = { + "withContext": bool(int(os.environ["ALT_WITH_CONTEXT"])), + "withHash": bool(int(os.environ["ALT_WITH_HASH"])), + "multiThreaded": bool(int(os.environ["ALT_MULTITHREADED"])), + "version": int(os.environ["ALT_VERSION"]), + } + + return alttext.AltTextHTML(descEngine, ocrEngine, langEngine, options) + + +def findHTML(path: str): + html_file = None + for root, _, files in os.walk(path): + for file_name in files: + if file_name.endswith(".html"): + html_file = default_storage.path(os.path.join(root, file_name)) + break + if html_file: + break + return html_file + + +def getSize(path: str): + size = 0 + for path, _, files in os.walk(path): + for f in files: + fp = os.path.join(path, f) + size += os.path.getsize(fp) + return size + + +def analyzeImageV2(alt: alttext.AltTextHTML, img: bs4.element.Tag, bookid: str): + imgRecord = images.jsonifyImage(images.getImageByBook(bookid, img["src"])) + context = [imgRecord["beforeContext"], imgRecord["afterContext"]] + imgData = alt.getImgData(img["src"]) + desc = alt.genDesc(imgData, img["src"], context) + chars = alt.genChars(imgData, img["src"]).strip() + thisAlt = alt.langEngine.refineAlt(desc, chars, context, None) + + images.updateImage( + bookid, + img["src"], + status="available", + genAlt=thisAlt, + genImageCaption=desc, + ocr=chars, + beforeContext=context[0], + afterContext=context[1], + ) + + return images.jsonifyImage(images.getImageByBook(bookid, img["src"])) + + +def analyzeSingularImageV2(alt: alttext.AltTextHTML, img: bs4.element.Tag, bookid: str): + books.updateBook(bookid, status="processing") + images.updateImage( + bookid, + img["src"], + status="processing", + ) + analyzeImageV2(alt, img, bookid) + books.updateBook(bookid, status="available") + return images.jsonifyImage(images.getImageByBook(bookid, img["src"])) + + +def analyzeImagesV2(alt: alttext.AltTextHTML, imgs: list[bs4.element.Tag], bookid: str): + books.updateBook(bookid, status="processing") + for img in imgs: + images.updateImage( + bookid, + img["src"], + status="processing", + ) + + if bool(int(os.environ["ALT_MULTITHREADED"])): + # TODO: TEST WITH OPENAI API + threads = [] + for img in imgs: + thread = threading.Thread(target=analyzeImageV2, args=(alt, img, bookid)) + thread.start() + threads.append(thread) + + for thread in threads: + thread.join() + else: + for img in imgs: + analyzeImageV2(alt, img, bookid) + + books.updateBook(bookid, status="available") + return books.jsonifyBook(books.getBook(bookid)) diff --git a/alttextbackend/data/books.py b/alttextbackend/data/books.py deleted file mode 100644 index e69de29..0000000 diff --git a/alttextbackend/data/images.py b/alttextbackend/data/images.py deleted file mode 100644 index e69de29..0000000 diff --git a/alttextbackend/data/postgres/books.py b/alttextbackend/data/postgres/books.py new file mode 100644 index 0000000..5a81300 --- /dev/null +++ b/alttextbackend/data/postgres/books.py @@ -0,0 +1,127 @@ +import uuid + +try: + from .config import Database +except ImportError: + from config import Database + +""" +BOOKS DATABASE ATTRIBUTES + *id: str + title: str + size: str + status: str + numImages: int + coverExt: str +""" + + +def createBookTable(): + db = Database() + query = "CREATE TABLE books (id varchar(255) NOT NULL PRIMARY KEY, title varchar(255), size varchar(255), status varchar(255), numImages int, coverExt varchar(255));" + db.sendQuery(query) + db.commit() + db.close() + + +def jsonifyBook(book: tuple): + return { + "id": book[0], + "title": book[1], + "size": book[2], + "status": book[3], + "numImages": book[4], + "coverExt": book[5], + } + + +def getBook(id: str): + db = Database() + query = "SELECT * FROM books WHERE id = %s" + params = (id,) + db.sendQuery(query, params) + book = db.fetchOne() + db.close() + return book + + +def getBooks(titleQ: str = None, limit: int = None, skip: int = None): + db = Database() + params = [] + query = "SELECT * FROM books" + + if titleQ: + lowerTitleQ = f"%{titleQ.lower()}%" + query += " WHERE LOWER(title) LIKE %s" + params.append(lowerTitleQ) + + if limit is not None: + query += " LIMIT %s" + params.append(limit) + + if skip is not None: + query += " OFFSET %s" + params.append(skip) + + db.sendQuery(query, params) + books = db.fetchAll() + db.close() + return books + + +def addBook( + title: str, + size: str, + numImages: int, + id: str = None, + status: str = "available", + coverExt: str = None, +): + if id == None: + id = str(uuid.uuid4()) + + db = Database() + query = "INSERT INTO books (id, title, status, numimages, size, coverext) VALUES (%s, %s, %s, %s, %s, %s);" + params = (id, title, status, numImages, size, coverExt) + db.sendQuery(query, params) + db.commit() + db.close() + return getBook(id) + + +def deleteBook(id: str): + db = Database() + query = "DELETE FROM books WHERE id = %s" + params = (id,) + db.sendQuery(query, params) + db.commit() + db.close() + + +def updateBook(id: str, title: str = None, status: str = None, coverExt: str = None): + db = Database() + + if title or status or coverExt: + params = [] + query = "UPDATE books SET" + + if title: + query += " title = %s," + params.append(title) + + if status: + query += " status = %s," + params.append(status) + + if coverExt: + query += " coverext = %s," + params.append(coverExt) + + query = query[:-1] + + query += " WHERE id = %s" + params.append(id) + db.sendQuery(query, params) + db.commit() + + db.close() diff --git a/alttextbackend/data/postgres/config.py b/alttextbackend/data/postgres/config.py new file mode 100644 index 0000000..3090e41 --- /dev/null +++ b/alttextbackend/data/postgres/config.py @@ -0,0 +1,32 @@ +import psycopg2 +import os + +class Database: + def __init__(self): + self.conn = psycopg2.connect( + database=os.environ['DATABASE_NAME'], + host=os.environ['DATABASE_HOST'], + user=os.environ['DATABASE_USER'], + password=os.environ['DATABASE_PASSWORD'], + port=os.environ['DATABASE_PORT'] + ) + self.cursor = self.conn.cursor() + + def sendQuery(self, query:str, params = None): + self.cursor.execute(query, params) + + def commit(self): + self.conn.commit() + + def fetchOne(self): + return self.cursor.fetchone() + + def fetchAll(self): + return self.cursor.fetchall() + + def fetchMany(self, size:int): + return self.cursor.fetchmany(size=size) + + def close(self): + self.cursor.close() + self.conn.close() \ No newline at end of file diff --git a/alttextbackend/data/postgres/images.py b/alttextbackend/data/postgres/images.py new file mode 100644 index 0000000..b76ca7c --- /dev/null +++ b/alttextbackend/data/postgres/images.py @@ -0,0 +1,208 @@ +try: + from .config import Database +except ImportError: + from config import Database + +""" +IMAGE DATABASE ATTRIBUTES + *bookid: str + *src: str + hash: str + status: str + alt: str + originalAlt: str + genAlt: str + genImageCaption: str + ocr: str + beforeContext: str + afterContext: str + additionalContext: str +""" + + +def createImageTable(): + db = Database() + query = "CREATE TABLE images (bookid varchar(255) NOT NULL, src varchar(255) NOT NULL, hash varchar(255), status varchar(255), alt varchar(1000), originalAlt varchar(1000), genAlt varchar(1000), genImageCaption varchar(1000), ocr varchar(1000), beforeContext varchar(2000), afterContext varchar(2000), additionalContext varchar(1000), CONSTRAINT PK_Image PRIMARY KEY (bookid, src), FOREIGN KEY (bookid) REFERENCES books(id) ON DELETE CASCADE);" + db.sendQuery(query) + db.commit() + db.close() + + +def jsonifyImage(image: tuple): + return { + "bookid": image[0], + "src": image[1], + "hash": image[2], + "status": image[3], + "alt": image[4], + "originalAlt": image[5], + "genAlt": image[6], + "genImageCaption": image[7], + "ocr": image[8], + "beforeContext": image[9], + "afterContext": image[10], + "additionalContext": image[11], + } + + +def getImageByBook(bookid: str, src: str): + db = Database() + query = "SELECT * FROM images WHERE bookid = %s AND src = %s" + params = (bookid, src) + db.sendQuery(query, params) + image = db.fetchOne() + db.close + return image + + +def getImagesByBook(bookid: str): + db = Database() + query = "SELECT * FROM images WHERE bookid = %s" + params = (bookid,) + db.sendQuery(query, params) + images = db.fetchAll() + db.close() + return images + + +def getImagesByHash(hash: str): + db = Database() + query = "SELECT * FROM images WHERE hash = %s" + params = (hash,) + db.sendQuery(query, params) + images = db.fetchAll() + db.close() + return images + + +def addImage( + bookid: str, + src: str, + hash: str = None, + status: str = "available", + alt: str = "", + originalAlt: str = None, + genAlt: str = None, + genImageCaption: str = None, + ocr: str = None, + beforeContext: str = None, + afterContext: str = None, + additionalContext: str = None, +): + db = Database() + query = "INSERT INTO images (bookid, src, hash, status, alt, originalalt, genalt, genimagecaption, ocr, beforecontext, aftercontext, additionalcontext) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s);" + if status != "available" and status != "processing": + status = "available" + if alt is not None: + alt = alt[:1000] + if originalAlt is not None: + originalAlt = originalAlt[:1000] + if genAlt is not None: + genAlt = genAlt[:1000] + if genImageCaption is not None: + genImageCaption = genImageCaption[:1000] + if ocr is not None: + ocr = ocr[:1000] + if beforeContext is not None: + beforeContext = beforeContext[:2000] + if afterContext is not None: + afterContext = afterContext[:2000] + if additionalContext is not None: + additionalContext = additionalContext[:1000] + params = ( + bookid, + src, + hash, + status, + alt, + originalAlt, + genAlt, + genImageCaption, + ocr, + beforeContext, + afterContext, + additionalContext, + ) + db.sendQuery(query, params) + db.commit() + db.close() + return getImageByBook(bookid, src) + + +def deleteImage(bookid: str, src: str): + db = Database() + query = "DELETE FROM images WHERE bookid = %s AND src = %s;" + params = (bookid, src) + db.sendQuery(query, params) + db.commit() + db.close() + + +def updateImage( + bookid: str, + src: str, + status: str = None, + alt: str = None, + genAlt: str = None, + genImageCaption: str = None, + ocr: str = None, + beforeContext: str = None, + afterContext: str = None, + additionalContext: str = None, +): + db = Database() + + if ( + status + or alt + or genAlt + or genImageCaption + or ocr + or beforeContext + or afterContext + or additionalContext + ): + params = [] + query = "UPDATE images SET" + + if status: + query += " status = %s," + params.append(status) + + if alt: + query += " alt = %s," + params.append(alt) + + if genAlt: + query += " genalt = %s," + params.append(genAlt) + + if genImageCaption: + query += " genimagecaption = %s," + params.append(genImageCaption) + + if ocr: + query += " ocr = %s," + params.append(ocr) + + if beforeContext: + query += " beforecontext = %s," + params.append(beforeContext) + + if afterContext: + query += " aftercontext = %s," + params.append(afterContext) + + if additionalContext: + query += " additionalcontext = %s," + params.append(additionalContext) + + query = query[:-1] + + query += " WHERE bookid = %s AND src = %s" + params.append(bookid) + params.append(src) + db.sendQuery(query, params) + db.commit() + + db.close() diff --git a/alttextbackend/data/postgres/test.py b/alttextbackend/data/postgres/test.py new file mode 100644 index 0000000..8ce8815 --- /dev/null +++ b/alttextbackend/data/postgres/test.py @@ -0,0 +1,57 @@ +import dotenv +from books import addBook, getBooks, getBook, updateBook +from images import ( + addImage, + getImagesByBook, + getImageByBook, + getImagesByHash, + updateImage, +) +from config import Database + +dotenv.load_dotenv() + +""" +createBookTable = "CREATE TABLE books (id varchar(255) NOT NULL PRIMARY KEY, title varchar(255), size varchar(255), status varchar(255), numImages int, coverExt varchar(255));" +createImageTable = "CREATE TABLE images (bookid varchar(255) NOT NULL, src varchar(255) NOT NULL, hash varchar(255), status varchar(255), alt varchar(255), originalAlt varchar(255), genAlt varchar(255), genImageCaption varchar(255), ocr varchar(255), beforeContext varchar(255), afterContext varchar(255), additionalContext varchar(255), CONSTRAINT PK_Image PRIMARY KEY (bookid, src), FOREIGN KEY (bookid) REFERENCES books(id) ON DELETE CASCADE);" +""" + +# db.sendQuery("SELECT * FROM books") +# print(db.fetchOne()) + +# addBook(title="Harry Potter", size="300kb", numImages=25) +""" +addBook(title="Harry Potter", size="300kb", numImages=25) +addBook(title="Harraoeu", size="300kb", numImages=25) +addBook(title="Hartter", size="300kb", numImages=25) +""" + +# getBooks(titleQ="Harry Potter", limit=1, skip=2) + +""" +addImage( + bookid="f1ac43cc-9f6d-4dc8-ac4f-aea0c4af5198", + src="sampleSrcMEOW", + hash="brown", + status="available", +) +""" + +# getImagesByBook("fa47d830-586a-485f-a579-67b33fd3eae3") + +# print(getImagesByHash("brown")) + +updateImage( + bookid="f1ac43cc-9f6d-4dc8-ac4f-aea0c4af5198", + src="sampleSrcMEOW", + status="bruh2", + beforeContext="before context be like", +) + + +# updateBook(id="72950", title="Test Title Two", status="available") + +db = Database() +# db.sendQuery("SELECT * FROM images;") +# print(db.fetchAll()) +db.close() diff --git a/alttextbackend/views/__pycache__/books.cpython-311.pyc b/alttextbackend/views/__pycache__/books.cpython-311.pyc index ffa36871c821924c6e5fdbd4063e4cc84a48fee1..7bf92c3682d933890dfdfc397712373542174c3a 100644 GIT binary patch literal 7921 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 literal 5893 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 diff --git a/alttextbackend/views/__pycache__/books_bookid.cpython-311.pyc b/alttextbackend/views/__pycache__/books_bookid.cpython-311.pyc index c98368b36dc7ca10a0df0d2172c4b8005921e0b4..271bf1c3bfec416451320d00d9d74da32831389b 100644 GIT binary patch literal 11380 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 A@&Et; literal 6378 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 J6NK1$_!moP3atPD diff --git a/alttextbackend/views/__pycache__/books_bookid_export.cpython-311.pyc b/alttextbackend/views/__pycache__/books_bookid_export.cpython-311.pyc index 0a3a2ce55053017549986b538fa821ca0e2cff50..daf9c43b977ec7e17ccca090fa8b65878b93925a 100644 GIT binary patch literal 6131 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 literal 2168 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 G&Hp!6sxj~Y literal 4842 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 delta 1150 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 K&0F*gq5lCc;|z)b diff --git a/alttextbackend/views/__pycache__/images_hash.cpython-311.pyc b/alttextbackend/views/__pycache__/images_hash.cpython-311.pyc index e7d5bad2439f2adc3e1d173ce2ff72503fa1d5b8..d5a388a027910e2152bf77510028e52af102e92c 100644 GIT binary patch delta 1146 zcmah{OH30%7@pZzx6dt)hKi8*h>1l~K?yNYF`yvE1ffMt8k1&WhuYGm&CCK~Aia3- zC>aw;ki=6x=)r?ms}Z`SQQ!pZ_tR!ym$-SHYkU zFe?AtMDOI4Q1sj?)1CthSTGN4n1Z&Hk|5Wmylg8e#a2_Q(B=Hf*zwX7bJ_uc!Bh|e z3ETvh_83?m1al#tnp0T@;59onN*N+bXHj0{{mU1&)Fg=czVOhPw~gfMY6 z;+W1HVP@Y@0o$V{rhTv_q9+h37G?>GLD$nidTSz!7DnhYd;w+JD%n2XA?DzvD^jP{ zC{+4Uy6@MRXt48r0@4ooQ;Exal+)|dl2V4Oe0FNI>~TDG833p0fV|%@068&N?pyMd zC3c^k8aewO7Ui#7-^gW&wkeH~Fte~m6w7qX7{m%!1N6FbYLdU|Y6#;3Cc^GE508(J zrhB@&(gXcB(uu*byMxJbSIgRF1`*fDAm^442Ns7-^1zjCv*-qLq%fPEUKAeJFEU(U z8SdPU>2yu$=xE{3`2Nh@RkC)55I&C+dxUw@I7)YvHu#LHs#V%HB30we%iM<1T{XI& zDBDKMy1YJAZ9D(Ac_Z3ejrP8uuU;Fg7>NxdQ8f}3J%RZmMq*wZ;K_m<`24WSfp5kQ z6-~_QNKfTWLJ$!<#K*Wh%AtXS*k+TcC&G0OJQuk{x73qYlW-S!)uXU>lZVF|+z>{@ zNl#@3SHIOv|2gzvesS_3urzZ8lPvsjbJ_<_q3UJ9Q4qcG^b|JtQF@g zj28>U$*?=|+E%a_uU@y}c7D;9-2dgZCPga;`eQ-A^ov*Qf;E>%eYl(H{6CT%2D>tZ za1+EUM=@RVv<`Pxz#-lQXDf$!3-na}$GaN+>ggQNgOHE&XncC_oACVjhhGf+H`BNf ANdN!< delta 1153 zcmZ`%%}*0S6rbsSwoAJOp?nC&pe7VZLlm$^V?;$UCY3KS*=U-T9k96DZDzJXjS&z2 z0g^rF!Gjm^phqsAjEV8$!6o6)?8&P)jC%24oVO)tLEpZ4dGGz+?9BVkdz=20PQ6Sf z6Nt;<pXgbEM#FDIoQA${Fohc1Ve~?!4@6KkQ~`qo!Y-*Xik&S6aliG zbW%nt@)cWm(ndP+RlC_~F#b}MHG+xz2@Gh*) zO3t8c>I;-+aj1+iHztDk%Obc-yQRog2v&!y%f;bq!6x9pL!ccElMC{?Q^YuInc&#>aJ zV<(~Wn@}(be;_s}w@0CWBX(UAxeLIzU&wzS`&dx8zb*GAFCx7jF6|(%g|!&xg0H&p zUc~}!;{iWf{-Ojfaj-75gZs3{PfFeRuHP?ND?e0}?szJ`nA<#ZJkawS zdVW*S2Wp-|zlCg^Hp7kC+b0%UYMG5nY3)FPTvP*AA3{jca zQfM~pY=m><&OE8WJTR*I59IEZKV5WMOJ;e|&CR=%S*^A#adD~3@ ziZC-2ItmjrJ2O~kB5JsigrDju$F-_9xpkTX1DG$Sa=Rs9jBBVbXw(`S3>vkDP6q$i v8X5^2)pwPy(cQ@!$_0&DLz$paw?uzK8O+N`%-?0Nd3^0FWWWCdir(!v{i7Wv diff --git a/alttextbackend/views/books.py b/alttextbackend/views/books.py index 5bc55d7..2af9958 100644 --- a/alttextbackend/views/books.py +++ b/alttextbackend/views/books.py @@ -1,35 +1,41 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status, permissions, serializers -from rest_framework.exceptions import ValidationError -from rest_framework.parsers import FormParser, MultiPartParser -from django.core.files.storage import default_storage -from django.core.files.base import ContentFile +import sys +import zipfile from uuid import uuid4 +import alttextbackend.data.analyze as analyze +import alttextbackend.data.postgres.books as books +import alttextbackend.data.postgres.images as images +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from rest_framework import serializers, status +from rest_framework.parsers import FormParser, MultiPartParser +from rest_framework.response import Response +from rest_framework.views import APIView + +sys.path.append("../") + class GetBooksSerializer(serializers.Serializer): titleQ = serializers.CharField(required=False) - authorQ = serializers.CharField(required=False) - sortBy = serializers.ChoiceField(choices=['title', 'author'], style={'base_template': 'radio.html'}, default = 'title') - sortOrder = serializers.ChoiceField(choices=['asc', 'desc'], style={'base_template': 'radio.html'}, default = 'asc') limit = serializers.IntegerField(min_value=1, required=False) skip = serializers.IntegerField(min_value=0, required=False) + class AddBookSerializer(serializers.Serializer): + id = serializers.CharField(required=False) title = serializers.CharField(required=True, allow_blank=False) - author = serializers.CharField(required=True, allow_blank=False) - description = serializers.CharField(required=False, allow_blank=True) - file = serializers.FileField(required=True) + book = serializers.FileField(required=True) cover = serializers.ImageField(required=False) + class BooksView(APIView): parser_classes = (FormParser, MultiPartParser) serializer_class = AddBookSerializer + def get_serializer_class(self): - if self.request.method == 'GET': + if self.request.method == "GET": return GetBooksSerializer - elif self.request.method == 'POST': + elif self.request.method == "POST": return AddBookSerializer return super().get_serializer_class() @@ -41,17 +47,14 @@ class BooksView(APIView): # Access validated data validated_data = serializer.validated_data - title_query = validated_data.get('titleQ') - author_query = validated_data.get('authorQ') - sort_by = validated_data.get('sortBy') - sort_order = validated_data.get('sortOrder') - limit = validated_data.get('limit') - skip = validated_data.get('skip') + titleQ = validated_data.get("titleQ", None) + limit = validated_data.get("limit", None) + skip = validated_data.get("skip", None) - # TODO: perform logic + # get array of books + result = books.getBooks(titleQ, limit, skip) - # TODO: return books - return Response(validated_data, status=status.HTTP_200_OK) + return Response(map(books.jsonifyBook, result), status=status.HTTP_200_OK) def post(self, request, *args, **kwargs): # validate request data @@ -61,31 +64,72 @@ class BooksView(APIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data + id = validated_data.get("id", uuid4()) + # check if id is already in use + book = books.getBook(id) + if book: + return Response( + {"error": "id already in use"}, status=status.HTTP_400_BAD_REQUEST + ) + # perform initial book processing - file = validated_data["file"] + file = validated_data["book"] if not file.name.endswith(".zip"): return Response( {"file": ["file must be a zip"]}, status=status.HTTP_400_BAD_REQUEST ) - id = uuid4() - books_path = "./books/" - default_storage.save(f"{books_path}{str(id)}.zip", ContentFile(file.read())) + book_path = f"./books/{str(id)}" + default_storage.save(f"{book_path}.zip", ContentFile(file.read())) + with zipfile.ZipFile(default_storage.path(f"{book_path}.zip"), "r") as zip_ref: + zip_ref.extractall(default_storage.path(f"{book_path}")) + default_storage.delete(f"{book_path}.zip") - # TODO: ensure book has valid root html file - - # TODO: analyze book and images, store them in database + # ensure book has valid root html file + html_file = analyze.findHTML(book_path) + if html_file == None: + default_storage.delete(book_path) + return Response( + {"error": "No HTML file found in the extracted folder"}, + status=status.HTTP_400_BAD_REQUEST, + ) # save cover image - covers_path = "./covers/" - default_storage.save( - f"{covers_path}{str(id)}.{validated_data['cover'].name.split('.')[-1]}", - ContentFile(validated_data["cover"].read()), - ) + coverExt = None + if "cover" in validated_data and validated_data["cover"] is not None: + coverExt = validated_data["cover"].name.split(".")[-1] + default_storage.save( + f"./covers/{str(id)}.{coverExt}", + ContentFile(validated_data["cover"].read()), + ) + alt = analyze.createAnalyzer() + alt.parseFile(html_file) + # store basic book info into database + size = analyze.getSize(book_path) + imgs = alt.getAllImgs() + books.addBook( + title=validated_data["title"], + size=str(size), + numImages=len(imgs), + id=id, + coverExt=coverExt, + ) + # store info for all images in database + for img in imgs: + context = alt.getContext(img) + thisHash = hash(alt.getImgData(img["src"])) + images.addImage( + bookid=id, + src=img["src"], + hash=thisHash, + alt=img["alt"], + originalAlt=img["alt"], + beforeContext=context[0], + afterContext=context[1], + ) + + book = books.getBook(id) return Response( - { - "book": validated_data.get("title"), - "description": validated_data.get("description"), - }, + books.jsonifyBook(book), status=status.HTTP_201_CREATED, ) diff --git a/alttextbackend/views/books_bookid.py b/alttextbackend/views/books_bookid.py index 16ec09a..990d525 100644 --- a/alttextbackend/views/books_bookid.py +++ b/alttextbackend/views/books_bookid.py @@ -1,102 +1,204 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status, permissions, serializers -from rest_framework.exceptions import ValidationError -from rest_framework.parsers import FormParser, MultiPartParser +import copy +import os +import shutil +import threading +import time + +import alttextbackend.data.analyze as analyzer +import alttextbackend.data.postgres.books as books +import alttextbackend.data.postgres.images as images from django.core.files.storage import default_storage +from rest_framework import serializers, status +from rest_framework.parsers import FormParser, MultiPartParser +from rest_framework.response import Response +from rest_framework.views import APIView from django.core.files.base import ContentFile -from uuid import uuid4 + class GetBookSerializer(serializers.Serializer): bookid = serializers.CharField(required=True) + class UpdateBookSerialzer(serializers.Serializer): bookid = serializers.CharField(required=True) title = serializers.CharField(required=False, allow_blank=False) - author = serializers.CharField(required=False, allow_blank=False) - description = serializers.CharField(required=False, allow_blank=True) cover = serializers.ImageField(required=False) + class AnalyzeBookSerializer(serializers.Serializer): bookid = serializers.CharField(required=True) + missingOnly = serializers.BooleanField(required=False, default=True) + waitForAnalysis = serializers.BooleanField(required=False, default=False) -class OverwriteBookSerializer(serializers.Serializer): - bookid = serializers.CharField(required=True) - file = serializers.FileField(required=True) class DeleteBookSerializer(serializers.Serializer): bookid = serializers.CharField(required=True) + class BooksBookidView(APIView): parser_classes = (FormParser, MultiPartParser) serializer_class = UpdateBookSerialzer + def get_serializer_class(self): - if self.request.method == 'GET': + if self.request.method == "GET": return GetBookSerializer - elif self.request.method == 'PATCH': + elif self.request.method == "PATCH": return UpdateBookSerialzer - elif self.request.method == 'PUT': + elif self.request.method == "PUT": return AnalyzeBookSerializer - elif self.request.method == 'DELETE': + elif self.request.method == "DELETE": return DeleteBookSerializer return super().get_serializer_class() def get(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() - serializer = serializer_class(data={"bookid": kwargs.get('bookid')}) + serializer = serializer_class(data={"bookid": kwargs.get("bookid")}) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data - # TODO: IMPLEMENT LOGIC + # get book from database + book = books.getBook(validated_data.get("bookid")) + if not book: + return Response( + {"error": "No book of that id was found in database."}, + status=status.HTTP_404_BAD_REQUEST, + ) - return Response(validated_data, status=status.HTTP_200_OK) + return Response(books.jsonifyBook(book), status=status.HTTP_200_OK) def patch(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() data = request.data - data['bookid'] = kwargs.get('bookid') + data["bookid"] = kwargs.get("bookid") serializer = serializer_class(data=data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data - # TODO: IMPLEMENT LOGIC + # check if book exists in database + book = books.getBook(validated_data.get("bookid")) + if not book: + return Response( + {"error": "No book of that id was found in database."}, + status=status.HTTP_404_BAD_REQUEST, + ) + book = books.jsonifyBook(book) + + # update book title and cover + title = validated_data.get("title", None) + coverExt = None + if "cover" in validated_data and validated_data["cover"] is not None: + coverExt = validated_data["cover"].name.split(".")[-1] + default_storage.delete( + f"./covers/{str(validated_data.get('bookid'))}.{book['coverExt']}" + ) + default_storage.save( + f"./covers/{str(validated_data.get('bookid'))}.{coverExt}", + ContentFile(validated_data["cover"].read()), + ) + + books.updateBook(validated_data.get("bookid"), title=title, coverExt=coverExt) + + book = books.jsonifyBook(books.getBook(validated_data.get("bookid"))) + + return Response(book, status=status.HTTP_200_OK) - return Response(validated_data, status=status.HTTP_200_OK) - def put(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() - serializer = serializer_class(data={"bookid": kwargs.get('bookid')}) + data = copy.deepcopy(request.query_params) + data["bookid"] = kwargs.get("bookid") + serializer = serializer_class(data=data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data - # TODO: IMPLEMENT LOGIC + bookid = validated_data.get("bookid") + # check for book's existence + book = books.getBook(bookid) + if not book: + return Response( + {"error": "Book not found in database."}, + status=status.HTTP_404_BAD_REQUEST, + ) - return Response(validated_data, status=status.HTTP_200_OK) - - def post(self, request, *args, **kwargs): - serializer_class = self.get_serializer_class() - data = request.data - data['bookid'] = kwargs.get('bookid') - serializer = serializer_class(data=request.data) - if not serializer.is_valid(): - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - validated_data = serializer.validated_data + html_file = analyzer.findHTML(f"./books/{str(validated_data.get('bookid'))}") + if html_file == None: + return Response( + {"error": "Failed to find HTML file in book directory."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) - # TODO: IMPLEMENT LOGIC + alt = analyzer.createAnalyzer() + alt.parseFile(html_file) + imgs = [] + if validated_data.get("missingOnly"): + imgs = alt.getNoAltImgs() + else: + imgs = alt.getAllImgs() + + # set book and all images to "processing" status + if validated_data.get("waitForAnalysis"): + analyzer.analyzeImagesV2(alt, imgs, bookid) + else: + threading.Thread( + target=analyzer.analyzeImagesV2, args=(alt, imgs, bookid) + ).start() + + book = books.jsonifyBook(books.getBook(bookid)) + if not validated_data.get("waitForAnalysis"): + book["status"] = "processing" + + return Response(book, status=status.HTTP_200_OK) - return Response(validated_data, status=status.HTTP_200_OK) - def delete(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() - serializer = serializer_class(data={"bookid": kwargs.get('bookid')}) + serializer = serializer_class(data={"bookid": kwargs.get("bookid")}) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data - # TODO: IMPLEMENT LOGIC + # check for book's existence + book = books.getBook(validated_data.get("bookid")) + if not book: + return Response( + {"error": "Book not found in database."}, + status=status.HTTP_404_BAD_REQUEST, + ) + book = books.jsonifyBook(book) + book["status"] = "deleted" - return Response(validated_data, status=status.HTTP_200_OK) + # delete book from table (this cascades to images table as well) + books.deleteBook(validated_data.get("bookid")) + + # delete book directory and cover image + try: + folder_path = f"./books/{str(validated_data.get('bookid'))}" + if default_storage.exists(folder_path): + shutil.rmtree(default_storage.path(folder_path)) + if book["coverExt"]: + try: + default_storage.delete( + f"./covers/{str(validated_data.get('bookid'))}.{book['coverExt']}" + ) + except: + return Response( + {"error": "Failed to delete cover image."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + else: + return Response( + {"error": "Failed to find book directory."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + except Exception: + return Response( + {"error": "Failed to delete book directory."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + return Response( + book, + status=status.HTTP_200_OK, + ) diff --git a/alttextbackend/views/books_bookid_export.py b/alttextbackend/views/books_bookid_export.py index 15b21cc..5744b44 100644 --- a/alttextbackend/views/books_bookid_export.py +++ b/alttextbackend/views/books_bookid_export.py @@ -1,25 +1,102 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status, permissions, serializers -from rest_framework.exceptions import ValidationError -from rest_framework.parsers import FormParser, MultiPartParser +import copy +import os +import shutil +import zipfile + +import alttextbackend.data.analyze as analyze +import alttextbackend.data.postgres.books as books +import alttextbackend.data.postgres.images as images from django.core.files.storage import default_storage -from django.core.files.base import ContentFile -from uuid import uuid4 +from django.http import HttpResponse +from rest_framework import serializers, status +from rest_framework.parsers import FormParser, MultiPartParser +from rest_framework.response import Response +from rest_framework.views import APIView + class ExportBookSerializer(serializers.Serializer): bookid = serializers.CharField(required=True) + name = serializers.CharField(required=False) + class BooksBookidExportView(APIView): parser_classes = (FormParser, MultiPartParser) serializer_class = ExportBookSerializer def get(self, request, *args, **kwargs): - serializer = self.serializer_class(data={"bookid": kwargs.get('bookid')}) + data = copy.deepcopy(request.query_params) + data["bookid"] = kwargs.get("bookid") + serializer = self.serializer_class(data=data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data - # TODO: IMPLEMENT LOGIC + bookid = validated_data.get("bookid") + # check if book exists in database + book = books.getBook(bookid) + if not book: + return Response( + {"error": "Book not found"}, status=status.HTTP_404_NOT_FOUND + ) - return Response(validated_data, status=status.HTTP_200_OK) + # find HTML file + bookid = str(validated_data.get("bookid")) + html_file = analyze.findHTML(f"./books/{bookid}") + if html_file == None: + return Response( + {"error": "Failed to find HTML file in book directory."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + # get all image tags in book + alt = analyze.createAnalyzer() + alt.parseFile(html_file) + imgs = alt.getAllImgs() + for img in imgs: + databaseImg = images.jsonifyImage(images.getImageByBook(bookid, img["src"])) + alt.setAlt(img["src"], databaseImg["alt"]) + + try: + shutil.copytree( + default_storage.path(f"./books/{bookid}"), f"./books/{bookid}-t" + ) + except Exception as e: + return Response( + {"error": "Failed to copy book into temp folder."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + + html_file = analyze.findHTML(f"./books/{bookid}-t") + if html_file == None: + return Response( + {"error": "Failed to find HTML file in temp book directory."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + default_storage.delete(html_file) + alt.exportToFile(html_file) + + # Zip the temp folder + zip_filename = f"./books/{bookid}-t.zip" + with zipfile.ZipFile(zip_filename, "w") as zipf: + for root, _, files in os.walk(f"./books/{bookid}-t"): + for file in files: + zipf.write( + os.path.join(root, file), + os.path.relpath( + os.path.join(root, file), f"./books/{bookid}-t" + ), + ) + + # Send the zip file as a response + filename = validated_data.get("name", f"{bookid}") + print(filename) + response = None + with open(zip_filename, "rb") as f: + response = HttpResponse(f, content_type="application/zip") + response["Content-Disposition"] = f"attachment; filename={filename}.zip" + + # Delete the temp zip and folder + os.remove(zip_filename) + shutil.rmtree(f"./books/{bookid}-t") + + return response diff --git a/alttextbackend/views/books_bookid_image.py b/alttextbackend/views/books_bookid_image.py index 6cdbc05..34365fd 100644 --- a/alttextbackend/views/books_bookid_image.py +++ b/alttextbackend/views/books_bookid_image.py @@ -1,74 +1,158 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status, permissions, serializers -from rest_framework.exceptions import ValidationError +import copy +import threading + +import alttextbackend.data.analyze as analyze +import alttextbackend.data.postgres.books as books +import alttextbackend.data.postgres.images as images +from rest_framework import serializers, status from rest_framework.parsers import FormParser, MultiPartParser -from django.core.files.storage import default_storage -from django.core.files.base import ContentFile -from uuid import uuid4 +from rest_framework.response import Response +from rest_framework.views import APIView + class GetImageBySrc(serializers.Serializer): bookid = serializers.CharField(required=True) src = serializers.CharField(required=True) + class UpdateImageBySrc(serializers.Serializer): bookid = serializers.CharField(required=True) src = serializers.CharField(required=True) alt = serializers.CharField(required=True) beforeContext = serializers.CharField(required=False) afterContext = serializers.CharField(required=False) + additionalContext = serializers.CharField(required=False) + class AnalyzeImageBySrc(serializers.Serializer): bookid = serializers.CharField(required=True) src = serializers.CharField(required=True) + waitForAnalysis = serializers.BooleanField(required=False, default=False) + class BooksBookidImageView(APIView): parser_classes = (FormParser, MultiPartParser) + def get_serializer_class(self): - if self.request.method == 'GET': + if self.request.method == "GET": return GetImageBySrc - elif self.request.method == 'PATCH': + elif self.request.method == "PATCH": return UpdateImageBySrc - elif self.request.method == 'PUT': + elif self.request.method == "PUT": return AnalyzeImageBySrc return super().get_serializer_class() def get(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() - data = request.query_params - data['bookid'] = kwargs.get('bookid') + data = copy.deepcopy(request.query_params) + data["bookid"] = kwargs.get("bookid") serializer = serializer_class(data=data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data - # TODO: IMPLEMENT LOGIC + # check if book exists in database + book = books.getBook(validated_data.get("bookid")) + if not book: + return Response( + {"error": "Book not found"}, status=status.HTTP_404_NOT_FOUND + ) + + # get image from database + img = images.getImageByBook( + validated_data.get("bookid"), validated_data.get("src") + ) + if img == None: + return Response( + {"error": "Image not found"}, status=status.HTTP_404_NOT_FOUND + ) + + return Response( + images.jsonifyImage(img), + status=status.HTTP_200_OK, + ) - return Response(validated_data, status=status.HTTP_200_OK) - def patch(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() - data = request.data + data = copy.deepcopy(request.data) data.update(request.query_params) - data['bookid'] = kwargs.get('bookid') + data["bookid"] = kwargs.get("bookid") serializer = serializer_class(data=data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data - # TODO: IMPLEMENT LOGIC + alt = validated_data.get("alt", None) + beforeContext = validated_data.get("beforeContext", None) + afterContext = validated_data.get("afterContext", None) + additionalContext = validated_data.get("additionalContext", None) + + img = images.getImageByBook( + validated_data.get("bookid"), validated_data.get("src") + ) + if img == None: + return Response( + {"error": "Image not found"}, status=status.HTTP_404_NOT_FOUND + ) + + # update image in database + images.updateImage( + bookid=validated_data.get("bookid"), + src=validated_data.get("src"), + alt=alt, + beforeContext=beforeContext, + afterContext=afterContext, + additionalContext=additionalContext, + ) + + img = images.getImageByBook( + validated_data.get("bookid"), validated_data.get("src") + ) + + return Response(images.jsonifyImage(img), status=status.HTTP_200_OK) - return Response(validated_data, status=status.HTTP_200_OK) - def put(self, request, *args, **kwargs): serializer_class = self.get_serializer_class() - data = request.query_params - data['bookid'] = kwargs.get('bookid') + data = copy.deepcopy(request.query_params) + data["bookid"] = kwargs.get("bookid") serializer = serializer_class(data=data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data - # TODO: IMPLEMENT LOGIC + # find HTML file + bookid = str(validated_data.get("bookid")) + html_file = analyze.findHTML(f"./books/{bookid}") + if html_file == None: + return Response( + {"error": "Failed to find HTML file in book directory."}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) - return Response(validated_data, status=status.HTTP_200_OK) + # generate alt for image + alt = analyze.createAnalyzer() + alt.parseFile(html_file) + img = alt.getImg(validated_data.get("src")) + if img == None: + return Response( + {"error": "Failed to find image in book."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + if validated_data.get("waitForAnalysis"): + analyze.analyzeSingularImageV2(alt, img, bookid) + else: + threading.Thread( + target=analyze.analyzeSingularImageV2, args=(alt, img, bookid) + ).start() + + image = images.jsonifyImage( + images.getImageByBook( + validated_data.get("bookid"), validated_data.get("src") + ) + ) + + if not validated_data.get("waitForAnalysis"): + image["status"] = "processing" + + return Response(image, status=status.HTTP_200_OK) diff --git a/alttextbackend/views/books_bookid_images.py b/alttextbackend/views/books_bookid_images.py index 7036cf0..a0fd432 100644 --- a/alttextbackend/views/books_bookid_images.py +++ b/alttextbackend/views/books_bookid_images.py @@ -1,25 +1,38 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status, permissions, serializers -from rest_framework.exceptions import ValidationError +import sys + +import alttextbackend.data.postgres.books as books +import alttextbackend.data.postgres.images as images +from rest_framework import serializers, status from rest_framework.parsers import FormParser, MultiPartParser -from django.core.files.storage import default_storage -from django.core.files.base import ContentFile -from uuid import uuid4 +from rest_framework.response import Response +from rest_framework.views import APIView + +sys.path.append("../") + class ImagesFromBookSerializer(serializers.Serializer): bookid = serializers.CharField(required=True) + class BooksBookidImagesView(APIView): parser_classes = (FormParser, MultiPartParser) serializer_class = ImagesFromBookSerializer def get(self, request, *args, **kwargs): - serializer = self.serializer_class(data={"bookid": kwargs.get('bookid')}) + serializer = self.serializer_class(data={"bookid": kwargs.get("bookid")}) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) validated_data = serializer.validated_data + id = validated_data.get("bookid") - # TODO: IMPLEMENT LOGIC + # check if book exists in database + book = books.getBook(id) + if not book: + return Response( + {"error": "Book not found"}, status=status.HTTP_404_NOT_FOUND + ) - return Response(validated_data, status=status.HTTP_200_OK) + # get images from database + imgs = images.getImagesByBook(id) + + return Response(map(images.jsonifyImage, imgs), status=status.HTTP_200_OK) diff --git a/alttextbackend/views/images_hash.py b/alttextbackend/views/images_hash.py index 0503f91..04b2fde 100644 --- a/alttextbackend/views/images_hash.py +++ b/alttextbackend/views/images_hash.py @@ -1,26 +1,26 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status, permissions, serializers -from rest_framework.exceptions import ValidationError +from rest_framework import serializers, status from rest_framework.parsers import FormParser, MultiPartParser -from django.core.files.storage import default_storage -from django.core.files.base import ContentFile -from uuid import uuid4 +from rest_framework.response import Response +from rest_framework.views import APIView + +import alttextbackend.data.postgres.images as images + class GetImagesByHashSerializer(serializers.Serializer): hash = serializers.CharField(required=True) + class ImagesHashView(APIView): parser_classes = (FormParser, MultiPartParser) serializer_class = GetImagesByHashSerializer def get(self, request, *args, **kwargs): - image_hash = kwargs.get('hash') - data = {'hash': image_hash} + image_hash = kwargs.get("hash") + data = {"hash": image_hash} serializer = self.serializer_class(data=data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - # TODO: IMPLEMENT LOGIC + imgs = images.getImagesByHash(image_hash) - return Response(data, status=status.HTTP_200_OK) + return Response(map(images.jsonifyImage, imgs), status=status.HTTP_200_OK) diff --git a/openapi.yaml b/openapi.yaml index e0f97b4..743b9fb 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -3,18 +3,12 @@ info: title: Alt-text Backend API description: |- This is the Alt-text Backend API based on the OpenAPI 3.0 specification. - # termsOfService: http://swagger.io/terms/ contact: email: da.cruz@aol.com - # license: - # name: Apache 2.0 - # url: http://www.apache.org/licenses/LICENSE-2.0.html version: 1.0.11 externalDocs: description: Find out more about Alt-text url: https://github.com/EbookFoundation/alt-text -# servers: -# - url: https://petstore3.swagger.io/api/v3 tags: - name: Books description: Everything regarding books @@ -36,31 +30,6 @@ paths: explode: true schema: type: string - - name: authorQ - in: query - description: String to match the author to. - required: false - explode: true - schema: - type: string - - name: sortBy - in: query - description: Field to sort by. - required: false - explode: true - schema: - type: string - enum: ["title", "author"] - default: "title" - - name: sortOrder - in: query - description: Order to sort by. - required: false - explode: true - schema: - type: string - enum: ["asc", "desc"] - default: "asc" - name: limit in: query description: Max number of books to return. @@ -100,15 +69,12 @@ paths: schema: type: object properties: + id: + type: string + description: Id of the book (optional). title: type: string description: Title of the book. - author: - type: string - description: Author of the book. - description: - type: string - description: Description of the book (optional). book: type: string description: Zip file of the book. @@ -167,12 +133,6 @@ paths: title: type: string description: Title of the book (optional). - author: - type: string - description: Author of the book (optional). - description: - type: string - description: Description of the book (optional). cover: type: string description: Cover image for the book (optional). @@ -195,32 +155,25 @@ paths: summary: Re-analyze an entire book. description: Re-analyze an entire book and overwrite current image data by its id. operationId: analyzeBook - responses: - '200': - description: Successful operation - content: - application/json: - schema: - $ref: '#/components/schemas/Book' - '500': - description: Internal Server Error - post: - tags: - - Books - summary: Upload a new book file to a book object. - description: Upload a new book to a given book object (by its id), and re-analyze it (essentially creating a new book, except keeping the same bookid). - operationId: overwriteBook - requestBody: - required: true - content: - multipart/form-data: - schema: - type: object - properties: - book: - type: string - description: Zip file of the book. - format: binary + parameters: + - name: missingOnly + in: query + description: If analyzing on upload, whether to analyze only the images without alt-text. + required: false + explode: true + schema: + type: boolean + example: true + default: true + - name: waitForAnalysis + in: query + description: Whether to wait for the analysis to complete before returning a response. + required: false + explode: true + schema: + type: boolean + example: false + default: false responses: '200': description: Successful operation @@ -255,6 +208,15 @@ paths: schema: type: string example: "123e4567-e89b-12d3-a456-426614174000" + - name: name + in: query + description: Alternative name for file download. + required: false + explode: true + schema: + type: string + default: "{bookid}" + example: "harry_potter" get: tags: - Books @@ -270,12 +232,6 @@ paths: type: string example: |- content of the file - # headers: - # Content-Disposition: - # description: File name to prompt for download - # schema: - # type: string - # example: attachment; filename="example.txt" '500': description: Internal Server Error /books/{bookid}/images: @@ -383,6 +339,9 @@ paths: afterContext: type: string description: New afterContext for the image (optional). + additionalContext: + type: string + description: New additionalContext for the image (optional). responses: '200': description: Successful operation @@ -398,6 +357,16 @@ paths: summary: Re-analyze an image. description: Generate an image's alt-text (written to genAlt field in image object). operationId: analyzeImageBySrc + parameters: + - name: waitForAnalysis + in: query + description: Whether to wait for the analysis to complete before returning a response (default = false). + required: false + explode: true + schema: + type: boolean + example: false + default: false responses: '200': description: Successful operation @@ -445,12 +414,6 @@ components: title: type: string example: "Diary of an Oxygen Thief" - author: - type: string - example: "Anonymous" - description: - type: string - example: "Hurt people hurt people." size: type: string example: "1.16MB" @@ -458,39 +421,53 @@ components: type: string example: "processing" enum: ["available", "processing", "deleted"] + default: "available" numImages: type: integer example: 4 Image: type: object properties: + bookid: + type: string + example: "123e4567-e89b-12d3-a456-426614174000" src: type: string example: "images/cover.png" hash: type: string example: "" - size: + status: type: string - example: "24KB" + example: "processing" + enum: ["available", "processing", "deleted"] + default: "available" alt: type: string example: "" + default: "originalAlt" originalAlt: type: string example: "" genAlt: type: string example: "" + default: "" genImageCaption: type: string example: "" + default: "" ocr: type: string example: "" + default: "" beforeContext: type: string example: "" afterContext: type: string - example: "" \ No newline at end of file + example: "" + additionalContext: + type: string + example: "" + default: "" \ No newline at end of file