From a0013eae8fc1af7079f38ba49fb2f87bf0b07305 Mon Sep 17 00:00:00 2001 From: wangyuzhan Date: Fri, 21 Nov 2025 10:41:05 +0800 Subject: [PATCH] update web dataset file --- trainer_web/static/css/style.css | 226 +++++++++++++++++++++++++++++++ trainer_web/static/js/app.js | 187 ++++++++++++++++++++++--- trainer_web/templates/index.html | 27 ++++ trainer_web/train_web_ui.py | 111 +++++++++++++++ 4 files changed, 530 insertions(+), 21 deletions(-) diff --git a/trainer_web/static/css/style.css b/trainer_web/static/css/style.css index dc9fd50..3a68d21 100644 --- a/trainer_web/static/css/style.css +++ b/trainer_web/static/css/style.css @@ -366,6 +366,232 @@ input[type="text"]:focus, input[type="number"]:focus, select:focus, textarea:foc font-weight: 600; color: var(--accent); } + +/* 文件浏览器模态框样式 */ +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + backdrop-filter: blur(5px); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + transition: opacity 0.3s ease; +} + +.modal.hidden { + display: none; +} + +.modal-content { + background: var(--panel-bg); + border-radius: var(--radius-lg); + border: 1px solid var(--border); + box-shadow: var(--shadow-xl); + width: 90%; + max-width: 600px; + max-height: 80vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.modal-header { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(139, 92, 246, 0.1); +} + +.modal-header h3 { + margin: 0; + color: var(--text); + font-size: 1.1rem; +} + +.modal-close { + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.5rem; + cursor: pointer; + padding: 0; + width: 30px; + height: 30px; + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--radius-sm); + transition: all 0.3s ease; +} + +.modal-close:hover { + background: rgba(239, 68, 68, 0.2); + color: #ef4444; +} + +.modal-body { + flex: 1; + padding: 1rem; + overflow-y: auto; +} + +.modal-footer { + padding: 1rem 1.5rem; + border-top: 1px solid var(--border); + display: flex; + gap: 0.5rem; + align-items: center; + background: rgba(22, 22, 32, 0.8); +} + +.modal-footer input { + flex: 1; +} + +.btn-secondary { + background: linear-gradient(135deg, var(--border-light) 0%, var(--border) 100%); + color: var(--text); + border: none; + padding: 0.5rem 1rem; + border-radius: var(--radius-md); + cursor: pointer; + transition: all 0.3s ease; + font-size: 0.8rem; + font-weight: 500; +} + +.btn-secondary:hover { + transform: translateY(-1px); + filter: brightness(1.1); +} + +/* 文件浏览器导航 */ +.file-browser-nav { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + padding: 0.5rem; + background: rgba(45, 55, 72, 0.5); + border-radius: var(--radius-md); +} + +.current-path { + font-family: 'Courier New', monospace; + font-size: 0.85rem; + color: var(--text-secondary); + flex: 1; + margin-right: 1rem; + padding: 0.25rem 0.5rem; + background: rgba(22, 22, 32, 0.5); + border-radius: var(--radius-sm); + border: 1px solid var(--border); +} + +.btn-navigate { + background: linear-gradient(135deg, var(--info-grad-start) 0%, var(--info-grad-end) 100%); + color: white; + border: none; + padding: 0.5rem; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.8rem; + transition: all 0.3s ease; +} + +.btn-navigate:hover { + transform: translateY(-1px); + filter: brightness(1.1); +} + +/* 快捷路径 */ +.quick-paths { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +.quick-path-btn { + background: rgba(139, 92, 246, 0.1); + color: var(--accent); + border: 1px solid rgba(139, 92, 246, 0.3); + padding: 0.4rem 0.8rem; + border-radius: var(--radius-sm); + cursor: pointer; + font-size: 0.75rem; + transition: all 0.3s ease; +} + +.quick-path-btn:hover { + background: rgba(139, 92, 246, 0.2); + transform: translateY(-1px); +} + +/* 文件列表 */ +.file-list { + max-height: 300px; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: rgba(22, 22, 32, 0.3); +} + +.file-item { + display: flex; + align-items: center; + padding: 0.75rem; + cursor: pointer; + transition: all 0.3s ease; + border-bottom: 1px solid rgba(45, 55, 72, 0.3); +} + +.file-item:last-child { + border-bottom: none; +} + +.file-item:hover { + background: rgba(139, 92, 246, 0.1); +} + +.file-item.selected { + background: rgba(139, 92, 246, 0.2); + border-left: 3px solid var(--accent); +} + +.file-icon { + margin-right: 0.75rem; + font-size: 1.2rem; + width: 20px; + text-align: center; +} + +.file-name { + flex: 1; + font-size: 0.9rem; + color: var(--text); +} + +.file-info { + font-size: 0.75rem; + color: var(--text-secondary); + text-align: right; +} + +.directory { + color: var(--info-grad-start); +} + +.file { + color: var(--text-secondary); +} .checkbox-group { display: flex; align-items: center; diff --git a/trainer_web/static/js/app.js b/trainer_web/static/js/app.js index 6606144..1fae11d 100644 --- a/trainer_web/static/js/app.js +++ b/trainer_web/static/js/app.js @@ -17,37 +17,182 @@ const hooks = { window.openTab = (evt, tabName) => _openTab(evt, tabName, hooks); -// 文件夹选择器功能 +// 文件夹选择器功能 - 支持远程文件浏览 window.selectFolder = async (inputId) => { + try { + // 检测是否为远程连接(通过检查主机名或端口) + const isRemote = window.location.hostname !== 'localhost' && window.location.hostname !== '127.0.0.1'; + + if (isRemote) { + // 远程连接:使用服务器端文件浏览 + openRemoteFileBrowser(inputId); + } else { + // 本地连接:尝试本地文件系统访问 + await openLocalFileBrowser(inputId); + } + } catch (error) { + console.warn('文件夹选择失败:', error); + if (error.name !== 'AbortError') { + // 降级到远程文件浏览 + openRemoteFileBrowser(inputId); + } + } +}; + +// 本地文件浏览器(兼容模式) +async function openLocalFileBrowser(inputId) { try { // 检查是否支持 File System Access API if ('showDirectoryPicker' in window) { const dirHandle = await window.showDirectoryPicker(); - const path = dirHandle.name; // 使用目录名称作为路径 + const path = dirHandle.name; document.getElementById(inputId).value = `./${path}`; } else { - // 降级方案:使用文件输入模拟 - const input = document.createElement('input'); - input.type = 'file'; - input.webkitdirectory = true; - input.onchange = (e) => { - const files = e.target.files; - if (files.length > 0) { - // 提取相对路径 - const path = files[0].webkitRelativePath.split('/')[0]; - document.getElementById(inputId).value = `./${path}`; - } - }; - input.click(); + // 降级到远程文件浏览 + await openRemoteFileBrowser(inputId); } } catch (error) { - console.warn('文件夹选择失败:', error); - // 如果用户取消选择,不显示错误 - if (error.name !== 'AbortError') { - alert('文件夹选择失败,请手动输入路径'); - } + // 如果本地失败,降级到远程文件浏览 + await openRemoteFileBrowser(inputId); } -}; +} + +// 远程文件浏览器 +let currentFileBrowserTarget = null; +let currentBrowsePath = './'; + +function openRemoteFileBrowser(inputId) { + currentFileBrowserTarget = inputId; + document.getElementById('file-browser-modal').classList.remove('hidden'); + + // 加载初始路径 + loadQuickPaths(); + browsePath('./'); +} + +function closeFileBrowser() { + document.getElementById('file-browser-modal').classList.add('hidden'); + currentFileBrowserTarget = null; + currentBrowsePath = './'; +} + +function confirmFileSelection() { + const selectedPath = document.getElementById('selected-path').value; + if (selectedPath && currentFileBrowserTarget) { + document.getElementById(currentFileBrowserTarget).value = selectedPath; + } + closeFileBrowser(); +} + +function navigateToParent() { + if (currentBrowsePath !== './') { + const parentPath = currentBrowsePath.includes('/') ? + currentBrowsePath.substring(0, currentBrowsePath.lastIndexOf('/')) : './'; + browsePath(parentPath || './'); + } +} + +async function loadQuickPaths() { + try { + const response = await fetch('/api/quick-paths'); + const data = await response.json(); + + const quickPathsContainer = document.getElementById('quick-paths'); + quickPathsContainer.innerHTML = ''; + + if (data.paths && data.paths.length > 0) { + data.paths.forEach(path => { + const btn = document.createElement('button'); + btn.className = 'quick-path-btn'; + btn.textContent = path.name; + btn.onclick = () => browsePath(path.path); + btn.title = path.path; + quickPathsContainer.appendChild(btn); + }); + } + } catch (error) { + console.warn('加载快捷路径失败:', error); + } +} + +async function browsePath(path) { + try { + currentBrowsePath = path; + document.getElementById('current-path').textContent = path; + + const response = await fetch(`/api/browse?path=${encodeURIComponent(path)}`); + const data = await response.json(); + + if (data.error) { + alert(`浏览失败: ${data.error}`); + return; + } + + renderFileList(data); + } catch (error) { + console.error('浏览路径失败:', error); + alert('浏览路径失败,请检查网络连接'); + } +} + +function renderFileList(data) { + const fileList = document.getElementById('file-list'); + fileList.innerHTML = ''; + + if (!data.items || data.items.length === 0) { + fileList.innerHTML = '
此目录为空
'; + return; + } + + // 先显示目录,再显示文件 + const directories = data.items.filter(item => item.type === 'directory'); + const files = data.items.filter(item => item.type === 'file'); + + // 渲染目录 + directories.forEach(item => { + const div = createFileItem(item, '📁'); + fileList.appendChild(div); + }); + + // 渲染文件 + files.forEach(item => { + const div = createFileItem(item, '📄'); + fileList.appendChild(div); + }); +} + +function createFileItem(item, icon) { + const div = document.createElement('div'); + div.className = 'file-item'; + div.onclick = () => selectFileItem(item); + + div.innerHTML = ` + ${icon} + ${item.name} + ${item.type === 'file' ? formatFileSize(item.size) : '文件夹'} + `; + + return div; +} + +function selectFileItem(item) { + if (item.type === 'directory') { + browsePath(item.path); + } else { + document.getElementById('selected-path').value = item.path; + // 文件被选中,可以高亮显示 + document.querySelectorAll('.file-item').forEach(el => el.classList.remove('selected')); + event.currentTarget.classList.add('selected'); + } +} + +function formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +} window.addEventListener('load', () => { initTrainForm(); diff --git a/trainer_web/templates/index.html b/trainer_web/templates/index.html index 4b94e02..4390001 100644 --- a/trainer_web/templates/index.html +++ b/trainer_web/templates/index.html @@ -319,6 +319,33 @@ + + + diff --git a/trainer_web/train_web_ui.py b/trainer_web/train_web_ui.py index 22b236b..5d9043d 100644 --- a/trainer_web/train_web_ui.py +++ b/trainer_web/train_web_ui.py @@ -11,6 +11,8 @@ from flask import Flask, render_template, request, jsonify, redirect, url_for from flask import g import time import psutil +import glob +import pathlib # 尝试导入torch来检测GPU try: @@ -320,6 +322,115 @@ def processes(): }) return jsonify(result) +@app.route('/api/browse') +def browse_files(): + """ + 浏览服务器文件系统 + 支持远程文件选择功能 + """ + try: + # 获取请求的路径参数 + path = request.args.get('path', './') + + # 安全检查:限制访问范围 + script_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.abspath(os.path.join(script_dir, '..')) + + # 解析请求的路径 + if path.startswith('./'): + # 相对路径,基于项目根目录 + full_path = os.path.abspath(os.path.join(project_root, path[2:])) + elif path.startswith('/'): + # 绝对路径,检查是否在项目目录内 + full_path = os.path.abspath(path) + else: + # 相对路径,基于项目根目录 + full_path = os.path.abspath(os.path.join(project_root, path)) + + # 安全检查:确保路径在项目目录内 + if not full_path.startswith(project_root): + full_path = project_root + + # 检查路径是否存在 + if not os.path.exists(full_path): + return jsonify({'error': '路径不存在', 'path': path}) + + # 获取目录内容 + if os.path.isdir(full_path): + items = [] + try: + # 列出目录内容 + for item in sorted(os.listdir(full_path)): + item_path = os.path.join(full_path, item) + + # 跳过隐藏文件和系统文件 + if item.startswith('.') or item.startswith('__'): + continue + + try: + stat = os.stat(item_path) + items.append({ + 'name': item, + 'path': os.path.relpath(item_path, project_root), + 'type': 'directory' if os.path.isdir(item_path) else 'file', + 'size': stat.st_size if os.path.isfile(item_path) else 0, + 'modified': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime)) + }) + except (OSError, PermissionError): + # 跳过无法访问的项目 + continue + + return jsonify({ + 'current_path': os.path.relpath(full_path, project_root), + 'absolute_path': full_path, + 'items': items, + 'parent': os.path.relpath(os.path.dirname(full_path), project_root) if full_path != project_root else None + }) + except (OSError, PermissionError) as e: + return jsonify({'error': f'无法访问目录: {str(e)}', 'path': path}) + + else: + # 如果是文件,返回文件信息 + stat = os.stat(full_path) + return jsonify({ + 'name': os.path.basename(full_path), + 'path': os.path.relpath(full_path, project_root), + 'type': 'file', + 'size': stat.st_size, + 'modified': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime)) + }) + + except Exception as e: + return jsonify({'error': f'浏览文件时出错: {str(e)}'}) + +@app.route('/api/quick-paths') +def quick_paths(): + """ + 返回常用路径快捷方式 + """ + try: + script_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.abspath(os.path.join(script_dir, '..')) + + quick_paths = [ + {'name': '项目根目录', 'path': './', 'type': 'directory'}, + {'name': '数据集目录', 'path': './dataset', 'type': 'directory'}, + {'name': '模型检查点', 'path': './checkpoints', 'type': 'directory'}, + {'name': '日志文件', 'path': './logfile', 'type': 'directory'} + ] + + # 验证路径是否存在 + valid_paths = [] + for item in quick_paths: + full_path = os.path.join(project_root, item['path'][2:] if item['path'].startswith('./') else item['path']) + if os.path.exists(full_path): + valid_paths.append(item) + + return jsonify({'paths': valid_paths}) + + except Exception as e: + return jsonify({'error': f'获取快捷路径时出错: {str(e)}'}) + @app.route('/logs/') def logs(process_id): # 直接从本地logfile目录读取日志文件