Source: LipShapeAnalyzer.js

import Globals from "./Globals";
import { MountFileWithProgress } from "./Utility/MountFileWithProgress";

let resource_loading = null;
export class LipShapeAnalyzer {
    static async Create(modulesManager, onProgress) {
        const bsc = modulesManager.modules.visage.module;
        const bscAnalyzer = bsc.GetLipShapeAnalysis();

        // file loads wrapped so they load lazily, but once-only
        if (!resource_loading) {
            // NOTE(Straza) byteLen added because if the server uses compression (gzip),
            // content-length will not be correct
            // there is no standard header for uncompressed size
            const resources = [
                {
                    name: "ls_c.bin",
                    path: Globals.pathToAssets + "Assets/Plugins/ls_c.bin",
                    byteLen: 1236,
                },
                {
                    name: "ls_m.js.json",
                    path: Globals.pathToAssets + "Assets/Plugins/ls_m.js.json",
                    byteLen: 80496,
                },
                {
                    name: "ls_m.js.bin",
                    path: Globals.pathToAssets + "Assets/Plugins/ls_m.js.bin",
                    byteLen: 3751840,
                },
            ];

            const curentprogresses = resources.reduce((acc, x) => {
                acc[x.name] = {
                    contentLength: x.byteLen,
                    receivedLength: 0,
                    progress: 0,
                    done: false,
                };
                return acc;
            }, {});

            const total = resources
                .map((k) => k.byteLen)
                .reduce((partialSum, a) => partialSum + a, 0);

            const start = () => {};
            const progress = (name) => (x) => {
                if (!onProgress) return;

                curentprogresses[name] = x;

                const keys = Object.keys(curentprogresses);
                const done = keys.every((k) => curentprogresses[k].done);

                let received = 0;

                if (done) {
                    received = total;
                } else {
                    received = keys
                        .map((k) => curentprogresses[k].receivedLength)
                        .reduce((partialSum, a) => partialSum + a, 0);
                }

                onProgress({
                    contentLength: total,
                    receivedLength: received,
                    progress: received / total,
                    done,
                });
            };

            const resource_promises = resources.map((r) =>
                MountFileWithProgress(
                    bsc.FS,
                    r.name,
                    r.path,
                    progress(r.name),
                    start,
                    r.byteLen,
                ),
            );

            resource_loading = Promise.all(resource_promises).catch(
                () => (resource_loading = null),
            );
        }
        await resource_loading;

        return new LipShapeAnalyzer(bscAnalyzer, bsc);
    }

    constructor(bscAnalyzer, bsc) {
        this.bscAnalyzer = bscAnalyzer;
        this.bsc = bsc;
    }

    promise = null;

    /**
     *@callback LipShapeCallback
     *@param {Object} LipShapeAnalyzerResult - Analyzed lip shape.
     *@param {LipShapeEstimates} LipShapeAnalyzerResult.estimates - Object containing model estimates for each of the 5 possible lip shape classes (Thin, Regular, Full, HeavyUpper, HeavyLower).
     *@param {number} LipShapeAnalyzerResult.frameIndex -  Index of the frame that has been analyzed.
     *@param { "OFF"|"OK"|"RECOVERING"|"INIT" } LipShapeAnalyzerResult.status - Indicates tracking status.
     */

    /**
     * Initiates the eye shape analysis. When a frame has been analyzed a call to the {@link EyeShapeCallback}
     * function is issued, with the analysis results passed as the argument.
     * <br/>
     * @param {LipShapeCallback} LipShapeCallback - callback which will be called every time a new frame is analyzed
     */
    StartAnalysis(LipShapeCallback) {
        const convert = (result) => {
            const status_mapping = new Map([
                [this.bsc.TrackStatus.off, "OFF"],
                [this.bsc.TrackStatus.init, "INIT"],
                [this.bsc.TrackStatus.recovering, "RECOVERING"],
                [this.bsc.TrackStatus.ok, "OK"],
            ]);

            let res = {
                frameIndex: result.frameIndex,
                estimates: result.estimates,
                status: status_mapping.get(result.status),
            };
            result.delete();

            return res;
        };

        const convertThenCall = (result) => {
            LipShapeCallback(convert(result));
        };

        this.bscAnalyzer.SetCallback(convertThenCall);
        this.bscAnalyzer.StartAnalysis();
    }

    /**
     * Stops the lip shape analysis and stops issuing calls to the callback given as a argument to the
     * {@link StartAnalysis} function.
     * <br/>
     */
    StopAnalysis() {
        this.bscAnalyzer.StopAnalysis();
    }

    /**
    * Resets the internal state of the lip shape analyzer. This is useful when switching between frames of different people.
     */
    Reset() {
        this.bscAnalyzer.Reset();
    }
}