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

文章目录

背景

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

架构示意图如下:

monorepo

首先全局安装pnpm

shell 复制代码
npm i pnpm -g

项目初始化

shell 复制代码
pnpm init

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

yaml 复制代码
packages:
  - 'packages/**'

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

shell 复制代码
mkdir packages
cd packages
pnpm create vite A
pnpm create vite B

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

如果项目B要依赖A:

shell 复制代码
pnpm add A --filter B

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

json 复制代码
{
	"dependencies": {
    	"A": "workspace:^",
  },
}

这样,B打包时,A包不需要发布成npm包,B就可以将A一同打包进dist。

同理,需要给B项目添加某个依赖包C,也是如下代码:

shell 复制代码
pnpm add C --filter B

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

shell 复制代码
pnpm i

多项目调试/打包

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

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

shell 复制代码
pnpm add inquirer --D -i

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

shell 复制代码
pnpm add execa --D -i

命令行写法:

js 复制代码
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"
	  })

启动项目的代码:

js 复制代码
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,代码如下:

js 复制代码
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,可以用调整输出文字的颜色:

js 复制代码
// 当前项目用绿色加粗
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文件服务。

js 复制代码
// 引入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

env 复制代码
VITE_MODEL_PATH=http://localhost:5180/

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

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

env 复制代码
VITE_MODEL_PATH=/

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

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

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

js 复制代码
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专栏,鼓励我持续产出,谢谢!

相关推荐
橙子小哥的代码世界7 分钟前
【深度学习】05-RNN循环神经网络-02- RNN循环神经网络的发展历史与演化趋势/LSTM/GRU/Transformer
人工智能·pytorch·rnn·深度学习·神经网络·lstm·transformer
985小水博一枚呀2 小时前
【深度学习基础模型】神经图灵机(Neural Turing Machines, NTM)详细理解并附实现代码。
人工智能·python·rnn·深度学习·lstm·ntm
万叶学编程2 小时前
Day02-JavaScript-Vue
前端·javascript·vue.js
SEU-WYL3 小时前
基于深度学习的任务序列中的快速适应
人工智能·深度学习
OCR_wintone4213 小时前
中安未来 OCR—— 开启高效驾驶证识别新时代
人工智能·汽车·ocr
matlabgoodboy3 小时前
“图像识别技术:重塑生活与工作的未来”
大数据·人工智能·生活
最近好楠啊3 小时前
Pytorch实现RNN实验
人工智能·pytorch·rnn
OCR_wintone4213 小时前
中安未来 OCR—— 开启文字识别新时代
人工智能·深度学习·ocr
学步_技术3 小时前
自动驾驶系列—全面解析自动驾驶线控制动技术:智能驾驶的关键执行器
人工智能·机器学习·自动驾驶·线控系统·制动系统