Spaces:
Sleeping
Sleeping
Download app.js from everydaytok/cron_server: direct link, hf CLI and curl.
- Browser
- Download file 3.35 kB
-
https://huggingface.co/spaces/everydaytok/cron_server/resolve/main/app.js
- Command line
-
hf download hf://spaces/everydaytok/cron_server/app.js
-
curl -L -o app.js https://huggingface.co/spaces/everydaytok/cron_server/resolve/main/app.js
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}`); | |
| }); | |