lodzi commited on
Commit
502da57
·
verified ·
1 Parent(s): c545b11

Upload 7 files

Browse files
README_CCR.txt ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CCR Webapp (Stepper Navigation)
2
+
3
+ ## Install
4
+ pip install streamlit pandas numpy
5
+
6
+ ## Run
7
+ streamlit run app.py
8
+
9
+ ## Features
10
+ - Bovenaan **stepper** (segmented control) met 3 stappen i.p.v. tab-switching.
11
+ - Onderaan **Campagne-info**: knop **Volgende →** die naar **Score** gaat.
12
+ - In **Score**: knop **Save rating now** die opslaat en doorstroomt naar **Output**.
13
+ - Brand & Naam naast elkaar, Audience dropdown (met 'Other...'), Channel bevat 'Integrated', Rater dropdown (Lode/Maarten).
14
+ - Live CCR% + YouTube-preview in Score stap (rechts).
app.py ADDED
@@ -0,0 +1,249 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import os, json, re
3
+ from datetime import date
4
+ import pandas as pd
5
+ import streamlit as st
6
+
7
+ st.set_page_config(page_title="Culturally Creative & Relevant Rater", page_icon="🗂️", layout="wide")
8
+
9
+ # --------- Constants ---------
10
+ DATA_DIR = "data"
11
+ RATINGS_CSV = os.path.join(DATA_DIR, "CCR_ratings.csv")
12
+ RESULTS_CSV = os.path.join(DATA_DIR, "CCR_results.csv")
13
+
14
+ DEFAULT_WEIGHTS = {
15
+ "CR_cultural_resonance": 0.22,
16
+ "OR_originality": 0.18,
17
+ "TI_timeliness": 0.12,
18
+ "IE_inclusivity_ethics": 0.10,
19
+ "SH_shareability": 0.15,
20
+ "BF_brand_channel_fit": 0.13,
21
+ "CQ_craft_quality": 0.10
22
+ }
23
+ DIMENSIONS = list(DEFAULT_WEIGHTS.keys())
24
+
25
+ AUDIENCE_OPTIONS = [
26
+ "BE urban 16-24",
27
+ "BE Gen Z 18-24",
28
+ "BE mainstream 25-44",
29
+ "NL mainstream 25-44",
30
+ "EU mainstream 25-44",
31
+ "FR urban 18-34",
32
+ "DE mainstream 18-49",
33
+ "Other...",
34
+ ]
35
+ CHANNEL_OPTIONS = ["TikTok","Instagram","YouTube","OOH","TV","Radio","Integrated","Other"]
36
+ RATER_OPTIONS = ["Lode","Maarten"]
37
+
38
+ os.makedirs(DATA_DIR, exist_ok=True)
39
+
40
+ # --------- Helpers ---------
41
+ def ensure_csv(path):
42
+ if not os.path.exists(path):
43
+ 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"]
44
+ pd.DataFrame(columns=cols).to_csv(path, index=False)
45
+
46
+ def load_ratings():
47
+ ensure_csv(RATINGS_CSV)
48
+ try:
49
+ return pd.read_csv(RATINGS_CSV)
50
+ except Exception:
51
+ 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"])
52
+
53
+ def save_rating(row: dict):
54
+ df = load_ratings()
55
+ df = pd.concat([df, pd.DataFrame([row])], ignore_index=True)
56
+ df.to_csv(RATINGS_CSV, index=False)
57
+
58
+ def to_0_100(x):
59
+ import pandas as pd
60
+ return (pd.to_numeric(x, errors="coerce") - 1.0)/4.0*100.0
61
+
62
+ def compute_results(weights: dict) -> pd.DataFrame:
63
+ df = load_ratings()
64
+ if df.empty:
65
+ return pd.DataFrame()
66
+
67
+ for d in DIMENSIONS:
68
+ df[f"{d}_100"] = to_0_100(df[d])
69
+ df["CCR_rater"] = 0.0
70
+ for d, w in weights.items():
71
+ df["CCR_rater"] += w * df[f"{d}_100"]
72
+
73
+ agg = {**{d: "mean" for d in DIMENSIONS},
74
+ **{f"{d}_100": "mean" for d in DIMENSIONS},
75
+ **{c: "mean" for c in ["flag_stereotype","flag_misappropriation","flag_sensitive_timing","flag_other_risk","neg_sentiment_ratio_estimate"] if c in df.columns},
76
+ "CCR_rater": "mean",
77
+ "campaign_name":"first",
78
+ "brand":"first",
79
+ "channel":"first",
80
+ "scene_audience":"first",
81
+ "country":"first",
82
+ "asset_youtube_url":"first"}
83
+ grouped = df.groupby("campaign_id", dropna=False).agg(agg).reset_index()
84
+ grouped["CCR_mean"] = grouped["CCR_rater"]
85
+ grouped.to_csv(RESULTS_CSV, index=False)
86
+ return grouped
87
+
88
+ def live_ccr_preview(scores: dict, weights: dict) -> float:
89
+ total = 0.0
90
+ for d,w in weights.items():
91
+ s100 = (scores.get(d, 3.0) - 1.0)/4.0*100.0
92
+ total += w * s100
93
+ return max(0.0, min(100.0, total))
94
+
95
+ def next_campaign_id(df: pd.DataFrame) -> str:
96
+ if df.empty or "campaign_id" not in df.columns or df["campaign_id"].dropna().empty:
97
+ return "CMP001"
98
+ last = str(df["campaign_id"].dropna().iloc[-1])
99
+ m = re.match(r"([A-Za-z]*)(\d+)$", last)
100
+ if m:
101
+ prefix, num = m.group(1) or "CMP", m.group(2)
102
+ nxt = int(num) + 1
103
+ return f"{prefix}{nxt:03d}"
104
+ return f"CMP{len(df)+1:03d}"
105
+
106
+ # --------- Session State Defaults ---------
107
+ if "step" not in st.session_state:
108
+ st.session_state.step = "Campagne-info"
109
+ if "info" not in st.session_state:
110
+ df0 = load_ratings()
111
+ st.session_state.info = {
112
+ "campaign_id": next_campaign_id(df0),
113
+ "campaign_name": "",
114
+ "brand": "",
115
+ "channel": CHANNEL_OPTIONS[0],
116
+ "scene_audience_choice": AUDIENCE_OPTIONS[0],
117
+ "scene_audience_custom": "",
118
+ "country": "BE",
119
+ "submit_date_iso": str(date.today()),
120
+ "asset_youtube_url": "",
121
+ "rater_id": "Lode",
122
+ "rater_notes": "",
123
+ }
124
+ if "scores" not in st.session_state:
125
+ st.session_state.scores = {d: 3.0 for d in DIMENSIONS}
126
+ if "risks" not in st.session_state:
127
+ st.session_state.risks = {"flag_stereotype":0,"flag_misappropriation":0,"flag_sensitive_timing":0,"flag_other_risk":0,"neg_sentiment_ratio_estimate":0.0}
128
+
129
+ # --------- Header ---------
130
+ st.title("Culturally Creative & Relevant Rater")
131
+
132
+ # --------- Stepper (segmented control) ---------
133
+ try:
134
+ st.session_state.step = st.segmented_control(
135
+ "Stap",
136
+ options=["Campagne-info","Score","Output"],
137
+ default=st.session_state.step,
138
+ )
139
+ except Exception:
140
+ # fallback to radio if segmented_control not available
141
+ st.session_state.step = st.radio("Stap", ["Campagne-info","Score","Output"], index=["Campagne-info","Score","Output"].index(st.session_state.step), horizontal=True)
142
+
143
+ # --------- RENDER: Campagne-info ---------
144
+ if st.session_state.step == "Campagne-info":
145
+ st.subheader("Campagne-info")
146
+ c_left, c_right = st.columns(2)
147
+ with c_left:
148
+ st.session_state.info["campaign_id"] = st.text_input("Campaign ID *", value=st.session_state.info["campaign_id"])
149
+ bn1, bn2 = st.columns(2)
150
+ with bn1:
151
+ st.session_state.info["brand"] = st.text_input("Brand", value=st.session_state.info["brand"])
152
+ with bn2:
153
+ st.session_state.info["campaign_name"] = st.text_input("Naam", value=st.session_state.info["campaign_name"])
154
+ 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)
155
+ with c_right:
156
+ 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)
157
+ if st.session_state.info["scene_audience_choice"] == "Other...":
158
+ st.session_state.info["scene_audience_custom"] = st.text_input("Custom audience", value=st.session_state.info["scene_audience_custom"])
159
+ st.session_state.info["country"] = st.text_input("Country", value=st.session_state.info["country"])
160
+ st.session_state.info["submit_date_iso"] = st.text_input("Date (YYYY-MM-DD)", value=st.session_state.info["submit_date_iso"])
161
+ 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)
162
+ 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=...")
163
+ 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.")
164
+
165
+ if st.button("Volgende →", type="primary"):
166
+ st.session_state.step = "Score"
167
+ st.rerun()
168
+
169
+ # --------- RENDER: Score ---------
170
+ elif st.session_state.step == "Score":
171
+ st.subheader("Score")
172
+ DESCRIPTIONS = {
173
+ "CR_cultural_resonance": {"title":"Cultural Resonance", "desc":"Raakt de campagne de cultuur/scene van de doelgroep? Insider cues, taal, symbolen."},
174
+ "OR_originality": {"title":"Originality", "desc":"Is het concept verrassend en vernieuwend vs. wat al bestaat?"},
175
+ "TI_timeliness": {"title":"Timeliness", "desc":"Sluit dit aan bij het momentum: trends, events, seizoen?"},
176
+ "IE_inclusivity_ethics": {"title":"Inclusivity & Ethics", "desc":"Respectvol en inclusief, zonder stereotypes of toe-eigening?"},
177
+ "SH_shareability": {"title":"Shareability", "desc":"Hoe deelbaar is het? Hook, quotables, remixedbaarheid."},
178
+ "BF_brand_channel_fit": {"title":"Brand & Channel Fit", "desc":"Matcht met merkcodes en benut het kanaal optimaal?"},
179
+ "CQ_craft_quality": {"title":"Craft Quality", "desc":"Sterke uitvoering: beeld/copy/sound/edit/montage."},
180
+ }
181
+ left, right = st.columns([1.6, 1])
182
+ with left:
183
+ for d in DIMENSIONS:
184
+ st.markdown(f"**{DESCRIPTIONS[d]['title']}**")
185
+ st.caption(DESCRIPTIONS[d]['desc'])
186
+ st.session_state.scores[d] = st.slider("", 1.0, 5.0, st.session_state.scores[d], 0.5, key=f"score_{d}")
187
+ r1, r2, r3, r4 = st.columns(4)
188
+ with r1:
189
+ st.session_state.risks["flag_stereotype"] = int(st.checkbox("Stereotype", value=bool(st.session_state.risks["flag_stereotype"])))
190
+ with r2:
191
+ st.session_state.risks["flag_misappropriation"] = int(st.checkbox("Misappropriation", value=bool(st.session_state.risks["flag_misappropriation"])))
192
+ with r3:
193
+ st.session_state.risks["flag_sensitive_timing"] = int(st.checkbox("Sensitive timing", value=bool(st.session_state.risks["flag_sensitive_timing"])))
194
+ with r4:
195
+ st.session_state.risks["flag_other_risk"] = int(st.checkbox("Other risk", value=bool(st.session_state.risks["flag_other_risk"])))
196
+ 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)
197
+ with right:
198
+ live_ccr = live_ccr_preview(st.session_state.scores, DEFAULT_WEIGHTS)
199
+ st.subheader("📊 Live Score")
200
+ st.markdown(f"<div style='font-size:72px;font-weight:800;line-height:1;'> {live_ccr:.0f}% </div>", unsafe_allow_html=True)
201
+ st.caption("CCR – gewogen (default weights)")
202
+ if st.session_state.info["asset_youtube_url"]:
203
+ st.markdown("---")
204
+ st.caption("YouTube preview")
205
+ st.video(st.session_state.info["asset_youtube_url"])
206
+
207
+ if st.button("Save rating now", type="primary"):
208
+ info = st.session_state.info
209
+ scene_audience = info["scene_audience_custom"] if info["scene_audience_choice"] == "Other..." else info["scene_audience_choice"]
210
+ if not info["campaign_id"].strip():
211
+ st.error("Campaign ID is verplicht.")
212
+ else:
213
+ row = {
214
+ "campaign_id": info["campaign_id"].strip(),
215
+ "campaign_name": info["campaign_name"].strip(),
216
+ "brand": info["brand"].strip(),
217
+ "channel": info["channel"],
218
+ "scene_audience": scene_audience.strip(),
219
+ "country": info["country"].strip(),
220
+ "submit_date_iso": info["submit_date_iso"].strip(),
221
+ "asset_youtube_url": info["asset_youtube_url"].strip(),
222
+ "rater_id": info["rater_id"],
223
+ "rater_notes": info["rater_notes"],
224
+ **st.session_state.scores,
225
+ **st.session_state.risks,
226
+ }
227
+ save_rating(row)
228
+ # increment campaign id for next entry
229
+ df_after = load_ratings()
230
+ st.session_state.info["campaign_id"] = next_campaign_id(df_after)
231
+ st.success("Rating opgeslagen.")
232
+ # Navigate to Output
233
+ st.session_state.step = "Output"
234
+ st.rerun()
235
+
236
+ # --------- RENDER: Output ---------
237
+ else: # Output
238
+ st.subheader("Output")
239
+ st.markdown("**Dataset (alle ratings)**")
240
+ st.dataframe(load_ratings(), use_container_width=True)
241
+ st.markdown("---")
242
+ st.markdown("**Per-campagne resultaten**")
243
+ res = compute_results(DEFAULT_WEIGHTS)
244
+ if not res.empty:
245
+ st.dataframe(res, use_container_width=True)
246
+ st.download_button("Download CCR_results.csv", res.to_csv(index=False).encode("utf-8"),
247
+ file_name="CCR_results.csv", mime="text/csv")
248
+ else:
249
+ st.info("Nog geen resultaten – voeg eerst één of meer ratings toe.")
data/CCR_default_weights.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "CR_cultural_resonance": 0.22,
3
+ "OR_originality": 0.18,
4
+ "TI_timeliness": 0.12,
5
+ "IE_inclusivity_ethics": 0.1,
6
+ "SH_shareability": 0.15,
7
+ "BF_brand_channel_fit": 0.13,
8
+ "CQ_craft_quality": 0.1
9
+ }
data/CCR_demo_ratings.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ campaign_id,campaign_name,brand,channel,scene_audience,country,submit_date_iso,rater_id,rater_notes,asset_youtube_url,CR_cultural_resonance,OR_originality,TI_timeliness,IE_inclusivity_ethics,SH_shareability,BF_brand_channel_fit,CQ_craft_quality,flag_stereotype,flag_misappropriation,flag_sensitive_timing,flag_other_risk,neg_sentiment_ratio_estimate
2
+ CMP001,Street Sparks 15s,Volt,TikTok,BE urban 16-24,BE,2025-09-01,Lode,"Sterke insider cues, sound is trending.",https://www.youtube.com/watch?v=dQw4w9WgXcQ,4.5,4.0,5.0,4.5,4.0,4.5,4.0,0,0,0,0,0.05
3
+ CMP002,Heritage Remix OOH,Aurora,OOH,EU mainstream 25-44,NL,2025-08-15,Maarten,"Craft top, maar appropriation-risico.",,3.0,3.0,3.0,2.5,2.0,3.0,4.5,0,1,0,0,0.28
data/CCR_ratings.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ campaign_id,rater_id,rater_notes,campaign_name,brand,channel,scene_audience,country,submit_date_iso,asset_youtube_url,CR_cultural_resonance,OR_originality,TI_timeliness,IE_inclusivity_ethics,SH_shareability,BF_brand_channel_fit,CQ_craft_quality,flag_stereotype,flag_misappropriation,flag_sensitive_timing,flag_other_risk,neg_sentiment_ratio_estimate
2
+ CMP001,Lode,,,,TikTok,BE urban 16-24,BE,2025-09-29,,3.0,3.0,3.0,3.0,3.0,3.0,3.0,0,0,0,0,0.0
data/CCR_results.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ campaign_id,CR_cultural_resonance,OR_originality,TI_timeliness,IE_inclusivity_ethics,SH_shareability,BF_brand_channel_fit,CQ_craft_quality,CR_cultural_resonance_100,OR_originality_100,TI_timeliness_100,IE_inclusivity_ethics_100,SH_shareability_100,BF_brand_channel_fit_100,CQ_craft_quality_100,flag_stereotype,flag_misappropriation,flag_sensitive_timing,flag_other_risk,neg_sentiment_ratio_estimate,CCR_rater,campaign_name,brand,channel,scene_audience,country,asset_youtube_url,CCR_mean
2
+ CMP001,3.0,3.0,3.0,3.0,3.0,3.0,3.0,50.0,50.0,50.0,50.0,50.0,50.0,50.0,0.0,0.0,0.0,0.0,0.0,50.0,,,TikTok,BE urban 16-24,BE,,50.0
data/CCR_rubric_template.csv ADDED
@@ -0,0 +1 @@
 
 
1
+ campaign_id,campaign_name,brand,channel,scene_audience,country,submit_date_iso,rater_id,rater_notes,asset_youtube_url,CR_cultural_resonance,OR_originality,TI_timeliness,IE_inclusivity_ethics,SH_shareability,BF_brand_channel_fit,CQ_craft_quality,flag_stereotype,flag_misappropriation,flag_sensitive_timing,flag_other_risk,neg_sentiment_ratio_estimate