refactor!: use presenton docker codebase
This commit is contained in:
parent
91619dddd9
commit
31073b6016
171 changed files with 5871 additions and 17444 deletions
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.venv
|
||||
.env
|
||||
.next
|
||||
node_modules
|
||||
out
|
||||
build
|
||||
.git
|
||||
.gitignore
|
||||
tmp
|
||||
197
.gitignore
vendored
197
.gitignore
vendored
|
|
@ -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
53
Dockerfile
Normal 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
43
Dockerfile.dev
Normal 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
157
README.md
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
## 💻📥 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
|
||||

|
||||
|
||||
### 4. Present on app
|
||||
### 4. Select theme
|
||||

|
||||
|
||||
### 5. Present on app
|
||||

|
||||
|
||||
### 5. Change theme
|
||||
### 6. Change theme
|
||||

|
||||
|
||||
### 6. Export presentation as PDF and PPTX
|
||||
### 7. Export presentation as PDF and PPTX
|
||||

|
||||
|
||||
## Community
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
})
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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 };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
98
app/main.ts
98
app/main.ts
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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
27
app/types/index.d.ts
vendored
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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, '_');
|
||||
}
|
||||
|
|
@ -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`);
|
||||
}
|
||||
23
build.js
23
build.js
|
|
@ -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
54
docker-compose.yml
Normal 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
29
nginx.conf
Normal 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
5984
package-lock.json
generated
File diff suppressed because it is too large
Load diff
40
package.json
40
package.json
|
|
@ -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
1
resources/ui/assets/css/tailwind.import.css
vendored
1
resources/ui/assets/css/tailwind.import.css
vendored
|
|
@ -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 |
|
|
@ -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>
|
||||
7
servers/fastapi/.vscode/settings.json
vendored
7
servers/fastapi/.vscode/settings.json
vendored
|
|
@ -1,7 +0,0 @@
|
|||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"tests"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
}
|
||||
1
servers/fastapi/api/assets/icons_vectorstore.json
Normal file
1
servers/fastapi/api/assets/icons_vectorstore.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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}",
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
109
servers/fastapi/api/services/redis.py
Normal file
109
servers/fastapi/api/services/redis.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
253
servers/fastapi/api/utils/supported_ollama_models.py
Normal file
253
servers/fastapi/api/utils/supported_ollama_models.py
Normal 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,
|
||||
}
|
||||
|
|
@ -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(
|
||||
46
servers/fastapi/api/utils/variable_length_models.py
Normal file
46
servers/fastapi/api/utils/variable_length_models.py
Normal 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
|
||||
BIN
servers/fastapi/assets/icons/deepseek.png
Normal file
BIN
servers/fastapi/assets/icons/deepseek.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
BIN
servers/fastapi/assets/icons/gemma.png
Normal file
BIN
servers/fastapi/assets/icons/gemma.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
BIN
servers/fastapi/assets/icons/meta.png
Normal file
BIN
servers/fastapi/assets/icons/meta.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
BIN
servers/fastapi/assets/icons/qwen.png
Normal file
BIN
servers/fastapi/assets/icons/qwen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
4037
servers/fastapi/poetry.lock
generated
4037
servers/fastapi/poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,3 +0,0 @@
|
|||
[virtualenvs]
|
||||
create = true
|
||||
in-project = true
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
9
servers/fastapi/ppt_config_generator/parsers.py
Normal file
9
servers/fastapi/ppt_config_generator/parsers.py
Normal 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)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
76
servers/fastapi/ppt_config_generator/structure_generator.py
Normal file
76
servers/fastapi/ppt_config_generator/structure_generator.py
Normal 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,
|
||||
)
|
||||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
121
servers/fastapi/requirements.txt
Normal file
121
servers/fastapi/requirements.txt
Normal 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
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue