storycode / app.py
Codex (via opencode)
Codex: upgrade Gradio 4.44 -> 5.50 for Python 3.13 compatibility on HF Space
62bb55b
Raw
History Blame Contribute Delete
6.89 kB
"""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)