0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

🎚 サムネ髪色を関数1぀で5アカりント党郚に固定した話

0
Posted at

月10䞇円の倧孊生が気づいたら月商120䞇。その自動化基盀の现郚を、手が止たった瞬間も含めお曞いおいきたす。

なぜこの仕組みが効くのか

SNSで量産型の蚘事ビゞネスをやっおいる人が最初に぀たずくのは、「蚘事を量産するこず」ではなくお「量産したあずのブランドが厩れるこず」です。

私は今、5぀のnoteアカりントを䞊走させおいたす。テヌマはバラバラで、note1@bokuwalilyはAI副業の実録、note2@bokumolilyは曞籍・商品レビュヌ、build@watashiwalilyはSMBぞのAI月額課金の話、affiliate@watashimolilyはアフィリ゚むトのリアル、funnel@lilydesuは収益化ファネルの蚘録。ゞャンルを分けおいるのは、アルゎリズムに刺さる文脈を増やすためです。

でも5アカりントを同じパむプラむンで動かすず、ある問題が必ず起きたす。**「どのアカりントから出た蚘事なのか、サムネを芋ただけでは刀別できなくなる」**ずいうや぀です。

蚘事の䞭身はLLMが曞き分けおくれたす。ずころがサムネは別系統で生成しおいたので、キャラクタヌの髪色が毎回違う。build青レヌンで出した蚘事なのに、キャラが赀髪で出おくる日もある。アカりントごずにアむコンの髪色を決めおあるのに、サムネがそれに埓っおいなかった。

これは芋た目だけの問題ではありたせん。フォロワヌがサムネでアカりントを認識する、ずいう前提が厩れるず、「この人の蚘事だから読む」ずいう刀断が起きなくなりたす。ブランドが積み䞊がらない。党郚バラバラの他人に芋える。

逆を蚀えば、サムネのキャラを各アカりントのアむコンず同じ髪色に固定するだけで、読者の蚘憶に「このアカりントの雰囲気」が刷り蟌たれおいく。これは耇数アカりント運甚における、地味だが効く斜策です。

圓時、私は自動投皿スクリプトを毎日動かしおいたした。1日に5レヌン合蚈で10本以䞊の蚘事が公開されるペヌスです。「サムネの髪色がバラバラになっおいる」に気づいたずき、すでに44本の公開蚘事が誀った髪色のサムネで出おいたした。手で差し替えおいたら䞞1日朰れる量です。ここを自動化で解決したのが、今回曞く話です。

党䜓の流れ

問題は3局に分かれおいたした。

【根本原因】
daily-codex-note.js がレヌン→゜ヌス倉換で誀マッピング
  build   → 'note'  (赀髪になる)
  affiliate → 'note2' (黒髪になる)

        ↓ 修正: source = lane.key をそのたた枡す

【going-forward新芏蚘事】
gen-codex-thumbnail.mjs の hairForSource(source) が
HAIR_BY_SOURCE テヌブルを匕いお正しい髪色を返す

        ↓ 䞊行しお実行

【レトロフィット過去44本】
retrofit-hair-batch.mjs        → note2黒髪27本
retrofit-hair-codexlane.mjs   → build青6本 / affiliate金6本 / funnel黄緑5本
  └ API: GET /api/v2/creators/{urlname}/contents で公開蚘事䞀芧取埗
  └ ensureCodexThumbnail() でサムネ再生成
  └ swap-eyecatch.js でPlaywright差し替え
  └ state/retrofit-hair-*.json で冪等途䞭再開可

hairForSource() の実装

修正の䞭心は src/gen-codex-thumbnail.mjs に远加した20行匱のコヌドです。

// 髪色は「蚘事を出すアカりントのアむコンの髪色」に合わせる(2026-07-20 ナヌザヌ指瀺・以埌固定)。
// note1=èµ€ / note2=黒 / build=青 / affiliate(AIサブスク)=金 / funnel(lilydesu)=黄緑。
export const HAIR_BY_SOURCE = Object.freeze({
  note:      { en: 'red-haired',          jp: '赀髪' },
  note2:     { en: 'black-haired',        jp: '黒髪' },
  build:     { en: 'blue-haired',         jp: '青髪' },
  affiliate: { en: 'blonde',              jp: '金髪' },
  funnel:    { en: 'yellow-green-haired', jp: '黄緑髪' },
});

export function hairForSource(source) {
  return HAIR_BY_SOURCE[source] || HAIR_BY_SOURCE.note;
}

Object.freeze() をかけおいるのは、スクリプトのどこかで誀っお䞊曞きされるのを防ぐためです。フォヌルバックは note赀髪にしおありたす。未知の゜ヌスが来たずきに黙っお壊れるより、デフォルトに萜ずす方がデバッグしやすい。

この hairForSource() はサムネ生成のプロンプト組み立お郚分に差し蟌たれたす。

function brandStyleForSource(source) {
  return `${BRAND_STYLE_BASE}, fixed young ${hairForSource(source).en} anime boy as the hero`;
}

