Kenneth Chew commited on
Commit
a6a8785
·
verified ·
1 Parent(s): ac68345

add sentiment demo app

Browse files
Files changed (6) hide show
  1. README.md +15 -8
  2. app/__init__.py +1 -0
  3. app/preprocessing.py +40 -0
  4. main.py +114 -0
  5. requirements.txt +2 -0
  6. sentiment.joblib +3 -0
README.md CHANGED
@@ -1,13 +1,20 @@
1
  ---
2
- title: Rocketml Sentiment
3
- emoji: 🌖
4
  colorFrom: blue
5
- colorTo: gray
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.12'
9
- app_file: app.py
10
- pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: RocketML Sentiment Demo
 
3
  colorFrom: blue
4
+ colorTo: indigo
5
  sdk: gradio
6
+ sdk_version: 5.50.0
7
+ app_file: main.py
8
+ python_version: "3.12"
9
+ short_description: TF-IDF + LogReg sentiment classifier (RocketML demo)
10
  ---
11
 
12
+ # RocketML -- sentiment demo
13
+
14
+ A live demo of the model served by [RocketML](https://github.com/kenzychew/RocketML):
15
+ a TF-IDF + LogisticRegression sentiment classifier trained on IMDB reviews. Type a
16
+ movie review and get a positive/negative label with a confidence score.
17
+
18
+ The point of RocketML is the platform around the model -- containerised serving,
19
+ CI to GHCR, Prometheus/Grafana monitoring, and a Helm chart for Kubernetes. This
20
+ Space is just the model on its own so you can try it.
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """RocketML serving application package."""
app/preprocessing.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Text preprocessing shared by training and serving.
2
+
3
+ The same cleaning must run at train time and at inference time, so it lives in
4
+ one place and is imported by both ``model/train.py`` and the serving app.
5
+ Cleaning is regex-only (no NLTK) to keep the serving image light.
6
+ """
7
+
8
+ import re
9
+
10
+ _HTML = re.compile(r"<[^>]+>")
11
+ _URL = re.compile(r"http\S+|www\S+|https\S+", re.MULTILINE)
12
+ _EMAIL = re.compile(r"\S+@\S+")
13
+ _NON_TEXT = re.compile(r"[^a-zA-Z0-9\s']")
14
+ _DOUBLE_APOS = re.compile(r"''")
15
+ _LONE_APOS = re.compile(r"\s'\s")
16
+ _WHITESPACE = re.compile(r"\s+")
17
+
18
+
19
+ def clean_text(text: str) -> str:
20
+ """Normalise raw review text for the sentiment model.
21
+
22
+ Strips HTML, lowercases, drops URLs/emails, keeps alphanumerics and
23
+ apostrophes, and collapses whitespace. Applied identically at train and
24
+ inference time so the model sees the same text distribution.
25
+
26
+ Args:
27
+ text: Raw input text.
28
+
29
+ Returns:
30
+ The cleaned text.
31
+ """
32
+ text = _HTML.sub(" ", text)
33
+ text = text.lower()
34
+ text = _URL.sub("", text)
35
+ text = _EMAIL.sub("", text)
36
+ text = _NON_TEXT.sub(" ", text)
37
+ text = _DOUBLE_APOS.sub("", text)
38
+ text = _LONE_APOS.sub(" ", text)
39
+ text = _WHITESPACE.sub(" ", text)
40
+ return text.strip()
main.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio demo for the RocketML sentiment model (Hugging Face Space).
2
+
3
+ The model is the same scikit-learn pipeline RocketML serves. Its TF-IDF step
4
+ references app.preprocessing.clean_text, so the app/ package ships alongside
5
+ this file for joblib.load to resolve.
6
+
7
+ The endpoint is public, so two light guards keep it from being hammered: a
8
+ per-client cooldown and a global daily prediction cap ("demo credits"). Both
9
+ counters are in-memory and reset if the instance restarts, which is acceptable
10
+ for a single-instance demo.
11
+ """
12
+
13
+ import os
14
+ import threading
15
+ import time
16
+ from datetime import date, datetime, timezone
17
+
18
+ import gradio as gr
19
+ import joblib
20
+
21
+ MODEL = joblib.load("sentiment.joblib")
22
+
23
+ DAILY_CAP = int(os.environ.get("DEMO_DAILY_CAP", "200"))
24
+ COOLDOWN_SECONDS = 3.0
25
+
26
+ EXAMPLES = [
27
+ "An absolute masterpiece -- beautifully acted and deeply moving.",
28
+ "Boring, predictable, and a complete waste of two hours.",
29
+ "The plot dragged, but the soundtrack was wonderful.",
30
+ ]
31
+
32
+ _lock = threading.Lock()
33
+ _day: date | None = None
34
+ _count = 0
35
+ _last_call: dict[str, float] = {}
36
+
37
+
38
+ def _client_id(request: gr.Request | None) -> str:
39
+ """Best-effort client identifier for throttling.
40
+
41
+ Args:
42
+ request: The incoming Gradio request, if any.
43
+
44
+ Returns:
45
+ The originating client IP (first X-Forwarded-For hop behind the
46
+ Cloud Run proxy), or a placeholder when unavailable.
47
+ """
48
+ if request is None:
49
+ return "unknown"
50
+ forwarded = request.headers.get("x-forwarded-for")
51
+ if forwarded:
52
+ return forwarded.split(",")[0].strip()
53
+ return request.client.host if request.client else "unknown"
54
+
55
+
56
+ def _check_limits(client: str) -> None:
57
+ """Enforce the per-client cooldown and the global daily cap.
58
+
59
+ Args:
60
+ client: Client identifier from _client_id.
61
+
62
+ Raises:
63
+ gr.Error: If the client is calling too fast or today's demo
64
+ credits are spent.
65
+ """
66
+ global _day, _count
67
+ with _lock:
68
+ today = datetime.now(timezone.utc).date()
69
+ if _day != today:
70
+ _day, _count = today, 0
71
+ _last_call.clear()
72
+ last = _last_call.get(client)
73
+ now = time.monotonic()
74
+ if last is not None and now - last < COOLDOWN_SECONDS:
75
+ raise gr.Error("One prediction every few seconds, please.")
76
+ if _count >= DAILY_CAP:
77
+ raise gr.Error("Sorry, out of demo credits for now -- try again tomorrow.")
78
+ _last_call[client] = now
79
+ _count += 1
80
+
81
+
82
+ def classify(text: str, request: gr.Request) -> dict[str, float]:
83
+ """Return the model's class probabilities for the given text.
84
+
85
+ Args:
86
+ text: Raw review text from the UI.
87
+ request: Injected by Gradio; used for rate limiting.
88
+
89
+ Returns:
90
+ Mapping of class label to probability, empty for blank input.
91
+ """
92
+ if not text or not text.strip():
93
+ return {}
94
+ _check_limits(_client_id(request))
95
+ probs = MODEL.predict_proba([text])[0]
96
+ return {str(label): float(p) for label, p in zip(MODEL.classes_, probs)}
97
+
98
+
99
+ demo = gr.Interface(
100
+ fn=classify,
101
+ inputs=gr.Textbox(lines=4, label="Text", placeholder="Type a movie review..."),
102
+ outputs=gr.Label(num_top_classes=2, label="Sentiment"),
103
+ title="RocketML -- sentiment demo",
104
+ description=(
105
+ "A TF-IDF + LogisticRegression sentiment classifier (trained on IMDB). "
106
+ "This is the model served by the RocketML platform: "
107
+ "https://github.com/kenzychew/RocketML -- the demo is lightly "
108
+ "rate-limited and has a daily prediction budget."
109
+ ),
110
+ examples=EXAMPLES,
111
+ )
112
+
113
+ if __name__ == "__main__":
114
+ demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", "7860")))
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ scikit-learn==1.9.0
2
+ joblib
sentiment.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c7ce940765dda1b0d9ccab79889f277dd546b6997cb69b840c37dd9d6a62d43f
3
+ size 926260