SPB Git
4.8 KB · 58 lines typescript
1import { detectWpNumber } from "@/lib/papers/identifiers";2/** Heuristic metadata proposal from title-page text. Every field is a suggestion for human review, never a fact. */3export type Proposal = { title: string | null; authors: string[]; abstract: string | null; keywords: string[]; jel: string[]; wp: { seriesCode: string; number: number } | null; version: string | null; dateText: string | null; confidence: Record<string, number> };4const STOP = /^(abstract|résumé|keywords?|jel|working paper|université|university|department|département|version|draft|preliminary|email|e-mail|tel|http)/i;5export function proposeMetadata(firstPages: string, pdfMetaTitle?: string | null, pdfMetaAuthor?: string | null): Proposal {6  const text = firstPages.replace(/\r/g, "");7  const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);8  const confidence: Record<string, number> = {};9  // Title: longest run of up to 3 consecutive Title-Case-ish lines near the top, before "Abstract", not a stop line.10  let title: string | null = null;11  const abstractIdx = lines.findIndex((l) => /^abstract\b/i.test(l));12  const head = lines.slice(0, abstractIdx > 0 ? abstractIdx : 40);13  const cands: string[] = [];14  for (let i = 0; i < head.length; i++) {15    const l = head[i]; if (STOP.test(l) || l.length < 12 || /@|\d{4}-\d{2}/.test(l)) continue;16    if (/^[A-Z]/.test(l) && !/[.;]$/.test(l) && !/^(by|and)\b/i.test(l)) {17      let acc = l; let j = i + 1;18      while (j < head.length && head[j].length > 8 && !STOP.test(head[j]) && !/@/.test(head[j]) && !/^(by|and)\b/i.test(head[j]) && acc.length < 300 && !/^[A-Z][a-z]+ [A-Z]/.test(head[j]) /* author-like */) { acc += " " + head[j]; j++; if (/[?:.]$/.test(head[j - 1])) break; }19      cands.push(acc);20    }21  }22  cands.sort((a, b) => b.length - a.length);23  title = cands[0] ?? null;24  if (pdfMetaTitle && pdfMetaTitle.length > 12 && !/^(untitled|main|paper|subject)/i.test(pdfMetaTitle)) { if (!title || pdfMetaTitle.length > 20) { title = title && title.length > pdfMetaTitle.length + 20 ? title : pdfMetaTitle; } }25  confidence.title = title ? (pdfMetaTitle && title === pdfMetaTitle ? 0.8 : 0.5) : 0;26  // Authors: lines with 2–4 capitalised words (allowing accents/hyphens), before Abstract, excluding title lines.27  const authors: string[] = [];28  const nameRe = /^(?:[A-ZÀ-Ý][a-zà-ÿ'’-]+(?:\s|-)){1,3}[A-ZÀ-Ý][a-zà-ÿ'’-]+(?:\s*[,*†‡§¶0-9]+)?$/;29  for (const l of head) { const cleaned = l.replace(/[*†‡§¶0-9,]+$/g, "").trim(); if (nameRe.test(cleaned) && !(title ?? "").includes(cleaned) && !/university|université|department|école|school/i.test(cleaned)) authors.push(cleaned); }30  if (!authors.length && pdfMetaAuthor) authors.push(...pdfMetaAuthor.split(/\s*(?:,|;|&|\band\b)\s*/).filter((a) => a.length > 3));31  confidence.authors = authors.length ? 0.5 : 0;32  // Abstract: text after "Abstract" until Keywords/JEL/1 Introduction.33  let abstract: string | null = null;34  if (abstractIdx >= 0) {35    const rest = lines.slice(abstractIdx + 1); const end = rest.findIndex((l) => /^(keywords?|jel|1\.?\s+introduction|introduction$|mots-clés)/i.test(l));36    abstract = rest.slice(0, end > 0 ? end : 40).join(" ").replace(/^abstract[:.]?\s*/i, "").replace(/\s+/g, " ").trim() || null;37    if (abstract && abstract.length < 80) abstract = null;38  }39  confidence.abstract = abstract ? 0.7 : 0;40  const kwLine = lines.find((l) => /^(keywords?|mots-clés)\s*[:.]/i.test(l));41  const keywords = kwLine ? kwLine.replace(/^(keywords?|mots-clés)\s*[:.]\s*/i, "").split(/[;,·•]/).map((k) => k.trim().replace(/\.$/, "")).filter((k) => k.length > 1 && k.length < 60) : [];42  const jelLine = lines.find((l) => /^jel\b/i.test(l));43  const jel = jelLine ? (jelLine.match(/\b[A-Z]\d{1,2}\b/g) ?? []) : [];44  const wp = detectWpNumber(text.slice(0, 4000));45  const vm = /\bversion\s*v?(\d+(?:\.\d+)?)/i.exec(text.slice(0, 6000)); const version = vm ? vm[1] : null;46  const dm = /\b(January|February|March|April|May|June|July|August|September|October|November|December|janvier|février|mars|avril|mai|juin|juillet|août|septembre|octobre|novembre|décembre)\s+(\d{4})\b/i.exec(text.slice(0, 6000));47  const dateText = dm ? `${dm[1]} ${dm[2]}` : null;48  confidence.wp = wp ? 0.9 : 0; confidence.date = dateText ? 0.6 : 0;49  return { title, authors, abstract, keywords, jel, wp, version, dateText, confidence };50}51/** Rough duplicate scoring between a candidate and an existing paper. */52export function titleSimilarity(a: string, b: string): number {53  const norm = (s: string) => s.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9 ]+/g, " ").split(/\s+/).filter((w) => w.length > 2);54  const A = new Set(norm(a)), B = new Set(norm(b)); if (!A.size || !B.size) return 0;55  let inter = 0; for (const w of A) if (B.has(w)) inter++;56  return inter / Math.max(A.size, B.size);57}58
Files · 734affbc3f56470ca2e43d159e14b200279bfbfc
Menu

Theme