"""1年の LP(/one-year)の本文を作る。グラフは SVG で、文字は図の外に置く。
使い方: python3 tools/one_year_lp.py → src/data/one-year/body.html を書き出す
色は白・青・黒だけ。数字は公開している作業ログから数えた(2026-10-25 の時点。10月は25日まで)
見た目の決まり(CSS)は src/pages/one-year.astro にある
"""
import math, json, re, glob, datetime, collections
from pathlib import Path
SITE = Path(__file__).resolve().parent.parent
JP = 'Noto Sans JP'; NUM = 'Inter'
TH = {
# ctx(前・比べる相手)は斜めのストライプ(白地に青い線/青地に白い線)。線を引くときは line を使う
'w': dict(ink='#0a0a0a', mute='#6b6f78', ctx='url(#oy-st-w)', line='#1a3fcc', hl='#1a3fcc', grid='#e9ebf0', axis='#0a0a0a', bg='#ffffff'),
'b': dict(ink='#ffffff', mute='#c9d6ff', ctx='url(#oy-st-b)', line='#ffffff', hl='#ffffff', grid='#ffffff30', axis='#ffffff', bg='#1a3fcc'),
}
M = [23, 29, 28, 41, 61, 33, 46, 45, 6, 8, 59]
ML = ['2025.12', '2026.1', '2', '3', '4', '5', '6', '7', '8', '9', '10']
STG = [(0, '画面を作る道具'), (2, 'コードを書かせる'), (4, '全部を|記録に残す'), (5, '書く AI と確かめる AI'), (10, '公開と発信|まで任せる')] # 「|」で改行
def svg(W, H, label):
return f''
def T(x, y, s, size=13, fill='#6b6f78', a='start', w=400, num=False, ls=None):
f = f' font-family="{NUM}"' if num else ''
l = f' letter-spacing="{ls}"' if ls else ''
return f'{s} '
def R(x, y, w, h, fill, title=None):
tt = f'{title} ' if title else ''
return f'{tt} '
def L(x1, y1, x2, y2, c, w=1):
return f' '
# ---------- 01:月ごとの本数(AI の分担の段を下に並べる) ----------
def g_monthly(t='w'):
c = TH[t]; W, H = 1080, 490; top, base = 60, 314; cw = W / 11; bw = cw * 0.76 # 59 の数字の上端が図の上端に来るように詰める
sc = lambda v: (base - top) * v / 61
s = [svg(W, H, '月ごとに公開した作業ログの本数。2025年12月23本から2026年10月59本。4月は61本、8月6本・9月8本')]
for i, v in enumerate(M):
x = cw * i + cw / 2; now = i == 10; end = i == 0
s.append(R(x - bw / 2, base - sc(v), bw, sc(v), c['hl'] if now else c['ctx'], f'{ML[i]}:{v}本'))
if now:
s.append(T(x, base - sc(v) - 16, '59', 72, c['ink'], 'middle', 500, True, '-2'))
elif end:
s.append(T(x, base - sc(v) - 14, '23', 36, c['ink'], 'middle', 500, True))
else:
s.append(T(x, base - sc(v) - 10, v, 15, c['mute'], 'middle', 400, True))
s.append(T(x, base + 26, ML[i], 13, c['ink'] if now else c['mute'], 'middle', 600 if now else 400, True))
s.append(L(0, base, W, base, c['axis'], 1.5))
# 出来事
x89 = cw * 9; y89 = base - sc(8) - 64
s.append(L(cw * 8 + 10, y89 + 10, cw * 10 - 10, y89 + 10, c['mute']) + L(cw * 8 + 10, y89 + 10, cw * 8 + 10, y89 + 18, c['mute']) + L(cw * 10 - 10, y89 + 10, cw * 10 - 10, y89 + 18, c['mute']))
s.append(T(x89, y89, '本業が増えた', 13, c['mute'], 'middle'))
# AI の分担(段):段の始まりに縦の線を立て、段と段の間を空ける。名前は段の幅に収まるよう2行に分ける
ys = base + 80
s.append(T(0, ys - 20, 'AI との分担', 12, c['mute'], w=700))
for k, (i, lab) in enumerate(STG):
x = cw * i + cw / 2 - bw / 2; now = i == 10
x2 = (cw * STG[k + 1][0] + cw / 2 - bw / 2 - 10) if k < 4 else cw * 10 + cw / 2 + bw / 2
col = c['hl'] if now else c['ink']
s.append(R(x, ys, x2 - x, 14, c['hl'] if now else c['ctx']))
s.append(R(x, ys - 8, 2, 52, col))
s.append(T(x + 10, ys + 40, f'{k + 1}', 22, col, 'start', 500, True))
for j, line in enumerate(lab.split('|')):
s.append(T(x + 10, ys + 62 + j * 18, line, 12, col if now else c['mute'], 'start', 700 if now else 400))
s.append(' '); return ''.join(s)
# ---------- 08:画面の中の AI(上)が、ロボットを動かす AI(下)を進めた ----------
def g_bridge(t='w'):
# 左に「画面の中で作った AI の使い方」、右に「それでロボットに起きたこと」を1行ずつ対にして、矢印でつなぐ
# 行はロボットで起きた順。画面に入ると、上の行から順に出る(CSS の .pt)
c = TH[t]; W = 1080; lw = 430; aw = 90; rx = lw + aw; rw = W - rx; hd = 44; rh = 112; gap = 12
# 左の小さい字は、その使い方で具体的に何をしたか(「|」で改行)
rows = [('2', 'コードを書かせる', 'アームを動かす Python を AI が書き、|実機で動かして目で確かめる', '2026.4.2', 'アームの全関節が、AI の書いたコードで動いた'),
('4', '書く AI と確かめる AI', 'AI が書いたアームのコードを、|実機で動かす前に別の AI に読ませる', '2026.5.14', '実機を動かす前に、致命的なバグを4件直した'),
('3', '全部を記録に残す', 'コマンドと実行ログを全部記事に残し、|肩が毎回約2°届かない原因を見つけた', '2026.10.22', 'お手本から学んだ AI が、初めてブロックを箱に入れた'),
('5', '夜も仕組みが回る', '3Dの店舗のシミュレーション18回を、|PC がまとめて回し続ける(6時間40分)', '2026.10.24', '3Dの店舗で、配膳ロボットが止まった回数が 57 → 20回')]
H = hd + len(rows) * (rh + gap) - gap
s = [svg(W, H, '画面の中で作った AI の使い方と、それでロボットに起きたこと。' + '。'.join(f'{a}({st.replace("|", "")})から、{d} {re.sub("<[^>]+>", "", e)}' for n, a, st, d, e in rows))]
s.append(T(0, 18, 'PIXELS', 13, c['hl'], w=600, num=True, ls='1.5') + T(72, 18, '画面の中で作った AI の使い方', 14, c['ink'], w=700))
s.append(T(rx, 18, 'MOTORS', 13, c['hl'], w=600, num=True, ls='1.5') + T(rx + 76, 18, 'それでロボットに起きたこと', 14, c['ink'], w=700))
for k, (n, a, st, d, e) in enumerate(rows):
y = hd + k * (rh + gap); my = y + rh / 2
g = f' '
# 段の番号は出さない(01 の段の番号で、ここでは順が飛んで意味が伝わらないため)
g += T(24, my - 18, a, 18, c['ink'], w=700) + ''.join(T(24, my + 8 + j * 20, ln, 13, c['mute']) for j, ln in enumerate(st.split('|')))
# 矢印:ページの中の矢印はすべて Inter の「→」と同じ形(細い線+開いた矢じり・青)にそろえる
ax0, ax1 = lw + 22, rx - 22
g += f' '
g += R(rx, y, rw, rh, c['hl']) + T(rx + 24, my - 6, d, 13, c['bg'], w=500, num=True) + T(rx + 24, my + 20, e, 17, c['bg'], w=700)
s.append(f'{g} ')
s.append(''); return ''.join(s)
# ---------- 02:公開するもの 2 → 8 ----------
def g_units(t='w'):
# 左に大きく「2 → 8」、右に4×2の大きな四角(案C)。前からある2つは線の四角、1年で増えた6つは塗りの四角。名前は四角の左下
c = TH[t]; W, gap, lw = 1080, 6, 340; cw = (W - lw - 3 * gap) / 4; ch = 132; H = ch * 2 + gap
items = ['作業ログ', '4コママンガ', '解説動画', 'ショート|動画', '配膳ロボット|の試算', 'LP・考え|のページ', '課題地図', 'SNS の|予約投稿']
s = [svg(W, H, '公開するものの種類。2025年12月は作業ログと4コママンガの2種類。2026年10月は解説動画・ショート動画・配膳ロボットの試算・LPと考えのページ・課題地図・SNSの予約投稿が増えて8種類')]
s.append(T(0, 120, '2', 120, c['mute'], w=500, num=True) + T(84, 104, '→', 40, c['hl'], w=400, num=True) + T(136, 120, '8', 120, c['hl'], w=500, num=True) + T(212, 120, '種類', 18, c['hl'], w=700))
s.append(T(0, 160, '2025.12', 13, c['mute'], num=True) + T(136, 160, '2026.10', 13, c['ink'], w=600, num=True))
# 凡例(左の下)
ly = H - 6
s.append(f' ' + T(20, ly, '2025.12 からある', 13, c['ink']))
s.append(R(150, ly - 12, 12, 12, c['hl']) + T(170, ly, '1年で増えた', 13, c['ink']))
for k in range(8):
r, col = divmod(k, 4); x = lw + col * (cw + gap); y = r * (ch + gap); old = k < 2
if old:
s.append(f' ')
else:
s.append(R(x, y, cw, ch, c['hl']))
lines = items[k].split('|')
for n, ln in enumerate(lines):
s.append(T(x + 16, y + ch - 18 - (len(lines) - 1 - n) * 22, ln, 16, c['hl'] if old else c['bg'], w=700))
s.append(''); return ''.join(s)
# ---------- 03:記事1本の文字数 ----------
def g_chars(t='b'):
# 目盛りの線と数字はなくし、値は点の左上の空いた所に書く。出来事の文字は図の上の段に置く
c = TH[t]; W, H = 1080, 400; pl, pr, top, base = 8, 8, 96, 350
P = [(0, 2794, '2025.12'), (3, 3042, '2026.3'), (5, 13991, '2026.5'), (10, 49746, '2026.10')]
x = lambda i: pl + (W - pl - pr) * i / 10; y = lambda v: base - (base - top) * v / 50000
s = [svg(W, H, '記事1本の文字数の中央値。2025年12月2,794字、2026年3月3,042字、5月13,991字、10月49,746字')]
xa, xb = x(3.5), x(4.5)
s.append(L(xa, 24, xa, base, c['mute']) + L(xb, 24, xb, base, c['mute']))
s.append(T(xa - 8, 18, '4月 コードを全文で', 13, c['ink'], 'end', 700) + T(xb + 8, 18, '5月 実行ログも全部', 13, c['ink'], 'start', 700))
d = 'M' + ' L'.join(f'{x(i):.1f},{y(v):.1f}' for i, v, _ in P)
# 面は塗らず、線だけ。画面に入ったら線が左から伸び、点と数字が線の到着に合わせて出る(CSS の .draw)
s.append(f' ')
for i, v, lab in P:
now = i == 10; tt = f' class="pt" style="--t:{0.2 + 1.6 * i / 10:.2f}s"'
g = f' '
if now:
g += T(x(i) - 36, y(v) - 6, '49,746', 48, c['ink'], 'end', 500, True, '-1') + T(x(i) - 16, y(v) - 6, '字', 14, c['ink'], 'end', 700)
elif i == 0:
g += T(x(i), y(v) - 16, f'{v:,}', 16, c['ink'], 'start', 400, True)
elif i == 5: # 線は右上へ上がるので、線にも5月の縦線にもかからない点の右下に置く
g += T(x(i) + 14, y(v) + 30, f'{v:,}', 16, c['ink'], 'start', 400, True)
else:
g += T(x(i) - 12, y(v) - 12, f'{v:,}', 16, c['ink'], 'end', 400, True)
s.append(f'{g} ')
s.append(T(x(i), base + 26, lab, 12, c['mute'], 'middle' if 0 < i < 10 else ('start' if i == 0 else 'end'), num=True))
s.append(L(0, base, W, base, c['axis'], 1.5))
s.append(''); return ''.join(s)
# ---------- 04:かかる時間 前 → いま ----------
def g_speed(t='w'):
# 前の時間を枠(幅いっぱい)、いまの時間を塗りの棒にして、同じ物差し(ふつうの目盛り)で並べる
# 画面に入ると、塗りの棒が前の長さから、いまの長さまで縮む(CSS の .shr)
c = TH[t]; W = 1080; rh = 216; H = rh * 2 - 56; bh = 44
rows = [('記事を送ってから、サイトに公開されるまで', '前は自分の手作業(本人の目安)、いまは AI と仕組みが公開する', '30〜45', '分', '約1', '分', 45, 1, '30倍以上速く'),
('3Dの店舗で、営業135分ぶんを回すのにかかる時間', '実際の店なら135分かかる営業を、PC の Unity で回す', '135', '分', '約8', '分', 135, 8, '約17倍速く')]
s = [svg(W, H, 'かかる時間。記事の公開は30〜45分から約1分で、30倍以上速く。店の135分のシミュレーションは約8分で回り、約17倍速く')]
for k, (lab, sub, a, au, b, bu, full, now, ratio) in enumerate(rows):
y = k * rh
s.append(T(0, y + 16, lab, 16, c['ink'], w=700) + T(W, y + 16, sub, 13, c['mute'], 'end'))
# 大きな数字:前 → いま。右端に何倍速くなったか
s.append(f'{a} {au} '
f'→ {b} {bu} ')
s.append(T(W, y + 76, ratio, 28, c['hl'], 'end', 700))
by = y + 96
s.append(f' ')
wn = max(W * now / full, 8)
s.append(f' ')
s.append(T(W, by + bh + 22, f'{full}分', 12, c['mute'], 'end', num=True) + T(0, by + bh + 22, '0', 12, c['mute'], num=True))
s.append(''); return ''.join(s)
# ---------- 05:実機テスト8回 ----------
def g_runs(t='b'):
# 朝(直す前)の1回と、夕方(直したあと)の8回を2段で並べる。何の8回かを図の中で言う
c = TH[t]; W = 1080; lw = 200; gap = 6; sz = (W - lw - 7 * gap) / 8; hh = 72; r2 = 150
H = r2 + hh + 64
s = [svg(W, H, 'ブロックを箱へ入れる AI を実機で動かした結果。朝、直す前はブロックの近くで止まった。夕方、お手本を直して学習し直すと8回とも途中で止まらず、2回つかめた。箱に入ったのは0回')]
def lab(y, a, b_):
return T(0, y + 30, a, 18, c['ink'], w=700) + T(0, y + 54, b_, 13, c['mute'])
# 朝
s.append(lab(0, '朝', '直す前のお手本で学習'))
s.append(f' ')
s.append(T(lw + sz + 20, 32, 'ブロックの近くで止まり、動かなくなった', 16, c['ink'], w=700) + T(lw + sz + 20, 56, 'お手本の4割が、止まって待っている時間だった', 13, c['mute']))
# 夕方
s.append(lab(r2 - 34, '夕方', '直したお手本で学習し直し'))
s.append(T(W, r2 - 18, '8回とも止まらず / つかめた 2回 / 箱に入った 0回', 14, c['ink'], 'end', 700))
for k in range(8):
x = lw + k * (sz + gap); grasp = k < 2; y = r2
s.append(R(x, y, sz, hh, c['hl'] if grasp else c['ctx'], f'{k + 1}回目'))
s.append(T(x, y + hh + 24, f'{k + 1}回目', 13, c['mute']) + T(x, y + hh + 46, 'つかめた' if grasp else '止まらず', 13, c['ink'] if grasp else c['mute'], w=700 if grasp else 400))
s.append(''); return ''.join(s)
# ---------- 06:時間(円1周=24時間) ----------
def g_time(t='w'):
# 2つの量は全体の内訳ではないので、割合の円ではなく「円1周=24時間」の時計の輪で長さを見せる
c = TH[t]; W, H = 1080, 352; R1, R2, SW = 132, 96, 26 # 下の輪の太さの半分まで入る高さ(190 + 132 + 13 + 余白)
def pt(cx, cy, r, h):
a = math.radians(h / 24 * 360 - 90); return cx + r * math.cos(a), cy + r * math.sin(a)
def arc(cx, cy, r, h0, h1, col, cls='ln'):
x0, y0 = pt(cx, cy, r, h0); x1, y1 = pt(cx, cy, r, h1); big = 1 if (h1 - h0) > 12 else 0
return f' '
def track(cx, cy, r):
return f' '
s = [svg(W, H, '円1周を24時間として表した時間。減った手作業の見込みは約21〜31時間で、1周と少し。PC が回した時間は約6時間40分で、4分の1周と少し')]
s.append(T(0, 16, '円1周 = 24時間', 13, c['mute'], w=700))
# 左:減った手作業 21〜31時間(外の輪が1周目、内の輪が2周目。確かな21時間までを塗り、幅の分をストライプ)
cx, cy = 150, 190
s.append(track(cx, cy, R1) + track(cx, cy, R2))
s.append(arc(cx, cy, R1, 0, 20.5, c['hl']) + arc(cx, cy, R1, 20.5, 23.99, c['ctx']) + arc(cx, cy, R2, 0, 6.75, c['ctx']))
s.append(T(cx, cy + 8, '21〜31', 34, c['hl'], 'middle', 500, True, '-1') + T(cx, cy + 32, '時間', 13, c['hl'], 'middle', 700))
tx = 310
s.append(T(tx, 128, '記事の公開で減った', 16, c['ink'], w=700) + T(tx, 152, '手作業(見込み)', 16, c['ink'], w=700) + T(tx, 178, '41本 × 1本30〜45分', 13, c['mute']) + T(tx, 198, '内の輪は2周目', 13, c['mute']) + T(tx, 218, 'ストライプは見込みの幅', 13, c['mute']))
# 右:PC が回した時間 6時間40分
cx2 = 690
s.append(track(cx2, cy, R1) + arc(cx2, cy, R1, 0, 6.667, c['hl']))
s.append(T(cx2, cy + 8, '6:40', 34, c['ink'], 'middle', 500, True, '-1') + T(cx2, cy + 32, '時間:分', 13, c['ink'], 'middle', 700))
tx2 = 850
s.append(T(tx2, 150, '私が見ていない間に', 16, c['ink'], w=700) + T(tx2, 174, 'PC が回した時間', 16, c['ink'], w=700) + T(tx2, 198, '3Dの店舗のシミュレーション18回', 13, c['mute']))
s.append(''); return ''.join(s)
# ---------- 07:失敗の年表 ----------
def g_lessons(t='b'):
c = TH[t]; W, H = 1080, 120; cw = W / 11; ya = 56
s = [svg(W, H, '任せすぎた失敗が起きた月。2026年3月、7月、10月')]
s.append(L(0, ya, W, ya, c['axis'], 1.5))
for i, lab in enumerate(ML):
s.append(L(cw * i + cw / 2, ya - 5, cw * i + cw / 2, ya + 5, c['mute']) + T(cw * i + cw / 2, ya + 28, lab, 12, c['mute'], 'middle', num=True))
for k, i in enumerate((3, 7, 10)):
x = cw * i + cw / 2
s.append(R(x - 12, ya - 12, 24, 24, c['hl']) + T(x, ya + 5, k + 1, 14, '#1a3fcc' if t == 'b' else '#fff', 'middle', 600, True))
s.append(''); return ''.join(s)
LESS = [('1', '2026.03', 'Unity のデータが、作業中の場面ごと消えた', 'ファイルをまとめて消す命令を禁止。区切りごとに必ず保存'),
('2', '2026.07', '原因を AI と7回取り違えた。AI は「安全の制限は緩めない」にこだわって止まった', '決めるのは私。AI には判断の材料と安全な試し方を出させる'),
('3', '2026.10', '直近20本のうち、研究の記録は7本だけだった', '週に1回、研究の記録が半分を下回っていないかを見る')]
# ---------- 08:GPU と止まった回数 ----------
def g_gpu(t='w'):
# 何の費用かを図の頭で言い切る(初めて見る人が主語を探さないように)
c = TH[t]; W, H = 540, 262; sc = lambda v: 400 * v / 750000; o = 42
s = [svg(W, H, 'ロボットのアームを動かす AI の学習に使った計算機の費用。学習用の PC を買う見積もりは約75万円(買わなかった)。クラウドの GPU を時間で借りた7回の合計は4,677円')]
s.append(T(0, 18, 'ロボットの AI の学習に使った計算機の費用', 16, c['ink'], w=700))
s.append(T(0, o + 18, '学習用の PC を買う見積もり(買わなかった)', 13, c['mute'], w=700))
s.append(R(0, o + 30, sc(750000), 44, c['ctx']) + T(sc(750000) + 12, o + 62, '75万円', 20, c['mute'], num=True))
s.append(T(0, o + 120, 'クラウドの GPU を時間で借りた(7回の合計)', 13, c['ink'], w=700))
s.append(R(0, o + 132, max(4, sc(4677)), 44, c['hl']) + T(16, o + 168, '4,677', 44, c['hl'], w=500, num=True) + T(132, o + 168, '円', 14, c['hl'], w=700))
s.append(''); return ''.join(s)
def g_stops(t='w'):
c = TH[t]; W, H = 540, 262; sc = lambda v: 400 * v / 60; o = 42
s = [svg(W, H, '3Dのファミレスで、配膳ロボット2台が60秒以上止まった回数。地図を渡す前は57回、地図を渡しドリンクバーの位置を動かした後は20回')]
s.append(T(0, 18, '3Dのファミレスで、配膳ロボットが60秒以上止まった回数', 16, c['ink'], w=700))
s.append(T(0, o + 18, 'ロボットに店の地図を渡す前', 13, c['mute'], w=700))
s.append(R(0, o + 30, sc(57), 44, c['ctx']) + T(sc(57) + 12, o + 62, '57回', 20, c['mute'], num=True))
s.append(T(0, o + 120, '地図を渡し、ドリンクバーの位置を動かした後', 13, c['ink'], w=700))
s.append(R(0, o + 132, sc(20), 44, c['hl']) + T(sc(20) + 14, o + 168, '20', 44, c['hl'], w=500, num=True) + T(sc(20) + 70, o + 168, '回', 14, c['hl'], w=700))
s.append(''); return ''.join(s)
def stat(n, u, lab, pre=''):
return f'
{f"{pre} " if pre else ""}{n}{u} {lab}
'
H1 = '画面の中で AI を 使いこなした 土台が、 ロボットを 動かす AI への 道になる。 '
# ---------- 作業ログへのリンク ----------
R_ = {
'LOG083': 'day83', 'LOG123': 'day123', 'LOG149': 'day149', 'LOG190': 'mecharm-descend-tip-shift-two-stage-tracking',
'LOG200': 'pivot-to-probe-control-and-external-review', 'LOG275': 'why-arm-stopped-grasping-root-cause-apick',
'LOG335': 'youtube-script-curation', 'LOG340': 'studio-to-astro-cloudflare', 'LOG342': 'pixelstomotors-com-domain',
'LOG346': 'issues-map-weekly-update', 'LOG350': 'measure-before-selling', 'LOG352': 'teach-replay-19-demos',
'LOG357': 'unity-family-restaurant-serving-robot', 'LOG358': 'pi05-learned-the-pauses', 'LOG361': 'pi05-first-block-in-box',
'LOG363': 'pi05-all-three-positions', 'LOG368': 'from-research-log-to-lp', 'LOG369': 'how-to-finish-a-design',
'LOG374': 'serving-robot-map-planner', 'LOG376': 'sns-autopost-x-linkedin', 'LOG377': 'manga-shorts-speech-bubbles',
}
def src_(*logs):
return '出典:' + '・'.join(f'{n} ' for n in logs) + '
'
# ---------- 最初の画面:前とあと+土台 ----------
ROWS = [ # AI の効果が伝わりやすい順:公開までの時間 → 文字数 → 本数 → 種類 → その土台で動いたロボット
('記事を送ってから公開まで', ('30〜45', '分', '手作業・本人の目安', 100), ('約1', '分', 'AI と仕組みが公開', 3), 'LOG342'),
('記事1本の文字数', ('2,794', '字', '', 6), ('49,746', '字', 'コードとログを全部残す', 100), 'LOG149'),
('月に公開した作業ログ', ('23', '本', '', 39), ('59', '本', '10月は25日まで', 100), ''),
('公開するものの種類', ('2', '種類', '作業ログ・4コママンガ', 25), ('8', '種類', '動画・試算・LP なども', 100), ''),
('動かしたロボット', ('0', '台', '画面の中の絵だけ', 0), ('1', '台', '机の上のロボットアーム', 100), 'LOG123'),
('自分で学習させた AI がブロックを箱に入れた場所', ('0', 'か所', '', 0), ('3', 'か所', 'お手本を取った3か所すべて', 100), 'LOG363'),
]
def row(k, lab, b, a, lg):
def bar(v, cls):
n, u, note, w = v
# 0 は棒を出さず、起点の縦線だけにする(ストライプの切れ端に見えないように)
i = ' ' if w == 0 else f' '
return f'{i}{n} {u} {f"{note} " if note else ""}
'
link = f'{lg} ' if lg else ''
return f'{lab}{link}
{bar(b, "b")}{bar(a, "a")}
'
# 土台の帯:毎日の本数を縦線で並べる(1本の縦線が1日・高さがその日に出した本数)。本数は作業ログの公開日(日本時間)で数える
def daily_counts(start=datetime.date(2025, 12, 10), end=datetime.date(2026, 10, 25), last=380):
# LP の数字は LOG380 までの時点(379本・LOG379 は欠番)。それより後の記事は数えない
J = datetime.timezone(datetime.timedelta(hours=9)); items = {}
for c in json.load(open(SITE / 'src/data/cards.json')):
if re.match(r'^LOG\d+$', c['day']): items[c['day']] = c['published']
for f in glob.glob(str(SITE / 'src/content/records/*.md')):
t = open(f).read(); d = re.search(r'^day:\s*"?(\d+)', t, re.M); dt = re.search(r'^date:\s*(\S+)', t, re.M)
if d: items['LOG' + d.group(1)] = dt.group(1)
items = {k: v for k, v in items.items() if int(k[3:]) <= last}
daily = collections.Counter()
for v in items.values():
v = v.replace('Z', '+00:00')
t = datetime.datetime.fromisoformat(v) if 'T' in v else datetime.datetime.fromisoformat(v[:10]).replace(tzinfo=J)
t = t.astimezone(J).date()
if start <= t <= end: daily[t] += 1
return start, end, daily
def beat():
start, end, daily = daily_counts(); n_days = (end - start).days + 1; W, H = 1000, 120; mx = max(daily.values())
assert sum(daily.values()) == 379, sum(daily.values())
bars = ''.join(f' '
for k in range(n_days) for n in [daily.get(start + datetime.timedelta(days=k), 0)] if n)
labs = []; d = datetime.date(2026, 1, 1)
while d <= end:
labs.append(f'{d.month} '); d = (d.replace(day=28) + datetime.timedelta(days=4)).replace(day=1)
return (f'{bars} '
f'
2025.12 {"".join(labs)}
')
FV = f"""
{H1}
FOUNDATION / 土台2025.12 — 2026.10
AI に書かせ、確かめさせ、記録させる
コードと実行ログを省かずに残した作業ログ。縦線1本が1日で、高さがその日に出した本数
本
{beat()}
AI に任せた作業は速く、多く、広くなり、その上でロボットは自分で学習させた AI が動かすものになった。
2025.12 2026.10
{''.join(row(k, *r) for k, r in enumerate(ROWS))}
"""
def PH(t):
# 文節で折り返す:「|」で区切った塊を inline-block にし、塊の途中では折り返さない(iPhone の Safari は word-break: auto-phrase を無視するため)
return ''.join(f'{x} ' for x in t.split('|'))
def sec(cls, n, en, h2, chart, note='', extra='', srcs=()):
# 見出しの改行は「\n」(広い画面だけ)。狭い画面では「|」で区切った文節の間でだけ折り返す
h2br = ' '.join(PH(line) for line in h2.split('\n'))
return f"""{n} {en}
{h2br} {f'
{note}
' if note else ''}
{chart}
{extra}{src_(*srcs) if srcs else ''}
"""
DEFS = (''
' '
' '
' ')
BODY = DEFS + FV + f"""
{sec('blue', '01', 'LOGS', '本業が増えて|月6本まで落ちた|作業ログが、\n公開まで |AI に任せた|10月は|月59本に戻った', g_monthly('b'), '月ごとの本数(10月は25日まで)。1日に何本も出すようになり記事の分け方も変わったので、生産性の倍率ではない。')}
{sec('', '02', 'OUTPUTS', '公開するものは、|2種類から|8種類に', g_units('w'), '', '', ('LOG346', 'LOG357', 'LOG368', 'LOG376', 'LOG377'))}
{sec('blue', '03', 'RECORDS', '誰でも|同じ手順で|再現できるよう、|コードとログを|省かず載せた。\n記事1本は|約18倍の長さに', g_chars('b'), '記事1本の文字数の中央値(コードとログを含む)。手で書けば省いていた部分も、AI に書かせて全部残した。研究の深さではなく、記録の形の違い。', '', ('LOG149', 'LOG190'))}
{sec('', '04', 'SPEED', '記事の公開は|手作業の|30分以上から|約1分に、\n3Dの店舗の|営業135分は |PC で|約8分に', g_speed('w'), '前の時間を枠、いまの時間を塗りで、同じ物差しに並べた。記事の公開の前の値は本人の目安。', '' + stat('10', '分', 'サイトに機能を足す(頼んでから確かめるまで)', '約') + stat('65', '回', '5時間でのデザインの直しと公開') + stat('20', '本', '4コママンガからひと晩で作ったショート') + '
', ('LOG340', 'LOG342', 'LOG350', 'LOG357', 'LOG369', 'LOG377'))}
{sec('blue', '05', 'REVIEW', 'アームを動かす AI を|学習し直す前に、|別の AI が|お手本のデータの|崩れを見つけた。\n直したデータで|学び直すと、|アームは|8回とも|途中で|止まらなくなった', g_runs('b'), 'ブロックを箱へ入れる AI(π0.5)を、机の上のアームで動かした1日(LOG358)。お手本の待ち時間を削ったデータを、学習の前に codex(コードやデータを確かめる AI)が確かめ、崩れを直してから学習し直した。', '' + stat('4', '件', 'アームのコードの致命的なバグ') + stat('29', 'か所', '解説動画の台本の事実の直し') + stat('18', '件', '判断のページの数字と言い切りの直し') + '
', ('LOG358', 'LOG200', 'LOG335', 'LOG368'))}
{sec('', '06', 'TIME', '作業ログの記事を|サイトに公開する|手作業は、\n41本で|約21〜31時間ぶん|減った計算', g_time('w'), '左は見込みで、実際に減った時間はまだ測っていない。右は通しで回した時間で、人の手が同じだけ空いたという意味ではない。', '', ('LOG374', 'LOG376'))}
{sec('blue', '07', 'LESSONS', 'AI に作業を|任せすぎて|起きた失敗が、\nいまの |AI との|決まりになった', g_lessons('b'), '', '' + ''.join(f'
' for n, d, w, r in LESS) + '
', ('LOG083', 'LOG275'))}
08 PIXELS → MOTORS
{PH('画面の中の |AI の土台が、|机の上の|ロボットでも|働いた')}
{g_gpu('w')}
{g_stops('w')}
画面の中で身につけた AI の使い方が、ロボットのどの出来事につながったか。ロボットで起きた順に並べた。
{g_bridge('w')}
{src_('LOG368', 'LOG374', 'LOG361', 'LOG363', 'LOG352', 'LOG200', 'LOG123')}
{PH("画面の中で|積み上げた|土台を、|次は|ロボットが働く|店舗や病院へ。")}
{PH("このページの数字の出どころは、|公開している|379本の作業ログ。")} {PH("コードも実行ログも失敗も|省かず載せているので、|同じ手順で|たどれる。")}
作業ログを読む→ """
if __name__ == '__main__': # 別のスクリプト(one_year_lab.py)から部品だけ読むときは書き出さない
out = SITE / 'src/data/one-year/body.html'
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(BODY)
print('書き出し', out)