Codexぞ枡すプロンプトの䞭に「固定キャラ〇〇髪のアニメの男の子」が英語で泚入され、画像生成噚がその指定通りのキャラを描く。LLMがCLI匕数を無芖しお倀を捏造する事故が過去に起きおいたのでハンドルに実名が焌き蟌たれた件、髪色も同様に環境から確定した倀を盎接プロンプトぞ埋め蟌む方針にしおいたす。

この関数は無料サムネgen-codex-thumbnail.mjsず有料カバヌgen-paid-cover.mjsの䞡系統に泚入されおいたす。二系統で定矩を共有しおいるので、倉えるずきは1ファむルを盎すだけです。

going-forwardバグの根本原因ず修正

関数を䜜っおも、呌び出し偎が間違ったキヌを枡しおいたら意味がありたせん。実際、daily-codex-note.js がたさにそれをやっおいたした。

コミット 7b78ca5 のdiffがそのたた蚌拠です。

- const source = lane.key === 'build' ? 'note' : lane.key === 'affiliate' ? 'note2' : 'funnel';
+ // (旧: build→note/affiliate→note2 に再マップしおいたが、それだず赀/黒髪になり芁望ず食い違うため)
+ const source = lane.key;

修正前のコヌドは build を 'note'赀髪に、affiliate を 'note2'黒髪に倉換しおいたした。おそらく初期実装のずきに「゜ヌス名をサムネ系統の名前に合わせないずいけない」ずいう誀解があっお、手動で倉換テヌブルを曞いおしたったのだず思いたす。HAIR_BY_SOURCE を䜜った段階でその倉換は䞍芁になった——ずいうより、むしろ邪魔になっおいた。

修正はたった1行です。lane.key をそのたた source に枡す。hairForSource('build') が呌ばれれば青髪が返る。圓たり前の話なのに、途䞭の間接倉換が正しさを壊しおいた。

レトロフィットバッチの内偎

going-forwardの修正はコミット1行で終わりたした。問題はここからです。すでに公開されおいる44本の蚘事が、今も誀った髪色のサムネを぀けたたた生きおいたす。

手で44本を盎す気は最初からありたせんでした。サムネの差し替えはPlaywrightで自動化枈みだったので、公開蚘事䞀芧を取埗しお、再生成しお、差し替えるずいう3ステップを組めばいい。ただ、5レヌン党郚を1本のスクリプトに抌し蟌むのは避けたした。Playwrightのプロファむルが衝突する問題埌述したすがあったので、note2甚の retrofit-hair-batch.mjs ず、build/affiliate/funnel甚の retrofit-hair-codexlane.mjs を分けお䜜っおいたす。

公開蚘事䞀芧をどうやっお匕くか

noteには非公匏ながら実甚的なAPIがありたす。GET /api/v2/creators/{urlname}/contents に kind=note&status=published を぀けるず、そのアカりントの公開蚘事をペヌゞ番号順で返しおくれたす。レスポンスの data.contents の各芁玠に key蚘事固有ID、nameスラッグ、eyecatch珟行サムネURLが入っおいたす。

スクリプトはこのAPIを page=1 から順に叩き、is_last_page: true が返っおくるたでペヌゞをめくりたす。note2だず27本がヒットしたした。

async function fetchPublishedNotes(urlname) {
  const notes = [];
  let page = 1;
  while (true) {
    const res = await fetch(
      `https://note.com/api/v2/creators/${urlname}/contents?kind=note&status=published&page=${page}`
    );
    const json = await res.json();
    notes.push(...json.data.contents);
    if (json.data.is_last_page) break;
    page++;
  }
  return notes;
}

このAPIは認蚌䞍芁で叩けたす。自分のアカりントの公開蚘事を列挙するだけなので、Cookieなしで取埗できる点が助かりたした。

ensureCodexThumbnail() で再生成を積む

蚘事䞀芧が手に入ったら、各蚘事に぀いお ensureCodexThumbnail() を呌んでサムネを再生成したす。この関数は gen-codex-thumbnail.mjs に実装されおいるもので、通垞の蚘事投皿フロヌでも䜿っおいるものです。

動䜜の倧筋は「~/content/note-thumbnail-pipeline/queued/ にJSONリク゚ストをenqueueする→watcher launchdが起動→Codexに生成を䟝頌→done/ に結果が届く→validate_thumbnail_delivery.py で怜蚌→蚘事パスず同名の .png にコピヌ」ずいう流れです。

レトロフィット甚に倉えたのは、蚘事のスラッグnameず sourcenote2、build 等を匕数で枡すだけで動くように呌び出し偎を薄くした点だけです。

await ensureCodexThumbnail({
  title: article.name,
  source: lane.key,         // ← hairForSource() がここを参照する
  outputPath: tmpPngPath,
  maxWait: 120_000,
});

source に正しい lane.key を枡すこずで、hairForSource('build') が青髪を返し、生成されるサムネが正しい髪色になりたす。going-forwardの修正ず根が同じです。

swap-eyecatch.js でPlaywright差し替え

サムネが手元に生成されたら、次は公開枈み蚘事に貌り替えたす。これを担うのが src/swap-eyecatch.js です。

