Spaces:
No application file
No application file
| from __future__ import annotations | |
| import os | |
| import json | |
| import subprocess | |
| import shutil | |
| import platform | |
| import sys | |
| from pathlib import Path | |
| from typing import Any, Dict | |
| import uuid | |
| from jinja2 import Environment, FileSystemLoader | |
| from pydantic import ValidationError | |
| from pydantic_model import Resume | |
| # Import conditionnel de gradio pour éviter les erreurs si non installé | |
| try: | |
| import gradio as gr | |
| except ImportError: | |
| gr = None | |
| PROJECT_ROOT = Path(__file__).parent | |
| TEMPLATES_DIR = PROJECT_ROOT / "templates" | |
| # Par défaut, toujours utiliser le template classic | |
| TEMPLATE_NAME = "classic.tex.j2" | |
| OUTPUT_DIR = PROJECT_ROOT / "build" | |
| # Activer explicitement le serveur MCP via variable d'environnement (équivalent à mcp_server=True) | |
| os.environ["GRADIO_MCP_SERVER"] = "True" | |
| def escape_latex_special_chars(text: str) -> str: | |
| """Escape special LaTeX characters in text.""" | |
| if not isinstance(text, str): | |
| return text | |
| # Il faut traiter \ en premier pour éviter d'échapper les échappements | |
| # Puis traiter les autres caractères spéciaux | |
| text = text.replace('\\', r'\textbackslash{}') | |
| latex_special_chars = { | |
| '&': r'\&', | |
| '%': r'\%', | |
| '$': r'\$', | |
| '#': r'\#', | |
| '^': r'\textasciicircum{}', | |
| '_': r'\_', | |
| '{': r'\{', | |
| '}': r'\}', | |
| '~': r'\textasciitilde{}', | |
| } | |
| for char, escape in latex_special_chars.items(): | |
| text = text.replace(char, escape) | |
| return text | |
| def escape_resume_data(data: Dict[str, Any]) -> Dict[str, Any]: | |
| """Recursively escape LaTeX special characters in resume data.""" | |
| if isinstance(data, dict): | |
| return {k: escape_resume_data(v) for k, v in data.items()} | |
| elif isinstance(data, list): | |
| return [escape_resume_data(item) for item in data] | |
| elif isinstance(data, str): | |
| return escape_latex_special_chars(data) | |
| else: | |
| return data | |
| def render_tex(resume_data: Dict[str, Any], output_tex_path: Path, template_name: str = TEMPLATE_NAME) -> None: | |
| try: | |
| resume = Resume.model_validate(resume_data) | |
| except ValidationError as e: | |
| # En mode serveur (Gradio/MCP), il ne faut pas quitter le process | |
| # Propager une erreur explicite pour que l'UI / MCP l'affiche correctement | |
| raise ValueError(f"Validation Pydantic échouée: {e}") from e | |
| env = Environment( | |
| loader=FileSystemLoader(str(TEMPLATES_DIR)), | |
| autoescape=False, | |
| trim_blocks=True, | |
| lstrip_blocks=True, | |
| ) | |
| # Configure custom delimiters to avoid clashes with LaTeX | |
| env.block_start_string = "<<%" | |
| env.block_end_string = "%>>" | |
| env.variable_start_string = "<<" | |
| env.variable_end_string = ">>" | |
| env.comment_start_string = "<#!" | |
| env.comment_end_string = "!#>" | |
| template = env.get_template(template_name) | |
| # Échapper les caractères spéciaux LaTeX dans les données | |
| escaped_data = escape_resume_data(resume.model_dump()) | |
| rendered = template.render(resume=escaped_data) | |
| output_tex_path.parent.mkdir(parents=True, exist_ok=True) | |
| output_tex_path.write_text(rendered, encoding="utf-8") | |
| def run_tectonic(tex_path: Path, outdir: Path) -> None: | |
| cmd = [ | |
| "tectonic", | |
| "--outdir", | |
| str(outdir), | |
| str(tex_path), | |
| ] | |
| subprocess.run(cmd, check=True) | |
| def run_latexmk(tex_path: Path, outdir: Path, *, xelatex: bool = False) -> None: | |
| # latexmk -pdf -synctex=1 -interaction=nonstopmode -output-directory=outdir tex | |
| cmd = ["latexmk"] | |
| if xelatex: | |
| cmd.append("-xelatex") | |
| else: | |
| cmd.append("-pdf") | |
| cmd += [ | |
| "-halt-on-error", | |
| "-file-line-error", | |
| "-synctex=1", | |
| "-interaction=nonstopmode", | |
| f"-output-directory={outdir}", | |
| str(tex_path), | |
| ] | |
| subprocess.run(cmd, check=True) | |
| def compile_pdf(output_tex_path: Path, output_pdf_path: Path, engine_preference: str = "tectonic") -> None: | |
| outdir = output_pdf_path.parent | |
| outdir.mkdir(parents=True, exist_ok=True) | |
| def tool_available(cmd: str) -> bool: | |
| return shutil.which(cmd) is not None | |
| def read_latex_log_tail() -> str: | |
| try: | |
| base = output_tex_path.stem | |
| log_path = outdir / f"{base}.log" | |
| if log_path.exists(): | |
| content = log_path.read_text(errors="ignore") | |
| tail = "\n".join(content.splitlines()[-120:]) | |
| return tail | |
| # tenter aussi le .xdv log de xelatex | |
| xdv_path = outdir / f"{base}.xdv" | |
| if xdv_path.exists(): | |
| return "(fichier .xdv présent, pas de .log généré)" | |
| except Exception: | |
| pass | |
| return "(pas de log disponible)" | |
| if engine_preference == "latexmk": | |
| try: | |
| # Sur Linux (HF Spaces), privilégier XeLaTeX pour éviter les soucis de polices (lmodern) | |
| use_xelatex = platform.system() == "Linux" | |
| run_latexmk(output_tex_path, outdir, xelatex=use_xelatex) | |
| except (FileNotFoundError, subprocess.CalledProcessError) as e: | |
| if tool_available("tectonic"): | |
| run_tectonic(output_tex_path, outdir) | |
| else: | |
| log_tail = read_latex_log_tail() | |
| raise RuntimeError( | |
| "Compilation LaTeX échouée avec latexmk et aucun fallback 'tectonic' disponible.\n" | |
| f"Commande: {e}\n\nDernières lignes du .log:\n{log_tail}" | |
| ) from e | |
| else: | |
| # default: try tectonic first, then fallback if available | |
| try: | |
| run_tectonic(output_tex_path, outdir) | |
| except (FileNotFoundError, subprocess.CalledProcessError) as e: | |
| if tool_available("latexmk"): | |
| try: | |
| run_latexmk(output_tex_path, outdir) | |
| except subprocess.CalledProcessError as e2: | |
| log_tail = read_latex_log_tail() | |
| raise RuntimeError( | |
| "Compilation LaTeX échouée avec tectonic puis latexmk.\n" | |
| f"Erreur latexmk: {e2}\n\nDernières lignes du .log:\n{log_tail}" | |
| ) from e2 | |
| else: | |
| raise RuntimeError( | |
| f"Compilation LaTeX échouée avec tectonic et aucun fallback 'latexmk' disponible. " | |
| f"Erreur tectonic: {e}" | |
| ) from e | |
| if not output_pdf_path.exists(): | |
| raise RuntimeError(f"PDF introuvable après compilation: {output_pdf_path}") | |
| def resume_generation_system_prompt() -> str: | |
| """ | |
| System prompt complet pour gérer tout le workflow de génération de CV professionnel. | |
| Ce prompt système fournit des instructions détaillées à l'assistant IA pour gérer | |
| efficacement tout le processus de création de CV, de la collecte d'informations | |
| à la génération du PDF final. | |
| Returns: | |
| str: Instructions système complètes pour l'assistant IA | |
| """ | |
| return """Tu es un assistant spécialisé dans l'onboarding pour la génération de CV. Ton rôle est de guider l'utilisateur pour remplir toutes les informations nécessaires à la création de son CV, de manière claire, structurée et conviviale. | |
| ## TES RÈGLES FONDAMENTALES | |
| 1. **Pose une seule question à la fois** - Ne jamais submerger l'utilisateur | |
| 2. **Fournis un exemple concret** pour chaque question afin de guider l'utilisateur | |
| 3. **Valide chaque réponse** et reformule la question si la réponse est vide ou invalide | |
| 4. **Ne fais jamais de supposition** sur l'utilisateur - Toujours demander et confirmer | |
| 5. **Utilise un ton convivial, simple et naturel** - Pas de jargon technique | |
| ## DÉMARRAGE OBLIGATOIRE | |
| Commence TOUJOURS par cette question : | |
| "Bonjour ! 👋 Je vais t'aider à créer ton CV professionnel. | |
| Pour commencer, dis-moi : est-ce que tu crées ton **premier CV** ou tu veux **mettre à jour un CV existant** ? | |
| Si c'est une mise à jour, peux-tu uploader ton ancien CV (fichier PDF, Word, etc.) pour que je puisse m'en inspirer ?" | |
| ## INFORMATIONS À COLLECTER (DANS CET ORDRE) | |
| ### 1. INFORMATIONS DE BASE | |
| - **pdf_title** | |
| - Question : "Quel titre veux-tu donner à ton CV ?" | |
| - Exemple : "CV - Marie Dupont" ou "Candidature Développeur - Pierre Martin" | |
| - **name** (sera automatiquement utilisé comme pdf_author) | |
| - Question : "Quel est ton nom complet ?" | |
| - Exemple : "Marie Dupont" ou "Pierre Martin" | |
| - **location** | |
| - Question : "Où habites-tu actuellement ?" | |
| - Exemple : "Paris, France" ou "Lyon, France" | |
| - **email** | |
| - Question : "Quelle est ton adresse email professionnelle ?" | |
| - Exemple : "marie.dupont@gmail.com" | |
| - **phone** | |
| - Question : "Quel est ton numéro de téléphone (avec l'indicatif pays) ?" | |
| - Exemple : "+33 6 12 34 56 78" ou "06 12 34 56 78" | |
| ### 2. LIENS PROFESSIONNELS (OPTIONNELS) | |
| - **website_url** et **website_label** | |
| - Question : "As-tu un site web personnel ou portfolio ? Si oui, donne-moi l'URL complète." | |
| - Exemple : "https://marie-dupont.fr" → label sera "marie-dupont.fr" | |
| - **linkedin_url** et **linkedin_handle** | |
| - Question : "As-tu un profil LinkedIn ? Si oui, donne-moi le lien complet." | |
| - Exemple : "https://linkedin.com/in/marie-dupont" → handle sera "marie-dupont" | |
| - **github_url** et **github_handle** | |
| - Question : "As-tu un profil GitHub ? Si oui, donne-moi le lien complet." | |
| - Exemple : "https://github.com/marie-dupont" → handle sera "marie-dupont" | |
| ### 3. CONTENU DU CV | |
| - **intro_paragraphs** | |
| - Question : "Écris une ou deux phrases qui te décrivent professionnellement." | |
| - Exemple : "Développeuse web avec 3 ans d'expérience, passionnée par les technologies modernes et l'innovation." | |
| - **quick_guide_items** | |
| - Question : "Liste 3 à 5 de tes principales compétences ou points forts." | |
| - Exemple : "Développement web, Gestion d'équipe, React/Node.js, Problem solving" | |
| - **education** | |
| - Question : "Parle-moi de ta formation. Pour chaque diplôme, indique : le nom du diplôme, l'école/université, les dates (format AAAA-AAAA), et éventuellement des mentions ou points importants." | |
| - Exemple : "Master en Informatique à l'École Polytechnique (2020-2022), spécialité IA, mention Bien" | |
| - ⚠️ **FORMAT DATES OBLIGATOIRE** : Toujours utiliser des tirets pour séparer les années (ex: "2020-2022", "2015-2018") | |
| - **experience** | |
| - Question : "Décris ton expérience professionnelle. Pour chaque poste : nom de l'entreprise, ton poste, lieu, dates (format Mois AAAA-Mois AAAA), et tes principales réalisations avec des chiffres si possible." | |
| - Exemple : "Développeur chez Google, Paris (Janvier 2022-présent) : Développé 3 nouvelles fonctionnalités, réduit les bugs de 40%" | |
| - ⚠️ **FORMAT DATES OBLIGATOIRE** : Utiliser des tirets et des espaces (ex: "Janvier 2022-Décembre 2024", "Avril-Juin 2022") | |
| - **publications** (si applicable) | |
| - Question : "As-tu publié des articles, recherches ou communications ? Si oui, donne-moi les détails." | |
| - Exemple : "Article sur l'IA publié en 2023 dans la revue TechReview" | |
| - **projects** | |
| - Question : "Quels sont tes projets personnels ou professionnels marquants ? Avec liens GitHub si possible." | |
| - Exemple : "Application mobile de gestion de tâches (React Native) - 1000+ téléchargements" | |
| - **languages** | |
| - Question : "Quels langages de programmation ou langues maîtrises-tu ?" | |
| - Exemple : "Python, JavaScript, TypeScript, Anglais, Espagnol" | |
| - **technologies** | |
| - Question : "Quelles technologies, frameworks ou outils utilises-tu ?" | |
| - Exemple : "React, Node.js, Docker, AWS, PostgreSQL" | |
| ## FINALISATION | |
| Une fois toutes les informations collectées : | |
| 1. **Présente un récapitulatif clair** de toutes les informations | |
| 2. **Demande confirmation** : "Ces informations sont-elles correctes ?" | |
| 3. **Si confirmé**, appelle la fonction `generate_resume_pdf` avec tous les paramètres | |
| 4. **Présente le résultat** : "Voici ton CV ! Tu peux le télécharger et me dire s'il faut ajuster quelque chose." | |
| ## GESTION DES RÉPONSES | |
| - **Si réponse vide** : "Cette information est importante pour ton CV. Peux-tu me donner cette information ?" | |
| - **Si réponse invalide** : "Je n'ai pas bien compris. Peux-tu reformuler ? Voici un exemple : [exemple]" | |
| - **Si l'utilisateur veut passer** : "D'accord, on peut laisser ça vide pour l'instant. On pourra l'ajouter plus tard si tu veux." | |
| ## FORMATS JSON POUR LES PARAMÈTRES | |
| Quand tu collectes des informations complexes, formate-les correctement : | |
| - **Listes simples** : ["item1", "item2", "item3"] | |
| - **Éducation** : [{"degree": "Master", "institution": "École", "date_range": "2020-2022", "field_of_study": "Informatique", "highlights": ["Mention Bien"]}] | |
| - **Expérience** : [{"company": "Google", "role": "Développeur", "location": "Paris", "date_range": "Janvier 2022-présent", "highlights": ["Réalisation 1", "Impact 2"]}] | |
| ⚠️ **RÈGLES CRITIQUES POUR LES DATES** : | |
| - **TOUJOURS** utiliser des tirets pour séparer les périodes : "2020-2022", "Avril-Juin 2022" | |
| - **JAMAIS** coller les dates : ❌ "20202022" ✅ "2020-2022" | |
| - Pour l'éducation : Format "AAAA-AAAA" (ex: "2015-2018") | |
| - Pour l'expérience : Format "Mois AAAA-Mois AAAA" (ex: "Janvier 2022-Décembre 2024") | |
| Commence maintenant par la question de démarrage !""" | |
| def validate_json_parameter(param_name: str, param_value: str) -> list: | |
| """ | |
| Validate and parse a JSON parameter. | |
| Args: | |
| param_name (str): Name of the parameter for error reporting | |
| param_value (str): JSON string to validate | |
| Returns: | |
| list: Parsed JSON data | |
| Raises: | |
| ValueError: If JSON is invalid | |
| """ | |
| try: | |
| parsed = json.loads(param_value) | |
| if not isinstance(parsed, list): | |
| raise ValueError(f"Parameter '{param_name}' must be a JSON array") | |
| return parsed | |
| except json.JSONDecodeError as e: | |
| raise ValueError(f"Invalid JSON in parameter '{param_name}': {str(e)}") | |
| def generate_resume_pdf( | |
| pdf_title: str, | |
| pdf_author: str, | |
| name: str, | |
| location: str, | |
| email: str, | |
| phone: str, | |
| last_updated_text: str = "", | |
| website_url: str = "", | |
| website_label: str = "", | |
| linkedin_url: str = "", | |
| linkedin_handle: str = "", | |
| github_url: str = "", | |
| github_handle: str = "", | |
| intro_paragraphs: str = "[]", | |
| quick_guide_items: str = "[]", | |
| education: str = "[]", | |
| experience: str = "[]", | |
| publications: str = "[]", | |
| projects: str = "[]", | |
| languages: str = "[]", | |
| technologies: str = "[]" | |
| ) -> str: | |
| """ | |
| Generate a professional PDF resume from structured data. | |
| Creates a LaTeX-based PDF resume. All JSON parameters must be valid JSON strings. | |
| Args: | |
| pdf_title (str): PDF document title (e.g., "John Doe Resume") | |
| pdf_author (str): Author name for PDF metadata (e.g., "John Doe") | |
| name (str): Full name (e.g., "John Doe") | |
| location (str): Current location (e.g., "Paris, France") | |
| email (str): Email address (e.g., "john.doe@example.com") | |
| phone (str): Phone with country code (e.g., "+33 1 23 45 67 89") | |
| last_updated_text (str): Optional update text (e.g., "Updated September 2024") or "" | |
| website_url (str): Website URL (e.g., "https://johndoe.dev") or "" | |
| website_label (str): Website display text (e.g., "johndoe.dev") or "" | |
| linkedin_url (str): LinkedIn URL (e.g., "https://linkedin.com/in/johndoe") or "" | |
| linkedin_handle (str): LinkedIn username (e.g., "johndoe") or "" | |
| github_url (str): GitHub URL (e.g., "https://github.com/johndoe") or "" | |
| github_handle (str): GitHub username (e.g., "johndoe") or "" | |
| intro_paragraphs (str): JSON array of strings. Example: ["I am a software engineer with 5+ years experience.", "Passionate about AI and machine learning."] | |
| quick_guide_items (str): JSON array of key skills. Example: ["Python Expert", "Team Leadership", "Agile Methodologies"] | |
| education (str): JSON array of objects. Example: [{"degree": "Master of Science", "institution": "MIT", "date_range": "2018-2020", "field_of_study": "Computer Science", "highlights": ["GPA: 3.9/4.0"]}]. IMPORTANT: date_range must use format "YYYY-YYYY" (e.g., "2015-2018", NOT "20152018") | |
| experience (str): JSON array of objects. Example: [{"company": "Google", "role": "Senior Engineer", "location": "Mountain View, CA", "date_range": "January 2020-Present", "highlights": ["Led team of 5 engineers", "Increased system performance by 40%"]}]. IMPORTANT: date_range must use format "Month YYYY-Month YYYY" with proper separators | |
| publications (str): JSON array of objects. Example: [{"title": "AI in Production", "authors": ["John Doe", "Jane Smith"], "date": "2023", "doi": "10.1000/xyz123"}] or "[]" | |
| projects (str): JSON array of objects. Example: [{"title": "E-commerce Platform", "repo_url": "https://github.com/johndoe/ecommerce", "repo_label": "github.com/johndoe/ecommerce", "highlights": ["Built with React and Node.js", "Handles 10k+ users"]}] | |
| languages (str): JSON array of programming languages. Example: ["Python", "JavaScript", "TypeScript", "Go"] | |
| technologies (str): JSON array of tools/frameworks. Example: ["React", "Node.js", "Docker", "AWS", "PostgreSQL"] | |
| Returns: | |
| str: File path to the generated PDF resume | |
| """ | |
| try: | |
| # Valider et parser tous les paramètres JSON | |
| intro_paragraphs_list = validate_json_parameter("intro_paragraphs", intro_paragraphs) | |
| quick_guide_items_list = validate_json_parameter("quick_guide_items", quick_guide_items) | |
| education_list = validate_json_parameter("education", education) | |
| experience_list = validate_json_parameter("experience", experience) | |
| publications_list = validate_json_parameter("publications", publications) | |
| projects_list = validate_json_parameter("projects", projects) | |
| languages_list = validate_json_parameter("languages", languages) | |
| technologies_list = validate_json_parameter("technologies", technologies) | |
| # Construire le dictionnaire Resume à partir des paramètres | |
| resume_data = { | |
| "meta": { | |
| "pdf_title": pdf_title, | |
| "pdf_author": pdf_author, | |
| "last_updated_text": last_updated_text if last_updated_text else None | |
| }, | |
| "header": { | |
| "name": name, | |
| "location": location, | |
| "email": email, | |
| "phone": phone, | |
| "website_url": website_url if website_url else None, | |
| "website_label": website_label if website_label else None, | |
| "linkedin_url": linkedin_url if linkedin_url else None, | |
| "linkedin_handle": linkedin_handle if linkedin_handle else None, | |
| "github_url": github_url if github_url else None, | |
| "github_handle": github_handle if github_handle else None | |
| }, | |
| "intro_paragraphs": intro_paragraphs_list, | |
| "quick_guide_items": quick_guide_items_list, | |
| "education": education_list, | |
| "experience": experience_list, | |
| "publications": publications_list, | |
| "projects": projects_list, | |
| "technologies_section": { | |
| "languages": languages_list, | |
| "technologies": technologies_list | |
| } | |
| } | |
| # Valider avec le modèle Pydantic | |
| resume = Resume.model_validate(resume_data) | |
| # Générer le PDF | |
| base_name = uuid.uuid4().hex | |
| out_dir = OUTPUT_DIR | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| out_tex = out_dir / f"{base_name}.tex" | |
| out_pdf = out_dir / f"{base_name}.pdf" | |
| render_tex(resume.model_dump(), out_tex, template_name=TEMPLATE_NAME) | |
| preferred_engine = "latexmk" if platform.system() == "Linux" else "tectonic" | |
| compile_pdf(out_tex, out_pdf, engine_preference=preferred_engine) | |
| # Retourner DIRECTEMENT le chemin pour que Gradio génère l'URL publique | |
| return str(out_pdf) | |
| except (ValueError, ValidationError) as e: | |
| # Pour les erreurs, lever une exception que Gradio peut gérer | |
| raise ValueError(str(e)) | |
| except Exception as err: # noqa: BLE001 | |
| # Pour toutes les autres erreurs | |
| raise ValueError(f"Unexpected error: {str(err)}") | |
| def launch_gradio(server_host: str = "127.0.0.1", server_port: int = 7860) -> None: | |
| """Lance une application Gradio (UI + MCP Server). | |
| L'UI propose: zone de texte JSON (schéma `Resume`) → bouton → PDF.\ | |
| Le serveur MCP est activé via `mcp_server=True` conformément à la doc | |
| officielle Gradio [Building an MCP Server with Gradio](https://www.gradio.app/guides/building-mcp-server-with-gradio). | |
| """ | |
| if gr is None: | |
| print("Gradio n'est pas installé. Ajoutez 'gradio' à pyproject.toml.") | |
| raise ImportError("Gradio n'est pas disponible") | |
| sample_path = PROJECT_ROOT / "example_inputs" / "sample.json" | |
| sample_value = "{}" | |
| if sample_path.exists(): | |
| try: | |
| sample_value = sample_path.read_text(encoding="utf-8") | |
| except Exception: # noqa: BLE001 | |
| pass | |
| with gr.Blocks(title="Générateur de CV PDF (LaTeX)") as demo: | |
| gr.Markdown("## Générateur de CV PDF avec champs structurés\nTemplate: `classic.tex.j2`.") | |
| with gr.Accordion("Mode d'emploi", open=True): | |
| gr.Markdown( | |
| "- Remplissez les champs ci-dessous pour créer votre CV.\n" | |
| "- Les champs marqués * sont obligatoires.\n" | |
| "- Les listes (éducation, expérience, etc.) doivent être au format JSON.\n" | |
| "- Cliquez sur 'Générer le PDF' pour créer votre CV.\n" | |
| ) | |
| with gr.Tab("Informations de base"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| ui_pdf_title = gr.Textbox(label="Titre du PDF *", value="Mon CV") | |
| ui_pdf_author = gr.Textbox(label="Auteur du PDF *", value="") | |
| ui_last_updated_text = gr.Textbox(label="Dernière mise à jour", value="", placeholder="ex: Septembre 2024") | |
| with gr.Column(): | |
| ui_name = gr.Textbox(label="Nom complet *", value="") | |
| ui_location = gr.Textbox(label="Localisation *", value="", placeholder="ex: Paris, France") | |
| ui_email = gr.Textbox(label="Email *", value="", placeholder="ex: nom@example.com") | |
| ui_phone = gr.Textbox(label="Téléphone *", value="", placeholder="ex: +33 1 23 45 67 89") | |
| with gr.Tab("Liens et réseaux sociaux"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| ui_website_url = gr.Textbox(label="URL du site web", value="", placeholder="https://monsite.com") | |
| ui_website_label = gr.Textbox(label="Label du site web", value="", placeholder="monsite.com") | |
| ui_linkedin_url = gr.Textbox(label="URL LinkedIn", value="", placeholder="https://linkedin.com/in/...") | |
| ui_linkedin_handle = gr.Textbox(label="Handle LinkedIn", value="", placeholder="monprofil") | |
| with gr.Column(): | |
| ui_github_url = gr.Textbox(label="URL GitHub", value="", placeholder="https://github.com/...") | |
| ui_github_handle = gr.Textbox(label="Handle GitHub", value="", placeholder="monprofil") | |
| with gr.Tab("Contenu du CV"): | |
| ui_intro_paragraphs = gr.Textbox( | |
| label="Paragraphes d'introduction (JSON)", | |
| value='["Ingénieur passionné avec 5 ans d\'expérience"]', | |
| lines=3, | |
| placeholder='["Paragraphe 1", "Paragraphe 2"]' | |
| ) | |
| ui_quick_guide_items = gr.Textbox( | |
| label="Points clés (JSON)", | |
| value='["Expert en Python", "Gestion d\'équipe"]', | |
| lines=3, | |
| placeholder='["Point 1", "Point 2"]' | |
| ) | |
| with gr.Row(): | |
| with gr.Column(): | |
| ui_education = gr.Textbox( | |
| label="Éducation (JSON)", | |
| value='[]', | |
| lines=5, | |
| placeholder='[{"degree": "Master", "institution": "Université", "date_range": "2020-2022", "field_of_study": "Informatique", "highlights": []}]' | |
| ) | |
| ui_experience = gr.Textbox( | |
| label="Expérience (JSON)", | |
| value='[]', | |
| lines=5, | |
| placeholder='[{"company": "Entreprise", "role": "Développeur", "location": "Paris", "date_range": "2022-2024", "highlights": ["Réalisation X", "Amélioration Y"]}]' | |
| ) | |
| with gr.Column(): | |
| ui_publications = gr.Textbox( | |
| label="Publications (JSON)", | |
| value='[]', | |
| lines=3, | |
| placeholder='[{"date": "2024", "title": "Article", "authors": ["Moi"], "doi_url": null, "doi_label": null}]' | |
| ) | |
| ui_projects = gr.Textbox( | |
| label="Projets (JSON)", | |
| value='[]', | |
| lines=3, | |
| placeholder='[{"title": "Projet", "repo_url": "https://github.com/...", "repo_label": "github.com/...", "highlights": []}]' | |
| ) | |
| with gr.Tab("Compétences"): | |
| with gr.Row(): | |
| ui_languages = gr.Textbox( | |
| label="Langages de programmation (JSON)", | |
| value='["Python", "JavaScript", "Java"]', | |
| lines=2, | |
| placeholder='["Python", "JavaScript", "Java"]' | |
| ) | |
| ui_technologies = gr.Textbox( | |
| label="Technologies (JSON)", | |
| value='["React", "Docker", "AWS"]', | |
| lines=2, | |
| placeholder='["React", "Docker", "AWS"]' | |
| ) | |
| with gr.Row(): | |
| generate_btn = gr.Button("Générer le PDF", variant="primary", size="lg") | |
| pdf_file = gr.File(label="PDF généré", file_count="single") | |
| def _on_click_structured( | |
| pdf_title, pdf_author, name, location, email, phone, | |
| last_updated_text, website_url, website_label, | |
| linkedin_url, linkedin_handle, github_url, github_handle, | |
| intro_paragraphs, quick_guide_items, education, experience, | |
| publications, projects, languages, technologies | |
| ) -> str: | |
| """Génère le PDF avec les champs structurés.""" | |
| try: | |
| return generate_resume_pdf( | |
| pdf_title, pdf_author, name, location, email, phone, | |
| last_updated_text, website_url, website_label, | |
| linkedin_url, linkedin_handle, github_url, github_handle, | |
| intro_paragraphs, quick_guide_items, education, experience, | |
| publications, projects, languages, technologies | |
| ) | |
| except Exception as err: # noqa: BLE001 | |
| raise gr.Error(str(err)) | |
| # Connecter l'interface utilisateur | |
| generate_btn.click( | |
| _on_click_structured, | |
| inputs=[ | |
| ui_pdf_title, ui_pdf_author, ui_name, ui_location, ui_email, ui_phone, | |
| ui_last_updated_text, ui_website_url, ui_website_label, | |
| ui_linkedin_url, ui_linkedin_handle, ui_github_url, ui_github_handle, | |
| ui_intro_paragraphs, ui_quick_guide_items, ui_education, ui_experience, | |
| ui_publications, ui_projects, ui_languages, ui_technologies | |
| ], | |
| outputs=[pdf_file], | |
| api_name=False | |
| ) | |
| # Section for API endpoints (hidden from UI but accessible for MCP) | |
| with gr.Tab("API Endpoints", visible=False): | |
| # API: generate_resume_pdf - Main resume generation tool | |
| with gr.Row(): | |
| with gr.Column(): | |
| # Required fields | |
| api_pdf_title = gr.Textbox(label="pdf_title", value="Professional Resume") | |
| api_pdf_author = gr.Textbox(label="pdf_author", value="John Doe") | |
| api_name = gr.Textbox(label="name", value="John Doe") | |
| api_location = gr.Textbox(label="location", value="Paris, France") | |
| api_email = gr.Textbox(label="email", value="john.doe@example.com") | |
| api_phone = gr.Textbox(label="phone", value="+33 1 23 45 67 89") | |
| # Optional fields | |
| api_last_updated_text = gr.Textbox(label="last_updated_text", value="") | |
| api_website_url = gr.Textbox(label="website_url", value="") | |
| api_website_label = gr.Textbox(label="website_label", value="") | |
| api_linkedin_url = gr.Textbox(label="linkedin_url", value="") | |
| api_linkedin_handle = gr.Textbox(label="linkedin_handle", value="") | |
| api_github_url = gr.Textbox(label="github_url", value="") | |
| api_github_handle = gr.Textbox(label="github_handle", value="") | |
| with gr.Column(): | |
| # JSON array fields with comprehensive examples for LLMs | |
| api_intro_paragraphs = gr.Textbox( | |
| label="intro_paragraphs", | |
| value='["Experienced software engineer with 5+ years in full-stack development.", "Passionate about building scalable systems and leading technical teams."]', | |
| placeholder='["Your intro paragraph 1", "Your intro paragraph 2"]', | |
| lines=2 | |
| ) | |
| api_quick_guide_items = gr.Textbox( | |
| label="quick_guide_items", | |
| value='["Python Expert", "Team Leadership", "System Architecture", "Agile Methodologies"]', | |
| placeholder='["Skill 1", "Skill 2", "Skill 3"]', | |
| lines=2 | |
| ) | |
| api_education = gr.Textbox( | |
| label="education", | |
| value='[{"degree": "Master of Science", "institution": "MIT", "date_range": "2018-2020", "field_of_study": "Computer Science", "highlights": ["GPA: 3.9/4.0", "Thesis: Machine Learning Systems"]}]', | |
| placeholder='[{"degree": "Your degree", "institution": "Your school", "date_range": "2018-2020", "field_of_study": "Your field", "highlights": ["Achievement 1"]}]', | |
| lines=3 | |
| ) | |
| api_experience = gr.Textbox( | |
| label="experience", | |
| value='[{"company": "Google", "role": "Senior Software Engineer", "location": "Mountain View, CA", "date_range": "2020-Present", "highlights": ["Led team of 8 engineers", "Improved system performance by 45%", "Launched 3 major features"]}]', | |
| placeholder='[{"company": "Company Name", "role": "Your Role", "location": "City, Country", "date_range": "2020-Present", "highlights": ["Achievement 1", "Achievement 2"]}]', | |
| lines=3 | |
| ) | |
| api_publications = gr.Textbox( | |
| label="publications", | |
| value='[]', | |
| placeholder='[{"title": "Paper Title", "authors": ["Author 1", "Author 2"], "date": "2023", "doi": "10.1000/xyz123"}]', | |
| lines=1 | |
| ) | |
| api_projects = gr.Textbox( | |
| label="projects", | |
| value='[{"title": "E-commerce Platform", "repo_url": "https://github.com/johndoe/ecommerce", "repo_label": "github.com/johndoe/ecommerce", "highlights": ["Built with React and Node.js", "Handles 10k+ concurrent users", "99.9% uptime"]}]', | |
| placeholder='[{"title": "Project Name", "repo_url": "https://github.com/user/repo", "repo_label": "github.com/user/repo", "highlights": ["Feature 1", "Feature 2"]}]', | |
| lines=2 | |
| ) | |
| api_languages = gr.Textbox( | |
| label="languages", | |
| value='["Python", "JavaScript", "TypeScript", "Go", "Rust"]', | |
| placeholder='["Language1", "Language2", "Language3"]', | |
| lines=1 | |
| ) | |
| api_technologies = gr.Textbox( | |
| label="technologies", | |
| value='["React", "Node.js", "Docker", "Kubernetes", "AWS", "PostgreSQL", "Redis"]', | |
| placeholder='["Tech1", "Tech2", "Framework1", "Database1"]', | |
| lines=1 | |
| ) | |
| api_resume_output = gr.File(label="Generated PDF Resume") | |
| api_resume_trigger = gr.Button("Generate Resume") | |
| # API: resume_generation_system_prompt - System prompt principal | |
| with gr.Row(): | |
| api_system_output = gr.Textbox(label="System Prompt for Resume Generation", lines=15) | |
| api_system_trigger = gr.Button("Get System Prompt") | |
| # Hook API names to the triggers | |
| api_resume_trigger.click( | |
| fn=generate_resume_pdf, | |
| inputs=[ | |
| api_pdf_title, api_pdf_author, api_name, api_location, api_email, api_phone, | |
| api_last_updated_text, api_website_url, api_website_label, | |
| api_linkedin_url, api_linkedin_handle, api_github_url, api_github_handle, | |
| api_intro_paragraphs, api_quick_guide_items, api_education, api_experience, | |
| api_publications, api_projects, api_languages, api_technologies | |
| ], | |
| outputs=[api_resume_output], | |
| api_name="generate_resume_pdf" | |
| ) | |
| api_system_trigger.click( | |
| fn=resume_generation_system_prompt, | |
| inputs=[], | |
| outputs=[api_system_output], | |
| api_name="resume_generation_system_prompt" | |
| ) | |
| # Launch the interface with MCP server enabled | |
| # Following best practices from Gradio MCP documentation | |
| demo.launch( | |
| mcp_server=True, # Enable MCP server | |
| server_name=server_host, | |
| server_port=server_port, | |
| share=False, # No sharing for production/HF Spaces | |
| allowed_paths=[str(OUTPUT_DIR)], # Allow access to generated PDFs | |
| show_api=True, # Show API documentation | |
| ) | |
| print(f"🚀 Resume Generator MCP Server is running!") | |
| print(f"📊 Web Interface: http://{server_host}:{server_port}") | |
| print(f"🔗 MCP Endpoint: http://{server_host}:{server_port}/gradio_api/mcp/sse") | |
| print(f"📋 API Schema: http://{server_host}:{server_port}/gradio_api/mcp/schema") | |
| def main() -> None: | |
| import argparse | |
| parser = argparse.ArgumentParser(description="Générer le CV PDF depuis des données JSON ou lancer l'UI Gradio") | |
| parser.add_argument( | |
| "--serve", | |
| action="store_true", | |
| help="Lancer l'interface Gradio (au lieu du mode CLI)", | |
| ) | |
| parser.add_argument( | |
| "--input-json", | |
| type=Path, | |
| required=False, | |
| help="Chemin du fichier JSON d'entrée (conforme au modèle Resume)", | |
| ) | |
| parser.add_argument( | |
| "--template", | |
| type=str, | |
| default=TEMPLATE_NAME, | |
| help="Nom du fichier template (dans le dossier templates)", | |
| ) | |
| parser.add_argument( | |
| "--server-host", | |
| type=str, | |
| default="127.0.0.1", | |
| help="Hôte pour l'UI Gradio (par défaut 127.0.0.1)", | |
| ) | |
| parser.add_argument( | |
| "--server-port", | |
| type=int, | |
| default=7860, | |
| help="Port pour l'UI Gradio (par défaut 7860)", | |
| ) | |
| parser.add_argument( | |
| "--out-dir", | |
| type=Path, | |
| default=OUTPUT_DIR, | |
| help="Dossier de sortie pour les fichiers générés (.tex, .pdf, artefacts LaTeX)", | |
| ) | |
| # Suppression de l'argument --basename: le nom est toujours un UUID | |
| parser.add_argument( | |
| "--engine", | |
| type=str, | |
| choices=["tectonic", "latexmk"], | |
| default=None, | |
| help="Moteur de compilation LaTeX à privilégier (par défaut: latexmk sous Linux, sinon tectonic)", | |
| ) | |
| parser.add_argument( | |
| "--out-tex", | |
| type=Path, | |
| default=None, | |
| help="Chemin du fichier .tex rendu (défaut: build/output.tex)", | |
| ) | |
| parser.add_argument( | |
| "--out-pdf", | |
| type=Path, | |
| default=None, | |
| help="Chemin du PDF de sortie (défaut: build/output.pdf)", | |
| ) | |
| args = parser.parse_args() | |
| # Mode serveur (UI Gradio) | |
| if args.serve: | |
| launch_gradio(server_host=args.server_host, server_port=args.server_port) | |
| return | |
| if not args.input_json: | |
| parser.error("--input-json est requis en mode CLI (sans --serve)") | |
| data = json.loads(args.input_json.read_text(encoding="utf-8")) | |
| # Nom de base: toujours un UUID | |
| base_name = uuid.uuid4().hex | |
| # Résoudre les chemins de sortie par défaut si non fournis | |
| out_tex = args.out_tex or (args.out_dir / f"{base_name}.tex") | |
| out_pdf = args.out_pdf or (args.out_dir / f"{base_name}.pdf") | |
| # Forcer le template classic par défaut si non précisé | |
| render_tex(data, out_tex, template_name=args.template or TEMPLATE_NAME) | |
| # Choix du moteur par défaut selon l'OS | |
| preferred_engine = args.engine | |
| if preferred_engine is None: | |
| preferred_engine = "latexmk" if platform.system() == "Linux" else "tectonic" | |
| compile_pdf(out_tex, out_pdf, engine_preference=preferred_engine) | |
| print(f"PDF généré: {out_pdf}") | |
| if __name__ == "__main__": | |
| main() | |