polats Claude Opus 4.8 (1M context) commited on
Commit
14e9729
·
1 Parent(s): 3bdc1e5

Built on Gradio: gr.Server custom frontend (Pixi + barracks via @app .api)

Browse files

Replace the FastAPI+iframe shell with gradio.Server: serve our own frontend at /,
expose the diary model fn as @app .api("/diary") called via @gradio/client. Custom
UI (Off-Brand) but unambiguously a Gradio app. Bump gradio 5.9.1 -> 6.15.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

__pycache__/app.cpython-313.pyc ADDED
Binary file (2.78 kB). View file
 
app.py CHANGED
@@ -1,61 +1,47 @@
1
- """Tiny Army — HF Space (Gradio app at the root, Pixi battlefield embedded).
2
 
3
- The deterministic combat engine + Pixi renderer are served as a static page under
4
- /battle and embedded in the Gradio app via an iframe so the Space root *is* a
5
- Gradio app (hackathon requirement) with the live battlefield shown inside it. The
6
- barracks war-diary generator is a stub for now, wired to a local llama.cpp small
 
 
 
7
  model during the hack.
8
  """
9
- import gradio as gr
10
- import uvicorn
11
- from fastapi import FastAPI
 
12
  from fastapi.staticfiles import StaticFiles
13
 
14
- app = FastAPI(title="Tiny Army")
15
 
 
 
16
 
17
- @app.get("/healthz")
18
- def healthz():
19
- return {"ok": True}
20
 
 
 
 
 
 
 
 
 
 
21
 
22
- # The Pixi battlefield (static: index.html + main.js + bundled engine.js) lives
23
- # under /battle so the Gradio app can iframe it.
24
- app.mount("/battle", StaticFiles(directory="web", html=True), name="battle")
25
 
 
 
 
 
 
 
26
 
27
- def write_diary(unit: str, traits: str) -> str:
28
- """Stub. Replaced during the hack by a local llama.cpp small model that writes
29
- a first-person war diary from the unit's memory + traits."""
30
- t = (traits or "").strip() or "untested"
31
- return (f"— Diary of {unit or 'a nameless soldier'} ({t}) —\n\n"
32
- f"(placeholder) Today I held the line. The small model will give me a "
33
- f"real voice soon, shaped by what I lived through on the field.")
34
-
35
-
36
- BATTLE_IFRAME = (
37
- '<iframe src="/battle/" title="Tiny Army battlefield" '
38
- 'style="width:100%;height:60vh;border:1px solid #20262e;border-radius:12px;'
39
- 'background:#0b0e12"></iframe>'
40
- )
41
-
42
- with gr.Blocks(title="Tiny Army", theme=gr.themes.Soft()) as demo:
43
- gr.Markdown("# ⚔️ Tiny Army\n"
44
- "*Every fighter writes its own legend — and the legend is true.* "
45
- "The deterministic auto-battler engine runs in your browser, below.")
46
- gr.HTML(BATTLE_IFRAME)
47
- gr.Markdown("### Barracks — war diaries\n"
48
- "_(skeleton — the diary is a stub; the hack wires it to a local "
49
- "llama.cpp small model that writes from each unit's lived experience.)_")
50
- with gr.Row():
51
- unit = gr.Textbox(label="Unit", value="Bram the Warrior")
52
- traits = gr.Textbox(label="Traits", value="Cautious, Veteran, Vengeful")
53
- out = gr.Textbox(label="War diary", lines=6)
54
- gr.Button("Write diary", variant="primary").click(write_diary, [unit, traits], out)
55
-
56
- # Gradio app at the root; /battle and /healthz were registered first so they win.
57
- app = gr.mount_gradio_app(app, demo, path="/")
58
 
59
 
60
  if __name__ == "__main__":
61
- uvicorn.run(app, host="0.0.0.0", port=7860)
 
