Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,6 @@ presentation-export/py/
.cache/presentation-export/
servers/fastapi/build/
servers/fastapi/dist/
servers/fastapi/fastembed_cache/
servers/fastapi/fastembed_cache/

.idea
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ services:
- LITEPARSE_NUM_WORKERS=${LITEPARSE_NUM_WORKERS:-1}
- OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL}
- OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY}
- CUSTOM_IMAGE_URL=${CUSTOM_IMAGE_URL}
- CUSTOM_IMAGE_API_KEY=${CUSTOM_IMAGE_API_KEY}
- CUSTOM_IMAGE_MODEL=${CUSTOM_IMAGE_MODEL}
- AUTH_USERNAME=${AUTH_USERNAME:-}
- AUTH_PASSWORD=${AUTH_PASSWORD:-}
- AUTH_OVERRIDE_FROM_ENV=${AUTH_OVERRIDE_FROM_ENV:-}
Expand Down Expand Up @@ -145,6 +148,9 @@ services:
- LITEPARSE_NUM_WORKERS=${LITEPARSE_NUM_WORKERS:-1}
- OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL}
- OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY}
- CUSTOM_IMAGE_URL=${CUSTOM_IMAGE_URL}
- CUSTOM_IMAGE_API_KEY=${CUSTOM_IMAGE_API_KEY}
- CUSTOM_IMAGE_MODEL=${CUSTOM_IMAGE_MODEL}
- AUTH_USERNAME=${AUTH_USERNAME:-}
- AUTH_PASSWORD=${AUTH_PASSWORD:-}
- AUTH_OVERRIDE_FROM_ENV=${AUTH_OVERRIDE_FROM_ENV:-}
Expand Down Expand Up @@ -219,6 +225,9 @@ services:
- LITEPARSE_NUM_WORKERS=${LITEPARSE_NUM_WORKERS:-1}
- OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL}
- OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY}
- CUSTOM_IMAGE_URL=${CUSTOM_IMAGE_URL}
- CUSTOM_IMAGE_API_KEY=${CUSTOM_IMAGE_API_KEY}
- CUSTOM_IMAGE_MODEL=${CUSTOM_IMAGE_MODEL}
- AUTH_USERNAME=${AUTH_USERNAME:-}
- AUTH_PASSWORD=${AUTH_PASSWORD:-}
- AUTH_OVERRIDE_FROM_ENV=${AUTH_OVERRIDE_FROM_ENV:-}
Expand Down Expand Up @@ -299,6 +308,9 @@ services:
- LITEPARSE_NUM_WORKERS=${LITEPARSE_NUM_WORKERS:-1}
- OPEN_WEBUI_IMAGE_URL=${OPEN_WEBUI_IMAGE_URL}
- OPEN_WEBUI_IMAGE_API_KEY=${OPEN_WEBUI_IMAGE_API_KEY}
- CUSTOM_IMAGE_URL=${CUSTOM_IMAGE_URL}
- CUSTOM_IMAGE_API_KEY=${CUSTOM_IMAGE_API_KEY}
- CUSTOM_IMAGE_MODEL=${CUSTOM_IMAGE_MODEL}
- AUTH_USERNAME=${AUTH_USERNAME:-}
- AUTH_PASSWORD=${AUTH_PASSWORD:-}
- AUTH_OVERRIDE_FROM_ENV=${AUTH_OVERRIDE_FROM_ENV:-}
Expand Down
18 changes: 18 additions & 0 deletions electron/app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ async function startServers(fastApiPort: number, nextjsPort: number) {
COMFYUI_WORKFLOW: process.env.COMFYUI_WORKFLOW,
DALL_E_3_QUALITY: process.env.DALL_E_3_QUALITY,
GPT_IMAGE_1_5_QUALITY: process.env.GPT_IMAGE_1_5_QUALITY,
OPEN_WEBUI_IMAGE_URL: process.env.OPEN_WEBUI_IMAGE_URL,
OPEN_WEBUI_IMAGE_API_KEY: process.env.OPEN_WEBUI_IMAGE_API_KEY,
CUSTOM_IMAGE_URL: process.env.CUSTOM_IMAGE_URL,
CUSTOM_IMAGE_API_KEY: process.env.CUSTOM_IMAGE_API_KEY,
CUSTOM_IMAGE_MODEL: process.env.CUSTOM_IMAGE_MODEL,
APP_DATA_DIRECTORY: appDataDir,
FASTAPI_PUBLIC_URL: process.env.NEXT_PUBLIC_FAST_API,
TEMP_DIRECTORY: tempDir,
Expand Down Expand Up @@ -388,6 +393,11 @@ app.whenReady().then(async () => {
COMFYUI_WORKFLOW: process.env.COMFYUI_WORKFLOW,
DALL_E_3_QUALITY: process.env.DALL_E_3_QUALITY,
GPT_IMAGE_1_5_QUALITY: process.env.GPT_IMAGE_1_5_QUALITY,
OPEN_WEBUI_IMAGE_URL: process.env.OPEN_WEBUI_IMAGE_URL,
OPEN_WEBUI_IMAGE_API_KEY: process.env.OPEN_WEBUI_IMAGE_API_KEY,
CUSTOM_IMAGE_URL: process.env.CUSTOM_IMAGE_URL,
CUSTOM_IMAGE_API_KEY: process.env.CUSTOM_IMAGE_API_KEY,
CUSTOM_IMAGE_MODEL: process.env.CUSTOM_IMAGE_MODEL,
})

const [fastApiPort, nextjsPort] = await findUnusedPorts();
Expand Down Expand Up @@ -422,3 +432,11 @@ app.on("will-quit", async (event) => {
event.preventDefault();
await forceQuitApp(0);
});

process.on("SIGINT", async () => {
await forceQuitApp(0);
});

process.on("SIGTERM", async () => {
await forceQuitApp(0);
});
10 changes: 10 additions & 0 deletions electron/app/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ interface FastApiEnv {
COMFYUI_WORKFLOW?: string,
DALL_E_3_QUALITY?: string,
GPT_IMAGE_1_5_QUALITY?: string,
OPEN_WEBUI_IMAGE_URL?: string,
OPEN_WEBUI_IMAGE_API_KEY?: string,
CUSTOM_IMAGE_URL?: string,
CUSTOM_IMAGE_API_KEY?: string,
CUSTOM_IMAGE_MODEL?: string,
APP_DATA_DIRECTORY?: string,
FASTAPI_PUBLIC_URL?: string,
TEMP_DIRECTORY?: string,
Expand Down Expand Up @@ -94,6 +99,11 @@ interface UserConfig {
COMFYUI_WORKFLOW?: string,
DALL_E_3_QUALITY?: string,
GPT_IMAGE_1_5_QUALITY?: string,
OPEN_WEBUI_IMAGE_URL?: string,
OPEN_WEBUI_IMAGE_API_KEY?: string,
CUSTOM_IMAGE_URL?: string,
CUSTOM_IMAGE_API_KEY?: string,
CUSTOM_IMAGE_MODEL?: string,
CODEX_MODEL?: string,
CODEX_ACCESS_TOKEN?: string,
CODEX_REFRESH_TOKEN?: string,
Expand Down
32 changes: 23 additions & 9 deletions electron/app/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,32 @@ export function setupEnv(fastApiPort: number, nextjsPort: number) {
}


export function killProcess(pid: number, signal: NodeJS.Signals = "SIGTERM") {
return new Promise((resolve, reject) => {
export function killProcess(pid: number, signal: NodeJS.Signals = "SIGTERM", timeoutMs: number = 3000): Promise<void> {
return new Promise((resolve) => {
treeKill(pid, signal, (err: any) => {
if (err) {
console.error(`Error killing process ${pid}:`, err)
reject(err)
} else {
console.log(`Process ${pid} killed (${signal})`)
resolve(true)
console.warn(`SIGTERM failed for PID ${pid}, sending SIGKILL`);
treeKill(pid, "SIGKILL", () => resolve());
return;
Comment on lines +53 to +59
}
})
})
// Poll to confirm process is dead
const start = Date.now();
const check = setInterval(() => {
try {
process.kill(pid, 0); // throws if process doesn't exist
if (Date.now() - start > timeoutMs) {
clearInterval(check);
console.warn(`PID ${pid} still alive after ${timeoutMs}ms, sending SIGKILL`);
treeKill(pid, "SIGKILL", () => resolve());
}
} catch {
clearInterval(check);
console.log(`PID ${pid} confirmed dead`);
resolve();
}
}, 100);
});
});
}

export async function findUnusedPorts(startPort: number = 40000, count: number = 2): Promise<number[]> {
Expand Down
3 changes: 2 additions & 1 deletion electron/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ const config = {
target: ["dmg"],
category: "public.app-category.productivity",
icon: "resources/ui/assets/images/presenton_short_filled.png",
identity: null,
},
linux: {
artifactName: "Presenton-${version}.${ext}",
Expand All @@ -89,7 +90,7 @@ const config = {
recommends: ["libreoffice"],
},
win: {
target: ["nsis", "appx"],
target: ["nsis"],
icon: "build/icon.ico",
artifactName: "Presenton-${version}.${ext}",
executableName: "Presenton",
Expand Down
462 changes: 231 additions & 231 deletions presentation-export/index.cjs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions servers/fastapi/enums/image_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ class ImageProvider(Enum):
GPT_IMAGE_1_5 = "gpt-image-1.5"
COMFYUI = "comfyui"
OPEN_WEBUI = "open_webui"
CUSTOM_IMAGE = "custom_image"
5 changes: 5 additions & 0 deletions servers/fastapi/models/user_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ class UserConfig(BaseModel):
OPEN_WEBUI_IMAGE_URL: Optional[str] = None
OPEN_WEBUI_IMAGE_API_KEY: Optional[str] = None

# Custom Image Provider (OpenAI-compatible)
CUSTOM_IMAGE_URL: Optional[str] = None
CUSTOM_IMAGE_API_KEY: Optional[str] = None
CUSTOM_IMAGE_MODEL: Optional[str] = None

# Dalle 3 Quality
DALL_E_3_QUALITY: Optional[str] = None
# Gpt Image 1.5 Quality
Expand Down
4 changes: 2 additions & 2 deletions servers/fastapi/services/chat/memory_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from services.image_generation_service import ImageGenerationService
from services.mem0_presentation_memory_service import MEM0_PRESENTATION_MEMORY_SERVICE
from templates.presentation_layout import SlideLayoutModel
from utils.asset_directory_utils import get_images_directory
from utils.asset_directory_utils import filesystem_path_to_app_data_url, get_images_directory
from utils.process_slides import (
process_old_and_new_slides_and_fetch_assets,
process_slide_and_fetch_assets,
Expand Down Expand Up @@ -220,7 +220,7 @@ async def generate_image(self, prompt: str) -> str:
if isinstance(image, ImageAsset):
self._sql_session.add(image)
await self._sql_session.commit()
return image.path
return filesystem_path_to_app_data_url(image.path)

return str(image)

Expand Down
55 changes: 55 additions & 0 deletions servers/fastapi/services/image_generation_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
get_pexels_api_key_env,
get_open_webui_image_url_env,
get_open_webui_image_api_key_env,
get_custom_image_url_env,
get_custom_image_api_key_env,
get_custom_image_model_env,
)
from utils.get_env import get_pixabay_api_key_env
from utils.get_env import get_comfyui_url_env
Expand All @@ -29,6 +32,7 @@
is_dalle3_selected,
is_comfyui_selected,
is_open_webui_selected,
is_custom_image_selected,
)
import uuid

Expand Down Expand Up @@ -59,6 +63,8 @@ def get_image_gen_func(self):
return self.generate_image_comfyui
elif is_open_webui_selected():
return self.generate_image_open_webui
elif is_custom_image_selected():
return self.generate_image_custom
return None

def is_stock_provider_selected(self):
Expand Down Expand Up @@ -233,6 +239,55 @@ async def generate_image_open_webui(

return image_path

async def generate_image_custom(
self, prompt: str, output_directory: str
) -> str:
"""
Generate an image using a custom OpenAI-compatible image generation endpoint.

Requires:
- CUSTOM_IMAGE_URL: Base URL of the OpenAI-compatible API (e.g. https://api.example.com/v1)
- CUSTOM_IMAGE_MODEL: Model name to use
- CUSTOM_IMAGE_API_KEY: Optional API key
"""
base_url = get_custom_image_url_env()
if not base_url:
raise ValueError("CUSTOM_IMAGE_URL environment variable is not set")

model = get_custom_image_model_env()
if not model:
raise ValueError("CUSTOM_IMAGE_MODEL environment variable is not set")

api_key = get_custom_image_api_key_env() or "not-needed"

client = AsyncOpenAI(base_url=base_url.rstrip("/"), api_key=api_key)
Comment on lines +261 to +263
result = await client.images.generate(
model=model,
prompt=prompt,
n=1,
)

image_path = os.path.join(output_directory, f"{uuid.uuid4()}.png")

item = result.data[0]
if item.b64_json:
with open(image_path, "wb") as f:
f.write(base64.b64decode(item.b64_json))
elif item.url:
async with aiohttp.ClientSession(trust_env=True) as session:
resp = await session.get(
item.url,
timeout=aiohttp.ClientTimeout(total=120),
)
if resp.status != 200:
raise Exception(f"Failed to download custom image: {resp.status}")
with open(image_path, "wb") as f:
f.write(await resp.read())
else:
raise Exception("Custom image provider returned no image data")

return image_path

async def _generate_image_google(
self, prompt: str, output_directory: str, model: str
) -> str:
Expand Down
21 changes: 21 additions & 0 deletions servers/fastapi/utils/asset_directory_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,27 @@ def resolve_image_path_to_filesystem(path_or_url: str) -> Optional[str]:
return resolve_app_path_to_filesystem(path_or_url)


def filesystem_path_to_app_data_url(path: str) -> str:
"""
Convert an absolute filesystem path inside APP_DATA_DIRECTORY to a
FastAPI-mounted /app_data/... URL so the frontend can resolve it correctly
on any OS (macOS paths like /Users/.../Library/... would otherwise break).
Returns the path unchanged if it doesn't fall under APP_DATA_DIRECTORY or
is already a URL/relative path.
"""
if not path or path.startswith("/app_data/") or path.startswith("/static/") or path.startswith("http"):
return path
app_data_dir = get_app_data_directory_env()
if app_data_dir:
normalized_app_data = app_data_dir.rstrip("/\\")
normalized_path = path.replace("\\", "/")
normalized_base = normalized_app_data.replace("\\", "/")
if normalized_path.startswith(normalized_base + "/"):
relative = normalized_path[len(normalized_base) + 1:]
return f"/app_data/{relative}"
return path


def get_images_directory():
images_directory = os.path.join(get_app_data_directory_env(), "images")
os.makedirs(images_directory, exist_ok=True)
Expand Down
14 changes: 14 additions & 0 deletions servers/fastapi/utils/get_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,3 +227,17 @@ def get_open_webui_image_url_env():

def get_open_webui_image_api_key_env():
return os.getenv("OPEN_WEBUI_IMAGE_API_KEY")


# Custom Image Provider (OpenAI-compatible)
def get_custom_image_url_env():
return os.getenv("CUSTOM_IMAGE_URL")


def get_custom_image_api_key_env():
return os.getenv("CUSTOM_IMAGE_API_KEY")


def get_custom_image_model_env():
return os.getenv("CUSTOM_IMAGE_MODEL")

4 changes: 4 additions & 0 deletions servers/fastapi/utils/image_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ def is_open_webui_selected() -> bool:
return ImageProvider.OPEN_WEBUI == get_selected_image_provider()


def is_custom_image_selected() -> bool:
return ImageProvider.CUSTOM_IMAGE == get_selected_image_provider()


def get_selected_image_provider() -> ImageProvider | None:
"""
Get the selected image provider from environment variables.
Expand Down
5 changes: 3 additions & 2 deletions servers/fastapi/utils/process_slides.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from services.icon_finder_service import ICON_FINDER_SERVICE
from services.image_generation_service import ImageGenerationService
from utils.dict_utils import get_dict_at_path, get_dict_paths_with_key, set_dict_at_path
from utils.asset_directory_utils import filesystem_path_to_app_data_url


async def process_slide_and_fetch_assets(
Expand Down Expand Up @@ -56,7 +57,7 @@ async def process_slide_and_fetch_assets(
image_dict = get_dict_at_path(slide.content, asset_path)
if isinstance(result, ImageAsset):
return_assets.append(result)
image_dict["__image_url__"] = result.path
image_dict["__image_url__"] = filesystem_path_to_app_data_url(result.path)
else:
image_dict["__image_url__"] = result
set_dict_at_path(slide.content, asset_path, image_dict)
Expand Down Expand Up @@ -169,7 +170,7 @@ async def process_old_and_new_slides_and_fetch_assets(
fetched_image = new_images[i]
if isinstance(fetched_image, ImageAsset):
new_assets.append(fetched_image)
image_url = fetched_image.path
image_url = filesystem_path_to_app_data_url(fetched_image.path)
else:
image_url = fetched_image
new_image_dicts[i]["__image_url__"] = image_url
Expand Down
14 changes: 14 additions & 0 deletions servers/fastapi/utils/set_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,17 @@ def set_open_webui_image_url_env(value: str):

def set_open_webui_image_api_key_env(value: str):
os.environ["OPEN_WEBUI_IMAGE_API_KEY"] = value


# Custom Image Provider (OpenAI-compatible)
def set_custom_image_url_env(value: str):
os.environ["CUSTOM_IMAGE_URL"] = value


def set_custom_image_api_key_env(value: str):
os.environ["CUSTOM_IMAGE_API_KEY"] = value


def set_custom_image_model_env(value: str):
os.environ["CUSTOM_IMAGE_MODEL"] = value

Loading