LazyHuman10 commited on
Commit
db7a1f1
·
1 Parent(s): 7cd5e9d

Add NPCverse Gradio server

Browse files
Files changed (1) hide show
  1. app.py +156 -5
app.py CHANGED
@@ -1,7 +1,158 @@
1
- import gradio as gr
2
 
3
- def greet(name):
4
- return "Hello " + name + "!!"
5
 
6
- demo = gr.Interface(fn=greet, inputs="text", outputs="text")
7
- demo.launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio Server backend for the NPCverse hackathon app."""
2
 
3
+ from __future__ import annotations
 
4
 
5
+ import base64
6
+ import io
7
+ import json
8
+ import os
9
+ from typing import Any
10
+
11
+ from dotenv import load_dotenv
12
+ from fastapi import File, UploadFile
13
+ from fastapi.responses import HTMLResponse, JSONResponse
14
+ from gradio import Server
15
+ from gradio.data_classes import FileData
16
+ from PIL import Image
17
+
18
+ load_dotenv()
19
+
20
+ from model_engine import ( # noqa: E402
21
+ DEFAULT_NPC,
22
+ analyze_image,
23
+ chat_respond,
24
+ check_new_secrets,
25
+ generate_npc,
26
+ get_friendship_label,
27
+ )
28
+ from share_card import generate_share_card # noqa: E402
29
+
30
+ app = Server()
31
+
32
+
33
+ @app.get("/", response_class=HTMLResponse)
34
+ async def homepage() -> HTMLResponse:
35
+ """Serve the NPCverse frontend HTML shell."""
36
+ index_path = os.path.join(os.path.dirname(__file__), "index.html")
37
+
38
+ try:
39
+ with open(index_path, "r", encoding="utf-8") as index_file:
40
+ return HTMLResponse(index_file.read())
41
+ except FileNotFoundError:
42
+ return HTMLResponse(
43
+ "<!doctype html><html><body><h1>UI loading...</h1></body></html>"
44
+ )
45
+
46
+
47
+ @app.get("/health")
48
+ async def health() -> JSONResponse:
49
+ """Return a lightweight health check for deployment probes."""
50
+ return JSONResponse({"status": "ok", "model": "MiniCPM-V-2_6"})
51
+
52
+
53
+ @app.api(name="summon_npc")
54
+ def summon_npc(image_path: FileData) -> dict:
55
+ """Analyze an uploaded image and summon a complete NPC profile."""
56
+ try:
57
+ description = analyze_image(image_path["path"])
58
+ npc = generate_npc(description)
59
+ return _json_safe_dict(npc)
60
+ except Exception:
61
+ return _json_safe_dict(DEFAULT_NPC)
62
+
63
+
64
+ @app.api(name="chat_with_npc")
65
+ def chat_with_npc(
66
+ npc_json: str,
67
+ history_json: str,
68
+ user_message: str,
69
+ msg_count: int,
70
+ unlocked_secrets_json: str,
71
+ ) -> dict:
72
+ """Continue an in-character NPC conversation and update progression state."""
73
+ npc = _loads_or_default(npc_json, DEFAULT_NPC)
74
+ history = _loads_or_default(history_json, [])
75
+ unlocked_secrets = _loads_or_default(unlocked_secrets_json, [])
76
+
77
+ if not isinstance(npc, dict):
78
+ npc = DEFAULT_NPC
79
+ if not isinstance(history, list):
80
+ history = []
81
+ if not isinstance(unlocked_secrets, list):
82
+ unlocked_secrets = []
83
+
84
+ current_msg_count = _safe_int(msg_count)
85
+ new_msg_count = current_msg_count + 1
86
+ newly_unlocked = check_new_secrets(new_msg_count, unlocked_secrets)
87
+ secret_texts = _get_secret_texts(npc, newly_unlocked)
88
+
89
+ response = chat_respond(
90
+ npc=npc,
91
+ history=history,
92
+ user_message=user_message,
93
+ msg_count=new_msg_count,
94
+ unlocked_secrets=[*unlocked_secrets, *newly_unlocked],
95
+ )
96
+
97
+ return {
98
+ "response": str(response),
99
+ "new_msg_count": new_msg_count,
100
+ "friendship_label": get_friendship_label(new_msg_count),
101
+ "friendship_pct": min(100.0, (current_msg_count / 55) * 100),
102
+ "newly_unlocked": newly_unlocked,
103
+ "secret_texts": secret_texts,
104
+ }
105
+
106
+
107
+ @app.api(name="generate_share_card")
108
+ def get_share_card(npc_json: str) -> dict:
109
+ """Generate a base64 PNG social share card for an NPC profile."""
110
+ npc = _loads_or_default(npc_json, DEFAULT_NPC)
111
+ if not isinstance(npc, dict):
112
+ npc = DEFAULT_NPC
113
+
114
+ image = generate_share_card(npc)
115
+ buffer = io.BytesIO()
116
+ image.save(buffer, format="PNG")
117
+ image_b64 = base64.b64encode(buffer.getvalue()).decode("utf-8")
118
+ return {"image_b64": f"data:image/png;base64,{image_b64}"}
119
+
120
+
121
+ def _loads_or_default(raw_json: str, default: Any) -> Any:
122
+ """Parse JSON input from the frontend, returning a fallback on invalid data."""
123
+ try:
124
+ return json.loads(raw_json)
125
+ except (TypeError, json.JSONDecodeError):
126
+ return default
127
+
128
+
129
+ def _safe_int(value: Any) -> int:
130
+ """Convert a value to a non-negative integer message count."""
131
+ try:
132
+ return max(0, int(value))
133
+ except (TypeError, ValueError):
134
+ return 0
135
+
136
+
137
+ def _get_secret_texts(npc: dict, secret_indices: list[int]) -> list[str]:
138
+ """Resolve unlocked secret indices to their corresponding secret text."""
139
+ secrets = npc.get("secrets", [])
140
+ if not isinstance(secrets, list):
141
+ return []
142
+
143
+ secret_texts: list[str] = []
144
+ for index in secret_indices:
145
+ try:
146
+ secret_texts.append(str(secrets[int(index)]))
147
+ except (TypeError, ValueError, IndexError):
148
+ continue
149
+ return secret_texts
150
+
151
+
152
+ def _json_safe_dict(payload: dict) -> dict:
153
+ """Round-trip a dictionary through JSON to ensure API-safe primitives."""
154
+ return json.loads(json.dumps(payload, ensure_ascii=False))
155
+
156
+
157
+ if __name__ == "__main__":
158
+ app.launch(server_name="0.0.0.0", server_port=7860, show_error=True)