Initial: presenton

This commit is contained in:
sauravniraula 2025-05-10 19:57:24 +05:45
commit 78e1006f2e
No known key found for this signature in database
GPG key ID: 60FCC1B5A5E83326
1909 changed files with 4087252 additions and 0 deletions

188
.gitignore vendored Normal file
View file

@ -0,0 +1,188 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
servers/fastapi/.env
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store
env
.venv
__pycache__
*.db
resources/nextjs
resources/fastapi
servers/nextjs/data

202
LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2025 presenton
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

0
README.md Normal file
View file

1
app/constants.ts Normal file
View file

@ -0,0 +1 @@
export const localhost = "http://0.0.0.0"

83
app/main.ts Normal file
View file

@ -0,0 +1,83 @@
require("dotenv").config();
import { app, BrowserWindow } from "electron";
import path from "path";
import { findTwoUnusedPorts, killProcess } from "./utils";
import { startFastApiServer, startNextJsServer } from "./servers";
import { ChildProcessByStdio } from "child_process";
import { localhost } from "./constants";
var isDev = process.env.DEBUG === "True";
var baseDir = isDev ? process.cwd() : process.resourcesPath;
var resourcesDir = path.join(baseDir, "resources");
var fastapiDir = isDev ? path.join(baseDir, "servers/fastapi") : path.join(resourcesDir, "fastapi");
var nextjsDir = isDev ? path.join(baseDir, "servers/nextjs") : path.join(resourcesDir, "nextjs");
var win: BrowserWindow | undefined;
var fastApiProcess: ChildProcessByStdio<any, any, any> | undefined;
var nextjsProcess: ChildProcessByStdio<any, any, any> | undefined;
const createWindow = () => {
win = new BrowserWindow({
webPreferences: {
webSecurity: false,
},
width: 1280,
height: 720,
});
};
async function startServers(fastApiPort: number, nextjsPort: number) {
try {
fastApiProcess = await startFastApiServer(
fastapiDir,
fastApiPort,
{
DEBUG: isDev ? "True" : "False",
LLM: process.env.LLM || "",
LIBREOFFICE: process.env.LIBREOFFICE || "",
OPENAI_API_KEY: process.env.OPENAI_API_KEY || "",
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY || "",
APP_DATA_DIRECTORY: process.env.APP_DATA_DIRECTORY || "",
TEMP_DIRECTORY: process.env.TEMP_DIRECTORY || "",
},
isDev
);
nextjsProcess = await startNextJsServer(
nextjsDir,
nextjsPort,
{
NEXT_PUBLIC_API: `${localhost}:${fastApiPort}`,
TEMP_DIRECTORY: process.env.TEMP_DIRECTORY || "",
},
isDev
);
} catch (error) {
console.error("Server startup error:", error);
}
}
async function stopServers() {
if (fastApiProcess?.pid) {
await killProcess(fastApiProcess.pid);
}
if (nextjsProcess?.pid) {
await killProcess(nextjsProcess.pid);
}
}
app.whenReady().then(async () => {
createWindow();
win?.loadFile(path.join(resourcesDir, "ui/homepage/index.html"));
win?.webContents.openDevTools();
const [fastApiPort, nextjsPort] = await findTwoUnusedPorts();
console.log(`FastAPI port: ${fastApiPort}, NextJS port: ${nextjsPort}`);
await startServers(fastApiPort, nextjsPort);
win?.loadURL(`${localhost}:${nextjsPort}/upload`);
});
app.on("window-all-closed", async () => {
await stopServers();
app.quit();
});

76
app/servers.ts Normal file
View file

@ -0,0 +1,76 @@
import { spawn, exec } from "child_process";
import util from "util";
import { localhost } from "./constants";
const execAsync = util.promisify(exec);
export async function startFastApiServer(
directory: string,
port: number,
env: FastApiEnv,
isDev: boolean,
) {
// Start FastAPI server
const startCommand = isDev ? [
".venv/bin/python",
["server.py", "--port", port.toString()],
] : [
"./fastapi", ["--port", port.toString()],
];
const fastApiProcess = spawn(
startCommand[0] as string,
startCommand[1] as string[],
{
cwd: directory,
stdio: ["inherit", "pipe", "pipe"],
env: { ...process.env, ...env },
}
);
fastApiProcess.stdout.on("data", (data: any) => {
console.log(`FastAPI: ${data}`);
});
fastApiProcess.stderr.on("data", (data: any) => {
console.error(`FastAPI Error: ${data}`);
});
// Wait for FastAPI server to start
await execAsync(`npx wait-on ${localhost}:${port}/docs`);
return fastApiProcess;
}
export async function startNextJsServer(
directory: string,
port: number,
env: NextJsEnv,
isDev: boolean,
) {
// Start NextJS server
const startCommand = isDev ? [
"npm",
["run", "dev", "--", "-p", port.toString()],
] : [
"npx",
["next", "start", "--", "-p", port.toString()],
];
const nextjsProcess = spawn(
startCommand[0] as string,
startCommand[1] as string[],
{
cwd: directory,
stdio: ["inherit", "pipe", "pipe"],
env: { ...process.env, ...env },
}
);
nextjsProcess.stdout.on("data", (data: any) => {
console.log(`NextJS: ${data}`);
});
nextjsProcess.stderr.on("data", (data: any) => {
console.error(`NextJS Error: ${data}`);
});
// Wait for NextJS server to start
await execAsync(`npx wait-on ${localhost}:${port}`);
return nextjsProcess;
}

14
app/types/index.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
interface FastApiEnv {
DEBUG: string,
LLM: string,
LIBREOFFICE: string,
OPENAI_API_KEY: string,
GOOGLE_API_KEY: string,
APP_DATA_DIRECTORY: string,
TEMP_DIRECTORY: string,
}
interface NextJsEnv {
NEXT_PUBLIC_API: string,
TEMP_DIRECTORY: string,
}

44
app/utils.ts Normal file
View file

@ -0,0 +1,44 @@
import net from 'net'
import treeKill from 'tree-kill'
export function killProcess(pid: number) {
return new Promise((resolve, reject) => {
treeKill(pid, "SIGTERM", (err: any) => {
if (err) {
console.error(`Error killing process ${pid}:`, err)
reject(err)
} else {
console.log(`Process ${pid} killed`)
resolve(true)
}
})
})
}
export async function findTwoUnusedPorts(startPort: number = 40000): Promise<[number, number]> {
const ports: number[] = [];
const isPortAvailable = (port: number): Promise<boolean> => {
return new Promise((resolve) => {
const server = net.createServer();
server.once('error', () => {
resolve(false);
});
server.once('listening', () => {
server.close();
resolve(true);
});
server.listen(port);
});
};
let currentPort = startPort;
while (ports.length < 2) {
if (await isPortAvailable(currentPort)) {
ports.push(currentPort);
}
currentPort++;
}
return [ports[0], ports[1]];
}

47
forge.config.js Normal file
View file

