原文:https://www.coderutil.com/article?id=203
一、前言
AI时代是一个快速发展的时代,它带来了很多机会和挑战。对于普通人来说,AI技术的应用可以帮助我们更高效地完成工作,提高生产力和工作质量。同时,AI技术也可以帮助我们更好地理解和解决问题,提高我们的智能水平。
然而,AI技术的应用也可能会给我们的生活和工作带来一些挑战。例如,AI技术可能会取代一些人类的工作,导致失业率的上升。此外,AI技术的发展也需要我们不断学习和适应,以保持其竞争力和适应性。
总之,AI时代对于普通人来说是一个充满机遇和挑战的时代。我们需要不断学习和适应,以保持我们的竞争力和适应性。
————以上内容由「文新一言」生成
思考:盒子能够做点什么?要不先接入个文生图玩一玩,刚好前两天百度智能云文心模型开放了文生图的API,那整一下呗……
二、画聊 - ShowCase
2.1 服务名称
画聊:依附于C圈交流社区,社区文生图服务。
2.1 服务入口
2.2 绘画编辑器
2.3 一键分享到C圈
这是效果演示,有兴趣的大家自己去体验,因为文生图的API服务现在费用还是很高的,所以我们目前每天限量前500名用户可以体验,每天每天限量免费3次,望理解。
三、申请流程
3.1 申请页面-百度文新一言大模型主页: https://cloud.baidu.com/wenxin.html
3.2 底部 AI开放能力/ 多模态内容生成/文心AI作画:
3.3 购买用量
按需购买就可以,一开始不用买太多,这里地方是文心开放的基础版能力(支持个人用户接入),高级版(作画效果更好)目前只支持企业介入(需要联系客服申请),我们就以个人为例,可以先购买600张图:
3.4点击立即使用:
需要先创建自己的应用(按照要求填写应用信息即可,比较简单),获取AK、SK:
现在文新AI作画服务我们已经申请购买好,做好前期准备工作后,接下来就是API对接啦(作为多年的API调用工程师,这个不是什么难事)
四、接入核心流程分析
4.1 API接口文档了解接入流程
接口文档: https://cloud.baidu.com/doc/NLP/s/Ml9i5amtk
4.2 接入流程梳理
接入流程主要有几下四个核心步骤:
1、请求百度智能云Oauth2.0结果获取token,token默认有效期30天;
2、业务册自己缓存token到redis;
3、携带token+body请求文新文生图模型api,提交生图任务,文心api返回本次提交的任务id;
4、业务测根据taskId轮训请求文心文生图结果api,查询生成结果。
4.3 模型设计
关于模型设计,有以下几个问题需要考虑到:
问题一:我现在选用的相对低价格的套餐,但是限流1qps,如果只是我个人使用的话这个并发支持量肯定没有任何问题的,但是如果要在盒子开放能力,那用户量大一点,并发高一点的时候1qps扛不住,会出现因为限流导致很多请求失败的问题,怎么解决?
方案:消息队列提交任务,所有请求排队处理,可以慢一点,但是要保障可用性。
问题二:毕竟是付费产品包,处于安全性、调用量可控的考虑,业务侧也需要做限流处理,不然一下子上百万的调用量要了命
方案:强制登录后使用,然后BY账号每次限流3到5次。
问题三:轮训还是socket?在4.2的接入流程中大家也发现了结果生成文新模型api是异步执行的,那就涉及到结果查询怎么相应给客户端?
方案:简单点吧,轮训请求吧,控制好轮训的频次(降低对服务端的压力)
提交任务模型设计:
客户端轮训请求任务id(轮训1):
客户端轮训请求任务结果(轮训2,结束轮训1):
上一步的轮询中我们已经拿到了任务id,现在可以拿着任务id轮训请求任务结果了(补充下,为什么这里要轮训,因为任务生成是有进度的,不是一下子就完成的,或许你的这次请求,任务只完成啦50%,还需要一到两次才能拿到完成的结果)
好啦,整个接入流程非常简单,弄清楚接入流程后就开发代码编码了。
五、技术实现
5.1接口设计
接口定义如下:
/**
* @Author 程序员七七
* @webSite https://www.coderutil.com
* @Date 2023/4/19 23:16
* @description
*/
@Slf4j
@Api(tags = "【AIG Open】文生图服务")
@RestController
@RequestMapping("/api/aig/image")
public class AIGImageController {
/**
* 获取绘画风格
*
* @return
*/
@ValidateLogin
@GetMapping("/list/style")
public APIResponseBean<List<BaiduAiImageStyleResponseVO>> styleList() {
// TODO
return APIResponseBeanUtil.success();
}
/**
* 获取绘画尺寸
*
* @return
*/
@ValidateLogin
@GetMapping("/list/resolution")
public APIResponseBean<List<BaiduAiImageResolutionResponseVO>> resolutionList() {
// TODO
return APIResponseBeanUtil.success();
}
/**
* 获取请求token
*
* @return
*/
@ValidateLogin
@RequestPermission
@GetMapping("/****/***")
public APIResponseBean<String> getAccessToken() {
// TODO
return APIResponseBeanUtil.success();
}
/**
* 提交绘画任务
*
* @return
*/
@ValidateLogin
@SafeClick(event = ClickEvent.SUBMIT, time = 10000, message = "10秒内只能提交一次")
@PostMapping("/task/submit")
public APIResponseBean submitTask(@RequestBody AIGImageCreateRequestVO aigImageCreateRequest) {
// TODO
// 限流1qps,这里需要消息队列提交任务
return APIResponseBeanUtil.success("提交成功,结果生成中...");
}
/**
* 查询任务id
*
* @return
*/
@ValidateLogin
@GetMapping("/getTaskId")
public APIResponseBean<Long> getTaskId() {
// TODO
return APIResponseBeanUtil.success();
}
/**
* 查询任务结果
*
* @return
*/
@ValidateLogin
@GetMapping("/result/{taskId}")
public APIResponseBean<String> getTaskResult(@PathVariable Long taskId) {
// TODO
return APIResponseBeanUtil.success();
}
/**
* 获取今日使用人数
*
* @return
*/
@GetMapping("/useUser/count")
public APIResponseBean<Long> getUseUserCount() {
// TODO
return APIResponseBeanUtil.success();
}
}
5.2 Token获取
应用信息配置:
# AIG properties config
aig.config.baidu.host=https://aip.baidubce.com
aig.config.baidu-text2image-token-url=${aig.config.baidu.host}/oauth/2.0/token?grant_type=client_credentials&client_id=%s&client_secret=%s
aig.config.baidu-text2image-submit-url=${aig.config.baidu.host}/rpc/2.0/ernievilg/v1/txt2img?access_token=%s
aig.config.baidu-text2image-data-url=${aig.config.baidu.host}/rpc/2.0/ernievilg/v1/getImg?access_token=%s
aig.config.baidu-ak=应用AppKey
aig.config.baidu-sk=应用SecretKey
维护配置类:
/**
* @Author 程序员七七
* @webSite https://www.coderutil.com
* @Date 2023/4/19 22:50
* @description
*/
@Data
@Component
@ConfigurationProperties(prefix = "aig.config")
public class AIGPropertiesConfig {
/**
* 百度智能云ak
*/
private String baiduAk;
/**
* sk
*/
private String baiduSk;
/**
* 获取智能创作token
*/
private String baiduText2imageTokenUrl;
/**
* 提交生成图片人物
*/
private String baiduText2imageSubmitUrl;
/**
* 查询生成结果
*/
private String baiduText2imageDataUrl;
}
token请求与缓存:
@Autowired
private AIGPropertiesConfig aigPropertiesConfig;
@Autowired
private RedisService redisService;
/**
* 获取请求token - 从缓存中获取
*
* @return
*/
public String getRequestTokenFromCache() {
String key = RedisKeyEnum.BAIDU_AIG_IMAGE_TOKEN_CACHE.getKey();
String cache = redisService.get(key);
if (StringUtils.isNotBlank(cache)) {
return cache;
}
String token = this.getRequestTokenFromApi();
if (StringUtils.isNotBlank(token)) {
redisService.set(key, token, RedisKeyEnum.BAIDU_AIG_IMAGE_TOKEN_CACHE.getExpireTime());
}
return token;
}
/**
* 获取请求token - API获取
*
* @return
*/
private synchronized String getRequestTokenFromApi() {
String tokenUrl = aigPropertiesConfig.getBaiduText2imageTokenUrl();
String requestTokenApi = String.format(tokenUrl, aigPropertiesConfig.getBaiduAk(), aigPropertiesConfig.getBaiduSk());
Map responseMap = HttpClientUtil.postFormForUrl(requestTokenApi, new HashMap<>(), Map.class);
return String.valueOf(responseMap.get("access_token"));
}
5.3 提交任务
消息队列设计:
这里我是给予redis自己实现了一个清亮的消息队列,你可以使用其他的MQ。
提交任务:
@Autowired
private AIGImageSubmitQueue aigImageSubmitQueue;
/**
* 提交文生图任务到队列
*/
AIGImageMessageDTO aigImageMessage = new AIGImageMessageDTO(req);
aigImageSubmitQueue.submit(aigImageMessage);
任务处理:
/**
* 提交任务
* @param aigImageMessage
*/
public void submit(AIGImageMessageDTO aigImageMessage) {
// 请求参数
BaiduAIImageCreateRequestVO request = aigImageMessage.getRequest();
// 获取token
String token = this.getRequestTokenFromCache();
String submitApi = aigPropertiesConfig.getBaiduText2imageSubmitUrl();
submitApi = String.format(submitApi, token);
try {
BaiduAiImageResponseTaskVO response = HttpClientUtil.postObjectForUrl(submitApi, request, BaiduAiImageResponseTaskVO.class);
this.doHandleTaskSubmitResult(aigImageMessage, response);
} catch (Exception e) {
log.error("AI作图任务提交异常, 参数:{}", JsonUtil.toJsonString(aigImageMessage), e);
}
}
/**
* 处理任务提交结果
*
* @param aigImageMessage
* @param response
*/
private void doHandleTaskSubmitResult(AIGImageMessageDTO aigImageMessage, BaiduAiImageResponseTaskVO response) {
String userId = aigImageMessage.getUserId();
Long taskId = response.getData().getTaskId();
this.refreshUserTaskIdCache(userId, taskId);
}
private void refreshUserTaskIdCache(String userId, Long taskId) {
// 缓存任务ID
String key = RedisKeyEnum.BAIDU_AIG_IMAGE_RESULT_TASK_ID_CACHE.getKey(userId);
redisService.set(key, String.valueOf(taskId), RedisKeyEnum.BAIDU_AIG_IMAGE_RESULT_TASK_ID_CACHE.getExpireTime());
}
5.4 请求结果
/**
* 查询任务结果
*
* @param userId
* @param taskId
* @return
*/
public BaiduAiImageResponseVO getAIGImageResult(String userId, Long taskId) {
Assert.isTrue(taskId != null, "任务ID参数缺失!");
// 安全考虑,登录用户只能查看自己的的任务id对应的结果(从缓存获取最近的taskid)
Long taskIdCache = getUserTaskIdFromCache(userId);
Assert.isTrue(taskId.equals(taskIdCache), "任务ID失效或者无权限查看!");
/**
* 优先从缓存获取 - 也算是对下游服务的一种保护,同时降低自己服务资源的
损耗 */
BaiduAiImageResponseVO response = this.getTaskResultFromCache(taskId);
if (response != null) {
return response;
}
// 缓存中没有,从api获取
response = this.getAIGImageResultFromAPI(userId, taskId);
if (response != null) {
//
this.refreshTaskResult(taskId, response);
}
return response;
}
/**
* 请求api查询生成结果
*
**/
public BaiduAiImageResponseVO getAIGImageResultFromAPI(String userId, Long taskId) {
// 获取token
String token = this.getRequestTokenFromCache();
String dataApi = aigPropertiesConfig.getBaiduText2imageDataUrl();
dataApi = String.format(dataApi, token);
Map<String, String> body = new HashMap<>();
body.put("taskId", String.valueOf(taskId));
try {
BaiduAiImageResponseVO response = HttpClientUtil.postObjectForUrl(dataApi, body, BaiduAiImageResponseVO.class);
if (response != null && response.getData() != null && response.getData().getStatus().equals(1)) {
// 图片已生成
response.getData().setImgUrls(new ArrayList<>());
// 图片结果上传到自己的存储服务(不使用原图,避免原图失效过期)
String img = response.getData().getImg();
// 下载百度云的网络图片到本地,并上传到盒子自己的OSS存储
UploadResultVO ossResult = fileUploadService.downloadNetworkImageAndUploadOSS(img, OSS_AIG_IMAGE_PATH, "png");
response.getData().setImg(ossResult.getUrl());
return response;
}
} catch (Exception e) {
log.error("AI作图任务结果查询异常, userId:{}, taskId:{}", userId, taskId, e);
}
return null;
}
5.5 前端实现
前端实现没什么好分享的(前端太差了我),就分享下我用的轮训方法:
需要注意加锁,拿到结果后就不要一直发起无效的请求啦
原文:https://www.coderutil.com/article?id=203