Merge pull request #286 from presenton/development
New templates and fields in /generate endpoint
10
Dockerfile
|
|
@ -6,9 +6,7 @@ RUN apt-get update && apt-get install -y \
|
|||
curl \
|
||||
libreoffice \
|
||||
fontconfig \
|
||||
imagemagick
|
||||
|
||||
RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
|
||||
chromium
|
||||
|
||||
|
||||
# Install Node.js 20 using NodeSource repository
|
||||
|
|
@ -22,7 +20,7 @@ WORKDIR /app
|
|||
# Set environment variables
|
||||
ENV APP_DATA_DIRECTORY=/app_data
|
||||
ENV TEMP_DIRECTORY=/tmp/presenton
|
||||
# ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi"
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
|
||||
|
||||
# Install ollama
|
||||
|
|
@ -31,7 +29,7 @@ RUN curl -fsSL https://ollama.com/install.sh | sh
|
|||
# Install dependencies for FastAPI
|
||||
RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \
|
||||
pathvalidate pdfplumber chromadb sqlmodel \
|
||||
anthropic google-genai openai fastmcp
|
||||
anthropic google-genai openai fastmcp dirtyjson
|
||||
RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu
|
||||
|
||||
# Install dependencies for Next.js
|
||||
|
|
@ -39,8 +37,6 @@ 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@136.0.7103.92 --install-deps
|
||||
|
||||
# Copy Next.js app
|
||||
COPY servers/nextjs/ /app/servers/nextjs/
|
||||
|
|
|
|||
|
|
@ -6,10 +6,7 @@ RUN apt-get update && apt-get install -y \
|
|||
curl \
|
||||
libreoffice \
|
||||
fontconfig \
|
||||
imagemagick
|
||||
|
||||
RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
|
||||
|
||||
chromium
|
||||
|
||||
# Install Node.js 20 using NodeSource repository
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
|
|
@ -24,7 +21,7 @@ RUN ls -a
|
|||
# Set environment variables
|
||||
ENV APP_DATA_DIRECTORY=/app_data
|
||||
ENV TEMP_DIRECTORY=/tmp/presenton
|
||||
# ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi"
|
||||
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
|
||||
|
||||
# Install ollama
|
||||
RUN curl -fsSL http://ollama.com/install.sh | sh
|
||||
|
|
@ -32,7 +29,7 @@ RUN curl -fsSL http://ollama.com/install.sh | sh
|
|||
# Install dependencies for FastAPI
|
||||
RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \
|
||||
pathvalidate pdfplumber chromadb sqlmodel \
|
||||
anthropic google-genai openai fastmcp
|
||||
anthropic google-genai openai fastmcp dirtyjson
|
||||
RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu
|
||||
|
||||
# Install dependencies for Next.js
|
||||
|
|
@ -40,9 +37,6 @@ WORKDIR /node_dependencies
|
|||
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
|
||||
RUN npm install --verbose
|
||||
|
||||
# Install chrome for puppeteer
|
||||
RUN npx puppeteer browsers install chrome@138.0.7204.94 --install-deps
|
||||
|
||||
RUN chmod -R 777 /node_dependencies
|
||||
|
||||
# Copy nginx configuration
|
||||
|
|
|
|||
20
README.md
|
|
@ -165,11 +165,19 @@ Content-Type: `application/json`
|
|||
|
||||
| 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) |
|
||||
| content | string | Yes | The content for generating the presentation |
|
||||
| slides_markdown | string[] \| null | No | The markdown for the slides |
|
||||
| instructions | string \| null | No | The instruction for generating the presentation |
|
||||
| tone | string | No | The tone to use for the text (default: "default"). Available options: "default", "casual", "professional", "funny", "educational", "sales_pitch" |
|
||||
| verbosity | string | No | How verbose the text should be (default: "standard"). Available options: "concise", "standard", "text-heavy" |
|
||||
| web_search | boolean | No | Whether to enable web search (default: false) |
|
||||
| n_slides | integer | No | Number of slides to generate (default: 8) |
|
||||
| language | string | No | Language for the presentation (default: "English") |
|
||||
| template | string | No | Presentation template (default: "general"). Available options: "classic", "general", "modern", "professional" + Custom templates |
|
||||
| export_as | string | No | Export format ("pptx" or "pdf", default: "pptx") |
|
||||
| template | string | No | Template to use for the presentation (default: "general") |
|
||||
| include_table_of_contents | boolean | No | Whether to include a table of contents (default: false) |
|
||||
| include_title_slide | boolean | No | Whether to include a title slide (default: true) |
|
||||
| files | string[] \| null | No | Files to use for the presentation. Use /api/v1/ppt/files/upload to upload files |
|
||||
| export_as | string | No | Export format (default: "pptx"). Available options: "pptx", "pdf" |
|
||||
|
||||
#### Response
|
||||
|
||||
|
|
@ -187,7 +195,7 @@ Content-Type: `application/json`
|
|||
curl -X POST http://localhost:5000/api/v1/ppt/presentation/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"prompt": "Introduction to Machine Learning",
|
||||
"content": "Introduction to Machine Learning",
|
||||
"n_slides": 5,
|
||||
"language": "English",
|
||||
"template": "general",
|
||||
|
|
@ -200,7 +208,7 @@ curl -X POST http://localhost:5000/api/v1/ppt/presentation/generate \
|
|||
```json
|
||||
{
|
||||
"presentation_id": "d3000f96-096c-4768-b67b-e99aed029b57",
|
||||
"path": "/static/user_data/d3000f96-096c-4768-b67b-e99aed029b57/Introduction_to_Machine_Learning.pptx",
|
||||
"path": "/app_data/d3000f96-096c-4768-b67b-e99aed029b57/Introduction_to_Machine_Learning.pptx",
|
||||
"edit_path": "/presentation?id=d3000f96-096c-4768-b67b-e99aed029b57"
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ events {
|
|||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types; # Required for SVG mime type
|
||||
default_type application/octet-stream; # Required for SVG mime type
|
||||
client_max_body_size 100M;
|
||||
|
||||
server {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import asyncio
|
||||
import json
|
||||
import math
|
||||
import traceback
|
||||
import uuid
|
||||
import dirtyjson
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -64,7 +66,8 @@ async def stream_outlines(
|
|||
presentation.tone,
|
||||
presentation.verbosity,
|
||||
presentation.instructions,
|
||||
True,
|
||||
presentation.include_title_slide,
|
||||
presentation.web_search,
|
||||
):
|
||||
# Give control to the event loop
|
||||
await asyncio.sleep(0)
|
||||
|
|
@ -81,8 +84,11 @@ async def stream_outlines(
|
|||
presentation_outlines_text += chunk
|
||||
|
||||
try:
|
||||
presentation_outlines_json = json.loads(presentation_outlines_text)
|
||||
presentation_outlines_json = dict(
|
||||
dirtyjson.loads(presentation_outlines_text)
|
||||
)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Failed to generate presentation outlines. Please try again.",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ from typing import List, Optional
|
|||
from fastapi import APIRouter, UploadFile, File, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
from services.documents_loader import DocumentsLoader
|
||||
from utils.asset_directory_utils import get_images_directory
|
||||
import uuid
|
||||
from constants.documents import PDF_MIME_TYPES
|
||||
|
|
@ -31,28 +32,32 @@ async def process_pdf_slides(
|
|||
):
|
||||
"""
|
||||
Process a PDF file to extract slide screenshots.
|
||||
|
||||
|
||||
This endpoint:
|
||||
1. Validates the uploaded PDF file
|
||||
2. Uses ImageMagick to convert PDF pages to PNG images
|
||||
3. Returns screenshot URLs for each slide/page
|
||||
|
||||
|
||||
Note: Font installation is not needed since PDFs already have fonts embedded.
|
||||
"""
|
||||
|
||||
|
||||
# Validate PDF file
|
||||
if pdf_file.content_type not in PDF_MIME_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid file type. Expected PDF file, got {pdf_file.content_type}"
|
||||
detail=f"Invalid file type. Expected PDF file, got {pdf_file.content_type}",
|
||||
)
|
||||
# Enforce 100MB size limit
|
||||
if hasattr(pdf_file, "size") and pdf_file.size and pdf_file.size > (100 * 1024 * 1024):
|
||||
if (
|
||||
hasattr(pdf_file, "size")
|
||||
and pdf_file.size
|
||||
and pdf_file.size > (100 * 1024 * 1024)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="PDF file exceeded max upload size of 100 MB",
|
||||
)
|
||||
|
||||
|
||||
# Create temporary directory for processing
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
try:
|
||||
|
|
@ -61,120 +66,51 @@ async def process_pdf_slides(
|
|||
with open(pdf_path, "wb") as f:
|
||||
pdf_content = await pdf_file.read()
|
||||
f.write(pdf_content)
|
||||
|
||||
|
||||
# Generate screenshots from PDF using ImageMagick
|
||||
screenshot_paths = await _generate_pdf_screenshots(pdf_path, temp_dir)
|
||||
screenshot_paths = await DocumentsLoader.get_page_images_from_pdf_async(
|
||||
pdf_path, temp_dir
|
||||
)
|
||||
print(f"Generated {len(screenshot_paths)} PDF screenshots")
|
||||
|
||||
|
||||
# Move screenshots to images directory and generate URLs
|
||||
images_dir = get_images_directory()
|
||||
presentation_id = uuid.uuid4()
|
||||
presentation_images_dir = os.path.join(images_dir, str(presentation_id))
|
||||
os.makedirs(presentation_images_dir, exist_ok=True)
|
||||
|
||||
|
||||
slides_data = []
|
||||
|
||||
|
||||
for i, screenshot_path in enumerate(screenshot_paths, 1):
|
||||
# Move screenshot to permanent location
|
||||
screenshot_filename = f"slide_{i}.png"
|
||||
permanent_screenshot_path = os.path.join(presentation_images_dir, screenshot_filename)
|
||||
|
||||
if os.path.exists(screenshot_path) and os.path.getsize(screenshot_path) > 0:
|
||||
permanent_screenshot_path = os.path.join(
|
||||
presentation_images_dir, screenshot_filename
|
||||
)
|
||||
|
||||
if (
|
||||
os.path.exists(screenshot_path)
|
||||
and os.path.getsize(screenshot_path) > 0
|
||||
):
|
||||
# Use shutil.copy2 instead of os.rename to handle cross-device moves
|
||||
shutil.copy2(screenshot_path, permanent_screenshot_path)
|
||||
screenshot_url = f"/app_data/images/{presentation_id}/{screenshot_filename}"
|
||||
screenshot_url = (
|
||||
f"/app_data/images/{presentation_id}/{screenshot_filename}"
|
||||
)
|
||||
else:
|
||||
# Fallback if screenshot generation failed or file is empty placeholder
|
||||
screenshot_url = "/static/images/placeholder.jpg"
|
||||
|
||||
slides_data.append(PdfSlideData(
|
||||
slide_number=i,
|
||||
screenshot_url=screenshot_url
|
||||
))
|
||||
|
||||
|
||||
slides_data.append(
|
||||
PdfSlideData(slide_number=i, screenshot_url=screenshot_url)
|
||||
)
|
||||
|
||||
return PdfSlidesResponse(
|
||||
success=True,
|
||||
slides=slides_data,
|
||||
total_slides=len(slides_data)
|
||||
success=True, slides=slides_data, total_slides=len(slides_data)
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error processing PDF slides: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Failed to process PDF: {str(e)}"
|
||||
status_code=500, detail=f"Failed to process PDF: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
async def _generate_pdf_screenshots(pdf_path: str, temp_dir: str) -> List[str]:
|
||||
"""Generate PNG screenshots of PDF pages using ImageMagick (same approach as PPTX endpoint)."""
|
||||
screenshots_dir = os.path.join(temp_dir, "screenshots")
|
||||
os.makedirs(screenshots_dir, exist_ok=True)
|
||||
|
||||
try:
|
||||
# Convert PDF to individual PNG images using ImageMagick
|
||||
print("Starting ImageMagick PNG conversion...")
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"convert",
|
||||
"-density", "150", # Same DPI as PPTX endpoint
|
||||
pdf_path,
|
||||
os.path.join(screenshots_dir, "slide_%03d.png")
|
||||
], check=True, capture_output=True, text=True, timeout=500)
|
||||
|
||||
print(f"ImageMagick conversion output: {result.stdout}")
|
||||
if result.stderr:
|
||||
print(f"ImageMagick conversion warnings: {result.stderr}")
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Exception("ImageMagick PNG conversion timed out after 500 seconds")
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr if e.stderr else str(e)
|
||||
raise Exception(f"ImageMagick PNG conversion failed: {error_msg}")
|
||||
|
||||
# Find generated PNG files (ImageMagick creates slide_000.png, slide_001.png, etc.)
|
||||
print("Checking for generated PNG files...")
|
||||
png_files = sorted([f for f in os.listdir(screenshots_dir) if f.startswith("slide_") and f.endswith('.png')])
|
||||
print(f"Generated PNG files: {png_files}")
|
||||
|
||||
if not png_files:
|
||||
raise Exception("ImageMagick failed to generate any PNG files")
|
||||
|
||||
# Determine page count from generated files
|
||||
page_count = len(png_files)
|
||||
print(f"Determined {page_count} pages from ImageMagick output")
|
||||
|
||||
# Rename files from slide_000.png format to slide_1.png format expected by the API
|
||||
# (Same renaming logic as PPTX endpoint)
|
||||
print("Renaming PNG files to expected format...")
|
||||
screenshot_paths = []
|
||||
for i in range(page_count):
|
||||
# ImageMagick generates slide_000.png, slide_001.png, etc.
|
||||
source_file = f"slide_{i:03d}.png"
|
||||
source_path = os.path.join(screenshots_dir, source_file)
|
||||
|
||||
# We need slide_1.png, slide_2.png, etc.
|
||||
target_file = f"slide_{i+1}.png"
|
||||
target_path = os.path.join(screenshots_dir, target_file)
|
||||
|
||||
if os.path.exists(source_path):
|
||||
# Rename to expected format
|
||||
shutil.move(source_path, target_path)
|
||||
screenshot_paths.append(target_path)
|
||||
print(f"✓ Renamed {source_file} to {target_file}")
|
||||
else:
|
||||
print(f"⚠ Warning: Expected file {source_file} not found, creating placeholder")
|
||||
# Create empty placeholder
|
||||
with open(target_path, 'w') as f:
|
||||
f.write("")
|
||||
screenshot_paths.append(target_path)
|
||||
|
||||
print(f"Successfully generated {len(screenshot_paths)} PDF page screenshots")
|
||||
return screenshot_paths
|
||||
|
||||
except Exception as e:
|
||||
# Re-raise the specific exceptions we've already handled
|
||||
if "timed out" in str(e) or "failed:" in str(e):
|
||||
raise
|
||||
# Handle any other unexpected exceptions
|
||||
raise Exception(f"PDF screenshot generation failed: {str(e)}")
|
||||
|
|
@ -12,6 +12,7 @@ import asyncio
|
|||
import xml.etree.ElementTree as ET
|
||||
import re
|
||||
|
||||
from services.documents_loader import DocumentsLoader
|
||||
from utils.asset_directory_utils import get_images_directory
|
||||
import uuid
|
||||
from constants.documents import POWERPOINT_TYPES
|
||||
|
|
@ -28,7 +29,9 @@ class SlideData(BaseModel):
|
|||
|
||||
|
||||
class FontAnalysisResult(BaseModel):
|
||||
internally_supported_fonts: List[Dict[str, str]] # [{"name": "Open Sans", "google_fonts_url": "..."}]
|
||||
internally_supported_fonts: List[
|
||||
Dict[str, str]
|
||||
] # [{"name": "Open Sans", "google_fonts_url": "..."}]
|
||||
not_supported_fonts: List[str] # ["Custom Font Name"]
|
||||
|
||||
|
||||
|
|
@ -38,30 +41,62 @@ class PptxSlidesResponse(BaseModel):
|
|||
total_slides: int
|
||||
fonts: Optional[FontAnalysisResult] = None
|
||||
|
||||
|
||||
# NEW: Fonts-only router and response for PPTX
|
||||
class PptxFontsResponse(BaseModel):
|
||||
success: bool
|
||||
fonts: FontAnalysisResult
|
||||
|
||||
|
||||
PPTX_FONTS_ROUTER = APIRouter(prefix="/pptx-fonts", tags=["PPTX Fonts"])
|
||||
|
||||
# NEW: Normalize font family names by removing style/weight/stretch descriptors and splitting camel case
|
||||
_STYLE_TOKENS = {
|
||||
# styles
|
||||
"italic", "italics", "ital", "oblique", "roman",
|
||||
"italic",
|
||||
"italics",
|
||||
"ital",
|
||||
"oblique",
|
||||
"roman",
|
||||
# combined style shortcuts
|
||||
"bolditalic", "bolditalics",
|
||||
"bolditalic",
|
||||
"bolditalics",
|
||||
# weights
|
||||
"thin", "hairline", "extralight", "ultralight", "light", "demilight", "semilight", "book",
|
||||
"regular", "normal", "medium", "semibold", "demibold", "bold", "extrabold", "ultrabold",
|
||||
"black", "extrablack", "ultrablack", "heavy",
|
||||
"thin",
|
||||
"hairline",
|
||||
"extralight",
|
||||
"ultralight",
|
||||
"light",
|
||||
"demilight",
|
||||
"semilight",
|
||||
"book",
|
||||
"regular",
|
||||
"normal",
|
||||
"medium",
|
||||
"semibold",
|
||||
"demibold",
|
||||
"bold",
|
||||
"extrabold",
|
||||
"ultrabold",
|
||||
"black",
|
||||
"extrablack",
|
||||
"ultrablack",
|
||||
"heavy",
|
||||
# width/stretch
|
||||
"narrow", "condensed", "semicondensed", "extracondensed", "ultracondensed",
|
||||
"expanded", "semiexpanded", "extraexpanded", "ultraexpanded",
|
||||
"narrow",
|
||||
"condensed",
|
||||
"semicondensed",
|
||||
"extracondensed",
|
||||
"ultracondensed",
|
||||
"expanded",
|
||||
"semiexpanded",
|
||||
"extraexpanded",
|
||||
"ultraexpanded",
|
||||
}
|
||||
# Modifiers commonly used with style tokens
|
||||
_STYLE_MODIFIERS = {"semi", "demi", "extra", "ultra"}
|
||||
|
||||
|
||||
def _insert_spaces_in_camel_case(value: str) -> str:
|
||||
# Insert space before capital letters preceded by lowercase or digits (e.g., MontserratBold -> Montserrat Bold)
|
||||
value = re.sub(r"(?<=[a-z0-9])([A-Z])", r" \1", value)
|
||||
|
|
@ -69,6 +104,7 @@ def _insert_spaces_in_camel_case(value: str) -> str:
|
|||
value = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1 \2", value)
|
||||
return value
|
||||
|
||||
|
||||
def normalize_font_family_name(raw_name: str) -> str:
|
||||
if not raw_name:
|
||||
return raw_name
|
||||
|
|
@ -111,69 +147,69 @@ def normalize_font_family_name(raw_name: str) -> str:
|
|||
def extract_fonts_from_oxml(xml_content: str) -> List[str]:
|
||||
"""
|
||||
Extract font names from OXML content.
|
||||
|
||||
|
||||
Args:
|
||||
xml_content: OXML content as string
|
||||
|
||||
|
||||
Returns:
|
||||
List of unique font names found in the OXML
|
||||
"""
|
||||
fonts = set()
|
||||
|
||||
|
||||
try:
|
||||
# Parse the XML content
|
||||
root = ET.fromstring(xml_content)
|
||||
|
||||
|
||||
# Define namespaces commonly used in OXML
|
||||
namespaces = {
|
||||
'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
|
||||
'p': 'http://schemas.openxmlformats.org/presentationml/2006/main',
|
||||
'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
|
||||
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
|
||||
"p": "http://schemas.openxmlformats.org/presentationml/2006/main",
|
||||
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
||||
}
|
||||
|
||||
|
||||
# Search for font references in various OXML elements
|
||||
# Look for latin fonts
|
||||
for font_elem in root.findall('.//a:latin', namespaces):
|
||||
if 'typeface' in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib['typeface'])
|
||||
|
||||
for font_elem in root.findall(".//a:latin", namespaces):
|
||||
if "typeface" in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib["typeface"])
|
||||
|
||||
# Look for east asian fonts
|
||||
for font_elem in root.findall('.//a:ea', namespaces):
|
||||
if 'typeface' in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib['typeface'])
|
||||
|
||||
for font_elem in root.findall(".//a:ea", namespaces):
|
||||
if "typeface" in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib["typeface"])
|
||||
|
||||
# Look for complex script fonts
|
||||
for font_elem in root.findall('.//a:cs', namespaces):
|
||||
if 'typeface' in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib['typeface'])
|
||||
|
||||
for font_elem in root.findall(".//a:cs", namespaces):
|
||||
if "typeface" in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib["typeface"])
|
||||
|
||||
# Look for font references in theme elements
|
||||
for font_elem in root.findall('.//a:font', namespaces):
|
||||
if 'typeface' in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib['typeface'])
|
||||
|
||||
for font_elem in root.findall(".//a:font", namespaces):
|
||||
if "typeface" in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib["typeface"])
|
||||
|
||||
# Look for rPr (run properties) font references
|
||||
for rpr_elem in root.findall('.//a:rPr', namespaces):
|
||||
for font_elem in rpr_elem.findall('.//a:latin', namespaces):
|
||||
if 'typeface' in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib['typeface'])
|
||||
|
||||
for rpr_elem in root.findall(".//a:rPr", namespaces):
|
||||
for font_elem in rpr_elem.findall(".//a:latin", namespaces):
|
||||
if "typeface" in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib["typeface"])
|
||||
|
||||
# Also search without namespace prefix for compatibility
|
||||
for font_elem in root.findall('.//latin'):
|
||||
if 'typeface' in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib['typeface'])
|
||||
|
||||
for font_elem in root.findall(".//latin"):
|
||||
if "typeface" in font_elem.attrib:
|
||||
fonts.add(font_elem.attrib["typeface"])
|
||||
|
||||
# Regex fallback for fonts that might be missed
|
||||
font_pattern = r'typeface="([^"]+)"'
|
||||
regex_fonts = re.findall(font_pattern, xml_content)
|
||||
fonts.update(regex_fonts)
|
||||
|
||||
|
||||
# Filter out system fonts and empty values
|
||||
system_fonts = {'+mn-lt', '+mj-lt', '+mn-ea', '+mj-ea', '+mn-cs', '+mj-cs', ''}
|
||||
system_fonts = {"+mn-lt", "+mj-lt", "+mn-ea", "+mj-ea", "+mn-cs", "+mj-cs", ""}
|
||||
fonts = {font for font in fonts if font not in system_fonts and font.strip()}
|
||||
|
||||
|
||||
return list(fonts)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error extracting fonts from OXML: {e}")
|
||||
return []
|
||||
|
|
@ -182,21 +218,23 @@ def extract_fonts_from_oxml(xml_content: str) -> List[str]:
|
|||
async def check_google_font_availability(font_name: str) -> bool:
|
||||
"""
|
||||
Check if a font is available in Google Fonts.
|
||||
|
||||
|
||||
Args:
|
||||
font_name: Name of the font to check
|
||||
|
||||
|
||||
Returns:
|
||||
True if font is available in Google Fonts, False otherwise
|
||||
"""
|
||||
try:
|
||||
formatted_name = font_name.replace(' ', '+')
|
||||
formatted_name = font_name.replace(" ", "+")
|
||||
url = f"https://fonts.googleapis.com/css2?family={formatted_name}&display=swap"
|
||||
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.head(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
||||
async with session.head(
|
||||
url, timeout=aiohttp.ClientTimeout(total=10)
|
||||
) as response:
|
||||
return response.status == 200
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error checking Google Font availability for {font_name}: {e}")
|
||||
return False
|
||||
|
|
@ -205,10 +243,10 @@ async def check_google_font_availability(font_name: str) -> bool:
|
|||
async def analyze_fonts_in_all_slides(slide_xmls: List[str]) -> FontAnalysisResult:
|
||||
"""
|
||||
Analyze fonts across all slides and determine Google Fonts availability.
|
||||
|
||||
|
||||
Args:
|
||||
slide_xmls: List of OXML content strings from all slides
|
||||
|
||||
|
||||
Returns:
|
||||
FontAnalysisResult with supported and unsupported fonts
|
||||
"""
|
||||
|
|
@ -222,45 +260,40 @@ async def analyze_fonts_in_all_slides(slide_xmls: List[str]) -> FontAnalysisResu
|
|||
normalized_fonts = {normalize_font_family_name(f) for f in raw_fonts}
|
||||
# Remove empties if any
|
||||
normalized_fonts = {f for f in normalized_fonts if f}
|
||||
|
||||
|
||||
if not normalized_fonts:
|
||||
return FontAnalysisResult(
|
||||
internally_supported_fonts=[],
|
||||
not_supported_fonts=[]
|
||||
)
|
||||
|
||||
return FontAnalysisResult(internally_supported_fonts=[], not_supported_fonts=[])
|
||||
|
||||
# Check each normalized font's availability in Google Fonts concurrently
|
||||
tasks = [check_google_font_availability(font) for font in normalized_fonts]
|
||||
results = await asyncio.gather(*tasks)
|
||||
|
||||
|
||||
internally_supported_fonts = []
|
||||
not_supported_fonts = []
|
||||
|
||||
|
||||
for font, is_available in zip(normalized_fonts, results):
|
||||
if is_available:
|
||||
formatted_name = font.replace(' ', '+')
|
||||
formatted_name = font.replace(" ", "+")
|
||||
google_fonts_url = f"https://fonts.googleapis.com/css2?family={formatted_name}&display=swap"
|
||||
internally_supported_fonts.append({
|
||||
"name": font,
|
||||
"google_fonts_url": google_fonts_url
|
||||
})
|
||||
internally_supported_fonts.append(
|
||||
{"name": font, "google_fonts_url": google_fonts_url}
|
||||
)
|
||||
else:
|
||||
not_supported_fonts.append(font)
|
||||
|
||||
|
||||
return FontAnalysisResult(
|
||||
internally_supported_fonts=internally_supported_fonts,
|
||||
not_supported_fonts=[]
|
||||
internally_supported_fonts=internally_supported_fonts, not_supported_fonts=[]
|
||||
)
|
||||
|
||||
|
||||
@PPTX_SLIDES_ROUTER.post("/process", response_model=PptxSlidesResponse)
|
||||
async def process_pptx_slides(
|
||||
pptx_file: UploadFile = File(..., description="PPTX file to process"),
|
||||
fonts: Optional[List[UploadFile]] = File(None, description="Optional font files")
|
||||
fonts: Optional[List[UploadFile]] = File(None, description="Optional font files"),
|
||||
):
|
||||
"""
|
||||
Process a PPTX file to extract slide screenshots and XML content.
|
||||
|
||||
|
||||
This endpoint:
|
||||
1. Validates the uploaded PPTX file
|
||||
2. Installs any provided font files
|
||||
|
|
@ -268,20 +301,24 @@ async def process_pptx_slides(
|
|||
4. Uses LibreOffice to generate slide screenshots
|
||||
5. Returns both screenshot URLs and XML content for each slide
|
||||
"""
|
||||
|
||||
|
||||
# Validate PPTX file
|
||||
if pptx_file.content_type not in POWERPOINT_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}"
|
||||
detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}",
|
||||
)
|
||||
# Enforce 100MB size limit
|
||||
if hasattr(pptx_file, "size") and pptx_file.size and pptx_file.size > (100 * 1024 * 1024):
|
||||
if (
|
||||
hasattr(pptx_file, "size")
|
||||
and pptx_file.size
|
||||
and pptx_file.size > (100 * 1024 * 1024)
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="PPTX file exceeded max upload size of 100 MB",
|
||||
)
|
||||
|
||||
|
||||
# Create temporary directory for processing
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
if True:
|
||||
|
|
@ -290,61 +327,82 @@ async def process_pptx_slides(
|
|||
with open(pptx_path, "wb") as f:
|
||||
pptx_content = await pptx_file.read()
|
||||
f.write(pptx_content)
|
||||
|
||||
|
||||
# Install fonts if provided
|
||||
if fonts:
|
||||
await _install_fonts(fonts, temp_dir)
|
||||
|
||||
|
||||
# Extract slide XMLs from PPTX
|
||||
slide_xmls = _extract_slide_xmls(pptx_path, temp_dir)
|
||||
|
||||
|
||||
# Convert PPTX to PDF
|
||||
pdf_path = await _convert_pptx_to_pdf(pptx_path, temp_dir)
|
||||
|
||||
# Generate screenshots using LibreOffice
|
||||
screenshot_paths = await _generate_screenshots(pptx_path, temp_dir)
|
||||
screenshot_paths = await DocumentsLoader.get_page_images_from_pdf_async(
|
||||
pdf_path, temp_dir
|
||||
)
|
||||
print(f"Screenshot paths: {screenshot_paths}")
|
||||
|
||||
|
||||
# Analyze fonts across all slides
|
||||
font_analysis = await analyze_fonts_in_all_slides(slide_xmls)
|
||||
print(f"Font analysis completed: {len(font_analysis.internally_supported_fonts)} supported, {len(font_analysis.not_supported_fonts)} not supported")
|
||||
|
||||
print(
|
||||
f"Font analysis completed: {len(font_analysis.internally_supported_fonts)} supported, {len(font_analysis.not_supported_fonts)} not supported"
|
||||
)
|
||||
|
||||
# Move screenshots to images directory and generate URLs
|
||||
images_dir = get_images_directory()
|
||||
presentation_id = uuid.uuid4()
|
||||
presentation_images_dir = os.path.join(images_dir, str(presentation_id))
|
||||
os.makedirs(presentation_images_dir, exist_ok=True)
|
||||
|
||||
|
||||
slides_data = []
|
||||
|
||||
for i, (xml_content, screenshot_path) in enumerate(zip(slide_xmls, screenshot_paths), 1):
|
||||
|
||||
for i, (xml_content, screenshot_path) in enumerate(
|
||||
zip(slide_xmls, screenshot_paths), 1
|
||||
):
|
||||
# Move screenshot to permanent location
|
||||
screenshot_filename = f"slide_{i}.png"
|
||||
permanent_screenshot_path = os.path.join(presentation_images_dir, screenshot_filename)
|
||||
|
||||
if os.path.exists(screenshot_path) and os.path.getsize(screenshot_path) > 0:
|
||||
permanent_screenshot_path = os.path.join(
|
||||
presentation_images_dir, screenshot_filename
|
||||
)
|
||||
|
||||
if (
|
||||
os.path.exists(screenshot_path)
|
||||
and os.path.getsize(screenshot_path) > 0
|
||||
):
|
||||
# Use shutil.copy2 instead of os.rename to handle cross-device moves
|
||||
shutil.copy2(screenshot_path, permanent_screenshot_path)
|
||||
screenshot_url = f"/app_data/images/{presentation_id}/{screenshot_filename}"
|
||||
screenshot_url = (
|
||||
f"/app_data/images/{presentation_id}/{screenshot_filename}"
|
||||
)
|
||||
else:
|
||||
# Fallback if screenshot generation failed or file is empty placeholder
|
||||
screenshot_url = "/static/images/placeholder.jpg"
|
||||
|
||||
|
||||
# Compute normalized fonts for this slide
|
||||
raw_slide_fonts = extract_fonts_from_oxml(xml_content)
|
||||
normalized_fonts = sorted({normalize_font_family_name(f) for f in raw_slide_fonts if f})
|
||||
|
||||
slides_data.append(SlideData(
|
||||
slide_number=i,
|
||||
screenshot_url=screenshot_url,
|
||||
xml_content=xml_content,
|
||||
normalized_fonts=normalized_fonts
|
||||
))
|
||||
|
||||
normalized_fonts = sorted(
|
||||
{normalize_font_family_name(f) for f in raw_slide_fonts if f}
|
||||
)
|
||||
|
||||
slides_data.append(
|
||||
SlideData(
|
||||
slide_number=i,
|
||||
screenshot_url=screenshot_url,
|
||||
xml_content=xml_content,
|
||||
normalized_fonts=normalized_fonts,
|
||||
)
|
||||
)
|
||||
|
||||
return PptxSlidesResponse(
|
||||
success=True,
|
||||
slides=slides_data,
|
||||
total_slides=len(slides_data),
|
||||
fonts=font_analysis
|
||||
fonts=font_analysis,
|
||||
)
|
||||
|
||||
|
||||
# NEW: Fonts-only endpoint leveraging the same font extraction/analysis
|
||||
@PPTX_FONTS_ROUTER.post("/process", response_model=PptxFontsResponse)
|
||||
async def process_pptx_fonts(
|
||||
|
|
@ -359,7 +417,7 @@ async def process_pptx_fonts(
|
|||
if pptx_file.content_type not in POWERPOINT_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}"
|
||||
detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}",
|
||||
)
|
||||
|
||||
# Create temporary directory for processing
|
||||
|
|
@ -381,6 +439,7 @@ async def process_pptx_fonts(
|
|||
fonts=font_analysis,
|
||||
)
|
||||
|
||||
|
||||
def _create_font_alias_config(raw_fonts: List[str]) -> str:
|
||||
"""Create a temporary fontconfig configuration that aliases variant family names to normalized root families.
|
||||
Returns the path to the config file.
|
||||
|
|
@ -395,13 +454,16 @@ def _create_font_alias_config(raw_fonts: List[str]) -> str:
|
|||
fd, fonts_conf_path = tempfile.mkstemp(prefix="fonts_alias_", suffix=".conf")
|
||||
os.close(fd)
|
||||
with open(fonts_conf_path, "w", encoding="utf-8") as cfg:
|
||||
cfg.write("""<?xml version='1.0'?>
|
||||
cfg.write(
|
||||
"""<?xml version='1.0'?>
|
||||
<!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
|
||||
<fontconfig>
|
||||
<include>/etc/fonts/fonts.conf</include>
|
||||
""")
|
||||
"""
|
||||
)
|
||||
for src, dst in mappings.items():
|
||||
cfg.write(f"""
|
||||
cfg.write(
|
||||
f"""
|
||||
<match target="pattern">
|
||||
<test name="family" compare="eq">
|
||||
<string>{src}</string>
|
||||
|
|
@ -410,30 +472,34 @@ def _create_font_alias_config(raw_fonts: List[str]) -> str:
|
|||
<string>{dst}</string>
|
||||
</edit>
|
||||
</match>
|
||||
""")
|
||||
"""
|
||||
)
|
||||
cfg.write("\n</fontconfig>\n")
|
||||
return fonts_conf_path
|
||||
|
||||
|
||||
async def _install_fonts(fonts: List[UploadFile], temp_dir: str) -> None:
|
||||
"""Install provided font files to the system."""
|
||||
fonts_dir = os.path.join(temp_dir, "fonts")
|
||||
os.makedirs(fonts_dir, exist_ok=True)
|
||||
|
||||
|
||||
for font_file in fonts:
|
||||
# Save font file
|
||||
font_path = os.path.join(fonts_dir, font_file.filename)
|
||||
with open(font_path, "wb") as f:
|
||||
font_content = await font_file.read()
|
||||
f.write(font_content)
|
||||
|
||||
|
||||
# Install font (copy to system fonts directory)
|
||||
try:
|
||||
subprocess.run([
|
||||
"cp", font_path, "/usr/share/fonts/truetype/"
|
||||
], check=True, capture_output=True)
|
||||
subprocess.run(
|
||||
["cp", font_path, "/usr/share/fonts/truetype/"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Warning: Failed to install font {font_file.filename}: {e}")
|
||||
|
||||
|
||||
# Refresh font cache
|
||||
try:
|
||||
subprocess.run(["fc-cache", "-f", "-v"], check=True, capture_output=True)
|
||||
|
|
@ -445,44 +511,48 @@ def _extract_slide_xmls(pptx_path: str, temp_dir: str) -> List[str]:
|
|||
"""Extract slide XML content from PPTX file."""
|
||||
slide_xmls = []
|
||||
extract_dir = os.path.join(temp_dir, "pptx_extract")
|
||||
|
||||
|
||||
try:
|
||||
# Unzip PPTX file
|
||||
with zipfile.ZipFile(pptx_path, 'r') as zip_ref:
|
||||
with zipfile.ZipFile(pptx_path, "r") as zip_ref:
|
||||
zip_ref.extractall(extract_dir)
|
||||
|
||||
|
||||
# Look for slides in ppt/slides/ directory
|
||||
slides_dir = os.path.join(extract_dir, "ppt", "slides")
|
||||
|
||||
|
||||
if not os.path.exists(slides_dir):
|
||||
raise Exception("No slides directory found in PPTX file")
|
||||
|
||||
|
||||
# Get all slide XML files and sort them numerically
|
||||
slide_files = [f for f in os.listdir(slides_dir) if f.startswith("slide") and f.endswith(".xml")]
|
||||
slide_files = [
|
||||
f
|
||||
for f in os.listdir(slides_dir)
|
||||
if f.startswith("slide") and f.endswith(".xml")
|
||||
]
|
||||
slide_files.sort(key=lambda x: int(x.replace("slide", "").replace(".xml", "")))
|
||||
|
||||
|
||||
# Read XML content from each slide
|
||||
for slide_file in slide_files:
|
||||
slide_path = os.path.join(slides_dir, slide_file)
|
||||
with open(slide_path, 'r', encoding='utf-8') as f:
|
||||
with open(slide_path, "r", encoding="utf-8") as f:
|
||||
slide_xmls.append(f.read())
|
||||
|
||||
|
||||
return slide_xmls
|
||||
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Failed to extract slide XMLs: {str(e)}")
|
||||
|
||||
|
||||
async def _generate_screenshots(pptx_path: str, temp_dir: str) -> List[str]:
|
||||
async def _convert_pptx_to_pdf(pptx_path: str, temp_dir: str) -> str:
|
||||
"""Generate PNG screenshots of PPTX slides using LibreOffice + ImageMagick."""
|
||||
screenshots_dir = os.path.join(temp_dir, "screenshots")
|
||||
os.makedirs(screenshots_dir, exist_ok=True)
|
||||
|
||||
|
||||
try:
|
||||
# First, get the number of slides by extracting XMLs
|
||||
slide_xmls = _extract_slide_xmls(pptx_path, temp_dir)
|
||||
slide_count = len(slide_xmls)
|
||||
|
||||
|
||||
# Build font alias config to force variant families to resolve to normalized root families
|
||||
raw_fonts: List[str] = []
|
||||
for xml in slide_xmls:
|
||||
|
|
@ -491,23 +561,32 @@ async def _generate_screenshots(pptx_path: str, temp_dir: str) -> List[str]:
|
|||
fonts_conf_path = _create_font_alias_config(raw_fonts)
|
||||
env = os.environ.copy()
|
||||
env["FONTCONFIG_FILE"] = fonts_conf_path
|
||||
|
||||
|
||||
print(f"Found {slide_count} slides in presentation")
|
||||
|
||||
|
||||
# Step 1: Convert PPTX to PDF using LibreOffice
|
||||
print("Starting LibreOffice PDF conversion...")
|
||||
pdf_filename = "temp_presentation.pdf"
|
||||
pdf_path = os.path.join(screenshots_dir, pdf_filename)
|
||||
|
||||
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"libreoffice",
|
||||
"--headless",
|
||||
"--convert-to", "pdf",
|
||||
"--outdir", screenshots_dir,
|
||||
pptx_path
|
||||
], check=True, capture_output=True, text=True, timeout=500, env=env)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"libreoffice",
|
||||
"--headless",
|
||||
"--convert-to",
|
||||
"pdf",
|
||||
"--outdir",
|
||||
screenshots_dir,
|
||||
pptx_path,
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=500,
|
||||
env=env,
|
||||
)
|
||||
|
||||
print(f"LibreOffice PDF conversion output: {result.stdout}")
|
||||
if result.stderr:
|
||||
print(f"LibreOffice PDF conversion warnings: {result.stderr}")
|
||||
|
|
@ -516,74 +595,19 @@ async def _generate_screenshots(pptx_path: str, temp_dir: str) -> List[str]:
|
|||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr if e.stderr else str(e)
|
||||
raise Exception(f"LibreOffice PDF conversion failed: {error_msg}")
|
||||
|
||||
|
||||
# Find the generated PDF file (LibreOffice uses original filename)
|
||||
pdf_files = [f for f in os.listdir(screenshots_dir) if f.endswith('.pdf')]
|
||||
pdf_files = [f for f in os.listdir(screenshots_dir) if f.endswith(".pdf")]
|
||||
if not pdf_files:
|
||||
raise Exception("LibreOffice failed to generate PDF file")
|
||||
|
||||
|
||||
actual_pdf_path = os.path.join(screenshots_dir, pdf_files[0])
|
||||
print(f"Generated PDF: {actual_pdf_path}")
|
||||
|
||||
# Step 2: Convert PDF to individual PNG images using ImageMagick
|
||||
print("Starting ImageMagick PNG conversion...")
|
||||
try:
|
||||
result = subprocess.run([
|
||||
"convert",
|
||||
"-density", "150",
|
||||
actual_pdf_path,
|
||||
os.path.join(screenshots_dir, "slide_%03d.png")
|
||||
], check=True, capture_output=True, text=True, timeout=500, env=env)
|
||||
|
||||
print(f"ImageMagick conversion output: {result.stdout}")
|
||||
if result.stderr:
|
||||
print(f"ImageMagick conversion warnings: {result.stderr}")
|
||||
except subprocess.TimeoutExpired:
|
||||
raise Exception("ImageMagick PNG conversion timed out after 120 seconds")
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = e.stderr if e.stderr else str(e)
|
||||
raise Exception(f"ImageMagick PNG conversion failed: {error_msg}")
|
||||
|
||||
# Find generated PNG files (ImageMagick creates slide_000.png, slide_001.png, etc.)
|
||||
print("Checking for generated PNG files...")
|
||||
png_files = sorted([f for f in os.listdir(screenshots_dir) if f.startswith("slide_") and f.endswith('.png')])
|
||||
print(f"Generated PNG files: {png_files}")
|
||||
|
||||
if not png_files:
|
||||
raise Exception("ImageMagick failed to generate any PNG files")
|
||||
|
||||
# Rename files from slide_000.png format to slide_1.png format expected by the API
|
||||
print("Renaming PNG files to expected format...")
|
||||
screenshot_paths = []
|
||||
for i in range(slide_count):
|
||||
# ImageMagick generates slide_000.png, slide_001.png, etc.
|
||||
source_file = f"slide_{i:03d}.png"
|
||||
source_path = os.path.join(screenshots_dir, source_file)
|
||||
|
||||
# We need slide_1.png, slide_2.png, etc.
|
||||
target_file = f"slide_{i+1}.png"
|
||||
target_path = os.path.join(screenshots_dir, target_file)
|
||||
|
||||
if os.path.exists(source_path):
|
||||
# Rename to expected format
|
||||
shutil.move(source_path, target_path)
|
||||
screenshot_paths.append(target_path)
|
||||
print(f"✓ Renamed {source_file} to {target_file}")
|
||||
else:
|
||||
print(f"⚠ Warning: Expected file {source_file} not found, creating placeholder")
|
||||
# Create empty placeholder
|
||||
with open(target_path, 'w') as f:
|
||||
f.write("")
|
||||
screenshot_paths.append(target_path)
|
||||
|
||||
print(f"Successfully generated {len(screenshot_paths)} slide screenshots")
|
||||
return screenshot_paths
|
||||
|
||||
return actual_pdf_path
|
||||
|
||||
except Exception as e:
|
||||
# Re-raise the specific exceptions we've already handled
|
||||
if "timed out" in str(e) or "failed:" in str(e):
|
||||
raise
|
||||
# Handle any other unexpected exceptions
|
||||
raise Exception(f"Screenshot generation failed: {str(e)}")
|
||||
|
||||
|
||||
|
|
@ -4,6 +4,7 @@ import math
|
|||
import os
|
||||
import random
|
||||
from typing import Annotated, List, Literal, Optional
|
||||
import dirtyjson
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import delete
|
||||
|
|
@ -45,7 +46,10 @@ from utils.llm_calls.generate_presentation_structure import (
|
|||
from utils.llm_calls.generate_slide_content import (
|
||||
get_slide_content_from_type_and_outline,
|
||||
)
|
||||
from utils.ppt_utils import select_toc_or_list_slide_layout_index
|
||||
from utils.ppt_utils import (
|
||||
get_presentation_title_from_outlines,
|
||||
select_toc_or_list_slide_layout_index,
|
||||
)
|
||||
from utils.process_slides import (
|
||||
process_slide_add_placeholder_assets,
|
||||
process_slide_and_fetch_assets,
|
||||
|
|
@ -486,7 +490,9 @@ async def generate_presentation_api(
|
|||
presentation_outlines_text += chunk
|
||||
|
||||
try:
|
||||
presentation_outlines_json = json.loads(presentation_outlines_text)
|
||||
presentation_outlines_json = dict(
|
||||
dirtyjson.loads(presentation_outlines_text)
|
||||
)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
raise HTTPException(
|
||||
|
|
@ -575,6 +581,7 @@ async def generate_presentation_api(
|
|||
content=request.content,
|
||||
n_slides=request.n_slides,
|
||||
language=request.language,
|
||||
title=get_presentation_title_from_outlines(presentation_outlines),
|
||||
outlines=presentation_outlines.model_dump(),
|
||||
layout=layout_model.model_dump(),
|
||||
structure=presentation_structure.model_dump(),
|
||||
|
|
@ -676,7 +683,9 @@ async def edit_presentation_with_new_content(
|
|||
slides_to_delete = []
|
||||
for each_slide in slides:
|
||||
updated_content = None
|
||||
new_slide_data = list(filter(lambda x: x.index == each_slide.index, data.data))
|
||||
new_slide_data = list(
|
||||
filter(lambda x: x.index == each_slide.index, data.slides)
|
||||
)
|
||||
if new_slide_data:
|
||||
updated_content = deep_update(each_slide.content, new_slide_data[0].content)
|
||||
new_slides.append(
|
||||
|
|
@ -718,7 +727,9 @@ async def derive_presentation_from_existing_one(
|
|||
new_slides = []
|
||||
for each_slide in slides:
|
||||
updated_content = None
|
||||
new_slide_data = list(filter(lambda x: x.index == each_slide.index, data.data))
|
||||
new_slide_data = list(
|
||||
filter(lambda x: x.index == each_slide.index, data.slides)
|
||||
)
|
||||
if new_slide_data:
|
||||
updated_content = deep_update(each_slide.content, new_slide_data[0].content)
|
||||
new_slides.append(
|
||||
|
|
|
|||
|
|
@ -10,5 +10,5 @@ class SlideContentUpdate(BaseModel):
|
|||
|
||||
class EditPresentationRequest(BaseModel):
|
||||
presentation_id: uuid.UUID
|
||||
data: List[SlideContentUpdate]
|
||||
slides: List[SlideContentUpdate]
|
||||
export_as: Literal["pptx", "pdf"] = "pptx"
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ dependencies = [
|
|||
"anthropic>=0.60.0",
|
||||
"asyncpg>=0.30.0",
|
||||
"chromadb>=1.0.15",
|
||||
"dirtyjson>=1.0.8",
|
||||
"docling>=2.43.0",
|
||||
"fastapi[standard]>=0.116.1",
|
||||
"fastmcp>=2.11.0",
|
||||
|
|
|
|||
|
|
@ -95,13 +95,19 @@ class DocumentsLoader:
|
|||
def load_powerpoint(self, file_path: str) -> str:
|
||||
return self.docling_service.parse_to_markdown(file_path)
|
||||
|
||||
def get_page_images_from_pdf(self, file_path: str, temp_dir: str):
|
||||
@classmethod
|
||||
def get_page_images_from_pdf(cls, file_path: str, temp_dir: str) -> List[str]:
|
||||
with pdfplumber.open(file_path) as pdf:
|
||||
images = []
|
||||
for page in pdf.pages:
|
||||
img = page.to_image(resolution=300)
|
||||
img.save(os.path.join(temp_dir, f"page_{page.page_number}.png"))
|
||||
img = page.to_image(resolution=150)
|
||||
image_path = os.path.join(temp_dir, f"page_{page.page_number}.png")
|
||||
img.save(image_path)
|
||||
images.append(image_path)
|
||||
return images
|
||||
|
||||
async def get_page_images_from_pdf_async(self, file_path: str, temp_dir: str):
|
||||
@classmethod
|
||||
async def get_page_images_from_pdf_async(cls, file_path: str, temp_dir: str):
|
||||
return await asyncio.to_thread(
|
||||
self.get_page_images_from_pdf, file_path, temp_dir
|
||||
cls.get_page_images_from_pdf, file_path, temp_dir
|
||||
)
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class IconFinderService:
|
|||
query_texts=[query],
|
||||
n_results=k,
|
||||
)
|
||||
return [f"/static/icons/bold/{each}.png" for each in result["ids"][0]]
|
||||
return [f"/static/icons/bold/{each}.svg" for each in result["ids"][0]]
|
||||
|
||||
|
||||
ICON_FINDER_SERVICE = IconFinderService()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import asyncio
|
||||
import dirtyjson
|
||||
import json
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
from fastapi import HTTPException
|
||||
|
|
@ -554,7 +555,7 @@ class LLMClient:
|
|||
)
|
||||
if content:
|
||||
if depth == 0:
|
||||
return json.loads(content)
|
||||
return dict(dirtyjson.loads(content))
|
||||
return content
|
||||
return None
|
||||
|
||||
|
|
@ -655,7 +656,7 @@ class LLMClient:
|
|||
)
|
||||
|
||||
if text_content:
|
||||
return json.loads(text_content)
|
||||
return dict(dirtyjson.loads(text_content))
|
||||
return None
|
||||
|
||||
async def _generate_anthropic_structured(
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.6 KiB |
1
servers/fastapi/static/icons/bold/acorn-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M216,112v16c0,53-88,88-88,112,0-24-88-59-88-112V112" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M80,56h96a48,48,0,0,1,48,48v0a8,8,0,0,1-8,8H40a8,8,0,0,1-8-8v0A48,48,0,0,1,80,56Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M128,56V48a32,32,0,0,1,32-32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 605 B |
|
Before Width: | Height: | Size: 3.9 KiB |
1
servers/fastapi/static/icons/bold/address-book-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="136" cy="112" r="32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="32" y1="72" x2="56" y2="72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="32" y1="128" x2="56" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="32" y1="184" x2="56" y2="184" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="40" y="48" width="192" height="160" rx="8" transform="translate(264 -8) rotate(90)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M92,163.21a60,60,0,0,1,88,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 994 B |
|
Before Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="176" y1="160" x2="216" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="176" y1="96" x2="216" y2="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="40" y="40" width="176" height="176" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="108" cy="120" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M76,168c3.55-13.8,17.09-24,32-24s28.46,10.19,32,24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="176" y1="40" x2="176" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 981 B |
|
Before Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="72" x2="128" y2="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M104,24h48" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="168 72 152 160 152 232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="88 72 104 160 104 232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M66.19,160a8,8,0,0,1-7.52-5.27l-26.18-72A8,8,0,0,1,40,72H216a8,8,0,0,1,7.52,10.73l-26.18,72a8,8,0,0,1-7.52,5.27Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 879 B |
|
Before Width: | Height: | Size: 3.4 KiB |
1
servers/fastapi/static/icons/bold/airplane-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M152,96l80,40v32l-80-16v32l16,16v32l-40-16L88,232V200l16-16V152L24,168V136l80-40V48a24,24,0,0,1,48,0Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 324 B |
|
Before Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="72" y1="216" x2="216" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M144,104h64a32,32,0,0,1,32,32v24H61.06a32,32,0,0,1-30.65-22.8L16.34,90.3A8,8,0,0,1,24,80h8l24,24H92.91L80.42,66.53A8,8,0,0,1,88,56h8Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 497 B |
|
Before Width: | Height: | Size: 3.6 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="104" y1="216" x2="244" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M232,180,55.37,130.54A32,32,0,0,1,32,99.73V48a8,8,0,0,1,10.53-7.59L48,42.24,60,75.46,104,88V48a8,8,0,0,1,10.53-7.59L120,42.24l24,57.2,64.56,18A32,32,0,0,1,232,148.32Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 531 B |
|
Before Width: | Height: | Size: 4.5 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="24" y1="216" x2="168" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M88,112.51,58.65,84a8,8,0,0,1,2.2-13.3L68,68l57.53,21.17,54.84-32.75a32,32,0,0,1,41,7.32L240,87.64l-147.41,88a32,32,0,0,1-38-4.32L18.53,136a8,8,0,0,1,2.32-13.19L24,121.27,55.79,132Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 545 B |
|
Before Width: | Height: | Size: 3.5 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M144,100h64a32,32,0,0,1,32,32v24H61.06a32,32,0,0,1-30.65-22.8L16.34,86.3A8,8,0,0,1,24,76h8l24,24H92.91L80.42,62.53A8,8,0,0,1,88,52h8Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="208" cy="204" r="20"/><circle cx="112" cy="204" r="20"/></svg>
|
||||
|
After Width: | Height: | Size: 424 B |
|
Before Width: | Height: | Size: 5.1 KiB |
1
servers/fastapi/static/icons/bold/airplane-tilt-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M88,224l24-24V176l24-24,48,72,24-24-32-88,33-31A24,24,0,0,0,175,47L144,80,56,48,32,72l72,48L80,144H56L32,168l40,16Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 338 B |
|
Before Width: | Height: | Size: 2.9 KiB |
1
servers/fastapi/static/icons/bold/airplay-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polygon points="128 160 176 216 80 216 128 160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M52,192H48a16,16,0,0,1-16-16V64A16,16,0,0,1,48,48H208a16,16,0,0,1,16,16V176a16,16,0,0,1-16,16h-4" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 468 B |
|
Before Width: | Height: | Size: 5.3 KiB |
1
servers/fastapi/static/icons/bold/alarm-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="136" r="88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="56" y1="32" x2="24" y2="64" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="200" y1="32" x2="232" y2="64" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="128 88 128 136 176 136" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 665 B |
|
Before Width: | Height: | Size: 5.9 KiB |
1
servers/fastapi/static/icons/bold/alien-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M216,112c0,48.6-56,120-88,120S40,160.6,40,112a88,88,0,0,1,176,0Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><ellipse cx="92" cy="124" rx="16.97" ry="29.39" transform="translate(-60.74 101.37) rotate(-45)"/><ellipse cx="164" cy="124" rx="29.39" ry="16.97" transform="translate(-39.65 152.28) rotate(-45)"/><line x1="116" y1="184" x2="140" y2="184" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 626 B |
|
Before Width: | Height: | Size: 2.1 KiB |
1
servers/fastapi/static/icons/bold/align-bottom-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="216" y1="216" x2="40" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="122" y="98" width="104" height="52" rx="8" transform="translate(50 298) rotate(-90)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="56" y="32" width="52" height="144" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 598 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="56" y1="232" x2="200" y2="232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="88" y="32" width="80" height="160" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 404 B |
|
Before Width: | Height: | Size: 2.2 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="32" x2="128" y2="52" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="204" x2="128" y2="224" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="108" x2="128" y2="148" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="64" y="52" width="128" height="56" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="40" y="148" width="176" height="56" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 839 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="48" x2="128" y2="88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="168" x2="128" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="40" y="88" width="176" height="80" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 545 B |
|
Before Width: | Height: | Size: 2.1 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="224" y1="128" x2="204" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="52" y1="128" x2="32" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="148" y1="128" x2="108" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="112" y="100" width="128" height="56" rx="8" transform="translate(304 -48) rotate(90)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="52" y="40" width="56" height="176" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 882 B |
|
Before Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="88" y="40" width="80" height="176" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="208" y1="128" x2="168" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="88" y1="128" x2="48" y2="128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 545 B |
|
Before Width: | Height: | Size: 2.2 KiB |
1
servers/fastapi/static/icons/bold/align-left-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="40" y1="40" x2="40" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="80" y="56" width="104" height="52" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="80" y="148" width="144" height="52" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 554 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="32" y1="56" x2="32" y2="200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="72" y="88" width="160" height="80" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 402 B |
|
Before Width: | Height: | Size: 2.2 KiB |
1
servers/fastapi/static/icons/bold/align-right-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="216" y1="40" x2="216" y2="216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="72" y="56" width="104" height="52" rx="8" transform="translate(248 164) rotate(180)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="32" y="148" width="144" height="52" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 599 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="224" y1="56" x2="224" y2="200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="24" y="88" width="160" height="80" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 404 B |
|
Before Width: | Height: | Size: 2.1 KiB |
1
servers/fastapi/static/icons/bold/align-top-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="216" y1="40" x2="40" y2="40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="122" y="106" width="104" height="52" rx="8" transform="translate(306 -42) rotate(90)" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="56" y="80" width="52" height="144" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 597 B |
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="56" y1="32" x2="200" y2="32" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><rect x="88" y="72" width="80" height="160" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 402 B |
|
Before Width: | Height: | Size: 4.9 KiB |
1
servers/fastapi/static/icons/bold/amazon-logo-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M240,168l-16,16s-36,40-96,40-96-40-96-40" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="208 168 240 168 240 200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="124" cy="132" r="44" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M168,176V84A44,44,0,0,0,87.12,60" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 682 B |
|
Before Width: | Height: | Size: 3.3 KiB |
1
servers/fastapi/static/icons/bold/ambulance-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="192" cy="192" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="80" cy="192" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="168" y1="192" x2="104" y2="192" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M56,192H32a8,8,0,0,1-8-8V72a8,8,0,0,1,8-8H184V169.37" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="92" y1="120" x2="132" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="112" y1="100" x2="112" y2="140" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="184" y1="120" x2="244" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M184,80h38.58A8,8,0,0,1,230,85l14,35v64a8,8,0,0,1-8,8H216" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 3.8 KiB |
1
servers/fastapi/static/icons/bold/anchor-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="128" y1="232" x2="128" y2="80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="128" cy="56" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="88" y1="120" x2="168" y2="120" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M216,144c0,64-88,24-88,88,0-64-88-24-88-88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 679 B |
|
Before Width: | Height: | Size: 4.2 KiB |
1
servers/fastapi/static/icons/bold/anchor-simple-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="64" r="24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="216" x2="128" y2="88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M56,120H32a96,96,0,0,0,192,0H200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 528 B |
|
Before Width: | Height: | Size: 4 KiB |
1
servers/fastapi/static/icons/bold/android-logo-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="164" cy="148" r="16"/><circle cx="92" cy="148" r="16"/><path d="M24,184V161.13C24,103.65,70.15,56.2,127.63,56A104,104,0,0,1,232,160v24a8,8,0,0,1-8,8H32A8,8,0,0,1,24,184Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="32" y1="48" x2="63.07" y2="79.07" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="224" y1="48" x2="193.1" y2="78.9" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 683 B |
|
Before Width: | Height: | Size: 2.8 KiB |
1
servers/fastapi/static/icons/bold/angle-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="72 32 72 200 240 200" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="32" y1="72" x2="72" y2="72" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M112,72.33A96,96,0,0,1,199.67,160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 534 B |
|
Before Width: | Height: | Size: 4.2 KiB |
1
servers/fastapi/static/icons/bold/angular-logo-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polygon points="128 232 208 192 224 72 128 32 32 72 48 192 128 232" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="168 160 128 88 88 160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="99.11" y1="140" x2="156.89" y2="140" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 569 B |
|
Before Width: | Height: | Size: 6.4 KiB |
1
servers/fastapi/static/icons/bold/aperture-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><circle cx="128" cy="128" r="96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="32" x2="164.68" y2="134.7" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="44.86" y1="80" x2="152.14" y2="99.58" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="44.86" y1="176" x2="115.46" y2="92.89" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="128" y1="224" x2="91.32" y2="121.3" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="211.14" y1="176" x2="103.86" y2="156.42" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="211.14" y1="80" x2="140.54" y2="163.11" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.7 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="104" y1="32" x2="128" y2="72.62" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="152" y1="32" x2="76.36" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="179.64" y1="160" x2="232" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="151.23" y1="111.93" x2="208" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="24" y1="160" x2="133.15" y2="160" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="52.73" y1="200" x2="48" y2="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 978 B |
|
Before Width: | Height: | Size: 1.9 KiB |
1
servers/fastapi/static/icons/bold/app-window-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="32" y="48" width="192" height="160" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="76" cy="92" r="16"/><circle cx="124" cy="92" r="16"/></svg>
|
||||
|
After Width: | Height: | Size: 329 B |
|
Before Width: | Height: | Size: 4.8 KiB |
1
servers/fastapi/static/icons/bold/apple-logo-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M216,73.52C204.53,62.66,185,56,168,56a63.72,63.72,0,0,0-40,14h0A63.71,63.71,0,0,0,88.88,56C52,55.5,23.06,86.3,24,123.19a119.62,119.62,0,0,0,37.65,84.12A31.92,31.92,0,0,0,83.6,216h87.7a31.75,31.75,0,0,0,23.26-10c15.85-17,21.44-33.2,21.44-33.2h0c-16.79-11.53-24-30.87-24-52.78,0-18.3,11.68-34.81,24-46.48Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M148,12a32.12,32.12,0,0,0-9.77,8.37" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 672 B |
|
Before Width: | Height: | Size: 6 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M131.23,224h-6.46a16,16,0,0,1-15.62-12.45l-12.74-56A16,16,0,0,1,112,136H144a16,16,0,0,1,15.62,19.55l-12.74,56A16,16,0,0,1,131.23,224Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><circle cx="128" cy="108" r="28" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M65.74,201.08a96,96,0,1,1,124.52,0" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 634 B |
|
Before Width: | Height: | Size: 3.4 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><path d="M40,161.61c72-59.69,104,56.47,176-3.22" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M40,97.61c72-59.69,104,56.47,176-3.22" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 409 B |
|
Before Width: | Height: | Size: 2 KiB |
1
servers/fastapi/static/icons/bold/archive-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><rect x="24" y="56" width="208" height="40" rx="8" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M216,96v96a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V96" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><line x1="104" y1="136" x2="152" y2="136" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 560 B |
|
Before Width: | Height: | Size: 4.3 KiB |
1
servers/fastapi/static/icons/bold/armchair-bold.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><line x1="92" y1="136" x2="164" y2="136" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M164,164V128a40,40,0,1,1,44,39.8V200a8,8,0,0,1-8,8H56a8,8,0,0,1-8-8V167.8A40,40,0,1,1,92,128v36" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M48,88.2V72A32,32,0,0,1,80,40h96a32,32,0,0,1,32,32V88.2" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 625 B |
|
Before Width: | Height: | Size: 3 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="88 152 24 152 24 88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M224,184A96,96,0,0,0,60.12,116.12L24,152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 402 B |
|
Before Width: | Height: | Size: 2.9 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="168 152 232 152 232 88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M32,184a96,96,0,0,1,163.88-67.88L232,152" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 405 B |
|
Before Width: | Height: | Size: 3.3 KiB |
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="256" height="256" fill="none"/><polyline points="80 152 32 104 80 56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><polyline points="136 152 88 104 136 56" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/><path d="M224,200a96,96,0,0,0-96-96H88" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="24"/></svg>
|
||||
|
After Width: | Height: | Size: 532 B |