refactor!: use presenton docker codebase

This commit is contained in:
sauravniraula 2025-06-23 15:13:04 +05:45
parent 91619dddd9
commit 31073b6016
No known key found for this signature in database
GPG key ID: 60FCC1B5A5E83326
171 changed files with 5871 additions and 17444 deletions

9
.dockerignore Normal file
View file

@ -0,0 +1,9 @@
.venv
.env
.next
node_modules
out
build
.git
.gitignore
tmp

197
.gitignore vendored
View file

@ -1,193 +1,10 @@
# 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
app_dist
env
.venv
build
__pycache__
*.db
resources/nextjs
resources/fastapi
servers/nextjs/data
servers/fastapi/build
servers/nextjs/out
dependencies
.pytest_cache
.next
node_modules
out
user_data
tmp

53
Dockerfile Normal file
View file

@ -0,0 +1,53 @@
FROM python:3.11-slim-bookworm
# Install Node.js and npm
RUN apt-get update && apt-get install -y \
nodejs \
npm \
nginx \
curl \
redis-server
# Create a working directory
WORKDIR /app
# Set environment variables
ENV APP_DATA_DIRECTORY=/app/user_data
ENV TEMP_DIRECTORY=/tmp/presenton
# Install ollama
RUN curl -fsSL https://ollama.com/install.sh | sh
# Install dependencies for FastAPI
COPY servers/fastapi/requirements.txt ./
RUN pip install -r requirements.txt
# Install dependencies for Next.js
WORKDIR /app/servers/nextjs
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
RUN npm install
# Install chrome for puppeteer
RUN npx puppeteer browsers install chrome --install-deps
# Copy Next.js app
COPY servers/nextjs/ /app/servers/nextjs/
# Build the Next.js app
WORKDIR /app/servers/nextjs
RUN npm run build
WORKDIR /app
# Copy FastAPI and start script
COPY servers/fastapi/ ./servers/fastapi/
COPY start.js LICENSE NOTICE ./
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Expose the port
EXPOSE 80
# Start the servers
CMD ["/bin/bash", "-c", "ollama serve & service nginx start && service redis-server start && node /app/start.js"]

43
Dockerfile.dev Normal file
View file

@ -0,0 +1,43 @@
FROM python:3.11-slim-bookworm
# Install Node.js and npm
RUN apt-get update && apt-get install -y \
nodejs \
npm \
nginx \
curl \
redis-server
# Change working directory
WORKDIR /app
RUN ls -a
# Set environment variables
ENV APP_DATA_DIRECTORY=/app/user_data
ENV TEMP_DIRECTORY=/tmp/presenton
# Install ollama
RUN curl -fsSL https://ollama.com/install.sh | sh
# Install dependencies for FastAPI
COPY servers/fastapi/requirements.txt ./
RUN pip install -r requirements.txt
# Install dependencies for Next.js
WORKDIR /app/servers/nextjs
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
RUN npm install
# Install chrome for puppeteer
RUN npx puppeteer browsers install chrome --install-deps
# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Expose the port
EXPOSE 80 3000 8000 6379
# Start the servers
CMD ["/bin/bash", "-c", "ollama serve & service nginx start & service redis-server start && node /app/start.js"]

157
README.md
View file

