diff --git a/.babelrc.js b/.babelrc.js deleted file mode 100644 index 7e47ea9..0000000 --- a/.babelrc.js +++ /dev/null @@ -1,5 +0,0 @@ -// .babelrc.js -module.exports = { - presets: [['next/babel']], - plugins: [['import', { libraryName: 'antd', style: true }]], -}; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bffb357..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index e00464a..4cea5d5 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -2,14 +2,14 @@ name: Docker Image CI on: push: - branches: [ "main" ] + branches: ["main"] pull_request: - branches: [ "main" ] + branches: ["main"] jobs: build: runs-on: ["self-hosted", "docker"] steps: - - uses: actions/checkout@v3 - - name: Build the Docker image - run: docker build . --file Dockerfile --tag emo:latest + - uses: actions/checkout@v4 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag emo:latest diff --git a/.gitignore b/.gitignore index 1437c53..5fa39f2 100644 --- a/.gitignore +++ b/.gitignore @@ -11,10 +11,14 @@ # next.js /.next/ /out/ - -# production /build +# turbopack +.turbo + +# typescript +*.tsbuildinfo + # misc .DS_Store *.pem @@ -32,3 +36,6 @@ yarn-error.log* # vercel .vercel + +# local: patch bundle for the feedsbrain/face-api.js fork (not part of the app) +/.fork-update diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..ec9df37 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +@feedsbrain:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} diff --git a/.tool-versions b/.tool-versions index 509957d..f5f4dda 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,2 +1 @@ -nodejs 14.17.3 -python 3.9.5 +nodejs 24.20.0 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/Dockerfile b/Dockerfile index d1291f1..8c4428b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,34 +1,35 @@ -# build environment -FROM node:14.17.3-buster as build +# syntax=docker/dockerfile:1 + +# ---------- build stage ---------- +FROM node:24-bookworm-slim AS build WORKDIR /app ARG APP_VERSION=latest +ENV NEXT_TELEMETRY_DISABLED=1 -ENV PATH /app/node_modules/.bin:$PATH - -COPY package.json ./ -COPY package-lock.json ./ +# `face-api.js` is installed from a git URL, so git must be present. +RUN apt-get update && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* -RUN npm install +COPY package.json package-lock.json ./ +RUN npm ci COPY . ./ -RUN sed -i 's/development/'$APP_VERSION'/' /app/public/version.json +RUN sed -i 's/development/'"$APP_VERSION"'/' /app/public/version.json RUN npm run build -# production environment -FROM node:14.17.3-buster-slim +# ---------- production stage ---------- +FROM node:24-bookworm-slim AS runner WORKDIR /app -ENV PATH /app/node_modules/.bin:$PATH -ENV NODE_ENV production - -COPY package.json ./ -COPY package-lock.json ./ - -RUN npm install --production +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 -COPY ./public ./public +COPY --from=build /app/package.json /app/package-lock.json ./ +COPY --from=build /app/node_modules ./node_modules COPY --from=build /app/.next ./.next +COPY --from=build /app/public ./public +COPY --from=build /app/next.config.mjs ./next.config.mjs EXPOSE 80 -CMD npm run start:prod +CMD ["npm", "run", "start:prod"] diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..8ab7528 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,220 @@ +# EMO — Migration Record (Node 14 / Next 11 → Node 24 / Next 16) + +What was actually changed on **2026-09-08** to modernize the app, in the order it +was done. Reproduce top-to-bottom on a fresh branch; commit per section so +regressions bisect cleanly. + +Target versions were "latest as of the migration date": Next **16.3.4**, React +**19.2**, antd **6.6.3**, Node **24.20.0**, TypeScript **5.9.3**, ESLint **9**. + +## 0. Prep + +- `git checkout -b chore/modernize` +- `.tool-versions` → `nodejs 24.20.0` (dropped the `python 3.9.5` line — only the + old native `canvas`/`tfjs-node` toolchain needed it; the browser build doesn't). +- Delete `node_modules` and `package-lock.json` — the old tree is unresolvable. + +## 1. `face-api.js` → the `feedsbrain` fork (git-installable) + +The fork is **not published to npm** and does **not commit its build output**, so +`npm i github:feedsbrain/face-api.js` would install a package whose +`main`/`module` point at a missing `build/`. Two-part fix: + +### 1a. Fork side — one commit, `7cc8a41` "chore: make package git-installable" + +In a clone of `feedsbrain/face-api.js` (branched off `master` @ `b14726b`): + +- `.gitignore`: remove the `build` line. +- `package.json`: add + ```json + "types": "./build/commonjs/index.d.ts", + "exports": { + ".": { + "types": "./build/commonjs/index.d.ts", + "import": "./build/es6/index.js", + "require": "./build/commonjs/index.js" + }, + "./package.json": "./package.json" + }, + "files": ["build", "dist", "README.md", "LICENSE"] + ``` +- `npm ci && npm run build` (Node 24) — produces `build/commonjs` + `build/es6` + (`tsc` + `tsc-es6`); commit `build/` (~4.8 MB) next to the already-committed + `dist/` UMD bundle. +- **No `prepare` script** — with `build/` committed, npm uses the checkout + as-is; a `prepare` would force every consumer to reinstall the fork's + devDeps (`canvas`, `@tensorflow/tfjs-node` native addons) on `npm i`. + +This commit is delivered as a bundle + patch in `./.fork-update/` +(`PUSH-INSTRUCTIONS.md`). **It must land on the fork's `master`** or `npm ci` +here fails on this one dependency. Prefer the bundle: it fast-forwards `master` +and keeps SHA `7cc8a41`, which the emo `package-lock.json` pins. + +### 1b. App side + +- `package.json` dependency: `"face-api.js": "github:feedsbrain/face-api.js#master"`. + `package-lock.json` resolves it to + `git+https://github.com/feedsbrain/face-api.js.git#7cc8a41…` with **no** + `hasInstallScript` (confirms the committed `build/` is used directly). +- `@tensorflow/tfjs` is **not** added — the fork depends on + `@tensorflow/tfjs-core` + `-backend-cpu` + `-backend-webgl` itself and its ES + build imports the backends so they self-register. +- **`src/lib/face.ts` is unchanged.** The fork preserves the 0.22.2 surface: + `loadSsdMobilenetv1Model` / `loadTinyFaceDetectorModel` / `loadFaceLandmarkModel` + / … and `new faceapi.SsdMobilenetv1Options()` all still work, no + `tf.setBackend()` call needed. +- `public/static/models/*` unchanged — same weight format. + +## 2. React 17 → 19 + +- `react@19` / `react-dom@19`; `-D @types/react@19 @types/react-dom@19 @types/node@24`. +- `GlobalFooter.tsx`: drop `React.FC<{}>` and the `React` import → plain arrow + component. +- `WebcamDetect.tsx`: typed refs + `useRef(null)` / `useRef(null)`, null-guard + before `.srcObject` / `.getTracks()`. +- React 19's `eslint-plugin-react-hooks@7` (pulled in by `eslint-config-next@16`) + promotes `react-hooks/immutability` and `react-hooks/set-state-in-effect` to + **errors**. In `WebcamDetect.tsx`: + - `startVideoCapture` / `stopVideoCapture` → `useCallback`, declared **before** + the effects that reference them. + - the self-recursive `setTimeout(startVideoCapture, …)` retry → a hoisted inner + `async function attempt() { … setTimeout(attempt, 500) … }`. + - `currentModel` was `useState` with a setter only ever called inside an effect + (a `set-state-in-effect` error) → plain `const currentModel = model ?? 'mobilenet'`. +- **No `@ant-design/v5-patch-for-react-19`** — antd 6 supports React 19 natively. + +## 3. Next.js 11 → 16 + App Router + +- `next@16`; `-D eslint-config-next@16 eslint@9`. +- **Delete `.babelrc.js`** and drop `babel-plugin-import` — re-enables SWC (and + Turbopack, now the default bundler; a stray babel config would silently pull + Babel back in). +- `next.config.js` → **`next.config.mjs`**, Less wrapper removed: + ```js + /** @type {import('next').NextConfig} */ + const nextConfig = { reactStrictMode: true } + export default nextConfig + ``` + Drop `next-plugin-antd-less`. + (`output: 'standalone'` was tried for a smaller Docker image and reverted — + Next 16 warns `"next start" does not work with "output: standalone"`, which + breaks the `start` / `start:prod` scripts.) +- **Pages Router → App Router.** Delete `src/pages/`. Add: + - `src/app/layout.tsx` — root layout (``/``), wraps + children in `` then ``; `export const metadata`; + `import '../styles/globals.css'`. + - `src/app/providers.tsx` — `'use client'`; `ConfigProvider` with the theme + token (context → must be a client component). + - `src/app/page.tsx` — server component, returns ``. + - `src/app/api/hello/route.ts` — `export const GET = () => NextResponse.json({ name: 'John Doe' })` + (replaces `pages/api/hello.js`). + - `src/components/CameraDetection.tsx` — `'use client'`; + `const WebcamDetect = dynamic(() => import('./WebcamDetect'), { ssr: false })` + inside ``. `ssr: false` keeps face-api.js / TF.js (which + touch `window`/`document` at import) off the server. A Server Component + can't pass `ssr: false`, hence this client wrapper. +- `src/components/WebcamDetect.tsx` gets `'use client'`. +- **`router.events` is gone in the App Router.** `useRouter().events.on('routeChangeStart', stopVideoCapture)` + → the mount `useEffect`'s cleanup calls `stopVideoCapture()` directly (plus an + `active` flag so a late `loadModels().then()` doesn't start the camera after + unmount). +- `src/layouts/DetectionLayout.tsx` — `React.FC` → `({ children }: Props)`, + removed stale `eslint-disable` comments. + +## 4. antd 4 → 6 + +- `antd@6 @ant-design/cssinjs @ant-design/nextjs-registry`. +- **`@ant-design/icons` removed entirely** — it was a dependency but nothing + imports it. +- SSR style extraction: `` from `@ant-design/nextjs-registry` in + `src/app/layout.tsx` (the App Router equivalent of the old `_document.tsx` + `StyleProvider` dance). +- Theme: `src/styles/variables.less` (`@primary-color: #14424d`) → deleted; + `src/app/providers.tsx`: + ```tsx + 'use client' + import { ConfigProvider, type ThemeConfig } from 'antd' + const theme: ThemeConfig = { token: { colorPrimary: '#14424d' } } + export default function ThemeProvider({ children }) { + return {children} + } + ``` +- Component API deltas actually hit: + - `` → `` + - `` → `` + - `` / `` / `` — unchanged. +- No `import 'antd/dist/*.css'` anywhere (there wasn't) — antd 6 injects its own. + +## 5. TypeScript config + +`tsconfig.json`: +- `"target": "ES2022"`, `"lib": ["dom","dom.iterable","esnext"]`. +- `"moduleResolution": "bundler"`, `"module": "esnext"`. +- `"incremental": true`; `"plugins": [{ "name": "next" }]`; `"paths": { "@/*": ["./src/*"] }`. +- `"strict": false` kept (typed refs are enough; tightening is a follow-up). +- `include` adds `.next/types/**/*.ts`. +- `-D typescript@5.9` (not `7.x` — `typescript-eslint`/`eslint-config-next` don't + support the native TS 7 compiler yet). +- `next build` rewrites `next-env.d.ts` and flips `jsx` `preserve` → `react-jsx` — + expected, leave it. + +## 6. ESLint (flat config, `next lint` removed) + +- Next 16 **removed `next lint`**; `next build` no longer lints. +- Delete `.eslintrc.json`. Add `eslint.config.mjs`: + ```js + import nextCoreWebVitals from 'eslint-config-next/core-web-vitals' + export default [ + ...nextCoreWebVitals, // v16 ships flat config; bundles next + next/typescript + { ignores: ['.next/**', 'node_modules/**', 'next-env.d.ts'] }, + ] + ``` + Do **not** wrap it in `FlatCompat` — `eslint-config-next/core-web-vitals` is + already a flat-config array in v16, and `compat.extends()` on it throws + `Converting circular structure to JSON`. +- `package.json` script: `"lint": "eslint"`. + +## 7. Dockerfile + +- Base `node:14.17.3-buster*` → `node:24-bookworm-slim` (build + runtime stages). +- Build stage: `apt-get install -y --no-install-recommends git` (needed to + resolve the `github:` dependency), then `npm ci`. +- Runtime stage: copy `node_modules`, `.next`, `public`, `package*.json`, + `next.config.mjs` from the build stage; `CMD ["npm", "run", "start:prod"]` + (still `next start -p 80`). No standalone (see §3). +- Keep the `sed` line patching `public/version.json`. + +## 8. CI + +- `.github/workflows/docker-image.yml`: `actions/checkout@v3` → `@v4`. It only + runs `docker build` on a self-hosted runner, so the Node bump rides along in + the image. + +## 9. Verification — results + +| Check | Result | +| --- | --- | +| `npm install` | clean, 0 vulnerabilities | +| `grep -R "babel-plugin-import\|antd-less\|variables.less\|\.babelrc" src` | no hits | +| `npm run build` | ✅ Next 16 / Turbopack; TypeScript check passes; routes `○ /`, `○ /_not-found`, `ƒ /api/hello` | +| `npm run lint` | ✅ 0 problems | +| `npm run dev` | ✅ `GET /` → 200 | +| `next start` | ✅ `GET /` → 200 (antd CSS-in-JS inlined, footer renders), `GET /api/hello` → 200 `{"name":"John Doe"}` | +| live webcam / `docker build` | not exercised (no camera / not run this pass) | + +## 10. Known risk areas + +- **The fork commit must be pushed** — `npm ci` pins `feedsbrain/face-api.js` + `7cc8a41`; it has to exist on the fork's `master`. `./.fork-update/` has the + bundle, patch, and instructions. +- **antd 6 major** — only the props above were touched; a wider audit wasn't done + since the app's antd usage is small (`Row`/`Col`/`Space`/`Card`/`Layout`). +- **`react-hooks@7` errors** — the `WebcamDetect` refactor (§2) is behaviour- + preserving but non-trivial; re-check the camera start/stop lifecycle in a real + browser. +- **Toolchain held back**: TS `5.9` and ESLint `9` rather than the `7.x` / `10` + that are also on npm now, for plugin compatibility. Revisit when + `typescript-eslint` supports TS 7. +- **`target: es5` dropped** — fine for the evergreen browsers `getUserMedia` + needs anyway. diff --git a/README.md b/README.md index bb07785..7e1763a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,44 @@ # EMO -Face and Emotion Detection Demo based on Next.js and Tensorflow.js +Face and Emotion Detection demo based on Next.js and TensorFlow.js (via +[`face-api.js`](https://github.com/feedsbrain/face-api.js)). + +## Stack + +| | | +|---|---| +| Runtime | Node.js 24 (see `.tool-versions`; Node ≥ 20.9 required) | +| Framework | Next.js 16 — App Router (`src/app`), Turbopack | +| UI | React 19 + Ant Design 6 (CSS-in-JS, no Less build) | +| Detection | `face-api.js` (feedsbrain fork: TensorFlow.js 4.x, TypeScript 5.9) | + +`face-api.js` is pulled straight from the fork's git repo: + +```json +"face-api.js": "github:feedsbrain/face-api.js#master" +``` + +The fork commits its compiled `build/` output, so no build step runs on install. + +## Development + +```bash +npm install +npm run dev # http://localhost:3000 +npm run build +npm start +npm run lint +``` + +The pre-trained weights live in `public/static/models` and are served from +`/static/models`. + +## Docker + +```bash +docker build -t emo:latest . +docker run --rm -p 80:80 emo:latest +``` + +The image is a multi-stage build on `node:24-bookworm-slim`; the runtime stage +copies `node_modules` + `.next` from the build stage and runs `next start -p 80`. diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..f6ea79f --- /dev/null +++ b/SPEC.md @@ -0,0 +1,114 @@ +# EMO — Modernization Spec (as built) + +Face and Emotion Detection demo (webcam → face detection, landmarks, expressions, +age/gender) built on Next.js + face-api.js (TensorFlow.js). + +This document records the modernization completed on **2026-09-08**. It is +descriptive, not a plan — see `MIGRATION.md` for the step-by-step of how it was +done. + +## 1. Goals + +- Move to the current Node.js release and the latest Next.js. +- Replace the unmaintained `face-api.js@0.22.2` with the modernized fork + `feedsbrain/face-api.js` (TensorFlow.js 4.x). +- Upgrade the UI stack (React, antd) and remove now-obsolete build plugins. +- Migrate to the **App Router** (`src/app`). +- No behavioural change: same detection pipeline, same on-screen output. + +## 2. Baseline (before) + +| Area | Version / detail | +| ----------- | ------------------------------------------------------------------------------------------------------- | +| Node | 14.17.3 (`.tool-versions`) | +| Next.js | 11.1.2 (Pages Router, `src/pages`) | +| React | 17.0.2 | +| face-api.js | 0.22.2 (`justadudewhohacks`) | +| antd | 4.16.13 + `@ant-design/icons@4` (imported but unused) | +| Build glue | `.babelrc.js` (forces Babel), `babel-plugin-import`, `next-plugin-antd-less`, `src/styles/variables.less` | +| TypeScript | 4.4.2, `target: es5` | +| ESLint | 7.32.0 + `eslint-config-next@11`, `.eslintrc.json` | +| Container | `node:14.17.3-buster`, 2-stage, `next start -p 80` | +| CI | `.github/workflows/docker-image.yml` | + +## 3. Target state (delivered) + +| Area | Delivered | +| ----------- | --------------------------------------------------------------------------------------------------------------------------------- | +| Node | **24.20.0** (`.tool-versions`); `engines.node >= 20.9.0`. Dockerfile `node:24-bookworm-slim`; CI unchanged (builds the image). | +| Next.js | **16.3.4** — App Router (`src/app`), Turbopack (default), SWC (no custom Babel). | +| React | **19.2.x** + `react-dom@19`, `@types/react@19`, `@types/react-dom@19`. No `@ant-design/v5-patch-for-react-19` (antd 6 needs none). | +| face-api.js | `github:feedsbrain/face-api.js#master`, pinned in the lockfile to commit `7cc8a41`. Same 0.22.2 public API; bundles TF.js 4.x. | +| antd | **6.6.3**, CSS-in-JS. No Less, no `babel-plugin-import`, no `@ant-design/icons` (was unused). | +| antd SSR | `@ant-design/nextjs-registry` `AntdRegistry` in `src/app/layout.tsx`; `@ant-design/cssinjs` as an explicit dep. | +| Theme | primary `#14424d` in `ConfigProvider theme={{ token: { colorPrimary } }}` — `src/app/providers.tsx` (`'use client'`). | +| TypeScript | **5.9.3**, `target: ES2022`, `moduleResolution: "bundler"`, `paths` `@/* → src/*`, `plugins: [{ name: "next" }]`. `strict: false`. | +| ESLint | **9.x** flat config (`eslint.config.mjs`) extending `eslint-config-next/core-web-vitals` (v16 ships flat config). `next lint` removed → `lint` script is `eslint`. | +| Container | `node:24-bookworm-slim`, 2-stage, keeps `npm run start:prod` (`next start -p 80`). `git` installed in the build stage for the git dep. `output: 'standalone'` was tried and reverted (it warns under `next start`). | + +### Source layout (after) + +- `src/app/layout.tsx` — root layout: ``/``, `AntdRegistry`, metadata. +- `src/app/providers.tsx` — `'use client'`; `ConfigProvider` theme token. +- `src/app/page.tsx` — server component, renders ``. +- `src/app/api/hello/route.ts` — Route Handler (`GET` → `{ name: 'John Doe' }`). +- `src/components/CameraDetection.tsx` — `'use client'`; `dynamic(() => import('./WebcamDetect'), { ssr: false })` inside `DetectionLayout`. +- `src/components/WebcamDetect.tsx` — `'use client'`; webcam capture + results UI. +- `src/lib/face.ts` — **unchanged**: model loading + detection loop (`onStartVideoHandle`), still the `loadSsdMobilenetv1Model` / `loadFaceLandmarkModel` / … free-function API and `SsdMobilenetv1Options`. +- `src/lib/types.ts` — **unchanged** (`asyncForEach`). +- `src/layouts/DetectionLayout.tsx`, `src/components/GlobalFooter.tsx` — de-`React.FC`'d, typed props. +- `public/static/models/*` — face-api.js model weights + manifests, **unchanged** (the fork loads the same format). +- `public/version.json` — `{ "appVersion": "development" }`, still patched at Docker build. + +Removed: `.babelrc.js`, `.eslintrc.json`, `next.config.js` (→ `next.config.mjs`), +`src/styles/variables.less`, `src/pages/*`. + +## 4. Functional requirements (unchanged) + +1. On load: request `getUserMedia({ video: 1280x720, audio: false })`, stream to `