Spaces:
Sleeping
Sleeping
| from fastapi import FastAPI, File, UploadFile, Form, HTTPException | |
| from fastapi.responses import FileResponse, HTMLResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi import BackgroundTasks | |
| import os | |
| import tempfile | |
| import re | |
| import json | |
| from pathlib import Path | |
| # Import your conversion function from meta.py | |
| from meta import process_excel_to_word | |
| app = FastAPI(title="QCM Converter API - META") | |
| # Enable CORS for all origins (you can restrict this in production) | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def validate_hex_color(color: str) -> bool: | |
| """Validate hex color format""" | |
| pattern = r'^[0-9A-Fa-f]{6}$' | |
| return bool(re.match(pattern, color)) | |
| async def root(): | |
| """Serve the HTML interface""" | |
| html_path = Path(__file__).parent / "index.html" | |
| if html_path.exists(): | |
| return html_path.read_text() | |
| return """ | |
| <html> | |
| <body> | |
| <h1>QCM Converter API - META</h1> | |
| <p>META Version: Answer tables only at the end of each module</p> | |
| <p>Upload your Excel files at <a href="/docs">/docs</a></p> | |
| </body> | |
| </html> | |
| """ | |
| async def convert_file( | |
| background_tasks: BackgroundTasks, | |
| file: UploadFile = File(...), | |
| images: UploadFile = File(None), # Optional ZIP file with images | |
| use_two_columns: bool = Form(True), | |
| add_separator_line: bool = Form(True), | |
| theme_color: str = Form("5FFFDF"), | |
| highlight_words: str = Form(None) # JSON string of words to highlight | |
| ): | |
| """ | |
| Convert Excel QCM file to Word document (META version) | |
| META Version Features: | |
| - NO empty tables after each course | |
| - ONLY answer tables at the end of each module | |
| Parameters: | |
| - file: Excel file (.xlsx) | |
| - images: Optional ZIP file containing images | |
| - use_two_columns: Use two-column layout | |
| - add_separator_line: Add separator line between columns | |
| - theme_color: Hex color code (without #) e.g., "5FFFDF" | |
| - highlight_words: JSON array of words to highlight (e.g., '["word1", "word2"]') | |
| """ | |
| # Validate file extension | |
| if not file.filename.endswith('.xlsx'): | |
| raise HTTPException(status_code=400, detail="Only .xlsx files are supported") | |
| # Validate color | |
| if not validate_hex_color(theme_color): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="Invalid color format. Use 6-character hex code (e.g., '5FFFDF')" | |
| ) | |
| original_name = Path(file.filename).stem | |
| temp_dir = tempfile.mkdtemp() | |
| temp_input_path = os.path.join(temp_dir, f"{original_name}.xlsx") | |
| # Save the Excel file | |
| with open(temp_input_path, "wb") as f: | |
| f.write(await file.read()) | |
| # Handle optional image ZIP file | |
| temp_images_path = None | |
| if images and images.filename: | |
| if not images.filename.endswith('.zip'): | |
| cleanup_files(temp_input_path) | |
| raise HTTPException(status_code=400, detail="Images must be in a ZIP file") | |
| temp_images_path = os.path.join(temp_dir, "images.zip") | |
| with open(temp_images_path, "wb") as f: | |
| f.write(await images.read()) | |
| output_filename = file.filename.replace('.xlsx', '_converted.docx') | |
| temp_output_path = tempfile.mktemp(suffix='.docx') | |
| # Parse highlight words from JSON string | |
| highlight_words_list = [] | |
| if highlight_words: | |
| try: | |
| highlight_words_list = json.loads(highlight_words) | |
| if not isinstance(highlight_words_list, list): | |
| highlight_words_list = [] | |
| except json.JSONDecodeError: | |
| # If it's not valid JSON, treat it as empty list | |
| highlight_words_list = [] | |
| try: | |
| process_excel_to_word( | |
| excel_file_path=temp_input_path, | |
| output_word_path=temp_output_path, | |
| image_folder=temp_images_path, # Can be None | |
| display_name=None, | |
| use_two_columns=use_two_columns, | |
| add_separator_line=add_separator_line, | |
| balance_method="dynamic", | |
| theme_hex=theme_color, | |
| highlight_words=highlight_words_list | |
| ) | |
| # Schedule cleanup as a background task | |
| files_to_cleanup = [temp_input_path, temp_output_path] | |
| if temp_images_path: | |
| files_to_cleanup.append(temp_images_path) | |
| background_tasks.add_task(cleanup_files, *files_to_cleanup) | |
| return FileResponse( | |
| temp_output_path, | |
| media_type="application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| filename=output_filename, | |
| background=None | |
| ) | |
| except Exception as e: | |
| files_to_cleanup = [temp_input_path, temp_output_path] | |
| if temp_images_path: | |
| files_to_cleanup.append(temp_images_path) | |
| cleanup_files(*files_to_cleanup) | |
| raise HTTPException(status_code=500, detail=f"Conversion failed: {str(e)}") | |
| def cleanup_files(*file_paths): | |
| """Clean up temporary files""" | |
| for file_path in file_paths: | |
| try: | |
| if os.path.exists(file_path): | |
| os.unlink(file_path) | |
| except Exception as e: | |
| print(f"Error cleaning up {file_path}: {e}") | |
| async def health_check(): | |
| """Health check endpoint""" | |
| return {"status": "healthy", "message": "QCM Converter API - META is running"} | |
| if __name__ == "__main__": | |
| import uvicorn | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |