Spaces:
Running
Running
Built on Gradio: gr.Server custom frontend (Pixi + barracks via @app .api)
Browse filesReplace 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 +0 -0
- app.py +33 -47
- requirements.txt +1 -4
- web/barracks.js +21 -0
- web/index.html +37 -4
- web/main.js +1 -1
__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
|
| 2 |
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
Gradio
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
model during the hack.
|
| 8 |
"""
|
| 9 |
-
import
|
| 10 |
-
|
| 11 |
-
from
|
|
|
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
|
| 14 |
-
app =
|
| 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 |
-
|
| 28 |
-
|
| 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 |
-
|
|
|
|
| 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 |
-
|
| 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 {
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
</style>
|
| 14 |
</head>
|
| 15 |
<body>
|
| 16 |
<div id="wrap">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
<div id="stage"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
</div>
|
| 19 |
-
<script type="module" src="
|
|
|
|
| 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 '
|
| 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 = [
|