330 lines
12 KiB
JavaScript
330 lines
12 KiB
JavaScript
import NavExt from './nav-ext';
|
||
import * as tf from '@tensorflow/tfjs';
|
||
import './tensorflow';
|
||
|
||
import * as Blockly from 'blockly/core';
|
||
NavExt.init();
|
||
window.tf = tf;
|
||
|
||
// let featureExtractor;
|
||
// featureExtractor = await tf.loadGraphModel("../common/media/tfmodel/model.json");
|
||
// window.featureExtractor = featureExtractor;
|
||
|
||
function closeModal() {
|
||
document.getElementById('modalOverlay').style.display = 'none';
|
||
}
|
||
|
||
// 从IndexedDB删除单个模型
|
||
async function deleteModel(modelName) {
|
||
try {
|
||
await tf.io.removeModel(`indexeddb://${modelName}`);
|
||
// 从UI移除
|
||
const modelItem = document.querySelector(`.model-item[data-model-name="${modelName}"]`);
|
||
if (modelItem) modelItem.remove();
|
||
} catch (error) {
|
||
console.error('删除模型失败:', error);
|
||
alert('删除模型失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 显示单个模型项
|
||
function displayModelItem(modelName) {
|
||
const modelsList = document.getElementById('imported-models');
|
||
if ([...modelsList.children].some(item => item.dataset.modelName === modelName)) {
|
||
return;
|
||
}
|
||
const modelItem = document.createElement('div');
|
||
modelItem.className = 'model-item';
|
||
modelItem.dataset.modelName = modelName;
|
||
modelItem.innerHTML = `
|
||
<div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 10px; border-bottom: 1px solid #eee;">
|
||
<span style="font-size: 1em; color: #333;">${modelName}</span>
|
||
<button class="delete-model" style="
|
||
background: #f44336;
|
||
color: white;
|
||
border: none;
|
||
padding: 4px 10px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 0.9em;
|
||
transition: background-color 0.2s;
|
||
">删除</button>
|
||
</div>
|
||
`;
|
||
|
||
// 绑定删除事件
|
||
modelItem.querySelector('.delete-model').addEventListener('click', () => {
|
||
deleteModel(modelName);
|
||
});
|
||
|
||
modelsList.appendChild(modelItem);
|
||
}
|
||
|
||
// 清空所有模型
|
||
async function clearAllModels() {
|
||
try {
|
||
const modelInfos = await tf.io.listModels();
|
||
const deletePromises = Object.keys(modelInfos)
|
||
.map(path => path.replace('indexeddb://', ''))
|
||
.map(modelName => tf.io.removeModel(`indexeddb://${modelName}`));
|
||
|
||
await Promise.all(deletePromises);
|
||
document.getElementById('imported-models').innerHTML = '';
|
||
} catch (error) {
|
||
console.error('清空模型失败:', error);
|
||
alert('清空模型失败: ' + error.message);
|
||
}
|
||
}
|
||
|
||
// 加载并显示所有模型
|
||
async function loadAndDisplayAllModels() {
|
||
try {
|
||
const modelInfos = await tf.io.listModels();
|
||
document.getElementById('imported-models').innerHTML = '';
|
||
for (const [path] of Object.entries(modelInfos)) {
|
||
const modelName = path.replace('indexeddb://', '');
|
||
displayModelItem(modelName);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载模型列表失败:', error);
|
||
}
|
||
}
|
||
|
||
async function createModal() {
|
||
const overlay = document.createElement('div');
|
||
overlay.id = 'modalOverlay';
|
||
Object.assign(overlay.style, {
|
||
display: 'none',
|
||
position: 'fixed',
|
||
top: '0',
|
||
left: '0',
|
||
width: '100%',
|
||
height: '100%',
|
||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||
zIndex: '20011216',
|
||
pointerEvents: 'auto'
|
||
});
|
||
const content = document.createElement('div');
|
||
Object.assign(content.style, {
|
||
backgroundColor: 'white',
|
||
width: '60%',
|
||
maxHeight: '80%',
|
||
margin: '12vh auto',
|
||
padding: '20px 30px',
|
||
borderRadius: '12px'
|
||
});
|
||
content.innerHTML = `
|
||
<h2 style="margin-bottom: 20px;">选择本地模型</h2>
|
||
<div style="margin-bottom: 25px; position: relative; min-height: 200px;"> <!-- 新增 min-height 和 position -->
|
||
<input type="file" id="model-upload" accept=".json,.bin" multiple style="display: none;">
|
||
<label for="model-upload" style="
|
||
background: #1216ab;
|
||
color: white;
|
||
padding: 10px 20px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
">选择模型文件</label>
|
||
<div style="display: flex; gap: 10px; margin-top: 15px; align-items: center;">
|
||
<span style="line-height: 36px;">导入模型名称:</span>
|
||
<input type="text" id="model-name" placeholder="模型名称" value="my-model" style="
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
flex: 1;
|
||
max-width: 200px;
|
||
">
|
||
<button id="model-handle" style="
|
||
background: #1216ab;
|
||
color: white;
|
||
padding: 10px 20px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
">保存模型</button>
|
||
</div>
|
||
<div style="margin-top: 15px; display: flex; gap: 20px;">
|
||
<div id="json-status" style="
|
||
background: #f5f5f5;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
flex: 1;
|
||
">
|
||
<span>❌ 模型结构描述文件(model.json)</span>
|
||
|
||
<div style="color: #666; font-size: 0.9em; margin-top: 5px;">未选择</div>
|
||
</div>
|
||
<div id="weights-status" style="
|
||
background: #f5f5f5;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
flex: 1;
|
||
">
|
||
<span>❌ 权重文件(model.weights.bin)</span>
|
||
<div style="color: #666; font-size: 0.9em; margin-top: 5px;">0 个已选择</div>
|
||
</div>
|
||
</div>
|
||
<div id="output" style="margin-bottom: 15px; min-height: 40px;"></div>
|
||
<div style="margin-top: 20px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||
<h3>已导入模型</h3>
|
||
<div style="display: flex; gap: 10px;">
|
||
<button id="refresh-models" style="
|
||
background: #4CAF50;
|
||
color: white;
|
||
padding: 5px 10px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
border: none;
|
||
">刷新</button>
|
||
<button id="clear-models" style="
|
||
background: #f44336;
|
||
color: white;
|
||
padding: 5px 10px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
border: none;
|
||
">清空</button>
|
||
</div>
|
||
</div>
|
||
<div id="imported-models" style="
|
||
background: #f5f5f5;
|
||
padding: 10px;
|
||
border-radius: 4px;
|
||
height: 200px;
|
||
overflow-y: auto;
|
||
">
|
||
<div style="color: #666; font-style: italic;">加载中...</div>
|
||
</div>
|
||
</div>
|
||
<div style="display: flex; justify-content: flex-end; margin-top: 15px;">
|
||
<button class="close-btn" style="
|
||
background: #e0e0e0;
|
||
border: none;
|
||
padding: 8px 20px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
">关闭</button>
|
||
</div>
|
||
</div>
|
||
`;
|
||
overlay.appendChild(content);
|
||
document.body.appendChild(overlay);
|
||
|
||
content.querySelector('.close-btn').addEventListener('click', closeModal);
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) closeModal();
|
||
});
|
||
// 获取DOM元素
|
||
const modelUpload = document.getElementById('model-upload');
|
||
const modelHandle = document.getElementById('model-handle');
|
||
const outputDiv = document.getElementById('output');
|
||
|
||
let jsonFile = null;
|
||
let weightFiles = [];
|
||
|
||
modelUpload.addEventListener('change', async (event) => {
|
||
const files = event.target.files;
|
||
|
||
// 获取状态元素
|
||
const jsonStatus = document.getElementById('json-status');
|
||
const weightsStatus = document.getElementById('weights-status');
|
||
|
||
// 重置状态显示(保持完整文件名描述)
|
||
jsonStatus.querySelector('span').textContent = '❌ 模型结构描述文件(model.json)';
|
||
jsonStatus.querySelector('div').textContent = '未选择';
|
||
weightsStatus.querySelector('span').textContent = '❌ 权重文件(model.weights.bin)';
|
||
weightsStatus.querySelector('div').textContent = '0 个已选择';
|
||
|
||
// 分离 JSON 和权重文件
|
||
weightFiles = [];
|
||
for (let i = 0; i < files.length; i++) {
|
||
if (files[i].name.endsWith('.json')) {
|
||
jsonFile = files[i];
|
||
} else {
|
||
weightFiles.push(files[i]);
|
||
}
|
||
}
|
||
|
||
if (!jsonFile) {
|
||
alert('未找到 model.json 文件');
|
||
return;
|
||
}
|
||
|
||
outputDiv.innerHTML = '正在处理上传的模型文件...';
|
||
|
||
if (jsonFile) {
|
||
jsonStatus.querySelector('span').textContent = '✅ 模型结构描述文件(model.json)';
|
||
jsonStatus.querySelector('div').textContent = '已选择';
|
||
const modelName = jsonFile.name.replace('.json', '');
|
||
document.getElementById('model-name').value = modelName;
|
||
}
|
||
|
||
if (weightFiles.length > 0) {
|
||
weightsStatus.querySelector('span').textContent = '✅ 权重文件(model.weights.bin)';
|
||
weightsStatus.querySelector('div').textContent = `${weightFiles.length} 个已选择`;
|
||
}
|
||
});
|
||
|
||
modelHandle.addEventListener('click', async () => {
|
||
try {
|
||
const modelNameInput = document.getElementById('model-name');
|
||
const modelName = modelNameInput.value || 'mixly-model';
|
||
|
||
const model = await tf.loadLayersModel(
|
||
tf.io.browserFiles([jsonFile, ...weightFiles])
|
||
);
|
||
await model.save(`indexeddb://${modelName}`);
|
||
loadAndDisplayAllModels();
|
||
outputDiv.innerHTML = `模型已成功保存为 ${modelName}!`;
|
||
} catch (error) {
|
||
outputDiv.innerHTML = `保存模型出错: ${error.message}`;
|
||
console.error(error);
|
||
}
|
||
})
|
||
|
||
content.querySelector('#refresh-models').addEventListener('click', loadAndDisplayAllModels);
|
||
content.querySelector('#clear-models').addEventListener('click', async () => {
|
||
if (confirm('确定要删除所有模型吗?此操作不可恢复!')) {
|
||
await clearAllModels();
|
||
}
|
||
});
|
||
}
|
||
|
||
createModal();
|
||
|
||
await loadAndDisplayAllModels();
|
||
|
||
function openModal() {
|
||
loadAndDisplayAllModels();
|
||
document.getElementById('modalOverlay').style.display = 'block';
|
||
}
|
||
|
||
const workspace = Blockly.getMainWorkspace();
|
||
workspace.registerButtonCallback('handleModels', function () {
|
||
openModal();
|
||
});
|
||
|
||
|
||
async function prepare_qmyixtxi(imgTensor) {
|
||
let net = null;
|
||
|
||
if (window.featureExtractor) {
|
||
net = window.featureExtractor;
|
||
} else {
|
||
net = await tf.loadGraphModel("../common/media/tfmodel/model.json");
|
||
window.featureExtractor = net;
|
||
}
|
||
const preprocessedImg = imgTensor
|
||
.resizeBilinear([224, 224])
|
||
.toFloat()
|
||
.div(tf.scalar(127.5))
|
||
.sub(tf.scalar(1))
|
||
.expandDims(0);
|
||
|
||
const features = window.featureExtractor.predict(preprocessedImg);
|
||
|
||
let activation = features;
|
||
return activation;
|
||
}
|
||
window.prepare_qmyixtxi = prepare_qmyixtxi;
|