Some checks failed
Profile Linker Docker Build / Build and push Docker image (push) Failing after 3s
- Integrated Cloudflare R2 for template storage and converted file management - Added Google Gemini AI for resume parsing and HTML generation - Created backend API endpoints for templates, conversion, and history - Refactored frontend to use real API instead of mock data - Fixed Docker networking issues (IPv6/IPv4) for R2 connectivity - Added resumeService.ts for frontend API integration - Updated Vite configuration for proper asset serving in Docker - Successfully tested with 13 templates from R2 bucket
180 lines
5.2 KiB
Python
180 lines
5.2 KiB
Python
from fastapi import APIRouter, UploadFile, File, Form, HTTPException
|
|
from fastapi.responses import JSONResponse
|
|
from typing import List, Dict, Optional
|
|
import io
|
|
|
|
from app.services.r2_service import r2_service
|
|
from app.services.ai_service import ai_service
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/templates", response_model=List[str])
|
|
async def get_templates():
|
|
"""
|
|
Get list of available resume templates from R2
|
|
"""
|
|
try:
|
|
templates = r2_service.list_templates()
|
|
return templates
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to fetch templates: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/templates/{template_name}")
|
|
async def get_template_content(template_name: str):
|
|
"""
|
|
Get the HTML content of a specific template
|
|
"""
|
|
try:
|
|
content = r2_service.get_template_content(template_name)
|
|
if content is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Template '{template_name}' not found"
|
|
)
|
|
return {"content": content}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to fetch template content: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.post("/convert")
|
|
async def convert_resume(
|
|
file: UploadFile = File(...),
|
|
template_name: str = Form(...)
|
|
):
|
|
"""
|
|
Convert a resume file using the specified template
|
|
1. Extract text from resume using Gemini AI
|
|
2. Get template content from R2
|
|
3. Generate formatted HTML using Gemini AI
|
|
4. Upload HTML and PDF to R2
|
|
5. Return URLs for download
|
|
"""
|
|
try:
|
|
# Validate file type
|
|
allowed_types = [
|
|
'application/pdf',
|
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
]
|
|
if file.content_type not in allowed_types:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="Invalid file type. Only PDF and DOCX files are allowed."
|
|
)
|
|
|
|
# Read file content
|
|
file_content = await file.read()
|
|
|
|
# Step 1: Extract text from resume
|
|
resume_text = await ai_service.extract_text_from_resume(
|
|
file_content,
|
|
file.content_type
|
|
)
|
|
if not resume_text:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Failed to extract text from resume"
|
|
)
|
|
|
|
# Step 2: Get template content
|
|
template_html = r2_service.get_template_content(template_name)
|
|
if not template_html:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Template '{template_name}' not found"
|
|
)
|
|
|
|
# Step 3: Generate formatted HTML
|
|
generated_html = await ai_service.generate_html_from_template(
|
|
resume_text,
|
|
template_html
|
|
)
|
|
if not generated_html:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Failed to generate formatted HTML"
|
|
)
|
|
|
|
# Step 4: Upload HTML to R2
|
|
base_filename = file.filename.rsplit('.', 1)[0]
|
|
html_filename = f"{base_filename}_{template_name}.html"
|
|
|
|
html_url = r2_service.upload_converted_file(
|
|
generated_html.encode('utf-8'),
|
|
html_filename,
|
|
'text/html',
|
|
metadata={
|
|
'original_filename': file.filename,
|
|
'template': template_name
|
|
}
|
|
)
|
|
|
|
if not html_url:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail="Failed to upload HTML to storage"
|
|
)
|
|
|
|
# Return response
|
|
return {
|
|
"success": True,
|
|
"html_url": html_url,
|
|
"html_content": generated_html,
|
|
"message": "Resume converted successfully"
|
|
}
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
print(f"Error converting resume: {e}")
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"An error occurred during conversion: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/history", response_model=List[Dict])
|
|
async def get_conversion_history(limit: int = 50):
|
|
"""
|
|
Get list of previously converted resumes from R2
|
|
"""
|
|
try:
|
|
files = r2_service.list_converted_resumes(limit=limit)
|
|
return files
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to fetch conversion history: {str(e)}"
|
|
)
|
|
|
|
|
|
@router.get("/download/{file_key:path}")
|
|
async def get_download_url(file_key: str):
|
|
"""
|
|
Get a presigned download URL for a file
|
|
"""
|
|
try:
|
|
url = r2_service.get_file_url(file_key)
|
|
if not url:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail="File not found"
|
|
)
|
|
return {"url": url}
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
raise HTTPException(
|
|
status_code=500,
|
|
detail=f"Failed to generate download URL: {str(e)}"
|
|
)
|