Spaces:
Configuration error
Configuration error
Upload 3 files
Browse files- README.md +25 -12
- app.py +53 -30
- requirements.txt +4 -4
README.md
CHANGED
|
@@ -1,17 +1,19 @@
|
|
| 1 |
-
# StreamSmart Recommender - Hugging Face Space
|
| 2 |
|
| 3 |
Files included:
|
| 4 |
- `app.py` - main Gradio app for Hugging Face Space
|
| 5 |
- `requirements.txt` - Python dependencies
|
| 6 |
-
- `
|
|
|
|
| 7 |
|
| 8 |
## 1) Hugging Face setup
|
| 9 |
Create a new **Gradio Space** and replace the default files with:
|
| 10 |
- `app.py`
|
| 11 |
- `requirements.txt`
|
| 12 |
|
| 13 |
-
|
| 14 |
-
- `
|
|
|
|
| 15 |
|
| 16 |
## 2) CSV formats
|
| 17 |
### Reviews CSV required columns
|
|
@@ -36,15 +38,26 @@ Rates should be decimals such as `0.82`, not percentages like `82%`.
|
|
| 36 |
|
| 37 |
## 3) n8n setup
|
| 38 |
In n8n:
|
| 39 |
-
1. Import `
|
| 40 |
-
2.
|
| 41 |
-
3.
|
| 42 |
-
4.
|
| 43 |
-
5.
|
| 44 |
-
|
| 45 |
-
## 4) What
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
- scores review sentiment with VADER
|
| 47 |
- merges title-level sentiment with watch metrics
|
| 48 |
- computes a weighted recommendation score
|
| 49 |
- labels each title with a business action
|
| 50 |
-
- sends
|
|
|
|
| 1 |
+
# StreamSmart Recommender - Hugging Face Space with 2 n8n workflows
|
| 2 |
|
| 3 |
Files included:
|
| 4 |
- `app.py` - main Gradio app for Hugging Face Space
|
| 5 |
- `requirements.txt` - Python dependencies
|
| 6 |
+
- `n8n_processing_workflow.json` - import into n8n for full-result intake
|
| 7 |
+
- `n8n_report_workflow.json` - import into n8n for top-recommendation reporting
|
| 8 |
|
| 9 |
## 1) Hugging Face setup
|
| 10 |
Create a new **Gradio Space** and replace the default files with:
|
| 11 |
- `app.py`
|
| 12 |
- `requirements.txt`
|
| 13 |
|
| 14 |
+
Add these Space secrets:
|
| 15 |
+
- `N8N_PROCESS_WEBHOOK_URL` = your production URL from the processing workflow
|
| 16 |
+
- `N8N_REPORT_WEBHOOK_URL` = your production URL from the report workflow
|
| 17 |
|
| 18 |
## 2) CSV formats
|
| 19 |
### Reviews CSV required columns
|
|
|
|
| 38 |
|
| 39 |
## 3) n8n setup
|
| 40 |
In n8n:
|
| 41 |
+
1. Import `n8n_processing_workflow.json`
|
| 42 |
+
2. Import `n8n_report_workflow.json`
|
| 43 |
+
3. Open each Webhook node and copy the **Production URL**
|
| 44 |
+
4. Paste those URLs into the two Hugging Face Space secrets above
|
| 45 |
+
5. Rebuild the Space
|
| 46 |
+
|
| 47 |
+
## 4) What each workflow does
|
| 48 |
+
### Processing workflow
|
| 49 |
+
- receives the full scored dataset from the app
|
| 50 |
+
- counts how many titles fall into each action category
|
| 51 |
+
- returns the top title and a short status message
|
| 52 |
+
|
| 53 |
+
### Report workflow
|
| 54 |
+
- receives the top recommendations and all results
|
| 55 |
+
- computes average recommendation score by genre
|
| 56 |
+
- returns a text summary you can show in the app
|
| 57 |
+
|
| 58 |
+
## 5) What the app does
|
| 59 |
- scores review sentiment with VADER
|
| 60 |
- merges title-level sentiment with watch metrics
|
| 61 |
- computes a weighted recommendation score
|
| 62 |
- labels each title with a business action
|
| 63 |
+
- sends results to two separate n8n workflows
|
app.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import os
|
| 2 |
import io
|
| 3 |
import json
|
| 4 |
-
from typing import
|
| 5 |
|
| 6 |
import numpy as np
|
| 7 |
import pandas as pd
|
|
@@ -168,9 +168,6 @@ def run_analysis(reviews_file, watch_file, use_demo: bool):
|
|
| 168 |
reviews["sentiment_score"] = reviews["review_text"].apply(compute_sentiment)
|
| 169 |
reviews["sentiment_label"] = reviews["sentiment_score"].apply(sentiment_label)
|
| 170 |
|
| 171 |
-
agg_dict = {
|
| 172 |
-
"sentiment_score": ["mean", "count"],
|
| 173 |
-
}
|
| 174 |
if "genre" in reviews.columns:
|
| 175 |
review_agg = reviews.groupby("title", as_index=False).agg(
|
| 176 |
avg_sentiment=("sentiment_score", "mean"),
|
|
@@ -216,13 +213,12 @@ def run_analysis(reviews_file, watch_file, use_demo: bool):
|
|
| 216 |
f"Average completion rate: {merged['completion_rate'].mean():.2f}"
|
| 217 |
)
|
| 218 |
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
"recommendation_score", "action"
|
| 224 |
-
]
|
| 225 |
]
|
|
|
|
| 226 |
|
| 227 |
top_plot = px.bar(
|
| 228 |
merged.head(10),
|
|
@@ -283,26 +279,44 @@ def update_title_choices(payload: str):
|
|
| 283 |
return gr.Dropdown(choices=choices, value=value)
|
| 284 |
|
| 285 |
|
| 286 |
-
def
|
| 287 |
-
|
| 288 |
-
raise gr.Error("Run the analysis first.")
|
| 289 |
-
|
| 290 |
-
webhook_url = os.getenv("N8N_WEBHOOK_URL", "").strip()
|
| 291 |
if not webhook_url:
|
| 292 |
-
return
|
| 293 |
-
"N8N_WEBHOOK_URL is not set yet. Add it as a Hugging Face Space secret, then try again."
|
| 294 |
-
)
|
| 295 |
|
| 296 |
-
|
| 297 |
-
top5 = data[:5]
|
| 298 |
-
response = requests.post(webhook_url, json={"app": APP_TITLE, "top_recommendations": top5, "all_results": data}, timeout=60)
|
| 299 |
response.raise_for_status()
|
| 300 |
|
| 301 |
try:
|
| 302 |
result = response.json()
|
| 303 |
-
return f"
|
| 304 |
except Exception:
|
| 305 |
-
return f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 306 |
|
| 307 |
|
| 308 |
with gr.Blocks(title=APP_TITLE) as demo:
|
|
@@ -335,11 +349,14 @@ with gr.Blocks(title=APP_TITLE) as demo:
|
|
| 335 |
|
| 336 |
with gr.Tab("4. n8n Automation"):
|
| 337 |
gr.Markdown(
|
| 338 |
-
"
|
| 339 |
-
"
|
|
|
|
| 340 |
)
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
| 343 |
|
| 344 |
with gr.Tab("5. Download"):
|
| 345 |
download_file = gr.File(label="Download processed CSV")
|
|
@@ -370,10 +387,16 @@ with gr.Blocks(title=APP_TITLE) as demo:
|
|
| 370 |
outputs=[detail_box],
|
| 371 |
)
|
| 372 |
|
| 373 |
-
|
| 374 |
-
fn=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
inputs=[payload_state],
|
| 376 |
-
outputs=[
|
| 377 |
)
|
| 378 |
|
| 379 |
if __name__ == "__main__":
|
|
|
|
| 1 |
import os
|
| 2 |
import io
|
| 3 |
import json
|
| 4 |
+
from typing import Tuple
|
| 5 |
|
| 6 |
import numpy as np
|
| 7 |
import pandas as pd
|
|
|
|
| 168 |
reviews["sentiment_score"] = reviews["review_text"].apply(compute_sentiment)
|
| 169 |
reviews["sentiment_label"] = reviews["sentiment_score"].apply(sentiment_label)
|
| 170 |
|
|
|
|
|
|
|
|
|
|
| 171 |
if "genre" in reviews.columns:
|
| 172 |
review_agg = reviews.groupby("title", as_index=False).agg(
|
| 173 |
avg_sentiment=("sentiment_score", "mean"),
|
|
|
|
| 213 |
f"Average completion rate: {merged['completion_rate'].mean():.2f}"
|
| 214 |
)
|
| 215 |
|
| 216 |
+
table_cols = [
|
| 217 |
+
"title", "genre", "avg_sentiment", "avg_watch_time", "completion_rate",
|
| 218 |
+
"drop_off_rate", "rewatch_rate", "click_through_rate", "review_count",
|
| 219 |
+
"recommendation_score", "action"
|
|
|
|
|
|
|
| 220 |
]
|
| 221 |
+
top_table = merged[table_cols]
|
| 222 |
|
| 223 |
top_plot = px.bar(
|
| 224 |
merged.head(10),
|
|
|
|
| 279 |
return gr.Dropdown(choices=choices, value=value)
|
| 280 |
|
| 281 |
|
| 282 |
+
def _post_to_webhook(env_name: str, body: dict, success_label: str) -> str:
|
| 283 |
+
webhook_url = os.getenv(env_name, "").strip()
|
|
|
|
|
|
|
|
|
|
| 284 |
if not webhook_url:
|
| 285 |
+
return f"{env_name} is not set yet. Add it as a Hugging Face Space secret, then try again."
|
|
|
|
|
|
|
| 286 |
|
| 287 |
+
response = requests.post(webhook_url, json=body, timeout=60)
|
|
|
|
|
|
|
| 288 |
response.raise_for_status()
|
| 289 |
|
| 290 |
try:
|
| 291 |
result = response.json()
|
| 292 |
+
return f"{success_label} ran successfully. Response: {json.dumps(result, indent=2)}"
|
| 293 |
except Exception:
|
| 294 |
+
return f"{success_label} ran successfully. Raw response: {response.text}"
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def send_to_processing_workflow(payload: str):
|
| 298 |
+
if not payload:
|
| 299 |
+
raise gr.Error("Run the analysis first.")
|
| 300 |
+
|
| 301 |
+
data = json.loads(payload)
|
| 302 |
+
return _post_to_webhook(
|
| 303 |
+
"N8N_PROCESS_WEBHOOK_URL",
|
| 304 |
+
{"app": APP_TITLE, "records": data, "record_count": len(data)},
|
| 305 |
+
"n8n processing workflow",
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def send_to_report_workflow(payload: str):
|
| 310 |
+
if not payload:
|
| 311 |
+
raise gr.Error("Run the analysis first.")
|
| 312 |
+
|
| 313 |
+
data = json.loads(payload)
|
| 314 |
+
top5 = data[:5]
|
| 315 |
+
return _post_to_webhook(
|
| 316 |
+
"N8N_REPORT_WEBHOOK_URL",
|
| 317 |
+
{"app": APP_TITLE, "top_recommendations": top5, "all_results": data},
|
| 318 |
+
"n8n report workflow",
|
| 319 |
+
)
|
| 320 |
|
| 321 |
|
| 322 |
with gr.Blocks(title=APP_TITLE) as demo:
|
|
|
|
| 349 |
|
| 350 |
with gr.Tab("4. n8n Automation"):
|
| 351 |
gr.Markdown(
|
| 352 |
+
"Set these Hugging Face Space secrets before using the buttons below:\n\n"
|
| 353 |
+
"- `N8N_PROCESS_WEBHOOK_URL`\n"
|
| 354 |
+
"- `N8N_REPORT_WEBHOOK_URL`"
|
| 355 |
)
|
| 356 |
+
process_btn = gr.Button("Send Full Results to n8n Processing Workflow")
|
| 357 |
+
process_status = gr.Textbox(label="Processing Workflow Status", lines=5)
|
| 358 |
+
report_btn = gr.Button("Send Top Recommendations to n8n Report Workflow")
|
| 359 |
+
report_status = gr.Textbox(label="Report Workflow Status", lines=5)
|
| 360 |
|
| 361 |
with gr.Tab("5. Download"):
|
| 362 |
download_file = gr.File(label="Download processed CSV")
|
|
|
|
| 387 |
outputs=[detail_box],
|
| 388 |
)
|
| 389 |
|
| 390 |
+
process_btn.click(
|
| 391 |
+
fn=send_to_processing_workflow,
|
| 392 |
+
inputs=[payload_state],
|
| 393 |
+
outputs=[process_status],
|
| 394 |
+
)
|
| 395 |
+
|
| 396 |
+
report_btn.click(
|
| 397 |
+
fn=send_to_report_workflow,
|
| 398 |
inputs=[payload_state],
|
| 399 |
+
outputs=[report_status],
|
| 400 |
)
|
| 401 |
|
| 402 |
if __name__ == "__main__":
|
requirements.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
gradio>=4.44.0
|
| 2 |
-
pandas>=2.2.
|
| 3 |
-
numpy>=1.26.
|
| 4 |
-
plotly>=5.
|
| 5 |
-
requests>=2.
|
| 6 |
vaderSentiment>=3.3.2
|
|
|
|
| 1 |
gradio>=4.44.0
|
| 2 |
+
pandas>=2.2.0
|
| 3 |
+
numpy>=1.26.0
|
| 4 |
+
plotly>=5.22.0
|
| 5 |
+
requests>=2.31.0
|
| 6 |
vaderSentiment>=3.3.2
|