From 2d462d8df401f7684f078656d4038c19911dff9d Mon Sep 17 00:00:00 2001 From: sudipnext Date: Sun, 8 Mar 2026 20:05:44 +0545 Subject: [PATCH 1/7] feat: implement database migration on startup and update dependencies for FastAPI --- Dockerfile | 2 +- Dockerfile.dev | 4 +- docker-compose.yml | 4 + electron/app/main.ts | 21 +- electron/app/utils/update-checker.ts | 257 +++++++++++++++++ electron/package-lock.json | 4 +- electron/package.json | 4 +- electron/servers/fastapi/alembic.ini | 41 +++ electron/servers/fastapi/alembic/env.py | 94 +++++++ .../servers/fastapi/alembic/script.py.mako | 27 ++ .../servers/fastapi/alembic/versions/.gitkeep | 0 .../alembic/versions/00b3c27a13bc_init.py | 135 +++++++++ electron/servers/fastapi/api/lifespan.py | 7 +- electron/servers/fastapi/migrations.py | 40 +++ electron/servers/fastapi/placeholder | Bin 0 -> 12288 bytes electron/servers/fastapi/pyproject.toml | 1 + electron/servers/fastapi/server.spec | 2 + .../services/pptx_presentation_creator.py | 266 +++++++++--------- electron/servers/fastapi/utils/get_env.py | 4 + electron/servers/fastapi/uv.lock | 28 ++ electron/version.json | 8 +- servers/fastapi/alembic.ini | 41 +++ servers/fastapi/alembic/env.py | 94 +++++++ servers/fastapi/alembic/script.py.mako | 27 ++ servers/fastapi/alembic/versions/.gitkeep | 0 .../alembic/versions/fd2ab04834cc_init.py | 136 +++++++++ servers/fastapi/api/lifespan.py | 7 +- servers/fastapi/migrations.py | 40 +++ servers/fastapi/placeholder | Bin 0 -> 12288 bytes servers/fastapi/pyproject.toml | 4 + .../services/pptx_presentation_creator.py | 137 +++++++++ servers/fastapi/utils/get_env.py | 4 + servers/fastapi/uv.lock | 143 ++++++---- test_server.py | 78 +++++ 34 files changed, 1460 insertions(+), 200 deletions(-) create mode 100644 electron/app/utils/update-checker.ts create mode 100644 electron/servers/fastapi/alembic.ini create mode 100644 electron/servers/fastapi/alembic/env.py create mode 100644 electron/servers/fastapi/alembic/script.py.mako create mode 100644 electron/servers/fastapi/alembic/versions/.gitkeep create mode 100644 electron/servers/fastapi/alembic/versions/00b3c27a13bc_init.py create mode 100644 electron/servers/fastapi/migrations.py create mode 100644 electron/servers/fastapi/placeholder create mode 100644 servers/fastapi/alembic.ini create mode 100644 servers/fastapi/alembic/env.py create mode 100644 servers/fastapi/alembic/script.py.mako create mode 100644 servers/fastapi/alembic/versions/.gitkeep create mode 100644 servers/fastapi/alembic/versions/fd2ab04834cc_init.py create mode 100644 servers/fastapi/migrations.py create mode 100644 servers/fastapi/placeholder create mode 100644 test_server.py diff --git a/Dockerfile b/Dockerfile index 59fe7945..b9c1bf32 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium RUN curl -fsSL https://ollama.com/install.sh | sh # Install dependencies for FastAPI -RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \ +RUN pip install alembic aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \ pathvalidate pdfplumber chromadb sqlmodel \ anthropic google-genai openai fastmcp dirtyjson RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu diff --git a/Dockerfile.dev b/Dockerfile.dev index 873e5355..72732d7e 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -25,10 +25,10 @@ ENV TEMP_DIRECTORY=/tmp/presenton ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium # Install ollama -RUN curl -fsSL http://ollama.com/install.sh | sh +# RUN curl -fsSL http://ollama.com/install.sh | sh # Install dependencies for FastAPI -RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \ +RUN pip install alembic aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \ pathvalidate pdfplumber chromadb sqlmodel \ anthropic google-genai openai fastmcp dirtyjson RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu diff --git a/docker-compose.yml b/docker-compose.yml index 1376740b..05c189f8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: volumes: - ./app_data:/app_data environment: + - MIGRATE_DATABASE_ON_STARTUP=true - CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS} - LLM=${LLM} - OPENAI_API_KEY=${OPENAI_API_KEY} @@ -56,6 +57,7 @@ services: volumes: - ./app_data:/app_data environment: + - MIGRATE_DATABASE_ON_STARTUP=true - CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS} - LLM=${LLM} - OPENAI_API_KEY=${OPENAI_API_KEY} @@ -92,6 +94,7 @@ services: - .:/app - ./app_data:/app_data environment: + - MIGRATE_DATABASE_ON_STARTUP=true - CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS} - LLM=${LLM} - OPENAI_API_KEY=${OPENAI_API_KEY} @@ -135,6 +138,7 @@ services: - .:/app - ./app_data:/app_data environment: + - MIGRATE_DATABASE_ON_STARTUP=true - CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS} - LLM=${LLM} - OPENAI_API_KEY=${OPENAI_API_KEY} diff --git a/electron/app/main.ts b/electron/app/main.ts index e86b9f24..5e6dea13 100644 --- a/electron/app/main.ts +++ b/electron/app/main.ts @@ -1,5 +1,5 @@ require("dotenv").config(); -import { app, BrowserWindow } from "electron"; +import { app, BrowserWindow, shell } from "electron"; import path from "path"; import fs from "fs"; import { findUnusedPorts, killProcess, setupEnv, setUserConfig } from "./utils"; @@ -9,6 +9,7 @@ import { appDataDir, baseDir, ensureDirectoriesExist, fastapiDir, isDev, localho import { setupIpcHandlers } from "./ipc"; import { setupLibreOfficeInstallHandlers } from "./ipc/libreoffice_install_handlers"; import { checkLibreOfficeBeforeWindow, getSofficePath } from "./utils/libreoffice-check"; +import { startUpdateChecker, stopUpdateChecker } from "./utils/update-checker"; var win: BrowserWindow | undefined; @@ -51,6 +52,16 @@ const createWindow = () => { preload: path.join(__dirname, 'preloads/index.js'), }, }); + + // Open external links (e.g. "Download update") in the system browser so the user + // sees download progress and can manage downloads normally. + win.webContents.setWindowOpenHandler(({ url }) => { + if (url.startsWith("http://") || url.startsWith("https://")) { + shell.openExternal(url); + return { action: "deny" }; + } + return { action: "allow" }; + }); }; async function startServers(fastApiPort: number, nextjsPort: number) { @@ -90,6 +101,7 @@ async function startServers(fastApiPort: number, nextjsPort: number) { APP_DATA_DIRECTORY: appDataDir, TEMP_DIRECTORY: tempDir, USER_CONFIG_PATH: userConfigPath, + MIGRATE_DATABASE_ON_STARTUP: "True", // Resolved by libreoffice-check.ts at startup; lets Python invoke the // exact binary path instead of relying on the system PATH. SOFFICE_PATH: getSofficePath(), @@ -180,9 +192,16 @@ app.whenReady().then(async () => { await startServers(fastApiPort, nextjsPort); win?.loadURL(`${localhost}:${nextjsPort}`); + + // Begin polling the version server for available updates + if (win) { + process.stderr.write("[Presenton] Starting update checker...\n"); + startUpdateChecker(win); + } }); app.on("window-all-closed", async () => { + stopUpdateChecker(); await stopServers(); app.quit(); }); diff --git a/electron/app/utils/update-checker.ts b/electron/app/utils/update-checker.ts new file mode 100644 index 00000000..c2caf67d --- /dev/null +++ b/electron/app/utils/update-checker.ts @@ -0,0 +1,257 @@ +import { net } from "electron"; +import { app, BrowserWindow } from "electron"; +import { isDev } from "./constants"; + +/** + * Version check URL — GitHub raw version.json (no API required). + * Override with UPDATE_SERVER_URL for local testing. + */ +const VERSION_JSON_URL = + process.env.UPDATE_SERVER_URL || + "https://raw.githubusercontent.com/presenton/presenton/refs/heads/main/electron/version.json"; + +const CURRENT_VERSION = app.getVersion(); + +/** Maximum number of fetch attempts (polls). */ +const MAX_ATTEMPTS = 3; + +/** Wait 2 minutes after load before first poll (10s in dev for testing). */ +const INITIAL_DELAY_MS = isDev ? 10 * 1_000 : 2 * 60 * 1_000; + +/** 1 minute between poll attempts (5s in dev for testing). */ +const POLL_INTERVAL_MS = isDev ? 5 * 1_000 : 1 * 60 * 1_000; + +/** Short delay before injecting banner to allow React/Next.js to mount. */ +const INJECT_DELAY_MS = isDev ? 500 : 1_000; + +function log(msg: string): void { + const line = `[UpdateChecker] ${msg}\n`; + process.stderr.write(line); + console.log(`[UpdateChecker] ${msg}`); +} + +interface VersionResponse { + version: string; + downloads: { + linux: string; + mac: string; + windows: string; + }; +} + +function getDownloadUrlForPlatform(downloads: VersionResponse["downloads"]): string { + const platform = process.platform; + if (platform === "darwin") return downloads.mac; + if (platform === "win32") return downloads.windows; + return downloads.linux; +} + +/** + * Simple semver comparison that strips pre-release labels for numeric comparison. + * Returns true if `remote` is strictly newer than `current`. + */ +function isNewerVersion(current: string, remote: string): boolean { + const toNumbers = (v: string) => + v + .replace(/[^0-9.]/g, "") + .split(".") + .map(Number); + + const curr = toNumbers(current); + const rem = toNumbers(remote); + const len = Math.max(curr.length, rem.length); + + for (let i = 0; i < len; i++) { + const c = curr[i] ?? 0; + const r = rem[i] ?? 0; + if (r > c) return true; + if (r < c) return false; + } + return false; +} + +async function fetchVersionInfo(): Promise { + try { + log(`Fetching ${VERSION_JSON_URL}...`); + const response = await net.fetch(VERSION_JSON_URL, { + method: "GET", + headers: { "User-Agent": `Presenton/${CURRENT_VERSION}` }, + }); + if (!response.ok) { + log(`Fetch failed: HTTP ${response.status}`); + return null; + } + const data = (await response.json()) as VersionResponse; + log(`Fetched version: ${data.version}`); + return data; + } catch (err) { + log(`Fetch error: ${err}`); + return null; + } +} + +/** Pending update to re-inject on navigation (production: React/Next.js may replace DOM). */ +let pendingUpdate: { version: string; downloadUrl: string } | null = null; + +/** + * Schedules banner injection after INJECT_DELAY_MS so React/Next.js can mount first. + * In production (.deb), the DOM may not be ready when did-finish-load fires. + */ +function scheduleBannerInjection( + win: BrowserWindow, + version: string, + downloadUrl: string +): void { + pendingUpdate = { version, downloadUrl }; + setTimeout(() => { + if (win.isDestroyed() || !pendingUpdate) return; + log(`Injecting banner now`); + injectUpdateBanner(win, pendingUpdate.version, pendingUpdate.downloadUrl); + }, INJECT_DELAY_MS); +} + +/** + * Injects a Cursor-style update banner at the bottom of the renderer window. + * Safe to call multiple times; a second call while the banner is visible is a no-op. + */ +function injectUpdateBanner( + win: BrowserWindow, + latest: string, + downloadUrl: string, + releaseNotes?: string +): void { + const notesHtml = releaseNotes + ? `${releaseNotes}` + : ""; + + const script = /* js */ ` + (function () { + if (document.getElementById('__presenton_update_banner__')) return; + + const banner = document.createElement('div'); + banner.id = '__presenton_update_banner__'; + banner.style.cssText = [ + 'position:fixed', + 'bottom:0', + 'left:0', + 'right:0', + 'background:#1e1e2e', + 'color:#cdd6f4', + 'display:flex', + 'align-items:center', + 'justify-content:space-between', + 'padding:10px 18px', + 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif', + 'font-size:13px', + 'z-index:2147483647', + 'border-top:1px solid #313244', + 'box-shadow:0 -4px 16px rgba(0,0,0,0.35)', + 'gap:12px', + ].join(';'); + + banner.innerHTML = \` + + + + Presenton ${latest} is available + — you have ${CURRENT_VERSION} + + ${notesHtml} + +
+ Download update + +
+ \`; + + document.body.appendChild(banner); + })(); + `; + + win.webContents.executeJavaScript(script).catch((err) => { + log(`Banner injection failed: ${err}`); + }); +} + +/** + * Polls for version info up to MAX_ATTEMPTS times with 1 min between attempts. + * Stops as soon as a successful response is received or all attempts are exhausted. + */ +async function checkForUpdatesWithRetry(win: BrowserWindow): Promise { + log(`Starting check (current: ${CURRENT_VERSION})`); + for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + if (win.isDestroyed()) { + log("Window destroyed, aborting"); + return; + } + + log(`Attempt ${attempt}/${MAX_ATTEMPTS}`); + const data = await fetchVersionInfo(); + + if (data) { + const newer = isNewerVersion(CURRENT_VERSION, data.version); + log(`Remote ${data.version} vs current ${CURRENT_VERSION} -> newer? ${newer}`); + if (newer) { + const downloadUrl = getDownloadUrlForPlatform(data.downloads); + log(`Injecting banner for ${data.version} (after ${INJECT_DELAY_MS}ms delay)`); + scheduleBannerInjection(win, data.version, downloadUrl); + } else { + log("No update needed, skipping banner"); + } + return; + } + + // Wait 1 minute before the next poll (skip delay after the last attempt) + if (attempt < MAX_ATTEMPTS) { + log(`Next poll in ${POLL_INTERVAL_MS / 1_000}s...`); + await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS)); + } + } + log("All attempts failed, no update info"); +} + +/** + * Starts the update checker. + * Waits 2 minutes after load, then polls 3 times with 1 min interval. + * Re-injects banner on every navigation (handles Next.js client routing). + */ +export function startUpdateChecker(win: BrowserWindow): void { + log("Registered, waiting for did-finish-load"); + let hasRunCheck = false; + + const onLoad = () => { + if (pendingUpdate) { + log("did-finish-load (navigation), re-injecting banner"); + scheduleBannerInjection(win, pendingUpdate.version, pendingUpdate.downloadUrl); + } else if (!hasRunCheck) { + hasRunCheck = true; + log(`did-finish-load fired, first poll in ${INITIAL_DELAY_MS / 1_000}s`); + setTimeout(() => { + if (win.isDestroyed()) return; + checkForUpdatesWithRetry(win); + }, INITIAL_DELAY_MS); + } + }; + + if (!win.webContents.isLoading()) { + log(`Page already loaded, first poll in ${INITIAL_DELAY_MS / 1_000}s`); + hasRunCheck = true; + setTimeout(() => { + if (win.isDestroyed()) return; + checkForUpdatesWithRetry(win); + }, INITIAL_DELAY_MS); + } + win.webContents.on("did-finish-load", onLoad); +} + +export function stopUpdateChecker(): void { + pendingUpdate = null; +} diff --git a/electron/package-lock.json b/electron/package-lock.json index ce9ae9d8..59e05b9f 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -1,12 +1,12 @@ { "name": "presenton", - "version": "0.6.1-beta", + "version": "0.6.0-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "presenton", - "version": "0.6.1-beta", + "version": "0.6.0-beta", "hasInstallScript": true, "dependencies": { "@tailwindcss/cli": "^4.1.5", diff --git a/electron/package.json b/electron/package.json index 59480128..f0352d6c 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,7 +1,7 @@ { "name": "presenton", "productName": "Presenton Open Source", - "version": "0.6.1-beta", + "version": "0.6.0-beta", "main": "app_dist/main.js", "description": "Open-Source AI Presentation Generator", "homepage": "https://presenton.ai", @@ -35,7 +35,7 @@ "build:css": "tailwindcss -i ./resources/ui/assets/css/tailwind.import.css -o ./resources/ui/assets/css/tailwind.css --watch", "build:vectorstore": "cd servers/fastapi && uv run python build_vectorstore.py", "build:nextjs": "rm -rf resources/nextjs && cd servers/nextjs && cross-env BUILD_TARGET=electron npm run build && cp -r .next-build ../../resources/nextjs && cp -r app/presentation-templates ../../resources/nextjs/presentation-templates", - "build:fastapi": "rm -rf resources/fastapi && npm run build:vectorstore && cd servers/fastapi && uv run python -m PyInstaller --distpath ../../resources server.spec", + "build:fastapi": "rm -rf resources/fastapi && npm run build:vectorstore && (cp ../servers/fastapi/alembic/versions/*.py servers/fastapi/alembic/versions/ 2>/dev/null || true) && cd servers/fastapi && uv run python -m PyInstaller --distpath ../../resources server.spec", "generate:version": "node generate_update.js", "build:electron": "npm run generate:version && 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", diff --git a/electron/servers/fastapi/alembic.ini b/electron/servers/fastapi/alembic.ini new file mode 100644 index 00000000..816710ec --- /dev/null +++ b/electron/servers/fastapi/alembic.ini @@ -0,0 +1,41 @@ +# Alembic configuration for CLI usage (e.g. alembic revision --autogenerate) +# migrations.py sets these programmatically when running at app startup. + +[alembic] +script_location = alembic +# sqlalchemy.url is overridden by env.py (uses DATABASE_URL or APP_DATA_DIRECTORY) +sqlalchemy.url = sqlite:///placeholder + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/electron/servers/fastapi/alembic/env.py b/electron/servers/fastapi/alembic/env.py new file mode 100644 index 00000000..8f77371d --- /dev/null +++ b/electron/servers/fastapi/alembic/env.py @@ -0,0 +1,94 @@ +import os +import sys +from logging.config import fileConfig +from pathlib import Path + +from alembic import context +from sqlalchemy import engine_from_config, pool +from sqlmodel import SQLModel + +# Make sure all models can be imported when alembic runs standalone. +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +# Import every SQL model so they register with SQLModel.metadata before +# autogenerate or migration execution reads it. +from models.sql.async_presentation_generation_status import ( # noqa: F401, E402 + AsyncPresentationGenerationTaskModel, +) +from models.sql.image_asset import ImageAsset # noqa: F401, E402 +from models.sql.key_value import KeyValueSqlModel # noqa: F401, E402 +from models.sql.ollama_pull_status import OllamaPullStatus # noqa: F401, E402 +from models.sql.presentation import PresentationModel # noqa: F401, E402 +from models.sql.presentation_layout_code import ( # noqa: F401, E402 + PresentationLayoutCodeModel, +) +from models.sql.slide import SlideModel # noqa: F401, E402 +from models.sql.template import TemplateModel # noqa: F401, E402 +from models.sql.webhook_subscription import WebhookSubscription # noqa: F401, E402 + +alembic_config = context.config + +if alembic_config.config_file_name is not None: + fileConfig(alembic_config.config_file_name) + +target_metadata = SQLModel.metadata + + +def _get_url() -> str: + """ + Prefer the URL injected by migrations.py via config.set_main_option, + falling back to the DATABASE_URL environment variable or a local SQLite DB. + """ + configured = alembic_config.get_main_option("sqlalchemy.url") + if configured: + return configured + + from utils.db_utils import get_database_url_and_connect_args + + url, _ = get_database_url_and_connect_args() + return ( + url + .replace("sqlite+aiosqlite://", "sqlite:///") + .replace("postgresql+asyncpg://", "postgresql://") + .replace("mysql+aiomysql://", "mysql://") + ) + + +def run_migrations_offline() -> None: + """Generate SQL script without connecting to the database.""" + url = _get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations against the live database.""" + configuration = dict(alembic_config.get_section(alembic_config.config_ini_section) or {}) + configuration["sqlalchemy.url"] = _get_url() + + connectable = engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/electron/servers/fastapi/alembic/script.py.mako b/electron/servers/fastapi/alembic/script.py.mako new file mode 100644 index 00000000..6ce33510 --- /dev/null +++ b/electron/servers/fastapi/alembic/script.py.mako @@ -0,0 +1,27 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/electron/servers/fastapi/alembic/versions/.gitkeep b/electron/servers/fastapi/alembic/versions/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/electron/servers/fastapi/alembic/versions/00b3c27a13bc_init.py b/electron/servers/fastapi/alembic/versions/00b3c27a13bc_init.py new file mode 100644 index 00000000..17fb3cc2 --- /dev/null +++ b/electron/servers/fastapi/alembic/versions/00b3c27a13bc_init.py @@ -0,0 +1,135 @@ +"""init + +Revision ID: 00b3c27a13bc +Revises: +Create Date: 2026-03-08 19:12:45.478149 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = '00b3c27a13bc' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('async_presentation_generation_tasks', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('message', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('error', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('imageasset', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('is_uploaded', sa.Boolean(), nullable=False), + sa.Column('path', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('extras', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('keyvaluesqlmodel', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('key', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('value', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_keyvaluesqlmodel_key'), 'keyvaluesqlmodel', ['key'], unique=False) + op.create_table('ollamapullstatus', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('last_updated', sa.DateTime(), nullable=True), + sa.Column('status', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('presentation_layout_codes', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('presentation', sa.Uuid(), nullable=False), + sa.Column('layout_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('layout_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('layout_code', sa.Text(), nullable=True), + sa.Column('fonts', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_presentation_layout_codes_presentation'), 'presentation_layout_codes', ['presentation'], unique=False) + op.create_table('presentations', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('content', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('n_slides', sa.Integer(), nullable=False), + sa.Column('language', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('file_paths', sa.JSON(), nullable=True), + sa.Column('outlines', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('layout', sa.JSON(), nullable=True), + sa.Column('structure', sa.JSON(), nullable=True), + sa.Column('instructions', sa.String(), nullable=True), + sa.Column('tone', sa.String(), nullable=True), + sa.Column('verbosity', sa.String(), nullable=True), + sa.Column('include_table_of_contents', sa.Boolean(), nullable=True), + sa.Column('include_title_slide', sa.Boolean(), nullable=True), + sa.Column('web_search', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('templates', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('webhook_subscriptions', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('url', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('secret', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('event', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_webhook_subscriptions_event'), 'webhook_subscriptions', ['event'], unique=False) + op.create_table('slides', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('presentation', sa.Uuid(), nullable=True), + sa.Column('layout_group', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('layout', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('index', sa.Integer(), nullable=False), + sa.Column('content', sa.JSON(), nullable=True), + sa.Column('html_content', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('speaker_note', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('properties', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['presentation'], ['presentations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_slides_presentation'), 'slides', ['presentation'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_slides_presentation'), table_name='slides') + op.drop_table('slides') + op.drop_index(op.f('ix_webhook_subscriptions_event'), table_name='webhook_subscriptions') + op.drop_table('webhook_subscriptions') + op.drop_table('templates') + op.drop_table('presentations') + op.drop_index(op.f('ix_presentation_layout_codes_presentation'), table_name='presentation_layout_codes') + op.drop_table('presentation_layout_codes') + op.drop_table('ollamapullstatus') + op.drop_index(op.f('ix_keyvaluesqlmodel_key'), table_name='keyvaluesqlmodel') + op.drop_table('keyvaluesqlmodel') + op.drop_table('imageasset') + op.drop_table('async_presentation_generation_tasks') + # ### end Alembic commands ### diff --git a/electron/servers/fastapi/api/lifespan.py b/electron/servers/fastapi/api/lifespan.py index 86f55379..6fe4e6c4 100644 --- a/electron/servers/fastapi/api/lifespan.py +++ b/electron/servers/fastapi/api/lifespan.py @@ -3,6 +3,7 @@ import os from fastapi import FastAPI +from migrations import migrate_database_on_startup from services.database import create_db_and_tables from utils.get_env import get_app_data_directory_env from utils.model_availability import ( @@ -14,10 +15,12 @@ from utils.model_availability import ( async def app_lifespan(_: FastAPI): """ Lifespan context manager for FastAPI application. - Initializes the application data directory and checks LLM model availability. - + Initializes the application data directory, runs Alembic migrations when + MIGRATE_DATABASE_ON_STARTUP=true, creates any missing tables, and checks + LLM model availability. """ os.makedirs(get_app_data_directory_env(), exist_ok=True) + await migrate_database_on_startup() await create_db_and_tables() await check_llm_and_image_provider_api_or_model_availability() yield diff --git a/electron/servers/fastapi/migrations.py b/electron/servers/fastapi/migrations.py new file mode 100644 index 00000000..0b2b578e --- /dev/null +++ b/electron/servers/fastapi/migrations.py @@ -0,0 +1,40 @@ +import asyncio +from pathlib import Path + +from alembic import command +from alembic.config import Config + +from utils.db_utils import get_database_url_and_connect_args +from utils.get_env import get_migrate_database_on_startup_env + + +async def migrate_database_on_startup() -> None: + if get_migrate_database_on_startup_env() not in ["true", "True"]: + return + + try: + await asyncio.to_thread(_run_migrations) + print("Migrations run successfully", flush=True) + except Exception as exc: + print(f"Error running migrations: {exc}", flush=True) + + +def _run_migrations() -> None: + # migrations.py lives at servers/fastapi/migrations.py + # so parents[0] = servers/fastapi/, where alembic/ lives alongside it. + base_dir = Path(__file__).resolve().parents[0] + config = Config() + config.set_main_option("script_location", str(base_dir / "alembic")) + + database_url, _ = get_database_url_and_connect_args() + + # Alembic uses synchronous engines; strip async driver prefixes. + database_url = ( + database_url + .replace("sqlite+aiosqlite://", "sqlite:///") + .replace("postgresql+asyncpg://", "postgresql://") + .replace("mysql+aiomysql://", "mysql://") + ) + + config.set_main_option("sqlalchemy.url", database_url) + command.upgrade(config, "head") diff --git a/electron/servers/fastapi/placeholder b/electron/servers/fastapi/placeholder new file mode 100644 index 0000000000000000000000000000000000000000..aa73729d84185a6c6e30386b3f7d5b2846669bf4 GIT binary patch literal 12288 zcmeI#KTE?v7zXgWDry2D-MYSGLPXHcf@cU=jH$gwq0^Dp2*Lc(HZ5Is^CS7q9L*IA zlBJVNd0w~+PmViszfA9V6lQfw&(*w0YU;Bt&uMp<={@p3Ec)0a{ip>n(k%is5kOB%gU#8y?D!;E?y!Dq!l1q5t34--~-e00Izz00bZa0SG_< L0uX=z1U3RccL-Qe literal 0 HcmV?d00001 diff --git a/electron/servers/fastapi/pyproject.toml b/electron/servers/fastapi/pyproject.toml index 286cfac3..18efd476 100644 --- a/electron/servers/fastapi/pyproject.toml +++ b/electron/servers/fastapi/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" description = "Add your description here" requires-python = ">=3.11,<3.12" dependencies = [ + "alembic>=1.14.0", "aiohttp>=3.12.15", "aiomysql>=0.2.0", "aiosqlite>=0.21.0", diff --git a/electron/servers/fastapi/server.spec b/electron/servers/fastapi/server.spec index 940ac1e4..7fd17588 100644 --- a/electron/servers/fastapi/server.spec +++ b/electron/servers/fastapi/server.spec @@ -41,9 +41,11 @@ a = Analysis( datas=[ ('assets', 'assets'), ('static', 'static'), + ('alembic', 'alembic'), ] + datas_fastembed_cache + datas_fastembed + datas_fastembed_vs + datas_onnx + datas_pptx + datas_docx2everything + datas_greenlet + datas_docling + datas_docling_core + datas_docling_parse + datas_docling_ibm + datas_docx, hiddenimports=[ 'aiosqlite', + 'alembic', 'sqlite3', 'numpy', 'pandas', diff --git a/electron/servers/fastapi/services/pptx_presentation_creator.py b/electron/servers/fastapi/services/pptx_presentation_creator.py index cc939820..b27c6145 100644 --- a/electron/servers/fastapi/services/pptx_presentation_creator.py +++ b/electron/servers/fastapi/services/pptx_presentation_creator.py @@ -1,10 +1,11 @@ import os from typing import List, Optional -from urllib.parse import unquote from lxml import etree from services.html_to_text_runs_service import ( parse_html_text_to_text_runs as parse_inline_html_to_runs, ) +import tempfile +import zipfile from pptx import Presentation from pptx.shapes.autoshape import Shape @@ -17,7 +18,6 @@ from pptx.oxml.xmlchemy import OxmlElement from pptx.util import Pt from pptx.dml.color import RGBColor -from utils.path_helpers import get_resource_path from models.pptx_models import ( PptxAutoShapeBoxModel, @@ -50,29 +50,6 @@ import uuid BLANK_SLIDE_LAYOUT = 6 -def _http_url_to_local_path(image_path: str) -> Optional[str]: - """ - If image_path is an HTTP(S) URL that actually points to a local file path - (e.g. http://127.0.0.1:40001/C:/Users/.../image.png or http://host/home/user/...), - return the resolved absolute path; otherwise return None. - """ - if not (image_path.startswith("http://") or image_path.startswith("https://")): - return None - parts = image_path.split("/", 3) - if len(parts) < 4: - return None - decoded = unquote(parts[3]) - # Windows: path after domain is like "C:/Users/..." or "D:/..." - if len(decoded) >= 2 and decoded[1] == ":": - potential_path = os.path.normpath(decoded) - else: - # Unix: path after domain is like "home/user/..." -> /home/user/... - potential_path = "/" + decoded if not decoded.startswith("/") else decoded - if os.path.isabs(potential_path) and os.path.exists(potential_path): - return potential_path - return None - - class PptxPresentationCreator: def __init__(self, ppt_model: PptxPresentationModel, temp_dir: str): self._temp_dir = temp_dir @@ -91,6 +68,140 @@ class PptxPresentationCreator: parent.append(element) return element + + def fix_keynote_compatibility(self, pptx_path: str): + """Patch pptx XML for stricter parsers like Keynote.""" + PRESENTATION_NS = "http://schemas.openxmlformats.org/presentationml/2006/main" + DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" + REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships" + NOTES_MASTER_REL_TYPE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" + ) + + def ensure_grp_sppr_xfrm(slide_path: str): + slide_tree = etree.parse(slide_path) + slide_root = slide_tree.getroot() + grp_sppr_elements = slide_root.findall( + f".//{{{PRESENTATION_NS}}}grpSpPr" + ) + changed = False + for grp_sppr in grp_sppr_elements: + xfrm = grp_sppr.find(f"{{{DRAWING_NS}}}xfrm") + if xfrm is None: + xfrm = etree.SubElement(grp_sppr, f"{{{DRAWING_NS}}}xfrm") + etree.SubElement(xfrm, f"{{{DRAWING_NS}}}off", x="0", y="0") + etree.SubElement(xfrm, f"{{{DRAWING_NS}}}ext", cx="0", cy="0") + etree.SubElement(xfrm, f"{{{DRAWING_NS}}}chOff", x="0", y="0") + etree.SubElement(xfrm, f"{{{DRAWING_NS}}}chExt", cx="0", cy="0") + changed = True + if changed: + slide_tree.write( + slide_path, + xml_declaration=True, + encoding="UTF-8", + standalone="yes", + ) + + with tempfile.TemporaryDirectory() as temp_dir: + extract_dir = os.path.join(temp_dir, "pptx_contents") + os.makedirs(extract_dir, exist_ok=True) + with zipfile.ZipFile(pptx_path, "r") as existing_zip: + existing_zip.extractall(extract_dir) + + ppt_dir = os.path.join(extract_dir, "ppt") + slides_dir = os.path.join(ppt_dir, "slides") + if os.path.isdir(slides_dir): + for file_name in os.listdir(slides_dir): + if file_name.endswith(".xml"): + ensure_grp_sppr_xfrm(os.path.join(slides_dir, file_name)) + + rels_path = os.path.join(ppt_dir, "_rels", "presentation.xml.rels") + presentation_path = os.path.join(ppt_dir, "presentation.xml") + if os.path.exists(rels_path) and os.path.exists(presentation_path): + rels_tree = etree.parse(rels_path) + rels_root = rels_tree.getroot() + rel_tag = f"{{{PACKAGE_REL_NS}}}Relationship" + notes_master_rel = None + existing_ids = set() + for rel in rels_root.findall(rel_tag): + rel_id = rel.get("Id") + if rel_id: + existing_ids.add(rel_id) + if rel.get("Type") == NOTES_MASTER_REL_TYPE: + notes_master_rel = rel + + notes_masters_dir = os.path.join(ppt_dir, "notesMasters") + has_notes_master = ( + os.path.isdir(notes_masters_dir) + and any( + name.endswith(".xml") for name in os.listdir(notes_masters_dir) + ) + ) + + if has_notes_master and notes_master_rel is None: + next_id = 1 + while f"rId{next_id}" in existing_ids: + next_id += 1 + notes_master_rel = etree.SubElement(rels_root, rel_tag) + notes_master_rel.set("Id", f"rId{next_id}") + notes_master_rel.set("Type", NOTES_MASTER_REL_TYPE) + notes_master_rel.set( + "Target", "notesMasters/notesMaster1.xml" + ) + rels_tree.write( + rels_path, + xml_declaration=True, + encoding="UTF-8", + standalone="yes", + ) + + if has_notes_master and notes_master_rel is not None: + presentation_tree = etree.parse(presentation_path) + presentation_root = presentation_tree.getroot() + notes_master_id_lst = presentation_root.find( + f"{{{PRESENTATION_NS}}}notesMasterIdLst" + ) + if notes_master_id_lst is None: + notes_master_id_lst = etree.Element( + f"{{{PRESENTATION_NS}}}notesMasterIdLst" + ) + sld_master_id_lst = presentation_root.find( + f"{{{PRESENTATION_NS}}}sldMasterIdLst" + ) + if sld_master_id_lst is not None: + insert_index = list(presentation_root).index( + sld_master_id_lst + ) + 1 + presentation_root.insert(insert_index, notes_master_id_lst) + else: + presentation_root.insert(0, notes_master_id_lst) + if not notes_master_id_lst.findall( + f"{{{PRESENTATION_NS}}}notesMasterId" + ): + notes_master_id = etree.SubElement( + notes_master_id_lst, + f"{{{PRESENTATION_NS}}}notesMasterId", + ) + notes_master_id.set( + f"{{{REL_NS}}}id", + notes_master_rel.get("Id"), + ) + presentation_tree.write( + presentation_path, + xml_declaration=True, + encoding="UTF-8", + standalone="yes", + ) + + with zipfile.ZipFile(pptx_path, "w", zipfile.ZIP_DEFLATED) as new_zip: + for root, _, files in os.walk(extract_dir): + for file_name in files: + full_path = os.path.join(root, file_name) + archive_name = os.path.relpath(full_path, extract_dir) + new_zip.write(full_path, archive_name) + + async def fetch_network_assets(self): image_urls = [] models_with_network_asset: List[PptxPictureBoxModel] = [] @@ -99,28 +210,6 @@ class PptxPresentationCreator: for each_shape in self._ppt_model.shapes: if isinstance(each_shape, PptxPictureBoxModel): image_path = each_shape.picture.path - - # Handle file:// URLs by converting to local path - if image_path.startswith("file://"): - image_path = image_path.replace("file:///", "") - # URL-decode the path (e.g. %20 -> space for macOS "Application Support") - image_path = unquote(image_path) - # 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 - each_shape.picture.path = image_path - each_shape.picture.is_network = False - continue - - # Handle /static/ URLs by converting to file path - if image_path.startswith("/static/"): - relative_static_path = image_path[len("/static/"):] - local_path = get_resource_path(os.path.join("static", relative_static_path)) - each_shape.picture.path = local_path - each_shape.picture.is_network = False - print(f"[PPTX] Converted /static/ URL: {image_path} -> {local_path}") - continue - if image_path.startswith("http"): if "app_data/" in image_path: relative_path = image_path.split("app_data/")[1] @@ -129,24 +218,6 @@ class PptxPresentationCreator: ) each_shape.picture.is_network = False continue - - # Handle HTTP URLs that point to local files (from Next.js server) - # Example: http://127.0.0.1:40001/home/user/.config/presenton/images/file.png - # Should become: /home/user/.config/presenton/images/file.png - if image_path.startswith('http://') or image_path.startswith('https://'): - # Extract the path after the domain - # Format: http://domain:port/actual/file/path - parts = image_path.split('/', 3) # ['http:', '', 'domain:port', 'actual/file/path'] - if len(parts) >= 4: - # Get the part after the domain and URL-decode it - potential_path = '/' + unquote(parts[3]) - # Check if it's an absolute file path - if os.path.isabs(potential_path) and os.path.exists(potential_path): - each_shape.picture.path = potential_path - each_shape.picture.is_network = False - print(f"[PPTX] Converted HTTP URL to local path: {image_path} -> {potential_path}") - continue - image_urls.append(image_path) models_with_network_asset.append(each_shape) @@ -154,34 +225,6 @@ class PptxPresentationCreator: for each_shape in each_slide.shapes: if isinstance(each_shape, PptxPictureBoxModel): image_path = each_shape.picture.path - - print(f"[PPTX] Processing image path: {image_path}") - - # Handle file:// URLs by converting to local path - if image_path.startswith("file://"): - original_path = image_path - image_path = image_path.replace("file:///", "") - # URL-decode the path (e.g. %20 -> space for macOS "Application Support") - image_path = unquote(image_path) - # 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 - each_shape.picture.path = image_path - each_shape.picture.is_network = False - print(f"[PPTX] Converted file:// URL: {original_path} -> {image_path}") - print(f"[PPTX] File exists after conversion: {os.path.exists(image_path)}") - continue - - # Handle /static/ URLs by converting to file path - if image_path.startswith("/static/"): - relative_static_path = image_path[len("/static/"):] - local_path = get_resource_path(os.path.join("static", relative_static_path)) - each_shape.picture.path = local_path - each_shape.picture.is_network = False - print(f"[PPTX] Converted /static/ URL: {image_path} -> {local_path}") - print(f"[PPTX] File exists after conversion: {os.path.exists(local_path)}") - continue - if image_path.startswith("http"): if "app_data" in image_path: relative_path = image_path.split("app_data/")[1] @@ -190,16 +233,6 @@ class PptxPresentationCreator: ) each_shape.picture.is_network = False continue - - # Handle HTTP URLs that point to local files (from Next.js server) - # e.g. http://127.0.0.1:40001/C:/Users/.../image.png (Windows) or http://host/home/user/... (Unix) - local_path = _http_url_to_local_path(image_path) - if local_path is not None: - each_shape.picture.path = local_path - each_shape.picture.is_network = False - print(f"[PPTX] Converted HTTP URL to local path: {image_path} -> {local_path}") - continue - image_urls.append(image_path) models_with_network_asset.append(each_shape) @@ -245,8 +278,6 @@ class PptxPresentationCreator: def add_and_populate_slide(self, slide_model: PptxSlideModel): slide = self._ppt.slides.add_slide(self._ppt.slide_layouts[BLANK_SLIDE_LAYOUT]) - - print(f"[PPTX] Adding slide with {len(slide_model.shapes)} shapes") if slide_model.background: self.apply_fill_to_shape(slide.background, slide_model.background) @@ -256,8 +287,6 @@ class PptxPresentationCreator: for shape_model in slide_model.shapes: model_type = type(shape_model) - - print(f"[PPTX] Processing shape type: {model_type.__name__}") if model_type is PptxPictureBoxModel: self.add_picture(slide, shape_model) @@ -283,15 +312,6 @@ class PptxPresentationCreator: def add_picture(self, slide: Slide, picture_model: PptxPictureBoxModel): image_path = picture_model.picture.path - - # Handle HTTP URLs that point to local files (e.g. Windows: http://host/C:/Users/.../image.png) - resolved = _http_url_to_local_path(image_path) - if resolved is not None: - image_path = resolved - - print(f"[PPTX] Adding picture: {image_path}") - print(f"[PPTX] File exists: {os.path.exists(image_path)}") - if ( picture_model.clip or picture_model.border_radius @@ -302,9 +322,8 @@ class PptxPresentationCreator: ): try: image = Image.open(image_path) - except Exception as e: - print(f"[PPTX] ERROR: Could not open image: {image_path}") - print(f"[PPTX] ERROR: Exception: {e}") + except Exception: + print(f"Could not open image: {image_path}") return image = image.convert("RGBA") @@ -339,13 +358,7 @@ class PptxPresentationCreator: picture_model.position, picture_model.margin ) - try: - slide.shapes.add_picture(image_path, *margined_position.to_pt_list()) - print(f"[PPTX] Successfully added picture to slide") - except Exception as e: - print(f"[PPTX] ERROR: Failed to add picture to slide: {e}") - print(f"[PPTX] ERROR: Image path: {image_path}") - print(f"[PPTX] ERROR: File exists: {os.path.exists(image_path)}") + slide.shapes.add_picture(image_path, *margined_position.to_pt_list()) def add_autoshape(self, slide: Slide, autoshape_box_model: PptxAutoShapeBoxModel): position = autoshape_box_model.position @@ -607,3 +620,4 @@ class PptxPresentationCreator: def save(self, path: str): self._ppt.save(path) + self.fix_keynote_compatibility(path) diff --git a/electron/servers/fastapi/utils/get_env.py b/electron/servers/fastapi/utils/get_env.py index e7454f87..74cf2e1f 100644 --- a/electron/servers/fastapi/utils/get_env.py +++ b/electron/servers/fastapi/utils/get_env.py @@ -138,3 +138,7 @@ def get_codex_account_id_env(): def get_codex_model_env(): return os.getenv("CODEX_MODEL") + + +def get_migrate_database_on_startup_env(): + return os.getenv("MIGRATE_DATABASE_ON_STARTUP") diff --git a/electron/servers/fastapi/uv.lock b/electron/servers/fastapi/uv.lock index 684fd397..e31671a0 100644 --- a/electron/servers/fastapi/uv.lock +++ b/electron/servers/fastapi/uv.lock @@ -106,6 +106,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, ] +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + [[package]] name = "altgraph" version = "0.17.5" @@ -1129,6 +1143,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, ] +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -1655,6 +1681,7 @@ dependencies = [ { name = "aiohttp" }, { name = "aiomysql" }, { name = "aiosqlite" }, + { name = "alembic" }, { name = "anthropic" }, { name = "asyncpg" }, { name = "dirtyjson" }, @@ -1681,6 +1708,7 @@ requires-dist = [ { name = "aiohttp", specifier = ">=3.12.15" }, { name = "aiomysql", specifier = ">=0.2.0" }, { name = "aiosqlite", specifier = ">=0.21.0" }, + { name = "alembic", specifier = ">=1.14.0" }, { name = "anthropic", specifier = ">=0.60.0" }, { name = "asyncpg", specifier = ">=0.30.0" }, { name = "dirtyjson", specifier = ">=1.0.8" }, diff --git a/electron/version.json b/electron/version.json index fe7799fb..20020a9c 100644 --- a/electron/version.json +++ b/electron/version.json @@ -1,8 +1,8 @@ { - "version": "0.6.1-beta", + "version": "0.6.0-beta", "downloads": { - "linux": "https://github.com/presenton/presenton/releases/download/electron-v0.6.1-beta/Presenton-0.6.1-beta.deb", - "mac": "https://github.com/presenton/presenton/releases/download/electron-v0.6.1-beta/Presenton-0.6.1-beta.dmg", - "windows": "https://github.com/presenton/presenton/releases/download/electron-v0.6.1-beta/Presenton-0.6.1-beta.exe" + "linux": "https://github.com/presenton/presenton/releases/download/electron-v0.6.0-beta/Presenton-0.6.0-beta.deb", + "mac": "https://github.com/presenton/presenton/releases/download/electron-v0.6.0-beta/Presenton-0.6.0-beta.dmg", + "windows": "https://github.com/presenton/presenton/releases/download/electron-v0.6.0-beta/Presenton-0.6.0-beta.exe" } } \ No newline at end of file diff --git a/servers/fastapi/alembic.ini b/servers/fastapi/alembic.ini new file mode 100644 index 00000000..816710ec --- /dev/null +++ b/servers/fastapi/alembic.ini @@ -0,0 +1,41 @@ +# Alembic configuration for CLI usage (e.g. alembic revision --autogenerate) +# migrations.py sets these programmatically when running at app startup. + +[alembic] +script_location = alembic +# sqlalchemy.url is overridden by env.py (uses DATABASE_URL or APP_DATA_DIRECTORY) +sqlalchemy.url = sqlite:///placeholder + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARNING +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARNING +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/servers/fastapi/alembic/env.py b/servers/fastapi/alembic/env.py new file mode 100644 index 00000000..8f77371d --- /dev/null +++ b/servers/fastapi/alembic/env.py @@ -0,0 +1,94 @@ +import os +import sys +from logging.config import fileConfig +from pathlib import Path + +from alembic import context +from sqlalchemy import engine_from_config, pool +from sqlmodel import SQLModel + +# Make sure all models can be imported when alembic runs standalone. +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +# Import every SQL model so they register with SQLModel.metadata before +# autogenerate or migration execution reads it. +from models.sql.async_presentation_generation_status import ( # noqa: F401, E402 + AsyncPresentationGenerationTaskModel, +) +from models.sql.image_asset import ImageAsset # noqa: F401, E402 +from models.sql.key_value import KeyValueSqlModel # noqa: F401, E402 +from models.sql.ollama_pull_status import OllamaPullStatus # noqa: F401, E402 +from models.sql.presentation import PresentationModel # noqa: F401, E402 +from models.sql.presentation_layout_code import ( # noqa: F401, E402 + PresentationLayoutCodeModel, +) +from models.sql.slide import SlideModel # noqa: F401, E402 +from models.sql.template import TemplateModel # noqa: F401, E402 +from models.sql.webhook_subscription import WebhookSubscription # noqa: F401, E402 + +alembic_config = context.config + +if alembic_config.config_file_name is not None: + fileConfig(alembic_config.config_file_name) + +target_metadata = SQLModel.metadata + + +def _get_url() -> str: + """ + Prefer the URL injected by migrations.py via config.set_main_option, + falling back to the DATABASE_URL environment variable or a local SQLite DB. + """ + configured = alembic_config.get_main_option("sqlalchemy.url") + if configured: + return configured + + from utils.db_utils import get_database_url_and_connect_args + + url, _ = get_database_url_and_connect_args() + return ( + url + .replace("sqlite+aiosqlite://", "sqlite:///") + .replace("postgresql+asyncpg://", "postgresql://") + .replace("mysql+aiomysql://", "mysql://") + ) + + +def run_migrations_offline() -> None: + """Generate SQL script without connecting to the database.""" + url = _get_url() + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations against the live database.""" + configuration = dict(alembic_config.get_section(alembic_config.config_ini_section) or {}) + configuration["sqlalchemy.url"] = _get_url() + + connectable = engine_from_config( + configuration, + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata, + compare_type=True, + ) + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/servers/fastapi/alembic/script.py.mako b/servers/fastapi/alembic/script.py.mako new file mode 100644 index 00000000..6ce33510 --- /dev/null +++ b/servers/fastapi/alembic/script.py.mako @@ -0,0 +1,27 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/servers/fastapi/alembic/versions/.gitkeep b/servers/fastapi/alembic/versions/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/servers/fastapi/alembic/versions/fd2ab04834cc_init.py b/servers/fastapi/alembic/versions/fd2ab04834cc_init.py new file mode 100644 index 00000000..cc863720 --- /dev/null +++ b/servers/fastapi/alembic/versions/fd2ab04834cc_init.py @@ -0,0 +1,136 @@ +"""init + +Revision ID: fd2ab04834cc +Revises: +Create Date: 2026-03-08 19:10:59.637680 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +import sqlmodel + + +# revision identifiers, used by Alembic. +revision: str = 'fd2ab04834cc' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('async_presentation_generation_tasks', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('status', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('message', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('error', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.Column('updated_at', sa.DateTime(), nullable=False), + sa.Column('data', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('imageasset', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('is_uploaded', sa.Boolean(), nullable=False), + sa.Column('path', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('extras', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('keyvaluesqlmodel', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('key', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('value', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_keyvaluesqlmodel_key'), 'keyvaluesqlmodel', ['key'], unique=False) + op.create_table('ollamapullstatus', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('last_updated', sa.DateTime(), nullable=True), + sa.Column('status', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('presentation_layout_codes', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('presentation', sa.Uuid(), nullable=False), + sa.Column('layout_id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('layout_name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('layout_code', sa.Text(), nullable=True), + sa.Column('fonts', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_presentation_layout_codes_presentation'), 'presentation_layout_codes', ['presentation'], unique=False) + op.create_table('presentations', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('content', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('n_slides', sa.Integer(), nullable=False), + sa.Column('language', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('file_paths', sa.JSON(), nullable=True), + sa.Column('outlines', sa.JSON(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('layout', sa.JSON(), nullable=True), + sa.Column('structure', sa.JSON(), nullable=True), + sa.Column('theme', sa.JSON(), nullable=True), + sa.Column('instructions', sa.String(), nullable=True), + sa.Column('tone', sa.String(), nullable=True), + sa.Column('verbosity', sa.String(), nullable=True), + sa.Column('include_table_of_contents', sa.Boolean(), nullable=True), + sa.Column('include_title_slide', sa.Boolean(), nullable=True), + sa.Column('web_search', sa.Boolean(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('templates', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('name', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('description', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('webhook_subscriptions', + sa.Column('id', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('url', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('secret', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('event', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_webhook_subscriptions_event'), 'webhook_subscriptions', ['event'], unique=False) + op.create_table('slides', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('presentation', sa.Uuid(), nullable=True), + sa.Column('layout_group', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('layout', sqlmodel.sql.sqltypes.AutoString(), nullable=False), + sa.Column('index', sa.Integer(), nullable=False), + sa.Column('content', sa.JSON(), nullable=True), + sa.Column('html_content', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('speaker_note', sqlmodel.sql.sqltypes.AutoString(), nullable=True), + sa.Column('properties', sa.JSON(), nullable=True), + sa.ForeignKeyConstraint(['presentation'], ['presentations.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_slides_presentation'), 'slides', ['presentation'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_slides_presentation'), table_name='slides') + op.drop_table('slides') + op.drop_index(op.f('ix_webhook_subscriptions_event'), table_name='webhook_subscriptions') + op.drop_table('webhook_subscriptions') + op.drop_table('templates') + op.drop_table('presentations') + op.drop_index(op.f('ix_presentation_layout_codes_presentation'), table_name='presentation_layout_codes') + op.drop_table('presentation_layout_codes') + op.drop_table('ollamapullstatus') + op.drop_index(op.f('ix_keyvaluesqlmodel_key'), table_name='keyvaluesqlmodel') + op.drop_table('keyvaluesqlmodel') + op.drop_table('imageasset') + op.drop_table('async_presentation_generation_tasks') + # ### end Alembic commands ### diff --git a/servers/fastapi/api/lifespan.py b/servers/fastapi/api/lifespan.py index 86f55379..6fe4e6c4 100644 --- a/servers/fastapi/api/lifespan.py +++ b/servers/fastapi/api/lifespan.py @@ -3,6 +3,7 @@ import os from fastapi import FastAPI +from migrations import migrate_database_on_startup from services.database import create_db_and_tables from utils.get_env import get_app_data_directory_env from utils.model_availability import ( @@ -14,10 +15,12 @@ from utils.model_availability import ( async def app_lifespan(_: FastAPI): """ Lifespan context manager for FastAPI application. - Initializes the application data directory and checks LLM model availability. - + Initializes the application data directory, runs Alembic migrations when + MIGRATE_DATABASE_ON_STARTUP=true, creates any missing tables, and checks + LLM model availability. """ os.makedirs(get_app_data_directory_env(), exist_ok=True) + await migrate_database_on_startup() await create_db_and_tables() await check_llm_and_image_provider_api_or_model_availability() yield diff --git a/servers/fastapi/migrations.py b/servers/fastapi/migrations.py new file mode 100644 index 00000000..0b2b578e --- /dev/null +++ b/servers/fastapi/migrations.py @@ -0,0 +1,40 @@ +import asyncio +from pathlib import Path + +from alembic import command +from alembic.config import Config + +from utils.db_utils import get_database_url_and_connect_args +from utils.get_env import get_migrate_database_on_startup_env + + +async def migrate_database_on_startup() -> None: + if get_migrate_database_on_startup_env() not in ["true", "True"]: + return + + try: + await asyncio.to_thread(_run_migrations) + print("Migrations run successfully", flush=True) + except Exception as exc: + print(f"Error running migrations: {exc}", flush=True) + + +def _run_migrations() -> None: + # migrations.py lives at servers/fastapi/migrations.py + # so parents[0] = servers/fastapi/, where alembic/ lives alongside it. + base_dir = Path(__file__).resolve().parents[0] + config = Config() + config.set_main_option("script_location", str(base_dir / "alembic")) + + database_url, _ = get_database_url_and_connect_args() + + # Alembic uses synchronous engines; strip async driver prefixes. + database_url = ( + database_url + .replace("sqlite+aiosqlite://", "sqlite:///") + .replace("postgresql+asyncpg://", "postgresql://") + .replace("mysql+aiomysql://", "mysql://") + ) + + config.set_main_option("sqlalchemy.url", database_url) + command.upgrade(config, "head") diff --git a/servers/fastapi/placeholder b/servers/fastapi/placeholder new file mode 100644 index 0000000000000000000000000000000000000000..aa73729d84185a6c6e30386b3f7d5b2846669bf4 GIT binary patch literal 12288 zcmeI#KTE?v7zXgWDry2D-MYSGLPXHcf@cU=jH$gwq0^Dp2*Lc(HZ5Is^CS7q9L*IA zlBJVNd0w~+PmViszfA9V6lQfw&(*w0YU;Bt&uMp<={@p3Ec)0a{ip>n(k%is5kOB%gU#8y?D!;E?y!Dq!l1q5t34--~-e00Izz00bZa0SG_< L0uX=z1U3RccL-Qe literal 0 HcmV?d00001 diff --git a/servers/fastapi/pyproject.toml b/servers/fastapi/pyproject.toml index c76aecaf..71922634 100644 --- a/servers/fastapi/pyproject.toml +++ b/servers/fastapi/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" description = "Add your description here" requires-python = ">=3.11,<3.12" dependencies = [ + "alembic>=1.14.0", "aiohttp>=3.12.15", "aiomysql>=0.2.0", "aiosqlite>=0.21.0", @@ -25,6 +26,9 @@ dependencies = [ "sqlmodel>=0.0.24", ] +[tool.uv] +index-strategy = "unsafe-best-match" + [[tool.uv.index]] url = "https://download.pytorch.org/whl/cpu" diff --git a/servers/fastapi/services/pptx_presentation_creator.py b/servers/fastapi/services/pptx_presentation_creator.py index 26b20b27..b27c6145 100644 --- a/servers/fastapi/services/pptx_presentation_creator.py +++ b/servers/fastapi/services/pptx_presentation_creator.py @@ -4,6 +4,8 @@ from lxml import etree from services.html_to_text_runs_service import ( parse_html_text_to_text_runs as parse_inline_html_to_runs, ) +import tempfile +import zipfile from pptx import Presentation from pptx.shapes.autoshape import Shape @@ -66,6 +68,140 @@ class PptxPresentationCreator: parent.append(element) return element + + def fix_keynote_compatibility(self, pptx_path: str): + """Patch pptx XML for stricter parsers like Keynote.""" + PRESENTATION_NS = "http://schemas.openxmlformats.org/presentationml/2006/main" + DRAWING_NS = "http://schemas.openxmlformats.org/drawingml/2006/main" + REL_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + PACKAGE_REL_NS = "http://schemas.openxmlformats.org/package/2006/relationships" + NOTES_MASTER_REL_TYPE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" + ) + + def ensure_grp_sppr_xfrm(slide_path: str): + slide_tree = etree.parse(slide_path) + slide_root = slide_tree.getroot() + grp_sppr_elements = slide_root.findall( + f".//{{{PRESENTATION_NS}}}grpSpPr" + ) + changed = False + for grp_sppr in grp_sppr_elements: + xfrm = grp_sppr.find(f"{{{DRAWING_NS}}}xfrm") + if xfrm is None: + xfrm = etree.SubElement(grp_sppr, f"{{{DRAWING_NS}}}xfrm") + etree.SubElement(xfrm, f"{{{DRAWING_NS}}}off", x="0", y="0") + etree.SubElement(xfrm, f"{{{DRAWING_NS}}}ext", cx="0", cy="0") + etree.SubElement(xfrm, f"{{{DRAWING_NS}}}chOff", x="0", y="0") + etree.SubElement(xfrm, f"{{{DRAWING_NS}}}chExt", cx="0", cy="0") + changed = True + if changed: + slide_tree.write( + slide_path, + xml_declaration=True, + encoding="UTF-8", + standalone="yes", + ) + + with tempfile.TemporaryDirectory() as temp_dir: + extract_dir = os.path.join(temp_dir, "pptx_contents") + os.makedirs(extract_dir, exist_ok=True) + with zipfile.ZipFile(pptx_path, "r") as existing_zip: + existing_zip.extractall(extract_dir) + + ppt_dir = os.path.join(extract_dir, "ppt") + slides_dir = os.path.join(ppt_dir, "slides") + if os.path.isdir(slides_dir): + for file_name in os.listdir(slides_dir): + if file_name.endswith(".xml"): + ensure_grp_sppr_xfrm(os.path.join(slides_dir, file_name)) + + rels_path = os.path.join(ppt_dir, "_rels", "presentation.xml.rels") + presentation_path = os.path.join(ppt_dir, "presentation.xml") + if os.path.exists(rels_path) and os.path.exists(presentation_path): + rels_tree = etree.parse(rels_path) + rels_root = rels_tree.getroot() + rel_tag = f"{{{PACKAGE_REL_NS}}}Relationship" + notes_master_rel = None + existing_ids = set() + for rel in rels_root.findall(rel_tag): + rel_id = rel.get("Id") + if rel_id: + existing_ids.add(rel_id) + if rel.get("Type") == NOTES_MASTER_REL_TYPE: + notes_master_rel = rel + + notes_masters_dir = os.path.join(ppt_dir, "notesMasters") + has_notes_master = ( + os.path.isdir(notes_masters_dir) + and any( + name.endswith(".xml") for name in os.listdir(notes_masters_dir) + ) + ) + + if has_notes_master and notes_master_rel is None: + next_id = 1 + while f"rId{next_id}" in existing_ids: + next_id += 1 + notes_master_rel = etree.SubElement(rels_root, rel_tag) + notes_master_rel.set("Id", f"rId{next_id}") + notes_master_rel.set("Type", NOTES_MASTER_REL_TYPE) + notes_master_rel.set( + "Target", "notesMasters/notesMaster1.xml" + ) + rels_tree.write( + rels_path, + xml_declaration=True, + encoding="UTF-8", + standalone="yes", + ) + + if has_notes_master and notes_master_rel is not None: + presentation_tree = etree.parse(presentation_path) + presentation_root = presentation_tree.getroot() + notes_master_id_lst = presentation_root.find( + f"{{{PRESENTATION_NS}}}notesMasterIdLst" + ) + if notes_master_id_lst is None: + notes_master_id_lst = etree.Element( + f"{{{PRESENTATION_NS}}}notesMasterIdLst" + ) + sld_master_id_lst = presentation_root.find( + f"{{{PRESENTATION_NS}}}sldMasterIdLst" + ) + if sld_master_id_lst is not None: + insert_index = list(presentation_root).index( + sld_master_id_lst + ) + 1 + presentation_root.insert(insert_index, notes_master_id_lst) + else: + presentation_root.insert(0, notes_master_id_lst) + if not notes_master_id_lst.findall( + f"{{{PRESENTATION_NS}}}notesMasterId" + ): + notes_master_id = etree.SubElement( + notes_master_id_lst, + f"{{{PRESENTATION_NS}}}notesMasterId", + ) + notes_master_id.set( + f"{{{REL_NS}}}id", + notes_master_rel.get("Id"), + ) + presentation_tree.write( + presentation_path, + xml_declaration=True, + encoding="UTF-8", + standalone="yes", + ) + + with zipfile.ZipFile(pptx_path, "w", zipfile.ZIP_DEFLATED) as new_zip: + for root, _, files in os.walk(extract_dir): + for file_name in files: + full_path = os.path.join(root, file_name) + archive_name = os.path.relpath(full_path, extract_dir) + new_zip.write(full_path, archive_name) + + async def fetch_network_assets(self): image_urls = [] models_with_network_asset: List[PptxPictureBoxModel] = [] @@ -484,3 +620,4 @@ class PptxPresentationCreator: def save(self, path: str): self._ppt.save(path) + self.fix_keynote_compatibility(path) diff --git a/servers/fastapi/utils/get_env.py b/servers/fastapi/utils/get_env.py index e7454f87..74cf2e1f 100644 --- a/servers/fastapi/utils/get_env.py +++ b/servers/fastapi/utils/get_env.py @@ -138,3 +138,7 @@ def get_codex_account_id_env(): def get_codex_model_env(): return os.getenv("CODEX_MODEL") + + +def get_migrate_database_on_startup_env(): + return os.getenv("MIGRATE_DATABASE_ON_STARTUP") diff --git a/servers/fastapi/uv.lock b/servers/fastapi/uv.lock index d3dcccac..f21c3a60 100644 --- a/servers/fastapi/uv.lock +++ b/servers/fastapi/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = "==3.11.*" resolution-markers = [ "platform_machine == 'aarch64' and sys_platform == 'linux'", @@ -106,6 +106,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f5/10/6c25ed6de94c49f88a91fa5018cb4c0f3625f31d5be9f771ebe5cc7cd506/aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0", size = 15792, upload-time = "2025-02-03T07:30:13.6Z" }, ] +[[package]] +name = "alembic" +version = "1.18.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/13/8b084e0f2efb0275a1d534838844926f798bd766566b1375174e2448cd31/alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc", size = 2056725, upload-time = "2026-02-10T16:00:47.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/29/6533c317b74f707ea28f8d633734dbda2119bbadfc61b2f3640ba835d0f7/alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a", size = 263893, upload-time = "2026-02-10T16:00:49.997Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -781,10 +795,10 @@ wheels = [ [[package]] name = "filelock" version = "3.18.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +source = { registry = "https://download.pytorch.org/whl/cpu" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de" }, ] [[package]] @@ -834,10 +848,10 @@ wheels = [ [[package]] name = "fsspec" version = "2025.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/02/0835e6ab9cfc03916fe3f78c0956cfcdb6ff2669ffa6651065d5ebf7fc98/fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58", size = 304432, upload-time = "2025-07-15T16:05:21.19Z" } +source = { registry = "https://download.pytorch.org/whl/cpu" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/02/0835e6ab9cfc03916fe3f78c0956cfcdb6ff2669ffa6651065d5ebf7fc98/fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21", size = 199597, upload-time = "2025-07-15T16:05:19.529Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e0/014d5d9d7a4564cf1c40b5039bc882db69fd881111e03ab3657ac0b218e2/fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21" }, ] [[package]] @@ -894,7 +908,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fc/2e/d4fcb2978f826358b673f779f78fa8a32ee37df11920dc2bb5589cbeecef/greenlet-3.2.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:784ae58bba89fa1fa5733d170d42486580cab9decda3484779f4759345b29822", size = 270219, upload-time = "2025-06-05T16:10:10.414Z" }, { url = "https://files.pythonhosted.org/packages/16/24/929f853e0202130e4fe163bc1d05a671ce8dcd604f790e14896adac43a52/greenlet-3.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0921ac4ea42a5315d3446120ad48f90c3a6b9bb93dd9b3cf4e4d84a66e42de83", size = 630383, upload-time = "2025-06-05T16:38:51.785Z" }, { url = "https://files.pythonhosted.org/packages/d1/b2/0320715eb61ae70c25ceca2f1d5ae620477d246692d9cc284c13242ec31c/greenlet-3.2.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d2971d93bb99e05f8c2c0c2f4aa9484a18d98c4c3bd3c62b65b7e6ae33dfcfaf", size = 642422, upload-time = "2025-06-05T16:41:35.259Z" }, - { url = "https://files.pythonhosted.org/packages/bd/49/445fd1a210f4747fedf77615d941444349c6a3a4a1135bba9701337cd966/greenlet-3.2.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c667c0bf9d406b77a15c924ef3285e1e05250948001220368e039b6aa5b5034b", size = 638375, upload-time = "2025-06-05T16:48:18.235Z" }, { url = "https://files.pythonhosted.org/packages/7e/c8/ca19760cf6eae75fa8dc32b487e963d863b3ee04a7637da77b616703bc37/greenlet-3.2.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:592c12fb1165be74592f5de0d70f82bc5ba552ac44800d632214b76089945147", size = 637627, upload-time = "2025-06-05T16:13:02.858Z" }, { url = "https://files.pythonhosted.org/packages/65/89/77acf9e3da38e9bcfca881e43b02ed467c1dedc387021fc4d9bd9928afb8/greenlet-3.2.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29e184536ba333003540790ba29829ac14bb645514fbd7e32af331e8202a62a5", size = 585502, upload-time = "2025-06-05T16:12:49.642Z" }, { url = "https://files.pythonhosted.org/packages/97/c6/ae244d7c95b23b7130136e07a9cc5aadd60d59b5951180dc7dc7e8edaba7/greenlet-3.2.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:93c0bb79844a367782ec4f429d07589417052e621aa39a5ac1fb99c5aa308edc", size = 1114498, upload-time = "2025-06-05T16:36:46.598Z" }, @@ -1091,13 +1104,12 @@ wheels = [ [[package]] name = "jinja2" version = "3.1.6" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://download.pytorch.org/whl/cpu" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, + { url = "https://download.pytorch.org/whl/jinja2-3.1.6-py3-none-any.whl" }, ] [[package]] @@ -1271,6 +1283,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197, upload-time = "2025-04-23T01:46:01.096Z" }, ] +[[package]] +name = "mako" +version = "1.3.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/38/bd5b78a920a64d708fe6bc8e0a2c075e1389d53bef8413725c63ba041535/mako-1.3.10.tar.gz", hash = "sha256:99579a6f39583fa7e5630a28c3c1f440e4e97a414b80372649c0ce338da2ea28", size = 392474, upload-time = "2025-04-10T12:44:31.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/fb/99f81ac72ae23375f22b7afdb7642aba97c00a713c217124420147681a2f/mako-1.3.10-py3-none-any.whl", hash = "sha256:baef24a52fc4fc514a0887ac600f9f1cff3d82c61d4d700a1fa84d597b88db59", size = 78509, upload-time = "2025-04-10T12:50:53.297Z" }, +] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -1397,8 +1421,9 @@ dill = [ name = "mpmath" version = "1.3.0" source = { registry = "https://download.pytorch.org/whl/cpu" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f" } wheels = [ - { url = "https://download.pytorch.org/whl/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c" }, + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c" }, ] [[package]] @@ -1449,10 +1474,10 @@ wheels = [ [[package]] name = "networkx" version = "3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +source = { registry = "https://download.pytorch.org/whl/cpu" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec" }, ] [[package]] @@ -1497,27 +1522,27 @@ wheels = [ [[package]] name = "numpy" version = "2.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } +source = { registry = "https://download.pytorch.org/whl/cpu" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48" } wheels = [ - { url = "https://files.pythonhosted.org/packages/96/26/1320083986108998bd487e2931eed2aeedf914b6e8905431487543ec911d/numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9", size = 21259016, upload-time = "2025-07-24T20:24:35.214Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2b/792b341463fa93fc7e55abbdbe87dac316c5b8cb5e94fb7a59fb6fa0cda5/numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168", size = 14451158, upload-time = "2025-07-24T20:24:58.397Z" }, - { url = "https://files.pythonhosted.org/packages/b7/13/e792d7209261afb0c9f4759ffef6135b35c77c6349a151f488f531d13595/numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b", size = 5379817, upload-time = "2025-07-24T20:25:07.746Z" }, - { url = "https://files.pythonhosted.org/packages/49/ce/055274fcba4107c022b2113a213c7287346563f48d62e8d2a5176ad93217/numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8", size = 6913606, upload-time = "2025-07-24T20:25:18.84Z" }, - { url = "https://files.pythonhosted.org/packages/17/f2/e4d72e6bc5ff01e2ab613dc198d560714971900c03674b41947e38606502/numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d", size = 14589652, upload-time = "2025-07-24T20:25:40.356Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b0/fbeee3000a51ebf7222016e2939b5c5ecf8000a19555d04a18f1e02521b8/numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3", size = 16938816, upload-time = "2025-07-24T20:26:05.721Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ec/2f6c45c3484cc159621ea8fc000ac5a86f1575f090cac78ac27193ce82cd/numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f", size = 16370512, upload-time = "2025-07-24T20:26:30.545Z" }, - { url = "https://files.pythonhosted.org/packages/b5/01/dd67cf511850bd7aefd6347aaae0956ed415abea741ae107834aae7d6d4e/numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097", size = 18884947, upload-time = "2025-07-24T20:26:58.24Z" }, - { url = "https://files.pythonhosted.org/packages/a7/17/2cf60fd3e6a61d006778735edf67a222787a8c1a7842aed43ef96d777446/numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220", size = 6599494, upload-time = "2025-07-24T20:27:09.786Z" }, - { url = "https://files.pythonhosted.org/packages/d5/03/0eade211c504bda872a594f045f98ddcc6caef2b7c63610946845e304d3f/numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170", size = 13087889, upload-time = "2025-07-24T20:27:29.558Z" }, - { url = "https://files.pythonhosted.org/packages/13/32/2c7979d39dafb2a25087e12310fc7f3b9d3c7d960df4f4bc97955ae0ce1d/numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89", size = 10459560, upload-time = "2025-07-24T20:27:46.803Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ea/50ebc91d28b275b23b7128ef25c3d08152bc4068f42742867e07a870a42a/numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15", size = 21130338, upload-time = "2025-07-24T20:57:54.37Z" }, - { url = "https://files.pythonhosted.org/packages/9f/57/cdd5eac00dd5f137277355c318a955c0d8fb8aa486020c22afd305f8b88f/numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec", size = 14375776, upload-time = "2025-07-24T20:58:16.303Z" }, - { url = "https://files.pythonhosted.org/packages/83/85/27280c7f34fcd305c2209c0cdca4d70775e4859a9eaa92f850087f8dea50/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712", size = 5304882, upload-time = "2025-07-24T20:58:26.199Z" }, - { url = "https://files.pythonhosted.org/packages/48/b4/6500b24d278e15dd796f43824e69939d00981d37d9779e32499e823aa0aa/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c", size = 6818405, upload-time = "2025-07-24T20:58:37.341Z" }, - { url = "https://files.pythonhosted.org/packages/9b/c9/142c1e03f199d202da8e980c2496213509291b6024fd2735ad28ae7065c7/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296", size = 14419651, upload-time = "2025-07-24T20:58:59.048Z" }, - { url = "https://files.pythonhosted.org/packages/8b/95/8023e87cbea31a750a6c00ff9427d65ebc5fef104a136bfa69f76266d614/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981", size = 16760166, upload-time = "2025-07-24T21:28:56.38Z" }, - { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619", size = 12977811, upload-time = "2025-07-24T21:29:18.234Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/1320083986108998bd487e2931eed2aeedf914b6e8905431487543ec911d/numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9" }, + { url = "https://files.pythonhosted.org/packages/c4/2b/792b341463fa93fc7e55abbdbe87dac316c5b8cb5e94fb7a59fb6fa0cda5/numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168" }, + { url = "https://files.pythonhosted.org/packages/b7/13/e792d7209261afb0c9f4759ffef6135b35c77c6349a151f488f531d13595/numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b" }, + { url = "https://files.pythonhosted.org/packages/49/ce/055274fcba4107c022b2113a213c7287346563f48d62e8d2a5176ad93217/numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8" }, + { url = "https://files.pythonhosted.org/packages/17/f2/e4d72e6bc5ff01e2ab613dc198d560714971900c03674b41947e38606502/numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d" }, + { url = "https://files.pythonhosted.org/packages/c8/b0/fbeee3000a51ebf7222016e2939b5c5ecf8000a19555d04a18f1e02521b8/numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3" }, + { url = "https://files.pythonhosted.org/packages/a9/ec/2f6c45c3484cc159621ea8fc000ac5a86f1575f090cac78ac27193ce82cd/numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f" }, + { url = "https://files.pythonhosted.org/packages/b5/01/dd67cf511850bd7aefd6347aaae0956ed415abea741ae107834aae7d6d4e/numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097" }, + { url = "https://files.pythonhosted.org/packages/a7/17/2cf60fd3e6a61d006778735edf67a222787a8c1a7842aed43ef96d777446/numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220" }, + { url = "https://files.pythonhosted.org/packages/d5/03/0eade211c504bda872a594f045f98ddcc6caef2b7c63610946845e304d3f/numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170" }, + { url = "https://files.pythonhosted.org/packages/13/32/2c7979d39dafb2a25087e12310fc7f3b9d3c7d960df4f4bc97955ae0ce1d/numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89" }, + { url = "https://files.pythonhosted.org/packages/cf/ea/50ebc91d28b275b23b7128ef25c3d08152bc4068f42742867e07a870a42a/numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15" }, + { url = "https://files.pythonhosted.org/packages/9f/57/cdd5eac00dd5f137277355c318a955c0d8fb8aa486020c22afd305f8b88f/numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec" }, + { url = "https://files.pythonhosted.org/packages/83/85/27280c7f34fcd305c2209c0cdca4d70775e4859a9eaa92f850087f8dea50/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712" }, + { url = "https://files.pythonhosted.org/packages/48/b4/6500b24d278e15dd796f43824e69939d00981d37d9779e32499e823aa0aa/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c" }, + { url = "https://files.pythonhosted.org/packages/9b/c9/142c1e03f199d202da8e980c2496213509291b6024fd2735ad28ae7065c7/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8023e87cbea31a750a6c00ff9427d65ebc5fef104a136bfa69f76266d614/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981" }, + { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619" }, ] [[package]] @@ -1858,27 +1883,27 @@ wheels = [ [[package]] name = "pillow" version = "11.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } +source = { registry = "https://download.pytorch.org/whl/cpu" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8" }, ] [[package]] @@ -1914,6 +1939,7 @@ dependencies = [ { name = "aiohttp" }, { name = "aiomysql" }, { name = "aiosqlite" }, + { name = "alembic" }, { name = "anthropic" }, { name = "asyncpg" }, { name = "chromadb" }, @@ -1937,6 +1963,7 @@ requires-dist = [ { name = "aiohttp", specifier = ">=3.12.15" }, { name = "aiomysql", specifier = ">=0.2.0" }, { name = "aiosqlite", specifier = ">=0.21.0" }, + { name = "alembic", specifier = ">=1.14.0" }, { name = "anthropic", specifier = ">=0.60.0" }, { name = "asyncpg", specifier = ">=0.30.0" }, { name = "chromadb", specifier = ">=1.0.15" }, @@ -2802,13 +2829,13 @@ wheels = [ [[package]] name = "sympy" version = "1.14.0" -source = { registry = "https://pypi.org/simple" } +source = { registry = "https://download.pytorch.org/whl/cpu" } dependencies = [ { name = "mpmath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5" }, ] [[package]] diff --git a/test_server.py b/test_server.py new file mode 100644 index 00000000..e1893671 --- /dev/null +++ b/test_server.py @@ -0,0 +1,78 @@ +""" +Presenton Version Server (test/dev stub) + +This simulates the remote version-check endpoint that the Electron app polls. +In production, replace UPDATE_SERVER_URL in the Electron app with your hosted URL. + +Usage: + python test_server.py [--port 8765] + +Endpoint: + GET /versions -> JSON with latest version and download info +""" + +import json +import argparse +from http.server import BaseHTTPRequestHandler, HTTPServer +from urllib.parse import urlparse + +VERSIONS = { + "latest": "0.7.0", + "versions": [ + "0.5.0", + "0.6.0", + "0.6.1-beta", + "0.7.0", + ], + "download_url": "https://github.com/presenton/presenton/releases/latest", + "release_notes": "Bug fixes, performance improvements, and new AI model support.", +} + + +class VersionHandler(BaseHTTPRequestHandler): + def do_GET(self): + parsed = urlparse(self.path) + + if parsed.path == "/versions": + body = json.dumps(VERSIONS, indent=2).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(body) + else: + self.send_response(404) + self.end_headers() + self.wfile.write(b'{"error": "Not found"}') + + def do_OPTIONS(self): + self.send_response(200) + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") + self.end_headers() + + def log_message(self, format, *args): + print(f"[VersionServer] {self.address_string()} - {format % args}", flush=True) + + +def main(): + parser = argparse.ArgumentParser(description="Presenton version check server") + parser.add_argument("--port", type=int, default=8765, help="Port to listen on") + parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to bind to") + args = parser.parse_args() + + server = HTTPServer((args.host, args.port), VersionHandler) + print(f"Presenton version server running at http://{args.host}:{args.port}", flush=True) + print(f" GET /versions -> version information", flush=True) + print(f" Current 'latest' set to: {VERSIONS['latest']}", flush=True) + print("Press Ctrl+C to stop.", flush=True) + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nServer stopped.", flush=True) + + +if __name__ == "__main__": + main() From 46044735c19ea943eaf2b9e96e5d4215fdc02002 Mon Sep 17 00:00:00 2001 From: sudipnext Date: Sun, 8 Mar 2026 20:19:32 +0545 Subject: [PATCH 2/7] feat: enhance update notification system with detailed changelog and message support --- electron/app/utils/update-checker.ts | 101 ++++++++++++++++++--------- electron/generate_update.js | 5 ++ electron/version.json | 1 + 3 files changed, 74 insertions(+), 33 deletions(-) diff --git a/electron/app/utils/update-checker.ts b/electron/app/utils/update-checker.ts index c2caf67d..1abddc63 100644 --- a/electron/app/utils/update-checker.ts +++ b/electron/app/utils/update-checker.ts @@ -32,6 +32,7 @@ function log(msg: string): void { interface VersionResponse { version: string; + message?: string; downloads: { linux: string; mac: string; @@ -91,7 +92,7 @@ async function fetchVersionInfo(): Promise { } /** Pending update to re-inject on navigation (production: React/Next.js may replace DOM). */ -let pendingUpdate: { version: string; downloadUrl: string } | null = null; +let pendingUpdate: { version: string; downloadUrl: string; message?: string } | null = null; /** * Schedules banner injection after INJECT_DELAY_MS so React/Next.js can mount first. @@ -100,79 +101,113 @@ let pendingUpdate: { version: string; downloadUrl: string } | null = null; function scheduleBannerInjection( win: BrowserWindow, version: string, - downloadUrl: string + downloadUrl: string, + message?: string ): void { - pendingUpdate = { version, downloadUrl }; + pendingUpdate = { version, downloadUrl, message }; setTimeout(() => { if (win.isDestroyed() || !pendingUpdate) return; log(`Injecting banner now`); - injectUpdateBanner(win, pendingUpdate.version, pendingUpdate.downloadUrl); + injectUpdateBanner(win, pendingUpdate.version, pendingUpdate.downloadUrl, pendingUpdate.message); }, INJECT_DELAY_MS); } +/** Escape HTML to prevent XSS; preserve newlines for display. */ +function escapeHtml(text: string): string { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/\n/g, "
"); +} + /** - * Injects a Cursor-style update banner at the bottom of the renderer window. - * Safe to call multiple times; a second call while the banner is visible is a no-op. + * Injects an update banner at the bottom, aligned with the app UI. + * Includes a "View details" overlay for changelog/message. */ function injectUpdateBanner( win: BrowserWindow, latest: string, downloadUrl: string, - releaseNotes?: string + message?: string ): void { - const notesHtml = releaseNotes - ? `${releaseNotes}` + const hasMessage = Boolean(message && message.trim()); + const safeMessage = hasMessage ? escapeHtml(message!.trim()) : ""; + const safeMessageJson = JSON.stringify(safeMessage); + const viewDetailsBtnHtml = hasMessage + ? '' : ""; const script = /* js */ ` (function () { if (document.getElementById('__presenton_update_banner__')) return; + const msgHtml = ${safeMessageJson}; + const banner = document.createElement('div'); banner.id = '__presenton_update_banner__'; banner.style.cssText = [ 'position:fixed', - 'bottom:0', - 'left:0', - 'right:0', - 'background:#1e1e2e', - 'color:#cdd6f4', + 'bottom:16px', + 'left:50%', + 'transform:translateX(-50%)', + 'max-width:min(560px,calc(100vw - 32px))', + 'width:100%', + 'background:rgba(255,255,255,0.95)', + 'backdrop-filter:blur(12px)', + '-webkit-backdrop-filter:blur(12px)', + 'color:#191919', 'display:flex', 'align-items:center', 'justify-content:space-between', - 'padding:10px 18px', + 'padding:12px 16px', 'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif', 'font-size:13px', - 'z-index:2147483647', - 'border-top:1px solid #313244', - 'box-shadow:0 -4px 16px rgba(0,0,0,0.35)', + 'z-index:2147483646', + 'border:1px solid rgba(148,163,184,0.3)', + 'border-radius:12px', + 'box-shadow:0 4px 24px rgba(0,0,0,0.08)', 'gap:12px', ].join(';'); banner.innerHTML = \` - + - Presenton ${latest} is available + Presenton ${latest} is available — you have ${CURRENT_VERSION} - ${notesHtml}
- Download update - + ${viewDetailsBtnHtml} + Download update +
\`; document.body.appendChild(banner); + + if (msgHtml) { + const overlay = document.createElement('div'); + overlay.id = '__presenton_update_overlay__'; + overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.4);display:none;align-items:center;justify-content:center;z-index:2147483647;padding:24px;'; + overlay.onclick = function(e) { if (e.target === overlay) overlay.style.display = 'none'; }; + overlay.innerHTML = \` +
+
+

What's new in ${latest}

+ +
+
+
+ \`; + document.body.appendChild(overlay); + document.getElementById('__presenton_overlay_content__').innerHTML = msgHtml; + document.getElementById('__presenton_view_details_btn__').onclick = function() { + document.getElementById('__presenton_update_overlay__').style.display = 'flex'; + }; + } })(); `; @@ -202,7 +237,7 @@ async function checkForUpdatesWithRetry(win: BrowserWindow): Promise { if (newer) { const downloadUrl = getDownloadUrlForPlatform(data.downloads); log(`Injecting banner for ${data.version} (after ${INJECT_DELAY_MS}ms delay)`); - scheduleBannerInjection(win, data.version, downloadUrl); + scheduleBannerInjection(win, data.version, downloadUrl, data.message); } else { log("No update needed, skipping banner"); } @@ -230,7 +265,7 @@ export function startUpdateChecker(win: BrowserWindow): void { const onLoad = () => { if (pendingUpdate) { log("did-finish-load (navigation), re-injecting banner"); - scheduleBannerInjection(win, pendingUpdate.version, pendingUpdate.downloadUrl); + scheduleBannerInjection(win, pendingUpdate.version, pendingUpdate.downloadUrl, pendingUpdate.message); } else if (!hasRunCheck) { hasRunCheck = true; log(`did-finish-load fired, first poll in ${INITIAL_DELAY_MS / 1_000}s`); diff --git a/electron/generate_update.js b/electron/generate_update.js index 87e928a2..958c57ae 100644 --- a/electron/generate_update.js +++ b/electron/generate_update.js @@ -1,11 +1,16 @@ const fs = require("fs"); const pkg = JSON.parse(fs.readFileSync("package.json")); +let existing = {}; +try { + existing = JSON.parse(fs.readFileSync("version.json", "utf8")); +} catch (_) {} const version = pkg.version; const update = { version, + message: process.env.UPDATE_MESSAGE || existing.message || "", downloads: { linux: `https://github.com/presenton/presenton/releases/download/electron-v${version}/Presenton-${version}.deb`, mac: `https://github.com/presenton/presenton/releases/download/electron-v${version}/Presenton-${version}.dmg`, diff --git a/electron/version.json b/electron/version.json index 20020a9c..89ee48cc 100644 --- a/electron/version.json +++ b/electron/version.json @@ -1,5 +1,6 @@ { "version": "0.6.0-beta", + "message": "• Bug fixes and performance improvements\n• New template designs\n• Improved export quality", "downloads": { "linux": "https://github.com/presenton/presenton/releases/download/electron-v0.6.0-beta/Presenton-0.6.0-beta.deb", "mac": "https://github.com/presenton/presenton/releases/download/electron-v0.6.0-beta/Presenton-0.6.0-beta.dmg", From 69b809accdd90cbe05f9a2d8b8cf684c41d07c3c Mon Sep 17 00:00:00 2001 From: sudipnext Date: Sun, 8 Mar 2026 20:21:51 +0545 Subject: [PATCH 3/7] feat: add MIGRATE_DATABASE_ON_STARTUP option to FastApiEnv interface --- electron/app/types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/electron/app/types/index.d.ts b/electron/app/types/index.d.ts index bf2ea930..e82c05b1 100644 --- a/electron/app/types/index.d.ts +++ b/electron/app/types/index.d.ts @@ -30,6 +30,7 @@ interface FastApiEnv { APP_DATA_DIRECTORY?: string, TEMP_DIRECTORY?: string, USER_CONFIG_PATH?: string, + MIGRATE_DATABASE_ON_STARTUP?: string, /** Absolute path to the soffice binary resolved at startup by libreoffice-check.ts. */ SOFFICE_PATH?: string, } From e0d219e5fe8c127e51a3ca83615b9ba65d878da2 Mon Sep 17 00:00:00 2001 From: sudipnext Date: Sun, 8 Mar 2026 20:34:34 +0545 Subject: [PATCH 4/7] feat: modify main window behavior to prevent app quit on "Skip for now" during LibreOffice check --- electron/app/main.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/electron/app/main.ts b/electron/app/main.ts index 5e6dea13..0cda3cb3 100644 --- a/electron/app/main.ts +++ b/electron/app/main.ts @@ -46,6 +46,7 @@ const createWindow = () => { win = new BrowserWindow({ width: 1280, height: 720, + show: false, // Shown after LibreOffice check so "Skip" doesn't quit the app icon: path.join(baseDir, "resources/ui/assets/images/presenton_short_filled.png"), webPreferences: { webSecurity: false, @@ -146,12 +147,18 @@ app.whenReady().then(async () => { // Register LibreOffice install handlers early so the installer window can use them setupLibreOfficeInstallHandlers(); + // Create main window BEFORE LibreOffice check so that when user clicks "Skip for now", + // the installer closes but the main window stays open (avoids app quit on window-all-closed). + createWindow(); + win?.loadFile(path.join(baseDir, "resources/ui/homepage/index.html")); + // Check for LibreOffice (required for custom template from PPTX). Shows installer // window if missing. Never blocks; always proceeds. await checkLibreOfficeBeforeWindow(); - createWindow(); - win?.loadFile(path.join(baseDir, "resources/ui/homepage/index.html")); + // Show and focus main window (was hidden to avoid app quit when user clicks "Skip for now") + win?.show(); + win?.focus(); setUserConfig({ CAN_CHANGE_KEYS: process.env.CAN_CHANGE_KEYS, From d185d10462b2727ed09b293aaa6205774bc25a98 Mon Sep 17 00:00:00 2001 From: sudipnext Date: Sun, 8 Mar 2026 20:55:20 +0545 Subject: [PATCH 5/7] feat: implement image path resolution utility to streamline image handling in FastAPI endpoints and PPTX presentation creator --- .../api/v1/ppt/endpoints/slide_to_html.py | 42 ++----------- .../services/pptx_presentation_creator.py | 53 +++++++++------- .../fastapi/utils/asset_directory_utils.py | 61 +++++++++++++++++++ 3 files changed, 98 insertions(+), 58 deletions(-) diff --git a/electron/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py b/electron/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py index 5025ce4a..00db8019 100644 --- a/electron/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py +++ b/electron/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py @@ -9,7 +9,7 @@ from openai import OpenAI from openai import APIError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, delete, func -from utils.asset_directory_utils import get_images_directory +from utils.asset_directory_utils import get_images_directory, resolve_image_path_to_filesystem from services.database import get_async_session from models.sql.presentation_layout_code import PresentationLayoutCodeModel from .prompts import ( @@ -454,28 +454,10 @@ async def convert_slide_to_html(request: SlideToHtmlRequest): ) # Resolve image path to actual file system path - image_path = request.image - - # Handle different path formats - if image_path.startswith("/app_data/images/"): - # Remove the /app_data/images/ prefix and join with actual images directory - relative_path = image_path[len("/app_data/images/") :] - actual_image_path = os.path.join(get_images_directory(), relative_path) - elif image_path.startswith("/static/"): - # Handle static files - relative_path = image_path[len("/static/") :] - actual_image_path = os.path.join("static", relative_path) - else: - # Assume it's already a full path or relative to images directory - if os.path.isabs(image_path): - actual_image_path = image_path - else: - actual_image_path = os.path.join(get_images_directory(), image_path) - - # Check if image file exists - if not os.path.exists(actual_image_path): + actual_image_path = resolve_image_path_to_filesystem(request.image) + if not actual_image_path: raise HTTPException( - status_code=404, detail=f"Image file not found: {image_path}" + status_code=404, detail=f"Image file not found: {request.image}" ) # Read and encode image to base64 @@ -546,20 +528,8 @@ async def convert_html_to_react(request: HtmlToReactRequest): image_b64 = None media_type = None if request.image: - image_path = request.image - if image_path.startswith("/app_data/images/"): - relative_path = image_path[len("/app_data/images/") :] - actual_image_path = os.path.join(get_images_directory(), relative_path) - elif image_path.startswith("/static/"): - relative_path = image_path[len("/static/") :] - actual_image_path = os.path.join("static", relative_path) - else: - actual_image_path = ( - image_path - if os.path.isabs(image_path) - else os.path.join(get_images_directory(), image_path) - ) - if os.path.exists(actual_image_path): + actual_image_path = resolve_image_path_to_filesystem(request.image) + if actual_image_path: with open(actual_image_path, "rb") as f: image_b64 = base64.b64encode(f.read()).decode("utf-8") ext = os.path.splitext(actual_image_path)[1].lower() diff --git a/electron/servers/fastapi/services/pptx_presentation_creator.py b/electron/servers/fastapi/services/pptx_presentation_creator.py index b27c6145..d3e51a7f 100644 --- a/electron/servers/fastapi/services/pptx_presentation_creator.py +++ b/electron/servers/fastapi/services/pptx_presentation_creator.py @@ -36,7 +36,9 @@ from models.pptx_models import ( PptxTextBoxModel, PptxTextRunModel, ) +from utils.asset_directory_utils import get_images_directory, resolve_image_path_to_filesystem from utils.download_helpers import download_files +from utils.get_env import get_app_data_directory_env from utils.image_utils import ( clip_image, create_circle_image, @@ -206,35 +208,36 @@ class PptxPresentationCreator: image_urls = [] models_with_network_asset: List[PptxPictureBoxModel] = [] + def _process_image_path(each_shape, image_path): + if not image_path.startswith("http"): + return + if "app_data/" in image_path: + relative_path = image_path.split("app_data/")[1] + app_data_dir = get_app_data_directory_env() + if app_data_dir: + each_shape.picture.path = os.path.join(app_data_dir, relative_path) + else: + each_shape.picture.path = os.path.join("/app_data", relative_path) + each_shape.picture.is_network = False + return + # Resolve HTTP URLs that contain absolute filesystem paths (Mac/Electron) + local_path = resolve_image_path_to_filesystem(image_path) + if local_path: + each_shape.picture.path = local_path + each_shape.picture.is_network = False + return + image_urls.append(image_path) + models_with_network_asset.append(each_shape) + if self._ppt_model.shapes: for each_shape in self._ppt_model.shapes: if isinstance(each_shape, PptxPictureBoxModel): - image_path = each_shape.picture.path - if image_path.startswith("http"): - if "app_data/" in image_path: - relative_path = image_path.split("app_data/")[1] - each_shape.picture.path = os.path.join( - "/app_data", relative_path - ) - each_shape.picture.is_network = False - continue - image_urls.append(image_path) - models_with_network_asset.append(each_shape) + _process_image_path(each_shape, each_shape.picture.path) for each_slide in self._slide_models: for each_shape in each_slide.shapes: if isinstance(each_shape, PptxPictureBoxModel): - image_path = each_shape.picture.path - if image_path.startswith("http"): - if "app_data" in image_path: - relative_path = image_path.split("app_data/")[1] - each_shape.picture.path = os.path.join( - "/app_data", relative_path - ) - each_shape.picture.is_network = False - continue - image_urls.append(image_path) - models_with_network_asset.append(each_shape) + _process_image_path(each_shape, each_shape.picture.path) if image_urls: image_paths = await download_files(image_urls, self._temp_dir) @@ -312,6 +315,12 @@ class PptxPresentationCreator: def add_picture(self, slide: Slide, picture_model: PptxPictureBoxModel): image_path = picture_model.picture.path + # Resolve /app_data/... to actual filesystem path (Electron) + if image_path.startswith("/app_data/"): + app_data_dir = get_app_data_directory_env() + if app_data_dir: + relative = image_path[len("/app_data/"):] + image_path = os.path.join(app_data_dir, relative) if ( picture_model.clip or picture_model.border_radius diff --git a/electron/servers/fastapi/utils/asset_directory_utils.py b/electron/servers/fastapi/utils/asset_directory_utils.py index a88196c1..a3e44be7 100644 --- a/electron/servers/fastapi/utils/asset_directory_utils.py +++ b/electron/servers/fastapi/utils/asset_directory_utils.py @@ -1,7 +1,68 @@ import os +from typing import Optional +from urllib.parse import urlparse, unquote + from utils.get_env import get_app_data_directory_env +def resolve_image_path_to_filesystem(path_or_url: str) -> Optional[str]: + """ + Resolve an image path or URL to an actual filesystem path. + + Handles: + - Path strings: /app_data/images/..., /static/..., absolute paths, relative + - HTTP URLs whose path component is an absolute filesystem path (Mac/Electron): + When img src is /Users/.../images/xxx.png, browser resolves to + http://origin/Users/.../images/xxx.png. Next.js returns 404 for these. + + Returns the filesystem path if the file exists, else None. + """ + if not path_or_url: + return None + # Extract path from HTTP URL if needed + path = path_or_url + if path_or_url.startswith("http"): + try: + parsed = urlparse(path_or_url) + path = unquote(parsed.path) + except Exception: + return None + # Handle /app_data/images/ + if path.startswith("/app_data/images/"): + relative = path[len("/app_data/images/"):] + app_data = get_app_data_directory_env() + if app_data: + actual = os.path.join(app_data, "images", relative) + if os.path.isfile(actual): + return actual + # Fallback: get_images_directory() + relative + actual = os.path.join(get_images_directory(), relative) + return actual if os.path.isfile(actual) else None + # Handle /app_data/ (other subdirs) + if path.startswith("/app_data/"): + relative = path[len("/app_data/"):] + app_data = get_app_data_directory_env() + if app_data: + actual = os.path.join(app_data, relative) + return actual if os.path.isfile(actual) else None + # Handle absolute filesystem path (e.g. from HTTP URL path on Mac) + if path.startswith("/Users/") or path.startswith("/home/") or path.startswith("/var/"): + return path if os.path.isfile(path) else None + if "Application Support" in path or ("Library" in path and "images" in path): + return path if os.path.isfile(path) else None + # Handle /static/ + if path.startswith("/static/"): + relative = path[len("/static/"):] + actual = os.path.join("static", relative) + return actual if os.path.isfile(actual) else None + # Absolute path as-is + if os.path.isabs(path): + return path if os.path.isfile(path) else None + # Relative to images directory + actual = os.path.join(get_images_directory(), path) + return actual if os.path.isfile(actual) else None + + def get_images_directory(): images_directory = os.path.join(get_app_data_directory_env(), "images") os.makedirs(images_directory, exist_ok=True) From bed2d56074921a353c364882bb2965cdcfdb0309 Mon Sep 17 00:00:00 2001 From: sudipnext Date: Sun, 8 Mar 2026 21:08:27 +0545 Subject: [PATCH 6/7] refactor: streamline image path resolution in FastAPI endpoints and PPTX presentation creator using new utility function --- .../api/v1/ppt/endpoints/slide_to_html.py | 42 ++----------- .../services/pptx_presentation_creator.py | 53 +++++++++------- .../fastapi/utils/asset_directory_utils.py | 61 +++++++++++++++++++ 3 files changed, 98 insertions(+), 58 deletions(-) diff --git a/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py b/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py index 5025ce4a..00db8019 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py +++ b/servers/fastapi/api/v1/ppt/endpoints/slide_to_html.py @@ -9,7 +9,7 @@ from openai import OpenAI from openai import APIError from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, delete, func -from utils.asset_directory_utils import get_images_directory +from utils.asset_directory_utils import get_images_directory, resolve_image_path_to_filesystem from services.database import get_async_session from models.sql.presentation_layout_code import PresentationLayoutCodeModel from .prompts import ( @@ -454,28 +454,10 @@ async def convert_slide_to_html(request: SlideToHtmlRequest): ) # Resolve image path to actual file system path - image_path = request.image - - # Handle different path formats - if image_path.startswith("/app_data/images/"): - # Remove the /app_data/images/ prefix and join with actual images directory - relative_path = image_path[len("/app_data/images/") :] - actual_image_path = os.path.join(get_images_directory(), relative_path) - elif image_path.startswith("/static/"): - # Handle static files - relative_path = image_path[len("/static/") :] - actual_image_path = os.path.join("static", relative_path) - else: - # Assume it's already a full path or relative to images directory - if os.path.isabs(image_path): - actual_image_path = image_path - else: - actual_image_path = os.path.join(get_images_directory(), image_path) - - # Check if image file exists - if not os.path.exists(actual_image_path): + actual_image_path = resolve_image_path_to_filesystem(request.image) + if not actual_image_path: raise HTTPException( - status_code=404, detail=f"Image file not found: {image_path}" + status_code=404, detail=f"Image file not found: {request.image}" ) # Read and encode image to base64 @@ -546,20 +528,8 @@ async def convert_html_to_react(request: HtmlToReactRequest): image_b64 = None media_type = None if request.image: - image_path = request.image - if image_path.startswith("/app_data/images/"): - relative_path = image_path[len("/app_data/images/") :] - actual_image_path = os.path.join(get_images_directory(), relative_path) - elif image_path.startswith("/static/"): - relative_path = image_path[len("/static/") :] - actual_image_path = os.path.join("static", relative_path) - else: - actual_image_path = ( - image_path - if os.path.isabs(image_path) - else os.path.join(get_images_directory(), image_path) - ) - if os.path.exists(actual_image_path): + actual_image_path = resolve_image_path_to_filesystem(request.image) + if actual_image_path: with open(actual_image_path, "rb") as f: image_b64 = base64.b64encode(f.read()).decode("utf-8") ext = os.path.splitext(actual_image_path)[1].lower() diff --git a/servers/fastapi/services/pptx_presentation_creator.py b/servers/fastapi/services/pptx_presentation_creator.py index b27c6145..d3e51a7f 100644 --- a/servers/fastapi/services/pptx_presentation_creator.py +++ b/servers/fastapi/services/pptx_presentation_creator.py @@ -36,7 +36,9 @@ from models.pptx_models import ( PptxTextBoxModel, PptxTextRunModel, ) +from utils.asset_directory_utils import get_images_directory, resolve_image_path_to_filesystem from utils.download_helpers import download_files +from utils.get_env import get_app_data_directory_env from utils.image_utils import ( clip_image, create_circle_image, @@ -206,35 +208,36 @@ class PptxPresentationCreator: image_urls = [] models_with_network_asset: List[PptxPictureBoxModel] = [] + def _process_image_path(each_shape, image_path): + if not image_path.startswith("http"): + return + if "app_data/" in image_path: + relative_path = image_path.split("app_data/")[1] + app_data_dir = get_app_data_directory_env() + if app_data_dir: + each_shape.picture.path = os.path.join(app_data_dir, relative_path) + else: + each_shape.picture.path = os.path.join("/app_data", relative_path) + each_shape.picture.is_network = False + return + # Resolve HTTP URLs that contain absolute filesystem paths (Mac/Electron) + local_path = resolve_image_path_to_filesystem(image_path) + if local_path: + each_shape.picture.path = local_path + each_shape.picture.is_network = False + return + image_urls.append(image_path) + models_with_network_asset.append(each_shape) + if self._ppt_model.shapes: for each_shape in self._ppt_model.shapes: if isinstance(each_shape, PptxPictureBoxModel): - image_path = each_shape.picture.path - if image_path.startswith("http"): - if "app_data/" in image_path: - relative_path = image_path.split("app_data/")[1] - each_shape.picture.path = os.path.join( - "/app_data", relative_path - ) - each_shape.picture.is_network = False - continue - image_urls.append(image_path) - models_with_network_asset.append(each_shape) + _process_image_path(each_shape, each_shape.picture.path) for each_slide in self._slide_models: for each_shape in each_slide.shapes: if isinstance(each_shape, PptxPictureBoxModel): - image_path = each_shape.picture.path - if image_path.startswith("http"): - if "app_data" in image_path: - relative_path = image_path.split("app_data/")[1] - each_shape.picture.path = os.path.join( - "/app_data", relative_path - ) - each_shape.picture.is_network = False - continue - image_urls.append(image_path) - models_with_network_asset.append(each_shape) + _process_image_path(each_shape, each_shape.picture.path) if image_urls: image_paths = await download_files(image_urls, self._temp_dir) @@ -312,6 +315,12 @@ class PptxPresentationCreator: def add_picture(self, slide: Slide, picture_model: PptxPictureBoxModel): image_path = picture_model.picture.path + # Resolve /app_data/... to actual filesystem path (Electron) + if image_path.startswith("/app_data/"): + app_data_dir = get_app_data_directory_env() + if app_data_dir: + relative = image_path[len("/app_data/"):] + image_path = os.path.join(app_data_dir, relative) if ( picture_model.clip or picture_model.border_radius diff --git a/servers/fastapi/utils/asset_directory_utils.py b/servers/fastapi/utils/asset_directory_utils.py index a88196c1..a3e44be7 100644 --- a/servers/fastapi/utils/asset_directory_utils.py +++ b/servers/fastapi/utils/asset_directory_utils.py @@ -1,7 +1,68 @@ import os +from typing import Optional +from urllib.parse import urlparse, unquote + from utils.get_env import get_app_data_directory_env +def resolve_image_path_to_filesystem(path_or_url: str) -> Optional[str]: + """ + Resolve an image path or URL to an actual filesystem path. + + Handles: + - Path strings: /app_data/images/..., /static/..., absolute paths, relative + - HTTP URLs whose path component is an absolute filesystem path (Mac/Electron): + When img src is /Users/.../images/xxx.png, browser resolves to + http://origin/Users/.../images/xxx.png. Next.js returns 404 for these. + + Returns the filesystem path if the file exists, else None. + """ + if not path_or_url: + return None + # Extract path from HTTP URL if needed + path = path_or_url + if path_or_url.startswith("http"): + try: + parsed = urlparse(path_or_url) + path = unquote(parsed.path) + except Exception: + return None + # Handle /app_data/images/ + if path.startswith("/app_data/images/"): + relative = path[len("/app_data/images/"):] + app_data = get_app_data_directory_env() + if app_data: + actual = os.path.join(app_data, "images", relative) + if os.path.isfile(actual): + return actual + # Fallback: get_images_directory() + relative + actual = os.path.join(get_images_directory(), relative) + return actual if os.path.isfile(actual) else None + # Handle /app_data/ (other subdirs) + if path.startswith("/app_data/"): + relative = path[len("/app_data/"):] + app_data = get_app_data_directory_env() + if app_data: + actual = os.path.join(app_data, relative) + return actual if os.path.isfile(actual) else None + # Handle absolute filesystem path (e.g. from HTTP URL path on Mac) + if path.startswith("/Users/") or path.startswith("/home/") or path.startswith("/var/"): + return path if os.path.isfile(path) else None + if "Application Support" in path or ("Library" in path and "images" in path): + return path if os.path.isfile(path) else None + # Handle /static/ + if path.startswith("/static/"): + relative = path[len("/static/"):] + actual = os.path.join("static", relative) + return actual if os.path.isfile(actual) else None + # Absolute path as-is + if os.path.isabs(path): + return path if os.path.isfile(path) else None + # Relative to images directory + actual = os.path.join(get_images_directory(), path) + return actual if os.path.isfile(actual) else None + + def get_images_directory(): images_directory = os.path.join(get_app_data_directory_env(), "images") os.makedirs(images_directory, exist_ok=True) From 9720a5c3e17cc620ba33d1978c0da0018f150605 Mon Sep 17 00:00:00 2001 From: sudipnext Date: Tue, 10 Mar 2026 19:20:13 +0545 Subject: [PATCH 7/7] Remove puppeteer dependency from package.json --- electron/app/ipc/index.ts | 2 - electron/app/ipc/template_api_handlers.ts | 94 --- electron/package-lock.json | 693 ++-------------------- electron/package.json | 2 - 4 files changed, 46 insertions(+), 745 deletions(-) delete mode 100644 electron/app/ipc/template_api_handlers.ts diff --git a/electron/app/ipc/index.ts b/electron/app/ipc/index.ts index db544191..7e470097 100644 --- a/electron/app/ipc/index.ts +++ b/electron/app/ipc/index.ts @@ -7,7 +7,6 @@ import { setupThemeHandlers } from "./theme_handlers"; import { setupUploadImage } from "./upload_image"; import { setupLogHandler } from "./log_handler"; import { setupApiHandlers } from "./api_handlers"; -import { setupTemplateHandlers } from "./template_api_handlers"; import { setupPresentationToPptxModelHandlers } from "./presentation_to_pptx_model_handlers"; export function setupIpcHandlers() { @@ -20,6 +19,5 @@ export function setupIpcHandlers() { setupUploadImage(); setupLogHandler(); setupApiHandlers(); - setupTemplateHandlers(); setupPresentationToPptxModelHandlers(); } \ No newline at end of file diff --git a/electron/app/ipc/template_api_handlers.ts b/electron/app/ipc/template_api_handlers.ts deleted file mode 100644 index 6b5aac05..00000000 --- a/electron/app/ipc/template_api_handlers.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { ipcMain, dialog } from "electron"; -import puppeteer from "puppeteer"; - -export function setupTemplateHandlers() { - // Handler for template API (with puppeteer) - ipcMain.handle("api:template", async (event, { group }: { group: string }) => { - if (!group) { - throw new Error("Missing group name"); - } - - const schemaPageUrl = `http://localhost/schema?group=${encodeURIComponent(group)}`; - - let browser; - try { - browser = await puppeteer.launch({ - executablePath: process.env.PUPPETEER_EXECUTABLE_PATH, - headless: true, - args: [ - "--no-sandbox", - "--disable-setuid-sandbox", - "--disable-dev-shm-usage", - "--disable-gpu", - "--disable-web-security", - "--disable-background-timer-throttling", - "--disable-backgrounding-occluded-windows", - "--disable-renderer-backgrounding", - "--disable-features=TranslateUI", - "--disable-ipc-flooding-protection", - ], - }); - - const page = await browser.newPage(); - await page.setViewport({ width: 1280, height: 720 }); - page.setDefaultNavigationTimeout(300000); - page.setDefaultTimeout(300000); - - await page.goto(schemaPageUrl, { - waitUntil: "networkidle0", - timeout: 300000, - }); - - await page.waitForSelector("[data-layouts]", { timeout: 300000 }); - await page.waitForSelector("[data-settings]", { timeout: 300000 }); - - const { dataLayouts, dataGroupSettings } = await page.$eval( - "[data-layouts]", - (el) => ({ - dataLayouts: el.getAttribute("data-layouts"), - dataGroupSettings: el.getAttribute("data-settings"), - }) - ); - - if (!dataLayouts || !dataGroupSettings) { - throw new Error("Could not find layouts or settings data"); - } - - const layouts = JSON.parse(dataLayouts); - const groupSettings = JSON.parse(dataGroupSettings); - - return { - layouts, - groupSettings, - }; - } catch (error) { - console.error("Error getting template:", error); - throw new Error(`Failed to get template: ${error instanceof Error ? error.message : String(error)}`); - } finally { - if (browser) { - await browser.close(); - } - } - }); - - // Handler for presentation_to_pptx_model API (simplified version) - ipcMain.handle("api:presentation-to-pptx-model", async (event, { id }: { id?: string }) => { - // Note: This is a simplified version since the full implementation is quite complex - // and involves puppeteer operations that might be better handled differently in Electron - try { - if (!id) { - throw new Error("Missing presentation ID"); - } - - // For now, return a placeholder response or implement a simplified version - // The full implementation would require significant adaptation for Electron context - return { - error: "This endpoint requires server-side implementation", - message: "Use the FastAPI backend for presentation to PPTX conversion" - }; - } catch (error) { - console.error("Error in presentation-to-pptx-model:", error); - throw new Error(`Failed to convert presentation: ${error instanceof Error ? error.message : String(error)}`); - } - }); -} \ No newline at end of file diff --git a/electron/package-lock.json b/electron/package-lock.json index 59e05b9f..08eeeae6 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -13,7 +13,6 @@ "@types/uuid": "^10.0.0", "dotenv": "^16.5.0", "electron-squirrel-startup": "^1.0.1", - "puppeteer": "^24.8.2", "serve-handler": "^6.1.6", "sharp": "^0.34.5", "tailwindcss": "^4.1.5", @@ -30,29 +29,6 @@ "typescript": "^5.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -1589,39 +1565,6 @@ "node": ">=14" } }, - "node_modules/@puppeteer/browsers": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.11.2.tgz", - "integrity": "sha512-GBY0+2lI9fDrjgb5dFL9+enKXqyOPok9PXg/69NVkjW3bikbK9RQrNrI3qccQXmDNN7ln4j/yL89Qgvj/tfqrw==", - "license": "Apache-2.0", - "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.3", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@puppeteer/browsers/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -1909,12 +1852,6 @@ "node": ">= 10" } }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "license": "MIT" - }, "node_modules/@types/cacheable-request": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", @@ -1976,7 +1913,7 @@ "version": "22.19.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -2022,6 +1959,7 @@ "version": "2.10.3", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2059,6 +1997,7 @@ "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, "license": "MIT", "engines": { "node": ">= 14" @@ -2095,6 +2034,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2104,6 +2044,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2243,6 +2184,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, "license": "Python-2.0" }, "node_modules/assert-plus": { @@ -2256,18 +2198,6 @@ "node": ">=0.8" } }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -2313,117 +2243,12 @@ "node": ">= 4.0.0" } }, - "node_modules/b4a": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.7.3.tgz", - "integrity": "sha512-5Q2mfq2WfGuFp3uS//0s6baOJLMoVduPYVeNmDYxu5OUA1/cBfvr2RIS7vi62LdNj/urk1hfmj867I3qt6uZ7Q==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.3.tgz", - "integrity": "sha512-9+kwVx8QYvt3hPWnmb19tPnh38c6Nihz8Lx3t0g9+4GoIf3/fTgYwM4Z6NxgI+B9elLQA7mLE9PpqcWtOMRDiQ==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.2.tgz", - "integrity": "sha512-T+V1+1srU2qYNBmJCXZkUY5vQ0B4FSlL3QDROnKQYOqeiQR8UbjNHlPa+TIbM4cuidiN9GaTaOZgSEgsvPbh5A==", - "license": "Apache-2.0", - "optional": true, - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.7.0.tgz", - "integrity": "sha512-oyXQNicV1y8nc2aKffH+BUHFRXmx6VrPzlnaEvMhram0nPBrKcEdcyBg5r08D0i8VxngHFAiVyn1QKXpSG0B8A==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "streamx": "^2.21.0" - }, - "peerDependencies": { - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", - "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", - "license": "Apache-2.0", - "optional": true, - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2445,15 +2270,6 @@ ], "license": "MIT" }, - "node_modules/basic-ftp": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.1.0.tgz", - "integrity": "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -2514,6 +2330,7 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, "license": "MIT", "engines": { "node": "*" @@ -2770,15 +2587,6 @@ "node": ">= 0.4" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2806,19 +2614,6 @@ "node": ">=10" } }, - "node_modules/chromium-bidi": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.0.1.tgz", - "integrity": "sha512-c+RLxH0Vg2x2syS9wPw378oJgiJNXtYXUvnVAldUlt5uaHekn0CCU7gPksNgHjrH1qFhmjVXQj4esvuthuC7OQ==", - "license": "Apache-2.0", - "dependencies": { - "mitt": "^3.0.1", - "zod": "^3.24.1" - }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, "node_modules/chromium-pickle-js": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz", @@ -2890,6 +2685,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -2927,6 +2723,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2939,6 +2736,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -2997,32 +2795,6 @@ "license": "MIT", "optional": true }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/crc": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", @@ -3100,19 +2872,11 @@ "node": ">= 8" } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3216,20 +2980,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3257,12 +3007,6 @@ "license": "MIT", "optional": true }, - "node_modules/devtools-protocol": { - "version": "0.0.1551306", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1551306.tgz", - "integrity": "sha512-CFx8QdSim8iIv+2ZcEOclBKTQY6BI1IEDa7Tm9YkwAXzEWFndTEzpTo5jAUhSnq24IC7xaDw0wvGcm96+Y3PEg==", - "license": "BSD-3-Clause" - }, "node_modules/dir-compare": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/dir-compare/-/dir-compare-4.2.0.tgz", @@ -3652,6 +3396,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/encoding": { @@ -3669,6 +3414,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -3691,6 +3437,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3703,15 +3450,6 @@ "dev": true, "license": "MIT" }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3773,6 +3511,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3792,67 +3531,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", @@ -3864,6 +3542,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, "license": "BSD-2-Clause", "dependencies": { "debug": "^4.1.1", @@ -3898,12 +3577,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3915,6 +3588,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, "license": "MIT", "dependencies": { "pend": "~1.2.0" @@ -4067,6 +3741,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -4115,6 +3790,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, "license": "MIT", "dependencies": { "pump": "^3.0.0" @@ -4126,20 +3802,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4361,6 +4023,7 @@ "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -4388,6 +4051,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -4457,22 +4121,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -4506,17 +4154,12 @@ "version": "10.1.0", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4530,6 +4173,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4636,16 +4280,11 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4661,12 +4300,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4971,12 +4604,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, "node_modules/lodash": { "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", @@ -5294,12 +4921,6 @@ "node": ">= 18" } }, - "node_modules/mitt": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", - "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", - "license": "MIT" - }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -5326,6 +4947,7 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, "license": "MIT" }, "node_modules/negotiator": { @@ -5338,15 +4960,6 @@ "node": ">= 0.6" } }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/node-abi": { "version": "4.26.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", @@ -5521,6 +5134,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -5605,38 +5219,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -5644,36 +5226,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -5749,6 +5301,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, "license": "MIT" }, "node_modules/picocolors": { @@ -5828,6 +5381,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -5847,44 +5401,11 @@ "node": ">=10" } }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -5901,45 +5422,6 @@ "node": ">=6" } }, - "node_modules/puppeteer": { - "version": "24.36.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.36.1.tgz", - "integrity": "sha512-uPiDUyf7gd7Il1KnqfNUtHqntL0w1LapEw5Zsuh8oCK8GsqdxySX1PzdIHKB2Dw273gWY4MW0zC5gy3Re9XlqQ==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.11.2", - "chromium-bidi": "13.0.1", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1551306", - "puppeteer-core": "24.36.1", - "typed-query-selector": "^2.12.0" - }, - "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core": { - "version": "24.36.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.36.1.tgz", - "integrity": "sha512-L7ykMWc3lQf3HS7ME3PSjp7wMIjJeW6+bKfH/RSTz5l6VUDGubnrC2BKj3UvM28Y5PMDFW0xniJOZHBZPpW1dQ==", - "license": "Apache-2.0", - "dependencies": { - "@puppeteer/browsers": "2.11.2", - "chromium-bidi": "13.0.1", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1551306", - "typed-query-selector": "^2.12.0", - "webdriver-bidi-protocol": "0.4.0", - "ws": "^8.19.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -5994,6 +5476,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -6024,15 +5507,6 @@ "dev": true, "license": "MIT" }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/responselike": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", @@ -6367,6 +5841,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6.0.0", @@ -6377,6 +5852,7 @@ "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, "license": "MIT", "dependencies": { "ip-address": "^10.0.1", @@ -6391,6 +5867,7 @@ "version": "8.0.5", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.2", @@ -6405,7 +5882,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "devOptional": true, + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -6462,17 +5939,6 @@ "node": ">= 6" } }, - "node_modules/streamx": { - "version": "2.23.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", - "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -6487,6 +5953,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6517,6 +5984,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6603,31 +6071,6 @@ "node": ">=10" } }, - "node_modules/tar-fs": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", - "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, "node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -6769,15 +6212,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/text-decoder": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz", - "integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/tiny-async-pool": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/tiny-async-pool/-/tiny-async-pool-1.3.0.tgz", @@ -6858,7 +6292,8 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" + "license": "0BSD", + "optional": true }, "node_modules/type-fest": { "version": "0.13.1", @@ -6874,17 +6309,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/typed-query-selector": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", - "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", - "license": "MIT" - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6898,7 +6327,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/unique-filename": { @@ -7000,12 +6429,6 @@ "defaults": "^1.0.3" } }, - "node_modules/webdriver-bidi-protocol": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.0.tgz", - "integrity": "sha512-U9VIlNRrq94d1xxR9JrCEAx5Gv/2W7ERSv8oWRoNe/QYbfccS0V3h/H6qeNeCRJxXGMhhnkqvwNrvPAYeuP9VA==", - "license": "Apache-2.0" - }, "node_modules/which": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", @@ -7026,6 +6449,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -7062,29 +6486,9 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, "license": "ISC" }, - "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", @@ -7099,6 +6503,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -7115,6 +6520,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -7133,6 +6539,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -7142,6 +6549,7 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, "license": "MIT", "dependencies": { "buffer-crc32": "~0.2.3", @@ -7160,15 +6568,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/electron/package.json b/electron/package.json index f0352d6c..93e92c8f 100644 --- a/electron/package.json +++ b/electron/package.json @@ -17,7 +17,6 @@ "typescript", "fastapi", "nextjs", - "puppeteer", "sharp", "template management", "slide generation", @@ -50,7 +49,6 @@ "@types/uuid": "^10.0.0", "dotenv": "^16.5.0", "electron-squirrel-startup": "^1.0.1", - "puppeteer": "^24.8.2", "serve-handler": "^6.1.6", "sharp": "^0.34.5", "tailwindcss": "^4.1.5",