Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .babelrc.js

This file was deleted.

3 changes: 0 additions & 3 deletions .eslintrc.json

This file was deleted.

10 changes: 5 additions & 5 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
11 changes: 9 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
# next.js
/.next/
/out/

# production
/build

# turbopack
.turbo

# typescript
*.tsbuildinfo

# misc
.DS_Store
*.pem
Expand All @@ -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
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@feedsbrain:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
3 changes: 1 addition & 2 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
nodejs 14.17.3
python 3.9.5
nodejs 24.20.0
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->

# 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.

<!-- END:nextjs-agent-rules -->
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
39 changes: 20 additions & 19 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
220 changes: 220 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -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<HTMLVideoElement>(null)` / `useRef<HTMLCanvasElement>(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 (`<html lang="en">`/`<body>`), wraps
children in `<AntdRegistry>` then `<ThemeProvider>`; `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 `<CameraDetection />`.
- `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 `<DetectionLayout>`. `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<Props>` → `({ 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: `<AntdRegistry>` 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 <ConfigProvider theme={theme}>{children}</ConfigProvider>
}
```
- Component API deltas actually hit:
- `<Card bordered={false}>` → `<Card variant="borderless">`
- `<Space direction="vertical">` → `<Space orientation="vertical">`
- `<Row>` / `<Col>` / `<Layout>` — 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.
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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`.
Loading
Loading