Addplace / component4_recommender.py
Kosala Nayanajith Deshapriya
Ad Placement Recommender - clean deploy
4dc7a21
Raw
History Blame Contribute Delete
5.78 kB
"""
Component 4: Smart Recommender
- Loads ranked_candidates.json from Component 3
- Applies business rules to filter and finalize ad placement timestamps
- Outputs final_recommendations.json β€” the creator-ready result
"""
import json
import os
# ── Business Rule Config (tweak these per creator/video) ──
CONFIG = {
"min_placement_score": 0.0, # minimum ML score to consider
"min_gap_seconds": 120, # minimum 3 min between placements
"skip_intro_pct": 0.20, # skip first 20% of video
"skip_outro_pct": 0.10, # skip last 10% of video
"max_placements": 3, # max sponsored segments per video
"short_video_threshold": 480, # videos <= 8 min β†’ max 1 placement
"post_peak_bonus": 0.05, # future: boost score if after sentiment peak
}
def load_ranked(path="ranked_candidates.json"):
with open(path) as f:
data = json.load(f)
placements = data["ranked_placements"]
# find total duration from highest timestamp as fallback
total_duration = max(p["timestamp"] for p in placements) / 0.80
print(f"[Component 4] Loaded {len(placements)} ranked candidates")
print(f" Estimated video duration: {int(total_duration//60)}m {int(total_duration%60)}s")
return placements, total_duration
def apply_rules(placements, total_duration, config=CONFIG):
print("\n[Applying Business Rules]")
# Rule 1: Score threshold
filtered = [p for p in placements if p["placement_score"] >= config["min_placement_score"]]
print(f" Rule 1 β€” Score >= {config['min_placement_score']}: {len(filtered)} passed")
# Rule 2: Skip intro
intro_cut = total_duration * config["skip_intro_pct"]
filtered = [p for p in filtered if p["timestamp"] >= intro_cut]
print(f" Rule 2 β€” Skip intro ({int(config['skip_intro_pct']*100)}%, < {int(intro_cut)}s removed): {len(filtered)} remain")
# Rule 3: Skip outro
outro_cut = total_duration * (1 - config["skip_outro_pct"])
filtered = [p for p in filtered if p["timestamp"] <= outro_cut]
print(f" Rule 3 β€” Skip outro ({int(config['skip_outro_pct']*100)}%, > {int(outro_cut)}s removed): {len(filtered)} remain")
# Rule 4: Max placements for short videos
max_allow = 1 if total_duration <= config["short_video_threshold"] else config["max_placements"]
print(f" Rule 4 β€” Max placements allowed: {max_allow}")
# Rule 5: Minimum gap enforcement (greedy selection by score)
selected = []
for p in sorted(filtered, key=lambda x: x["placement_score"], reverse=True):
if len(selected) >= max_allow:
break
too_close = any(abs(p["timestamp"] - s["timestamp"]) < config["min_gap_seconds"] for s in selected)
if not too_close:
selected.append(p)
print(f" Rule 5 β€” Min gap {config['min_gap_seconds']}s enforced: {len(selected)} final placements")
return selected
def format_output(selected, total_duration, config=CONFIG):
output = {
"video_duration_seconds": round(total_duration, 1),
"video_duration_formatted": f"{int(total_duration//60)}m {int(total_duration%60)}s",
"total_placements_recommended": len(selected),
"config_used": config,
"recommendations": []
}
for i, p in enumerate(sorted(selected, key=lambda x: x["timestamp"])):
output["recommendations"].append({
"placement_number": i + 1,
"timestamp_seconds": p["timestamp"],
"timestamp_formatted": p["timestamp_formatted"],
"type": p["type"],
"placement_score": p["placement_score"],
"retention_at_t": p["retention_at_t"],
"confidence": (
"HIGH" if p["placement_score"] >= 0.75 else
"MEDIUM" if p["placement_score"] >= 0.50 else
"LOW"
),
"creator_note": (
f"Place sponsored segment at {p['timestamp_formatted']} β€” "
f"natural {p['type'].replace('_', ' ')} detected, "
f"{p['retention_at_t']:.1f}% viewers still watching."
)
})
return output
def print_summary(output):
print("\n" + "=" * 55)
print(" FINAL RECOMMENDATIONS FOR CREATOR")
print("=" * 55)
print(f" Video Duration : {output['video_duration_formatted']}")
print(f" Placements : {output['total_placements_recommended']}")
print()
if not output["recommendations"]:
print(" ⚠️ No suitable placement found.")
print(" Suggestion: Lower min_placement_score in CONFIG")
print(" or collect more video data for better ML labels.")
else:
for r in output["recommendations"]:
print(f" πŸ“ Placement {r['placement_number']}")
print(f" Timestamp : {r['timestamp_formatted']}")
print(f" Type : {r['type']}")
print(f" Score : {r['placement_score']} ({r['confidence']})")
print(f" Retention : {r['retention_at_t']}% viewers watching")
print(f" Note : {r['creator_note']}")
print()
def run(ranked_path="ranked_candidates.json", output_path="final_recommendations.json"):
print("=" * 55)
print(" COMPONENT 4: SMART RECOMMENDER")
print("=" * 55)
placements, total_duration = load_ranked(ranked_path)
selected = apply_rules(placements, total_duration)
output = format_output(selected, total_duration)
print_summary(output)
with open(output_path, "w") as f:
json.dump(output, f, indent=2)
print(f"βœ… Component 4 Complete!")
print(f" β†’ {output_path}")
print(f" Next: python dashboard.py (Streamlit visualization)")
if __name__ == "__main__":
run()