@lacspace/trends 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,51 @@
1
+ Lacspace Free Licence
2
+ Version 1.0, August 2026
3
+
4
+ Copyright (c) 2026 Lacspace
5
+
6
+ PREAMBLE
7
+
8
+ This software is published by Lacspace under the Lacspace Free Licence — a free,
9
+ permissive licence that lets you use this software for any purpose, including in
10
+ commercial products and services, at no cost. It grants the same freedoms as
11
+ common permissive open-source licences; the only condition is that this notice
12
+ travels with the software. The canonical, always-current text of this licence is
13
+ maintained at https://lacspace.com/licenses/lacspace-free-1.0
14
+
15
+ GRANT OF RIGHTS
16
+
17
+ Permission is hereby granted, free of charge, to any person or organisation
18
+ obtaining a copy of this software and its associated documentation and data files
19
+ (the "Software"), to deal in the Software without restriction, including without
20
+ limitation the rights to use, copy, modify, merge, publish, distribute,
21
+ sublicense, and/or sell copies of the Software, and to permit persons to whom the
22
+ Software is furnished to do so, subject to the conditions below. These rights are
23
+ granted for any purpose, personal or commercial, and are perpetual, worldwide,
24
+ non-exclusive, and royalty-free.
25
+
26
+ CONDITIONS
27
+
28
+ The above copyright notice, this permission notice, and the name of this licence
29
+ ("Lacspace Free Licence") shall be included in all copies or substantial portions
30
+ of the Software.
31
+
32
+ TRADEMARKS
33
+
34
+ This licence does not grant permission to use the trade names, trademarks, service
35
+ marks, logos, or product names of Lacspace, except as required to reproduce the
36
+ notice above or to describe the origin of the Software in a truthful manner.
37
+
38
+ DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
42
+ FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
43
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
44
+ AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
45
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
46
+
47
+ ---
48
+
49
+ The Lacspace Free Licence is a source-available, permissive licence and is not (as
50
+ of this version) an OSI-approved licence. In substance it grants the same freedoms
51
+ as the MIT Licence. Learn more at https://lacspace.com/licenses
package/README.md ADDED
@@ -0,0 +1,47 @@
1
+ # @lacspace/trends
2
+
3
+ **Trending topics and hashtags for a region — from official/public sources only, with story-relevance gating so you never trend-jack.** Merges Google Trends daily RSS, YouTube `mostPopular` (optional key) and Wikipedia pageviews across scripts, scores them (multi-source terms rank up), scores each against your story, and builds per-platform hashtag sets with limits and a shadow-ban filter.
4
+
5
+ ```bash
6
+ npm i @lacspace/trends
7
+ ```
8
+
9
+ ```ts
10
+ import { trends, hashtagsFor } from "@lacspace/trends";
11
+
12
+ const { trends: list, sources, warnings } = await trends({
13
+ geo: "NP",
14
+ youtubeApiKey: process.env.YOUTUBE_API_KEY, // optional
15
+ relevanceTo: "NEPSE index rose as investors returned", // optional: gate to your story
16
+ minRelevance: 0.3,
17
+ });
18
+ // list → [{ term, score, sources:["google","wikipedia"], volume, links, relevance }]
19
+
20
+ hashtagsFor(list.map((t) => t.term), "instagram"); // ["#Dashain", "#NEPSE", …] capped at 30
21
+ ```
22
+
23
+ ## Why
24
+
25
+ Picking trending topics/hashtags by hand misses the window; asking an LLM "what's trending" invents or hallucinates. `trends` pulls only **real, official/public signals**, merges them deterministically, and — crucially — scores each trend's relevance to *your* story so you ride only topics you can legitimately cover.
26
+
27
+ ## Sources (official/public only)
28
+
29
+ - **Google Trends** daily trending-searches RSS for a `geo` (with approx traffic + news links).
30
+ - **YouTube Data API** `videos.list chart=mostPopular regionCode` — optional, needs an API key.
31
+ - **Wikipedia pageviews** top articles (free REST API, no key; `ne.wikipedia` or `en.wikipedia`).
32
+
33
+ Each source is bring-your-own-`fetch`; a failing source becomes a `warnings` entry, never a thrown error.
34
+
35
+ ## API
36
+
37
+ - **`trends(options)`** → `{ trends, sources, warnings }`. Options: `geo`, `sources`, `youtubeApiKey`, `wikipediaProject`, `relevanceTo` (+ `minRelevance`), `fetch`, `signal`.
38
+ - **`mergeTrends(raw[])`** → merges raw trends across scripts (Dashain ≡ दशैं) and boosts multi-source agreement.
39
+ - **`relevanceTo(trends, story, { min? })`** → attaches a 0–1 `relevance` (token overlap via `@lacspace/keyphrase` + `@lacspace/translit`), optionally filtering.
40
+ - **`hashtagsFor(terms, platform, { banned?, max?, extra? })`** → camel-cased, de-duplicated hashtags within the platform limit (IG 30, X 3, TikTok 6…), dropping banned/shadow-ban-prone tags.
41
+ - **Source fns** `googleTrendsDaily`, `youtubeMostPopular`, `wikipediaTop` and `parseGoogleTrendsRss` are exported.
42
+ - **`describe()`** → machine-readable command schema for an AI "conductor".
43
+
44
+ Merging is **exact-key** (after cross-script romanization) so it never fuses two genuinely different topics; differently-phrased mentions stay separate by design.
45
+
46
+ ## Licence
47
+ [Lacspace Free Licence v1.0](https://developer.lacspace.com/licenses/lacspace-free-1.0) — free for personal and commercial use.
package/dist/index.cjs ADDED
@@ -0,0 +1,230 @@
1
+ 'use strict';
2
+
3
+ var keyphrase = require('@lacspace/keyphrase');
4
+ var translit = require('@lacspace/translit');
5
+
6
+ // src/index.ts
7
+
8
+ // src/sources.ts
9
+ function fetchImpl(f) {
10
+ const impl = f ?? globalThis.fetch;
11
+ if (!impl) throw new Error("trends: no fetch available \u2014 pass options.fetch");
12
+ return impl;
13
+ }
14
+ function rankWeights(n) {
15
+ return Array.from({ length: n }, (_, i) => n <= 1 ? 1 : 1 - 0.9 * i / (n - 1));
16
+ }
17
+ function parseTraffic(s) {
18
+ if (!s) return void 0;
19
+ const m = s.replace(/,/g, "").match(/(\d+)\s*(\+)?/);
20
+ return m ? Number(m[1]) : void 0;
21
+ }
22
+ async function googleTrendsDaily(options = {}) {
23
+ const geo = options.geo ?? "NP";
24
+ const url = `https://trends.google.com/trends/trendingsearches/daily/rss?geo=${encodeURIComponent(geo)}`;
25
+ const res = await fetchImpl(options.fetch)(url, { signal: options.signal });
26
+ if (!res.ok) throw new Error(`google-trends ${res.status}`);
27
+ return parseGoogleTrendsRss(await res.text());
28
+ }
29
+ function parseGoogleTrendsRss(xml) {
30
+ const items = [...xml.matchAll(/<item>([\s\S]*?)<\/item>/g)].map((m) => m[1]);
31
+ const out = [];
32
+ for (const item of items) {
33
+ const title = decode(first(item, /<title>([\s\S]*?)<\/title>/));
34
+ if (!title) continue;
35
+ const volume = parseTraffic(first(item, /<ht:approx_traffic>([\s\S]*?)<\/ht:approx_traffic>/));
36
+ const links = [...item.matchAll(/<ht:news_item_url>([\s\S]*?)<\/ht:news_item_url>/g)].map((m) => decode(m[1]));
37
+ out.push({ term: title, volume, links });
38
+ }
39
+ const w = rankWeights(out.length);
40
+ return out.map((o, i) => ({ term: o.term, source: "google", weight: w[i], volume: o.volume, links: o.links }));
41
+ }
42
+ async function youtubeMostPopular(options) {
43
+ if (!options.apiKey) return [];
44
+ const u = new URL("https://www.googleapis.com/youtube/v3/videos");
45
+ u.searchParams.set("part", "snippet,statistics");
46
+ u.searchParams.set("chart", "mostPopular");
47
+ u.searchParams.set("regionCode", options.regionCode ?? "NP");
48
+ u.searchParams.set("maxResults", String(options.maxResults ?? 25));
49
+ u.searchParams.set("key", options.apiKey);
50
+ const res = await fetchImpl(options.fetch)(u.toString(), { signal: options.signal });
51
+ if (!res.ok) throw new Error(`youtube ${res.status}`);
52
+ const data = await res.json();
53
+ const rows = data.items ?? [];
54
+ const w = rankWeights(rows.length);
55
+ return rows.map((it, i) => ({
56
+ term: it.snippet?.title ?? "",
57
+ source: "youtube",
58
+ weight: w[i],
59
+ volume: it.statistics?.viewCount ? Number(it.statistics.viewCount) : void 0,
60
+ links: it.id ? [`https://youtu.be/${it.id}`] : []
61
+ })).filter((t) => t.term);
62
+ }
63
+ async function wikipediaTop(options = {}) {
64
+ const project = options.project ?? "en.wikipedia";
65
+ const d = options.date ?? new Date(Date.now() - 864e5);
66
+ const yyyy = d.getUTCFullYear();
67
+ const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
68
+ const dd = String(d.getUTCDate()).padStart(2, "0");
69
+ const url = `https://wikimedia.org/api/rest_v1/metrics/pageviews/top/${project}/all-access/${yyyy}/${mm}/${dd}`;
70
+ const res = await fetchImpl(options.fetch)(url, { headers: { "User-Agent": "lacspace-trends/1.0 (contact@lacspace.com)" }, signal: options.signal });
71
+ if (!res.ok) throw new Error(`wikipedia ${res.status}`);
72
+ const data = await res.json();
73
+ const articles = data.items?.[0]?.articles ?? [];
74
+ const filtered = articles.filter((a) => !/^(Main_Page|Special:|Wikipedia:|विशेष:|मुख्य_पृष्ठ)/.test(a.article));
75
+ const w = rankWeights(filtered.length);
76
+ return filtered.map((a, i) => ({
77
+ term: String(a.article).replace(/_/g, " "),
78
+ source: "wikipedia",
79
+ weight: w[i],
80
+ volume: a.views,
81
+ links: [`https://${project}.org/wiki/${encodeURIComponent(a.article)}`]
82
+ }));
83
+ }
84
+ function first(s, re) {
85
+ const m = s.match(re);
86
+ return m ? m[1].trim() : void 0;
87
+ }
88
+ function decode(s) {
89
+ return (s ?? "").replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
90
+ }
91
+
92
+ // src/index.ts
93
+ var VERSION = "1.0.0";
94
+ function keyOf(term) {
95
+ const roman = translit.detectScript(term) === "ne" ? translit.transliterate(term, { from: "ne", to: "en" }) : term;
96
+ return roman.toLowerCase().replace(/[^a-z0-9]+/g, "");
97
+ }
98
+ function tokens(s) {
99
+ const roman = translit.detectScript(s) === "ne" ? translit.transliterate(s, { from: "ne", to: "en" }) : s;
100
+ return (roman.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((w) => w.length > 2);
101
+ }
102
+ function mergeTrends(raws) {
103
+ const groups = /* @__PURE__ */ new Map();
104
+ for (const r of raws) {
105
+ const k = keyOf(r.term);
106
+ if (!k) continue;
107
+ let g = groups.get(k);
108
+ if (!g) {
109
+ g = { best: r, score: 0, sources: /* @__PURE__ */ new Set(), links: /* @__PURE__ */ new Set(), volume: 0 };
110
+ groups.set(k, g);
111
+ }
112
+ g.score += r.weight;
113
+ g.sources.add(r.source);
114
+ for (const l of r.links ?? []) g.links.add(l);
115
+ if (r.volume) g.volume = Math.max(g.volume, r.volume);
116
+ if (r.weight > g.best.weight) g.best = r;
117
+ }
118
+ const out = [];
119
+ for (const g of groups.values()) {
120
+ const boost = 1 + 0.5 * (g.sources.size - 1);
121
+ out.push({
122
+ term: g.best.term,
123
+ score: round(g.score * boost),
124
+ sources: [...g.sources],
125
+ volume: g.volume || void 0,
126
+ links: [...g.links].slice(0, 5)
127
+ });
128
+ }
129
+ return out.sort((a, b) => b.score - a.score);
130
+ }
131
+ function relevanceTo(trends2, story, options = {}) {
132
+ const kp = keyphrase.keyphrase(story, { max: 20 });
133
+ const storyTokens = /* @__PURE__ */ new Set();
134
+ for (const p of kp.phrases ?? []) for (const t of tokens(p.phrase)) storyTokens.add(t);
135
+ for (const t of tokens(story)) storyTokens.add(t);
136
+ const scored = trends2.map((t) => {
137
+ const tt = tokens(t.term);
138
+ if (!tt.length || storyTokens.size === 0) return { ...t, relevance: 0 };
139
+ const hits = tt.filter((w) => storyTokens.has(w)).length;
140
+ const rel = round(hits / tt.length);
141
+ return { ...t, relevance: rel };
142
+ });
143
+ return options.min !== void 0 ? scored.filter((t) => (t.relevance ?? 0) >= options.min) : scored;
144
+ }
145
+ var HASHTAG_LIMITS = {
146
+ instagram: 30,
147
+ tiktok: 6,
148
+ x: 3,
149
+ facebook: 3,
150
+ youtube: 15,
151
+ threads: 5,
152
+ linkedin: 5,
153
+ telegram: 10
154
+ };
155
+ var DEFAULT_BANNED = /* @__PURE__ */ new Set(["followme", "follow4follow", "like4like", "nepaltiktok", "viral", "fyp"]);
156
+ function toHashtag(term) {
157
+ const roman = translit.detectScript(term) === "ne" ? translit.transliterate(term, { from: "ne", to: "en" }) : term;
158
+ const camel = roman.replace(/[^a-zA-Z0-9]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
159
+ return camel ? "#" + camel : "";
160
+ }
161
+ function hashtagsFor(terms, platform, options = {}) {
162
+ const limit = options.max ?? HASHTAG_LIMITS[platform] ?? 10;
163
+ const banned = /* @__PURE__ */ new Set([...DEFAULT_BANNED, ...(options.banned ?? []).map((b) => b.toLowerCase().replace(/^#/, ""))]);
164
+ const seen = /* @__PURE__ */ new Set();
165
+ const out = [];
166
+ for (const raw of [...terms, ...options.extra ?? []]) {
167
+ const tag = toHashtag(raw);
168
+ const bare = tag.slice(1).toLowerCase();
169
+ if (!tag || bare.length < 2 || banned.has(bare) || seen.has(bare)) continue;
170
+ seen.add(bare);
171
+ out.push(tag);
172
+ if (out.length >= limit) break;
173
+ }
174
+ return out;
175
+ }
176
+ async function trends(options = {}) {
177
+ const want = options.sources ?? ["google", "wikipedia", ...options.youtubeApiKey ? ["youtube"] : []];
178
+ const warnings = [];
179
+ const used = [];
180
+ const raws = [];
181
+ const run = async (s, fn) => {
182
+ try {
183
+ const r = await fn();
184
+ raws.push(...r);
185
+ used.push(s);
186
+ } catch (e) {
187
+ warnings.push(`${s}: ${e.message}`);
188
+ }
189
+ };
190
+ await Promise.all(
191
+ want.map((s) => {
192
+ if (s === "google") return run("google", () => googleTrendsDaily({ geo: options.geo, fetch: options.fetch, signal: options.signal }));
193
+ if (s === "wikipedia") return run("wikipedia", () => wikipediaTop({ project: options.wikipediaProject, fetch: options.fetch, signal: options.signal }));
194
+ if (s === "youtube" && options.youtubeApiKey) return run("youtube", () => youtubeMostPopular({ apiKey: options.youtubeApiKey, regionCode: options.geo, fetch: options.fetch, signal: options.signal }));
195
+ return Promise.resolve();
196
+ })
197
+ );
198
+ let merged = mergeTrends(raws);
199
+ if (options.relevanceTo) merged = relevanceTo(merged, options.relevanceTo, { min: options.minRelevance });
200
+ return { trends: merged, sources: used, warnings };
201
+ }
202
+ function describe() {
203
+ return {
204
+ name: "@lacspace/trends",
205
+ version: VERSION,
206
+ summary: "Trending topics for a region from official/public sources only (Google Trends RSS, YouTube mostPopular, Wikipedia pageviews), merged + scored, with story-relevance gating and per-platform hashtag sets.",
207
+ sources: ["google", "youtube", "wikipedia"],
208
+ commands: [
209
+ { name: "trends", input: { type: "object", properties: { geo: { type: "string" }, sources: { type: "array" }, youtubeApiKey: { type: "string" }, relevanceTo: { type: "string" }, minRelevance: { type: "number" } } }, output: "{ trends:[{term,score,sources,volume,links,relevance}], sources, warnings }" },
210
+ { name: "relevanceTo", input: { type: "object", properties: { trends: { type: "array" }, story: { type: "string" }, min: { type: "number" } }, required: ["trends", "story"] }, output: "Trend[] (with relevance)" },
211
+ { name: "hashtagsFor", input: { type: "object", properties: { terms: { type: "array" }, platform: { type: "string" }, max: { type: "integer" }, banned: { type: "array" } }, required: ["terms", "platform"] }, output: "string[]" }
212
+ ]
213
+ };
214
+ }
215
+ function round(n) {
216
+ return Math.round(n * 1e3) / 1e3;
217
+ }
218
+
219
+ exports.HASHTAG_LIMITS = HASHTAG_LIMITS;
220
+ exports.describe = describe;
221
+ exports.googleTrendsDaily = googleTrendsDaily;
222
+ exports.hashtagsFor = hashtagsFor;
223
+ exports.mergeTrends = mergeTrends;
224
+ exports.parseGoogleTrendsRss = parseGoogleTrendsRss;
225
+ exports.relevanceTo = relevanceTo;
226
+ exports.trends = trends;
227
+ exports.wikipediaTop = wikipediaTop;
228
+ exports.youtubeMostPopular = youtubeMostPopular;
229
+ //# sourceMappingURL=index.cjs.map
230
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sources.ts","../src/index.ts"],"names":["detectScript","transliterate","trends","keyphrase"],"mappings":"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","file":"index.cjs","sourcesContent":["export type TrendSource = \"google\" | \"youtube\" | \"wikipedia\";\n\nexport interface RawTrend {\n term: string;\n source: TrendSource;\n /** 0–1 weight within this source (1 = top). */\n weight: number;\n /** Approx search traffic / views, when the source gives it. */\n volume?: number;\n /** Supporting links (news items / article). */\n links?: string[];\n}\n\nexport type FetchLike = (url: string, init?: { headers?: Record<string, string>; signal?: AbortSignal }) => Promise<{ ok: boolean; status: number; text(): Promise<string>; json(): Promise<any> }>;\n\nfunction fetchImpl(f?: FetchLike): FetchLike {\n const impl = f ?? (globalThis as any).fetch;\n if (!impl) throw new Error(\"trends: no fetch available — pass options.fetch\");\n return impl;\n}\n\nfunction rankWeights(n: number): number[] {\n // Linear decay from 1 (rank 1) to ~0.1 (rank n).\n return Array.from({ length: n }, (_, i) => (n <= 1 ? 1 : 1 - (0.9 * i) / (n - 1)));\n}\n\nfunction parseTraffic(s: string | undefined): number | undefined {\n if (!s) return undefined;\n const m = s.replace(/,/g, \"\").match(/(\\d+)\\s*(\\+)?/);\n return m ? Number(m[1]) : undefined;\n}\n\n// --- Google Trends daily RSS (geo=NP by default) -----------------------------------\n/** Fetch + parse the Google Trends \"daily trending searches\" RSS for a region. */\nexport async function googleTrendsDaily(options: { geo?: string; fetch?: FetchLike; signal?: AbortSignal } = {}): Promise<RawTrend[]> {\n const geo = options.geo ?? \"NP\";\n const url = `https://trends.google.com/trends/trendingsearches/daily/rss?geo=${encodeURIComponent(geo)}`;\n const res = await fetchImpl(options.fetch)(url, { signal: options.signal });\n if (!res.ok) throw new Error(`google-trends ${res.status}`);\n return parseGoogleTrendsRss(await res.text());\n}\n\n/** Parse Google Trends daily RSS (title + ht:approx_traffic + ht:news_item links). */\nexport function parseGoogleTrendsRss(xml: string): RawTrend[] {\n const items = [...xml.matchAll(/<item>([\\s\\S]*?)<\\/item>/g)].map((m) => m[1]!);\n const out: { term: string; volume?: number; links: string[] }[] = [];\n for (const item of items) {\n const title = decode(first(item, /<title>([\\s\\S]*?)<\\/title>/));\n if (!title) continue;\n const volume = parseTraffic(first(item, /<ht:approx_traffic>([\\s\\S]*?)<\\/ht:approx_traffic>/));\n const links = [...item.matchAll(/<ht:news_item_url>([\\s\\S]*?)<\\/ht:news_item_url>/g)].map((m) => decode(m[1]!));\n out.push({ term: title, volume, links });\n }\n const w = rankWeights(out.length);\n return out.map((o, i) => ({ term: o.term, source: \"google\", weight: w[i]!, volume: o.volume, links: o.links }));\n}\n\n// --- YouTube Data API v3: most popular in a region (needs an API key) --------------\nexport async function youtubeMostPopular(options: { apiKey: string; regionCode?: string; maxResults?: number; fetch?: FetchLike; signal?: AbortSignal }): Promise<RawTrend[]> {\n if (!options.apiKey) return [];\n const u = new URL(\"https://www.googleapis.com/youtube/v3/videos\");\n u.searchParams.set(\"part\", \"snippet,statistics\");\n u.searchParams.set(\"chart\", \"mostPopular\");\n u.searchParams.set(\"regionCode\", options.regionCode ?? \"NP\");\n u.searchParams.set(\"maxResults\", String(options.maxResults ?? 25));\n u.searchParams.set(\"key\", options.apiKey);\n const res = await fetchImpl(options.fetch)(u.toString(), { signal: options.signal });\n if (!res.ok) throw new Error(`youtube ${res.status}`);\n const data = await res.json();\n const rows = (data.items ?? []) as any[];\n const w = rankWeights(rows.length);\n return rows.map((it, i) => ({\n term: it.snippet?.title ?? \"\",\n source: \"youtube\" as const,\n weight: w[i]!,\n volume: it.statistics?.viewCount ? Number(it.statistics.viewCount) : undefined,\n links: it.id ? [`https://youtu.be/${it.id}`] : [],\n })).filter((t) => t.term);\n}\n\n// --- Wikipedia pageviews top (free REST API, no key) -------------------------------\nexport async function wikipediaTop(options: { project?: string; date?: Date; fetch?: FetchLike; signal?: AbortSignal } = {}): Promise<RawTrend[]> {\n const project = options.project ?? \"en.wikipedia\";\n const d = options.date ?? new Date(Date.now() - 86400000); // yesterday (today may be incomplete)\n const yyyy = d.getUTCFullYear();\n const mm = String(d.getUTCMonth() + 1).padStart(2, \"0\");\n const dd = String(d.getUTCDate()).padStart(2, \"0\");\n const url = `https://wikimedia.org/api/rest_v1/metrics/pageviews/top/${project}/all-access/${yyyy}/${mm}/${dd}`;\n const res = await fetchImpl(options.fetch)(url, { headers: { \"User-Agent\": \"lacspace-trends/1.0 (contact@lacspace.com)\" }, signal: options.signal });\n if (!res.ok) throw new Error(`wikipedia ${res.status}`);\n const data = await res.json();\n const articles = (data.items?.[0]?.articles ?? []) as any[];\n const filtered = articles.filter((a) => !/^(Main_Page|Special:|Wikipedia:|विशेष:|मुख्य_पृष्ठ)/.test(a.article));\n const w = rankWeights(filtered.length);\n return filtered.map((a, i) => ({\n term: String(a.article).replace(/_/g, \" \"),\n source: \"wikipedia\" as const,\n weight: w[i]!,\n volume: a.views,\n links: [`https://${project}.org/wiki/${encodeURIComponent(a.article)}`],\n }));\n}\n\nfunction first(s: string, re: RegExp): string | undefined {\n const m = s.match(re);\n return m ? m[1]!.trim() : undefined;\n}\nfunction decode(s: string | undefined): string {\n return (s ?? \"\")\n .replace(/<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>/g, \"$1\")\n .replace(/&amp;/g, \"&\").replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\").replace(/&quot;/g, '\"').replace(/&#39;/g, \"'\")\n .trim();\n}\n","import { keyphrase } from \"@lacspace/keyphrase\";\nimport { transliterate, detectScript } from \"@lacspace/translit\";\nimport { FetchLike, googleTrendsDaily, RawTrend, TrendSource, wikipediaTop, youtubeMostPopular } from \"./sources.js\";\n\nexport * from \"./sources.js\";\n\nconst VERSION = \"1.0.0\";\n\nexport interface Trend {\n /** Display term (original casing/script of the strongest source). */\n term: string;\n /** Merged score, higher = more trending (multi-source terms rank up). */\n score: number;\n sources: TrendSource[];\n volume?: number;\n links: string[];\n /** 0–1 relevance to a story, set by relevanceTo / trends({ relevanceTo }). */\n relevance?: number;\n}\n\n/** A cross-script, lowercased key so \"Dashain\" and \"दशैं\" merge. */\nfunction keyOf(term: string): string {\n const roman = detectScript(term) === \"ne\" ? transliterate(term, { from: \"ne\", to: \"en\" }) : term;\n return roman.toLowerCase().replace(/[^a-z0-9]+/g, \"\");\n}\n\nfunction tokens(s: string): string[] {\n const roman = detectScript(s) === \"ne\" ? transliterate(s, { from: \"ne\", to: \"en\" }) : s;\n return (roman.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((w) => w.length > 2);\n}\n\n/** Merge raw trends from several sources into one scored, de-duplicated list. */\nexport function mergeTrends(raws: RawTrend[]): Trend[] {\n const groups = new Map<string, { best: RawTrend; score: number; sources: Set<TrendSource>; links: Set<string>; volume: number }>();\n for (const r of raws) {\n const k = keyOf(r.term);\n if (!k) continue;\n let g = groups.get(k);\n if (!g) {\n g = { best: r, score: 0, sources: new Set(), links: new Set(), volume: 0 };\n groups.set(k, g);\n }\n g.score += r.weight;\n g.sources.add(r.source);\n for (const l of r.links ?? []) g.links.add(l);\n if (r.volume) g.volume = Math.max(g.volume, r.volume);\n if (r.weight > g.best.weight) g.best = r; // strongest source sets the display term\n }\n const out: Trend[] = [];\n for (const g of groups.values()) {\n // Multi-source agreement is a strong signal → multiply by a diversity boost.\n const boost = 1 + 0.5 * (g.sources.size - 1);\n out.push({\n term: g.best.term,\n score: round(g.score * boost),\n sources: [...g.sources],\n volume: g.volume || undefined,\n links: [...g.links].slice(0, 5),\n });\n }\n return out.sort((a, b) => b.score - a.score);\n}\n\n/** Score each trend's relevance to a story (0–1) and attach it; optionally filter. */\nexport function relevanceTo(trends: Trend[], story: string, options: { min?: number } = {}): Trend[] {\n const kp = keyphrase(story, { max: 20 } as any);\n const storyTokens = new Set<string>();\n for (const p of kp.phrases ?? []) for (const t of tokens(p.phrase)) storyTokens.add(t);\n for (const t of tokens(story)) storyTokens.add(t);\n const scored = trends.map((t) => {\n const tt = tokens(t.term);\n if (!tt.length || storyTokens.size === 0) return { ...t, relevance: 0 };\n const hits = tt.filter((w) => storyTokens.has(w)).length;\n const rel = round(hits / tt.length);\n return { ...t, relevance: rel };\n });\n return options.min !== undefined ? scored.filter((t) => (t.relevance ?? 0) >= options.min!) : scored;\n}\n\n// --- hashtags ----------------------------------------------------------------------\nexport const HASHTAG_LIMITS: Record<string, number> = {\n instagram: 30, tiktok: 6, x: 3, facebook: 3, youtube: 15, threads: 5, linkedin: 5, telegram: 10,\n};\n// A conservative starter list of broken/shadow-ban-prone tags; callers extend.\nconst DEFAULT_BANNED = new Set([\"followme\", \"follow4follow\", \"like4like\", \"nepaltiktok\", \"viral\", \"fyp\"]);\n\nfunction toHashtag(term: string): string {\n const roman = detectScript(term) === \"ne\" ? transliterate(term, { from: \"ne\", to: \"en\" }) : term;\n const camel = roman.replace(/[^a-zA-Z0-9]+/g, \" \").trim().split(/\\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(\"\");\n return camel ? \"#\" + camel : \"\";\n}\n\n/** Build a platform-appropriate hashtag set from trend terms (+ optional extra tags). */\nexport function hashtagsFor(\n terms: string[],\n platform: keyof typeof HASHTAG_LIMITS | string,\n options: { banned?: string[]; max?: number; extra?: string[] } = {},\n): string[] {\n const limit = options.max ?? HASHTAG_LIMITS[platform] ?? 10;\n const banned = new Set([...DEFAULT_BANNED, ...(options.banned ?? []).map((b) => b.toLowerCase().replace(/^#/, \"\"))]);\n const seen = new Set<string>();\n const out: string[] = [];\n for (const raw of [...terms, ...(options.extra ?? [])]) {\n const tag = toHashtag(raw);\n const bare = tag.slice(1).toLowerCase();\n if (!tag || bare.length < 2 || banned.has(bare) || seen.has(bare)) continue;\n seen.add(bare);\n out.push(tag);\n if (out.length >= limit) break;\n }\n return out;\n}\n\nexport interface TrendsOptions {\n geo?: string;\n /** Sources to query. Default: google + wikipedia (youtube added iff youtubeApiKey). */\n sources?: TrendSource[];\n youtubeApiKey?: string;\n wikipediaProject?: string;\n /** Attach + filter by relevance to this story. */\n relevanceTo?: string;\n minRelevance?: number;\n fetch?: FetchLike;\n signal?: AbortSignal;\n}\n\nexport interface TrendsResult {\n trends: Trend[];\n sources: TrendSource[];\n warnings: string[];\n}\n\n/** Fetch, merge and score trends from the configured official/public sources. */\nexport async function trends(options: TrendsOptions = {}): Promise<TrendsResult> {\n const want = options.sources ?? ([\"google\", \"wikipedia\", ...(options.youtubeApiKey ? [\"youtube\"] as const : [])] as TrendSource[]);\n const warnings: string[] = [];\n const used: TrendSource[] = [];\n const raws: RawTrend[] = [];\n const run = async (s: TrendSource, fn: () => Promise<RawTrend[]>) => {\n try {\n const r = await fn();\n raws.push(...r);\n used.push(s);\n } catch (e) {\n warnings.push(`${s}: ${(e as Error).message}`);\n }\n };\n await Promise.all(\n want.map((s) => {\n if (s === \"google\") return run(\"google\", () => googleTrendsDaily({ geo: options.geo, fetch: options.fetch, signal: options.signal }));\n if (s === \"wikipedia\") return run(\"wikipedia\", () => wikipediaTop({ project: options.wikipediaProject, fetch: options.fetch, signal: options.signal }));\n if (s === \"youtube\" && options.youtubeApiKey) return run(\"youtube\", () => youtubeMostPopular({ apiKey: options.youtubeApiKey!, regionCode: options.geo, fetch: options.fetch, signal: options.signal }));\n return Promise.resolve();\n }),\n );\n let merged = mergeTrends(raws);\n if (options.relevanceTo) merged = relevanceTo(merged, options.relevanceTo, { min: options.minRelevance });\n return { trends: merged, sources: used, warnings };\n}\n\n/** Machine-readable descriptor for an AI \"conductor\". */\nexport function describe() {\n return {\n name: \"@lacspace/trends\",\n version: VERSION,\n summary: \"Trending topics for a region from official/public sources only (Google Trends RSS, YouTube mostPopular, Wikipedia pageviews), merged + scored, with story-relevance gating and per-platform hashtag sets.\",\n sources: [\"google\", \"youtube\", \"wikipedia\"],\n commands: [\n { name: \"trends\", input: { type: \"object\", properties: { geo: { type: \"string\" }, sources: { type: \"array\" }, youtubeApiKey: { type: \"string\" }, relevanceTo: { type: \"string\" }, minRelevance: { type: \"number\" } } }, output: \"{ trends:[{term,score,sources,volume,links,relevance}], sources, warnings }\" },\n { name: \"relevanceTo\", input: { type: \"object\", properties: { trends: { type: \"array\" }, story: { type: \"string\" }, min: { type: \"number\" } }, required: [\"trends\", \"story\"] }, output: \"Trend[] (with relevance)\" },\n { name: \"hashtagsFor\", input: { type: \"object\", properties: { terms: { type: \"array\" }, platform: { type: \"string\" }, max: { type: \"integer\" }, banned: { type: \"array\" } }, required: [\"terms\", \"platform\"] }, output: \"string[]\" },\n ],\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n"]}
@@ -0,0 +1,182 @@
1
+ type TrendSource = "google" | "youtube" | "wikipedia";
2
+ interface RawTrend {
3
+ term: string;
4
+ source: TrendSource;
5
+ /** 0–1 weight within this source (1 = top). */
6
+ weight: number;
7
+ /** Approx search traffic / views, when the source gives it. */
8
+ volume?: number;
9
+ /** Supporting links (news items / article). */
10
+ links?: string[];
11
+ }
12
+ type FetchLike = (url: string, init?: {
13
+ headers?: Record<string, string>;
14
+ signal?: AbortSignal;
15
+ }) => Promise<{
16
+ ok: boolean;
17
+ status: number;
18
+ text(): Promise<string>;
19
+ json(): Promise<any>;
20
+ }>;
21
+ /** Fetch + parse the Google Trends "daily trending searches" RSS for a region. */
22
+ declare function googleTrendsDaily(options?: {
23
+ geo?: string;
24
+ fetch?: FetchLike;
25
+ signal?: AbortSignal;
26
+ }): Promise<RawTrend[]>;
27
+ /** Parse Google Trends daily RSS (title + ht:approx_traffic + ht:news_item links). */
28
+ declare function parseGoogleTrendsRss(xml: string): RawTrend[];
29
+ declare function youtubeMostPopular(options: {
30
+ apiKey: string;
31
+ regionCode?: string;
32
+ maxResults?: number;
33
+ fetch?: FetchLike;
34
+ signal?: AbortSignal;
35
+ }): Promise<RawTrend[]>;
36
+ declare function wikipediaTop(options?: {
37
+ project?: string;
38
+ date?: Date;
39
+ fetch?: FetchLike;
40
+ signal?: AbortSignal;
41
+ }): Promise<RawTrend[]>;
42
+
43
+ interface Trend {
44
+ /** Display term (original casing/script of the strongest source). */
45
+ term: string;
46
+ /** Merged score, higher = more trending (multi-source terms rank up). */
47
+ score: number;
48
+ sources: TrendSource[];
49
+ volume?: number;
50
+ links: string[];
51
+ /** 0–1 relevance to a story, set by relevanceTo / trends({ relevanceTo }). */
52
+ relevance?: number;
53
+ }
54
+ /** Merge raw trends from several sources into one scored, de-duplicated list. */
55
+ declare function mergeTrends(raws: RawTrend[]): Trend[];
56
+ /** Score each trend's relevance to a story (0–1) and attach it; optionally filter. */
57
+ declare function relevanceTo(trends: Trend[], story: string, options?: {
58
+ min?: number;
59
+ }): Trend[];
60
+ declare const HASHTAG_LIMITS: Record<string, number>;
61
+ /** Build a platform-appropriate hashtag set from trend terms (+ optional extra tags). */
62
+ declare function hashtagsFor(terms: string[], platform: keyof typeof HASHTAG_LIMITS | string, options?: {
63
+ banned?: string[];
64
+ max?: number;
65
+ extra?: string[];
66
+ }): string[];
67
+ interface TrendsOptions {
68
+ geo?: string;
69
+ /** Sources to query. Default: google + wikipedia (youtube added iff youtubeApiKey). */
70
+ sources?: TrendSource[];
71
+ youtubeApiKey?: string;
72
+ wikipediaProject?: string;
73
+ /** Attach + filter by relevance to this story. */
74
+ relevanceTo?: string;
75
+ minRelevance?: number;
76
+ fetch?: FetchLike;
77
+ signal?: AbortSignal;
78
+ }
79
+ interface TrendsResult {
80
+ trends: Trend[];
81
+ sources: TrendSource[];
82
+ warnings: string[];
83
+ }
84
+ /** Fetch, merge and score trends from the configured official/public sources. */
85
+ declare function trends(options?: TrendsOptions): Promise<TrendsResult>;
86
+ /** Machine-readable descriptor for an AI "conductor". */
87
+ declare function describe(): {
88
+ name: string;
89
+ version: string;
90
+ summary: string;
91
+ sources: string[];
92
+ commands: ({
93
+ name: string;
94
+ input: {
95
+ type: string;
96
+ properties: {
97
+ geo: {
98
+ type: string;
99
+ };
100
+ sources: {
101
+ type: string;
102
+ };
103
+ youtubeApiKey: {
104
+ type: string;
105
+ };
106
+ relevanceTo: {
107
+ type: string;
108
+ };
109
+ minRelevance: {
110
+ type: string;
111
+ };
112
+ trends?: undefined;
113
+ story?: undefined;
114
+ min?: undefined;
115
+ terms?: undefined;
116
+ platform?: undefined;
117
+ max?: undefined;
118
+ banned?: undefined;
119
+ };
120
+ required?: undefined;
121
+ };
122
+ output: string;
123
+ } | {
124
+ name: string;
125
+ input: {
126
+ type: string;
127
+ properties: {
128
+ trends: {
129
+ type: string;
130
+ };
131
+ story: {
132
+ type: string;
133
+ };
134
+ min: {
135
+ type: string;
136
+ };
137
+ geo?: undefined;
138
+ sources?: undefined;
139
+ youtubeApiKey?: undefined;
140
+ relevanceTo?: undefined;
141
+ minRelevance?: undefined;
142
+ terms?: undefined;
143
+ platform?: undefined;
144
+ max?: undefined;
145
+ banned?: undefined;
146
+ };
147
+ required: string[];
148
+ };
149
+ output: string;
150
+ } | {
151
+ name: string;
152
+ input: {
153
+ type: string;
154
+ properties: {
155
+ terms: {
156
+ type: string;
157
+ };
158
+ platform: {
159
+ type: string;
160
+ };
161
+ max: {
162
+ type: string;
163
+ };
164
+ banned: {
165
+ type: string;
166
+ };
167
+ geo?: undefined;
168
+ sources?: undefined;
169
+ youtubeApiKey?: undefined;
170
+ relevanceTo?: undefined;
171
+ minRelevance?: undefined;
172
+ trends?: undefined;
173
+ story?: undefined;
174
+ min?: undefined;
175
+ };
176
+ required: string[];
177
+ };
178
+ output: string;
179
+ })[];
180
+ };
181
+
182
+ export { type FetchLike, HASHTAG_LIMITS, type RawTrend, type Trend, type TrendSource, type TrendsOptions, type TrendsResult, describe, googleTrendsDaily, hashtagsFor, mergeTrends, parseGoogleTrendsRss, relevanceTo, trends, wikipediaTop, youtubeMostPopular };
@@ -0,0 +1,182 @@
1
+ type TrendSource = "google" | "youtube" | "wikipedia";
2
+ interface RawTrend {
3
+ term: string;
4
+ source: TrendSource;
5
+ /** 0–1 weight within this source (1 = top). */
6
+ weight: number;
7
+ /** Approx search traffic / views, when the source gives it. */
8
+ volume?: number;
9
+ /** Supporting links (news items / article). */
10
+ links?: string[];
11
+ }
12
+ type FetchLike = (url: string, init?: {
13
+ headers?: Record<string, string>;
14
+ signal?: AbortSignal;
15
+ }) => Promise<{
16
+ ok: boolean;
17
+ status: number;
18
+ text(): Promise<string>;
19
+ json(): Promise<any>;
20
+ }>;
21
+ /** Fetch + parse the Google Trends "daily trending searches" RSS for a region. */
22
+ declare function googleTrendsDaily(options?: {
23
+ geo?: string;
24
+ fetch?: FetchLike;
25
+ signal?: AbortSignal;
26
+ }): Promise<RawTrend[]>;
27
+ /** Parse Google Trends daily RSS (title + ht:approx_traffic + ht:news_item links). */
28
+ declare function parseGoogleTrendsRss(xml: string): RawTrend[];
29
+ declare function youtubeMostPopular(options: {
30
+ apiKey: string;
31
+ regionCode?: string;
32
+ maxResults?: number;
33
+ fetch?: FetchLike;
34
+ signal?: AbortSignal;
35
+ }): Promise<RawTrend[]>;
36
+ declare function wikipediaTop(options?: {
37
+ project?: string;
38
+ date?: Date;
39
+ fetch?: FetchLike;
40
+ signal?: AbortSignal;
41
+ }): Promise<RawTrend[]>;
42
+
43
+ interface Trend {
44
+ /** Display term (original casing/script of the strongest source). */
45
+ term: string;
46
+ /** Merged score, higher = more trending (multi-source terms rank up). */
47
+ score: number;
48
+ sources: TrendSource[];
49
+ volume?: number;
50
+ links: string[];
51
+ /** 0–1 relevance to a story, set by relevanceTo / trends({ relevanceTo }). */
52
+ relevance?: number;
53
+ }
54
+ /** Merge raw trends from several sources into one scored, de-duplicated list. */
55
+ declare function mergeTrends(raws: RawTrend[]): Trend[];
56
+ /** Score each trend's relevance to a story (0–1) and attach it; optionally filter. */
57
+ declare function relevanceTo(trends: Trend[], story: string, options?: {
58
+ min?: number;
59
+ }): Trend[];
60
+ declare const HASHTAG_LIMITS: Record<string, number>;
61
+ /** Build a platform-appropriate hashtag set from trend terms (+ optional extra tags). */
62
+ declare function hashtagsFor(terms: string[], platform: keyof typeof HASHTAG_LIMITS | string, options?: {
63
+ banned?: string[];
64
+ max?: number;
65
+ extra?: string[];
66
+ }): string[];
67
+ interface TrendsOptions {
68
+ geo?: string;
69
+ /** Sources to query. Default: google + wikipedia (youtube added iff youtubeApiKey). */
70
+ sources?: TrendSource[];
71
+ youtubeApiKey?: string;
72
+ wikipediaProject?: string;
73
+ /** Attach + filter by relevance to this story. */
74
+ relevanceTo?: string;
75
+ minRelevance?: number;
76
+ fetch?: FetchLike;
77
+ signal?: AbortSignal;
78
+ }
79
+ interface TrendsResult {
80
+ trends: Trend[];
81
+ sources: TrendSource[];
82
+ warnings: string[];
83
+ }
84
+ /** Fetch, merge and score trends from the configured official/public sources. */
85
+ declare function trends(options?: TrendsOptions): Promise<TrendsResult>;
86
+ /** Machine-readable descriptor for an AI "conductor". */
87
+ declare function describe(): {
88
+ name: string;
89
+ version: string;
90
+ summary: string;
91
+ sources: string[];
92
+ commands: ({
93
+ name: string;
94
+ input: {
95
+ type: string;
96
+ properties: {
97
+ geo: {
98
+ type: string;
99
+ };
100
+ sources: {
101
+ type: string;
102
+ };
103
+ youtubeApiKey: {
104
+ type: string;
105
+ };
106
+ relevanceTo: {
107
+ type: string;
108
+ };
109
+ minRelevance: {
110
+ type: string;
111
+ };
112
+ trends?: undefined;
113
+ story?: undefined;
114
+ min?: undefined;
115
+ terms?: undefined;
116
+ platform?: undefined;
117
+ max?: undefined;
118
+ banned?: undefined;
119
+ };
120
+ required?: undefined;
121
+ };
122
+ output: string;
123
+ } | {
124
+ name: string;
125
+ input: {
126
+ type: string;
127
+ properties: {
128
+ trends: {
129
+ type: string;
130
+ };
131
+ story: {
132
+ type: string;
133
+ };
134
+ min: {
135
+ type: string;
136
+ };
137
+ geo?: undefined;
138
+ sources?: undefined;
139
+ youtubeApiKey?: undefined;
140
+ relevanceTo?: undefined;
141
+ minRelevance?: undefined;
142
+ terms?: undefined;
143
+ platform?: undefined;
144
+ max?: undefined;
145
+ banned?: undefined;
146
+ };
147
+ required: string[];
148
+ };
149
+ output: string;
150
+ } | {
151
+ name: string;
152
+ input: {
153
+ type: string;
154
+ properties: {
155
+ terms: {
156
+ type: string;
157
+ };
158
+ platform: {
159
+ type: string;
160
+ };
161
+ max: {
162
+ type: string;
163
+ };
164
+ banned: {
165
+ type: string;
166
+ };
167
+ geo?: undefined;
168
+ sources?: undefined;
169
+ youtubeApiKey?: undefined;
170
+ relevanceTo?: undefined;
171
+ minRelevance?: undefined;
172
+ trends?: undefined;
173
+ story?: undefined;
174
+ min?: undefined;
175
+ };
176
+ required: string[];
177
+ };
178
+ output: string;
179
+ })[];
180
+ };
181
+
182
+ export { type FetchLike, HASHTAG_LIMITS, type RawTrend, type Trend, type TrendSource, type TrendsOptions, type TrendsResult, describe, googleTrendsDaily, hashtagsFor, mergeTrends, parseGoogleTrendsRss, relevanceTo, trends, wikipediaTop, youtubeMostPopular };
package/dist/index.js ADDED
@@ -0,0 +1,219 @@
1
+ import { keyphrase } from '@lacspace/keyphrase';
2
+ import { detectScript, transliterate } from '@lacspace/translit';
3
+
4
+ // src/index.ts
5
+
6
+ // src/sources.ts
7
+ function fetchImpl(f) {
8
+ const impl = f ?? globalThis.fetch;
9
+ if (!impl) throw new Error("trends: no fetch available \u2014 pass options.fetch");
10
+ return impl;
11
+ }
12
+ function rankWeights(n) {
13
+ return Array.from({ length: n }, (_, i) => n <= 1 ? 1 : 1 - 0.9 * i / (n - 1));
14
+ }
15
+ function parseTraffic(s) {
16
+ if (!s) return void 0;
17
+ const m = s.replace(/,/g, "").match(/(\d+)\s*(\+)?/);
18
+ return m ? Number(m[1]) : void 0;
19
+ }
20
+ async function googleTrendsDaily(options = {}) {
21
+ const geo = options.geo ?? "NP";
22
+ const url = `https://trends.google.com/trends/trendingsearches/daily/rss?geo=${encodeURIComponent(geo)}`;
23
+ const res = await fetchImpl(options.fetch)(url, { signal: options.signal });
24
+ if (!res.ok) throw new Error(`google-trends ${res.status}`);
25
+ return parseGoogleTrendsRss(await res.text());
26
+ }
27
+ function parseGoogleTrendsRss(xml) {
28
+ const items = [...xml.matchAll(/<item>([\s\S]*?)<\/item>/g)].map((m) => m[1]);
29
+ const out = [];
30
+ for (const item of items) {
31
+ const title = decode(first(item, /<title>([\s\S]*?)<\/title>/));
32
+ if (!title) continue;
33
+ const volume = parseTraffic(first(item, /<ht:approx_traffic>([\s\S]*?)<\/ht:approx_traffic>/));
34
+ const links = [...item.matchAll(/<ht:news_item_url>([\s\S]*?)<\/ht:news_item_url>/g)].map((m) => decode(m[1]));
35
+ out.push({ term: title, volume, links });
36
+ }
37
+ const w = rankWeights(out.length);
38
+ return out.map((o, i) => ({ term: o.term, source: "google", weight: w[i], volume: o.volume, links: o.links }));
39
+ }
40
+ async function youtubeMostPopular(options) {
41
+ if (!options.apiKey) return [];
42
+ const u = new URL("https://www.googleapis.com/youtube/v3/videos");
43
+ u.searchParams.set("part", "snippet,statistics");
44
+ u.searchParams.set("chart", "mostPopular");
45
+ u.searchParams.set("regionCode", options.regionCode ?? "NP");
46
+ u.searchParams.set("maxResults", String(options.maxResults ?? 25));
47
+ u.searchParams.set("key", options.apiKey);
48
+ const res = await fetchImpl(options.fetch)(u.toString(), { signal: options.signal });
49
+ if (!res.ok) throw new Error(`youtube ${res.status}`);
50
+ const data = await res.json();
51
+ const rows = data.items ?? [];
52
+ const w = rankWeights(rows.length);
53
+ return rows.map((it, i) => ({
54
+ term: it.snippet?.title ?? "",
55
+ source: "youtube",
56
+ weight: w[i],
57
+ volume: it.statistics?.viewCount ? Number(it.statistics.viewCount) : void 0,
58
+ links: it.id ? [`https://youtu.be/${it.id}`] : []
59
+ })).filter((t) => t.term);
60
+ }
61
+ async function wikipediaTop(options = {}) {
62
+ const project = options.project ?? "en.wikipedia";
63
+ const d = options.date ?? new Date(Date.now() - 864e5);
64
+ const yyyy = d.getUTCFullYear();
65
+ const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
66
+ const dd = String(d.getUTCDate()).padStart(2, "0");
67
+ const url = `https://wikimedia.org/api/rest_v1/metrics/pageviews/top/${project}/all-access/${yyyy}/${mm}/${dd}`;
68
+ const res = await fetchImpl(options.fetch)(url, { headers: { "User-Agent": "lacspace-trends/1.0 (contact@lacspace.com)" }, signal: options.signal });
69
+ if (!res.ok) throw new Error(`wikipedia ${res.status}`);
70
+ const data = await res.json();
71
+ const articles = data.items?.[0]?.articles ?? [];
72
+ const filtered = articles.filter((a) => !/^(Main_Page|Special:|Wikipedia:|विशेष:|मुख्य_पृष्ठ)/.test(a.article));
73
+ const w = rankWeights(filtered.length);
74
+ return filtered.map((a, i) => ({
75
+ term: String(a.article).replace(/_/g, " "),
76
+ source: "wikipedia",
77
+ weight: w[i],
78
+ volume: a.views,
79
+ links: [`https://${project}.org/wiki/${encodeURIComponent(a.article)}`]
80
+ }));
81
+ }
82
+ function first(s, re) {
83
+ const m = s.match(re);
84
+ return m ? m[1].trim() : void 0;
85
+ }
86
+ function decode(s) {
87
+ return (s ?? "").replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, "$1").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").trim();
88
+ }
89
+
90
+ // src/index.ts
91
+ var VERSION = "1.0.0";
92
+ function keyOf(term) {
93
+ const roman = detectScript(term) === "ne" ? transliterate(term, { from: "ne", to: "en" }) : term;
94
+ return roman.toLowerCase().replace(/[^a-z0-9]+/g, "");
95
+ }
96
+ function tokens(s) {
97
+ const roman = detectScript(s) === "ne" ? transliterate(s, { from: "ne", to: "en" }) : s;
98
+ return (roman.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((w) => w.length > 2);
99
+ }
100
+ function mergeTrends(raws) {
101
+ const groups = /* @__PURE__ */ new Map();
102
+ for (const r of raws) {
103
+ const k = keyOf(r.term);
104
+ if (!k) continue;
105
+ let g = groups.get(k);
106
+ if (!g) {
107
+ g = { best: r, score: 0, sources: /* @__PURE__ */ new Set(), links: /* @__PURE__ */ new Set(), volume: 0 };
108
+ groups.set(k, g);
109
+ }
110
+ g.score += r.weight;
111
+ g.sources.add(r.source);
112
+ for (const l of r.links ?? []) g.links.add(l);
113
+ if (r.volume) g.volume = Math.max(g.volume, r.volume);
114
+ if (r.weight > g.best.weight) g.best = r;
115
+ }
116
+ const out = [];
117
+ for (const g of groups.values()) {
118
+ const boost = 1 + 0.5 * (g.sources.size - 1);
119
+ out.push({
120
+ term: g.best.term,
121
+ score: round(g.score * boost),
122
+ sources: [...g.sources],
123
+ volume: g.volume || void 0,
124
+ links: [...g.links].slice(0, 5)
125
+ });
126
+ }
127
+ return out.sort((a, b) => b.score - a.score);
128
+ }
129
+ function relevanceTo(trends2, story, options = {}) {
130
+ const kp = keyphrase(story, { max: 20 });
131
+ const storyTokens = /* @__PURE__ */ new Set();
132
+ for (const p of kp.phrases ?? []) for (const t of tokens(p.phrase)) storyTokens.add(t);
133
+ for (const t of tokens(story)) storyTokens.add(t);
134
+ const scored = trends2.map((t) => {
135
+ const tt = tokens(t.term);
136
+ if (!tt.length || storyTokens.size === 0) return { ...t, relevance: 0 };
137
+ const hits = tt.filter((w) => storyTokens.has(w)).length;
138
+ const rel = round(hits / tt.length);
139
+ return { ...t, relevance: rel };
140
+ });
141
+ return options.min !== void 0 ? scored.filter((t) => (t.relevance ?? 0) >= options.min) : scored;
142
+ }
143
+ var HASHTAG_LIMITS = {
144
+ instagram: 30,
145
+ tiktok: 6,
146
+ x: 3,
147
+ facebook: 3,
148
+ youtube: 15,
149
+ threads: 5,
150
+ linkedin: 5,
151
+ telegram: 10
152
+ };
153
+ var DEFAULT_BANNED = /* @__PURE__ */ new Set(["followme", "follow4follow", "like4like", "nepaltiktok", "viral", "fyp"]);
154
+ function toHashtag(term) {
155
+ const roman = detectScript(term) === "ne" ? transliterate(term, { from: "ne", to: "en" }) : term;
156
+ const camel = roman.replace(/[^a-zA-Z0-9]+/g, " ").trim().split(/\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
157
+ return camel ? "#" + camel : "";
158
+ }
159
+ function hashtagsFor(terms, platform, options = {}) {
160
+ const limit = options.max ?? HASHTAG_LIMITS[platform] ?? 10;
161
+ const banned = /* @__PURE__ */ new Set([...DEFAULT_BANNED, ...(options.banned ?? []).map((b) => b.toLowerCase().replace(/^#/, ""))]);
162
+ const seen = /* @__PURE__ */ new Set();
163
+ const out = [];
164
+ for (const raw of [...terms, ...options.extra ?? []]) {
165
+ const tag = toHashtag(raw);
166
+ const bare = tag.slice(1).toLowerCase();
167
+ if (!tag || bare.length < 2 || banned.has(bare) || seen.has(bare)) continue;
168
+ seen.add(bare);
169
+ out.push(tag);
170
+ if (out.length >= limit) break;
171
+ }
172
+ return out;
173
+ }
174
+ async function trends(options = {}) {
175
+ const want = options.sources ?? ["google", "wikipedia", ...options.youtubeApiKey ? ["youtube"] : []];
176
+ const warnings = [];
177
+ const used = [];
178
+ const raws = [];
179
+ const run = async (s, fn) => {
180
+ try {
181
+ const r = await fn();
182
+ raws.push(...r);
183
+ used.push(s);
184
+ } catch (e) {
185
+ warnings.push(`${s}: ${e.message}`);
186
+ }
187
+ };
188
+ await Promise.all(
189
+ want.map((s) => {
190
+ if (s === "google") return run("google", () => googleTrendsDaily({ geo: options.geo, fetch: options.fetch, signal: options.signal }));
191
+ if (s === "wikipedia") return run("wikipedia", () => wikipediaTop({ project: options.wikipediaProject, fetch: options.fetch, signal: options.signal }));
192
+ if (s === "youtube" && options.youtubeApiKey) return run("youtube", () => youtubeMostPopular({ apiKey: options.youtubeApiKey, regionCode: options.geo, fetch: options.fetch, signal: options.signal }));
193
+ return Promise.resolve();
194
+ })
195
+ );
196
+ let merged = mergeTrends(raws);
197
+ if (options.relevanceTo) merged = relevanceTo(merged, options.relevanceTo, { min: options.minRelevance });
198
+ return { trends: merged, sources: used, warnings };
199
+ }
200
+ function describe() {
201
+ return {
202
+ name: "@lacspace/trends",
203
+ version: VERSION,
204
+ summary: "Trending topics for a region from official/public sources only (Google Trends RSS, YouTube mostPopular, Wikipedia pageviews), merged + scored, with story-relevance gating and per-platform hashtag sets.",
205
+ sources: ["google", "youtube", "wikipedia"],
206
+ commands: [
207
+ { name: "trends", input: { type: "object", properties: { geo: { type: "string" }, sources: { type: "array" }, youtubeApiKey: { type: "string" }, relevanceTo: { type: "string" }, minRelevance: { type: "number" } } }, output: "{ trends:[{term,score,sources,volume,links,relevance}], sources, warnings }" },
208
+ { name: "relevanceTo", input: { type: "object", properties: { trends: { type: "array" }, story: { type: "string" }, min: { type: "number" } }, required: ["trends", "story"] }, output: "Trend[] (with relevance)" },
209
+ { name: "hashtagsFor", input: { type: "object", properties: { terms: { type: "array" }, platform: { type: "string" }, max: { type: "integer" }, banned: { type: "array" } }, required: ["terms", "platform"] }, output: "string[]" }
210
+ ]
211
+ };
212
+ }
213
+ function round(n) {
214
+ return Math.round(n * 1e3) / 1e3;
215
+ }
216
+
217
+ export { HASHTAG_LIMITS, describe, googleTrendsDaily, hashtagsFor, mergeTrends, parseGoogleTrendsRss, relevanceTo, trends, wikipediaTop, youtubeMostPopular };
218
+ //# sourceMappingURL=index.js.map
219
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sources.ts","../src/index.ts"],"names":["trends"],"mappings":"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","file":"index.js","sourcesContent":["export type TrendSource = \"google\" | \"youtube\" | \"wikipedia\";\n\nexport interface RawTrend {\n term: string;\n source: TrendSource;\n /** 0–1 weight within this source (1 = top). */\n weight: number;\n /** Approx search traffic / views, when the source gives it. */\n volume?: number;\n /** Supporting links (news items / article). */\n links?: string[];\n}\n\nexport type FetchLike = (url: string, init?: { headers?: Record<string, string>; signal?: AbortSignal }) => Promise<{ ok: boolean; status: number; text(): Promise<string>; json(): Promise<any> }>;\n\nfunction fetchImpl(f?: FetchLike): FetchLike {\n const impl = f ?? (globalThis as any).fetch;\n if (!impl) throw new Error(\"trends: no fetch available — pass options.fetch\");\n return impl;\n}\n\nfunction rankWeights(n: number): number[] {\n // Linear decay from 1 (rank 1) to ~0.1 (rank n).\n return Array.from({ length: n }, (_, i) => (n <= 1 ? 1 : 1 - (0.9 * i) / (n - 1)));\n}\n\nfunction parseTraffic(s: string | undefined): number | undefined {\n if (!s) return undefined;\n const m = s.replace(/,/g, \"\").match(/(\\d+)\\s*(\\+)?/);\n return m ? Number(m[1]) : undefined;\n}\n\n// --- Google Trends daily RSS (geo=NP by default) -----------------------------------\n/** Fetch + parse the Google Trends \"daily trending searches\" RSS for a region. */\nexport async function googleTrendsDaily(options: { geo?: string; fetch?: FetchLike; signal?: AbortSignal } = {}): Promise<RawTrend[]> {\n const geo = options.geo ?? \"NP\";\n const url = `https://trends.google.com/trends/trendingsearches/daily/rss?geo=${encodeURIComponent(geo)}`;\n const res = await fetchImpl(options.fetch)(url, { signal: options.signal });\n if (!res.ok) throw new Error(`google-trends ${res.status}`);\n return parseGoogleTrendsRss(await res.text());\n}\n\n/** Parse Google Trends daily RSS (title + ht:approx_traffic + ht:news_item links). */\nexport function parseGoogleTrendsRss(xml: string): RawTrend[] {\n const items = [...xml.matchAll(/<item>([\\s\\S]*?)<\\/item>/g)].map((m) => m[1]!);\n const out: { term: string; volume?: number; links: string[] }[] = [];\n for (const item of items) {\n const title = decode(first(item, /<title>([\\s\\S]*?)<\\/title>/));\n if (!title) continue;\n const volume = parseTraffic(first(item, /<ht:approx_traffic>([\\s\\S]*?)<\\/ht:approx_traffic>/));\n const links = [...item.matchAll(/<ht:news_item_url>([\\s\\S]*?)<\\/ht:news_item_url>/g)].map((m) => decode(m[1]!));\n out.push({ term: title, volume, links });\n }\n const w = rankWeights(out.length);\n return out.map((o, i) => ({ term: o.term, source: \"google\", weight: w[i]!, volume: o.volume, links: o.links }));\n}\n\n// --- YouTube Data API v3: most popular in a region (needs an API key) --------------\nexport async function youtubeMostPopular(options: { apiKey: string; regionCode?: string; maxResults?: number; fetch?: FetchLike; signal?: AbortSignal }): Promise<RawTrend[]> {\n if (!options.apiKey) return [];\n const u = new URL(\"https://www.googleapis.com/youtube/v3/videos\");\n u.searchParams.set(\"part\", \"snippet,statistics\");\n u.searchParams.set(\"chart\", \"mostPopular\");\n u.searchParams.set(\"regionCode\", options.regionCode ?? \"NP\");\n u.searchParams.set(\"maxResults\", String(options.maxResults ?? 25));\n u.searchParams.set(\"key\", options.apiKey);\n const res = await fetchImpl(options.fetch)(u.toString(), { signal: options.signal });\n if (!res.ok) throw new Error(`youtube ${res.status}`);\n const data = await res.json();\n const rows = (data.items ?? []) as any[];\n const w = rankWeights(rows.length);\n return rows.map((it, i) => ({\n term: it.snippet?.title ?? \"\",\n source: \"youtube\" as const,\n weight: w[i]!,\n volume: it.statistics?.viewCount ? Number(it.statistics.viewCount) : undefined,\n links: it.id ? [`https://youtu.be/${it.id}`] : [],\n })).filter((t) => t.term);\n}\n\n// --- Wikipedia pageviews top (free REST API, no key) -------------------------------\nexport async function wikipediaTop(options: { project?: string; date?: Date; fetch?: FetchLike; signal?: AbortSignal } = {}): Promise<RawTrend[]> {\n const project = options.project ?? \"en.wikipedia\";\n const d = options.date ?? new Date(Date.now() - 86400000); // yesterday (today may be incomplete)\n const yyyy = d.getUTCFullYear();\n const mm = String(d.getUTCMonth() + 1).padStart(2, \"0\");\n const dd = String(d.getUTCDate()).padStart(2, \"0\");\n const url = `https://wikimedia.org/api/rest_v1/metrics/pageviews/top/${project}/all-access/${yyyy}/${mm}/${dd}`;\n const res = await fetchImpl(options.fetch)(url, { headers: { \"User-Agent\": \"lacspace-trends/1.0 (contact@lacspace.com)\" }, signal: options.signal });\n if (!res.ok) throw new Error(`wikipedia ${res.status}`);\n const data = await res.json();\n const articles = (data.items?.[0]?.articles ?? []) as any[];\n const filtered = articles.filter((a) => !/^(Main_Page|Special:|Wikipedia:|विशेष:|मुख्य_पृष्ठ)/.test(a.article));\n const w = rankWeights(filtered.length);\n return filtered.map((a, i) => ({\n term: String(a.article).replace(/_/g, \" \"),\n source: \"wikipedia\" as const,\n weight: w[i]!,\n volume: a.views,\n links: [`https://${project}.org/wiki/${encodeURIComponent(a.article)}`],\n }));\n}\n\nfunction first(s: string, re: RegExp): string | undefined {\n const m = s.match(re);\n return m ? m[1]!.trim() : undefined;\n}\nfunction decode(s: string | undefined): string {\n return (s ?? \"\")\n .replace(/<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>/g, \"$1\")\n .replace(/&amp;/g, \"&\").replace(/&lt;/g, \"<\").replace(/&gt;/g, \">\").replace(/&quot;/g, '\"').replace(/&#39;/g, \"'\")\n .trim();\n}\n","import { keyphrase } from \"@lacspace/keyphrase\";\nimport { transliterate, detectScript } from \"@lacspace/translit\";\nimport { FetchLike, googleTrendsDaily, RawTrend, TrendSource, wikipediaTop, youtubeMostPopular } from \"./sources.js\";\n\nexport * from \"./sources.js\";\n\nconst VERSION = \"1.0.0\";\n\nexport interface Trend {\n /** Display term (original casing/script of the strongest source). */\n term: string;\n /** Merged score, higher = more trending (multi-source terms rank up). */\n score: number;\n sources: TrendSource[];\n volume?: number;\n links: string[];\n /** 0–1 relevance to a story, set by relevanceTo / trends({ relevanceTo }). */\n relevance?: number;\n}\n\n/** A cross-script, lowercased key so \"Dashain\" and \"दशैं\" merge. */\nfunction keyOf(term: string): string {\n const roman = detectScript(term) === \"ne\" ? transliterate(term, { from: \"ne\", to: \"en\" }) : term;\n return roman.toLowerCase().replace(/[^a-z0-9]+/g, \"\");\n}\n\nfunction tokens(s: string): string[] {\n const roman = detectScript(s) === \"ne\" ? transliterate(s, { from: \"ne\", to: \"en\" }) : s;\n return (roman.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((w) => w.length > 2);\n}\n\n/** Merge raw trends from several sources into one scored, de-duplicated list. */\nexport function mergeTrends(raws: RawTrend[]): Trend[] {\n const groups = new Map<string, { best: RawTrend; score: number; sources: Set<TrendSource>; links: Set<string>; volume: number }>();\n for (const r of raws) {\n const k = keyOf(r.term);\n if (!k) continue;\n let g = groups.get(k);\n if (!g) {\n g = { best: r, score: 0, sources: new Set(), links: new Set(), volume: 0 };\n groups.set(k, g);\n }\n g.score += r.weight;\n g.sources.add(r.source);\n for (const l of r.links ?? []) g.links.add(l);\n if (r.volume) g.volume = Math.max(g.volume, r.volume);\n if (r.weight > g.best.weight) g.best = r; // strongest source sets the display term\n }\n const out: Trend[] = [];\n for (const g of groups.values()) {\n // Multi-source agreement is a strong signal → multiply by a diversity boost.\n const boost = 1 + 0.5 * (g.sources.size - 1);\n out.push({\n term: g.best.term,\n score: round(g.score * boost),\n sources: [...g.sources],\n volume: g.volume || undefined,\n links: [...g.links].slice(0, 5),\n });\n }\n return out.sort((a, b) => b.score - a.score);\n}\n\n/** Score each trend's relevance to a story (0–1) and attach it; optionally filter. */\nexport function relevanceTo(trends: Trend[], story: string, options: { min?: number } = {}): Trend[] {\n const kp = keyphrase(story, { max: 20 } as any);\n const storyTokens = new Set<string>();\n for (const p of kp.phrases ?? []) for (const t of tokens(p.phrase)) storyTokens.add(t);\n for (const t of tokens(story)) storyTokens.add(t);\n const scored = trends.map((t) => {\n const tt = tokens(t.term);\n if (!tt.length || storyTokens.size === 0) return { ...t, relevance: 0 };\n const hits = tt.filter((w) => storyTokens.has(w)).length;\n const rel = round(hits / tt.length);\n return { ...t, relevance: rel };\n });\n return options.min !== undefined ? scored.filter((t) => (t.relevance ?? 0) >= options.min!) : scored;\n}\n\n// --- hashtags ----------------------------------------------------------------------\nexport const HASHTAG_LIMITS: Record<string, number> = {\n instagram: 30, tiktok: 6, x: 3, facebook: 3, youtube: 15, threads: 5, linkedin: 5, telegram: 10,\n};\n// A conservative starter list of broken/shadow-ban-prone tags; callers extend.\nconst DEFAULT_BANNED = new Set([\"followme\", \"follow4follow\", \"like4like\", \"nepaltiktok\", \"viral\", \"fyp\"]);\n\nfunction toHashtag(term: string): string {\n const roman = detectScript(term) === \"ne\" ? transliterate(term, { from: \"ne\", to: \"en\" }) : term;\n const camel = roman.replace(/[^a-zA-Z0-9]+/g, \" \").trim().split(/\\s+/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(\"\");\n return camel ? \"#\" + camel : \"\";\n}\n\n/** Build a platform-appropriate hashtag set from trend terms (+ optional extra tags). */\nexport function hashtagsFor(\n terms: string[],\n platform: keyof typeof HASHTAG_LIMITS | string,\n options: { banned?: string[]; max?: number; extra?: string[] } = {},\n): string[] {\n const limit = options.max ?? HASHTAG_LIMITS[platform] ?? 10;\n const banned = new Set([...DEFAULT_BANNED, ...(options.banned ?? []).map((b) => b.toLowerCase().replace(/^#/, \"\"))]);\n const seen = new Set<string>();\n const out: string[] = [];\n for (const raw of [...terms, ...(options.extra ?? [])]) {\n const tag = toHashtag(raw);\n const bare = tag.slice(1).toLowerCase();\n if (!tag || bare.length < 2 || banned.has(bare) || seen.has(bare)) continue;\n seen.add(bare);\n out.push(tag);\n if (out.length >= limit) break;\n }\n return out;\n}\n\nexport interface TrendsOptions {\n geo?: string;\n /** Sources to query. Default: google + wikipedia (youtube added iff youtubeApiKey). */\n sources?: TrendSource[];\n youtubeApiKey?: string;\n wikipediaProject?: string;\n /** Attach + filter by relevance to this story. */\n relevanceTo?: string;\n minRelevance?: number;\n fetch?: FetchLike;\n signal?: AbortSignal;\n}\n\nexport interface TrendsResult {\n trends: Trend[];\n sources: TrendSource[];\n warnings: string[];\n}\n\n/** Fetch, merge and score trends from the configured official/public sources. */\nexport async function trends(options: TrendsOptions = {}): Promise<TrendsResult> {\n const want = options.sources ?? ([\"google\", \"wikipedia\", ...(options.youtubeApiKey ? [\"youtube\"] as const : [])] as TrendSource[]);\n const warnings: string[] = [];\n const used: TrendSource[] = [];\n const raws: RawTrend[] = [];\n const run = async (s: TrendSource, fn: () => Promise<RawTrend[]>) => {\n try {\n const r = await fn();\n raws.push(...r);\n used.push(s);\n } catch (e) {\n warnings.push(`${s}: ${(e as Error).message}`);\n }\n };\n await Promise.all(\n want.map((s) => {\n if (s === \"google\") return run(\"google\", () => googleTrendsDaily({ geo: options.geo, fetch: options.fetch, signal: options.signal }));\n if (s === \"wikipedia\") return run(\"wikipedia\", () => wikipediaTop({ project: options.wikipediaProject, fetch: options.fetch, signal: options.signal }));\n if (s === \"youtube\" && options.youtubeApiKey) return run(\"youtube\", () => youtubeMostPopular({ apiKey: options.youtubeApiKey!, regionCode: options.geo, fetch: options.fetch, signal: options.signal }));\n return Promise.resolve();\n }),\n );\n let merged = mergeTrends(raws);\n if (options.relevanceTo) merged = relevanceTo(merged, options.relevanceTo, { min: options.minRelevance });\n return { trends: merged, sources: used, warnings };\n}\n\n/** Machine-readable descriptor for an AI \"conductor\". */\nexport function describe() {\n return {\n name: \"@lacspace/trends\",\n version: VERSION,\n summary: \"Trending topics for a region from official/public sources only (Google Trends RSS, YouTube mostPopular, Wikipedia pageviews), merged + scored, with story-relevance gating and per-platform hashtag sets.\",\n sources: [\"google\", \"youtube\", \"wikipedia\"],\n commands: [\n { name: \"trends\", input: { type: \"object\", properties: { geo: { type: \"string\" }, sources: { type: \"array\" }, youtubeApiKey: { type: \"string\" }, relevanceTo: { type: \"string\" }, minRelevance: { type: \"number\" } } }, output: \"{ trends:[{term,score,sources,volume,links,relevance}], sources, warnings }\" },\n { name: \"relevanceTo\", input: { type: \"object\", properties: { trends: { type: \"array\" }, story: { type: \"string\" }, min: { type: \"number\" } }, required: [\"trends\", \"story\"] }, output: \"Trend[] (with relevance)\" },\n { name: \"hashtagsFor\", input: { type: \"object\", properties: { terms: { type: \"array\" }, platform: { type: \"string\" }, max: { type: \"integer\" }, banned: { type: \"array\" } }, required: [\"terms\", \"platform\"] }, output: \"string[]\" },\n ],\n };\n}\n\nfunction round(n: number): number {\n return Math.round(n * 1000) / 1000;\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@lacspace/trends",
3
+ "version": "1.0.0",
4
+ "description": "Trending topics and hashtags for a region from official/public sources only — Google Trends daily RSS (geo), YouTube Data API mostPopular (optional key), and Wikipedia pageviews — merged across scripts and scored (multi-source terms rank up), with story-relevance gating so you never trend-jack unrelated topics, and per-platform hashtag sets with limits + a banned/shadow-ban filter. Bring-your-own-fetch, deterministic given inputs. Reuses @lacspace/keyphrase and @lacspace/translit.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
12
+ "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
13
+ }
14
+ },
15
+ "files": ["dist"],
16
+ "sideEffects": false,
17
+ "scripts": { "build": "tsup", "prepublishOnly": "npm run build" },
18
+ "keywords": [
19
+ "trends", "trending", "hashtags", "google-trends", "youtube", "wikipedia-pageviews",
20
+ "nepal", "social-media", "newsroom", "typescript"
21
+ ],
22
+ "dependencies": {
23
+ "@lacspace/keyphrase": "^1.0.0",
24
+ "@lacspace/translit": "^1.0.4"
25
+ },
26
+ "author": "Lacspace <contact@lacspace.com>",
27
+ "license": "SEE LICENSE IN LICENSE",
28
+ "homepage": "https://developer.lacspace.com/packages/trends",
29
+ "repository": { "type": "git", "url": "git+https://github.com/lacspace/npm-packages.git", "directory": "trends" },
30
+ "bugs": { "url": "https://github.com/lacspace/npm-packages/issues" },
31
+ "engines": { "node": ">=18" },
32
+ "publishConfig": { "access": "public" }
33
+ }