|
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455 |
- import logging
-
- from structs.headline import Headline
- from structs.story import Story
-
- logger = logging.getLogger(__name__)
-
-
- def cluster_stories(headlines: list[Headline], threshold: float) -> list[Story]:
- """Group headlines into stories using transitive similarity.
-
- Any two headlines whose cosine similarity meets ``threshold`` are linked,
- and links are unioned transitively, so a chain of near-duplicates collapses
- into a single story cluster even when the endpoints are not directly similar.
- """
- n = len(headlines)
- if n == 0:
- logger.info("No headlines to cluster.")
- return []
-
- parent = list(range(n))
-
- def find(x):
- while parent[x] != x:
- parent[x] = parent[parent[x]]
- x = parent[x]
- return x
-
- def union(a, b):
- ra, rb = find(a), find(b)
- if ra != rb:
- parent[rb] = ra
-
- links = 0
- for i in range(n):
- for j in range(i + 1, n):
- try:
- score = headlines[i].compare_headlines(headlines[j])
- except Exception as e:
- logger.error("Error comparing headlines [%d] and [%d] during clustering: %s",
- i, j, e, exc_info=True)
- continue
- if score >= threshold:
- union(i, j)
- links += 1
-
- # Assemble clusters (connected components) keyed by root index.
- components = {}
- for idx in range(n):
- components.setdefault(find(idx), []).append(headlines[idx])
-
- stories = [Story(members) for members in components.values()]
- logger.info("Clustering complete: %d headlines -> %d stories via %d similarity links.",
- n, len(stories), links)
- return stories
|