Spaces:
Running
Running
stouchinglhh
Claude
sync processor from GitLab c697b6e: 版本 1.19.4 / 前端 1.12.4 —— 服务端 i18n(首屏不再闪英文)、推理模块并入模型页、前端抽出 shared/、缓存键改用前端号
bb59211 Download processor/web/static/js/train/checkpoints.js from Stouching-Robotics/DataFlowing: direct link, hf CLI and curl.
- Browser
- Download file 7.26 kB
-
https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/processor/web/static/js/train/checkpoints.js
- Command line
-
hf download hf://spaces/Stouching-Robotics/DataFlowing/processor/web/static/js/train/checkpoints.js
-
curl -L -o checkpoints.js https://huggingface.co/spaces/Stouching-Robotics/DataFlowing/resolve/main/processor/web/static/js/train/checkpoints.js
7.26 kB
| /** 「模型输出」页签 —— 每个 checkpoint 一个可展开块 + 文件明细表。 | |
| * | |
| * 数据全部来自磁盘真实文件(后端 rglob 出来的),没有任何凑数的假行。 | |
| * | |
| * **下载已经能用了** —— 文件表逐行一个链接,走后端 | |
| * ``GET /runs/{run}/checkpoints/{step}/files/{name:path}``(逐文件而不是整包: | |
| * 一个 checkpoint 约 600MB,其中 training_state/ 只在续训时有用)。 | |
| * | |
| * **其余操作按钮仍然 disabled**:删除 / 推理 / 离线部署都还没有端点。做成可点 | |
| * 但没反应是最糟的 —— 用户会反复点、以为坏了。disabled 加上 title 说明,一眼就 | |
| * 知道"还没接",和有端点了再放开是两回事。 | |
| */ | |
| import { el, clear } from '../shared/dom.js'; | |
| import { bytes, dateTime } from '../shared/format.js'; | |
| import { tr } from '../shared/text.js'; | |
| import { checkpointFileUrl } from './api.js'; | |
| /** 逐个文件一行。文件名可能很长(pretrained_model/xxx.safetensors), | |
| * 用 break-all 而不是省略号 —— 训练产物看的就是完整名字。 */ | |
| function fileRow(runName, checkpoint, file) { | |
| return el('tr', { class: 'border-b border-gray-800/60 last:border-0' }, [ | |
| el('td', { class: 'py-1.5 pr-4 text-gray-300 font-mono text-[11px] break-all', text: file.name }), | |
| el('td', { class: 'py-1.5 pr-4 text-gray-400 text-right whitespace-nowrap', text: bytes(file.size) }), | |
| el('td', { class: 'py-1.5 pr-4 text-gray-500 whitespace-nowrap', text: dateTime(file.mtime) }), | |
| el('td', { class: 'py-1.5 text-right whitespace-nowrap' }, [ | |
| // 带 download 属性:浏览器直接存盘,不跳转到一个只会显示二进制内容的页面。 | |
| el('a', { | |
| class: 'text-blue-400 hover:underline', | |
| href: checkpointFileUrl(runName, checkpoint.name, file.name), | |
| download: true, | |
| text: tr('train_btn_download', 'Download'), | |
| }), | |
| ]), | |
| ]); | |
| } | |
| function fileTable(runName, checkpoint) { | |
| return el('table', { class: 'w-full text-xs mt-3' }, [ | |
| el('thead', {}, [ | |
| el('tr', { class: 'border-b border-gray-800' }, [ | |
| el('th', { class: 'text-left py-1.5 pr-4 font-medium text-gray-500', text: tr('train_col_name', 'Name') }), | |
| el('th', { class: 'text-right py-1.5 pr-4 font-medium text-gray-500', text: tr('train_col_size', 'Size') }), | |
| el('th', { class: 'text-left py-1.5 pr-4 font-medium text-gray-500', text: tr('train_col_mtime', 'Modified') }), | |
| el('th', { class: 'text-right py-1.5 font-medium text-gray-500' }), | |
| ]), | |
| ]), | |
| el('tbody', {}, checkpoint.files.map((f) => fileRow(runName, checkpoint, f))), | |
| ]); | |
| } | |
| /** **真的还没接端点**的按钮 —— disabled,title 说明原因。 */ | |
| function pendingButton(label, hint) { | |
| return el('button', { | |
| class: 'text-[11px] px-2 py-1 rounded border border-gray-700 text-gray-600 cursor-not-allowed', | |
| text: label, | |
| disabled: true, | |
| title: hint, | |
| }); | |
| } | |
| /** 能点的按钮。 */ | |
| function actionButton(label, hint, onClick) { | |
| return el('button', { | |
| class: 'text-[11px] px-2 py-1 rounded border border-gray-600 text-gray-300 ' | |
| + 'hover:border-gray-500 hover:text-gray-100', | |
| title: hint, | |
| text: label, | |
| on: { click: onClick }, | |
| }); | |
| } | |
| function checkpointBlock(runName, checkpoint, expanded, handlers) { | |
| const body = el('div', { class: expanded ? '' : 'hidden' }, [fileTable(runName, checkpoint)]); | |
| const chevron = el('span', { | |
| class: 'text-gray-500 text-[10px] transition-transform', | |
| text: expanded ? '▾' : '▸', | |
| }); | |
| const toggle = () => { | |
| body.classList.toggle('hidden'); | |
| const open = !body.classList.contains('hidden'); | |
| chevron.textContent = open ? '▾' : '▸'; | |
| }; | |
| return el('div', { class: 'bg-gray-900 rounded-lg border border-gray-800 p-4' }, [ | |
| el('div', { class: 'flex items-center flex-wrap gap-3' }, [ | |
| el('iconify-icon', { icon: 'ant-design:folder-outlined', class: 'text-base' }), | |
| el('span', { class: 'text-gray-200 font-mono font-semibold', text: checkpoint.name }), | |
| el('span', { | |
| class: 'text-[10px] px-1.5 py-0.5 rounded bg-blue-900/50 text-blue-300', | |
| text: tr('train_step_badge', 'Step'), | |
| }), | |
| el('span', { | |
| class: 'text-xs text-gray-500', | |
| text: `${checkpoint.file_count} ${tr('train_files_unit', 'files')}` | |
| + ` · ${bytes(checkpoint.total_bytes)}`, | |
| }), | |
| el('div', { class: 'ml-auto flex gap-2' }, [ | |
| // 「下载」= **展开文件表**(那里每行才有真下载)。不做整包下载:一个 | |
| // checkpoint 约 600MB,其中 training_state/(优化器状态,约 400MB)只在 | |
| // 续训时有用;拿去推理/部署只要 pretrained_model/(约 200MB)。 | |
| // 要"一键整包"得后端加一个压 zip 的端点,本期没有。 | |
| actionButton(tr('train_btn_download', 'Download'), | |
| tr('train_btn_hint_download_row', | |
| 'Expand the file list — each file downloads individually'), | |
| () => { if (body.classList.contains('hidden')) toggle(); }), | |
| // 「推理」= 跳到「模型推理」页签并选中本检查点。**纯前端跳转,不需要后端** —— | |
| // 那正是"我要拿这个模型去推理"的自然动作。 | |
| actionButton(tr('train_btn_infer', 'Inference'), | |
| tr('train_btn_hint_infer_row', | |
| 'Open the Inference tab with this checkpoint selected'), | |
| () => handlers?.onInfer?.(checkpoint)), | |
| pendingButton(tr('train_btn_delete', 'Delete'), | |
| tr('train_btn_hint_delete', 'Destructive; no endpoint yet')), | |
| pendingButton(tr('train_btn_deploy', 'Offline Deploy'), | |
| tr('train_btn_hint_deploy', 'Belongs to the Deployment module')), | |
| ]), | |
| ]), | |
| el('button', { | |
| class: 'mt-3 text-xs text-gray-500 hover:text-gray-300 flex items-center gap-1.5', | |
| on: { click: toggle }, | |
| }, [chevron, el('span', { text: tr('train_file_details', 'File details') })]), | |
| body, | |
| ]); | |
| } | |
| /** | |
| * @param {HTMLElement} container | |
| * @param {object} run 后端给的一个 run(`{name, checkpoints, …}`)—— 需要 name | |
| * 才能拼出文件下载地址(`/runs/{name}/checkpoints/{step}/files/…`) | |
| * @param {{onInfer?: (checkpoint: object) => void}} [handlers] | |
| * 由 train/main.js 注入 —— 本模块不认识"页签"这回事,只负责喊一声。 | |
| */ | |
| export function renderCheckpoints(container, run, handlers = {}) { | |
| clear(container); | |
| const checkpoints = run.checkpoints || []; | |
| if (checkpoints.length === 0) { | |
| container.appendChild(el('div', { | |
| class: 'text-center text-gray-600 text-sm py-10', | |
| text: tr('train_checkpoints_empty', 'No checkpoints under this run.'), | |
| })); | |
| return; | |
| } | |
| // **全部默认折叠。** 曾经让最新的那个默认展开,但一进页面就被一大堆文件名 | |
| // (每个 checkpoint 11 个文件)顶下去,反而看不到下面还有几个 checkpoint。 | |
| // 折叠状态下每行就是一行摘要,扫一眼能看全;要看细节自己点。 | |
| container.appendChild(el('div', { class: 'space-y-3' }, | |
| checkpoints.map((c) => checkpointBlock(run.name, c, false, handlers)))); | |
| } | |