feat: Complete Smart Resume Formatter with R2 and Gemini AI integration
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
This commit is contained in:
Laxmi Khilnani
2025-10-14 21:43:41 +05:30
parent ee030b70bc
commit cda50356b4
34 changed files with 2604 additions and 360 deletions

View File

@@ -0,0 +1,179 @@
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)}"
)