ToolSignal Pro 운영 중에 작성한 자동화 스크립트 일부를 정리해 공유합니다. API 키·접근 토큰·개인 경로·계정 식별자 등 민감 정보는 모두 제거했습니다. 자유롭게 참고하시되, 그대로 옮겨 쓰실 때는 환경 맞춰 경로·키를 채워 주세요.
라이선스: 자유 사용. 출처 표시 권장 (toolsignalpro.com).
1. sess136_chrome_to_hybrid.py
사장님 Gemini Web 구독으로 chart chrome 이미지 생성, 클립보드로 받아서 PIL composer 통과 후 Drive 등록까지 자동.
"""sess136 - Take pre-generated chrome PNG (from Gemini Web UI), do Drive
upload + slot_coords update + manifest upsert + compose hybrid sample +
Desktop copy + sample Drive upload. Same end-state as
sess134_batch_8kinds_final.py minus the Gemini API call.
Usage:
python _scratch/sess136_chrome_to_hybrid.py radar
"""
import json, os, shutil, sys, time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from webapp.seo.drive_assets import upload_file
from webapp.seo.asset_manifest import upsert_entry
from webapp.seo.image_composer import compose
SAMPLES = {
"radar": {
"title": "AI 도구 4축 평가 - 종합",
"subtitle": "속도 / 정확도 / 비용 / 통합성 4축으로 평가.",
"axis_1": "속도",
"axis_2": "정확도",
"axis_3": "비용 효율",
"axis_4": "통합성",
"series_label": "Claude Code 종합 점수 (vs 평균)",
"sources": "Anthropic eval suite · Cursor benchmarks · 자체 측정",
2. webapp/seo/asset_manifest.py
비주얼 자산 단일 source-of-truth. upsert + reuse-aware pick(). 동일 글에 같은 자산 반복 사용 방지.
"""sess134 — Visual asset manifest (single source of truth).
Boss directive 2026-05-22 (累計):
"글이 벌써 50개가 넘는데... 이미지 자료들 최적화 잘해서 모아두고, 썸네일,
차트, 등등으로 활용할 수 있게... 잘 준비해두고 구글 드라이브에 업로드."
Schema per asset entry — see `docs/sess134_visual_pipeline_prompts.md`
PROMPT D. Source of truth lives at `logs/visual_assets_manifest.json`
(write-through JSON cache). Drive holds the actual binary; this file holds
the index so the webapp doesn't hit Drive on every page view.
Public surface:
- `add_entry(entry)` register a new asset
- `list_entries(**filters)` filter by category/kind/theme/version/tag
- `get_entry(asset_id)` fetch one
- `mark_used(asset_id, post_id)` use_count++, last_used_post_id update
- `pick(tags, *, kind, theme, limit)` reuse-aware ranking
- `soft_delete(asset_id)` sets deleted_at, keeps record
- `sync_from_drive()` recover from Drive if manifest is lost
"""
from __future__ import annotations
import json
import logging
import threading
import time
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Optional
3. webapp/seo/image_composer.py (excerpt)
PIL + Pretendard 6-weight composer. chrome PNG 위에 slot_coords JSON 따라 텍스트·레전드 오버레이.
"""sess134 - Hybrid layer image composer (PIL + Pretendard + slot_coords).
Per PROMPT E in docs/sess134_visual_pipeline_prompts.md:
Layer 1 = Gemini chrome PNG (text-free background, motif art).
Layer 2 = Python PIL data composition (deterministic slot coordinates).
Output = WebP merged image.
The chrome lives in Drive (download on demand + small local cache).
The slot_coords JSON lives in `slot_coords/{kind}.json` in this repo and is
the single source of truth for where each editable field renders.
Public surface:
- `compose(template_id, data, *, output_path=None) -> dict`
- `load_slot_coords(kind) -> dict`
- `validate_data(data, slot_coords) -> list[str]` list of missing slots
- `fetch_chrome(drive_file_id, cache_dir) -> Path`
"""
from __future__ import annotations
import json
import logging
import shutil
import time
import urllib.request
from pathlib import Path
from typing import Any, Optional
logger = logging.getLogger(__name__)
4. webapp/seo/inline_charts_neural.py (excerpt)
16종 inline neural-light SVG 차트 템플릿. faint dot-grid + violet glow nodes. 외부 이미지 의존 0.
"""sess134 — Neural-light inline chart components.
Boss directive 2026-05-22:
"사이트 hero의 AI 신경망 파티클 느낌을 얹고, 깔끔하면서 기존 이미지 안 깨고,
그러면서도 임팩트는 있어야."
Design rules (read this before adding a new chart kind):
- NO card border, NO box-shadow, NO chrome background (no-boundaries doctrine).
- Light canvas (#ffffff / #fdfcff). White on white site doctrine.
- Subtle dot-grid backdrop layer behind every chart — VERY faint
(opacity 0.06) so it whispers, never shouts.
- Every data point is a node: small circle + soft violet glow.
- Connections are synapse-like thin lines, sometimes gradient.
- Typography: Pretendard 800 ink (#1a1a2e) titles, 13px subtitle.
- KPI accent uses sess125 particle violet (#7c5bff).
- Brand footer: one right-aligned line in #94a3b8, very light.
Palette pulled directly from sess125 SESS114-hero particle network so the
charts and the homepage hero feel like one continuous system:
- node ink: #2d2850 (deep navy purple, sess125 NODE_DOT base)
- glow: #7c5bff (violet, sess125 NODE_GLOW base + sess113 accent)
- link line: #46366e (sess125 LINK_BASE base, used at 0.18-0.32 alpha)
- cyan: #38bdf8 (secondary accent — kept from inline_charts_dark)
- mint: #34d399 (good / positive)
- rose: #f87171 (bad / negative)
Caller (compatible with inline_charts_dark dispatcher signature):
from webapp.seo.inline_charts_neural import render_inline_chart
html = render_inline_chart("treemap", data={...})
5. webapp/seo/chart_recommender.py
순수 Python intent classifier. 제목·라벨·키워드 → comparison/review/news intent 매핑 + 적합 차트 종류 추천 (LLM 호출 없음).
"""sess133 — Article-aware chart recommendation engine.
Inputs: article title, topic, keywords, label, content type, optional outline.
Output: list of 1-3 chart recommendations, each with reason + best section +
primary flag. The frontend / writer pipeline can then surface these as
"Best Fit / Optional" badges and let the user accept or override.
Intent vocabulary (matches editorial content types we publish):
- comparison : "A vs B vs C", "Best of X", "Top N", side-by-side picks
- review : single-product deep dive, verdict
- howto : step-by-step guide
- news : weekly news roundup, market move, breaking
- market : market share, growth, size, trend
- pricing : pricing tier / cost / API token cost
- technical : architecture, stack, flow diagram
- tutorial : code-along, setup walkthrough
- listicle : bullet list, "5 ways to ..."
Chart catalog (16 — see webapp/seo/inline_charts_dark.py).
Pure-Python keyword classifier; no LLM call. Cheap and predictable. The writer
UI can override the suggestion any time.
"""
from __future__ import annotations
import re
from typing import Any
6. webapp/seo/realtime_facts.py
USD-KRW 실시간 환율 3-tier fallback. open.er-api → Yahoo → 캐시. 글에 환율 인용 시 LLM 추정값 대신 실제 값.
"""sess134 — Real-time fact resolver (환율 / 현재 시점 데이터 수집 루트).
Boss directive 2026-05-22:
"환율이나 현재 시점에 정확해야 하는 정보들 수집 루트 명확하게."
The blog publishes articles whose body text quotes USD-KRW rate, weekly
financial figures, GDP/inflation snapshots, etc. The LLM (Gemini) does NOT
know today's exact rate or this week's index value — its answers are
training-cutoff plausible guesses. To honour `feedback_reality_check_in_
comparison.md` ("가격 비교 글에 반드시 실제 한도 + 장기 누적 비용 동반") and
`feedback_research_before_writing.md` ("글쓰기전에 다양하게 검색해보고
크롤링했으면 현실반영. 빈약한 글의 이유는 검색 부족"), we resolve those
facts at write-time / publish-time from authoritative APIs, with a small
cache so we are not hammering free tiers.
Sources (priority order):
- **USD-KRW exchange rate**:
1. exchangerate-api.com /v6/{key}/latest/USD (free tier, 1500/mo) —
if key configured.
2. open.er-api.com /v6/latest/USD (free, no-key, daily).
3. Yahoo Finance unofficial JSON (last resort).
4. Cached previous value with `stale=true` (graceful fallback).
- **한국은행 ECOS** (BOK statistics — base rate, CPI, etc.):
ECOS OpenAPI /api/StatisticSearch/{key}/json/kr/1/100/722Y001 —
if `ecos_api_key` configured. Returns time-series.
- **Generic "what is today's X" lookups**:
Pluggable. We expose a `resolve_fact(slug, *, fallback=None)` interface
so callers can ask `resolve_fact("usd_krw")`, `resolve_fact("kr_base_
rate")`, etc. without knowing the upstream source.
7. 매크로켬.bat (바탕화면)
Windows Task Scheduler 'ToolSignal Autonomous Loop' 활성. ASCII + CRLF + 한국어 cmd 안전.
@echo off
REM ============================================================
REM Macro START - enable Auto-Paste Loop Task Scheduler
REM
REM ASCII-pure file. Display strings English to dodge cp949/utf8.
REM ============================================================
setlocal
set TASKNAME=ToolSignal Autonomous Loop
echo.
echo ============================================================
echo MACRO START ( auto-paste 1 ON )
echo ============================================================
echo Target task: %TASKNAME%
echo ------------------------------------------------------------
schtasks /Change /TN "%TASKNAME%" /ENABLE
echo ------------------------------------------------------------
echo Status:
schtasks /Query /TN "%TASKNAME%" /FO LIST | findstr "Status"
echo ============================================================
echo.
endlocal
ping 127.0.0.1 -n 4 1>NUL
exit /b 0
8. 매크로멈춤.bat (바탕화면)
동일 task 비활성. 자율주행 잠시 멈출 때 사용.
@echo off
REM ============================================================
REM Macro STOP - disable Auto-Paste Loop Task Scheduler
REM
REM ASCII-pure file. Display strings English to dodge cp949/utf8.
REM ============================================================
setlocal
set TASKNAME=ToolSignal Autonomous Loop
echo.
echo ============================================================
echo MACRO STOP ( auto-paste 1 OFF )
echo ============================================================
echo Target task: %TASKNAME%
echo ------------------------------------------------------------
schtasks /Change /TN "%TASKNAME%" /DISABLE
echo ------------------------------------------------------------
echo Status:
schtasks /Query /TN "%TASKNAME%" /FO LIST | findstr "Status"
echo ============================================================
echo.
endlocal
ping 127.0.0.1 -n 4 1>NUL
exit /b 0
── 학습 노트 (실패 실험) ──
부록 sess136_blob_receiver.py — 실패 실험 / 학습 노트
본 코드는 작동하지 않은 우회 시도입니다. HTTPS 페이지에서 localhost로 POST 보내려고 했지만 Content-Security-Policy connect-src 정책에 차단됐습니다. 결국 클립보드 + PIL ImageGrab 경로로 우회. 같은 함정 피하실 분들 참고용.
"""sess136 - tiny HTTP server to receive blob bytes from Gemini Chrome MCP."""
import os, sys, time
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
OUTDIR = Path(__file__).resolve().parents[1] / "_scratch"
OUTDIR.mkdir(parents=True, exist_ok=True)
class H(BaseHTTPRequestHandler):
def _cors(self):
self.send_header("Access-Control-Allow-Origin", "*")
self.send_header("Access-Control-Allow-Methods", "POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "*")
def do_OPTIONS(self):
self.send_response(204)
self._cors()
self.end_headers()
def do_POST(self):
try:
name = self.headers.get("X-Filename") or self.path.lstrip("/") or "blob.bin"
name = name.replace("..", "").replace("/", "_").replace("\\", "_")
length = int(self.headers.get("Content-Length", "0"))
data = self.rfile.read(length) if length > 0 else b""
outpath = OUTDIR / name
outpath.write_bytes(data)
print(f"[recv] {name} {len(data):,} bytes -> {outpath}", flush=True)
self.send_response(200)
본 코드는 sess136 시점 정리본입니다. 이후 보강된 버전이 webapp 안에 살아 있을 수 있습니다. 의견·개선 제안은 사이트 푸터 연락처로 부탁드립니다.