CCR / app.py
lodzi's picture
Upload 7 files
502da57 verified
import os, json, re
from datetime import date
import pandas as pd
import streamlit as st
st.set_page_config(page_title="Culturally Creative & Relevant Rater", page_icon="🗂️", layout="wide")
# --------- Constants ---------
DATA_DIR = "data"
RATINGS_CSV = os.path.join(DATA_DIR, "CCR_ratings.csv")
RESULTS_CSV = os.path.join(DATA_DIR, "CCR_results.csv")
DEFAULT_WEIGHTS = {
"CR_cultural_resonance": 0.22,
"OR_originality": 0.18,
"TI_timeliness": 0.12,
"IE_inclusivity_ethics": 0.10,
"SH_shareability": 0.15,
"BF_brand_channel_fit": 0.13,
"CQ_craft_quality": 0.10
}
DIMENSIONS = list(DEFAULT_WEIGHTS.keys())
AUDIENCE_OPTIONS = [
"BE urban 16-24",
"BE Gen Z 18-24",
"BE mainstream 25-44",
"NL mainstream 25-44",
"EU mainstream 25-44",
"FR urban 18-34",
"DE mainstream 18-49",
"Other...",
]
CHANNEL_OPTIONS = ["TikTok","Instagram","YouTube","OOH","TV","Radio","Integrated","Other"]
RATER_OPTIONS = ["Lode","Maarten"]
os.makedirs(DATA_DIR, exist_ok=True)
# --------- Helpers ---------
def ensure_csv(path):
if not os.path.exists(path):
cols = ["campaign_id","rater_id","rater_notes","campaign_name","brand","channel","scene_audience","country","submit_date_iso","asset_youtube_url",*DIMENSIONS,"flag_stereotype","flag_misappropriation","flag_sensitive_timing","flag_other_risk","neg_sentiment_ratio_estimate"]
pd.DataFrame(columns=cols).to_csv(path, index=False)
def load_ratings():
ensure_csv(RATINGS_CSV)
try:
return pd.read_csv(RATINGS_CSV)
except Exception:
return pd.DataFrame(columns=["campaign_id","rater_id","rater_notes","campaign_name","brand","channel","scene_audience","country","submit_date_iso","asset_youtube_url",*DIMENSIONS,"flag_stereotype","flag_misappropriation","flag_sensitive_timing","flag_other_risk","neg_sentiment_ratio_estimate"])
def save_rating(row: dict):
df = load_ratings()
df = pd.concat([df, pd.DataFrame([row])], ignore_index=True)
df.to_csv(RATINGS_CSV, index=False)
def to_0_100(x):
import pandas as pd
return (pd.to_numeric(x, errors="coerce") - 1.0)/4.0*100.0
def compute_results(weights: dict) -> pd.DataFrame:
df = load_ratings()
if df.empty:
return pd.DataFrame()
for d in DIMENSIONS:
df[f"{d}_100"] = to_0_100(df[d])
df["CCR_rater"] = 0.0
for d, w in weights.items():
df["CCR_rater"] += w * df[f"{d}_100"]
agg = {**{d: "mean" for d in DIMENSIONS},
**{f"{d}_100": "mean" for d in DIMENSIONS},
**{c: "mean" for c in ["flag_stereotype","flag_misappropriation","flag_sensitive_timing","flag_other_risk","neg_sentiment_ratio_estimate"] if c in df.columns},
"CCR_rater": "mean",
"campaign_name":"first",
"brand":"first",
"channel":"first",
"scene_audience":"first",
"country":"first",
"asset_youtube_url":"first"}
grouped = df.groupby("campaign_id", dropna=False).agg(agg).reset_index()
grouped["CCR_mean"] = grouped["CCR_rater"]
grouped.to_csv(RESULTS_CSV, index=False)
return grouped
def live_ccr_preview(scores: dict, weights: dict) -> float:
total = 0.0
for d,w in weights.items():
s100 = (scores.get(d, 3.0) - 1.0)/4.0*100.0
total += w * s100
return max(0.0, min(100.0, total))
def next_campaign_id(df: pd.DataFrame) -> str:
if df.empty or "campaign_id" not in df.columns or df["campaign_id"].dropna().empty:
return "CMP001"
last = str(df["campaign_id"].dropna().iloc[-1])
m = re.match(r"([A-Za-z]*)(\d+)$", last)
if m:
prefix, num = m.group(1) or "CMP", m.group(2)
nxt = int(num) + 1
return f"{prefix}{nxt:03d}"
return f"CMP{len(df)+1:03d}"
# --------- Session State Defaults ---------
if "step" not in st.session_state:
st.session_state.step = "Campagne-info"
if "info" not in st.session_state:
df0 = load_ratings()
st.session_state.info = {
"campaign_id": next_campaign_id(df0),
"campaign_name": "",
"brand": "",
"channel": CHANNEL_OPTIONS[0],
"scene_audience_choice": AUDIENCE_OPTIONS[0],
"scene_audience_custom": "",
"country": "BE",
"submit_date_iso": str(date.today()),
"asset_youtube_url": "",
"rater_id": "Lode",
"rater_notes": "",
}
if "scores" not in st.session_state:
st.session_state.scores = {d: 3.0 for d in DIMENSIONS}
if "risks" not in st.session_state:
st.session_state.risks = {"flag_stereotype":0,"flag_misappropriation":0,"flag_sensitive_timing":0,"flag_other_risk":0,"neg_sentiment_ratio_estimate":0.0}
# --------- Header ---------
st.title("Culturally Creative & Relevant Rater")
# --------- Stepper (segmented control) ---------
try:
st.session_state.step = st.segmented_control(
"Stap",
options=["Campagne-info","Score","Output"],
default=st.session_state.step,
)
except Exception:
# fallback to radio if segmented_control not available
st.session_state.step = st.radio("Stap", ["Campagne-info","Score","Output"], index=["Campagne-info","Score","Output"].index(st.session_state.step), horizontal=True)
# --------- RENDER: Campagne-info ---------
if st.session_state.step == "Campagne-info":
st.subheader("Campagne-info")
c_left, c_right = st.columns(2)
with c_left:
st.session_state.info["campaign_id"] = st.text_input("Campaign ID *", value=st.session_state.info["campaign_id"])
bn1, bn2 = st.columns(2)
with bn1:
st.session_state.info["brand"] = st.text_input("Brand", value=st.session_state.info["brand"])
with bn2:
st.session_state.info["campaign_name"] = st.text_input("Naam", value=st.session_state.info["campaign_name"])
st.session_state.info["channel"] = st.selectbox("Channel", CHANNEL_OPTIONS, index=CHANNEL_OPTIONS.index(st.session_state.info["channel"]) if st.session_state.info["channel"] in CHANNEL_OPTIONS else 0)
with c_right:
st.session_state.info["scene_audience_choice"] = st.selectbox("Scene / Audience", AUDIENCE_OPTIONS, index=AUDIENCE_OPTIONS.index(st.session_state.info["scene_audience_choice"]) if st.session_state.info["scene_audience_choice"] in AUDIENCE_OPTIONS else 0)
if st.session_state.info["scene_audience_choice"] == "Other...":
st.session_state.info["scene_audience_custom"] = st.text_input("Custom audience", value=st.session_state.info["scene_audience_custom"])
st.session_state.info["country"] = st.text_input("Country", value=st.session_state.info["country"])
st.session_state.info["submit_date_iso"] = st.text_input("Date (YYYY-MM-DD)", value=st.session_state.info["submit_date_iso"])
st.session_state.info["rater_id"] = st.selectbox("Rater", ["Lode","Maarten"], index=["Lode","Maarten"].index(st.session_state.info["rater_id"]) if st.session_state.info["rater_id"] in ["Lode","Maarten"] else 0)
st.session_state.info["asset_youtube_url"] = st.text_input("YouTube URL (optioneel)", value=st.session_state.info["asset_youtube_url"], placeholder="https://www.youtube.com/watch?v=...")
st.session_state.info["rater_notes"] = st.text_area("Rater notes", value=st.session_state.info["rater_notes"], height=90, placeholder="Kernobservaties, insider cues, etc.")
if st.button("Volgende →", type="primary"):
st.session_state.step = "Score"
st.rerun()
# --------- RENDER: Score ---------
elif st.session_state.step == "Score":
st.subheader("Score")
DESCRIPTIONS = {
"CR_cultural_resonance": {"title":"Cultural Resonance", "desc":"Raakt de campagne de cultuur/scene van de doelgroep? Insider cues, taal, symbolen."},
"OR_originality": {"title":"Originality", "desc":"Is het concept verrassend en vernieuwend vs. wat al bestaat?"},
"TI_timeliness": {"title":"Timeliness", "desc":"Sluit dit aan bij het momentum: trends, events, seizoen?"},
"IE_inclusivity_ethics": {"title":"Inclusivity & Ethics", "desc":"Respectvol en inclusief, zonder stereotypes of toe-eigening?"},
"SH_shareability": {"title":"Shareability", "desc":"Hoe deelbaar is het? Hook, quotables, remixedbaarheid."},
"BF_brand_channel_fit": {"title":"Brand & Channel Fit", "desc":"Matcht met merkcodes en benut het kanaal optimaal?"},
"CQ_craft_quality": {"title":"Craft Quality", "desc":"Sterke uitvoering: beeld/copy/sound/edit/montage."},
}
left, right = st.columns([1.6, 1])
with left:
for d in DIMENSIONS:
st.markdown(f"**{DESCRIPTIONS[d]['title']}**")
st.caption(DESCRIPTIONS[d]['desc'])
st.session_state.scores[d] = st.slider("", 1.0, 5.0, st.session_state.scores[d], 0.5, key=f"score_{d}")
r1, r2, r3, r4 = st.columns(4)
with r1:
st.session_state.risks["flag_stereotype"] = int(st.checkbox("Stereotype", value=bool(st.session_state.risks["flag_stereotype"])))
with r2:
st.session_state.risks["flag_misappropriation"] = int(st.checkbox("Misappropriation", value=bool(st.session_state.risks["flag_misappropriation"])))
with r3:
st.session_state.risks["flag_sensitive_timing"] = int(st.checkbox("Sensitive timing", value=bool(st.session_state.risks["flag_sensitive_timing"])))
with r4:
st.session_state.risks["flag_other_risk"] = int(st.checkbox("Other risk", value=bool(st.session_state.risks["flag_other_risk"])))
st.session_state.risks["neg_sentiment_ratio_estimate"] = st.slider("Neg sentiment ratio", 0.0, 1.0, float(st.session_state.risks["neg_sentiment_ratio_estimate"]), 0.01)
with right:
live_ccr = live_ccr_preview(st.session_state.scores, DEFAULT_WEIGHTS)
st.subheader("📊 Live Score")
st.markdown(f"<div style='font-size:72px;font-weight:800;line-height:1;'> {live_ccr:.0f}% </div>", unsafe_allow_html=True)
st.caption("CCR – gewogen (default weights)")
if st.session_state.info["asset_youtube_url"]:
st.markdown("---")
st.caption("YouTube preview")
st.video(st.session_state.info["asset_youtube_url"])
if st.button("Save rating now", type="primary"):
info = st.session_state.info
scene_audience = info["scene_audience_custom"] if info["scene_audience_choice"] == "Other..." else info["scene_audience_choice"]
if not info["campaign_id"].strip():
st.error("Campaign ID is verplicht.")
else:
row = {
"campaign_id": info["campaign_id"].strip(),
"campaign_name": info["campaign_name"].strip(),
"brand": info["brand"].strip(),
"channel": info["channel"],
"scene_audience": scene_audience.strip(),
"country": info["country"].strip(),
"submit_date_iso": info["submit_date_iso"].strip(),
"asset_youtube_url": info["asset_youtube_url"].strip(),
"rater_id": info["rater_id"],
"rater_notes": info["rater_notes"],
**st.session_state.scores,
**st.session_state.risks,
}
save_rating(row)
# increment campaign id for next entry
df_after = load_ratings()
st.session_state.info["campaign_id"] = next_campaign_id(df_after)
st.success("Rating opgeslagen.")
# Navigate to Output
st.session_state.step = "Output"
st.rerun()
# --------- RENDER: Output ---------
else: # Output
st.subheader("Output")
st.markdown("**Dataset (alle ratings)**")
st.dataframe(load_ratings(), use_container_width=True)
st.markdown("---")
st.markdown("**Per-campagne resultaten**")
res = compute_results(DEFAULT_WEIGHTS)
if not res.empty:
st.dataframe(res, use_container_width=True)
st.download_button("Download CCR_results.csv", res.to_csv(index=False).encode("utf-8"),
file_name="CCR_results.csv", mime="text/csv")
else:
st.info("Nog geen resultaten – voeg eerst één of meer ratings toe.")