Skip to content

Commit 82192ca

Browse files
committed
Java: MultiDataSource 新增支持 CVAuto 上传视频并自动抽帧
1 parent e467151 commit 82192ca

File tree

5 files changed

+193
-85
lines changed

5 files changed

+193
-85
lines changed

APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/README.md

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
CVAuto
33
</h1>
44

5-
<p align="center">👁 <b>零代码零标注 CV AI 自动化测试平台</b> 🚀 <br><b>零代码快速自动化测试 CV 计算机视觉 AI 人工智能图像识别算法的功能、效果、性能</b><br>适合 算法测试 工程师/专家、AI/机器学习/算法 工程师/专家/研究员/科学家、算法 应用/开发 工程师/专家 等</p>
5+
<p align="center">👁 <b>零代码零标注 CV AI 自动化测试平台</b> 🚀 <br><b>零代码快速自动化测试 CV 计算机视觉 AI 人工智能图像识别算法的功能、效果、性能</b><br>适合 算法 应用/开发 工程师/专家、算法测试 工程师/专家、AI/机器学习/算法 工程师/专家/研究员/科学家 等</p>
66

77
<p align="center" >
88
<a href="https://deepwiki.com/TommyLemon/CVAuto">English</a>
@@ -212,27 +212,23 @@ https://github.com/TommyLemon/APIAuto/issues
212212
<br />
213213
<br />
214214

215-
### 其它项目
215+
### 生态项目
216216