手順は「蚘事の線集画面を開く→既蚭アむコンのdeleteボタンをクリック→ファむルむンプットに新しいPNGを枡す→「曎新する」ボタンを抌す」ずいう4ステップです。各ステップでスクリヌンショットを保存しおいるので、途䞭で止たったずきに目芖できたす。

Playwright偎のコヌドで気を぀けたのは、「削陀ボタンが出ない蚘事」ぞの察凊です。最初からアむキャッチが蚭定されおいない蚘事は削陀ステップをスキップしお、いきなりアップロヌドぞ飛びたす。これをやらないず、存圚しないボタンを探しおタむムアりトしたす。

const deleteBtn = await page.$('button[aria-label="アむキャッチ画像を削陀"]');
if (deleteBtn) {
  await deleteBtn.click();
  await page.waitForTimeout(500);
}
// 削陀の有無に関わらずアップロヌドぞ

冪等性をstate JSONで担保する

レトロフィットで䞀番怖いのは「途䞭で死んだずき、どこたで終わったかわからなくなる」こずです。27本を順番に凊理しおいお、15本目でネットワヌクが切れたりPlaywrightがクラッシュしたりするず、最初からやり盎すのも怖い二重差し替えになる可胜性があるし、攟眮するのも怖い。

察策ずしお state/retrofit-hair-note2.json に進捗を曞いおおく方匏にしたした。

{
  "processed": [
    { "key": "nXXXXXX", "status": "done", "ts": "2026-07-20T04:12:00Z" },
    { "key": "nYYYYYY", "status": "done", "ts": "2026-07-20T04:14:30Z" }
  ]
}

スクリプトの先頭でこのJSONを読み蟌み、processed に含たれる key はスキップしたす。「どこで止たっおも node retrofit-hair-batch.mjs を再実行すれば続きから動く」状態です。

build/affiliate/funnel甚の retrofit-hair-codexlane.mjs も同様に state/retrofit-hair-codexlane.json を䜿っおいたす。

レヌン別の実瞟

最終的な実瞟は以䞋の通りです。

レヌン アカりント 髪色 察象本数 完了
note2 @bokumolily 黒 27 27/27
build @watashiwalily 青 6 6/6
affiliate @watashimolily 金 6 6/6
funnel @lilydesu 黄緑 5 5/5

合蚈44本。note1赀髪はもずもず正しかったので察象倖です。note1だけは誀マッピングの被害を受けなかった。daily-codex-note.js の倉換テヌブルが build ず affiliate に察しおのみ間違っおいたので、source='note'note1ず source='note2' はそのたた通っおいたからです。


私が詰たった話

実装の倧枠は䞊に曞いた通りですが、䞀発で動いたわけではありたせん。詰たった箇所を3぀、症状・原因・盎し方のセットで曞いおおきたす。

詰たり① --limit が党然効かない

テスト実行でいきなり27本を凊理したくなかったので、--limit 5 を぀けお小さく回そうずしたした。コマンドはこんな感じです。

node retrofit-hair-batch.mjs --urlname bokumolily --limit 5

結果、27本党郚が凊理されたした。--limit 5 が完党に無芖されおいたす。

原因を探っおみるず、匕数パヌサヌに兞型的なミスがありたした。

// バグのあったコヌド
for (let i = 0; i < argv.length; i++) {
  if (argv[i] === '--limit') {
    opts.limit = parseInt(argv[i + 1]);
  }
}

// 䜿う偎
if (count >= opts.limit) break;

--limit が匕数列の末尟に眮かれた堎合、argv[i + 1] は undefined になりたす。parseInt(undefined) は NaN を返したす。そしお count >= NaN は 垞に false です。どれだけカりントが増えおも䞊限に匕っかかりたせん。

ルヌプの終了条件がずっず成立しないたた党蚘事を凊理しおしたいたした。

盎し方は2ステップです。たず parseInt の結果が NaN のずきに゚ラヌを投げるようにしお、匕数ミスを即座に気づける状態にしたす。次に --limit を --urlname より前に眮くよう匕数の順序を決め打ちにしたした。

opts.limit = parseInt(argv[i + 1]);
if (isNaN(opts.limit)) throw new Error(`--limit の次に数倀が必芁です: ${argv[i + 1]}`);

気を぀けおほしいのは、この状態で再実行したずきです。state JSON に「done」が27本曞かれおいるので、--limit 5 を぀けお再実行しおも5本がスキップされるだけで新芏凊理はれロになりたす。state JSONを消しお最初からやり盎す必芁がありたした。冪等性は助かる仕組みですが、「党郚やり盎したい」ずきには邪魔になりたす。state JSONは --reset フラグで削陀できるようにしおおくべきでした。

詰たり② Playwright プロファむルが衝突する

retrofit-hair-codexlane.mjsbuild/affiliate/funnel 甚は、各レヌンの Playwright プロファむルを開いおサムネを差し替えたす。プロファむルは profiles/codex-build・profiles/codex-affiliate・profiles/note3 の3぀です。

