webarkit / Nft-Marker-Creator-App

Improved NFT-Marker-Creator app to create your NFT markers for all compatible WebAR projects.
Other
6 stars 4 forks source link

Not running in docker container #5

Open anuj9196 opened 1 month ago

anuj9196 commented 1 month ago

I have been trying to run the application to generate markers in a docker container running Python as base image and node 18 as additional dependency. But it stucks on

Module._createNftDataSet(heapSpace, imageData.dpi, imageData.sizeX, imageData.sizeY, imageData.nc, StrBuffer)
Screenshot 2024-10-26 at 2 29 24 PM

Full Dockerfile is like

FROM python:3.12-slim
ENV PYTHONUNBUFFERED=1

# Create a group and user to run our app
ARG APP_USER=tapxreality
RUN groupadd -r ${APP_USER} && useradd --no-log-init -r -m -g ${APP_USER} ${APP_USER}

RUN set -ex \
    && RUN_DEPS=" \
    libpcre3 \
    mime-support \
    libmagic1 \
    libcurl4-nss-dev libssl-dev \
    weasyprint \
    libpq-dev postgresql postgresql-contrib \
    vim \
    nodejs npm\                                                         # <-- Installing nodejs 18.x
    " \
    && seq 1 8 | xargs -I{} mkdir -p /usr/share/man/man{} \
    && apt-get update && apt-get install -y --no-install-recommends $RUN_DEPS \
    && rm -rf /var/lib/apt/lists/* \
    # Create directories
    && mkdir /app/ \
    && mkdir /config/ \
    && mkdir /scripts/ \
    && mkdir -p /static_cdn/static_root/ \
    && chown -R ${APP_USER} /static_cdn/

ADD requirements/ /

WORKDIR /app/

COPY nft-marker-creator /nft-marker-creator

# Install node dependency
RUN npm install --prefix /nft-marker-creator

