├── .env.example ├── .gitignore ├── README.md ├── backend ├── api │ ├── api.py │ ├── dependencies.py │ ├── healthchecker.py │ ├── routers │ │ ├── __init__.py │ │ ├── auth.py │ │ └── documents.py │ └── utils.py ├── dockerfile ├── domain │ ├── auth │ │ └── model.py │ ├── document │ │ └── model.py │ └── exceptions.py ├── infrastructure │ └── postgres │ │ ├── alembic.ini │ │ ├── alembic │ │ ├── README │ │ ├── env.py │ │ ├── script.py.mako │ │ └── versions │ │ │ ├── 2025_01_24-113341de12cd_init.py │ │ │ └── 2025_01_25-fd2bf9ac8821_change_user.py │ │ ├── database.py │ │ ├── models │ │ ├── __init__.py │ │ ├── document.py │ │ └── user.py │ │ └── service │ │ └── auth │ │ └── repo.py ├── requirements.txt └── settings.py ├── docker-compose.yml ├── example_images ├── architecture.png ├── cv_md.gif ├── example_work.gif └── photo_2023-11-18_20-53-46-3.jpg ├── frontend ├── app.py ├── dockerfile ├── healthchecker.py ├── requirements.txt ├── settings.py └── templates │ ├── assets │ ├── animatecss │ │ └── animate.css │ ├── bootstrap │ │ ├── css │ │ │ ├── bootstrap-grid.min.css │ │ │ ├── bootstrap-reboot.min.css │ │ │ └── bootstrap.min.css │ │ └── js │ │ │ └── bootstrap.bundle.min.js │ ├── formoid │ │ └── formoid.min.js │ ├── images │ │ ├── 25231-512x512.png │ │ ├── ds_logo.png │ │ ├── ds_logo_book.png │ │ ├── hashes.json │ │ ├── logo-1.webp │ │ ├── logo.webp │ │ ├── logo5.png │ │ ├── mbr-1-179x179.png │ │ ├── mbr-1-1920x1143.png │ │ ├── mbr-179x179.png │ │ └── mbr-2-179x194.png │ ├── mobirise │ │ └── css │ │ │ └── mbr-additional.css │ ├── project.mobirise │ ├── smoothscroll │ │ └── smooth-scroll.js │ ├── socicon │ │ ├── css │ │ │ └── styles.css │ │ └── fonts │ │ │ ├── socicon.eot │ │ │ ├── socicon.svg │ │ │ ├── socicon.ttf │ │ │ ├── socicon.woff │ │ │ └── socicon.woff2 │ ├── theme │ │ ├── css │ │ │ └── style.css │ │ └── js │ │ │ └── script.js │ ├── web │ │ └── assets │ │ │ └── mobirise-icons │ │ │ ├── mobirise-icons.css │ │ │ ├── mobirise-icons.eot │ │ │ ├── mobirise-icons.svg │ │ │ ├── mobirise-icons.ttf │ │ │ └── mobirise-icons.woff │ └── ytplayer │ │ └── index.js │ ├── dashboard.html │ ├── home.html │ ├── index.html │ ├── login.html │ └── signup.html ├── neural_worker ├── celery_services.py ├── config.py ├── dockerfile ├── infrastructure │ └── postgres │ │ ├── database.py │ │ └── models │ │ ├── __init__.py │ │ └── document.py ├── requirements.txt ├── settings.py └── utils.py ├── rabbit_conf └── rabbitmq.conf └── streamlit ├── config.py ├── dockerfile ├── requirements.txt ├── static └── lottie_5.json └── ui.py /.env.example: -------------------------------------------------------------------------------- 1 | # Neural worker 2 | NW_CONCURRENCY_COUNT=2 3 | NW_OPENAI_KEY="" 4 | NW_BASE_OPENAI_URL="" 5 | NW_MODEL_NAME=gpt-4o 6 | 7 | 8 | # Postgres config 9 | POSTGRES_USER=sus 10 | POSTGRES_PASSWORD=sus 11 | POSTGRES_HOST=postgres 12 | POSTGRES_PORT=5432 13 | POSTGRES_DB=sus 14 | 15 | # Redis config 16 | REDIS_HOST=redis 17 | REDIS_PORT=6379 18 | REDIS_BACKEND_DB = 0 19 | REDIS_CELERY_DB = 1 20 | 21 | # RabbitMQ 22 | RABBITMQ_HOST=rabbitmq 23 | RABBITMQ_AMQP_PORT=5672 24 | RABBITMQ_CONSOLE_PORT=15672 25 | RABBITMQ_ERLANG_COOKIE=erlang 26 | RABBITMQ_LOGIN=admin 27 | RABBITMQ_PASSWORD=admin 28 | 29 | # MinIO config 30 | MINIO_HOST=minio 31 | MINIO_BUCKET=documents 32 | MINIO_API_PORT=9000 33 | MINIO_CONSOLE_PORT=9090 34 | MINIO_ROOT_USER=admin 35 | MINIO_ROOT_PASSWORD=admin_password 36 | 37 | # Backend App config 38 | API_HOST=backend 39 | API_PORT=8080 40 | API_JWT_SECRET= 41 | API_JWT_ALGORITHM=HS256 42 | API_JWT_EXPIRES_H=48 43 | API_HC_TIMEOUT=30 44 | API_HC_SLEEP=5 45 | API_COOKIE_NAME=ds_auth 46 | 47 | # Frontend App config 48 | FRONT_PORT=5020 49 | 50 | # Flower config 51 | FLOWER_PORT=5555 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | .vscode 4 | 5 | __pycache__ 6 | 7 | venv/ 8 | venv_demo/ 9 | test/ 10 | tests/ 11 | training_data/ 12 | trained_weights/ 13 | training_pipeline/ 14 | data_utils/ 15 | rabbitmq-data/ 16 | pgdata/ 17 | minio_config/ 18 | minio_data/ 19 | 20 | .env 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
6 | 7 | 8 | 9 |

DeepScriptum

10 | 11 |

12 | Convert any file into it's MarkDown source 13 |
14 |

15 |
16 | 17 | 18 |
19 | Table of Contents 20 |
    21 |
  1. 22 | Current state and updates 23 |
  2. 24 |
  3. 25 | About The Project 26 | 29 |
  4. 30 |
  5. 31 | Getting Started 32 | 35 |
  6. 36 |
  7. Acknowledgments
  8. 37 |
38 |
39 | 40 | 41 | ## Current state and updates 42 | 43 | This branch contains demo DeepScriptum implementation and map-reduce approach to perfom OCR in parallel. 44 | Service provides fast transformation of any file to .md 45 | The program allows you to quickly transform files 46 | Easy to use interface 47 | 48 | ### December 2024: 49 | Implementation with GPT-4o. Includes FastAPI backend (Celery used for task managing), PostgreSQL and HTML/CSS frontend. 50 | 51 | 52 | 53 | ### October 2024: 54 | Deep dive into OCR, understanding how it work and how to use it for the project. 55 | 56 | 57 | 58 | ## About The Project 59 | 60 | DeepScriptum is an advanced project designed to seamlessly convert any type of document into Markdown (.md) format. Utilizing cutting-edge AI technology, it ensures accurate and efficient translation while preserving the original structure and content. Ideal for developers, writers, and businesses, DeepScriptum streamlines documentation processes and enhances collaboration. 61 | 62 |

(back to top)

63 | 64 | 65 | 66 | ## Architecture 67 | 68 | ![alt text](example_images/architecture.png) 69 | 70 | ### Installation 71 | 72 | 1) Git clone or download 73 | 2) Create .env file in root due to `.env.example` 74 | 3) `sudo docker-compose build && sudo docker-compose up` 75 | 76 | Congrats! You can access your service on `localhost` 77 | 78 |

(back to top)

79 | 80 | ### Built With 81 | 82 | * [PostgreSQL](https://www.postgresql.org/) 83 | * [RabbitMQ](https://www.rabbitmq.com/) 84 | * [FastAPI](https://fastapi.tiangolo.com/) 85 | * [Celery](https://github.com/celery/celery) 86 | * [OpenAI GPT](https://openai.com/) 87 | 88 |

(back to top)

89 | 90 | 91 | 92 | ## Authors 93 | 94 | - Vyaznikov Pavel 95 | - Busko Nikita 96 | - Rychkov Pavel 97 | - Orlova Angelina 98 | 99 |

(back to top)