1
+ """Tiny Army — HF Space, built on Gradio via `gr.Server`.
2
 
3
+ `gr.Server` is Gradio's server (a FastAPI subclass): we serve our own custom
4
+ frontend (the Pixi battlefield + barracks) and expose the small-model functions as
5
+ Gradio endpoints with `@app.api(...)`, called from the frontend via @gradio/client.
6
+ So the UI is custom (🎨 Off-Brand) but the app is genuinely a Gradio app.
7
+
8
+ The combat engine runs client-side (web/engine.js, a bundle of the auto-battler's
9
+ src/engine). The diary endpoint is a stub for now, wired to a local llama.cpp small
10
  model during the hack.
11
  """
12
+ import os
13
+
14
+ from gradio import Server
15
+ from fastapi.responses import HTMLResponse
16
  from fastapi.staticfiles import StaticFiles
17
 
18
+ app = Server()
19
 
20
+ HERE = os.path.dirname(os.path.abspath(__file__))
21
+ WEB = os.path.join(HERE, "web")
22
 
 
 
 
23
 
24
+ @app.api(name="diary")
25
+ def diary(unit: str = "", traits: str = "") -> str:
26
+ """Write a unit's war diary. Stub — replaced during the hack by a local
27
+ llama.cpp small model that writes from the unit's memory + traits."""
28
+ t = (traits or "").strip() or "untested"
29
+ u = (unit or "").strip() or "a nameless soldier"
30
+ return (f"— Diary of {u} ({t}) —\n\n"
31
+ f"(placeholder) Today I held the line. A local small model will give me "
32
+ f"a real voice soon, shaped by what I lived through on the field.")
33
 
 
 
 
34
 
35
+ # Custom frontend: index at "/", its assets under "/web" (kept off "/" so Gradio's
36
+ # own API routes stay intact).
37
+ @app.get("/", response_class=HTMLResponse)
38
+ def index():
39
+ with open(os.path.join(WEB, "index.html"), encoding="utf-8") as f:
40
+ return f.read()
41
 
42
+
43
+ app.mount("/web", StaticFiles(directory=WEB), name="web")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
 
46
  if __name__ == "__main__":
47
+ app.launch(server_name="0.0.0.0", server_port=7860, show_error=True)
requirements.txt CHANGED
@@ -1,4 +1 @@
1
- fastapi==0.115.6
2
- uvicorn==0.34.0
3
- gradio==5.9.1
4
- requests==2.32.3
 
1
+ gradio==6.15.2
 
 
 
web/barracks.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Barracks — calls the Gradio server's @app.api("/diary") endpoint from this
2
+ // custom frontend via the official @gradio/client. This is what makes the Space a
3
+ // real Gradio app: model calls flow through Gradio's backend.
4
+ import { Client } from 'https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js'
5
+
6
+ const out = document.getElementById('diary')
7
+ const btn = document.getElementById('write')
8
+ let client = null
9
+
10
+ btn.addEventListener('click', async () => {
11
+ const unit = document.getElementById('unit').value
12
+ const traits = document.getElementById('traits').value
13
+ out.textContent = '…writing…'
14
+ try {
15
+ client = client || await Client.connect(window.location.origin)
16
+ const r = await client.predict('/diary', { unit, traits })
17
+ out.textContent = Array.isArray(r.data) ? r.data[0] : String(r.data)
18
+ } catch (e) {
19
+ out.textContent = '(diary endpoint error: ' + (e && e.message ? e.message : e) + ')'
20
+ }
21
+ })
web/index.html CHANGED
@@ -5,17 +5,50 @@
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
  <title>Tiny Army</title>
7
  <style>
 
8
  html, body { margin: 0; height: 100%; background: #0b0e12; color: #f4ecd8;
9
  font-family: ui-monospace, Menlo, monospace; }
