Parthnuwal7 commited on
Commit
0e9ab9b
·
1 Parent(s): debd89c

fix: stop taste worlds getting near-identical names

Browse files

Four of seven worlds came back as permutations of Pop/Music/Electronic/Asian,
which made genuinely different groups unreadable. The clustering was fine; the
naming was not.

YouTube's topicCategories are extremely coarse -- Bollywood playback, K-pop and
Western EDM all return "Pop music, Music, Electronic music" -- and the prompt was
telling the model to use them, so it faithfully echoed near-identical inputs.

Now the channel names lead, generic topics are filtered out, topics are demoted to
a trailing hint, and the prompt asks for distinct names with worked examples.
Duplicate and reordered-duplicate names are rejected on the way back in, since two
worlds sharing a name is worse than falling back to channel labels.

Live result: "Bollywood Playback Singers", "Western Pop Artists", "IPL Cricket"
in place of "Asian, Pop, Music", "Pop, Electronic, Music", "Cricket, Sport".

services/cluster_naming.py CHANGED
@@ -25,12 +25,30 @@ MAX_CHANNELS_SENT = 5 # enough to characterise a cluster, no more than need
25
  MAX_NAME_LENGTH = 40
26
  TIMEOUT = 30
27
 
 
 
 
 
 
 
 
 
 
28
  SYSTEM_PROMPT = (
29
- "You name groups of YouTube channels. For each numbered group you are given some "
30
- "channel names and the topic categories YouTube assigns them. Reply with JSON only: "
31
- "an object mapping each group number (as a string) to a short human name of at most "
32
- "four words, for example {\"0\": \"IPL Cricket\"}. Use the topics given; do not "
33
- "invent facts. No prose, no code fences."
 
 
 
 
 
 
 
 
 
34
  )
35
 
36
 
@@ -40,21 +58,36 @@ def channel_label(channels: List[str]) -> str:
40
 
41
 
42
  def _prompt(clusters: List[Dict], facts: Dict[str, Dict]) -> str:
 
 
 
 
 
 
43
  lines = []
44
  for cluster in clusters:
45
  names = cluster["channels"][:MAX_CHANNELS_SENT]
46
  topics = []
47
  for name in names:
48
  for topic in (facts.get(name) or {}).get("topics", []):
49
- if topic not in topics:
50
  topics.append(topic)
51
- line = f"{cluster['index']}: channels = {', '.join(names)}"
52
  if topics:
53
- line += f"; topics = {', '.join(topics[:6])}"
54
  lines.append(line)
55
  return "\n".join(lines)
56
 
