【Modelground】个人AI产品MVP迭代平台(3)——工程化架构设计

背景

Modelground中的项目,基本都依赖Mediapipe模型,因此,有很强的需要对Mediapipe进行封装,其余项目都调用这个封装库。从架构上,这种结构的项目很容易联想到Monorepo,即多项目管理。现代包管理器对monorepo形式的仓库已有较好的支持,例如yarn、lerna等。Modelground采用的是其中的一种:pnpm

架构示意图如下:

架构示意图

monorepo

首先全局安装pnpm

npm i pnpm -g

项目初始化

pnpm init

创建pnpm-workspace.yaml,定义包目录

packages:
  - 'packages/**'

创建packages文件夹,添加第项目A和项目B

mkdir packages
cd packages
pnpm create vite A
pnpm create vite B

此时,packages中会出现名称为A和B的两个项目文件夹。

如果项目B要依赖A:

pnpm add A --filter B

此时,B项目的packages.json如下:

{
	"dependencies": {
    	"A": "workspace:^",
  },
}

这样,B打包时,A包不需要发布成npm包,B就可以将A一同打包进dist。
同理,需要给B项目添加某个依赖包C,也是如下代码:

pnpm add C --filter B

最后,统一安装整个项目的包

pnpm i

多项目调试/打包

Modelground中的项目有依赖关系,例如B依赖A,有时候我们会同时修改A和B项目的代码,如果每次都要手动启动两个项目,步骤过于繁琐,因此强需求一套自动化代码去调试某个项目前,自动开启其依赖项目。

我们首先需要一个终端的命令行选项,根目录安装inquirer

pnpm add inquirer --D -i

其次,在js文件中更方便地执行shell,需要安装execa

pnpm add execa --D -i

命令行写法:

inquirer
      .prompt([
        {
          type: "list",
          message: `选择要启动的项目:`,
          name: "mono",    // 存储答案的字段
          default: 'home',   // 默认启动项
          choices: ['home', 'fitness-count', 'ml-video', 'shooter-game', 'generate-ai'], // 想启动的项目名列表
        }
      ]).then({mono: prd} => {
      	  console.log(prd)
		  // 选择启动的项目名,例如"home"
	  })

在这里插入图片描述
启动项目的代码:

const projectServer = execa('pnpm', ['--F', prd, 'run', 'dev'], { stdio: 'pipe' }); // 等价 $pnpm --F prd run dev
projectServer.stdout.on('data', (data) => { console.log(data) }); // 监听运行输出
projectServer.stderr.on('data', (data) => { console.error(data) }); // 监听报错输出

如何在项目A启动完成后,再启动B?
一种解法是在A项目启动后,监听stdout中的输出信息,如果出现"built in",就启动B,代码如下:

let hasRun = false;
const A = execa('pnpm', ['--F', 'A', 'run', 'dev'], { stdio: 'pipe' });
A.stdout.on('data', (data) => {
        console.log(data)
        // A运行起来后,再运行当前启动项目,仅运行一次
        if (data.includes('built in') && !hasRun) {
          hasRun = true;
          const B = execa('pnpm', ['--F', 'B', 'run', 'dev'], { stdio: 'pipe' });
          B.stdout.on('data', (data) => { console.log(data) });
          B.stderr.on('data', (data) => { console.error(data) });
        }
      });
modelServer.stderr.on('data', (data) => { console.error(data) });

如果想区分不同项目的输出信息,可以安装一个chalk,可以用调整输出文字的颜色:

// 当前项目用绿色加粗
function projectTitle() {
  return chalk.green.bold('当前项目服务:');
}

// 公共静态文件用黄色加粗
function publicTitle() {
  return chalk.yellow.bold('公共模型服务:');
}

// 模型依赖用蓝色加粗
function modelTitle() {
  return chalk.blue.bold('mediapipe模型服务:');
}

// stdOut用白色
function stdOut(data) {
  return chalk.white(data);
}

// stdErr用红色
function stdErr(data) {
  return chalk.red(data);
}

// 改造上述代码
A.stdout.on('data', (data) => { console.log(projectTitle(), stdOut(data)) });
A.stderr.on('data', (data) => { console.log(projectTitle(), stdErr(data)) });

效果图:
在这里插入图片描述
同理,也可以实现多项目打包代码同步远程仓库,这里就不赘述。

公共静态资源服务

Mediapipe模型所需的预训练模型体积相对较大,由于内部采用fetch方法去请求预训练模型,因此没法放在公共依赖包中,只能放在项目的public下。但是如果每个项目都去存放一些模型,往往有重复问题,因此强需求一个公共静态资源服务,将所有的预训练模型提取到一个公共目录下。

在packages下创建一个public-assets文件夹,将公共模型都放入该文件夹下。
在这里插入图片描述
创建一个server.js,写一个简单的node文件服务。

// 引入http模块
const http = require('http');
// 引入fs模块
const fs = require('fs');
// 引入path模块
const path = require('path');

// 创建HTTP服务器
const server = http.createServer((req, res) => {
  // 构建请求的文件路径
  const filePath = path.join(__dirname, '/', req.url === '/' ? 'index.html' : req.url);
  // 检查文件是否存在
  fs.exists(filePath, (exist) => {
    if (!exist) {
      // 如果文件不存在,返回404
      res.writeHead(404, { 'Content-Type': 'text/html' });
      res.end('404 Not Found');
      return;
    }

    // 读取文件内容
    fs.readFile(filePath, (err, content) => {
      if (err) {
        res.writeHead(500, { 'Content-Type': 'text/html' });
        res.end('500 Internal Server Error');
      } else {
        // 设置响应头
        const extname = path.extname(filePath);
        let contentType = 'text/plain';
        if (extname === '.task') {
          contentType = 'application/octet-stream'
        } else if (extname === '.wasm') {
          contentType = 'application/wasm';
        } else if (extname === '.tflite') {
          contentType = 'application/octet-stream';
        }
        res.writeHead(200, {
        	'Content-Type': contentType,
        	"access-control-allow-origin": "*", // 解决不同端口跨域问题
        });
        res.end(content, 'utf-8');
      }
    });
  });
});