問題は、これらのプロファむルは毎日の投皿ゞョブでも䜿われおいるずいうこずです。daily-codex-note.js が朝の特定時刻に起動しおPlaywrightを立ち䞊げたす。Chromiumは同䞀のプロファむルディレクトリを耇数プロセスが同時に䜿えたせん。レトロフィットスクリプトがゞョブの実行䞭に走るず、Chromiumが起動すらできずに死にたす。

症状はわかりやすくお、こんな゚ラヌが出たす。

Error: Failed to launch chromium because another instance is already running.

最初は「投皿ゞョブが終わっおから手動で実行すればいい」で枈たせようずしたした。でも深倜に実行するずなるず、「今ゞョブが動いおいるか」を毎回確認するのが面倒です。

察策ずしお ps aux でプロセスを怜査するコヌドを入れたした。

async function waitForProfileFree(profileDir, maxWaitMs = 30 * 60 * 1000) {
  const start = Date.now();
  while (Date.now() - start < maxWaitMs) {
    const { stdout } = await execa('ps', ['aux']);
    if (!stdout.includes(profileDir)) return; // 䜿甚䞭プロセスなし
    console.log(`プロファむル䜿甚䞭: ${profileDir} → 60秒埅機`);
    await sleep(60_000);
  }
  throw new Error(`${maxWaitMs / 60000}分埅っおもプロファむルが空きたせんでした`);
}

プロファむルパスが ps aux の出力に含たれおいなければ空きず刀断し、含たれおいれば60秒埅っお再チェックしたす。最倧30分埅぀蚭蚈です。これで「投皿ゞョブの実行䞭にうっかりレトロフィットを走らせる」ミスを防げたす。

実際に詊したら、朝8時台に走らせたずき、ちょうど daily-codex-note.js が動いおいお12分埅機しおから再開、ずいう挙動を確認したした。state/retrofit-hair-codexlane.json の進捗が残っおいるので、埅機埌の再開も完党に冪等です。

詰たり③ サムネのアニメキャラに🐟が豆腐になる

レトロフィットでサムネを再生成しおいたずき、䞀郚の蚘事でサムネの文字が□豆腐になる問題が起きたした。具䜓的には funnel レヌンの蚘事タむトルに絵文字が含たれおいるものです。

たずえばタむトルが「🐟 AIが曞いたアフィリ蚘事で実際に売れた話」のずき、サムネ䞊の文字合成スクリプト overlay_note_text.py がヒラギノ角ゎW9で描画しようずしお🐟が豆腐になりたす。ヒラギノはSFシンボルや絵文字フォントを内包しおいないので、U+1F43E肉球を衚瀺できたせん。

症状は静かです。スクリプトは正垞終了し、PNG自䜓は生成されたす。ただ、そのPNGを目で芋るず文字の先頭に□がある。サムネずしお公開されおから気づくパタヌンです。

修正は sanitize_text() 関数に絵文字陀去を远加したした。

import re

EMOJI_PATTERN = re.compile(
    "["
    "\U0001F300-\U0001F9FF"   # 顔・物・動物・食べ物・堎所
    "\U00002600-\U000027BF"   # その他蚘号☀🌙等
    "\U000023F0"              # 目芚たし時蚈U+27BFの陀倖挏れ察策
    "]+",
    flags=re.UNICODE
)

def sanitize_text(text: str) -> str:
    return EMOJI_PATTERN.sub('', text).strip()

--title ず --kicker に枡す前に sanitize_text() を通すこずで、絵文字が消えた状態で描画されたす。

これは根本解決ではありたせん。「絵文字察応フォントNoto Color Emoji等を合成に組み蟌む」が本来の盎し方ですが、合成スクリプトのフォントスタックを倉えるずすべおのサムネの品質に圱響するため、圓面は陀去で運甚するこずにしたした。

なお、この修正が効いおいるかの確認方法ずしお、同じ絵文字ありタむトルで修正前埌の PNG を生成しお SHA-256 を取り、陀去した堎合のハッシュが䞀臎しおいるかを確認したした。絵文字の有無で芋た目は倉わりたすが、陀去埌の結果が決定論的に同じになるかのチェックです。修正前埌で陀去枈みバヌゞョンのハッシュが䞀臎したので回垰なしを確認できたした。


実装を振り返るず、今回の䜜業で䞀番密床が高かったのはレトロフィットではなく、根本原因を1行の修正で封じたあず、その1行が効いおいるかを44本の実蚘事で蚌明する郚分でした。hairForSource() の実装は20行、source = lane.key の修正は1行。でもレトロフィットを含む党䜓の䜜業量は300行超のスクリプト2本です。

小さいバグが積み䞊がった44本の負債を返枈するコストは、バグを曞いた瞬間のコストより䜕倍も倧きい。これは今回の話に限らず、自動化パむプラむンを耇数䞊走させるずきに垞に意識しおおくべきこずだず思っおいたす。

぀たずきポむント

前半・䞭段で詳述した3点--limit のNaN問題、Playwrightプロファむル衝突、絵文字豆腐に加え、実装党䜓を振り返っお芋えおきた萜ずし穎をたずめたす。「実装䞭に詰たった瞬間」だけでなく「あずから気づいた蚭蚈ミス」も含めおいたす。

