Codex (via opencode)
Codex: upgrade Gradio 4.44 -> 5.50 for Python 3.13 compatibility on HF Space
62bb55b | """StoryCode — understand the code you built, as a story. Gradio frontend. | |
| Flow: upload a .zip (or paste a file) -> static analysis builds the factual | |
| ProjectModel -> MiniCPM narrates it (map per file, reduce to a story) -> you read | |
| the Story, the Architecture Map, and what's Safe to Edit. Changing style/difficulty | |
| re-narrates only (fast); it never re-analyses the code. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import gradio as gr | |
| import config | |
| import db | |
| import diagram | |
| import narrate | |
| from analyzer import analyze_project | |
| from ingest import from_text, from_zip, from_github | |
| from schema import ProjectModel | |
| from ui import theme | |
| db.init_db() | |
| SAMPLE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), | |
| "scripts", "sample_project") | |
| STYLE_CHOICES = [(f"{s.emoji} {s.label}", s.key) for s in config.STYLES] | |
| DIFF_CHOICES = [(d.label, d.key) for d in config.DIFFICULTIES] | |
| # --- core analysis ---------------------------------------------------------- | |
| def _ingest_input(zip_path: str | None, pasted: str | None, github_url: str | None): | |
| if zip_path: | |
| return from_zip(zip_path) | |
| if github_url and github_url.strip(): | |
| return from_github(github_url.strip()) | |
| if pasted and pasted.strip(): | |
| return from_text(pasted) | |
| raise gr.Error("Upload a .zip, paste a file, or enter a GitHub URL.") | |
| def _render_all(model: ProjectModel, story): | |
| secret = "" | |
| if model.secrets_found: | |
| n = len(model.secrets_found) | |
| secret = theme.banner( | |
| f"🔒 We spotted and <strong>hid {n} likely secret(s)</strong> " | |
| "(API keys / passwords) before analysing. Remove them from your code too.", | |
| "warn") | |
| skipped = "" | |
| if model.note: | |
| skipped = theme.banner(model.note, "info") | |
| return ( | |
| theme.story_html(story), | |
| theme.plain_panel_html(story), | |
| diagram.render_html(model), | |
| theme.safe_to_edit_html(model), | |
| theme.deps_html(model), | |
| gr.update(value=secret + skipped, visible=bool(secret or skipped)), | |
| ) | |
| def analyze(zip_path, pasted, github_url, style, difficulty, progress=gr.Progress()): | |
| ingested = _ingest_input(zip_path, pasted, github_url) | |
| if not ingested.files: | |
| raise gr.Error("No code files found in there. Supported: Python, JS/TS, HTML, CSS, JSON, YAML.") | |
| progress(0.05, desc="Reading your project structure…") | |
| model = analyze_project(ingested) | |
| def _p(i, total, path): | |
| progress(0.1 + 0.7 * i / max(1, total), desc=f"Understanding file {i} of {total}…") | |
| summaries = narrate.summarise_files(model, progress=_p) | |
| progress(0.9, desc="Writing your story…") | |
| story = narrate.tell_story(model, summaries, style, difficulty) | |
| out = _render_all(model, story) | |
| return (*out, model, summaries, gr.update(selected="story")) | |
| def analyze_sample(style, difficulty, progress=gr.Progress()): | |
| from ingest import from_folder | |
| ingested = from_folder(SAMPLE_DIR, name="doc-qa (sample)") | |
| progress(0.05, desc="Reading the sample project…") | |
| model = analyze_project(ingested) | |
| def _p(i, total, path): | |
| progress(0.1 + 0.7 * i / max(1, total), desc=f"Understanding file {i} of {total}…") | |
| summaries = narrate.summarise_files(model, progress=_p) | |
| progress(0.9, desc="Writing your story…") | |
| story = narrate.tell_story(model, summaries, style, difficulty) | |
| out = _render_all(model, story) | |
| return (*out, model, summaries, gr.update(selected="story")) | |
| def restyle(model: ProjectModel, summaries, style, difficulty): | |
| """Re-narrate with a new style/difficulty — no re-analysis.""" | |
| if model is None: | |
| return gr.update(), gr.update() | |
| story = narrate.tell_story(model, summaries, style, difficulty) | |
| return theme.story_html(story), theme.plain_panel_html(story) | |
| # --- layout ----------------------------------------------------------------- | |
| def build() -> gr.Blocks: | |
| with gr.Blocks(theme=theme.THEME, css=theme.css(), title="StoryCode", | |
| head=theme.MERMAID_HEAD) as demo: | |
| model_state = gr.State(None) | |
| summ_state = gr.State(None) | |
| gr.HTML(theme.header_html()) | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| gr.HTML(theme.sources_html()) | |
| zip_in = gr.File(label="Project .zip", file_types=[".zip"], type="filepath") | |
| with gr.Accordion("…or paste a single file", open=False): | |
| paste_in = gr.Code(label="Paste code here", language="python", lines=10) | |
| with gr.Accordion("…or paste a GitHub URL", open=False): | |
| github_in = gr.Textbox( | |
| label="GitHub URL", | |
| placeholder="https://github.com/owner/repo", | |
| lines=1, | |
| ) | |
| with gr.Row(): | |
| go_btn = gr.Button("Tell me the story", variant="primary", | |
| elem_classes="primary", scale=2) | |
| sample_btn = gr.Button("Try the sample project", scale=1) | |
| with gr.Column(scale=1): | |
| style_in = gr.Radio(STYLE_CHOICES, value=config.DEFAULT_STYLE, | |
| label="Story style") | |
| diff_in = gr.Radio(DIFF_CHOICES, value=config.DEFAULT_DIFFICULTY, | |
| label="Explain it for…") | |
| notice = gr.HTML(visible=False) | |
| with gr.Tabs() as tabs: | |
| with gr.Tab("📖 The Story", id="story"): | |
| with gr.Row(): | |
| story_box = gr.HTML(elem_classes="sc-story-wrap") | |
| with gr.Column(scale=1, min_width=240): | |
| plain_box = gr.HTML() | |
| with gr.Tab("🗺️ Architecture Map", id="map"): | |
| gr.HTML('<div class="sc-legend">Boxes are your files, grouped by job. ' | |
| "An arrow means “this file uses that one”.</div>") | |
| map_box = gr.HTML() | |
| with gr.Tab("🚦 Safe to Edit", id="safe"): | |
| safe_box = gr.HTML() | |
| with gr.Tab("📦 Dependencies", id="deps"): | |
| deps_box = gr.HTML() | |
| outputs = [story_box, plain_box, map_box, safe_box, deps_box, notice, | |
| model_state, summ_state, tabs] | |
| go_btn.click(analyze, inputs=[zip_in, paste_in, github_in, style_in, diff_in], | |
| outputs=outputs) | |
| sample_btn.click(analyze_sample, inputs=[style_in, diff_in], | |
| outputs=outputs) | |
| # Restyle live without re-analysing. | |
| for ctrl in (style_in, diff_in): | |
| ctrl.change(restyle, inputs=[model_state, summ_state, style_in, diff_in], | |
| outputs=[story_box, plain_box]) | |
| return demo | |
| if __name__ == "__main__": | |
| build().launch(show_api=False) | |