Docs/Skills and code/src/

src/series-analysis/signals.js

Code101 lines5.7 KBsrc/

import { normalizePublishedAt } from "../analysis/temporal.js";

const DEVANAGARI = /[ऀ-ॿ]/;
const HINGLISH_HINTS = /\b(kaise|kyun|kyu|kya|hui|hua|hai|barbaad|doob|dooba|sach|kahani|isne|inki|iski|mein)\b/i;
const STOP_CAPS = new Set(["why", "how", "the", "what", "when", "who", "this", "that", "from", "with", "and", "for", "case", "study", "story", "explained", "full", "complete", "documentary", "india", "indian", "business", "hindi", "english", "part", "episode", "ft", "vs", "of", "in", "to", "is", "a", "an", "it", "its", "i", "my", "we", "you", "your", "brutal", "real", "truth", "reality", "painful", "shocking", "biggest", "worst", "top", "new", "no", "not", "rise", "fall", "collapse", "downfall", "death", "end", "went", "wrong", "tragic", "shocking", "largest", "giant", "killed", "died", "failed", "destroyed", "collapsed", "betrayal", "inside", "behind", "secret", "dark", "untold", "forgotten", "hindi", "case", "study"]);
const GENERIC_NAME_WORDS = new Set(["airlines", "airways", "group", "india", "motors", "bank", "payments", "future", "coffee", "first", "limited", "ltd", "company", "corp"]);

const STRUCTURE_PATTERNS = [
  [/\b(why|how)\b.{2,60}\b(died|dead|failed|collapsed|crashed|shut|destroyed|killed|ruined|sank|vanished|disappeared)\b/i, "why-how-X-died"],
  [/\b(fall|downfall|collapse|death|end)\s+of\b/i, "the-fall-of-X"],
  [/\brise\s*(and|&|n)\s*fall\b/i, "rise-and-fall"],
  [/\bwhat\s+(really\s+)?happened\s+to\b/i, "what-happened-to-X"],
  [/\b(case study|what went wrong|biggest mistake)\b/i, "case-study"],
  [/\b(scam|fraud|ponzi)\b/i, "scam-story"],
  [/\b(kaise|kyun|kyu)\b.{0,40}\b(barbaad|doob|dooba|band|khatam|fail)\b/i, "hinglish-why-died"]
];

// Views on these come from the guest / IP / news cycle, not from packaging a new channel can reuse.
const NON_REPLICABLE = [
  [/\b(podcast|interview|ft\.?|feat\.?|conversation with|in conversation|live session|q&a|ama)\b/i, "podcast/interview"],
  [/\b(trailer|teaser|web series|full movie|scene|clip|song|official video|episode \d+|ep\.? ?\d+)\b/i, "fiction/clip"],
  [/\b(shorts?)\b/i, "shorts"],
  [/\b(reaction|react|roast|meme)\b/i, "reaction/meme"],
  [/\b(breaking news|live updates|budget \d{4})\b/i, "news"]
];

export const norm = value => String(value ?? "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[’]/g, "'").replace(/\s+/g, " ").trim();
export const clamp01 = x => Math.max(0, Math.min(1, x));

export function detectLanguage(text) {
  const t = String(text ?? "");
  if (DEVANAGARI.test(t)) return "hi";
  if (HINGLISH_HINTS.test(t)) return "hinglish";
  return "en";
}

/** Failure-vocabulary hits in a title. Short Latin terms match on word boundaries, the rest as substrings. */
export function failureHits(title, failureTerms) {
  const t = norm(title);
  return failureTerms.filter(term => {
    const x = norm(term);
    return /^[a-z']+$/.test(x) && x.length <= 5 ? new RegExp(`\\b${x}\\b`).test(t) : t.includes(x);
  });
}

export const structureMatches = title => STRUCTURE_PATTERNS.filter(([re]) => re.test(title)).map(([, name]) => name);
export const nonReplicableFlags = title => NON_REPLICABLE.filter(([re]) => re.test(title)).map(([, name]) => name);

const escapeRe = s => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

/**
 * Best guess at which company a title is about, so the final list isn't ten videos on the same one.
 * Known companies match on their leading brand word ("Kingfisher Airlines" and "Kingfisher" collapse to one subject);
 * otherwise the first run of capitalised non-generic words.
 */
export function guessSubject(title, knownCompanies = []) {
  const t = norm(title);
  for (const name of knownCompanies.map(norm).filter(Boolean)) {
    // First distinctive word of the name: "Big Bazaar Future Group" -> bazaar, "Byju's" -> byju. Whole-word match, so "big" can't hit "biggest".
    const head = name.split(/\s+/).map(w => w.replace(/'s$/, "")).find(w => w.length >= 4 && !GENERIC_NAME_WORDS.has(w));
    if (head && new RegExp(`\\b${escapeRe(head)}\\b`).test(t)) return { key: head, source: "known" };
  }
  const tokens = String(title ?? "").replace(/[|:–—\-?!,()[\]"]/g, " ").split(/\s+/).filter(Boolean);
  let best = [], run = [];
  for (const tok of tokens) {
    const clean = tok.replace(/[^A-Za-z0-9&'.]/g, "");
    // Possessives ("World's Largest...") are almost always generic nouns, not the company the video is about.
    if (/^[A-Z][A-Za-z0-9&'.]*$/.test(clean) && !/['’]s$/i.test(tok) && !STOP_CAPS.has(clean.toLowerCase())) {
      run.push(clean);
      if (run.length > best.length) best = [...run];
    } else run = [];
  }
  return best.length ? { key: norm(best[0]).replace(/'s$/, ""), source: "capitalised" } : { key: null, source: "none" };
}

export function ageDaysFrom(publishedAtIso, now = Date.now()) {
  const ms = Date.parse(publishedAtIso ?? "");
  return Number.isFinite(ms) ? Math.max(0, (now - ms) / 86400000) : null;
}

/** Search and feed results only carry relative text ("4 yr ago"); anchor it to when we saw it. */
export function publishedFromText(text, anchorIso) {
  const r = normalizePublishedAt(text, anchorIso);
  return { iso: r.normalized, precision: r.precision };
}

export function median(values) {
  const v = values.filter(Number.isFinite).sort((a, b) => a - b);
  if (!v.length) return null;
  const m = Math.floor(v.length / 2);
  return v.length % 2 ? v[m] : (v[m - 1] + v[m]) / 2;
}

/** Share of the sorted sample below x (ties count half). Rank-based, so a 30M-view outlier doesn't flatten everyone else. */
export function percentileRank(sorted, x) {
  if (!sorted.length || !Number.isFinite(x)) return null;
  let lo = 0, eq = 0;
  for (const v of sorted) { if (v < x) lo++; else if (v === x) eq++; }
  return (lo + eq / 2) / sorted.length;
}

Shown as it is in the repository at build time (2026-11-03); the home directory appears as <home>. If this page and the file differ, the file is right. Back to the catalogue.