10
- #wrap { display: flex; flex-direction: column; height: 100vh; }
11
- #stage { flex: 1; min-height: 0; }
12
- canvas { display: block; width: 100%; height: 100%; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  </style>
14
  </head>
15
  <body>
16
  <div id="wrap">
 
 
 
 
 
 
17
  <div id="stage"></div>
 
 
 
 
 
 
 
 
 
 
18
  </div>
19
- <script type="module" src="./main.js"></script>
 
20
  </body>
21
  </html>
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
  <title>Tiny Army</title>
7
  <style>
8
+ :root { color-scheme: dark; }
9
  html, body { margin: 0; height: 100%; background: #0b0e12; color: #f4ecd8;
10
  font-family: ui-monospace, Menlo, monospace; }
11
+ #wrap { max-width: 980px; margin: 0 auto; padding: 14px; display: flex;
12
+ flex-direction: column; gap: 12px; min-height: 100vh; box-sizing: border-box; }
13
+ header h1 { margin: 0; font-size: 20px; }
14
+ header p { margin: 4px 0 0; color: #9aa4b2; font-size: 13px; line-height: 1.5; }
15
+ #stage { height: 56vh; border: 1px solid #20262e; border-radius: 12px;
16
+ overflow: hidden; background: #0b0e12; }
17
+ #stage canvas { display: block; width: 100%; height: 100%; }
18
+ .barracks { border: 1px solid #20262e; border-radius: 12px; padding: 12px; background: #10151b; }
19
+ .barracks h2 { margin: 0 0 8px; font-size: 14px; }
20
+ .row { display: flex; gap: 8px; flex-wrap: wrap; }
21
+ .row label { flex: 1; min-width: 180px; font-size: 12px; color: #9aa4b2;
22
+ display: flex; flex-direction: column; gap: 4px; }
23
+ input { background: #0b0e12; color: #f4ecd8; border: 1px solid #2a323c;
24
+ border-radius: 8px; padding: 8px; font: inherit; }
25
+ button { margin-top: 8px; background: #6b5bff; color: #fff; border: 0;
26
+ border-radius: 10px; padding: 10px 14px; font: inherit; cursor: pointer; }
27
+ #diary { margin-top: 8px; white-space: pre-wrap; min-height: 84px; color: #e7e0cf; }
28
+ footer { color: #5b6573; font-size: 11px; text-align: center; }
29
  </style>
30
  </head>
31
  <body>
32
  <div id="wrap">
33
+ <header>
34
+ <h1>⚔️ Tiny Army</h1>
35
+ <p>Every fighter writes its own legend — and the legend is true. The
36
+ deterministic engine runs in your browser; diaries come from a local small
37
+ model. <strong>Built on Gradio (gr.Server) — custom frontend.</strong></p>
38
+ </header>
39
  <div id="stage"></div>
40
+ <div class="barracks">
41
+ <h2>Barracks — war diary</h2>
42
+ <div class="row">
43
+ <label>Unit <input id="unit" value="Bram the Warrior" /></label>
44
+ <label>Traits <input id="traits" value="Cautious, Veteran, Vengeful" /></label>
45
+ </div>
46
+ <button id="write">Write diary</button>
47
+ <div id="diary"></div>
48
+ </div>
49
+ <footer>gr.Server custom frontend · model calls via @gradio/client → @app.api("/diary")</footer>
50
  </div>
51
+ <script type="module" src="/web/main.js"></script>
52
+ <script type="module" src="/web/barracks.js"></script>
53
  </body>
54
  </html>
web/main.js CHANGED
@@ -1,7 +1,7 @@
1
  // Tiny Army — Pixi stage driven by the real deterministic combat engine
2
  // (engine.js is an esbuild bundle of the auto-battler's src/engine/teamBattle.js).
3
  import * as PIXI from 'https://cdn.jsdelivr.net/npm/pixi.js@8/dist/pixi.min.mjs'
4
- import { makeTeamBattle, step, FIELD } from './engine.js'
5
 
6
  // A small demo line-up; later this comes from the player's saved roster.
7
  const PLAYERS = [
 
1
  // Tiny Army — Pixi stage driven by the real deterministic combat engine
2
  // (engine.js is an esbuild bundle of the auto-battler's src/engine/teamBattle.js).
3
  import * as PIXI from 'https://cdn.jsdelivr.net/npm/pixi.js@8/dist/pixi.min.mjs'
4
+ import { makeTeamBattle, step, FIELD } from '/web/engine.js'
5
 
6
  // A small demo line-up; later this comes from the player's saved roster.
7
  const PLAYERS = [