@ -5,17 +5,15 @@
# Open-Source, Locally-Run AI Presentation Generator (Gamma Alternative)
**Presenton** is an open-source desktop application for generating presentations with AI — all running locally on your device. Stay in control of your data and privacy while using models like OpenAI, Gemini, and others. Just plug in your own API keys and only pay for what you use.
**Presenton** is an open-source application for generating presentations with AI — all running locally on your device. Stay in control of your data and privacy while using models like OpenAI, Gemini, and others. Just plug in your own API keys and only pay for what you use.
![Demo](readme_assets/demo.gif)
## 💻📥 Download Desktop App
[Download Link](https://presenton.ai/download)
## ✨ More Freedom with AI Presentations
* ✅ **Bring Your Own Key** — Only pay for what you use. OpenAI, Gemini (More coming soon...)
* ✅ **Ollama Support** — Run open-source models locally with Ollama integration
* ✅ **Runs Locally** — All code runs on your device
* ✅ **Privacy-First** — No tracking, no data stored by us
* ✅ **Flexible** — Generate presentations from prompts or outlines
@ -28,12 +26,12 @@
##### Linux/MacOS (Bash/Zsh Shell):
```bash
docker run -it --name presenton -p 5000:80 -v "./user_data:/app/user_data" ghcr.io/presenton/presenton:latest
docker run -it --name presenton -p 5000:80 -v "./user_data:/app/user_data" ghcr.io/presenton/presenton:v0.3.0-beta
```
##### Windows (PowerShell):
```bash
docker run -it --name presenton -p 5000:80 -v "${PWD}\user_data:/app/user_data" ghcr.io/presenton/presenton:latest
docker run -it --name presenton -p 5000:80 -v "${PWD}\user_data:/app/user_data" ghcr.io/presenton/presenton:v0.3.0-beta
```
#### 2. Open Presenton
@ -41,21 +39,141 @@ Open http://localhost:5000 on browser of your choice to use Presenton.
> **Note: You can replace 5000 with any other port number of your choice to run Presenton on a different port number.**
## Running electron app using source code
## Deployment Configurations
Before following these steps make sure [Poetry](https://python-poetry.org/docs/) is installed on your system.
You may want to directly provide your API KEYS as environment variables and keep them hidden. You can set these environment variables to achieve it.
#### 1. Clone this repository
```git clone https://github.com/presenton/presenton.git```
- **CAN_CHANGE_KEYS=[true/false]**: Set this to **false** if you want to keep API Keys hidden and make them unmodifiable.
- **LLM=[openai/google/ollama]**: Select **LLM** of your choice.
- **OPENAI_API_KEY=[Your OpenAI API Key]**: Provide this if **LLM** is set to **openai**
- **GOOGLE_API_KEY=[Your Google API Key]**: Provide this if **LLM** is set to **google**
- **OLLAMA_MODEL=[Ollama Model Name]**: Provide this if **LLM** is set to **ollama**
- **PEXELS_API_KEY=[Your Pexels API Key]**: Provide this if **LLM** is set to **ollama**
> Note: Switch to **windows_build** branch to run Presenton on Windows
### Using Openai
```bash
docker run -it --name presenton -p 5000:80 -e LLM="openai" -e OPENAI_API_KEY="******" -e CAN_CHANGE_KEYS="false" -v "./user_data:/app/user_data" ghcr.io/presenton/presenton:v0.3.0-beta
```
#### 2. Setup Electron, Python and NextJS Environments.
```cd presenton && npm run setup:env```
### Using Ollama
```bash
docker run -it --name presenton -p 5000:80 -e LLM="ollama" -e OLLAMA_MODEL="llama3.2:3b" -e PEXELS_API_KEY="*******" -e CAN_CHANGE_KEYS="false" -v "./user_data:/app/user_data" ghcr.io/presenton/presenton:v0.3.0-beta
```
#### 3. Run Presenton
```npm run dev```
#### Running Presenton with GPU Support
To use GPU acceleration with Ollama models, you need to install and configure the NVIDIA Container Toolkit. This allows Docker containers to access your NVIDIA GPU.
Once the NVIDIA Container Toolkit is installed and configured, you can run Presenton with GPU support by adding the `--gpus=all` flag:
```bash
docker run -it --name presenton --gpus=all -p 5000:80 -e LLM="ollama" -e OLLAMA_MODEL="llama3.2:3b" -e PEXELS_API_KEY="*******" -e CAN_CHANGE_KEYS="false" -v "./user_data:/app/user_data" ghcr.io/presenton/presenton:v0.3.0-beta
```
> **Note:** GPU acceleration significantly improves the performance of Ollama models, especially for larger models. Make sure you have sufficient GPU memory for your chosen model.
#### Supported Ollama Models:
##### Llama Models:
| Model | Size | Graph Support |
|-------|------|---------------|
| `llama3:8b` | 4.7GB | ❌ No |
| `llama3:70b` | 40GB | ✅ Yes |
| `llama3.1:8b` | 4.9GB | ❌ No |
| `llama3.1:70b` | 43GB | ✅ Yes |
| `llama3.1:405b` | 243GB | ✅ Yes |
| `llama3.2:1b` | 1.3GB | ❌ No |
| `llama3.2:3b` | 2GB | ❌ No |
| `llama3.3:70b` | 43GB | ✅ Yes |
| `llama4:16x17b` | 67GB | ✅ Yes |
| `llama4:128x17b` | 245GB | ✅ Yes |
##### Gemma Models:
| Model | Size | Graph Support |
|-------|------|---------------|
| `gemma3:1b` | 815MB | ❌ No |
| `gemma3:4b` | 3.3GB | ❌ No |
| `gemma3:12b` | 8.1GB | ❌ No |
| `gemma3:27b` | 17GB | ✅ Yes |
##### DeepSeek Models:
| Model | Size | Graph Support |
|-------|------|---------------|
| `deepseek-r1:1.5b` | 1.1GB | ❌ No |
| `deepseek-r1:7b` | 4.7GB | ❌ No |
| `deepseek-r1:8b` | 5.2GB | ❌ No |
| `deepseek-r1:14b` | 9GB | ❌ No |
| `deepseek-r1:32b` | 20GB | ✅ Yes |
| `deepseek-r1:70b` | 43GB | ✅ Yes |
| `deepseek-r1:671b` | 404GB | ✅ Yes |
##### Qwen Models:
| Model | Size | Graph Support |
|-------|------|---------------|
| `qwen3:0.6b` | 523MB | ❌ No |
| `qwen3:1.7b` | 1.4GB | ❌ No |
| `qwen3:4b` | 2.6GB | ❌ No |
| `qwen3:8b` | 5.2GB | ❌ No |
| `qwen3:14b` | 9.3GB | ❌ No |
| `qwen3:30b` | 19GB | ✅ Yes |
| `qwen3:32b` | 20GB | ✅ Yes |
| `qwen3:235b` | 142GB | ✅ Yes |
> **Note:** Models with graph support can generate charts and diagrams in presentations. Larger models provide better quality but require more system resources.
## Using Presenton API
### Generate Presentation
Endpoint: `/api/v1/ppt/generate/presentation`
Method: `POST`
Content-Type: `multipart/form-data`
#### Request Body
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| prompt | string | Yes | The main topic or prompt for generating the presentation |
| n_slides | integer | No | Number of slides to generate (default: 8, min: 5, max: 15) |
| language | string | No | Language for the presentation (default: "English") |
| theme | string | No | Presentation theme (default: "light"). Available options: "light", "dark", "cream", "royal_blue", "faint_yellow", "light_red", "dark_pink" |
| documents | File[] | No | Optional list of document files to include in the presentation. Supported file types: PDF, TXT, PPTX, DOCX |
| export_as | string | No | Export format ("pptx" or "pdf", default: "pptx") |
#### Response
```json
{
"presentation_id": "string",
"path": "string",
"edit_path": "string"
}
```
#### Example Request
```bash
curl -X POST http://localhost:5000/api/v1/ppt/generate/presentation \
-F "prompt=Introduction to Machine Learning" \
-F "n_slides=5" \
-F "language=English" \
-F "theme=light" \
-F "export_as=pptx"
```
#### Example Response
```json
{
"presentation_id": "d3000f96-096c-4768-b67b-e99aed029b57",
"path": "/static/user_data/d3000f96-096c-4768-b67b-e99aed029b57/Introduction_to_Machine_Learning.pptx",
"edit_path": "/presentation?id=d3000f96-096c-4768-b67b-e99aed029b57"
}
```
## Features
@ -68,13 +186,16 @@ Before following these steps make sure [Poetry](https://python-poetry.org/docs/)
### 3. Review and edit outline
![Demo](readme_assets/images/outline.png)
### 4. Present on app
### 4. Select theme
![Demo](readme_assets/images/select-theme.png)
### 5. Present on app
![Demo](readme_assets/images/present.png)
### 5. Change theme
### 6. Change theme
![Demo](readme_assets/images/change-theme.png)
### 6. Export presentation as PDF and PPTX
### 7. Export presentation as PDF and PPTX
![Demo](readme_assets/images/export-presentation.png)
## Community

View file

@ -1,58 +0,0 @@
import { BrowserWindow, ipcMain, } from "electron";
import { baseDir, downloadsDir } from "../utils/constants";
import fs from "fs";
import path from "path";
import { showFileDownloadedDialog } from "../utils/dialog";
import { sanitizeFilename } from "../utils";
export function setupExportHandlers() {
ipcMain.handle("file-downloaded", async (_, filePath: string): Promise<IPCStatus> => {
const fileName = path.basename(filePath);
const destinationPath = path.join(downloadsDir, fileName);
await fs.promises.rename(filePath, destinationPath);
const success = await showFileDownloadedDialog(destinationPath);
return { success };
});
ipcMain.handle("export-as-pdf", async (_, id: string, title: string) => {
const ppt_url = `${process.env.NEXT_PUBLIC_URL}/pdf-maker?id=${id}`;
const browser = new BrowserWindow({
width: 1280,
height: 720,
icon: path.join(baseDir, "resources/ui/assets/images/presenton_short_filled.png"),
webPreferences: {
webSecurity: false,
preload: path.join(__dirname, '../preloads/index.js'),
},
show: false,
});
browser.loadURL(ppt_url);
const success = await new Promise((resolve, _) => {
browser.webContents.on('did-finish-load', async () => {
// Wait for 1 second to make sure the page is loaded
await new Promise((resolve, _) => {
setTimeout(resolve, 1000);
});
const pdfBuffer = await browser.webContents.printToPDF({
printBackground: true,
pageSize: { width: 1280 / 96, height: 720 / 96 },
margins: { top: 0, right: 0, bottom: 0, left: 0 }
});
browser.close();
const sanitizedTitle = sanitizeFilename(title);
const destinationPath = path.join(downloadsDir, `${sanitizedTitle}.pdf`);
await fs.promises.writeFile(destinationPath, pdfBuffer);
const success = await showFileDownloadedDialog(destinationPath);
resolve(success);
});
});
return { success };
})
}

View file

@ -1,32 +0,0 @@
import { ipcMain } from 'electron';
import { settingsStore } from '../services/settings-store';
const FOOTER_KEY = 'footer';
export function setupFooterHandlers() {
ipcMain.handle('get-footer', async () => {
try {
const properties = settingsStore.get(FOOTER_KEY);
return { properties };
} catch (error) {
console.error('Error retrieving footer properties:', error);
throw error;
}
});
ipcMain.handle('set-footer', async (_, properties: any) => {
try {
if (!properties) {
throw new Error('Properties are required');
}
settingsStore.set(FOOTER_KEY, properties);
return { success: true };
} catch (error) {
console.error('Error saving footer properties:', error);
throw error;
}
});
}

View file

@ -1,18 +0,0 @@
import { setupExportHandlers } from "./export_handlers";
import { setupUserConfigHandlers } from "./user_config_handlers";
import { setupSlideMetadataHandlers } from "./slide_metadata";
import { setupReadFile } from "./read_file";
import { setupFooterHandlers } from "./footer_handlers";
import { setupThemeHandlers } from "./theme_handlers";
import { setupUploadImage } from "./upload_image";
import { setupLogHandler } from "./log_handler";
export function setupIpcHandlers() {
setupExportHandlers();
setupUserConfigHandlers();
setupSlideMetadataHandlers();
setupReadFile();
setupFooterHandlers();
setupThemeHandlers();
setupUploadImage();
setupLogHandler();
}

View file

@ -1,50 +0,0 @@
import { ipcMain } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { userDataDir } from '../utils/constants';
export function setupLogHandler() {
// Ensure logs directory exists
const logsDir = path.join(userDataDir, 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const logFilePath = path.join(logsDir, 'nextjs.log');
// Handle log writing through IPC - non-blocking
ipcMain.handle('write-nextjs-log', (_, logData: string) => {
try {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] ${logData}\n`;
// Use non-blocking write
fs.appendFile(logFilePath, logEntry, (err) => {
if (err) {
console.error('Error writing to log file:', err);
}
});
return { success: true };
} catch (error) {
console.error('Error in log handler:', error);
return { success: false, error: (error as Error).message };
}
});
// Handle log clearing
ipcMain.handle('clear-nextjs-logs', () => {
try {
// Create a new empty file, effectively clearing the old one
fs.writeFile(logFilePath, '', (err) => {
if (err) {
console.error('Error clearing log file:', err);
}
});
return { success: true };
} catch (error) {
console.error('Error in clear logs handler:', error);
return { success: false, error: (error as Error).message };
}
});
}

View file

@ -1,9 +0,0 @@
import { ipcMain } from "electron";
import fs from "fs";
import path from "path";
export function setupReadFile() {
ipcMain.handle("read-file", async (_, filePath: string) => {
const normalizedPath = path.normalize(filePath);
return fs.readFileSync(normalizedPath, 'utf-8');
});
}

View file

@ -1,351 +0,0 @@
import { BrowserWindow, ipcMain } from "electron";
import fs from 'fs';
import path from 'path';
import { tempDir } from "../utils/constants";
interface Position {
left: number;
top: number;
width: number;
height: number;
}
interface FontStyles {
name: string;
size: number;
bold: boolean;
weight: number;
color: string;
}
interface TextElement {
position: Position;
paragraphs: {
alignment: number;
text: string;
font: FontStyles;
}[];
}
interface PictureElement {
position: Position;
picture: {
is_network: boolean;
path: string;
};
shape: string | null;
object_fit: {
fit: string | null;
focus: number[];
};
overlay: string | null;
border_radius: number[];
}
interface BoxElement {
position: Position;
type: number;
fill: {
color: string;
};
border_radius: number;
stroke: {
color: string;
thickness: number;
};
shadow: {
radius: number;
color: string;
offset: number;
opacity: number;
angle: number;
};
}
interface LineElement {
position: Position;
lineType: number;
thickness: string;
color: string;
}
interface GraphElement {
position: Position;
picture: {
is_network: boolean;
path: string;
};
border_radius: number[];
}
type SlideElement = TextElement | PictureElement | BoxElement | LineElement | GraphElement;
export function setupSlideMetadataHandlers() {
ipcMain.handle("get-slide-metadata", async (_, url: string, theme: string, customColors?: any) => {
let win: BrowserWindow | null = null;
try {
win = new BrowserWindow({
width: 1920,
height: 1080,
webPreferences: {
webSecurity: false,
preload: path.join(__dirname, '../preloads/index.js'),
},
show: false,
});
await win.loadURL(url, { userAgent: 'electron' });
await win.webContents.executeJavaScript(`
new Promise((resolve) => {
const check = () => {
const el = document.querySelector('[data-element-type="slide-container"]');
if (el) return resolve(true);
setTimeout(check, 200);
};
check();
});
`);
const metadata = await win.webContents.executeJavaScript(
`
(() => {
const rgbToHex = (color) => {
if (!color || color === "transparent" || color === "none") return "000000";
if (color.startsWith("#")) return color.replace("#", "");
const matches = color.match(/\\d+/g);
if (!matches) return "000000";
const [r, g, b] = matches.map(x => parseInt(x));
return [r, g, b].map(x => x.toString(16).padStart(2, "0")).join("");
};
const slidesMetadata = [];
const slideContainers = document.querySelectorAll('[data-element-type="slide-container"]');
slideContainers.forEach((container) => {
const containerEl = container;
containerEl.style.width = "1280px";
containerEl.style.height = "720px";
containerEl.style.transform = "none";
const containerRect = containerEl.getBoundingClientRect();
const slideIndex = parseInt(containerEl.getAttribute("data-slide-index") || "0");
const backgroundColor = rgbToHex(window.getComputedStyle(containerEl).backgroundColor);
const elements = [];
const slideElements = containerEl.querySelectorAll('[data-slide-element]:not([data-element-type="slide-container"])');
slideElements.forEach((element) => {
const el = element;
const elementRect = el.getBoundingClientRect();
const computedStyle = window.getComputedStyle(el);
const position = {
left: Math.round(elementRect.left - containerRect.left),
top: Math.round(elementRect.top - containerRect.top),
width: Math.round(elementRect.width),
height: Math.round(elementRect.height),
};
const elementType = el.getAttribute("data-element-type");
if (!elementType) return;
switch (elementType) {
case "text":
elements.push({
position,
paragraphs: [{
alignment: el.getAttribute("data-is-align") === 'true' ? 2 : 1,
text: el.getAttribute("data-text-content") || el.textContent || "",
font: {
name: computedStyle.fontFamily.split('_')[2] || 'Inter',
size: parseInt(computedStyle.fontSize),
bold: parseInt(computedStyle.fontWeight) >= 500,
weight: parseInt(computedStyle.fontWeight),
color: rgbToHex(computedStyle.color),
},
}],
});
break;
case "picture":
const imgEl = el.tagName.toLowerCase() === "img" ? el : el.querySelector("img");
if (imgEl) {
elements.push({
position,
picture: {
is_network: imgEl.src.startsWith("http"),
path: imgEl.src || imgEl.getAttribute("data-image-path") || "",
},
shape: imgEl.getAttribute('data-image-type'),
object_fit: {
fit: imgEl.getAttribute('data-object-fit'),
focus: [
parseFloat(imgEl.getAttribute('data-focial-point-x') || '0'),
parseFloat(imgEl.getAttribute('data-focial-point-y') || '0'),
],
},
overlay: el.getAttribute("data-is-icon") ? "ffffff" : null,
border_radius: Array(4).fill(parseInt(computedStyle.borderRadius) || 0),
});
}
break;
case "graph":
elements.push({
position,
picture: {
is_network: true,
path: \`__GRAPH_PLACEHOLDER__\${el.getAttribute("data-element-id")}\`,
},
border_radius: [0, 0, 0, 0],
});
break;
case "slide-box":
case "filledbox":
const boxShadow = computedStyle.boxShadow;
let shadowRadius = 0;
let shadowColor = "000000";
let shadowOffsetX = 0;
let shadowOffsetY = 0;
let shadowOpacity = 0;
if (boxShadow && boxShadow !== "none") {
const boxShadowRegex =
/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+),?\\s*([\\d.]+)?\\)?\\s+(-?\\d+)px\\s+(-?\\d+)px\\s+(-?\\d+)px/;
const match = boxShadow.match(boxShadowRegex);
if (match) {
const r = match[1];
const g = match[2];
const b = match[3];
const rgbStr = "rgb(" + r + ", " + g + ", " + b + ")";
shadowColor = rgbToHex(rgbStr);
shadowOpacity = match[4] ? parseFloat(match[4]) : 1;
shadowOffsetX = parseInt(match[5]);
shadowOffsetY = parseInt(match[6]);
shadowRadius = parseInt(match[7]);
}
}
elements.push({
position,
type:
computedStyle.borderRadius === "9999px" ||
computedStyle.borderRadius === "50%"
? 9
: 5,
fill: {
color: rgbToHex(computedStyle.backgroundColor),
},
border_radius: parseInt(computedStyle.borderRadius) || 0,
stroke: {
color: rgbToHex(computedStyle.borderColor),
thickness: parseInt(computedStyle.borderWidth) || 0,
},
shadow: {
radius: shadowRadius,
color: shadowColor,
offset: Math.sqrt(
shadowOffsetX * shadowOffsetX +
shadowOffsetY * shadowOffsetY
),
opacity: shadowOpacity,
angle: Math.round(
(Math.atan2(shadowOffsetY, shadowOffsetX) * 180) / Math.PI
),
},
});
break;
case "line":
elements.push({
position,
lineType: 1,
thickness: computedStyle.borderWidth || computedStyle.height,
color: rgbToHex(
computedStyle.borderColor || computedStyle.backgroundColor
),
});
break;
}
});
slidesMetadata.push({ slideIndex, backgroundColor, elements });
});
return slidesMetadata;
})();
`
)
// ✅ Handle Graphs: capture each graph element as an image
const graphIds: { id: string; bounds: Electron.Rectangle }[] = await win.webContents.executeJavaScript(`
(() => {
return Array.from(document.querySelectorAll('[data-element-type="graph"]')).map(el => el.getAttribute("data-element-id"));
})();
`);
for (const id of graphIds) {
try {
// Scroll into view first
await win.webContents.executeJavaScript(`
document.querySelector('[data-element-id="${id}"]').scrollIntoView({ behavior: 'instant', block: 'center' });
`);
// Wait a bit for any animations/rendering to complete
await new Promise((r) => setTimeout(r, 2000));
const bounds: Electron.Rectangle = await win.webContents.executeJavaScript(`
(() => {
const el = document.querySelector('[data-element-id="${id}"]');
if (!el) return null;
const rect = el.getBoundingClientRect();
return {
x: Math.round(rect.left),
y: Math.round(rect.top),
width: Math.round(rect.width),
height: Math.round(rect.height),
};
})();
`);
const image = await win.webContents.capturePage(bounds);
const buffer = image.toJPEG(100);
if (buffer.length === 0) {
console.error("Empty buffer! Graph not captured.");
continue;
}
const filePath = path.join(tempDir, `chart-${id}-${Date.now()}.jpeg`);
fs.writeFileSync(filePath, buffer);
// Update metadata
metadata.forEach((slide: any) => {
slide.elements.forEach((element: any) => {
if ("picture" in element && element.picture.path === `__GRAPH_PLACEHOLDER__${id}`) {
element.picture.path = filePath;
}
});
});
} catch (err) {
console.error(`Failed to capture or save chart-${id}:`, err);
}
}
return metadata;
} catch (error) {
console.error("Error during page preparation:", error);
throw error;
} finally {
// if (browser) await browser.close();
if (win) win.close();
}
});
}

View file

@ -1,32 +0,0 @@
import { ipcMain } from 'electron';
import { settingsStore } from '../services/settings-store';
const THEME_KEY = 'theme';
export function setupThemeHandlers() {
ipcMain.handle('get-theme', async () => {
try {
const theme = settingsStore.get(THEME_KEY);
return { theme };
} catch (error) {
console.error('Error retrieving theme:', error);
throw error;
}
});
ipcMain.handle('set-theme', async (_, themeData: any) => {
try {
if (!themeData) {
throw new Error('Theme data is required');
}
settingsStore.set(THEME_KEY, themeData);
return { success: true };
} catch (error) {
console.error('Error saving theme:', error);
throw error;
}
});
}

View file

@ -1,28 +0,0 @@
import { ipcMain } from "electron";
import path from "path";
import fs from "fs";
import crypto from "crypto";
import { userDataDir } from "../utils/constants";
export function setupUploadImage() {
ipcMain.handle("upload-image", async (_, file: Buffer) => {
try {
// Create uploads directory if it doesn't exist
const uploadsDir = path.join(userDataDir, "uploads");
fs.mkdirSync(uploadsDir, { recursive: true });
// Generate unique filename
const filename = `${crypto.randomBytes(16).toString('hex')}.png`;
const filePath = path.join(uploadsDir, filename);
// Write file to disk
await fs.writeFileSync(filePath, file);
// Return the relative path that can be used in the frontend
return filePath;
} catch (error) {
console.error("Error saving image:", error);
throw error;
}
});
}

View file

@ -1,12 +0,0 @@
import { ipcMain } from "electron";
import { getUserConfig, setUserConfig } from "../utils";
export function setupUserConfigHandlers() {
ipcMain.handle("get-user-config", async (_, __) => {
return getUserConfig();
});
ipcMain.handle("set-user-config", async (_, userConfig: UserConfig) => {
setUserConfig(userConfig);
});
}

View file

@ -1,98 +0,0 @@
require("dotenv").config();
import { app, BrowserWindow } from "electron";
import path from "path";
import { findUnusedPorts, killProcess, setupEnv, setUserConfig } from "./utils";
import { startFastApiServer, startNextJsServer } from "./utils/servers";
import { ChildProcessByStdio } from "child_process";
import { baseDir, fastapiDir, isDev, localhost, nextjsDir, tempDir, userConfigPath, userDataDir } from "./utils/constants";
import { setupIpcHandlers } from "./ipc";
var win: BrowserWindow | undefined;
var fastApiProcess: ChildProcessByStdio<any, any, any> | undefined;
var nextjsProcess: any;
app.commandLine.appendSwitch('gtk-version', '3');
const createWindow = () => {
win = new BrowserWindow({
width: 1280,
height: 720,
icon: path.join(baseDir, "resources/ui/assets/images/presenton_short_filled.png"),
webPreferences: {
webSecurity: false,
preload: path.join(__dirname, 'preloads/index.js'),
},
});
};
async function startServers(fastApiPort: number, nextjsPort: number) {
try {
fastApiProcess = await startFastApiServer(
fastapiDir,
fastApiPort,
{
DEBUG: isDev ? "True" : "False",
LLM: process.env.LLM,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
APP_DATA_DIRECTORY: userDataDir,
TEMP_DIRECTORY: tempDir,
USER_CONFIG_PATH: userConfigPath,
},
isDev,
);
nextjsProcess = await startNextJsServer(
nextjsDir,
nextjsPort,
{
NEXT_PUBLIC_FAST_API: process.env.NEXT_PUBLIC_FAST_API,
TEMP_DIRECTORY: process.env.TEMP_DIRECTORY,
NEXT_PUBLIC_URL: process.env.NEXT_PUBLIC_URL,
NEXT_PUBLIC_USER_CONFIG_PATH: process.env.NEXT_PUBLIC_USER_CONFIG_PATH,
},
isDev,
)
} catch (error) {
console.error("Server startup error:", error);
}
}
async function stopServers() {
if (fastApiProcess?.pid) {
await killProcess(fastApiProcess.pid);
}
if (nextjsProcess) {
if (isDev) {
await killProcess(nextjsProcess.pid);
} else {
nextjsProcess.close();
}
}
}
app.whenReady().then(async () => {
createWindow();
win?.loadFile(path.join(baseDir, "resources/ui/homepage/index.html"));
setUserConfig({
LLM: process.env.LLM,
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
GOOGLE_API_KEY: process.env.GOOGLE_API_KEY,
})
const [fastApiPort, nextjsPort] = await findUnusedPorts();
console.log(`FastAPI port: ${fastApiPort}, NextJS port: ${nextjsPort}`);
//? Setup environment variables to be used in the preloads
setupEnv(fastApiPort, nextjsPort);
setupIpcHandlers();
await startServers(fastApiPort, nextjsPort);
win?.loadURL(`${localhost}:${nextjsPort}`);
});
app.on("window-all-closed", async () => {
await stopServers();
app.quit();
});

View file

@ -1,26 +0,0 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('env', {
NEXT_PUBLIC_FAST_API: process.env.NEXT_PUBLIC_FAST_API || '',
NEXT_PUBLIC_URL: process.env.NEXT_PUBLIC_URL || '',
TEMP_DIRECTORY: process.env.TEMP_DIRECTORY || '',
NEXT_PUBLIC_USER_CONFIG_PATH: process.env.NEXT_PUBLIC_USER_CONFIG_PATH || '',
});
contextBridge.exposeInMainWorld('electron', {
fileDownloaded: (filePath: string) => ipcRenderer.invoke("file-downloaded", filePath),
exportAsPDF: (id: string, title: string) => ipcRenderer.invoke("export-as-pdf", id, title),
getUserConfig: () => ipcRenderer.invoke("get-user-config"),
setUserConfig: (userConfig: UserConfig) => ipcRenderer.invoke("set-user-config", userConfig),
readFile: (filePath: string) => ipcRenderer.invoke("read-file", filePath),
getSlideMetadata: (url: string, theme: string, customColors?: any, tempDirectory?: string) =>
ipcRenderer.invoke("get-slide-metadata", url, theme, customColors, tempDirectory),
getFooter: (userId: string) => ipcRenderer.invoke("get-footer", userId),
setFooter: (userId: string, properties: any) => ipcRenderer.invoke("set-footer", userId, properties),
getTheme: (userId: string) => ipcRenderer.invoke("get-theme", userId),
setTheme: (userId: string, themeData: any) => ipcRenderer.invoke("set-theme", userId, themeData),
uploadImage: (file: Buffer) => ipcRenderer.invoke("upload-image", file),
writeNextjsLog: (logData: string) => ipcRenderer.invoke("write-nextjs-log", logData),
clearNextjsLogs: () => ipcRenderer.invoke("clear-nextjs-logs"),
});

27
app/types/index.d.ts vendored
View file

@ -1,27 +0,0 @@
interface FastApiEnv {
DEBUG?: string,
LLM?: string,
OPENAI_API_KEY?: string,
GOOGLE_API_KEY?: string,
APP_DATA_DIRECTORY?: string,
TEMP_DIRECTORY?: string,
USER_CONFIG_PATH?: string,
}
interface NextJsEnv {
NEXT_PUBLIC_FAST_API?: string,
TEMP_DIRECTORY?: string,
NEXT_PUBLIC_URL?: string,
NEXT_PUBLIC_USER_CONFIG_PATH?: string,
}
interface UserConfig {
LLM?: string,
OPENAI_API_KEY?: string,
GOOGLE_API_KEY?: string,
}
interface IPCStatus {
success: boolean,
message?: string,
}

View file

@ -1,16 +0,0 @@
import { app } from "electron"
import path from "path"
export const localhost = "http://127.0.0.1"
export const isDev = !app.isPackaged;
export const baseDir = app.getAppPath();
export const fastapiDir = isDev ? path.join(baseDir, "servers/fastapi") : path.join(baseDir, "resources/fastapi");
export const nextjsDir = isDev ? path.join(baseDir, "servers/nextjs") : path.join(baseDir, "resources/nextjs");
export const tempDir = path.join(app.getPath("temp"), "presenton")
export const userDataDir = app.getPath("userData")
export const downloadsDir = app.getPath("downloads")
export const userConfigPath = path.join(userDataDir, "userConfig.json")
export const logsDir = path.join(userDataDir, "logs")

View file

@ -1,26 +0,0 @@
import { shell } from "electron";
import { dialog } from "electron";
import path from "path";
export async function showFileDownloadedDialog(filePath: string): Promise<boolean> {
try {
const { response } = await dialog.showMessageBox({
type: 'question',
buttons: ['Open File', 'Open Folder', 'Cancel'],
defaultId: 0,
title: 'File Downloaded',
message: 'What would you like to do?'
});
if (response === 0) {
await shell.openPath(filePath);
} else if (response === 1) {
await shell.openPath(path.dirname(filePath));
}
return true;
} catch (error: any) {
console.error('Error handling downloaded file:', error);
return false;
}
}

View file

@ -1,83 +0,0 @@
import net from 'net'
import treeKill from 'tree-kill'
import fs from 'fs'
import { localhost, tempDir, userConfigPath } from './constants'
export function setUserConfig(userConfig: UserConfig) {
let existingConfig: UserConfig = {}
if (fs.existsSync(userConfigPath)) {
const configData = fs.readFileSync(userConfigPath, 'utf-8')
existingConfig = JSON.parse(configData)
}
const mergedConfig: UserConfig = {
LLM: userConfig.LLM || existingConfig.LLM,
OPENAI_API_KEY: userConfig.OPENAI_API_KEY || existingConfig.OPENAI_API_KEY,
GOOGLE_API_KEY: userConfig.GOOGLE_API_KEY || existingConfig.GOOGLE_API_KEY
}
fs.writeFileSync(userConfigPath, JSON.stringify(mergedConfig))
}
export function getUserConfig(): UserConfig {
if (!fs.existsSync(userConfigPath)) {
return {}
}
const configData = fs.readFileSync(userConfigPath, 'utf-8')
return JSON.parse(configData)
}
export function setupEnv(fastApiPort: number, nextjsPort: number) {
process.env.NEXT_PUBLIC_FAST_API = `${localhost}:${fastApiPort}`;
process.env.TEMP_DIRECTORY = tempDir;
process.env.NEXT_PUBLIC_USER_CONFIG_PATH = userConfigPath;
process.env.NEXT_PUBLIC_URL = `${localhost}:${nextjsPort}`;
}
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 findUnusedPorts(startPort: number = 40000, count: number = 2): Promise<number[]> {
const ports: number[] = [];
console.log(`Finding ${count} unused ports starting from ${startPort}`);
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 < count) {
if (await isPortAvailable(currentPort)) {
ports.push(currentPort);
}
currentPort++;
}
return ports;
}
export function sanitizeFilename(filename: string): string {
return filename.replace(/[\\/:*?"<>|]/g, '_');
}

View file

@ -1,117 +0,0 @@
import { spawn } from "child_process";
import { localhost, logsDir, userDataDir } from "./constants";
import http from "http";
import fs from "fs";
// @ts-ignore
import handler from "serve-handler";
import path from "path";
export async function startFastApiServer(
directory: string,
port: number,
env: FastApiEnv,
isDev: boolean,
) {
// Start FastAPI server
const startCommand = isDev ? [
".venv/bin/python",
["server_autoreload.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) => {
fs.appendFileSync(path.join(logsDir, "fastapi-server.log"), data);
console.log(`FastAPI: ${data}`);
});
fastApiProcess.stderr.on("data", (data: any) => {
fs.appendFileSync(path.join(logsDir, "fastapi-server.log"), data);
console.error(`FastAPI: ${data}`);
});
// Wait for FastAPI server to start
await waitForServer(`${localhost}:${port}/docs`);
return fastApiProcess;
}
export async function startNextJsServer(
directory: string,
port: number,
env: NextJsEnv,
isDev: boolean,
) {
let nextjsProcess;
if (isDev) {
// Start NextJS development server
nextjsProcess = spawn(
"npm",
["run", "dev", "--", "-p", port.toString()],
{
cwd: directory,
stdio: ["inherit", "pipe", "pipe"],
env: { ...process.env, ...env },
}
);
nextjsProcess.stdout.on("data", (data: any) => {
fs.appendFileSync(path.join(logsDir, "nextjs-server.log"), data);
console.log(`NextJS: ${data}`);
});
nextjsProcess.stderr.on("data", (data: any) => {
fs.appendFileSync(path.join(logsDir, "nextjs-server.log"), data);
console.error(`NextJS: ${data}`);
});
} else {
// Start NextJS build server
nextjsProcess = startNextjsBuildServer(directory, port);
}
// Wait for NextJS server to start
await waitForServer(`${localhost}:${port}`);
return nextjsProcess;
}
async function startNextjsBuildServer(directory: string, port: number) {
const server = http.createServer((req, res) => {
return handler(req, res, {
public: directory,
cleanUrls: true,
});
});
server.listen(port);
return server;
}
async function waitForServer(url: string, timeout = 30000): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
await new Promise<void>((resolve, reject) => {
http.get(url, (res) => {
if (res.statusCode === 200 || res.statusCode === 304) {
resolve();
} else {
reject(new Error(`Unexpected status code: ${res.statusCode}`));
}
}).on('error', reject);
});
return;
} catch (error) {
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
throw new Error(`Server did not start within ${timeout}ms`);
}

View file

@ -1,23 +0,0 @@
const builder = require("electron-builder")
const config = {
appId: "ai.presenton",
asar: false,
directories: {
output: "dist",
},
files: [
"resources",
"app_dist",
"node_modules",
"NOTICE",
],
linux: {
artifactName: "Presenton-${version}.${ext}",
target: ["AppImage"],
icon: "resources/ui/assets/images/presenton_short_filled.png",
},
}
builder.build({ config })

54
docker-compose.yml Normal file
View file

@ -0,0 +1,54 @@
services:
production:
# image: ghcr.io/presenton/presenton:latest
build:
context: .
dockerfile: Dockerfile
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
ports:
# You can replace 5000 with any other port number of your choice to run Presenton on a different port number.
- "5000:80"
volumes:
- ./user_data:/app/user_data
environment:
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
- LLM=${LLM}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
- OLLAMA_MODEL=${OLLAMA_MODEL}
- PEXELS_API_KEY=${PEXELS_API_KEY}
development:
build:
context: .
dockerfile: Dockerfile.dev
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
ports:
- "5000:80"
- "3000:3000"
- "8000:8000"
volumes:
- .:/app
environment:
- NODE_ENV=development
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
- LLM=${LLM}
- OPENAI_API_KEY=${OPENAI_API_KEY}
- GOOGLE_API_KEY=${GOOGLE_API_KEY}
- OLLAMA_MODEL=${OLLAMA_MODEL}
- PEXELS_API_KEY=${PEXELS_API_KEY}
- LANGCHAIN_TRACING_V2=${LANGCHAIN_TRACING_V2}
- LANGCHAIN_API_KEY=${LANGCHAIN_API_KEY}
- LANGCHAIN_PROJECT=${LANGCHAIN_PROJECT}

29
nginx.conf Normal file
View file

@ -0,0 +1,29 @@
user www-data;
worker_processes auto;
pid /run/nginx.pid;
error_log /var/log/nginx/error.log;
events {
worker_connections 1024;
}
http {
client_max_body_size 20M;
server {
listen 80;
server_name localhost;
location / {
proxy_pass http://localhost:3000;
proxy_read_timeout 30m;
proxy_connect_timeout 30m;
}
location /api/v1/ {
proxy_pass http://localhost:8000;
proxy_read_timeout 30m;
proxy_connect_timeout 30m;
}
}
}

5984
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,40 +0,0 @@
{
"name": "presenton",
"main": "app_dist/main.js",
"version": "0.2.0-beta",
"description": "Presenton Open Source",
"homepage": "https://presenton.ai",
"author": {
"name": "Presenton",
"email": "contact@presenton.ai"
},
"scripts": {
"dev": "rm -rf app_dist && tsc && electron .",
"setup:env": "npm install && cd servers/fastapi && poetry env remove --all && poetry install && .venv/bin/pip uninstall -y hf_xet && cd ../../servers/nextjs && npm install",
"install:pyinstaller": "cd servers/fastapi && .venv/bin/pip install pyinstaller",
"build:ts": "rm -rf app_dist && tsc",
"build:css": "tailwindcss -i ./resources/ui/assets/css/tailwind.import.css -o ./resources/ui/assets/css/tailwind.css --watch",
"build:nextjs": "rm -rf resources/nextjs && cd servers/nextjs && npm run build && cp -r out ../../resources/nextjs",
"build:fastapi": "rm -rf resources/fastapi && cd servers/fastapi && .venv/bin/pyinstaller --distpath ../../resources server.spec",
"build:electron": "rm -rf app_dist && tsc && node build.js",
"build:all": "npm run clean:build && npm run setup:env && npm run build:ts && npm run install:pyinstaller && npm run build:nextjs && npm run build:fastapi && npm run build:electron",
"clean:build": "rm -rf resources/nextjs && rm -rf resources/fastapi && rm -rf app_dist"
},
"devDependencies": {
"electron": "^36.1.0",
"electron-builder": "^26.0.12",
"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",
"puppeteer": "^24.8.2",
"serve-handler": "^6.1.6",
"tailwindcss": "^4.1.5",
"tree-kill": "^1.2.2"
}
}

File diff suppressed because it is too large Load diff

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 861 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View file

@ -1,32 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Presenton</title>
<link rel="stylesheet" href="../assets/css/tailwind.css">
<script src="./script.js"></script>
<style>
.loading-circle {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body class="bg-gray-900 text-white flex flex-col items-center justify-center h-screen">
<img src="../assets/images/presenton_logo.png" alt="Presenton Logo" class="h-20">
<p class="mt-20 text-lg">Just a moment...</p>
<div class="loading-circle mt-10"></div>
</body>
</html>

View file

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

View file

@ -0,0 +1 @@
{}

View file

@ -1,12 +1,33 @@
import os
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
import ollama
from sqlmodel import SQLModel
from contextlib import asynccontextmanager
from api.routers.presentation.router import presentation_router
from api.services.database import sql_engine
from api.utils import update_env_with_user_config
from api.utils.supported_ollama_models import SUPPORTED_OLLAMA_MODELS
from api.utils.utils import is_ollama_selected, update_env_with_user_config
can_change_keys = os.getenv("CAN_CHANGE_KEYS") != "false"
# Ollama model download
if not can_change_keys and is_ollama_selected():
ollama_model = os.getenv("OLLAMA_MODEL")
pexels_api_key = os.getenv("PEXELS_API_KEY")
if not (ollama_model or pexels_api_key):
raise Exception("OLLAMA_MODEL and PEXELS_API_KEY must be provided")
if ollama_model not in SUPPORTED_OLLAMA_MODELS:
raise Exception(f"Model {ollama_model} is not supported")
print("-" * 50)
print("Pulling model: ", ollama_model)
for event in ollama.pull(ollama_model, stream=True):
print(event)
print("Pulled model: ", ollama_model)
print("-" * 50)
@asynccontextmanager
@ -29,7 +50,8 @@ app.add_middleware(
@app.middleware("http")
async def update_env_middleware(request: Request, call_next):
update_env_with_user_config()
if can_change_keys:
update_env_with_user_config()
return await call_next(request)

View file

@ -62,3 +62,14 @@ class UserConfig(BaseModel):
LLM: Optional[str] = None
OPENAI_API_KEY: Optional[str] = None
GOOGLE_API_KEY: Optional[str] = None
OLLAMA_MODEL: Optional[str] = None
PEXELS_API_KEY: Optional[str] = None
class OllamaModelMetadata(BaseModel):
label: str
value: str
description: str
icon: str
size: str
supports_graph: bool

View file

@ -6,7 +6,7 @@ from api.routers.presentation.models import (
DecomposeDocumentsRequest,
DecomposeDocumentsResponse,
)
from api.services.instances import temp_file_service
from api.services.instances import TEMP_FILE_SERVICE
from api.services.logging import LoggingService
from document_processor.loader import DocumentsLoader
@ -20,7 +20,7 @@ class DecomposeDocumentsHandler:
)
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
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(
@ -34,7 +34,7 @@ class DecomposeDocumentsHandler:
document_paths = []
for parsed_doc in parsed_documents:
file_path = temp_file_service.create_temp_file_path(
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")

View file

@ -4,7 +4,7 @@ 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
from api.utils import get_presentation_dir
from api.utils.utils import get_presentation_dir
class DeletePresentationHandler:

View file

@ -1,5 +1,5 @@
import asyncio
from typing import Literal
import os
import uuid
from sqlalchemy import update
@ -8,13 +8,17 @@ from api.models import LogMetadata
from api.routers.presentation.models import (
EditPresentationSlideRequest,
)
from api.services.instances import temp_file_service
from api.services.instances import TEMP_FILE_SERVICE
from api.services.logging import LoggingService
from api.utils import get_presentation_dir, get_presentation_images_dir
from api.utils.supported_ollama_models import SUPPORTED_OLLAMA_MODELS
from api.utils.utils import (
get_presentation_dir,
get_presentation_images_dir,
is_ollama_selected,
)
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.models.other_models import SlideType
from ppt_generator.models.query_and_prompt_models import (
IconQueryCollectionWithData,
ImagePromptWithThemeAndAspectRatio,
@ -38,12 +42,12 @@ class PresentationEditHandler:
self.prompt = data.prompt
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
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)
TEMP_FILE_SERVICE.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
@ -61,9 +65,16 @@ class PresentationEditHandler:
).first()
slide_to_edit = SlideModel.from_dict(slide_to_edit_sql.model_dump(mode="json"))
new_slide_type = SlideType(
(await get_slide_type_from_prompt(self.prompt, slide_to_edit)).slide_type
)
new_slide_type = await get_slide_type_from_prompt(self.prompt, slide_to_edit)
new_slide_type = new_slide_type.slide_type
if is_ollama_selected():
model = SUPPORTED_OLLAMA_MODELS[os.getenv("OLLAMA_MODEL")]
if not model.supports_graph:
if new_slide_type == 5:
new_slide_type = 1
elif new_slide_type == 9:
new_slide_type = 6
edited_content = await get_edited_slide_content_model(
self.prompt,
@ -173,7 +184,7 @@ class PresentationEditHandler:
update(SlideSqlModel)
.where(SlideSqlModel.id == slide_to_edit.id)
.values(
type=new_slide_type.value,
type=new_slide_type,
images=list(new_slide_images.values()),
icons=list(new_slide_icons.values()),
content=new_slide_model.content.model_dump(mode="json"),

View file

@ -9,9 +9,9 @@ from api.routers.presentation.models import (
PresentationAndPath,
)
from api.services.logging import LoggingService
from api.services.instances import temp_file_service
from api.services.instances import TEMP_FILE_SERVICE
from api.sql_models import PresentationSqlModel
from api.utils import get_presentation_dir, sanitize_filename
from api.utils.utils import get_presentation_dir, sanitize_filename
from ppt_generator.pptx_presentation_creator import PptxPresentationCreator
from api.services.database import get_sql_session
@ -22,12 +22,12 @@ class ExportAsPptxHandler(FetchPresentationAssetsMixin):
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
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)
TEMP_FILE_SERVICE.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(

View file

@ -1,11 +1,21 @@
import os
import random
import uuid
from fastapi import HTTPException
from api.models import LogMetadata, SessionModel
from api.routers.presentation.handlers.list_supported_ollama_models import (
SUPPORTED_OLLAMA_MODELS,
)
from api.routers.presentation.models import PresentationGenerateRequest
from api.services.logging import LoggingService
from api.sql_models import KeyValueSqlModel
from api.sql_models import KeyValueSqlModel, PresentationSqlModel
from api.services.database import get_sql_session
from api.utils.utils import is_ollama_selected
from ppt_config_generator.models import PresentationMarkdownModel, SlideStructureModel
from ppt_config_generator.structure_generator import generate_presentation_structure
SLIDES_WITHOUT_GRAPH = [2, 4, 6, 7, 8]
class PresentationGenerateDataHandler:
@ -20,8 +30,8 @@ class PresentationGenerateDataHandler:
extra=log_metadata.model_dump(),
)
if not self.data.titles:
raise HTTPException(400, "Titles can not be empty")
if not self.data.outlines:
raise HTTPException(400, "Outlines can not be empty")
key_value_model = KeyValueSqlModel(
id=self.session,
@ -29,6 +39,58 @@ class PresentationGenerateDataHandler:
value=self.data.model_dump(mode="json"),
)
if is_ollama_selected():
with get_sql_session() as sql_session:
presentation = sql_session.get(
PresentationSqlModel, self.data.presentation_id
)
presentation_structure = await generate_presentation_structure(
PresentationMarkdownModel(
**{
"title": presentation.title,
"slides": presentation.outlines,
"notes": presentation.notes,
}
)
)
supports_graph = True
model = SUPPORTED_OLLAMA_MODELS[os.getenv("OLLAMA_MODEL")]
supports_graph = model.supports_graph
for each in presentation_structure.slides:
if each.type > 9:
each.type = random.choice(SLIDES_WITHOUT_GRAPH)
if each.type == 3:
each.type = 6
if not supports_graph:
if each.type == 5:
each.type = 1
elif each.type == 9:
each.type = 6
presentation_outlines_len = len(presentation.outlines)
missing_slides_len = presentation_outlines_len - len(
presentation_structure.slides
)
if missing_slides_len > 0:
for index in range(missing_slides_len):
selected_type = (
random.choice(SLIDES_WITHOUT_GRAPH)
if index != missing_slides_len - 1
else 1
)
presentation_structure.slides.append(
SlideStructureModel(type=selected_type)
)
elif missing_slides_len < 0:
presentation_structure.slides = presentation_structure.slides[
:presentation_outlines_len
]
presentation.structure = presentation_structure.model_dump(mode="json")
sql_session.commit()
sql_session.refresh(presentation)
with get_sql_session() as sql_session:
sql_session.add(key_value_model)
sql_session.commit()

View file

@ -6,8 +6,8 @@ from api.routers.presentation.models import (
PresentationAndPaths,
)
from api.services.logging import LoggingService
from api.services.instances import temp_file_service
from api.utils import get_presentation_dir, get_presentation_images_dir
from api.services.instances import TEMP_FILE_SERVICE
from api.utils.utils import get_presentation_dir, get_presentation_images_dir
from image_processor.images_finder import generate_image
@ -17,7 +17,7 @@ class GenerateImageHandler:
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
self.temp_dir = TEMP_FILE_SERVICE.create_temp_dir(self.session)
self.presentation_dir = get_presentation_dir(self.data.presentation_id)

View file

@ -1,24 +1,24 @@
import uuid
import re
from api.models import LogMetadata
from api.routers.presentation.models import GenerateTitleRequest
from api.services.instances import temp_file_service
from api.routers.presentation.models import GenerateOutlinesRequest
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 ppt_config_generator.ppt_outlines_generator import generate_ppt_content
from api.services.database import get_sql_session
class PresentationTitlesGenerateHandler:
def __init__(self, data: GenerateTitleRequest):
class PresentationOutlinesGenerateHandler:
def __init__(self, data: GenerateOutlinesRequest):
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
self.temp_dir = TEMP_FILE_SERVICE.create_temp_dir(self.session)
def __del__(self):
temp_file_service.cleanup_temp_dir(self.temp_dir)
TEMP_FILE_SERVICE.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
@ -32,15 +32,21 @@ class PresentationTitlesGenerateHandler:
PresentationSqlModel, self.data.presentation_id
)
presentation_titles: PresentationTitlesModel = await generate_ppt_titles(
presentation_content = await generate_ppt_content(
presentation.prompt,
presentation.n_slides,
presentation.summary,
presentation.language,
presentation.summary,
)
presentation_content.slides = presentation_content.slides[
: presentation.n_slides
]
presentation.title = presentation_titles.presentation_title
presentation.titles = presentation_titles.titles
presentation.title = presentation_content.title
presentation.outlines = [
each.model_dump() for each in presentation_content.slides
]
presentation.notes = presentation_content.notes
sql_session.commit()
sql_session.refresh(presentation)

View file

@ -0,0 +1,190 @@
from typing import List
import uuid, aiohttp
from fastapi import HTTPException
from api.models import LogMetadata
from api.routers.presentation.handlers.export_as_pptx import ExportAsPptxHandler
from api.routers.presentation.handlers.upload_files import UploadFilesHandler
from api.routers.presentation.mixins.fetch_assets_on_generation import (
FetchAssetsOnPresentationGenerationMixin,
)
from api.routers.presentation.models import (
ExportAsRequest,
GeneratePresentationRequest,
PresentationAndPath,
PresentationPathAndEditPath,
)
from api.services.database import get_sql_session
from api.services.instances import TEMP_FILE_SERVICE
from api.services.logging import LoggingService
from api.sql_models import PresentationSqlModel, SlideSqlModel
from api.utils.utils import get_presentation_dir, is_ollama_selected
from document_processor.loader import DocumentsLoader
from ppt_config_generator.document_summary_generator import generate_document_summary
from ppt_config_generator.models import PresentationMarkdownModel
from ppt_config_generator.ppt_outlines_generator import generate_ppt_content
from ppt_generator.generator import generate_presentation
from ppt_generator.models.llm_models import (
LLM_CONTENT_TYPE_MAPPING,
LLMPresentationModel,
)
from langchain_core.output_parsers import JsonOutputParser
from ppt_generator.models.slide_model import SlideModel
output_parser = JsonOutputParser(pydantic_object=LLMPresentationModel)
class GeneratePresentationHandler(FetchAssetsOnPresentationGenerationMixin):
def __init__(self, presentation_id: str, data: GeneratePresentationRequest):
self.session = str(uuid.uuid4())
self.presentation_id = presentation_id
self.data = data
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):
if is_ollama_selected():
raise HTTPException(
status_code=400,
detail="Ollama is not currently supported for this endpoint",
)
documents_and_images_path = await UploadFilesHandler(
documents=self.data.documents,
images=None,
).post(logging_service, log_metadata)
summary = None
if documents_and_images_path.documents:
documents_loader = DocumentsLoader(documents_and_images_path.documents)
await documents_loader.load_documents(self.temp_dir)
print("-" * 40)
print("Generating Document Summary")
summary = await generate_document_summary(documents_loader.documents)
print("-" * 40)
print("Generating PPT Outline")
presentation_content = await generate_ppt_content(
self.data.prompt,
self.data.n_slides,
self.data.language,
summary,
)
print("-" * 40)
print("Generating Presentation")
presentation_text = (
await generate_presentation(
PresentationMarkdownModel(
title=presentation_content.title,
slides=presentation_content.slides,
notes=presentation_content.notes,
)
)
).content
print("-" * 40)
print("Parsing Presentation")
presentation_json = output_parser.parse(presentation_text)
slide_models: List[SlideModel] = []
for i, slide in enumerate(presentation_json["slides"]):
slide["index"] = i
slide["presentation"] = self.presentation_id
slide["content"] = (
LLM_CONTENT_TYPE_MAPPING[slide["type"]](**slide["content"])
.to_content()
.model_dump(mode="json")
)
slide_model = SlideModel(**slide)
slide_models.append(slide_model)
print("-" * 40)
print("Fetching Theme Colors")
async with aiohttp.ClientSession() as session:
async with session.get(
f"http://localhost/api/get-theme-from-name?theme={self.data.theme.value}",
) as response:
self.theme = await response.json()
print("-" * 40)
print("Fetching Slide Assets")
async for result in self.fetch_slide_assets(slide_models):
print(result)
slide_sql_models = [
SlideSqlModel(**each.model_dump(mode="json")) for each in slide_models
]
presentation = PresentationSqlModel(
id=self.presentation_id,
prompt=self.data.prompt,
n_slides=self.data.n_slides,
language=self.data.language,
summary=summary,
theme=self.theme,
title=presentation_content.title,
outlines=[each.model_dump() for each in presentation_content.slides],
notes=presentation_content.notes,
)
with get_sql_session() as sql_session:
sql_session.add(presentation)
sql_session.add_all(slide_sql_models)
sql_session.commit()
for each in slide_sql_models:
sql_session.refresh(each)
if self.data.export_as == "pptx":
print("-" * 40)
print("Fetching Slide Metadata for Export")
async with aiohttp.ClientSession() as session:
async with session.post(
f"http://localhost/api/slide-metadata",
json={
"url": f"http://localhost/presentation?id={self.presentation_id}",
"theme": self.theme["name"],
"customColors": self.theme["colors"],
},
) as response:
export_request_body = await response.json()
print("-" * 40)
print("Exporting Presentation")
export_request_body["presentation_id"] = self.presentation_id
export_request = ExportAsRequest(**export_request_body)
presentation_and_path = await ExportAsPptxHandler(export_request).post(
logging_service, log_metadata
)
else:
print("-" * 40)
print("Exporting Presentation as PDF")
async with aiohttp.ClientSession() as session:
async with session.post(
f"http://localhost/api/export-as-pdf",
json={
"url": f"http://localhost/pdf-maker?id={self.presentation_id}",
"title": presentation_content.title,
},
) as response:
response_json = await response.json()
presentation_and_path = PresentationAndPath(
presentation_id=self.presentation_id,
path=response_json["path"].replace("app", "static"),
)
presentation_and_path.path = presentation_and_path.path.replace("app", "static")
return PresentationPathAndEditPath(
**presentation_and_path.model_dump(),
edit_path=f"/presentation?id={self.presentation_id}",
)

View file

@ -3,7 +3,7 @@ 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.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
@ -21,11 +21,9 @@ class GeneratePresentationRequirementsHandler:
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)
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(
@ -33,7 +31,7 @@ class GeneratePresentationRequirementsHandler:
extra=log_metadata.model_dump(),
)
all_document_paths = [*self.documents, *self.research_reports]
all_document_paths = [*self.documents]
documents_loader = DocumentsLoader(all_document_paths)
await documents_loader.load_documents(self.temp_dir)

View file

@ -2,7 +2,7 @@ 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 api.services.instances import TEMP_FILE_SERVICE
from research_report.generator import get_report
@ -11,7 +11,7 @@ class GenerateResearchReportHandler:
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
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(
@ -22,7 +22,7 @@ class GenerateResearchReportHandler:
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)
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)

View file

@ -1,4 +1,3 @@
import asyncio
import json
from typing import List
@ -8,6 +7,9 @@ from sqlmodel import delete
from api.models import LogMetadata, SSECompleteResponse, SSEResponse, SSEStatusResponse
from api.routers.presentation.mixins.fetch_assets_on_generation import (
FetchAssetsOnPresentationGenerationMixin,
)
from api.routers.presentation.models import (
PresentationAndSlides,
PresentationGenerateRequest,
@ -15,31 +17,38 @@ from api.routers.presentation.models import (
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, get_presentation_images_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 api.utils.utils import get_presentation_dir, is_ollama_selected
from ppt_config_generator.models import (
PresentationMarkdownModel,
PresentationStructureModel,
)
from ppt_generator.generator import generate_presentation_stream
from ppt_generator.models.llm_models import LLMPresentationModel
from ppt_generator.models.content_type_models import CONTENT_TYPE_MAPPING
from ppt_generator.models.llm_models import (
LLM_CONTENT_TYPE_MAPPING,
LLMPresentationModel,
LLMSlideModel,
)
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 api.services.instances import TEMP_FILE_SERVICE
from langchain_core.output_parsers import JsonOutputParser
from ppt_generator.slide_generator import get_slide_content_from_type_and_outline
output_parser = JsonOutputParser(pydantic_object=LLMPresentationModel)
class PresentationGenerateStreamHandler:
class PresentationGenerateStreamHandler(FetchAssetsOnPresentationGenerationMixin):
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.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)
TEMP_FILE_SERVICE.cleanup_temp_dir(self.temp_dir)
async def get(self, *args, **kwargs):
with get_sql_session() as sql_session:
@ -53,8 +62,8 @@ class PresentationGenerateStreamHandler:
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
self.title = self.data.title or ""
self.outlines = self.data.outlines
return StreamingResponse(
self.get_stream(*args, **kwargs), media_type="text/event-stream"
@ -68,13 +77,13 @@ class PresentationGenerateStreamHandler:
extra=log_metadata.model_dump(),
)
if not self.titles:
raise HTTPException(400, "Titles can not be empty")
if not self.outlines:
raise HTTPException(400, "Outlines 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.outlines = [each.model_dump() for each in self.outlines]
presentation.title = self.title or presentation.title
presentation.theme = self.theme
sql_session.exec(
delete(SlideSqlModel).where(
@ -84,48 +93,37 @@ class PresentationGenerateStreamHandler:
sql_session.commit()
sql_session.refresh(presentation)
self.presentation = 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()
self.presentation_json = None
print("-" * 40)
print(presentation_text)
print("-" * 40)
presentation_json = output_parser.parse(presentation_text)
print("-" * 40)
print(presentation_json)
print("-" * 40)
# self.presentation_json will be mutated by the generator
if is_ollama_selected():
async for result in self.generate_presentation_ollama():
yield result
else:
async for result in self.generate_presentation_openai_google():
yield result
slide_models: List[SlideModel] = []
for i, content in enumerate(presentation_json["slides"]):
content["index"] = i
content["presentation"] = presentation.id
slide_model = SlideModel(**content)
for i, slide in enumerate(self.presentation_json["slides"]):
slide["index"] = i
slide["presentation"] = self.presentation.id
slide["content"] = (
LLM_CONTENT_TYPE_MAPPING[slide["type"]](**slide["content"])
.to_content()
.model_dump(mode="json")
)
slide_model = SlideModel(**slide)
slide_models.append(slide_model)
async for result in self.fetch_slide_assets(slide_models):
yield result
print("-" * 40)
print(slide_models)
print("-" * 40)
slide_sql_models = [
SlideSqlModel(**each.model_dump(mode="json")) for each in slide_models
]
@ -139,52 +137,68 @@ class PresentationGenerateStreamHandler:
yield SSEStatusResponse(status="Packing slide data").to_string()
response = PresentationAndSlides(
presentation=presentation, slides=slide_sql_models
presentation=self.presentation, slides=slide_sql_models
).to_response_dict()
yield SSECompleteResponse(key="presentation", value=response).to_string()
async def fetch_slide_assets(self, slide_models: List[SlideModel]):
image_prompts = []
icon_queries = []
for each_slide_model in slide_models:
slide_model_utils = SlideModelUtils(self.theme, each_slide_model)
image_prompts.extend(slide_model_utils.get_image_prompts())
icon_queries.extend(slide_model_utils.get_icon_queries())
if icon_queries:
icon_vector_store = get_icons_vectorstore()
images_directory = get_presentation_images_dir(self.presentation_id)
coroutines = [
generate_image(
each,
images_directory,
async def generate_presentation_openai_google(self):
presentation_text = ""
async for chunk in generate_presentation_stream(
PresentationMarkdownModel(
title=self.title,
slides=self.outlines,
notes=self.presentation.notes,
)
for each in image_prompts
] + [get_icon(icon_vector_store, each) for each in icon_queries]
):
presentation_text += chunk.content
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": chunk.content}),
).to_string()
assets_future = asyncio.gather(*coroutines)
self.presentation_json = output_parser.parse(presentation_text)
while not assets_future.done():
status = SSEStatusResponse(status="Fetching slide assets").to_string()
yield status
await asyncio.sleep(5)
async def generate_presentation_ollama(self):
presentation_structure = PresentationStructureModel(
**self.presentation.structure
)
slide_models = []
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": '{ "slides": [ '}),
).to_string()
n_slides = len(presentation_structure.slides)
for i, slide_structure in enumerate(presentation_structure.slides):
# Informing about the start of the slide
# This is to make sure that the client renders slide n
# when it receives start chunk of slide n + 1
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": "{"}),
).to_string()
assets = await assets_future
slide_content = await get_slide_content_from_type_and_outline(
slide_structure.type, self.outlines[i]
)
slide_model = LLMSlideModel(
type=slide_structure.type,
content=slide_content.model_dump(mode="json"),
)
slide_models.append(slide_model)
chunk = json.dumps(slide_model.model_dump(mode="json"))
image_prompts_len = len(image_prompts)
if i < n_slides - 1:
chunk += ","
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": chunk[1:]}),
).to_string()
yield SSEResponse(
event="response",
data=json.dumps({"type": "chunk", "chunk": " ] }"}),
).to_string()
images = assets[:image_prompts_len]
icons = assets[image_prompts_len:]
for each_slide_model in slide_models:
each_slide_model.images = images[: each_slide_model.images_count]
images = images[each_slide_model.images_count :]
each_slide_model.icons = icons[: each_slide_model.icons_count]
icons = icons[each_slide_model.icons_count :]
yield SSEStatusResponse(status="Slide assets fetched").to_string()
self.presentation_json = LLMPresentationModel(
slides=slide_models,
).model_dump(mode="json")

View file

@ -0,0 +1,31 @@
import ollama
from api.models import LogMetadata
from api.routers.presentation.models import OllamaModelStatusResponse
from api.services.logging import LoggingService
class ListPulledOllamaModelsHandler:
async def get(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message("Listing Ollama models"),
extra=log_metadata.model_dump(),
)
response = ollama.list()
logging_service.logger.info(
logging_service.message(response.model_dump(mode="json")),
extra=log_metadata.model_dump(),
)
return [
OllamaModelStatusResponse(
name=model.model,
size=model.size,
status="pulled",
downloaded=model.size,
done=True,
)
for model in response.models
]

View file

@ -0,0 +1,16 @@
from api.models import LogMetadata, OllamaModelMetadata
from api.routers.presentation.models import OllamaSupportedModelsResponse
from api.services.logging import LoggingService
from api.utils.supported_ollama_models import SUPPORTED_OLLAMA_MODELS
class ListSupportedOllamaModelsHandler:
async def get(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
logging_service.message("Listing supported Ollama models"),
extra=log_metadata.model_dump(),
)
return OllamaSupportedModelsResponse(
models=SUPPORTED_OLLAMA_MODELS.values(),
)

View file

@ -0,0 +1,104 @@
import asyncio
import json
from fastapi import BackgroundTasks, HTTPException
from api.models import LogMetadata
from api.routers.presentation.handlers.list_supported_ollama_models import (
SUPPORTED_OLLAMA_MODELS,
)
from api.routers.presentation.models import OllamaModelStatusResponse
from api.services.instances import REDIS_SERVICE
from api.services.logging import LoggingService
import ollama
class PullOllamaModelHandler:
def __init__(self, name: str):
self.name = name
async def get(
self,
logging_service: LoggingService,
log_metadata: LogMetadata,
background_tasks: BackgroundTasks,
):
logging_service.logger.info(
logging_service.message(self.name),
extra=log_metadata.model_dump(),
)
if self.name not in SUPPORTED_OLLAMA_MODELS:
raise HTTPException(
status_code=400,
detail=f"Model {self.name} is not supported",
)
pulled_models = ollama.list().models
filtered_models = list(
filter(lambda model: model.model == self.name, pulled_models)
)
# If the model is already pulled, return the model
if filtered_models:
return OllamaModelStatusResponse(
name=self.name,
size=filtered_models[0].size,
status="pulled",
downloaded=filtered_models[0].size,
done=True,
)
saved_model_status = REDIS_SERVICE.get(f"ollama_models/{self.name}")
# If the model is being pulled, return the model
if saved_model_status:
return json.loads(saved_model_status)
# If the model is not being pulled, pull the model
background_tasks.add_task(self.pull_model_in_background)
return OllamaModelStatusResponse(
name=self.name,
status="pulling",
done=False,
)
async def pull_model_in_background(self):
await asyncio.to_thread(self.pull_model)
def pull_model(self):
saved_model_status = OllamaModelStatusResponse(
name=self.name,
status="pulling",
done=False,
)
log_event_count = 0
for event in ollama.pull(self.name, stream=True):
log_event_count += 1
if log_event_count != 1 and log_event_count % 20 != 0:
continue
if event.completed:
saved_model_status.downloaded = event.completed
if not saved_model_status.size and event.total:
saved_model_status.size = event.total
if event.status:
saved_model_status.status = event.status
REDIS_SERVICE.set(
f"ollama_models/{self.name}",
json.dumps(saved_model_status.model_dump(mode="json")),
)
saved_model_status.done = True
saved_model_status.status = "pulled"
saved_model_status.downloaded = saved_model_status.size
REDIS_SERVICE.set(
f"ollama_models/{self.name}",
json.dumps(saved_model_status.model_dump(mode="json")),
)
return saved_model_status

View file

@ -6,7 +6,7 @@ from api.routers.presentation.models import (
)
from api.services.logging import LoggingService
from image_processor.icons_finder import get_icons
from api.services.instances import temp_file_service
from api.services.instances import TEMP_FILE_SERVICE
from image_processor.icons_vectorstore_utils import get_icons_vectorstore
@ -16,7 +16,7 @@ class SearchIconHandler:
self.data = data
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
self.temp_dir = TEMP_FILE_SERVICE.create_temp_dir(self.session)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):

View file

@ -11,14 +11,9 @@ from api.routers.presentation.models import (
)
from api.services.logging import LoggingService
from api.sql_models import PresentationSqlModel, SlideSqlModel
from api.utils import (
download_files,
get_presentation_dir,
get_presentation_images_dir,
replace_file_name,
)
from api.utils.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
from api.services.instances import TEMP_FILE_SERVICE
class UpdateSlideModelsHandler:
@ -27,12 +22,12 @@ class UpdateSlideModelsHandler:
self.data = data
self.presentation_id = data.presentation_id
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir()
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)
TEMP_FILE_SERVICE.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(
@ -43,24 +38,21 @@ class UpdateSlideModelsHandler:
presentation_id = self.data.presentation_id
new_slides = self.data.slides
images_dir = get_presentation_images_dir(self.presentation_id)
# Handle images
images_local_paths = []
images_download_links = []
for new_slide in new_slides:
new_images = new_slide.images or []
for i, image in enumerate(new_images):
if image.startswith("http"):
parsed_url = unquote(urlparse(image).path)
image_name = replace_file_name(
os.path.basename(parsed_url), str(uuid.uuid4())
)
image_path = os.path.join(images_dir, image_name)
image_path = f"{self.presentation_dir}/images/{image_name}"
images_local_paths.append(image_path)
images_download_links.append(image)
getattr(new_slide, "images")[i] = image_path
new_slide.images[i] = image_path
if images_download_links:
await download_files(images_download_links, images_local_paths)

View file

@ -6,7 +6,7 @@ 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
from api.services.instances import TEMP_FILE_SERVICE
class UploadFilesHandler:
@ -20,7 +20,7 @@ class UploadFilesHandler:
self.images = images
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
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):
@ -46,7 +46,7 @@ class UploadFilesHandler:
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(
temp_path = TEMP_FILE_SERVICE.create_temp_file_path(
doc.filename, self.temp_dir
)
with open(temp_path, "wb") as f:

View file

@ -5,10 +5,10 @@ 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.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
from api.utils.utils import get_presentation_dir
class UploadPresentationThumbnailHandler:
@ -18,12 +18,12 @@ class UploadPresentationThumbnailHandler:
self.thumbnail = thumbnail
self.session = str(uuid.uuid4())
self.temp_dir = temp_file_service.create_temp_dir(self.session)
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)
TEMP_FILE_SERVICE.cleanup_temp_dir(self.temp_dir)
async def post(self, logging_service: LoggingService, log_metadata: LogMetadata):
logging_service.logger.info(

View file

@ -0,0 +1,58 @@
import asyncio
from typing import List
from api.models import SSEStatusResponse
from api.utils.utils import get_presentation_images_dir
from image_processor.icons_finder import get_icon
from image_processor.icons_vectorstore_utils import get_icons_vectorstore
from image_processor.images_finder import generate_image
from ppt_generator.models.slide_model import SlideModel
from ppt_generator.slide_model_utils import SlideModelUtils
class FetchAssetsOnPresentationGenerationMixin:
async def fetch_slide_assets(self, slide_models: List[SlideModel]):
image_prompts = []
icon_queries = []
for each_slide_model in slide_models:
slide_model_utils = SlideModelUtils(self.theme, each_slide_model)
image_prompts.extend(slide_model_utils.get_image_prompts())
icon_queries.extend(slide_model_utils.get_icon_queries())
if icon_queries:
icon_vector_store = get_icons_vectorstore()
images_directory = get_presentation_images_dir(self.presentation_id)
coroutines = [
generate_image(
each,
images_directory,
)
for each in image_prompts
] + [get_icon(icon_vector_store, each) for each in icon_queries]
assets_future = asyncio.gather(*coroutines)
while not assets_future.done():
status = SSEStatusResponse(status="Fetching slide assets").to_string()
yield status
await asyncio.sleep(5)
assets = await assets_future
image_prompts_len = len(image_prompts)
images = assets[:image_prompts_len]
icons = assets[image_prompts_len:]
for each_slide_model in slide_models:
each_slide_model.images = images[: each_slide_model.images_count]
images = images[each_slide_model.images_count :]
each_slide_model.icons = icons[: each_slide_model.icons_count]
icons = icons[each_slide_model.icons_count :]
yield SSEStatusResponse(status="Slide assets fetched").to_string()

View file

@ -1,7 +1,7 @@
import os
from urllib.parse import unquote, urlparse
import uuid
from api.utils import download_files, replace_file_name
from api.utils.utils import download_files, replace_file_name
from ppt_generator.models.pptx_models import PptxPictureBoxModel
@ -16,18 +16,29 @@ class FetchPresentationAssetsMixin:
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)
if image_path.startswith("http://localhost:3000/static"):
image_path = image_path.replace(
"http://localhost:3000/static", ""
)
image_path = "/app" + image_path
elif image_path.startswith("http://localhost/static"):
image_path = image_path.replace(
"http://localhost/static", ""
)
image_path = "/app" + image_path
else:
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)
elif image_path.startswith("file://"):
image_path = image_path.replace("file:///", "")
# Check if it's a Windows path (has colon at index 1)
if not (len(image_path) > 1 and image_path[1] == ':'):
image_path = '/' + image_path
if not (len(image_path) > 1 and image_path[1] == ":"):
image_path = "/" + image_path
each_shape.picture.path = image_path
each_shape.picture.is_network = False

View file

@ -1,6 +1,11 @@
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime
from enum import Enum
from typing import List, Literal, Optional
from fastapi import UploadFile
from pydantic import BaseModel, Field
from api.models import OllamaModelMetadata
from ppt_config_generator.models import SlideMarkdownModel
from ppt_generator.models.pptx_models import PptxPresentationModel
from ppt_generator.models.query_and_prompt_models import (
IconCategoryEnum,
@ -8,6 +13,17 @@ from ppt_generator.models.query_and_prompt_models import (
)
from ppt_generator.models.slide_model import SlideModel
from api.sql_models import PresentationSqlModel, SlideSqlModel
from ollama._types import ModelDetails
class ThemeEnum(Enum):
DARK = "dark"
LIGHT = "light"
ROYAL_BLUE = "royal_blue"
CREAM = "cream"
LIGHT_RED = "light_red"
DARK_PINK = "dark_pink"
FAINT_YELLOW = "faint_yellow"
class DocumentsAndImagesPath(BaseModel):
@ -33,7 +49,7 @@ class GeneratePresentationRequirementsRequest(BaseModel):
images: Optional[List[str]] = None
class GenerateTitleRequest(BaseModel):
class GenerateOutlinesRequest(BaseModel):
presentation_id: str
@ -41,8 +57,8 @@ class PresentationGenerateRequest(BaseModel):
presentation_id: str
theme: Optional[dict] = None
images: Optional[List[str]] = None
watermark: bool = True
titles: List[str]
outlines: List[SlideMarkdownModel]
title: Optional[str] = None
class GenerateImageRequest(BaseModel):
@ -133,6 +149,31 @@ class PresentationAndPaths(BaseModel):
paths: List[str]
class PresentationPathAndEditPath(PresentationAndPath):
edit_path: str
class UpdatePresentationTitlesRequest(BaseModel):
presentation_id: str
titles: List[str]
class GeneratePresentationRequest(BaseModel):
prompt: str
n_slides: int = Field(default=8, ge=5, le=15)
language: str = Field(default="English")
theme: ThemeEnum = Field(default=ThemeEnum.LIGHT)
documents: Optional[List[UploadFile]] = None
export_as: Literal["pptx", "pdf"] = Field(default="pptx")
class OllamaModelStatusResponse(BaseModel):
name: str
size: Optional[int] = None
downloaded: Optional[int] = None
status: str
done: bool
class OllamaSupportedModelsResponse(BaseModel):
models: List[OllamaModelMetadata]

View file

@ -1,6 +1,6 @@
from typing import Annotated, List, Optional
import uuid
from fastapi import APIRouter, Body, File, UploadFile, Depends
from fastapi import APIRouter, BackgroundTasks, Body, File, Form, UploadFile
from api.models import SessionModel
from api.request_utils import RequestUtils
@ -17,6 +17,9 @@ 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 import (
GeneratePresentationHandler,
)
from api.routers.presentation.handlers.generate_presentation_requirements import (
GeneratePresentationRequirementsHandler,
)
@ -26,11 +29,18 @@ from api.routers.presentation.handlers.generate_research_report import (
from api.routers.presentation.handlers.generate_stream import (
PresentationGenerateStreamHandler,
)
from api.routers.presentation.handlers.generate_titles import (
PresentationTitlesGenerateHandler,
from api.routers.presentation.handlers.generate_outlines import (
PresentationOutlinesGenerateHandler,
)
from api.routers.presentation.handlers.get_presentation import GetPresentationHandler
from api.routers.presentation.handlers.get_presentations import GetPresentationsHandler
from api.routers.presentation.handlers.list_ollama_pulled_models import (
ListPulledOllamaModelsHandler,
)
from api.routers.presentation.handlers.list_supported_ollama_models import (
ListSupportedOllamaModelsHandler,
)
from api.routers.presentation.handlers.pull_ollama_model import PullOllamaModelHandler
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 (
@ -53,31 +63,36 @@ from api.routers.presentation.models import (
EditPresentationSlideRequest,
ExportAsRequest,
GenerateImageRequest,
GeneratePresentationRequest,
GeneratePresentationRequirementsRequest,
GenerateResearchReportRequest,
OllamaModelStatusResponse,
OllamaSupportedModelsResponse,
PresentationAndPath,
PresentationAndPaths,
PresentationAndSlides,
GenerateTitleRequest,
GenerateOutlinesRequest,
PresentationAndUrls,
PresentationGenerateRequest,
PresentationPathAndEditPath,
SearchIconRequest,
SearchImageRequest,
UpdatePresentationThemeRequest,
PresentationUpdateRequest,
)
from api.sql_models import PresentationSqlModel
from api.utils import handle_errors
from api.utils.utils import handle_errors
from ppt_generator.models.slide_model import SlideModel
presentation_router = APIRouter(prefix="/ppt")
route_prefix = "/api/v1/ppt"
presentation_router = APIRouter(prefix=route_prefix)
@presentation_router.get(
"/user_presentations", response_model=List[PresentationSqlModel]
)
async def get_user_presentations():
request_utils = RequestUtils("/ppt/user_presentations")
request_utils = RequestUtils(f"{route_prefix}/user_presentations")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
GetPresentationsHandler().get, logging_service, log_metadata
@ -86,7 +101,7 @@ async def get_user_presentations():
@presentation_router.get("/presentation", response_model=PresentationAndSlides)
async def get_presentation_from_id(presentation_id: str):
request_utils = RequestUtils("/ppt/presentation")
request_utils = RequestUtils(f"{route_prefix}/presentation")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
@ -100,7 +115,7 @@ async def upload_files(
documents: Annotated[Optional[List[UploadFile]], File()] = None,
images: Annotated[Optional[List[UploadFile]], File()] = None,
):
request_utils = RequestUtils("/ppt/files/upload")
request_utils = RequestUtils(f"{route_prefix}/files/upload")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
UploadFilesHandler(documents, images).post,
@ -113,7 +128,7 @@ async def upload_files(
async def generate_research_report(
data: GenerateResearchReportRequest,
):
request_utils = RequestUtils("/ppt/report/generate")
request_utils = RequestUtils(f"{route_prefix}/report/generate")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
GenerateResearchReportHandler(data).post, logging_service, log_metadata
@ -122,7 +137,7 @@ async def generate_research_report(
@presentation_router.post("/files/decompose", response_model=DecomposeDocumentsResponse)
async def decompose_documents(data: DecomposeDocumentsRequest):
request_utils = RequestUtils("/ppt/files/decompose")
request_utils = RequestUtils(f"{route_prefix}/files/decompose")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
DecomposeDocumentsHandler(data).post, logging_service, log_metadata
@ -134,7 +149,7 @@ async def update_document(
path: Annotated[str, Body()],
file: Annotated[UploadFile, File()],
):
request_utils = RequestUtils("/ppt/document/update")
request_utils = RequestUtils(f"{route_prefix}/document/update")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
UpdateParsedDocumentHandler(path, file).post,
@ -147,7 +162,7 @@ async def update_document(
async def create_presentation(
data: GeneratePresentationRequirementsRequest,
):
request_utils = RequestUtils("/ppt/create")
request_utils = RequestUtils(f"{route_prefix}/create")
presentation_id = str(uuid.uuid4())
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
@ -159,14 +174,14 @@ async def create_presentation(
)
@presentation_router.post("/titles/generate", response_model=PresentationSqlModel)
async def generate_titles(data: GenerateTitleRequest):
request_utils = RequestUtils("/ppt/titles/generate")
@presentation_router.post("/outlines/generate", response_model=PresentationSqlModel)
async def generate_outlines(data: GenerateOutlinesRequest):
request_utils = RequestUtils(f"{route_prefix}/outlines/generate")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
return await handle_errors(
PresentationTitlesGenerateHandler(data).post,
PresentationOutlinesGenerateHandler(data).post,
logging_service,
log_metadata,
)
@ -176,7 +191,7 @@ async def generate_titles(data: GenerateTitleRequest):
async def submit_presentation_generation_data(
data: PresentationGenerateRequest,
):
request_utils = RequestUtils("/ppt/generate/data")
request_utils = RequestUtils(f"{route_prefix}/generate/data")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
@ -187,7 +202,7 @@ async def submit_presentation_generation_data(
@presentation_router.get("/generate/stream")
async def presentation_generation_stream(presentation_id: str, session: str):
request_utils = RequestUtils("/ppt/generate/stream")
request_utils = RequestUtils(f"{route_prefix}/generate/stream")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
@ -203,7 +218,7 @@ async def update_presentation(
presentation_id: Annotated[str, Body()],
thumbnail: Annotated[UploadFile, File()],
):
request_utils = RequestUtils("/ppt/presentation/thumbnail")
request_utils = RequestUtils(f"{route_prefix}/presentation/thumbnail")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
@ -218,7 +233,7 @@ async def update_presentation(
async def update_presentation(
data: UpdatePresentationThemeRequest,
):
request_utils = RequestUtils("/ppt/presentation/theme")
request_utils = RequestUtils(f"{route_prefix}/presentation/theme")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
@ -233,7 +248,7 @@ async def update_presentation(
async def update_presentation(
data: EditPresentationSlideRequest,
):
request_utils = RequestUtils("/ppt/edit")
request_utils = RequestUtils(f"{route_prefix}/edit")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id
)
@ -244,7 +259,7 @@ async def update_presentation(
@presentation_router.post("/slides/update", response_model=PresentationAndSlides)
async def update_slide_models(data: PresentationUpdateRequest):
request_utils = RequestUtils("/ppt/slides/update")
request_utils = RequestUtils(f"{route_prefix}/slides/update")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
@ -255,7 +270,7 @@ async def update_slide_models(data: PresentationUpdateRequest):
@presentation_router.post("/image/generate", response_model=PresentationAndPaths)
async def generate_image(data: GenerateImageRequest):
request_utils = RequestUtils("/ppt/image/generate")
request_utils = RequestUtils(f"{route_prefix}/image/generate")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
@ -266,7 +281,7 @@ async def generate_image(data: GenerateImageRequest):
@presentation_router.post("/image/search", response_model=PresentationAndUrls)
async def search_image(data: SearchImageRequest):
request_utils = RequestUtils("/ppt/image/search")
request_utils = RequestUtils(f"{route_prefix}/image/search")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
@ -277,7 +292,7 @@ async def search_image(data: SearchImageRequest):
@presentation_router.post("/icon/search", response_model=PresentationAndPaths)
async def search_icon(data: SearchIconRequest):
request_utils = RequestUtils("/ppt/icon/search")
request_utils = RequestUtils(f"{route_prefix}/icon/search")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
@ -290,7 +305,7 @@ async def search_icon(data: SearchIconRequest):
"/presentation/export_as_pptx", response_model=PresentationAndPath
)
async def export_as_pptx(data: ExportAsRequest):
request_utils = RequestUtils("/ppt/presentation/export_as_pptx")
request_utils = RequestUtils(f"{route_prefix}/presentation/export_as_pptx")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=data.presentation_id,
)
@ -301,7 +316,7 @@ async def export_as_pptx(data: ExportAsRequest):
@presentation_router.delete("/delete", status_code=204)
async def delete_presentation(presentation_id: str):
request_utils = RequestUtils("/ppt/delete")
request_utils = RequestUtils(f"{route_prefix}/delete")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
@ -312,10 +327,62 @@ async def delete_presentation(presentation_id: str):
@presentation_router.delete("/slide/delete", status_code=204)
async def delete_slide(slide_id: str, presentation_id: str):
request_utils = RequestUtils("/ppt/slide/delete")
request_utils = RequestUtils(f"{route_prefix}/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
)
@presentation_router.post(
"/generate/presentation", response_model=PresentationPathAndEditPath
)
async def generate_presentation(data: Annotated[GeneratePresentationRequest, Form()]):
presentation_id = str(uuid.uuid4())
request_utils = RequestUtils(f"{route_prefix}/generate/presentation")
logging_service, log_metadata = await request_utils.initialize_logger(
presentation_id=presentation_id,
)
return await handle_errors(
GeneratePresentationHandler(presentation_id, data).post,
logging_service,
log_metadata,
)
# Ollama Support
@presentation_router.get(
"/ollama/list-supported-models", response_model=OllamaSupportedModelsResponse
)
async def list_supported_ollama_models():
request_utils = RequestUtils(f"{route_prefix}/ollama/list-supported-models")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
ListSupportedOllamaModelsHandler().get, logging_service, log_metadata
)
@presentation_router.get(
"/ollama/list-pulled-models", response_model=List[OllamaModelStatusResponse]
)
async def list_pulled_ollama_models():
request_utils = RequestUtils(f"{route_prefix}/ollama/list-pulled-models")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
ListPulledOllamaModelsHandler().get, logging_service, log_metadata
)
@presentation_router.get("/ollama/pull-model", response_model=OllamaModelStatusResponse)
async def pull_ollama_model(name: str, background_tasks: BackgroundTasks):
request_utils = RequestUtils(f"{route_prefix}/ollama/pull-model")
logging_service, log_metadata = await request_utils.initialize_logger()
return await handle_errors(
PullOllamaModelHandler(name).get,
logging_service,
log_metadata,
background_tasks=background_tasks,
)

View file

@ -1,4 +1,6 @@
from api.services.redis import RedisService
from api.services.temp_file import TempFileService
temp_file_service = TempFileService()
TEMP_FILE_SERVICE = TempFileService()
REDIS_SERVICE = RedisService()

View file

@ -1,6 +1,4 @@
import os
from typing import Any
import logging
from logging import Logger
@ -9,10 +7,6 @@ class LoggingService:
def __init__(self, stream_name: str):
self._logger = Logger(stream_name)
log_file_path = os.path.join(os.getenv("APP_DATA_DIRECTORY"), "logs", "api.log")
os.makedirs(os.path.dirname(log_file_path), exist_ok=True)
self._logger.addHandler(logging.FileHandler(log_file_path))
@property
def logger(self) -> Logger:
return self._logger

View file

@ -0,0 +1,109 @@
import os
from typing import Any, Optional
import redis
from redis.exceptions import RedisError
class RedisService:
def __init__(self):
self.redis_host = os.getenv("REDIS_HOST", "localhost")
self.redis_port = int(os.getenv("REDIS_PORT", "6379"))
self.redis_db = int(os.getenv("REDIS_DB", "0"))
self.redis_password = os.getenv("REDIS_PASSWORD")
self.client = self._create_client()
def _create_client(self) -> redis.Redis:
return redis.Redis(
host=self.redis_host,
port=self.redis_port,
db=self.redis_db,
password=self.redis_password,
decode_responses=True,
)
def set(self, key: str, value: Any, expire: Optional[int] = None) -> bool:
try:
return self.client.set(key, value, ex=expire)
except RedisError:
return False
def get(self, key: str) -> Optional[str]:
try:
return self.client.get(key)
except RedisError:
return None
def delete(self, key: str) -> bool:
try:
return bool(self.client.delete(key))
except RedisError:
return False
def exists(self, key: str) -> bool:
try:
return bool(self.client.exists(key))
except RedisError:
return False
def set_hash(self, name: str, mapping: dict) -> bool:
try:
return self.client.hmset(name, mapping)
except RedisError:
return False
def get_hash(self, name: str) -> Optional[dict]:
try:
return self.client.hgetall(name)
except RedisError:
return None
def delete_hash(self, name: str, *fields: str) -> int:
try:
return self.client.hdel(name, *fields)
except RedisError:
return 0
def set_list(self, name: str, values: list) -> bool:
try:
self.client.delete(name)
if values:
self.client.rpush(name, *values)
return True
except RedisError:
return False
def get_list(self, name: str, start: int = 0, end: int = -1) -> Optional[list]:
try:
return self.client.lrange(name, start, end)
except RedisError:
return None
def add_to_set(self, name: str, *values: str) -> int:
try:
return self.client.sadd(name, *values)
except RedisError:
return 0
def get_set(self, name: str) -> Optional[set]:
try:
return self.client.smembers(name)
except RedisError:
return None
def remove_from_set(self, name: str, *values: str) -> int:
try:
return self.client.srem(name, *values)
except RedisError:
return 0
def clear(self) -> bool:
try:
return self.client.flushdb()
except RedisError:
return False
def close(self):
try:
self.client.close()
except RedisError:
pass

View file

@ -3,8 +3,6 @@ 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())
@ -18,7 +16,13 @@ class PresentationSqlModel(SQLModel, table=True):
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(
structure: Optional[dict] = Field(
sa_column=Column(JSON, nullable=True), default=None
)
notes: Optional[List[str]] = Field(
sa_column=Column(JSON, nullable=True), default=None
)
outlines: Optional[List[dict]] = Field(
sa_column=Column(JSON, nullable=True), default=None
)
language: Optional[str] = None

View file

@ -0,0 +1,253 @@
from api.models import OllamaModelMetadata
SUPPORTED_LLAMA_MODELS = {
"llama3:8b": OllamaModelMetadata(
label="Llama 3:8b",
value="llama3:8b",
description="❌ Graphs not supported.",
size="4.7GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/meta.png",
),
"llama3:70b": OllamaModelMetadata(
label="Llama 3:70b",
value="llama3:70b",
description="✅ Graphs supported.",
size="40GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/meta.png",
),
"llama3.1:8b": OllamaModelMetadata(
label="Llama 3.1:8b",
value="llama3.1:8b",
description="❌ Graphs not supported.",
size="4.9GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/meta.png",
),
"llama3.1:70b": OllamaModelMetadata(
label="Llama 3.1:70b",
value="llama3.1:70b",
description="✅ Graphs supported.",
size="43GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/meta.png",
),
"llama3.1:405b": OllamaModelMetadata(
label="Llama 3.1:405b",
value="llama3.1:405b",
description="✅ Graphs supported.",
size="243GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/meta.png",
),
"llama3.2:1b": OllamaModelMetadata(
label="Llama 3.2:1b",
value="llama3.2:1b",
description="❌ Graphs not supported.",
size="1.3GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/meta.png",
),
"llama3.2:3b": OllamaModelMetadata(
label="Llama 3.2:3b",
value="llama3.2:3b",
description="❌ Graphs not supported.",
size="2GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/meta.png",
),
"llama3.3:70b": OllamaModelMetadata(
label="Llama 3.3:70b",
value="llama3.3:70b",
description="✅ Graphs supported.",
size="43GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/meta.png",
),
"llama4:16x17b": OllamaModelMetadata(
label="Llama 4:16x17b",
value="llama4:16x17b",
description="✅ Graphs supported.",
size="67GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/meta.png",
),
"llama4:128x17b": OllamaModelMetadata(
label="Llama 4:128x17b",
value="llama4:128x17b",
description="✅ Graphs supported.",
size="245GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/meta.png",
),
}
SUPPORTED_GEMMA_MODELS = {
"gemma3:1b": OllamaModelMetadata(
label="Gemma 3:1b",
value="gemma3:1b",
description="❌ Graphs not supported.",
size="815MB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/gemma.png",
),
"gemma3:4b": OllamaModelMetadata(
label="Gemma 3:4b",
value="gemma3:4b",
description="❌ Graphs not supported.",
size="3.3GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/gemma.png",
),
"gemma3:12b": OllamaModelMetadata(
label="Gemma 3:12b",
value="gemma3:12b",
description="❌ Graphs not supported.",
size="8.1GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/gemma.png",
),
"gemma3:27b": OllamaModelMetadata(
label="Gemma 3:27b",
value="gemma3:27b",
description="✅ Graphs supported.",
size="17GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/gemma.png",
),
}
SUPPORTED_DEEPSEEK_MODELS = {
"deepseek-r1:1.5b": OllamaModelMetadata(
label="DeepSeek R1:1.5b",
value="deepseek-r1:1.5b",
description="❌ Graphs not supported.",
size="1.1GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/deepseek.png",
),
"deepseek-r1:7b": OllamaModelMetadata(
label="DeepSeek R1:7b",
value="deepseek-r1:7b",
description="❌ Graphs not supported.",
size="4.7GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/deepseek.png",
),
"deepseek-r1:8b": OllamaModelMetadata(
label="DeepSeek R1:8b",
value="deepseek-r1:8b",
description="❌ Graphs not supported.",
size="5.2GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/deepseek.png",
),
"deepseek-r1:14b": OllamaModelMetadata(
label="DeepSeek R1:14b",
value="deepseek-r1:14b",
description="❌ Graphs not supported.",
size="9GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/deepseek.png",
),
"deepseek-r1:32b": OllamaModelMetadata(
label="DeepSeek R1:32b",
value="deepseek-r1:32b",
description="✅ Graphs supported.",
size="20GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/deepseek.png",
),
"deepseek-r1:70b": OllamaModelMetadata(
label="DeepSeek R1:70b",
value="deepseek-r1:70b",
description="✅ Graphs supported.",
size="43GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/deepseek.png",
),
"deepseek-r1:671b": OllamaModelMetadata(
label="DeepSeek R1:671b",
value="deepseek-r1:671b",
description="✅ Graphs supported.",
size="404GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/deepseek.png",
),
}
SUPPORTED_QWEN_MODELS = {
"qwen3:0.6b": OllamaModelMetadata(
label="Qwen 3:0.6b",
value="qwen3:0.6b",
description="❌ Graphs not supported.",
size="523MB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/qwen.png",
),
"qwen3:1.7b": OllamaModelMetadata(
label="Qwen 3:1.7b",
value="qwen3:1.7b",
description="❌ Graphs not supported.",
size="1.4GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/qwen.png",
),
"qwen3:4b": OllamaModelMetadata(
label="Qwen 3:4b",
value="qwen3:4b",
description="❌ Graphs not supported.",
size="2.6GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/qwen.png",
),
"qwen3:8b": OllamaModelMetadata(
label="Qwen 3:8b",
value="qwen3:8b",
description="❌ Graphs not supported.",
size="5.2GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/qwen.png",
),
"qwen3:14b": OllamaModelMetadata(
label="Qwen 3:14b",
value="qwen3:14b",
description="❌ Graphs not supported.",
size="9.3GB",
supports_graph=False,
icon="/static/servers/fastapi/assets/icons/qwen.png",
),
"qwen3:30b": OllamaModelMetadata(
label="Qwen 3:30b",
value="qwen3:30b",
description="✅ Graphs supported.",
size="19GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/qwen.png",
),
"qwen3:32b": OllamaModelMetadata(
label="Qwen 3:32b",
value="qwen3:32b",
description="✅ Graphs supported.",
size="20GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/qwen.png",
),
"qwen3:235b": OllamaModelMetadata(
label="Qwen 3:235b",
value="qwen3:235b",
description="✅ Graphs supported.",
size="142GB",
supports_graph=True,
icon="/static/servers/fastapi/assets/icons/qwen.png",
),
}
SUPPORTED_OLLAMA_MODELS = {
**SUPPORTED_LLAMA_MODELS,
**SUPPORTED_GEMMA_MODELS,
**SUPPORTED_DEEPSEEK_MODELS,
**SUPPORTED_QWEN_MODELS,
}

View file

@ -9,11 +9,48 @@ from typing import List, Optional
import aiohttp
from fastapi import HTTPException, UploadFile
from fastapi.responses import StreamingResponse
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_ollama import ChatOllama
from langchain_openai import ChatOpenAI
from api.models import LogMetadata, UserConfig
from api.services.logging import LoggingService
def is_ollama_selected() -> bool:
return os.getenv("LLM") == "ollama"
def get_large_model():
selected_llm = os.getenv("LLM")
if selected_llm == "openai":
return ChatOpenAI(model="gpt-4.1")
elif selected_llm == "google":
return ChatGoogleGenerativeAI(model="gemini-2.0-flash")
else:
return ChatOllama(model=os.getenv("OLLAMA_MODEL"), temperature=0.8)
def get_small_model():
selected_llm = os.getenv("LLM")
if selected_llm == "openai":
return ChatOpenAI(model="gpt-4.1-mini")
elif selected_llm == "google":
return ChatGoogleGenerativeAI(model="gemini-2.0-flash")
else:
return ChatOllama(model=os.getenv("OLLAMA_MODEL"), temperature=0.8)
def get_nano_model():
selected_llm = os.getenv("LLM")
if selected_llm == "openai":
return ChatOpenAI(model="gpt-4.1-nano")
elif selected_llm == "google":
return ChatGoogleGenerativeAI(model="gemini-2.0-flash")
else:
return ChatOllama(model=os.getenv("OLLAMA_MODEL"), temperature=0.8)
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)
@ -44,6 +81,8 @@ def get_user_config():
LLM=existing_config.LLM or os.getenv("LLM"),
OPENAI_API_KEY=existing_config.OPENAI_API_KEY or os.getenv("OPENAI_API_KEY"),
GOOGLE_API_KEY=existing_config.GOOGLE_API_KEY or os.getenv("GOOGLE_API_KEY"),
OLLAMA_MODEL=existing_config.OLLAMA_MODEL or os.getenv("OLLAMA_MODEL"),
PEXELS_API_KEY=existing_config.PEXELS_API_KEY or os.getenv("PEXELS_API_KEY"),
)
@ -55,11 +94,17 @@ def update_env_with_user_config():
os.environ["OPENAI_API_KEY"] = user_config.OPENAI_API_KEY
if user_config.GOOGLE_API_KEY:
os.environ["GOOGLE_API_KEY"] = user_config.GOOGLE_API_KEY
if user_config.OLLAMA_MODEL:
os.environ["OLLAMA_MODEL"] = user_config.OLLAMA_MODEL
if user_config.PEXELS_API_KEY:
os.environ["PEXELS_API_KEY"] = user_config.PEXELS_API_KEY
def get_resource(relative_path):
base_path = getattr(
sys, "_MEIPASS", os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys,
"_MEIPASS",
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
)
return os.path.join(base_path, relative_path)
@ -73,11 +118,11 @@ def replace_file_name(old_name: str, new_name: str) -> str:
def save_uploaded_files(
temp_file_service, files: List[UploadFile], file_paths: List[str], temp_dir: str
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(
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)
@ -85,6 +130,7 @@ def save_uploaded_files(
async def download_file(url: str, save_path: str, headers: Optional[dict] = None):
os.makedirs(os.path.dirname(save_path), exist_ok=True)
try:
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
@ -101,6 +147,7 @@ async def download_file(url: str, save_path: str, headers: Optional[dict] = None
print(f"Failed to download file. HTTP status: {response.status}")
return False
except Exception as e:
print(e)
print(f"Error while downloading file from {url} to {save_path}")
return False
@ -117,12 +164,12 @@ async def download_files(urls: List[str], save_paths: List[str]):
async def handle_errors(
func, logging_service: LoggingService, log_metadata: LogMetadata
func, logging_service: LoggingService, log_metadata: LogMetadata, **kwargs
):
try:
logging_service.logger.info(f"START", extra=log_metadata.model_dump())
response = await func(
logging_service=logging_service, log_metadata=log_metadata
logging_service=logging_service, log_metadata=log_metadata, **kwargs
)
is_stream = isinstance(response, StreamingResponse)
logging_service.logger.info(

View file

@ -0,0 +1,46 @@
from typing import List, Optional
from pydantic import Field
from ppt_config_generator.models import (
PresentationMarkdownModel,
PresentationStructureModel,
SlideMarkdownModel,
SlideStructureModel,
)
class SlideMarkdownModelWithValidation(SlideMarkdownModel):
title: str = Field(
description="Title of the slide in about 3 to 5 words",
min_length=10,
max_length=50,
)
def get_presentation_markdown_model_with_n_slides(n_slides: int):
class PresentationMarkdownModelWithNSlides(PresentationMarkdownModel):
title: str = Field(
description="Title of the presentation in about 3 to 8 words",
min_length=10,
max_length=50,
)
notes: Optional[List[str]] = Field(
description="Important notes for the presentation styling and formatting",
min_length=0,
max_length=10,
)
slides: List[SlideMarkdownModelWithValidation] = Field(
description="List of slides", min_items=n_slides, max_items=n_slides
)
return PresentationMarkdownModelWithNSlides
def get_presentation_structure_model_with_n_slides(n_slides: int):
class PresentationStructureModelWithNSlides(PresentationStructureModel):
slides: List[SlideStructureModel] = Field(
description="List of slide structure",
min_items=n_slides,
max_items=n_slides,
)
return PresentationStructureModelWithNSlides

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -101,44 +101,32 @@ class PieChartDataModel(BaseModel):
return [clip_text(category) for category in self.categories]
class TableDataModel(BaseModel):
categories: List[str]
series: List[BarSeriesModel]
# class TableDataModel(BaseModel):
# categories: List[str]
# series: List[BarSeriesModel]
def get_categories(self) -> List[str]:
return [clip_text(category) for category in self.categories]
# def get_categories(self) -> List[str]:
# return [clip_text(category) for category in self.categories]
class GraphTypeEnum(Enum):
pie = "pie"
bar = "bar"
scatter = "scatter"
bubble = "bubble"
line = "line"
table = "table"
class GraphModel(BaseModel):
id: Optional[str] = None
style: Optional[dict] = {}
name: str
type: GraphTypeEnum
presentation: Optional[str] = None
unit: Optional[str] = Field(
default="Unit of the data in the graph. Example: %, kg, million USD, tonnes, etc."
)
data: (
PieChartDataModel
| LineChartDataModel
| BubbleChartDataModel
| BarGraphDataModel
| TableDataModel
description="Unit of the data in the graph. Example: %, kg, million USD, tonnes, etc."
)
data: PieChartDataModel | LineChartDataModel | BarGraphDataModel
GRAPH_TYPE_MAPPING = {
GraphTypeEnum.pie: PieChartDataModel,
GraphTypeEnum.bar: BarGraphDataModel,
GraphTypeEnum.line: LineChartDataModel,
GraphTypeEnum.bubble: BubbleChartDataModel,
}

View file

@ -1,6 +1,6 @@
from typing import List, Optional
from api.utils import get_resource
from api.utils.utils import get_resource
from ppt_generator.models.query_and_prompt_models import (
IconCategoryEnum,
IconQueryCollectionWithData,
@ -13,7 +13,7 @@ async def get_icon(
input: IconQueryCollectionWithData,
) -> str:
try:
query = input.icon_query.queries[0]
query = input.icon_query
results = vector_store.similarity_search(query=query, k=1)
icon_name = results[0].page_content
return get_resource(f"assets/icons/bold/{icon_name}.png")

View file

@ -3,7 +3,7 @@ import os
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.documents import Document
from api.utils import get_resource
from api.utils.utils import get_resource
from langchain_community.embeddings.fastembed import FastEmbedEmbeddings
# Pyinstaller

View file

@ -9,21 +9,31 @@ from openai import OpenAI
from ppt_generator.models.query_and_prompt_models import (
ImagePromptWithThemeAndAspectRatio,
)
from api.utils import get_resource
from api.utils.utils import download_file, get_resource, is_ollama_selected
async def generate_image(
input: ImagePromptWithThemeAndAspectRatio,
output_directory: str,
) -> str:
image_prompt = f"{input.image_prompt}, {input.theme_prompt}"
is_ollama = is_ollama_selected()
image_prompt = (
input.image_prompt
if is_ollama
else f"{input.image_prompt}, {input.theme_prompt}"
)
print(f"Request - Generating Image for {image_prompt}")
try:
image_gen_func = (
generate_image_openai
if os.getenv("LLM") == "openai"
else generate_image_google
get_image_from_pexels
if is_ollama
else (
generate_image_openai
if os.getenv("LLM") == "openai"
else generate_image_google
)
)
image_path = await image_gen_func(image_prompt, output_directory)
if image_path and os.path.exists(image_path):
@ -72,3 +82,16 @@ async def generate_image_google(prompt: str, output_directory: str) -> str:
f.write(base64.b64decode(base64_image))
return image_path
async def get_image_from_pexels(prompt: str, output_directory: str) -> str:
async with aiohttp.ClientSession() as session:
response = await session.get(
f"https://api.pexels.com/v1/search?query={prompt}&per_page=1",
headers={"Authorization": f'{os.getenv("PEXELS_API_KEY")}'},
)
data = await response.json()
image_url = data["photos"][0]["src"]["large"]
image_path = os.path.join(output_directory, f"{str(uuid.uuid4())}.jpg")
await download_file(image_url, image_path)
return image_path

View file

@ -1,11 +1,11 @@
import asyncio
import os
from api.services.instances import temp_file_service
from api.services.instances import TEMP_FILE_SERVICE
import pdfplumber
def get_page_images_from_pdf(document_path: str, temp_dir: str):
images_temp_dir = temp_file_service.create_dir_in_dir(temp_dir)
images_temp_dir = TEMP_FILE_SERVICE.create_dir_in_dir(temp_dir)
with pdfplumber.open(document_path) as pdf:
for page in pdf.pages:

File diff suppressed because it is too large Load diff

View file

@ -1,3 +0,0 @@
[virtualenvs]
create = true
in-project = true

View file

@ -8,20 +8,20 @@ from langchain_core.prompts import ChatPromptTemplate
from langchain_core.messages import BaseMessage
from langchain_text_splitters import CharacterTextSplitter
from api.utils.utils import get_nano_model
sysmte_prompt = """
Generate a blog-style summary of the provided document in **more than 2000 words**, focusing on **prominently featuring any numerical data and statistics**. Maintain as much information as possible.
Generate a blog-style summary of the provided document in **more than 2000 words**.
Maintain as much information as possible.
### Output Format
- Provide the summary in a **blog format** with an **engaging introduction** and a **clear structure**.
- Ensure the **logical flow** of the document is preserved.
- Emphasize any **numbers, statistics, and data points**.
### Notes
- **Emphasize numerical data and statistics** in the summary.
- **Retain the main ideas and essential details** from the document.
- Use **engaging language** suitable for a blog audience to enhance readability.
- **Show line-breaks** clearly.
- If **slides structure is mentioned** in document, structure the summary in the same way.
"""
@ -35,14 +35,7 @@ prompt_template = ChatPromptTemplate.from_messages(
async def generate_document_summary(documents: List[Document]):
model = (
ChatOpenAI(model="gpt-4.1-nano", max_completion_tokens=8000)
if os.getenv("LLM") == "openai"
else ChatGoogleGenerativeAI(model="gemini-2.0-flash", max_output_tokens=8000)
)
# text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
# encoding_name="cl100k_base", chunk_size=200000, chunk_overlap=0
# )
model = get_nano_model()
text_splitter = CharacterTextSplitter(chunk_size=200000, chunk_overlap=0)
chain = prompt_template | model
@ -54,5 +47,5 @@ async def generate_document_summary(documents: List[Document]):
coroutines.append(coroutine)
completions: List[BaseMessage] = await asyncio.gather(*coroutines)
combined = "\n\n".join([completion.content for completion in completions])
combined = "\n\n\n\n".join([completion.content for completion in completions])
return combined

View file

@ -1,9 +1,40 @@
from typing import List
from typing import List, Optional
from pydantic import BaseModel, Field
class PresentationTitlesModel(BaseModel):
presentation_title: str = Field("Title of this presentation in about 3 to 8 words")
titles: List[str] = Field(
description="List of title of every slide in presentation in about 2 to 8 words"
class SlideStructureModel(BaseModel):
type: int = Field(description="Type of the slide", gte=1, lte=9)
class PresentationStructureModel(BaseModel):
slides: List[SlideStructureModel] = Field(description="List of slide structure")
class SlideMarkdownModel(BaseModel):
title: str = Field(
description="Title of the slide in about 3 to 5 words",
)
body: str = Field(
description="Content of the slide in markdown format",
)
class PresentationMarkdownModel(BaseModel):
title: str = Field(
description="Title of the presentation in about 3 to 8 words",
)
notes: Optional[List[str]] = Field(description="Notes for the presentation")
slides: List[SlideMarkdownModel] = Field(description="List of slides")
def to_string(self):
message = f"# Presentation Title: {self.title} \n\n"
for i, slide in enumerate(self.slides):
message += f"## Slide {i+1}:\n"
message += f" - Title: {slide.title} \n"
message += f" - Body: {slide.body} \n"
if self.notes:
message += f"# Notes: \n"
for note in self.notes:
message += f" - {note} \n"
return message

View file

@ -0,0 +1,9 @@
from langchain.schema import BaseOutputParser
class StripMarkdownOutputParser(BaseOutputParser):
def parse(self, text: str) -> str:
# Remove triple backticks and any optional language hint like ```markdown
import re
return re.sub(r"^```[\w]*\n?|```$", "", text.strip(), flags=re.MULTILINE)

View file

@ -0,0 +1,84 @@
from typing import Optional
from langchain_core.prompts import ChatPromptTemplate
from api.utils.utils import get_large_model
from api.utils.variable_length_models import (
get_presentation_markdown_model_with_n_slides,
)
from ppt_config_generator.models import PresentationMarkdownModel
from ppt_generator.fix_validation_errors import get_validated_response
user_prompt_text = {
"type": "text",
"text": """
**Input:**
- Prompt: {prompt}
- Output Language: {language}
- Number of Slides: {n_slides}
- Additional Information: {content}
""",
}
def get_prompt_template():
return ChatPromptTemplate.from_messages(
[
(
"system",
"""
Create a presentation based on the provided prompt, number of slides, output language, and additional informational details.
Format the output in the specified JSON schema with structured markdown content.
# Steps
1. Identify key points from the provided prompt, including the topic, number of slides, output language, and additional content directions.
2. Create a concise and descriptive title reflecting the main topic, adhering to the specified language.
3. Generate a clear title for each slide.
4. Develop comprehensive content using markdown structure:
* Use bullet points (- or *) for lists.
* Use **bold** for emphasis, *italic* for secondary emphasis, and `code` for technical terms.
5. Provide important points from prompt as notes.
# Notes
- Content must be generated for every slide.
- Images or Icons information provided in **Input** must be included in the **notes**.
- Notes should cleary define if it is for specific slide or for the presentation.
- Slide **body** should not contain slide **title**.
- Slide **title** should not contain "Slide 1", "Slide 2", etc.
- Slide **title** should not be in markdown format.
- There must be exact **Number of Slides** as specified.
""",
),
(
"user",
[user_prompt_text],
),
],
)
async def generate_ppt_content(
prompt: Optional[str],
n_slides: int,
language: Optional[str] = None,
content: Optional[str] = None,
) -> PresentationMarkdownModel:
model = get_large_model()
response_model = get_presentation_markdown_model_with_n_slides(n_slides)
chain = get_prompt_template() | model.with_structured_output(
response_model.model_json_schema()
)
return await get_validated_response(
chain,
{
"prompt": prompt,
"n_slides": n_slides,
"language": language or "English",
"content": content,
},
response_model,
PresentationMarkdownModel,
)

View file

@ -1,85 +0,0 @@
import os
from typing import Optional
from langchain_core.prompts import ChatPromptTemplate
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_openai import ChatOpenAI
from ppt_config_generator.models import PresentationTitlesModel
from ppt_generator.fix_validation_errors import get_validated_response
user_prompt_text = {
"type": "text",
"text": """
**Input:**
- Prompt: {prompt}
- Output Language: {language}
- Number of Slides: {n_slides}
- Content: {content}
""",
}
def get_prompt_template():
return ChatPromptTemplate.from_messages(
[
(
"system",
"""
Generate titles for the presentation based on the prompt and additional information.
# Steps
1. Analyze the prompt and additional information.
2. Visualize presentation with **Number of Slides**.
3. Use provided input or any information you have on this topic.
4. Check if slide titles are provided in **Input**.
5. Generate title for each slide if not provided in **Input**.
6. If slide titles are provided in **Input** then use them as it is.
7. In case if slides for chapter is provided then analyze all chapter content and then structurally generate titles considering all slide content. \
Keep the flow as per given chapter content. Ensure that titles are generated to cover all the content in the chapter.
# Notes
- Generate output in language mentioned in **Input**.
- Ensure the prompt and additional information remains the main focus of the presentation.
- **Additional Information** serves as supporting information, providing depth and details.
- Slide titles should maintain a logical and coherent flow throughout the presentation.
- Slide **Title** should not contain slide number like (Slide 1, Slide 2, etc)
- Slide **Title** can have 3 to 8 words.
- Slide **Title** must not use any other special characters except ":".
- Presentation **Title** should be around 3 to 8 words.
- Extract titles from the **Additional Information** or **Prompt** if provided.
- If presentation flow is mentioned in **Additional Information** then use it to generate titles.
- If Chapter Content is provided than strictly adhere to it and then generate titles in the same content flow as chapter content.
""",
),
(
"user",
[user_prompt_text],
),
],
)
async def generate_ppt_titles(
prompt: Optional[str],
n_slides: int,
content: Optional[str],
language: Optional[str] = None,
) -> PresentationTitlesModel:
model = (
ChatOpenAI(model="gpt-4.1-nano")
if os.getenv("LLM") == "openai"
else ChatGoogleGenerativeAI(model="gemini-2.0-flash")
).with_structured_output(PresentationTitlesModel.model_json_schema())
chain = get_prompt_template() | model
return await get_validated_response(
chain,
{
"prompt": prompt,
"n_slides": n_slides,
"language": language or "English",
"content": content,
},
PresentationTitlesModel,
)

View file

@ -0,0 +1,76 @@
from langchain_core.prompts import ChatPromptTemplate
from api.utils.utils import get_small_model
from api.utils.variable_length_models import (
get_presentation_structure_model_with_n_slides,
)
from ppt_config_generator.models import (
PresentationStructureModel,
PresentationMarkdownModel,
)
from ppt_generator.fix_validation_errors import get_validated_response
prompt = ChatPromptTemplate.from_messages(
[
(
"system",
"""
You're a professional presentation designer with years of experience in designing clear and engaging presentations.
# Slide Types
- **1**: contains title, description and image.
- **2**: contains title and list of items.
- **4**: contains title and list of items with images.
- **5**: contains title, description and a graph.
- **6**: contains title, description and list of items.
- **7**: contains title and list of items with icons.
- **8**: contains title, description and list of items with icons.
- **9**: contains title, list of items and a graph.
# Steps
1. Analyze provided Number of slides, Presentation title, Slides content and Slide types.
2. Select appropriate slide type for each slide.
3. Provide output in json format as per given schema.
# Notes
- Slide type should be selected based on provided content for slide and notes.
- Feel free to select slide type with images and icons.
- Introduction and Conclusion should have type **1**.
- Don't fall into patterns like always using type 2 and after type 1.
- Each presentation should have its own unique flow and rhythm.
- Do not select type **3** for any slide.
- Do not select type **5** or **9** if outline does not have table.
- Select type for {n_slides} slides.
**Go through notes and steps and make sure they are all followed. Rule breaks are strictly not allowed.**
""",
),
(
"human",
"""
{data}
""",
),
]
)
async def generate_presentation_structure(
presentation_outline: PresentationMarkdownModel,
) -> PresentationStructureModel:
model = get_small_model()
response_model = get_presentation_structure_model_with_n_slides(
len(presentation_outline.slides)
)
chain = prompt | model.with_structured_output(response_model.model_json_schema())
return await get_validated_response(
chain,
{
"n_slides": len(presentation_outline.slides),
"data": presentation_outline.to_string(),
},
response_model,
PresentationStructureModel,
)

View file

@ -1,10 +1,13 @@
import os
from typing import Optional
from fastapi import HTTPException
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from pydantic import BaseModel, ValidationError
from api.utils.utils import get_large_model
def get_prompt_template():
return ChatPromptTemplate(
@ -38,11 +41,7 @@ def get_prompt_template():
async def fix_validation_errors(response_model: BaseModel, response, errors):
model = (
ChatOpenAI(model="o3-mini", reasoning_effort="high")
if os.getenv("LLM") == "openai"
else ChatGoogleGenerativeAI(model="gemini-2.5-flash-preview-04-17")
)
model = get_large_model()
chain = get_prompt_template() | model.with_structured_output(
response_model.model_json_schema()
@ -51,18 +50,25 @@ async def fix_validation_errors(response_model: BaseModel, response, errors):
async def get_validated_response(
chain, input_dict, response_model: BaseModel, retries: int = 1
chain,
input_dict,
response_model: BaseModel,
validation_model: Optional[BaseModel] = None,
retries: int = 1,
):
response = await chain.ainvoke(input_dict)
validation_model = validation_model or response_model
attempt = 0
while retries >= attempt:
attempt += 1
print("-" * 50)
print(f"Validation Retry attempt - {attempt}")
try:
if response and type(response) is list:
response = response[0]["args"]
validated_response = response_model(**response)
validated_response = validation_model(**response)
return validated_response
except ValidationError as e:
if retries < attempt:
@ -78,7 +84,6 @@ async def get_validated_response(
}
)
print(f"Validation Retry attempt - {attempt}")
response = await fix_validation_errors(
response_model, response, error_details
)

View file

@ -1,96 +1,111 @@
import os
from typing import AsyncIterator, List
from typing import AsyncIterator
from langchain_core.messages import (
HumanMessage,
AIMessageChunk,
AIMessage,
)
from api.utils.utils import get_large_model
from ppt_config_generator.models import PresentationMarkdownModel
from ppt_generator.models.llm_models_with_validations import (
LLMPresentationModelWithValidation,
)
from langchain_google_genai import ChatGoogleGenerativeAI
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage, AIMessageChunk
from ppt_generator.models.llm_models import LLMPresentationModel
CREATE_PRESENTATION_PROMPT = """
You're a professional presenter with years of experience in creating clear and engaging presentations.
You're a professional presenter with years of experience in creating clear and engaging presentations.
Create a presentation using the provided slide titles, images, and additional data, following specified steps and guidelines.
Create a presentation using the provided title, slide titles and body following specified steps and guidelines.
Analyze all inputs, including slide titles, graphs, summary, big idea, story and spreadsheet content to construct each slide with appropriate content and format.
Analyze all inputs, to construct each slide with appropriate content and format.
# Slide Types
- **1**: contains title, description and image.
- **2**: contains title and list of items.
- **4**: contains title and list of items with images.
- **5**: contains title, description and a graph.
- **6**: contains title, description and list of items.
- **7**: contains title and list of items with icons.
- **8**: contains title, description and list of items with icons.
# Steps
1. Analyze Prompt, and other provided data.
2. Use Slide titles provided in **Titles**.
3. Generate Slide Content for each slide. Make sure it has all the context and information required to create this individual slide from.
4. Select slide type.
5. Output should be in json format as per given schema.
6. **Adherence to schema should be beyond all the rules mentioned in notes.**
# Slide Types
- **1**: contains title, description and image.
- **2**: contains title and list of items.
- **4**: contains title and list of items with images.
- **5**: contains title, description and a graph.
- **6**: contains title, description and list of items.
- **7**: contains title and list of items with icons.
- **8**: contains title, description and list of items with icons.
# Notes
- Generate output in language mentioned in *Input*.
- Distribute contexts mentioned in prompt to slides using **info** field.
- User prompt should be respected beyond all rules or constraints.
- If the presentation is academic, then make only take the chapter text as context and create presentation according to that text and structure. Don't assume or put text or context which is not in the text.
- If **Story** is provided, presentation should follow the story flow.
- When you have to express single numbers like percentage or figures, you should use inforgraphics but for a collection of numbers in series you can use charts.
- Freely select type with images and icons.
- Introduction and Conclusion should have *Type 1* if graph is not assigned.
- Try to select **different types for every slides**.
- Don't select Type **3** for any slide.
- Make sure to give presentation in said language. You must translate and understand given context and text is in any other language.
- Do not include same graph twice in presentation without any changes to the other.
- Every series in a graph should have data in same unit. Example: all series should be in percentage or all series should be in number of items.
- Type **9** and **5** should be only picked if graph is available.
- **Strictly keep the text under given limit.**
- For slide content follow these rules:
- Highlighting in markdown format should be used to emphasize numbers and data.
- Adhere to length contraints in **body** and **description**. Focus on direct communication within character constrainsts than lengthy explanation.
- **body** and **description** in slides should never exceed character limits of 200 characters.
- Specify **don't include text in image** in image prompt.
- All the numbers should be bolded with **bold** tag in body or description of slide.
- Image prompt should cleary define how image should look like.
- Image prompt should not ask to generate **numbers, graphs, dashboard and report**.
- Examples of image prompts:
- a travel agent presenting a detailed itinerary with photos of destinations, showcasing specific experiences, highlighting travel highlights
- a person smiling while traveling, with a beautiful background scenery, such as mountains, beach, or city, golden hour lighting
- a humanoid robot standing tall, gazing confidently at the horizon, bathed in warm sunlight, the background showing a futuristic cityscape with sleek buildings and flying vehicles
- Descriptions should be clear and to the point.
- Descriptions should not use words like "This slide", "This presentation".
- If **body** contains items, *choose number of items randomly between mentioned constraints.*
- **Icon queries** must be a generic **single word noun**.
- Provide 3 icon query for each icon where,
- First one should be specific like "Led bulb".
- Second one should be more generic that first like "bulb".
- Third one should be simplest like "light".
# Steps
1. Analyze provided presentation title, slide titles and body.
2. Select slide type for each slide.
3. Output should be in json format as per given schema.
4. **Adherence to schema should be beyond all the rules mentioned in notes.**
**Go through notes and steps and make sure they are all followed. Rule breaks are strictly not allowed.**
# Notes
- Generate output in language mentioned in *Input*.
- Freely select type with images and icons.
- Introduction and Conclusion should have *Type 1* if graph is not assigned.
- Try to select **different types for every slides**.
- Don't select Type **3** for any slide.
- Do not include same graph twice in presentation without any changes to the other.
- Every series in a graph should have data in same unit. Example: all series should be in percentage or all series should be in number of items.
- Type **9** and **5** should be only picked if graph is available.
- **Strictly keep the text under given limit.**
- For slide content follow these rules:
- Highlighting in markdown format should be used to emphasize numbers and data.
- Adhere to length contraints in **body** and **description**. Focus on direct communication within character constrainsts than lengthy explanation.
- **body** and **description** in slides should never exceed character limits of 200 characters.
- Specify **don't include text in image** in image prompt.
- All the numbers should be bolded with **bold** tag in body or description of slide.
- Image prompt should cleary define how image should look like.
- Image prompt should not ask to generate **numbers, graphs, dashboard and report**.
- Examples of image prompts:
- a travel agent presenting a detailed itinerary with photos of destinations, showcasing specific experiences, highlighting travel highlights
- a person smiling while traveling, with a beautiful background scenery, such as mountains, beach, or city, golden hour lighting
- a humanoid robot standing tall, gazing confidently at the horizon, bathed in warm sunlight, the background showing a futuristic cityscape with sleek buildings and flying vehicles
- Descriptions should be clear and to the point.
- Descriptions should not use words like "This slide", "This presentation".
- If **body** contains items, *choose number of items randomly between mentioned constraints.*
- **Icon queries** must be a generic **single word noun**.
- Provide 3 icon query for each icon where,
- First one should be specific like "Led bulb".
- Second one should be more generic that first like "bulb".
- Third one should be simplest like "light".
**Follow the all the length constraints provided in the schema and notes.**
**Go through notes and steps and make sure they are all followed. Rule breaks are strictly not allowed.**
"""
schema = LLMPresentationModelWithValidation.model_json_schema()
system_prompt = f"""
{CREATE_PRESENTATION_PROMPT}
Follow this schema while giving out response: {schema}.
Make description short and obey the character limits. Output should be in JSON format. Give out only JSON, nothing else.
"""
ollama_system_prompt = f"""
{CREATE_PRESENTATION_PROMPT}
Make description short and obey the character limits. Output should be in JSON format. Give out only JSON, nothing else.
"""
def get_model_and_messages(
presentation_outline: PresentationMarkdownModel,
):
user_message = HumanMessage(presentation_outline.to_string())
model = get_large_model()
return model, system_prompt, user_message
def generate_presentation_stream(
titles: List[str],
prompt: str,
n_slides: int,
language: str,
summary: str,
presentation_outline: PresentationMarkdownModel,
) -> AsyncIterator[AIMessageChunk]:
schema = LLMPresentationModel.model_json_schema()
system_prompt = f"{CREATE_PRESENTATION_PROMPT} -|0|--|0|- Follow this schema while giving out response: {schema}. Make description short and obey the character limits. Output should be in JSON format. Give out only JSON, nothing else."
system_prompt = SystemMessage(system_prompt.replace("-|0|-", "\n"))
user_message = f"Prompt: {prompt}-|0|--|0|- Number of Slides: {n_slides}-|0|--|0|- Presentation Language: {language} -|0|--|0|- Slide Titles: {titles} -|0|--|0|- Reference Document: {summary}"
user_message = HumanMessage(user_message.replace("-|0|-", "\n"))
model = (
ChatOpenAI(model="gpt-4.1")
if os.getenv("LLM") == "openai"
else ChatGoogleGenerativeAI(model="gemini-2.0-flash")
)
model, system_prompt, user_message = get_model_and_messages(presentation_outline)
return model.astream([system_prompt, user_message])
async def generate_presentation(
presentation_outline: PresentationMarkdownModel,
) -> AIMessage:
model, system_prompt, user_message = get_model_and_messages(presentation_outline)
return await model.ainvoke([system_prompt, user_message])

View file

@ -1,7 +1,17 @@
from typing import List, Mapping
from pydantic import BaseModel, Field
from pydantic import BaseModel
from ppt_generator.models.other_models import SlideType
from ppt_generator.models.other_models import (
TYPE1,
TYPE2,
TYPE3,
TYPE4,
TYPE5,
TYPE6,
TYPE7,
TYPE8,
TYPE9,
)
from graph_processor.models import GraphModel
@ -9,68 +19,181 @@ class HeadingModel(BaseModel):
heading: str
description: str
def to_llm_content(self, image_prompt: str = None, icon_query: str = None):
from ppt_generator.models.llm_models import (
LLMHeadingModel,
LLMHeadingModelWithImagePrompt,
LLMHeadingModelWithIconQuery,
)
class IconQueryCollectionModel(BaseModel):
queries: List[str]
if image_prompt:
return LLMHeadingModelWithImagePrompt(
heading=self.heading,
description=self.description,
image_prompt=image_prompt,
)
elif icon_query:
return LLMHeadingModelWithIconQuery(
heading=self.heading,
description=self.description,
icon_query=icon_query,
)
return LLMHeadingModel(
heading=self.heading,
description=self.description,
)
class SlideContentModel(BaseModel):
title: str
def to_llm_content(self):
raise NotImplementedError("to_llm_content method not implemented")
class Type1Content(SlideContentModel):
body: str
image_prompts: List[str]
def to_llm_content(self):
from ppt_generator.models.llm_models import LLMType1Content
return LLMType1Content(
title=self.title,
body=self.body,
image_prompt=self.image_prompts[0] if self.image_prompts else "",
)
class Type2Content(SlideContentModel):
body: List[HeadingModel]
def to_llm_content(self):
from ppt_generator.models.llm_models import LLMType2Content
return LLMType2Content(
title=self.title,
body=[item.to_llm_content() for item in self.body],
)
class Type3Content(SlideContentModel):
body: List[HeadingModel]
image_prompts: List[str]
def to_llm_content(self):
from ppt_generator.models.llm_models import LLMType3Content
return LLMType3Content(
title=self.title,
body=[item.to_llm_content() for item in self.body],
image_prompt=self.image_prompts[0] if self.image_prompts else "",
)
class Type4Content(SlideContentModel):
body: List[HeadingModel]
image_prompts: List[str]
def to_llm_content(self):
from ppt_generator.models.llm_models import LLMType4Content
llm_body = []
for i, item in enumerate(self.body):
image_prompt = self.image_prompts[i] if i < len(self.image_prompts) else ""
llm_body.append(item.to_llm_content(image_prompt=image_prompt))
return LLMType4Content(
title=self.title,
body=llm_body,
)
class Type5Content(SlideContentModel):
body: str
graph: GraphModel
def to_llm_content(self):
from ppt_generator.models.llm_models import LLMType5Content
return LLMType5Content(
title=self.title,
body=self.body,
graph=self.graph,
)
class Type6Content(SlideContentModel):
description: str
body: List[HeadingModel]
def to_llm_content(self):
from ppt_generator.models.llm_models import LLMType6Content
return LLMType6Content(
title=self.title,
description=self.description,
body=[item.to_llm_content() for item in self.body],
)
class Type7Content(SlideContentModel):
body: List[HeadingModel]
icon_queries: List[IconQueryCollectionModel]
icon_queries: List[str]
def to_llm_content(self):
from ppt_generator.models.llm_models import LLMType7Content
llm_body = []
for i, item in enumerate(self.body):
icon_query = self.icon_queries[i] if i < len(self.icon_queries) else ""
llm_body.append(item.to_llm_content(icon_query=icon_query))
return LLMType7Content(
title=self.title,
body=llm_body,
)
class Type8Content(SlideContentModel):
description: str
body: List[HeadingModel]
icon_queries: List[IconQueryCollectionModel]
icon_queries: List[str]
def to_llm_content(self):
from ppt_generator.models.llm_models import LLMType8Content
llm_body = []
for i, item in enumerate(self.body):
icon_query = self.icon_queries[i] if i < len(self.icon_queries) else ""
llm_body.append(item.to_llm_content(icon_query=icon_query))
return LLMType8Content(
title=self.title,
description=self.description,
body=llm_body,
)
class Type9Content(SlideContentModel):
body: List[HeadingModel]
graph: GraphModel
def to_llm_content(self):
from ppt_generator.models.llm_models import LLMType9Content
CONTENT_TYPE_MAPPING: Mapping[SlideType, SlideContentModel] = {
SlideType.type1: Type1Content,
SlideType.type2: Type2Content,
SlideType.type3: Type3Content,
SlideType.type4: Type4Content,
SlideType.type5: Type5Content,
SlideType.type6: Type6Content,
SlideType.type7: Type7Content,
SlideType.type8: Type8Content,
SlideType.type9: Type9Content,
return LLMType9Content(
title=self.title,
body=[item.to_llm_content() for item in self.body],
graph=self.graph,
)
CONTENT_TYPE_MAPPING: Mapping[int, SlideContentModel] = {
TYPE1: Type1Content,
TYPE2: Type2Content,
TYPE3: Type3Content,
TYPE4: Type4Content,
TYPE5: Type5Content,
TYPE6: Type6Content,
TYPE7: Type7Content,
TYPE8: Type8Content,
TYPE9: Type9Content,
}

View file

@ -1,220 +1,182 @@
from typing import List, Mapping
from pydantic import BaseModel, Field
from pydantic import BaseModel
from graph_processor.models import GraphModel
from ppt_generator.models.content_type_models import SlideContentModel
from ppt_generator.models.other_models import SlideType
from ppt_generator.models.content_type_models import (
HeadingModel,
SlideContentModel,
Type1Content,
Type2Content,
Type3Content,
Type4Content,
Type5Content,
Type6Content,
Type7Content,
Type8Content,
Type9Content,
)
from ppt_generator.models.other_models import (
TYPE1,
TYPE2,
TYPE3,
TYPE4,
TYPE5,
TYPE6,
TYPE7,
TYPE8,
TYPE9,
)
class LLMHeadingModel(BaseModel):
heading: str = Field(
description="List item heading to show in slide body",
max_length=35,
)
description: str = Field(
description="Description of list item in less than 20 words.",
max_length=180,
min_length=100,
)
heading: str
description: str
def to_content(self) -> HeadingModel:
return HeadingModel(
heading=self.heading,
description=self.description,
)
class LLMIconQueryCollectionModel(BaseModel):
queries: List[str] = Field(
description="Multiple queries to generate simillar icons matching heading and description"
)
class LLMHeadingModelWithImagePrompt(LLMHeadingModel):
image_prompt: str
class LLMHeadingModelWithIconQuery(LLMHeadingModel):
icon_query: str
class LLMSlideContentModel(BaseModel):
title: str = Field(description="Title of the slide")
title: str
@classmethod
def get_notes(cls) -> str:
return ""
def to_content(self) -> SlideContentModel:
raise NotImplementedError("to_content method not implemented")
class LLMType1Content(LLMSlideContentModel):
body: str = Field(
description="Slide content summary in less than 15 words. This will be shown in text box in slide.",
max_length=230,
min_length=150,
)
image_prompts: List[str] = Field(
description="Prompt used to generate image for this slide. Only one prompt is allowed.",
min_length=1,
max_length=1,
)
body: str
image_prompt: str
def to_content(self) -> Type1Content:
return Type1Content(
title=self.title,
body=self.body,
image_prompts=[self.image_prompt],
)
class LLMType2Content(LLMSlideContentModel):
body: List[LLMHeadingModel] = Field(
"List items to show in slide's body",
min_length=1,
max_length=4,
)
body: List[LLMHeadingModel]
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 4 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
def to_content(self) -> Type2Content:
return Type2Content(
title=self.title,
body=[each.to_content() for each in self.body],
)
class LLMType3Content(LLMSlideContentModel):
body: List[LLMHeadingModel] = Field(
"List items to show in slide's body",
min_length=3,
max_length=3,
)
image_prompts: List[str] = Field(
description="Prompt used to generate image for this slide",
min_length=1,
max_length=1,
)
body: List[LLMHeadingModel]
image_prompt: str
@classmethod
def get_notes(cls):
return """
- The **Body** should include **3 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
def to_content(self) -> Type3Content:
return Type3Content(
title=self.title,
body=[each.to_content() for each in self.body],
image_prompts=[self.image_prompt],
)
class LLMType4Content(LLMSlideContentModel):
body: List[LLMHeadingModel] = Field(
"List items to show in slide's body",
min_length=1,
max_length=3,
)
image_prompts: List[str] = Field(
description="Prompts used to generate image for each item in body",
min_length=1,
max_length=3,
)
body: List[LLMHeadingModelWithImagePrompt]
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 3 HeadingModels**.
- **Image prompts** should contain one prompt for each item in body.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
def to_content(self) -> Type4Content:
return Type4Content(
title=self.title,
body=[each.to_content() for each in self.body],
image_prompts=[each.image_prompt for each in self.body],
)
class LLMType5Content(LLMSlideContentModel):
body: str = Field(
description="Slide content summary in less than 15 words. This will be shown in text box in slide.",
max_length=230,
min_length=150,
)
graph: GraphModel = Field(description="Graph to show in slide")
body: str
graph: GraphModel
def to_content(self) -> Type5Content:
return Type5Content(
title=self.title,
body=self.body,
graph=self.graph,
)
class LLMType6Content(LLMSlideContentModel):
description: str = Field(
description="Slide content summary in less than 15 words. This will be shown in text box in slide.",
)
body: List[LLMHeadingModel] = Field(
description="List items to show in slide's body",
min_length=1,
max_length=3,
)
description: str
body: List[LLMHeadingModel]
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 3 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
def to_content(self) -> Type6Content:
return Type6Content(
title=self.title,
description=self.description,
body=[each.to_content() for each in self.body],
)
class LLMType7Content(LLMSlideContentModel):
body: List[LLMHeadingModel] = Field(
description="List items to show in slide's body",
min_length=1,
max_length=4,
)
icon_queries: List[LLMIconQueryCollectionModel] = Field(
description="One icon query collection model for every item in body to search icon",
min_length=1,
max_length=4,
)
body: List[LLMHeadingModelWithIconQuery]
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 4 HeadingModels**.
- Each **IconQueryCollectionModel** must contain 3 *queries*.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
def to_content(self) -> Type7Content:
return Type7Content(
title=self.title,
body=[each.to_content() for each in self.body],
icon_queries=[each.icon_query for each in self.body],
)
class LLMType8Content(LLMSlideContentModel):
description: str = Field(
description="Slide content summary in less than 15 words. This will be shown in text box in slide.",
max_length=230,
min_length=150,
)
body: List[LLMHeadingModel] = Field(
"List items to show in slide's body",
min_length=1,
max_length=3,
)
icon_queries: List[LLMIconQueryCollectionModel] = Field(
description="One icon query collection model for every item in body to search icon"
)
description: str
body: List[LLMHeadingModelWithImagePrompt]
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 3 HeadingModels**.
- Each **IconQueryCollectionModel** must contain 3 *queries*.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
def to_content(self) -> Type8Content:
return Type8Content(
title=self.title,
description=self.description,
body=[each.to_content() for each in self.body],
icon_queries=[each.image_prompt for each in self.body],
)
class LLMType9Content(LLMSlideContentModel):
body: List[LLMHeadingModel] = Field(
"List items to show in slide's body",
min_length=1,
max_length=3,
)
graph: GraphModel = Field(description="Graph to show in slide")
body: List[LLMHeadingModel]
graph: GraphModel
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 3 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
def to_content(self) -> Type9Content:
return Type9Content(
title=self.title,
body=[each.to_content() for each in self.body],
graph=self.graph,
)
LLM_CONTENT_TYPE_MAPPING: Mapping[SlideType, LLMSlideContentModel] = {
SlideType.type1: LLMType1Content,
SlideType.type2: LLMType2Content,
SlideType.type3: LLMType3Content,
SlideType.type4: LLMType4Content,
SlideType.type5: LLMType5Content,
SlideType.type6: LLMType6Content,
SlideType.type7: LLMType7Content,
SlideType.type8: LLMType8Content,
SlideType.type9: LLMType9Content,
LLM_CONTENT_TYPE_MAPPING: Mapping[int, LLMSlideContentModel] = {
TYPE1: LLMType1Content,
TYPE2: LLMType2Content,
TYPE3: LLMType3Content,
TYPE4: LLMType4Content,
TYPE5: LLMType5Content,
TYPE6: LLMType6Content,
TYPE7: LLMType7Content,
TYPE8: LLMType8Content,
TYPE9: LLMType9Content,
}
class LLMSlideModel(BaseModel):
type: SlideType
type: int
content: (
LLMType1Content
| LLMType2Content
| LLMType3Content
| LLMType4Content
| LLMType5Content
| LLMType6Content
@ -225,7 +187,4 @@ class LLMSlideModel(BaseModel):
class LLMPresentationModel(BaseModel):
title: str
n_slides: int
titles: list[str]
slides: list[LLMSlideModel]

View file

@ -0,0 +1,295 @@
from typing import List, Mapping
from pydantic import Field
from graph_processor.models import GraphModel
from ppt_generator.models.other_models import (
TYPE1,
TYPE2,
TYPE3,
TYPE4,
TYPE5,
TYPE6,
TYPE7,
TYPE8,
TYPE9,
)
from ppt_generator.models.llm_models import (
LLMHeadingModel,
LLMHeadingModelWithImagePrompt,
LLMHeadingModelWithIconQuery,
LLMSlideContentModel,
LLMType1Content,
LLMType2Content,
LLMType3Content,
LLMType4Content,
LLMType5Content,
LLMType6Content,
LLMType7Content,
LLMType8Content,
LLMType9Content,
LLMSlideModel,
LLMPresentationModel,
)
class LLMHeadingModelWithValidation(LLMHeadingModel):
heading: str = Field(
description="List item heading to show in slide body",
min_length=10,
max_length=30,
)
description: str = Field(
description="Description of list item in less than 20 words.",
min_length=80,
max_length=150,
)
class LLMHeadingModelWithImagePromptWithValidation(LLMHeadingModelWithImagePrompt):
image_prompt: str = Field(
description="Prompt used to generate image for this item",
min_length=10,
max_length=50,
)
class LLMHeadingModelWithIconQueryWithValidation(LLMHeadingModelWithIconQuery):
icon_query: str = Field(
description="Icon query to generate icon for this item",
min_length=10,
max_length=50,
)
class LLMType1ContentWithValidation(LLMType1Content):
title: str = Field(
description="Title of the slide",
min_length=10,
max_length=50,
)
body: str = Field(
description="Slide content summary in less than 30 words.",
min_length=100,
max_length=200,
)
image_prompt: str = Field(
description="Prompt used to generate image for this slide.",
min_length=10,
max_length=50,
)
@classmethod
def get_notes(cls):
return ""
class LLMType2ContentWithValidation(LLMType2Content):
title: str = Field(
description="Title of the slide",
min_length=10,
max_length=50,
)
body: List[LLMHeadingModelWithValidation] = Field(
description="List items to show in slide's body",
min_length=1,
max_length=4,
)
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 4 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
class LLMType3ContentWithValidation(LLMType3Content):
title: str = Field(
description="Title of the slide",
min_length=10,
max_length=50,
)
body: List[LLMHeadingModelWithValidation] = Field(
description="List items to show in slide's body",
min_length=3,
max_length=3,
)
image_prompt: str = Field(
description="Prompt used to generate image for this slide",
min_length=10,
max_length=50,
)
@classmethod
def get_notes(cls):
return """
- The **Body** should include **3 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
class LLMType4ContentWithValidation(LLMType4Content):
title: str = Field(
description="Title of the slide",
min_length=10,
max_length=50,
)
body: List[LLMHeadingModelWithImagePromptWithValidation] = Field(
description="List items to show in slide's body",
min_length=1,
max_length=3,
)
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 3 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
class LLMType5ContentWithValidation(LLMType5Content):
title: str = Field(
description="Title of the slide",
min_length=10,
max_length=50,
)
body: str = Field(
description="Slide content summary in less than 30 words.",
min_length=100,
max_length=250,
)
graph: GraphModel = Field(description="Graph to show in slide")
@classmethod
def get_notes(self):
return ""
class LLMType6ContentWithValidation(LLMType6Content):
title: str = Field(
description="Title of the slide",
min_length=10,
max_length=50,
)
description: str = Field(
description="Slide content summary in less than 20 words.",
min_length=80,
max_length=150,
)
body: List[LLMHeadingModelWithValidation] = Field(
description="List items to show in slide's body",
min_length=1,
max_length=3,
)
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 3 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
class LLMType7ContentWithValidation(LLMType7Content):
title: str = Field(
description="Title of the slide",
min_length=10,
max_length=50,
)
body: List[LLMHeadingModelWithIconQueryWithValidation] = Field(
description="List items to show in slide's body",
min_length=1,
max_length=4,
)
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 4 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
class LLMType8ContentWithValidation(LLMType8Content):
title: str = Field(
description="Title of the slide",
min_length=10,
max_length=50,
)
description: str = Field(
description="Slide content summary in less than 20 words.",
min_length=80,
max_length=150,
)
body: List[LLMHeadingModelWithImagePromptWithValidation] = Field(
description="List items to show in slide's body",
min_length=1,
max_length=3,
)
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 3 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
class LLMType9ContentWithValidation(LLMType9Content):
title: str = Field(
description="Title of the slide",
min_length=10,
max_length=50,
)
body: List[LLMHeadingModelWithValidation] = Field(
description="List items to show in slide's body",
min_length=1,
max_length=3,
)
graph: GraphModel = Field(description="Graph to show in slide")
@classmethod
def get_notes(cls):
return """
- The **Body** should include **1 to 3 HeadingModels**.
- Each **Heading** must consist of **1 to 3 words**.
- Each item **Description** can be upto 10 words.
"""
LLM_CONTENT_TYPE_WITH_VALIDATION_MAPPING: Mapping[int, LLMSlideContentModel] = {
TYPE1: LLMType1ContentWithValidation,
TYPE2: LLMType2ContentWithValidation,
TYPE3: LLMType3ContentWithValidation,
TYPE4: LLMType4ContentWithValidation,
TYPE5: LLMType5ContentWithValidation,
TYPE6: LLMType6ContentWithValidation,
TYPE7: LLMType7ContentWithValidation,
TYPE8: LLMType8ContentWithValidation,
TYPE9: LLMType9ContentWithValidation,
}
class LLMSlideModelWithValidation(LLMSlideModel):
type: int
content: (
LLMType1ContentWithValidation
| LLMType2ContentWithValidation
| LLMType4ContentWithValidation
| LLMType5ContentWithValidation
| LLMType6ContentWithValidation
| LLMType7ContentWithValidation
| LLMType8ContentWithValidation
| LLMType9ContentWithValidation
)
class LLMPresentationModelWithValidation(LLMPresentationModel):
slides: list[LLMSlideModelWithValidation]

View file

@ -1,33 +1,24 @@
from enum import Enum
from pydantic import BaseModel, Field
# """
# 1. contains title, description and an image.
TYPE1 = 1
# 2. contains title and list of items.
TYPE2 = 2
# 3. contains title, list of items and an image.
TYPE3 = 3
# 4. contains title and list of items and multiple images.
TYPE4 = 4
# 5. contains title, description and a graph.
TYPE5 = 5
# 6. contains title, description and list of items.
TYPE6 = 6
# 7. contains title, list of items and icons.
TYPE7 = 7
# 8. contains title, description, list of items and icons.
TYPE8 = 8
# 9. contains title, list of items and a graph.
# """
class SlideType(Enum):
type1 = 1
type2 = 2
type3 = 3
type4 = 4
type5 = 5
type6 = 6
type7 = 7
type8 = 8
type9 = 9
TYPE9 = 9
class SlideTypeModel(BaseModel):
slide_type: int = Field(
default=1, gte=1, lte=9, description="Slide type from 1 to 9"
)
slide_type: int = Field(gte=1, lte=9, description="Slide type from 1 to 9")

View file

@ -3,8 +3,6 @@ from typing import Optional
from pydantic import BaseModel
from ppt_generator.models.content_type_models import IconQueryCollectionModel
class ImageAspectRatio(Enum):
r_1_1 = "1:1"
@ -39,4 +37,4 @@ class IconQueryCollectionWithData(BaseModel):
category: IconCategoryEnum = IconCategoryEnum.solid
index: int
theme: Optional[dict] = None
icon_query: IconQueryCollectionModel
icon_query: str

View file

@ -2,7 +2,6 @@ import uuid
from typing import List, Optional
from pydantic import BaseModel
from ppt_generator.models.other_models import SlideType
from ppt_generator.models.content_type_models import (
CONTENT_TYPE_MAPPING,
Type1Content,
@ -20,7 +19,7 @@ from ppt_generator.models.content_type_models import (
class SlideModel(BaseModel):
id: Optional[str] = None
index: int
type: SlideType
type: int
design_index: Optional[int] = None
images: Optional[List[str]] = None
icons: Optional[List[str]] = None

View file

@ -1,19 +1,60 @@
from typing import Optional
import os
from langchain_google_genai import ChatGoogleGenerativeAI
from api.utils.utils import get_large_model, get_small_model
from ppt_config_generator.models import SlideMarkdownModel
from ppt_generator.fix_validation_errors import get_validated_response
from ppt_generator.models.content_type_models import (
CONTENT_TYPE_MAPPING,
)
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from ppt_generator.models.other_models import SlideType, SlideTypeModel
from ppt_generator.models.llm_models import (
LLM_CONTENT_TYPE_MAPPING,
LLMSlideContentModel,
)
from ppt_generator.models.llm_models_with_validations import (
LLM_CONTENT_TYPE_WITH_VALIDATION_MAPPING,
)
from ppt_generator.models.other_models import SlideTypeModel
from ppt_generator.models.slide_model import SlideModel
prompt_template_from_slide = ChatPromptTemplate.from_messages(
prompt_template_to_generate_slide_content = ChatPromptTemplate.from_messages(
[
(
"system",
"""
Generate structured slide based on provided title and outline, follow mentioned steps and notes and provide structured output.
# Steps
1. Analyze the outline and title.
2. Generate structured slide based on the outline and title.
3. Generate image prompts and icon queries if mentioned in schema.
4. Generate graph if mentioned in schema.
# Notes
- Slide body should not use words like "This slide", "This presentation".
- Rephrase the slide body to make it flow naturally.
- Do not use markdown formatting in slide body.
- **Icon query** must be a generic single word noun.
- **Image prompt** should be a 2-3 words phrase.
- Try to make paragraphs as short as possible.
{notes}
""",
),
(
"user",
"""
## Slide Title
{title}
## Slide Outline
{outline}
""",
),
]
)
prompt_template_to_edit_slide_content = ChatPromptTemplate.from_messages(
[
(
"system",
@ -46,7 +87,7 @@ prompt_template_from_slide = ChatPromptTemplate.from_messages(
)
prompt_template_from_slide_type = ChatPromptTemplate.from_messages(
prompt_template_to_select_slide_type = ChatPromptTemplate.from_messages(
[
(
"system",
@ -63,8 +104,6 @@ prompt_template_from_slide_type = ChatPromptTemplate.from_messages(
- **7**: contains title and list of items with icons.
- **8**: contains title, description and list of items with icons.
- **9**: contains title, list of items and a graph.
- **10**: contains title, list of inforgraphic charts with supporting information.
- **11**: contains title, a single inforgraphic chart and description.
# Notes
- Do not select different slide type than current unless absolutely necessary as per user prompt.
@ -84,25 +123,44 @@ prompt_template_from_slide_type = ChatPromptTemplate.from_messages(
)
async def get_slide_content_from_type_and_outline(
slide_type: int, outline: SlideMarkdownModel
) -> LLMSlideContentModel:
content_type_model_type = LLM_CONTENT_TYPE_WITH_VALIDATION_MAPPING[slide_type]
validation_model = LLM_CONTENT_TYPE_MAPPING[slide_type]
model = get_small_model().with_structured_output(
content_type_model_type.model_json_schema()
)
chain = prompt_template_to_generate_slide_content | model
return await get_validated_response(
chain,
{
"title": outline.title,
"outline": outline.body,
"notes": content_type_model_type.get_notes(),
},
content_type_model_type,
validation_model,
)
async def get_edited_slide_content_model(
prompt: str,
slide_type: SlideType,
slide_type: int,
slide: SlideModel,
theme: Optional[dict] = None,
language: Optional[str] = None,
):
model = (
ChatOpenAI(model="gpt-4.1-mini")
if os.getenv("LLM") == "openai"
else ChatGoogleGenerativeAI(model="gemini-2.0-flash")
)
model = get_large_model()
content_type_model_type = CONTENT_TYPE_MAPPING[slide_type]
chain = prompt_template_from_slide | model.with_structured_output(
content_type_model_type = LLM_CONTENT_TYPE_WITH_VALIDATION_MAPPING[slide_type]
validation_model = LLM_CONTENT_TYPE_MAPPING[slide_type]
chain = prompt_template_to_edit_slide_content | model.with_structured_output(
content_type_model_type.model_json_schema()
)
slide_data = slide.content.model_dump_json()
return await get_validated_response(
slide_data = slide.content.to_llm_content().model_dump_json()
edited_content = await get_validated_response(
chain,
{
"prompt": prompt,
@ -112,24 +170,23 @@ async def get_edited_slide_content_model(
"notes": "",
},
content_type_model_type,
validation_model,
)
return edited_content.to_content()
async def get_slide_type_from_prompt(
prompt: str,
slide: SlideModel,
) -> SlideTypeModel:
model = (
ChatOpenAI(model="gpt-4.1-mini")
if os.getenv("LLM") == "openai"
else ChatGoogleGenerativeAI(model="gemini-2.0-flash")
)
model = get_small_model()
chain = prompt_template_from_slide_type | model.with_structured_output(
chain = prompt_template_to_select_slide_type | model.with_structured_output(
SlideTypeModel.model_json_schema()
)
slide_data = slide.content.model_dump_json()
slide_data = slide.content.to_llm_content().model_dump_json()
return await get_validated_response(
chain,
{

View file

@ -1,31 +1,40 @@
from typing import List, Optional
from ppt_generator.models.other_models import SlideType
from ppt_generator.models.other_models import (
TYPE1,
TYPE2,
TYPE3,
TYPE4,
TYPE5,
TYPE6,
TYPE7,
TYPE8,
TYPE9,
)
from ppt_generator.models.query_and_prompt_models import (
IconCategoryEnum,
IconFrameEnum,
IconQueryCollectionWithData,
ImageAspectRatio,
ImagePromptWithThemeAndAspectRatio,
)
from ppt_generator.models.slide_model import SlideModel
SLIDE_WITHOUT_IMAGE = [
SlideType.type2,
SlideType.type5,
SlideType.type6,
SlideType.type7,
SlideType.type8,
SlideType.type9,
SLIDES_WITHOUT_IMAGES = [
TYPE2,
TYPE5,
TYPE6,
TYPE7,
TYPE8,
TYPE9,
]
SLIDE_WITHOUT_ICON = [
SlideType.type1,
SlideType.type2,
SlideType.type3,
SlideType.type4,
SlideType.type5,
SlideType.type6,
SlideType.type9,
SLIDES_WITHOUT_ICONS = [
TYPE1,
TYPE2,
TYPE3,
TYPE4,
TYPE5,
TYPE6,
TYPE9,
]
@ -49,15 +58,15 @@ class SlideModelUtils:
def get_image_prompts(self) -> List[ImagePromptWithThemeAndAspectRatio]:
theme_prompt = THEME_PROMPTS.get(self.theme["name"], "") if self.theme else ""
if self.type in SLIDE_WITHOUT_IMAGE:
if self.type in SLIDES_WITHOUT_IMAGES:
return []
aspect_ratio = ImageAspectRatio.r_1_1
if self.type is SlideType.type3:
if self.type is TYPE3:
aspect_ratio = ImageAspectRatio.r_2_3
elif self.type is SlideType.type4:
elif self.type is TYPE4:
count = len(self.content.body)
aspect_ratio = (
ImageAspectRatio.r_5_4 if count == 3 else ImageAspectRatio.r_21_9
@ -73,7 +82,7 @@ class SlideModelUtils:
]
def get_icon_queries(self) -> List[IconQueryCollectionWithData]:
if self.type in SLIDE_WITHOUT_ICON:
if self.type in SLIDES_WITHOUT_ICONS:
return []
category = IconCategoryEnum.solid

View file

@ -1,31 +0,0 @@
[tool.poetry]
name = "presenton-fastapi-server"
version = "0.1.0"
description = ""
authors = ["Your Name <you@example.com>"]
readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.11"
fastapi = { extras = ["standard"], version = "^0.115.12" }
langchain = "^0.3.25"
sqlmodel = "^0.0.24"
python-pptx = "^1.0.2"
python-docx = "^1.1.2"
langchain-openai = "^0.3.16"
langchain-google-genai = "^2.1.4"
langchain-community = "^0.3.23"
pdfplumber = "^0.11.6"
fastembed = "^0.6.1"
[[tool.poetry.source]]
name = "pytorch-cpu"
url = "https://download.pytorch.org/whl/cpu"
priority = "explicit"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View file

@ -0,0 +1,121 @@
aiohappyeyeballs==2.6.1
aiohttp==3.11.18
aiosignal==1.3.2
annotated-types==0.7.0
anyio==4.9.0
async-timeout==5.0.1
attrs==25.3.0
cachetools==5.5.2
certifi==2025.4.26
cffi==1.17.1
charset-normalizer==3.4.2
click==8.1.8
colorama==0.4.6
coloredlogs==15.0.1
cryptography==44.0.3
dataclasses-json==0.6.7
distro==1.9.0
dnspython==2.7.0
email_validator==2.2.0
fastapi==0.115.12
fastapi-cli==0.0.7
fastembed==0.7.0
filelock==3.18.0
filetype==1.2.0
flatbuffers==25.2.10
frozenlist==1.6.0
fsspec==2025.3.2
google-ai-generativelanguage==0.6.18
google-api-core==2.24.2
google-auth==2.40.1
googleapis-common-protos==1.70.0
greenlet==3.2.2
grpcio==1.72.0rc1
grpcio-status==1.72.0rc1
h11==0.16.0
httpcore==1.0.9
httptools==0.6.4
httpx==0.28.1
httpx-sse==0.4.0
huggingface-hub==0.31.2
humanfriendly==10.0
idna==3.10
Jinja2==3.1.6
jiter==0.9.0
jsonpatch==1.33
jsonpointer==3.0.0
langchain==0.3.25
langchain-community==0.3.24
langchain-core==0.3.65
langchain-google-genai==2.1.4
langchain-ollama==0.3.3
langchain-openai==0.3.16
langchain-text-splitters==0.3.8
langsmith==0.3.45
loguru==0.7.3
lxml==5.4.0
markdown-it-py==3.0.0
MarkupSafe==3.0.2
marshmallow==3.26.1
mdurl==0.1.2
mmh3==5.1.0
mpmath==1.3.0
multidict==6.4.3
mypy_extensions==1.1.0
numpy==2.2.5
ollama==0.5.1
onnxruntime==1.22.0
openai==1.78.1
orjson==3.10.18
packaging==24.2
pdfminer.six==20250327
pdfplumber==0.11.6
pillow==11.2.1
propcache==0.3.1
proto-plus==1.26.1
protobuf==6.31.0
py_rust_stemmers==0.1.5
pyasn1==0.6.1
pyasn1_modules==0.4.2
pycparser==2.22
pydantic==2.11.4
pydantic-settings==2.9.1
pydantic_core==2.33.2
Pygments==2.19.1
pypdfium2==4.30.1
pyreadline3==3.5.4
python-docx==1.1.2
python-dotenv==1.1.0
python-multipart==0.0.20
python-pptx==1.0.2
PyYAML==6.0.2
redis==6.2.0
regex==2024.11.6
requests==2.32.3
requests-toolbelt==1.0.0
rich==14.0.0
rich-toolkit==0.14.6
rsa==4.9.1
shellingham==1.5.4
sniffio==1.3.1
SQLAlchemy==2.0.41
sqlmodel==0.0.24
starlette==0.46.2
sympy==1.14.0
tenacity==9.1.2
tiktoken==0.9.0
tokenizers==0.21.1
tqdm==4.67.1
typer==0.15.4
typing-inspect==0.9.0
typing-inspection==0.4.0
typing_extensions==4.13.2
urllib3==2.4.0
uvicorn==0.34.2
uvloop==0.21.0
watchfiles==1.0.5
websockets==15.0.1
win32_setctime==1.2.0
XlsxWriter==3.2.3
yarl==1.20.0
zstandard==0.23.0

View file

@ -14,4 +14,4 @@ if __name__ == "__main__":
)
args = parser.parse_args()
uvicorn.run("api.main:app", host="127.0.0.1", port=args.port, log_level="info")
uvicorn.run("api.main:app", host="0.0.0.0", port=args.port, log_level="info")

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