@ -0,0 +1,47 @@
const { FusesPlugin } = require('@electron-forge/plugin-fuses');
const { FuseV1Options, FuseVersion } = require('@electron/fuses');
module.exports = {
packagerConfig: {
asar: true,
extraResource: [
'resources',
]
},
rebuildConfig: {},
makers: [
// {
// name: '@electron-forge/maker-squirrel',
// config: {},
// },
// {
// name: '@electron-forge/maker-zip',
// platforms: ['darwin'],
// },
{
name: '@electron-forge/maker-deb',
config: {},
},
// {
// name: '@electron-forge/maker-rpm',
// config: {},
// },
],
plugins: [
{
name: '@electron-forge/plugin-auto-unpack-natives',
config: {},
},
// Fuses are used to enable/disable various Electron functionality
// at package time, before code signing the application
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};

1684
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

32
package.json Normal file
View file

@ -0,0 +1,32 @@
{
"name": "presenton_open_source",
"main": "dist/main.js",
"version": "0.0.0",
"description": "Presenton Open Source",
"scripts": {
"dev": "tsc && electron --gtk-version=3 .",
"start": "tsc && electron-forge start",
"package": "tsc && electron-forge package",
"make": "tsc && electron-forge make",
"setup:env": "cd servers/fastapi && poetry env remove --all && poetry install",
"build:ts": "tsc",
"build:css": "tailwindcss -i ./resources/ui/assets/tailwind.import.css -o ./resources/ui/assets/tailwind.css",
"build:nextjs": "rm -rf resources/nextjs && mkdir -p resources/nextjs && cd servers/nextjs && npm run build && cp -r .next ../../resources/nextjs",
"build:fastapi": "rm -rf resources/fastapi && cd servers/fastapi && .venv/bin/pyinstaller --name fastapi --distpath ../../resources server.py",
"clean:build": "rm -rf resources/nextjs && rm -rf resources/fastapi"
},
"devDependencies": {
"electron": "^36.1.0",
"typescript": "^5.8.3"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@tailwindcss/cli": "^4.1.5",
"dotenv": "^16.5.0",
"electron-squirrel-startup": "^1.0.1",
"tailwindcss": "^4.1.5",
"tree-kill": "^1.2.2"
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
@import 'tailwindcss';

View file

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<title>Presenton Open Source</title>
<link rel="stylesheet" href="../assets/tailwind.css">
<script src="./script.js"></script>
</head>
<body class="bg-gray-900 text-white flex flex-col items-center justify-center h-screen">
<h1 class="text-4xl font-bold">Presenton Open Source</h1>
<p class="mt-4">Waiting for server to start...</p>
</body>
</html>

View file

7
servers/fastapi/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}

View file

@ -0,0 +1,50 @@
# Build stage
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
# Install build dependencies and pip packages in one layer
RUN apt-get update && apt-get install -y --no-install-recommends \
libreoffice \
poppler-utils \
ffmpeg \
fonts-noto \
fonts-dejavu \
fonts-liberation \
fonts-freefont-ttf \
fonts-roboto \
fonts-noto-core \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir -r requirements.txt
# Final stage
FROM python:3.11-slim
WORKDIR /app
# Install only runtime dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libreoffice \
poppler-utils \
ffmpeg \
fonts-noto \
fonts-dejavu \
fonts-liberation \
fonts-freefont-ttf \
fonts-roboto \
fonts-noto-core \
&& rm -rf /var/lib/apt/lists/*
# Copy installed Python packages from builder
COPY --from=builder /usr/local/lib/python3.11/site-packages/ /usr/local/lib/python3.11/site-packages/
COPY --from=builder /usr/local/bin/ /usr/local/bin/
# Copy application code and fonts
COPY . /app
COPY ./fonts /usr/share/fonts/
RUN fc-cache -fv
CMD ["python", "server.py"]

View file

View file

@ -0,0 +1,27 @@
import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import SQLModel
from contextlib import asynccontextmanager
from api.routers.presentation.router import presentation_router
from api.services.database import sql_engine
@asynccontextmanager
async def lifespan(_: FastAPI):
os.makedirs(os.getenv("APP_DATA_DIRECTORY"), exist_ok=True)
SQLModel.metadata.create_all(sql_engine)
yield
app = FastAPI(lifespan=lifespan)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(presentation_router)

View file

@ -0,0 +1,37 @@
from typing import Optional
from pydantic import BaseModel
from api.sql_models import PresentationSqlModel
class LogMetadata(BaseModel):
presentation: Optional[str] = None
title: Optional[str] = None
endpoint: Optional[str] = None
status_code: Optional[int] = None
@classmethod
def from_presentation(
cls, presentation: PresentationSqlModel, endpoint: Optional[str] = None
):
return cls(
presentation=presentation.id,
title=presentation.title,
endpoint=endpoint,
)
@property
def stream_name(self):
return f"Endpoint - {self.endpoint}, Presentation - {self.presentation}"
class SessionModel(BaseModel):
session: str
class SSEResponse(BaseModel):
event: str
data: str
def to_string(self):
return f"event: {self.event}\ndata: {self.data}\n\n"

View file

@ -0,0 +1,18 @@
from typing import Optional
from api.models import LogMetadata
from api.services.logging import LoggingService
class RequestUtils:
def __init__(self, endpoint: str):
self.endpoint = endpoint
async def initialize_logger(
self,
presentation_id: Optional[str] = None,
):
metadata = LogMetadata(presentation=presentation_id, endpoint=self.endpoint)
logging_service = LoggingService(metadata.stream_name)
return logging_service, metadata

View file

@ -0,0 +1,58 @@
import asyncio
from typing import List
import uuid
from api.models import LogMetadata
from api.routers.presentation.models import (
DecomposeDocumentsRequest,
DecomposeDocumentsResponse,
)
from api.services.instances import temp_file_service
from api.services.logging import LoggingService
from document_processor.loader import DocumentsLoader
class DecomposeDocumentsHandler:
def __init__(self, data: DecomposeDocumentsRequest):
self.data = data
self.documents = list(
filter(lambda doc: not doc.endswith(".csv"), self.data.documents or [])
)
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
documents_loader = DocumentsLoader(self.documents)
await documents_loader.load_documents(self.temp_dir)
parsed_documents = documents_loader.documents
document_paths = []
for parsed_doc in parsed_documents:
file_path = temp_file_service.create_temp_file_path(
f"{str(uuid.uuid4())}.txt", self.temp_dir
)
parsed_doc = parsed_doc.page_content.replace("<br>", "\n")
with open(file_path, "w") as text_file:
text_file.write(parsed_doc)
document_paths.append(file_path)
documents = {}
for index, each in enumerate(self.documents):
documents[each] = document_paths[index]
response = DecomposeDocumentsResponse(
documents=documents,
)
logging_service.logger.info(
logging_service.message(response.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,21 @@
from api.models import LogMetadata
from api.services.logging import LoggingService
from api.sql_models import PresentationSqlModel
from api.services.database import get_sql_session
class DeletePresentationHandler:
def __init__(self, id):
self.id = id
async def delete(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message({"presentation": self.id}),
extra=log_metadata.model_dump(),
)
with get_sql_session() as sql_session:
presentation = sql_session.get(PresentationSqlModel, self.id)
sql_session.delete(presentation)
sql_session.commit()

View file

@ -0,0 +1,21 @@
from api.models import LogMetadata
from api.services.logging import LoggingService
from api.services.database import get_sql_session
from api.sql_models import SlideSqlModel
class DeleteSlideHandler:
def __init__(self, id):
self.id = id
async def delete(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message({"slide": self.id}),
extra=log_metadata.model_dump(),
)
with get_sql_session() as sql_session:
slide = sql_session.get(SlideSqlModel, self.id)
sql_session.delete(slide)
sql_session.commit()

View file

@ -0,0 +1,231 @@
import asyncio
import os
from typing import List, Tuple
import uuid
from sqlmodel import select
from api.models import LogMetadata
from api.routers.presentation.models import (
EditPresentationSlideRequest,
)
from api.services.instances import temp_file_service
from api.services.logging import LoggingService
from api.utils import get_presentation_dir
from image_processor.images_finder import generate_image
from image_processor.icons_finder import get_icon
from ppt_generator.models.slide_model import SlideModel
from ppt_generator.slide_generator import (
get_edited_slide_content_model,
get_slide_type_from_prompt,
)
from ppt_generator.slide_model_utils import SlideModelUtils
from api.sql_models import PresentationSqlModel, SlideSqlModel
from api.services.database import get_sql_session
class PresentationEditHandler:
def __init__(self, data: EditPresentationSlideRequest):
self.data = data
self.presentation_id = data.presentation_id
self.slide_index = data.index
self.prompt = data.prompt
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
self.presentation_dir = get_presentation_dir(self.presentation_id)
def __del__(self):
temp_file_service.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
with get_sql_session() as sql_session:
presentation = sql_session.get(PresentationSqlModel, self.presentation_id)
slide_to_edit = sql_session.exec(
select(SlideSqlModel).where(SlideSqlModel.index == self.slide_index)
).first()
slide_to_edit = SlideModel.from_dict(slide_to_edit.model_dump(mode="json"))
new_slide_type = await get_slide_type_from_prompt(
self.prompt, slide_to_edit
)
edited_content = await get_edited_slide_content_model(
self.prompt,
new_slide_type.slide_type,
slide_to_edit,
presentation.theme,
presentation.language,
)
new_slide_model = SlideModel(
id=slide_to_edit.id,
index=slide_to_edit.index,
type=new_slide_type.slide_type,
design_index=slide_to_edit.design_index,
images=None,
icons=None,
presentation=slide_to_edit.presentation,
content=edited_content,
)
# Images to delete - is list of cloud paths
# Images to generate - is list of index of images to generate
images_to_delete, images_to_generate, icons_to_delete, icons_to_generate = (
self.get_all_assets_to_generate_and_delete(
slide_to_edit,
new_slide_model,
)
)
new_image_paths = slide_to_edit.images or []
new_icon_paths = slide_to_edit.icons or []
images_count = len(new_image_paths)
icons_count = len(new_icon_paths)
for index in images_to_generate:
file_key = f"{self.presentation_dir}/images/{str(uuid.uuid4())}.jpg"
if index < images_count:
new_image_paths.pop(index)
new_image_paths.insert(index, file_key)
else:
new_image_paths.append(file_key)
for index in icons_to_generate:
file_key = f"{self.presentation_dir}/icons/{str(uuid.uuid4())}.png"
if index < icons_count:
new_icon_paths.pop(index)
new_icon_paths.insert(index, file_key)
else:
new_icon_paths.append(file_key)
if new_image_paths:
new_slide_model.images = new_image_paths
if new_icon_paths:
new_slide_model.icons = new_icon_paths
# Generate and Delete Images and Icons
objects_to_delete = [*images_to_delete, *icons_to_delete]
if objects_to_delete:
for each in objects_to_delete:
os.remove(each)
new_image_prompts = {}
new_icon_queries = {}
if images_to_generate:
slide_model_utils = SlideModelUtils(presentation.theme, new_slide_model)
image_prompts = slide_model_utils.get_image_prompts()
for image_index in images_to_generate:
new_image_prompts[new_slide_model.images[image_index]] = (
image_prompts[image_index]
)
if icons_to_generate:
slide_model_utils = SlideModelUtils(presentation.theme, new_slide_model)
icon_queries = slide_model_utils.get_icon_queries()
for icon_index in icons_to_generate:
new_icon_queries[new_slide_model.icons[icon_index]] = icon_queries[
icon_index
]
coroutines = [
generate_image(value, key) for key, value in new_image_prompts.items()
] + [get_icon(value, key) for key, value in new_icon_queries.items()]
await asyncio.gather(*coroutines)
slide_to_edit.images = new_slide_model.images
slide_to_edit.icons = new_slide_model.icons
slide_to_edit.content = new_slide_model.content
slide_to_edit.type = new_slide_type.slide_type
sql_session.commit()
sql_session.refresh(slide_to_edit)
logging_service.logger.info(
logging_service.message(slide_to_edit.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return slide_to_edit
def get_all_assets_to_generate_and_delete(
self,
old_slide_model: SlideModel,
new_slide_model: SlideModel,
) -> Tuple[List[str], List[str], List[str], List[str]]:
images_to_delete, images_to_generate = self.get_assets_to_generate_and_delete(
old_slide_model,
new_slide_model,
"image_prompts",
"images",
)
icons_to_delete, icons_to_generate = self.get_assets_to_generate_and_delete(
old_slide_model,
new_slide_model,
"icon_queries",
"icons",
)
return images_to_delete, images_to_generate, icons_to_delete, icons_to_generate
def get_assets_to_generate_and_delete(
self,
old_slide_model: SlideModel,
new_slide_model: SlideModel,
content_attr: str,
slide_model_attr: str,
) -> Tuple[List[str], List[str]]:
items_to_delete = []
items_to_generate = []
existing_paths = getattr(old_slide_model, slide_model_attr, [])
new_content_items = getattr(new_slide_model.content, content_attr, [])
old_content_items = getattr(old_slide_model.content, content_attr, [])
# Case 1: No new items but slide has existing items - delete all
if not new_content_items and existing_paths:
items_to_delete.extend(existing_paths)
return items_to_delete, items_to_generate
# Case 2: New items but slide has no existing items - generate all
if new_content_items and not existing_paths:
items_to_generate = [idx for idx in range(len(new_content_items))]
return items_to_delete, items_to_generate
# Case 3: Both new and existing items - compare and update
if new_content_items and existing_paths:
new_count = len(new_content_items)
old_count = len(existing_paths)
generate_idx = []
for idx in range(max(new_count, old_count)):
# Generate additional new items
if idx >= old_count:
generate_idx.append(idx)
# Delete excess old items
elif idx >= new_count:
items_to_delete.append(existing_paths[idx])
# Compare and update changed items
else:
old_value = old_content_items[idx]
new_value = new_content_items[idx]
if old_value != new_value:
items_to_delete.append(existing_paths[idx])
generate_idx.append(idx)
if generate_idx:
items_to_generate = generate_idx
filtered_items_to_delete = []
for each in items_to_delete:
if not each:
continue
filtered_items_to_delete.append(each)
return filtered_items_to_delete, items_to_generate

View file

@ -0,0 +1,58 @@
import os
import uuid
from api.models import LogMetadata
from api.routers.presentation.mixins.fetch_presentation_assets import (
FetchPresentationAssetsMixin,
)
from api.routers.presentation.models import (
ExportAsRequest,
PresentationAndPath,
)
from api.services.instances import temp_file_service
from api.services.logging import LoggingService
from api.utils import get_presentation_dir
from image_processor.image_from_pptx import get_pdf_from_pptx
from ppt_generator.pptx_presentation_creator import PptxPresentationCreator
class ExportAsPDFHandler(FetchPresentationAssetsMixin):
def __init__(self, data: ExportAsRequest):
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
self.presentation_dir = get_presentation_dir(self.data.presentation_id)
def __del__(self):
temp_file_service.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
await self.fetch_presentation_assets()
ppt_path = os.path.join(self.presentation_dir, "presentation.pptx")
ppt_creator = PptxPresentationCreator(self.data.pptx_model, self.temp_dir)
ppt_creator.create_ppt()
ppt_creator.save(ppt_path)
pdf_path = get_pdf_from_pptx(ppt_path, self.presentation_dir)
response = PresentationAndPath(
presentation_id=self.data.presentation_id,
path=pdf_path,
)
logging_service.logger.info(
logging_service.message(response.model_dump()),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,61 @@
import os
import uuid
from api.models import LogMetadata
from api.routers.presentation.mixins.fetch_presentation_assets import (
FetchPresentationAssetsMixin,
)
from api.routers.presentation.models import (
ExportAsRequest,
PresentationAndPath,
)
from api.services.logging import LoggingService
from api.services.instances import temp_file_service
from api.sql_models import PresentationSqlModel
from api.utils import get_presentation_dir
from ppt_generator.pptx_presentation_creator import PptxPresentationCreator
from api.services.database import get_sql_session
class ExportAsPptxHandler(FetchPresentationAssetsMixin):
def __init__(self, data: ExportAsRequest):
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
self.presentation_dir = get_presentation_dir(self.data.presentation_id)
def __del__(self):
temp_file_service.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
await self.fetch_presentation_assets()
ppt_path = os.path.join(self.presentation_dir, "presentation.pptx")
ppt_creator = PptxPresentationCreator(self.data.pptx_model, self.temp_dir)
ppt_creator.create_ppt()
ppt_creator.save(ppt_path)
response = PresentationAndPath(
presentation_id=self.data.presentation_id, path=ppt_path
)
with get_sql_session() as sql_session:
presentation = sql_session.get(
PresentationSqlModel, self.data.presentation_id
)
presentation.file = ppt_path
sql_session.commit()
logging_service.logger.info(
logging_service.message(response.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,43 @@
import uuid
from fastapi import HTTPException
from api.models import LogMetadata, SessionModel
from api.routers.presentation.models import PresentationGenerateRequest
from api.services.logging import LoggingService
from api.sql_models import KeyValueSqlModel
from api.services.database import get_sql_session
class PresentationGenerateDataHandler:
def __init__(self, data: PresentationGenerateRequest):
self.data = data
self.session = str(uuid.uuid4())
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump()),
extra=log_metadata.model_dump(),
)
if not self.data.titles:
raise HTTPException(400, "Titles can not be empty")
key_value_model = KeyValueSqlModel(
id=self.session,
key=self.session,
value=self.data.model_dump(mode="json"),
)
with get_sql_session() as sql_session:
sql_session.add(key_value_model)
sql_session.commit()
sql_session.refresh(key_value_model)
response = SessionModel(session=self.session)
logging_service.logger.info(
logging_service.message(response),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,44 @@
import uuid
from api.models import LogMetadata
from api.routers.presentation.models import (
GenerateImageRequest,
PresentationAndPaths,
)
from api.services.logging import LoggingService
from api.services.instances import temp_file_service
from api.utils import get_presentation_dir
from image_processor.images_finder import generate_image
class GenerateImageHandler:
def __init__(self, data: GenerateImageRequest):
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
self.presentation_dir = get_presentation_dir(self.data.presentation_id)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
image_path = temp_file_service.create_temp_file_path(
self.presentation_dir, "generated_images", str(uuid.uuid4()) + ".jpg"
)
await generate_image(self.data.prompt, image_path)
response = PresentationAndPaths(
presentation_id=self.data.presentation_id, paths=[image_path]
)
logging_service.logger.info(
logging_service.message(response.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,61 @@
import uuid
from api.models import LogMetadata
from api.routers.presentation.models import GeneratePresentationRequirementsRequest
from api.services.logging import LoggingService
from api.services.database import get_sql_session
from api.services.instances import temp_file_service
from api.sql_models import PresentationSqlModel
from document_processor.loader import DocumentsLoader
from ppt_config_generator.document_summary_generator import generate_document_summary
class GeneratePresentationRequirementsHandler:
def __init__(
self,
presentation_id: str,
data: GeneratePresentationRequirementsRequest,
):
self.data = data
self.presentation_id = presentation_id
self.prompt = data.prompt
self.n_slides = data.n_slides
self.documents = data.documents or []
self.language = data.language
self.research_reports = data.research_reports or []
self.images = data.images or []
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
all_document_paths = [*self.documents, *self.research_reports]
documents_loader = DocumentsLoader(all_document_paths)
await documents_loader.load_documents(self.temp_dir)
summary = await generate_document_summary(documents_loader.documents)
presentation = PresentationSqlModel(
id=self.presentation_id,
prompt=self.prompt,
n_slides=self.n_slides,
language=self.language,
summary=summary,
)
with get_sql_session() as sql_session:
sql_session.add(presentation)
sql_session.commit()
sql_session.refresh(presentation)
logging_service.logger.info(
logging_service.message(presentation.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return presentation

View file

@ -0,0 +1,32 @@
import uuid
from api.models import LogMetadata
from api.routers.presentation.models import GenerateResearchReportRequest
from api.services.logging import LoggingService
from api.services.instances import temp_file_service
from research_report.generator import get_report
class GenerateResearchReportHandler:
def __init__(self, data: GenerateResearchReportRequest):
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
report = await get_report(self.data.query, self.data.language)
file_name = f"{report[:30]}.txt"
file_path = temp_file_service.create_temp_file_path(file_name, self.temp_dir)
with open(file_path, "w") as text_file:
text_file.write(report)
logging_service.logger.info(
logging_service.message(file_path), extra=log_metadata.model_dump()
)
return file_path

View file

@ -0,0 +1,231 @@
import asyncio
import json
import os
from typing import List
import uuid
from fastapi import HTTPException
from fastapi.responses import StreamingResponse
from sqlmodel import delete, select
from api.models import LogMetadata, SSEResponse
from api.routers.presentation.models import (
PresentationAndSlides,
PresentationGenerateRequest,
)
from api.services.database import get_sql_session
from api.services.logging import LoggingService
from api.sql_models import KeyValueSqlModel, PresentationSqlModel, SlideSqlModel
from api.utils import get_presentation_dir
from image_processor.icons_vectorstore_utils import get_icons_vectorstore
from image_processor.images_finder import generate_image
from image_processor.icons_finder import get_icon
from ppt_generator.generator import generate_presentation_stream
from ppt_generator.models.llm_models import LLMPresentationModel
from ppt_generator.models.slide_model import SlideModel
from ppt_generator.slide_model_utils import SlideModelUtils
from api.services.instances import temp_file_service
from langchain_core.output_parsers import JsonOutputParser
output_parser = JsonOutputParser(pydantic_object=LLMPresentationModel)
class PresentationGenerateStreamHandler:
def __init__(self, presentation_id: str, session: str):
self.session = session
self.presentation_id = presentation_id
self.temp_dir = temp_file_service.create_temp_dir(self.session)
self.presentation_dir = get_presentation_dir(self.presentation_id)
def __del__(self):
temp_file_service.cleanup_temp_dir(self.temp_dir)
async def get(self, *args, **kwargs):
with get_sql_session() as sql_session:
key_value_model = sql_session.get(KeyValueSqlModel, self.session)
if not key_value_model.value:
raise HTTPException(400, "Data not found for provided session")
self.data = PresentationGenerateRequest(**key_value_model.value)
self.presentation_id = self.data.presentation_id
self.theme = self.data.theme
self.images = self.data.images
self.titles = self.data.titles
self.watermark = self.data.watermark
return StreamingResponse(
self.get_stream(*args, **kwargs), media_type="text/event-stream"
)
async def get_stream(
self, logging_service: LoggingService, log_metadata: LogMetadata
):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
if not self.titles:
raise HTTPException(400, "Titles can not be empty")
with get_sql_session() as sql_session:
presentation = sql_session.get(PresentationSqlModel, self.presentation_id)
presentation.n_slides = len(self.titles)
presentation.titles = self.titles
presentation.theme = self.theme
sql_session.exec(
delete(SlideSqlModel).where(
SlideSqlModel.presentation == self.presentation_id
)
)
sql_session.commit()
sql_session.refresh(presentation)
yield SSEResponse(
event="response", data=json.dumps({"status": "Analyzing information 📊"})
).to_string()
presentation_text = ""
async for chunk in generate_presentation_stream(
self.titles,
presentation.prompt or "create presentation",
presentation.n_slides,
presentation.language,
presentation.summary,
):
presentation_text += chunk.content
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": chunk.content}),
).to_string()
presentation_json = output_parser.parse(presentation_text)
print("-" * 40)
print(presentation_json)
print("-" * 40)
slide_models: List[SlideModel] = []
for i, content in enumerate(presentation_json["slides"]):
content["index"] = i
content["presentation"] = presentation.id
slide_model = SlideModel(**content)
slide_content = slide_model.content
has_images = hasattr(slide_content, "image_prompts")
has_icons = hasattr(slide_content, "icon_queries")
if has_images:
slide_model.images = [
os.path.join(
self.presentation_dir,
"images",
f"{str(uuid.uuid4())}.jpg",
)
for _ in range(len(slide_content.image_prompts))
]
if has_icons:
slide_model.icons = [
os.path.join(
self.presentation_dir,
"icons",
f"{str(uuid.uuid4())}.png",
)
for _ in range(len(slide_content.icon_queries))
]
slide_models.append(slide_model)
yield SSEResponse(
event="response",
data=json.dumps({"type": "status", "status": "Fetching slide assets"}),
).to_string()
async for result in self.fetch_slide_assets(slide_models):
yield result
slide_sql_models = [
SlideSqlModel(**each.model_dump(mode="json")) for each in slide_models
]
with get_sql_session() as sql_session:
sql_session.add_all(slide_sql_models)
sql_session.commit()
for each in slide_sql_models:
sql_session.refresh(each)
yield SSEResponse(
event="response",
data=json.dumps({"type": "status", "status": "Packing slide data"}),
).to_string()
response = PresentationAndSlides(
presentation=presentation, slides=slide_sql_models
).to_response_dict()
yield SSEResponse(
event="response",
data=json.dumps({"type": "complete", "presentation": response}),
).to_string()
yield SSEResponse(
event="response",
data=json.dumps({"type": "closing", "content": "First Warning"}),
).to_string()
await asyncio.sleep(3)
yield SSEResponse(
event="response",
data=json.dumps({"type": "closing", "content": "Final Warning"}),
).to_string()
async def fetch_slide_assets(self, slide_models: List[SlideModel]):
image_prompts = []
icon_queries = []
image_paths = []
icon_paths = []
for each_slide_model in slide_models:
slide_model_utils = SlideModelUtils(self.theme, each_slide_model)
if each_slide_model.images:
prompts = slide_model_utils.get_image_prompts()
image_prompts.extend(prompts)
image_paths.extend(each_slide_model.images)
if each_slide_model.icons:
icon_queries.extend(slide_model_utils.get_icon_queries())
icon_paths.extend(each_slide_model.icons)
if icon_paths:
icon_vector_store = get_icons_vectorstore()
coroutines = [
generate_image(
each,
image_path,
)
for each, image_path in zip(image_prompts, image_paths)
] + [
get_icon(icon_vector_store, each, icon_path)
for each, icon_path in zip(icon_queries, icon_paths)
]
assets_future = asyncio.gather(*coroutines)
while not assets_future.done():
status = SSEResponse(
event="response",
data=json.dumps({"status": "Fetching slide assets..."}),
).to_string()
yield status
await asyncio.sleep(5)
await assets_future
yield SSEResponse(
event="response", data=json.dumps({"status": "Slide assets fetched"})
).to_string()

View file

@ -0,0 +1,53 @@
import uuid
from api.models import LogMetadata
from api.routers.presentation.models import GenerateTitleRequest
from api.services.instances import temp_file_service
from api.services.logging import LoggingService
from api.sql_models import PresentationSqlModel
from ppt_config_generator.models import PresentationTitlesModel
from ppt_config_generator.ppt_title_summary_generator import generate_ppt_titles
from api.services.database import get_sql_session
class PresentationTitlesGenerateHandler:
def __init__(self, data: GenerateTitleRequest):
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
def __del__(self):
temp_file_service.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
with get_sql_session() as sql_session:
presentation = sql_session.get(
PresentationSqlModel, self.data.presentation_id
)
presentation_titles: PresentationTitlesModel = await generate_ppt_titles(
presentation.prompt,
presentation.n_slides,
presentation.summary,
presentation.language,
)
presentation.title = presentation_titles.presentation_title
presentation.titles = presentation_titles.titles
sql_session.commit()
sql_session.refresh(presentation)
logging_service.logger.info(
logging_service.message(presentation.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return presentation

View file

@ -0,0 +1,37 @@
import asyncio
from sqlmodel import select
from api.models import LogMetadata
from api.routers.presentation.models import PresentationAndSlides
from api.services.logging import LoggingService
from api.sql_models import PresentationSqlModel, SlideSqlModel
from api.services.database import get_sql_session
class GetPresentationHandler:
def __init__(self, id: str):
self.id = id
async def get(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message({"presentation": self.id}),
extra=log_metadata.model_dump(),
)
with get_sql_session() as sql_session:
presentation = sql_session.get(PresentationSqlModel, self.id)
slide_models = sql_session.exec(
select(SlideSqlModel).where(SlideSqlModel.presentation == self.id)
).all()
response = PresentationAndSlides(
presentation=presentation, slides=slide_models
).to_response_dict()
logging_service.logger.info(
logging_service.message(response),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,25 @@
from sqlmodel import select
from api.models import LogMetadata
from api.services.logging import LoggingService
from api.sql_models import PresentationSqlModel
from api.services.database import get_sql_session
class GetPresentationsHandler:
async def get(self, logging_service: LoggingService, log_metadata: LogMetadata):
with get_sql_session() as sql_session:
presentations = sql_session.exec(select(PresentationSqlModel)).all()
for each in presentations:
each.data = None
each.summary = None
logging_service.logger.info(
logging_service.message(
[each.model_dump(mode="json") for each in presentations]
),
extra=log_metadata.model_dump(),
)
return presentations

View file

@ -0,0 +1,48 @@
import uuid
from api.models import LogMetadata
from api.routers.presentation.models import (
PresentationAndPaths,
SearchIconRequest,
)
from api.services.logging import LoggingService
from image_processor.icons_finder import get_icons
from api.services.instances import temp_file_service
from image_processor.icons_vectorstore_utils import get_icons_vectorstore
class SearchIconHandler:
def __init__(self, data: SearchIconRequest):
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
vector_store = get_icons_vectorstore()
icon_paths = await get_icons(
vector_store,
self.data.query or "",
self.data.page,
self.data.limit,
self.data.category,
self.temp_dir,
)
response = PresentationAndPaths(
presentation_id=self.data.presentation_id, paths=icon_paths
)
logging_service.logger.info(
logging_service.message(response.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,30 @@
import uuid
from api.models import LogMetadata
from api.routers.presentation.models import PresentationAndUrls, SearchImageRequest
from api.services.logging import LoggingService
class SearchImageHandler:
def __init__(self, data: SearchImageRequest):
self.data = data
self.session = str(uuid.uuid4())
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
response = PresentationAndUrls(
presentation_id=self.data.presentation_id, urls=[]
)
logging_service.logger.info(
logging_service.message(response.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,22 @@
from fastapi import UploadFile
from api.models import LogMetadata
from api.services.logging import LoggingService
class UpdateParsedDocumentHandler:
def __init__(self, file_path: str, file: UploadFile):
self.file_path = file_path
self.file = file
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message({"path": self.file_path, "file": self.file}),
extra=log_metadata.model_dump(),
)
with open(self.file_path, "wb") as f:
f.write(await self.file.read())
return {"message": "File saved successfully"}

View file

@ -0,0 +1,39 @@
from api.models import LogMetadata
from api.routers.presentation.models import UpdatePresentationThemeRequest
from api.services.logging import LoggingService
from api.sql_models import PreferencesSqlModel, PresentationSqlModel
from api.services.database import get_sql_session
class UpdatePresentationThemeHandler:
def __init__(self, data: UpdatePresentationThemeRequest):
self.data = data
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
with get_sql_session() as sql_session:
presentation = sql_session.get(
PresentationSqlModel, self.data.presentation_id
)
preferences = sql_session.get(PreferencesSqlModel, 0)
if not preferences:
preferences = PreferencesSqlModel(id=0, theme=None)
sql_session.add(preferences)
sql_session.commit()
sql_session.refresh(preferences)
if self.data.theme:
theme_name = self.data.theme.get("name", None)
if theme_name and theme_name.lower() == "custom":
preferences.theme = self.data.theme
presentation.theme = self.data.theme
sql_session.commit()
return {"message": "Theme updated successfully"}

View file

@ -0,0 +1,91 @@
import os
from typing import List
from urllib.parse import unquote, urlparse
import uuid
from sqlmodel import delete
from api.models import LogMetadata
from api.routers.presentation.models import (
PresentationUpdateRequest,
PresentationAndSlides,
)
from api.services.logging import LoggingService
from api.sql_models import PresentationSqlModel, SlideSqlModel
from api.utils import download_files, get_presentation_dir, replace_file_name
from api.services.database import get_sql_session
from api.services.instances import temp_file_service
class UpdateSlideModelsHandler:
def __init__(self, data: PresentationUpdateRequest):
self.data = data
self.presentation_id = data.presentation_id
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir()
self.presentation_dir = get_presentation_dir(self.presentation_id)
def __del__(self):
temp_file_service.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(self.data.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
presentation_id = self.data.presentation_id
new_slides = self.data.slides
# Handle assets (images and icons)
assets_local_paths = []
assets_download_links = []
for new_slide in new_slides:
new_images = new_slide.images or []
new_icons = new_slide.icons or []
for new_assets, asset_type in [
(new_images, "images"),
(new_icons, "icons"),
]:
for i, asset in enumerate(new_assets):
if asset.startswith("http"):
parsed_url = unquote(urlparse(asset).path)
image_name = replace_file_name(
os.path.basename(parsed_url), str(uuid.uuid4())
)
asset_path = (
f"{self.presentation_dir}/{asset_type}/{image_name}"
)
assets_local_paths.append(asset_path)
assets_download_links.append(asset)
getattr(new_slide, asset_type)[i] = asset_path
if assets_download_links:
await download_files(assets_download_links, assets_local_paths)
with get_sql_session() as sql_session:
slide_sql_models = [
SlideSqlModel(**each.model_dump(mode="json")) for each in new_slides
]
to_update_slides_ids = [each.id for each in slide_sql_models]
sql_session.exec(
delete(SlideSqlModel).where(SlideSqlModel.id.in_(to_update_slides_ids))
)
sql_session.add_all(slide_sql_models)
sql_session.commit()
for each in slide_sql_models:
sql_session.refresh(each)
presentation = sql_session.get(PresentationSqlModel, presentation_id)
response = PresentationAndSlides(
presentation=presentation, slides=slide_sql_models
)
response = response.to_response_dict()
logging_service.logger.info(
logging_service.message(response),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,69 @@
from typing import List, Optional
import uuid
from fastapi import UploadFile
from api.models import LogMetadata
from api.routers.presentation.models import DocumentsAndImagesPath
from api.services.logging import LoggingService
from api.validators import validate_files
from document_processor.loader import UPLOAD_ACCEPTED_DOCUMENTS
from api.services.instances import temp_file_service
class UploadFilesHandler:
def __init__(
self,
documents: Optional[List[UploadFile]],
images: Optional[List[UploadFile]],
):
self.documents = documents
self.images = images
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
print("Upload Temp Dir: " + self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(
{
"documents": self.documents,
"images": self.images,
}
),
extra=log_metadata.model_dump(),
)
validate_files(self.documents, True, True, 50, UPLOAD_ACCEPTED_DOCUMENTS)
validate_files(
self.images, True, True, 10, ["image/jpeg", "image/png", "image/webp"]
)
self.documents = self.documents or []
self.images = self.images or []
temp_documents: List[str] = []
if self.documents or self.images:
all_documents = self.documents + self.images
for doc in all_documents:
temp_path = temp_file_service.create_temp_file_path(
doc.filename, self.temp_dir
)
with open(temp_path, "wb") as f:
content = await doc.read()
f.write(content)
temp_documents.append(temp_path)
documents_count = len(temp_documents)
response = DocumentsAndImagesPath(
documents=temp_documents[:documents_count],
images=temp_documents[documents_count:],
)
logging_service.logger.info(
logging_service.message(response.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,59 @@
import os
import uuid
from fastapi import UploadFile
from api.models import LogMetadata
from api.routers.presentation.models import PresentationAndPath
from api.services.logging import LoggingService
from api.services.instances import temp_file_service
from api.sql_models import PresentationSqlModel
from api.services.database import get_sql_session
from api.utils import get_presentation_dir
class UploadPresentationThumbnailHandler:
def __init__(self, presentation_id: str, thumbnail: UploadFile):
self.presentation_id = presentation_id
self.thumbnail = thumbnail
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
self.presentation_dir = get_presentation_dir(self.presentation_id)
def __del__(self):
temp_file_service.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message(
{
"presentation_id": self.presentation_id,
"thumbnail": self.thumbnail,
}
),
extra=log_metadata.model_dump(),
)
with get_sql_session() as sql_session:
presentation = sql_session.get(PresentationSqlModel, self.presentation_id)
with open(os.path.join(self.presentation_dir, "thumbnail.jpg"), "wb") as f:
f.write(await self.thumbnail.read())
presentation.thumbnail = os.path.join(
self.presentation_dir, "thumbnail.jpg"
)
sql_session.commit()
sql_session.refresh(presentation)
response = PresentationAndPath(
presentation_id=self.presentation_id, path=presentation.thumbnail
)
logging_service.logger.info(
logging_service.message(response.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return response

View file

@ -0,0 +1,30 @@
import os
from urllib.parse import unquote, urlparse
import uuid
from api.utils import download_files, replace_file_name
from ppt_generator.models.pptx_models import PptxPictureBoxModel
class FetchPresentationAssetsMixin:
async def fetch_presentation_assets(self):
image_urls = []
image_local_paths = []
for each_slide in self.data.pptx_model.slides:
for each_shape in each_slide.shapes:
if isinstance(each_shape, PptxPictureBoxModel):
image_path = each_shape.picture.path
if image_path.startswith("http"):
image_urls.append(image_path)
parsed_url = unquote(urlparse(image_path).path)
image_name = replace_file_name(
os.path.basename(parsed_url), str(uuid.uuid4())
)
image_path = os.path.join(self.temp_dir, image_name)
image_local_paths.append(image_path)
each_shape.picture.path = image_path
each_shape.picture.is_network = False
await download_files(image_urls, image_local_paths)

View file

@ -0,0 +1,138 @@
from typing import List, Optional
from pydantic import BaseModel
from ppt_generator.models.pptx_models import PptxPresentationModel
from ppt_generator.models.query_and_prompt_models import (
IconCategoryEnum,
ImagePromptWithThemeAndAspectRatio,
)
from ppt_generator.models.slide_model import SlideModel
from api.sql_models import PresentationSqlModel, SlideSqlModel
class DocumentsAndImagesPath(BaseModel):
documents: Optional[List[str]] = None
images: Optional[List[str]] = None
class GenerateResearchReportRequest(BaseModel):
language: Optional[str] = None
query: str
class DecomposeDocumentsRequest(DocumentsAndImagesPath):
pass
class GeneratePresentationRequirementsRequest(BaseModel):
prompt: Optional[str] = None
n_slides: int
language: str
documents: Optional[List[str]] = None
research_reports: Optional[List[str]] = None
images: Optional[List[str]] = None
class GenerateTitleRequest(BaseModel):
presentation_id: str
class PresentationGenerateRequest(BaseModel):
presentation_id: str
theme: Optional[dict] = None
images: Optional[List[str]] = None
watermark: bool = True
titles: List[str]
class GenerateImageRequest(BaseModel):
presentation_id: str
prompt: ImagePromptWithThemeAndAspectRatio
class SearchImageRequest(BaseModel):
presentation_id: str
query: Optional[str] = None
page: int = 1
limit: int = 10
class SearchIconRequest(BaseModel):
presentation_id: str
query: Optional[str] = None
category: Optional[IconCategoryEnum] = None
page: int = 1
limit: int = 10
class SlideEditRequest(BaseModel):
index: int
prompt: str
class EditPresentationRequest(BaseModel):
presentation_id: str
watermark: bool = True
changes: List[SlideEditRequest]
class EditPresentationSlideRequest(BaseModel):
presentation_id: str
index: int
prompt: str
class UpdatePresentationThemeRequest(BaseModel):
presentation_id: str
theme: Optional[dict] = None
class ExportAsRequest(BaseModel):
presentation_id: str
pptx_model: PptxPresentationModel
class DecomposeDocumentsResponse(BaseModel):
documents: dict
class PresentationAndSlides(BaseModel):
presentation: PresentationSqlModel
slides: List[SlideSqlModel]
def to_response_dict(self):
presentation = self.presentation.model_dump(mode="json")
return {
"presentation": presentation,
"slides": [each.model_dump(mode="json") for each in self.slides],
}
class PresentationUpdateRequest(BaseModel):
presentation_id: str
slides: List[SlideModel]
class PresentationAndUrl(BaseModel):
presentation_id: str
url: str
class PresentationAndUrls(BaseModel):
presentation_id: str
urls: List[str]
class PresentationAndPath(BaseModel):
presentation_id: str
path: str
class PresentationAndPaths(BaseModel):
presentation_id: str
paths: List[str]
class UpdatePresentationTitlesRequest(BaseModel):
presentation_id: str
titles: List[str]

View file

@ -0,0 +1,335 @@
from typing import Annotated, List, Optional
import uuid
from fastapi import APIRouter, Body, File, UploadFile, Depends
from api.models import SessionModel
from api.request_utils import RequestUtils
from api.routers.presentation.handlers.decompose_documents import (
DecomposeDocumentsHandler,
)
from api.routers.presentation.handlers.delete_presentation import (
DeletePresentationHandler,
)
from api.routers.presentation.handlers.delete_slide import DeleteSlideHandler
from api.routers.presentation.handlers.edit import PresentationEditHandler
from api.routers.presentation.handlers.export_as_pdf import ExportAsPDFHandler
from api.routers.presentation.handlers.export_as_pptx import ExportAsPptxHandler
from api.routers.presentation.handlers.generate_data import (
PresentationGenerateDataHandler,
)
from api.routers.presentation.handlers.generate_image import GenerateImageHandler
from api.routers.presentation.handlers.generate_presentation_requirements import (
GeneratePresentationRequirementsHandler,
)
from api.routers.presentation.handlers.generate_research_report import (
GenerateResearchReportHandler,
)
from api.routers.presentation.handlers.generate_stream import (
PresentationGenerateStreamHandler,
)
from api.routers.presentation.handlers.generate_titles import (
PresentationTitlesGenerateHandler,
)
from api.routers.presentation.handlers.get_presentation import GetPresentationHandler
from api.routers.presentation.handlers.get_presentations import GetPresentationsHandler
from api.routers.presentation.handlers.search_icon import SearchIconHandler
from api.routers.presentation.handlers.search_image import SearchImageHandler
from api.routers.presentation.handlers.update_parsed_document import (
UpdateParsedDocumentHandler,
)
from api.routers.presentation.handlers.update_presentation_theme import (
UpdatePresentationThemeHandler,
)
from api.routers.presentation.handlers.update_slide_models import (
UpdateSlideModelsHandler,
)
from api.routers.presentation.handlers.upload_files import UploadFilesHandler
from api.routers.presentation.handlers.upload_presentation_thumbnail import (
UploadPresentationThumbnailHandler,
)
from api.routers.presentation.models import (
DecomposeDocumentsRequest,
DecomposeDocumentsResponse,
DocumentsAndImagesPath,
EditPresentationSlideRequest,
ExportAsRequest,
GenerateImageRequest,
GeneratePresentationRequirementsRequest,
GenerateResearchReportRequest,
PresentationAndPath,
PresentationAndSlides,
GenerateTitleRequest,
PresentationAndUrl,
PresentationAndUrls,
PresentationGenerateRequest,
SearchIconRequest,
SearchImageRequest,
UpdatePresentationThemeRequest,
PresentationUpdateRequest,
)
from api.sql_models import PresentationSqlModel
from api.utils import handle_errors
from ppt_generator.models.slide_model import SlideModel
presentation_router = APIRouter(prefix="/ppt")
@presentation_router.get(
"/user_presentations", response_model=List[PresentationSqlModel]
)
async def get_user_presentations():
request_utils = RequestUtils("/ppt/user_presentations")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
GetPresentationsHandler().get, logging_service, log_metadata
)
@presentation_router.get("/presentation", response_model=PresentationAndSlides)
async def get_presentation_from_id(presentation_id: str):
request_utils = RequestUtils("/ppt/presentation")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
return await handle_errors(
GetPresentationHandler(presentation_id).get, logging_service, log_metadata
)
@presentation_router.post("/files/upload", response_model=DocumentsAndImagesPath)
async def upload_files(
documents: Annotated[Optional[List[UploadFile]], File()] = None,
images: Annotated[Optional[List[UploadFile]], File()] = None,
):
request_utils = RequestUtils("/ppt/files/upload")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
UploadFilesHandler(documents, images).post,
logging_service,
log_metadata,
)
@presentation_router.post("/report/generate", response_model=str)
async def generate_research_report(
data: GenerateResearchReportRequest,
):
request_utils = RequestUtils("/ppt/report/generate")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
GenerateResearchReportHandler(data).post, logging_service, log_metadata
)
@presentation_router.post("/files/decompose", response_model=DecomposeDocumentsResponse)
async def decompose_documents(data: DecomposeDocumentsRequest):
request_utils = RequestUtils("/ppt/files/decompose")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
DecomposeDocumentsHandler(data).post, logging_service, log_metadata
)
@presentation_router.post("/document/update")
async def update_document(
path: Annotated[str, Body()],
file: Annotated[UploadFile, File()],
):
request_utils = RequestUtils("/ppt/document/update")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
UpdateParsedDocumentHandler(path, file).post,
logging_service,
log_metadata,
)
@presentation_router.post("/create", response_model=PresentationSqlModel)
async def create_presentation(
data: GeneratePresentationRequirementsRequest,
):
request_utils = RequestUtils("/ppt/create")
presentation_id = str(uuid.uuid4())
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
return await handle_errors(
GeneratePresentationRequirementsHandler(presentation_id, data).post,
logging_service,
log_metadata,
)
@presentation_router.post("/titles/generate", response_model=PresentationSqlModel)
async def generate_titles(data: GenerateTitleRequest):
request_utils = RequestUtils("/ppt/titles/generate")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
return await handle_errors(
PresentationTitlesGenerateHandler(data).post,
logging_service,
log_metadata,
)
@presentation_router.post("/generate/data", response_model=SessionModel)
async def submit_presentation_generation_data(
data: PresentationGenerateRequest,
):
request_utils = RequestUtils("/ppt/generate/data")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
return await handle_errors(
PresentationGenerateDataHandler(data).post, logging_service, log_metadata
)
@presentation_router.get("/generate/stream")
async def presentation_generation_stream(presentation_id: str, session: str):
request_utils = RequestUtils("/ppt/generate/stream")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
return await handle_errors(
PresentationGenerateStreamHandler(presentation_id, session).get,
logging_service,
log_metadata,
)
@presentation_router.post("/presentation/thumbnail", response_model=PresentationAndPath)
async def update_presentation(
presentation_id: Annotated[str, Body()],
thumbnail: Annotated[UploadFile, File()],
):
request_utils = RequestUtils("/ppt/presentation/thumbnail")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
return await handle_errors(
UploadPresentationThumbnailHandler(presentation_id, thumbnail).post,
logging_service,
log_metadata,
)
@presentation_router.post("/presentation/theme")
async def update_presentation(
data: UpdatePresentationThemeRequest,
):
request_utils = RequestUtils("/ppt/presentation/theme")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
return await handle_errors(
UpdatePresentationThemeHandler(data).post,
logging_service,
log_metadata,
)
@presentation_router.post("/edit", response_model=SlideModel)
async def update_presentation(
data: EditPresentationSlideRequest,
):
request_utils = RequestUtils("/ppt/edit")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id
)
return await handle_errors(
PresentationEditHandler(data).post, logging_service, log_metadata
)
@presentation_router.post("/slides/update", response_model=PresentationAndSlides)
async def update_slide_models(data: PresentationUpdateRequest):
request_utils = RequestUtils("/ppt/slides/update")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
return await handle_errors(
UpdateSlideModelsHandler(data).post, logging_service, log_metadata
)
@presentation_router.post("/image/generate", response_model=PresentationAndUrls)
async def generate_image(data: GenerateImageRequest):
request_utils = RequestUtils("/ppt/image/generate")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
return await handle_errors(
GenerateImageHandler(data).post, logging_service, log_metadata
)
@presentation_router.post("/image/search", response_model=PresentationAndUrls)
async def search_image(data: SearchImageRequest):
request_utils = RequestUtils("/ppt/image/search")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
return await handle_errors(
SearchImageHandler(data).post, logging_service, log_metadata
)
@presentation_router.post("/icon/search", response_model=PresentationAndUrls)
async def search_icon(data: SearchIconRequest):
request_utils = RequestUtils("/ppt/icon/search")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
return await handle_errors(
SearchIconHandler(data).post, logging_service, log_metadata
)
@presentation_router.post(
"/presentation/export_as_pptx", response_model=PresentationAndUrl
)
async def export_as_pptx(data: ExportAsRequest):
request_utils = RequestUtils("/ppt/presentation/export_as_pptx")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
return await handle_errors(
ExportAsPptxHandler(data).post, logging_service, log_metadata
)
@presentation_router.post(
"/presentation/export_as_pdf", response_model=PresentationAndUrl
)
async def export_as_pdf(data: ExportAsRequest):
request_utils = RequestUtils("/ppt/presentation/export_as_pdf")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
return await handle_errors(
ExportAsPDFHandler(data).post, logging_service, log_metadata
)
@presentation_router.delete("/delete", status_code=204)
async def delete_presentation(presentation_id: str):
request_utils = RequestUtils("/ppt/delete")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
return await handle_errors(
DeletePresentationHandler(presentation_id).delete, logging_service, log_metadata
)
@presentation_router.delete("/slide/delete", status_code=204)
async def delete_slide(slide_id: str, presentation_id: str):
request_utils = RequestUtils("/ppt/slide/delete")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
return await handle_errors(
DeleteSlideHandler(slide_id).delete, logging_service, log_metadata
)

View file

@ -0,0 +1,16 @@
from contextlib import contextmanager
from sqlalchemy import create_engine
from sqlmodel import Session
sql_url = "sqlite:///sqlite.db"
sql_engine = create_engine(sql_url, connect_args={"check_same_thread": False})
@contextmanager
def get_sql_session():
session = Session(sql_engine)
try:
yield session
finally:
session.close()

View file

@ -0,0 +1,4 @@
from api.services.temp_file import TempFileService
temp_file_service = TempFileService()

View file

@ -0,0 +1,15 @@
from typing import Any
from logging import Logger
class LoggingService:
def __init__(self, stream_name: str):
self._logger = Logger(stream_name)
@property
def logger(self) -> Logger:
return self._logger
def message(self, msg: Any):
return {"msg": msg}

View file

@ -0,0 +1,64 @@
import os
import uuid
from typing import Optional, Union
class TempFileService:
base_dir = os.getenv("TEMP_DIRECTORY")
def __init__(self):
os.makedirs(self.base_dir, exist_ok=True)
def create_dir_in_dir(self, base_dir: str, dir_name: Optional[str] = None) -> str:
temp_dir = os.path.join(base_dir, dir_name if dir_name else str(uuid.uuid4()))
os.makedirs(temp_dir, exist_ok=True)
return temp_dir
def create_temp_dir(self, dir_name: Optional[str] = None) -> str:
return self.create_dir_in_dir(self.base_dir, dir_name)
def create_temp_file_path(
self, file_path: str, dir_path: Optional[str] = None
) -> str:
if dir_path is None:
dir_path = self.base_dir
full_path = os.path.join(dir_path, file_path)
os.makedirs(os.path.dirname(full_path), exist_ok=True)
return full_path
def create_temp_file(
self, file_path: str, content: Union[bytes, str], dir_path: Optional[str] = None
) -> str:
file_path = self.create_temp_file_path(file_path, dir_path)
mode = "wb" if isinstance(content, bytes) else "w"
with open(file_path, mode) as f:
f.write(content)
return file_path
def read_temp_file(self, file_path: str, binary: bool = True) -> Union[bytes, str]:
mode = "rb" if binary else "r"
with open(file_path, mode) as f:
return f.read()
def cleanup_temp_file(self, file_path: str):
if os.path.exists(file_path):
os.remove(file_path)
def delete_dir_files(self, dir_path: str):
if os.path.exists(dir_path):
for root, dirs, files in os.walk(dir_path, topdown=False):
for name in files:
os.remove(os.path.join(root, name))
for name in dirs:
os.rmdir(os.path.join(root, name))
def cleanup_temp_dir(self, dir_path: str):
if os.path.exists(dir_path):
self.delete_dir_files(dir_path)
os.rmdir(dir_path)
def cleanup_base_dir(self):
self.cleanup_temp_dir(self.base_dir)

View file

@ -0,0 +1,56 @@
from datetime import datetime
from typing import List, Optional
import uuid
from sqlmodel import SQLModel, Field, Column, JSON
from ppt_generator.models.other_models import SlideType
def get_random_uuid() -> str:
return str(uuid.uuid4())
class PresentationSqlModel(SQLModel, table=True):
id: str = Field(default_factory=get_random_uuid, primary_key=True)
created_at: datetime = Field(default=datetime.now())
prompt: Optional[str] = None
n_slides: int
theme: Optional[dict] = Field(sa_column=Column(JSON, nullable=True), default=None)
file: Optional[str] = None
title: Optional[str] = None
titles: Optional[List[str]] = Field(
sa_column=Column(JSON, nullable=True), default=None
)
language: Optional[str] = None
summary: Optional[str] = None
thumbnail: Optional[str] = None
data: Optional[dict] = Field(sa_column=Column(JSON, nullable=True), default=None)
class SlideSqlModel(SQLModel, table=True):
id: str = Field(default_factory=get_random_uuid, primary_key=True)
index: int = Field(index=True)
type: int
design_index: Optional[int] = None
images: Optional[List[str]] = Field(
sa_column=Column(JSON, nullable=True), default=None
)
icons: Optional[List[str]] = Field(
sa_column=Column(JSON, nullable=True), default=None
)
presentation: str
content: dict = Field(sa_column=Column(JSON, nullable=False), default=None)
properties: Optional[dict] = Field(
sa_column=Column(JSON, nullable=True), default=None
)
class KeyValueSqlModel(SQLModel, table=True):
id: str = Field(default_factory=get_random_uuid, primary_key=True)
key: str = Field(index=True)
value: dict = Field(sa_column=Column(JSON, nullable=True), default=None)
class PreferencesSqlModel(SQLModel, table=True):
id: int = Field(default=0, primary_key=True)
theme: Optional[dict] = Field(sa_column=Column(JSON, nullable=True), default=None)

View file

@ -0,0 +1,103 @@
import asyncio
import os
import traceback
from typing import List, Optional
import aiohttp
from fastapi import HTTPException, UploadFile
from fastapi.responses import StreamingResponse
from api.models import LogMetadata
from api.services.logging import LoggingService
def get_presentation_dir(presentation_id: str) -> str:
presentation_dir = os.path.join(os.getenv("APP_DATA_DIRECTORY"), presentation_id)
os.makedirs(presentation_dir, exist_ok=True)
return presentation_dir
def replace_file_name(old_name: str, new_name: str) -> str:
splitted = old_name.split(".")
if len(splitted) < 1:
return new_name
else:
return ".".join([new_name, splitted[-1]])
def save_uploaded_files(
temp_file_service, files: List[UploadFile], file_paths: List[str], temp_dir: str
) -> List:
full_file_paths = []
for index, each_file in enumerate(files):
temp_file_path = temp_file_service.create_temp_file(
file_paths[index], each_file.file.read(), dir_path=temp_dir
)
full_file_paths.append(temp_file_path)
return full_file_paths
async def download_file(url: str, save_path: str, headers: Optional[dict] = None):
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
if response.status == 200:
with open(save_path, "wb") as file:
while True:
chunk = await response.content.read(1024)
if not chunk:
break
file.write(chunk)
print(f"File downloaded successfully to {save_path}")
return True
else:
print(f"Failed to download file. HTTP status: {response.status}")
return False
except Exception as e:
print(f"Error while downloading file from {url} to {save_path}")
return False
async def download_files(urls: List[str], save_paths: List[str]):
for url, save_path in zip(urls, save_paths):
print(url)
print(save_path)
print("-" * 10)
coroutines = [
download_file(url, save_paths[index]) for index, url in enumerate(urls)
]
await asyncio.gather(*coroutines)
async def handle_errors(
func, logging_service: LoggingService, log_metadata: LogMetadata
):
try:
logging_service.logger.info(f"START", extra=log_metadata.model_dump())
response = await func(
logging_service=logging_service, log_metadata=log_metadata
)
is_stream = isinstance(response, StreamingResponse)
logging_service.logger.info(
"STREAMING" if is_stream else "END", extra=log_metadata.model_dump()
)
return response
except HTTPException as e:
log_metadata.status_code = e.status_code
logging_service.logger.error(
f"Raised HTTPException - {e.detail}", extra=log_metadata.model_dump()
)
raise e
except Exception as e:
print(traceback.print_stack())
print(traceback.print_exc())
log_metadata.status_code = 400
logging_service.logger.critical(
"Unhandled Exception",
exc_info=True,
stack_info=True,
extra=log_metadata.model_dump(),
)
raise HTTPException(400, "Something went wrong while processing your request.")

View file

@ -0,0 +1,26 @@
from typing import List
from fastapi import HTTPException, UploadFile
def validate_files(
field,
nullable: bool,
multiple: bool,
max_size: int,
accepted_types: List[str],
):
if field:
files: List[UploadFile] = field if multiple else [field]
for each_file in files:
if (max_size * 1024 * 1024) < each_file.size:
raise HTTPException(
400,
f"File '{each_file.filename}' exceeded max upload size of {max_size} MB",
)
elif each_file.content_type not in accepted_types:
raise HTTPException(400, f"File '{each_file.filename}' not accepted.")
elif not (field or nullable):
raise HTTPException(400, "File must be provided.")

File diff suppressed because it is too large Load diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Some files were not shown because too many files have changed in this diff Show more