refactor: reduces images size to around 2.5gb

This commit is contained in:
sauravniraula 2026-04-21 17:43:07 +05:45
parent ba5d51ad76
commit 501a155ad0
No known key found for this signature in database
GPG key ID: 60FCC1B5A5E83326
11 changed files with 248 additions and 158 deletions

View file

@ -1,6 +1,39 @@
.git
.gitignore
node_modules
**/node_modules
**/.next
**/.next-build
**/.venv
**/.pytest_cache
**/__pycache__
**/*.pyc
**/debug
**/fastembed_cache
**/chroma
.cache
app_data
presentation-export
servers/fastapi/tests
servers/fastapi/presenton_backend.egg-info
servers/fastapi/.python-version
servers/fastapi/placeholder
servers/nextjs/cypress
servers/nextjs/cypress.config.ts
servers/nextjs/README.md
servers/nextjs/tsconfig.tsbuildinfo
**/*.cy.ts
**/*.cy.tsx
# Keep only the LiteParse runner from the Electron tree.
electron/*
!electron/resources
electron/resources/*
!electron/resources/document-extraction
electron/resources/document-extraction/*
!electron/resources/document-extraction/liteparse_runner.mjs
servers/fastapi/tmp
servers/fastapi/debug
servers/fastapi/.venv
@ -8,4 +41,4 @@ servers/fastapi/.venv
servers/nextjs/node_modules
servers/nextjs/.next
container.db
container.db

View file

@ -1,60 +1,108 @@
# syntax=docker/dockerfile:1.4
FROM python:3.11-slim-trixie
# syntax=docker/dockerfile:1.7
FROM python:3.11-slim-trixie AS fastapi-builder
WORKDIR /app/servers/fastapi
ENV UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy
RUN python -m venv --without-pip /opt/venv \
&& pip install --no-cache-dir uv
COPY servers/fastapi/pyproject.toml servers/fastapi/uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv export --frozen --no-dev --no-emit-project -o /tmp/requirements.txt \
&& uv pip install --python /opt/venv/bin/python -r /tmp/requirements.txt
COPY servers/fastapi /app/servers/fastapi
RUN --mount=type=cache,target=/root/.cache/uv \
uv pip install --python /opt/venv/bin/python --no-deps .
RUN --mount=type=cache,target=/root/.cache \
/opt/venv/bin/python scripts/warm_fastembed_cache.py
FROM node:20-bookworm-slim AS nextjs-builder
WORKDIR /app/servers/nextjs
ENV NEXT_TELEMETRY_DISABLED=1 \
PUPPETEER_SKIP_DOWNLOAD=true
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY servers/nextjs /app/servers/nextjs
RUN npm run build \
&& rm -rf .next-build/cache
FROM node:20-bookworm-slim AS assets-builder
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates unzip \
&& rm -rf /var/lib/apt/lists/*
COPY package.json /app/
RUN mkdir -p /app/document-extraction-liteparse \
&& npm --prefix /app/document-extraction-liteparse init -y \
&& npm --prefix /app/document-extraction-liteparse install @llamaindex/liteparse@1.4.0 --omit=dev
COPY electron/resources/document-extraction/liteparse_runner.mjs /app/document-extraction-liteparse/liteparse_runner.mjs
COPY scripts/sync-presentation-export.cjs /app/scripts/sync-presentation-export.cjs
RUN node /app/scripts/sync-presentation-export.cjs --force \
&& chmod +x /app/presentation-export/py/convert-linux-x64
FROM python:3.11-slim-trixie AS runtime
WORKDIR /app
ARG INSTALL_CHROMIUM=true
ARG INSTALL_TESSERACT=true
ARG INSTALL_LIBREOFFICE=true
# LiteParse uses Node + @llamaindex/liteparse (same runner as Electron); OCR uses Tesseract.
ENV APP_DATA_DIRECTORY=/app_data \
TEMP_DIRECTORY=/tmp/presenton \
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium \
UV_SYSTEM_PYTHON=1 \
UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
PATH="/root/.local/bin:${PATH}" \
EXPORT_PACKAGE_ROOT=/app/presentation-export \
EXPORT_RUNTIME_DIR=/app/presentation-export \
BUILT_PYTHON_MODULE_PATH=/app/presentation-export/py/convert-linux-x64 \
PRESENTON_APP_ROOT=/app
PRESENTON_APP_ROOT=/app \
PATH="/opt/venv/bin:${PATH}" \
NODE_ENV=production \
START_OLLAMA=false
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl unzip \
nginx libreoffice fontconfig chromium imagemagick zstd \
tesseract-ocr tesseract-ocr-eng \
&& curl -LsSf https://astral.sh/uv/install.sh | sh \
&& rm -rf /var/lib/apt/lists/*
RUN set -eux; \
packages="ca-certificates curl nginx fontconfig imagemagick zstd"; \
if [ "$INSTALL_LIBREOFFICE" = "true" ]; then packages="$packages libreoffice"; fi; \
if [ "$INSTALL_CHROMIUM" = "true" ]; then packages="$packages chromium"; fi; \
if [ "$INSTALL_TESSERACT" = "true" ]; then packages="$packages tesseract-ocr tesseract-ocr-eng"; fi; \
apt-get update; \
apt-get install -y --no-install-recommends $packages; \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -; \
apt-get install -y --no-install-recommends nodejs; \
rm -rf /var/lib/apt/lists/*
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app/scripts /app/servers/fastapi /app/servers/nextjs
COPY package.json package-lock.json /app/
RUN npm --prefix /app install --omit=dev
COPY --from=fastapi-builder /opt/venv /opt/venv
COPY --from=fastapi-builder /app/servers/fastapi /app/servers/fastapi
RUN mkdir -p /app/document-extraction-liteparse \
&& npm --prefix /app/document-extraction-liteparse init -y \
&& npm --prefix /app/document-extraction-liteparse install @llamaindex/liteparse@1.4.0 --omit=dev
COPY electron/resources/document-extraction/liteparse_runner.mjs /app/document-extraction-liteparse/liteparse_runner.mjs
COPY --from=assets-builder /app/package.json /app/package.json
COPY --from=assets-builder /app/document-extraction-liteparse /app/document-extraction-liteparse
COPY --from=assets-builder /app/presentation-export /app/presentation-export
COPY --from=assets-builder /app/scripts/sync-presentation-export.cjs /app/scripts/sync-presentation-export.cjs
COPY scripts/sync-presentation-export.cjs /app/scripts/sync-presentation-export.cjs
RUN node /app/scripts/sync-presentation-export.cjs --force \
&& chmod +x /app/presentation-export/py/convert-linux-x64
COPY --from=nextjs-builder /app/servers/nextjs/.next-build/standalone/ /app/servers/nextjs/
COPY --from=nextjs-builder /app/servers/nextjs/public /app/servers/nextjs/public
COPY --from=nextjs-builder /app/servers/nextjs/.next-build/static /app/servers/nextjs/.next-build/static
RUN curl -fsSL https://ollama.com/install.sh | sh
COPY servers/fastapi /app/servers/fastapi
WORKDIR /app/servers/fastapi
RUN --mount=type=cache,target=/root/.cache/uv \
uv export --frozen --no-dev --no-emit-project -o /tmp/requirements.txt \
&& uv pip install --system -r /tmp/requirements.txt \
&& uv pip install --system --no-deps .
WORKDIR /app/servers/nextjs
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
RUN npm install
COPY servers/nextjs/ /app/servers/nextjs/
RUN npm run build
WORKDIR /app
COPY start.js LICENSE NOTICE ./
COPY nginx.conf /etc/nginx/nginx.conf

View file

@ -23,6 +23,7 @@ services:
- ANTHROPIC_MODEL=${ANTHROPIC_MODEL}
- OLLAMA_URL=${OLLAMA_URL}
- OLLAMA_MODEL=${OLLAMA_MODEL}
- START_OLLAMA=${START_OLLAMA:-false}
- CUSTOM_LLM_URL=${CUSTOM_LLM_URL}
- CUSTOM_LLM_API_KEY=${CUSTOM_LLM_API_KEY}
- CUSTOM_MODEL=${CUSTOM_MODEL}
@ -77,6 +78,7 @@ services:
- ANTHROPIC_MODEL=${ANTHROPIC_MODEL}
- OLLAMA_URL=${OLLAMA_URL}
- OLLAMA_MODEL=${OLLAMA_MODEL}
- START_OLLAMA=${START_OLLAMA:-false}
- CUSTOM_LLM_URL=${CUSTOM_LLM_URL}
- CUSTOM_LLM_API_KEY=${CUSTOM_LLM_API_KEY}
- CUSTOM_MODEL=${CUSTOM_MODEL}
@ -115,8 +117,8 @@ services:
- presenton_document_extraction_liteparse:/app/document-extraction-liteparse
- ./app_data:/app_data
environment:
# Dockerfile.dev does not install ollama; use a host daemon via OLLAMA_URL or omit.
- START_EMBEDDED_OLLAMA=false
# Ollama is not baked into the image; set START_OLLAMA=true for runtime install, or use OLLAMA_URL.
- START_OLLAMA=${START_OLLAMA:-false}
- MIGRATE_DATABASE_ON_STARTUP=true
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
- LLM=${LLM}
@ -172,7 +174,7 @@ services:
- presenton_document_extraction_liteparse:/app/document-extraction-liteparse
- ./app_data:/app_data
environment:
- START_EMBEDDED_OLLAMA=false
- START_OLLAMA=${START_OLLAMA:-false}
- MIGRATE_DATABASE_ON_STARTUP=true
- CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS}
- LLM=${LLM}

View file

@ -14,20 +14,17 @@ dependencies = [
"aiosqlite>=0.21.0",
"anthropic>=0.60.0",
"asyncpg>=0.30.0",
"chromadb>=1.0.15",
"dirtyjson>=1.0.8",
"fastapi[standard]>=0.116.1",
"fastembed-vectorstore>=0.5.2",
"fastmcp>=2.11.0",
"google-genai>=1.28.0",
"mem0ai[nlp]>=0.1.115",
"mem0ai>=0.1.115",
"nltk>=3.9.1",
"openai>=1.98.0",
"pathvalidate>=3.3.1",
"pdfplumber>=0.11.7",
"pytest>=8.4.1",
"python-pptx>=1.0.2",
"redis>=6.2.0",
"sqlmodel>=0.0.24",
]

View file

@ -0,0 +1,23 @@
from pathlib import Path
import sys
FASTAPI_ROOT = Path(__file__).resolve().parents[1]
if str(FASTAPI_ROOT) not in sys.path:
sys.path.insert(0, str(FASTAPI_ROOT))
from services.icon_finder_service import ICON_FINDER_SERVICE
def main() -> None:
if not ICON_FINDER_SERVICE.ensure_initialized():
raise RuntimeError("Failed to prepare fastembed cache for icon search")
print(
f"Fastembed cache prepared at {ICON_FINDER_SERVICE.cache_directory}"
)
if __name__ == "__main__":
main()

View file

@ -112,11 +112,13 @@ class IconFinderService:
self._initialization_failed = True
# Keep vectorstore as None so search_icons returns empty results
async def search_icons(self, query: str, k: int = 1):
def ensure_initialized(self) -> bool:
if not self._initialized and not self._initialization_failed:
self._initialize_icons_collection()
if not self.vectorstore or self._initialization_failed:
return self.vectorstore is not None and not self._initialization_failed
async def search_icons(self, query: str, k: int = 1):
if not self.ensure_initialized():
# Return empty list if vectorstore failed to initialize
return []

View file

@ -89,6 +89,10 @@ class Mem0PresentationMemoryService:
"history_db_path": self._history_db_path,
}
@staticmethod
def _is_nonfatal_mem0_error(exc: BaseException) -> bool:
return isinstance(exc, (Exception, SystemExit))
async def _get_client(self):
if not self._enabled:
return None
@ -121,7 +125,9 @@ class Mem0PresentationMemoryService:
self._qdrant_path,
self._history_db_path,
)
except Exception:
except BaseException as exc:
if not self._is_nonfatal_mem0_error(exc):
raise
LOGGER.exception("Failed to initialize Mem0 OSS Memory")
self._client = None
@ -147,7 +153,9 @@ class Mem0PresentationMemoryService:
try:
await asyncio.to_thread(_add)
except Exception:
except BaseException as exc:
if not self._is_nonfatal_mem0_error(exc):
raise
LOGGER.exception(
"Failed to add mem0 memory for presentation_id=%s", presentation_id
)
@ -257,7 +265,9 @@ class Mem0PresentationMemoryService:
try:
response = await asyncio.to_thread(_search)
except Exception:
except BaseException as exc:
if not self._is_nonfatal_mem0_error(exc):
raise
LOGGER.exception(
"Failed to search mem0 context for presentation_id=%s", presentation_id
)

View file

@ -341,48 +341,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "chromadb"
version = "1.0.15"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bcrypt" },
{ name = "build" },
{ name = "grpcio" },
{ name = "httpx" },
{ name = "importlib-resources" },
{ name = "jsonschema" },
{ name = "kubernetes" },
{ name = "mmh3" },
{ name = "numpy" },
{ name = "onnxruntime" },
{ name = "opentelemetry-api" },
{ name = "opentelemetry-exporter-otlp-proto-grpc" },
{ name = "opentelemetry-sdk" },
{ name = "orjson" },
{ name = "overrides" },
{ name = "posthog" },
{ name = "pybase64" },
{ name = "pydantic" },
{ name = "pypika" },
{ name = "pyyaml" },
{ name = "rich" },
{ name = "tenacity" },
{ name = "tokenizers" },
{ name = "tqdm" },
{ name = "typer" },
{ name = "typing-extensions" },
{ name = "uvicorn", extra = ["standard"] },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/e2/0653b2e539db5512d2200c759f1bc7f9ef5609fe47f3c7d24b82f62dc00f/chromadb-1.0.15.tar.gz", hash = "sha256:3e910da3f5414e2204f89c7beca1650847f2bf3bd71f11a2e40aad1eb31050aa", size = 1218840, upload-time = "2025-07-02T17:07:09.875Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/5a/866c6f0c2160cbc8dca0cf77b2fb391dcf435b32a58743da1bc1a08dc442/chromadb-1.0.15-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:51791553014297798b53df4e043e9c30f4e8bd157647971a6bb02b04bfa65f82", size = 18838820, upload-time = "2025-07-02T17:07:07.632Z" },
{ url = "https://files.pythonhosted.org/packages/e1/18/ff9b58ab5d334f5ecff7fdbacd6761bac467176708fa4d2500ae7c048af0/chromadb-1.0.15-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:48015803c0631c3a817befc276436dc084bb628c37fd4214047212afb2056291", size = 18057131, upload-time = "2025-07-02T17:07:05.15Z" },
{ url = "https://files.pythonhosted.org/packages/31/49/74e34cc5aeeb25aff2c0ede6790b3671e14c1b91574dd8f98d266a4c5aad/chromadb-1.0.15-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b73cd6fb32fcdd91c577cca16ea6112b691d72b441bb3f2140426d1e79e453a", size = 18595284, upload-time = "2025-07-02T17:06:59.102Z" },
{ url = "https://files.pythonhosted.org/packages/cb/33/190df917a057067e37f8b48d082d769bed8b3c0c507edefc7b6c6bb577d0/chromadb-1.0.15-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:479f1b401af9e7c20f50642ffb3376abbfd78e2b5b170429f7c79eff52e367db", size = 19526626, upload-time = "2025-07-02T17:07:02.163Z" },
{ url = "https://files.pythonhosted.org/packages/a1/30/6890da607358993f87a01e80bcce916b4d91515ce865f07dc06845cb472f/chromadb-1.0.15-cp39-abi3-win_amd64.whl", hash = "sha256:e0cb3b93fdc42b1786f151d413ef36299f30f783a30ce08bf0bfb12e552b4190", size = 19520490, upload-time = "2025-07-02T17:07:11.559Z" },
]
[[package]]
name = "click"
version = "8.2.1"
@ -1761,20 +1719,17 @@ dependencies = [
{ name = "alembic" },
{ name = "anthropic" },
{ name = "asyncpg" },
{ name = "chromadb" },
{ name = "dirtyjson" },
{ name = "fastapi", extra = ["standard"] },
{ name = "fastembed-vectorstore" },
{ name = "fastmcp" },
{ name = "google-genai" },
{ name = "mem0ai", extra = ["nlp"] },
{ name = "mem0ai" },
{ name = "nltk" },
{ name = "openai" },
{ name = "pathvalidate" },
{ name = "pdfplumber" },
{ name = "pytest" },
{ name = "python-pptx" },
{ name = "redis" },
{ name = "sqlmodel" },
]
@ -1786,20 +1741,17 @@ requires-dist = [
{ name = "alembic", specifier = ">=1.14.0" },
{ name = "anthropic", specifier = ">=0.60.0" },
{ name = "asyncpg", specifier = ">=0.30.0" },
{ name = "chromadb", specifier = ">=1.0.15" },
{ name = "dirtyjson", specifier = ">=1.0.8" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.116.1" },
{ name = "fastembed-vectorstore", specifier = ">=0.5.2" },
{ name = "fastmcp", specifier = ">=2.11.0" },
{ name = "google-genai", specifier = ">=1.28.0" },
{ name = "mem0ai", extras = ["nlp"], specifier = ">=0.1.115" },
{ name = "mem0ai", specifier = ">=0.1.115" },
{ name = "nltk", specifier = ">=3.9.1" },
{ name = "openai", specifier = ">=1.98.0" },
{ name = "pathvalidate", specifier = ">=3.3.1" },
{ name = "pdfplumber", specifier = ">=0.11.7" },
{ name = "pytest", specifier = ">=8.4.1" },
{ name = "python-pptx", specifier = ">=1.0.2" },
{ name = "redis", specifier = ">=6.2.0" },
{ name = "sqlmodel", specifier = ">=0.0.24" },
]
@ -2084,22 +2036,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" },
]
[[package]]
name = "pytest"
version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "pygments" },
]
sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@ -2199,18 +2135,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/68/69/77d1a971c4b933e8c79403e99bcbb790463da5e48333cc4fd5d412c63c98/qdrant_client-1.17.1-py3-none-any.whl", hash = "sha256:6cda4064adfeaf211c751f3fbc00edbbdb499850918c7aff4855a9a759d56cbd", size = 389947, upload-time = "2026-03-13T17:13:43.156Z" },
]
[[package]]
name = "redis"
version = "6.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "async-timeout", marker = "python_full_version < '3.11.3'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ea/9a/0551e01ba52b944f97480721656578c8a7c46b51b99d66814f85fe3a4f3e/redis-6.2.0.tar.gz", hash = "sha256:e821f129b75dde6cb99dd35e5c76e8c49512a5a0d8dfdc560b2fbd44b85ca977", size = 4639129, upload-time = "2025-05-28T05:01:18.91Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/67/e60968d3b0e077495a8fee89cf3f2373db98e528288a48f1ee44967f6e8c/redis-6.2.0-py3-none-any.whl", hash = "sha256:c8ddf316ee0aab65f04a11229e94a64b2618451dab7a67cb2f77eb799d872d5e", size = 278659, upload-time = "2025-05-28T05:01:16.955Z" },
]
[[package]]
name = "referencing"
version = "0.36.2"

View file

@ -2,6 +2,7 @@
const nextConfig = {
reactStrictMode: false,
distDir: ".next-build",
output: "standalone",
// Rewrites for development - proxy font requests to FastAPI backend

View file

@ -37,6 +37,8 @@
".next-build/types/**/*.ts"
],
"exclude": [
"node_modules"
"node_modules",
"**/*.cy.ts",
"**/*.cy.tsx"
]
}