100 | -------------------------------------------------------------------------------- /backend/api/api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from contextlib import asynccontextmanager 3 | from time import sleep 4 | 5 | import aioboto3 6 | from fastapi import FastAPI, Request 7 | from fastapi.responses import JSONResponse 8 | from starlette.middleware.cors import CORSMiddleware 9 | 10 | import settings 11 | import api.healthchecker as hc 12 | from infrastructure.postgres import database 13 | from .routers import auth_router, doc_router 14 | 15 | 16 | @asynccontextmanager 17 | async def init_tables(app: FastAPI): 18 | urls_to_check = [f"{settings.minio_settings.URI}/minio/health/live"] 19 | 20 | hc.Readiness(urls=urls_to_check, logger=app.state.Logger).run() 21 | 22 | # Init DB tables if not exist 23 | for _ in range(5): 24 | try: 25 | async with database.engine.begin() as conn: 26 | await conn.run_sync(database.Base.metadata.create_all) 27 | except: 28 | print("DB is not ready yet") 29 | sleep(settings.app_settings.HC_SLEEP) 30 | 31 | # create bucket if not exist 32 | async with aioboto3.Session().client( 33 | "s3", 34 | endpoint_url=settings.minio_settings.URI, 35 | aws_access_key_id=settings.minio_settings.ROOT_USER, 36 | aws_secret_access_key=settings.minio_settings.ROOT_PASSWORD, 37 | ) as s3_client: 38 | try: 39 | await s3_client.head_bucket(Bucket=settings.minio_settings.BUCKET) 40 | except Exception: 41 | await s3_client.create_bucket(Bucket=settings.minio_settings.BUCKET) 42 | 43 | yield 44 | 45 | 46 | app = FastAPI( 47 | docs_url="/api/docs", 48 | openapi_url="/api/openapi.json", 49 | lifespan=init_tables, 50 | ) 51 | 52 | app.state.Logger = logging.getLogger(name="deepscriptum_backend") 53 | app.state.Logger.setLevel("DEBUG") 54 | 55 | app.include_router(auth_router, prefix="/api") 56 | app.include_router(doc_router, prefix="/api") 57 | 58 | 59 | @app.get("/") 60 | async def root(): 61 | return {"info": "DeepScriptum API. See /docs for documentation"} 62 | 63 | 64 | @app.middleware("http") 65 | async def db_session_middleware(request: Request, call_next): 66 | request.state.db = database.async_session_factory() 67 | async with aioboto3.Session().client( 68 | "s3", 69 | endpoint_url=settings.minio_settings.URI, 70 | aws_access_key_id=settings.minio_settings.ROOT_USER, 71 | aws_secret_access_key=settings.minio_settings.ROOT_PASSWORD, 72 | ) as s3_client: 73 | request.state.s3 = s3_client 74 | try: 75 | response = await call_next(request) 76 | except Exception as exc: 77 | print(exc, flush=True) 78 | detail = getattr(exc, "detail", None) 79 | unexpected_error = not detail 80 | if unexpected_error: 81 | args = getattr(exc, "args", None) 82 | detail = args[0] if args else str(exc) 83 | status_code = getattr(exc, "status_code", 500) 84 | response = JSONResponse( 85 | content={"detail": str(detail), "success": False}, 86 | status_code=status_code, 87 | ) 88 | finally: 89 | await request.state.db.close() 90 | 91 | return response 92 | 93 | 94 | origins = [ 95 | "http://frontend", 96 | f"http://frontend:{settings.front_settings.PORT}", 97 | "http://localhost", 98 | f"http://localhost:{settings.front_settings.PORT}", 99 | f"http://127.0.0.1:{settings.front_settings.PORT}", 100 | f"http://0.0.0.0:{settings.front_settings.PORT}", 101 | ] 102 | 103 | app.add_middleware( 104 | CORSMiddleware, 105 | allow_origins=origins, 106 | allow_methods=["*"], 107 | allow_headers=["*"], 108 | allow_credentials=True, 109 | ) 110 | -------------------------------------------------------------------------------- /backend/api/dependencies.py: -------------------------------------------------------------------------------- 1 | from fastapi import Depends, Request, HTTPException 2 | from fastapi.security import OAuth2PasswordBearer 3 | from jose import JWTError 4 | from pydantic import ValidationError 5 | from sqlalchemy.ext.asyncio import AsyncSession 6 | 7 | from infrastructure.postgres.service.auth.repo import UserAuthRepository 8 | from domain.exceptions import InvalidTokenError 9 | 10 | 11 | oauth2_scheme = OAuth2PasswordBearer(tokenUrl="fake_url") 12 | 13 | 14 | def get_db(request: Request): 15 | return request.state.db 16 | 17 | 18 | def get_auth_repository( 19 | session: AsyncSession = Depends(get_db), 20 | ): 21 | return UserAuthRepository(session) 22 | 23 | 24 | def get_current_user(token: str = Depends(oauth2_scheme)) -> str: 25 | try: 26 | # print(f"Received token for verification: {token}") 27 | user_id = UserAuthRepository.verify_token(token) 28 | if not user_id: 29 | # print("Token verification returned no user_id.") 30 | raise InvalidTokenError(detail="Invalid token or user not found") 31 | # print(f"Token verified. User ID: {user_id}") 32 | return user_id 33 | except InvalidTokenError as e: 34 | print(f"InvalidTokenError: {e.detail}") 35 | raise HTTPException( 36 | status_code=401, 37 | detail=e.detail or "Could not validate credentials", 38 | headers={"WWW-Authenticate": "Bearer"}, 39 | ) 40 | except ( 41 | JWTError, 42 | ValidationError, 43 | ) as e_jwt_val: 44 | print(f"JWTError/ValidationError: {str(e_jwt_val)}") 45 | raise HTTPException( 46 | status_code=401, 47 | detail="Could not validate credentials (JWT/Validation Error)", 48 | headers={"WWW-Authenticate": "Bearer"}, 49 | ) 50 | except Exception as e_generic: 51 | print(f"Generic exception during token verification: {str(e_generic)}") 52 | raise HTTPException( 53 | status_code=500, 54 | detail="An unexpected error occurred during token verification.", 55 | headers={"WWW-Authenticate": "Bearer"}, 56 | ) 57 | -------------------------------------------------------------------------------- /backend/api/healthchecker.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, Any 2 | from time import sleep 3 | import requests 4 | 5 | from settings import app_settings 6 | 7 | 8 | class Readiness: 9 | """Class that handles /readiness endpoint.""" 10 | 11 | urls: Optional[List[str]] = None 12 | logger: Any = None 13 | 14 | def __init__( 15 | self, 16 | urls: List[str], 17 | logger: Any, 18 | ) -> None: 19 | """ 20 | :param urls: list of service urls to check. 21 | :param task: list of futures or coroutines 22 | :param logger: Logger object. 23 | :param client: HTTPClient object. 24 | """ 25 | 26 | Readiness.urls = urls or [] 27 | Readiness.logger = logger 28 | 29 | Readiness.status = False 30 | 31 | @classmethod 32 | def _make_request(cls, url: str) -> None: 33 | """Check readiness of the specified service.""" 34 | 35 | while True: 36 | cls.logger.info( 37 | f"Trying to connect to '{url}'", 38 | ) 39 | try: 40 | response = requests.get(url=f"{url}", timeout=app_settings.HC_TIMEOUT) 41 | if response.status_code: 42 | cls.logger.info( 43 | f"Successfully connected to '{url}'", 44 | ) 45 | break 46 | 47 | cls.logger.warning( 48 | f"Failed to connect to '{url}'", 49 | ) 50 | except Exception as e: 51 | cls.logger.warning( 52 | f"Failed to connect to '{url}': {str(e)}", 53 | ) 54 | 55 | sleep(app_settings.HC_SLEEP) 56 | 57 | @classmethod 58 | def _check_readiness(cls) -> None: 59 | """Check readiness of all services.""" 60 | 61 | cls.logger.info( 62 | f"Running readiness checks.", 63 | ) 64 | [cls._make_request(url) for url in cls.urls or []] 65 | 66 | cls.logger.info( 67 | f"Successfully finished readiness checks.", 68 | ) 69 | 70 | @classmethod 71 | def run(cls) -> None: 72 | cls._check_readiness() 73 | -------------------------------------------------------------------------------- /backend/api/routers/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth import router as auth_router 2 | from .documents import router as doc_router -------------------------------------------------------------------------------- /backend/api/routers/auth.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Depends, Response 2 | from fastapi.responses import JSONResponse 3 | 4 | from api.dependencies import get_auth_repository 5 | from domain.auth.model import Token, Token, UserAuth 6 | from infrastructure.postgres.service.auth.repo import UserAuthRepository 7 | from settings import app_settings 8 | 9 | 10 | router = APIRouter(prefix="/auth", tags=["Authorization"]) 11 | 12 | 13 | @router.post("/login", response_model=Token) 14 | async def login( 15 | auth_form: UserAuth, 16 | user_auth_service: UserAuthRepository = Depends(get_auth_repository), 17 | ): 18 | token = await user_auth_service.authenticate_user(auth_form) 19 | return {"access_token": token.access_token, "token_type": "bearer"} 20 | 21 | 22 | @router.post("/logout") 23 | async def logout(): 24 | return {"success": True} 25 | 26 | 27 | @router.post("/register", response_model=Token) 28 | async def register( 29 | user_data: UserAuth, 30 | user_auth_service: UserAuthRepository = Depends(get_auth_repository), 31 | ): 32 | token = await user_auth_service.register_new_user(user_data) 33 | return {"access_token": token.access_token, "token_type": "bearer"} 34 | -------------------------------------------------------------------------------- /backend/api/routers/documents.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | from datetime import datetime 3 | 4 | from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Request 5 | from fastapi.responses import JSONResponse 6 | from celery import Celery 7 | from celery.result import AsyncResult 8 | import sqlalchemy as sa 9 | 10 | from domain.auth.model import ( 11 | BaseUser, 12 | RoleEnum, 13 | ShareRequest, 14 | ShareResponse, 15 | DocumentUpdateRequest, 16 | ) 17 | from domain.exceptions import BaseAPIException 18 | from infrastructure.postgres.models.document import DocumentDAO 19 | from infrastructure.postgres.models.user import DocumentUserDAO, UserDAO 20 | from api.dependencies import get_current_user 21 | import settings 22 | from api.utils import process_file 23 | 24 | 25 | router = APIRouter(prefix="/documents", tags=["Documents"]) 26 | 27 | celery = Celery(__name__) 28 | celery.conf.broker_url = settings.rabbitmq_settings.URI 29 | 30 | 31 | @router.get("/") 32 | async def get_all_docs( 33 | request: Request, 34 | user: str = Depends(get_current_user), 35 | ): 36 | _, pg_session = request.state.s3, request.state.db 37 | user_docs = {} 38 | 39 | q = ( 40 | sa.select(DocumentUserDAO) 41 | .where(DocumentUserDAO.user_id == user) 42 | .order_by(DocumentUserDAO.last_access_at.asc()) 43 | ) 44 | q = await pg_session.execute(q) 45 | rows = q.scalars().all() 46 | 47 | if not rows: 48 | return user_docs 49 | 50 | for row in rows: 51 | q = sa.select(DocumentDAO).where(DocumentDAO.id == row.document_id) 52 | q = await pg_session.execute(q) 53 | res = q.fetchone() 54 | 55 | user_docs.update( 56 | { 57 | str(row.document_id): { 58 | "name": res[0].name, 59 | "s3_md_id": str(res[0].s3_md_id), 60 | } 61 | } 62 | ) 63 | 64 | return JSONResponse(content=user_docs) 65 | 66 | 67 | @router.post("/ocr") 68 | async def create_ocr_task( 69 | request: Request, 70 | document: UploadFile = File(), 71 | user: str = Depends(get_current_user), 72 | ): 73 | s3_session, pg_session = request.state.s3, request.state.db 74 | 75 | doc_binary = document.file.read() 76 | document.file.seek(0) 77 | 78 | _ = process_file(doc_binary) # check if file is pdf 79 | doc_s3_uuid = str(uuid4()) 80 | 81 | try: 82 | await s3_session.upload_fileobj( 83 | document.file, 84 | settings.minio_settings.BUCKET, 85 | doc_s3_uuid, 86 | ExtraArgs={ 87 | "Metadata": { 88 | "ext": document.filename.split(".")[-1], 89 | }, 90 | }, 91 | ) 92 | except: 93 | raise BaseAPIException(status_code=500, detail="S3 error") 94 | 95 | pg_raw_document = DocumentDAO( 96 | id=doc_s3_uuid, 97 | name=".".join(document.filename.split(".")[:-1]), 98 | s3_raw_id=doc_s3_uuid, 99 | ) 100 | pg_session.add(pg_raw_document) 101 | await pg_session.commit() 102 | 103 | pg_doc_user = DocumentUserDAO( 104 | user_id=user, document_id=doc_s3_uuid, role=RoleEnum.owner 105 | ) 106 | pg_session.add(pg_doc_user) 107 | await pg_session.commit() 108 | 109 | celery.send_task("images", task_id=doc_s3_uuid) 110 | 111 | return JSONResponse( 112 | status_code=201, 113 | content={ 114 | "msg": "The task has been created successfully", 115 | "doc_id": doc_s3_uuid, 116 | }, 117 | ) 118 | 119 | 120 | @router.get("/{document_id}/status") 121 | async def check_doc_status( 122 | request: Request, 123 | document_id: str, 124 | user: BaseUser = Depends(get_current_user), 125 | ): 126 | _, pg_session = request.state.s3, request.state.db 127 | 128 | q = sa.select(DocumentDAO).where(DocumentDAO.id == document_id) 129 | q = await pg_session.execute(q) 130 | res = q.fetchone()[0] 131 | 132 | if res.s3_md_id: 133 | return {"s3_md_id": res.s3_md_id} 134 | 135 | return {"s3_md_id": "0"} 136 | 137 | 138 | @router.get("/{document_db_id}/content") 139 | async def get_document_content_by_db_id( 140 | request: Request, 141 | document_db_id: str, 142 | user_id: str = Depends(get_current_user), 143 | ): 144 | s3_session, pg_session = request.state.s3, request.state.db 145 | 146 | access_stmt = sa.select(DocumentUserDAO).where( 147 | DocumentUserDAO.user_id == user_id, 148 | DocumentUserDAO.document_id == document_db_id, 149 | ) 150 | access_result = await pg_session.execute(access_stmt) 151 | if not access_result.scalar_one_or_none(): 152 | raise HTTPException(status_code=403, detail="Access denied to this document") 153 | 154 | doc_stmt = sa.select(DocumentDAO.s3_md_id, DocumentDAO.name).where( 155 | DocumentDAO.id == document_db_id 156 | ) 157 | doc_info_result = await pg_session.execute(doc_stmt) 158 | doc_info = doc_info_result.one_or_none() 159 | 160 | if not doc_info or not doc_info.s3_md_id: 161 | raise HTTPException( 162 | status_code=404, detail="Document or its MD content not found" 163 | ) 164 | 165 | s3_md_id_to_fetch = doc_info.s3_md_id 166 | document_name = doc_info.name 167 | 168 | try: 169 | response_s3 = await s3_session.get_object( 170 | Bucket=settings.minio_settings.BUCKET, Key=s3_md_id_to_fetch 171 | ) 172 | async with response_s3["Body"] as stream: 173 | content = await stream.read() 174 | 175 | return { 176 | "content": content.decode("utf-8"), 177 | "name": document_name, 178 | "id": document_db_id, 179 | "s3_md_id": s3_md_id_to_fetch, 180 | } 181 | except Exception as e: 182 | request.state.Logger.error(f"S3 error fetching {s3_md_id_to_fetch}: {e}") 183 | raise HTTPException( 184 | status_code=500, detail="Could not fetch document content from storage" 185 | ) 186 | 187 | 188 | @router.put("/{s3_md_id}") 189 | async def update_document_content( 190 | request: Request, 191 | s3_md_id: str, 192 | update_data: DocumentUpdateRequest, 193 | user_id: str = Depends(get_current_user), 194 | ): 195 | s3_session, pg_session = request.state.s3, request.state.db 196 | 197 | s3_key_to_update = None 198 | db_doc_id_for_check = None 199 | 200 | doc_check_stmt = sa.select(DocumentDAO.id).where( 201 | DocumentDAO.s3_md_id == s3_md_id 202 | ) 203 | doc_check_res = await pg_session.execute(doc_check_stmt) 204 | db_doc_id_for_check = doc_check_res.scalar_one_or_none() 205 | if not db_doc_id_for_check: 206 | raise HTTPException( 207 | status_code=404, 208 | detail="Document not found by S3 MD ID", 209 | ) 210 | s3_key_to_update = s3_md_id 211 | 212 | permission_stmt = sa.select(DocumentUserDAO).where( 213 | DocumentUserDAO.document_id == db_doc_id_for_check, 214 | DocumentUserDAO.user_id == user_id, 215 | DocumentUserDAO.role.in_([RoleEnum.owner, RoleEnum.editor]), 216 | ) 217 | permission = (await pg_session.execute(permission_stmt)).scalar_one_or_none() 218 | if not permission: 219 | raise HTTPException( 220 | status_code=403, 221 | detail="You don't have permission to edit this document", 222 | ) 223 | 224 | try: 225 | content_bytes = update_data.content.encode("utf-8") 226 | 227 | await s3_session.put_object( 228 | Bucket=settings.minio_settings.BUCKET, 229 | Key=s3_key_to_update, 230 | Body=content_bytes, 231 | ContentType="text/markdown; charset=utf-8", 232 | ) 233 | 234 | stmt_update_access = ( 235 | sa.update(DocumentUserDAO) 236 | .where( 237 | DocumentUserDAO.document_id == db_doc_id_for_check, 238 | DocumentUserDAO.user_id == user_id, 239 | ) 240 | .values(last_access_at=datetime.utcnow()) 241 | ) 242 | await pg_session.execute(stmt_update_access) 243 | await pg_session.commit() 244 | 245 | return {"message": "Document updated successfully"} 246 | 247 | except Exception as e: 248 | request.state.Logger.error( 249 | f"Error updating document {s3_key_to_update} in S3: {e}" 250 | ) 251 | raise HTTPException( 252 | status_code=500, 253 | detail="Could not save document content", 254 | ) 255 | 256 | 257 | @router.get("/{document_id}") 258 | async def get_document( 259 | request: Request, 260 | document_id: str, 261 | user: BaseUser = Depends(get_current_user), 262 | ): 263 | s3_session, _ = request.state.s3, request.state.db 264 | 265 | response = await s3_session.get_object( 266 | Bucket=settings.minio_settings.BUCKET, Key=document_id 267 | ) 268 | async with response["Body"] as stream: 269 | content = await stream.read() 270 | 271 | return {"content": content.decode("utf-8")} 272 | 273 | 274 | @router.post("/{document_id}/share") 275 | async def share_document_with_user( 276 | request: Request, 277 | document_id: str, 278 | share_data: ShareRequest, 279 | current_user_id: str = Depends(get_current_user), 280 | ): 281 | pg_session = request.state.db 282 | 283 | doc_stmt = sa.select(DocumentDAO).where(DocumentDAO.id == document_id) 284 | doc_result = await pg_session.execute(doc_stmt) 285 | document = doc_result.scalar_one_or_none() 286 | if not document: 287 | raise HTTPException(status_code=404, detail="Document not found") 288 | 289 | permission_stmt = sa.select(DocumentUserDAO).where( 290 | DocumentUserDAO.document_id == document_id, 291 | DocumentUserDAO.user_id == current_user_id, 292 | ) 293 | permission_result = await pg_session.execute(permission_stmt) 294 | user_permission = permission_result.scalar_one_or_none() 295 | 296 | if not user_permission or user_permission.role != RoleEnum.owner: 297 | raise HTTPException( 298 | status_code=403, detail="You do not have permission to share this document" 299 | ) 300 | 301 | if share_data.role == RoleEnum.owner: 302 | raise HTTPException( 303 | status_code=400, 304 | detail="Cannot assign 'owner' role. Ownership is established at creation.", 305 | ) 306 | 307 | target_user_stmt = sa.select(UserDAO).where(UserDAO.email == share_data.email) 308 | target_user_result = await pg_session.execute(target_user_stmt) 309 | target_user = target_user_result.scalar_one_or_none() 310 | 311 | if not target_user: 312 | raise HTTPException( 313 | status_code=404, detail=f"User with email '{share_data.email}' not found" 314 | ) 315 | 316 | if str(target_user.id) == current_user_id: 317 | raise HTTPException( 318 | status_code=400, 319 | detail="Cannot change your own role as an owner via this sharing method.", 320 | ) 321 | 322 | existing_share_stmt = sa.select(DocumentUserDAO).where( 323 | DocumentUserDAO.document_id == document_id, 324 | DocumentUserDAO.user_id == target_user.id, 325 | ) 326 | existing_share_result = await pg_session.execute(existing_share_stmt) 327 | existing_share_entry = existing_share_result.scalar_one_or_none() 328 | 329 | if existing_share_entry: 330 | if existing_share_entry.role == share_data.role: 331 | return ShareResponse( 332 | message=f"User {share_data.email} already has '{share_data.role.value}' access to this document.", 333 | user_email=share_data.email, 334 | role_assigned=share_data.role, 335 | ) 336 | existing_share_entry.role = share_data.role 337 | existing_share_entry.last_access_at = datetime.utcnow() 338 | message = f"Access role for user {share_data.email} updated to '{share_data.role.value}'." 339 | else: 340 | new_share_entry = DocumentUserDAO( 341 | user_id=target_user.id, 342 | document_id=document_id, 343 | role=share_data.role, 344 | last_access_at=datetime.utcnow(), 345 | ) 346 | pg_session.add(new_share_entry) 347 | message = f"Document shared with user {share_data.email} as '{share_data.role.value}'." 348 | 349 | await pg_session.commit() 350 | 351 | return ShareResponse( 352 | message=message, user_email=share_data.email, role_assigned=share_data.role 353 | ) 354 | -------------------------------------------------------------------------------- /backend/api/utils.py: -------------------------------------------------------------------------------- 1 | import magic 2 | 3 | from domain.exceptions import BaseAPIException 4 | 5 | 6 | def process_file(byte_data): 7 | file_type = detect_file_type(byte_data) 8 | if file_type == "unknown" or file_type != "pdf": 9 | raise BaseAPIException(status_code=400, detail="Unsupported format") 10 | 11 | return file_type 12 | 13 | 14 | def detect_file_type(byte_data): 15 | mime = magic.Magic(mime=True) 16 | mime_types = { 17 | "application/pdf": "pdf", 18 | } 19 | return mime_types.get(mime.from_buffer(byte_data), "unknown") 20 | -------------------------------------------------------------------------------- /backend/dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | 3 | RUN apt-get update && apt-get install poppler-utils -y 4 | 5 | WORKDIR /backend 6 | 7 | COPY requirements.txt /backend/ 8 | RUN pip install -r requirements.txt 9 | COPY . /backend/ -------------------------------------------------------------------------------- /backend/domain/auth/model.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from datetime import datetime 3 | 4 | from pydantic import BaseModel, Field, EmailStr 5 | 6 | 7 | class RoleEnum(enum.Enum): 8 | owner = "owner" 9 | viewer = "viewer" 10 | editor = "editor" 11 | 12 | 13 | class ShareRequest(BaseModel): 14 | email: EmailStr 15 | role: RoleEnum 16 | 17 | 18 | class DocumentUpdateRequest(BaseModel): 19 | content: str 20 | 21 | 22 | class ShareResponse(BaseModel): 23 | message: str 24 | user_email: EmailStr 25 | role_assigned: RoleEnum 26 | 27 | 28 | class BaseUser(BaseModel): 29 | email: str 30 | 31 | class Config: 32 | from_attributes = True 33 | 34 | 35 | class UserAuth(BaseUser): 36 | password: str = Field(min_length=4) 37 | 38 | 39 | class JWTPayload(BaseModel): 40 | user: BaseUser 41 | sub: str 42 | iat: datetime 43 | exp: datetime 44 | 45 | 46 | class Token(BaseModel): 47 | access_token: str 48 | -------------------------------------------------------------------------------- /backend/domain/document/model.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class ShareRoleEnum(enum.Enum): 5 | private = "закрытый доступ" 6 | viewer = "только просмотр" 7 | edit = "редактирование" 8 | -------------------------------------------------------------------------------- /backend/domain/exceptions.py: -------------------------------------------------------------------------------- 1 | from http import HTTPStatus 2 | from typing import Optional 3 | 4 | 5 | class BaseAPIException(Exception): 6 | def __init__(self, *, status_code: HTTPStatus, detail: str): 7 | self.detail = detail 8 | self.status_code = status_code 9 | 10 | 11 | class EntityNotFoundError(BaseAPIException): 12 | def __str__(self): 13 | return self.detail 14 | 15 | def __init__(self, *, detail: Optional[str] = None): 16 | super().__init__(detail=detail or "no data", status_code=HTTPStatus.NOT_FOUND) 17 | 18 | 19 | class EntityAlreadyExistsError(BaseAPIException): 20 | def __str__(self): 21 | return self.detail 22 | 23 | def __init__(self, *, detail: str): 24 | super().__init__(detail=detail, status_code=HTTPStatus.CONFLICT) 25 | 26 | 27 | class BaseAuthError(BaseAPIException): 28 | def __init__(self, *, detail: str): 29 | super().__init__(detail=detail, status_code=HTTPStatus.FORBIDDEN) 30 | 31 | 32 | class InvalidTokenError(BaseAuthError): 33 | pass 34 | 35 | 36 | class InvalidCredentialsError(BaseAuthError): 37 | pass 38 | -------------------------------------------------------------------------------- /backend/infrastructure/postgres/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts. 5 | # Use forward slashes (/) also on windows to provide an os agnostic path 6 | script_location = infrastructure/postgres/alembic 7 | 8 | file_template = %%(year)d_%%(month).2d_%%(day).2d-%%(rev)s_%%(slug)s 9 | 10 | prepend_sys_path = . 11 | 12 | version_path_separator = os 13 | 14 | # sqlalchemy.url = driver://user:pass@localhost/dbname 15 | 16 | [loggers] 17 | keys = root,sqlalchemy,alembic 18 | 19 | [handlers] 20 | keys = console 21 | 22 | [formatters] 23 | keys = generic 24 | 25 | [logger_root] 26 | level = WARNING 27 | handlers = console 28 | qualname = 29 | 30 | [logger_sqlalchemy] 31 | level = WARNING 32 | handlers = 33 | qualname = sqlalchemy.engine 34 | 35 | [logger_alembic] 36 | level = INFO 37 | handlers = 38 | qualname = alembic 39 | 40 | [handler_console] 41 | class = StreamHandler 42 | args = (sys.stderr,) 43 | level = NOTSET 44 | formatter = generic 45 | 46 | [formatter_generic] 47 | format = %(levelname)-5.5s [%(name)s] %(message)s 48 | datefmt = %H:%M:%S 49 | -------------------------------------------------------------------------------- /backend/infrastructure/postgres/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration with an async dbapi. -------------------------------------------------------------------------------- /backend/infrastructure/postgres/alembic/env.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from logging.config import fileConfig 3 | 4 | from alembic import context 5 | from sqlalchemy import pool 6 | from sqlalchemy.engine import Connection 7 | from sqlalchemy.ext.asyncio import async_engine_from_config 8 | 9 | from settings import postgres_settings 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | if config.config_file_name is not None: 18 | fileConfig(config.config_file_name) 19 | 20 | config.set_main_option("sqlalchemy.url", postgres_settings.URI) 21 | # add your model's MetaData object here 22 | # for 'autogenerate' support 23 | # from myapp import mymodel 24 | # target_metadata = mymodel.Base.metadata 25 | from infrastructure.postgres.database import Base 26 | from infrastructure.postgres.models import * 27 | 28 | target_metadata = Base.metadata 29 | 30 | # other values from the config, defined by the needs of env.py, 31 | # can be acquired: 32 | # my_important_option = config.get_main_option("my_important_option") 33 | # ... etc. 34 | 35 | 36 | def run_migrations_offline() -> None: 37 | """Run migrations in 'offline' mode. 38 | 39 | This configures the context with just a URL 40 | and not an Engine, though an Engine is acceptable 41 | here as well. By skipping the Engine creation 42 | we don't even need a DBAPI to be available. 43 | 44 | Calls to context.execute() here emit the given string to the 45 | script output. 46 | 47 | """ 48 | url = config.get_main_option("sqlalchemy.url") 49 | context.configure( 50 | url=url, 51 | target_metadata=target_metadata, 52 | literal_binds=True, 53 | dialect_opts={"paramstyle": "named"}, 54 | ) 55 | 56 | with context.begin_transaction(): 57 | context.run_migrations() 58 | 59 | 60 | def do_run_migrations(connection: Connection) -> None: 61 | context.configure(connection=connection, target_metadata=target_metadata) 62 | 63 | with context.begin_transaction(): 64 | context.run_migrations() 65 | 66 | 67 | async def run_async_migrations() -> None: 68 | """In this scenario we need to create an Engine 69 | and associate a connection with the context. 70 | 71 | """ 72 | 73 | connectable = async_engine_from_config( 74 | config.get_section(config.config_ini_section, {}), 75 | prefix="sqlalchemy.", 76 | poolclass=pool.NullPool, 77 | ) 78 | 79 | async with connectable.connect() as connection: 80 | await connection.run_sync(do_run_migrations) 81 | 82 | await connectable.dispose() 83 | 84 | 85 | def run_migrations_online() -> None: 86 | """Run migrations in 'online' mode.""" 87 | 88 | asyncio.run(run_async_migrations()) 89 | 90 | 91 | if context.is_offline_mode(): 92 | run_migrations_offline() 93 | else: 94 | run_migrations_online() 95 | -------------------------------------------------------------------------------- /backend/infrastructure/postgres/alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | ${imports if imports else ""} 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = ${repr(up_revision)} 16 | down_revision: Union[str, None] = ${repr(down_revision)} 17 | branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} 18 | depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} 19 | 20 | 21 | def upgrade() -> None: 22 | ${upgrades if upgrades else "pass"} 23 | 24 | 25 | def downgrade() -> None: 26 | ${downgrades if downgrades else "pass"} 27 | -------------------------------------------------------------------------------- /backend/infrastructure/postgres/alembic/versions/2025_01_24-113341de12cd_init.py: -------------------------------------------------------------------------------- 1 | """Init 2 | 3 | Revision ID: 113341de12cd 4 | Revises: 5 | Create Date: 2025-01-24 22:53:32.647795 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = '113341de12cd' 16 | down_revision: Union[str, None] = None 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.create_table('documents', 24 | sa.Column('id', sa.Uuid(), nullable=False), 25 | sa.Column('name', sa.String(length=100), nullable=False), 26 | sa.Column('s3_md_id', sa.String(length=50), nullable=True), 27 | sa.Column('s3_raw_id', sa.String(length=50), nullable=True), 28 | sa.Column('upload_date', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=False), 29 | sa.Column('common_share_role_type', sa.Enum('private', 'viewer', 'edit', name='shareroleenum'), nullable=False), 30 | sa.PrimaryKeyConstraint('id') 31 | ) 32 | op.create_table('subscriptions', 33 | sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), 34 | sa.Column('name', sa.String(length=20), nullable=False), 35 | sa.PrimaryKeyConstraint('id') 36 | ) 37 | op.create_table('users', 38 | sa.Column('id', sa.Uuid(), nullable=False), 39 | sa.Column('email', sa.String(length=100), nullable=False), 40 | sa.Column('password_hash', sa.String(), nullable=False), 41 | sa.Column('name', sa.String(length=100), nullable=True), 42 | sa.Column('sub_level_id', sa.Integer(), nullable=False), 43 | sa.ForeignKeyConstraint(['sub_level_id'], ['subscriptions.id'], ), 44 | sa.PrimaryKeyConstraint('id'), 45 | sa.UniqueConstraint('email') 46 | ) 47 | op.create_table('document_users', 48 | sa.Column('user_id', sa.Uuid(), nullable=False), 49 | sa.Column('document_id', sa.Uuid(), nullable=False), 50 | sa.Column('last_access_at', sa.TIMESTAMP(), server_default=sa.text('now()'), nullable=False), 51 | sa.Column('role', sa.Enum('owner', 'viewer', 'editor', name='roleenum'), nullable=False), 52 | sa.ForeignKeyConstraint(['document_id'], ['documents.id'], ), 53 | sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), 54 | sa.PrimaryKeyConstraint('user_id', 'document_id') 55 | ) 56 | # ### end Alembic commands ### 57 | 58 | 59 | def downgrade() -> None: 60 | # ### commands auto generated by Alembic - please adjust! ### 61 | op.drop_table('document_users') 62 | op.drop_table('users') 63 | op.drop_table('subscriptions') 64 | op.drop_table('documents') 65 | # ### end Alembic commands ### 66 | -------------------------------------------------------------------------------- /backend/infrastructure/postgres/alembic/versions/2025_01_25-fd2bf9ac8821_change_user.py: -------------------------------------------------------------------------------- 1 | """Change user 2 | 3 | Revision ID: fd2bf9ac8821 4 | Revises: 113341de12cd 5 | Create Date: 2025-01-25 00:20:37.140786 6 | 7 | """ 8 | from typing import Sequence, Union 9 | 10 | from alembic import op 11 | import sqlalchemy as sa 12 | 13 | 14 | # revision identifiers, used by Alembic. 15 | revision: str = 'fd2bf9ac8821' 16 | down_revision: Union[str, None] = '113341de12cd' 17 | branch_labels: Union[str, Sequence[str], None] = None 18 | depends_on: Union[str, Sequence[str], None] = None 19 | 20 | 21 | def upgrade() -> None: 22 | # ### commands auto generated by Alembic - please adjust! ### 23 | op.alter_column('users', 'sub_level_id', 24 | existing_type=sa.INTEGER(), 25 | nullable=True) 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade() -> None: 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | op.alter_column('users', 'sub_level_id', 32 | existing_type=sa.INTEGER(), 33 | nullable=False) 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /backend/infrastructure/postgres/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.ext.asyncio import ( 2 | AsyncSession, 3 | create_async_engine, 4 | ) 5 | from sqlalchemy.orm import sessionmaker, DeclarativeBase 6 | 7 | from settings import postgres_settings 8 | 9 | 10 | class Base(DeclarativeBase): 11 | pass 12 | 13 | 14 | engine = create_async_engine( 15 | postgres_settings.URI, 16 | ) 17 | async_session_factory = sessionmaker( 18 | engine, 19 | class_=AsyncSession, 20 | expire_on_commit=False, 21 | autocommit=False, 22 | autoflush=False, 23 | ) 24 | 25 | 26 | from .models import * # pylint: disable=C0413 # isort:skip # noqa: F403, E402 27 | -------------------------------------------------------------------------------- /backend/infrastructure/postgres/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .user import * 2 | from .document import * 3 | -------------------------------------------------------------------------------- /backend/infrastructure/postgres/models/document.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from datetime import datetime 3 | 4 | from sqlalchemy import String, TIMESTAMP, Enum 5 | from sqlalchemy.orm import Mapped, mapped_column 6 | from sqlalchemy.sql import func 7 | 8 | from domain.document.model import ShareRoleEnum 9 | from infrastructure.postgres.database import Base 10 | 11 | 12 | class DocumentDAO(Base): 13 | __tablename__ = "documents" 14 | 15 | id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) 16 | name: Mapped[str] = mapped_column(String(100)) 17 | s3_md_id: Mapped[str] = mapped_column(String(50), nullable=True) 18 | s3_raw_id: Mapped[str] = mapped_column(String(50), nullable=True) 19 | upload_date: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now()) 20 | common_share_role_type: Mapped[ShareRoleEnum] = mapped_column( 21 | type_=Enum(ShareRoleEnum), default=ShareRoleEnum.private 22 | ) 23 | -------------------------------------------------------------------------------- /backend/infrastructure/postgres/models/user.py: -------------------------------------------------------------------------------- 1 | import enum 2 | import uuid 3 | from datetime import datetime 4 | 5 | from sqlalchemy import String, ForeignKey, TIMESTAMP, Enum 6 | from sqlalchemy.orm import Mapped, mapped_column 7 | from sqlalchemy.sql import func 8 | 9 | from domain.auth.model import RoleEnum 10 | from infrastructure.postgres.database import Base 11 | 12 | 13 | # TODO: Remove with Enum? 14 | class SubscriptionDAO(Base): 15 | __tablename__ = "subscriptions" 16 | 17 | id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) 18 | name: Mapped[str] = mapped_column(String(20)) 19 | #price ...? 20 | 21 | 22 | class UserDAO(Base): 23 | __tablename__ = "users" 24 | 25 | id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) 26 | email: Mapped[str] = mapped_column(String(100), unique=True) 27 | password_hash: Mapped[str] = mapped_column() 28 | name: Mapped[str] = mapped_column(String(100), nullable=True) 29 | # register_date: Mapped[date] 30 | sub_level_id: Mapped[int] = mapped_column(ForeignKey("subscriptions.id")) 31 | 32 | 33 | class DocumentUserDAO(Base): 34 | __tablename__ = "document_users" 35 | 36 | user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("users.id"), primary_key=True) 37 | document_id: Mapped[uuid.UUID] = mapped_column( 38 | ForeignKey("documents.id"), primary_key=True 39 | ) 40 | last_access_at: Mapped[datetime] = mapped_column( 41 | TIMESTAMP, server_default=func.now() 42 | ) 43 | role: Mapped[RoleEnum] = mapped_column(type_=Enum(RoleEnum)) 44 | -------------------------------------------------------------------------------- /backend/infrastructure/postgres/service/auth/repo.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | from datetime import datetime, timedelta 3 | 4 | from passlib.hash import bcrypt 5 | import sqlalchemy as sa 6 | from jose import jwt, JWTError 7 | from pydantic import ValidationError 8 | 9 | from domain.auth.model import BaseUser, JWTPayload, Token, UserAuth 10 | from domain.exceptions import InvalidCredentialsError, InvalidTokenError 11 | from infrastructure.postgres.models import UserDAO 12 | from settings import app_settings 13 | 14 | 15 | class UserAuthRepository: 16 | def __init__(self, session): 17 | self.session = session 18 | 19 | @classmethod 20 | def hash_password(cls, password: str) -> str: 21 | return bcrypt.hash(password) 22 | 23 | @classmethod 24 | def verify_password(cls, plain_password: str, hashed_password: str) -> bool: 25 | return bcrypt.verify(plain_password, hashed_password) 26 | 27 | @classmethod 28 | def verify_token(cls, token: str) -> str: 29 | try: 30 | payload_raw = jwt.decode( 31 | token, app_settings.JWT_SECRET, algorithms=[app_settings.JWT_ALGORITHM] 32 | ) 33 | payload = JWTPayload.model_validate(payload_raw) 34 | except (JWTError, ValidationError): 35 | raise InvalidTokenError(detail="Could not validate credentials") 36 | 37 | return payload.sub 38 | 39 | @classmethod 40 | def create_token(cls, user: UserDAO) -> Token: 41 | now = datetime.utcnow() 42 | jwt_payload = JWTPayload( 43 | user=BaseUser(email=user.email), 44 | sub=str(user.id), 45 | iat=now, 46 | exp=now + timedelta(hours=app_settings.JWT_EXPIRES_H), 47 | ) 48 | token = jwt.encode( 49 | jwt_payload.model_dump(), 50 | app_settings.JWT_SECRET, 51 | algorithm=app_settings.JWT_ALGORITHM, 52 | ) 53 | return Token(access_token=token) 54 | 55 | async def register_new_user(self, user_data: UserAuth) -> Token: 56 | user = await self.get_by_email(user_data.email) 57 | 58 | if user: 59 | raise InvalidCredentialsError(detail="User with such email already exists") 60 | 61 | new_user = await self.create_user(user_data) 62 | 63 | return self.create_token(new_user) 64 | 65 | async def authenticate_user(self, auth_form: UserAuth) -> Token: 66 | user = await self.get_by_email(auth_form.email) 67 | 68 | if not user: 69 | raise InvalidCredentialsError(detail="No such user") 70 | 71 | if not self.verify_password(auth_form.password, user.password_hash): # type: ignore 72 | raise InvalidCredentialsError(detail="Incorrect password") 73 | 74 | token = self.create_token(user) 75 | 76 | return token 77 | 78 | async def create_user(self, user_data: UserAuth) -> UserDAO: 79 | new_user = UserDAO( 80 | email=user_data.email, 81 | password_hash=self.hash_password(user_data.password), 82 | ) 83 | self.session.add(new_user) 84 | await self.session.commit() 85 | return new_user 86 | 87 | async def get_by_email(self, email: str) -> Optional[UserDAO]: 88 | stmt = sa.select(UserDAO).filter_by(email=email) 89 | return (await self.session.execute(stmt)).scalar() 90 | -------------------------------------------------------------------------------- /backend/requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.14.1 2 | aiohttp==3.11.11 3 | aioboto3==13.4.0 4 | annotated-types==0.7.0 5 | anyio==4.8.0 6 | async-timeout==5.0.1 7 | asyncpg==0.30.0 8 | passlib==1.7.4 9 | click==8.1.8 10 | ecdsa==0.19.0 11 | exceptiongroup==1.2.2 12 | fastapi==0.115.7 13 | greenlet==3.1.1 14 | h11==0.14.0 15 | idna==3.10 16 | Mako==1.3.8 17 | MarkupSafe==3.0.2 18 | pyasn1==0.6.1 19 | pydantic==2.10.5 20 | pydantic-settings==2.7.1 21 | pydantic[email]==2.10.5 22 | pydantic_core==2.27.2 23 | python-dotenv==1.0.1 24 | python-jose==3.3.0 25 | python-multipart==0.0.20 26 | rsa==4.9 27 | six==1.17.0 28 | sniffio==1.3.1 29 | SQLAlchemy==2.0.37 30 | starlette==0.45.2 31 | typing_extensions==4.12.2 32 | uvicorn==0.34.0 33 | requests==2.32.3 34 | python-magic==0.4.27 35 | celery==5.3.4 -------------------------------------------------------------------------------- /backend/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | from pydantic import computed_field 3 | 4 | 5 | class ToolConfig: 6 | env_file_encoding = "utf8" 7 | extra = "ignore" 8 | 9 | 10 | class AppSettings(BaseSettings): 11 | PORT: int = 8080 12 | JWT_SECRET: str = "secret" 13 | JWT_ALGORITHM: str = "HS256" 14 | JWT_EXPIRES_H: int = 48 15 | COOKIE_NAME: str = "ds_auth" 16 | 17 | HC_TIMEOUT: int = 30 # secs 18 | HC_SLEEP: int = 5 # secs 19 | 20 | class Config(ToolConfig): 21 | env_prefix = "api_" 22 | 23 | 24 | class RedisSettings(BaseSettings): 25 | HOST: str = "redis" 26 | PORT: int = 6379 27 | BACKEND_DB: int = 0 28 | 29 | @computed_field(return_type=str) 30 | @property 31 | def URI(self): 32 | return f"redis://{self.HOST}:{self.PORT}/{self.BACKEND_DB}" 33 | 34 | class Config(ToolConfig): 35 | env_prefix = "redis_" 36 | 37 | 38 | class RabbitMQSettings(BaseSettings): 39 | HOST: str = "rabbitmq" 40 | AMQP_PORT: int = 5672 41 | LOGIN: str = "admin" 42 | PASSWORD: str = "admin" 43 | 44 | @computed_field(return_type=str) 45 | @property 46 | def URI(self): 47 | return f"amqp://{self.LOGIN}:{self.PASSWORD}@{self.HOST}:{self.AMQP_PORT}/" 48 | 49 | class Config(ToolConfig): 50 | env_prefix = "rabbitmq_" 51 | 52 | 53 | class MinIOSettings(BaseSettings): 54 | HOST: str = "minio" 55 | BUCKET: str = "user_docs" 56 | API_PORT: int = 9000 57 | ROOT_USER: str = "admin" 58 | ROOT_PASSWORD: str = "admin_password" 59 | 60 | @computed_field(return_type=str) 61 | @property 62 | def URI(self): 63 | return f"http://{self.HOST}:{self.API_PORT}" 64 | 65 | class Config(ToolConfig): 66 | env_prefix = "minio_" 67 | 68 | 69 | class PostgresSettings(BaseSettings): 70 | HOST: str = "localhost" 71 | PORT: int = 5432 72 | USER: str = "postgres" 73 | PASSWORD: str = "passwd" 74 | DB: str = "ds" 75 | MAX_OVERFLOW: int = 15 76 | POOL_SIZE: int = 15 77 | 78 | @computed_field(return_type=str) 79 | @property 80 | def URI(self): 81 | return f"postgresql+asyncpg://{self.USER}:{self.PASSWORD}@{self.HOST}:{self.PORT}/{self.DB}" 82 | 83 | class Config(ToolConfig): 84 | env_prefix = "postgres_" 85 | 86 | 87 | class FrontAppSettings(BaseSettings): 88 | PORT: int = 5000 89 | 90 | class Config(ToolConfig): 91 | env_prefix = "front_" 92 | 93 | 94 | postgres_settings = PostgresSettings() 95 | app_settings = AppSettings() 96 | redis_settings = RedisSettings() 97 | minio_settings = MinIOSettings() 98 | rabbitmq_settings = RabbitMQSettings() 99 | front_settings = FrontAppSettings() 100 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | minio: 3 | image: minio/minio:latest 4 | container_name: minio 5 | 6 | ports: 7 | - "${MINIO_API_PORT}:${MINIO_API_PORT}" # api 8 | - "${MINIO_CONSOLE_PORT}:${MINIO_CONSOLE_PORT}" # console 9 | 10 | environment: 11 | MINIO_ROOT_USER: ${MINIO_ROOT_USER} 12 | MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} 13 | 14 | volumes: 15 | - ./minio_data:/data 16 | - ./minio_config:/root/.minio 17 | 18 | command: server /data --console-address ":${MINIO_CONSOLE_PORT}" 19 | 20 | redis: 21 | restart: "no" 22 | mem_limit: 1G 23 | container_name: redis_backend 24 | 25 | image: redis:6.2-alpine 26 | 27 | environment: 28 | - ALLOW_EMPTY_PASSWORD=yes 29 | - REDIS_PORT_NUMBER=${REDIS_PORT} 30 | ports: 31 | - "${REDIS_PORT}:${REDIS_PORT}" 32 | 33 | rabbitmq: 34 | restart: "no" 35 | mem_limit: 2G 36 | container_name: rabbitmq_broker 37 | 38 | image: "rabbitmq:3-management" 39 | 40 | ports: 41 | - "${RABBITMQ_AMQP_PORT}:${RABBITMQ_AMQP_PORT}" # api 42 | - "${RABBITMQ_CONSOLE_PORT}:${RABBITMQ_CONSOLE_PORT}" # console 43 | 44 | volumes: 45 | - ./rabbitmq-data:/var/lib/rabbitmq/mnesia 46 | - ./rabbit_conf/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf 47 | 48 | environment: 49 | RABBITMQ_ERLANG_COOKIE: ${RABBITMQ_ERLANG_COOKIE} 50 | RABBITMQ_DEFAULT_USER: ${RABBITMQ_LOGIN} 51 | RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD} 52 | 53 | worker: 54 | restart: "no" 55 | mem_limit: 1G 56 | container_name: worker 57 | 58 | build: 59 | context: ./neural_worker 60 | dockerfile: Dockerfile 61 | command: bash -c "celery -A celery_services worker -l info -E -c ${NW_CONCURRENCY_COUNT} --loglevel=info" 62 | 63 | volumes: 64 | - ./neural_worker:/neural_worker 65 | 66 | env_file: 67 | - ./.env 68 | 69 | depends_on: 70 | - redis 71 | - rabbitmq 72 | 73 | celery-flower: 74 | image: mher/flower:0.9.7 75 | command: ['flower', '--broker=redis://${REDIS_HOST}:${REDIS_PORT}/${REDIS_CELERY_DB}'] 76 | 77 | environment: 78 | - FLOWER_PORT=${FLOWER_PORT} 79 | 80 | ports: 81 | - ${FLOWER_PORT}:${FLOWER_PORT} 82 | depends_on: 83 | - rabbitmq 84 | 85 | backend: 86 | restart: "no" 87 | mem_limit: 4G 88 | container_name: backend_api 89 | 90 | build: 91 | context: ./backend 92 | dockerfile: Dockerfile 93 | command: bash -c "alembic -c infrastructure/postgres/alembic.ini upgrade head && uvicorn api.api:app --host 0.0.0.0 --port ${API_PORT}" 94 | 95 | volumes: 96 | - ./backend:/backend 97 | 98 | env_file: 99 | - ./.env 100 | 101 | ports: 102 | - "${API_PORT}:${API_PORT}" 103 | depends_on: 104 | postgres: 105 | condition: service_healthy 106 | rabbitmq: 107 | condition: service_started 108 | 109 | frontend: 110 | restart: "no" 111 | mem_limit: 1G 112 | container_name: frontend_api 113 | 114 | build: 115 | context: ./frontend 116 | dockerfile: Dockerfile 117 | command: bash -c "uvicorn app:app --host 0.0.0.0 --port ${FRONT_PORT}" 118 | 119 | volumes: 120 | - ./frontend:/frontend 121 | 122 | env_file: 123 | - ./.env 124 | 125 | ports: 126 | - "${FRONT_PORT}:${FRONT_PORT}" 127 | 128 | postgres: 129 | restart: always 130 | container_name: postgres 131 | mem_limit: 100m 132 | image: postgres:13.3 133 | 134 | environment: 135 | POSTGRES_DB: ${POSTGRES_DB} 136 | POSTGRES_USER: ${POSTGRES_USER} 137 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} 138 | PGDATA: "/var/lib/postgresql/data/pgdata" 139 | 140 | volumes: 141 | - ./pgdata:/var/lib/postgresql/data/pgdata 142 | 143 | command: "-p ${POSTGRES_PORT}" 144 | ports: 145 | - "${POSTGRES_PORT}:${POSTGRES_PORT}" 146 | 147 | healthcheck: 148 | test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] 149 | interval: 3s 150 | timeout: 3s 151 | retries: 5 152 | -------------------------------------------------------------------------------- /example_images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/example_images/architecture.png -------------------------------------------------------------------------------- /example_images/cv_md.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/example_images/cv_md.gif -------------------------------------------------------------------------------- /example_images/example_work.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/example_images/example_work.gif -------------------------------------------------------------------------------- /example_images/photo_2023-11-18_20-53-46-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/example_images/photo_2023-11-18_20-53-46-3.jpg -------------------------------------------------------------------------------- /frontend/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from contextlib import asynccontextmanager 3 | 4 | from fastapi import FastAPI, Request 5 | from fastapi.templating import Jinja2Templates 6 | from fastapi.staticfiles import StaticFiles 7 | 8 | import healthchecker as hc 9 | import settings 10 | 11 | 12 | @asynccontextmanager 13 | async def init_tables(app: FastAPI): 14 | urls_to_check = [settings.back_settings.URI] 15 | 16 | hc.Readiness(urls=urls_to_check, logger=app.state.Logger).run() 17 | 18 | yield 19 | 20 | 21 | app = FastAPI( 22 | docs_url="/api/docs", 23 | openapi_url="/api/openapi.json", 24 | lifespan=init_tables, 25 | ) 26 | 27 | app.state.Logger = logging.getLogger(name="deepscriptum_backend") 28 | app.state.Logger.setLevel("DEBUG") 29 | 30 | templates = Jinja2Templates("templates") 31 | app.mount("/assets", StaticFiles(directory="templates/assets"), name="assets") 32 | 33 | 34 | @app.get("/") 35 | async def start_page(request: Request): 36 | return templates.TemplateResponse(name="index.html", request=request) 37 | 38 | 39 | @app.get("/auth/login") 40 | async def login_page(request: Request): 41 | return templates.TemplateResponse( 42 | name="login.html", 43 | context={ 44 | "request": request, 45 | "LOGIN_ENDPOINT": "http://0.0.0.0:8080/api/auth/login", 46 | }, 47 | ) 48 | 49 | 50 | @app.get("/auth/register") 51 | async def register_page(request: Request): 52 | return templates.TemplateResponse( 53 | name="signup.html", 54 | context={ 55 | "request": request, 56 | "REGISTER_ENDPOINT": "http://0.0.0.0:8080/api/auth/register", 57 | }, 58 | ) 59 | 60 | 61 | @app.get("/home") 62 | async def home_page(request: Request): 63 | return templates.TemplateResponse(name="home.html", request=request) 64 | 65 | 66 | @app.get("/editor/{document_id}") 67 | async def editor_page(request: Request): 68 | return templates.TemplateResponse(name="dashboard.html", request=request) 69 | -------------------------------------------------------------------------------- /frontend/dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | 3 | RUN apt-get update && apt-get install -y 4 | 5 | WORKDIR /frontend 6 | 7 | COPY requirements.txt /frontend/ 8 | RUN pip install -r requirements.txt 9 | COPY . /frontend/ -------------------------------------------------------------------------------- /frontend/healthchecker.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, List, Any 2 | from time import sleep 3 | import requests 4 | 5 | from settings import back_settings 6 | 7 | 8 | class Readiness: 9 | """Class that handles /readiness endpoint.""" 10 | 11 | urls: Optional[List[str]] = None 12 | logger: Any = None 13 | 14 | def __init__( 15 | self, 16 | urls: List[str], 17 | logger: Any, 18 | ) -> None: 19 | """ 20 | :param urls: list of service urls to check. 21 | :param task: list of futures or coroutines 22 | :param logger: Logger object. 23 | :param client: HTTPClient object. 24 | """ 25 | 26 | Readiness.urls = urls or [] 27 | Readiness.logger = logger 28 | 29 | Readiness.status = False 30 | 31 | @classmethod 32 | def _make_request(cls, url: str) -> None: 33 | """Check readiness of the specified service.""" 34 | 35 | while True: 36 | cls.logger.info( 37 | f"Trying to connect to '{url}'", 38 | ) 39 | try: 40 | response = requests.get(url=f"{url}", timeout=back_settings.HC_TIMEOUT) 41 | if response.status_code: 42 | cls.logger.info( 43 | f"Successfully connected to '{url}'", 44 | ) 45 | break 46 | 47 | cls.logger.warning( 48 | f"Failed to connect to '{url}'", 49 | ) 50 | except Exception as e: 51 | cls.logger.warning( 52 | f"Failed to connect to '{url}': {str(e)}", 53 | ) 54 | 55 | sleep(back_settings.HC_SLEEP) 56 | 57 | @classmethod 58 | def _check_readiness(cls) -> None: 59 | """Check readiness of all services.""" 60 | 61 | cls.logger.info( 62 | f"Running readiness checks.", 63 | ) 64 | [cls._make_request(url) for url in cls.urls or []] 65 | 66 | cls.logger.info( 67 | f"Successfully finished readiness checks.", 68 | ) 69 | 70 | @classmethod 71 | def run(cls) -> None: 72 | cls._check_readiness() 73 | -------------------------------------------------------------------------------- /frontend/requirements.txt: -------------------------------------------------------------------------------- 1 | uvicorn==0.34.0 2 | fastapi==0.115.7 3 | jinja2==3.1.5 4 | pydantic==2.10.5 5 | pydantic-settings==2.7.1 6 | pydantic_core==2.27.2 7 | requests==2.32.3 -------------------------------------------------------------------------------- /frontend/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | from pydantic import computed_field 3 | 4 | 5 | class ToolConfig: 6 | env_file_encoding = "utf8" 7 | extra = "ignore" 8 | 9 | 10 | class BackendAppSettings(BaseSettings): 11 | HOST: str = "backend" 12 | PORT: int = 8080 13 | JWT_SECRET: str = "secret" 14 | JWT_ALGORITHM: str = "HS256" 15 | JWT_EXPIRES_H: int = 48 16 | COOKIE_NAME: str = "ds_auth" 17 | 18 | HC_TIMEOUT: int = 30 # secs 19 | HC_SLEEP: int = 5 # secs 20 | 21 | @computed_field(return_type=str) 22 | @property 23 | def URI(self): 24 | return f"http://{self.HOST}:{self.PORT}/" 25 | 26 | class Config(ToolConfig): 27 | env_prefix = "api_" 28 | 29 | 30 | class FrontAppSettings(BaseSettings): 31 | PORT: int = 5000 32 | 33 | class Config(ToolConfig): 34 | env_prefix = "front_" 35 | 36 | 37 | back_settings = BackendAppSettings() 38 | front_settings = FrontAppSettings() 39 | -------------------------------------------------------------------------------- /frontend/templates/assets/bootstrap/css/bootstrap-reboot.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Reboot v5.0.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors 4 | * Copyright 2011-2021 Twitter, Inc. 5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) 7 | */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important} -------------------------------------------------------------------------------- /frontend/templates/assets/formoid/formoid.min.js: -------------------------------------------------------------------------------- 1 | var Formoid = (function () { 2 | 3 | var API_URL = 'https://formoid.net/api/push'; 4 | 5 | function $ajax(url, settings) { 6 | return new Promise(function (resolve, reject) { 7 | var xhr = new XMLHttpRequest(); 8 | xhr.open(settings.type, url); 9 | xhr.onload = function () { 10 | if (xhr.status !== 200) { 11 | return reject(new Error('Incorrect server response.')); 12 | } 13 | resolve(xhr.responseText); 14 | }; 15 | xhr.onerror = function () { 16 | var message = 'Failed to query the server. '; 17 | if ('onLine' in navigator && !navigator.onLine) { 18 | message += 'No connection to the Internet.'; 19 | } else { 20 | message += 'Check the connection and try again.'; 21 | } 22 | reject(new Error(message)); 23 | }; 24 | xhr.send(settings.data); 25 | }) 26 | }; 27 | 28 | var prop = function (name, args) { 29 | name = '__' + name + '__'; 30 | if (args.length) { 31 | this[name] = args[0]; 32 | return this; 33 | } 34 | return this[name]; 35 | }; 36 | 37 | var Form = function (settings) { 38 | settings = settings || {}; 39 | this.__email__ = settings.email || ''; 40 | this.__title__ = settings.title || ''; 41 | this.__data__ = settings.data || []; 42 | }; 43 | 44 | Form.prototype.email = function (value) { 45 | return prop.call(this, 'email', arguments); 46 | }; 47 | 48 | Form.prototype.title = function (value) { 49 | return prop.call(this, 'title', arguments); 50 | }; 51 | 52 | Form.prototype.data = function (value) { 53 | return prop.call(this, 'data', arguments); 54 | }; 55 | 56 | Form.prototype.send = function (data) { 57 | return $ajax(API_URL, { 58 | type: 'POST', 59 | data: JSON.stringify({ 60 | email: this.__email__, 61 | form: { 62 | title: this.__title__, 63 | data: (arguments.length ? data : this.__data__) 64 | } 65 | }) 66 | }) 67 | .then(function(responseText) { 68 | var data; 69 | try { 70 | data = JSON.parse(responseText); 71 | } catch (e) { 72 | throw new Error('Incorrect server response.'); 73 | } 74 | if (data.error) { 75 | throw new Error(data.error); 76 | } 77 | return data.response; 78 | }); 79 | }; 80 | 81 | return { 82 | Form: function (settings) { 83 | return new Form(settings); 84 | } 85 | } 86 | 87 | })(); 88 | 89 | const formModalDOM = document.createElement('div'); 90 | let formModal; 91 | 92 | formModalDOM.classList.add('modal'); 93 | formModalDOM.setAttribute('tabindex', -1); 94 | formModalDOM.style.overflow = 'hidden'; 95 | 96 | if (typeof bootstrap !== 'undefined') { 97 | if (bootstrap.Tooltip.VERSION.startsWith(5)) { 98 | //bs5 99 | formModalDOM.innerHTML = ` 100 | ` 108 | } else { 109 | // bs4 110 | formModalDOM.innerHTML = ` 111 | ` 119 | } 120 | } else if ($.fn.Tooltip) { 121 | // bs3 122 | formModalDOM.innerHTML = ` 123 | ` 131 | } 132 | 133 | if (bootstrap) { 134 | formModal = new bootstrap.Modal(formModalDOM); 135 | } 136 | 137 | var isValidEmail = function (input) { 138 | return input.value ? /^([^@]+?)@(([a-z0-9]-*)*[a-z0-9]+\.)+([a-z0-9]+)$/i.test(input.value) : true; 139 | }; 140 | 141 | var formComponents = document.querySelectorAll('[data-form-type="formoid"]'); 142 | 143 | formComponents.forEach(function (component) { 144 | var formData, 145 | form = component.tagName === 'FORM' ? component : component.querySelector('form'), 146 | alert = component.querySelector('[data-form-alert]'), 147 | title = component.getAttribute('data-form-title') ? component : component.querySelector('[data-form-title]'), 148 | submit = component.querySelector('[type="submit"]'), 149 | inputArr = component.querySelectorAll('[data-form-field]'), 150 | alertSuccess = alert.innerHTML; 151 | 152 | form.addEventListener('change', function (event) { 153 | if (event.target.type === 'file') { 154 | if (event.target.files[0].size > 1000000) { 155 | formModal._element.querySelector('.modal-body p').innerText = 'File size must be less than 1mb'; 156 | formModal._element.querySelector('.modal-content').classList.add('alert-danger'); 157 | formModal._element.querySelector('.modal-content').style.backgroundColor = '#ff9966'; 158 | formModal.show(); 159 | submit.classList.add('btn-loading'); 160 | submit.setAttribute('disabled', true); 161 | } 162 | } 163 | }); 164 | 165 | form.addEventListener('submit', function (event) { 166 | event.stopPropagation(); 167 | event.preventDefault(); 168 | 169 | if (submit.classList.contains('btn-loading')) return; 170 | 171 | var inputs = inputArr; 172 | 173 | form.classList.add('form-active'); 174 | submit.classList.add('btn-loading'); 175 | submit.setAttribute('disabled', true); 176 | alert.innerHTML = ''; 177 | 178 | formData = formData || Formoid.Form({ 179 | email: component.querySelector('[data-form-email]').value, 180 | title: title.getAttribute('data-form-title') || title.innerText 181 | }); 182 | 183 | function parseInput(input) { 184 | return new Promise(function (resolve, reject) { 185 | // valide email 186 | if (input.getAttribute('name') === 'email' && !isValidEmail(input)) { 187 | return reject(new Error('Form is not valid')); 188 | } 189 | var name = input.getAttribute('data-form-field') || input.getAttribute('name'); 190 | switch (input.getAttribute('type')) { 191 | case 'file': 192 | var file = input.files[0]; 193 | if (!file) return resolve(); 194 | var reader = new FileReader() 195 | reader.onloadend = function () { 196 | resolve([name, reader.result]); 197 | }; 198 | reader.onerror = function () { 199 | reject(reader.error); 200 | }; 201 | reader.readAsDataURL(file); 202 | break; 203 | case 'checkbox': 204 | resolve([name, input.checked ? input.value : 'No']); 205 | break; 206 | case 'radio': 207 | resolve(input.checked && [name, input.value]); 208 | break; 209 | default: 210 | resolve([name, input.value]); 211 | } 212 | }); 213 | } 214 | 215 | Promise 216 | .all(Array.prototype.map.call(inputs, function (input) { return parseInput(input); })) 217 | .then(function (data) { return formData.send(data.filter(function (v) { return v; })); }) 218 | .then(function (message) { 219 | form.reset(); 220 | form.classList.remove('form-active'); 221 | formModal._element.querySelector('.modal-body p').innerText = alertSuccess || message; 222 | formModal._element.querySelector('.modal-content').classList.add('alert-success'); 223 | formModal._element.querySelector('.modal-content').style.backgroundColor = '#70c770' 224 | formModal.show(); 225 | }, function (err) { 226 | formModal._element.querySelector('.modal-body p').innerText = err.message; 227 | formModal._element.querySelector('.modal-content').classList.add('alert-danger'); 228 | formModal._element.querySelector('.modal-content').style.backgroundColor = '#ff9966' 229 | }) 230 | .then(function () { 231 | submit.classList.remove('btn-loading'); 232 | submit.removeAttribute('disabled'); 233 | }); 234 | }); 235 | 236 | inputArr.forEach(function (elem) { 237 | elem.addEventListener('focus', function () { 238 | submit.classList.remove('btn-loading') 239 | submit.removeAttribute('disabled'); 240 | }); 241 | }); 242 | }) -------------------------------------------------------------------------------- /frontend/templates/assets/images/25231-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/images/25231-512x512.png -------------------------------------------------------------------------------- /frontend/templates/assets/images/ds_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/images/ds_logo.png -------------------------------------------------------------------------------- /frontend/templates/assets/images/ds_logo_book.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/images/ds_logo_book.png -------------------------------------------------------------------------------- /frontend/templates/assets/images/hashes.json: -------------------------------------------------------------------------------- 1 | {"E2t1WsdXw6XOIXSXhc5Drw==":"db3ac2ea-8a28-465e-9d63-4f2b52fdb62f.webp","fBNknm0AMALqWnNjDvM4lg==":"db3ac2ea-8a28-465e-9d63-4f2b52fdb62f-1.webp","QswyXOUyYUkW94cyFLOW3g==":"dall%C2%B7e%202025-01-24%2020.38.29%20-%20a%20minimalist%20and%20modern%20logo%20design%20for%20a%20service%20that%20converts%20any%20type%20of%20document%20into%20a%20.md%20(markdown)%20file.%20the%20logo%20features%20a%20sleek%20document%20ic.webp","ghTRyzosF071jaLKriB/PA==":"logo.webp","lZy7H0qZVRx0dcao84JeQA==":"25231-512x512.png","jw1nE+q++S+4SuaISBBdGQ==":"fast-179x179.jpeg","9YlSDOEbP2ZBO0pyMDMgfg==":"fast-1-179x179.jpeg","omXbhEyX5tz3skkNlxrbRQ==":"mbr-179x179.png","9w0pOzTn1+evxXBRiVyGYg==":"mbr-1-179x179.png","Whv3ao+UE/5Z3K03YY+kPA==":"mbr-179x119.jpg","cJMGOEF6AgEGPxSkCrw8pw==":"mbr-179x194.png","k6+1JKw4s+NJhRhfke+JVA==":"mbr-1-179x194.png","Is8+vA3aAgTwFxt0emPrgQ==":"mbr-2-179x194.png","EeqGZYucfEdGBKB2pteh7w==":"logo-1.webp","ztGBw3vSPcX6UkBEzQWeRQ==":"mbr-1920x1143.png","YuE2fTHLiPtijz2DGQmb+A==":"mbr-1-1920x1143.png"} -------------------------------------------------------------------------------- /frontend/templates/assets/images/logo-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/images/logo-1.webp -------------------------------------------------------------------------------- /frontend/templates/assets/images/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/images/logo.webp -------------------------------------------------------------------------------- /frontend/templates/assets/images/logo5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/images/logo5.png -------------------------------------------------------------------------------- /frontend/templates/assets/images/mbr-1-179x179.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/images/mbr-1-179x179.png -------------------------------------------------------------------------------- /frontend/templates/assets/images/mbr-1-1920x1143.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/images/mbr-1-1920x1143.png -------------------------------------------------------------------------------- /frontend/templates/assets/images/mbr-179x179.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/images/mbr-179x179.png -------------------------------------------------------------------------------- /frontend/templates/assets/images/mbr-2-179x194.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/images/mbr-2-179x194.png -------------------------------------------------------------------------------- /frontend/templates/assets/smoothscroll/smooth-scroll.js: -------------------------------------------------------------------------------- 1 | (function(){function C(){if(!D&&document.body){D=!0;var a=document.body,b=document.documentElement,d=window.innerHeight,c=a.scrollHeight;l=0<=document.compatMode.indexOf("CSS")?b:a;m=a;f.keyboardSupport&&window.addEventListener("keydown",M,!1);if(top!=self)v=!0;else if(ca&&c>d&&(a.offsetHeight<=d||b.offsetHeight<=d)){var e=document.createElement("div");e.style.cssText="position:absolute; z-index:-10000; top:0; left:0; right:0; height:"+l.scrollHeight+"px";document.body.appendChild(e);var g;w=function(){g|| 2 | (g=setTimeout(function(){e.style.height="0";e.style.height=l.scrollHeight+"px";g=null},500))};setTimeout(w,10);window.addEventListener("resize",w,!1);z=new da(w);z.observe(a,{attributes:!0,childList:!0,characterData:!1});l.offsetHeight<=d&&(d=document.createElement("div"),d.style.clear="both",a.appendChild(d))}f.fixedBackground||(a.style.backgroundAttachment="scroll",b.style.backgroundAttachment="scroll")}}function N(a,b,d){ea(b,d);if(1!=f.accelerationMax){var c=Date.now()-E;cb?.99:-.99,lastY:0>d?.99:-.99,start:Date.now()});if(!F){c=O();var e=a===c||a===document.body;null==a.$scrollBehavior&&fa(a)&&(a.$scrollBehavior=a.style.scrollBehavior,a.style.scrollBehavior="auto");var g=function(c){c=Date.now();for(var k=0,l=0,h=0;h=f.animationTime,q=m?1:p/f.animationTime;f.pulseAlgorithm&&(p=q,1<=p?q=1:0>=p?q=0:(1==f.pulseNormalize&&(f.pulseNormalize/= 4 | P(1)),q=P(p)));p=n.x*q-n.lastX>>0;q=n.y*q-n.lastY>>0;k+=p;l+=q;n.lastX+=p;n.lastY+=q;m&&(t.splice(h,1),h--)}e?window.scrollBy(k,l):(k&&(a.scrollLeft+=k),l&&(a.scrollTop+=l));b||d||(t=[]);t.length?Q(g,a,1E3/f.frameRate+1):(F=!1,null!=a.$scrollBehavior&&(a.style.scrollBehavior=a.$scrollBehavior,a.$scrollBehavior=null))};Q(g,a,0);F=!0}}function R(a){D||C();var b=a.target;if(a.defaultPrevented||a.ctrlKey||r(m,"embed")||r(b,"embed")&&/\.pdf/i.test(b.src)||r(m,"object")||b.shadowRoot)return!0;var d=-a.wheelDeltaX|| 5 | a.deltaX||0,c=-a.wheelDeltaY||a.deltaY||0;ha&&(a.wheelDeltaX&&x(a.wheelDeltaX,120)&&(d=a.wheelDeltaX/Math.abs(a.wheelDeltaX)*-120),a.wheelDeltaY&&x(a.wheelDeltaY,120)&&(c=a.wheelDeltaY/Math.abs(a.wheelDeltaY)*-120));d||c||(c=-a.wheelDelta||0);1===a.deltaMode&&(d*=40,c*=40);b=S(b);if(!b)return v&&G?(Object.defineProperty(a,"target",{value:window.frameElement}),parent.wheel(a)):!0;if(ia(c))return!0;1.2a?!0:b}}function x(a,b){return Math.floor(a/b)==a/b}function K(a){return x(h[0],a)&&x(h[1],a)&&x(h[2],a)}function P(a){a*=f.pulseScale;if(1>a)var b= 11 | a-(1-Math.exp(-a));else b=Math.exp(-1),a=1-Math.exp(-(a-1)),b+=a*(1-b);return b*f.pulseNormalize}function y(a){for(var b in a)aa.hasOwnProperty(b)&&(f[b]=a[b])}var aa={frameRate:150,animationTime:400,stepSize:100,pulseAlgorithm:!0,pulseScale:4,pulseNormalize:1,accelerationDelta:50,accelerationMax:3,keyboardSupport:!0,arrowScroll:50,fixedBackground:!0,excluded:""},f=aa,v=!1,B={x:0,y:0},D=!1,l=document.documentElement,m,z,w,h=[],Z,ha=/^Mac/.test(navigator.platform),g={left:37,up:38,right:39,down:40, 12 | spacebar:32,pageup:33,pagedown:34,end:35,home:36},ja={37:1,38:1,39:1,40:1},t=[],F=!1,E=Date.now(),J=function(){var a=0;return function(b){return b.uniqueID||(b.uniqueID=a++)}}(),W={},H={},V,A={};if(window.localStorage&&localStorage.SS_deltaBuffer)try{h=localStorage.SS_deltaBuffer.split(",")}catch(a){}var Q=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||function(a,b,d){window.setTimeout(a,d||1E3/60)}}(),da=window.MutationObserver|| 13 | window.WebKitMutationObserver||window.MozMutationObserver,O=function(){var a=document.scrollingElement;return function(){if(!a){var b=document.createElement("div");b.style.cssText="height:10000px;width:1px;";document.body.appendChild(b);var d=document.body.scrollTop;window.scrollBy(0,3);a=document.body.scrollTop!=d?document.body:document.documentElement;window.scrollBy(0,-3);document.body.removeChild(b)}return a}}(),k=window.navigator.userAgent,u=/Edge/.test(k),G=/chrome/i.test(k)&&!u;u=/safari/i.test(k)&& 14 | !u;var ka=/mobile/i.test(k),la=/Windows NT 6.1/i.test(k)&&/rv:11/i.test(k),ca=u&&(/Version\/8/i.test(k)||/Version\/9/i.test(k));k=(G||u||la)&&!ka;var ba=!1;try{window.addEventListener("test",null,Object.defineProperty({},"passive",{get:function(){ba=!0}}))}catch(a){}u=ba?{passive:!1}:!1;var L="onwheel"in document.createElement("div")?"wheel":"mousewheel";L&&k&&(window.addEventListener(L,R,u||!1),window.addEventListener("mousedown",U,!1),window.addEventListener("load",C,!1));y.destroy=function(){z&& 15 | z.disconnect();window.removeEventListener(L,R,!1);window.removeEventListener("mousedown",U,!1);window.removeEventListener("keydown",M,!1);window.removeEventListener("resize",w,!1);window.removeEventListener("load",C,!1)};window.SmoothScrollOptions&&y(window.SmoothScrollOptions);"function"===typeof define&&define.amd?define(function(){return y}):"object"==typeof exports?module.exports=y:window.SmoothScroll=y})(); 16 | -------------------------------------------------------------------------------- /frontend/templates/assets/socicon/css/styles.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @font-face { 4 | font-family: 'Socicon'; 5 | src: url('../fonts/socicon.eot'); 6 | src: url('../fonts/socicon.eot?#iefix') format('embedded-opentype'), 7 | url('../fonts/socicon.woff2') format('woff2'), 8 | url('../fonts/socicon.ttf') format('truetype'), 9 | url('../fonts/socicon.woff') format('woff'), 10 | url('../fonts/socicon.svg#socicon') format('svg'); 11 | font-weight: normal; 12 | font-style: normal; 13 | font-display: swap; 14 | } 15 | 16 | [data-icon]:before { 17 | font-family: "socicon" !important; 18 | content: attr(data-icon); 19 | font-style: normal !important; 20 | font-weight: normal !important; 21 | font-variant: normal !important; 22 | text-transform: none !important; 23 | speak: none; 24 | line-height: 1; 25 | -webkit-font-smoothing: antialiased; 26 | -moz-osx-font-smoothing: grayscale; 27 | } 28 | 29 | [class^="socicon-"], [class*=" socicon-"] { 30 | /* use !important to prevent issues with browser extensions that change fonts */ 31 | font-family: 'Socicon' !important; 32 | speak: none; 33 | font-style: normal; 34 | font-weight: normal; 35 | font-variant: normal; 36 | text-transform: none; 37 | line-height: 1; 38 | 39 | /* Better Font Rendering =========== */ 40 | -webkit-font-smoothing: antialiased; 41 | -moz-osx-font-smoothing: grayscale; 42 | } 43 | 44 | .socicon-500px:before { 45 | content: "\e000"; 46 | } 47 | .socicon-8tracks:before { 48 | content: "\e001"; 49 | } 50 | .socicon-airbnb:before { 51 | content: "\e002"; 52 | } 53 | .socicon-alliance:before { 54 | content: "\e003"; 55 | } 56 | .socicon-amazon:before { 57 | content: "\e004"; 58 | } 59 | .socicon-amplement:before { 60 | content: "\e005"; 61 | } 62 | .socicon-android:before { 63 | content: "\e006"; 64 | } 65 | .socicon-angellist:before { 66 | content: "\e007"; 67 | } 68 | .socicon-apple:before { 69 | content: "\e008"; 70 | } 71 | .socicon-appnet:before { 72 | content: "\e009"; 73 | } 74 | .socicon-baidu:before { 75 | content: "\e00a"; 76 | } 77 | .socicon-bandcamp:before { 78 | content: "\e00b"; 79 | } 80 | .socicon-battlenet:before { 81 | content: "\e00c"; 82 | } 83 | .socicon-mixer:before { 84 | content: "\e00d"; 85 | } 86 | .socicon-bebee:before { 87 | content: "\e00e"; 88 | } 89 | .socicon-bebo:before { 90 | content: "\e00f"; 91 | } 92 | .socicon-behance:before { 93 | content: "\e010"; 94 | } 95 | .socicon-blizzard:before { 96 | content: "\e011"; 97 | } 98 | .socicon-blogger:before { 99 | content: "\e012"; 100 | } 101 | .socicon-buffer:before { 102 | content: "\e013"; 103 | } 104 | .socicon-chrome:before { 105 | content: "\e014"; 106 | } 107 | .socicon-coderwall:before { 108 | content: "\e015"; 109 | } 110 | .socicon-curse:before { 111 | content: "\e016"; 112 | } 113 | .socicon-dailymotion:before { 114 | content: "\e017"; 115 | } 116 | .socicon-deezer:before { 117 | content: "\e018"; 118 | } 119 | .socicon-delicious:before { 120 | content: "\e019"; 121 | } 122 | .socicon-deviantart:before { 123 | content: "\e01a"; 124 | } 125 | .socicon-diablo:before { 126 | content: "\e01b"; 127 | } 128 | .socicon-digg:before { 129 | content: "\e01c"; 130 | } 131 | .socicon-discord:before { 132 | content: "\e01d"; 133 | } 134 | .socicon-disqus:before { 135 | content: "\e01e"; 136 | } 137 | .socicon-douban:before { 138 | content: "\e01f"; 139 | } 140 | .socicon-draugiem:before { 141 | content: "\e020"; 142 | } 143 | .socicon-dribbble:before { 144 | content: "\e021"; 145 | } 146 | .socicon-drupal:before { 147 | content: "\e022"; 148 | } 149 | .socicon-ebay:before { 150 | content: "\e023"; 151 | } 152 | .socicon-ello:before { 153 | content: "\e024"; 154 | } 155 | .socicon-endomodo:before { 156 | content: "\e025"; 157 | } 158 | .socicon-envato:before { 159 | content: "\e026"; 160 | } 161 | .socicon-etsy:before { 162 | content: "\e027"; 163 | } 164 | .socicon-facebook:before { 165 | content: "\e028"; 166 | } 167 | .socicon-feedburner:before { 168 | content: "\e029"; 169 | } 170 | .socicon-filmweb:before { 171 | content: "\e02a"; 172 | } 173 | .socicon-firefox:before { 174 | content: "\e02b"; 175 | } 176 | .socicon-flattr:before { 177 | content: "\e02c"; 178 | } 179 | .socicon-flickr:before { 180 | content: "\e02d"; 181 | } 182 | .socicon-formulr:before { 183 | content: "\e02e"; 184 | } 185 | .socicon-forrst:before { 186 | content: "\e02f"; 187 | } 188 | .socicon-foursquare:before { 189 | content: "\e030"; 190 | } 191 | .socicon-friendfeed:before { 192 | content: "\e031"; 193 | } 194 | .socicon-github:before { 195 | content: "\e032"; 196 | } 197 | .socicon-goodreads:before { 198 | content: "\e033"; 199 | } 200 | .socicon-google:before { 201 | content: "\e034"; 202 | } 203 | .socicon-googlescholar:before { 204 | content: "\e035"; 205 | } 206 | .socicon-googlegroups:before { 207 | content: "\e036"; 208 | } 209 | .socicon-googlephotos:before { 210 | content: "\e037"; 211 | } 212 | .socicon-googleplus:before { 213 | content: "\e038"; 214 | } 215 | .socicon-grooveshark:before { 216 | content: "\e039"; 217 | } 218 | .socicon-hackerrank:before { 219 | content: "\e03a"; 220 | } 221 | .socicon-hearthstone:before { 222 | content: "\e03b"; 223 | } 224 | .socicon-hellocoton:before { 225 | content: "\e03c"; 226 | } 227 | .socicon-heroes:before { 228 | content: "\e03d"; 229 | } 230 | .socicon-smashcast:before { 231 | content: "\e03e"; 232 | } 233 | .socicon-horde:before { 234 | content: "\e03f"; 235 | } 236 | .socicon-houzz:before { 237 | content: "\e040"; 238 | } 239 | .socicon-icq:before { 240 | content: "\e041"; 241 | } 242 | .socicon-identica:before { 243 | content: "\e042"; 244 | } 245 | .socicon-imdb:before { 246 | content: "\e043"; 247 | } 248 | .socicon-instagram:before { 249 | content: "\e044"; 250 | } 251 | .socicon-issuu:before { 252 | content: "\e045"; 253 | } 254 | .socicon-istock:before { 255 | content: "\e046"; 256 | } 257 | .socicon-itunes:before { 258 | content: "\e047"; 259 | } 260 | .socicon-keybase:before { 261 | content: "\e048"; 262 | } 263 | .socicon-lanyrd:before { 264 | content: "\e049"; 265 | } 266 | .socicon-lastfm:before { 267 | content: "\e04a"; 268 | } 269 | .socicon-line:before { 270 | content: "\e04b"; 271 | } 272 | .socicon-linkedin:before { 273 | content: "\e04c"; 274 | } 275 | .socicon-livejournal:before { 276 | content: "\e04d"; 277 | } 278 | .socicon-lyft:before { 279 | content: "\e04e"; 280 | } 281 | .socicon-macos:before { 282 | content: "\e04f"; 283 | } 284 | .socicon-mail:before { 285 | content: "\e050"; 286 | } 287 | .socicon-medium:before { 288 | content: "\e051"; 289 | } 290 | .socicon-meetup:before { 291 | content: "\e052"; 292 | } 293 | .socicon-mixcloud:before { 294 | content: "\e053"; 295 | } 296 | .socicon-modelmayhem:before { 297 | content: "\e054"; 298 | } 299 | .socicon-mumble:before { 300 | content: "\e055"; 301 | } 302 | .socicon-myspace:before { 303 | content: "\e056"; 304 | } 305 | .socicon-newsvine:before { 306 | content: "\e057"; 307 | } 308 | .socicon-nintendo:before { 309 | content: "\e058"; 310 | } 311 | .socicon-npm:before { 312 | content: "\e059"; 313 | } 314 | .socicon-odnoklassniki:before { 315 | content: "\e05a"; 316 | } 317 | .socicon-openid:before { 318 | content: "\e05b"; 319 | } 320 | .socicon-opera:before { 321 | content: "\e05c"; 322 | } 323 | .socicon-outlook:before { 324 | content: "\e05d"; 325 | } 326 | .socicon-overwatch:before { 327 | content: "\e05e"; 328 | } 329 | .socicon-patreon:before { 330 | content: "\e05f"; 331 | } 332 | .socicon-paypal:before { 333 | content: "\e060"; 334 | } 335 | .socicon-periscope:before { 336 | content: "\e061"; 337 | } 338 | .socicon-persona:before { 339 | content: "\e062"; 340 | } 341 | .socicon-pinterest:before { 342 | content: "\e063"; 343 | } 344 | .socicon-play:before { 345 | content: "\e064"; 346 | } 347 | .socicon-player:before { 348 | content: "\e065"; 349 | } 350 | .socicon-playstation:before { 351 | content: "\e066"; 352 | } 353 | .socicon-pocket:before { 354 | content: "\e067"; 355 | } 356 | .socicon-qq:before { 357 | content: "\e068"; 358 | } 359 | .socicon-quora:before { 360 | content: "\e069"; 361 | } 362 | .socicon-raidcall:before { 363 | content: "\e06a"; 364 | } 365 | .socicon-ravelry:before { 366 | content: "\e06b"; 367 | } 368 | .socicon-reddit:before { 369 | content: "\e06c"; 370 | } 371 | .socicon-renren:before { 372 | content: "\e06d"; 373 | } 374 | .socicon-researchgate:before { 375 | content: "\e06e"; 376 | } 377 | .socicon-residentadvisor:before { 378 | content: "\e06f"; 379 | } 380 | .socicon-reverbnation:before { 381 | content: "\e070"; 382 | } 383 | .socicon-rss:before { 384 | content: "\e071"; 385 | } 386 | .socicon-sharethis:before { 387 | content: "\e072"; 388 | } 389 | .socicon-skype:before { 390 | content: "\e073"; 391 | } 392 | .socicon-slideshare:before { 393 | content: "\e074"; 394 | } 395 | .socicon-smugmug:before { 396 | content: "\e075"; 397 | } 398 | .socicon-snapchat:before { 399 | content: "\e076"; 400 | } 401 | .socicon-songkick:before { 402 | content: "\e077"; 403 | } 404 | .socicon-soundcloud:before { 405 | content: "\e078"; 406 | } 407 | .socicon-spotify:before { 408 | content: "\e079"; 409 | } 410 | .socicon-stackexchange:before { 411 | content: "\e07a"; 412 | } 413 | .socicon-stackoverflow:before { 414 | content: "\e07b"; 415 | } 416 | .socicon-starcraft:before { 417 | content: "\e07c"; 418 | } 419 | .socicon-stayfriends:before { 420 | content: "\e07d"; 421 | } 422 | .socicon-steam:before { 423 | content: "\e07e"; 424 | } 425 | .socicon-storehouse:before { 426 | content: "\e07f"; 427 | } 428 | .socicon-strava:before { 429 | content: "\e080"; 430 | } 431 | .socicon-streamjar:before { 432 | content: "\e081"; 433 | } 434 | .socicon-stumbleupon:before { 435 | content: "\e082"; 436 | } 437 | .socicon-swarm:before { 438 | content: "\e083"; 439 | } 440 | .socicon-teamspeak:before { 441 | content: "\e084"; 442 | } 443 | .socicon-teamviewer:before { 444 | content: "\e085"; 445 | } 446 | .socicon-technorati:before { 447 | content: "\e086"; 448 | } 449 | .socicon-telegram:before { 450 | content: "\e087"; 451 | } 452 | .socicon-tripadvisor:before { 453 | content: "\e088"; 454 | } 455 | .socicon-tripit:before { 456 | content: "\e089"; 457 | } 458 | .socicon-triplej:before { 459 | content: "\e08a"; 460 | } 461 | .socicon-tumblr:before { 462 | content: "\e08b"; 463 | } 464 | .socicon-twitch:before { 465 | content: "\e08c"; 466 | } 467 | .socicon-twitter:before { 468 | content: "\e08d"; 469 | } 470 | .socicon-uber:before { 471 | content: "\e08e"; 472 | } 473 | .socicon-ventrilo:before { 474 | content: "\e08f"; 475 | } 476 | .socicon-viadeo:before { 477 | content: "\e090"; 478 | } 479 | .socicon-viber:before { 480 | content: "\e091"; 481 | } 482 | .socicon-viewbug:before { 483 | content: "\e092"; 484 | } 485 | .socicon-vimeo:before { 486 | content: "\e093"; 487 | } 488 | .socicon-vine:before { 489 | content: "\e094"; 490 | } 491 | .socicon-vkontakte:before { 492 | content: "\e095"; 493 | } 494 | .socicon-warcraft:before { 495 | content: "\e096"; 496 | } 497 | .socicon-wechat:before { 498 | content: "\e097"; 499 | } 500 | .socicon-weibo:before { 501 | content: "\e098"; 502 | } 503 | .socicon-whatsapp:before { 504 | content: "\e099"; 505 | } 506 | .socicon-wikipedia:before { 507 | content: "\e09a"; 508 | } 509 | .socicon-windows:before { 510 | content: "\e09b"; 511 | } 512 | .socicon-wordpress:before { 513 | content: "\e09c"; 514 | } 515 | .socicon-wykop:before { 516 | content: "\e09d"; 517 | } 518 | .socicon-xbox:before { 519 | content: "\e09e"; 520 | } 521 | .socicon-xing:before { 522 | content: "\e09f"; 523 | } 524 | .socicon-yahoo:before { 525 | content: "\e0a0"; 526 | } 527 | .socicon-yammer:before { 528 | content: "\e0a1"; 529 | } 530 | .socicon-yandex:before { 531 | content: "\e0a2"; 532 | } 533 | .socicon-yelp:before { 534 | content: "\e0a3"; 535 | } 536 | .socicon-younow:before { 537 | content: "\e0a4"; 538 | } 539 | .socicon-youtube:before { 540 | content: "\e0a5"; 541 | } 542 | .socicon-zapier:before { 543 | content: "\e0a6"; 544 | } 545 | .socicon-zerply:before { 546 | content: "\e0a7"; 547 | } 548 | .socicon-zomato:before { 549 | content: "\e0a8"; 550 | } 551 | .socicon-zynga:before { 552 | content: "\e0a9"; 553 | } 554 | .socicon-spreadshirt:before { 555 | content: "\e901"; 556 | } 557 | .socicon-gamejolt:before { 558 | content: "\e902"; 559 | } 560 | .socicon-trello:before { 561 | content: "\e903"; 562 | } 563 | .socicon-tunein:before { 564 | content: "\e904"; 565 | } 566 | .socicon-bloglovin:before { 567 | content: "\e905"; 568 | } 569 | .socicon-gamewisp:before { 570 | content: "\e906"; 571 | } 572 | .socicon-messenger:before { 573 | content: "\e907"; 574 | } 575 | .socicon-pandora:before { 576 | content: "\e908"; 577 | } 578 | .socicon-augment:before { 579 | content: "\e909"; 580 | } 581 | .socicon-bitbucket:before { 582 | content: "\e90a"; 583 | } 584 | .socicon-fyuse:before { 585 | content: "\e90b"; 586 | } 587 | .socicon-yt-gaming:before { 588 | content: "\e90c"; 589 | } 590 | .socicon-sketchfab:before { 591 | content: "\e90d"; 592 | } 593 | .socicon-mobcrush:before { 594 | content: "\e90e"; 595 | } 596 | .socicon-microsoft:before { 597 | content: "\e90f"; 598 | } 599 | .socicon-realtor:before { 600 | content: "\e910"; 601 | } 602 | .socicon-tidal:before { 603 | content: "\e911"; 604 | } 605 | .socicon-qobuz:before { 606 | content: "\e912"; 607 | } 608 | .socicon-natgeo:before { 609 | content: "\e913"; 610 | } 611 | .socicon-mastodon:before { 612 | content: "\e914"; 613 | } 614 | .socicon-unsplash:before { 615 | content: "\e915"; 616 | } 617 | .socicon-homeadvisor:before { 618 | content: "\e916"; 619 | } 620 | .socicon-angieslist:before { 621 | content: "\e917"; 622 | } 623 | .socicon-codepen:before { 624 | content: "\e918"; 625 | } 626 | .socicon-slack:before { 627 | content: "\e919"; 628 | } 629 | .socicon-openaigym:before { 630 | content: "\e91a"; 631 | } 632 | .socicon-logmein:before { 633 | content: "\e91b"; 634 | } 635 | .socicon-fiverr:before { 636 | content: "\e91c"; 637 | } 638 | .socicon-gotomeeting:before { 639 | content: "\e91d"; 640 | } 641 | .socicon-aliexpress:before { 642 | content: "\e91e"; 643 | } 644 | .socicon-guru:before { 645 | content: "\e91f"; 646 | } 647 | .socicon-appstore:before { 648 | content: "\e920"; 649 | } 650 | .socicon-homes:before { 651 | content: "\e921"; 652 | } 653 | .socicon-zoom:before { 654 | content: "\e922"; 655 | } 656 | .socicon-alibaba:before { 657 | content: "\e923"; 658 | } 659 | .socicon-craigslist:before { 660 | content: "\e924"; 661 | } 662 | .socicon-wix:before { 663 | content: "\e925"; 664 | } 665 | .socicon-redfin:before { 666 | content: "\e926"; 667 | } 668 | .socicon-googlecalendar:before { 669 | content: "\e927"; 670 | } 671 | .socicon-shopify:before { 672 | content: "\e928"; 673 | } 674 | .socicon-freelancer:before { 675 | content: "\e929"; 676 | } 677 | .socicon-seedrs:before { 678 | content: "\e92a"; 679 | } 680 | .socicon-bing:before { 681 | content: "\e92b"; 682 | } 683 | .socicon-doodle:before { 684 | content: "\e92c"; 685 | } 686 | .socicon-bonanza:before { 687 | content: "\e92d"; 688 | } 689 | .socicon-squarespace:before { 690 | content: "\e92e"; 691 | } 692 | .socicon-toptal:before { 693 | content: "\e92f"; 694 | } 695 | .socicon-gust:before { 696 | content: "\e930"; 697 | } 698 | .socicon-ask:before { 699 | content: "\e931"; 700 | } 701 | .socicon-trulia:before { 702 | content: "\e932"; 703 | } 704 | .socicon-loomly:before { 705 | content: "\e933"; 706 | } 707 | .socicon-ghost:before { 708 | content: "\e934"; 709 | } 710 | .socicon-upwork:before { 711 | content: "\e935"; 712 | } 713 | .socicon-fundable:before { 714 | content: "\e936"; 715 | } 716 | .socicon-booking:before { 717 | content: "\e937"; 718 | } 719 | .socicon-googlemaps:before { 720 | content: "\e938"; 721 | } 722 | .socicon-zillow:before { 723 | content: "\e939"; 724 | } 725 | .socicon-niconico:before { 726 | content: "\e93a"; 727 | } 728 | .socicon-toneden:before { 729 | content: "\e93b"; 730 | } 731 | .socicon-crunchbase:before { 732 | content: "\e93c"; 733 | } 734 | .socicon-homefy:before { 735 | content: "\e93d"; 736 | } 737 | .socicon-calendly:before { 738 | content: "\e93e"; 739 | } 740 | .socicon-livemaster:before { 741 | content: "\e93f"; 742 | } 743 | .socicon-udemy:before { 744 | content: "\e940"; 745 | } 746 | .socicon-codered:before { 747 | content: "\e941"; 748 | } 749 | .socicon-origin:before { 750 | content: "\e942"; 751 | } 752 | .socicon-nextdoor:before { 753 | content: "\e943"; 754 | } 755 | .socicon-portfolio:before { 756 | content: "\e944"; 757 | } 758 | .socicon-instructables:before { 759 | content: "\e945"; 760 | } 761 | .socicon-gitlab:before { 762 | content: "\e946"; 763 | } 764 | .socicon-hackernews:before { 765 | content: "\e947"; 766 | } 767 | .socicon-smashwords:before { 768 | content: "\e948"; 769 | } 770 | .socicon-kobo:before { 771 | content: "\e949"; 772 | } 773 | .socicon-bookbub:before { 774 | content: "\e94a"; 775 | } 776 | .socicon-mailru:before { 777 | content: "\e94b"; 778 | } 779 | .socicon-moddb:before { 780 | content: "\e94c"; 781 | } 782 | .socicon-indiedb:before { 783 | content: "\e94d"; 784 | } 785 | .socicon-traxsource:before { 786 | content: "\e94e"; 787 | } 788 | .socicon-gamefor:before { 789 | content: "\e94f"; 790 | } 791 | .socicon-pixiv:before { 792 | content: "\e950"; 793 | } 794 | .socicon-myanimelist:before { 795 | content: "\e951"; 796 | } 797 | .socicon-blackberry:before { 798 | content: "\e952"; 799 | } 800 | .socicon-wickr:before { 801 | content: "\e953"; 802 | } 803 | .socicon-spip:before { 804 | content: "\e954"; 805 | } 806 | .socicon-napster:before { 807 | content: "\e955"; 808 | } 809 | .socicon-beatport:before { 810 | content: "\e956"; 811 | } 812 | .socicon-hackerone:before { 813 | content: "\e957"; 814 | } 815 | .socicon-internet:before { 816 | content: "\e958"; 817 | } 818 | .socicon-ubuntu:before { 819 | content: "\e959"; 820 | } 821 | .socicon-artstation:before { 822 | content: "\e95a"; 823 | } 824 | .socicon-invision:before { 825 | content: "\e95b"; 826 | } 827 | .socicon-torial:before { 828 | content: "\e95c"; 829 | } 830 | .socicon-collectorz:before { 831 | content: "\e95d"; 832 | } 833 | .socicon-seenthis:before { 834 | content: "\e95e"; 835 | } 836 | .socicon-googleplaymusic:before { 837 | content: "\e95f"; 838 | } 839 | .socicon-debian:before { 840 | content: "\e960"; 841 | } 842 | .socicon-filmfreeway:before { 843 | content: "\e961"; 844 | } 845 | .socicon-gnome:before { 846 | content: "\e962"; 847 | } 848 | .socicon-itchio:before { 849 | content: "\e963"; 850 | } 851 | .socicon-jamendo:before { 852 | content: "\e964"; 853 | } 854 | .socicon-mix:before { 855 | content: "\e965"; 856 | } 857 | .socicon-sharepoint:before { 858 | content: "\e966"; 859 | } 860 | .socicon-tinder:before { 861 | content: "\e967"; 862 | } 863 | .socicon-windguru:before { 864 | content: "\e968"; 865 | } 866 | .socicon-cdbaby:before { 867 | content: "\e969"; 868 | } 869 | .socicon-elementaryos:before { 870 | content: "\e96a"; 871 | } 872 | .socicon-stage32:before { 873 | content: "\e96b"; 874 | } 875 | .socicon-tiktok:before { 876 | content: "\e96c"; 877 | } 878 | .socicon-gitter:before { 879 | content: "\e96d"; 880 | } 881 | .socicon-letterboxd:before { 882 | content: "\e96e"; 883 | } 884 | .socicon-threema:before { 885 | content: "\e96f"; 886 | } 887 | .socicon-splice:before { 888 | content: "\e970"; 889 | } 890 | .socicon-metapop:before { 891 | content: "\e971"; 892 | } 893 | .socicon-naver:before { 894 | content: "\e972"; 895 | } 896 | .socicon-remote:before { 897 | content: "\e973"; 898 | } 899 | .socicon-flipboard:before { 900 | content: "\e974"; 901 | } 902 | .socicon-googlehangouts:before { 903 | content: "\e975"; 904 | } 905 | .socicon-dlive:before { 906 | content: "\e976"; 907 | } 908 | .socicon-vsco:before { 909 | content: "\e977"; 910 | } 911 | .socicon-stitcher:before { 912 | content: "\e978"; 913 | } 914 | .socicon-avvo:before { 915 | content: "\e979"; 916 | } 917 | .socicon-redbubble:before { 918 | content: "\e97a"; 919 | } 920 | .socicon-society6:before { 921 | content: "\e97b"; 922 | } 923 | .socicon-zazzle:before { 924 | content: "\e97c"; 925 | } 926 | .socicon-eitaa:before { 927 | content: "\e97d"; 928 | } 929 | .socicon-soroush:before { 930 | content: "\e97e"; 931 | } 932 | .socicon-bale:before { 933 | content: "\e97f"; 934 | } 935 | -------------------------------------------------------------------------------- /frontend/templates/assets/socicon/fonts/socicon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/socicon/fonts/socicon.eot -------------------------------------------------------------------------------- /frontend/templates/assets/socicon/fonts/socicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/socicon/fonts/socicon.ttf -------------------------------------------------------------------------------- /frontend/templates/assets/socicon/fonts/socicon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/socicon/fonts/socicon.woff -------------------------------------------------------------------------------- /frontend/templates/assets/socicon/fonts/socicon.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/socicon/fonts/socicon.woff2 -------------------------------------------------------------------------------- /frontend/templates/assets/theme/css/style.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | section { 3 | background-color: #ffffff; 4 | } 5 | 6 | body { 7 | font-style: normal; 8 | line-height: 1.5; 9 | font-weight: 400; 10 | color: #232323; 11 | position: relative; 12 | } 13 | 14 | button { 15 | background-color: transparent; 16 | border-color: transparent; 17 | } 18 | 19 | .embla__button, 20 | .carousel-control { 21 | background-color: #edefea !important; 22 | opacity: 0.8 !important; 23 | color: #464845 !important; 24 | border-color: #edefea !important; 25 | } 26 | 27 | .carousel .close, 28 | .modalWindow .close { 29 | background-color: #edefea !important; 30 | color: #464845 !important; 31 | border-color: #edefea !important; 32 | opacity: 0.8 !important; 33 | } 34 | 35 | .carousel .close:hover, 36 | .modalWindow .close:hover { 37 | opacity: 1 !important; 38 | } 39 | 40 | .carousel-indicators li { 41 | background-color: #edefea !important; 42 | border: 2px solid #464845 !important; 43 | } 44 | 45 | .carousel-indicators li:hover, 46 | .carousel-indicators li:active { 47 | opacity: 0.8 !important; 48 | } 49 | 50 | .embla__button:hover, 51 | .carousel-control:hover { 52 | background-color: #edefea !important; 53 | opacity: 1 !important; 54 | } 55 | 56 | .modalWindow-video-container { 57 | height: 80%; 58 | } 59 | 60 | section, 61 | .container, 62 | .container-fluid { 63 | position: relative; 64 | word-wrap: break-word; 65 | } 66 | 67 | a.mbr-iconfont:hover { 68 | text-decoration: none; 69 | } 70 | 71 | .article .lead p, 72 | .article .lead ul, 73 | .article .lead ol, 74 | .article .lead pre, 75 | .article .lead blockquote { 76 | margin-bottom: 0; 77 | } 78 | 79 | a { 80 | font-style: normal; 81 | font-weight: 400; 82 | cursor: pointer; 83 | } 84 | a, a:hover { 85 | text-decoration: none; 86 | } 87 | 88 | .mbr-section-title { 89 | font-style: normal; 90 | line-height: 1.3; 91 | } 92 | 93 | .mbr-section-subtitle { 94 | line-height: 1.3; 95 | } 96 | 97 | .mbr-text { 98 | font-style: normal; 99 | line-height: 1.7; 100 | } 101 | 102 | h1, 103 | h2, 104 | h3, 105 | h4, 106 | h5, 107 | h6, 108 | .display-1, 109 | .display-2, 110 | .display-4, 111 | .display-5, 112 | .display-7, 113 | span, 114 | p, 115 | a { 116 | line-height: 1; 117 | word-break: break-word; 118 | word-wrap: break-word; 119 | font-weight: 400; 120 | } 121 | 122 | b, 123 | strong { 124 | font-weight: bold; 125 | } 126 | 127 | input:-webkit-autofill, input:-webkit-autofill:hover, input:-webkit-autofill:focus, input:-webkit-autofill:active { 128 | transition-delay: 9999s; 129 | -webkit-transition-property: background-color, color; 130 | transition-property: background-color, color; 131 | } 132 | 133 | textarea[type=hidden] { 134 | display: none; 135 | } 136 | 137 | section { 138 | background-position: 50% 50%; 139 | background-repeat: no-repeat; 140 | background-size: cover; 141 | } 142 | section .mbr-background-video, 143 | section .mbr-background-video-preview { 144 | position: absolute; 145 | bottom: 0; 146 | left: 0; 147 | right: 0; 148 | top: 0; 149 | } 150 | 151 | .hidden { 152 | visibility: hidden; 153 | } 154 | 155 | .mbr-z-index20 { 156 | z-index: 20; 157 | } 158 | 159 | /*! Base colors */ 160 | .mbr-white { 161 | color: #ffffff; 162 | } 163 | 164 | .mbr-black { 165 | color: #111111; 166 | } 167 | 168 | .mbr-bg-white { 169 | background-color: #ffffff; 170 | } 171 | 172 | .mbr-bg-black { 173 | background-color: #000000; 174 | } 175 | 176 | /*! Text-aligns */ 177 | .align-left { 178 | text-align: left; 179 | } 180 | 181 | .align-center { 182 | text-align: center; 183 | } 184 | 185 | .align-right { 186 | text-align: right; 187 | } 188 | 189 | /*! Font-weight */ 190 | .mbr-light { 191 | font-weight: 300; 192 | } 193 | 194 | .mbr-regular { 195 | font-weight: 400; 196 | } 197 | 198 | .mbr-semibold { 199 | font-weight: 500; 200 | } 201 | 202 | .mbr-bold { 203 | font-weight: 700; 204 | } 205 | 206 | /*! Media */ 207 | .media-content { 208 | flex-basis: 100%; 209 | } 210 | 211 | .media-container-row { 212 | display: flex; 213 | flex-direction: row; 214 | flex-wrap: wrap; 215 | justify-content: center; 216 | align-content: center; 217 | align-items: start; 218 | } 219 | .media-container-row .media-size-item { 220 | width: 400px; 221 | } 222 | 223 | .media-container-column { 224 | display: flex; 225 | flex-direction: column; 226 | flex-wrap: wrap; 227 | justify-content: center; 228 | align-content: center; 229 | align-items: stretch; 230 | } 231 | .media-container-column > * { 232 | width: 100%; 233 | } 234 | 235 | @media (min-width: 992px) { 236 | .media-container-row { 237 | flex-wrap: nowrap; 238 | } 239 | } 240 | figure { 241 | margin-bottom: 0; 242 | overflow: hidden; 243 | } 244 | 245 | figure[mbr-media-size] { 246 | transition: width 0.1s; 247 | } 248 | 249 | img, 250 | iframe { 251 | display: block; 252 | width: 100%; 253 | } 254 | 255 | .card { 256 | background-color: transparent; 257 | border: none; 258 | } 259 | 260 | .card-box { 261 | width: 100%; 262 | } 263 | 264 | .card-img { 265 | text-align: center; 266 | flex-shrink: 0; 267 | -webkit-flex-shrink: 0; 268 | } 269 | 270 | .media { 271 | max-width: 100%; 272 | margin: 0 auto; 273 | } 274 | 275 | .mbr-figure { 276 | align-self: center; 277 | } 278 | 279 | .media-container > div { 280 | max-width: 100%; 281 | } 282 | 283 | .mbr-figure img, 284 | .card-img img { 285 | width: 100%; 286 | } 287 | 288 | @media (max-width: 991px) { 289 | .media-size-item { 290 | width: auto !important; 291 | } 292 | .media { 293 | width: auto; 294 | } 295 | .mbr-figure { 296 | width: 100% !important; 297 | } 298 | } 299 | /*! Buttons */ 300 | .mbr-section-btn { 301 | margin-left: -0.6rem; 302 | margin-right: -0.6rem; 303 | font-size: 0; 304 | } 305 | 306 | .btn { 307 | font-weight: 600; 308 | border-width: 1px; 309 | font-style: normal; 310 | margin: 0.6rem 0.6rem; 311 | white-space: normal; 312 | transition: all 0.2s ease-in-out; 313 | display: inline-flex; 314 | align-items: center; 315 | justify-content: center; 316 | word-break: break-word; 317 | } 318 | 319 | .btn-sm { 320 | font-weight: 600; 321 | letter-spacing: 0px; 322 | transition: all 0.3s ease-in-out; 323 | } 324 | 325 | .btn-md { 326 | font-weight: 600; 327 | letter-spacing: 0px; 328 | transition: all 0.3s ease-in-out; 329 | } 330 | 331 | .btn-lg { 332 | font-weight: 600; 333 | letter-spacing: 0px; 334 | transition: all 0.3s ease-in-out; 335 | } 336 | 337 | .btn-form { 338 | margin: 0; 339 | } 340 | .btn-form:hover { 341 | cursor: pointer; 342 | } 343 | 344 | nav .mbr-section-btn { 345 | margin-left: 0rem; 346 | margin-right: 0rem; 347 | } 348 | 349 | /*! Btn icon margin */ 350 | .btn .mbr-iconfont, 351 | .btn.btn-sm .mbr-iconfont { 352 | order: 1; 353 | cursor: pointer; 354 | margin-left: 0.5rem; 355 | vertical-align: sub; 356 | } 357 | 358 | .btn.btn-md .mbr-iconfont, 359 | .btn.btn-md .mbr-iconfont { 360 | margin-left: 0.8rem; 361 | } 362 | 363 | .mbr-regular { 364 | font-weight: 400; 365 | } 366 | 367 | .mbr-semibold { 368 | font-weight: 500; 369 | } 370 | 371 | .mbr-bold { 372 | font-weight: 700; 373 | } 374 | 375 | [type=submit] { 376 | -webkit-appearance: none; 377 | } 378 | 379 | /*! Full-screen */ 380 | .mbr-fullscreen .mbr-overlay { 381 | min-height: 100vh; 382 | } 383 | 384 | .mbr-fullscreen { 385 | display: flex; 386 | display: -moz-flex; 387 | display: -ms-flex; 388 | display: -o-flex; 389 | align-items: center; 390 | min-height: 100vh; 391 | padding-top: 3rem; 392 | padding-bottom: 3rem; 393 | } 394 | 395 | /*! Map */ 396 | .map { 397 | height: 25rem; 398 | position: relative; 399 | } 400 | .map iframe { 401 | width: 100%; 402 | height: 100%; 403 | } 404 | 405 | /*! Scroll to top arrow */ 406 | .mbr-arrow-up { 407 | bottom: 25px; 408 | right: 90px; 409 | position: fixed; 410 | text-align: right; 411 | z-index: 5000; 412 | color: #ffffff; 413 | font-size: 22px; 414 | } 415 | 416 | .mbr-arrow-up a { 417 | background: rgba(0, 0, 0, 0.2); 418 | border-radius: 50%; 419 | color: #fff; 420 | display: inline-block; 421 | height: 60px; 422 | width: 60px; 423 | border: 2px solid #fff; 424 | outline-style: none !important; 425 | position: relative; 426 | text-decoration: none; 427 | transition: all 0.3s ease-in-out; 428 | cursor: pointer; 429 | text-align: center; 430 | } 431 | .mbr-arrow-up a:hover { 432 | background-color: rgba(0, 0, 0, 0.4); 433 | } 434 | .mbr-arrow-up a i { 435 | line-height: 60px; 436 | } 437 | 438 | .mbr-arrow-up-icon { 439 | display: block; 440 | color: #fff; 441 | } 442 | 443 | .mbr-arrow-up-icon::before { 444 | content: "›"; 445 | display: inline-block; 446 | font-family: serif; 447 | font-size: 22px; 448 | line-height: 1; 449 | font-style: normal; 450 | position: relative; 451 | top: 6px; 452 | left: -4px; 453 | transform: rotate(-90deg); 454 | } 455 | 456 | /*! Arrow Down */ 457 | .mbr-arrow { 458 | position: absolute; 459 | bottom: 45px; 460 | left: 50%; 461 | width: 60px; 462 | height: 60px; 463 | cursor: pointer; 464 | background-color: rgba(80, 80, 80, 0.5); 465 | border-radius: 50%; 466 | transform: translateX(-50%); 467 | } 468 | @media (max-width: 767px) { 469 | .mbr-arrow { 470 | display: none; 471 | } 472 | } 473 | .mbr-arrow > a { 474 | display: inline-block; 475 | text-decoration: none; 476 | outline-style: none; 477 | animation: arrowdown 1.7s ease-in-out infinite; 478 | color: #ffffff; 479 | } 480 | .mbr-arrow > a > i { 481 | position: absolute; 482 | top: -2px; 483 | left: 15px; 484 | font-size: 2rem; 485 | } 486 | 487 | #scrollToTop a i::before { 488 | content: ""; 489 | position: absolute; 490 | display: block; 491 | border-bottom: 2.5px solid #fff; 492 | border-left: 2.5px solid #fff; 493 | width: 27.8%; 494 | height: 27.8%; 495 | left: 50%; 496 | top: 51%; 497 | transform: translateY(-30%) translateX(-50%) rotate(135deg); 498 | } 499 | 500 | @keyframes arrowdown { 501 | 0% { 502 | transform: translateY(0px); 503 | } 504 | 50% { 505 | transform: translateY(-5px); 506 | } 507 | 100% { 508 | transform: translateY(0px); 509 | } 510 | } 511 | @media (max-width: 500px) { 512 | .mbr-arrow-up { 513 | left: 0; 514 | right: 0; 515 | text-align: center; 516 | } 517 | } 518 | /*Gradients animation*/ 519 | @keyframes gradient-animation { 520 | from { 521 | background-position: 0% 100%; 522 | animation-timing-function: ease-in-out; 523 | } 524 | to { 525 | background-position: 100% 0%; 526 | animation-timing-function: ease-in-out; 527 | } 528 | } 529 | .bg-gradient { 530 | background-size: 200% 200%; 531 | animation: gradient-animation 5s infinite alternate; 532 | -webkit-animation: gradient-animation 5s infinite alternate; 533 | } 534 | 535 | .menu .navbar-brand { 536 | display: -webkit-flex; 537 | } 538 | .menu .navbar-brand span { 539 | display: flex; 540 | display: -webkit-flex; 541 | } 542 | .menu .navbar-brand .navbar-caption-wrap { 543 | display: -webkit-flex; 544 | } 545 | .menu .navbar-brand .navbar-logo img { 546 | display: -webkit-flex; 547 | width: auto; 548 | } 549 | @media (min-width: 768px) and (max-width: 991px) { 550 | .menu .navbar-toggleable-sm .navbar-nav { 551 | display: -ms-flexbox; 552 | } 553 | } 554 | @media (max-width: 991px) { 555 | .menu .navbar-collapse { 556 | max-height: 93.5vh; 557 | } 558 | .menu .navbar-collapse.show { 559 | overflow: auto; 560 | } 561 | } 562 | @media (min-width: 992px) { 563 | .menu .navbar-nav.nav-dropdown { 564 | display: -webkit-flex; 565 | } 566 | .menu .navbar-toggleable-sm .navbar-collapse { 567 | display: -webkit-flex !important; 568 | } 569 | .menu .collapsed .navbar-collapse { 570 | max-height: 93.5vh; 571 | } 572 | .menu .collapsed .navbar-collapse.show { 573 | overflow: auto; 574 | } 575 | } 576 | @media (max-width: 767px) { 577 | .menu .navbar-collapse { 578 | max-height: 80vh; 579 | } 580 | } 581 | 582 | .nav-link .mbr-iconfont { 583 | margin-right: 0.5rem; 584 | } 585 | 586 | .navbar { 587 | display: -webkit-flex; 588 | -webkit-flex-wrap: wrap; 589 | -webkit-align-items: center; 590 | -webkit-justify-content: space-between; 591 | } 592 | 593 | .navbar-collapse { 594 | -webkit-flex-basis: 100%; 595 | -webkit-flex-grow: 1; 596 | -webkit-align-items: center; 597 | } 598 | 599 | .nav-dropdown .link { 600 | padding: 0.667em 1.667em !important; 601 | margin: 0 !important; 602 | } 603 | 604 | .nav { 605 | display: -webkit-flex; 606 | -webkit-flex-wrap: wrap; 607 | } 608 | 609 | .row { 610 | display: -webkit-flex; 611 | -webkit-flex-wrap: wrap; 612 | } 613 | 614 | .justify-content-center { 615 | -webkit-justify-content: center; 616 | } 617 | 618 | .form-inline { 619 | display: -webkit-flex; 620 | } 621 | 622 | .card-wrapper { 623 | -webkit-flex: 1; 624 | } 625 | 626 | .carousel-control { 627 | z-index: 10; 628 | display: -webkit-flex; 629 | } 630 | 631 | .carousel-controls { 632 | display: -webkit-flex; 633 | } 634 | 635 | .media { 636 | display: -webkit-flex; 637 | } 638 | 639 | .form-group:focus { 640 | outline: none; 641 | } 642 | 643 | .jq-selectbox__select { 644 | padding: 7px 0; 645 | position: relative; 646 | } 647 | 648 | .jq-selectbox__dropdown { 649 | overflow: hidden; 650 | border-radius: 10px; 651 | position: absolute; 652 | top: 100%; 653 | left: 0 !important; 654 | width: 100% !important; 655 | } 656 | 657 | .jq-selectbox__trigger-arrow { 658 | right: 0; 659 | transform: translateY(-50%); 660 | } 661 | 662 | .jq-selectbox li { 663 | padding: 1.07em 0.5em; 664 | } 665 | 666 | input[type=range] { 667 | padding-left: 0 !important; 668 | padding-right: 0 !important; 669 | } 670 | 671 | .modal-dialog, 672 | .modal-content { 673 | height: 100%; 674 | } 675 | 676 | .modal-dialog .carousel-inner { 677 | height: calc(100vh - 1.75rem); 678 | } 679 | @media (max-width: 575px) { 680 | .modal-dialog .carousel-inner { 681 | height: calc(100vh - 1rem); 682 | } 683 | } 684 | 685 | .carousel-item { 686 | text-align: center; 687 | } 688 | 689 | .carousel-item img { 690 | margin: auto; 691 | } 692 | 693 | .navbar-toggler { 694 | align-self: flex-start; 695 | padding: 0.25rem 0.75rem; 696 | font-size: 1.25rem; 697 | line-height: 1; 698 | background: transparent; 699 | border: 1px solid transparent; 700 | border-radius: 0.25rem; 701 | } 702 | 703 | .navbar-toggler:focus, 704 | .navbar-toggler:hover { 705 | text-decoration: none; 706 | box-shadow: none; 707 | } 708 | 709 | .navbar-toggler-icon { 710 | display: inline-block; 711 | width: 1.5em; 712 | height: 1.5em; 713 | vertical-align: middle; 714 | content: ""; 715 | background: no-repeat center center; 716 | background-size: 100% 100%; 717 | } 718 | 719 | .navbar-toggler-left { 720 | position: absolute; 721 | left: 1rem; 722 | } 723 | 724 | .navbar-toggler-right { 725 | position: absolute; 726 | right: 1rem; 727 | } 728 | 729 | .card-img { 730 | width: auto; 731 | } 732 | 733 | .menu .navbar.collapsed:not(.beta-menu) { 734 | flex-direction: column; 735 | } 736 | 737 | .carousel-item.active, 738 | .carousel-item-next, 739 | .carousel-item-prev { 740 | display: flex; 741 | } 742 | 743 | .note-air-layout .dropup .dropdown-menu, 744 | .note-air-layout .navbar-fixed-bottom .dropdown .dropdown-menu { 745 | bottom: initial !important; 746 | } 747 | 748 | html, 749 | body { 750 | height: auto; 751 | min-height: 100vh; 752 | } 753 | 754 | .dropup .dropdown-toggle::after { 755 | display: none; 756 | } 757 | 758 | .form-asterisk { 759 | font-family: initial; 760 | position: absolute; 761 | top: -2px; 762 | font-weight: normal; 763 | } 764 | 765 | .form-control-label { 766 | position: relative; 767 | cursor: pointer; 768 | margin-bottom: 0.357em; 769 | padding: 0; 770 | } 771 | 772 | .alert { 773 | color: #ffffff; 774 | border-radius: 0; 775 | border: 0; 776 | font-size: 1.1rem; 777 | line-height: 1.5; 778 | margin-bottom: 1.875rem; 779 | padding: 1.25rem; 780 | position: relative; 781 | text-align: center; 782 | } 783 | .alert.alert-form::after { 784 | background-color: inherit; 785 | bottom: -7px; 786 | content: ""; 787 | display: block; 788 | height: 14px; 789 | left: 50%; 790 | margin-left: -7px; 791 | position: absolute; 792 | transform: rotate(45deg); 793 | width: 14px; 794 | } 795 | 796 | .form-control { 797 | background-color: #ffffff; 798 | background-clip: border-box; 799 | color: #232323; 800 | line-height: 1rem !important; 801 | height: auto; 802 | padding: 1.2rem 2rem; 803 | transition: border-color 0.25s ease 0s; 804 | border: 1px solid transparent !important; 805 | border-radius: 4px; 806 | box-shadow: rgba(0, 0, 0, 0.07) 0px 1px 1px 0px, rgba(0, 0, 0, 0.07) 0px 1px 3px 0px, rgba(0, 0, 0, 0.03) 0px 0px 0px 1px; 807 | } 808 | .form-active .form-control:invalid { 809 | border-color: red; 810 | } 811 | 812 | .row > * { 813 | padding-right: 1rem; 814 | padding-left: 1rem; 815 | } 816 | 817 | form .row { 818 | margin-left: -0.6rem; 819 | margin-right: -0.6rem; 820 | } 821 | form .row [class*=col] { 822 | padding-left: 0.6rem; 823 | padding-right: 0.6rem; 824 | } 825 | 826 | form .mbr-section-btn { 827 | padding-left: 0.6rem; 828 | padding-right: 0.6rem; 829 | } 830 | 831 | form .form-check-input { 832 | margin-top: 0.5; 833 | } 834 | 835 | textarea.form-control { 836 | line-height: 1.5rem !important; 837 | } 838 | 839 | .form-group { 840 | margin-bottom: 1.2rem; 841 | } 842 | 843 | .form-control, 844 | form .btn { 845 | min-height: 48px; 846 | } 847 | 848 | .gdpr-block label span.textGDPR input[name=gdpr] { 849 | top: 7px; 850 | } 851 | 852 | .form-control:focus { 853 | box-shadow: none; 854 | } 855 | 856 | :focus { 857 | outline: none; 858 | } 859 | 860 | .mbr-overlay { 861 | background-color: #000; 862 | bottom: 0; 863 | left: 0; 864 | opacity: 0.5; 865 | position: absolute; 866 | right: 0; 867 | top: 0; 868 | z-index: 0; 869 | pointer-events: none; 870 | } 871 | 872 | blockquote { 873 | font-style: italic; 874 | padding: 3rem; 875 | font-size: 1.09rem; 876 | position: relative; 877 | border-left: 3px solid; 878 | } 879 | 880 | ul, 881 | ol, 882 | pre, 883 | blockquote { 884 | margin-bottom: 2.3125rem; 885 | } 886 | 887 | .mt-4 { 888 | margin-top: 2rem !important; 889 | } 890 | 891 | .mb-4 { 892 | margin-bottom: 2rem !important; 893 | } 894 | 895 | .container, 896 | .container-fluid { 897 | padding-left: 16px; 898 | padding-right: 16px; 899 | } 900 | 901 | .row { 902 | margin-left: -16px; 903 | margin-right: -16px; 904 | } 905 | .row > [class*=col] { 906 | padding-left: 16px; 907 | padding-right: 16px; 908 | } 909 | 910 | @media (min-width: 992px) { 911 | .container-fluid { 912 | padding-left: 32px; 913 | padding-right: 32px; 914 | } 915 | } 916 | @media (max-width: 991px) { 917 | .mbr-container { 918 | padding-left: 16px; 919 | padding-right: 16px; 920 | } 921 | } 922 | .app-video-wrapper > img { 923 | opacity: 1; 924 | } 925 | 926 | .app-video-wrapper { 927 | background: transparent; 928 | } 929 | 930 | .item { 931 | position: relative; 932 | } 933 | 934 | .dropdown-menu .dropdown-menu { 935 | left: 100%; 936 | } 937 | 938 | .dropdown-item + .dropdown-menu { 939 | display: none; 940 | } 941 | 942 | .dropdown-item:hover + .dropdown-menu, 943 | .dropdown-menu:hover { 944 | display: block; 945 | } 946 | 947 | @media (min-aspect-ratio: 16/9) { 948 | .mbr-video-foreground { 949 | height: 300% !important; 950 | top: -100% !important; 951 | } 952 | } 953 | @media (max-aspect-ratio: 16/9) { 954 | .mbr-video-foreground { 955 | width: 300% !important; 956 | left: -100% !important; 957 | } 958 | }.engine { 959 | position: absolute; 960 | text-indent: -2400px; 961 | text-align: center; 962 | padding: 0; 963 | top: 0; 964 | left: -2400px; 965 | } -------------------------------------------------------------------------------- /frontend/templates/assets/web/assets/mobirise-icons/mobirise-icons.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'MobiriseIcons'; 3 | src: url('mobirise-icons.eot?spat4u'); 4 | src: url('mobirise-icons.eot?spat4u#iefix') format('embedded-opentype'), 5 | url('mobirise-icons.ttf?spat4u') format('truetype'), 6 | url('mobirise-icons.woff?spat4u') format('woff'), 7 | url('mobirise-icons.svg?spat4u#MobiriseIcons') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | font-display: swap; 11 | } 12 | 13 | [class^="mbri-"], [class*=" mbri-"] { 14 | /* use !important to prevent issues with browser extensions that change fonts */ 15 | font-family: MobiriseIcons !important; 16 | speak: none; 17 | font-style: normal; 18 | font-weight: normal; 19 | font-variant: normal; 20 | text-transform: none; 21 | line-height: 1; 22 | 23 | /* Better Font Rendering =========== */ 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | .mbri-add-submenu:before { 29 | content: "\e900"; 30 | } 31 | .mbri-alert:before { 32 | content: "\e901"; 33 | } 34 | .mbri-align-center:before { 35 | content: "\e902"; 36 | } 37 | .mbri-align-justify:before { 38 | content: "\e903"; 39 | } 40 | .mbri-align-left:before { 41 | content: "\e904"; 42 | } 43 | .mbri-align-right:before { 44 | content: "\e905"; 45 | } 46 | .mbri-android:before { 47 | content: "\e906"; 48 | } 49 | .mbri-apple:before { 50 | content: "\e907"; 51 | } 52 | .mbri-arrow-down:before { 53 | content: "\e908"; 54 | } 55 | .mbri-arrow-next:before { 56 | content: "\e909"; 57 | } 58 | .mbri-arrow-prev:before { 59 | content: "\e90a"; 60 | } 61 | .mbri-arrow-up:before { 62 | content: "\e90b"; 63 | } 64 | .mbri-bold:before { 65 | content: "\e90c"; 66 | } 67 | .mbri-bookmark:before { 68 | content: "\e90d"; 69 | } 70 | .mbri-bootstrap:before { 71 | content: "\e90e"; 72 | } 73 | .mbri-briefcase:before { 74 | content: "\e90f"; 75 | } 76 | .mbri-browse:before { 77 | content: "\e910"; 78 | } 79 | .mbri-bulleted-list:before { 80 | content: "\e911"; 81 | } 82 | .mbri-calendar:before { 83 | content: "\e912"; 84 | } 85 | .mbri-camera:before { 86 | content: "\e913"; 87 | } 88 | .mbri-cart-add:before { 89 | content: "\e914"; 90 | } 91 | .mbri-cart-full:before { 92 | content: "\e915"; 93 | } 94 | .mbri-cash:before { 95 | content: "\e916"; 96 | } 97 | .mbri-change-style:before { 98 | content: "\e917"; 99 | } 100 | .mbri-chat:before { 101 | content: "\e918"; 102 | } 103 | .mbri-clock:before { 104 | content: "\e919"; 105 | } 106 | .mbri-close:before { 107 | content: "\e91a"; 108 | } 109 | .mbri-cloud:before { 110 | content: "\e91b"; 111 | } 112 | .mbri-code:before { 113 | content: "\e91c"; 114 | } 115 | .mbri-contact-form:before { 116 | content: "\e91d"; 117 | } 118 | .mbri-credit-card:before { 119 | content: "\e91e"; 120 | } 121 | .mbri-cursor-click:before { 122 | content: "\e91f"; 123 | } 124 | .mbri-cust-feedback:before { 125 | content: "\e920"; 126 | } 127 | .mbri-database:before { 128 | content: "\e921"; 129 | } 130 | .mbri-delivery:before { 131 | content: "\e922"; 132 | } 133 | .mbri-desktop:before { 134 | content: "\e923"; 135 | } 136 | .mbri-devices:before { 137 | content: "\e924"; 138 | } 139 | .mbri-down:before { 140 | content: "\e925"; 141 | } 142 | .mbri-download:before { 143 | content: "\e989"; 144 | } 145 | .mbri-drag-n-drop:before { 146 | content: "\e927"; 147 | } 148 | .mbri-drag-n-drop2:before { 149 | content: "\e928"; 150 | } 151 | .mbri-edit:before { 152 | content: "\e929"; 153 | } 154 | .mbri-edit2:before { 155 | content: "\e92a"; 156 | } 157 | .mbri-error:before { 158 | content: "\e92b"; 159 | } 160 | .mbri-extension:before { 161 | content: "\e92c"; 162 | } 163 | .mbri-features:before { 164 | content: "\e92d"; 165 | } 166 | .mbri-file:before { 167 | content: "\e92e"; 168 | } 169 | .mbri-flag:before { 170 | content: "\e92f"; 171 | } 172 | .mbri-folder:before { 173 | content: "\e930"; 174 | } 175 | .mbri-gift:before { 176 | content: "\e931"; 177 | } 178 | .mbri-github:before { 179 | content: "\e932"; 180 | } 181 | .mbri-globe:before { 182 | content: "\e933"; 183 | } 184 | .mbri-globe-2:before { 185 | content: "\e934"; 186 | } 187 | .mbri-growing-chart:before { 188 | content: "\e935"; 189 | } 190 | .mbri-hearth:before { 191 | content: "\e936"; 192 | } 193 | .mbri-help:before { 194 | content: "\e937"; 195 | } 196 | .mbri-home:before { 197 | content: "\e938"; 198 | } 199 | .mbri-hot-cup:before { 200 | content: "\e939"; 201 | } 202 | .mbri-idea:before { 203 | content: "\e93a"; 204 | } 205 | .mbri-image-gallery:before { 206 | content: "\e93b"; 207 | } 208 | .mbri-image-slider:before { 209 | content: "\e93c"; 210 | } 211 | .mbri-info:before { 212 | content: "\e93d"; 213 | } 214 | .mbri-italic:before { 215 | content: "\e93e"; 216 | } 217 | .mbri-key:before { 218 | content: "\e93f"; 219 | } 220 | .mbri-laptop:before { 221 | content: "\e940"; 222 | } 223 | .mbri-layers:before { 224 | content: "\e941"; 225 | } 226 | .mbri-left-right:before { 227 | content: "\e942"; 228 | } 229 | .mbri-left:before { 230 | content: "\e943"; 231 | } 232 | .mbri-letter:before { 233 | content: "\e944"; 234 | } 235 | .mbri-like:before { 236 | content: "\e945"; 237 | } 238 | .mbri-link:before { 239 | content: "\e946"; 240 | } 241 | .mbri-lock:before { 242 | content: "\e947"; 243 | } 244 | .mbri-login:before { 245 | content: "\e948"; 246 | } 247 | .mbri-logout:before { 248 | content: "\e949"; 249 | } 250 | .mbri-magic-stick:before { 251 | content: "\e94a"; 252 | } 253 | .mbri-map-pin:before { 254 | content: "\e94b"; 255 | } 256 | .mbri-menu:before { 257 | content: "\e94c"; 258 | } 259 | .mbri-mobile:before { 260 | content: "\e94d"; 261 | } 262 | .mbri-mobile2:before { 263 | content: "\e94e"; 264 | } 265 | .mbri-mobirise:before { 266 | content: "\e94f"; 267 | } 268 | .mbri-more-horizontal:before { 269 | content: "\e950"; 270 | } 271 | .mbri-more-vertical:before { 272 | content: "\e951"; 273 | } 274 | .mbri-music:before { 275 | content: "\e952"; 276 | } 277 | .mbri-new-file:before { 278 | content: "\e953"; 279 | } 280 | .mbri-numbered-list:before { 281 | content: "\e954"; 282 | } 283 | .mbri-opened-folder:before { 284 | content: "\e955"; 285 | } 286 | .mbri-pages:before { 287 | content: "\e956"; 288 | } 289 | .mbri-paper-plane:before { 290 | content: "\e957"; 291 | } 292 | .mbri-paperclip:before { 293 | content: "\e958"; 294 | } 295 | .mbri-photo:before { 296 | content: "\e959"; 297 | } 298 | .mbri-photos:before { 299 | content: "\e95a"; 300 | } 301 | .mbri-pin:before { 302 | content: "\e95b"; 303 | } 304 | .mbri-play:before { 305 | content: "\e95c"; 306 | } 307 | .mbri-plus:before { 308 | content: "\e95d"; 309 | } 310 | .mbri-preview:before { 311 | content: "\e95e"; 312 | } 313 | .mbri-print:before { 314 | content: "\e95f"; 315 | } 316 | .mbri-protect:before { 317 | content: "\e960"; 318 | } 319 | .mbri-question:before { 320 | content: "\e961"; 321 | } 322 | .mbri-quote-left:before { 323 | content: "\e962"; 324 | } 325 | .mbri-quote-right:before { 326 | content: "\e963"; 327 | } 328 | .mbri-refresh:before { 329 | content: "\e964"; 330 | } 331 | .mbri-responsive:before { 332 | content: "\e965"; 333 | } 334 | .mbri-right:before { 335 | content: "\e966"; 336 | } 337 | .mbri-rocket:before { 338 | content: "\e967"; 339 | } 340 | .mbri-sad-face:before { 341 | content: "\e968"; 342 | } 343 | .mbri-sale:before { 344 | content: "\e969"; 345 | } 346 | .mbri-save:before { 347 | content: "\e96a"; 348 | } 349 | .mbri-search:before { 350 | content: "\e96b"; 351 | } 352 | .mbri-setting:before { 353 | content: "\e96c"; 354 | } 355 | .mbri-setting2:before { 356 | content: "\e96d"; 357 | } 358 | .mbri-setting3:before { 359 | content: "\e96e"; 360 | } 361 | .mbri-share:before { 362 | content: "\e96f"; 363 | } 364 | .mbri-shopping-bag:before { 365 | content: "\e970"; 366 | } 367 | .mbri-shopping-basket:before { 368 | content: "\e971"; 369 | } 370 | .mbri-shopping-cart:before { 371 | content: "\e972"; 372 | } 373 | .mbri-sites:before { 374 | content: "\e973"; 375 | } 376 | .mbri-smile-face:before { 377 | content: "\e974"; 378 | } 379 | .mbri-speed:before { 380 | content: "\e975"; 381 | } 382 | .mbri-star:before { 383 | content: "\e976"; 384 | } 385 | .mbri-success:before { 386 | content: "\e977"; 387 | } 388 | .mbri-sun:before { 389 | content: "\e978"; 390 | } 391 | .mbri-sun2:before { 392 | content: "\e979"; 393 | } 394 | .mbri-tablet:before { 395 | content: "\e97a"; 396 | } 397 | .mbri-tablet-vertical:before { 398 | content: "\e97b"; 399 | } 400 | .mbri-target:before { 401 | content: "\e97c"; 402 | } 403 | .mbri-timer:before { 404 | content: "\e97d"; 405 | } 406 | .mbri-to-ftp:before { 407 | content: "\e97e"; 408 | } 409 | .mbri-to-local-drive:before { 410 | content: "\e97f"; 411 | } 412 | .mbri-touch-swipe:before { 413 | content: "\e980"; 414 | } 415 | .mbri-touch:before { 416 | content: "\e981"; 417 | } 418 | .mbri-trash:before { 419 | content: "\e982"; 420 | } 421 | .mbri-underline:before { 422 | content: "\e983"; 423 | } 424 | .mbri-unlink:before { 425 | content: "\e984"; 426 | } 427 | .mbri-unlock:before { 428 | content: "\e985"; 429 | } 430 | .mbri-up-down:before { 431 | content: "\e986"; 432 | } 433 | .mbri-up:before { 434 | content: "\e987"; 435 | } 436 | .mbri-update:before { 437 | content: "\e988"; 438 | } 439 | .mbri-upload:before { 440 | content: "\e926"; 441 | } 442 | .mbri-user:before { 443 | content: "\e98a"; 444 | } 445 | .mbri-user2:before { 446 | content: "\e98b"; 447 | } 448 | .mbri-users:before { 449 | content: "\e98c"; 450 | } 451 | .mbri-video:before { 452 | content: "\e98d"; 453 | } 454 | .mbri-video-play:before { 455 | content: "\e98e"; 456 | } 457 | .mbri-watch:before { 458 | content: "\e98f"; 459 | } 460 | .mbri-website-theme:before { 461 | content: "\e990"; 462 | } 463 | .mbri-wifi:before { 464 | content: "\e991"; 465 | } 466 | .mbri-windows:before { 467 | content: "\e992"; 468 | } 469 | .mbri-zoom-out:before { 470 | content: "\e993"; 471 | } 472 | .mbri-redo:before { 473 | content: "\e994"; 474 | } 475 | .mbri-undo:before { 476 | content: "\e995"; 477 | } 478 | -------------------------------------------------------------------------------- /frontend/templates/assets/web/assets/mobirise-icons/mobirise-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/web/assets/mobirise-icons/mobirise-icons.eot -------------------------------------------------------------------------------- /frontend/templates/assets/web/assets/mobirise-icons/mobirise-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/web/assets/mobirise-icons/mobirise-icons.ttf -------------------------------------------------------------------------------- /frontend/templates/assets/web/assets/mobirise-icons/mobirise-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pavviaz/DeepScriptum/3c53f304b768809f8c5ff40fc6a88f81f2c74422/frontend/templates/assets/web/assets/mobirise-icons/mobirise-icons.woff -------------------------------------------------------------------------------- /frontend/templates/assets/ytplayer/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | yt-player. MIT License. Feross Aboukhadijeh */ 3 | var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.arrayIteratorImpl=function(a){var b=0;return function(){return bc&&(c=Math.max(c+e,0));c=b._player.getDuration()-Number(a)){b.seek(0);for(var c=$jscomp.makeIterator(b.replayInterval.entries()), 33 | d=c.next();!d.done;d=c.next()){d=$jscomp.makeIterator(d.value);var e=d.next().value;d.next();Object.hasOwnProperty.call(b.replayInterval,e)&&(clearInterval(b.replayInterval[e].interval),b.replayInterval.splice(e,1))}}},1E3*Number(a))})};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.pause=function(){this._ready?this._player.pauseVideo():this._queueCommand("pause")}; 34 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.stop=function(){this._ready?this._player.stopVideo():this._queueCommand("stop")};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.seek=function(a){this._ready?this._player.seekTo(a,!0):this._queueCommand("seek",a)}; 35 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype._optimizeDisplayHandler=function(a){if(this._player){var b=this._player.i;a=a.split(",");if(b){var c;if(c=b.parentElement){var d=window.getComputedStyle(c);var e=c.clientHeight+parseFloat(d.marginTop,10)+parseFloat(d.marginBottom,10)+parseFloat(d.borderTopWidth,10)+parseFloat(d.borderBottomWidth,10);c=c.clientWidth+parseFloat(d.marginLeft,10)+parseFloat(d.marginRight, 36 | 10)+parseFloat(d.borderLeftWidth,10)+parseFloat(d.borderRightWidth,10);e+=80;b.style.width=c+"px";b.style.height=Math.ceil(parseFloat(b.style.width,10)/1.7)+"px";b.style.marginTop=Math.ceil(-((parseFloat(b.style.height,10)-e)/2))+"px";b.style.marginLeft=0;if(d=parseFloat(b.style.height,10)c&&(b.style.marginLeft=-((parseFloat(b.style.width,10)-c)/2)+"px")}}}}}; 38 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.stopResize=function(){window.removeEventListener("resize",this._resizeListener);this._resizeListener=null}; 39 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.stopReplay=function(a){for(var b=$jscomp.makeIterator(this.replayInterval.entries()),c=b.next();!c.done;c=b.next()){c=$jscomp.makeIterator(c.value);var d=c.next().value;c.next();Object.hasOwnProperty.call(this.replayInterval,d)&&a===this.replayInterval[d].iframeParent&&(clearInterval(this.replayInterval[d].interval),this.replayInterval.splice(d,1))}}; 40 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.setVolume=function(a){this._ready?this._player.setVolume(a):this._queueCommand("setVolume",a)};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.loadPlaylist=function(){this._ready?this._player.loadPlaylist(this.videoId):this._queueCommand("loadPlaylist",this.videoId)}; 41 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.setLoop=function(a){this._ready?this._player.setLoop(a):this._queueCommand("setLoop",a)};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.getVolume=function(){return this._ready&&this._player.getVolume()||0}; 42 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.mute=function(){this._ready?this._player.mute():this._queueCommand("mute")};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.unMute=function(){this._ready?this._player.unMute():this._queueCommand("unMute")}; 43 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.isMuted=function(){return this._ready&&this._player.isMuted()||!1};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.setSize=function(a,b){this._ready?this._player.setSize(a,b):this._queueCommand("setSize",a,b)}; 44 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.setPlaybackRate=function(a){this._ready?this._player.setPlaybackRate(a):this._queueCommand("setPlaybackRate",a)}; 45 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.setPlaybackQuality=function(a){this._ready?this._player.setPlaybackQuality(a):this._queueCommand("setPlaybackQuality",a)};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.getPlaybackRate=function(){return this._ready&&this._player.getPlaybackRate()||1}; 46 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.getAvailablePlaybackRates=function(){return this._ready&&this._player.getAvailablePlaybackRates()||[1]};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.getDuration=function(){return this._ready&&this._player.getDuration()||0}; 47 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.getProgress=function(){return this._ready&&this._player.getVideoLoadedFraction()||0};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.getState=function(){return this._ready&&YOUTUBE_STATES[this._player.getPlayerState()]||"unstarted"}; 48 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.getCurrentTime=function(){return this._ready&&this._player.getCurrentTime()||0};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype.destroy=function(){this._destroy()}; 49 | C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype._destroy=function(a){this.destroyed||(this.destroyed=!0,this._player&&(this._player.stopVideo&&this._player.stopVideo(),this._player.destroy()),this._player=this._api=this._opts=this._id=this.videoId=null,this._ready=!1,this._queue=null,this._stopInterval(),this.removeListener("playing",this._startInterval),this.removeListener("paused",this._stopInterval), 50 | this.removeListener("buffering",this._stopInterval),this.removeListener("unstarted",this._stopInterval),this.removeListener("ended",this._stopInterval),a&&this.emit("error",a))};C_$developer$desktop$Release$release$win_ia32_unpacked$resources$_app_asar$web$app$themes$startm5$plugins$ytplayer$index$classdecl$var0.prototype._queueCommand=function(a,b){for(var c=[],d=1;d 2 | 3 | 4 | 5 | 6 | 7 | Login - DeepScriptum 8 | 9 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 181 | 182 | 183 | 184 | 185 |
186 | 187 | 188 |

