"""짝 선호(가정 A-12) — 원문 하나와 두 장 그림을 주고 「어느 장이 원문을 더 잘 시각화하나(A/B/같음)」를 묻는다. 모델 gemini-3.5-flash(judge 와 같은 계열 · GEMINI_API_KEY · AI Studio). 위치 편향을 막으려 순서를 바꿔 2회 묻고, 두 답이 같은 쪽을 가리킬 때만 승패로 센다(그 밖 = 같음 또는 불일치). python3 pairpref.py [동시=16] jobs 한 줄 = {id, cmp, text, X:{label, imgs:[경로]}, Y:{label, imgs:[경로]}} — 결과 한 줄 = job + calls[2] + verdict(X|Y|same|split) 단가 = 입력 $1.5 · 출력(생각 포함) $9 / 1M (aippt src/lib/judge-eval/config.ts).""" import json, os, sys, base64, time, urllib.request, concurrent.futures, threading MODEL = 'gemini-3.5-flash'; PIN, POUT = 1.5e-6, 9.0e-6 HERE = os.path.dirname(os.path.abspath(__file__)) SYSTEM = open(f'{HERE}/pairpref-system.txt').read().strip() USER = open(f'{HERE}/pairpref-user.txt').read().strip() KEY = os.environ['GEMINI_API_KEY'] URL = f'https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent?key={KEY}' def img_part(p): return {'inline_data': {'mime_type': 'image/webp', 'data': base64.b64encode(open(p, 'rb').read()).decode()}} def ask(text, A, B): parts = [{'text': USER.replace('{ORIGINAL}', text)}] for lab, imgs in (('A', A), ('B', B)): parts.append({'text': f'\n[장 {lab}]' + (f' (그림 {len(imgs)}장 — 같은 결과의 이어지는 장)' if len(imgs) > 1 else '')}) parts += [img_part(p) for p in imgs] body = {'system_instruction': {'parts': [{'text': SYSTEM}]}, 'contents': [{'role': 'user', 'parts': parts}], 'generationConfig': {'temperature': 0, 'responseMimeType': 'application/json', 'responseSchema': {'type': 'OBJECT', 'properties': {'answer': {'type': 'STRING', 'enum': ['A', 'B', 'same']}, 'reason': {'type': 'STRING'}}, 'required': ['answer', 'reason']}}} data = json.dumps(body).encode() for t in range(5): try: r = json.load(urllib.request.urlopen(urllib.request.Request(URL, data, {'Content-Type': 'application/json'}), timeout=180)) u = r.get('usageMetadata') or {} tin = u.get('promptTokenCount', 0); tout = u.get('candidatesTokenCount', 0) + u.get('thoughtsTokenCount', 0) txt = ''.join(p.get('text', '') for p in r['candidates'][0]['content']['parts'] if not p.get('thought')) ans = json.loads(txt) return dict(answer=ans['answer'], reason=ans.get('reason'), tin=tin, tout=tout, usd=tin * PIN + tout * POUT, model=r.get('modelVersion')) except Exception as e: err = str(e)[:200]; time.sleep(5 * (t + 1)) return dict(answer=None, error=err, tin=0, tout=0, usd=0) def run(job): X, Y = job['X']['imgs'], job['Y']['imgs'] c1 = ask(job['text'], X, Y) # A = X c2 = ask(job['text'], Y, X) # A = Y m1 = {'A': 'X', 'B': 'Y', 'same': 'same'}.get(c1['answer']); m2 = {'A': 'Y', 'B': 'X', 'same': 'same'}.get(c2['answer']) v = m1 if (m1 == m2 and m1) else ('error' if not (m1 and m2) else 'split') return dict(job, calls=[c1, c2], mapped=[m1, m2], verdict=v) if __name__ == '__main__': jobs = [json.loads(l) for l in open(sys.argv[1])]; out = sys.argv[2]; conc = int(sys.argv[3]) if len(sys.argv) > 3 else 16 done = set() if os.path.exists(out): done = {json.loads(l)['id'] for l in open(out) if l.strip()} todo = [j for j in jobs if j['id'] not in done]; lock = threading.Lock(); usd = 0.0; n = 0 with open(out, 'a') as f, concurrent.futures.ThreadPoolExecutor(conc) as ex: for res in ex.map(run, todo): with lock: f.write(json.dumps(res, ensure_ascii=False) + '\n'); f.flush() usd += sum(c['usd'] for c in res['calls']); n += 1 if n % 25 == 0: print(f'[{time.strftime("%T")}] {n}/{len(todo)} ${usd:.3f}', flush=True) print(f'끝 {n} 짝 ${usd:.3f}')