Refactor dental dictionary into manage.py CLI
Reorganize .smallclaw/databases/ around a single entry point with
subcommands (add-terms / add-images / verify / reassign / stats),
absorb pmc_reassign.py into reassign, and route every image source
through inline vision-model verification before commit.
Layout
config.py paths, endpoints, API-key locations (no more
hard-coded absolute paths in source files)
manage.py argparse dispatcher
workflow_terms.py seed-JSON import (JSONC supported, dedupes on
korean/english)
workflow_images.py renamed from dental_image_workflow.py; main()
converted to run(verify=True, ...)
workflow_verify.py validate_image() / verify_db() + verify_updates()
gate used by every add-images source
seeds/ recovered seed_all.json, seed_periodontics.json
scratch/ ad-hoc work area replacing the /tmp habit
(only README.md is tracked)
docs/howto.md moved + expanded
docs/archive_image_rounds/ one-off round scripts + their results
docs/archive_validation/ model-comparison + validation history
Verification
- VERIFY_MODEL defaults to qwen3.5:397b-cloud (50-image test on
procedure-heavy sample: 9.8s/img avg, zero timeouts, Kimi-level
rigor — see archive_validation/).
- Prompt strengthened: in-image text/captions are not valid grounds
for "적합"; technique terms require visible procedure steps, not
generic device/anatomy photos.
- All 9 add-images sources gated through verify_updates() before
commit; --no-verify escape hatch for bulk runs.
.gitignore: API key files, dental_images/, __pycache__/, scratch/*
(README.md kept).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,14 @@
|
||||
.smallclaw/config.json
|
||||
.smallclaw/credentials/
|
||||
.smallclaw/vault/
|
||||
.smallclaw/*_api_key.txt
|
||||
.smallclaw/*_api_token.txt
|
||||
|
||||
# --- DENTAL DICT PROJECT (.smallclaw/databases/) ---
|
||||
.smallclaw/databases/dental_images/
|
||||
.smallclaw/databases/__pycache__/
|
||||
.smallclaw/databases/scratch/*
|
||||
!.smallclaw/databases/scratch/README.md
|
||||
|
||||
# --- RUNTIME DATA ---
|
||||
.localclaw/sessions/
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
"""Shared configuration for the dental dictionary project.
|
||||
|
||||
All paths and service endpoints live here so the workflow scripts never
|
||||
hard-code absolute paths. Everything is derived from this file's own
|
||||
location, so the project directory can be moved without edits.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# --- Directories -----------------------------------------------------------
|
||||
HERE = os.path.dirname(os.path.abspath(__file__)) # .smallclaw/databases
|
||||
SMALLCLAW = os.path.abspath(os.path.join(HERE, "..")) # .smallclaw
|
||||
HOMECLAW = os.path.abspath(os.path.join(SMALLCLAW, "..")) # repo root
|
||||
|
||||
DB_PATH = os.path.join(HERE, "dental_dict.db")
|
||||
IMG_BASE = os.path.join(HERE, "dental_images")
|
||||
SEEDS_DIR = os.path.join(HERE, "seeds")
|
||||
|
||||
# Images are served by the gateway via this URL prefix. The path resolves
|
||||
# through the symlink workspace/uploads/dental_images -> dental_images/
|
||||
# (see docs/howto.md). image_url values in the DB use this prefix.
|
||||
IMG_URL_PREFIX = "/api/files/uploads/dental_images"
|
||||
|
||||
# --- Image validation (Ollama) --------------------------------------------
|
||||
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
|
||||
# qwen3.5:397b-cloud — 50장 테스트(2026-05-15)에서 Kimi급 엄격함 + ~10s/장,
|
||||
# 타임아웃 0건. 술식 용어 부적합 판정 정확. add-images 인라인 검증도 이 모델을
|
||||
# 쓰므로 대량 실행 시 속도가 부담되면 `add-images --no-verify` 사용.
|
||||
VERIFY_MODEL = "qwen3.5:397b-cloud"
|
||||
|
||||
# --- API key files ---------------------------------------------------------
|
||||
# Kept under .smallclaw/ and git-ignored. Each file holds a single key.
|
||||
ROBOFLOW_API_KEY_FILE = os.path.join(SMALLCLAW, "roboflow_api_key.txt")
|
||||
HUGGINGFACE_API_KEY_FILE = os.path.join(SMALLCLAW, "huggingface_api_key.txt")
|
||||
KAGGLE_API_TOKEN_FILE = os.path.join(SMALLCLAW, "kaggle_api_token.txt")
|
||||
|
||||
|
||||
def read_key(path):
|
||||
"""Return the trimmed contents of an API key file, or None if missing."""
|
||||
try:
|
||||
with open(path, encoding="utf-8") as f:
|
||||
return f.read().strip()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
@@ -0,0 +1,216 @@
|
||||
"""
|
||||
Kaggle oral disease image test.
|
||||
- Picks ~7 images per class (49 total)
|
||||
- Validates each with Gemini via Ollama
|
||||
- Updates dental_dict.db with local /api/files/... paths
|
||||
"""
|
||||
|
||||
import sqlite3, json, base64, os, random, urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = "/home/kim/homeclaw/.smallclaw/databases/dental_dict.db"
|
||||
KAGGLE_BASE = "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training"
|
||||
API_FILE_PREFIX = "/api/files/uploads/dental_images/kaggle/Training"
|
||||
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
|
||||
MODEL = "gemini-3-flash-preview:cloud"
|
||||
RESULTS_FILE = "/tmp/kaggle_test_results.json"
|
||||
IMAGES_PER_CLASS = 7
|
||||
|
||||
# Class → best matching DB terms (id, korean, english)
|
||||
CLASS_TERMS = {
|
||||
"CaS": [
|
||||
(840, "재발성아프타", "Recurrent aphthous stomatitis", "구강내과"),
|
||||
(841, "아프타주요형", "Major aphthous ulcer", "구강내과"),
|
||||
(842, "아프타소아형", "Minor aphthous ulcer", "구강내과"),
|
||||
(843, "헤르페스성아프타", "Herpetiform aphthous ulcer", "구강내과"),
|
||||
(406, "구강아프타", "oral aphthae", "구강내과"),
|
||||
(538, "구강아프타성궤양", "Recurrent aphthous ulcer", "구강병리"),
|
||||
(3848, "구강점막아프토성궤양", "Aphthous ulcer of oral mucosa", "구강병리"),
|
||||
],
|
||||
"OLP": [
|
||||
(847, "구강편평태선_내과", "Oral lichen planus", "구강내과"),
|
||||
(3052, "편평태선", "lichen planus", "구강병리"),
|
||||
(3053, "구육성편평태선", "erosive lichen planus", "구강병리"),
|
||||
],
|
||||
"CoS": [
|
||||
(848, "포진성치은구내염", "Herpetic gingivostomatitis", "구강내과"),
|
||||
(3554, "구강점막포진", "Oral mucosal herpes", "구강내과"),
|
||||
(3555, "대상포진구강병변", "Oral herpes zoster", "구강내과"),
|
||||
(3825, "허퍼스바이러스구강감염", "Herpes simplex oral infection", "구강병리"),
|
||||
],
|
||||
"MC": [
|
||||
(128, "편평세포암종", "squamous cell carcinoma", "구강병리"),
|
||||
(506, "편평상피암", "Squamous cell carcinoma", "구강병리"),
|
||||
(563, "구강편평세포암", "Oral squamous cell carcinoma", "구강병리"),
|
||||
(3672, "기저세포편평세포암", "Basaloid squamous cell carcinoma", "구강병리"),
|
||||
],
|
||||
"OC": [
|
||||
(128, "편평세포암종", "squamous cell carcinoma", "구강병리"),
|
||||
(506, "편평상피암", "Squamous cell carcinoma", "구강병리"),
|
||||
(563, "구강편평세포암", "Oral squamous cell carcinoma", "구강병리"),
|
||||
(3009, "미세침습성편평상피세포암", "microinvasive squamous cell carcinoma", "구강병리"),
|
||||
],
|
||||
"OT": [
|
||||
(114, "구강 칸디다증", "oral candidiasis", "구강내과"),
|
||||
(853, "위축형칸디다증", "Atrophic candidiasis", "구강내과"),
|
||||
],
|
||||
"Gum": [
|
||||
(520, "과형성치은염", "Hyperplastic gingivitis", "구강병리"),
|
||||
],
|
||||
}
|
||||
|
||||
# Full class names for Gemini prompt
|
||||
CLASS_NAMES = {
|
||||
"CaS": "Canker Sores (아프타성 궤양)",
|
||||
"OLP": "Oral Lichen Planus (구강 편평태선)",
|
||||
"CoS": "Cold Sores / Herpes (구순포진/헤르페스)",
|
||||
"MC": "Mouth Cancer (구강암)",
|
||||
"OC": "Oral Cancer (구강암)",
|
||||
"OT": "Oral Thrush (구강 칸디다증)",
|
||||
"Gum": "Gum Disease (치은염/치주 질환)",
|
||||
}
|
||||
|
||||
|
||||
def gemini_validate(image_path: str, term_korean: str, term_english: str, category: str, class_label: str) -> dict:
|
||||
with open(image_path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode()
|
||||
|
||||
ext = Path(image_path).suffix.lower().lstrip(".")
|
||||
mime = "jpeg" if ext in ("jpg", "jpeg") else ext or "jpeg"
|
||||
|
||||
prompt = (
|
||||
f"이 이미지는 '{CLASS_NAMES[class_label]}' 라벨이 붙은 Kaggle 구강질환 데이터셋 사진입니다.\n"
|
||||
f"치과 용어: {term_korean} ({term_english}), 카테고리: {category}\n"
|
||||
f"이 이미지가 위 치과 용어를 설명하는 데 적합한가요?\n"
|
||||
f"반드시 아래 형식으로만 답하세요:\n적합: 예/아니오\n이유: (한 줄)"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": prompt,
|
||||
"images": [b64],
|
||||
}
|
||||
],
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(OLLAMA_URL, data=data, headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
resp = json.loads(r.read())
|
||||
|
||||
text = resp.get("message", {}).get("content", "")
|
||||
relevant = "적합: 예" in text
|
||||
reason = ""
|
||||
for line in text.splitlines():
|
||||
if line.startswith("이유:"):
|
||||
reason = line.replace("이유:", "").strip()
|
||||
return {"relevant": relevant, "reason": reason, "response": text}
|
||||
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Load existing results
|
||||
results = {}
|
||||
if os.path.exists(RESULTS_FILE):
|
||||
with open(RESULTS_FILE) as f:
|
||||
results = json.load(f)
|
||||
|
||||
# Track used image paths and term IDs
|
||||
used_image_paths = {v["image_path"] for v in results.values() if "image_path" in v}
|
||||
assigned_term_ids = {v["term_id"] for v in results.values() if v.get("assigned")}
|
||||
|
||||
for class_label, terms in CLASS_TERMS.items():
|
||||
class_dir = Path(KAGGLE_BASE) / class_label
|
||||
if not class_dir.exists():
|
||||
print(f"[SKIP] {class_label} — directory not found")
|
||||
continue
|
||||
|
||||
images = sorted([f for f in class_dir.iterdir() if f.suffix.lower() in (".jpg", ".jpeg", ".png")])
|
||||
random.shuffle(images)
|
||||
|
||||
term_idx = 0
|
||||
tested = 0
|
||||
|
||||
for img_path in images:
|
||||
if tested >= IMAGES_PER_CLASS:
|
||||
break
|
||||
if str(img_path) in used_image_paths:
|
||||
continue
|
||||
|
||||
term_id, term_korean, term_english, term_cat = terms[term_idx % len(terms)]
|
||||
term_idx += 1
|
||||
|
||||
result_key = f"{class_label}_{img_path.name}"
|
||||
if result_key in results:
|
||||
tested += 1
|
||||
continue
|
||||
|
||||
print(f"[{class_label}] {img_path.name} → [{term_korean}] ... ", end="", flush=True)
|
||||
try:
|
||||
v = gemini_validate(str(img_path), term_korean, term_english, term_cat, class_label)
|
||||
api_path = f"{API_FILE_PREFIX}/{class_label}/{img_path.name}"
|
||||
|
||||
results[result_key] = {
|
||||
"class": class_label,
|
||||
"image_path": str(img_path),
|
||||
"api_path": api_path,
|
||||
"term_id": term_id,
|
||||
"term_korean": term_korean,
|
||||
"term_english": term_english,
|
||||
"category": term_cat,
|
||||
**v,
|
||||
"assigned": False,
|
||||
}
|
||||
|
||||
if v["relevant"] and term_id not in assigned_term_ids:
|
||||
# Check term doesn't already have an image
|
||||
cur.execute("SELECT image_url FROM terms WHERE id=?", (term_id,))
|
||||
row = cur.fetchone()
|
||||
if row and not row[0]:
|
||||
cur.execute("UPDATE terms SET image_url=? WHERE id=?", (api_path, term_id))
|
||||
conn.commit()
|
||||
results[result_key]["assigned"] = True
|
||||
assigned_term_ids.add(term_id)
|
||||
print(f"✓ 배정")
|
||||
else:
|
||||
print(f"✓ (이미 이미지 있음)")
|
||||
else:
|
||||
status = "부적합" if not v["relevant"] else "중복 term"
|
||||
print(f"✗ {status} — {v['reason'][:50]}")
|
||||
|
||||
used_image_paths.add(str(img_path))
|
||||
tested += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
results[result_key] = {"class": class_label, "image_path": str(img_path), "error": str(e), "assigned": False}
|
||||
tested += 1
|
||||
|
||||
# Checkpoint
|
||||
with open(RESULTS_FILE, "w") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
conn.close()
|
||||
|
||||
# Summary
|
||||
total = len(results)
|
||||
relevant = sum(1 for v in results.values() if v.get("relevant"))
|
||||
assigned = sum(1 for v in results.values() if v.get("assigned"))
|
||||
errors = sum(1 for v in results.values() if "error" in v)
|
||||
|
||||
print(f"\n=== 결과 요약 ===")
|
||||
print(f"총 테스트: {total}")
|
||||
print(f"적합: {relevant} ({relevant*100//total if total else 0}%)")
|
||||
print(f"DB 배정: {assigned}")
|
||||
print(f"오류: {errors}")
|
||||
print(f"결과 저장: {RESULTS_FILE}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,222 @@
|
||||
"""
|
||||
Kaggle oral disease image test - round 2 (100 more images).
|
||||
Expanded term mapping + fixed CoS/Gum coverage.
|
||||
"""
|
||||
|
||||
import sqlite3, json, base64, os, random, urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = "/home/kim/homeclaw/.smallclaw/databases/dental_dict.db"
|
||||
KAGGLE_BASE = "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training"
|
||||
API_FILE_PREFIX = "/api/files/uploads/dental_images/kaggle/Training"
|
||||
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
|
||||
MODEL = "gemini-3-flash-preview:cloud"
|
||||
RESULTS_FILE = "/tmp/kaggle_test_results.json"
|
||||
RESULTS_FILE2 = "/tmp/kaggle_test_results2.json"
|
||||
IMAGES_PER_CLASS = 15 # 15 × 7 = 105
|
||||
|
||||
CLASS_TERMS = {
|
||||
"CaS": [
|
||||
(840, "재발성아프타", "Recurrent aphthous stomatitis", "구강내과"),
|
||||
(841, "아프타주요형", "Major aphthous ulcer", "구강내과"),
|
||||
(842, "아프타소아형", "Minor aphthous ulcer", "구강내과"),
|
||||
(843, "헤르페스성아프타", "Herpetiform aphthous ulcer", "구강내과"),
|
||||
(406, "구강아프타", "oral aphthae", "구강내과"),
|
||||
(538, "구강아프타성궤양", "Recurrent aphthous ulcer", "구강병리"),
|
||||
(3848, "구강점막아프토성궤양", "Aphthous ulcer of oral mucosa", "구강병리"),
|
||||
(3849, "재발성궤양성구내염", "Recurrent aphthous stomatitis", "구강병리"),
|
||||
(3850, "거대아프토성궤양", "Major aphthous ulcer", "구강병리"),
|
||||
(3851, "포진성아프토성궤양", "Herpetiform aphthous ulcer", "구강병리"),
|
||||
],
|
||||
"OLP": [
|
||||
(847, "구강편평태선_내과", "Oral lichen planus", "구강내과"),
|
||||
(3052, "편평태선", "lichen planus", "구강병리"),
|
||||
(3053, "구육성편평태선", "erosive lichen planus", "구강병리"),
|
||||
(879, "구강점막백반증관리", "Oral leukoplakia management", "구강내과"),
|
||||
(537, "모발상백반증", "Hairy leukoplakia", "구강병리"),
|
||||
(117, "홍반증", "erythroplakia", "구강내과"),
|
||||
(3575, "구강증식성백반", "Oral proliferative verrucous leukoplakia", "구강내과"),
|
||||
(3566, "구강헨리히증", "Oral hairy leukoplakia", "구강내과"),
|
||||
],
|
||||
"CoS": [
|
||||
(848, "포진성치은구내염", "Herpetic gingivostomatitis", "구강내과"),
|
||||
(3554, "구강점막포진", "Oral mucosal herpes", "구강내과"),
|
||||
(3555, "대상포진구강병변", "Oral herpes zoster", "구강내과"),
|
||||
(3825, "허퍼스바이러스구강감염", "Herpes simplex oral infection", "구강병리"),
|
||||
(3823, "구강포진성인두염", "Herpangina", "구강병리"),
|
||||
],
|
||||
"MC": [
|
||||
(128, "편평세포암종", "squamous cell carcinoma", "구강병리"),
|
||||
(506, "편평상피암", "Squamous cell carcinoma", "구강병리"),
|
||||
(563, "구강편평세포암", "Oral squamous cell carcinoma", "구강병리"),
|
||||
(3672, "기저세포편평세포암", "Basaloid squamous cell carcinoma", "구강병리"),
|
||||
(3009, "미세침습성편평상피세포암", "microinvasive squamous cell carcinoma", "구강병리"),
|
||||
(3881, "구강편평상피암종분화도", "Oral squamous cell carcinoma grading", "구강병리"),
|
||||
],
|
||||
"OC": [
|
||||
(128, "편평세포암종", "squamous cell carcinoma", "구강병리"),
|
||||
(506, "편평상피암", "Squamous cell carcinoma", "구강병리"),
|
||||
(563, "구강편평세포암", "Oral squamous cell carcinoma", "구강병리"),
|
||||
(3009, "미세침습성편평상피세포암", "microinvasive squamous cell carcinoma", "구강병리"),
|
||||
(3881, "구강편평상피암종분화도", "Oral squamous cell carcinoma grading", "구강병리"),
|
||||
],
|
||||
"OT": [
|
||||
(114, "구강 칸디다증", "oral candidiasis", "구강내과"),
|
||||
(853, "위축형칸디다증", "Atrophic candidiasis", "구강내과"),
|
||||
],
|
||||
"Gum": [
|
||||
(520, "과형성치은염", "Hyperplastic gingivitis", "구강병리"),
|
||||
(3947, "괴사성궤양성치은염", "necrotizing ulcerative gingivitis", "구강병리"),
|
||||
(3958, "염증성치은비대", "inflammatory gingival enlargement", "구강병리"),
|
||||
(3959, "약물유발치은비대", "drug-induced gingival enlargement", "구강병리"),
|
||||
(3965, "치은농포성육아종", "pyogenic granuloma of gingiva", "구강병리"),
|
||||
(4007, "발육성치은과증식증", "hereditary gingival fibromatosis", "구강병리"),
|
||||
(4028, "선형치은홍반", "linear gingival erythema", "구강병리"),
|
||||
(4047, "치은섬유종", "fibrous gingival hyperplasia", "구강병리"),
|
||||
(4187, "만성궤양성치은염", "Chronic ulcerative gingivitis", "구강병리"),
|
||||
(4230, "과각화성치은병변", "Hyperkeratotic gingival lesion", "구강병리"),
|
||||
],
|
||||
}
|
||||
|
||||
CLASS_NAMES = {
|
||||
"CaS": "Canker Sores (아프타성 궤양)",
|
||||
"OLP": "Oral Lichen Planus / Leukoplakia (구강 편평태선/백반증)",
|
||||
"CoS": "Cold Sores / Herpes (구순포진/헤르페스)",
|
||||
"MC": "Mouth Cancer (구강암)",
|
||||
"OC": "Oral Cancer (구강암)",
|
||||
"OT": "Oral Thrush (구강 칸디다증)",
|
||||
"Gum": "Gum Disease (치은염/치주 질환)",
|
||||
}
|
||||
|
||||
|
||||
def gemini_validate(image_path, term_korean, term_english, category, class_label):
|
||||
with open(image_path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode()
|
||||
|
||||
prompt = (
|
||||
f"이 이미지는 '{CLASS_NAMES[class_label]}' 라벨이 붙은 Kaggle 구강질환 데이터셋 사진입니다.\n"
|
||||
f"치과 용어: {term_korean} ({term_english}), 카테고리: {category}\n"
|
||||
f"이 이미지가 위 치과 용어를 설명하는 데 적합한가요?\n"
|
||||
f"반드시 아래 형식으로만 답하세요:\n적합: 예/아니오\n이유: (한 줄)"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [{"role": "user", "content": prompt, "images": [b64]}],
|
||||
"stream": False,
|
||||
}
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(OLLAMA_URL, data=data, headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
resp = json.loads(r.read())
|
||||
|
||||
text = resp.get("message", {}).get("content", "")
|
||||
relevant = "적합: 예" in text
|
||||
reason = next((l.replace("이유:", "").strip() for l in text.splitlines() if l.startswith("이유:")), "")
|
||||
return {"relevant": relevant, "reason": reason, "response": text}
|
||||
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
|
||||
# Load round 1 results to avoid re-using same images
|
||||
used_image_paths = set()
|
||||
if os.path.exists(RESULTS_FILE):
|
||||
with open(RESULTS_FILE) as f:
|
||||
r1 = json.load(f)
|
||||
used_image_paths = {v["image_path"] for v in r1.values() if "image_path" in v}
|
||||
|
||||
# Load round 2 checkpoint
|
||||
results = {}
|
||||
if os.path.exists(RESULTS_FILE2):
|
||||
with open(RESULTS_FILE2) as f:
|
||||
results = json.load(f)
|
||||
used_image_paths |= {v["image_path"] for v in results.values() if "image_path" in v}
|
||||
|
||||
assigned_term_ids = {v["term_id"] for v in results.values() if v.get("assigned")}
|
||||
# Also include round 1 assignments
|
||||
if os.path.exists(RESULTS_FILE):
|
||||
with open(RESULTS_FILE) as f:
|
||||
for v in json.load(f).values():
|
||||
if v.get("assigned"):
|
||||
assigned_term_ids.add(v["term_id"])
|
||||
|
||||
for class_label, terms in CLASS_TERMS.items():
|
||||
class_dir = Path(KAGGLE_BASE) / class_label
|
||||
if not class_dir.exists():
|
||||
continue
|
||||
|
||||
images = sorted([f for f in class_dir.iterdir() if f.suffix.lower() in (".jpg", ".jpeg", ".png")])
|
||||
random.shuffle(images)
|
||||
|
||||
term_idx = 0
|
||||
tested = 0
|
||||
|
||||
for img_path in images:
|
||||
if tested >= IMAGES_PER_CLASS:
|
||||
break
|
||||
if str(img_path) in used_image_paths:
|
||||
continue
|
||||
|
||||
term_id, term_korean, term_english, term_cat = terms[term_idx % len(terms)]
|
||||
term_idx += 1
|
||||
|
||||
result_key = f"r2_{class_label}_{img_path.name}"
|
||||
if result_key in results:
|
||||
tested += 1
|
||||
continue
|
||||
|
||||
print(f"[{class_label}] {img_path.name} → [{term_korean}] ... ", end="", flush=True)
|
||||
try:
|
||||
v = gemini_validate(str(img_path), term_korean, term_english, term_cat, class_label)
|
||||
api_path = f"{API_FILE_PREFIX}/{class_label}/{img_path.name}"
|
||||
|
||||
entry = {
|
||||
"class": class_label, "image_path": str(img_path), "api_path": api_path,
|
||||
"term_id": term_id, "term_korean": term_korean, "term_english": term_english,
|
||||
"category": term_cat, **v, "assigned": False,
|
||||
}
|
||||
|
||||
if v["relevant"] and term_id not in assigned_term_ids:
|
||||
cur.execute("SELECT image_url FROM terms WHERE id=?", (term_id,))
|
||||
row = cur.fetchone()
|
||||
if row and not row[0]:
|
||||
cur.execute("UPDATE terms SET image_url=? WHERE id=?", (api_path, term_id))
|
||||
conn.commit()
|
||||
entry["assigned"] = True
|
||||
assigned_term_ids.add(term_id)
|
||||
print(f"✓ 배정")
|
||||
else:
|
||||
print(f"✓ (이미 이미지 있음)")
|
||||
else:
|
||||
print(f"✗ {'부적합' if not v['relevant'] else '중복'} — {v['reason'][:50]}")
|
||||
|
||||
results[result_key] = entry
|
||||
used_image_paths.add(str(img_path))
|
||||
tested += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
results[result_key] = {"class": class_label, "image_path": str(img_path), "error": str(e), "assigned": False}
|
||||
tested += 1
|
||||
|
||||
with open(RESULTS_FILE2, "w") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
conn.close()
|
||||
|
||||
total = len(results)
|
||||
relevant = sum(1 for v in results.values() if v.get("relevant"))
|
||||
assigned = sum(1 for v in results.values() if v.get("assigned"))
|
||||
errors = sum(1 for v in results.values() if "error" in v)
|
||||
|
||||
print(f"\n=== Round 2 결과 ===")
|
||||
print(f"총 테스트: {total}")
|
||||
print(f"적합: {relevant} ({relevant*100//total if total else 0}%)")
|
||||
print(f"DB 배정: {assigned}")
|
||||
print(f"오류: {errors}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,306 @@
|
||||
"""
|
||||
Kaggle oral disease image test — Round 3 (~105 images).
|
||||
|
||||
Changes vs Round 2:
|
||||
- Scans Training + Testing + Validation splits (all fresh in Testing/Validation)
|
||||
- CoS: 구순포진 terms (ID 5202-5204) added → should match lip herpes images
|
||||
- OT: expanded to 8 candidiasis variants
|
||||
- MC: differentiated from OC with melanoma / verrucous / spindle cell terms
|
||||
- OC: precancerous / carcinoma in situ focused terms
|
||||
"""
|
||||
|
||||
import sqlite3, json, base64, os, random, urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = "/home/kim/homeclaw/.smallclaw/databases/dental_dict.db"
|
||||
KAGGLE_BASE = "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle"
|
||||
SPLITS = ["Training", "Testing", "Validation"]
|
||||
API_FILE_PREFIX = "/api/files/uploads/dental_images/kaggle"
|
||||
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
|
||||
MODEL = "gemini-3-flash-preview:cloud"
|
||||
R1_FILE = "/tmp/kaggle_test_results.json"
|
||||
R2_FILE = "/tmp/kaggle_test_results2.json"
|
||||
R3_FILE = "/tmp/kaggle_test_results3.json"
|
||||
|
||||
# Per-class image limits for this round
|
||||
CLASS_LIMITS = {
|
||||
"CaS": 15,
|
||||
"CoS": 15,
|
||||
"Gum": 15,
|
||||
"MC": 20,
|
||||
"OC": 15,
|
||||
"OLP": 15,
|
||||
"OT": 10,
|
||||
}
|
||||
|
||||
CLASS_TERMS = {
|
||||
"CaS": [
|
||||
(840, "재발성아프타", "Recurrent aphthous stomatitis", "구강내과"),
|
||||
(841, "아프타주요형", "Major aphthous ulcer", "구강내과"),
|
||||
(842, "아프타소아형", "Minor aphthous ulcer", "구강내과"),
|
||||
(406, "구강아프타", "oral aphthae", "구강내과"),
|
||||
(538, "구강아프타성궤양", "Recurrent aphthous ulcer", "구강병리"),
|
||||
(3848, "구강점막아프토성궤양", "Aphthous ulcer of oral mucosa", "구강병리"),
|
||||
(3849, "재발성궤양성구내염", "Recurrent aphthous stomatitis", "구강병리"),
|
||||
(3850, "거대아프토성궤양", "Major aphthous ulcer", "구강병리"),
|
||||
(3851, "포진성아프토성궤양", "Herpetiform aphthous ulcer", "구강병리"),
|
||||
(3522, "만성구강궤양", "Chronic oral ulcer", "구강내과"),
|
||||
(4325, "베체트병 구강궤양 아구형", "Behcet disease oral ulcer minor form", "구강병리"),
|
||||
(4326, "베체트병 구강궤양 대형", "Behcet disease oral ulcer major form", "구강병리"),
|
||||
(3675, "호산구성궤양", "Eosinophilic ulcer", "구강병리"),
|
||||
],
|
||||
"CoS": [
|
||||
# 구순포진 terms — new in Round 3, should match lip herpes images
|
||||
(5202, "구순포진", "Herpes labialis", "구강내과"),
|
||||
(5203, "재발성구순포진", "Recurrent herpes labialis", "구강내과"),
|
||||
(5204, "구순단순포진", "Herpes simplex labialis", "구강병리"),
|
||||
# Keep some intraoral herpes terms for variety
|
||||
(848, "포진성치은구내염", "Herpetic gingivostomatitis", "구강내과"),
|
||||
(3554, "구강점막포진", "Oral mucosal herpes", "구강내과"),
|
||||
(4316, "면역저하환자 구강헤르페스", "Oral herpes in immunocompromised patient", "구강병리"),
|
||||
],
|
||||
"Gum": [
|
||||
(520, "과형성치은염", "Hyperplastic gingivitis", "구강병리"),
|
||||
(3947, "괴사성궤양성치은염", "necrotizing ulcerative gingivitis", "구강병리"),
|
||||
(3958, "염증성치은비대", "inflammatory gingival enlargement", "구강병리"),
|
||||
(3959, "약물유발치은비대", "drug-induced gingival enlargement", "구강병리"),
|
||||
(3965, "치은농포성육아종", "pyogenic granuloma of gingiva", "구강병리"),
|
||||
(4007, "발육성치은과증식증", "hereditary gingival fibromatosis", "구강병리"),
|
||||
(4028, "선형치은홍반", "linear gingival erythema", "구강병리"),
|
||||
(4047, "치은섬유종", "fibrous gingival hyperplasia", "구강병리"),
|
||||
(4187, "만성궤양성치은염", "Chronic ulcerative gingivitis", "구강병리"),
|
||||
(4230, "과각화성치은병변", "Hyperkeratotic gingival lesion", "구강병리"),
|
||||
(4298, "임신성치은염", "Pregnancy gingivitis", "구강병리"),
|
||||
(4299, "임신성치은과형성", "Pregnancy gingival hyperplasia", "구강병리"),
|
||||
(3952, "괴사성궤양성치주염", "necrotizing ulcerative periodontitis", "구강병리"),
|
||||
(3955, "치은육아종성비대", "gingival granulomatous enlargement", "구강병리"),
|
||||
(4319, "방사선성치은괴사", "Radiation gingival necrosis", "구강병리"),
|
||||
],
|
||||
"MC": [
|
||||
# Differentiated from OC: aggressive/rare malignancies
|
||||
(3537, "구강악성흑색종", "Oral melanoma", "구강내과"),
|
||||
(4096, "구강점막악성흑색종", "Oral mucosal malignant melanoma", "구강병리"),
|
||||
(3888, "구강편평상피우상종", "Oral verrucous carcinoma", "구강병리"),
|
||||
(3008, "방추세포암", "spindle cell carcinoma", "구강병리"),
|
||||
(553, "법랑상피종", "Ameloblastic carcinoma", "구강병리"),
|
||||
(3879, "구강상피내암종", "Oral carcinoma in situ", "구강병리"),
|
||||
(4202, "림포상피암종", "Lymphoepithelial carcinoma", "구강병리"),
|
||||
(3672, "기저세포편평세포암", "Basaloid squamous cell carcinoma", "구강병리"),
|
||||
(128, "편평세포암종", "squamous cell carcinoma", "구강병리"),
|
||||
(506, "편평상피암", "Squamous cell carcinoma", "구강병리"),
|
||||
(563, "구강편평세포암", "Oral squamous cell carcinoma", "구강병리"),
|
||||
(3881, "구강편평상피암종분화도", "Oral squamous cell carcinoma grading", "구강병리"),
|
||||
(4331, "HPV양성 구강편평세포암종", "HPV-positive oral squamous cell carcinoma", "구강병리"),
|
||||
(4188, "WHO 구강암 이급 분류", "WHO binary grading of oral cancer", "구강병리"),
|
||||
],
|
||||
"OC": [
|
||||
# Focused on early/precancerous lesions
|
||||
(3571, "구강전구병변", "Oral precancerous lesion", "구강내과"),
|
||||
(3572, "구강잠재악성병변", "Oral potentially malignant disorder", "구강내과"),
|
||||
(3600, "구강악성신생물", "Oral malignant neoplasm", "구강내과"),
|
||||
(3879, "구강상피내암종", "Oral carcinoma in situ", "구강병리"),
|
||||
(128, "편평세포암종", "squamous cell carcinoma", "구강병리"),
|
||||
(506, "편평상피암", "Squamous cell carcinoma", "구강병리"),
|
||||
(563, "구강편평세포암", "Oral squamous cell carcinoma", "구강병리"),
|
||||
(3009, "미세침습성편평상피세포암", "microinvasive squamous cell carcinoma", "구강병리"),
|
||||
(4242, "구강점막가성암", "Pseudocarcinoma of oral mucosa", "구강병리"),
|
||||
(4332, "HPV음성 구강편평세포암종", "HPV-negative oral squamous cell carcinoma", "구강병리"),
|
||||
],
|
||||
"OLP": [
|
||||
(847, "구강편평태선_내과", "Oral lichen planus", "구강내과"),
|
||||
(3052, "편평태선", "lichen planus", "구강병리"),
|
||||
(3053, "구육성편평태선", "erosive lichen planus", "구강병리"),
|
||||
(879, "구강점막백반증관리", "Oral leukoplakia management", "구강내과"),
|
||||
(537, "모발상백반증", "Hairy leukoplakia", "구강병리"),
|
||||
(117, "홍반증", "erythroplakia", "구강내과"),
|
||||
(3575, "구강증식성백반", "Oral proliferative verrucous leukoplakia", "구강내과"),
|
||||
(3566, "구강헨리히증", "Oral hairy leukoplakia", "구강내과"),
|
||||
(3571, "구강전구병변", "Oral precancerous lesion", "구강내과"),
|
||||
],
|
||||
"OT": [
|
||||
(114, "구강 칸디다증", "oral candidiasis", "구강내과"),
|
||||
(851, "구강아메리카리아", "Oral candidiasis", "구강내과"),
|
||||
(852, "위막형칸디다증", "Pseudomembranous candidiasis", "구강내과"),
|
||||
(853, "위축형칸디다증", "Atrophic candidiasis", "구강내과"),
|
||||
(3854, "급성위막아감", "Acute pseudomembranous candidiasis", "구강병리"),
|
||||
(3855, "만성위축아감", "Chronic atrophic candidiasis", "구강병리"),
|
||||
(3856, "만성증식아감", "Chronic hyperplastic candidiasis", "구강병리"),
|
||||
(3602, "구강아구창성병변", "Oral candidiasis variant", "구강내과"),
|
||||
(4315, "면역저하환자 구강칸디다증", "Oral candidiasis in immunocompromised patient", "구강병리"),
|
||||
],
|
||||
}
|
||||
|
||||
CLASS_NAMES = {
|
||||
"CaS": "Canker Sores (아프타성 궤양)",
|
||||
"OLP": "Oral Lichen Planus / Leukoplakia (구강 편평태선/백반증)",
|
||||
"CoS": "Cold Sores / Herpes labialis (구순포진/입술 헤르페스)",
|
||||
"MC": "Mouth Cancer (구강암)",
|
||||
"OC": "Oral Cancer (구강암 — 초기/전구병변)",
|
||||
"OT": "Oral Thrush (구강 칸디다증)",
|
||||
"Gum": "Gum Disease (치은염/치주 질환)",
|
||||
}
|
||||
|
||||
|
||||
def gemini_validate(image_path, term_korean, term_english, category, class_label):
|
||||
with open(image_path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode()
|
||||
|
||||
prompt = (
|
||||
f"이 이미지는 '{CLASS_NAMES[class_label]}' 라벨이 붙은 Kaggle 구강질환 데이터셋 사진입니다.\n"
|
||||
f"치과 용어: {term_korean} ({term_english}), 카테고리: {category}\n"
|
||||
f"이 이미지가 위 치과 용어를 설명하는 데 적합한가요?\n"
|
||||
f"반드시 아래 형식으로만 답하세요:\n적합: 예/아니오\n이유: (한 줄)"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [{"role": "user", "content": prompt, "images": [b64]}],
|
||||
"stream": False,
|
||||
}
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(OLLAMA_URL, data=data, headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
resp = json.loads(r.read())
|
||||
|
||||
text = resp.get("message", {}).get("content", "")
|
||||
relevant = "적합: 예" in text
|
||||
reason = next((l.replace("이유:", "").strip() for l in text.splitlines() if l.startswith("이유:")), "")
|
||||
return {"relevant": relevant, "reason": reason, "response": text}
|
||||
|
||||
|
||||
def load_used(files):
|
||||
used_paths, assigned_ids = set(), set()
|
||||
for fpath in files:
|
||||
if not os.path.exists(fpath):
|
||||
continue
|
||||
data = json.load(open(fpath))
|
||||
for v in data.values():
|
||||
if "image_path" in v:
|
||||
used_paths.add(v["image_path"])
|
||||
if v.get("assigned"):
|
||||
assigned_ids.add(v["term_id"])
|
||||
return used_paths, assigned_ids
|
||||
|
||||
|
||||
def collect_images(class_label):
|
||||
"""Gather all available images across splits, shuffled."""
|
||||
images = []
|
||||
for split in SPLITS:
|
||||
class_dir = Path(KAGGLE_BASE) / split / class_label
|
||||
if class_dir.exists():
|
||||
images.extend([f for f in class_dir.iterdir()
|
||||
if f.suffix.lower() in (".jpg", ".jpeg", ".png")])
|
||||
random.shuffle(images)
|
||||
return images
|
||||
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
|
||||
used_image_paths, assigned_term_ids = load_used([R1_FILE, R2_FILE])
|
||||
|
||||
results = {}
|
||||
if os.path.exists(R3_FILE):
|
||||
results = json.load(open(R3_FILE))
|
||||
for v in results.values():
|
||||
if "image_path" in v:
|
||||
used_image_paths.add(v["image_path"])
|
||||
if v.get("assigned"):
|
||||
assigned_term_ids.add(v["term_id"])
|
||||
|
||||
for class_label, terms in CLASS_TERMS.items():
|
||||
limit = CLASS_LIMITS[class_label]
|
||||
images = collect_images(class_label)
|
||||
term_idx = tested = 0
|
||||
|
||||
for img_path in images:
|
||||
if tested >= limit:
|
||||
break
|
||||
if str(img_path) in used_image_paths:
|
||||
continue
|
||||
|
||||
term_id, term_korean, term_english, term_cat = terms[term_idx % len(terms)]
|
||||
term_idx += 1
|
||||
|
||||
# Determine split for API path
|
||||
split_part = img_path.parts[-3] # Training/Testing/Validation
|
||||
result_key = f"r3_{class_label}_{split_part}_{img_path.name}"
|
||||
if result_key in results:
|
||||
tested += 1
|
||||
continue
|
||||
|
||||
api_path = f"{API_FILE_PREFIX}/{split_part}/{class_label}/{img_path.name}"
|
||||
print(f"[{class_label}/{split_part}] {img_path.name} → [{term_korean}] ... ", end="", flush=True)
|
||||
|
||||
try:
|
||||
v = gemini_validate(str(img_path), term_korean, term_english, term_cat, class_label)
|
||||
entry = {
|
||||
"class": class_label, "split": split_part,
|
||||
"image_path": str(img_path), "api_path": api_path,
|
||||
"term_id": term_id, "term_korean": term_korean,
|
||||
"term_english": term_english, "category": term_cat,
|
||||
**v, "assigned": False,
|
||||
}
|
||||
|
||||
if v["relevant"] and term_id not in assigned_term_ids:
|
||||
cur.execute("SELECT image_url FROM terms WHERE id=?", (term_id,))
|
||||
row = cur.fetchone()
|
||||
if row and not row[0]:
|
||||
cur.execute("UPDATE terms SET image_url=? WHERE id=?", (api_path, term_id))
|
||||
conn.commit()
|
||||
entry["assigned"] = True
|
||||
assigned_term_ids.add(term_id)
|
||||
print("✓ 배정")
|
||||
else:
|
||||
print("✓ (이미 이미지 있음)")
|
||||
else:
|
||||
print(f"✗ {'부적합' if not v['relevant'] else '중복'} — {v['reason'][:50]}")
|
||||
|
||||
results[result_key] = entry
|
||||
used_image_paths.add(str(img_path))
|
||||
tested += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
results[result_key] = {
|
||||
"class": class_label, "split": split_part,
|
||||
"image_path": str(img_path), "error": str(e), "assigned": False,
|
||||
}
|
||||
tested += 1
|
||||
|
||||
with open(R3_FILE, "w") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
conn.close()
|
||||
|
||||
total = len(results)
|
||||
relevant = sum(1 for v in results.values() if v.get("relevant"))
|
||||
assigned = sum(1 for v in results.values() if v.get("assigned"))
|
||||
errors = sum(1 for v in results.values() if "error" in v)
|
||||
|
||||
print(f"\n=== Round 3 결과 ===")
|
||||
print(f"총 테스트: {total}")
|
||||
print(f"적합: {relevant} ({relevant*100//total if total else 0}%)")
|
||||
print(f"DB 배정: {assigned}")
|
||||
print(f"오류: {errors}")
|
||||
|
||||
# Per-class breakdown
|
||||
from collections import defaultdict
|
||||
cls_stats = defaultdict(lambda: {"total": 0, "relevant": 0, "assigned": 0})
|
||||
for v in results.values():
|
||||
cl = v.get("class", "?")
|
||||
cls_stats[cl]["total"] += 1
|
||||
if v.get("relevant"):
|
||||
cls_stats[cl]["relevant"] += 1
|
||||
if v.get("assigned"):
|
||||
cls_stats[cl]["assigned"] += 1
|
||||
print("\n클래스별:")
|
||||
for cl, st in sorted(cls_stats.items()):
|
||||
t, r, a = st["total"], st["relevant"], st["assigned"]
|
||||
pct = r * 100 // t if t else 0
|
||||
print(f" {cl}: {t}장, 적합 {r}({pct}%), 배정 {a}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,285 @@
|
||||
"""
|
||||
salmansajid05/oral-diseases 데이터셋 — Round 4 (~105 images)
|
||||
|
||||
클래스:
|
||||
- Calculus : 치석/치태 (예방치학, 기초치의학)
|
||||
- Gingivitis : 치은염/치주염 (구강병리, 기초치의학, 노인치의학)
|
||||
- Caries : 치아우식증 (기초치의학, 노인치의학, 예방치학)
|
||||
- hypodontia : 치아발육이상 (구강병리)
|
||||
- MouthUlcer : 구강궤양/구내염 (구강내과, 구강병리)
|
||||
- Discolor : 치아변색/착색 (심미치의학, 예방치학, 구강내과)
|
||||
"""
|
||||
|
||||
import sqlite3, json, base64, os, random, urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = "/home/kim/homeclaw/.smallclaw/databases/dental_dict.db"
|
||||
BASE = "/home/kim/homeclaw/.smallclaw/databases/dental_images/salmansajid"
|
||||
API_PREFIX = "/api/files/uploads/dental_images/salmansajid"
|
||||
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
|
||||
MODEL = "gemini-3-flash-preview:cloud"
|
||||
R4_FILE = "/tmp/kaggle_test_results4.json"
|
||||
|
||||
CLASS_DIRS = {
|
||||
"Calculus": "Calculus/Calculus",
|
||||
"Gingivitis": "Gingivitis/Gingivitis",
|
||||
"Caries": "Data caries/Data caries/caries orignal data set/done",
|
||||
"Hypodontia": "hypodontia/hypodontia",
|
||||
"MouthUlcer": "Mouth Ulcer/Mouth Ulcer/ulcer original dataset/ulcer original dataset",
|
||||
"Discolor": "Tooth Discoloration/Tooth Discoloration /tooth discoloration original dataset/tooth discoloration original dataset",
|
||||
}
|
||||
|
||||
CLASS_LIMITS = {
|
||||
"Calculus": 20,
|
||||
"Gingivitis": 20,
|
||||
"Caries": 15,
|
||||
"Hypodontia": 15,
|
||||
"MouthUlcer": 20,
|
||||
"Discolor": 15,
|
||||
}
|
||||
|
||||
CLASS_NAMES = {
|
||||
"Calculus": "Dental Calculus/Tartar (치석 — 치아에 부착된 석회화된 치태)",
|
||||
"Gingivitis": "Gingivitis/Periodontitis (치은염/치주염 — 잇몸 염증)",
|
||||
"Caries": "Dental Caries (치아우식증 — 충치)",
|
||||
"Hypodontia": "Hypodontia/Tooth Development Anomaly (선천결치/치아발육이상)",
|
||||
"MouthUlcer": "Mouth Ulcer/Oral Ulcer (구강궤양/구내염)",
|
||||
"Discolor": "Tooth Discoloration/Staining (치아변색/착색)",
|
||||
}
|
||||
|
||||
CLASS_TERMS = {
|
||||
"Calculus": [
|
||||
(940, "치석_기초", "Dental calculus", "기초치의학"),
|
||||
(939, "치태_기초", "Dental plaque", "기초치의학"),
|
||||
(811, "치은연상치석제거", "Supragingival scaling", "예방치학"),
|
||||
(810, "치은연하치석제거", "Subgingival scaling", "예방치학"),
|
||||
(786, "전문적치석제거", "Professional calculus removal", "예방치학"),
|
||||
(775, "치석지수", "Calculus index", "예방치학"),
|
||||
(776, "치태지수_예방", "Plaque score", "예방치학"),
|
||||
(806, "치면세균막관리프로그램","Plaque control program", "예방치학"),
|
||||
(371, "치태관리", "dental plaque control", "예방치학"),
|
||||
(101, "플라크 지수", "plaque index", "예방치학"),
|
||||
(814, "치태색소제거", "Plaque disclosing", "예방치학"),
|
||||
(815, "치태염색제", "Disclosing agent", "예방치학"),
|
||||
(420, "석회화", "calcification", "구강병리"),
|
||||
],
|
||||
"Gingivitis": [
|
||||
(943, "치은염_기초", "Gingivitis", "기초치의학"),
|
||||
(942, "치주염_기초", "Periodontitis", "기초치의학"),
|
||||
(2768, "노인성 치주염", "Senile Periodontitis", "노인치의학"),
|
||||
(382, "치은염예방", "gingivitis prevention", "예방치학"),
|
||||
(3954, "화농성치주염", "suppurative periodontitis", "구강병리"),
|
||||
(3952, "괴사성궤양성치주염", "necrotizing ulcerative periodontitis","구강병리"),
|
||||
(4317, "HIV연관 괴사성궤양성치주염","HIV-associated necrotizing ulcerative periodontitis","구강병리"),
|
||||
(4298, "임신성치은염", "Pregnancy gingivitis", "구강병리"),
|
||||
(358, "치근단염", "Apical Periodontitis", "기초치의학"),
|
||||
],
|
||||
"Caries": [
|
||||
(941, "치아우식_기초", "Dental caries", "기초치의학"),
|
||||
(357, "치아우식증", "Dental Caries", "기초치의학"),
|
||||
(2765, "근면 우식증", "Root Caries", "노인치의학"),
|
||||
(2766, "경부 우식증", "Cervical Caries", "노인치의학"),
|
||||
(2858, "치은 연하 우식", "Subgingival Caries", "노인치의학"),
|
||||
(4310, "방사선우식증", "Radiation caries", "구강병리"),
|
||||
(100, "우식 위험도 평가", "caries risk assessment", "예방치학"),
|
||||
(374, "치아우식증예방", "dental caries prevention", "예방치학"),
|
||||
(379, "치아우식활성도검사", "caries activity test", "예방치학"),
|
||||
(800, "치아우식활성검사", "Caries activity test", "예방치학"),
|
||||
(2857, "노인 치근면 우식 지수", "Root Caries Index", "노인치의학"),
|
||||
],
|
||||
"Hypodontia": [
|
||||
(3969, "선천성무치증", "congenital anodontia", "구강병리"),
|
||||
(4013, "과잉치", "supernumerary tooth", "구강병리"),
|
||||
(925, "법랑질결손", "Enamel hypoplasia", "기초치의학"),
|
||||
(3678, "법랑질형성부전증_저형성형","Hypoplastic amelogenesis imperfecta","구강병리"),
|
||||
(3807, "발육성낭종", "Developmental cyst", "구강병리"),
|
||||
(3890, "구강섬유성과잉치", "Fibrous dysplasia of oral bone", "구강병리"),
|
||||
(4277, "발육성측방치성낭종", "Developmental lateral periodontal cyst","구강병리"),
|
||||
(513, "비발육성낭", "Nasopalatine cyst", "구강병리"),
|
||||
],
|
||||
"MouthUlcer": [
|
||||
(405, "만성구내염", "chronic stomatitis", "구강내과"),
|
||||
(843, "헤르페스성아프타", "Herpetiform aphthous ulcer", "구강내과"),
|
||||
(848, "포진성치은구내염", "Herpetic gingivostomatitis", "구강내과"),
|
||||
(874, "방사선성구내염", "Radiation-induced stomatitis", "구강내과"),
|
||||
(875, "화학요법구내염", "Chemotherapy-induced stomatitis", "구강내과"),
|
||||
(3611, "구강알레르기성접촉구내염","Oral allergic contact stomatitis","구강내과"),
|
||||
(438, "궤양성병변", "ulcerative lesion", "구강병리"),
|
||||
(3675, "호산구성궤양", "Eosinophilic ulcer", "구강병리"),
|
||||
(3850, "거대아프토성궤양", "Major aphthous ulcer", "구강병리"),
|
||||
(3851, "포진성아프토성궤양", "Herpetiform aphthous ulcer", "구강병리"),
|
||||
(3853, "구강점막아구성구내염", "Necrotizing ulcerative stomatitis","구강병리"),
|
||||
(3962, "만성결절성구내염", "chronic nodular stomatitis", "구강병리"),
|
||||
(4008, "반상 농피증", "pyostomatitis vegetans", "구강병리"),
|
||||
(4090, "알레르기접촉성구내염", "Allergic contact stomatitis", "구강병리"),
|
||||
(4215, "메토트렉세이트 구내염", "Methotrexate stomatitis", "구강병리"),
|
||||
(396, "방사선구내염", "radiation stomatitis", "구강내과"),
|
||||
],
|
||||
"Discolor": [
|
||||
(2375, "치아 변색", "Tooth Discoloration", "심미치의학"),
|
||||
(2376, "외인성 변색", "Extrinsic Discoloration", "심미치의학"),
|
||||
(2377, "내인성 변색", "Intrinsic Discoloration", "심미치의학"),
|
||||
(2378, "테트라사이클린 착색", "Tetracycline Staining", "심미치의학"),
|
||||
(2445, "치아 착색", "Dental Staining", "심미치의학"),
|
||||
(784, "불소중독", "Fluorosis", "예방치학"),
|
||||
(873, "약물유발구강착색", "Drug-induced oral pigmentation", "구강내과"),
|
||||
(3535, "구강은汞착색", "Oral amalgam tattoo", "구강내과"),
|
||||
(1955, "변색", "Tarnish", "치과생체재료학"),
|
||||
(2340, "착색 레진", "Tinting", "심미치의학"),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def gemini_validate(image_path, term_korean, term_english, category, class_label):
|
||||
with open(image_path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode()
|
||||
|
||||
prompt = (
|
||||
f"이 이미지는 '{CLASS_NAMES[class_label]}' 카테고리의 구강 질환 사진입니다.\n"
|
||||
f"치과 용어: {term_korean} ({term_english}), 카테고리: {category}\n"
|
||||
f"이 이미지가 위 치과 용어를 설명하는 데 적합한가요?\n"
|
||||
f"반드시 아래 형식으로만 답하세요:\n적합: 예/아니오\n이유: (한 줄)"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [{"role": "user", "content": prompt, "images": [b64]}],
|
||||
"stream": False,
|
||||
}
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(OLLAMA_URL, data=data, headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
resp = json.loads(r.read())
|
||||
|
||||
text = resp.get("message", {}).get("content", "")
|
||||
relevant = "적합: 예" in text
|
||||
reason = next((l.replace("이유:", "").strip() for l in text.splitlines() if l.startswith("이유:")), "")
|
||||
return {"relevant": relevant, "reason": reason, "response": text}
|
||||
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
|
||||
results = {}
|
||||
used_paths = set()
|
||||
assigned_ids = set()
|
||||
|
||||
if os.path.exists(R4_FILE):
|
||||
results = json.load(open(R4_FILE))
|
||||
for v in results.values():
|
||||
if "image_path" in v:
|
||||
used_paths.add(v["image_path"])
|
||||
if v.get("assigned"):
|
||||
assigned_ids.add(v["term_id"])
|
||||
|
||||
# Also collect already-assigned term IDs from all previous rounds
|
||||
for prev_file in [
|
||||
"/tmp/kaggle_test_results.json",
|
||||
"/tmp/kaggle_test_results2.json",
|
||||
"/tmp/kaggle_test_results3.json",
|
||||
]:
|
||||
if os.path.exists(prev_file):
|
||||
for v in json.load(open(prev_file)).values():
|
||||
if v.get("assigned"):
|
||||
assigned_ids.add(v["term_id"])
|
||||
|
||||
for class_label, rel_dir in CLASS_DIRS.items():
|
||||
class_dir = Path(BASE) / rel_dir
|
||||
if not class_dir.exists():
|
||||
print(f"[SKIP] {class_label} — {class_dir} not found")
|
||||
continue
|
||||
|
||||
images = [f for f in class_dir.iterdir() if f.suffix.lower() in (".jpg", ".jpeg", ".png")]
|
||||
random.shuffle(images)
|
||||
|
||||
terms = CLASS_TERMS[class_label]
|
||||
limit = CLASS_LIMITS[class_label]
|
||||
term_idx = tested = 0
|
||||
|
||||
for img_path in images:
|
||||
if tested >= limit:
|
||||
break
|
||||
if str(img_path) in used_paths:
|
||||
continue
|
||||
|
||||
term_id, term_korean, term_english, term_cat = terms[term_idx % len(terms)]
|
||||
term_idx += 1
|
||||
|
||||
result_key = f"r4_{class_label}_{img_path.name}"
|
||||
if result_key in results:
|
||||
tested += 1
|
||||
continue
|
||||
|
||||
# Build API path preserving subdirectory structure
|
||||
api_path = f"{API_PREFIX}/{rel_dir}/{img_path.name}"
|
||||
print(f"[{class_label}] {img_path.name[:30]} → [{term_korean}] ... ", end="", flush=True)
|
||||
|
||||
try:
|
||||
v = gemini_validate(str(img_path), term_korean, term_english, term_cat, class_label)
|
||||
entry = {
|
||||
"class": class_label, "image_path": str(img_path), "api_path": api_path,
|
||||
"term_id": term_id, "term_korean": term_korean, "term_english": term_english,
|
||||
"category": term_cat, **v, "assigned": False,
|
||||
}
|
||||
|
||||
if v["relevant"] and term_id not in assigned_ids:
|
||||
cur.execute("SELECT image_url FROM terms WHERE id=?", (term_id,))
|
||||
row = cur.fetchone()
|
||||
if row and not row[0]:
|
||||
cur.execute("UPDATE terms SET image_url=? WHERE id=?", (api_path, term_id))
|
||||
conn.commit()
|
||||
entry["assigned"] = True
|
||||
assigned_ids.add(term_id)
|
||||
print("✓ 배정")
|
||||
else:
|
||||
print("✓ (이미 이미지 있음)")
|
||||
else:
|
||||
print(f"✗ {'부적합' if not v['relevant'] else '중복'} — {v['reason'][:50]}")
|
||||
|
||||
results[result_key] = entry
|
||||
used_paths.add(str(img_path))
|
||||
tested += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
results[result_key] = {
|
||||
"class": class_label, "image_path": str(img_path),
|
||||
"error": str(e), "assigned": False,
|
||||
}
|
||||
tested += 1
|
||||
|
||||
with open(R4_FILE, "w") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
conn.close()
|
||||
|
||||
total = len(results)
|
||||
relevant = sum(1 for v in results.values() if v.get("relevant"))
|
||||
assigned = sum(1 for v in results.values() if v.get("assigned"))
|
||||
errors = sum(1 for v in results.values() if "error" in v)
|
||||
|
||||
print(f"\n=== Round 4 결과 ===")
|
||||
print(f"총 테스트: {total}")
|
||||
print(f"적합: {relevant} ({relevant*100//total if total else 0}%)")
|
||||
print(f"DB 배정: {assigned}")
|
||||
print(f"오류: {errors}")
|
||||
|
||||
from collections import defaultdict
|
||||
cls_stats = defaultdict(lambda: {"total": 0, "relevant": 0, "assigned": 0})
|
||||
for v in results.values():
|
||||
cl = v.get("class", "?")
|
||||
cls_stats[cl]["total"] += 1
|
||||
if v.get("relevant"):
|
||||
cls_stats[cl]["relevant"] += 1
|
||||
if v.get("assigned"):
|
||||
cls_stats[cl]["assigned"] += 1
|
||||
print("\n클래스별:")
|
||||
for cl, st in sorted(cls_stats.items()):
|
||||
t, r, a = st["total"], st["relevant"], st["assigned"]
|
||||
pct = r * 100 // t if t else 0
|
||||
print(f" {cl}: {t}장, 적합 {r}({pct}%), 배정 {a}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,249 @@
|
||||
"""
|
||||
교정 카테고리 이미지 업그레이드 — Round 5 (~100 images)
|
||||
|
||||
문제: 345개 교정 term이 엉뚱한 파노라마 X선(ismaelportog)으로 채워져 있음
|
||||
해결: Gemini 검증 후 더 적합한 이미지로 교체
|
||||
|
||||
데이터셋:
|
||||
1. cepha400: 측면 두개계측 X선 400장 → 분석/계측/수술 관련 term
|
||||
2. Profile (Concave/Convex/Plane): 측면 안모 사진 → 골격 분류/교합급 관련 term
|
||||
|
||||
교체 조건: image_url이 'ismaelportog/Panoramic' 패턴인 term만 대상
|
||||
"""
|
||||
|
||||
import sqlite3, json, base64, os, random, urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
DB_PATH = "/home/kim/homeclaw/.smallclaw/databases/dental_dict.db"
|
||||
BASE = "/home/kim/homeclaw/.smallclaw/databases/dental_images"
|
||||
API_PREFIX = "/api/files/uploads/dental_images"
|
||||
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
|
||||
MODEL = "gemini-3-flash-preview:cloud"
|
||||
R5_FILE = "/tmp/kaggle_test_results5.json"
|
||||
|
||||
GENERIC_PATTERN = "ismaelportog/Panoramic"
|
||||
|
||||
CLASS_DIRS = {
|
||||
"Cepha": "cephalometric_landmarks/cepha400/cepha400",
|
||||
"Concave": "cephalometric_profile/Cephalometric Profile Dataset/Concave",
|
||||
"Convex": "cephalometric_profile/Cephalometric Profile Dataset/Convex",
|
||||
"Plane": "cephalometric_profile/Cephalometric Profile Dataset/Plane",
|
||||
}
|
||||
|
||||
CLASS_LIMITS = {
|
||||
"Cepha": 40,
|
||||
"Concave": 25,
|
||||
"Convex": 20,
|
||||
"Plane": 15,
|
||||
}
|
||||
|
||||
CLASS_NAMES = {
|
||||
"Cepha": "측면 두개계측 방사선사진 (Lateral Cephalometric Radiograph) — 교정 진단용 X선",
|
||||
"Concave": "오목한 측면 안모 (Concave Facial Profile) — 하악전돌/III급 골격",
|
||||
"Convex": "볼록한 측면 안모 (Convex Facial Profile) — 상악전돌/II급 골격",
|
||||
"Plane": "평탄한 측면 안모 (Straight Facial Profile) — I급 정상 골격",
|
||||
}
|
||||
|
||||
CLASS_TERMS = {
|
||||
"Cepha": [
|
||||
(1545, "두부 계측 분석", "Cephalometric Analysis", "교정"),
|
||||
(1546, "두부 계측 추적", "Cephalometric Tracing", "교정"),
|
||||
(1547, "스테이너 분석", "Steiner Analysis", "교정"),
|
||||
(1548, "트위드 분석", "Tweed Analysis", "교정"),
|
||||
(963, "악교정수술", "Orthognathic Surgery", "교정"),
|
||||
(966, "악간고정", "Intermaxillary Fixation", "교정"),
|
||||
(1596, "악골 성장", "Jaw Growth", "교정"),
|
||||
(1597, "성장 발육 평가", "Growth Assessment", "교정"),
|
||||
(1598, "성장 모니터링", "Growth Monitoring", "교정"),
|
||||
(1013, "성장개량", "Growth Modification", "교정"),
|
||||
(4486, "골격 표식점", "skeletal landmark", "교정"),
|
||||
(1552, "벌트 분석", "Pont Analysis", "교정"),
|
||||
(1026, "볼튼분석", "Bolton Analysis", "교정"),
|
||||
(1027, "치궁길이분석", "Arch Length Analysis", "교정"),
|
||||
(1025, "모형분석", "Cast Analysis", "교정"),
|
||||
(1551, "모형 분석", "Model Analysis", "교정"),
|
||||
(971, "딥바이트", "Deep Bite", "교정"),
|
||||
(972, "오픈바이트", "Open Bite", "교정"),
|
||||
(4517, "수직골성 개교합", "vertical skeletal open bite", "교정"),
|
||||
(4518, "수평골격성 부정교합","horizontal skeletal malocclusion","교정"),
|
||||
(4381, "골격성 개교합", "skeletal open bite", "교정"),
|
||||
],
|
||||
"Concave": [
|
||||
(1537, "하악 전돌증", "Mandibular Protrusion", "교정"),
|
||||
(1012, "앵글류3급", "Angle Class III", "교정"),
|
||||
(4338, "앵글 클래스 III 관계","Angle Class III relationship","교정"),
|
||||
(4385, "제3급 교합", "Class III occlusion", "교정"),
|
||||
(987, "클래스쓰리견인", "Class III Elastics", "교정"),
|
||||
(4367, "골격성 반대교합", "skeletal crossbite", "교정"),
|
||||
(973, "크로스바이트", "Crossbite", "교정"),
|
||||
],
|
||||
"Convex": [
|
||||
(1536, "상악 전돌증", "Maxillary Protrusion", "교정"),
|
||||
(42, "상악 전돌", "maxillary protrusion", "교정"),
|
||||
(1011, "앵글류2급", "Angle Class II", "교정"),
|
||||
(4337, "앵글 클래스 II 관계","Angle Class II relationship", "교정"),
|
||||
(4384, "제2급 교합", "Class II occlusion", "교정"),
|
||||
(986, "클래스투견인", "Class II Elastics", "교정"),
|
||||
(4374, "치성 전돌", "dental protrusion", "교정"),
|
||||
(4575, "클래스 II 견인", "Class II elastics", "교정"),
|
||||
],
|
||||
"Plane": [
|
||||
(1010, "앵글류1급", "Angle Class I", "교정"),
|
||||
(4336, "앵글 클래스 I 관계","Angle Class I relationship", "교정"),
|
||||
(4383, "제1급 교합", "Class I occlusion", "교정"),
|
||||
(967, "앵글 분류", "Angle Classification", "교정"),
|
||||
(4454, "스켈레탈 앵커크리지","skeletal anchorage", "교정"),
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def gemini_validate(image_path, term_korean, term_english, category, class_label):
|
||||
with open(image_path, "rb") as f:
|
||||
b64 = base64.b64encode(f.read()).decode()
|
||||
|
||||
prompt = (
|
||||
f"이 이미지는 '{CLASS_NAMES[class_label]}' 종류의 사진입니다.\n"
|
||||
f"치과 교정 용어: {term_korean} ({term_english})\n"
|
||||
f"이 이미지가 위 교정 용어를 설명하거나 예시하는 데 적합한가요?\n"
|
||||
f"반드시 아래 형식으로만 답하세요:\n적합: 예/아니오\n이유: (한 줄)"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [{"role": "user", "content": prompt, "images": [b64]}],
|
||||
"stream": False,
|
||||
}
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(OLLAMA_URL, data=data, headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
resp = json.loads(r.read())
|
||||
|
||||
text = resp.get("message", {}).get("content", "")
|
||||
relevant = "적합: 예" in text
|
||||
reason = next((l.replace("이유:", "").strip() for l in text.splitlines() if l.startswith("이유:")), "")
|
||||
return {"relevant": relevant, "reason": reason, "response": text}
|
||||
|
||||
|
||||
def is_generic_placeholder(url):
|
||||
return url and GENERIC_PATTERN in url
|
||||
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
|
||||
results = {}
|
||||
used_paths = set()
|
||||
upgraded_ids = set()
|
||||
|
||||
if os.path.exists(R5_FILE):
|
||||
results = json.load(open(R5_FILE))
|
||||
for v in results.values():
|
||||
if "image_path" in v:
|
||||
used_paths.add(v["image_path"])
|
||||
if v.get("upgraded"):
|
||||
upgraded_ids.add(v["term_id"])
|
||||
|
||||
for class_label, rel_dir in CLASS_DIRS.items():
|
||||
class_dir = Path(BASE) / rel_dir
|
||||
if not class_dir.exists():
|
||||
print(f"[SKIP] {class_label} — not found")
|
||||
continue
|
||||
|
||||
images = [f for f in class_dir.iterdir() if f.suffix.lower() in (".jpg", ".jpeg", ".png", ".bmp")]
|
||||
random.shuffle(images)
|
||||
|
||||
terms = CLASS_TERMS[class_label]
|
||||
limit = CLASS_LIMITS[class_label]
|
||||
term_idx = tested = 0
|
||||
|
||||
for img_path in images:
|
||||
if tested >= limit:
|
||||
break
|
||||
if str(img_path) in used_paths:
|
||||
continue
|
||||
|
||||
term_id, term_korean, term_english, term_cat = terms[term_idx % len(terms)]
|
||||
term_idx += 1
|
||||
|
||||
result_key = f"r5_{class_label}_{img_path.name}"
|
||||
if result_key in results:
|
||||
tested += 1
|
||||
continue
|
||||
|
||||
# Only upgrade if term has a generic placeholder
|
||||
cur.execute("SELECT image_url FROM terms WHERE id=?", (term_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
continue
|
||||
current_url = row[0]
|
||||
if not is_generic_placeholder(current_url):
|
||||
print(f"[{class_label}] {img_path.name[:25]} → [{term_korean}] SKIP (이미 좋은 이미지)")
|
||||
continue
|
||||
|
||||
api_path = f"{API_PREFIX}/{rel_dir}/{img_path.name}"
|
||||
print(f"[{class_label}] {img_path.name[:25]} → [{term_korean}] ... ", end="", flush=True)
|
||||
|
||||
try:
|
||||
v = gemini_validate(str(img_path), term_korean, term_english, term_cat, class_label)
|
||||
entry = {
|
||||
"class": class_label, "image_path": str(img_path), "api_path": api_path,
|
||||
"term_id": term_id, "term_korean": term_korean, "term_english": term_english,
|
||||
"category": term_cat, "prev_url": current_url, **v, "upgraded": False,
|
||||
}
|
||||
|
||||
if v["relevant"] and term_id not in upgraded_ids:
|
||||
cur.execute("UPDATE terms SET image_url=? WHERE id=?", (api_path, term_id))
|
||||
conn.commit()
|
||||
entry["upgraded"] = True
|
||||
upgraded_ids.add(term_id)
|
||||
print("✓ 교체")
|
||||
else:
|
||||
print(f"✗ {'부적합' if not v['relevant'] else '중복'} — {v['reason'][:50]}")
|
||||
|
||||
results[result_key] = entry
|
||||
used_paths.add(str(img_path))
|
||||
tested += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"ERROR: {e}")
|
||||
results[result_key] = {
|
||||
"class": class_label, "image_path": str(img_path),
|
||||
"error": str(e), "upgraded": False,
|
||||
}
|
||||
tested += 1
|
||||
|
||||
with open(R5_FILE, "w") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
conn.close()
|
||||
|
||||
total = len(results)
|
||||
relevant = sum(1 for v in results.values() if v.get("relevant"))
|
||||
upgraded = sum(1 for v in results.values() if v.get("upgraded"))
|
||||
errors = sum(1 for v in results.values() if "error" in v)
|
||||
|
||||
print(f"\n=== Round 5 교정 업그레이드 결과 ===")
|
||||
print(f"총 테스트: {total}")
|
||||
print(f"적합: {relevant} ({relevant*100//total if total else 0}%)")
|
||||
print(f"교체 완료: {upgraded}")
|
||||
print(f"오류: {errors}")
|
||||
|
||||
from collections import defaultdict
|
||||
cls_stats = defaultdict(lambda: {"total": 0, "relevant": 0, "upgraded": 0})
|
||||
for v in results.values():
|
||||
cl = v.get("class", "?")
|
||||
cls_stats[cl]["total"] += 1
|
||||
if v.get("relevant"):
|
||||
cls_stats[cl]["relevant"] += 1
|
||||
if v.get("upgraded"):
|
||||
cls_stats[cl]["upgraded"] += 1
|
||||
print("\n클래스별:")
|
||||
for cl, st in sorted(cls_stats.items()):
|
||||
t, r, u = st["total"], st["relevant"], st["upgraded"]
|
||||
pct = r * 100 // t if t else 0
|
||||
print(f" {cl}: {t}장, 적합 {r}({pct}%), 교체 {u}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,639 @@
|
||||
{
|
||||
"CaS__400.jpg": {
|
||||
"class": "CaS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CaS/_400.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CaS/_400.jpg",
|
||||
"term_id": 840,
|
||||
"term_korean": "재발성아프타",
|
||||
"term_english": "Recurrent aphthous stomatitis",
|
||||
"category": "구강내과",
|
||||
"relevant": true,
|
||||
"reason": "입술 점막에 흰색 위막과 주변부 홍반을 동반한 전형적인 아프타성 궤양의 특징이 잘 나타나 있습니다.",
|
||||
"response": "적합: 예\n이유: 입술 점막에 흰색 위막과 주변부 홍반을 동반한 전형적인 아프타성 궤양의 특징이 잘 나타나 있습니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"CaS__418.jpg": {
|
||||
"class": "CaS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CaS/_418.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CaS/_418.jpg",
|
||||
"term_id": 841,
|
||||
"term_korean": "아프타주요형",
|
||||
"term_english": "Major aphthous ulcer",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "사진 속 궤양은 크기가 작고 비교적 얕아 보여, 주요형(Major)보다는 전형적인 소수형(Minor) 아프타성 궤양에 가깝습니다.",
|
||||
"response": "적합: 아니오\n이유: 사진 속 궤양은 크기가 작고 비교적 얕아 보여, 주요형(Major)보다는 전형적인 소수형(Minor) 아프타성 궤양에 가깝습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"CaS__440.jpg": {
|
||||
"class": "CaS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CaS/_440.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CaS/_440.jpg",
|
||||
"term_id": 842,
|
||||
"term_korean": "아프타소아형",
|
||||
"term_english": "Minor aphthous ulcer",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "사진 속 다수의 작은 궤양이 군집을 이루는 양상은 소아형(Minor)보다는 포진형(Herpetiform) 아프타성 궤양의 전형적인 특징입니다.",
|
||||
"response": "적합: 아니오\n이유: 사진 속 다수의 작은 궤양이 군집을 이루는 양상은 소아형(Minor)보다는 포진형(Herpetiform) 아프타성 궤양의 전형적인 특징입니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"CaS__421.jpg": {
|
||||
"class": "CaS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CaS/_421.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CaS/_421.jpg",
|
||||
"term_id": 843,
|
||||
"term_korean": "헤르페스성아프타",
|
||||
"term_english": "Herpetiform aphthous ulcer",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "사진은 단일 궤양의 형태를 보이고 있으나, 헤르페스성 아프타는 다수의 미세한 궤양이 군집을 이루는 특징이 있습니다.",
|
||||
"response": "적합: 아니오\n이유: 사진은 단일 궤양의 형태를 보이고 있으나, 헤르페스성 아프타는 다수의 미세한 궤양이 군집을 이루는 특징이 있습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"CaS__456.jpg": {
|
||||
"class": "CaS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CaS/_456.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CaS/_456.jpg",
|
||||
"term_id": 406,
|
||||
"term_korean": "구강아프타",
|
||||
"term_english": "oral aphthae",
|
||||
"category": "구강내과",
|
||||
"relevant": true,
|
||||
"reason": "입술 점막에 전형적인 원형의 황백색 가성막과 주변부 홍반을 동반한 아프타성 궤양의 특징이 잘 관찰됩니다.",
|
||||
"response": "적합: 예\n이유: 입술 점막에 전형적인 원형의 황백색 가성막과 주변부 홍반을 동반한 아프타성 궤양의 특징이 잘 관찰됩니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"CaS__409.jpg": {
|
||||
"class": "CaS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CaS/_409.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CaS/_409.jpg",
|
||||
"term_id": 538,
|
||||
"term_korean": "구강아프타성궤양",
|
||||
"term_english": "Recurrent aphthous ulcer",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "구강 점막에 발생한 황백색의 중심부와 주변의 붉은 염증성 후광을 동반한 전형적인 아프타성 궤양의 양상을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 구강 점막에 발생한 황백색의 중심부와 주변의 붉은 염증성 후광을 동반한 전형적인 아프타성 궤양의 양상을 잘 보여줍니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"CaS__460.jpg": {
|
||||
"class": "CaS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CaS/_460.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CaS/_460.jpg",
|
||||
"term_id": 3848,
|
||||
"term_korean": "구강점막아프토성궤양",
|
||||
"term_english": "Aphthous ulcer of oral mucosa",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "구강 점막에 황백색 가막과 주변부 홍반을 동반한 전형적인 아프타성 궤양의 특징이 명확하게 관찰됩니다.",
|
||||
"response": "적합: 예\n이유: 구강 점막에 황백색 가막과 주변부 홍반을 동반한 전형적인 아프타성 궤양의 특징이 명확하게 관찰됩니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"OLP__453.jpg": {
|
||||
"class": "OLP",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OLP/_453.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OLP/_453.jpg",
|
||||
"term_id": 847,
|
||||
"term_korean": "구강편평태선_내과",
|
||||
"term_english": "Oral lichen planus",
|
||||
"category": "구강내과",
|
||||
"relevant": true,
|
||||
"reason": "구강 편평태선의 전형적인 특징인 그물 모양의 하얀 선(Wickham striae)이 볼 점막에 잘 나타나 있습니다.",
|
||||
"response": "적합: 예\n이유: 구강 편평태선의 전형적인 특징인 그물 모양의 하얀 선(Wickham striae)이 볼 점막에 잘 나타나 있습니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"OLP__418.jpg": {
|
||||
"class": "OLP",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OLP/_418.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OLP/_418.jpg",
|
||||
"term_id": 3052,
|
||||
"term_korean": "편평태선",
|
||||
"term_english": "lichen planus",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "구강 점막에 편평태선의 전형적인 특징인 그물 모양의 흰색 선(Wickham 선)이 명확하게 나타나 있습니다.",
|
||||
"response": "적합: 예\n이유: 구강 점막에 편평태선의 전형적인 특징인 그물 모양의 흰색 선(Wickham 선)이 명확하게 나타나 있습니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"OLP__403.jpg": {
|
||||
"class": "OLP",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OLP/_403.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OLP/_403.jpg",
|
||||
"term_id": 3053,
|
||||
"term_korean": "구육성편평태선",
|
||||
"term_english": "erosive lichen planus",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "하얀 그물 무늬(Wickham striae)와 함께 붉은 미란(erosion) 부위가 뚜렷하게 관찰되어 구육성(미란성) 편평태선의 특징을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 하얀 그물 무늬(Wickham striae)와 함께 붉은 미란(erosion) 부위가 뚜렷하게 관찰되어 구육성(미란성) 편평태선의 특징을 잘 보여줍니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"OLP__422.jpg": {
|
||||
"class": "OLP",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OLP/_422.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OLP/_422.jpg",
|
||||
"term_id": 847,
|
||||
"term_korean": "구강편평태선_내과",
|
||||
"term_english": "Oral lichen planus",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "해당 이미지는 구강 편평태선의 전형적인 특징인 백색 그물무늬보다는 지도상설(Geographic tongue)의 양상에 더 가깝습니다.",
|
||||
"response": "적합: 아니오\n이유: 해당 이미지는 구강 편평태선의 전형적인 특징인 백색 그물무늬보다는 지도상설(Geographic tongue)의 양상에 더 가깝습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OLP__416.jpg": {
|
||||
"class": "OLP",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OLP/_416.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OLP/_416.jpg",
|
||||
"term_id": 3052,
|
||||
"term_korean": "편평태선",
|
||||
"term_english": "lichen planus",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "구강 점막에 구강 편평태선의 전형적인 특징인 그물 모양의 하얀 선(위컴 선)과 붉은 염증 양상이 잘 나타나 있습니다.",
|
||||
"response": "적합: 예\n이유: 구강 점막에 구강 편평태선의 전형적인 특징인 그물 모양의 하얀 선(위컴 선)과 붉은 염증 양상이 잘 나타나 있습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OLP__425.jpg": {
|
||||
"class": "OLP",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OLP/_425.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OLP/_425.jpg",
|
||||
"term_id": 3053,
|
||||
"term_korean": "구육성편평태선",
|
||||
"term_english": "erosive lichen planus",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "백색 선상 병소(Wickham striae)와 함께 기저부의 붉은 미란(erosion) 부위가 뚜렷하게 관찰되어 해당 질환의 특징을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 백색 선상 병소(Wickham striae)와 함께 기저부의 붉은 미란(erosion) 부위가 뚜렷하게 관찰되어 해당 질환의 특징을 잘 보여줍니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OLP__466.jpg": {
|
||||
"class": "OLP",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OLP/_466.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OLP/_466.jpg",
|
||||
"term_id": 847,
|
||||
"term_korean": "구강편평태선_내과",
|
||||
"term_english": "Oral lichen planus",
|
||||
"category": "구강내과",
|
||||
"relevant": true,
|
||||
"reason": "입술과 구강 점막에 구강 편평태선의 전형적인 특징인 그물 모양의 흰색 선(Wickham 선조)이 뚜렷하게 관찰됩니다.",
|
||||
"response": "적합: 예\n이유: 입술과 구강 점막에 구강 편평태선의 전형적인 특징인 그물 모양의 흰색 선(Wickham 선조)이 뚜렷하게 관찰됩니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"CoS__443.jpg": {
|
||||
"class": "CoS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CoS/_443.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CoS/_443.jpg",
|
||||
"term_id": 848,
|
||||
"term_korean": "포진성치은구내염",
|
||||
"term_english": "Herpetic gingivostomatitis",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "이미지는 입술 외부에 국한된 구순포진(Herpes labialis)을 보여주며, 잇몸과 구강 점막의 광범위한 염증을 동반하는 포진성치은구내염의 특징을 나타내지 않습니다.",
|
||||
"response": "적합: 아니오\n이유: 이미지는 입술 외부에 국한된 구순포진(Herpes labialis)을 보여주며, 잇몸과 구강 점막의 광범위한 염증을 동반하는 포진성치은구내염의 특징을 나타내지 않습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"CoS__423.jpg": {
|
||||
"class": "CoS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CoS/_423.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CoS/_423.jpg",
|
||||
"term_id": 3554,
|
||||
"term_korean": "구강점막포진",
|
||||
"term_english": "Oral mucosal herpes",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "이미지 속 병소는 입술에 나타나는 구순포진(Herpes labialis)이며, 구강 내 점막에 발생하는 구강점막포진과는 발생 부위가 다릅니다.",
|
||||
"response": "적합: 아니오\n이유: 이미지 속 병소는 입술에 나타나는 구순포진(Herpes labialis)이며, 구강 내 점막에 발생하는 구강점막포진과는 발생 부위가 다릅니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"CoS__454.jpg": {
|
||||
"class": "CoS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CoS/_454.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CoS/_454.jpg",
|
||||
"term_id": 3555,
|
||||
"term_korean": "대상포진구강병변",
|
||||
"term_english": "Oral herpes zoster",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "이 이미지는 단순포진 바이러스에 의한 구순포진(Cold sore)을 보여주며, 신경절을 따라 발생하는 대상포진(Herpes Zoster)의 전형적인 병변과는 차이가 있습니다.",
|
||||
"response": "적합: 아니오\n이유: 이 이미지는 단순포진 바이러스에 의한 구순포진(Cold sore)을 보여주며, 신경절을 따라 발생하는 대상포진(Herpes Zoster)의 전형적인 병변과는 차이가 있습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"CoS__405.jpg": {
|
||||
"class": "CoS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CoS/_405.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CoS/_405.jpg",
|
||||
"term_id": 3825,
|
||||
"term_korean": "허퍼스바이러스구강감염",
|
||||
"term_english": "Herpes simplex oral infection",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "입술 주변에 발생한 수포와 가피(딱지) 등 헤르페스 바이러스 감염에 의한 구순포진의 전형적인 임상 양상을 잘 보여주고 있습니다.",
|
||||
"response": "적합: 예\n이유: 입술 주변에 발생한 수포와 가피(딱지) 등 헤르페스 바이러스 감염에 의한 구순포진의 전형적인 임상 양상을 잘 보여주고 있습니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"CoS__419.jpg": {
|
||||
"class": "CoS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CoS/_419.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CoS/_419.jpg",
|
||||
"term_id": 848,
|
||||
"term_korean": "포진성치은구내염",
|
||||
"term_english": "Herpetic gingivostomatitis",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "이미지는 입술 주위의 구순포진을 보여주나, 포진성치은구내염의 핵심 특징인 잇몸 및 구강 내부 점막의 염증과 수포는 관찰되지 않습니다.",
|
||||
"response": "적합: 아니오\n이유: 이미지는 입술 주위의 구순포진을 보여주나, 포진성치은구내염의 핵심 특징인 잇몸 및 구강 내부 점막의 염증과 수포는 관찰되지 않습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"CoS__431.jpg": {
|
||||
"class": "CoS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CoS/_431.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CoS/_431.jpg",
|
||||
"term_id": 3554,
|
||||
"term_korean": "구강점막포진",
|
||||
"term_english": "Oral mucosal herpes",
|
||||
"category": "구강내과",
|
||||
"relevant": true,
|
||||
"reason": "입술과 그 주변에 발생한 전형적인 헤르페스 바이러스 감염(구순포진) 증상을 명확하게 보여주기 때문입니다.",
|
||||
"response": "적합: 예\n이유: 입술과 그 주변에 발생한 전형적인 헤르페스 바이러스 감염(구순포진) 증상을 명확하게 보여주기 때문입니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"CoS__436.jpg": {
|
||||
"class": "CoS",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/CoS/_436.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/CoS/_436.jpg",
|
||||
"term_id": 3555,
|
||||
"term_korean": "대상포진구강병변",
|
||||
"term_english": "Oral herpes zoster",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "이미지는 단순포진(HSV-1)으로 인한 구순포진을 보여주며, 신경 분절을 따라 편측성으로 발생하는 대상포진(VZV)의 임상적 특징과는 다릅니다.",
|
||||
"response": "적합: 아니오\n이유: 이미지는 단순포진(HSV-1)으로 인한 구순포진을 보여주며, 신경 분절을 따라 편측성으로 발생하는 대상포진(VZV)의 임상적 특징과는 다릅니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"MC__414.jpg": {
|
||||
"class": "MC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/MC/_414.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/MC/_414.jpg",
|
||||
"term_id": 128,
|
||||
"term_korean": "편평세포암종",
|
||||
"term_english": "squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "입술에 발생한 궤양 및 종괴 형태의 병소가 구강 편평세포암종의 전형적인 임상 특징을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 입술에 발생한 궤양 및 종괴 형태의 병소가 구강 편평세포암종의 전형적인 임상 특징을 잘 보여줍니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"MC__465.jpg": {
|
||||
"class": "MC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/MC/_465.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/MC/_465.jpg",
|
||||
"term_id": 506,
|
||||
"term_korean": "편평상피암",
|
||||
"term_english": "Squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "구강 점막에 발생한 불규칙한 형태의 궤양 및 증식성 병소가 편평상피암의 전형적인 임상 양상을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 구강 점막에 발생한 불규칙한 형태의 궤양 및 증식성 병소가 편평상피암의 전형적인 임상 양상을 잘 보여줍니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"MC__407.jpg": {
|
||||
"class": "MC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/MC/_407.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/MC/_407.jpg",
|
||||
"term_id": 563,
|
||||
"term_korean": "구강편평세포암",
|
||||
"term_english": "Oral squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "하순(아랫입술)에 발생한 궤양과 융기된 경계 등 구강편평세포암의 전형적인 임상적 특징을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 하순(아랫입술)에 발생한 궤양과 융기된 경계 등 구강편평세포암의 전형적인 임상적 특징을 잘 보여줍니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"MC__456.jpg": {
|
||||
"class": "MC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/MC/_456.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/MC/_456.jpg",
|
||||
"term_id": 3672,
|
||||
"term_korean": "기저세포편평세포암",
|
||||
"term_english": "Basaloid squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": false,
|
||||
"reason": "사진 속 병소는 작고 매끄러운 양성 결절의 양상이며, 공격적이고 궤양성인 기저세포편평세포암의 전형적인 임상 모습과 다릅니다.",
|
||||
"response": "적합: 아니오\n이유: 사진 속 병소는 작고 매끄러운 양성 결절의 양상이며, 공격적이고 궤양성인 기저세포편평세포암의 전형적인 임상 모습과 다릅니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"MC__441.jpg": {
|
||||
"class": "MC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/MC/_441.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/MC/_441.jpg",
|
||||
"term_id": 128,
|
||||
"term_korean": "편평세포암종",
|
||||
"term_english": "squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": false,
|
||||
"reason": "이미지의 병소는 표면이 매끄럽고 경계가 명확한 결절 형태로, 전형적인 편평세포암종의 불규칙한 궤양이나 침윤성 종괴 양상과는 차이가 있습니다.",
|
||||
"response": "적합: 아니오\n이유: 이미지의 병소는 표면이 매끄럽고 경계가 명확한 결절 형태로, 전형적인 편평세포암종의 불규칙한 궤양이나 침윤성 종괴 양상과는 차이가 있습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"MC__418.jpg": {
|
||||
"class": "MC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/MC/_418.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/MC/_418.jpg",
|
||||
"term_id": 506,
|
||||
"term_korean": "편평상피암",
|
||||
"term_english": "Squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "구강 내 점막에 발생한 백색 병소와 불규칙한 표면이 편평상피암의 전형적인 임상 양상을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 구강 내 점막에 발생한 백색 병소와 불규칙한 표면이 편평상피암의 전형적인 임상 양상을 잘 보여줍니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"MC__417.jpg": {
|
||||
"class": "MC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/MC/_417.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/MC/_417.jpg",
|
||||
"term_id": 563,
|
||||
"term_korean": "구강편평세포암",
|
||||
"term_english": "Oral squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "입술 부위에 나타난 궤양 및 각화성 병변이 구강편평세포암의 전형적인 임상 특징을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 입술 부위에 나타난 궤양 및 각화성 병변이 구강편평세포암의 전형적인 임상 특징을 잘 보여줍니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OC__403.jpg": {
|
||||
"class": "OC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OC/_403.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OC/_403.jpg",
|
||||
"term_id": 128,
|
||||
"term_korean": "편평세포암종",
|
||||
"term_english": "squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "혀 측면에 발생한 불규칙한 외향성 종괴와 궤양은 구강 편평세포암종의 전형적인 임상 양상을 잘 나타냅니다.",
|
||||
"response": "적합: 예\n이유: 혀 측면에 발생한 불규칙한 외향성 종괴와 궤양은 구강 편평세포암종의 전형적인 임상 양상을 잘 나타냅니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OC__406.jpg": {
|
||||
"class": "OC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OC/_406.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OC/_406.jpg",
|
||||
"term_id": 506,
|
||||
"term_korean": "편평상피암",
|
||||
"term_english": "Squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "혀 측면에 발생한 불규칙한 형태의 궤양성 종괴는 구강 편평상피암의 전형적인 임상적 특징을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 혀 측면에 발생한 불규칙한 형태의 궤양성 종괴는 구강 편평상피암의 전형적인 임상적 특징을 잘 보여줍니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OC__429.jpg": {
|
||||
"class": "OC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OC/_429.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OC/_429.jpg",
|
||||
"term_id": 563,
|
||||
"term_korean": "구강편평세포암",
|
||||
"term_english": "Oral squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "이미지 속 병변은 구강편평세포암의 전형적인 임상 양상인 경계가 불분명하고 침윤성인 궤양과 종괴 형성을 뚜렷하게 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 이미지 속 병변은 구강편평세포암의 전형적인 임상 양상인 경계가 불분명하고 침윤성인 궤양과 종괴 형성을 뚜렷하게 보여줍니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OC__421.jpg": {
|
||||
"class": "OC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OC/_421.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OC/_421.jpg",
|
||||
"term_id": 3009,
|
||||
"term_korean": "미세침습성편평상피세포암",
|
||||
"term_english": "microinvasive squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": false,
|
||||
"reason": "사진 속 병소는 크기가 크고 궤양과 증식성 특징이 뚜렷하여 미세침습 상태보다 훨씬 더 진행된 편평상피세포암의 임상적 양상을 보입니다.",
|
||||
"response": "적합: 아니오\n이유: 사진 속 병소는 크기가 크고 궤양과 증식성 특징이 뚜렷하여 미세침습 상태보다 훨씬 더 진행된 편평상피세포암의 임상적 양상을 보입니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OC__408.jpg": {
|
||||
"class": "OC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OC/_408.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OC/_408.jpg",
|
||||
"term_id": 128,
|
||||
"term_korean": "편평세포암종",
|
||||
"term_english": "squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "상악 구개 부위의 거대한 병변과 전반적으로 불량한 구강 상태가 편평세포암종의 임상적 징후를 잘 나타내고 있습니다.",
|
||||
"response": "적합: 예\n이유: 상악 구개 부위의 거대한 병변과 전반적으로 불량한 구강 상태가 편평세포암종의 임상적 징후를 잘 나타내고 있습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OC__425.jpg": {
|
||||
"class": "OC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OC/_425.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OC/_425.jpg",
|
||||
"term_id": 506,
|
||||
"term_korean": "편평상피암",
|
||||
"term_english": "Squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "혀 측면의 경계가 불규칙한 궤양성 병변은 구강 편평상피암의 전형적인 임상적 특징을 잘 나타내고 있습니다.",
|
||||
"response": "적합: 예\n이유: 혀 측면의 경계가 불규칙한 궤양성 병변은 구강 편평상피암의 전형적인 임상적 특징을 잘 나타내고 있습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OC__422.jpg": {
|
||||
"class": "OC",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OC/_422.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OC/_422.jpg",
|
||||
"term_id": 563,
|
||||
"term_korean": "구강편평세포암",
|
||||
"term_english": "Oral squamous cell carcinoma",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "혀 측면에서 관찰되는 불규칙한 궤양 및 증식성 병변이 구강편평세포암의 전형적인 임상적 특징을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 혀 측면에서 관찰되는 불규칙한 궤양 및 증식성 병변이 구강편평세포암의 전형적인 임상적 특징을 잘 보여줍니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OT__431.jpg": {
|
||||
"class": "OT",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OT/_431.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OT/_431.jpg",
|
||||
"term_id": 114,
|
||||
"term_korean": "구강 칸디다증",
|
||||
"term_english": "oral candidiasis",
|
||||
"category": "구강내과",
|
||||
"relevant": true,
|
||||
"reason": "구강 점막에 발생하는 백색의 가막과 주변부 발적 등 구강 칸디다증의 전형적인 임상 양상을 뚜렷하게 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 구강 점막에 발생하는 백색의 가막과 주변부 발적 등 구강 칸디다증의 전형적인 임상 양상을 뚜렷하게 보여줍니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"OT__434.jpg": {
|
||||
"class": "OT",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OT/_434.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OT/_434.jpg",
|
||||
"term_id": 853,
|
||||
"term_korean": "위축형칸디다증",
|
||||
"term_english": "Atrophic candidiasis",
|
||||
"category": "구강내과",
|
||||
"relevant": true,
|
||||
"reason": "구강 점막이 붉게 변하는 홍반성 병소의 특징이 잘 나타나 있어 위축형 칸디다증을 설명하기에 적합합니다.",
|
||||
"response": "적합: 예\n이유: 구강 점막이 붉게 변하는 홍반성 병소의 특징이 잘 나타나 있어 위축형 칸디다증을 설명하기에 적합합니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"OT__408.jpg": {
|
||||
"class": "OT",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OT/_408.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OT/_408.jpg",
|
||||
"term_id": 114,
|
||||
"term_korean": "구강 칸디다증",
|
||||
"term_english": "oral candidiasis",
|
||||
"category": "구강내과",
|
||||
"relevant": true,
|
||||
"reason": "구강 점막에 나타나는 특징적인 백색 위막과 홍반성 병소가 구강 칸디다증의 임상 양상을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 구강 점막에 나타나는 특징적인 백색 위막과 홍반성 병소가 구강 칸디다증의 임상 양상을 잘 보여줍니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OT__414.jpg": {
|
||||
"class": "OT",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OT/_414.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OT/_414.jpg",
|
||||
"term_id": 853,
|
||||
"term_korean": "위축형칸디다증",
|
||||
"term_english": "Atrophic candidiasis",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "사진은 백색 위막이 특징인 위막성 칸디다증을 보여주며, 붉은 발적이 주된 증상인 위축형 칸디다증과는 차이가 있습니다.",
|
||||
"response": "적합: 아니오\n이유: 사진은 백색 위막이 특징인 위막성 칸디다증을 보여주며, 붉은 발적이 주된 증상인 위축형 칸디다증과는 차이가 있습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OT__411.jpg": {
|
||||
"class": "OT",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OT/_411.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OT/_411.jpg",
|
||||
"term_id": 114,
|
||||
"term_korean": "구강 칸디다증",
|
||||
"term_english": "oral candidiasis",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "이 이미지는 구강 칸디다증의 전형적인 백색 반점보다는 혀의 유두가 길어지고 변색된 설모증(Hairy Tongue)의 특징을 보여줍니다.",
|
||||
"response": "적합: 아니오\n이유: 이 이미지는 구강 칸디다증의 전형적인 백색 반점보다는 혀의 유두가 길어지고 변색된 설모증(Hairy Tongue)의 특징을 보여줍니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OT__433.jpg": {
|
||||
"class": "OT",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OT/_433.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OT/_433.jpg",
|
||||
"term_id": 853,
|
||||
"term_korean": "위축형칸디다증",
|
||||
"term_english": "Atrophic candidiasis",
|
||||
"category": "구강내과",
|
||||
"relevant": false,
|
||||
"reason": "위축형 칸디다증은 점막이 붉고 매끈하게 변하는 것이 특징이나, 이 사진은 백색 반점이 나타나는 위막성 칸디다증의 양상을 주로 보이고 있습니다.",
|
||||
"response": "적합: 아니오\n이유: 위축형 칸디다증은 점막이 붉고 매끈하게 변하는 것이 특징이나, 이 사진은 백색 반점이 나타나는 위막성 칸디다증의 양상을 주로 보이고 있습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"OT__410.jpg": {
|
||||
"class": "OT",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/OT/_410.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/OT/_410.jpg",
|
||||
"term_id": 114,
|
||||
"term_korean": "구강 칸디다증",
|
||||
"term_english": "oral candidiasis",
|
||||
"category": "구강내과",
|
||||
"relevant": true,
|
||||
"reason": "구강 점막에 위막성 칸디다증의 전형적인 특징인 하얀 위막과 주변의 홍반성 병소가 명확하게 관찰됩니다.",
|
||||
"response": "적합: 예\n이유: 구강 점막에 위막성 칸디다증의 전형적인 특징인 하얀 위막과 주변의 홍반성 병소가 명확하게 관찰됩니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"Gum__437.jpg": {
|
||||
"class": "Gum",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/Gum/_437.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/Gum/_437.jpg",
|
||||
"term_id": 520,
|
||||
"term_korean": "과형성치은염",
|
||||
"term_english": "Hyperplastic gingivitis",
|
||||
"category": "구강병리",
|
||||
"relevant": false,
|
||||
"reason": "이미지의 해상도가 매우 낮고 흐릿하여 잇몸 조직의 증식(과형성) 상태를 명확하게 관찰할 수 없습니다.",
|
||||
"response": "적합: 아니오\n이유: 이미지의 해상도가 매우 낮고 흐릿하여 잇몸 조직의 증식(과형성) 상태를 명확하게 관찰할 수 없습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"Gum__427.jpg": {
|
||||
"class": "Gum",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/Gum/_427.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/Gum/_427.jpg",
|
||||
"term_id": 520,
|
||||
"term_korean": "과형성치은염",
|
||||
"term_english": "Hyperplastic gingivitis",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "잇몸 조직이 눈에 띄게 붓고 비대해진 상태가 관찰되어 과형성치은염의 임상적 특징을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 잇몸 조직이 눈에 띄게 붓고 비대해진 상태가 관찰되어 과형성치은염의 임상적 특징을 잘 보여줍니다.",
|
||||
"assigned": true
|
||||
},
|
||||
"Gum__429.jpg": {
|
||||
"class": "Gum",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/Gum/_429.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/Gum/_429.jpg",
|
||||
"term_id": 520,
|
||||
"term_korean": "과형성치은염",
|
||||
"term_english": "Hyperplastic gingivitis",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "치석 침착과 만성 염증으로 인해 잇몸이 비정상적으로 부어오르고 증식된 양상이 잘 나타나 있습니다.",
|
||||
"response": "적합: 예\n이유: 치석 침착과 만성 염증으로 인해 잇몸이 비정상적으로 부어오르고 증식된 양상이 잘 나타나 있습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"Gum__446.jpg": {
|
||||
"class": "Gum",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/Gum/_446.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/Gum/_446.jpg",
|
||||
"term_id": 520,
|
||||
"term_korean": "과형성치은염",
|
||||
"term_english": "Hyperplastic gingivitis",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "잇몸이 비대해져 치아의 상당 부분을 덮고 있는 모습이 과형성치은염의 전형적인 임상 양상을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 잇몸이 비대해져 치아의 상당 부분을 덮고 있는 모습이 과형성치은염의 전형적인 임상 양상을 잘 보여줍니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"Gum__442.jpg": {
|
||||
"class": "Gum",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/Gum/_442.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/Gum/_442.jpg",
|
||||
"term_id": 520,
|
||||
"term_korean": "과형성치은염",
|
||||
"term_english": "Hyperplastic gingivitis",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "잇몸이 비정상적으로 부풀어 오르고 치아 면을 덮는 듯한 과증식 양상이 전형적으로 나타나 있습니다.",
|
||||
"response": "적합: 예\n이유: 잇몸이 비정상적으로 부풀어 오르고 치아 면을 덮는 듯한 과증식 양상이 전형적으로 나타나 있습니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"Gum__420.jpg": {
|
||||
"class": "Gum",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/Gum/_420.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/Gum/_420.jpg",
|
||||
"term_id": 520,
|
||||
"term_korean": "과형성치은염",
|
||||
"term_english": "Hyperplastic gingivitis",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "잇몸 조직이 비정상적으로 증식하여 비대해지고 치아의 상당 부분을 덮고 있는 모습이 과형성치은염의 특징을 잘 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 잇몸 조직이 비정상적으로 증식하여 비대해지고 치아의 상당 부분을 덮고 있는 모습이 과형성치은염의 특징을 잘 보여줍니다.",
|
||||
"assigned": false
|
||||
},
|
||||
"Gum__425.jpg": {
|
||||
"class": "Gum",
|
||||
"image_path": "/home/kim/homeclaw/.smallclaw/databases/dental_images/kaggle/Training/Gum/_425.jpg",
|
||||
"api_path": "/api/files/uploads/dental_images/kaggle/Training/Gum/_425.jpg",
|
||||
"term_id": 520,
|
||||
"term_korean": "과형성치은염",
|
||||
"term_english": "Hyperplastic gingivitis",
|
||||
"category": "구강병리",
|
||||
"relevant": true,
|
||||
"reason": "잇몸 조직이 비정상적으로 증식하여 덩어리 형태를 형성한 과형성 상태를 명확하게 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 잇몸 조직이 비정상적으로 증식하여 덩어리 형태를 형성한 과형성 상태를 명확하게 보여줍니다.",
|
||||
"assigned": false
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
PMC OA 이미지 추출 — 교정 장치/치료 term 50개 (placeholder 교체)
|
||||
"""
|
||||
import sqlite3, time, subprocess, re, json, urllib.request, urllib.parse, os
|
||||
|
||||
DB_PATH = "/home/kim/homeclaw/.smallclaw/databases/dental_dict.db"
|
||||
RESULTS_FILE = "/tmp/pmc_ortho_results.json"
|
||||
GENERIC_PATTERN = "ismaelportog/Panoramic"
|
||||
MAX_TERMS = 50
|
||||
DELAY = 0.4
|
||||
|
||||
# 교정 장치/치료 용어 - PMC 검색에 적합한 것들 우선 선별
|
||||
TARGET_IDS = [
|
||||
44, # 공간 유지 장치 space maintainer
|
||||
47, # 치아 이동 tooth movement
|
||||
204, # 고정원 anchorage
|
||||
205, # 설측 교정 장치 lingual orthodontic appliance
|
||||
206, # 헤드기어 headgear
|
||||
207, # 교정력 orthodontic force
|
||||
210, # 고무줄 intermaxillary elastics
|
||||
212, # 치간 절삭 interproximal reduction
|
||||
215, # 결찰선 ligature wire
|
||||
218, # 정출 extrusion orthodontic
|
||||
219, # 압하 intrusion orthodontic
|
||||
220, # 기능성 교정 장치 functional appliance
|
||||
221, # 치열궁 확장 arch expansion
|
||||
978, # 쿼드헬릭스 quad helix
|
||||
979, # 급속구개확대 rapid palatal expansion
|
||||
980, # 구개확대기 palatal expander
|
||||
984, # 코일스프링 coil spring orthodontic
|
||||
985, # 탄성사 elastic band orthodontic
|
||||
988, # 교정철선 archwire
|
||||
993, # 고무결찰 elastic ligature
|
||||
994, # 셀프라이게이팅브라켓 self-ligating bracket
|
||||
995, # 세라믹브라켓 ceramic bracket
|
||||
996, # 메탈브라켓 metal bracket
|
||||
997, # 버튼 orthodontic button
|
||||
1000, # 간격유지자 space maintainer
|
||||
1001, # 공간폐쇄 space closure
|
||||
1002, # 발치교정 extraction orthodontics
|
||||
1003, # 비발치교정 non-extraction orthodontics
|
||||
1004, # 치아혼잡 dental crowding
|
||||
1006, # 보상곡선 curve of spee
|
||||
1015, # 악태조정장치 bionator
|
||||
1017, # 프랭켈교정기 frankel appliance
|
||||
1018, # 활성보유기 active retainer
|
||||
1019, # 하와리테이너 hawley retainer
|
||||
1020, # 고정식보유기 fixed retainer
|
||||
1022, # 재발 relapse orthodontic
|
||||
1025, # 모형분석 cast analysis
|
||||
1026, # 볼튼분석 bolton analysis
|
||||
1028, # 혼잡도 crowding index
|
||||
1030, # 악궁확대 arch expansion
|
||||
1039, # 골막하개조 subperiosteal remodeling
|
||||
1042, # 교정치아이동 orthodontic tooth movement
|
||||
1043, # 최적교정력 optimal orthodontic force
|
||||
1045, # 백색반점 white spot lesion
|
||||
966, # 악간고정 intermaxillary fixation
|
||||
968, # 생역학 biomechanics orthodontic
|
||||
969, # 회전교정 rotation correction
|
||||
974, # 스커즈바이트 scissors bite
|
||||
977, # 팔랜탈바 palatal bar
|
||||
981, # 니타놀와이어 nickel-titanium wire
|
||||
]
|
||||
|
||||
|
||||
def esearch(query, retmax=3):
|
||||
url = (
|
||||
"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
|
||||
f"?db=pmc&term={urllib.parse.quote(query)}"
|
||||
"%20AND%20open+access[filter]"
|
||||
f"&retmax={retmax}&retmode=json"
|
||||
)
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "DentalDictBot/2.0"})
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
return json.loads(resp.read()).get("esearchresult", {}).get("idlist", [])
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def first_figure_url(pmcid):
|
||||
article_url = f"https://www.ncbi.nlm.nih.gov/pmc/articles/PMC{pmcid}/"
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["curl", "-s", "-L", "--max-time", "20",
|
||||
"-A", "Mozilla/5.0 (compatible; DentalDictBot/2.0)",
|
||||
article_url],
|
||||
capture_output=True, text=True, timeout=25,
|
||||
)
|
||||
html = result.stdout
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
matches = re.findall(
|
||||
r'src="(https://cdn\.ncbi\.nlm\.nih\.gov/pmc/blobs/[^"]+\.(?:jpg|jpeg|png))"',
|
||||
html,
|
||||
)
|
||||
if matches:
|
||||
return matches[0]
|
||||
|
||||
fig_blocks = re.findall(r'<figure[^>]*>(.*?)</figure>', html, re.DOTALL)
|
||||
for block in fig_blocks:
|
||||
m = re.search(r'src="(https?://[^"]+\.(?:jpg|jpeg|png))"', block)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
|
||||
results = {}
|
||||
if os.path.exists(RESULTS_FILE):
|
||||
results = json.load(open(RESULTS_FILE))
|
||||
|
||||
cur.execute(
|
||||
f"SELECT id, korean, english FROM terms "
|
||||
f"WHERE id IN ({','.join(str(i) for i in TARGET_IDS)}) "
|
||||
f"AND image_url LIKE '%{GENERIC_PATTERN}%'"
|
||||
)
|
||||
terms = cur.fetchall()
|
||||
print(f"대상 term: {len(terms)}개")
|
||||
|
||||
updated = 0
|
||||
for i, (term_id, korean, english) in enumerate(terms):
|
||||
if str(term_id) in results:
|
||||
if results[str(term_id)].get("updated"):
|
||||
updated += 1
|
||||
continue
|
||||
|
||||
query = f"{english} orthodontic dental"
|
||||
time.sleep(DELAY)
|
||||
pmcids = esearch(query, retmax=3)
|
||||
|
||||
found = False
|
||||
for pmcid in pmcids:
|
||||
time.sleep(DELAY)
|
||||
img_url = first_figure_url(pmcid)
|
||||
if img_url:
|
||||
cur.execute("UPDATE terms SET image_url=? WHERE id=?", (img_url, term_id))
|
||||
conn.commit()
|
||||
results[str(term_id)] = {
|
||||
"korean": korean, "english": english,
|
||||
"pmcid": pmcid, "image_url": img_url, "updated": True
|
||||
}
|
||||
print(f" [{i+1}/{len(terms)}] {korean} ({english}) → PMC{pmcid} ✓")
|
||||
updated += 1
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
results[str(term_id)] = {
|
||||
"korean": korean, "english": english,
|
||||
"pmcid": None, "image_url": None, "updated": False
|
||||
}
|
||||
print(f" [{i+1}/{len(terms)}] {korean} — 이미지 없음")
|
||||
|
||||
with open(RESULTS_FILE, "w") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
conn.close()
|
||||
print(f"\n=== PMC 교정 결과 ===")
|
||||
print(f"처리: {len(terms)}개, 업데이트: {updated}개")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,352 @@
|
||||
{
|
||||
"44": {
|
||||
"korean": "공간 유지 장치",
|
||||
"english": "space maintainer",
|
||||
"pmcid": "13157596",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/3a8a/13157596/b061a42a7a76/cureus-0018-00000106753-i01.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"47": {
|
||||
"korean": "치아 이동",
|
||||
"english": "tooth movement",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"204": {
|
||||
"korean": "고정원",
|
||||
"english": "anchorage",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"205": {
|
||||
"korean": "설측 교정 장치",
|
||||
"english": "lingual orthodontic appliance",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"206": {
|
||||
"korean": "헤드기어",
|
||||
"english": "headgear",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"207": {
|
||||
"korean": "교정력",
|
||||
"english": "orthodontic force",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"210": {
|
||||
"korean": "고무줄",
|
||||
"english": "intermaxillary elastics",
|
||||
"pmcid": "12689034",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2163/12689034/cb7f80fee1b8/medi-104-e46057-g001.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"212": {
|
||||
"korean": "치간 절삭",
|
||||
"english": "interproximal reduction",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"215": {
|
||||
"korean": "결찰선",
|
||||
"english": "ligature wire",
|
||||
"pmcid": "12689034",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2163/12689034/cb7f80fee1b8/medi-104-e46057-g001.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"218": {
|
||||
"korean": "정출",
|
||||
"english": "extrusion",
|
||||
"pmcid": "13154446",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b184/13154446/c2691ff77f6b/12903_2026_8186_Fig1_HTML.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"219": {
|
||||
"korean": "압하",
|
||||
"english": "intrusion",
|
||||
"pmcid": "12689034",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2163/12689034/cb7f80fee1b8/medi-104-e46057-g001.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"220": {
|
||||
"korean": "기능성 교정 장치",
|
||||
"english": "functional appliance",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"221": {
|
||||
"korean": "치열궁 확장",
|
||||
"english": "arch expansion",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"966": {
|
||||
"korean": "악간고정",
|
||||
"english": "Intermaxillary Fixation",
|
||||
"pmcid": "13153796",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/9d0c/13153796/fb09d55a5bf8/acfs-2025-0038f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"968": {
|
||||
"korean": "생역학",
|
||||
"english": "Biomechanics",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"969": {
|
||||
"korean": "회전교정",
|
||||
"english": "Rotation Correction",
|
||||
"pmcid": "12689034",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2163/12689034/cb7f80fee1b8/medi-104-e46057-g001.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"974": {
|
||||
"korean": "스커즈바이트",
|
||||
"english": "Scissors Bite",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"977": {
|
||||
"korean": "팔랜탈바",
|
||||
"english": "Palatal Bar",
|
||||
"pmcid": "13160604",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/48ae/13160604/0c0273a471cf/10-1055-s-0045-1810610-i2524080-toc.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"978": {
|
||||
"korean": "쿼드헬릭스",
|
||||
"english": "Quad Helix",
|
||||
"pmcid": "13137170",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/3df8/13137170/accad3fe4d3a/gr1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"979": {
|
||||
"korean": "급속구개확대",
|
||||
"english": "Rapid Palatal Expansion",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"980": {
|
||||
"korean": "구개확대기",
|
||||
"english": "Palatal Expander",
|
||||
"pmcid": "13160605",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/cc81/13160605/1d6750a20d05/10-1055-s-0045-1810017-i2544198-1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"981": {
|
||||
"korean": "니타놀와이어",
|
||||
"english": "Nickel-Titanium Wire",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"984": {
|
||||
"korean": "코일스프링",
|
||||
"english": "Coil Spring",
|
||||
"pmcid": "13157600",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/32f4/13157600/df44474cc835/cureus-0018-00000106758-i01.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"985": {
|
||||
"korean": "탄성사",
|
||||
"english": "Elastic Band",
|
||||
"pmcid": "13160609",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/d25e/13160609/207b3cd8442c/10-1055-s-0045-1809534-i2534158-1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"988": {
|
||||
"korean": "교정철선",
|
||||
"english": "Archwire",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"993": {
|
||||
"korean": "고무결찰",
|
||||
"english": "Elastic Ligature",
|
||||
"pmcid": "12689034",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2163/12689034/cb7f80fee1b8/medi-104-e46057-g001.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"994": {
|
||||
"korean": "셀프라이게이팅브라켓",
|
||||
"english": "Self-ligating Bracket",
|
||||
"pmcid": "12689034",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2163/12689034/cb7f80fee1b8/medi-104-e46057-g001.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"995": {
|
||||
"korean": "세라믹브라켓",
|
||||
"english": "Ceramic Bracket",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"996": {
|
||||
"korean": "메탈브라켓",
|
||||
"english": "Metal Bracket",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"997": {
|
||||
"korean": "버튼",
|
||||
"english": "Orthodontic Button",
|
||||
"pmcid": "13158537",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/bd6d/13158537/a4ce971c9c7c/IEJ-59-1145-g003.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1000": {
|
||||
"korean": "간격유지자",
|
||||
"english": "Space Maintainer",
|
||||
"pmcid": "13157596",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/3a8a/13157596/b061a42a7a76/cureus-0018-00000106753-i01.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1001": {
|
||||
"korean": "공간폐쇄",
|
||||
"english": "Space Closure",
|
||||
"pmcid": "13159182",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/aee3/13159182/74b9e8629da6/12903_2026_8238_Fig1_HTML.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1002": {
|
||||
"korean": "발치교정",
|
||||
"english": "Extraction Orthodontics",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1003": {
|
||||
"korean": "비발치교정",
|
||||
"english": "Non-extraction Orthodontics",
|
||||
"pmcid": "12689034",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2163/12689034/cb7f80fee1b8/medi-104-e46057-g001.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1004": {
|
||||
"korean": "치아혼잡",
|
||||
"english": "Dental Crowding",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1006": {
|
||||
"korean": "보상곡선",
|
||||
"english": "Curve of Spee",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1015": {
|
||||
"korean": "악태조정장치",
|
||||
"english": "Bionator",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1017": {
|
||||
"korean": "프랭켈교정기",
|
||||
"english": "Frankel Appliance",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1018": {
|
||||
"korean": "활성보유기",
|
||||
"english": "Active Retainer",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1019": {
|
||||
"korean": "하와리테이너",
|
||||
"english": "Hawley Retainer",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1020": {
|
||||
"korean": "고정식보유기",
|
||||
"english": "Fixed Retainer",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1022": {
|
||||
"korean": "재발",
|
||||
"english": "Relapse",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1025": {
|
||||
"korean": "모형분석",
|
||||
"english": "Cast Analysis",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1026": {
|
||||
"korean": "볼튼분석",
|
||||
"english": "Bolton Analysis",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1028": {
|
||||
"korean": "혼잡도",
|
||||
"english": "Crowding Index",
|
||||
"pmcid": "13160606",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/bcea/13160606/0d96e841428e/10-1055-s-0045-1807732-i2514032-1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1030": {
|
||||
"korean": "악궁확대",
|
||||
"english": "Arch Expansion",
|
||||
"pmcid": "13159996",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b137/13159996/0ba8e49c48f2/cjag020f1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1039": {
|
||||
"korean": "골막하개조",
|
||||
"english": "Subperiosteal Remodeling",
|
||||
"pmcid": "13140965",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/d3a2/13140965/50031937b83b/CRE2-12-e70324-g001.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1042": {
|
||||
"korean": "교정치아이동",
|
||||
"english": "Orthodontic Tooth Movement",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1043": {
|
||||
"korean": "최적교정력",
|
||||
"english": "Optimal Orthodontic Force",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1045": {
|
||||
"korean": "백색반점",
|
||||
"english": "White Spot Lesion",
|
||||
"pmcid": "13160624",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/809a/13160624/a950da8bbd84/10-1055-s-0045-1812493-i2564336-1.jpg",
|
||||
"updated": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
PMC OA + Gemini 검증 — 교정 term 이미지 (최대 50개)
|
||||
|
||||
흐름:
|
||||
1. 각 term 영문명으로 PMC 검색
|
||||
2. 각 PMC 기사에서 CDN 이미지 URL 최대 3개 추출
|
||||
3. Gemini로 각 이미지 검증 → 적합하면 DB 업데이트, 아니면 다음 이미지 시도
|
||||
"""
|
||||
|
||||
import sqlite3, time, subprocess, re, json, base64, urllib.request, urllib.parse, os
|
||||
|
||||
DB_PATH = "/home/kim/homeclaw/.smallclaw/databases/dental_dict.db"
|
||||
RESULTS_FILE = "/tmp/pmc_ortho_validated_results.json"
|
||||
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
|
||||
MODEL = "gemini-3-flash-preview:cloud"
|
||||
MAX_TERMS = 50
|
||||
ESEARCH_DELAY = 0.4
|
||||
FETCH_DELAY = 0.4
|
||||
|
||||
TARGET_IDS = [
|
||||
44, 205, 206, 207, 210, 212, 215, 218, 219, 220,
|
||||
221, 966, 968, 969, 974, 977, 978, 979, 980, 981,
|
||||
982, 983, 984, 985, 988, 989, 990, 991, 992, 993,
|
||||
994, 995, 996, 997, 1000, 1001, 1002, 1003, 1004, 1006,
|
||||
1007, 1008, 1009, 1014, 1015, 1017, 1018, 1019, 1020, 1022,
|
||||
1025, 1026, 1027, 1028, 1030, 1039, 1042, 1043, 1044, 1045,
|
||||
47, 204,
|
||||
]
|
||||
|
||||
|
||||
def esearch(query, retmax=3):
|
||||
url = (
|
||||
"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi"
|
||||
f"?db=pmc&term={urllib.parse.quote(query)}"
|
||||
"%20AND%20open+access[filter]"
|
||||
f"&retmax={retmax}&retmode=json"
|
||||
)
|
||||
try:
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "DentalDictBot/2.0"})
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
return json.loads(resp.read()).get("esearchresult", {}).get("idlist", [])
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def extract_figure_urls(pmcid, max_imgs=3):
|
||||
"""CDN 이미지 URL을 최대 max_imgs개 추출 (flowchart/diagram 제외)."""
|
||||
article_url = f"https://www.ncbi.nlm.nih.gov/pmc/articles/PMC{pmcid}/"
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["curl", "-s", "-L", "--max-time", "20",
|
||||
"-A", "Mozilla/5.0 (compatible; DentalDictBot/2.0)",
|
||||
article_url],
|
||||
capture_output=True, text=True, timeout=25,
|
||||
)
|
||||
html = result.stdout
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
# CDN 이미지 URL 수집
|
||||
matches = re.findall(
|
||||
r'src="(https://cdn\.ncbi\.nlm\.nih\.gov/pmc/blobs/[^"]+\.(?:jpg|jpeg|png))"',
|
||||
html,
|
||||
)
|
||||
# 중복 제거, 순서 유지
|
||||
seen = set()
|
||||
urls = []
|
||||
for u in matches:
|
||||
if u not in seen:
|
||||
seen.add(u)
|
||||
urls.append(u)
|
||||
return urls[:max_imgs]
|
||||
|
||||
|
||||
def gemini_validate(img_url, term_korean, term_english):
|
||||
try:
|
||||
req = urllib.request.Request(img_url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
b64 = base64.b64encode(r.read()).decode()
|
||||
except Exception as e:
|
||||
return False, f"다운로드 실패: {e}"
|
||||
|
||||
prompt = (
|
||||
f"치과 교정 용어: {term_korean} ({term_english})\n"
|
||||
f"이 이미지가 위 교정 용어를 설명하거나 예시하는 데 적합한가요?\n"
|
||||
f"흐름도(flowchart), 그래프, 표는 '아니오'로 답하세요.\n"
|
||||
f"반드시 아래 형식으로만 답하세요:\n적합: 예/아니오\n이유: (한 줄)"
|
||||
)
|
||||
payload = {
|
||||
"model": MODEL,
|
||||
"messages": [{"role": "user", "content": prompt, "images": [b64]}],
|
||||
"stream": False,
|
||||
}
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(OLLAMA_URL, data=data, headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
resp = json.loads(r.read())
|
||||
text = resp.get("message", {}).get("content", "")
|
||||
relevant = "적합: 예" in text
|
||||
reason = next((l.replace("이유:", "").strip() for l in text.splitlines() if l.startswith("이유:")), "")
|
||||
return relevant, reason
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def main():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
cur = conn.cursor()
|
||||
|
||||
results = {}
|
||||
if os.path.exists(RESULTS_FILE):
|
||||
results = json.load(open(RESULTS_FILE))
|
||||
|
||||
# NULL이거나 generic placeholder인 target term 가져오기
|
||||
id_list = ",".join(str(i) for i in TARGET_IDS)
|
||||
cur.execute(
|
||||
f"SELECT id, korean, english FROM terms "
|
||||
f"WHERE id IN ({id_list}) "
|
||||
f"AND (image_url IS NULL OR image_url = '' OR image_url LIKE '%ismaelportog%')"
|
||||
)
|
||||
terms = cur.fetchall()[:MAX_TERMS]
|
||||
print(f"대상: {len(terms)}개 term\n")
|
||||
|
||||
updated = skipped = failed = 0
|
||||
|
||||
for i, (term_id, korean, english) in enumerate(terms):
|
||||
key = str(term_id)
|
||||
if key in results and results[key].get("updated"):
|
||||
updated += 1
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
query = f"{english} orthodontic dental"
|
||||
time.sleep(ESEARCH_DELAY)
|
||||
pmcids = esearch(query, retmax=5)
|
||||
|
||||
found = False
|
||||
tried_urls = []
|
||||
|
||||
for pmcid in pmcids:
|
||||
time.sleep(FETCH_DELAY)
|
||||
img_urls = extract_figure_urls(pmcid, max_imgs=3)
|
||||
|
||||
for img_url in img_urls:
|
||||
if img_url in tried_urls:
|
||||
continue
|
||||
tried_urls.append(img_url)
|
||||
|
||||
relevant, reason = gemini_validate(img_url, korean, english)
|
||||
print(f" [{i+1}/{len(terms)}] {korean} | PMC{pmcid} | {'✓' if relevant else '✗'} {reason[:45]}")
|
||||
|
||||
if relevant:
|
||||
cur.execute("UPDATE terms SET image_url=? WHERE id=?", (img_url, term_id))
|
||||
conn.commit()
|
||||
results[key] = {"korean": korean, "english": english,
|
||||
"pmcid": pmcid, "image_url": img_url, "updated": True}
|
||||
updated += 1
|
||||
found = True
|
||||
break
|
||||
|
||||
if found:
|
||||
break
|
||||
|
||||
if not found:
|
||||
results[key] = {"korean": korean, "english": english,
|
||||
"pmcid": None, "image_url": None, "updated": False}
|
||||
print(f" [{i+1}/{len(terms)}] {korean} — 적합 이미지 없음")
|
||||
failed += 1
|
||||
|
||||
with open(RESULTS_FILE, "w") as f:
|
||||
json.dump(results, f, ensure_ascii=False, indent=2)
|
||||
|
||||
conn.close()
|
||||
total = len(terms)
|
||||
print(f"\n=== PMC+Gemini 교정 결과 ===")
|
||||
print(f"처리: {total}개 | 성공: {updated - skipped}개 | 실패: {failed}개 | 재개(skip): {skipped}개")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,352 @@
|
||||
{
|
||||
"44": {
|
||||
"korean": "공간 유지 장치",
|
||||
"english": "space maintainer",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"47": {
|
||||
"korean": "치아 이동",
|
||||
"english": "tooth movement",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"204": {
|
||||
"korean": "고정원",
|
||||
"english": "anchorage",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"205": {
|
||||
"korean": "설측 교정 장치",
|
||||
"english": "lingual orthodontic appliance",
|
||||
"pmcid": "12689034",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2163/12689034/8dc1ca7e9f4c/medi-104-e46057-g003.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"206": {
|
||||
"korean": "헤드기어",
|
||||
"english": "headgear",
|
||||
"pmcid": "13150257",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/6dfc/13150257/caa8e885a15f/973206300220634F1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"207": {
|
||||
"korean": "교정력",
|
||||
"english": "orthodontic force",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"210": {
|
||||
"korean": "고무줄",
|
||||
"english": "intermaxillary elastics",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"212": {
|
||||
"korean": "치간 절삭",
|
||||
"english": "interproximal reduction",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"215": {
|
||||
"korean": "결찰선",
|
||||
"english": "ligature wire",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"218": {
|
||||
"korean": "정출",
|
||||
"english": "extrusion",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"219": {
|
||||
"korean": "압하",
|
||||
"english": "intrusion",
|
||||
"pmcid": "13154446",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b184/13154446/c2691ff77f6b/12903_2026_8186_Fig1_HTML.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"220": {
|
||||
"korean": "기능성 교정 장치",
|
||||
"english": "functional appliance",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"221": {
|
||||
"korean": "치열궁 확장",
|
||||
"english": "arch expansion",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/f5808dc577bd/10.1177_03000605261444867-fig3.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"966": {
|
||||
"korean": "악간고정",
|
||||
"english": "Intermaxillary Fixation",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"968": {
|
||||
"korean": "생역학",
|
||||
"english": "Biomechanics",
|
||||
"pmcid": "13160625",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/8be2/13160625/06522f5d7fc0/10-1055-s-0045-1810443-i2524114-2.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"969": {
|
||||
"korean": "회전교정",
|
||||
"english": "Rotation Correction",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/f5808dc577bd/10.1177_03000605261444867-fig3.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"974": {
|
||||
"korean": "스커즈바이트",
|
||||
"english": "Scissors Bite",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"977": {
|
||||
"korean": "팔랜탈바",
|
||||
"english": "Palatal Bar",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"978": {
|
||||
"korean": "쿼드헬릭스",
|
||||
"english": "Quad Helix",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"979": {
|
||||
"korean": "급속구개확대",
|
||||
"english": "Rapid Palatal Expansion",
|
||||
"pmcid": "13160605",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/cc81/13160605/1d6750a20d05/10-1055-s-0045-1810017-i2544198-1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"980": {
|
||||
"korean": "구개확대기",
|
||||
"english": "Palatal Expander",
|
||||
"pmcid": "13160605",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/cc81/13160605/1d6750a20d05/10-1055-s-0045-1810017-i2544198-1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"981": {
|
||||
"korean": "니타놀와이어",
|
||||
"english": "Nickel-Titanium Wire",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"982": {
|
||||
"korean": "티엠에이철선",
|
||||
"english": "TMA Wire",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"983": {
|
||||
"korean": "스테인리스와이어",
|
||||
"english": "Stainless Steel Wire",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"984": {
|
||||
"korean": "코일스프링",
|
||||
"english": "Coil Spring",
|
||||
"pmcid": "13154446",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/b184/13154446/c2691ff77f6b/12903_2026_8186_Fig1_HTML.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"985": {
|
||||
"korean": "탄성사",
|
||||
"english": "Elastic Band",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"988": {
|
||||
"korean": "교정철선",
|
||||
"english": "Archwire",
|
||||
"pmcid": "13157596",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/3a8a/13157596/b061a42a7a76/cureus-0018-00000106753-i01.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"989": {
|
||||
"korean": "둥근철선",
|
||||
"english": "Round Wire",
|
||||
"pmcid": "13150233",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/7274/13150233/14d5c43fad35/973206300220947F2B.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"990": {
|
||||
"korean": "네모난철선",
|
||||
"english": "Rectangular Wire",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"991": {
|
||||
"korean": "세로형철선",
|
||||
"english": "Square Wire",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"992": {
|
||||
"korean": "슬롯크기",
|
||||
"english": "Slot Size",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"993": {
|
||||
"korean": "고무결찰",
|
||||
"english": "Elastic Ligature",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"994": {
|
||||
"korean": "셀프라이게이팅브라켓",
|
||||
"english": "Self-ligating Bracket",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"995": {
|
||||
"korean": "세라믹브라켓",
|
||||
"english": "Ceramic Bracket",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"996": {
|
||||
"korean": "메탈브라켓",
|
||||
"english": "Metal Bracket",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"997": {
|
||||
"korean": "버튼",
|
||||
"english": "Orthodontic Button",
|
||||
"pmcid": "13157596",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/3a8a/13157596/b061a42a7a76/cureus-0018-00000106753-i01.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1000": {
|
||||
"korean": "간격유지자",
|
||||
"english": "Space Maintainer",
|
||||
"pmcid": "13118652",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/7ea3/13118652/b9db6cd429bf/pediatrrep-18-00046-g002.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1001": {
|
||||
"korean": "공간폐쇄",
|
||||
"english": "Space Closure",
|
||||
"pmcid": "12689034",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2163/12689034/2374fcb708be/medi-104-e46057-g002.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1002": {
|
||||
"korean": "발치교정",
|
||||
"english": "Extraction Orthodontics",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"1003": {
|
||||
"korean": "비발치교정",
|
||||
"english": "Non-extraction Orthodontics",
|
||||
"pmcid": "12689034",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2163/12689034/2374fcb708be/medi-104-e46057-g002.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1004": {
|
||||
"korean": "치아혼잡",
|
||||
"english": "Dental Crowding",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/bd2e10c7b2b2/10.1177_03000605261444867-fig2.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1006": {
|
||||
"korean": "보상곡선",
|
||||
"english": "Curve of Spee",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/bd2e10c7b2b2/10.1177_03000605261444867-fig2.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1007": {
|
||||
"korean": "윌슨곡선",
|
||||
"english": "Curve of Wilson",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"1008": {
|
||||
"korean": "교합평면",
|
||||
"english": "Occlusal Plane",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/c42d3ebdcfc2/10.1177_03000605261444867-fig1.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1009": {
|
||||
"korean": "교합곡선",
|
||||
"english": "Occlusal Curve",
|
||||
"pmcid": "13158486",
|
||||
"image_url": "https://cdn.ncbi.nlm.nih.gov/pmc/blobs/2670/13158486/bd2e10c7b2b2/10.1177_03000605261444867-fig2.jpg",
|
||||
"updated": true
|
||||
},
|
||||
"1014": {
|
||||
"korean": "기능성교정기",
|
||||
"english": "Functional Appliance",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"1015": {
|
||||
"korean": "악태조정장치",
|
||||
"english": "Bionator",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"1017": {
|
||||
"korean": "프랭켈교정기",
|
||||
"english": "Frankel Appliance",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"1018": {
|
||||
"korean": "활성보유기",
|
||||
"english": "Active Retainer",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
},
|
||||
"1019": {
|
||||
"korean": "하와리테이너",
|
||||
"english": "Hawley Retainer",
|
||||
"pmcid": null,
|
||||
"image_url": null,
|
||||
"updated": false
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,166 @@
|
||||
{
|
||||
"gemini-3-flash-preview:cloud": [
|
||||
{
|
||||
"id": 1542,
|
||||
"korean": "기능성 부정교합",
|
||||
"category": "교정",
|
||||
"gemini": false,
|
||||
"result": false,
|
||||
"elapsed": 6.79,
|
||||
"reason": "이 이미지는 연구 문헌 선정 과정을 나타내는 흐름도(flowchart)이므로 부적합합니다.",
|
||||
"response": "적합: 아니오\n이유: 이 이미지는 연구 문헌 선정 과정을 나타내는 흐름도(flowchart)이므로 부적합합니다."
|
||||
},
|
||||
{
|
||||
"id": 419,
|
||||
"korean": "퇴행변성",
|
||||
"category": "구강병리",
|
||||
"gemini": false,
|
||||
"result": false,
|
||||
"elapsed": 4.95,
|
||||
"reason": "제시된 이미지는 원숭이의 모습으로, 구강 내 조직이나 세포의 퇴행성 변화를 설명하기에 부적절합니다.",
|
||||
"response": "적합: 아니오\n이유: 제시된 이미지는 원숭이의 모습으로, 구강 내 조직이나 세포의 퇴행성 변화를 설명하기에 부적절합니다."
|
||||
},
|
||||
{
|
||||
"id": 526,
|
||||
"korean": "치종",
|
||||
"category": "구강병리",
|
||||
"gemini": false,
|
||||
"result": false,
|
||||
"elapsed": 5.54,
|
||||
"reason": "이미지는 방사선 투과성 낭종을 보여주며, 방사선 불투과성 덩어리로 나타나는 치종의 전형적인 특징을 보여주지 않습니다.",
|
||||
"response": "적합: 아니오\n이유: 이미지는 방사선 투과성 낭종을 보여주며, 방사선 불투과성 덩어리로 나타나는 치종의 전형적인 특징을 보여주지 않습니다."
|
||||
},
|
||||
{
|
||||
"id": 3021,
|
||||
"korean": "시멘트아세포종",
|
||||
"category": "구강병리",
|
||||
"gemini": false,
|
||||
"result": true,
|
||||
"elapsed": 6.27,
|
||||
"reason": "치아 뿌리에 부착된 방사선 불투과성 종괴와 그 주변의 투과성 테두리가 시멘트아세포종의 전형적인 방사선학적 특징을 잘 나타냅니다.",
|
||||
"response": "적합: 예\n이유: 치아 뿌리에 부착된 방사선 불투과성 종괴와 그 주변의 투과성 테두리가 시멘트아세포종의 전형적인 방사선학적 특징을 잘 나타냅니다."
|
||||
},
|
||||
{
|
||||
"id": 90,
|
||||
"korean": "유치 치수 치료",
|
||||
"category": "소아치과",
|
||||
"gemini": true,
|
||||
"result": false,
|
||||
"elapsed": 4.31,
|
||||
"reason": "해당 이미지는 통계 차트(그래프)에 해당하므로 제시된 기준에 따라 적합하지 않습니다.",
|
||||
"response": "적합: 아니오\n이유: 해당 이미지는 통계 차트(그래프)에 해당하므로 제시된 기준에 따라 적합하지 않습니다."
|
||||
},
|
||||
{
|
||||
"id": 280,
|
||||
"korean": "치수절단술",
|
||||
"category": "소아치과",
|
||||
"gemini": true,
|
||||
"result": false,
|
||||
"elapsed": 4.25,
|
||||
"reason": "제공된 이미지는 통계 차트(그래프)이므로 지침에 따라 부적합합니다.",
|
||||
"response": "적합: 아니오\n이유: 제공된 이미지는 통계 차트(그래프)이므로 지침에 따라 부적합합니다."
|
||||
},
|
||||
{
|
||||
"id": 646,
|
||||
"korean": "소아전신마취",
|
||||
"category": "소아치과",
|
||||
"gemini": true,
|
||||
"result": false,
|
||||
"elapsed": 5.7,
|
||||
"reason": "이 이미지는 흐름도(flowchart) 형식을 띠고 있으며, 소아전신마취라는 구체적인 의료 행위보다는 소아의 치과 공포와 관련된 심리적 요인들을 설명하고 있습니다.",
|
||||
"response": "적합: 아니오\n이유: 이 이미지는 흐름도(flowchart) 형식을 띠고 있으며, 소아전신마취라는 구체적인 의료 행위보다는 소아의 치과 공포와 관련된 심리적 요인들을 설명하고 있습니다."
|
||||
},
|
||||
{
|
||||
"id": 650,
|
||||
"korean": "유치근관치료",
|
||||
"category": "소아치과",
|
||||
"gemini": true,
|
||||
"result": false,
|
||||
"elapsed": 4.93,
|
||||
"reason": "해당 이미지는 연구 문헌 선정 과정을 나타내는 흐름도(flowchart)이므로 조건에 따라 부적합합니다.",
|
||||
"response": "적합: 아니오\n이유: 해당 이미지는 연구 문헌 선정 과정을 나타내는 흐름도(flowchart)이므로 조건에 따라 부적합합니다."
|
||||
}
|
||||
],
|
||||
"gemma4:31b-cloud": [
|
||||
{
|
||||
"id": 1542,
|
||||
"korean": "기능성 부정교합",
|
||||
"category": "교정",
|
||||
"gemini": false,
|
||||
"result": false,
|
||||
"elapsed": 1.52,
|
||||
"reason": "해당 이미지는 연구 논문 선정 과정을 나타내는 흐름도(flowchart)입니다.",
|
||||
"response": "적합: 아니오\n이유: 해당 이미지는 연구 논문 선정 과정을 나타내는 흐름도(flowchart)입니다."
|
||||
},
|
||||
{
|
||||
"id": 419,
|
||||
"korean": "퇴행변성",
|
||||
"category": "구강병리",
|
||||
"gemini": false,
|
||||
"result": false,
|
||||
"elapsed": 1.1,
|
||||
"reason": "해당 이미지는 원숭이의 모습일 뿐, 구강병리적 퇴행변성을 보여주는 의학적 이미지나 예시가 아닙니다.",
|
||||
"response": "적합: 아니오\n이유: 해당 이미지는 원숭이의 모습일 뿐, 구강병리적 퇴행변성을 보여주는 의학적 이미지나 예시가 아닙니다."
|
||||
},
|
||||
{
|
||||
"id": 526,
|
||||
"korean": "치종",
|
||||
"category": "구강병리",
|
||||
"gemini": false,
|
||||
"result": true,
|
||||
"elapsed": 1.81,
|
||||
"reason": "CT 영상에서 치아 조직과 유사한 고밀도의 종괴가 관찰되어 치종(Odontoma)의 전형적인 방사선학적 특징을 보여줍니다.",
|
||||
"response": "적합: 예\n이유: CT 영상에서 치아 조직과 유사한 고밀도의 종괴가 관찰되어 치종(Odontoma)의 전형적인 방사선학적 특징을 보여줍니다."
|
||||
},
|
||||
{
|
||||
"id": 3021,
|
||||
"korean": "시멘트아세포종",
|
||||
"category": "구강병리",
|
||||
"gemini": false,
|
||||
"result": true,
|
||||
"elapsed": 1.14,
|
||||
"reason": "치아 뿌리 끝에 부착된 방사선 불투과성 덩어리와 주변의 투과성 테두리가 시멘트아세포종의 전형적인 방사선학적 특징을 보여줍니다.",
|
||||
"response": "적합: 예\n이유: 치아 뿌리 끝에 부착된 방사선 불투과성 덩어리와 주변의 투과성 테두리가 시멘트아세포종의 전형적인 방사선학적 특징을 보여줍니다."
|
||||
},
|
||||
{
|
||||
"id": 90,
|
||||
"korean": "유치 치수 치료",
|
||||
"category": "소아치과",
|
||||
"gemini": true,
|
||||
"result": false,
|
||||
"elapsed": 1.16,
|
||||
"reason": "해당 이미지는 통계 수치를 나타내는 막대 그래프입니다.",
|
||||
"response": "적합: 아니오\n이유: 해당 이미지는 통계 수치를 나타내는 막대 그래프입니다."
|
||||
},
|
||||
{
|
||||
"id": 280,
|
||||
"korean": "치수절단술",
|
||||
"category": "소아치과",
|
||||
"gemini": true,
|
||||
"result": false,
|
||||
"elapsed": 1.78,
|
||||
"reason": "해당 이미지는 재료의 강도를 나타내는 그래프이므로 적합하지 않습니다.",
|
||||
"response": "적합: 아니오\n이유: 해당 이미지는 재료의 강도를 나타내는 그래프이므로 적합하지 않습니다."
|
||||
},
|
||||
{
|
||||
"id": 646,
|
||||
"korean": "소아전신마취",
|
||||
"category": "소아치과",
|
||||
"gemini": true,
|
||||
"result": false,
|
||||
"elapsed": 1.8,
|
||||
"reason": "해당 이미지는 소아 치과 공포의 전달과 부모의 역할에 관한 흐름도입니다.",
|
||||
"response": "적합: 아니오\n이유: 해당 이미지는 소아 치과 공포의 전달과 부모의 역할에 관한 흐름도입니다."
|
||||
},
|
||||
{
|
||||
"id": 650,
|
||||
"korean": "유치근관치료",
|
||||
"category": "소아치과",
|
||||
"gemini": true,
|
||||
"result": false,
|
||||
"elapsed": 1.65,
|
||||
"reason": "해당 이미지는 문헌 선정 과정을 나타내는 흐름도(PRISMA flow diagram)입니다.",
|
||||
"response": "적합: 아니오\n이유: 해당 이미지는 문헌 선정 과정을 나타내는 흐름도(PRISMA flow diagram)입니다."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
# 치과 용어 사전 (dental_dict) — 작업 가이드
|
||||
|
||||
## 디렉터리 구조 (`.smallclaw/databases/`)
|
||||
|
||||
```
|
||||
dental_dict.db SQLite DB (terms / categories / references_tb + FTS5)
|
||||
config.py 경로·엔드포인트·API 키 위치 (하드코딩 금지, 여기서만 관리)
|
||||
manage.py 단일 진입점 — 모든 작업은 서브커맨드로
|
||||
workflow_terms.py 용어 추가 로직
|
||||
workflow_images.py 이미지 매핑 로직 (9개 소스)
|
||||
workflow_verify.py 이미지 적합성 검증 로직
|
||||
seeds/ 용어 seed JSON 파일 (JSONC: // 주석 허용)
|
||||
scratch/ 1회성 스크립트·로그·결과 작업 공간 (git-ignore, /tmp 대체)
|
||||
docs/howto.md 이 문서
|
||||
docs/archive_image_rounds/ 완료된 라운드 스크립트·결과 보관
|
||||
docs/archive_validation/ 모델 비교·검증 결과 데이터 보관
|
||||
dental_images/ 다운로드된 이미지 (517MB) — git 미추적
|
||||
```
|
||||
|
||||
## 명령어
|
||||
|
||||
```bash
|
||||
cd .smallclaw/databases
|
||||
|
||||
python3 manage.py stats # 카테고리별 용어/이미지 커버리지
|
||||
python3 manage.py add-terms --all # seeds/ 의 모든 JSON 임포트
|
||||
python3 manage.py add-terms seed_perio.json # 특정 seed 파일만
|
||||
python3 manage.py add-images --source all # 9개 소스 전체 이미지 매핑
|
||||
python3 manage.py add-images --source pmc --pmc-category 치주
|
||||
python3 manage.py verify --category 교정 --fix # 잘못 매핑된 이미지 검증·제거
|
||||
python3 manage.py reassign # 플레이스홀더 초기화 후 PMC 재배정
|
||||
```
|
||||
|
||||
각 서브커맨드의 옵션은 `manage.py <명령> --help` 참조.
|
||||
|
||||
## 용어 추가 (add-terms)
|
||||
|
||||
seed 파일은 `seeds/` 의 JSON 배열. 항목은 `korean` 필수, 나머지(`english`,
|
||||
`latin`, `abbreviation`, `definition`, `category`, `synonyms`, `related_ids`,
|
||||
`pmids`, `icd_code`, `notes`)는 선택. `korean` 또는 `english` 가 이미 있으면
|
||||
건너뛰므로 재실행해도 안전. `//` 줄 주석 사용 가능.
|
||||
|
||||
## 이미지 매핑 (add-images)
|
||||
|
||||
`--source all` 실행 순서:
|
||||
|
||||
1. Kaggle 로컬 데이터셋
|
||||
2. Roboflow 로컬 COCO JSON
|
||||
3. Hugging Face 로컬 데이터셋
|
||||
4. Radiopaedia URL 매핑
|
||||
5. ITU 카탈로그 (placeholder)
|
||||
6. Roboflow REST API (추가 프로젝트)
|
||||
7. Zenodo REST API
|
||||
8. Mendeley Data API
|
||||
9. PMC Open Access figure URL
|
||||
10. Wikimedia Commons (가장 마지막, 가장 느림)
|
||||
|
||||
PMC는 이미지 커버리지가 낮은 카테고리부터 자동 처리 (정적 목록 없이 매 실행 시
|
||||
DB에서 동적 계산). 특정 카테고리만 처리하려면 `--pmc-category <카테고리>`.
|
||||
|
||||
**검증 후 저장**: 모든 소스는 저장 전에 vision 모델로 이미지 적합성을 검증한다
|
||||
(`workflow_verify.verify_updates`). PMC·Wikimedia는 원래부터 인라인 검증을 했고,
|
||||
나머지 소스(kaggle/roboflow/huggingface/roboflow_api/zenodo/mendeley)는 수집한
|
||||
후보를 일괄 검증해 부적합 건을 버린 뒤 commit 한다. Radiopaedia는 이미지가 아닌
|
||||
문서 URL을 `source_url`에 저장하므로 검증 대상이 아니다. 검증을 건너뛰려면
|
||||
`--no-verify` (Ollama 미동작 시나 빠른 대량 실행용 — PMC·Wikimedia는 항상 검증).
|
||||
|
||||
## 이미지 검증 (verify)
|
||||
|
||||
이미 `image_url` 이 있는 용어를 vision 모델(Ollama)로 재검사. `--fix` 를 주면
|
||||
부적합 매핑을 `image_url = NULL` 로 비워서 다음 `add-images` 실행 시 다시 채움.
|
||||
1회성 라운드 배정 스크립트는 `docs/archive_image_rounds/` 에 보관됨.
|
||||
|
||||
## 작업 디렉터리 규칙
|
||||
|
||||
새 ad-hoc 스크립트·로그·결과 JSON은 `/tmp` 가 아니라 **`scratch/`** 에 만든다.
|
||||
재부팅에 안전하고 프로젝트와 함께 이동한다. 작업이 끝나면:
|
||||
- 보존 가치 있음 → `docs/archive_image_rounds/` (라운드 스크립트류) 또는
|
||||
`docs/archive_validation/` (검증 결과 JSON) 로 이동.
|
||||
- 보존 가치 없음 → 삭제.
|
||||
|
||||
## 이미지 디렉터리 / 심링크
|
||||
|
||||
게이트웨이는 `image_url` 의 `/api/files/uploads/dental_images/...` 경로를
|
||||
`workspace/uploads/dental_images` 심링크를 통해 서빙한다. 이 심링크는
|
||||
`→ .smallclaw/databases/dental_images` 를 가리키며 **git에 추적되지 않으므로**
|
||||
새 환경에서는 수동 재생성이 필요하다:
|
||||
|
||||
```bash
|
||||
ln -s ../../.smallclaw/databases/dental_images workspace/uploads/dental_images
|
||||
```
|
||||
|
||||
## 주의사항
|
||||
|
||||
- **DB 쓰기 패턴**: 모든 함수는 수집 후 일괄 `commit()` (루프 안 증분 쓰기 금지)
|
||||
- **진행 출력**: 백그라운드 스크립트는 `print(..., flush=True)` 필수
|
||||
- **Wikimedia**: 429 오류 시 자동 30초 대기, `--delay 6` 이상 권장
|
||||
- **PMC**: `--pmc-delay 0.4` 이하로 낮추면 NCBI 차단 위험
|
||||
- 이미 매핑된 용어는 건너뜀 (`image_url IS NULL OR image_url = ''` 조건)
|
||||
- API 키는 `.smallclaw/{roboflow,huggingface}_api_key.txt`,
|
||||
`.smallclaw/kaggle_api_token.txt` — git-ignore 처리됨, 커밋 금지
|
||||
```
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Import dental terms from JSON into dental_dict.db"""
|
||||
|
||||
import json, sqlite3, sys
|
||||
from datetime import datetime
|
||||
|
||||
def import_terms(json_path: str, db_path: str):
|
||||
with open(json_path, encoding='utf-8') as f:
|
||||
terms = json.load(f)
|
||||
|
||||
db = sqlite3.connect(db_path)
|
||||
now = datetime.now().isoformat()
|
||||
inserted = skipped = 0
|
||||
|
||||
for t in terms:
|
||||
existing = db.execute(
|
||||
"SELECT id FROM terms WHERE korean = ? OR english = ?",
|
||||
(t.get('korean',''), t.get('english',''))
|
||||
).fetchone()
|
||||
if existing:
|
||||
print(f" SKIP (이미 존재): {t.get('korean')} / {t.get('english')}")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
db.execute("""
|
||||
INSERT INTO terms
|
||||
(korean, english, latin, abbreviation, definition,
|
||||
category, synonyms, related_ids, pmids, icd_code, notes,
|
||||
created_at, updated_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
""", (
|
||||
t.get('korean',''),
|
||||
t.get('english',''),
|
||||
t.get('latin',''),
|
||||
t.get('abbreviation',''),
|
||||
t.get('definition',''),
|
||||
t.get('category',''),
|
||||
t.get('synonyms',''),
|
||||
t.get('related_ids',''),
|
||||
t.get('pmids',''),
|
||||
t.get('icd_code',''),
|
||||
t.get('notes',''),
|
||||
now, now
|
||||
))
|
||||
print(f" OK: {t.get('korean')} ({t.get('english')})")
|
||||
inserted += 1
|
||||
|
||||
db.commit()
|
||||
db.close()
|
||||
print(f"\n완료 — 삽입: {inserted}개, 스킵: {skipped}개")
|
||||
|
||||
if __name__ == '__main__':
|
||||
db_path = '/home/kim/homeclaw/.smallclaw/databases/dental_dict.db'
|
||||
json_path = sys.argv[1] if len(sys.argv) > 1 else 'seed_periodontics.json'
|
||||
import_terms(json_path, db_path)
|
||||
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Dental dictionary management CLI — single entry point.
|
||||
|
||||
Subcommands:
|
||||
add-terms Import terms from JSON seed files (seeds/) -> workflow_terms
|
||||
add-images Map terms to images from local/remote sources -> workflow_images
|
||||
verify Re-check that mapped images fit their terms -> workflow_verify
|
||||
reassign Clear placeholder images, then re-fill via PMC -> workflow_images
|
||||
stats Print per-category term / image-coverage report
|
||||
|
||||
Run `manage.py <subcommand> --help` for options. See docs/howto.md.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
from config import DB_PATH
|
||||
import workflow_images
|
||||
import workflow_terms
|
||||
import workflow_verify
|
||||
|
||||
|
||||
def cmd_add_terms(args):
|
||||
workflow_terms.run(seed_files=args.files, all_seeds=args.all)
|
||||
|
||||
|
||||
def cmd_add_images(args):
|
||||
workflow_images.run(
|
||||
source=args.source,
|
||||
verify=not args.no_verify,
|
||||
max_wikimedia=args.max_wikimedia,
|
||||
wikimedia_category=args.wikimedia_category,
|
||||
delay=args.delay,
|
||||
max_pmc=args.max_pmc,
|
||||
pmc_delay=args.pmc_delay,
|
||||
pmc_category=args.pmc_category,
|
||||
pmc_model=args.pmc_model,
|
||||
)
|
||||
|
||||
|
||||
def cmd_verify(args):
|
||||
workflow_verify.verify_db(
|
||||
category=args.category,
|
||||
limit=args.limit,
|
||||
fix=args.fix,
|
||||
model=args.model,
|
||||
)
|
||||
|
||||
|
||||
def cmd_reassign(args):
|
||||
"""Null out placeholder images, then re-fill via PMC OA + validation."""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
n = conn.execute(
|
||||
"SELECT COUNT(*) FROM terms WHERE image_url LIKE ?", (args.placeholder,)
|
||||
).fetchone()[0]
|
||||
if n:
|
||||
conn.execute(
|
||||
"UPDATE terms SET image_url = NULL WHERE image_url LIKE ?",
|
||||
(args.placeholder,),
|
||||
)
|
||||
conn.commit()
|
||||
print(f"Cleared {n} placeholder images matching '{args.placeholder}'\n")
|
||||
else:
|
||||
print(f"No placeholders matching '{args.placeholder}'\n")
|
||||
workflow_images.map_pmc_oa(
|
||||
conn, max_terms=args.max_pmc, delay=args.pmc_delay,
|
||||
category=args.pmc_category, model=args.pmc_model,
|
||||
)
|
||||
conn.close()
|
||||
|
||||
|
||||
def cmd_stats(args):
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
total, imgs = conn.execute(
|
||||
"SELECT COUNT(*), SUM(CASE WHEN image_url IS NOT NULL AND image_url != '' "
|
||||
"THEN 1 ELSE 0 END) FROM terms"
|
||||
).fetchone()
|
||||
print(f"Terms: {total} Images: {imgs} ({100.0 * imgs / total:.1f}%)\n")
|
||||
print(f"{'category':<16} {'terms':>6} {'images':>7} {'cov':>7}")
|
||||
print("-" * 40)
|
||||
for cat, t, n, pct in conn.execute("""
|
||||
SELECT category AS cat, COUNT(*) AS t,
|
||||
SUM(CASE WHEN image_url IS NOT NULL AND image_url != '' THEN 1 ELSE 0 END) AS n,
|
||||
ROUND(100.0 * SUM(CASE WHEN image_url IS NOT NULL AND image_url != ''
|
||||
THEN 1 ELSE 0 END) / COUNT(*), 1) AS pct
|
||||
FROM terms GROUP BY cat ORDER BY pct ASC, t DESC
|
||||
"""):
|
||||
print(f"{cat:<16} {t:>6} {n:>7} {pct:>6}%")
|
||||
conn.close()
|
||||
|
||||
|
||||
def build_parser():
|
||||
p = argparse.ArgumentParser(prog="manage.py", description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
sub = p.add_subparsers(dest="command", required=True)
|
||||
|
||||
# add-terms
|
||||
t = sub.add_parser("add-terms", help="Import terms from JSON seed files")
|
||||
t.add_argument("files", nargs="*", help="Seed file names (relative to seeds/) or paths")
|
||||
t.add_argument("--all", action="store_true", help="Import every *.json in seeds/")
|
||||
t.set_defaults(func=cmd_add_terms)
|
||||
|
||||
# add-images
|
||||
i = sub.add_parser("add-images", help="Map terms to images from a source")
|
||||
i.add_argument("--source", default="all",
|
||||
choices=["all"] + workflow_images.SOURCES,
|
||||
help="Image source (default: all)")
|
||||
i.add_argument("--no-verify", action="store_true",
|
||||
help="Skip vision-model verification before saving "
|
||||
"(faster bulk runs; pmc/wikimedia always verify)")
|
||||
i.add_argument("--max-wikimedia", type=int, default=50)
|
||||
i.add_argument("--wikimedia-category", default=None)
|
||||
i.add_argument("--delay", type=int, default=5,
|
||||
help="Delay between Wikimedia requests (s)")
|
||||
i.add_argument("--max-pmc", type=int, default=200)
|
||||
i.add_argument("--pmc-delay", type=float, default=0.4)
|
||||
i.add_argument("--pmc-category", default=None)
|
||||
i.add_argument("--pmc-model", default="gemini-3-flash-preview:cloud")
|
||||
i.set_defaults(func=cmd_add_images)
|
||||
|
||||
# verify
|
||||
v = sub.add_parser("verify", help="Re-check that mapped images fit their terms")
|
||||
v.add_argument("--category", default=None, help="Limit to one category")
|
||||
v.add_argument("--limit", type=int, default=None, help="Max terms to check")
|
||||
v.add_argument("--fix", action="store_true",
|
||||
help="Clear bad mappings (image_url -> NULL)")
|
||||
v.add_argument("--model", default=workflow_verify.VERIFY_MODEL)
|
||||
v.set_defaults(func=cmd_verify)
|
||||
|
||||
# reassign
|
||||
r = sub.add_parser("reassign", help="Clear placeholder images, re-fill via PMC")
|
||||
r.add_argument("--placeholder", default="%ismaelportog%",
|
||||
help="SQL LIKE pattern for placeholder image_urls")
|
||||
r.add_argument("--max-pmc", type=int, default=200)
|
||||
r.add_argument("--pmc-delay", type=float, default=0.4)
|
||||
r.add_argument("--pmc-category", default=None)
|
||||
r.add_argument("--pmc-model", default="gemma4:31b-cloud",
|
||||
help="Vision model for PMC image validation")
|
||||
r.set_defaults(func=cmd_reassign)
|
||||
|
||||
# stats
|
||||
s = sub.add_parser("stats", help="Per-category coverage report")
|
||||
s.set_defaults(func=cmd_stats)
|
||||
|
||||
return p
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = build_parser().parse_args(argv)
|
||||
args.func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,12 @@
|
||||
# scratch/
|
||||
|
||||
치과 사전 작업의 1회성 스크립트·로그·결과 임시 저장소.
|
||||
git 미추적이며 재부팅에는 안전 (`/tmp` 대체).
|
||||
|
||||
**규칙:**
|
||||
- 새 ad-hoc 스크립트·로그·결과 JSON은 여기에 만든다 (`/tmp` 쓰지 말 것).
|
||||
- 끝난 작업은:
|
||||
- 보존 가치 있음 → `../docs/archive_image_rounds/` 또는 `../docs/archive_validation/` 으로 이동.
|
||||
- 보존 가치 없음 → 삭제.
|
||||
|
||||
`.gitignore` 에 디렉터리 자체가 등록되어 있어 README.md만 추적된다.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,106 @@
|
||||
"""Term-addition workflow for the dental dictionary.
|
||||
|
||||
Imports terms from JSON seed files in seeds/ into dental_dict.db. Seed files
|
||||
may contain `//` line comments (JSONC) — they are stripped before parsing.
|
||||
|
||||
A seed entry is an object with at least `korean`; any of these keys are used:
|
||||
korean, english, latin, abbreviation, definition, category,
|
||||
synonyms, related_ids, pmids, icd_code, notes
|
||||
|
||||
Duplicates (same korean OR same english) are skipped, so re-running a seed
|
||||
file is safe. Called by manage.py's `add-terms` subcommand.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
from config import DB_PATH, SEEDS_DIR
|
||||
|
||||
FIELDS = [
|
||||
"korean", "english", "latin", "abbreviation", "definition",
|
||||
"category", "synonyms", "related_ids", "pmids", "icd_code", "notes",
|
||||
]
|
||||
|
||||
|
||||
def load_jsonc(path):
|
||||
"""Load a JSON file that may contain `//` line comments."""
|
||||
with open(path, encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
# Strip // comments that are not inside a string. Seed files only use
|
||||
# // at the start of (optionally indented) lines, so a line-wise strip
|
||||
# is sufficient and avoids a full JSON tokenizer.
|
||||
cleaned = re.sub(r'^\s*//.*$', '', text, flags=re.MULTILINE)
|
||||
return json.loads(cleaned)
|
||||
|
||||
|
||||
def import_file(conn, path):
|
||||
"""Import one seed file. Returns (inserted, skipped)."""
|
||||
terms = load_jsonc(path)
|
||||
now = datetime.now().isoformat()
|
||||
inserted = skipped = 0
|
||||
|
||||
for t in terms:
|
||||
korean = (t.get("korean") or "").strip()
|
||||
english = (t.get("english") or "").strip()
|
||||
if not korean:
|
||||
continue
|
||||
existing = conn.execute(
|
||||
"SELECT id FROM terms WHERE korean = ? OR (english != '' AND english = ?)",
|
||||
(korean, english),
|
||||
).fetchone()
|
||||
if existing:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
values = [t.get(f, "") or "" for f in FIELDS]
|
||||
conn.execute(
|
||||
f"INSERT INTO terms ({', '.join(FIELDS)}, created_at, updated_at) "
|
||||
f"VALUES ({', '.join('?' * len(FIELDS))}, ?, ?)",
|
||||
(*values, now, now),
|
||||
)
|
||||
inserted += 1
|
||||
|
||||
conn.commit()
|
||||
return inserted, skipped
|
||||
|
||||
|
||||
def run(seed_files=None, all_seeds=False):
|
||||
"""Import seed files into the DB.
|
||||
|
||||
seed_files: explicit list of paths (or names relative to seeds/).
|
||||
all_seeds: if True, import every *.json under seeds/.
|
||||
Returns total inserted count.
|
||||
"""
|
||||
if all_seeds:
|
||||
paths = sorted(
|
||||
os.path.join(SEEDS_DIR, f)
|
||||
for f in os.listdir(SEEDS_DIR)
|
||||
if f.endswith(".json")
|
||||
)
|
||||
else:
|
||||
paths = []
|
||||
for s in (seed_files or []):
|
||||
paths.append(s if os.path.isabs(s) or os.path.exists(s)
|
||||
else os.path.join(SEEDS_DIR, s))
|
||||
|
||||
if not paths:
|
||||
print("No seed files given. Use --all or pass file names from seeds/.")
|
||||
return 0
|
||||
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
total_in = total_skip = 0
|
||||
for path in paths:
|
||||
if not os.path.exists(path):
|
||||
print(f" ! not found: {path}")
|
||||
continue
|
||||
ins, skip = import_file(conn, path)
|
||||
print(f" {os.path.basename(path)}: +{ins} inserted, {skip} skipped")
|
||||
total_in += ins
|
||||
total_skip += skip
|
||||
conn.close()
|
||||
|
||||
print(f"\nTotal: {total_in} inserted, {total_skip} skipped")
|
||||
return total_in
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Image-verification workflow for the dental dictionary.
|
||||
|
||||
Uses a vision model via Ollama to check whether an image actually depicts
|
||||
the dental term it is mapped to. Two entry points:
|
||||
|
||||
validate_image(src, korean, english, ...) -> dict
|
||||
Validate a single image (local path or http URL) against a term.
|
||||
|
||||
verify_db(category=None, limit=None, fix=False)
|
||||
Re-check terms that already have an image_url and report mismatches.
|
||||
With fix=True, clears bad mappings (image_url -> NULL) so a later
|
||||
`add-images` pass can re-fill them.
|
||||
|
||||
The one-off per-round assignment scripts that originally lived in /tmp are
|
||||
archived under docs/archive_image_rounds/ for reference.
|
||||
Called by manage.py's `verify` subcommand.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
from config import DB_PATH, IMG_BASE, IMG_URL_PREFIX, OLLAMA_URL, VERIFY_MODEL
|
||||
|
||||
_PROMPT = (
|
||||
"치과 용어: {korean} ({english}){cat}\n"
|
||||
"{hint}"
|
||||
"이 이미지가 위 치과 용어를 설명하거나 예시하는 데 적합한가요?\n"
|
||||
"다음 기준을 엄격히 적용하세요:\n"
|
||||
"- 흐름도(flowchart), 그래프, 표, 통계 차트는 반드시 '아니오'.\n"
|
||||
"- 이미지 안에 적힌 글자·라벨·제목·캡션은 판정 근거로 삼지 마세요. "
|
||||
"오직 시각적 내용만으로 판단합니다.\n"
|
||||
"- 용어가 특정 술식·기법이면, 일반적인 장치·기구·해부 사진만으로는 '아니오'. "
|
||||
"해당 술식의 과정이나 결과가 실제로 드러나야 '예'.\n"
|
||||
"반드시 아래 형식으로만 답하세요:\n적합: 예/아니오\n이유: (한 줄)"
|
||||
)
|
||||
|
||||
|
||||
def _load_image_b64(src):
|
||||
"""Return base64 of an image given a local path or an http(s) URL."""
|
||||
if src.startswith("http://") or src.startswith("https://"):
|
||||
req = urllib.request.Request(src, headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=15) as r:
|
||||
return base64.b64encode(r.read()).decode()
|
||||
with open(src, "rb") as f:
|
||||
return base64.b64encode(f.read()).decode()
|
||||
|
||||
|
||||
def _resolve(image_url):
|
||||
"""Map a stored image_url to something _load_image_b64 can open.
|
||||
|
||||
Handles the three forms source functions produce:
|
||||
- http(s)://... -> used as-is
|
||||
- /api/files/uploads/dental_images/<rel> -> IMG_BASE/<rel>
|
||||
- dental_images/<rel> -> IMG_BASE/<rel>
|
||||
"""
|
||||
if image_url.startswith("http"):
|
||||
return image_url
|
||||
s = image_url
|
||||
if IMG_URL_PREFIX in s:
|
||||
s = s.split(IMG_URL_PREFIX, 1)[1].lstrip("/")
|
||||
elif s.startswith("dental_images/"):
|
||||
s = s[len("dental_images/"):]
|
||||
return os.path.join(IMG_BASE, s)
|
||||
|
||||
|
||||
def verify_updates(conn, updates, source="", model=VERIFY_MODEL, delay=0.0):
|
||||
"""Filter a list of (image_url, term_id) candidate mappings.
|
||||
|
||||
Keeps only those whose image the vision model judges a fit for the term.
|
||||
Used by workflow_images source functions to gate their batch writes so
|
||||
that every source verifies before saving. Returns the filtered list.
|
||||
"""
|
||||
if not updates:
|
||||
return updates
|
||||
tag = f"[{source}] " if source else ""
|
||||
print(f" {tag}verifying {len(updates)} candidate mappings "
|
||||
f"(model={model})...", flush=True)
|
||||
kept, dropped = [], 0
|
||||
for image_url, term_id in updates:
|
||||
row = conn.execute(
|
||||
"SELECT korean, english, category FROM terms WHERE id=?", (term_id,)
|
||||
).fetchone()
|
||||
if not row:
|
||||
continue
|
||||
korean, english, category = row
|
||||
res = validate_image(_resolve(image_url), korean, english, category,
|
||||
model=model)
|
||||
if res["ok"]:
|
||||
kept.append((image_url, term_id))
|
||||
else:
|
||||
dropped += 1
|
||||
print(f" ✗ {korean} — {res['reason'][:60]}", flush=True)
|
||||
if delay:
|
||||
time.sleep(delay)
|
||||
print(f" {tag}verified: {len(kept)} kept, {dropped} dropped", flush=True)
|
||||
return kept
|
||||
|
||||
|
||||
def validate_image(src, korean, english, category=None, hint=None,
|
||||
model=VERIFY_MODEL):
|
||||
"""Ask the vision model whether `src` fits the term.
|
||||
|
||||
Returns {"ok": bool, "reason": str, "raw": str}. On any error,
|
||||
ok is False and reason explains why.
|
||||
"""
|
||||
try:
|
||||
b64 = _load_image_b64(src)
|
||||
except Exception as e:
|
||||
return {"ok": False, "reason": f"load failed: {e}", "raw": ""}
|
||||
|
||||
prompt = _PROMPT.format(
|
||||
korean=korean,
|
||||
english=english or "",
|
||||
cat=f", 카테고리: {category}" if category else "",
|
||||
hint=f"{hint}\n" if hint else "",
|
||||
)
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt, "images": [b64]}],
|
||||
"stream": False,
|
||||
}
|
||||
try:
|
||||
data = json.dumps(payload).encode()
|
||||
req = urllib.request.Request(
|
||||
OLLAMA_URL, data=data, headers={"Content-Type": "application/json"}
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=60) as r:
|
||||
text = json.loads(r.read()).get("message", {}).get("content", "")
|
||||
except Exception as e:
|
||||
return {"ok": False, "reason": f"model error: {e}", "raw": ""}
|
||||
|
||||
ok = "적합: 예" in text
|
||||
reason = next(
|
||||
(l.split(":", 1)[1].strip() for l in text.splitlines() if l.startswith("이유:")),
|
||||
"",
|
||||
)
|
||||
return {"ok": ok, "reason": reason, "raw": text}
|
||||
|
||||
|
||||
def verify_db(category=None, limit=None, fix=False, delay=0.3, model=VERIFY_MODEL):
|
||||
"""Re-check terms that already have an image_url.
|
||||
|
||||
Reports each mismatch; with fix=True also clears it (image_url -> NULL).
|
||||
Returns the number of mismatches found.
|
||||
"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
sql = (
|
||||
"SELECT id, korean, english, category, image_url FROM terms "
|
||||
"WHERE image_url IS NOT NULL AND image_url != ''"
|
||||
)
|
||||
params = []
|
||||
if category:
|
||||
sql += " AND category = ?"
|
||||
params.append(category)
|
||||
sql += " ORDER BY category, id"
|
||||
if limit:
|
||||
sql += " LIMIT ?"
|
||||
params.append(limit)
|
||||
|
||||
rows = conn.execute(sql, params).fetchall()
|
||||
print(f"Verifying {len(rows)} mapped terms"
|
||||
f"{f' in {category}' if category else ''} (model={model})", flush=True)
|
||||
|
||||
bad = []
|
||||
for i, (tid, korean, english, cat, image_url) in enumerate(rows):
|
||||
res = validate_image(_resolve(image_url), korean, english, cat, model=model)
|
||||
if not res["ok"]:
|
||||
bad.append(tid)
|
||||
print(f" ✗ [{cat}] {korean} — {res['reason'][:60]}", flush=True)
|
||||
if (i + 1) % 25 == 0:
|
||||
print(f" ... {i + 1}/{len(rows)} checked, {len(bad)} bad", flush=True)
|
||||
time.sleep(delay)
|
||||
|
||||
if fix and bad:
|
||||
conn.executemany(
|
||||
"UPDATE terms SET image_url = NULL WHERE id = ?", [(t,) for t in bad]
|
||||
)
|
||||
conn.commit()
|
||||
print(f"\nCleared {len(bad)} bad mappings (image_url -> NULL)")
|
||||
else:
|
||||
print(f"\n{len(bad)} mismatches found"
|
||||
f"{' (run with --fix to clear them)' if bad else ''}")
|
||||
conn.close()
|
||||
return len(bad)
|
||||
@@ -1,51 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('.smallclaw/databases/dental_dict.db');
|
||||
|
||||
// Read the agent output file for basic dental science terms
|
||||
const fs = require('fs');
|
||||
let content = '';
|
||||
try {
|
||||
const raw = fs.readFileSync('/tmp/claude-1000/-home-kim-homeclaw/8027e1d8-f200-4835-bc15-3865e99e1ac9/tasks/abc3ed4a3adbcf430.output', 'utf8');
|
||||
const lines = raw.trim().split('\n');
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
if (obj.type === 'assistant' && obj.message?.content) {
|
||||
for (const b of obj.message.content) {
|
||||
if (b.type === 'text') content += b.text;
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
const arrayMatch = content.match(/\[[\s\S]*\]/);
|
||||
if (arrayMatch) {
|
||||
const terms = JSON.parse(arrayMatch[0]);
|
||||
const existing = db.prepare("SELECT korean FROM terms WHERE category = '기초치의학'").all();
|
||||
const existingNames = new Set(existing.map(t => t.korean));
|
||||
const newTerms = terms.filter(t => !existingNames.has(t.korean));
|
||||
console.log(`Total terms: ${terms.length}, Existing: ${existing.length}, New: ${newTerms.length}`);
|
||||
|
||||
const insert = db.prepare('INSERT INTO terms (id, korean, english, latin, abbreviation, definition, category, synonyms, related_ids, pmids, icd_code, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
let maxId = db.prepare('SELECT MAX(id) as m FROM terms').get().m;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
let inserted = 0;
|
||||
for (const t of newTerms) {
|
||||
try {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin || '', t.abbreviation || '', t.definition, '기초치의학', t.synonyms || '', '', '', t.icd_code || '', '', now, now);
|
||||
inserted++;
|
||||
} catch(e) { console.log(`Error: ${t.korean}: ${e.message}`); }
|
||||
}
|
||||
console.log(`Inserted: ${inserted}`);
|
||||
} else {
|
||||
console.log('No JSON array found');
|
||||
}
|
||||
} catch(e) {
|
||||
console.log('Error reading file:', e.message);
|
||||
}
|
||||
|
||||
db.exec("INSERT INTO terms_fts(terms_fts) VALUES('rebuild')");
|
||||
const cats = db.prepare('SELECT category, COUNT(*) as cnt FROM terms GROUP BY category ORDER BY cnt DESC').all();
|
||||
cats.forEach(c => console.log(`${c.category}: ${c.cnt}`));
|
||||
console.log('Total:', db.prepare('SELECT COUNT(*) as t FROM terms').get().t);
|
||||
db.close();
|
||||
@@ -1,54 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('.smallclaw/databases/dental_dict.db');
|
||||
|
||||
// Check existing terms for dedup
|
||||
const perioExisting = db.prepare("SELECT korean FROM terms WHERE category = '치주'").all();
|
||||
const perioNames = new Set(perioExisting.map(t => t.korean));
|
||||
|
||||
const pathExisting = db.prepare("SELECT korean FROM terms WHERE category = '구강병리'").all();
|
||||
const pathNames = new Set(pathExisting.map(t => t.korean));
|
||||
|
||||
// Periodontic terms to add
|
||||
const perioTerms = [
|
||||
{"korean":"치은열구액","english":"Gingival crevicular fluid","latin":"","abbreviation":"GCF","definition":"치은열구 내로 삼출되는 혈청 유래 액체로, 염증 매개체와 세균 대사 산물을 포함하여 치주질환의 진단 지표로 활용된다.","synonyms":"치은삼출액,열구액","icd_code":""},
|
||||
{"korean":"상피부착","english":"Epithelial attachment","latin":"","abbreviation":"","definition":"접합상피가 치아 표면의 법랑질 또는 백악질에 하프데스모좀와 기저판을 통해 부착된 구조로, 치주조직의 방어 기전에 중요하다.","synonyms":"접합상피부착","icd_code":""}
|
||||
];
|
||||
|
||||
// Oral pathology terms to add
|
||||
const pathTerms = [
|
||||
{"korean":"구강유두종","english":"Oral papilloma","latin":"Papilloma oris","abbreviation":"","definition":"구강점막에 발생하는 양성 상피성 종양으로, 인유두종바이러스 감염과 관련되어 유두状 증식을 보인다.","synonyms":"구강사마귀,점막유두종","icd_code":"D10.3"},
|
||||
{"korean":"선양성섬유성과형성","english":"Adenomatoid hyperplasia","latin":"","abbreviation":"","definition":"타액선 조직의 선양 구조를伴하는 섬유성 과형성 병변으로, 구개에 호발하며 양성 경과를 보인다.","synonyms":"","icd_code":""}
|
||||
];
|
||||
|
||||
const insert = db.prepare('INSERT INTO terms (id, korean, english, latin, abbreviation, definition, category, synonyms, related_ids, pmids, icd_code, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
let maxId = db.prepare('SELECT MAX(id) as m FROM terms').get().m;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
let perioInserted = 0;
|
||||
for (const t of perioTerms) {
|
||||
if (!perioNames.has(t.korean)) {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin, t.abbreviation, t.definition, '치주', t.synonyms, '', '', t.icd_code, '', now, now);
|
||||
perioInserted++;
|
||||
} else {
|
||||
console.log(`DUPLICATE 치주: ${t.korean}`);
|
||||
}
|
||||
}
|
||||
|
||||
let pathInserted = 0;
|
||||
for (const t of pathTerms) {
|
||||
if (!pathNames.has(t.korean)) {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin, t.abbreviation, t.definition, '구강병리', t.synonyms, '', '', t.icd_code, '', now, now);
|
||||
pathInserted++;
|
||||
} else {
|
||||
console.log(`DUPLICATE 구강병리: ${t.korean}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`치주 inserted: ${perioInserted}, 구강병리 inserted: ${pathInserted}`);
|
||||
|
||||
db.exec("INSERT INTO terms_fts(terms_fts) VALUES('rebuild')");
|
||||
|
||||
const cats = db.prepare('SELECT category, COUNT(*) as cnt FROM terms GROUP BY category ORDER BY cnt DESC').all();
|
||||
cats.forEach(c => console.log(`${c.category}: ${c.cnt}`));
|
||||
console.log('Total:', db.prepare('SELECT COUNT(*) as t FROM terms').get().t);
|
||||
db.close();
|
||||
@@ -1,32 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('.smallclaw/databases/dental_dict.db');
|
||||
|
||||
const terms = [
|
||||
{"korean":"골유착률","english":"Osseointegration rate","latin":"","abbreviation":"","definition":"임플란트 매식체와 골 사이에 골유착이 형성되는 비율로 임플란트 성공의 중요 지표이다.","synonyms":"골융합률","icd_code":""},
|
||||
{"korean":"골개조","english":"Bone remodeling","latin":"","abbreviation":"","definition":"골조직에서 파골세포에 의한 골흡수와 조골세포에 의한 골형성이 순환하여 일어나는 생리적 과정이다.","synonyms":"골리모델링","icd_code":""},
|
||||
{"korean":"골흡수지","english":"Bone resorption index","latin":"","abbreviation":"","definition":"골흡수 정도를 정량적으로 표현하는 지표로 임플란트 주위 골소실 평가에 사용된다.","synonyms":"골소실지수","icd_code":""},
|
||||
{"korean":"임플란트생존율","english":"Implant survival rate","latin":"","abbreviation":"","definition":"임플란트가 구강 내에 존재하는 비율로 기능 여부와 관계없이 계산한다.","synonyms":"생존율","icd_code":""},
|
||||
{"korean":"임플란트성공률","english":"Implant success rate","latin":"","abbreviation":"","definition":"임플란트가 정해진 성공 기준을 충족하는 비율로 골유착, 동요도, 방사선 소견 등을 종합적으로 평가한다.","synonyms":"성공률","icd_code":""},
|
||||
{"korean":"이식편","english":"Graft","latin":"","abbreviation":"","definition":"골이식이나 연조직 이식 시 공여부에서 채취하여 수혜부에 이식하는 조직편이다.","synonyms":"그래프트","icd_code":""},
|
||||
{"korean":"이식재","english":"Graft material","latin":"","abbreviation":"","definition":"골이식이나 조직이식에 사용되는 재료로 자가골, 동종골, 이종골, 합성골 등이 있다.","synonyms":"이식물질","icd_code":""}
|
||||
];
|
||||
|
||||
const existing = db.prepare("SELECT korean FROM terms WHERE category = '임플란트'").all();
|
||||
const existingNames = new Set(existing.map(t => t.korean));
|
||||
const newTerms = terms.filter(t => !existingNames.has(t.korean));
|
||||
console.log(`New terms to insert: ${newTerms.length}`);
|
||||
|
||||
const insert = db.prepare('INSERT INTO terms (id, korean, english, latin, abbreviation, definition, category, synonyms, related_ids, pmids, icd_code, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
let maxId = db.prepare('SELECT MAX(id) as m FROM terms').get().m;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const t of newTerms) {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin, t.abbreviation, t.definition, '임플란트', t.synonyms, '', '', t.icd_code, '', now, now);
|
||||
}
|
||||
|
||||
db.exec("INSERT INTO terms_fts(terms_fts) VALUES('rebuild')");
|
||||
|
||||
const cats = db.prepare('SELECT category, COUNT(*) as cnt FROM terms GROUP BY category ORDER BY cnt DESC').all();
|
||||
cats.forEach(c => console.log(`${c.category}: ${c.cnt}`));
|
||||
console.log('Total:', db.prepare('SELECT COUNT(*) as t FROM terms').get().t);
|
||||
db.close();
|
||||
@@ -1,132 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('.smallclaw/databases/dental_dict.db');
|
||||
|
||||
const terms = [
|
||||
{"korean":"네비게이션수술","english":"Navigation surgery","latin":"","abbreviation":"","definition":"컴퓨터 시스템을 이용하여 임플란트 식립 위치와 방향을 실시간으로 안내하는 수술 기법이다.","synonyms":"항법수술,컴퓨터네비게이션수술","icd_code":""},
|
||||
{"korean":"가이드수술","english":"Guided surgery","latin":"","abbreviation":"","definition":"수술 가이드 템플릿을 이용하여 임플란트 식립 위치를 미리 계획하고 정확하게 식립하는 수술 기법이다.","synonyms":"가이드식립술","icd_code":""},
|
||||
{"korean":"플랩리스수술","english":"Flapless surgery","latin":"","abbreviation":"","definition":"점막절개를 하지 않고 임플란트를 식립하는 최소침습 수술 기법이다.","synonyms":"무판막수술","icd_code":""},
|
||||
{"korean":"골확장술","english":"Bone expansion technique","latin":"","abbreviation":"","definition":"치조골의 폭경이 부족한 경우 골을 압축하여 확장하는 수술 기법이다.","synonyms":"골팽창술","icd_code":""},
|
||||
{"korean":"치조골분할술","english":"Ridge splitting technique","latin":"","abbreviation":"","definition":"치조골의 폭경이 부족한 경우 골을 수직으로 분할하여 임플란트 식립 공간을 확보하는 수술 기법이다.","synonyms":"치조제분할술,골분할술","icd_code":""},
|
||||
{"korean":"측벽접근술","english":"Lateral window approach","latin":"","abbreviation":"","definition":"상악동 측벽에 창을 만들어 상악동 점막을 거상하고 골이식을 시행하는 수술 기법이다.","synonyms":"측벽천공술,측방접근술","icd_code":""},
|
||||
{"korean":"내거상술","english":"Crestal sinus lift","latin":"","abbreviation":"","definition":"치조정을 통해 상악동 점막을 거상하는 최소침습적 상악동 거상 기법이다.","synonyms":"치조정접근법,내측상악동거상술","icd_code":""},
|
||||
{"korean":"상악동점막천공","english":"Sinus membrane perforation","latin":"","abbreviation":"","definition":"상악동 거상술 중 슈나이더란막이 천공되는 합병증이다.","synonyms":"슈나이더란막천공,상악동막천공","icd_code":""},
|
||||
{"korean":"즉시발치와폐쇄","english":"Immediate socket closure","latin":"","abbreviation":"","definition":"발치 즉시 발치와를 봉합하여 폐쇄하는 기법이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"발치와보존","english":"Socket preservation","latin":"","abbreviation":"","definition":"발치 후 골이식재를 충전하여 치조골의 흡수를 방지하는 기법이다.","synonyms":"발치와보존술","icd_code":""},
|
||||
{"korean":"발치와유지","english":"Socket maintenance","latin":"","abbreviation":"","definition":"발치와의 형태와 골량을 유지하기 위한 처치이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"자가골","english":"Autogenous bone","latin":"Os autologum","abbreviation":"","definition":"환자 자신의 다른 부위에서 채취하여 이식하는 골이다.","synonyms":"자가골이식재","icd_code":""},
|
||||
{"korean":"동종골","english":"Allograft bone","latin":"","abbreviation":"","definition":"동종의 다른 개체에서 채취하여 처리한 골이식재이다.","synonyms":"동종골이식재","icd_code":""},
|
||||
{"korean":"이종골","english":"Xenograft bone","latin":"","abbreviation":"","definition":"다른 종류의 동물에서 채취하여 처리한 골이식재로 주로 우골이나 돼지골을 사용한다.","synonyms":"이종골이식재","icd_code":""},
|
||||
{"korean":"합성골","english":"Alloplastic bone graft material","latin":"","abbreviation":"","definition":"합성물질로 제조된 골이식재로 하이드록시아파타이트, 트리칼슘포스페이트 등이 있다.","synonyms":"합성골이식재,인공골","icd_code":""},
|
||||
{"korean":"골이식재","english":"Bone graft material","latin":"","abbreviation":"","definition":"골결손부를 충전하거나 골재생을 유도하기 위해 사용하는 재료이다.","synonyms":"골이식물","icd_code":""},
|
||||
{"korean":"차폐막","english":"Barrier membrane","latin":"","abbreviation":"","definition":"골유도재생술에서 빠르게 증식하는 결합조직의 침투를 차단하여 골재생을 유도하는 막이다.","synonyms":"차단막,멤브레인","icd_code":""},
|
||||
{"korean":"흡수성차폐막","english":"Resorbable membrane","latin":"","abbreviation":"","definition":"체내에서 흡수되는 차폐막으로 콜라겐 막 등이 있다.","synonyms":"흡수성차단막","icd_code":""},
|
||||
{"korean":"비흡수성차폐막","english":"Non-resorbable membrane","latin":"","abbreviation":"","definition":"체내에서 흡수되지 않아 이차 수술로 제거해야 하는 차폐막으로 ePTFE 막 등이 있다.","synonyms":"비흡수성차단막","icd_code":""},
|
||||
{"korean":"골형성단백질","english":"Bone morphogenetic protein","latin":"","abbreviation":"BMP","definition":"골형성을 유도하는 단백질로 골재생을 촉진하는 생리활성물질이다.","synonyms":"골형태형성단백질","icd_code":""},
|
||||
{"korean":"혈소판풍부혈장","english":"Platelet-rich plasma","latin":"","abbreviation":"PRP","definition":"원심분리하여 혈소판을 농축한 혈장으로 성장인자가 풍부하여 골재생을 촉진한다.","synonyms":"농축혈소판혈장","icd_code":""},
|
||||
{"korean":"혈소판풍부섬유소","english":"Platelet-rich fibrin","latin":"","abbreviation":"PRF","definition":"원심분리하여 혈소판과 섬유소를 농축한 제제로 성장인자를 서서히 방출하여 조직 재생을 촉진한다.","synonyms":"농축혈소판섬유소","icd_code":""},
|
||||
{"korean":"농축성장인자","english":"Concentrated growth factor","latin":"","abbreviation":"CGF","definition":"혈액을 원심분리하여 얻은 농축된 성장인자 제제로 조직 재생을 촉진한다.","synonyms":"","icd_code":""},
|
||||
{"korean":"골대체재","english":"Bone substitute","latin":"","abbreviation":"","definition":"자가골을 대체하여 사용하는 골이식 재료로 동종골, 이종골, 합성골 등이 있다.","synonyms":"골대체물","icd_code":""},
|
||||
{"korean":"어버트먼트","english":"Abutment","latin":"","abbreviation":"","definition":"임플란트 매식체와 보철물 사이를 연결하는 구조물이다.","synonyms":"지대주","icd_code":""},
|
||||
{"korean":"힐링어버트먼트","english":"Healing abutment","latin":"","abbreviation":"","definition":"임플란트 식립 후 치은 치유를 유도하기 위해 매식체 상부에 임시로 연결하는 부품이다.","synonyms":"치유지대주,힐링캡","icd_code":""},
|
||||
{"korean":"커버스크루","english":"Cover screw","latin":"","abbreviation":"","definition":"1차 수술 시 매식체 상부 구멍을 덮어 골과 점막의 침투를 방지하는 나사이다.","synonyms":"폐쇄나사,커버캡","icd_code":""},
|
||||
{"korean":"인상코핑","english":"Impression coping","latin":"","abbreviation":"","definition":"임플란트 인상 채득 시 매식체의 위치와 방향을 정확하게 모형에 전달하기 위해 사용하는 부품이다.","synonyms":"인상체,트랜스퍼피스","icd_code":""},
|
||||
{"korean":"아날로그","english":"Implant analog","latin":"","abbreviation":"","definition":"구강 내 임플란트 매식체의 위치를 작업 모형에서 재현하기 위해 사용하는 금속 복제품이다.","synonyms":"매식체복제물","icd_code":""},
|
||||
{"korean":"랩어버트먼트","english":"Lab abutment","latin":"","abbreviation":"","definition":"기공소에서 보철물 제작을 위해 사용하는 어버트먼트이다.","synonyms":"기공소어버트먼트","icd_code":""},
|
||||
{"korean":"임시어버트먼트","english":"Temporary abutment","latin":"","abbreviation":"","definition":"임시 보철물 제작을 위해 사용하는 어버트먼트이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"스캔어버트먼트","english":"Scan abutment","latin":"","abbreviation":"","definition":"구강 스캐너로 디지털 인상을 채득할 때 임플란트 위치를 인식하기 위해 연결하는 어버트먼트이다.","synonyms":"스캔바디","icd_code":""},
|
||||
{"korean":"커스텀어버트먼트","english":"Custom abutment","latin":"","abbreviation":"","definition":"환자의 임플란트 위치와 치은 형태에 맞게 개별적으로 제작한 어버트먼트이다.","synonyms":"개별어버트먼트,맞춤형어버트먼트","icd_code":""},
|
||||
{"korean":"모식어버트먼트","english":"Stock abutment","latin":"","abbreviation":"","definition":"임플란트 제조사에서 미리 제작하여 제공하는 규격화된 어버트먼트이다.","synonyms":"표준어버트먼트","icd_code":""},
|
||||
{"korean":"모놀리식어버트먼트","english":"Monolithic abutment","latin":"","abbreviation":"","definition":"어버트먼트와 보철물이 일체형으로 제작된 구조물이다.","synonyms":"일체형어버트먼트","icd_code":""},
|
||||
{"korean":"앵글어버트먼트","english":"Angled abutment","latin":"","abbreviation":"","definition":"매식체와 보철물 사이에 각도를 주어 연결하는 어버트먼트로 경사 식립 시 사용한다.","synonyms":"각도어버트먼트","icd_code":""},
|
||||
{"korean":"유니버설어버트먼트","english":"Universal abutment","latin":"","abbreviation":"","definition":"다양한 임플란트 시스템에 호환되도록 설계된 어버트먼트이다.","synonyms":"범용어버트먼트","icd_code":""},
|
||||
{"korean":"표면처리","english":"Surface treatment","latin":"","abbreviation":"","definition":"임플란트 매식체 표면의 골유착을 향상시키기 위해 물리적 또는 화학적 가공을 하는 과정이다.","synonyms":"표면가공","icd_code":""},
|
||||
{"korean":"표면개조","english":"Surface modification","latin":"","abbreviation":"","definition":"임플란트 표면의 화학적, 물리적 특성을 변경하여 생체반응을 개선하는 처치이다.","synonyms":"표면변성","icd_code":""},
|
||||
{"korean":"샌드블라스팅","english":"Sandblasting","latin":"","abbreviation":"","definition":"고압의 마모 입자를 임플란트 표면에 분사하여 표면 거칠기를 증가시키는 표면 처리 기법이다.","synonyms":"모래분사,그리트블라스팅","icd_code":""},
|
||||
{"korean":"산부식","english":"Acid etching","latin":"","abbreviation":"","definition":"산 용액으로 임플란트 표면을 부식시켜 미세 구조를 형성하는 표면 처리 기법이다.","synonyms":"산에칭","icd_code":""},
|
||||
{"korean":"SLA표면","english":"SLA surface","latin":"","abbreviation":"SLA","definition":"샌드블라스팅과 산부식을 결합한 임플란트 표면 처리 기법으로 골유착을 향상시킨다.","synonyms":"SLA표면처리","icd_code":""},
|
||||
{"korean":"하이드록시아파타이트코팅","english":"Hydroxyapatite coating","latin":"","abbreviation":"HA coating","definition":"임플란트 표면에 하이드록시아파타이트를 코팅하여 골전도성을 향상시키는 표면 처리 기법이다.","synonyms":"HA코팅,수산화인회석코팅","icd_code":""},
|
||||
{"korean":"표면거칠기","english":"Surface roughness","latin":"","abbreviation":"Ra","definition":"임플란트 매식체 표면의 미세한 거칠기 정도로 골유착에 영향을 미치는 중요한 인자이다.","synonyms":"표면조도","icd_code":""},
|
||||
{"korean":"나노표면처리","english":"Nanosurface treatment","latin":"","abbreviation":"","definition":"나노미터 단위의 미세 구조를 임플란트 표면에 형성하는 표면 처리 기법이다.","synonyms":"나노표면가공","icd_code":""},
|
||||
{"korean":"티타늄","english":"Titanium","latin":"","abbreviation":"Ti","definition":"임플란트 매식체의 주 재료로 사용되는 생체적합성 금속 원소이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"티타늄합금","english":"Titanium alloy","latin":"","abbreviation":"","definition":"티타늄에 알루미늄, 바나듐 등을 합금하여 강도를 높인 임플란트 재료이다.","synonyms":"Ti합금","icd_code":""},
|
||||
{"korean":"지르코니아","english":"Zirconia","latin":"","abbreviation":"","definition":"심미성이 뛰어난 세라믹 재료로 임플란트 매식체 및 어버트먼트에 사용된다.","synonyms":"산화지르코늄","icd_code":""},
|
||||
{"korean":"생체적합성","english":"Biocompatibility","latin":"","abbreviation":"","definition":"생체 내에서 독성이나 거부반응 없이 조직과 조화롭게 공존하는 재료의 성질이다.","synonyms":"생체친화성","icd_code":""},
|
||||
{"korean":"생체재료","english":"Biomaterial","latin":"","abbreviation":"","definition":"의료 목적으로 생체 내에 이식하거나 접촉하는 물질로 임플란트 재료 등이 포함된다.","synonyms":"생체물질","icd_code":""},
|
||||
{"korean":"생체활성","english":"Bioactivity","latin":"","abbreviation":"","definition":"생체 재료가 주변 조직과 화학적 반응을 일으켜 결합을 형성하는 능력이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"구강스캐너","english":"Intraoral scanner","latin":"","abbreviation":"","definition":"구강 내 치아와 점막의 형태를 디지털로 스캔하여 3차원 모델을 생성하는 장치이다.","synonyms":"구내스캐너","icd_code":""},
|
||||
{"korean":"디지털인상","english":"Digital impression","latin":"","abbreviation":"","definition":"구강 스캐너를 이용하여 인상을 디지털 방식으로 채득하는 기법이다.","synonyms":"디지털인상채득","icd_code":""},
|
||||
{"korean":"원추형단층촬영","english":"Cone beam computed tomography","latin":"","abbreviation":"CBCT","definition":"원추형 X선 빔으로 구강 부위의 3차원 단층 영상을 촬영하는 장비로 임플란트 계획에 필수적이다.","synonyms":"CBCT,치과용CT","icd_code":""},
|
||||
{"korean":"수술가이드템플릿","english":"Surgical guide template","latin":"","abbreviation":"","definition":"3차원 계획에 기반하여 임플란트 식립 위치와 방향을 임상에서 정확히 재현하기 위한 수술 보조 기구이다.","synonyms":"수술가이드,수술템플릿","icd_code":""},
|
||||
{"korean":"3D임플란트계획","english":"3D implant planning","latin":"","abbreviation":"","definition":"CT 영상과 디지털 모델을 이용하여 3차원으로 임플란트 식립 위치를 계획하는 과정이다.","synonyms":"3차원임플란트계획","icd_code":""},
|
||||
{"korean":"방사선가이드","english":"Radiographic guide","latin":"","abbreviation":"","definition":"CT 촬영 시 환자 구강 내 장착하여 기존 치아 위치와 임플란트 계획 위치를 영상에서 확인할 수 있게 하는 장치이다.","synonyms":"방사선규격가이드","icd_code":""},
|
||||
{"korean":"오버덴처","english":"Overdenture","latin":"","abbreviation":"","definition":"자연치나 임플란트 위에 지지되는 가철성 보철물로 구개판을 덮는 형태의 의치이다.","synonyms":"상부의치","icd_code":""},
|
||||
{"korean":"임플란트오버덴처","english":"Implant overdenture","latin":"","abbreviation":"","definition":"임플란트에 의해 지지 및 유지되는 가철성 보철물이다.","synonyms":"임플란트지지가철성의치","icd_code":""},
|
||||
{"korean":"임플란트지지단일관","english":"Implant-supported single crown","latin":"","abbreviation":"","definition":"하나의 임플란트에 의해 지지되는 단일 치관 보철물이다.","synonyms":"임플란트단일관","icd_code":""},
|
||||
{"korean":"보철물적합","english":"Prosthesis fit","latin":"","abbreviation":"","definition":"보철물이 임플란트나 어버트먼트에 정확하게 맞물리는 정도이다.","synonyms":"보철물적합도","icd_code":""},
|
||||
{"korean":"수동적적합","english":"Passive fit","latin":"","abbreviation":"","definition":"보철물이 임플란트에 나사로 체결될 때 응력이 발생하지 않고 자연스럽게 적합되는 상태이다.","synonyms":"수동적적합도","icd_code":""},
|
||||
{"korean":"임시보철물","english":"Temporary prosthesis","latin":"","abbreviation":"","definition":"최종 보철물 제작 전까지 임시로 장착하는 보철물로 치유 기간 동안 기능과 심미를 유지한다.","synonyms":"가보철물","icd_code":""},
|
||||
{"korean":"임시관","english":"Temporary crown","latin":"","abbreviation":"","definition":"임플란트 치유 기간 동안 임시로 장착하는 치관으로 연조직 형성과 심미를 유지한다.","synonyms":"프로비저널크라운","icd_code":""},
|
||||
{"korean":"프로비저널","english":"Provisional restoration","latin":"","abbreviation":"","definition":"최종 보철물 장착 전까지 사용하는 임시 수복물로 치은 형태 유도와 교합 안정을 돕는다.","synonyms":"임시수복물","icd_code":""},
|
||||
{"korean":"마스터모형","english":"Master cast","latin":"","abbreviation":"","definition":"임플란트 인상을 통해 제작한 작업 모형으로 보철물 제작의 기준이 된다.","synonyms":"주모형,작업모형","icd_code":""},
|
||||
{"korean":"응력분산","english":"Stress distribution","latin":"","abbreviation":"","definition":"임플란트에 가해지는 하중이 주변 골에 고르게 분산되는 현상이다.","synonyms":"하중분산","icd_code":""},
|
||||
{"korean":"하중분산","english":"Load distribution","latin":"","abbreviation":"","definition":"저작력 등의 하중이 임플란트와 주변 골에 고르게 분산되는 것을 말한다.","synonyms":"력분산","icd_code":""},
|
||||
{"korean":"생체역학","english":"Biomechanics","latin":"","abbreviation":"","definition":"생체 조직과 임플란트에 작용하는 역학적 원리를 연구하는 학문이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"교합력","english":"Occlusal force","latin":"","abbreviation":"","definition":"상하 치아가 교합할 때 발생하는 힘으로 임플란트 하중 설계의 중요한 인자이다.","synonyms":"저작력","icd_code":""},
|
||||
{"korean":"교합접촉","english":"Occlusal contact","latin":"","abbreviation":"","definition":"상하 치아가 교합할 때 접촉하는 부위로 임플란트 보철물 설계에 중요하다.","synonyms":"","icd_code":""},
|
||||
{"korean":"측방력","english":"Lateral force","latin":"","abbreviation":"","definition":"임플란트 장축에 수직 방향으로 작용하는 힘으로 과도한 측방력은 임플란트 실패의 원인이 된다.","synonyms":"비축방력","icd_code":""},
|
||||
{"korean":"축방하중","english":"Axial loading","latin":"","abbreviation":"","definition":"임플란트 장축 방향을 따라 작용하는 하중으로 임플란트에 가장 유리한 하중 방향이다.","synonyms":"축방력","icd_code":""},
|
||||
{"korean":"연조직이식","english":"Soft tissue graft","latin":"","abbreviation":"","definition":"임플란트 주위 치은을 증가시키거나 개선하기 위해 연조직을 이식하는 술식이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"결합조직이식","english":"Connective tissue graft","latin":"","abbreviation":"CTG","definition":"구개부에서 채취한 결합조직을 이식하여 치은 두께와 각화은를 증가시키는 술식이다.","synonyms":"CTG","icd_code":""},
|
||||
{"korean":"상피하결합조직이식","english":"Subepithelial connective tissue graft","latin":"","abbreviation":"SCTG","definition":"구개부에서 상피를 제거한 결합조직을 치은 밑에 이식하는 술식으로 임플란트 주위 연조직 증강에 널리 사용된다.","synonyms":"SCTG","icd_code":""},
|
||||
{"korean":"치은연증가술","english":"Gingival augmentation","latin":"","abbreviation":"","definition":"치은연의 두께나 폭경을 증가시키는 수술로 임플란트 주위 연조직 개선에 활용된다.","synonyms":"치은증대술","icd_code":""},
|
||||
{"korean":"각화은이식","english":"Keratinized gingiva graft","latin":"","abbreviation":"","definition":"각화은을 이식하여 임플란트 주위 각화은 폭을 증가시키는 술식이다.","synonyms":"각화은증대술","icd_code":""},
|
||||
{"korean":"연조직관리","english":"Soft tissue management","latin":"","abbreviation":"","definition":"임플란트 주위 연조직의 형태, 두께, 건강을 유지하고 개선하는 처치이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"골재건","english":"Bone reconstruction","latin":"","abbreviation":"","definition":"골결손부를 이식재나 재생술식으로 복원하는 과정이다.","synonyms":"골복원","icd_code":""},
|
||||
{"korean":"골흡수","english":"Bone resorption","latin":"","abbreviation":"","definition":"파골세포에 의해 골조직이 분해되어 소실되는 현상이다.","synonyms":"골흡수현상","icd_code":""},
|
||||
{"korean":"파골세포","english":"Osteoclast","latin":"Osteoclastus","abbreviation":"","definition":"골조직을 흡수 분해하는 거대 다핵세포로 골흡수에 관여한다.","synonyms":"골파세포","icd_code":""},
|
||||
{"korean":"조골세포","english":"Osteoblast","latin":"Osteoblastus","abbreviation":"","definition":"새로운 골기질을 합성하고 분비하는 세포로 골형성에 관여한다.","synonyms":"골아세포","icd_code":""},
|
||||
{"korean":"골세포","english":"Osteocyte","latin":"Osteocytus","abbreviation":"","definition":"골기질 내에 매립된 세포로 골조직의 유지와 대사 조절에 관여한다.","synonyms":"골세포","icd_code":""},
|
||||
{"korean":"이차안정성","english":"Secondary stability","latin":"","abbreviation":"","definition":"임플란트 식립 후 골유착이 진행되면서 얻어지는 안정성으로 골개조에 의해 형성된다.","synonyms":"2차안정성","icd_code":""},
|
||||
{"korean":"치조골위축","english":"Alveolar bone atrophy","latin":"","abbreviation":"","definition":"치아 상실 후 치조골이 흡수되어 폭경과 높이가 감소하는 현상이다.","synonyms":"치조골흡수","icd_code":""},
|
||||
{"korean":"골융합부전","english":"Osseointegration failure","latin":"","abbreviation":"","definition":"임플란트 매식체와 골 사이에 융합이 형성되지 않는 상태이다.","synonyms":"골유착부전","icd_code":""},
|
||||
{"korean":"연조직감염","english":"Soft tissue infection","latin":"","abbreviation":"","definition":"임플란트 주위 연조직에 발생하는 세균 감염으로 부종, 발적, 동통을 동반한다.","synonyms":"","icd_code":""},
|
||||
{"korean":"신경손상","english":"Nerve injury","latin":"","abbreviation":"","definition":"임플란트 수술 중 하치조신경이나 하악신경 등이 손상되어 감각 이상이나 마비를 유발하는 합병증이다.","synonyms":"신경장애","icd_code":""},
|
||||
{"korean":"임플란트탈락","english":"Implant loss","latin":"","abbreviation":"","definition":"골유착이 실패하거나 임플란트주위염 등으로 인해 임플란트가 제거되는 상태이다.","synonyms":"임플란트상실","icd_code":""},
|
||||
{"korean":"생역학적폭","english":"Biologic width","latin":"","abbreviation":"","definition":"치은 열구 상피와 결합조직 부착이 차지하는 공간으로 임플란트 주위에도 존재하며 보철물 마진 설정에 중요하다.","synonyms":"생물학적폭경","icd_code":""},
|
||||
{"korean":"항생제예방요법","english":"Antibiotic prophylaxis","latin":"","abbreviation":"","definition":"임플란트 수술 전후 감염 예방을 위해 항생제를 예방적으로 투여하는 처치이다.","synonyms":"항생제예방투여","icd_code":""},
|
||||
{"korean":"임플란트유지관리","english":"Implant maintenance","latin":"","abbreviation":"","definition":"임플란트 식립 후 장기적인 성공을 위해 정기적으로 실시하는 청결 관리와 검진이다.","synonyms":"임플란트유지보존","icd_code":""},
|
||||
{"korean":"임플란트표면세정","english":"Implant surface decontamination","latin":"","abbreviation":"","definition":"임플란트주위염 치료 시 매식체 표면의 세균과 독소를 제거하는 처치이다.","synonyms":"임플란트표면소독","icd_code":""}
|
||||
];
|
||||
|
||||
// Deduplicate against existing terms
|
||||
const existing = db.prepare("SELECT korean FROM terms WHERE category = '임플란트'").all();
|
||||
const existingNames = new Set(existing.map(t => t.korean));
|
||||
const newTerms = terms.filter(t => !existingNames.has(t.korean));
|
||||
|
||||
console.log(`Total terms generated: ${terms.length}`);
|
||||
console.log(`Existing 임플란트 terms: ${existing.length}`);
|
||||
console.log(`New terms after dedup: ${newTerms.length}`);
|
||||
|
||||
// Insert new terms
|
||||
const insert = db.prepare('INSERT INTO terms (id, korean, english, latin, abbreviation, definition, category, synonyms, related_ids, pmids, icd_code, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
let maxId = db.prepare('SELECT MAX(id) as m FROM terms').get().m;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
let inserted = 0;
|
||||
for (const t of newTerms) {
|
||||
try {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin, t.abbreviation, t.definition, '임플란트', t.synonyms, '', '', t.icd_code, '', now, now);
|
||||
inserted++;
|
||||
} catch(e) {
|
||||
console.log(`Error inserting ${t.korean}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild FTS
|
||||
db.exec("INSERT INTO terms_fts(terms_fts) VALUES('rebuild')");
|
||||
|
||||
// Show final counts
|
||||
const cats = db.prepare('SELECT category, COUNT(*) as cnt FROM terms GROUP BY category ORDER BY cnt DESC').all();
|
||||
cats.forEach(c => console.log(`${c.category}: ${c.cnt}`));
|
||||
console.log('Total:', db.prepare('SELECT COUNT(*) as t FROM terms').get().t);
|
||||
db.close();
|
||||
@@ -1,50 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('.smallclaw/databases/dental_dict.db');
|
||||
const fs = require('fs');
|
||||
|
||||
let content = '';
|
||||
try {
|
||||
const raw = fs.readFileSync('/tmp/claude-1000/-home-kim-homeclaw/8027e1d8-f200-4835-bc15-3865e99e1ac9/tasks/a1f7b0aa8e5ca4261.output', 'utf8');
|
||||
const lines = raw.trim().split('\n');
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const obj = JSON.parse(line);
|
||||
if (obj.type === 'assistant' && obj.message?.content) {
|
||||
for (const b of obj.message.content) {
|
||||
if (b.type === 'text') content += b.text;
|
||||
}
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
const arrayMatch = content.match(/\[[\s\S]*\]/);
|
||||
if (arrayMatch) {
|
||||
const terms = JSON.parse(arrayMatch[0]);
|
||||
const existing = db.prepare("SELECT korean FROM terms WHERE category = '구강내과'").all();
|
||||
const existingNames = new Set(existing.map(t => t.korean));
|
||||
const newTerms = terms.filter(t => !existingNames.has(t.korean));
|
||||
console.log(`Total terms: ${terms.length}, Existing: ${existing.length}, New: ${newTerms.length}`);
|
||||
|
||||
const insert = db.prepare('INSERT INTO terms (id, korean, english, latin, abbreviation, definition, category, synonyms, related_ids, pmids, icd_code, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
let maxId = db.prepare('SELECT MAX(id) as m FROM terms').get().m;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
let inserted = 0;
|
||||
for (const t of newTerms) {
|
||||
try {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin || '', t.abbreviation || '', t.definition, '구강내과', t.synonyms || '', '', '', t.icd_code || '', '', now, now);
|
||||
inserted++;
|
||||
} catch(e) { console.log(`Error: ${t.korean}: ${e.message}`); }
|
||||
}
|
||||
console.log(`Inserted: ${inserted}`);
|
||||
} else {
|
||||
console.log('No JSON array found');
|
||||
}
|
||||
} catch(e) {
|
||||
console.log('Error reading file:', e.message);
|
||||
}
|
||||
|
||||
db.exec("INSERT INTO terms_fts(terms_fts) VALUES('rebuild')");
|
||||
const cats = db.prepare('SELECT category, COUNT(*) as cnt FROM terms GROUP BY category ORDER BY cnt DESC').all();
|
||||
cats.forEach(c => console.log(`${c.category}: ${c.cnt}`));
|
||||
console.log('Total:', db.prepare('SELECT COUNT(*) as t FROM terms').get().t);
|
||||
db.close();
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('.smallclaw/databases/dental_dict.db');
|
||||
|
||||
// 소아치과 terms - extracted from agent output
|
||||
const terms = [
|
||||
{"korean":"행동조절","english":"Behavior management","latin":"","abbreviation":"","definition":"소아치과 진료 시 환아의 협조도를 높이고 불안을 감소시키기 위해 적용하는 행동학적 접근법이다.","synonyms":"행동관리","icd_code":""},
|
||||
{"korean":"행동유도","english":"Behavior guidance","latin":"","abbreviation":"","definition":"소아 환자의 긍정적인 치과 태도를 형성하기 위해 사용하는 체계적인 행동조절 기법이다.","synonyms":"행동통제,행동지도","icd_code":""},
|
||||
{"korean":"치과불안","english":"Dental anxiety","latin":"","abbreviation":"","definition":"치과 진료와 관련하여 느끼는 불안감으로 소아 환자의 협조도 저하의 주요 원인이다.","synonyms":"치과공포,치과두려움","icd_code":"Z71.2"},
|
||||
{"korean":"치과공포증","english":"Dental phobia","latin":"","abbreviation":"","definition":"치과 진료에 대해 비합리적이고 극심한 공포를 느끼는 상태로 일반적인 행동유도만으로는 극복하기 어렵다.","synonyms":"치과공포장애","icd_code":"F40.2"},
|
||||
{"korean":"신체적억제","english":"Physical restraint","latin":"","abbreviation":"","definition":"소아 환자의 안전을 확보하고 진료를 수행하기 위해 신체적 움직임을 제한하는 조치이다.","synonyms":"물리적억제,신체고정","icd_code":""},
|
||||
{"korean":"체위조절","english":"Postural management","latin":"","abbreviation":"","definition":"소아 환자의 체위를 조절하여 구강 내 시야 확보와 행동조절을 돕는 방법이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"전신마취","english":"General anesthesia","latin":"","abbreviation":"GA","definition":"매우 비협조적인 소아 환자나 특수요구 아동의 치과 치료를 위해 의식을 잃게 하는 마취 방법이다.","synonyms":"전신마취술","icd_code":""},
|
||||
{"korean":"의식하진정","english":"Conscious sedation","latin":"","abbreviation":"","definition":"환아의 의식과 보호성 반사는 유지하면서 불안과 통증을 감소시키는 진정 방법이다.","synonyms":"각성진정","icd_code":""},
|
||||
{"korean":"산화질소흡입요법","english":"Nitrous oxide inhalation sedation","latin":"","abbreviation":"","definition":"산화질소와 산소 혼합 가스를 흡입시켜 경미한 진정과 불안 완화를 유도하는 약물적 행동조절 기법이다.","synonyms":"웃음가스,스케어가스","icd_code":""},
|
||||
{"korean":"심부진정","english":"Deep sedation","latin":"","abbreviation":"","definition":"의식이 억제되어 통증 자극에 반응할 수 있으나 자발적 환기는 유지하기 어려운 진정 상태이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"정맥진정","english":"Intravenous sedation","latin":"","abbreviation":"IV sedation","definition":"정맥 경로로 진정제를 투여하여 소아 환자의 불안을 완화하고 협조도를 높이는 방법이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"경구진정","english":"Oral sedation","latin":"","abbreviation":"","definition":"경구로 진정제를 투여하여 소아 환자의 불안과 공포를 감소시키는 약물적 행동조절법이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"두개안면성장","english":"Craniofacial growth","latin":"","abbreviation":"","definition":"두개골과 안면부의 성장 발육 과정으로 소아치과 및 교정 진료의 핵심 지식이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"악골성장","english":"Jaw growth","latin":"","abbreviation":"","definition":"상악골과 하악골의 성장으로 치아 맹출과 교합 형성에 필수적이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"골령","english":"Bone age","latin":"","abbreviation":"","definition":"방사선학적 소견을 바탕으로 평가하는 골격의 성숙도로 성장 발육 평가에 사용된다.","synonyms":"골연령","icd_code":""},
|
||||
{"korean":"치령","english":"Dental age","latin":"","abbreviation":"","definition":"치아의 맹출 상태와 형성 정도를 기준으로 평가하는 치아적 성숙도이다.","synonyms":"치아연령","icd_code":""},
|
||||
{"korean":"성장곡선","english":"Growth curve","latin":"","abbreviation":"","definition":"소아의 신체 및 두개안면 성장을 연령별로 도식화한 곡선으로 정상 발육 범위를 평가한다.","synonyms":"","icd_code":""},
|
||||
{"korean":"성장발육이상","english":"Developmental disorder","latin":"","abbreviation":"","definition":"소아의 정상적인 성장 발육 과정에서 벗어난 이상으로 두개안면 기형이나 치아 발육 장애를 포함한다.","synonyms":"발달장애","icd_code":""},
|
||||
{"korean":"하악성장","english":"Mandibular growth","latin":"","abbreviation":"","definition":"하악골의 성장으로 주로 후방 및 하방으로 이루어지며 교합 발육에 영향을 미친다.","synonyms":"","icd_code":""},
|
||||
{"korean":"상악성장","english":"Maxillary growth","latin":"","abbreviation":"","definition":"상악골의 성장으로 주하방 및 전방 이동과 봉합부 성장에 의해 이루어진다.","synonyms":"","icd_code":""},
|
||||
{"korean":"신생아치","english":"Natal teeth","latin":"dentes natales","abbreviation":"","definition":"출생 시 이미 구강 내에 존재하는 치아로 주로 하악 전치부에 발생한다.","synonyms":"출생치","icd_code":"K00.4"},
|
||||
{"korean":"신생아출생치","english":"Neonatal teeth","latin":"dentes neonatales","abbreviation":"","definition":"출생 후 30일 이내에 맹출하는 치아로 신생아치보다 늦게 나타난다.","synonyms":"","icd_code":"K00.4"},
|
||||
{"korean":"맹출낭종","english":"Eruption cyst","latin":"cystis eruptionis","abbreviation":"","definition":"치아 맹출 직전에 치은에 형성되는 낭종으로 혈액이 차 있으면 맹출혈종이라 한다.","synonyms":"맹출혈종","icd_code":"K09.0"},
|
||||
{"korean":"함낭종","english":"Dentigerous cyst","latin":"cystis dentigera","abbreviation":"","definition":"미맹출 치아의 치관부에 부착하여 발생하는 낭종으로 소아에서 과잉치와 연관될 수 있다.","synonyms":"치아포함낭종,여과낭종","icd_code":"K09.0"},
|
||||
{"korean":"점액낭종","english":"Mucocele","latin":"","abbreviation":"","definition":"구강 내 소타액선의 배출관이 손상되어 형성되는 점액류낭종으로 하순에 호발한다.","synonyms":"점액류,뮤코셀","icd_code":"K11.6"},
|
||||
{"korean":"설하낭종","english":"Ranula","latin":"","abbreviation":"","definition":"설하선 또는 악하선의 배출관 손상으로 인해 구강저에 형성되는 점막낭종의 일종이다.","synonyms":"설하점액류","icd_code":"K11.6"},
|
||||
{"korean":"소아아구창","english":"Pediatric oral candidiasis","latin":"candidiasis oralis paediatrica","abbreviation":"","definition":"Candida 균에 의한 소아 구강 진균 감염으로 영유아에서 위막형 백반으로 나타난다.","synonyms":"소아구강캔디다증","icd_code":"B37.0"},
|
||||
{"korean":"소아구강헤르페스","english":"Pediatric oral herpes","latin":"","abbreviation":"","definition":"단순포진 바이러스에 의한 소아 구강 내 감염으로 초기 감염 시 전신 증상을 동반할 수 있다.","synonyms":"소아구순포진","icd_code":"B00.1"},
|
||||
{"korean":"수족구병","english":"Hand-foot-mouth disease","latin":"","abbreviation":"HFMD","definition":"콕사키바이러스에 의한 소아 감염증으로 구강 내 수포와 수족부 발진이 특징이다.","synonyms":"수족구증","icd_code":"B08.4"},
|
||||
{"korean":"헤르팡기니염","english":"Herpangina","latin":"","abbreviation":"","definition":"콕사키바이러스에 의해 연구개와 편도와에 수포성 궤양을 유발하는 소아 급성 감염증이다.","synonyms":"","icd_code":"B08.5"},
|
||||
{"korean":"소아재발성아프타","english":"Pediatric recurrent aphthous ulcer","latin":"ulcus aphthosum recidivans paediatricum","abbreviation":"","definition":"소아 구강 점막에 반복적으로 발생하는 통증성 궤양으로 점막의 비각화 부위에 호발한다.","synonyms":"소아아프타성궤양","icd_code":"K12.0"},
|
||||
{"korean":"소아구강염","english":"Pediatric stomatitis","latin":"stomatitis paediatrica","abbreviation":"","definition":"소아 구강 점막의 염증성 질환으로 바이러스, 세균, 진균 등 다양한 원인으로 발생한다.","synonyms":"","icd_code":"K12.0"},
|
||||
{"korean":"치아탈구","english":"Tooth luxation","latin":"luxatio dentis","abbreviation":"","definition":"외력에 의해 치아가 치조와 내에서 위치 이상을 보이는 손상으로 치주인대 손상을 동반한다.","synonyms":"치아탈구손상","icd_code":"S03.2"},
|
||||
{"korean":"치아동요","english":"Tooth mobility","latin":"","abbreviation":"","definition":"치아가 정상 범위 이상으로 움직이는 상태로 외상이나 치주질환에 의해 발생할 수 있다.","synonyms":"치아흔들림","icd_code":""},
|
||||
{"korean":"치아전위","english":"Lateral luxation","latin":"luxatio lateralis dentis","abbreviation":"","definition":"치아가 원래 위치에서 측방으로 변위되면서 치조골 압박을 동반하는 외상성 탈구이다.","synonyms":"측방탈구","icd_code":"S03.2"},
|
||||
{"korean":"함입탈구","english":"Intrusive luxation","latin":"luxatio intrusiva dentis","abbreviation":"","definition":"치아가 치조와 내로 밀려 들어가는 외상으로 유치에서 영구치 발육에 영향을 줄 수 있다.","synonyms":"압입탈구","icd_code":"S03.2"},
|
||||
{"korean":"만출탈구","english":"Extrusive luxation","latin":"luxatio extrusiva dentis","abbreviation":"","definition":"치아가 치조와에서 부분적으로 빠져 나오는 외상성 탈구로 치주인대 손상을 동반한다.","synonyms":"만출위치이상","icd_code":"S03.2"},
|
||||
{"korean":"치아탈락","english":"Tooth avulsion","latin":"avulsio dentis","abbreviation":"","definition":"치아가 치조와에서 완전히 탈락된 외상으로 즉각적인 재식이 예후에 중요하다.","synonyms":"완전탈구,치아완전탈출","icd_code":"S03.2"},
|
||||
{"korean":"치아근절","english":"Root fracture","latin":"fractura radicis dentis","abbreviation":"","definition":"치근에 발생하는 파절로 외상에 의해 발생하며 근단부 근접도에 따라 예후가 달라진다.","synonyms":"치근파절","icd_code":"S02.5"},
|
||||
{"korean":"치관파절","english":"Crown fracture","latin":"fractura coronae dentis","abbreviation":"","definition":"치관부에 발생하는 파절로 법랑질 또는 상아질 치수 침범 여부에 따라 분류된다.","synonyms":"치관골절","icd_code":"S02.5"},
|
||||
{"korean":"치아외상","english":"Dental trauma","latin":"trauma dentale","abbreviation":"","definition":"외력에 의해 치아 및 주위 조직에 발생하는 손상으로 소아에서 빈번히 발생한다.","synonyms":"치아손상","icd_code":"S03.2"},
|
||||
{"korean":"치아재식","english":"Tooth replantation","latin":"replantatio dentis","abbreviation":"","definition":"탈락된 치아를 치조와에 다시 심는 처치로 탈락 후 경과 시간이 예후를 결정한다.","synonyms":"치아재식술","icd_code":""},
|
||||
{"korean":"치아고정술","english":"Tooth splinting","latin":"","abbreviation":"","definition":"동요치나 외상 치아를 인접 치아에 고정하여 안정화하는 처치이다.","synonyms":"치아동요고정","icd_code":""},
|
||||
{"korean":"유치외상","english":"Primary tooth trauma","latin":"","abbreviation":"","definition":"유치에 발생하는 외상으로 후속 영구치 발육에 영향을 미칠 수 있어 주의가 필요하다.","synonyms":"유치손상","icd_code":"S03.2"},
|
||||
{"korean":"미성숙영구치외상","english":"Immature permanent tooth trauma","latin":"","abbreviation":"","definition":"근단이 미폐쇄된 영구치에 발생하는 외상으로 치근 발육과 치수 생존에 특별한 고려가 필요하다.","synonyms":"","icd_code":""},
|
||||
{"korean":"치은열상","english":"Gingival laceration","latin":"","abbreviation":"","definition":"외력에 의해 치은이나 구강 점막에 발생하는 절개상 또는 열상이다.","synonyms":"","icd_code":"S01.5"},
|
||||
{"korean":"하악골절","english":"Mandibular fracture","latin":"fractura mandibulae","abbreviation":"","definition":"하악골에 발생하는 골절로 소아에서는 과두부 골절이 빈번하며 성장 장애를 유발할 수 있다.","synonyms":"하악골골절","icd_code":"S02.6"},
|
||||
{"korean":"구강위생지도","english":"Oral hygiene instruction","latin":"","abbreviation":"OHI","definition":"소아 및 보호자에게 올바른 양치질 방법과 구강 관리에 대해 교육하는 과정이다.","synonyms":"구강위생교육","icd_code":""},
|
||||
{"korean":"치실사용법","english":"Flossing technique","latin":"","abbreviation":"","definition":"치간부 치태를 제거하기 위해 치실을 사용하는 방법으로 소아에서는 보호자의 도움이 필요하다.","synonyms":"치실사용","icd_code":""},
|
||||
{"korean":"치간칫솔","english":"Interdental brush","latin":"","abbreviation":"","definition":"치간부 세척을 위한 작은 브러시로 유치열 후기 및 혼합치열기에 사용할 수 있다.","synonyms":"","icd_code":""},
|
||||
{"korean":"소아구강건강교육","english":"Pediatric oral health education","latin":"","abbreviation":"","definition":"소아와 보호자에게 구강 질환 예방과 올바른 관리법에 대해 제공하는 체계적인 교육이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"치아재광화","english":"Tooth remineralization","latin":"","abbreviation":"","definition":"초기 우식 병소에서 무기질이 재침착되어 치질이 회복되는 과정이다.","synonyms":"재광화","icd_code":""},
|
||||
{"korean":"재광화치료","english":"Remineralization therapy","latin":"","abbreviation":"","definition":"불소나 인산칼슘 등을 이용하여 초기 우식 병소의 재광화를 촉진하는 비침습적 치료법이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"APF겔","english":"Acidulated phosphate fluoride gel","latin":"","abbreviation":"APF","definition":"1.23% 불화나트륨을 함유한 산성인산불소 겔로 전문가 불소 도포에 사용된다.","synonyms":"산성인산불소겔","icd_code":""},
|
||||
{"korean":"최소침습치과","english":"Minimally invasive dentistry","latin":"","abbreviation":"MID","definition":"치질 보존을 최대화하고 불필요한 삭제를 최소화하는 소아치과 치료 철학이다.","synonyms":"MI치과,최소침습술식","icd_code":""},
|
||||
{"korean":"소아예방적식이상담","english":"Pediatric dietary counseling","latin":"","abbreviation":"","definition":"소아의 우식 위험을 감소시키기 위해 당 섭취 빈도와 식이 습관에 대해 제공하는 상담이다.","synonyms":"식이상담","icd_code":""},
|
||||
{"korean":"복합레진수복","english":"Composite resin restoration","latin":"","abbreviation":"","definition":"복합레진을 이용한 치아 수복으로 소아에서 심미성과 치질 보존이 중요한 경우에 사용한다.","synonyms":"레진수복,복합수복","icd_code":""},
|
||||
{"korean":"글라스아이오노머충전","english":"Glass ionomer restoration","latin":"","abbreviation":"GI","definition":"글라스아이오노머 시멘트를 이용한 충전으로 불소 방출 효과가 있어 소아치과에서 활용된다.","synonyms":"GI충전","icd_code":""},
|
||||
{"korean":"RMGI충전","english":"Resin-modified glass ionomer restoration","latin":"","abbreviation":"RMGI","definition":"레진변성 글라스아이오노머를 이용한 수복으로 기계적 강도와 불소 방출을 겸비한다.","synonyms":"레진변성글라스아이오노머충전","icd_code":""},
|
||||
{"korean":"개방성샌드위치법","english":"Open sandwich technique","latin":"","abbreviation":"","definition":"복합레진과 글라스아이오노머를 층상으로 적용하여 수복하는 기법으로 소아치과에서 활용된다.","synonyms":"","icd_code":""},
|
||||
{"korean":"유치발거","english":"Primary tooth extraction","latin":"","abbreviation":"","definition":"유치를 발거하는 처치로 조기 상실 시 공간 유지 장치 고려가 필요하다.","synonyms":"유치발치","icd_code":""},
|
||||
{"korean":"예방적확대","english":"Extension for prevention","latin":"","abbreviation":"","definition":"GV Black이 제안한 개념으로 우식 재발을 방지하기 위해 와동을 예방적으로 확대하는 방식이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"공간유지장치","english":"Space maintainer","latin":"","abbreviation":"","definition":"유치 조기 상실 시 후속 영구치를 위한 공간을 유지하기 위해 장착하는 장치이다.","synonyms":"공간보존장치","icd_code":""},
|
||||
{"korean":"공간회복장치","english":"Space regainer","latin":"","abbreviation":"","definition":"상실된 공간을 재확보하기 위해 사용하는 장치로 부분적으로 상실된 공간의 회복이 가능하다.","synonyms":"","icd_code":""},
|
||||
{"korean":"고정식공간유지장치","english":"Fixed space maintainer","latin":"","abbreviation":"","definition":"치아에 접착 또는 고정되어 환자가 제거할 수 없는 공간 유지 장치이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"가철식공간유지장치","english":"Removable space maintainer","latin":"","abbreviation":"","definition":"환자가 장착과 제거가 가능한 공간 유지 장치로 구강 위생 관리가 용이하다.","synonyms":"","icd_code":""},
|
||||
{"korean":"디스탈슈즈","english":"Distal shoe appliance","latin":"","abbreviation":"","definition":"제2유구치 상실 후 제1영구대구치의 맹출을 유도하기 위해 사용하는 공간 유지 장치이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"밴드앤루프","english":"Band and loop","latin":"","abbreviation":"","definition":"단일 치아 상실 시 사용하는 고정식 공간 유지 장치로 밴드와 루프로 구성된다.","synonyms":"","icd_code":""},
|
||||
{"korean":"린구얼아치","english":"Lingual arch","latin":"","abbreviation":"","definition":"하악 양측 후방치아에 밴드를 장착하고 설측에 와이어를 연결하는 공간 유지 장치이다.","synonyms":"설측궁","icd_code":""},
|
||||
{"korean":"트랜스팔라탈아치","english":"Transpalatal arch","latin":"","abbreviation":"TPA","definition":"상악 양측 대구치 밴드를 구개부 와이어로 연결하는 장치로 공간 유지와 회전 방지에 사용한다.","synonyms":"횡구개궁","icd_code":""},
|
||||
{"korean":"낸스고정궁","english":"Nance holding arch","latin":"","abbreviation":"","definition":"상악 제1대구치의 전방 이동을 방지하기 위해 구개부 아크릴 버튼을 이용하는 장치이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"조기치아상실","english":"Premature tooth loss","latin":"","abbreviation":"","definition":"정상 교환 시기보다 이르게 치아가 상실되는 것으로 공간 상실의 원인이 된다.","synonyms":"","icd_code":""},
|
||||
{"korean":"공간분석","english":"Space analysis","latin":"","abbreviation":"","definition":"혼합치열기에 사용 가능한 공간과 필요한 공간을 측정하여 공간 과잉 또는 부족을 평가하는 분석이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"차단교정","english":"Interceptive orthodontics","latin":"","abbreviation":"","definition":"혼합치열기에 진행되는 악안면 부조화를 조기에 차단하여 정상 발육을 유도하는 교정 치료이다.","synonyms":"가로막이교정","icd_code":""},
|
||||
{"korean":"기능성교정장치","english":"Functional appliance","latin":"","abbreviation":"","definition":"악골의 기능적 움직임을 이용하여 악골 성장을 유도하는 가철성 교정 장치이다.","synonyms":"기능적교정장치","icd_code":""},
|
||||
{"korean":"구외견인","english":"Extraoral traction","latin":"","abbreviation":"","definition":"두부나 경부를 지점으로 하악 또는 상악에 외부에서 견인력을 가하는 장치이다.","synonyms":"구외력","icd_code":""},
|
||||
{"korean":"전방견인장치","english":"Protraction headgear","latin":"","abbreviation":"","definition":"상악을 전방으로 견인하여 제3급 부정교합을 치료하는 구외 견인 장치이다.","synonyms":"전방견인면궁","icd_code":""},
|
||||
{"korean":"급속구개확장","english":"Rapid palatal expansion","latin":"","abbreviation":"RPE","definition":"구개 정중봉합을 개방하여 상악골을 확장하는 처치로 성장기 소아에서 효과적이다.","synonyms":"급속구개확대","icd_code":""},
|
||||
{"korean":"구개확장장치","english":"Palatal expander","latin":"","abbreviation":"","definition":"상악 치궁의 폭경을 확장하기 위해 사용하는 장치로 고정식과 가철식이 있다.","synonyms":"","icd_code":""},
|
||||
{"korean":"순면벽판","english":"Lip bumper","latin":"","abbreviation":"","definition":"하순의 압력을 차단하여 하악 후방치아의 원심 이동을 유도하고 전치부 공간을 확보하는 장치이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"구강습관","english":"Oral habit","latin":"","abbreviation":"","definition":"치아나 악골에 영향을 미치는 구강 내 습관으로 손가락 빨기, 입술 빨기, 이갈이 등이 있다.","synonyms":"구강악습관","icd_code":""},
|
||||
{"korean":"손가락빨기","english":"Finger sucking","latin":"","abbreviation":"","definition":"영유아가 손가락을 빠는 습관으로 지속 시 전치부 개방교합과 상악 전돌을 유발할 수 있다.","synonyms":"","icd_code":""},
|
||||
{"korean":"소아이갈이","english":"Pediatric bruxism","latin":"","abbreviation":"","definition":"소아에서 무의식적으로 치아를 악물거나 가는 습관으로 수면 중에 흔히 관찰된다.","synonyms":"소아갈이증","icd_code":"F45.8"},
|
||||
{"korean":"입술빨기","english":"Lip sucking","latin":"","abbreviation":"","definition":"하순을 상악 전치 뒤에 물고 빠는 습관으로 전치부 부정교합을 유발할 수 있다.","synonyms":"","icd_code":""},
|
||||
{"korean":"특수요구치과","english":"Special needs dentistry","latin":"","abbreviation":"","definition":"신체적, 정신적 장애나 특수한 의료 요구를 가진 환자를 대상으로 하는 치과 진료 분야이다.","synonyms":"특수치과","icd_code":""},
|
||||
{"korean":"장애아구강관리","english":"Oral management for children with disabilities","latin":"","abbreviation":"","definition":"장애를 가진 소아 환자의 구강 건강을 유지하고 치과 치료를 제공하는 포괄적 관리이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"자폐스펙트럼장애","english":"Autism spectrum disorder","latin":"","abbreviation":"ASD","definition":"사회적 상호작용과 의사소통에 어려움을 보이는 발달장애로 치과 진료 시 특별한 행동조절이 필요하다.","synonyms":"자폐증","icd_code":"F84.0"},
|
||||
{"korean":"뇌성마비","english":"Cerebral palsy","latin":"","abbreviation":"CP","definition":"비진행성 뇌 손상에 의해 운동 장애가 나타나는 질환으로 구강 관리에 특별한 주의가 필요하다.","synonyms":"뇌성마비증","icd_code":"G80"},
|
||||
{"korean":"다운증후군","english":"Down syndrome","latin":"","abbreviation":"","definition":"21번 염색체 삼염색체증에 의한 선천성 질환으로 구강 내 특징적 소견과 치과 진료 시 주의점이 있다.","synonyms":"","icd_code":"Q90"},
|
||||
{"korean":"주의력결핍과다행동장애","english":"Attention deficit hyperactivity disorder","latin":"","abbreviation":"ADHD","definition":"주의력 저하, 과다행동, 충동성을 특징으로 하는 신경발달장애로 치과 진료 시 행동조절이 중요하다.","synonyms":"","icd_code":"F90"},
|
||||
{"korean":"유아우식증","english":"Early childhood caries","latin":"","abbreviation":"ECC","definition":"71개월 이하 영유아에서 발생하는 광범위한 우식으로 야간 수유 습관과 밀접한 관련이 있다.","synonyms":"조기소아우식","icd_code":"K02"},
|
||||
{"korean":"우식유병률","english":"Caries prevalence","latin":"","abbreviation":"","definition":"특정 인구 집단에서 우식을 경험한 사람의 비율을 나타내는 역학 지표이다.","synonyms":"우식발생률","icd_code":""},
|
||||
{"korean":"우식경험률","english":"Caries experience rate","latin":"","abbreviation":"","definition":"개인이 과거에 우식을 경험한 치아 수를 나타내는 지표로 DMF 지수로 표현된다.","synonyms":"","icd_code":""},
|
||||
{"korean":"우유병우식","english":"Nursing bottle caries","latin":"","abbreviation":"","definition":"야간 우유병 수유 습관에 의해 상악 전치부에 광범위하게 발생하는 유아우식증의 일형이다.","synonyms":"젖병우식,우유병썩은이","icd_code":"K02"},
|
||||
{"korean":"치아과잉증","english":"Hyperdontia","latin":"hyperodontia","abbreviation":"","definition":"정상 치열보다 많은 치아가 존재하는 상태로 소아에서 맹출 지연이나 위치 이상을 유발할 수 있다.","synonyms":"과잉치,다발치","icd_code":"K00.1"},
|
||||
{"korean":"선천성치아결손","english":"Congenitally missing teeth","latin":"anodontia partialis","abbreviation":"","definition":"선천적으로 치아가 형성되지 않은 상태로 소아에서 영구치 결손이 흔하게 관찰된다.","synonyms":"선천결손치,무치아증","icd_code":"K00.0"},
|
||||
{"korean":"소아치은염","english":"Pediatric gingivitis","latin":"","abbreviation":"","definition":"소아에서 발생하는 치은의 염증으로 치태 축적이 주요 원인이다.","synonyms":"","icd_code":"K05.1"},
|
||||
{"korean":"소아치주질환","english":"Pediatric periodontal disease","latin":"","abbreviation":"","definition":"소아에서 발생하는 치주 조직의 질환으로 전신 질환과 연관될 수 있다.","synonyms":"","icd_code":"K05.3"},
|
||||
{"korean":"소아치은비대증","english":"Pediatric gingival enlargement","latin":"","abbreviation":"","definition":"소아에서 치은 조직이 비정상적으로 증식된 상태로 약물성 또는 유전성 원인이 흔하다.","synonyms":"소아치은증식증","icd_code":"K06.1"},
|
||||
{"korean":"유아구강검진","english":"Infant oral health examination","latin":"","abbreviation":"","definition":"생후 12개월 이내에 실시하는 최초 구강 건강 평가로 위험 인자 파악과 보호자 교육이 목적이다.","synonyms":"","icd_code":""}
|
||||
];
|
||||
|
||||
const existing = db.prepare("SELECT korean FROM terms WHERE category = '소아치과'").all();
|
||||
const existingNames = new Set(existing.map(t => t.korean));
|
||||
const newTerms = terms.filter(t => !existingNames.has(t.korean));
|
||||
console.log(`Existing: ${existing.length}, New: ${newTerms.length}`);
|
||||
|
||||
const insert = db.prepare('INSERT INTO terms (id, korean, english, latin, abbreviation, definition, category, synonyms, related_ids, pmids, icd_code, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
let maxId = db.prepare('SELECT MAX(id) as m FROM terms').get().m;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const t of newTerms) {
|
||||
try {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin, t.abbreviation, t.definition, '소아치과', t.synonyms, '', '', t.icd_code, '', now, now);
|
||||
} catch(e) { console.log(`Error: ${t.korean}: ${e.message}`); }
|
||||
}
|
||||
|
||||
db.exec("INSERT INTO terms_fts(terms_fts) VALUES('rebuild')");
|
||||
const cats = db.prepare('SELECT category, COUNT(*) as cnt FROM terms GROUP BY category ORDER BY cnt DESC').all();
|
||||
cats.forEach(c => console.log(`${c.category}: ${c.cnt}`));
|
||||
console.log('Total:', db.prepare('SELECT COUNT(*) as t FROM terms').get().t);
|
||||
db.close();
|
||||
@@ -1,59 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('.smallclaw/databases/dental_dict.db');
|
||||
|
||||
// Periodontic terms
|
||||
const perioTerms = [
|
||||
{"korean":"접합상피","english":"Junctional epithelium","latin":"epithelium junctionale","abbreviation":"JE","definition":"치아 표면에 부착되어 치은열구의 기저를 형성하는 비각화 상피로, 치은과 치아 사이의 상피부착을 유지하는 중요한 구조이다.","synonyms":"","icd_code":""},
|
||||
{"korean":"치은점막이계","english":"Mucogingival junction","latin":"","abbreviation":"MGJ","definition":"부착은과 치조점막의 경계선으로, 임상적으로 부착은의 폭을 평가하는 중요한 기준점이 된다.","synonyms":"점막은경계","icd_code":""},
|
||||
{"korean":"치은유두","english":"Interdental papilla","latin":"papilla interdentalis","abbreviation":"","definition":"인접 치아 사이의 치은 부위로, 치간 공간을 채우며 치태 축적과 치주질환의 시작점이 되기 쉬운 영역이다.","synonyms":"치간유두,치간치은","icd_code":""},
|
||||
{"korean":"치은연상치석","english":"Supragingival calculus","latin":"","abbreviation":"","definition":"치은연 상방에 위치한 석회화된 치석으로, 주로 타액선 개구부 근처에 형성되며 육안으로 관찰 가능하다.","synonyms":"치은연상결석","icd_code":""},
|
||||
{"korean":"치은연하치석","english":"Subgingival calculus","latin":"","abbreviation":"","definition":"치은연 하방 치근면에 부착된 석회화된 치석으로, 치주낭 내에 위치하며 치은연하 염증을 유지하고 악화시키는 주요 인자이다.","synonyms":"치은연하결석","icd_code":""},
|
||||
{"korean":"치조점막","english":"Alveolar mucosa","latin":"tunica mucosa alveolaris","abbreviation":"","definition":"치조골을 덮는 느슨한 결합조직의 점막으로, 부착은과 달리 이동성이 있으며 치은점막이계를 경계로 부착은과 구분된다.","synonyms":"","icd_code":""},
|
||||
{"korean":"치주낭상피","english":"Pocket epithelium","latin":"","abbreviation":"","definition":"치주낭의 벽을 형성하는 상피로, 치주염 진행 시 접합상피가 치근철측으로 이동하면서 형성되며 치근면으로부터 박리된 상태이다.","synonyms":"낭상피","icd_code":""},
|
||||
{"korean":"치조골개조","english":"Alveolar bone remodeling","latin":"","abbreviation":"","definition":"치조골의 끊임없는 골흡수와 골형성 과정으로, 정상적인 골대사와 치주질환 시 병적 골흡수 모두에서 일어나는 생리적 과정이다.","synonyms":"치조골재형성","icd_code":""},
|
||||
{"korean":"부착은폭","english":"Width of attached gingiva","latin":"","abbreviation":"","definition":"치은연에서 치은점막이계까지의 치은 전체 폭에서 치은열구 깊이를 뺀 값으로, 치은의 건강과 치주수술 계획에 중요한 임상 지표이다.","synonyms":"부착은폭경","icd_code":""},
|
||||
{"korean":"치은섬유","english":"Gingival fibers","latin":"","abbreviation":"","definition":"치은 고유층에 분포하는 콜라겐 섬유 집단으로, 치은의 형태와 강도를 유지하며 치아에 대한 치은의 부착을 강화하는 역할을 한다.","synonyms":"치은교원섬유","icd_code":""},
|
||||
{"korean":"치주낭세척","english":"Periodontal pocket irrigation","latin":"","abbreviation":"","definition":"치주낭 내에 소독용액이나 항생제 용액을 관주하여 치주낭 내 세균과 독소를 제거하는 치주치료 보조 방법이다.","synonyms":"치주낭관주,낭세척","icd_code":""}
|
||||
];
|
||||
|
||||
// Oral pathology terms
|
||||
const pathTerms = [
|
||||
{"korean":"골륜","english":"Torus","latin":"Torus palatinus / Torus mandibularis","abbreviation":"","definition":"구개 또는 하악골의 설측에 발생하는 양성 골증식성 병변으로, 발육성 원인으로 형성되며 임상적으로 흔히 관찰된다.","synonyms":"토러스, 구개골륜, 하악골륜","icd_code":"M27.8"},
|
||||
{"korean":"언거종","english":"Epulis","latin":"Epulis","abbreviation":"","definition":"치은에 발생하는 반응성 증식성 병변으로, 만성 자극에 의한 섬유성 또는 육아종성 조직 증식을 특징으로 한다.","synonyms":"치은유탁종, 유탁성치은종, 치은증식종","icd_code":"K06.8"},
|
||||
{"korean":"시멘트아세포종","english":"Cementoblastoma","latin":"Cementoblastoma","abbreviation":"","definition":"치근의 시멘트질을 형성하는 세포 기원의 양성 종양으로, 치근과 융합된 방사선투과상과 종양성 시멘트질 형성을 보인다.","synonyms":"시멘트모세포종","icd_code":"D16.5"},
|
||||
{"korean":"괴사성타액선화생","english":"Necrotizing Sialometaplasia","latin":"Sialometaplasia necroticans","abbreviation":"NSM","definition":"구개의 타액선에 발생하는 자가한정성 괴사성 염증질환으로, 악성 종양과 임상 및 조직학적으로 유사하여 감별진단이 중요하다.","synonyms":"괴사성침새화생","icd_code":"K11.8"},
|
||||
{"korean":"과립세포종","english":"Granular Cell Tumor","latin":"Tumor granulocellulare","abbreviation":"GCT","definition":"신경외배엽 기원의 양성 종양으로, 구강 내에서는 설부에 호발하며 과립 세포와 사이조직의 위성증식을 특징으로 한다.","synonyms":"립과립세포종, 아베리코스종","icd_code":"D21"},
|
||||
{"korean":"구강흑색점","english":"Oral Melanotic Macule","latin":"Macula melanotica oris","abbreviation":"","definition":"구강점막에 발생하는 국소적 멜라닌 색소침착 병변으로, 하순 및 치은에 호발하며 양성이지만 악성흑색종과 감별이 필요하다.","synonyms":"구강멜라닌점, 흑색반","icd_code":"K13.0"},
|
||||
{"korean":"치은비대증","english":"Gingival Hyperplasia","latin":"Hyperplasia gingivae","abbreviation":"","definition":"치은 조직의 비정상적 증식으로 인해 치은이 비대해지는 상태로, 약물성, 유전성, 염증성 원인 등이 있으며 임상적으로 흔히 관찰된다.","synonyms":"치은과형성, 치은증식증","icd_code":"K06.1"}
|
||||
];
|
||||
|
||||
// Insert periodontic terms
|
||||
const perioExisting = db.prepare("SELECT korean FROM terms WHERE category = '치주'").all();
|
||||
const perioNames = new Set(perioExisting.map(t => t.korean));
|
||||
const newPerioTerms = perioTerms.filter(t => !perioNames.has(t.korean));
|
||||
console.log(`Periodontic: ${perioExisting.length} existing, ${newPerioTerms.length} new`);
|
||||
|
||||
// Insert oral pathology terms
|
||||
const pathExisting = db.prepare("SELECT korean FROM terms WHERE category = '구강병리'").all();
|
||||
const pathNames = new Set(pathExisting.map(t => t.korean));
|
||||
const newPathTerms = pathTerms.filter(t => !pathNames.has(t.korean));
|
||||
console.log(`Oral Pathology: ${pathExisting.length} existing, ${newPathTerms.length} new`);
|
||||
|
||||
const insert = db.prepare('INSERT INTO terms (id, korean, english, latin, abbreviation, definition, category, synonyms, related_ids, pmids, icd_code, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
let maxId = db.prepare('SELECT MAX(id) as m FROM terms').get().m;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const t of newPerioTerms) {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin, t.abbreviation, t.definition, '치주', t.synonyms, '', '', t.icd_code, '', now, now);
|
||||
}
|
||||
|
||||
for (const t of newPathTerms) {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin, t.abbreviation, t.definition, '구강병리', t.synonyms, '', '', t.icd_code, '', now, now);
|
||||
}
|
||||
|
||||
db.exec("INSERT INTO terms_fts(terms_fts) VALUES('rebuild')");
|
||||
|
||||
const cats = db.prepare('SELECT category, COUNT(*) as cnt FROM terms GROUP BY category ORDER BY cnt DESC').all();
|
||||
cats.forEach(c => console.log(`${c.category}: ${c.cnt}`));
|
||||
console.log('Total:', db.prepare('SELECT COUNT(*) as t FROM terms').get().t);
|
||||
db.close();
|
||||
File diff suppressed because one or more lines are too long
@@ -1,32 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('.smallclaw/databases/dental_dict.db');
|
||||
|
||||
const terms = [
|
||||
{"korean":"매몰","english":"Investing","latin":"","abbreviation":"","definition":"주조를 위해 왁스패턴을 매몰재로 감싸 몰드를 형성하는 과정이다. 소빙 및 주조의 전 단계로 필수적인 보철 과정이다.","synonyms":"매몰과정","icd_code":""},
|
||||
{"korean":"매몰재","english":"Investment material","latin":"","abbreviation":"","definition":"왁스패턴 주위를 채워 주조용 몰드를 형성하는 내열성 재료이다. 석고결합매몰재, 인산결합매몰재 등이 있다.","synonyms":"","icd_code":""},
|
||||
{"korean":"피개의치","english":"Overlay denture","latin":"","abbreviation":"","definition":"자연치아의 치관을 단축한 후 그 위를 덮어 제작하는 가철성 의치이다. 치조제의 흡수를 감소하고 잔존 치아의 지지를 활용할 수 있다.","synonyms":"오버레이의치","icd_code":""},
|
||||
{"korean":"수렴각","english":"Convergence angle","latin":"","abbreviation":"","definition":"치아 형성 시 대향벽이 이루는 각도로, 지대치의 유지와 안정에 영향을 미친다. 일반적으로 6도 이하의 수렴각이 권장된다.","synonyms":"테이퍼각","icd_code":""},
|
||||
{"korean":"유지형","english":"Retention form","latin":"","abbreviation":"","definition":"치아 형성 시 보철물의 탈락을 방지하기 위해 부여하는 형태적 특징이다. 벽의 평행성, 형태의 깊이 등이 유지형에 해당한다.","synonyms":"","icd_code":""},
|
||||
{"korean":"저항형","english":"Resistance form","latin":"","abbreviation":"","definition":"치아 형성 시 교합력에 의한 보철물의 변위나 치아의 파절을 방지하기 위한 형태적 특징이다. 저항형은 형성의 깊이와 폭경 비 등에 의해 결정된다.","synonyms":"","icd_code":""},
|
||||
{"korean":"소빙","english":"Burnout","latin":"","abbreviation":"","definition":"매몰된 왁스패턴을 가열로 제거하여 주조용 공간을 형성하는 과정이다. 적절한 소빙 사이클이 주조의 성공에 필수적이다.","synonyms":"소각","icd_code":""}
|
||||
];
|
||||
|
||||
const existing = db.prepare("SELECT korean FROM terms WHERE category = '보철'").all();
|
||||
const existingNames = new Set(existing.map(t => t.korean));
|
||||
const newTerms = terms.filter(t => !existingNames.has(t.korean));
|
||||
console.log(`New terms to insert: ${newTerms.length}`);
|
||||
|
||||
const insert = db.prepare('INSERT INTO terms (id, korean, english, latin, abbreviation, definition, category, synonyms, related_ids, pmids, icd_code, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
let maxId = db.prepare('SELECT MAX(id) as m FROM terms').get().m;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const t of newTerms) {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin, t.abbreviation, t.definition, '보철', t.synonyms, '', '', t.icd_code, '', now, now);
|
||||
}
|
||||
|
||||
db.exec("INSERT INTO terms_fts(terms_fts) VALUES('rebuild')");
|
||||
|
||||
const cats = db.prepare('SELECT category, COUNT(*) as cnt FROM terms GROUP BY category ORDER BY cnt DESC').all();
|
||||
cats.forEach(c => console.log(`${c.category}: ${c.cnt}`));
|
||||
console.log('Total:', db.prepare('SELECT COUNT(*) as t FROM terms').get().t);
|
||||
db.close();
|
||||
@@ -1,27 +0,0 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('.smallclaw/databases/dental_dict.db');
|
||||
|
||||
const terms = [
|
||||
{"korean":"수복와동","english":"Cavity preparation","latin":"","abbreviation":"","definition":"치아우식 제거 후 수복물을 위해 치아에 형성하는 와동의 형태이다.","synonyms":"와동형성","icd_code":""},
|
||||
{"korean":"와동분류","english":"Cavity classification","latin":"","abbreviation":"","definition":"치아우식 와동의 위치와 형태에 따라 분류하는 체계로 Black의 분류가 널리 사용된다.","synonyms":"블랙분류","icd_code":""}
|
||||
];
|
||||
|
||||
const existing = db.prepare("SELECT korean FROM terms WHERE category = '보철'").all();
|
||||
const existingNames = new Set(existing.map(t => t.korean));
|
||||
const newTerms = terms.filter(t => !existingNames.has(t.korean));
|
||||
console.log(`New terms to insert: ${newTerms.length}`);
|
||||
|
||||
const insert = db.prepare('INSERT INTO terms (id, korean, english, latin, abbreviation, definition, category, synonyms, related_ids, pmids, icd_code, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
|
||||
let maxId = db.prepare('SELECT MAX(id) as m FROM terms').get().m;
|
||||
const now = new Date().toISOString();
|
||||
|
||||
for (const t of newTerms) {
|
||||
insert.run(++maxId, t.korean, t.english, t.latin, t.abbreviation, t.definition, '보철', t.synonyms, '', '', t.icd_code, '', now, now);
|
||||
}
|
||||
|
||||
db.exec("INSERT INTO terms_fts(terms_fts) VALUES('rebuild')");
|
||||
|
||||
const cats = db.prepare('SELECT category, COUNT(*) as cnt FROM terms GROUP BY category ORDER BY cnt DESC').all();
|
||||
cats.forEach(c => console.log(`${c.category}: ${c.cnt}`));
|
||||
console.log('Total:', db.prepare('SELECT COUNT(*) as t FROM terms').get().t);
|
||||
db.close();
|
||||
Reference in New Issue
Block a user