【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专栏,鼓励我持续产出,谢谢!

相关推荐
_AaronWong1 小时前
Electron 实现仿豆包划词取词功能:从 AI 生成到落地踩坑记
前端·javascript·vue.js
西门老铁1 小时前
🦞OpenClaw 让 MacMini 脱销了,而我拿出了6年陈的安卓机
人工智能
cxxcode1 小时前
I/O 多路复用:从浏览器到 Linux 内核
前端
用户5433081441941 小时前
AI 时代,前端逆向的门槛已经低到离谱 — 以 Upwork 为例
前端
JarvanMo1 小时前
Flutter 版本的 material_ui 已经上架 pub.dev 啦!快来抢先体验吧。
前端
恋猫de小郭2 小时前
AI 可以让 WIFI 实现监控室内人体位置和姿态,无需摄像头?
前端·人工智能·ai编程
哀木2 小时前
给自己整一个 claude code,解锁编程新姿势
前端
程序员鱼皮2 小时前
GitHub 关注突破 2w,我总结了 10 个涨星涨粉技巧!
前端·后端·github
UrbanJazzerati2 小时前
Vue3 父子组件通信完全指南
前端·面试
是一碗螺丝粉2 小时前
5分钟上手LangChain.js:用DeepSeek给你的App加上AI能力
前端·人工智能·langchain