57
 
 
 
 
 
 
 
 
 
 
58
  def name_clusters(
59
  clusters: List[Dict],
60
  facts: Optional[Dict[str, Dict]],
@@ -112,11 +145,26 @@ def name_clusters(
112
 
113
  names: Dict[int, str] = {}
114
  valid = {c["index"] for c in clusters}
 
 
115
  for key, value in (raw or {}).items():
116
  try:
117
  index = int(key)
118
  except (TypeError, ValueError):
119
  continue
120
- if index in valid and isinstance(value, str) and value.strip():
121
- names[index] = value.strip()[:MAX_NAME_LENGTH]
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  return names
 
25
  MAX_NAME_LENGTH = 40
26
  TIMEOUT = 30
27
 
28
+ # Topics so broad they describe nearly every channel; they crowd out the useful ones.
29
+ GENERIC_TOPICS = {
30
+ "Music", "Entertainment", "Lifestyle (sociology)", "Society", "Hobby",
31
+ "Knowledge", "Film", "Performing arts",
32
+ }
33
+
34
+ # Ignored when deciding whether two names are really the same.
35
+ FILLER_WORDS = {"and", "the", "of", "a", "an", "your", "music"}
36
+
37
  SYSTEM_PROMPT = (
38
+ "You name groups of YouTube channels. For each numbered group you get the channel "
39
+ "names, and sometimes the topic categories YouTube assigns them.\n\n"
40
+ "The channel names are the real signal. YouTube's topic categories are extremely "
41
+ "coarse -- Bollywood playback singers, K-pop and Western EDM all come back as "
42
+ "'Pop music, Music, Electronic music' -- so treat them as a weak hint and never "
43
+ "just echo them back.\n\n"
44
+ "Name each group for what those specific channels actually are: "
45
+ "'Bollywood Playback' rather than 'Asian, Pop, Music'; 'IPL Cricket' rather than "
46
+ "'Sport, Lifestyle'; 'Indian News' rather than 'Entertainment, Society'.\n\n"
47
+ "Every name must be DISTINCT from the others -- two groups sharing a name, or the "
48
+ "same words in a different order, makes the list unreadable. If two groups look "
49
+ "similar, find what separates them.\n\n"
50
+ "Reply with JSON only: an object mapping each group number (as a string) to a name "
51
+ "of at most four words, e.g. {\"0\": \"IPL Cricket\"}. No prose, no code fences."
52
  )
53
 
54
 
 
58
 
59
 
60
  def _prompt(clusters: List[Dict], facts: Dict[str, Dict]) -> str:
61
+ """One line per group: channel names first, topics as a trailing hint.
62
+
63
+ Topics are collected per cluster but kept short. Deduplicating them across a
64
+ whole cluster is what flattened every music world into the same three words,
65
+ so the channel names lead and the taxonomy only ever supplements them.
66
+ """
67
  lines = []
68
  for cluster in clusters:
69
  names = cluster["channels"][:MAX_CHANNELS_SENT]
70
  topics = []
71
  for name in names:
72
  for topic in (facts.get(name) or {}).get("topics", []):
73
+ if topic not in topics and topic not in GENERIC_TOPICS:
74
  topics.append(topic)
75
+ line = f"{cluster['index']}: {', '.join(names)}"
76
  if topics:
77
+ line += f" (hint: {', '.join(topics[:4])})"
78
  lines.append(line)
79
  return "\n".join(lines)
80
 
81
 
82
+ def _normalise(name: str) -> str:
83
+ """A comparison key that treats reorderings as the same name.
84
+
85
+ "Pop, Electronic, Music" and "Electronic, Pop, Music" are not two names.
86
+ """
87
+ words = re.findall(r"[a-z]+", name.lower())
88
+ return " ".join(sorted(w for w in words if w not in FILLER_WORDS))
89
+
90
+
91
  def name_clusters(
92
  clusters: List[Dict],
93
  facts: Optional[Dict[str, Dict]],
 
145
 
146
  names: Dict[int, str] = {}
147
  valid = {c["index"] for c in clusters}
148
+ seen: set = set()
149
+
150
  for key, value in (raw or {}).items():
151
  try:
152
  index = int(key)
153
  except (TypeError, ValueError):
154
  continue
155
+ if index not in valid or not isinstance(value, str) or not value.strip():
156
+ continue
157
+
158
+ name = value.strip()[:MAX_NAME_LENGTH]
159
+ signature = _normalise(name)
160
+
161
+ # A repeated name is worse than none: two worlds labelled the same cannot be
162
+ # told apart, and the channel-label fallback at least stays true. Drop the
163
+ # collision rather than show it.
164
+ if not signature or signature in seen:
165
+ continue
166
+
167
+ seen.add(signature)
168
+ names[index] = name
169
+
170
  return names
tests/test_cluster_naming.py CHANGED
@@ -168,3 +168,54 @@ def test_names_for_unknown_cluster_indexes_are_ignored():
168
  result = name_clusters(CLUSTERS, FACTS, KEY, consented=True, session=session)
169
 
170
  assert result == {0: "Cricket"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  result = name_clusters(CLUSTERS, FACTS, KEY, consented=True, session=session)
169
 
170
  assert result == {0: "Cricket"}
171
+
172
+
173
+ # --- name quality ------------------------------------------------------------------
174
+
175
+ def test_channels_are_presented_before_topics():
176
+ """The channel names carry the signal; YouTube's topics are coarse.
177
+
178
+ Every music cluster comes back from YouTube as "Pop music, Music, Electronic
179
+ music" whether it is Bollywood playback or Western EDM, so a prompt that leads
180
+ with topics produces near-identical names for genuinely different worlds.
181
+ """
182
+ from services.cluster_naming import _prompt
183
+
184
+ text = _prompt(CLUSTERS, FACTS)
185
+ first_line = text.splitlines()[0]
186
+
187
+ assert first_line.index("rajasthanroyals") < first_line.index("Cricket")
188
+
189
+
190
+ def test_the_prompt_asks_for_distinct_names():
191
+ from services.cluster_naming import SYSTEM_PROMPT
192
+
193
+ lowered = SYSTEM_PROMPT.lower()
194
+ assert "distinct" in lowered or "different from each other" in lowered
195
+
196
+
197
+ def test_the_prompt_warns_that_topics_are_coarse():
198
+ """Without this the model treats the taxonomy as authoritative and echoes it."""
199
+ from services.cluster_naming import SYSTEM_PROMPT
200
+
201
+ assert "coarse" in SYSTEM_PROMPT.lower() or "generic" in SYSTEM_PROMPT.lower()
202
+
203
+
204
+ def test_duplicate_names_are_rejected():
205
+ """Two worlds sharing a name is worse than no name -- the card cannot be read."""
206
+ session = FakeSession(reply('{"0": "Pop Music", "1": "Pop Music"}'))
207
+
208
+ result = name_clusters(CLUSTERS, FACTS, KEY, consented=True, session=session)
209
+
210
+ assert len(set(result.values())) == len(result), f"duplicate names survived: {result}"
211
+
212
+
213
+ def test_near_duplicate_names_are_rejected():
214
+ """"Pop, Electronic, Music" and "Electronic, Pop, Music" are the same name."""
215
+ session = FakeSession(
216
+ reply('{"0": "Pop, Electronic, Music", "1": "Electronic, Pop, Music"}')
217
+ )
218
+
219
+ result = name_clusters(CLUSTERS, FACTS, KEY, consented=True, session=session)
220
+
221
+ assert len(result) <= 1, f"a reordering of the same words survived: {result}"