Files
Momento/memento-note/lib/ai/services/slide-source-assets.ts
Antigravity 556a0b2f3f feat(credits): solde IA unique (option M), packs Stripe et polish billing
Un pot de crédits global (BASIC 100 / PRO 1 000 / BUSINESS 4 000) remplace
les seaux par fonction côté débit. Packs one-shot S/M/L, admin sans seaux
legacy, usage multi-features, hints de coût, anti-flash thème et hydratation
admin. Inclut aussi le pipeline slides renforcé et la page publique polishée.
2026-07-16 20:43:18 +00:00

104 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Server-side extraction of usable assets from a note BEFORE any slide LLM call.
* Inspired by PPTAgent (retrieve relevant content per slide) and DeckForge (data_needs).
* Critical for STEM notes: formulas must be harvested, not hoped for from the model.
*/
export interface SourceAssets {
formulas: string[]
numbers: Array<{ label: string; value: number; raw: string }>
keySentences: string[]
hasMath: boolean
hasNumbers: boolean
wordCount: number
}
/** Extract LaTeX / equation-like fragments from note plain text or HTML. */
export function extractFormulas(raw: string): string[] {
if (!raw) return []
const found: string[] = []
const push = (s: string) => {
const t = s.replace(/\s+/g, ' ').trim()
if (t.length >= 2 && t.length <= 280 && !found.includes(t)) found.push(t)
}
// $$ ... $$
for (const m of raw.matchAll(/\$\$([\s\S]+?)\$\$/g)) push(m[1] || '')
// \[ ... \]
for (const m of raw.matchAll(/\\\[([\s\S]+?)\\\]/g)) push(m[1] || '')
// \( ... \)
for (const m of raw.matchAll(/\\\(([\s\S]+?)\\\)/g)) push(m[1] || '')
// $ ... $ (single line)
for (const m of raw.matchAll(/(?<![\\$])\$([^$\n]{2,120})\$(?!\$)/g)) push(m[1] || '')
// Common latex commands in free text
for (const m of raw.matchAll(
/(?:^|[\s(])((?:\\frac\{[^}]+\}\{[^}]+\}|\\partial|\\nabla|\\sum|\\int|\\frac|\\cdot|\\times|\\left|\\right|y'|y''|d[xy]\/d[xy]|∂)[^\n]{0,100})/g,
)) {
push(m[1] || '')
}
// Equation-like: f(x) = ..., dy/dx = ...
for (const m of raw.matchAll(
/(?:^|\n)\s*([A-Za-zΑ-ω∂∇][A-Za-z0-9_'"]*\s*(?:\([^)]*\))?\s*[=≈≡]\s*[^\n]{3,100})/g,
)) {
const line = (m[1] || '').trim()
if (/[=≈]/.test(line) && /[a-zA-Z0-9]/.test(line)) push(line)
}
return found.slice(0, 24)
}
export function extractNumbers(raw: string): Array<{ label: string; value: number; raw: string }> {
if (!raw) return []
const out: Array<{ label: string; value: number; raw: string }> = []
const re =
/(?:^|[^\d])((?:≈|~)?\s*-?\d+(?:[.,]\d+)?)\s*(%|€|\$|k|m|bn|ms|s|°C)?\b/gi
let m: RegExpExecArray | null
while ((m = re.exec(raw)) !== null && out.length < 20) {
const numStr = (m[1] || '').replace(/[≈~\s]/g, '').replace(',', '.')
const value = parseFloat(numStr)
if (!Number.isFinite(value)) continue
const unit = m[2] || ''
// crude label: 40 chars before
const start = Math.max(0, m.index - 40)
const ctx = raw.slice(start, m.index).replace(/\s+/g, ' ').trim()
const label = ctx.split(/[.;:!?\n]/).pop()?.trim().slice(-32) || `n${out.length + 1}`
out.push({ label, value: unit === '%' ? value : value, raw: `${numStr}${unit}` })
}
return out
}
export function extractKeySentences(raw: string, max = 16): string[] {
if (!raw) return []
const plain = raw
.replace(/<[^>]+>/g, ' ')
.replace(/\$\$[\s\S]+?\$\$/g, ' ')
.replace(/\\\[[\s\S]+?\\\]/g, ' ')
.replace(/\s+/g, ' ')
.trim()
const parts = plain
.split(/(?<=[.!?])\s+/)
.map((s) => s.trim())
.filter((s) => s.length >= 40 && s.length <= 220)
const out: string[] = []
for (const p of parts) {
if (out.length >= max) break
if (!out.some((x) => x.slice(0, 40) === p.slice(0, 40))) out.push(p)
}
return out
}
export function extractSourceAssets(raw: string): SourceAssets {
const formulas = extractFormulas(raw)
const numbers = extractNumbers(raw)
const keySentences = extractKeySentences(raw)
const wordCount = raw.replace(/<[^>]+>/g, ' ').split(/\s+/).filter(Boolean).length
return {
formulas,
numbers,
keySentences,
hasMath: formulas.length > 0 || /équat|equat|différen|differen|dériv|deriv|intégr|integr|EDO|ODE|PDE|latex/i.test(raw),
hasNumbers: numbers.length >= 2,
wordCount,
}
}