Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 3 additions & 2 deletions backend/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

from app.routes import api_keys, papers, scripts, slides, media, images, auth, papertovideo, youtube_upload, feedback, patents, reels, podcast, external_api, poster, business_brief
from app.routes import api_keys, papers, scripts, slides, media, images, auth, papertovideo, youtube_upload, feedback, patents, reels, podcast, external_api, poster, business_brief, webpage
from app.workers import init_worker_pool, close_worker_pool
## Removed: from app.database import create_tables (no longer needed)

Expand All @@ -31,7 +31,7 @@
temp_dirs = [
"temp/arxiv_sources", "temp/images", "temp/title_slides",
"temp/videos", "temp/audio", "temp/latex_template",
"temp/slides", "temp/scripts"
"temp/slides", "temp/scripts", "temp/webpages"
]

for dir_path in temp_dirs:
Expand Down Expand Up @@ -197,6 +197,7 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE
app.include_router(podcast.router, prefix="/api/podcast", tags=["podcast"])
app.include_router(external_api.router, prefix="/api/external", tags=["External API"])
app.include_router(business_brief.router, prefix="/api/business-brief", tags=["Business Brief"])
app.include_router(webpage.router, prefix="/api/webpage", tags=["Webpage"])

instrumentator = Instrumentator(
should_group_status_codes=True,
Expand Down
19 changes: 18 additions & 1 deletion backend/app/models/request_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,21 @@ class BusinessBriefResponse(BaseModel):
status: str

class BusinessBriefUpdateRequest(BaseModel):
sections: Dict[str, str]
sections: Dict[str, str]


class WebpageGenerateRequest(BaseModel):
variant_count: int = 1


class WebpageVariant(BaseModel):
variant_id: str
theme: str
preview_url: str
download_url: str
created_at: str


class WebpageGenerateResponse(BaseModel):
paper_id: str
variants: List[WebpageVariant]
152 changes: 152 additions & 0 deletions backend/app/routes/webpage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import os
import random
from pathlib import Path

from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import FileResponse, HTMLResponse

from app.auth.dependencies import get_current_user
from app.models.request_models import WebpageGenerateRequest, WebpageGenerateResponse, WebpageVariant
from app.routes.api_keys import get_api_keys
from app.services.storage_manager import storage_manager
from app.services.webpage_generator import STYLE_PROFILES, clear_variants, generate_variant, list_variants, save_variant_meta

router = APIRouter()


def _resolve_existing_path(raw_path: str | None) -> Path | None:
if not raw_path:
return None

p = Path(raw_path)
backend_dir = Path(__file__).resolve().parents[2]
repo_dir = Path(__file__).resolve().parents[3]

candidates = [p, backend_dir / p, repo_dir / p]
if raw_path.startswith("backend/"):
tail = Path(raw_path[len("backend/"):])
candidates.append(backend_dir / tail)

for cand in candidates:
if cand.exists():
return cand
return None


@router.post("/{paper_id}/generate", response_model=WebpageGenerateResponse)
async def generate_webpage(
paper_id: str,
request: WebpageGenerateRequest,
current_user: dict = Depends(get_current_user),
api_keys: dict = Depends(get_api_keys),
):
paper_info = storage_manager.get_paper(paper_id)
if not paper_info:
raise HTTPException(status_code=404, detail="Paper not found")

gemini_key = api_keys.get("gemini_key") or os.getenv("GEMINI_API_KEY")
if not gemini_key:
raise HTTPException(status_code=400, detail="Gemini API key not configured")

variants = []
count = 1
clear_variants(paper_id)

unique_profiles = random.sample(STYLE_PROFILES, k=count)
used_theme_names = set()
Comment thread
ram-from-tvl marked this conversation as resolved.
for idx in range(count):
last_error = None
variant_id = None
meta = None

# Retry a few times because model outputs can occasionally violate constraints.
for attempt in range(4):
preferred = unique_profiles[idx]
if preferred["name"] in used_theme_names:
pool = [p for p in STYLE_PROFILES if p["name"] not in used_theme_names]
preferred = random.choice(pool) if pool else random.choice(STYLE_PROFILES)

try:
variant_id, meta = generate_variant(
paper_id=paper_id,
user_id=current_user.get("id", "local-user"),
paper_info=paper_info,
api_key=gemini_key,
variant_index=idx,
profile_override=preferred,
)
used_theme_names.add(meta["theme"])
break
except Exception as exc:
last_error = exc
continue

if not variant_id or not meta:
raise HTTPException(status_code=502, detail=f"Unable to generate high-quality webpage variant: {last_error}")

save_variant_meta(paper_id, meta)
variants.append(
WebpageVariant(
variant_id=variant_id,
theme=meta["theme"],
preview_url=f"/api/webpage/{paper_id}/preview/{variant_id}",
download_url=f"/api/webpage/{paper_id}/download/{variant_id}",
created_at=meta["created_at"],
)
)

return WebpageGenerateResponse(paper_id=paper_id, variants=variants)


@router.get("/{paper_id}/variants", response_model=list[WebpageVariant])
async def get_variants(paper_id: str, current_user: dict = Depends(get_current_user)):
_ = current_user
metas = list_variants(paper_id)
out = []
for meta in metas:
variant_id = meta.get("variant_id")
if not variant_id:
continue
out.append(
WebpageVariant(
variant_id=variant_id,
theme=meta.get("theme", "Custom"),
preview_url=f"/api/webpage/{paper_id}/preview/{variant_id}",
download_url=f"/api/webpage/{paper_id}/download/{variant_id}",
created_at=meta.get("created_at", ""),
)
)
return out


@router.get("/{paper_id}/preview/{variant_id}")
async def preview_variant(paper_id: str, variant_id: str, current_user: dict = Depends(get_current_user)):
_ = current_user
path = Path(f"temp/webpages/{paper_id}/{variant_id}.html")
if not path.exists():
raise HTTPException(status_code=404, detail="Variant not found")
return HTMLResponse(path.read_text(encoding="utf-8"))


@router.get("/{paper_id}/download/{variant_id}")
async def download_variant(paper_id: str, variant_id: str, current_user: dict = Depends(get_current_user)):
_ = current_user
path = Path(f"temp/webpages/{paper_id}/{variant_id}.html")
if not path.exists():
raise HTTPException(status_code=404, detail="Variant not found")
return FileResponse(path, media_type="text/html", filename=f"webpage_{variant_id}.html")


@router.get("/{paper_id}/asset/{image_name}")
async def get_variant_asset(paper_id: str, image_name: str, current_user: dict = Depends(get_current_user)):
_ = current_user
paper_info = storage_manager.get_paper(paper_id)
if not paper_info:
raise HTTPException(status_code=404, detail="Paper not found")

for raw_path in paper_info.get("image_files", []):
resolved = _resolve_existing_path(raw_path)
if resolved and resolved.name == image_name:
return FileResponse(resolved)
Comment thread
ram-from-tvl marked this conversation as resolved.

raise HTTPException(status_code=404, detail="Asset not found")
Loading