Spaces:
Sleeping
Sleeping
| """ | |
| 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() | |