feat(board): 更新python_pyodide下 Teachable Machine
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import * as Blockly from 'blockly/core';
|
import * as Blockly from 'blockly/core';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
import { createApp } from 'vue';
|
||||||
import mixlySprite from './pixi-sprite';
|
import mixlySprite from './pixi-sprite';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
@@ -14,8 +15,11 @@ import { KernelLoader } from '@basthon/kernel-loader';
|
|||||||
import StatusBarImage from './statusbar-image';
|
import StatusBarImage from './statusbar-image';
|
||||||
import StatusBarFileSystem from './statusbar-filesystem';
|
import StatusBarFileSystem from './statusbar-filesystem';
|
||||||
import StatusBarTool from './statusbar-tool';
|
import StatusBarTool from './statusbar-tool';
|
||||||
|
import TeachableMachineApp from './teachableMachine/App.vue';
|
||||||
import LOADER_TEMPLATE from '../templates/html/loader.html';
|
import LOADER_TEMPLATE from '../templates/html/loader.html';
|
||||||
|
|
||||||
|
import 'element-plus/dist/index.css';
|
||||||
|
|
||||||
|
|
||||||
export default class PythonShell {
|
export default class PythonShell {
|
||||||
static {
|
static {
|
||||||
@@ -57,6 +61,8 @@ export default class PythonShell {
|
|||||||
this.statusBarImage = StatusBarImage.init();
|
this.statusBarImage = StatusBarImage.init();
|
||||||
this.statusBarFileSystem = StatusBarFileSystem.init();
|
this.statusBarFileSystem = StatusBarFileSystem.init();
|
||||||
this.statusBarTool = StatusBarTool.init();
|
this.statusBarTool = StatusBarTool.init();
|
||||||
|
const teachableMachineApp = createApp(TeachableMachineApp);
|
||||||
|
teachableMachineApp.mount(this.statusBarTool.getContent()[0]);
|
||||||
this.pythonShell = new PythonShell();
|
this.pythonShell = new PythonShell();
|
||||||
this.pyodide = window.pyodide;
|
this.pyodide = window.pyodide;
|
||||||
this.interruptBuffer = new Uint8Array(new ArrayBuffer(1));
|
this.interruptBuffer = new Uint8Array(new ArrayBuffer(1));
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, provide, reactive, ref } from 'vue';
|
||||||
|
import { ElConfigProvider } from 'element-plus';
|
||||||
|
import teachableModel from './components/teachableModel.vue';
|
||||||
|
|
||||||
|
// import 'element-plus/theme-chalk/el-message.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-message-box.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-notification.css';
|
||||||
|
// import './styles/index.scss';
|
||||||
|
|
||||||
|
|
||||||
|
const userInfo = reactive({
|
||||||
|
is_Login: false,
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiurl = ref('http://127.0.0.1:5174')
|
||||||
|
provide('apiurl', apiurl.value)
|
||||||
|
|
||||||
|
provide('userInfo', userInfo)
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取当前的LS里的已经登录的信息
|
||||||
|
if (
|
||||||
|
localStorage.getItem('myAIplatformUsername')
|
||||||
|
&& localStorage.getItem('myAIplatformPassword')
|
||||||
|
) {
|
||||||
|
userInfo.is_Login = true
|
||||||
|
userInfo.username = localStorage.getItem('myAIplatformUsername')
|
||||||
|
userInfo.password = localStorage.getItem('myAIplatformPassword')
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
userInfo.is_Login = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElConfigProvider>
|
||||||
|
<teachableModel />
|
||||||
|
</ElConfigProvider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* #app {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--ep-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
height: calc(100vh - var(--ep-menu-item-height) - 4px);
|
||||||
|
background-color: #feffff;
|
||||||
|
} */
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
<script setup>
|
||||||
|
import { provide, ref } from 'vue';
|
||||||
|
import { CirclePlusFilled } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage, ElSplitter, ElSplitterPanel, ElCard, ElButton, ElIcon } from 'element-plus';
|
||||||
|
import ClassBox from './teachableModel/ClassBox.vue';
|
||||||
|
import CameraBox from './teachableModel/CameraBox.vue';
|
||||||
|
import ModelArea from './teachableModel/ModelArea.vue';
|
||||||
|
import TrainArea from './teachableModel/TrainArea.vue';
|
||||||
|
|
||||||
|
// import 'element-plus/theme-chalk/src/message.scss';
|
||||||
|
// import 'element-plus/theme-chalk/src/splitter.scss';
|
||||||
|
// import 'element-plus/theme-chalk/src/splitter-panel.scss';
|
||||||
|
// import 'element-plus/theme-chalk/src/card.scss';
|
||||||
|
// import 'element-plus/theme-chalk/src/button.scss';
|
||||||
|
// import 'element-plus/theme-chalk/src/icon.scss';
|
||||||
|
|
||||||
|
|
||||||
|
// 存放所有拍摄的内容
|
||||||
|
const shotList = ref([])
|
||||||
|
provide('shotList', shotList)
|
||||||
|
|
||||||
|
// 存放类别和每个类别内含有的图片样本
|
||||||
|
const picList = ref([
|
||||||
|
{ title: '类别 1', list: [], disabled: false },
|
||||||
|
{ title: '类别 2', list: [], disabled: false },
|
||||||
|
])
|
||||||
|
provide('picList', picList)
|
||||||
|
|
||||||
|
// 训练状态
|
||||||
|
const states = ref({
|
||||||
|
isTraining: 0, // 0为未开始,1为训练中,2为训练完成
|
||||||
|
})
|
||||||
|
provide('states', states)
|
||||||
|
|
||||||
|
// 向某个类别添加一个样本
|
||||||
|
async function handleAddSample(idx, data) {
|
||||||
|
// 类别已被禁用
|
||||||
|
if (picList.value[idx].disabled) {
|
||||||
|
ElMessage.error('该类别已被禁用')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 可能传入 base64 字符串上传图片
|
||||||
|
if (data) {
|
||||||
|
// data 是 base64,说明是上传图片
|
||||||
|
picList.value[idx].list.push(data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 否则是拍摄,调用摄像头拍摄
|
||||||
|
await handleParentShot()
|
||||||
|
// 检查是否成功拍摄到图片
|
||||||
|
if (
|
||||||
|
!shotList.value[shotList.value.length - 1]
|
||||||
|
|| shotList.value[shotList.value.length - 1] === 'data:,'
|
||||||
|
) {
|
||||||
|
ElMessage.error('未获取到有效样本')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 拍摄到的图片添加到类别中
|
||||||
|
picList.value[idx].list.push(shotList.value[shotList.value.length - 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加一个类别
|
||||||
|
function handleAddClass() {
|
||||||
|
picList.value.push({
|
||||||
|
title: `类别 ${picList.value.length + 1}`,
|
||||||
|
list: [],
|
||||||
|
disabled: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除一个类别
|
||||||
|
function handleDeleteClass(idx) {
|
||||||
|
picList.value.splice(idx, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁用/启用一个类别
|
||||||
|
function handleDisableClass(idx) {
|
||||||
|
picList.value[idx].disabled = !picList.value[idx].disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重命名一个类别
|
||||||
|
function handleRenameClass(idx, newTitle) {
|
||||||
|
picList.value[idx].title = newTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清空一个类别的样本
|
||||||
|
function handleClearSamples(idx) {
|
||||||
|
picList.value[idx].list = []
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示模型区域与否
|
||||||
|
const showModelArea = ref(true)
|
||||||
|
|
||||||
|
// 令摄像头拍摄一张图片加入shotList
|
||||||
|
const cameraBoxRef = ref(null)
|
||||||
|
async function handleParentShot() {
|
||||||
|
await cameraBoxRef.value?.captureShot()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 训练模型
|
||||||
|
const modelAreaRef = ref(null)
|
||||||
|
async function handleTrain() {
|
||||||
|
await modelAreaRef.value.train()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="main-splitter">
|
||||||
|
<ElSplitter>
|
||||||
|
<ElSplitterPanel size="70%" :min="200">
|
||||||
|
<div class="class-area">
|
||||||
|
<ClassBox v-for="(item, idx) in picList" :key="idx" :title="item.title" :pic-list="item.list"
|
||||||
|
:disabled="item.disabled" @add-sample="(data) => handleAddSample(idx, data)" @delete-sample="
|
||||||
|
(url) => (picList[idx].list = picList[idx].list.filter((i) => i !== url))
|
||||||
|
" @delete-class="handleDeleteClass(idx)" @disable-class="handleDisableClass(idx)"
|
||||||
|
@rename-class="(newTitle) => handleRenameClass(idx, newTitle)"
|
||||||
|
@clear-samples="handleClearSamples(idx)">
|
||||||
|
<ElButton type="primary" size="mini" @click="handleAddSample(idx)">
|
||||||
|
添加样本
|
||||||
|
</ElButton>
|
||||||
|
</ClassBox>
|
||||||
|
<ElCard class="add-class-card" @click="handleAddClass">
|
||||||
|
<div class="add-class-content">
|
||||||
|
添加一个类别
|
||||||
|
<ElIcon>
|
||||||
|
<CirclePlusFilled />
|
||||||
|
</ElIcon>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</ElSplitterPanel>
|
||||||
|
<ElSplitterPanel :min="200">
|
||||||
|
<CameraBox ref="cameraBoxRef" />
|
||||||
|
<transition name="fade-scale">
|
||||||
|
<ModelArea v-if="showModelArea" ref="modelAreaRef" @shot="handleParentShot" />
|
||||||
|
</transition>
|
||||||
|
<TrainArea @train="handleTrain" />
|
||||||
|
</ElSplitterPanel>
|
||||||
|
</ElSplitter>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #011216;
|
||||||
|
margin: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: linear-gradient(to right, #d7dee1, transparent, transparent, transparent);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.teachable-model-container {
|
||||||
|
width: 80%;
|
||||||
|
height: 80vh;
|
||||||
|
margin: 20px;
|
||||||
|
padding: 5px;
|
||||||
|
border: 2px solid #000;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0px 0px 10px #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-splitter {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: var(--el-border-color-light) 0px 0px 10px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-class-card {
|
||||||
|
margin: 10px auto;
|
||||||
|
border: 1px dotted #000;
|
||||||
|
max-width: 50vw;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-class-card:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-enter-active {
|
||||||
|
transition: all 0.5s cubic-bezier(0.55, 0, 0.1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-leave-active {
|
||||||
|
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-enter-to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-leave-from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-scale-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineExpose, inject, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
|
const videoRef = ref(null)
|
||||||
|
const shotList = inject('shotList')
|
||||||
|
|
||||||
|
// 初始化摄像头
|
||||||
|
onMounted(() => {
|
||||||
|
navigator.mediaDevices
|
||||||
|
.getUserMedia({ video: true })
|
||||||
|
.then((stream) => {
|
||||||
|
if (videoRef.value) {
|
||||||
|
videoRef.value.srcObject = stream
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('无法访问摄像头:', err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 拍摄照片,转为正方形,存入shotList
|
||||||
|
function captureShot() {
|
||||||
|
const video = videoRef.value
|
||||||
|
if (!video)
|
||||||
|
return
|
||||||
|
const vw = video.videoWidth
|
||||||
|
const vh = video.videoHeight
|
||||||
|
let sx = 0
|
||||||
|
let sy = 0
|
||||||
|
let sw = vw
|
||||||
|
let sh = vh
|
||||||
|
if (vw > vh) {
|
||||||
|
sx = (vw - vh) / 2
|
||||||
|
sw = vh
|
||||||
|
}
|
||||||
|
else if (vh > vw) {
|
||||||
|
sy = (vh - vw) / 2
|
||||||
|
sh = vw
|
||||||
|
}
|
||||||
|
const size = Math.min(sw, sh)
|
||||||
|
const canvas = document.createElement('canvas')
|
||||||
|
canvas.width = size
|
||||||
|
canvas.height = size
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
ctx.drawImage(video, sx, sy, size, size, 0, 0, size, size)
|
||||||
|
shotList.value.push(canvas.toDataURL('image/png'))
|
||||||
|
return canvas.toDataURL('image/png')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法
|
||||||
|
defineExpose({ captureShot })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="camera-box">
|
||||||
|
<video id="webcam" ref="videoRef" class="video-crop" autoplay playsinline muted height="100%" width="100%" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.camera-box {
|
||||||
|
margin: 10px auto;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 250px;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
/* background-color: black; */
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.video-crop {
|
||||||
|
max-width: 250px;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
border-radius: 10px;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: center center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.camera-box {
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
<script setup>
|
||||||
|
import { Camera, CirclePlus, Delete, EditPen, More } from '@element-plus/icons-vue';
|
||||||
|
import { saveAs } from 'file-saver';
|
||||||
|
import JSZip from 'jszip';
|
||||||
|
import { nextTick, ref, watch, TransitionGroup } from 'vue';
|
||||||
|
import { ElCard, ElDropdown, ElDropdownMenu, ElDropdownItem, ElButton, ElUpload, ElImage, ElInput, ElIcon } from 'element-plus';
|
||||||
|
|
||||||
|
// import 'element-plus/theme-chalk/el-card.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-dropdown.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-dropdown-menu.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-dropdown-item.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-button.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-upload.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-image.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-input.css';
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: '类别',
|
||||||
|
},
|
||||||
|
picList: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'add-sample',
|
||||||
|
'delete-sample',
|
||||||
|
'rename-class',
|
||||||
|
'delete-class',
|
||||||
|
'disable-class',
|
||||||
|
'clear-samples',
|
||||||
|
])
|
||||||
|
|
||||||
|
const localTitle = ref(props.title)
|
||||||
|
const editing = ref(false)
|
||||||
|
const inputTitle = ref(localTitle.value)
|
||||||
|
const inputRef = ref(null)
|
||||||
|
const addSampleTimer = ref(null)
|
||||||
|
|
||||||
|
function editTitle() {
|
||||||
|
if (!editing.value) {
|
||||||
|
editing.value = true
|
||||||
|
inputTitle.value = localTitle.value
|
||||||
|
nextTick(() => {
|
||||||
|
if (inputRef.value && inputRef.value.input) {
|
||||||
|
inputRef.value.input.select()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveTitle() {
|
||||||
|
if (inputTitle.value.trim()) {
|
||||||
|
localTitle.value = inputTitle.value.trim()
|
||||||
|
emit('rename-class', localTitle.value)
|
||||||
|
}
|
||||||
|
editing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddSample() {
|
||||||
|
emit('add-sample')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteSample(url) {
|
||||||
|
emit('delete-sample', url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function startAddSample() {
|
||||||
|
handleAddSample()
|
||||||
|
addSampleTimer.value = setInterval(() => {
|
||||||
|
handleAddSample()
|
||||||
|
}, 200) // 200ms 可根据需要调整
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopAddSample() {
|
||||||
|
if (addSampleTimer.value) {
|
||||||
|
clearInterval(addSampleTimer.value)
|
||||||
|
addSampleTimer.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.title,
|
||||||
|
(val) => {
|
||||||
|
localTitle.value = val
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function handleDeleteClass() {
|
||||||
|
emit('delete-class')
|
||||||
|
}
|
||||||
|
function handleDisableClass() {
|
||||||
|
emit('disable-class')
|
||||||
|
}
|
||||||
|
function handleClearSamples() {
|
||||||
|
emit('clear-samples')
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleExportSamples() {
|
||||||
|
// 导出为zip
|
||||||
|
const zip = new JSZip()
|
||||||
|
props.picList.forEach((url, idx) => {
|
||||||
|
const base64 = url.split(',')[1]
|
||||||
|
zip.file(`sample_${idx + 1}.png`, base64, { base64: true })
|
||||||
|
})
|
||||||
|
zip.generateAsync({ type: 'blob' }).then((content) => {
|
||||||
|
saveAs(content, 'samples.zip')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增:处理图片上传
|
||||||
|
// 修改处理函数
|
||||||
|
function handleUploadSample(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 新增ZIP文件处理
|
||||||
|
if (file.type === 'application/zip' || file.name.endsWith('.zip')) {
|
||||||
|
const zip = new JSZip()
|
||||||
|
zip
|
||||||
|
.loadAsync(file)
|
||||||
|
.then((zipContents) => {
|
||||||
|
const imageFiles = Object.values(zipContents.files).filter(
|
||||||
|
file => !file.dir && file.name.match(/\.(png|jpg|jpeg|gif|bmp)$/i),
|
||||||
|
)
|
||||||
|
|
||||||
|
Promise.all(
|
||||||
|
imageFiles.map((zipFile) => {
|
||||||
|
return zip
|
||||||
|
.file(zipFile.name)
|
||||||
|
.async('blob')
|
||||||
|
.then((blob) => {
|
||||||
|
const extractedFile = new File([blob], zipFile.name, {
|
||||||
|
type: blob.type,
|
||||||
|
})
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsDataURL(extractedFile)
|
||||||
|
return new Promise(r => (reader.onload = r))
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
).then((readers) => {
|
||||||
|
readers.forEach(e => emit('add-sample', e.target.result))
|
||||||
|
resolve(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(reject)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 原有图片处理逻辑
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (e) => {
|
||||||
|
emit('add-sample', e.target.result)
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
reader.onerror = reject
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElCard class="class-box">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="card-header-title" style="cursor: pointer" @click="editTitle">
|
||||||
|
<template v-if="!editing">
|
||||||
|
{{ localTitle }}
|
||||||
|
<ElIcon @click.stop="editTitle">
|
||||||
|
<EditPen />
|
||||||
|
</ElIcon>
|
||||||
|
<span v-if="props.disabled" class="disabled-tag"
|
||||||
|
style="color: #f56c6c; margin-left: 8px; font-size: 13px">
|
||||||
|
禁用
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ElInput ref="inputRef" v-model="inputTitle" style="width: 120px" @keyup.enter="saveTitle"
|
||||||
|
@blur="saveTitle" />
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ElDropdown class="card-header-title">
|
||||||
|
<span class="el-dropdown-link">
|
||||||
|
<ElIcon>
|
||||||
|
<More />
|
||||||
|
</ElIcon>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<ElDropdownMenu class="class-box-dropdown">
|
||||||
|
<ElDropdownItem class="class-box-dropdown-item" @click="handleDeleteClass">
|
||||||
|
<span style="color: #f56c6c">删除类别</span>
|
||||||
|
</ElDropdownItem>
|
||||||
|
<ElDropdownItem class="class-box-dropdown-item" @click="handleDisableClass">
|
||||||
|
{{ props.disabled ? "启用" : "禁用" }}类别
|
||||||
|
</ElDropdownItem>
|
||||||
|
<ElDropdownItem class="class-box-dropdown-item" :disabled="props.picList.length === 0"
|
||||||
|
@click="handleClearSamples">
|
||||||
|
清空样本
|
||||||
|
</ElDropdownItem>
|
||||||
|
<ElDropdownItem class="class-box-dropdown-item" :disabled="props.picList.length === 0"
|
||||||
|
@click="handleExportSamples">
|
||||||
|
导出样本
|
||||||
|
</ElDropdownItem>
|
||||||
|
</ElDropdownMenu>
|
||||||
|
</template>
|
||||||
|
</ElDropdown>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="card-content" style="flex-direction: column; display: flex">
|
||||||
|
<div class="card-content-info">
|
||||||
|
图像样本 {{ props.picList.length }}
|
||||||
|
<div style="display: flex; gap: 4px">
|
||||||
|
<ElButton type="primary" size="small" :disabled="props.disabled" @mousedown="startAddSample"
|
||||||
|
@mouseup="stopAddSample" @mouseleave="stopAddSample">
|
||||||
|
拍摄样本
|
||||||
|
<ElIcon>
|
||||||
|
<Camera />
|
||||||
|
</ElIcon>
|
||||||
|
</ElButton>
|
||||||
|
<ElUpload :show-file-list="false" :before-upload="handleUploadSample" accept="image/*,.zip"
|
||||||
|
:disabled="props.disabled">
|
||||||
|
<ElButton plain type="primary" size="small" :disabled="props.disabled">
|
||||||
|
上传样本
|
||||||
|
<ElIcon>
|
||||||
|
<CirclePlus />
|
||||||
|
</ElIcon>
|
||||||
|
</ElButton>
|
||||||
|
</ElUpload>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<TransitionGroup name="fade" tag="div" class="card-content-img">
|
||||||
|
<div v-for="url in props.picList.slice().reverse()" :key="url" class="card-content-img-wrapper"
|
||||||
|
style="position: relative; display: inline-block">
|
||||||
|
<ElImage :src="url" class="card-content-img-item" :class="{ 'img-disabled': props.disabled }" />
|
||||||
|
<ElIcon class="delete-icon" @click="handleDeleteSample(url)">
|
||||||
|
<Delete />
|
||||||
|
</ElIcon>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</ElCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-title:hover {
|
||||||
|
color: #409eff;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content-img {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
width: 100%;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content-img-item {
|
||||||
|
margin: 2px;
|
||||||
|
height: 60px;
|
||||||
|
aspect-ratio: 1/1;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.8);
|
||||||
|
object-fit: cover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content-info {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content-img-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
font-size: 16px;
|
||||||
|
z-index: 2;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 2px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-icon:hover {
|
||||||
|
background: #f56c6c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content-img-wrapper:hover .delete-icon {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-disabled {
|
||||||
|
filter: grayscale(1) brightness(0.7);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled-tag {
|
||||||
|
color: #f56c6c;
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-box {
|
||||||
|
margin: 10px auto;
|
||||||
|
max-width: 50vw;
|
||||||
|
cursor: default;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
<script setup>
|
||||||
|
// import * as mobilenet from "@tensorflow-models/mobilenet";
|
||||||
|
import * as tf from '@tensorflow/tfjs';
|
||||||
|
import * as tfvis from '@tensorflow/tfjs-vis';
|
||||||
|
import { inject, ref } from 'vue';
|
||||||
|
import { ElMessage, ElButton, ElCard, ElRow, ElCol, ElInput, ElProgress, ElUpload } from 'element-plus';
|
||||||
|
|
||||||
|
// import 'element-plus/theme-chalk/el-message.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-button.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-card.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-row.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-col.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-input.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-progress.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-upload.css';
|
||||||
|
|
||||||
|
|
||||||
|
const emit = defineEmits(['shot'])
|
||||||
|
// 类别及其样本的列表
|
||||||
|
const picList = inject('picList')
|
||||||
|
// 图片列表
|
||||||
|
const shotList = inject('shotList')
|
||||||
|
// 训练状态
|
||||||
|
const states = inject('states')
|
||||||
|
|
||||||
|
// 用来显示进度条和名称
|
||||||
|
const classList = ref(
|
||||||
|
picList.value.map((item, idx) => ({
|
||||||
|
name: item.title,
|
||||||
|
progress: 0,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
const progressColors = ['#FF6F61', '#42A5F5', '#66BB6A', '#FFA726', '#AB47BC']
|
||||||
|
// 用来存储模型
|
||||||
|
let featureExtractor
|
||||||
|
// 用来存储损失值
|
||||||
|
let lossValues = []
|
||||||
|
// 存储被训练的模型
|
||||||
|
let model
|
||||||
|
|
||||||
|
// 单击训练按钮后训练模型
|
||||||
|
async function train() {
|
||||||
|
// 可视化相关
|
||||||
|
const visPanel = document.getElementById('vis-left-panel')
|
||||||
|
if (visPanel)
|
||||||
|
visPanel.innerHTML = '训练准备中……'
|
||||||
|
showVisPanel.value = true
|
||||||
|
console.log('正在加载Mobilenet……')
|
||||||
|
// net = await mobilenet.load();
|
||||||
|
featureExtractor = await tf.loadGraphModel('/teachableModel/model.json')
|
||||||
|
|
||||||
|
console.log('Mobilenet加载完成。')
|
||||||
|
if (visPanel)
|
||||||
|
visPanel.innerHTML = ''
|
||||||
|
|
||||||
|
// 准备数据
|
||||||
|
const NUM_CLASSES = picList.value.length
|
||||||
|
let xs = null
|
||||||
|
let ys = null
|
||||||
|
// 准备模型
|
||||||
|
model = tf.sequential()
|
||||||
|
// 添加全连接层
|
||||||
|
model.add(
|
||||||
|
tf.layers.dense({
|
||||||
|
inputShape: [1280],
|
||||||
|
units: 128,
|
||||||
|
activation: 'relu',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// 添加分类层
|
||||||
|
model.add(
|
||||||
|
tf.layers.dense({
|
||||||
|
units: NUM_CLASSES,
|
||||||
|
activation: 'softmax',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// 编译模型
|
||||||
|
model.compile({
|
||||||
|
optimizer: tf.train.adam(0.001),
|
||||||
|
loss: 'categoricalCrossentropy',
|
||||||
|
metrics: ['accuracy'],
|
||||||
|
})
|
||||||
|
|
||||||
|
for (let classId = 0; classId < picList.value.length; classId++) {
|
||||||
|
if (picList.value[classId].disabled)
|
||||||
|
continue
|
||||||
|
const images = picList.value[classId].list
|
||||||
|
for (let i = 0; i < images.length; i++) {
|
||||||
|
// 加载图片
|
||||||
|
const imgElement = new Image()
|
||||||
|
imgElement.src = images[i]
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
imgElement.onload = resolve
|
||||||
|
})
|
||||||
|
// 将图片转化为张量
|
||||||
|
const imgTensor = tf.browser.fromPixels(imgElement)
|
||||||
|
|
||||||
|
// 使用神经网络模型进行推理,获取名为"conv_preds"的卷积层激活值
|
||||||
|
// let activation = net.infer(imgTensor, "conv_preds");
|
||||||
|
// 加载特征提取器和你的模型
|
||||||
|
|
||||||
|
// 预处理图像并提取特征
|
||||||
|
const preprocessedImg = imgTensor
|
||||||
|
.resizeBilinear([224, 224])
|
||||||
|
.toFloat()
|
||||||
|
.div(tf.scalar(127.5))
|
||||||
|
.sub(tf.scalar(1))
|
||||||
|
.expandDims(0)
|
||||||
|
|
||||||
|
const features = featureExtractor.predict(preprocessedImg)
|
||||||
|
|
||||||
|
// 将特征输入你的模型
|
||||||
|
// const predictions = net.predict(features);
|
||||||
|
let activation = features
|
||||||
|
// if (activation.shape.length === 3) {
|
||||||
|
// activation = activation.reshape([1, 1024]);
|
||||||
|
// }
|
||||||
|
// let activation = net.predict(imgTensor);
|
||||||
|
// 检查激活值张量的维度,如果是3维张量则进行形状重塑
|
||||||
|
// 3维形状通常为 [height, width, channels],重塑为 [1, 1024] 的张量
|
||||||
|
// 为了适配后续分类层或特征可视化的输入要求
|
||||||
|
|
||||||
|
// 转换为one-hot编码
|
||||||
|
const y = tf.oneHot(tf.tensor1d([classId]).toInt(), NUM_CLASSES)
|
||||||
|
// 初始化xs和ys
|
||||||
|
if (xs == null) {
|
||||||
|
xs = activation.clone()
|
||||||
|
ys = y.clone()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const oldXs = xs
|
||||||
|
xs = oldXs.concat(activation, 0)
|
||||||
|
oldXs.dispose()
|
||||||
|
|
||||||
|
const oldYs = ys
|
||||||
|
ys = oldYs.concat(y, 0)
|
||||||
|
oldYs.dispose()
|
||||||
|
}
|
||||||
|
y.dispose()
|
||||||
|
imgTensor.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 训练过程可视化相关
|
||||||
|
lossValues = []
|
||||||
|
const metrics = ['loss', 'acc', 'val_loss', 'val_acc', 'accuracy', 'val_accuracy']
|
||||||
|
const container = document.getElementById('vis-left-panel') || {
|
||||||
|
name: '训练过程',
|
||||||
|
tab: '训练',
|
||||||
|
}
|
||||||
|
// 训练模型
|
||||||
|
await model.fit(xs, ys, {
|
||||||
|
epochs: 20,
|
||||||
|
batchSize: 16,
|
||||||
|
shuffle: true,
|
||||||
|
validationSplit: 0.2,
|
||||||
|
callbacks: tfvis.show.fitCallbacks(container, metrics, {
|
||||||
|
callbacks: ['onEpochEnd'],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.log('训练完成')
|
||||||
|
// 与显示进度条相关
|
||||||
|
classList.value = picList.value
|
||||||
|
.filter(item => item.disabled !== true)
|
||||||
|
.map((item, idx) => ({
|
||||||
|
name: item.title,
|
||||||
|
progress: 0,
|
||||||
|
}))
|
||||||
|
console.log(classList.value)
|
||||||
|
states.value.isTraining = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
// 没训练完成跳过
|
||||||
|
if (states.value.isTraining !== 2)
|
||||||
|
return
|
||||||
|
// 输入是“上传图片”时跳过
|
||||||
|
if (uploadedImg.value !== '')
|
||||||
|
return
|
||||||
|
// 通知摄像头拍摄照片
|
||||||
|
emit('shot')
|
||||||
|
// 加载拍摄的照片
|
||||||
|
const img = shotList.value[shotList.value.length - 1]
|
||||||
|
if (!img || img === 'data:,') {
|
||||||
|
ElMessage.error('未获取到有效样本')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const imgElement = new Image()
|
||||||
|
imgElement.src = img
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
imgElement.onload = resolve
|
||||||
|
})
|
||||||
|
// 将图片转换为张量
|
||||||
|
const imgTensor = tf.browser.fromPixels(imgElement)
|
||||||
|
// let activation = net.infer(imgTensor, "conv_preds");
|
||||||
|
let resized = tf.image.resizeBilinear(imgTensor, [224, 224])
|
||||||
|
let batched = resized.expandDims(0)
|
||||||
|
let normalized = batched.div(255)
|
||||||
|
// let activation = net.predict(imgTensor);
|
||||||
|
let activation = featureExtractor.predict(normalized)
|
||||||
|
const pred = model.predict(activation)
|
||||||
|
const predArr = await pred.data()
|
||||||
|
console.log(predArr)
|
||||||
|
classList.value = [
|
||||||
|
...classList.value.map((item, idx) => ({
|
||||||
|
...item,
|
||||||
|
progress: Number((predArr[idx] * 100).toFixed(2)),
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
console.log(classList.value)
|
||||||
|
imgTensor.dispose()
|
||||||
|
activation.dispose()
|
||||||
|
pred.dispose()
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
const showVisPanel = ref(false)
|
||||||
|
const uploadedImg = ref('')
|
||||||
|
const uploadedResult = ref('')
|
||||||
|
const panelTop = ref('20%')
|
||||||
|
const panelRight = ref('20%')
|
||||||
|
|
||||||
|
async function exportModel() {
|
||||||
|
if (!model) {
|
||||||
|
ElMessage.error('模型尚未训练完成')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 导出模型到本地文件系统
|
||||||
|
await model.save(`downloads://${modelName.value == '' ? 'my-model' : modelName.value}`)
|
||||||
|
ElMessage.success('模型导出成功')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
ElMessage.error(`模型导出失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleUpload(file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
uploadedImg.value = e.target.result
|
||||||
|
if (!featureExtractor || !model) {
|
||||||
|
ElMessage.error('请先完成模型训练')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const imgElement = new window.Image()
|
||||||
|
imgElement.src = uploadedImg.value
|
||||||
|
await new Promise(resolve => (imgElement.onload = resolve))
|
||||||
|
const imgTensor = tf.browser.fromPixels(imgElement)
|
||||||
|
// let activation = net.infer(imgTensor, "conv_preds");
|
||||||
|
let resized = tf.image.resizeBilinear(imgTensor, [224, 224])
|
||||||
|
let batched = resized.expandDims(0)
|
||||||
|
let normalized = batched.div(255)
|
||||||
|
// let activation = net.predict(imgTensor);
|
||||||
|
let activation = featureExtractor.predict(normalized)
|
||||||
|
const pred = model.predict(activation)
|
||||||
|
const predArr = await pred.data()
|
||||||
|
classList.value = [
|
||||||
|
...classList.value.map((item, idx) => ({
|
||||||
|
...item,
|
||||||
|
progress: Number((predArr[idx] * 100).toFixed(2)),
|
||||||
|
})),
|
||||||
|
]
|
||||||
|
const maxIdx = predArr.indexOf(Math.max(...predArr))
|
||||||
|
uploadedResult.value = classList.value[maxIdx]?.name || '未知'
|
||||||
|
imgTensor.dispose()
|
||||||
|
activation.dispose()
|
||||||
|
pred.dispose()
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchToUpload() {
|
||||||
|
uploadedImg.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
let startX = 0
|
||||||
|
let startY = 0
|
||||||
|
let startTop = 0
|
||||||
|
let startRight = 0
|
||||||
|
|
||||||
|
function onDragStart(e) {
|
||||||
|
if (e.button !== 0)
|
||||||
|
return
|
||||||
|
startX = e.clientX
|
||||||
|
startY = e.clientY
|
||||||
|
const topVal = document.getElementById('vis-left-panel-wrapper').style.top
|
||||||
|
const rightVal = document.getElementById('vis-left-panel-wrapper').style.right
|
||||||
|
startTop = topVal.endsWith('%')
|
||||||
|
? (window.innerHeight * Number.parseFloat(topVal)) / 100
|
||||||
|
: Number.parseFloat(topVal)
|
||||||
|
startRight = rightVal.endsWith('%')
|
||||||
|
? (window.innerWidth * Number.parseFloat(rightVal)) / 100
|
||||||
|
: Number.parseFloat(rightVal)
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', onDragging)
|
||||||
|
document.addEventListener('mouseup', onDragEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragging(e) {
|
||||||
|
const deltaX = e.clientX - startX
|
||||||
|
const deltaY = e.clientY - startY
|
||||||
|
let newTop = startTop + deltaY
|
||||||
|
let newRight = startRight - deltaX
|
||||||
|
newTop = Math.max(0, Math.min(window.innerHeight - 100, newTop))
|
||||||
|
newRight = Math.max(0, Math.min(window.innerWidth - 200, newRight))
|
||||||
|
panelTop.value = `${newTop}px`
|
||||||
|
panelRight.value = `${newRight}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDragEnd() {
|
||||||
|
document.removeEventListener('mousemove', onDragging)
|
||||||
|
document.removeEventListener('mouseup', onDragEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ train })
|
||||||
|
|
||||||
|
const modelName = ref('')
|
||||||
|
async function saveModel() {
|
||||||
|
if (!model) {
|
||||||
|
ElMessage.error('模型尚未训练完成')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 导出模型到本地文件系统
|
||||||
|
await model.save(`indexeddb://${modelName.value == '' ? 'my-model' : modelName.value}`)
|
||||||
|
ElMessage.success('模型保存成功')
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
ElMessage.error(`模型保存失败: ${error.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-show="showVisPanel" id="vis-left-panel-wrapper" class="vis-left-panel-wrapper"
|
||||||
|
:style="`right: ${panelRight}; top: ${panelTop};`">
|
||||||
|
<div class="vis-left-panel-inner-wrapper">
|
||||||
|
<div class="vis-left-panel-title" style="" @mousedown="onDragStart">
|
||||||
|
<span> 训练过程可视化 </span>
|
||||||
|
<ElButton size="small" plain @click="showVisPanel = false">
|
||||||
|
隐藏
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
<div id="vis-left-panel" class="vis-left-panel">
|
||||||
|
训练准备中……
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ElCard v-if="states.isTraining === 2" class="model-area">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
模型
|
||||||
|
<ElButton v-if="!showVisPanel" size="small" style="margin-left: 10px" type="primary" plain
|
||||||
|
@click="showVisPanel = true">
|
||||||
|
显示训练过程
|
||||||
|
</ElButton>
|
||||||
|
<ElButton v-if="states.isTraining === 2" size="small" style="margin-left: 10px" type="success"
|
||||||
|
plain @click="exportModel">
|
||||||
|
模型导出至本地
|
||||||
|
</ElButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<ElRow class="model-item" style="flex-direction: column; align-items: flex-center">
|
||||||
|
<ElRow>
|
||||||
|
<ElCol :span="6" style="display: flex; align-items: center; text-align: right;">
|
||||||
|
名称:
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="12">
|
||||||
|
<ElInput v-model="modelName" placeholder="请输入模型名称" />
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="4" style="display: flex; align-items: center;">
|
||||||
|
<ElButton style="margin: auto 5px" type="primary" plain size="small" @click="saveModel">
|
||||||
|
保存
|
||||||
|
</ElButton>
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
<ElRow class="model-item">
|
||||||
|
<b>输入</b>
|
||||||
|
</ElRow>
|
||||||
|
<div v-if="!uploadedImg" style="margin: auto">
|
||||||
|
上方拍摄内容
|
||||||
|
<ElUpload :show-file-list="false" accept="image/*" :before-upload="handleUpload">
|
||||||
|
<ElButton size="small" type="success">
|
||||||
|
切换为上传图片
|
||||||
|
</ElButton>
|
||||||
|
</ElUpload>
|
||||||
|
</div>
|
||||||
|
<div v-else style="margin: auto">
|
||||||
|
下方上传图片 <br>
|
||||||
|
<ElButton size="small" type="success" plain @click="switchToUpload">
|
||||||
|
切换为上方拍摄内容
|
||||||
|
</ElButton>
|
||||||
|
<ElUpload :show-file-list="false" accept="image/*" :before-upload="handleUpload">
|
||||||
|
<ElButton size="small" type="success">
|
||||||
|
重新上传一张
|
||||||
|
</ElButton>
|
||||||
|
</ElUpload>
|
||||||
|
<img :src="uploadedImg" alt="用户上传图片"
|
||||||
|
style="max-width: 100%; max-height: 150px; border-radius: 10px">
|
||||||
|
</div>
|
||||||
|
</ElRow>
|
||||||
|
<ElRow class="model-item">
|
||||||
|
<b>输出</b>
|
||||||
|
</ElRow>
|
||||||
|
<ElRow v-for="(item, idx) in classList" :key="`${item.name}-${idx}`" class="model-item">
|
||||||
|
<ElCol :span="6">
|
||||||
|
{{ item.name }}
|
||||||
|
</ElCol>
|
||||||
|
<ElCol :span="18">
|
||||||
|
<ElProgress class="progress" :text-inside="true" :stroke-width="20" :percentage="item.progress"
|
||||||
|
:color="progressColors[idx % progressColors.length]" striped :format="(p) => `${p}%`" />
|
||||||
|
</ElCol>
|
||||||
|
</ElRow>
|
||||||
|
</ElCard>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.vis-left-panel-wrapper {
|
||||||
|
transition: all 0.3s;
|
||||||
|
position: fixed;
|
||||||
|
width: 400px;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 0 20px #0001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vis-left-panel-inner-wrapper {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px #0001;
|
||||||
|
padding: 10px 10px 0 10px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vis-left-panel-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
cursor: move;
|
||||||
|
user-select: none;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vis-left-panel {
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-area {
|
||||||
|
margin: 10px auto;
|
||||||
|
width: 85%;
|
||||||
|
max-width: 300px;
|
||||||
|
border-radius: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject, ref, watch } from 'vue';
|
||||||
|
import { Warning } from '@element-plus/icons-vue';
|
||||||
|
import { ElButton, ElPopover, ElCard, ElText, ElIcon } from 'element-plus';
|
||||||
|
|
||||||
|
// import 'element-plus/theme-chalk/el-button.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-popover.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-card.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-text.css';
|
||||||
|
// import 'element-plus/theme-chalk/el-icon.css';
|
||||||
|
|
||||||
|
|
||||||
|
const emit = defineEmits(['train'])
|
||||||
|
|
||||||
|
// 获取类别及其样本
|
||||||
|
const picList = inject('picList')
|
||||||
|
// 同步训练状态
|
||||||
|
const states = inject('states')
|
||||||
|
|
||||||
|
// 训练按钮是否可用
|
||||||
|
const isTrainButtonEnabled = ref(false)
|
||||||
|
const note_text = ref('待训练……')
|
||||||
|
// 检查训练按钮是否可用
|
||||||
|
function checkTrainButtonEnabled() {
|
||||||
|
// 只考虑未被禁用的类别
|
||||||
|
const enabledList = Array.isArray(picList.value)
|
||||||
|
? picList.value.filter(item => item.disabled === false)
|
||||||
|
: []
|
||||||
|
return (
|
||||||
|
states.value.isTraining !== 1
|
||||||
|
&& enabledList.length > 1
|
||||||
|
&& enabledList.every(item => Array.isArray(item.list) && item.list.length > 0)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// 监听类别列表和训练状态变化,更新训练按钮的可用性
|
||||||
|
watch(
|
||||||
|
[picList.value, () => states.value.isTraining],
|
||||||
|
() => {
|
||||||
|
isTrainButtonEnabled.value = checkTrainButtonEnabled()
|
||||||
|
if (states.value.isTraining === 0) {
|
||||||
|
note_text.value = '待训练……'
|
||||||
|
}
|
||||||
|
else if (states.value.isTraining === 1) {
|
||||||
|
note_text.value = '训练中……'
|
||||||
|
}
|
||||||
|
else if (states.value.isTraining === 2) {
|
||||||
|
note_text.value = '训练完成'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
// 训练模型
|
||||||
|
function handleTrain() {
|
||||||
|
if (isTrainButtonEnabled.value) {
|
||||||
|
emit('train')
|
||||||
|
states.value.isTraining = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElCard class="train-area">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
训练
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<ElPopover class="box-item" placement="bottom" :disabled="isTrainButtonEnabled">
|
||||||
|
<ElIcon>
|
||||||
|
<Warning />
|
||||||
|
</ElIcon>
|
||||||
|
至少有两个类别,且每个类别都需有图像样本。
|
||||||
|
<template #reference>
|
||||||
|
<ElButton :disabled="!isTrainButtonEnabled" type="primary" style="max-width: 100%" @click="handleTrain">
|
||||||
|
训练模型
|
||||||
|
</ElButton>
|
||||||
|
</template>
|
||||||
|
</ElPopover>
|
||||||
|
<br>
|
||||||
|
<ElText>{{ note_text }}</ElText>
|
||||||
|
</ElCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.train-area {
|
||||||
|
margin: 10px auto;
|
||||||
|
width: 85%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
|
|
||||||
|
export const isDark = useDark()
|
||||||
|
export const toggleDark = useToggle(isDark)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './dark'
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// only scss variables
|
||||||
|
|
||||||
|
$--colors: (
|
||||||
|
"primary": ("base": #589ef8,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
@forward "element-plus/theme-chalk/src/dark/var.scss" with ($colors: $--colors);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
$--colors: (
|
||||||
|
'primary': ('base': green,
|
||||||
|
),
|
||||||
|
'success': ('base': #21ba45,
|
||||||
|
),
|
||||||
|
'warning': ('base': #f2711c,
|
||||||
|
),
|
||||||
|
'danger': ('base': #db2828,
|
||||||
|
),
|
||||||
|
'error': ('base': #db2828,
|
||||||
|
),
|
||||||
|
'info': ('base': #42b8dd,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// we can add this to custom namespace, default is 'el'
|
||||||
|
@forward 'element-plus/theme-chalk/src/mixins/config.scss' with ($namespace: 'ep'
|
||||||
|
);
|
||||||
|
|
||||||
|
// You should use them in scss, because we calculate it by sass.
|
||||||
|
// comment next lines to use default color
|
||||||
|
@forward 'element-plus/theme-chalk/src/common/var.scss' with ( // do not use same name, it will override.
|
||||||
|
$colors: $--colors,
|
||||||
|
$button-padding-horizontal: ('default': 50px));
|
||||||
|
|
||||||
|
// if you want to import all
|
||||||
|
// @use "element-plus/theme-chalk/src/index.scss" as *;
|
||||||
|
|
||||||
|
// You can comment it to hide debug info.
|
||||||
|
// @debug $--colors;
|
||||||
|
|
||||||
|
// custom dark variables
|
||||||
|
@use './dark.scss';
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// import dark theme
|
||||||
|
@use 'element-plus/theme-chalk/src/dark/css-vars.scss' as *;
|
||||||
|
|
||||||
|
// :root {
|
||||||
|
// --ep-color-primary: red;
|
||||||
|
// }
|
||||||
|
|
||||||
|
code {
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
background-color: var(--ep-color-primary-light-9);
|
||||||
|
color: var(--ep-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nprogress .bar {
|
||||||
|
background: rgb(13, 148, 136);
|
||||||
|
opacity: 0.75;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1031;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
6596
boards/default_src/python_pyodide/package-lock.json
generated
6596
boards/default_src/python_pyodide/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,19 +12,30 @@
|
|||||||
"publish:board": "npm publish --registry https://registry.npmjs.org/"
|
"publish:board": "npm publish --registry https://registry.npmjs.org/"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@vue/compiler-sfc": "^3.5.21",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"crypto-browserify": "^3.12.0",
|
"crypto-browserify": "^3.12.0",
|
||||||
"path-browserify": "^1.0.1",
|
"path-browserify": "^1.0.1",
|
||||||
|
"sass": "^1.93.0",
|
||||||
|
"sass-loader": "^16.0.5",
|
||||||
"stream-browserify": "^3.0.0",
|
"stream-browserify": "^3.0.0",
|
||||||
"vm-browserify": "^1.1.2"
|
"vm-browserify": "^1.1.2",
|
||||||
|
"vue-loader": "^17.0.0",
|
||||||
|
"vue-style-loader": "^4.1.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@basthon/kernel-loader": "^0.62.21",
|
"@basthon/kernel-loader": "^0.62.21",
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"@tensorflow/tfjs": "^4.22.0",
|
"@tensorflow/tfjs": "^4.22.0",
|
||||||
|
"@tensorflow/tfjs-vis": "^1.5.1",
|
||||||
"@zenfs/core": "^1.4.0",
|
"@zenfs/core": "^1.4.0",
|
||||||
"@zenfs/dom": "^1.0.6",
|
"@zenfs/dom": "^1.0.6",
|
||||||
|
"element-plus": "^2.11.3",
|
||||||
|
"file-saver": "^2.0.5",
|
||||||
"idb-keyval": "^6.2.1",
|
"idb-keyval": "^6.2.1",
|
||||||
"pixi.js": "^3.0.8"
|
"jszip": "^3.10.1",
|
||||||
|
"pixi.js": "^3.0.8",
|
||||||
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"main": "./export.js",
|
"main": "./export.js",
|
||||||
"author": "Mixly Team",
|
"author": "Mixly Team",
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
const path = require("path");
|
const path = require('path');
|
||||||
const common = require("../../../webpack.common");
|
const common = require('../../../webpack.common');
|
||||||
const { merge } = require("webpack-merge");
|
const { merge } = require('webpack-merge');
|
||||||
|
const { VueLoaderPlugin } = require('vue-loader');
|
||||||
|
|
||||||
module.exports = merge(common, {
|
|
||||||
|
const webpackConfig = merge(common, {
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@mixly/python': path.resolve(__dirname, '../python'),
|
'@mixly/python': path.resolve(__dirname, '../python'),
|
||||||
'@mixly/python-mixpy': path.resolve(__dirname, '../python_mixpy')
|
'@mixly/python-mixpy': path.resolve(__dirname, '../python_mixpy'),
|
||||||
|
'vue$': 'vue/dist/vue.esm-bundler.js',
|
||||||
|
'@': __dirname
|
||||||
},
|
},
|
||||||
extensions: ['.ts', '.js'],
|
extensions: ['*', '.ts', '.js', '.vue', '.json'],
|
||||||
fallback: {
|
fallback: {
|
||||||
// for ocaml bundle
|
// for ocaml bundle
|
||||||
constants: require.resolve('constants-browserify'),
|
constants: require.resolve('constants-browserify'),
|
||||||
@@ -28,7 +32,21 @@ module.exports = merge(common, {
|
|||||||
{
|
{
|
||||||
resourceQuery: /asset-url/,
|
resourceQuery: /asset-url/,
|
||||||
type: 'asset/resource',
|
type: 'asset/resource',
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
test: /\.scss$/,
|
||||||
|
use: ['vue-style-loader', 'css-loader', 'sass-loader'],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
});
|
plugins: [
|
||||||
|
new VueLoaderPlugin()
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
webpackConfig.module.rules.unshift({
|
||||||
|
test: /\.vue$/,
|
||||||
|
use: ['vue-loader']
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = webpackConfig;
|
||||||
94
package-lock.json
generated
94
package-lock.json
generated
@@ -44,10 +44,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/helper-validator-identifier": {
|
"node_modules/@babel/helper-validator-identifier": {
|
||||||
"version": "7.24.7",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
@@ -306,10 +307,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/trace-mapping": {
|
"node_modules/@jridgewell/trace-mapping": {
|
||||||
"version": "0.3.25",
|
"version": "0.3.25",
|
||||||
@@ -3671,9 +3673,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -3681,6 +3683,7 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"nanoid": "bin/nanoid.cjs"
|
"nanoid": "bin/nanoid.cjs"
|
||||||
},
|
},
|
||||||
@@ -4045,10 +4048,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
@@ -4127,9 +4131,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.4.39",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.39.tgz",
|
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4145,10 +4149,11 @@
|
|||||||
"url": "https://github.com/sponsors/ai"
|
"url": "https://github.com/sponsors/ai"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.0.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.0"
|
"source-map-js": "^1.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
@@ -4975,10 +4980,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -5889,9 +5895,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@babel/helper-validator-identifier": {
|
"@babel/helper-validator-identifier": {
|
||||||
"version": "7.24.7",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
|
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@babel/highlight": {
|
"@babel/highlight": {
|
||||||
@@ -6088,9 +6094,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@jridgewell/sourcemap-codec": {
|
"@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.5",
|
||||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"@jridgewell/trace-mapping": {
|
"@jridgewell/trace-mapping": {
|
||||||
@@ -8659,9 +8665,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nanoid": {
|
"nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.11",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.7.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
|
||||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"natural-compare": {
|
"natural-compare": {
|
||||||
@@ -8931,9 +8937,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"picocolors": {
|
"picocolors": {
|
||||||
"version": "1.0.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
|
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"picomatch": {
|
"picomatch": {
|
||||||
@@ -8991,14 +8997,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"postcss": {
|
"postcss": {
|
||||||
"version": "8.4.39",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.39.tgz",
|
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"nanoid": "^3.3.7",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.0.1",
|
"picocolors": "^1.1.1",
|
||||||
"source-map-js": "^1.2.0"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"postcss-loader": {
|
"postcss-loader": {
|
||||||
@@ -9609,9 +9615,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"source-map-js": {
|
"source-map-js": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"source-map-support": {
|
"source-map-support": {
|
||||||
|
|||||||
Reference in New Issue
Block a user