Skip to content

Commit 14ba2b3

Browse files
ralyodioclaude
andauthored
A show's Topics are the publisher's own categories, not counted phrases (#193)
Live, a podcast's Broadcast section listed "episode, find, version, post" beside its real categories: phrases the crawler counted out of the text, which are the directory's reading of the show and not what the host said it is about. The Broadcast Topics key now carries only the feed's own category tags. The profile-level Topics keep both kinds, as before, since that section is the directory's index of the person and says so. Claude-Session: https://claude.ai/code/session_01S7yeJUHGxA4P5N74xnsRPQ Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent cdc9459 commit 14ba2b3

4 files changed

Lines changed: 32 additions & 14 deletions

File tree

‎apps/web/src/lib/openprofile.js‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function label(network) {
8282
* site_url?: string|null, links?: Array<{ network: string, url: string, handle?: string|null }> },
8383
* feeds: Array<{ id: string, slug: string, title: string, kind?: string|null, role?: string|null,
8484
* feed_url?: string|null, site_url?: string|null, language?: string|null }>,
85-
* topicsByFeed?: Map<string, string[]>,
85+
* topicsByFeed?: Map<string, Array<{ keyword: string, source: string }>>,
8686
* base: string,
8787
* }} input
8888
* @returns {import('@profullstack/openprofile').OpenProfileDoc}
@@ -112,13 +112,23 @@ export function generateAuthorProfile({ person, feeds, topicsByFeed, base }) {
112112
const seen = new Set();
113113
for (const f of feeds) {
114114
for (const t of topicsByFeed?.get(String(f.id)) ?? []) {
115-
const key = t.toLowerCase();
115+
const key = t.keyword.toLowerCase();
116116
if (seen.has(key) || topics.length >= MAX_TOPICS) continue;
117117
seen.add(key);
118-
topics.push(t);
118+
topics.push(t.keyword);
119119
}
120120
}
121121

122+
// A show's own Topics are the publisher's category tags and nothing else:
123+
// a phrase the crawler counted out of the transcript ("episode", "listen")
124+
// is the directory's reading, not what the host said the show is about.
125+
const showTopics = (f) =>
126+
(topicsByFeed?.get(String(f.id)) ?? [])
127+
.filter((t) => t.source === 'category')
128+
.slice(0, 8)
129+
.map((t) => t.keyword)
130+
.join(', ') || null;
131+
122132
// Broadcast: one `### <show>` group per show they own. A feed they merely
123133
// write in is somebody else's show and is not theirs to describe.
124134
const shows = feeds.filter(
@@ -131,7 +141,7 @@ export function generateAuthorProfile({ person, feeds, topicsByFeed, base }) {
131141
Language: f.language ?? null,
132142
Feed: f.feed_url ?? null,
133143
Listen: f.site_url ?? `${base}/${encodeURIComponent(String(f.slug))}`,
134-
Topics: (topicsByFeed?.get(String(f.id)) ?? []).slice(0, 8).join(', ') || null,
144+
Topics: showTopics(f),
135145
};
136146
const section = keyedSection('Broadcast', keys);
137147
return section ? `### ${f.title}\n\n${section.body}` : null;
@@ -144,7 +154,7 @@ export function generateAuthorProfile({ person, feeds, topicsByFeed, base }) {
144154
Language: shows[0].language ?? null,
145155
Feed: shows[0].feed_url ?? null,
146156
Listen: shows[0].site_url ?? `${base}/${encodeURIComponent(String(shows[0].slug))}`,
147-
Topics: (topicsByFeed?.get(String(shows[0].id)) ?? []).slice(0, 8).join(', ') || null,
157+
Topics: showTopics(shows[0]),
148158
})
149159
: groups.some(Boolean)
150160
? { title: 'Broadcast', name: 'broadcast', body: groups.filter(Boolean).join('\n\n') }

‎apps/web/test/openprofile.test.js‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ const FEEDS = [
5353
},
5454
];
5555
const TOPICS = new Map([
56-
['f1', ['history', 'mathematics']],
57-
['f2', ['mathematics', 'writing']],
56+
['f1', [{ keyword: 'history', source: 'category' }, { keyword: 'mathematics', source: 'content' }]],
57+
['f2', [{ keyword: 'mathematics', source: 'content' }, { keyword: 'writing', source: 'category' }]],
5858
]);
5959

6060
test('the generated file says what the author published, and nothing they did not', () => {
@@ -84,8 +84,9 @@ test('the generated file says what the author published, and nothing they did no
8484
Language: 'en',
8585
Feed: 'https://ada.example/podcast/feed.xml',
8686
Listen: 'https://ada.example/podcast',
87-
Topics: 'history, mathematics',
87+
Topics: 'history',
8888
});
89+
assert.equal(shows[0].Topics, 'history', "a show's Topics are the publisher's own categories, not counted phrases");
8990
assert.equal('Seeking' in shows[0], false);
9091
assert.equal('Since' in shows[0], false, 'the feed window is not when the show started');
9192
assert.equal(doc.sections.some((s) => s.name === 'guest'), false, 'no Guest section unless the person wrote one');

‎packages/db/src/profiles.js‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,16 @@ export async function profilesForUser(db, userId) {
145145

146146
/**
147147
* The topics of several feeds at once, strongest first per feed, as
148-
* `feed_id -> keyword[]`. What the Topics of a profile and the `Topics` key of
149-
* each Broadcast group are made of.
148+
* `feed_id -> [{ keyword, source }]`. What the Topics of a profile and the
149+
* `Topics` key of each Broadcast group are made of. `source` rides along
150+
* because the two differ: 'category' is the publisher's own tag and
151+
* 'content' is a phrase counted out of their text, and a show's Broadcast
152+
* lists only the first kind.
150153
*
151154
* @param {Client} db
152155
* @param {string[]} feedIds
153156
* @param {number} [perFeed]
154-
* @returns {Promise<Map<string, string[]>>}
157+
* @returns {Promise<Map<string, Array<{ keyword: string, source: string }>>>}
155158
*/
156159
export async function keywordsForFeeds(db, feedIds, perFeed = 8) {
157160
const out = new Map();
@@ -165,7 +168,7 @@ export async function keywordsForFeeds(db, feedIds, perFeed = 8) {
165168
for (const row of rows) {
166169
const id = String(row.feed_id);
167170
const list = out.get(id) ?? [];
168-
if (list.length < perFeed) list.push(String(row.keyword));
171+
if (list.length < perFeed) list.push({ keyword: String(row.keyword), source: String(row.source) });
169172
out.set(id, list);
170173
}
171174
return out;

‎packages/db/test/profiles.test.js‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,12 @@ test('no row means no profile: the generated file stands', async () => {
7070

7171
test('the Topics inputs come from the feeds themselves', async () => {
7272
const topics = await profiles.keywordsForFeeds(db, [String(podcast.id), String(blog.id)]);
73-
assert.deepEqual(topics.get(String(podcast.id)), ['history', 'mathematics'], 'category first, then by count');
74-
assert.deepEqual(topics.get(String(blog.id)), ['mathematics']);
73+
assert.deepEqual(
74+
topics.get(String(podcast.id)),
75+
[{ keyword: 'history', source: 'category' }, { keyword: 'mathematics', source: 'content' }],
76+
'category first, then by count',
77+
);
78+
assert.deepEqual(topics.get(String(blog.id)), [{ keyword: 'mathematics', source: 'content' }]);
7579
assert.equal((await profiles.keywordsForFeeds(db, [])).size, 0);
7680
});
7781

0 commit comments

Comments
 (0)