View file

@ -10,6 +10,7 @@ const __dirname = dirname(__filename);
const fastapiDir = join(__dirname, "servers/fastapi");
const nextjsDir = join(__dirname, "servers/nextjs");
const nextjsStandaloneServer = join(nextjsDir, "server.js");
const exportSyncScript = join(__dirname, "scripts/sync-presentation-export.cjs");
const args = process.argv.slice(2);
@ -56,28 +57,60 @@ const setupNodeModules = () => {
});
};
const runNodeScript = (scriptPath, scriptArgs) => {
const runCommand = (command, commandArgs, options = {}) => {
return new Promise((resolve, reject) => {
const scriptProcess = spawn(process.execPath, [scriptPath, ...scriptArgs], {
cwd: __dirname,
stdio: "inherit",
env: process.env,
const child = spawn(command, commandArgs, {
cwd: options.cwd || __dirname,
stdio: options.stdio || "inherit",
env: options.env || process.env,
});
scriptProcess.on("error", (err) => {
child.on("error", (err) => {
reject(err);
});
scriptProcess.on("exit", (code) => {
child.on("exit", (code) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`Script failed with exit code: ${code}`));
reject(new Error(`${command} exited with code: ${code}`));
}
});
});
};
const runNodeScript = (scriptPath, scriptArgs) => {
return runCommand(process.execPath, [scriptPath, ...scriptArgs], {
cwd: __dirname,
});
};
const isTruthyEnv = (value) => {
if (value == null) {
return false;
}
return !["", "0", "false", "no", "off"].includes(
String(value).trim().toLowerCase()
);
};
const isOllamaInstalled = () =>
existsSync("/usr/bin/ollama") || existsSync("/usr/local/bin/ollama");
const shouldStartOllama = () => isTruthyEnv(process.env.START_OLLAMA);
const ensureOllamaRuntime = async () => {
if (!shouldStartOllama() || isOllamaInstalled()) {
return;
}
console.log("START_OLLAMA=true; installing Ollama runtime...");
await runCommand("sh", ["-c", "curl -fsSL https://ollama.com/install.sh | sh"], {
cwd: "/",
});
};
const ensurePresentationExportRuntime = async () => {
if (process.env.ENSURE_PRESENTATION_EXPORT_RUNTIME === "false") {
return;
@ -195,21 +228,32 @@ const startServers = async () => {
console.error("App MCP process failed to start:", err);
});
const useStandaloneNextjs = !isDev && existsSync(nextjsStandaloneServer);
const nextjsProcess = spawn(
"npm",
[
"run",
isDev ? "dev" : "start",
"--",
"-H",
"127.0.0.1",
"-p",
nextjsPort.toString(),
],
useStandaloneNextjs ? process.execPath : "npm",
useStandaloneNextjs
? [nextjsStandaloneServer]
: [
"run",
isDev ? "dev" : "start",
"--",
"-H",
"127.0.0.1",
"-p",
nextjsPort.toString(),
],
{
cwd: nextjsDir,
stdio: "inherit",
env: process.env,
env:
useStandaloneNextjs
? {
...process.env,
HOSTNAME: "127.0.0.1",
PORT: nextjsPort.toString(),
}
: process.env,
}
);
@ -217,16 +261,15 @@ const startServers = async () => {
console.error("Next.js process failed to start:", err);
});
const startEmbeddedOllama =
process.env.START_EMBEDDED_OLLAMA !== "false" &&
process.env.START_EMBEDDED_OLLAMA !== "0";
const shouldStartOllamaRuntime = shouldStartOllama();
const ollamaInstalled = isOllamaInstalled();
const exitPromises = [
new Promise((resolve) => fastApiProcess.on("exit", resolve)),
new Promise((resolve) => nextjsProcess.on("exit", resolve)),
];
if (startEmbeddedOllama) {
if (shouldStartOllamaRuntime && ollamaInstalled) {
const ollamaProcess = spawn("ollama", ["serve"], {
cwd: "/",
stdio: "inherit",
@ -236,9 +279,13 @@ const startServers = async () => {
console.error("Ollama process failed to start:", err);
});
exitPromises.push(new Promise((resolve) => ollamaProcess.on("exit", resolve)));
} else if (shouldStartOllamaRuntime) {
console.log(
"Ollama requested, but the binary is not installed. Set START_OLLAMA=true to install it at startup, or set OLLAMA_URL to a remote daemon."
);
} else {
console.log(
"Embedded Ollama disabled (START_EMBEDDED_OLLAMA=false); use OLLAMA_URL for a remote daemon if needed."
"Ollama disabled (START_OLLAMA=false); use OLLAMA_URL for a remote daemon if needed."
);
}
@ -271,6 +318,7 @@ const startNginx = () => {
const main = async () => {
await ensurePresentationExportRuntime();
await ensureOllamaRuntime();
if (isDev) {
await setupNodeModules();