likes
comments
collection
share

和妹子逛完街,写了个 AI 智能穿搭系统背景 故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个

作者站长头像
站长
· 阅读数 50

想直接看成品演示的可以直接划到文章底部

背景

故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个小时。虽然这个时间不多, 但这个时间黑神话悟空足矣让我打完虎先锋

回家我就灵光一闪,是不是可以搞一个AI智能穿搭,只需要上传自己的照片和对应的衣服图片就能实现在线试衣服呢?

说干就干,我就开始构思方案,画原型。 俗话说万事开头难,事实上这个构思到动工就耗费了我一个礼拜,因为一直在构思怎么样的交互场景会让用户使用起来比较丝滑,并且容易上手。

目前实现的功能有:

  • ✅ 用户信息展示
  • ✅ AI 生成穿搭
  • ✅ 风格大厅

待完成:

  • 私人衣柜
  • AI 换鞋

经过

1. 画产品原型

起初第一个版本的产品原型由于是自己构思没有任何参考,直接上手撸代码的,想到啥就画啥,所以布局非常传统,配色也非常普通(蚂蚁蓝),所以感觉没有太多的时尚气息(个人觉得丑的一逼,不像是互联网的产物)。因为重构掉了,老的现在没有了,我懒就不重新找回来截图了,直接画个当时的样子,大概长成下面这样:

和妹子逛完街,写了个 AI 智能穿搭系统背景 故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个

丑的我忍不了,我就去设计师专门用的网站参(chao)考(xi)了一下,找来找去,终于有了下面的最终版原型图

和妹子逛完街,写了个 AI 智能穿搭系统背景 故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个

2. 配色选择

大家知道,所有的UI设计,都离不开主题色的选择,比如:淘宝橙、飞猪橙、果粒橙...,目的一方面是为了打造品牌形象,另一方面也是为了提升品牌辨识度,让你看到这个颜色就会想起它

那我必须也得跟上时代的潮流,选了 #c1a57b 这款低调而又不失奢华的色值作为主题色,英雄不问出处,问就是借鉴。

3. 技术选型

我对技术的定义是:技术永远服务于产品,能高效全面帮助我开发出一款应用,并且能保证后续的稳定性和可维护性,啥技术我都行。当然如果这门技术我优先会从我属性的板块去找。

经过各种权衡和比较,最后敲定下来了技术选型方案:

  • 前端:taro (为了后续可能会有小程序端做准备)
  • 后端:koajs (实际使用的是midway,基于koajs,主要是比较喜欢koa的轻量化架构)
  • 数据库:mongodb (别问,问就是简单易上手)
  • 代码仓库:gitea
  • CI:gitea-runner
  • 部署工具:pm2
  • 静态文件托管:阿里云OSS

4. 撸代码

这里我只挑一些个人感觉相对需要注意的地方展开讲讲

4.1 图片转存

由于我生成图片的API图片链接会在一天之后失效,所以我需要在调用任务详情的时候,把这个文件转存到我自己的oss服务器,这里我总结出来的思路是:【1. 保存在本地暂存文件夹】-【2. 调用node流式读取接口】-【3. 保存到oss】-【4. 返回替换原来的链接】

具体代码参考如下:

const tempDir = path.join(tmpdir(), 'temp-upload-files')
const link = url.parse(src);
const fileName = path.basename(link.pathname)
const localPath = path.join(tempDir, `/${fileName}`); // 生成保存路径
let request
if (link.protocol === 'https:') {
    request = https
} else {
    request = http
}
request.get(src, async (response) => {
    const fileStream = await fs.createWriteStream(localPath);  // 保存到本地暂存路径
    
    await response.pipe(fileStream);
    fileStream.on("error", (error) => {
      console.error("保存图片出错:", error);
      reject(error)
    });
    fileStream.on('finish', async res => {
      console.log('暂存完成,开始上传:', res)
      let result = await this.ossService.put(`/${params.saveDir || 'tmp'}/${fileName}`, localPath);
      if (!result) return
      resolve(result)
    });
});

这里的request因为我不想引入其它的库所以这样写,如果有更好的方案,可以在评论区告知一下。

这里需要注意的一个地方是,上传的这个 localPath 最好是自己做一下处理,我这边没有处理,因为可能两个用户同时上传,他们的文件名称相同的时候,可能会出现覆盖的情况,包括后面的oss保存也是。

4.2 文件流式上传中间件

因为默认的接口处理是不处理流式调用的,所以需要自己创建一个中间件来拦截处理一下,下面给出我的参考代码:


class SSE {
    ctx: Context
    constructor(ctx: Context) {
        ctx.status = 200;
        ctx.set('Content-Type', 'text/event-stream');
        ctx.set('Cache-Control', 'no-cache');
        ctx.set('Connection', 'keep-alive');
        ctx.res.writeHead(200, {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
            'Transfer-Encoding': 'chunked'
        });
        ctx.res.flushHeaders();
        this.ctx = ctx;
    }
    send(data: any) {
        // string
        if (typeof data === "string") {
            this.push(data);
        } else if (data.id) {
            this.push(`id: ${data.id}\n`);
        } else if (data.event) {
            this.push(`event: ${data.event}\n`);
        } else {
          const text = JSON.stringify(data)
          this.push(`data: ${text}\n\n`);
        }
    }
    push(data: any) {
        this.ctx.res.write(data);
        this.ctx.res.flushHeaders();
    }
    close() {
        this.ctx.res.end();
    }
}

@Middleware()
export class StreamMiddleware implements IMiddleware<Context, NextFunction> {
  // ?------------ 中间件处理逻辑 -----------------
    resolve() {
        return async (ctx: Context, next: NextFunction) => {

            if (ctx.res.headersSent) {
                if (!ctx.sse) {
                    console.error('[sse]: response headers already sent, unable to create sse stream');
                }
                return await next();
            }

            const sse = new SSE(ctx);
            ctx.sse = sse;
            await next();

            if (!ctx.body) {
                ctx.body = ctx.sse;
            } else {
                ctx.sse.send(ctx.body);
                ctx.body = sse;
            }
        };
    }

    public match(ctx: Context): boolean {
      // ?------------ 不带 stream 前缀默认都不是流式接口 -----------------
      if (ctx.path.indexOf('stream') < 0) return false
    }

    static getName(): string {
      return 'stream';
    }
}

4.3 mongodb 数据库的权限

这里尽量不要使用root权限的数据库角色,可以创建一个只有当前数据库权限的角色,具体可以网上找相关文档,怎么为某个collection创建账户。

实机演示

1. 提交素材,创建任务

和妹子逛完街,写了个 AI 智能穿搭系统背景 故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个

2. 获取生成图片

和妹子逛完街,写了个 AI 智能穿搭系统背景 故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个

3. 展示大厅(待完善)

和妹子逛完街,写了个 AI 智能穿搭系统背景 故事起源在和一个妹子去逛衣服店的时候,试来试去的难以取舍,最终消耗了我一个

结语

当然现在目前这个还是内测版本,功能还不够健全,还有很多地方需要打磨,包括用户信息页面的展示是否合理,UI的排版,数据库表的设计等等

通过观察生活用现有的技术创造一些价值,对我来说就是一种幸福且有意义的事儿。

如果想要体验的可以后台私信我。如果你也有很棒的想法想交流一下,也可以私我。

我是dev,下期见(太懒了我,更新频率太低)

个人博客

灵感中心

转载自:https://juejin.cn/post/7407374655109283851
评论
请登录