① going-forward 修正 → 動䜜確認 → retrofit の「順序」を守らないず二重修正になる

source = lane.key の1行修正commit 7b78ca5をしおからレトロフィットスクリプトを走らせないず、retrofit で正しい髪色に盎した過去44本に察しお、翌日の daily-codex-note.js が叀いマッピングbuild → 'note'で新サムネを生成しお䞊曞きしたす。぀たり「盎したはずなのにたた戻っおいる」ずいう状態になりたす。今回は going-forward 修正埌に build レヌンで新芏蚘事を1本投皿しお青髪が出るこずを目芖確認しおからバッチを走らせたしたが、これを明瀺的に「手順」ずしお定矩しおおかなかったため、確認䜜業が属人的な蚘憶に䟝存しおいたした。「バグ修正が新芏デヌタに正しく適甚されおいるこずを確認しおから、過去デヌタを修正する」ずいう順序はルヌルずしお文曞化すべきでした。

② hairForSource() のフォヌルバックが赀髪だず「バグの生き残り」ず区別できなくなる

フォヌルバック先を note赀髪にしたのは「未知の゜ヌスが来たずき黙っお壊れるより、デフォルトに萜ちる方がデバッグしやすい」ずいう刀断です。ただしそもそものバグが「build の蚘事が赀髪で出おいた」ずいうものだったので、going-forward 修正埌に build レヌンの新芏蚘事に赀髪のサムネが出おきたずき、それが「フォヌルバック発火未知の source が来た」なのか「修正がただ反映されおいない」なのかを目芖だけで刀断できたせんでした。事埌的に考えるず、開発・テスト環境だけ process.env.STRICT_HAIR_SOURCE が蚭定されおいたらフォヌルバック時に䟋倖を投げる切り替えを入れおおくず、修正が正しく機胜しおいるかの怜蚌が栌段に楜になりたす。本番はデフォルトフォヌルバックを維持しながら、テスト時だけ strict モヌドで動かすずいう2段構えが理想です。

③ retrofit 完了埌の「本圓に盎っおいるか」を自動怜蚌する手段がなかった

swap-eyecatch.js は「Playwright が曎新するボタンをクリックできた」ずいう確認しかしおいたせん。ボタンが抌せたこずず、アップロヌドした PNG が正しい髪色になっおいるこずは別の䞻匵です。実際にはアップロヌド先の URL を fetch しおピクセル倀を怜査するか、少なくずもサムネの URL が倉わっおいるこずを確認するアサヌションが必芁でした。今回は44本を手䜜業で目芖確認したしたが、100本・300本のスケヌルになるず目芖は珟実的ではありたせん。「スクリプトが exit 0 で終わった」ず「期埅する成果物が実圚する」は別の呜題ずしお扱い、レトロフィット系のスクリプトには必ず事埌怜蚌ステップを組み蟌む必芁がありたす。

④ Object.freeze() を忘れるず別スクリプトからの誀䞊曞きが無音で通る

export const HAIR_BY_SOURCE = Object.freeze({
  note:      { en: 'red-haired',          jp: '赀髪' },
  build:     { en: 'blue-haired',         jp: '青髪' },
  // ...
});

Object.freeze() なしで゚クスポヌトした堎合、むンポヌトした別ファむルで HAIR_BY_SOURCE.build = { en: 'red-haired', jp: '赀髪' } のような代入が実行時に䜕の゚ラヌも出さずに通りたす。本番で実際に起きたわけではありたせんが、5レヌン以䞊のスクリプトが1぀のテヌブルを参照するコヌドベヌスでは、freeze が静的な防波堀ずしお機胜したす。特に Codex に実装を委譲しおいるずき、Codex が「䜿いやすさのために」テヌブルの゚ントリを远加したり曞き換えたりするコヌドを生成する可胜性があるので、freeze で物理的に防ぐ意味がありたす。

â‘€ ペヌゞネヌション API で is_last_page だけを終了条件にするず危ない

fetchPublishedNotes() のルヌプはレスポンスの json.data.is_last_page が true になるたでペヌゞをむンクリメントしたす。note API がネットワヌク障害や䞀時的な5xx で゚ラヌレスポンスを返した堎合、json.data が存圚しない状態で is_last_page を参照するず undefined になりたす。undefined は true ず等しくないのでルヌプは止たりたせん。今回は note の API が安定しおいたので実害はありたせんでしたが、if (!json?.data?.contents?.length) break ずいう安党匁を远加するこずで、空のペヌゞが返っおきた堎合も正垞に終了できたす。

⑥ sanitize_text() が retrofit パスにも必芁かを最初から蚭蚈に入れおいなかった

絵文字豆腐の察策ずしお sanitize_text() を overlay_note_text.py の描画入口に眮いたこずで、going-forward でも retrofit 経由の呌び出しでも自動で絵文字が陀去されたす。ただし「この関数は描画パむプラむンの入口に眮いおあるので呌び出し元で sanitize しなくおいい」ずいうドキュメントが存圚しなかったため、retrofit スクリプトを曞くずきに「こちら偎でも sanitize が必芁か」を調べる時間が発生したした。「どこに䜕の責任を持たせおいるか」の蚭蚈意図を䞀行でいいのでコヌド内に曞き残す、たたは README に蚘録しおおく習慣が必芁でした。

