cron_server / app.js
everydaytok's picture
Upload 2 files
cccae01 verified
Raw History Blame Contribute Delete
3.35 kB
// app.js — the entire HuggingFace instance. One job: embeddings.
// Runs a sentence-embedding model IN this Space (transformers.js), so the
// "heavy task" — loading and running the model — happens on HF's compute,
// not on the phone/Termux box running the local server. Everything else
// (text generation) is a lightweight SDK call the local server makes
// directly to HF Inference API / Gemini — it never touches this file.
import express from 'express';
import cors from 'cors';
import { pipeline } from '@xenova/transformers';
const app = express();
app.use(cors());
app.use(express.json({ limit: '2mb' }));
const PORT = process.env.PORT || 7860; // 7860 is the HF Spaces convention
const MODEL_ID = process.env.EMBED_MODEL_ID || 'Xenova/all-MiniLM-L6-v2';
// Loaded once, lazily, on first request — then reused. Avoids paying the
// model-load cost on every /embed call, and avoids paying it at all if the
// Space spins up but nothing calls it yet.
let extractorPromise = null;
function getExtractor() {
if (!extractorPromise) {
console.log(`Loading embedding model: ${MODEL_ID}`);
extractorPromise = pipeline('feature-extraction', MODEL_ID);
}
return extractorPromise;
}
function meanPoolAndNormalize(output) {
// transformers.js feature-extraction with pooling:'mean', normalize:true
// already returns a flat, normalized vector — this just guards against
// shape surprises across model families.
const data = output.data ? Array.from(output.data) : output;
return Array.isArray(data[0]) ? data.flat() : data;
}
app.get('/', (_req, res) => {
res.json({ ok: true, service: 'morning-briefing-embeddings', model: MODEL_ID });
});
app.get('/health', async (_req, res) => {
res.json({ ok: true, modelLoaded: extractorPromise !== null });
});
// POST /embed { text: string } -> { vector: number[], model: string }
app.post('/embed', async (req, res) => {
try {
const { text } = req.body;
if (!text || typeof text !== 'string') {
return res.status(400).json({ error: 'Body must include a "text" string' });
}
const extractor = await getExtractor();
const output = await extractor(text, { pooling: 'mean', normalize: true });
const vector = meanPoolAndNormalize(output);
res.json({ vector, model: MODEL_ID });
} catch (e) {
console.error(e);
res.status(500).json({ error: e.message });
}
});
// POST /embed/batch { texts: string[] } -> { vectors: number[][], model: string }
// Same model instance, run sequentially — transformers.js on CPU doesn't
// gain much from manual batching here, and this keeps the file small.
app.post('/embed/batch', async (req, res) => {
try {
const { texts } = req.body;
if (!Array.isArray(texts) || texts.some(t => typeof t !== 'string')) {
return res.status(400).json({ error: 'Body must include a "texts" string array' });
}
const extractor = await getExtractor();
const vectors = [];
for (const text of texts) {
const output = await extractor(text, { pooling: 'mean', normalize: true });
vectors.push(meanPoolAndNormalize(output));
}
res.json({ vectors, model: MODEL_ID });
} catch (e) {
console.error(e);
res.status(500).json({ error: e.message });
}
});
app.listen(PORT, () => {
console.log(`Embeddings server listening on port ${PORT}`);
});