File size: 4,635 Bytes
bb59211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/** 「训练参数」页签 —— 渲染 train_config.json 里的真实超参。
 *
 * 数据是**训练当时实际用的那份配置**(lerobot-train 自己写进 checkpoint 的),
 * 不是我们事后补的默认值。
 *
 * 缺字段就**整行不渲染**。不同 LeRobot 版本的键名会变(我们比参考实现少
 * `scheduler_state.json`、多四个 pre/postprocessor 文件就是同一类差异),写死
 * "缺了就显示 N/A"会让人误以为训练时那个参数是 null。
 */

import { el, clear } from '../shared/dom.js';
import { thousands } from '../shared/format.js';
import { tr } from '../shared/text.js';

/** 分组 → 每行 [显示名, 取值路径, 可选格式化函数]。
 *
 *  分组标题存的是 **i18n key** 而不是翻好的文字:它是模块级常量,在这里调
 *  `tr()` 会在模块加载那一刻定死,用户之后再切语言就不会跟着变。渲染时再取。 */
const GROUPS = [
  {
    titleKey: 'train_group_train',
    titleFallback: 'Training',
    rows: [
      ['steps', 'steps', thousands],
      ['batch_size', 'batch_size'],
      ['save_freq', 'save_freq', thousands],
      ['seed', 'seed'],
      ['num_workers', 'num_workers'],
    ],
  },
  {
    titleKey: 'train_group_policy',
    titleFallback: 'Policy',
    rows: [
      ['type', 'policy.type'],
      ['dim_model', 'policy.dim_model'],
      ['chunk_size', 'policy.chunk_size'],
      ['n_action_steps', 'policy.n_action_steps'],
      ['vision_backbone', 'policy.vision_backbone'],
      ['kl_weight', 'policy.kl_weight'],
    ],
  },
  {
    titleKey: 'train_group_optimizer',
    titleFallback: 'Optimizer',
    rows: [
      ['type', 'optimizer.type'],
      ['lr', 'optimizer.lr'],
      ['weight_decay', 'optimizer.weight_decay'],
      ['grad_clip_norm', 'optimizer.grad_clip_norm'],
    ],
  },
  {
    titleKey: 'train_group_dataset',
    titleFallback: 'Dataset',
    rows: [
      ['repo_id', 'dataset.repo_id'],
      ['root', 'dataset.root'],
      ['eval_split', 'dataset.eval_split'],
      ['episodes', 'dataset.episodes'],
    ],
  },
];

/** 按点号路径取值;任一层缺失返回 undefined(不抛)。 */
function pick(obj, path) {
  return path.split('.').reduce(
    (acc, key) => (acc === null || acc === undefined ? undefined : acc[key]),
    obj,
  );
}

/** 值 → 显示文本。null/undefined/空串统一成 '—',数组/对象转紧凑 JSON。 */
function display(value, format) {
  if (value === null || value === undefined || value === '') return '—';
  if (format) return format(value);
  if (Array.isArray(value) || typeof value === 'object') {
    return JSON.stringify(value);
  }
  return String(value);
}

function renderGroup(group, config) {
  // 先算一遍哪些行有值 —— 全组都没值的分组直接不渲染,避免出现空卡片。
  const present = group.rows
    .map(([label, path, format]) => [label, pick(config, path), format])
    .filter(([, value]) => value !== undefined && value !== null);

  if (!present.length) return null;

  return el('div', { class: 'bg-gray-900 rounded-lg border border-gray-800 p-4' }, [
    el('div', {
      class: 'text-xs font-semibold text-gray-400 mb-3',
      text: tr(group.titleKey, group.titleFallback),
    }),
    el('div', { class: 'space-y-1.5' }, present.map(([label, value, format]) => (
      el('div', { class: 'flex items-baseline gap-3 text-xs' }, [
        el('span', { class: 'text-gray-500 shrink-0 w-32', text: label }),
        el('span', {
          class: 'text-gray-200 font-mono break-all',
          text: display(value, format),
        }),
      ])
    ))),
  ]);
}

/**
 * @param {HTMLElement} container
 * @param {object} config        train_config.json 的内容
 * @param {{step: number}} meta  这份配置取自哪个 checkpoint
 */
export function renderParams(container, config, meta = {}) {
  clear(container);

  if (!config || Object.keys(config).length === 0) {
    container.appendChild(emptyState(
      tr('train_params_empty',
         'No train_config.json in this checkpoint; parameters unavailable.'),
    ));
    return;
  }

  container.appendChild(el('div', {
    class: 'text-xs text-gray-500 mb-4',
    text: `${tr('train_params_source', 'Source:')} `
      + `checkpoints/${meta.step ?? '?'}/pretrained_model/train_config.json`,
  }));

  const cards = GROUPS.map((g) => renderGroup(g, config)).filter(Boolean);
  container.appendChild(
    el('div', { class: 'grid grid-cols-1 md:grid-cols-2 gap-4' }, cards),
  );
}

function emptyState(message) {
  return el('div', {
    class: 'text-center text-gray-600 text-sm py-10',
    text: message,
  });
}