217-
[APIJSON](https://github.com/Tencent/APIJSON) 🚀 腾讯零代码、全功能、强安全 ORM 库 🏆 后端接口和文档零代码,前端(客户端) 定制返回 JSON 的数据和结构
217+
[APIJSON](https://github.com/Tencent/APIJSON) 🏆 腾讯实时 零代码、全功能、强安全 ORM 库 🚀 后端接口和文档零代码,前端(客户端) 定制返回 JSON 的数据和结构
218218

219-
[APIAuto](https://github.com/TommyLemon/APIAuto) 敏捷开发最强大易用的 HTTP 接口工具,机器学习零代码测试、生成代码与静态检查、生成文档与光标悬浮注释,集 文档、测试、Mock、调试、管理 于一体的一站式体验
219+
[APIAuto](https://github.com/TommyLemon/APIAuto) ☔ 敏捷开发最强大易用的接口工具,零代码测试与 AI 问答、生成代码与静态检查、生成文档与光标悬浮注释,腾讯、华为、SHEIN、传音、工行等使用
220220

221-
[UnitAuto](https://github.com/TommyLemon/UnitAuto) 机器学习单元测试平台,零代码、全方位、自动化 测试 方法/函数 的正确性和可用性
221+
[UnitAuto](https://github.com/TommyLemon/UnitAuto) ☀️ 最先进、最省事、ROI 最高的单元测试,零代码、全方位、自动化 测试 方法/函数,用户包含腾讯、快手、某 500 强巨头等
222222

223-
[SQLAuto](https://github.com/TommyLemon/SQLAuto) 智能零代码自动化测试 SQL 语句执行结果的数据库工具,任意增删改查、任意 SQL 模板变量、一键批量生成参数组合、快速构造大量测试数据
224-
225-
[UIGO](https://github.com/TommyLemon/UIGO) 📱 零代码快准稳 UI 智能录制回放平台 🚀 自动兼容任意宽高比分辨率屏幕,自动精准等待网络请求,录制回放快、准、稳!
223+
[SQLAuto](https://github.com/TommyLemon/SQLAuto) 智能零代码自动化测试 SQL 数据库工具,任意增删改查、任意 SQL 模板变量、一键批量生成参数组合、快速构造大量测试数据
226224

225+
[UIGO](https://github.com/TommyLemon/UIGO) 📱 零代码快准稳 UI 智能录制回放平台 🚀 3 像素内自动精准定位,2 毫秒内自动精准等待,用户包含腾讯,微信团队邀请分享
227226

228227
### 持续更新
229228
https://github.com/TommyLemon/CVAuto/commits
230229

231-
232230
### 我要赞赏
233-
**创作不易、坚持更难,右上角点亮 ⭐ Star 支持/收藏下本项目吧,谢谢 ^_^** <br />
231+
**创作不易、坚持更难,右上角点亮 ⭐ Star 收藏/支持下本项目吧,谢谢 ^_^** <br />
234232
https://github.com/TommyLemon/CVAuto
235233

236234

237-
238-
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*Copyright ©2025 TommyLemon(https://github.com/TommyLemon/CVAuto)
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use FileUtil file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.*/
14+
15+
16+
/**util for string
17+
* @author Lemon
18+
*/
19+
var FileUtil = {
20+
TAG: 'FileUtil',
21+
22+
/**
23+
* 抽取某一帧,输出 Blob
24+
*/
25+
captureFrame: function(video, time) {
26+
return new Promise((resolve) => {
27+
const v = document.createElement("video");
28+
v.src = video.src;
29+
v.currentTime = time;
30+
31+
v.onseeked = () => {
32+
const canvas = document.createElement("canvas");
33+
const w = video.videoWidth;
34+
const h = video.videoHeight;
35+
canvas.width = w;
36+
canvas.height = h;
37+
const ctx = canvas.getContext("2d");
38+
ctx.drawImage(v, 0, 0, w, h);
39+
canvas.toBlob((blob) => resolve(blob), "image/jpeg", 0.8);
40+
};
41+
});
42+
},
43+
};
44+
45+
if (typeof module == 'object') {
46+
module.exports = FileUtil;
47+
}
48+
49+
//校正(自动补全等)字符串>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/debug.log

Lines changed: 0 additions & 6 deletions
This file was deleted.

APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/index.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
</span>
9090
<a v-show="isDelayShow" >2s</a>
9191

92-
<input type="file" id="imageInput" accept="image/*" style="opacity: 0; width: 1px; display: none" :multiple="allowMultiple" @change="handleFileSelect($event)" >
92+
<input type="file" id="imageInput" accept="image/*,video/*" style="opacity: 0; width: 1px; display: none" :multiple="allowMultiple" @change="handleFileSelect($event)" >
9393

9494
<iframe v-show="User.id == null || User.id <= 0" src="https://ghbtns.com/github-btn.html?user=Tencent&amp;repo=APIJSON&amp;type=star&amp;count=true&amp;size=small" frameborder="0" scrolling="0" width="160px" height="18px"></iframe>
9595

@@ -509,7 +509,7 @@
509509
<li style="width: 100%; background-color: white; " v-for="(item, index) in randomSubs" :id="'randomSubItem' + index" >
510510
<img id="vRandomSubImg" :src="(item.Random || {}).img || 'img/add_light.png'" style="height: 90px; min-width: 90px" @click="onClickAddRandom(currentRandomIndex, index)">
511511

512-
<a class="hint--rounded hint--no-animate" ref="randomSubTexts" @mouseover="setRequestHint(index, item, true)" href="javascript:void(0)" @click="restoreRandom(index, item)" :style="{ color: index == currentRandomSubIndex ? 'black' : 'gray' }">{{ Math.round(100*((item.Random || {}).size || 0)/8/1024/1024)/100 + 'M ' + ((item.Random || {}).width || '') + "x" + ((item.Random || {}).height || '') }}</a>
512+
<a class="hint--rounded hint--no-animate" ref="randomSubTexts" @mouseover="setRequestHint(index, item, true)" href="javascript:void(0)" @click="restoreRandom(index, item)" :style="{ color: index == currentRandomSubIndex ? 'black' : 'gray' }">{{ Math.round(100*((item.Random || {}).size || 0)/1024/1024)/100 + 'M ' + ((item.Random || {}).width || '') + "x" + ((item.Random || {}).height || '') }}</a>
513513
<input style="width: 96%" v-model="(item.Random || {}).name" @keyup="doOnKeyUp(event, 'randomSub', false, item)" />
514514

515515
<div :style="{ background: item.compareColor }" v-show="! isRandomEditable && item.compareType != null" style="position: absolute;top: 8px;right: 36px;display: inline-block;">
@@ -1080,6 +1080,7 @@
10801080
<script type="text/javascript" language="JavaScript" charset="UTF-8" src="apijson/JSONRequest.js" ></script>
10811081
<script type="text/javascript" language="JavaScript" charset="UTF-8" src="apijson/JSONResponse.js" ></script>
10821082
<script type="text/javascript" language="JavaScript" charset="UTF-8" src="apijson/CodeUtil.js" ></script>
1083+
<script type="text/javascript" language="JavaScript" charset="UTF-8" src="apijson/FileUtil.js" ></script>
10831084
<!-- 必须在main.js前 TODO 可能有冲突,代码写入vue文件? >>>>>>>>>>>>>>>> -->
10841085
<script src="https://unpkg.com/json5@^2.0.0/dist/index.min.js"></script>
10851086

APIJSON-Java-Server/APIJSONBoot-MultiDataSource/src/main/resources/static/cv/js/main.js

Lines changed: 133 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
var Vue = require('./vue.min'); // 某些版本不兼容 require('vue');
4848
var StringUtil = require('../apijson/StringUtil');
4949
var CodeUtil = require('../apijson/CodeUtil');
50+
var FileUtil = require('../apijson/FileUtil');
5051
var JSONObject = require('../apijson/JSONObject');
5152
var JSONResponse = require('../apijson/JSONResponse');
5253
var JSONRequest = require('../apijson/JSONRequest');
@@ -6784,97 +6785,164 @@ https://github.com/Tencent/APIJSON/issues
67846785
const doc = (this.currentRemoteItem || {}).Document || {}
67856786
const random = cri.Random || {}
67866787
const ind = isSub && this.currentRandomSubIndex != null ? this.currentRandomSubIndex : this.currentRandomIndex;
6788+
const index = ind != null && ind >= 0 ? ind : -1; // items.length;
6789+
const server = this.server
67876790

67886791
var selectedFiles = Array.from(event.target.files);
67896792
// const previewList = document.getElementById('previewList');
67906793
// previewList.innerHTML = '';
67916794

67926795
selectedFiles.forEach((file, i) => {
67936796
const reader = new FileReader();
6794-
reader.onload = (e) => {
6797+
reader.onload = (evt) => {
67956798
// const img = document.createElement('img');
6796-
// img.src = e.target.result;
6799+
// img.src = evt.target.result;
67976800
// img.style.height = '100%';
67986801
// img.style.margin = '1px';
67996802
// previewList.appendChild(img);
68006803

6801-
const index = ind != null && ind >= 0 ? ind : -1; // items.length;
6802-
var item = JSONResponse.deepMerge({
6803-
Random: {
6804-
id: -(index || 0) - 1, //表示未上传
6805-
toId: random.id,
6806-
userId: random.userId || doc.userId,
6807-
documentId: random.documentId || doc.id,
6808-
count: 1,
6809-
name: '分析位于 ' + index + ' 的这张图片',
6810-
img: e.target.result,
6811-
config: ''
6804+
function callback(file, img, name, size, width, height, index, rank) {
6805+
6806+
var item = JSONResponse.deepMerge({
6807+
Random: {
6808+
id: -(index || 0) - 1, //表示未上传
6809+
toId: random.id,
6810+
userId: random.userId || doc.userId,
6811+
documentId: random.documentId || doc.id,
6812+
count: 1,
6813+
name: '分析位于 ' + index + ' 的这张图片',
6814+
img: img,
6815+
config: ''
6816+
}
6817+
}, items[index] || {});
6818+
item.status = 'uploading';
6819+
6820+
const r = item.Random || {};
6821+
r.name = r.file = name;
6822+
r.size = size;
6823+
r.width = width;
6824+
r.height = height;
6825+
r.rank = rank;
6826+
6827+
if (index < 0) { // || r.id == null || r.id <= 0) {
6828+
items.unshift(item);
6829+
} else {
6830+
items[index] = item;
68126831
}
6813-
}, items[index] || {});
6814-
item.status = 'uploading';
68156832

6816-
const r = item.Random || {};
6817-
r.name = r.file = file.name;
6818-
r.size = file.size;
6819-
r.width = file.width;
6820-
r.height = file.height;
6821-
6822-
if (index < 0) { // || r.id == null || r.id <= 0) {
6823-
items.unshift(item);
6824-
} else {
6825-
items[index] = item;
6826-
}
6833+
if (isSub) {
6834+
App.randomSubs = items;
6835+
} else {
6836+
App.randoms = items;
6837+
}
68276838

6828-
if (isSub) {
6829-
this.randomSubs = items;
6830-
}
6831-
else {
6832-
this.randoms = items;
6833-
}
6839+
try {
6840+
Vue.set(items, index < 0 ? 0 : index, item);
6841+
} catch (e) {
6842+
console.error(e)
6843+
}
68346844

6835-
try {
6836-
Vue.set(items, index < 0 ? 0 : index, item);
6837-
} catch (e) {
6838-
console.error(e)
6839-
}
6845+
const formData = new FormData();
6846+
formData.append('file', file);
68406847

6841-
const formData = new FormData();
6842-
formData.append('file', file);
6848+
fetch(server + '/upload', {
6849+
method: 'POST',
6850+
body: formData
6851+
})
6852+
.then(response => response.json())
6853+
.then(data => {
6854+
var path = data.path;
6855+
if (StringUtil.isEmpty(path, true) || data.size == null) {
6856+
throw new Error('上传失败!' + JSON.stringify(data || {}));
6857+
}
68436858

6844-
fetch(this.server + '/upload', {
6845-
method: 'POST',
6846-
body: formData
6847-
})
6848-
.then(response => response.json())
6849-
.then(data => {
6850-
var path = data.path;
6851-
if (StringUtil.isEmpty(path, true) || data.size == null) {
6852-
throw new Error('上传失败!' + JSON.stringify(data || {}));
6853-
}
6859+
console.log('Upload successful:', data);
6860+
item.status = 'done';
6861+
if (!(server.includes('localhost') || server.includes('127.0.0.1'))) {
6862+
r.img = (path.startsWith('/') ? server + path : path) || r.img;
6863+
}
68546864

6855-
console.log('Upload successful:', data);
6856-
item.status = 'done';
6857-
if (! (App.server.includes('localhost') || App.server.includes('127.0.0.1'))) {
6858-
r.img = (path.startsWith('/') ? App.server + path : path) || r.img;
6859-
}
6865+
try {
6866+
Vue.set(items, index < 0 ? 0 : index, item);
6867+
} catch (e) {
6868+
console.error(e)
6869+
}
68606870

6861-
try {
6862-
Vue.set(items, index < 0 ? 0 : index, item);
6863-
} catch (e) {
6864-
console.error(e)
6865-
}
6871+
App.updateRandom(r)
6872+
})
6873+
.catch(error => {
6874+
console.error('Upload failed:', error);
6875+
item.status = 'failed';
6876+
});
6877+
}
6878+
6879+
if (file.type && file.type.startsWith("video/")) {
6880+
// === 视频处理 ===
6881+
this.extractFramesAndUpload(file, 5, async (frameBlob, rank, totalFrames) => {
6882+
const reader2 = new FileReader();
6883+
reader2.onload = (evt) => { // TODO 传 toId,视频作为分组,次数作为抽取帧数
6884+
var fn = file.name + '-' + rank + '.jpg'
6885+
callback(new File([frameBlob], fn), reader2.result, fn, frameBlob.size, frameBlob.width, frameBlob.height, -1, rank)
6886+
}
6887+
reader2.readAsDataURL(frameBlob);
6888+
});
6889+
} else {
6890+
callback(file, reader.result, file.name, file.size, file.width, file.height, index)
6891+
}
68666892

6867-
App.updateRandom(r)
6868-
})
6869-
.catch(error => {
6870-
console.error('Upload failed:', error);
6871-
item.status = 'failed';
6872-
});
68736893
};
68746894
reader.readAsDataURL(file);
68756895
});
68766896
},
68776897

6898+
/**
6899+
* 从视频抽帧(倒序),支持并发上传
6900+
* @param {File} file - 视频文件
6901+
* @param {number} concurrency - 最大并发数
6902+
* @param {function} onFrameReady - 回调(frameBlob, rank, totalFrames)
6903+
*/
6904+
extractFramesAndUpload: function(file, concurrency, onFrameReady) {
6905+
const url = URL.createObjectURL(file);
6906+
const video = document.createElement("video");
6907+
video.src = url;
6908+
video.preload = "metadata";
6909+
6910+
video.onloadedmetadata = async () => {
6911+
const duration = video.duration;
6912+
let timestamps = [];
6913+
6914+
if (duration <= 100) {
6915+
for (let t = Math.floor(duration); t >= 0; t--) timestamps.push(t);
6916+
} else {
6917+
const step = duration / 100;
6918+
for (let i = 100; i >= 0; i--) timestamps.push(i * step);
6919+
}
6920+
6921+
const totalFrames = timestamps.length;
6922+
let index = 0;
6923+
6924+
async function worker() {
6925+
while (index < totalFrames) {
6926+
const i = index++;
6927+
const time = timestamps[i];
6928+
const frameBlob = await FileUtil.captureFrame(video, time);
6929+
// rank 按倒序:0 表示最新帧,totalFrames-1 表示最早帧
6930+
const rank = i;
6931+
onFrameReady(frameBlob, rank, totalFrames);
6932+
}
6933+
}
6934+
6935+
// 并发执行
6936+
const workers = [];
6937+
for (let i = 0; i < concurrency; i++) {
6938+
workers.push(worker());
6939+
}
6940+
await Promise.all(workers);
6941+
6942+
URL.revokeObjectURL(url);
6943+
};
6944+
},
6945+
68786946
uploadImage: function(randomIndex, randomSubIndex) {
68796947
const isSub = randomSubIndex != null;
68806948
const items = (isSub ? this.randomSubs : this.randoms) || [];

0 commit comments

Comments
 (0)