最近AIGC 简直是杀疯了,领导动不动就让我们在APP 里引入大语言模型,引入AI画图……说搞就搞!本期基于最近在app 里引入AI画图小程序的操作,给大家做一波实践分享。
Scribble Diffusion 是一个简单的在线服务,它使用 AI 将粗略的草图转换为精致的图像,每一张图像都是不同的(而且没有版权困扰)。简单来说,我们只需要「用画笔描绘一张草图,在输入描述后稍等片刻」,随后就会为你生成一幅画。这幅画可以多次生成,每次生成的结果也都大不相同。
Scribble Diffusion 的能力大概是这样的(左边是我画的,右边是 TA 画的)
a photo of grassland with cloud(有云朵的草地)
the sun setting behind the mountains(山后落日)
A lovely kitten(小猫咪呀)
我发现 Scribble Diffusion 作画的能力非常出乎意料,而且可以根据你的描述来定义不同的照片风格(比如照片,油画,素描等等),于是就产生了把这个AI 画图小程序内嵌到公司App中的想法。另外把小程序内嵌App是通过 FinClip 容器技术实现的。(毕竟开箱即用,也不需要做什么配置)。
调研官网之后发现官网中的元素非常简单,正因如此,我觉得把「Scribble Diffusion」搬运到小程序里大概要分这样几步:
- 使用 canvas 实现画板,能够在小程序中进行绘画;
- 提供输入框与生成按钮,能够补充图片的描述和生成的按钮;
- 获取生成的图片链接进行展示;
像那些如何写代码,账号注册和创建小程序的流程,各位看官可以看这里:从零到一,我也能写小程序
看了这篇文章,即使让我现在就从头写一个能够正常运行的小程序,也没有原本想象中的那么难了。
使用小程序实现画板
我们可以使用小程序来实现一个画板,使用 canvas 标签实现画笔功能。用户可以在画板上绘制画作,也可以选择清空画板操作。
下面是一个示例代码:
<!--画布区域-->
<view class="canvas_area">
<canvas id="myCanvas" canvas-id="myCanvas" class="myCanvas"
disable-scroll="false"
bindtouchstart="touchStart"
bindtouchmove="touchMove"
bindtouchend="touchEnd">
</canvas>
</view>
<view class="clearBtn" bindtap="reset">
清空画板
</view>
Page({
data: {
isProcessing: false,
prompt: '',
scribble: null,
pen : 2, //画笔粗细默认值
color : '#000000', // 画笔颜色默认值
result: null,
text: ''
},
startX: 0, //保存X坐标轴变量
startY: 0, //保存X坐标轴变量
onLoad(params) {
wx.createSelectorQuery().select('#myCanvas').context((res) => {
this.context = res.context
}).exec()
},
//手指触摸动作开始
touchStart: function (e) {
//得到触摸点的坐标
this.startX = e.changedTouches[0].x
this.startY = e.changedTouches[0].y
// this.context = wx.createContext()
this.context.setStrokeStyle(this.data.color)
this.context.setLineWidth(this.data.pen)
this.context.setLineCap('round') // 让线条圆润
this.context.beginPath()
},
//手指触摸后移动
touchMove: function (e) {
var startX1 = e.changedTouches[0].x
var startY1 = e.changedTouches[0].y
this.context.moveTo(this.startX, this.startY)
this.context.lineTo(startX1, startY1)
this.context.stroke()
this.startX = startX1;
this.startY = startY1;
//只是一个记录方法调用的容器,用于生成记录绘制行为的actions数组。context跟<canvas/>不存在对应关系,一个context生成画布的绘制动作数组可以应用于多个<canvas/>
wx.drawCanvas({
canvasId: 'myCanvas',
reserve: true,
actions: this.context.getActions() // 获取绘图动作数组
})
},
//手指触摸动作结束
touchEnd: function () {
var imageData = wx.canvasGetImageData({
canvasId: 'myCanvas',
height: 250,
width: 250,
x: 0,
y: 0,
success(res){
return res.data
}
})
},
//清除画板
reset: function(){
this.context.clearRect(0, 0, 400, 400);
this.context.draw(true)
}
})
提供输入框和生成按钮
我们需要提供一个 input 输入框,供用户输入 prompt;同时,我们需要提供一个按钮,点击时会触发响应事件,将 canvas 内容生成图片,同时将 prompt 输入作为参数,提交给服务端进行图片生成。
这里是示例代码:
<!-- 输入框 -->
<view class="imageDes">
<view class="formInput">
<input class="input" type="text" name="go" placeholder="用关键词描述画的内容" bindinput="update"/>
</view>
</view>
Page({
... 省略上述代码
// 更新表单提交按钮状态
update(e){
this.setData({
prompt : e.detail.value
})
},
})
获取生成的图片链接并展示
当用户点击生成图片按钮后,我们会将 canvas 内容和用户输入的 prompt 作为参数提交给服务端进行图片生成。服务端会返回生成的图片链接,我们需要将它展示给用户。
在下面的示例代码中,我们服务端发送 POST 请求,然后解析返回的 JSON 数据,获取图片链接,并将其添加到页面中。用户就可以看到生成的图片了。
<!-- 绘图结果 -->
<view class="result" wx:if="{{result}}">
<view class="resultBox">
<view class="content">
<image class="content" src="{{result}}" mode="aspectFit" />
</view>
<view class="download">
<view class="btn" bindtap="download">
下载
</view>
</view>
</view>
</view>
Page({
... 省略上述代码
async getCanvasImage() {
return new Promise((resolve, reject) => {
wx.canvasToTempFilePath({
x: 0,
y: 0,
width: 250,
height: 250,
destWidth: 250,
destHeight: 250,
canvasId: 'myCanvas',
success(res) {
console.log(res.tempFilePath)
resolve(res.tempFilePath)
},
fail(err) {
console.log(err)
}
})
})
},
async upload(image) {
return new Promise((resolve) => {
wx.uploadFile({
url: 'xxxxxx',
filePath: image,
name: 'file',
success (res){
const data = JSON.parse(res.data)
resolve(data.url)
},
fail(err){
console.log('上传失败')
console.log(err)
}
})
})
},
async sleep(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, time)
})
},
async handleSubmit(e) {
if (!this.data.prompt) {
return
}
wx.showLoading({
title: '生成中',
})
try {
const prompt = this.data.prompt
const image = await this.getCanvasImage()
this.setData({
error: null,
isProcessing: true
});
const url = await this.upload(image)
console.log('图片', url)
const body = {
prompt: prompt,
image: url,
};
const response = await my_fetch.fetch( {
url: "https://scribblediffusion.com/api/predictions",
method: "POST",
headers: {
"Content-Type": "application/json",
},
params: JSON.stringify(body),
});
let prediction = response.data;
console.log('预测', prediction)
if (response.statusCode !== 201) {
wx.showToast({
title: '生成失败',
duration: 2000
})
this.setData({
error: '生成失败'
});
return;
}
while (
prediction.status !== "succeeded" &&
prediction.status !== "failed"
) {
console.log(prediction.status)
await this.sleep(500);
const response = await my_fetch.fetch({
url:"https://scribblediffusion.com/api/predictions/" + prediction.id,
});
prediction = response.data;
if (response.statusCode !== 200) {
this.setData({
error: prediction.detail
});
return;
}
}
if (Array.isArray(prediction.output) && prediction.output.length > 1) {
wx.hideLoading()
this.setData({
isProcessing: false,
result: prediction.output[1]
});
} else {
wx.hideLoading()
wx.showToast({
title: '生成失败',
duration: 2000
})
this.setData({
isProcessing: false,
error: '生成失败'
})
}
} catch (error) {
wx.hideLoading()
console.log(error)
wx.showToast({
title: '生成失败',
duration: 2000
})
}
},
})
生成完小程序之后,再通过 FinClip 上传小程序就可以在 App 获得画板功能啦!我把这个小程序上传到了「FinClip 小程序应用市场」中,你可以扫描下方的二维码随意体验,总的来说,还是挺好玩的。
如果你对小程序与 AI 的结合有什么奇思妙想,欢迎留言探讨!
当然,如果你对 Scribble Diffusion 有更多奇怪的想法想付诸实践,开发者也已经将项目文件进行了开源,欢迎尝试~