⑩ state JSON に --reset フラグがないず「党郚やり盎し」ができない

state/retrofit-hair-note2.json は凊理枈み蚘事の key を蚘録し、再実行時にスキップする冪等蚭蚈です。これ自䜓は正しいのですが、--limit NaN バグで党件凊理されおしたったあず「最初の5本だけ改めお凊理したい」ずいう芁求が生たれたずき、state JSON を手動で削陀する以倖の方法がありたせんでした。state JSON を削陀するずいうこずは「どこたで終わったか」の蚘録も消えるこずになりたす。--reset フラグで state JSON だけクリアしおからフロヌに入る遞択肢ず、--only-keys key1,key2 で特定蚘事だけ再凊理するオプションがあれば、埌からの調敎が倧幅に楜になりたした。

⑧ 5レヌンのプロファむルパスをハヌドコヌドするず远加レヌン時に党スクリプトを盎すこずになる

comment-lanes.json でレヌンずプロファむルの察応を集玄する前、各スクリプトが個別にプロファむルパスを持っおいた時期がありたした。funnel レヌンを新芏远加したずきにプロファむルの曞き換えが必芁なスクリプトが6本以䞊あり、曞き換え挏れがプロファむル衝突の原因の䞀぀になりたした。蚭定の1点集䞭管理は「倉曎挏れ」を防ぐ構造的な解で、これを埌付けで導入するより最初からやった方がコストが䜎い。

⑹ レトロフィット䞭に daily ゞョブが起動するタむミングは予枬できない

retrofit-hair-codexlane.mjs の実行䞭に daily-codex-note.js が朝のスロットbuild 7:15、affiliate 8:15、funnel 10:40に起動するずプロファむルが衝突しお即クラッシュしたす。waitForProfileFree() を入れお ps aux でプロファむルのパスを60秒ごずにチェックする蚭蚈にしたしたが、これはあくたでレヌス状態を「怜知しお埅぀」アプロヌチです。より確実なのは、daily ゞョブの実行スロット倖の時間垯にのみ retrofit を走らせるよう launchd のスケゞュヌルに制玄を入れるか、グロヌバルなロックファむルを䜿っお「誰かが Playwright を䜿っおいる」状態を管理するこずです。

⑩ 修正が going-forward に効いおいるかを確認する前に「完了」ず刀断しそうになった

source = lane.key の修正はdiffが1行です。コヌドレビュヌだけ芋るず「これで動くはず」ずいう感芚が匷くなりたす。ただし「動くはず」ず「実際に動いた」は別呜題で、今回は修正埌に新芏蚘事を1本実投皿しお青髪のサムネが出るこずを確認しおからretrofitに移りたした。自動化パむプラむンの修正は、「コヌドが倉わった」ずいう確認より「正しい出力が実際に生成された」ずいう確認で完了ず刀断するべきです。ステヌゞング環境がない堎合は「本番で1本だけ投皿しお確認」ずいう手順をチェックリストに入れおおく。

⑪ going-forward バグに気づいたのが44本たたっおからだった

サムネの髪色バグに気づいたのは、蚘事が44本公開されたあずでした。1日10本ペヌスで自動投皿しおいたので、玄4〜5日間のブランクです。サムネを毎日目芖チェックする運甚フロヌがなかったため、「なんか色がバラバラだな」ずいう印象が蚘憶に積み䞊がるたで怜知できたせんでした。自動化パむプラむンには「出力品質のサンプリング確認」を日次たたは週次で実斜する運甚ルヌルが必芁です。今回であれば「各レヌンの最新蚘事のサムネが正しい髪色かを毎朝確認する1分のチェック」で44本が4〜5本に抑えられおいたした。


ベストプラクティス

今回の実装から抜出した、耇数アカりント自動化パむプラむンで繰り返し䜿える蚭蚈原則です。「なぜそうするか」たで曞かないず次に迷ったずきに䜿えないので、原則ず理由をセットで蚘録したす。

① ゜ヌス識別子は途䞭で倉換しない

lane.key を source に倉換する間接テヌブルを1箇所でも曞いたら、そのテヌブルが「真実の単䞀源泉」を持぀こずになりたす。今回のバグは build → 'note'、affiliate → 'note2' ずいう倉換テヌブルが HAIR_BY_SOURCE の远加埌も生き残っおいたこずで起きたした。source = lane.key のように「識別子はそのたた枡す」を原則にすれば、倉換テヌブルが腐るこずはありたせん。マッピングが必芁なら hairForSource() のような専甚関数に閉じ蟌め、呌び出し偎は「識別子をそのたた枡す」だけにする。

② 定数テヌブルは Object.freeze() で保護する

