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
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:
@@ -1,6 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.endpoints import people
|
||||
from app.api.endpoints import people, resumes
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(people.router, prefix="/people", tags=["people"])
|
||||
api_router.include_router(resumes.router, prefix="/resumes", tags=["resumes"])
|
||||
|
||||
179
backend/app/api/endpoints/resumes.py
Normal file
179
backend/app/api/endpoints/resumes.py
Normal 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)}"
|
||||
)
|
||||
Reference in New Issue
Block a user