File size: 2,881 Bytes
f44aac9
 
 
 
 
 
 
 
 
 
 
 
 
e50180d
f44aac9
 
 
 
 
 
 
 
 
 
 
e50180d
f44aac9
 
 
 
 
 
 
 
 
 
e50180d
f44aac9
 
 
 
 
902a11f
f44aac9
 
 
 
 
 
 
 
9eec184
f44aac9
 
 
 
9eec184
f44aac9
 
 
 
 
 
 
 
 
e50180d
f44aac9
 
9eec184
f44aac9
 
 
 
 
 
 
e50180d
f44aac9
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
from __future__ import annotations

from dataclasses import dataclass

from hackathon_advisor.data import ProjectIndex, SearchHit, tokenize


@dataclass(frozen=True)
class ScoreCard:
    originality: int
    delight: int
    ai_necessity: int
    feasibility: int
    goal_fit: int
    verdict: str
    echoes: tuple[SearchHit, ...]

    @property
    def overall(self) -> float:
        return round(
            (
                self.originality * 0.30
                + self.delight * 0.20
                + self.ai_necessity * 0.20
                + self.feasibility * 0.15
                + self.goal_fit * 0.15
            ),
            1,
        )

    def to_dict(self) -> dict:
        return {
            "originality": self.originality,
            "delight": self.delight,
            "ai_necessity": self.ai_necessity,
            "feasibility": self.feasibility,
            "goal_fit": self.goal_fit,
            "overall": self.overall,
            "verdict": self.verdict,
            "echoes": [
                {
                    "score": round(hit.score, 3),
                    "page_number": hit.page_number,
                    "matched_terms": list(hit.matched_terms),
                    "project": hit.project.to_public_dict(),
                }
                for hit in self.echoes
            ],
        }


def score_idea(index: ProjectIndex, title: str, pitch: str, goals: list[str] | None = None) -> ScoreCard:
    text = f"{title} {pitch}".strip()
    hits = index.search(text, limit=4)
    top_overlap = hits[0].score if hits else 0.0
    tokens = set(tokenize(text))
    goals = goals or []

    originality = clamp_score(10 - round(top_overlap * 18))
    delight = clamp_score(4 + _keyword_count(tokens, {"story", "visual", "game", "ritual", "share", "voice"}) * 2)
    ai_necessity = clamp_score(
        3
        + _keyword_count(tokens, {"agent", "model", "embed", "search", "personal", "speech", "local"}) * 2
    )
    complexity_penalty = _keyword_count(tokens, {"realtime", "video", "multiplayer", "payments", "social"})
    feasibility = clamp_score(8 - complexity_penalty)
    goal_fit = clamp_score(
        4
        + _keyword_count(tokens, {"local", "offline", "small", "llama", "fine", "trace", "gradio"}) * 2
        + min(len(goals), 3)
    )
    verdict = "UNWRITTEN" if top_overlap < 0.16 else f"ECHO x{sum(1 for hit in hits if hit.score >= 0.12)}"
    return ScoreCard(
        originality=originality,
        delight=delight,
        ai_necessity=ai_necessity,
        feasibility=feasibility,
        goal_fit=goal_fit,
        verdict=verdict,
        echoes=tuple(hits),
    )


def clamp_score(value: int) -> int:
    return max(1, min(10, value))


def _keyword_count(tokens: set[str], keywords: set[str]) -> int:
    return sum(1 for keyword in keywords if any(token.startswith(keyword) for token in tokens))