vladmandic / face-api

FaceAPI: AI-powered Face Detection & Rotation Tracking, Face Description & Recognition, Age & Gender & Emotion Prediction for Browser and NodeJS using TensorFlow/JS
https://vladmandic.github.io/face-api/demo/webcam.html
MIT License
824 stars 149 forks source link

`TypeError: this.util.TextEncoder is not a constructor` during Next.js esm production build #140

Closed mxmzb closed 1 year ago

mxmzb commented 1 year ago

Issue Description

I am trying to build with the new appDir feature of Next.js, and I'm getting this:

TypeError: this.util.TextEncoder is not a constructor
    at new iR (webpack-internal:///(sc_client)/./node_modules/@vladmandic/face-api/dist/face-api.esm.js:138:46701)
    at eval (webpack-internal:///(sc_client)/./node_modules/@vladmandic/face-api/dist/face-api.esm.js:138:47177)
    at Module.(sc_client)/./node_modules/@vladmandic/face-api/dist/face-api.esm.js (/Volumes/Projects/project/packages/project-app-web/.next/server/app/page.js:370:1)
    at __webpack_require__ (/Volumes/Projects/project/packages/project-app-web/.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(sc_client)/./src/components/StreamCapture/StreamCapture.tsx:13:99)
    at Module.(sc_client)/./src/components/StreamCapture/StreamCapture.tsx (/Volumes/Projects/project/packages/project-app-web/.next/server/app/page.js:5338:1)
    at __webpack_require__ (/Volumes/Projects/project/packages/project-app-web/.next/server/webpack-runtime.js:33:43)
    at eval (webpack-internal:///(sc_client)/./app/page.tsx:16:97)
    at Module.(sc_client)/./app/page.tsx (/Volumes/Projects/project/packages/project-app-web/.next/server/app/page.js:4623:1)
    at __webpack_require__ (/Volumes/Projects/project/packages/project-app-web/.next/server/webpack-runtime.js:33:43)
    at /Volumes/Projects/project/packages/project-app-web/node_modules/next/dist/compiled/react-server-dom-webpack/client.js:106:27
    at processTicksAndRejections (node:internal/process/task_queues:96:5)

Dev environment works absolutely perfectly.

Steps to Reproduce

Create a page in /app directory where you:

// 1. use JS directive `"use client"`:
// https://beta.nextjs.org/docs/rendering/server-and-client-components#convention

// 2. import @vladmandic/face-api:
import * as faceapi from "@vladmandic/face-api/dist/face-api.esm.js";

// 3. 
React.useEffect(() => {
    const loadModel = async () => {
      // these are the lines where it breaks!! 🚨👇🚨👇🚨👇🚨 
      await faceapi.nets.faceExpressionNet.loadFromUri("/models");
      await faceapi.nets.ssdMobilenetv1.loadFromUri("/models");
    };

    loadModel();
  }, []);

Expected Behavior

It builds

**Environment

Additional

I found https://stackoverflow.com/questions/65562209/this-util-textencoder-is-not-a-constructor-only-in-electron-app-works-in-chrome which seems absolutely related to me but haven't looked how to fix the package, yet.

vladmandic commented 1 year ago

When you do:

import * as faceapi from "@vladmandic/face-api/dist/face-api.esm.js";

NextJS will ALSO try to import it server-side as it is primarily SSR framework.
And since you're importing browser ESM, name space faceapi.tf.util does not exist as its not needed in browser (since browsers implements methods like TextEncoder natively).

A good rule for NextJS and any other server-side framework is to explicitly load library client-side only.

Example:

'use client';

import { useState, useEffect } from 'react';
import * as Faceapi from '@vladmandic/face-api/types/face-api'; // import typedefs only

export default function FaceAPI() {
  const [faceapi, setFaceapi] = useState<typeof Faceapi>(); // create state variable with strong typing
  const [options, setOptions] = useState<Faceapi.SsdMobilenetv1Options>(); // create state variable with strong typing
  const modelPath = 'https://vladmandic.github.io/face-api/model/' // can also be local path

  useEffect(() => {
    const initFaceAPI = async () => {
      if (options) return; // already initialized
      const instance = await import('@vladmandic/face-api/dist/face-api.esm'); // actual dynamic import that happens client-side only even if its actually part of webpack (there is no extra fetch request)
      setFaceapi(instance); // assign import to state variable so it can be reused
      if (!faceapi) return; // safety check if import faIled
      await faceapi.tf.setBackend('webgl');
      await faceapi.tf.ready();
      console.log({ faceapi: faceapi.version, tfjs: faceapi.tf.version_core, backend: faceapi.tf.getBackend(), flags: faceapi.tf.ENV['flags'], platform: faceapi.tf.ENV['platformName'] });
      await faceapi.nets.ssdMobilenetv1.load(modelPath);
      await faceapi.nets.faceLandmark68Net.load(modelPath);
      await faceapi.nets.faceRecognitionNet.load(modelPath);
      await faceapi.nets.faceExpressionNet.load(modelPath);
      const opt = new faceapi.SsdMobilenetv1Options({ minConfidence: 0.1, maxResults: 1 }); // create options well as we wont need multiple instances anyhow
      setOptions(opt); // assign options object to state variable
    };

    initFaceAPI();
  }, [faceapi, options]);

  return (
    <div>
      <p>FaceAPI version {faceapi?.version}</p>
    </div>
  );
}
mxmzb commented 1 year ago

@vladmandic thank you, much appreciated! This was super helpful and accurate! 🙏🙏🙏🙏🙏🤠

mxmzb commented 1 year ago

@vladmandic The code works in development mode, but I've realized there is an issue with when building for production.

Here is what I'm getting in browser console after production build:

CleanShot 2022-12-25 at 14 12 37@2x
useEffect(() => {
  const initFaceAPI = async () => {
    try {
      // 🚨🚨🚨 this is what fails 👇
      const instance = await import('@vladmandic/face-api/dist/face-api.esm');
    } catch(e) {
      console.log({ e })
    }
  };

  initFaceAPI();
}, [faceapi]);

Do you have any idea what could be going wrong (not too sure yet if this is more an issue with Next.js or face-api, still looking into it)? Thank you and merry Christmas! 🎄

mxmzb commented 1 year ago

FYI: I found the following solution (for now):

// next.config.js
webpack: (config) => {
  config.optimization.minimize = false;
  return config;
},