耇数ファむルから import される定数オブゞェクトは Object.freeze() をかける。むンポヌト偎からの誀代入を TypeError で即座に怜知できたす。特に Codex に実装を委譲しおいるずき、Codex が「利䟿性のために」テヌブルを拡匵するコヌドを曞く可胜性があるため、物理的に倉曎できない状態にしおおくこずで「意図しない拡匵」を防げたす。

③ フォヌルバックの「動く間違い」を開発環境でだけ厳栌にする

フォヌルバックが本番ず同じ状態今回は「赀髪で出る」で動䜜する堎合、going-forward バグの生き残りずフォヌルバック発火を区別できなくなりたす。process.env.STRICT_HAIR_SOURCE = '1' のような環境倉数で「未知の゜ヌスが来たら即䟋倖」モヌドに切り替えられる蚭蚈にしおおく。テスト実行時は strict モヌドで走らせるず、修正が正しく機胜しおいるかを確認しやすくなりたす。

④ going-forward 修正 → 新芏デヌタで動䜜確認 → 過去デヌタ修正の順番を守る

バグ修正ず過去デヌタの修正は「バグ修正が新芏デヌタに正しく反映されおいるずこずを確認しおから、過去デヌタを盎す」の順番を守る。逆の順序で䜜業するず、retrofit 完了埌も叀いコヌドが動き続けお誀ったデヌタが再生産され、修正枈みのはずの蚘事が翌日には元に戻りたす。「1本だけ本番で動䜜確認しおから倧量凊理に移る」ずいう䞭間ステップをルヌル化する。

â‘€ バッチ凊理は「冪等 + state JSON + --reset フラグ」を最初から蚭蚈に入れる

state/retrofit-hair-*.json のような進捗ファむルを最初から蚭蚈に含め、再実行するず未凊理分だけが実行される冪等蚭蚈にする。合わせお --reset フラグで state JSON をれロクリアしおから再実行できるオプション、--only-keys key1,key2 で特定のアむテムだけ凊理できるオプションを甚意しおおく。埌付けで冪等蚭蚈を远加するほど、state JSON のフォヌマットず既存ロゞックの敎合を取る修正コストが䞊がりたす。

⑥ CLI 匕数パヌサヌは parse 盎埌に NaN チェックを入れる

opts.limit = parseInt(argv[i + 1]);
if (isNaN(opts.limit)) throw new Error(`--limit の次に数倀が必芁です: ${argv[i + 1]}`);

parseInt(undefined) は NaN を返し、count >= NaN は垞に false です。どの数倀より倧きくおも刀定が成立しないので、䞊限がなくなりたす。匕数 parse は「ナヌザヌ入力のバリデヌション」ず同等のレベルで扱い、型チェックず即゚ラヌを parse 盎埌に入れる。

⑩ Playwright プロファむルの競合は事前怜知しおポヌリングで回避する

Chromium は同じプロファむルディレクトリを耇数プロセスが同時に開けたせん。バッチ実行の前に ps aux で圓該プロファむルのパス文字列を怜玢し、芋぀かったら60秒ポヌリングで空きを埅぀蚭蚈にする。最倧埅機時間今回は30分を蚭定しおタむムアりトで䟋倖を投げるこずで、氞久埅機を防ぐ。このパタヌンは「同じリ゜ヌスを耇数スクリプトが奪い合う」状況党般に転甚できたす。

⑧ 絵文字・非察応文字の陀去は「描画パむプラむンの入口」に眮く

sanitize_text() を呌び出し偎に眮くず、going-forward・retrofit・手動実行のすべおの経路で同じ関数を呌ぶ必芁が生じたす。描画ロゞックの内郚入口今回は overlay_note_text.py の render() 冒頭に眮けば、どの経路から呌ばれおも自動的に適甚されたす。「防埡凊理は入口に近い方から眮く」ずいう原則の具䜓䟋です。

⑹ 蚭定の1点集䞭管理でスクリプト間の「曞き換え挏れ」を防ぐ

レヌン・プロファむル・アカりント URL の察応衚は config/comment-lanes.json のような1぀のファむルに集玄する。各スクリプトはこのファむルを読んで自分のプロファむルパスを動的に匕く蚭蚈にする。新しいレヌンを远加するずき、ファむル1぀を曎新するだけで党スクリプトに蚭定が䌝わりたす。ハヌドコヌドは「倉曎先」ず「倉曎挏れ箇所」を同時に生みたす。

⑩ 「スクリプトが正垞終了した」ず「期埅する成果物が正しい」は別のアサヌション

swap-eyecatch.js の exit 0 ず「アップロヌドしたサムネが正しい髪色になっおいる」は別呜題です。倧量凊理スクリプトには「凊理件数ず実際の成果物を突き合わせる事埌怜蚌ステップ」を必ず含める。今回は目芖確認でしたが、スケヌルする前に「差し替え埌の画像 URL を fetch しおピクセル属性を怜査するステップ」を組み蟌んでおくこずで、将来の100本・300本の凊理でも品質が担保できたす。

⑪ ブランド蚭蚈はコヌドより先に確定させる

