尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
Local Vite + React app that reliably extracts structured logic models from PDF/DOCX/PPTX via Gemini for downstream processing, supports human editing, and exports CSV / branded PDFs.
1. npm install
2. Set GEMINI_API_KEY in .env.local
3. npm run dev — Express API (:3011) + Vite (:3000)
Builds the client, then serves API + dist from Express on :3011 (override with PORT).檢視完整 README ↗Local Vite + React app that reliably extracts structured logic models from PDF/DOCX/PPTX via Gemini for downstream processing, supports human editing, and exports CSV / branded PDFs.
npm installGEMINI_API_KEY in .env.localnpm run dev — Express API (:3011) + Vite (:3000)npm start
Builds the client, then serves API + dist from Express on :3011 (override with PORT).
npm run dev — API + Vite clientnpm run build — client onlynpm run typecheck — tsc --noEmitnpm run replay — replay post-processing over saved model answers, offline and free (-- --score to also score it)npm run benchmark:accuracy — extract the synthetic benchmark documents and score them (-- --capture first time)npm run split:survey — every blessed snapshot cell that the run-on splitter would divide, and into whatnpm start — build + production serverBoth Gemini calls read their model from server/geminiModelConfig.ts. The default is the rolling
alias gemini-flash-latest, because a dated snapshot gets sunset for new API keys —
gemini-2.5-flash already returns 404 "no longer available to new users" on a freshly created key.
The price of a rolling alias is that Google can change what answers with no change here, and on
2026-10-07 it did: the alias moved to gemini-3.8-flash and extraction accuracy moved with it. So
two things are true of this codebase and should stay true.
The model is configuration, not code. Override either call without touching a file:
LM_EXTRACT_MODEL=gemini-3.7-flash npm run benchmark:accuracy # score a candidate
LM_EXTRACT_MODEL=gemini-3.7-flash npm run regression:check # the real set, on his machine
LM_DETECT_MODEL=gemini-3.8-flash npm run dev # page-group detection only
LM_EXTRACT_THINKING_LEVEL is there for the same reason, as an experiment knob with no shipped
effect: unset (and =default) sends no thinkingConfig at all, which is the measured default —
thinkingLevel: low scored the invented benchmark at 100% and cost two real documents accuracy
(PR #36, reverted by #37). An unrecognised value refuses to start rather than quietly running the
default and filing its numbers under the candidate's name.
A blank value means unset. CANDIDATE_MODEL_IDS in that module is the short list worth scoring
when the served model changes — a list, deliberately, because picking one model and tuning to it is
the same bet in different clothes, due again at the next rotation. Membership means "worth
measuring", never "known good".
Every result records which model produced it. served_model_id in the extraction-log CSV, the
servedModelId field on regression snapshots and in benchmark run records, and the LM_DUMP_RAW
dumps all carry response.modelVersion — what answered, as distinct from the alias we asked for.
Absent means unknown and never "the same as the alias". npm run regression:check says out loud
when a baseline was captured on a different served model than the one answering, because a diff
spanning a model change attributes nothing.
What none of this does is make extraction model-independent. The prompt is tuned against observed
model behaviour and extraction quality is a property of the model, so a model change always needs
measuring on fixtures/regression-set/ as well as the benchmark. What it removes is the cost of
measuring it.
npm run benchmark:accuracy -- --input=text-only # send no page images, text track only
npm run regression:check -- --input=text-only # same, on the real set (never blesses)
text-only asks what the page rasters buy; see scripts/inputArm.ts for what it cannot see. The
prompt variant on every row reads text-only, so its results cannot pool with the default.
Post-processing never moves an item between domains. It may fix shape (colons, inline labels,
run-on cells, exact restated duplicates) and fill an empty field from labelled source text, and it
reports anything it would once have moved as a review blocker. See normalizeExtractedLogicModel.
The same API is exposed two ways so one codebase serves both targets:
server.ts) on :3011api/ (/api/health, /api/gemini/extract,
/api/gemini/detect-logic-models, /api/convert/pptx-to-pdf)The Gemini extract and detect routes delegate to shared handlers in server/apiCore.ts, so their
request/response behavior stays identical between the two paths. /api/health and the PPTX convert
route diverge: /api/health shares only getApiKey() — server.ts's handler additionally reports
libreOfficeWasm and a 'production'|'development' mode, while api/health.ts omits
libreOfficeWasm entirely and reports mode from VERCEL_ENV || NODE_ENV || 'unknown'. The PPTX
convert route shares server/pptxConvertApi.ts's handlePptxToPdfRequest, but the two entry points
parse the request body differently (Express uses express.raw; the Vercel function hands the
handler whatever Vercel itself already parsed) — found via codebase audit
(docs/specs/codebase-audit-2026-09-19.md #18).
Setup:
GEMINI_API_KEY (Production + Preview).https<!-- -->://<your-app>/api/health returns {"ok":true,"configured":true,...}.PowerPoint needs two things on a hosted deployment, and both are easy to lose:
getLibreOfficeWasmPath() builds the path at runtime — so a bundler that ships a
function by tracing its imports leaves them out. vercel.json names them with includeFiles,
and gives that one function 2048MB: LibreOffice peaks at ~1.07GB converting a one-line document,
which is already over the 1024MB the route used to run with. 2048MB is the ceiling on a Hobby
account — a deployment that asks for more is rejected outright at build time — so a deck heavy
enough to need more than that has no headroom left to take.application/octet-stream, not the
OOXML presentation type: a serverless host parses the body by content type and hands the
function undefined for anything outside its short list. Sent honestly, the deck's bytes never
arrived at all and the route answered 400 before LibreOffice was reached. The function also
reads the request stream itself as a backstop.GET /api/convert/pptx-to-pdf answers whether that deployment can convert PowerPoint:
{"ok":true,"libreOfficeWasm":true}. It lives on the convert route rather than in /api/health
because each function is bundled separately, so only that function can answer for its own files.
Hosted limits to be aware of:
/api/gemini/extract).config/brand.tsdocs/specs/.cursor/rules/ — see AGENTS.md