// 设置监听端口
const port = 5180;
server.listen(port, () => {
  console.log(`Server running at http://localhost:${port}/`);
});

通过 node server.js 就可以开启该文件服务。

我们在每个项目中新建环境变量文件.env.development

VITE_MODEL_PATH=http://localhost:5180/

在实际请求模型时,通过vite的环境变量就可以取到该变量import.meta.env.VITE_MODEL_PATH

同理,在正式环境时,模型的请求地址就变成了项目的路由,如果是根路由,设定.env.production

VITE_MODEL_PATH=/

这样,不同环境下就能正常获取模型文件。

公共模型拷贝入项目的public文件夹

简单说,就是利用shell的cp命令,拷贝文件,直接上build代码:

import inquirer from "inquirer";
import { execaCommand } from "execa";

// 各项目所需的模型文件
const copyConfig = {
  'shooter-game': {
    'packages/public-assets/wasm/vision_wasm_internal.js': 'packages/shooter-game/dist/wasm',
    'packages/public-assets/wasm/vision_wasm_internal.wasm': 'packages/shooter-game/dist/wasm',
    'packages/public-assets/models/ObjectDetection/rim_ball_model_v1.tflite': 'packages/shooter-game/dist/models/ObjectDetection'
  },
  'ml-video': {
    "packages/public-assets/models/ObjectDetection/rim_ball_model_v1.tflite": 'packages/ml-video/dist/models/ObjectDetection',
    "packages/public-assets/models/ObjectDetection/efficientdet_lite0.tflite": 'packages/ml-video/dist/models/ObjectDetection',
    "packages/public-assets/models/ObjectDetection/efficientdet_lite2.tflite": 'packages/ml-video/dist/models/ObjectDetection',
    "packages/public-assets/models/PoseLandMarker/pose_landmarker_full.task": 'packages/ml-video/dist/models/PoseLandMarker',
    "packages/public-assets/models/PoseLandMarker/pose_landmarker_lite.task": 'packages/ml-video/dist/models/PoseLandMarker',
    "packages/public-assets/models/HandLandMarker/hand_landmarker.task": 'packages/ml-video/dist/models/HandLandMarker',
    "packages/public-assets/models/FaceLandMarker/face_landmarker.task": 'packages/ml-video/dist/models/FaceLandMarker',
    "packages/public-assets/wasm/vision_wasm_internal.js": 'packages/ml-video/dist/wasm',
    "packages/public-assets/wasm/vision_wasm_internal.wasm": 'packages/ml-video/dist/wasm'
  }
}

async function run() {
  try {
    const { mono: prd } = await inquirer
      .prompt([
        {
          type: "list",
          message: `选择要构建的项目:`,
          name: "mono",    // 存储答案的字段
          default: 'home',   // 默认启动项
          choices: ['home', 'fitness-count', 'mediapipe-model-core', 'ml-video', 'shooter-game', 'generate-ai'],
        }
      ]);
    // 先打包,有了dist文件夹再拷贝文件
    let result = await execaCommand(`pnpm --filter ${prd} run build`, { stdio: "inherit" });
    
    const copy = copyConfig[prd];
    if (!copy) return;
    
    const pa = Object.entries(copy);
    for (let i = 0; i < pa.length; i++) {
      const from = pa[i][0]; // 待拷贝的文件
      const to = pa[i][1]; // 目标文件夹
      await execaCommand(`mkdir -p ${to}`); // 如果没有该文件夹,先新建
      await execaCommand(`cp ${from} ${to}`); // 执行拷贝
    }
  } catch (err) {
    console.error(err);
  }
}

run();

总结

这套架构是我在开发Modelground过程中,逐渐摸索出来的比较成熟的架构。很多坑都是过程中发现并解决,并不是一开始就能考虑到的。

总结而言,依赖monorepo多项目管理模式,实现项目依赖,并行开发。通过流水线模式,简化项目启动流程。通过公共模型服务,减少冗余静态文件复制动作,在打包时统一拷贝。

以上,就是Modelground的工程化架构设计内容,极大减少了本人开发耗时,可以将精力集中在构思创意上。

欢迎访问Modelground体验已有模型https://tryiscool.space

在这里插入图片描述
如果本文对你有帮助,希望能得到你的三连+订阅Modelground专栏,鼓励我持续产出,谢谢!

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-07 13:08:01       98 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-07 13:08:01       106 阅读
  3. 在Django里面运行非项目文件

    2024-06-07 13:08:01       87 阅读
  4. Python语言-面向对象

    2024-06-07 13:08:01       96 阅读

热门阅读

  1. ffmplay 源码解读

    2024-06-07 13:08:01       20 阅读
  2. MySQL清空所有表的数据的方法

    2024-06-07 13:08:01       26 阅读
  3. Python里cv2是什么包?怎么安装使用?

    2024-06-07 13:08:01       26 阅读
  4. VBA实战(Excel)(4):实用功能整理

    2024-06-07 13:08:01       28 阅读
  5. 【Linux】软链接和硬链接

    2024-06-07 13:08:01       31 阅读
  6. 【HTML】tabindex

    2024-06-07 13:08:01       27 阅读
  7. PostgreSQL和MySQL架构模型的区别

    2024-06-07 13:08:01       29 阅读