feat: Add support for dynamic FastAPI origin in image asset handling

This commit is contained in:
sudipnext 2026-03-29 17:11:22 +05:45
parent c6ad9de46b
commit f200c84d0c
4 changed files with 47 additions and 7 deletions

View file

@ -7,10 +7,23 @@ from sqlalchemy import JSON, Column, DateTime
from sqlmodel import Field, SQLModel
from utils.datetime_utils import get_current_utc_datetime
from utils.get_env import get_app_data_directory_env
from utils.get_env import get_app_data_directory_env, get_next_public_fast_api_env
from utils.path_helpers import get_resource_path
def _with_fastapi_origin(path: str) -> str:
"""Prefix relative web paths with FastAPI origin when available."""
if path.startswith("http://") or path.startswith("https://"):
return path
fastapi_origin = (get_next_public_fast_api_env() or "").strip()
if not fastapi_origin:
return path
normalized_path = path if path.startswith("/") else f"/{path}"
return f"{fastapi_origin.rstrip('/')}{normalized_path}"
class ImageAsset(SQLModel, table=True):
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
created_at: datetime = Field(
@ -36,6 +49,10 @@ class ImageAsset(SQLModel, table=True):
if path.startswith("http://") or path.startswith("https://"):
return path
# Already a web path under known mounts
if path.startswith("/app_data/") or path.startswith("/static/"):
return _with_fastapi_origin(path)
# Normalize filesystem path
real_path = os.path.realpath(path)
@ -46,7 +63,7 @@ class ImageAsset(SQLModel, table=True):
if real_path.startswith(app_data_dir_real):
rel = os.path.relpath(real_path, app_data_dir_real)
rel_web = rel.replace(os.sep, "/")
return f"/app_data/{rel_web}"
return _with_fastapi_origin(f"/app_data/{rel_web}")
# Map packaged static assets to /static/...
static_root = get_resource_path("static")
@ -54,7 +71,7 @@ class ImageAsset(SQLModel, table=True):
if real_path.startswith(static_root_real):
rel = os.path.relpath(real_path, static_root_real)
rel_web = rel.replace(os.sep, "/")
return f"/static/{rel_web}"
return _with_fastapi_origin(f"/static/{rel_web}")
# Fallback: return the original path (may be absolute or relative);
# frontend can decide how to handle unusual cases.

View file

@ -1,5 +1,6 @@
import uvicorn
import argparse
import os
from api.main import app
if __name__ == "__main__":
@ -12,10 +13,14 @@ if __name__ == "__main__":
)
args = parser.parse_args()
reload = args.reload == "true"
host = "127.0.0.1"
# Provide a predictable public URL for services that need absolute asset links.
os.environ.setdefault("FASTAPI_PUBLIC_URL", f"http://{host}:{args.port}")
uvicorn.run(
"api.main:app",
host="127.0.0.1",
host=host,
port=args.port,
log_level="info",
reload=reload,

View file

@ -12,6 +12,7 @@ from models.sql.image_asset import ImageAsset
from utils.get_env import (
get_dall_e_3_quality_env,
get_gpt_image_1_5_quality_env,
get_next_public_fast_api_env,
get_pexels_api_key_env,
)
from utils.get_env import get_pixabay_api_key_env
@ -59,6 +60,17 @@ class ImageGenerationService:
def is_stock_provider_selected(self):
return is_pixels_selected() or is_pixabay_selected()
def _to_frontend_url(self, path: str) -> str:
if path.startswith("http://") or path.startswith("https://"):
return path
fastapi_origin = (get_next_public_fast_api_env() or "").strip()
if not fastapi_origin:
return path
normalized_path = path if path.startswith("/") else f"/{path}"
return f"{fastapi_origin.rstrip('/')}{normalized_path}"
async def generate_image(self, prompt: ImagePrompt) -> str | ImageAsset:
"""
Generates an image based on the provided prompt.
@ -69,11 +81,11 @@ class ImageGenerationService:
"""
if self.is_image_generation_disabled:
print("Image generation is disabled. Using placeholder image.")
return "/static/images/placeholder.jpg"
return self._to_frontend_url("/static/images/placeholder.jpg")
if not self.image_gen_func:
print("No image generation function found. Using placeholder image.")
return "/static/images/placeholder.jpg"
return self._to_frontend_url("/static/images/placeholder.jpg")
image_prompt = prompt.get_image_prompt(
with_theme=not self.is_stock_provider_selected()
@ -99,11 +111,13 @@ class ImageGenerationService:
"theme_prompt": prompt.theme_prompt,
},
)
elif image_path.startswith("/app_data/") or image_path.startswith("/static/"):
return self._to_frontend_url(image_path)
raise Exception(f"Image not found at {image_path}")
except Exception as e:
print(f"Error generating image: {e}")
return "/static/images/placeholder.jpg"
return self._to_frontend_url("/static/images/placeholder.jpg")
async def generate_image_openai(
self, prompt: str, output_directory: str, model: str, quality: str

View file

@ -142,3 +142,7 @@ def get_codex_model_env():
def get_migrate_database_on_startup_env():
return os.getenv("MIGRATE_DATABASE_ON_STARTUP")
def get_next_public_fast_api_env():
return os.getenv("NEXT_PUBLIC_FAST_API")