RUN set -ex \
    && BUILD_DEPS=" \
    build-essential \
    libpcre3-dev \
    libpq-dev \
    " \
    && apt-get update && apt-get install -y --no-install-recommends $BUILD_DEPS \
    && pip install --no-cache-dir -r /dev-requirements.txt \
    && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false $BUILD_DEPS \
    && rm -rf /var/lib/apt/lists/*

COPY ./src /app/
COPY scripts/ /scripts/
COPY library_data /library_data

# uWSGI will listen on this port
EXPOSE 8000
ENV UWSGI_WSGI_FILE=tapxreality/wsgi.py

# Change to a non-root user
USER ${APP_USER}:${APP_USER}

ENTRYPOINT ["/scripts/docker/entrypoint.sh"]

Then running the node application using `

command = ["node", "/nft-marker-creator/app.js", "-i", file_path, "-o", output_dir]

process = subprocess.Popen(
        command,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        universal_newlines=True  # Ensures text output instead of bytes
 )

NOTE: I have made some changes to the src/NFTMarkerCreator.js file to allow absolute path to the file and output directory

kalwalt commented 1 month ago

Hi @anuj9196 At the moment i have no idea what it could be the reason, at least i will try your dockerfile. Can you share the changes you made to src/NFTMarkerCreator.js?

anuj9196 commented 1 month ago

@kalwalt These are the changes

  1. First I moved out the NFTMarkerCreator.js from src to root (project directory), so that it can be directly accessed using node app.js, without specifying the src/ path.
  2. I renamed the file to app.js to make it shorter to type
  3. I updated the srcImage and outputPath to allow any path in flag -i and -o repsectively. The input and output directories can be any place if absolute path is passed.
    node app.js -i /static/media/image.png -o /result/output

The app.js file content is

const path = require("path");
const fs = require('fs');
const sharp = require('sharp');
const { prompt } = require('enquirer');
var Module = require('./build/NftMarkerCreator_wasm.js');

// GLOBAL VARs
var params = [
];

var validImageExt = [".jpg", ".jpeg", ".png"];

var srcImage;

var outputPath = 'output/';

var buffer;

var foundInputPath = {
    b: false,
    i: -1
}

var foundOutputPath = {
    b: false,
    i: -1
}

var noConf = false;
var withDemo = false;
var onlyConfidence = false;
var isZFT = false;

var imageData = {
    sizeX: 0,
    sizeY: 0,
    nc: 0,
    dpi: 0,
    array: []
}

Module.onRuntimeInitialized = async function () {
    console.log('arguments...: ', process.argv);

    for (let j = 2; j < process.argv.length; j++) {
        if (process.argv[j].indexOf('-i') !== -1 || process.argv[j].indexOf('-I') !== -1) {
            foundInputPath.b = true;
            foundInputPath.i = j + 1;
            j++;
        } else if (process.argv[j] === "-NoConf") {
            noConf = true;
        } else if (process.argv[j] === "-Demo") {
            withDemo = true;
        } else if (process.argv[j] === "-zft") {
            isZFT = true;
        } else if (process.argv[j] === "-onlyConfidence") {
            onlyConfidence = true;
        } else if (process.argv[j].indexOf('-o') !== -1 || process.argv[j].indexOf('-O') !== -1) {
            foundOutputPath.b = true;
            foundOutputPath.i = j + 1;
            j++;
        } else {
            console.log(process.argv[j]);
            params.push(process.argv[j]);
        }
    }

    if (!foundInputPath.b) {
        const response = await prompt({
            type: 'input',
            name: 'inputPath',
            message: 'Image path not present, to continue provide a path to image:'
          });
        srcImage = response.inputPath;
    } else {
        srcImage = process.argv[foundInputPath.i];
    }

    if (!srcImage) {
        console.log("\nERROR: No image in INPUT command!\n e.g:(-i /PATH/TO/IMAGE)\n");
        process.exit(1);
    } else {
        console.log('checking file path...: ', srcImage);
        if (!srcImage.startsWith('/')) {
            // Relative path
            srcImage = path.join(__dirname, srcImage);
        }

        console.log('image path is: ', srcImage);
        // srcImage = path.join(__dirname, process.argv[foundInputPath.i]);
    }

    if (foundOutputPath.b) {
        outputPath = process.argv[foundOutputPath.i];

        if (!outputPath.startsWith('/')) {
            // relative path
            outputPath = path.join(__dirname, outputPath);
            // outputPath = '/' + outputPath;
        }

        if (!outputPath.endsWith('/')) {
            outputPath += '/';
        }

        console.log('Set output path: ' + outputPath);
    }

    let fileNameWithExt = path.basename(srcImage);
    let fileName = path.parse(fileNameWithExt).name;
    let extName = path.parse(fileNameWithExt).ext;

    let foundExt = false;
    for (let ext in validImageExt) {
        if (extName.toLowerCase() === validImageExt[ext]) {
            foundExt = true;
            break;
        }
    }

    if (!foundExt) {
        console.log("\nERROR: Invalid image TYPE!\n Valid types:(jpg,JPG,jpeg,JPEG,png,PNG)\n");
        process.exit(1);
    }

    if (!fs.existsSync(srcImage)) {
        console.log("\nERROR: Not possible to read image, probably invalid image PATH!\n");
        process.exit(1);
    } else {
        buffer = fs.readFileSync(srcImage);
    }

    console.log('Check output path: ' + outputPath);
    if (!fs.existsSync(outputPath)) {
        fs.mkdirSync(outputPath);
    }

    if (extName.toLowerCase() === ".jpg" || extName.toLowerCase() === ".jpeg" || extName.toLowerCase() === ".png") {
        await processImage(buffer);
    }

    let confidence = calculateQuality();

    let txt = " - - - - - ";
    if (confidence.l != 0) {
        let str = txt.split(" ");
        str.pop();
        str.shift();
        for (let i = 0; i < parseInt(confidence.l); i++) {
            str[i] = " *";
        }
        str.push(" ");
        txt = str.join("");
    }

    if (onlyConfidence) {
        console.log("%f", confidence.l);
        process.exit(0);
    }

    console.log("\nConfidence level: [" + txt + "] %f/5 || Entropy: %f || Current max: 5.17 min: 4.6\n", confidence.l, confidence.e)

    if (noConf) {
        await askToContinue();
    }

    let paramStr = params.join(' ');

    let StrBuffer = Module._malloc(paramStr.length + 1);
    Module.stringToUTF8(paramStr, StrBuffer);

    console.log('Write Success');
    let heapSpace = Module._malloc(imageData.array.length * imageData.array.BYTES_PER_ELEMENT);
    Module.HEAPU8.set(imageData.array, heapSpace);

    console.log('Setting Heap Success.. Continue to Create ImageSet..');
    Module._createNftDataSet(heapSpace, imageData.dpi, imageData.sizeX, imageData.sizeY, imageData.nc, StrBuffer)
    console.log('Create NFT Dataset complete...')

    Module._free(heapSpace);
    Module._free(StrBuffer);

    let filenameIset = "tempFilename.iset";
    let filenameFset = "tempFilename.fset";
    let filenameFset3 = "tempFilename.fset3";

    let ext = ".iset";
    let ext2 = ".fset";
    let ext3 = ".fset3";

    let content = Module.FS.readFile(filenameIset);
    let contentFset = Module.FS.readFile(filenameFset);
    let contentFset3 = Module.FS.readFile(filenameFset3);

    if (isZFT) {
        console.log("CREATING ZFT FILE");
        let iset = Buffer.from(content.buffer);
        let fset = Buffer.from(contentFset.buffer);
        let fset3 = Buffer.from(contentFset3.buffer);

        let obj = {
            iset: iset.toString('hex'),
            fset: fset.toString('hex'),
            fset3: fset3.toString('hex')
        }

        let strObj = JSON.stringify(obj);

        let StrBufferZip = Module._malloc(strObj.length + 1);
        Module.stringToUTF8(strObj, StrBufferZip);

        Module._compressZip(StrBufferZip, strObj.length);

        let contentBin = Module.FS.readFile("tempBinFile.bin");

        // fs.writeFileSync(path.join(__dirname, '/output/') + fileName + ".zft", contentBin);
        fs.writeFileSync(outputPath + fileName + ".zft", contentBin);

        Module._free(StrBufferZip);

        if (withDemo) {
            console.log("\nFinished marker creation!\nNow configuring demo! \n")

            const markerDir = path.join(__dirname, './../demo/public/marker/');

            if (!fs.existsSync(markerDir)) {
                fs.mkdirSync(markerDir);
            }

            let demoHTML = fs.readFileSync("./demo/nft.html").toString('utf8').split("\n");
            addNewMarker(demoHTML, fileName);
            let newHTML = demoHTML.join('\n');

            fs.writeFileSync("./demo/nft.html", newHTML, { encoding: 'utf8', flag: 'w' });

            const files = fs.readdirSync(markerDir);
            for (const file of files) {
                fs.unlink(path.join(markerDir, file), err => {
                    if (err) throw err;
                });
            }

            fs.writeFileSync(markerDir + fileName + ".zft", contentBin);

            console.log("Finished!\nTo run demo use: 'npm run demo'");
        }

    } else {
        console.log("CREATING ISET, FSET AND FSET3 FILES");
        // fs.writeFileSync(path.join(__dirname, outputPath) + fileName + ext, content);
        // fs.writeFileSync(path.join(__dirname, outputPath) + fileName + ext2, contentFset);
        // fs.writeFileSync(path.join(__dirname, outputPath) + fileName + ext3, contentFset3);
        fs.writeFileSync(outputPath + fileName + ext, content);
        fs.writeFileSync(outputPath + fileName + ext2, contentFset);
        fs.writeFileSync(outputPath + fileName + ext3, contentFset3);

        if (withDemo) {
            console.log("\nFinished marker creation!\nNow configuring demo! \n")

            const markerDir = path.join(__dirname, './../demo/public/marker/');

            if (!fs.existsSync(markerDir)) {
                fs.mkdirSync(markerDir);
            }

            let demoHTML = fs.readFileSync("./../demo/nft.html").toString('utf8').split("\n");
            addNewMarker(demoHTML, fileName);
            let newHTML = demoHTML.join('\n');

            fs.writeFileSync("./../demo/nft.html", newHTML, { encoding: 'utf8', flag: 'w' });

            const files = fs.readdirSync(markerDir);
            for (const file of files) {
                fs.unlink(path.join(markerDir, file), err => {
                    if (err) throw err;
                });
            }

            fs.writeFileSync(markerDir + fileName + ext, content);
            fs.writeFileSync(markerDir + fileName + ext2, contentFset);
            fs.writeFileSync(markerDir + fileName + ext3, contentFset3);

            console.log("Finished!\nTo run demo use: 'npm run demo'");
        }
    }

    process.exit(0);
}

async function processImage(buf) {
    const image = sharp(buf);
    await image.metadata()
        .then(async metadata => {
            if (metadata.density) {
                imageData.dpi = metadata.density;
            } else {
                console.log("\nWARNING: No DPI value found! Using 150 as default value!\n");
                imageData.dpi = 150;
            }
            if(metadata.width){
                imageData.sizeX = metadata.width;
            } else {
                await metadataWidth();
            }
            if(metadata.height){
                imageData.sizeY = metadata.height;
            } else {
                await metadataHeigth();
            }
            if(metadata.channels){
                imageData.nc = metadata.channels;
            } else {
                await metadataChannels();
            }
            return image
            .raw()
            .toBuffer()
        })   
        .then(data => {
            let dt = data.buffer

            let verifyColorSpace = detectColorSpace(dt);

            if (verifyColorSpace === 1) {
                console.log("Color Space is 1 channel!");
            } else if (verifyColorSpace === 3) {
                console.log("Color Space is 3 channels!");
            }

            let uint = new Uint8Array(dt);
            if(imageData.nc === verifyColorSpace){
                console.log("Color Space is equal to metadata.channels!");
            }else{
                console.log("Color Space is not equal to metadata.channels!");
                //process.exit(1);
            }
            imageData.nc = verifyColorSpace;
            imageData.array = uint;
        })
        .catch(function (err) {
            console.error("Error extracting metadata: " + err);
            process.exit(1);
        });
}

async function extractMetadata(buf) {
    return await sharp(buf).metadata()
        .then(function (metadata) {
            if (metadata.density) {
                imageData.dpi = metadata.density;
            } else {
                console.log("\nWARNING: No DPI value found! Using 150 as default value!\n");
                imageData.dpi = 150;
            }
            imageData.sizeX = metadata.width;
            imageData.sizeY = metadata.height;
            imageData.nc = metadata.channels;
        }).catch(function (err) {
            console.error("Error extracting metadata: " + err);
            process.exit(1);
        });
}

function detectColorSpace(arr) {

    let target = parseInt(arr.length / 4);

    let counter = 0;

    for (let j = 0; j < arr.length; j += 4) {
        let r = arr[j];
        let g = arr[j + 1];
        let b = arr[j + 2];

        if (r === g && r === b) {
            counter++;
        }
    }

    if (target === counter) {
        return 1;
    } else {
        return 3;
    }
}

function rgbaToRgb(arr) {
    let newArr = [];
    let BGColor = {
        R: 255,
        G: 255,
        B: 255
    }

    for (let i = 0; i < arr.length; i += 4) {

        let r = parseInt(255 * (((1 - arr[i + 3]) * BGColor.R) + (arr[i + 3] * arr[i])));
        let g = parseInt(255 * (((1 - arr[i + 3]) * BGColor.G) + (arr[i + 3] * arr[i + 1])));
        let b = parseInt(255 * (((1 - arr[i + 3]) * BGColor.B) + (arr[i + 3] * arr[i + 2])));

        newArr.push(r);
        newArr.push(g);
        newArr.push(b);
    }
    return newArr;
}

function calculateQuality() {
    let gray = toGrayscale(imageData.array);
    let hist = getHistogram(gray);
    let ent = 0;
    let totSize = imageData.sizeX * imageData.sizeY;
    for (let i = 0; i < 255; i++) {
        if (hist[i] > 0) {
            let temp = (hist[i] / totSize) * (Math.log(hist[i] / totSize));
            ent += temp;
        }
    }

    let entropy = (-1 * ent).toFixed(2);
    let oldRange = (5.17 - 4.6);
    let newRange = (5 - 0);
    let level = (((entropy - 4.6) * newRange) / oldRange);

    if (level > 5) {
        level = 5;
    } else if (level < 0) {
        level = 0;
    }
    return { l: level.toFixed(2), e: entropy };
}

function toGrayscale(arr) {
    let gray = [];
    for (let i = 0; i < arr.length; i += 3) {
        let avg = (arr[i] + arr[i + 1] + arr[i + 2]) / 3;
        gray.push(parseInt(avg));
    }
    return gray;
}

function getHistogram(arr) {
    let hist = [256];
    for (let i = 0; i < arr.length; i++) {
        hist[i] = 0;
    }
    for (let i = 0; i < arr.length; i++) {
        hist[arr[i]]++;
    }
    return hist;
}

function addNewMarker(text, name) {
    for (let i = 0; i < text.length; i++) {
        if (text[i].trim().includes("<script>MARKER_NAME =")) {
            text[i] = "<script>MARKER_NAME = '" + name + "'</script>"
            break;
        }
    }
}

async function askToContinue() {
    const response = await prompt({
        type: 'input',
        name: 'answer',
        message: 'Do you want to continue? (Y/N)\n'
      });

    if (response.answer == "n") {
        console.log("\nProcess finished by the user! \n");
        process.exit(1);
    }
}

async function metadataWidth() {
    const responseToProceed = await prompt({
        type: 'input',
        name: 'answer',
        message: 'Metadata width not present do you want to inform it? (Y/N)\n'
      });

    if (responseToProceed.answer == "n") {
        console.log("\nProcess finished by the user! \n");
        process.exit(1);
    } else {
        const responseAfterEnquiry = await prompt({
            type: 'input',
            name: 'width',
            message: 'Inform the width: e.g 200\n'
          });
        if (responseAfterEnquiry.width) {
            imageData.sizeX = responseAfterEnquiry.width;
        }
    }
}

async function metadataHeigth() {
    const responseToProceed = await prompt({
        type: 'input',
        name: 'answer',
        message: 'Metadata height not present do you want to inform it? (Y/N)\n'
      });

    if (responseToProceed.answer == "n") {
        console.log("\nProcess finished by the user! \n");
        process.exit(1);
    } else {
        const responseAfterEnquiry = await prompt({
            type: 'input',
            name: 'height',
            message: 'Inform the height: e.g 400\n'
          });
        if (responseAfterEnquiry.height) {
            imageData.sizeY = responseAfterEnquiry.height;
        }
    }
}

async function metadataChannels() {
    const responseToProceed = await prompt({
        type: 'input',
        name: 'answer',
        message: 'Metadata channels not present do you want to inform it? (Y/N)\n'
      });

    if (responseToProceed.answer == "n") {
        console.log("\nProcess finished by the user! \n");
        process.exit(1);
    } else {
        const responseAfterEnquiry = await prompt({
            type: 'input',
            name: 'channels',
            message: 'Inform the number of channels: e.g 3\n'
          });
        if (responseAfterEnquiry.channels) {
            imageData.nc = responseAfterEnquiry.channels;
        }
    }
}

NOTE: Path of demo and build are not updated. NOTE: If you feel these changes are good to merged in this repo, I can create a PR for the same.

anuj9196 commented 1 month ago

I think the issue is not with the library itself but with the dependencies required to run C++ executables. I tried using the python base image instead of the slim version.

FROM python:3.12
#FROM python:3.12-slim

And the command is executing. However, the stdout of the C++ command is not streaming live and it takes approx 15 minutes to complete the execution.

Screenshot 2024-10-27 at 11 03 09 AM
kalwalt commented 1 month ago

First I moved out the NFTMarkerCreator.js from src to root (project directory), so that it can be directly accessed using node app.js, without specifying the src/ path. I renamed the file to app.js to make it shorter to type

Maybe we can setup a script to add the NFTMarkerCreator.js to the system envirnonment vars. I will try as i have a bit of time.

And the command is executing. However, the stdout of the C++ command is not streaming live and it takes approx 15 minutes to complete the execution.

Probably it depends on dpi and width, height of the image you are providing for the NFT creations.

NOTE: If you feel these changes are good to merged in this repo, I can create a PR for the same.

I will think about, maybe i can create another repository and add the NFT-Marker-Creator-app as git submodule. I planned to do in the past but never had the time. @ThorstenBux in the past made an app to create NFT markers in a server, but i don't think it is working anymore. The repository is this https://github.com/webarkit/NFT-Creator-WS

kalwalt commented 1 month ago

I'm trying to build the image but it fails at the end because missed dev-requirements.txt file, i think required for python. @anuj9196 Can you share also this? Thank you.

P.S. and maybe /scripts/docker/entrypoint.sh ?

anuj9196 commented 1 month ago

@kalwalt Please check the content of requested files

dev-requirements.txt

django==5.0.6
djangorestframework==3.15.1
django-allauth==0.63.3
dj-rest-auth==6.0.0
django-oauth-toolkit==2.4.0
celery[sqs]==5.4.0
django-celery-beat==2.6.0
django-celery-results==2.5.1
django-filter==24.2
django-hosts==6.0
whitenoise==6.6.0
sentry-sdk==2.5.1
django-cors-headers==4.3.1
pre-commit==3.7.1
flake8==7.0.0
django-admin-search==0.3.15
boto3==1.34.125
django-storages==1.14.3
dj-database-url
psycopg==3.1.12
#django-postgres-extra==2.0.8  # No Django 5 support
#mysqlclient
django-safedelete==1.4.0
django-json-widget
jsonschema==4.23.0
jschon==0.11.0
django-ordered-model==3.7.4
django-silk
channels==4.1.0
channels_redis==4.2.0
daphne==4.1.2
uWSGI==2.0.26
pandas==2.2.2
openpyxl==3.1.5
django-import-export==4.0.8
django-libsass==0.9
opencv-python-headless==4.10.0.84
numpy==2.0.0
jet-django==1.9.11
dnspython==2.6.1
tld==0.13
django-countries==7.6.1
django-sequences==3.0
swapper==1.3.0
django-two-factor-auth==1.16.0
pyotp
geoip2==4.8.0
user-agents==2.2.0
css-inline==0.14.1
stripe
weasyprint==62.2
django-weasyprint==2.3.0
plotly==5.22.0
kaleido
markdown==3.6
newrelic
django-htmx

entrypoint.sh

#!/bin/sh
#set -e

# Accept other commands
exec "$@"
kalwalt commented 1 month ago

I was able to build the docker image with some modifications, creating a container and running a command to build a nft markers. But i avoided to use python as main command, instead i used the classical command node NFTMarkerCreator.js -i pinball.jpg for testing. I haven't tested yet your modifications to the js script but i will do as a next step. I had to do this because probably in your setup you have other scripts, and was a nonsense to me try with them. I would develop a basic docker image so anyone may fork the repository and modify in base of the needs. Probably will switch to a ubuntu base layer instead of python, infact i saw it enlarge a lot in size. In regards of nfts creation times i don't see they are increased, probably it depends on dpi, image size.

kalwalt commented 3 weeks ago

@anuj9196 tested your mod to NFTMarkerCreator.js, it works perfectly in my local docker image and also when running the old style app (without docker i mean). Those changes are necessary for the docker image, many thanks for this! Soon i will upload on a branch this code, referencing this issue.