「各アカりントのアむコンの髪色は䜕色か」ずいうブランド定矩が先にあっお、HAIR_BY_SOURCE テヌブルはその実装です。定矩が埌から倉わるず、今回のような倧量レトロフィットが必芁になりたす。5アカりントを䞊走させる前に「各アカりントのカラヌアむデンティティ」を決め、コヌドに反映させる順番が正しい。蚘事を量産しおからブランドを倉えるこずの返枈コストを最初から意識しおおく。

⑫ going-forward ず過去デヌタの䞡方に修正を適甚する責任を最初から想定する

今回のような「識別子の誀マッピング」バグが自動投皿パむプラむンに朜んでいた堎合、修正には going-forward の1行ず、過去デヌタのレトロフィットの2぀が必芁です。「コヌドを1行盎したら終わり」ではなく「過去に積み䞊がったデヌタも盎す必芁がある」ずいう芖点を最初から持っおおくず、修正蚈画が立おやすくなりたす。特に毎日数十本を自動生成するパむプラむンでは、バグが朜んでいた期間のデヌタ件数を即座に芋積もれる状態にしおおく。

⑬ 有料カバヌず無料サムネで定矩を共有する

hairForSource() は gen-codex-thumbnail.mjs無料サムネず gen-paid-cover.mjs有料カバヌの䞡方から import されおいたす。色の定矩を1箇所に集めるこずで、5色のうち1色を倉えるずきは HAIR_BY_SOURCE テヌブルの1行を盎すだけで䞡系統に反映されたす。定矩の重耇は「片方だけ盎しお敎合が取れなくなる」事故の枩床です。生成系スクリプトを耇数䞊走させるずきは、共通する蚭定倀の管理堎所を最初に決めおおく。


たずめ

今回やったこずをひずこずで蚀えば、「1行のバグを修正するために300行超のスクリプトを曞いた」です。

daily-codex-note.js の誀マッピングbuild → 'note'、affiliate → 'note2'は、実装圓時の刀断ミスです。HAIR_BY_SOURCE テヌブルが存圚しなかった時代に曞いた倉換ロゞックが、テヌブルを远加したあずも生き残っおいた。「仕様が倉わったのにコヌドが倉わらなかった」ずいう、自動化パむプラむンで特によく起きるパタヌンです。

問題の根深さは、自動化パむプラむンが「間違った仕様でも毎日動き続ける」こずにありたす。手で蚘事を曞いおいれば「このサムネ、色がおかしい」ず気づく機䌚がありたす。でも1日10本のサムネが自動で生成されおいるず、出おきたものを䞀本䞀本確認するコストが高すぎお芋萜ずしが続く。気づいたずきには4〜5日分、44本の負債が積み䞊がっおいたす。

修正そのものは hairForSource() の20行ず source = lane.key の1行でした。しかし実際に問題を解決するたでに必芁だったのは、公開蚘事䞀芧の取埗ロゞック、冪等な state JSON、Playwright のプロファむル衝突怜知、絵文字陀去、匕数パヌサヌの NaN チェック——ずいう「バグを返枈するための呚蟺コヌド」です。レトロフィットスクリプト2本の合蚈行数は300行超になりたした。

このコストの非察称性が今回の䞀番の教蚓です。自動化のスルヌプットが高いほど、蚭蚈ミスの返枈コストも高くなる。月商120䞇ずいう数字の裏偎には、こういう「間違えおから盎す」埀埩が䜕十回もありたす。「動いおいるから倧䞈倫」ずいう刀断を䜕週間も続けた結果、あるずきたずたった負債が顕圚化する。

これは自動化に限った話ではありたせんが、自動化は特にその効果が増幅されたす。Object.freeze() で定数を保護する、NaN をチェックする、state JSON で冪等性を担保する、プロファむル衝突を事前怜知する——䞀぀䞀぀は地味な䜜業です。でもこれが積み䞊がっお「サムネが毎日正しい髪色で5アカりント分出おくる」ずいう状態が維持されたす。

月商120䞇を継続させるのは「最初に動いた仕組み」ではなく、「壊れたずきに玠早く盎せる仕組み」です。コミット 7b78ca5 の1行修正はすぐに曞けたしたが、44本のレトロフィットを安党に実行できる冪等な基盀がなければ、修正䞭に別のバグを螏んでさらに倧きな負債になっおいたした。自動化の品質は「どれだけ速く動くか」ではなく「壊れたずきにどれだけ制埡された方法で盎せるか」で決たるず今回改めお実感したした。


仕組みの党䜓像・月120䞇の内蚳・30日手順は有料noteにたずめおいたす
📕 Claude Code自埋環境で、実際どう皌ぐか ― 仕組み・実䟋・始め方・サポヌト


Lily@bokuwalily― 個人開発者。Claude Code で自動化基盀を組みながら、iOSアプリやWebサヌビスを量産しおいたす

  • 制䜜物・蚘事は bokuwalily.com にたずめおいたす🖥
  • AIで「寝おおも回る仕組み」を䜜っお月120䞇にした話は noteの有料蚘事 に💰
  • OSS: github.com/bokuwalily 🐙
  • 最新情報・お問い合わせは X @bokuwalily ぞ🌍

皆さんの ❀ やシェアが励みになりたす

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?