Spaces:
Sleeping
Sleeping
| import io | |
| import os | |
| import re | |
| import tempfile | |
| import textwrap | |
| import urllib.request | |
| import zipfile | |
| import gradio as gr | |
| from fpdf import FPDF | |
| from google import genai | |
| from google.genai import types | |
| # --------------------------------------------------------------------------- | |
| # Gemini setup | |
| # --------------------------------------------------------------------------- | |
| GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY", "") | |
| # --------------------------------------------------------------------------- | |
| # Font helper | |
| # --------------------------------------------------------------------------- | |
| FONT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "fonts") | |
| FONT_PATH = os.path.join(FONT_DIR, "DejaVuSans.ttf") | |
| FONT_BOLD_PATH = os.path.join(FONT_DIR, "DejaVuSans-Bold.ttf") | |
| DEJAVU_ZIP_URL = ( | |
| "https://github.com/dejavu-fonts/dejavu-fonts/releases/download/" | |
| "version_2_37/dejavu-fonts-ttf-2.37.zip" | |
| ) | |
| def _ensure_fonts(): | |
| if os.path.exists(FONT_PATH) and os.path.exists(FONT_BOLD_PATH): | |
| return | |
| os.makedirs(FONT_DIR, exist_ok=True) | |
| data = urllib.request.urlopen(DEJAVU_ZIP_URL).read() | |
| with zipfile.ZipFile(io.BytesIO(data)) as zf: | |
| for name in zf.namelist(): | |
| basename = os.path.basename(name) | |
| if basename == "DejaVuSans.ttf": | |
| with open(FONT_PATH, "wb") as f: | |
| f.write(zf.read(name)) | |
| elif basename == "DejaVuSans-Bold.ttf": | |
| with open(FONT_BOLD_PATH, "wb") as f: | |
| f.write(zf.read(name)) | |
| _ensure_fonts() | |
| # --------------------------------------------------------------------------- | |
| # Prompt template | |
| # --------------------------------------------------------------------------- | |
| SYSTEM_PROMPT = textwrap.dedent("""\ | |
| You are a world-class project architect, curriculum designer, and technical | |
| mentor with 15+ years of experience shipping production software and teaching | |
| developers at every level. Your job is to turn a **topic**, a **skill level**, | |
| and a **project difficulty** (1-10) into an extremely detailed, actionable | |
| project blueprint that leaves ZERO ambiguity. | |
| Use the EXACT ## headers below (including the ## markdown prefix). | |
| Do NOT add any extra top-level headers or preamble before the first section. | |
| ## Project Title | |
| A creative, memorable, and descriptive title. Include a short tagline in | |
| italics on the next line that summarises the project in one sentence. | |
| ## Project Description | |
| Write 5-7 sentences covering: | |
| - What the project IS and what problem it solves. | |
| - Who the target user/audience is. | |
| - Why this project matters (real-world relevance, career value). | |
| - What the finished product looks like (paint a vivid picture). | |
| - One sentence on the key technical challenge that makes it interesting. | |
| ## Goals & Learning Outcomes | |
| Provide **8-10** concrete, measurable outcomes using action verbs. | |
| Group them into: | |
| - **Core Outcomes** (must-achieve) β at least 5 | |
| - **Stretch Outcomes** (nice-to-have) β at least 3 | |
| ## Architecture Overview | |
| - **Architecture pattern** and WHY it fits this project. | |
| - **Component diagram** β list every major module/service. | |
| - **Data flow** β how data moves through the system. | |
| - **Folder / file structure** β tree view with comments. | |
| ## Step-by-Step Instructions | |
| Provide **at least 10** numbered steps. For EACH step include: | |
| 1. **Step title** in bold. | |
| 2. **Objective** β one sentence. | |
| 3. **Sub-tasks** β 3-6 specific actions. | |
| 4. **Deliverable** β what the learner should have at the end. | |
| 5. **Estimated time** β realistic range. | |
| 6. **Checkpoint** β how to verify the step is done correctly. | |
| ## Tools & Technologies Needed | |
| Bulleted list of every tool, library, framework, language, API, and service. | |
| For each: name/version, what it is used for, install command or link. | |
| ## Project Timeline | |
| | Phase | Steps Covered | Estimated Duration | Key Milestone | | |
| At least 4 phases: Setup, Core Build, Polish, Deploy. | |
| ## Tips & Common Mistakes | |
| At least 8 tips: | |
| - **Do's** (best practices) β at least 4 | |
| - **Don'ts** (pitfalls and how to avoid) β at least 4 | |
| ## Bonus Challenges | |
| 5-7 stretch goals ordered by difficulty. | |
| Each: one-line description + a hint on how to approach it. | |
| ## Resources & References | |
| 5-8 high-quality links with a one-line note on what the learner gains. | |
| ## Master Prompt | |
| Write a **minimum of 1500 words** β a ready-to-use AI prompt the reader | |
| can copy-paste into any AI coding assistant to build this project. | |
| Include: full context, tech stack, folder structure, sample data, | |
| architecture guidance, DB schema, API contracts, coding style rules, | |
| implementation order, edge cases, testing strategy, deployment checklist, | |
| and a 10+ item verification checklist. | |
| Wrap the ENTIRE master prompt inside a fenced code block. | |
| """) | |
| def build_user_prompt(topic: str, level: str, difficulty: int) -> str: | |
| level_context = { | |
| "Beginner": ( | |
| "The learner is new to this field. Use simple language, explain " | |
| "every concept on first use, provide copy-paste-ready code " | |
| "snippets, and include extra checkpoints so they never feel lost." | |
| ), | |
| "Intermediate": ( | |
| "The learner has foundational knowledge but needs guidance on " | |
| "architecture decisions, best practices, and connecting multiple " | |
| "components. Challenge them but don't overwhelm." | |
| ), | |
| "Advanced": ( | |
| "The learner is experienced. Focus on production-grade patterns, " | |
| "performance optimization, scalability, security, CI/CD, and " | |
| "professional-level engineering decisions. Skip basics." | |
| ), | |
| } | |
| difficulty_context = ( | |
| "very simple, minimal features, perfect for learning fundamentals" | |
| if difficulty <= 3 | |
| else "moderate complexity, multiple integrated features, real-world useful" | |
| if difficulty <= 6 | |
| else "high complexity, production-grade, ambitious scope with advanced patterns" | |
| ) | |
| return ( | |
| f"**Topic:** {topic}\n" | |
| f"**Skill Level:** {level}\n" | |
| f"**Level Context:** {level_context.get(level, level_context['Intermediate'])}\n" | |
| f"**Project Difficulty:** {difficulty}/10 ({difficulty_context})\n\n" | |
| "Generate the full, highly detailed project plan now." | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Gemini API call | |
| # --------------------------------------------------------------------------- | |
| def call_gemini(topic: str, level: str, difficulty: int) -> str: | |
| if not GEMINI_API_KEY: | |
| raise gr.Error( | |
| "GEMINI_API_KEY is not set. Add it as a Secret in your " | |
| "Hugging Face Space settings." | |
| ) | |
| client = genai.Client(api_key=GEMINI_API_KEY) | |
| try: | |
| response = client.models.generate_content( | |
| model="gemini-2.0-flash-lite", | |
| contents=build_user_prompt(topic, level, difficulty), | |
| config=types.GenerateContentConfig( | |
| system_instruction=SYSTEM_PROMPT, | |
| temperature=0.9, | |
| max_output_tokens=8192, | |
| ), | |
| ) | |
| except Exception as e: | |
| msg = str(e) | |
| if "429" in msg or "RESOURCE_EXHAUSTED" in msg: | |
| raise gr.Error( | |
| "API quota exceeded. Please wait a few minutes and try again, " | |
| "or upgrade your Gemini API plan at https://ai.dev/rate-limit" | |
| ) | |
| raise gr.Error(f"Gemini API error: {msg}") | |
| return response.text | |
| # --------------------------------------------------------------------------- | |
| # Markdown β section parser | |
| # --------------------------------------------------------------------------- | |
| SECTION_TITLES = [ | |
| "Project Title", | |
| "Project Description", | |
| "Goals & Learning Outcomes", | |
| "Step-by-Step Instructions", | |
| "Tools & Technologies Needed", | |
| "Tips & Common Mistakes", | |
| "Bonus Challenges", | |
| "Master Prompt", | |
| ] | |
| def parse_sections(md_text: str) -> dict: | |
| parts = re.split(r"(?m)^##\s+", md_text) | |
| sections = {} | |
| for part in parts: | |
| if not part.strip(): | |
| continue | |
| first_line, _, body = part.partition("\n") | |
| title = first_line.strip().rstrip("#").strip() | |
| sections[title] = body.strip() | |
| return sections | |
| # --------------------------------------------------------------------------- | |
| # PDF generator | |
| # --------------------------------------------------------------------------- | |
| class ProjectPDF(FPDF): | |
| DARK = (30, 30, 46) | |
| ACCENT = (114, 137, 218) | |
| LIGHT = (248, 248, 242) | |
| SUBTEXT = (166, 172, 205) | |
| def __init__(self): | |
| super().__init__() | |
| self.add_font("DejaVu", "", FONT_PATH, uni=True) | |
| self.add_font("DejaVu", "B", FONT_BOLD_PATH, uni=True) | |
| self.set_auto_page_break(auto=True, margin=20) | |
| def cover_page(self, title, topic, level, difficulty): | |
| self.add_page() | |
| self.set_fill_color(*self.DARK) | |
| self.rect(0, 0, 210, 297, "F") | |
| self.set_fill_color(*self.ACCENT) | |
| self.rect(0, 80, 210, 6, "F") | |
| self.set_y(100) | |
| self.set_font("DejaVu", "B", 26) | |
| self.set_text_color(*self.LIGHT) | |
| self.multi_cell(0, 14, title, align="C") | |
| self.ln(12) | |
| self.set_font("DejaVu", "", 13) | |
| self.set_text_color(*self.SUBTEXT) | |
| self.cell( | |
| 0, | |
| 10, | |
| f"Topic: {topic} | Level: {level} | Difficulty: {difficulty}/10", | |
| align="C", | |
| new_x="LMARGIN", | |
| new_y="NEXT", | |
| ) | |
| self.ln(6) | |
| self.set_font("DejaVu", "", 11) | |
| self.cell( | |
| 0, | |
| 10, | |
| "Generated by Prompt2Project β powered by Google Gemini", | |
| align="C", | |
| new_x="LMARGIN", | |
| new_y="NEXT", | |
| ) | |
| def section_header(self, title): | |
| self.ln(6) | |
| self.set_fill_color(*self.ACCENT) | |
| self.set_text_color(255, 255, 255) | |
| self.set_font("DejaVu", "B", 14) | |
| self.cell(0, 10, f" {title}", fill=True, new_x="LMARGIN", new_y="NEXT") | |
| self.ln(4) | |
| def body_text(self, text): | |
| self.set_text_color(50, 50, 50) | |
| self.set_font("DejaVu", "", 10) | |
| for line in text.split("\n"): | |
| stripped = line.strip() | |
| if not stripped: | |
| self.ln(3) | |
| continue | |
| if stripped.startswith(("- ", "* ", "β’ ")): | |
| bullet_text = stripped.lstrip("-*β’ ").strip() | |
| self.cell(6) | |
| self.set_font("DejaVu", "B", 10) | |
| self.cell(5, 6, "β’") | |
| self.set_font("DejaVu", "", 10) | |
| self.multi_cell(self.w - self.r_margin - self.get_x(), 6, bullet_text) | |
| elif re.match(r"^\d+[\.\)]\s", stripped): | |
| num, _, rest = stripped.partition(" ") | |
| self.set_font("DejaVu", "B", 10) | |
| self.cell(10, 6, num) | |
| self.set_font("DejaVu", "", 10) | |
| self.multi_cell(self.w - self.r_margin - self.get_x(), 6, rest) | |
| elif stripped.startswith("**") and "**" in stripped[2:]: | |
| self.set_font("DejaVu", "B", 11) | |
| self.multi_cell(0, 7, stripped.replace("**", "")) | |
| self.set_font("DejaVu", "", 10) | |
| else: | |
| self.multi_cell(0, 6, stripped.replace("**", "")) | |
| def generate_pdf(sections, topic, level, difficulty): | |
| pdf = ProjectPDF() | |
| title = sections.get("Project Title", "Untitled Project") | |
| pdf.cover_page(title, topic, level, difficulty) | |
| pdf.add_page() | |
| for sec_title in SECTION_TITLES: | |
| if sec_title == "Project Title": | |
| continue | |
| body = sections.get(sec_title, "") | |
| if not body: | |
| continue | |
| if sec_title == "Master Prompt": | |
| pdf.add_page() | |
| pdf.section_header(sec_title) | |
| pdf.body_text(body) | |
| tmp = tempfile.NamedTemporaryFile( | |
| delete=False, suffix=".pdf", prefix="prompt2project_" | |
| ) | |
| pdf.output(tmp.name) | |
| return tmp.name | |
| # --------------------------------------------------------------------------- | |
| # Main handler | |
| # --------------------------------------------------------------------------- | |
| def generate_project(topic: str, level: str, difficulty: int): | |
| if not topic or not topic.strip(): | |
| raise gr.Error("Please enter a topic.") | |
| md_text = call_gemini(topic.strip(), level, int(difficulty)) | |
| sections = parse_sections(md_text) | |
| pdf_path = generate_pdf(sections, topic.strip(), level, int(difficulty)) | |
| return md_text, pdf_path | |
| # --------------------------------------------------------------------------- | |
| # CSS | |
| # --------------------------------------------------------------------------- | |
| CSS = """ | |
| body, .gradio-container { | |
| background: #0f0f1a !important; | |
| } | |
| /* ββ constrain page width ββ */ | |
| .gradio-container > .main, | |
| .gradio-container .tabs, | |
| .gradio-container > div { | |
| max-width: 860px !important; | |
| margin-left: auto !important; | |
| margin-right: auto !important; | |
| } | |
| #header { | |
| text-align: center; | |
| padding: 2rem 0 1.5rem; | |
| max-width: 860px; | |
| margin: 0 auto; | |
| } | |
| #header h1 { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| font-size: 2.2rem; | |
| font-weight: 800; | |
| margin-bottom: 0.25rem; | |
| } | |
| #header p { color: #888; font-size: 1rem; margin: 0; } | |
| /* ββ cards ββ */ | |
| #config-card, #output-card, #pdf-row { | |
| background: #1a1a2e !important; | |
| border: 1px solid #2a2a4a !important; | |
| border-radius: 16px !important; | |
| padding: 1.75rem 2rem !important; | |
| margin-bottom: 1.25rem !important; | |
| } | |
| /* ββ inputs ββ */ | |
| #config-card input, #config-card textarea, #config-card select { | |
| background: #0f0f1a !important; | |
| border: 1px solid #2a2a4a !important; | |
| color: #e0e0f0 !important; | |
| border-radius: 8px !important; | |
| } | |
| #config-card label, #config-card .label-wrap span { color: #aaa !important; } | |
| /* ββ generate button ββ */ | |
| #btn-row { | |
| display: flex !important; | |
| justify-content: center !important; | |
| padding: 0.75rem 0 0.25rem !important; | |
| } | |
| #generate-btn { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important; | |
| color: white !important; | |
| font-weight: 700 !important; | |
| font-size: 1rem !important; | |
| border: none !important; | |
| border-radius: 10px !important; | |
| width: auto !important; | |
| min-width: 220px !important; | |
| max-width: 260px !important; | |
| padding: 10px 32px !important; | |
| margin: 0 auto 1.5rem auto !important; | |
| display: block !important; | |
| } | |
| #generate-btn:hover { opacity: 0.88 !important; transform: translateY(-1px) !important; } | |
| /* ββ output card text ββ */ | |
| #output-card p, #output-card li, | |
| #output-card h1, #output-card h2, #output-card h3, | |
| #output-card h4, #output-card code { color: #e0e0f0 !important; } | |
| #output-card code { | |
| background: #0f0f1a !important; | |
| border-radius: 4px !important; | |
| padding: 2px 5px !important; | |
| } | |
| #output-card pre { | |
| background: #0f0f1a !important; | |
| border: 1px solid #2a2a4a !important; | |
| border-radius: 8px !important; | |
| padding: 1rem !important; | |
| } | |
| footer { display: none !important; } | |
| """ | |
| # --------------------------------------------------------------------------- | |
| # Gradio UI | |
| # NOTE: gr.Blocks() accepts ONLY title= in this Gradio version. | |
| # css goes into demo.launch(css=CSS) β same pattern as Intervexa. | |
| # --------------------------------------------------------------------------- | |
| with gr.Blocks(title="Prompt2Project") as demo: | |
| gr.HTML(""" | |
| <div id="header"> | |
| <h1>π Prompt2Project</h1> | |
| <p>Generate a complete project plan & Master Prompt β powered by Gemini</p> | |
| </div> | |
| """) | |
| with gr.Group(elem_id="config-card"): | |
| gr.Markdown("### βοΈ Configuration") | |
| with gr.Row(): | |
| topic_input = gr.Textbox( | |
| label="π Topic", | |
| placeholder="e.g. Machine Learning, Web Development, Game Designβ¦", | |
| scale=3, | |
| lines=1, | |
| max_lines=1, | |
| ) | |
| level_input = gr.Dropdown( | |
| label="π Skill Level", | |
| choices=["Beginner", "Intermediate", "Advanced"], | |
| value="Intermediate", | |
| scale=1, | |
| ) | |
| difficulty_input = gr.Slider( | |
| label="π― Difficulty (1β10)", | |
| minimum=1, | |
| maximum=10, | |
| step=1, | |
| value=5, | |
| scale=2, | |
| ) | |
| generate_btn = gr.Button("β¨ Generate Project", elem_id="generate-btn") | |
| with gr.Group(elem_id="output-card"): | |
| gr.Markdown("### π Generated Project Plan") | |
| md_output = gr.Markdown( | |
| value="*Your project plan will appear here after clicking Generateβ¦*" | |
| ) | |
| with gr.Group(elem_id="pdf-row"): | |
| pdf_output = gr.File(label="π Download Project PDF", interactive=False) | |
| gr.HTML( | |
| "<center style='color:#444;font-size:0.8rem;margin-top:0.5rem;padding-bottom:1rem;'>" | |
| "Built with β€οΈ using Gradio & β€οΈ Google Gemini β€οΈ" | |
| "</center>" | |
| ) | |
| generate_btn.click( | |
| fn=generate_project, | |
| inputs=[topic_input, level_input, difficulty_input], | |
| outputs=[md_output, pdf_output], | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(css=CSS) | |