Log in

189 |
190 | 191 |
192 |
193 | 194 |
195 | 196 |

197 |

Don't have an account yet? Sign up

198 |
199 | 200 |
201 |

DeepScriptum | ©

202 |
203 | 204 | 299 | 300 | 301 | 302 | -------------------------------------------------------------------------------- /frontend/templates/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Sign Up - DeepScriptum 8 | 9 | 11 | 12 | 13 | 14 | 15 | 17 | 18 | 180 | 181 | 182 | 183 | 184 |
185 | 186 | 187 |

Sign Up

188 |
189 | 190 |
191 |
192 | 193 |
194 |
195 | 196 |
197 | 198 |

199 |

Already have an account? Log in

200 |
201 | 202 |
203 |

DeepScriptum | ©

204 |
205 | 206 | 302 | 303 | 304 | 305 | -------------------------------------------------------------------------------- /neural_worker/celery_services.py: -------------------------------------------------------------------------------- 1 | import io 2 | from multiprocessing.pool import ThreadPool 3 | from typing import List 4 | from uuid import uuid4 5 | 6 | from celery import Celery 7 | from openai import OpenAI 8 | import boto3 9 | from sqlalchemy import update 10 | import httpx 11 | 12 | import config 13 | import settings 14 | from utils import process_file 15 | from infrastructure.postgres import database 16 | from infrastructure.postgres.models import DocumentDAO 17 | 18 | 19 | REPLACERS = {"latex": config.LATEX_REPLACER, "md": config.MD_REPLACER} 20 | 21 | 22 | client = OpenAI( 23 | api_key=settings.app_settings.OPENAI_KEY, 24 | http_client=httpx.Client(proxy=settings.app_settings.PROXY), 25 | ) 26 | 27 | celery = Celery(__name__) 28 | celery.conf.broker_url = settings.rabbitmq_settings.URI 29 | celery.conf.result_backend = settings.redis_settings.URI 30 | 31 | 32 | @celery.task( 33 | name="images", bind=True, time_limit=600, soft_time_limit=540, track_started=True 34 | ) 35 | def process_images(self, decode_type: str = "md"): 36 | task_id = self.request.id 37 | 38 | s3_client = boto3.client( 39 | "s3", 40 | endpoint_url=settings.minio_settings.URI, 41 | aws_access_key_id=settings.minio_settings.ROOT_USER, 42 | aws_secret_access_key=settings.minio_settings.ROOT_PASSWORD, 43 | ) 44 | 45 | response = s3_client.get_object(Bucket=settings.minio_settings.BUCKET, Key=task_id) 46 | data = response["Body"].read() 47 | imgs_binary = process_file(data, file_type=response["Metadata"]["ext"]) 48 | 49 | replacer = REPLACERS[decode_type] 50 | imgs_enum = [(idx, img, replacer) for idx, img in enumerate(imgs_binary)] 51 | 52 | try: 53 | with ThreadPool(len(imgs_enum)) as thread_pool: 54 | map_results = thread_pool.starmap(_apply_map_ocr, imgs_enum) 55 | except Exception as exc: 56 | raise self.retry(exc=exc, countdown=5) 57 | 58 | reduce_result = _apply_reduce_ocr(map_results, decode_type, replacer) 59 | 60 | doc_s3_uuid = str(uuid4()) 61 | try: 62 | s3_client.upload_fileobj( 63 | io.BytesIO(reduce_result.encode("utf-8")), 64 | settings.minio_settings.BUCKET, 65 | doc_s3_uuid, 66 | ExtraArgs={"ContentType": "text/markdown"}, 67 | ) 68 | except: 69 | raise self.retry(exc=exc, countdown=5) 70 | 71 | pg_session = database.session_factory() 72 | stmt = ( 73 | update(DocumentDAO) 74 | .where(DocumentDAO.id == task_id) 75 | .values(s3_md_id=doc_s3_uuid) 76 | ) 77 | pg_session.execute(stmt) 78 | pg_session.commit() 79 | 80 | return doc_s3_uuid 81 | 82 | 83 | def _apply_map_ocr(idx: int, img_binary: str, replacer: tuple): 84 | payload = { 85 | "model": settings.app_settings.MODEL_NAME, 86 | "messages": [ 87 | { 88 | "role": "user", 89 | "content": [ 90 | { 91 | "type": "text", 92 | "text": config.DEFAULT_MAP_PROMPT.format( 93 | idx + 1, replacer[0], replacer[0], replacer[0] 94 | ), 95 | }, 96 | { 97 | "type": "image_url", 98 | "image_url": {"url": f"data:image/jpeg;base64,{img_binary}"}, 99 | }, 100 | ], 101 | } 102 | ], 103 | } 104 | 105 | try: 106 | response = client.chat.completions.create( 107 | model=settings.app_settings.MODEL_NAME, 108 | messages=payload["messages"], 109 | ) 110 | content = response.choices[0].message.content 111 | content = content.lstrip(replacer[1]).rstrip(replacer[2]).strip("\n") 112 | except Exception as e: 113 | print(f"An error occurred: {e}") 114 | return None 115 | 116 | return content 117 | 118 | 119 | def _apply_reduce_ocr(texts: List[str], decode_type: str, replacer: tuple): 120 | payload = { 121 | "model": settings.app_settings.MODEL_NAME, 122 | "messages": [ 123 | { 124 | "role": "user", 125 | "content": [ 126 | { 127 | "type": "text", 128 | "text": config.DEFAULT_REDUCE_PROMPT.format( 129 | len(texts), 130 | decode_type, 131 | config.REDUCE_SPLITTER, 132 | decode_type, 133 | f"\n{config.REDUCE_SPLITTER}\n".join(texts), 134 | ), 135 | } 136 | ], 137 | } 138 | ], 139 | } 140 | 141 | try: 142 | response = client.chat.completions.create( 143 | model=settings.app_settings.MODEL_NAME, 144 | messages=payload["messages"], 145 | ) 146 | content = response.choices[0].message.content 147 | content = content.lstrip(replacer[1]).rstrip(replacer[2]).strip("\n") 148 | except Exception as e: 149 | print(f"An error occurred: {e}") 150 | return None 151 | 152 | return content 153 | 154 | 155 | @celery.task( 156 | name="texts", bind=True, time_limit=600, soft_time_limit=540, track_started=True 157 | ) 158 | def process_texts(self, decode_type: str = "md"): 159 | task_id = self.request.id 160 | 161 | replacer = REPLACERS[decode_type] 162 | if texts == ["unknown"]: 163 | return "Unknown data type" 164 | texts_enum = [(idx, text, replacer) for idx, text in enumerate(texts)] 165 | 166 | try: 167 | with ThreadPool(len(texts_enum)) as thread_pool: 168 | map_results = thread_pool.starmap(_apply_map_text, texts_enum) 169 | except Exception as exc: 170 | raise self.retry(exc=exc, countdown=5) 171 | 172 | reduce_result = _apply_reduce_text(map_results, decode_type, replacer) 173 | return reduce_result 174 | 175 | 176 | def _apply_map_text(idx: int, text: str, replacer: tuple): 177 | payload = { 178 | "model": settings.app_settings.MODEL_NAME, 179 | "messages": [ 180 | { 181 | "role": "user", 182 | "content": [ 183 | { 184 | "type": "text", 185 | "text": config.DEFAULT_MAP_PROMPT.format( 186 | idx + 1, replacer[0], replacer[0], replacer[0] 187 | ), 188 | }, 189 | { 190 | "type": "text", 191 | "text": text, 192 | }, 193 | ], 194 | } 195 | ], 196 | } 197 | 198 | try: 199 | response = client.chat.completions.create( 200 | model=settings.app_settings.MODEL_NAME, 201 | messages=payload["messages"], 202 | ) 203 | content = response.choices[0].message.content 204 | content = content.lstrip(replacer[1]).rstrip(replacer[2]).strip("\n") 205 | except Exception as e: 206 | print(f"An error occurred: {e}") 207 | return None 208 | 209 | return content 210 | 211 | 212 | def _apply_reduce_text(texts: List[str], decode_type: str, replacer: tuple): 213 | payload = { 214 | "model": settings.app_settings.MODEL_NAME, 215 | "messages": [ 216 | { 217 | "role": "user", 218 | "content": [ 219 | { 220 | "type": "text", 221 | "text": config.DEFAULT_REDUCE_PROMPT.format( 222 | len(texts), 223 | decode_type, 224 | config.REDUCE_SPLITTER, 225 | decode_type, 226 | f"\n{config.REDUCE_SPLITTER}\n".join(texts), 227 | ), 228 | } 229 | ], 230 | } 231 | ], 232 | } 233 | 234 | try: 235 | response = client.chat.completions.create( 236 | model=settings.app_settings.MODEL_NAME, 237 | messages=payload["messages"], 238 | ) 239 | content = response.choices[0].message.content 240 | content = content.lstrip(replacer[1]).rstrip(replacer[2]).strip("\n") 241 | except Exception as e: 242 | print(f"An error occurred: {e}") 243 | return None 244 | 245 | return content 246 | -------------------------------------------------------------------------------- /neural_worker/config.py: -------------------------------------------------------------------------------- 1 | DEFAULT_MAP_PROMPT = """ 2 | The given image is the {}st page of the document. 3 | Decode it into {} markup preserving every text block, table etc. 4 | Ignore all the images in document. 5 | Wrap inline equations with '$ EQUATION $' and separate with '$$\\n EQUATION \\n$$'. DONT PLACE ANY MUMBERS AFTER IT! 6 | Write nothing more but {}. 7 | Always put '```{}' before and '```' after doc respectively 8 | """ 9 | MD_REPLACER = ("md", "```md", "```") 10 | LATEX_REPLACER = ("latex", "```latex", "```") 11 | 12 | DEFAULT_REDUCE_PROMPT = """ 13 | This is {}-pages document, decoded in {}. 14 | The pages are splitted with '{}'. 15 | Combine them into one single document, preserving all the contents. 16 | Write nothing more but combined document. 17 | Dont place any numbers after the equations. 18 | If you work with LaTeX, add all the necessary packages in the beginning. 19 | Always put '```{}' before and '```' after doc respectively. 20 | 21 | Pages: 22 | {} 23 | """ 24 | REDUCE_SPLITTER = "---!!!---" 25 | -------------------------------------------------------------------------------- /neural_worker/dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | 3 | RUN apt-get update && apt-get install libgl1 ffmpeg poppler-utils -y 4 | 5 | WORKDIR /neural_worker 6 | 7 | COPY requirements.txt /neural_worker/ 8 | RUN pip install -r requirements.txt 9 | COPY . /neural_worker/ -------------------------------------------------------------------------------- /neural_worker/infrastructure/postgres/database.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy.orm import sessionmaker, DeclarativeBase 3 | 4 | from settings import postgres_settings 5 | 6 | 7 | class Base(DeclarativeBase): 8 | pass 9 | 10 | 11 | engine = create_engine( 12 | postgres_settings.URI, # Изменено 13 | ) 14 | session_factory = sessionmaker( 15 | bind=engine, # Изменено 16 | expire_on_commit=False, 17 | autocommit=False, 18 | autoflush=False, 19 | ) 20 | 21 | from .models import * # pylint: disable=C0413 # isort:skip # noqa: F403, E402 22 | -------------------------------------------------------------------------------- /neural_worker/infrastructure/postgres/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .document import * 2 | -------------------------------------------------------------------------------- /neural_worker/infrastructure/postgres/models/document.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import enum 3 | from datetime import datetime 4 | 5 | from sqlalchemy import String, TIMESTAMP, Enum 6 | from sqlalchemy.orm import Mapped, mapped_column 7 | from sqlalchemy.sql import func 8 | 9 | from infrastructure.postgres.database import Base 10 | 11 | 12 | class ShareRoleEnum(enum.Enum): 13 | private = "закрытый доступ" 14 | viewer = "только просмотр" 15 | edit = "редактирование" 16 | 17 | 18 | class DocumentDAO(Base): 19 | __tablename__ = "documents" 20 | 21 | id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4) 22 | name: Mapped[str] = mapped_column(String(100)) 23 | s3_md_id: Mapped[str] = mapped_column(String(50), nullable=True) 24 | s3_raw_id: Mapped[str] = mapped_column(String(50), nullable=True) 25 | upload_date: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now()) 26 | common_share_role_type: Mapped[ShareRoleEnum] = mapped_column( 27 | type_=Enum(ShareRoleEnum), default=ShareRoleEnum.private 28 | ) 29 | -------------------------------------------------------------------------------- /neural_worker/requirements.txt: -------------------------------------------------------------------------------- 1 | celery==5.3.4 2 | requests==2.32.3 3 | pydantic==2.10.5 4 | pydantic-settings==2.7.1 5 | pydantic_core==2.27.2 6 | openai==1.60.1 7 | redis==5.2.1 8 | pdf2image==1.17.0 9 | SQLAlchemy==2.0.37 10 | boto3==1.36.6 11 | psycopg2-binary==2.9.10 -------------------------------------------------------------------------------- /neural_worker/settings.py: -------------------------------------------------------------------------------- 1 | from pydantic_settings import BaseSettings 2 | from pydantic import computed_field 3 | 4 | 5 | class ToolConfig: 6 | env_file_encoding = "utf8" 7 | extra = "ignore" 8 | 9 | 10 | class NWSettings(BaseSettings): 11 | OPENAI_KEY: str = "" 12 | PROXY: str = "" 13 | MODEL_NAME: str = "gpt-4o" 14 | 15 | class Config(ToolConfig): 16 | env_prefix = "nw_" 17 | 18 | 19 | class RedisSettings(BaseSettings): 20 | HOST: str = "redis" 21 | PORT: int = 6379 22 | CELERY_DB: int = 1 23 | 24 | @computed_field(return_type=str) 25 | @property 26 | def URI(self): 27 | return f"redis://{self.HOST}:{self.PORT}/{self.CELERY_DB}" 28 | 29 | class Config(ToolConfig): 30 | env_prefix = "redis_" 31 | 32 | 33 | class RabbitMQSettings(BaseSettings): 34 | HOST: str = "rabbitmq" 35 | AMQP_PORT: int = 5672 36 | LOGIN: str = "admin" 37 | PASSWORD: str = "admin" 38 | 39 | @computed_field(return_type=str) 40 | @property 41 | def URI(self): 42 | return f"amqp://{self.LOGIN}:{self.PASSWORD}@{self.HOST}:{self.AMQP_PORT}/" 43 | 44 | class Config(ToolConfig): 45 | env_prefix = "rabbitmq_" 46 | 47 | 48 | class MinIOSettings(BaseSettings): 49 | HOST: str = "minio" 50 | BUCKET: str = "user_docs" 51 | API_PORT: int = 9000 52 | ROOT_USER: str = "admin" 53 | ROOT_PASSWORD: str = "admin_password" 54 | 55 | @computed_field(return_type=str) 56 | @property 57 | def URI(self): 58 | return f"http://{self.HOST}:{self.API_PORT}" 59 | 60 | class Config(ToolConfig): 61 | env_prefix = "minio_" 62 | 63 | 64 | class PostgresSettings(BaseSettings): 65 | HOST: str = "localhost" 66 | PORT: int = 5432 67 | USER: str = "postgres" 68 | PASSWORD: str = "passwd" 69 | DB: str = "ds" 70 | MAX_OVERFLOW: int = 15 71 | POOL_SIZE: int = 15 72 | 73 | @computed_field(return_type=str) 74 | @property 75 | def URI(self): 76 | return f"postgresql://{self.USER}:{self.PASSWORD}@{self.HOST}:{self.PORT}/{self.DB}" 77 | 78 | class Config(ToolConfig): 79 | env_prefix = "postgres_" 80 | 81 | 82 | postgres_settings = PostgresSettings() 83 | app_settings = NWSettings() 84 | redis_settings = RedisSettings() 85 | minio_settings = MinIOSettings() 86 | rabbitmq_settings = RabbitMQSettings() 87 | -------------------------------------------------------------------------------- /neural_worker/utils.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | import base64 3 | 4 | from pdf2image import convert_from_bytes 5 | 6 | 7 | def process_file(byte_data, file_type): 8 | estimator_dict = { 9 | "pdf": process_pdf, 10 | } 11 | 12 | estimator = estimator_dict[file_type] 13 | data = estimator(byte_data) 14 | 15 | return data 16 | 17 | 18 | def process_pdf(byte_data): 19 | pdf_imgs = convert_from_bytes(byte_data) 20 | pdf_base64 = [] 21 | for img in pdf_imgs: 22 | buffered = BytesIO() 23 | img.save(buffered, format="JPEG") 24 | pdf_base64.append(base64.b64encode(buffered.getvalue()).decode()) 25 | 26 | return pdf_base64 27 | -------------------------------------------------------------------------------- /rabbit_conf/rabbitmq.conf: -------------------------------------------------------------------------------- 1 | heartbeat = 610 -------------------------------------------------------------------------------- /streamlit/config.py: -------------------------------------------------------------------------------- 1 | API_PDF = "http://api:8000/pdf" 2 | API_STATUS = "http://api:8000/status" 3 | 4 | ANIM_PATH = "static/lottie_5.json" 5 | -------------------------------------------------------------------------------- /streamlit/dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | 3 | WORKDIR /streamlit 4 | 5 | COPY requirements.txt /streamlit/ 6 | RUN pip install -r requirements.txt 7 | COPY . /streamlit/ -------------------------------------------------------------------------------- /streamlit/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | streamlit 3 | streamlit_ace 4 | streamlit_lottie -------------------------------------------------------------------------------- /streamlit/static/lottie_5.json: -------------------------------------------------------------------------------- 1 | {"v":"5.7.1","fr":60,"ip":0,"op":357,"w":1400,"h":650,"nm":"TextScaner","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"SearchShape","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":120,"s":[0]},{"t":150,"s":[90]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[1133.5,291,0],"to":[-84.363,0,0],"ti":[54.47,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":120,"s":[300.5,291,0],"to":[-138.833,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":180,"s":[252.5,399,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":193,"s":[253,399,0],"to":[0,0,0],"ti":[0,0,0]},{"t":295,"s":[688,399,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[400,400],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 1","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0,0],"ix":2},"a":{"a":0,"k":[200,200,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[69.158,48.004],[48.964,-67.814],[-69.17,-48.004],[-52.065,30.145],[0,0],[-12.858,11.975],[12.203,12.606],[0.409,0.379]],"o":[[48.976,-67.802],[-69.158,-48.004],[-48.964,67.802],[49.362,34.262],[0,0],[12.215,12.606],[12.858,-11.964],[-0.386,-0.401],[0,0]],"v":[[278.67,237.359],[242.107,27.667],[28.22,63.524],[64.794,273.205],[231.484,279.961],[344.611,390.204],[390.006,391.34],[391.176,346.845],[390.006,345.698]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.035,0],[0.035,53.579],[-54.662,0.023],[-0.105,-53.51],[54.674,-0.092]],"o":[[-54.65,0.011],[-0.012,-53.579],[54.58,-0.023],[0.094,53.59],[-0.023,0]],"v":[[153.351,247.637],[54.358,150.665],[153.281,53.625],[252.251,150.447],[153.422,247.637]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[153.351,247.637]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.219607843137,0.196078431373,0.674509803922,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"last line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[527.5,399.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[523,55],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":16,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.21568627451,0.196078431373,0.674509803922,1],"ix":4},"o":{"a":0,"k":74.902,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"last line","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"center line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[700,297.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[868,55],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":16,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.21568627451,0.196078431373,0.674509803922,1],"ix":4},"o":{"a":0,"k":74.902,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"center line","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"top line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[700,195.5,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[868,55],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":16,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"fl","c":{"a":0,"k":[0.21568627451,0.196078431373,0.674509803922,1],"ix":4},"o":{"a":0,"k":74.902,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"top line","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":3600,"st":0,"bm":0}],"markers":[]} -------------------------------------------------------------------------------- /streamlit/ui.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import requests 4 | 5 | import streamlit as st 6 | import streamlit_ace as st_ace 7 | from streamlit_lottie import st_lottie_spinner 8 | 9 | from config import API_PDF, API_STATUS, ANIM_PATH 10 | 11 | 12 | st.set_page_config(page_title="DeepScriptum", page_icon="📘", layout="wide") 13 | 14 | st.title("📘 :violet[Deep]Scriptum") 15 | 16 | st.header( 17 | "This is DeepScriptum demo page, showing the " 18 | "capabilities of modern VLLMs to perform " 19 | "end2end OCR over any document" 20 | ) 21 | 22 | st.divider() 23 | 24 | st.subheader("Use fields below to perform doc recogniton!") 25 | 26 | result_format = st.radio("Choose output format:", ("md", "latex"), key="result_format") 27 | 28 | uploaded_files = st.file_uploader( 29 | "Drag PDF file or images here (PNG, JPG)", 30 | type=["pdf", "png", "jpg"], 31 | accept_multiple_files=True, 32 | help="You can upload PDF or images (PNG, JPG)", 33 | key="uploaded_files", 34 | ) 35 | 36 | 37 | def valid_files(files): 38 | if len(files) == 1 and files[0].type == "application/pdf": 39 | return True 40 | elif all(file.type in ["image/png", "image/jpeg"] for file in files): 41 | # TODO it should be possible 42 | return False 43 | else: 44 | return False 45 | 46 | 47 | if uploaded_files and not valid_files(uploaded_files): 48 | st.error("For now only PDFs are available to upload") 49 | 50 | 51 | def load_lottiefile(filepath: str): 52 | with open(filepath, "r") as f: 53 | return json.load(f) 54 | 55 | 56 | lottie_spinner = load_lottiefile(ANIM_PATH) 57 | 58 | if "s" not in st.session_state: 59 | st.session_state.s = requests.Session() 60 | 61 | 62 | if uploaded_files and valid_files(uploaded_files) and st.button("OCR this!"): 63 | files = [("pdf_file", (file.name, file, file.type)) for file in uploaded_files] 64 | r = st.session_state.s.post( 65 | url=API_PDF, data={"decode_type": result_format}, files=files 66 | ) 67 | 68 | if r.status_code == 201: 69 | with st_lottie_spinner(lottie_spinner, height=600, width=600): 70 | status_response = -1 71 | while status_response != 200: 72 | status_response = st.session_state.s.get(API_STATUS) 73 | if status_response.status_code == 200: 74 | st.session_state.md = status_response.json()["content"] 75 | break 76 | elif status_response.status_code == 500: 77 | st.error("Something went wrong while file processing...") 78 | break 79 | time.sleep(2) 80 | else: 81 | raise Exception("Can't send data") 82 | 83 | if "md" in st.session_state: 84 | col1, col2 = st.columns(2) 85 | with col1: 86 | st.session_state.md = st_ace.st_ace( 87 | value=st.session_state.md, 88 | language="markdown" if result_format == "md" else "latex", 89 | theme="chrome", 90 | key="editor", 91 | height="700px", 92 | wrap=True, 93 | ) 94 | 95 | with col2: 96 | custom_html = f""" 97 |
98 | {st.session_state.md} 99 |
100 | """ 101 | st.write(custom_html, unsafe_allow_html=True) 102 | 103 | st.download_button( 104 | "Download edited file", st.session_state.md, file_name=f"file.{result_format}" 105 | ) 106 | --------------------------------------------------------------------------------