# 오래된 영어책 한 권을 끝까지 번역하려면: 텍스트 추출, 로컬 모델, 재개와 대조 뷰어 수백 쪽짜리 외국어 책을 한국어로 읽고 싶을 때 진짜 난관은 한 문장을 번역하는 능력이 아니다. **끝까지 빠짐없이 번역하고, 중간에 멈춰도 이어 가고, 원문과 번역문의 대응이 맞는지 확인하는 능력**이다. 한 문단의 품질이 아무리 좋아도 책의 중간에서 누락이 일어나거나 장 제목이 다음 문단에 붙어 버리면 완역본으로 믿기 어렵다. 공개된 옛 책의 디지털 본문을 로컬 언어모델과 결합하는 작업은 이 세 문제를 분리해서 풀어야 한다. ## OCR보다 먼저 확인할 것: 이미 읽을 수 있는 텍스트가 있는가 출발점이 PDF라 해도 무조건 OCR부터 돌릴 필요는 없다. 문자를 선택·복사할 수 있는 PDF와 이미지로만 이루어진 스캔 PDF는 입력 조건이 다르다. 후자라면 OCRmyPDF·Tesseract 같은 인식 도구를 거쳐 텍스트를 만들어야 하지만, 전자라면 기존 텍스트 레이어를 추출하는 편이 낫다. 표·각주·2단 편집·수식·스캔 기울기·얼룩은 두 방식 모두에서 별도의 검수가 필요하다. 더 빠른 지름길도 있다. Internet Archive처럼 이미 스캔한 도서를 `djvu.txt` 형태의 평문으로 제공하는 경우다. 프랭크 필(Frank Peel)의 *The Risings of the Luddites*를 읽기 위해 사용한 입력은 [Internet Archive의 텍스트 파일](https://archive.org/stream/risingsluddites01peelgoog/risingsluddites01peelgoog_djvu.txt)이었다. 이 경우 추가 OCR은 생략할 수 있다. 다만 **OCR 작업이 불필요하다는 말은 OCR 오류가 없다는 말이 아니다.** 이미 인식된 결과물 속에 잘못된 철자, 깨진 행, 페이지 머리말, 목차의 점선과 페이지 번호가 들어 있을 수 있다. 독자의 목표에 따라 산출물도 달라진다. 뜻을 읽기 위한 한국어 평문, 원문과 번역을 대조하기 위한 병기본, 판형과 쪽배치를 재현하는 PDF는 서로 난도가 다르다. 첫 단계의 성공 목표는 정교한 조판보다 **문단과 순서가 보존된 내용 대조본**으로 두는 편이 안전하다. ## 여섯 공정은 각각 독립해야 한다 입력 판별 → 원문 확보 → 전처리 → 분할 → 번역 → 복원·검수·출력의 순서가 기본 골격이다. | 공정 | 입력과 처리 | 보존해야 할 것 | 흔한 실패 | | --- | --- | --- | --- | | 원문 확보 | PDF 텍스트, OCR 결과 또는 URL의 TXT | 출처와 원본 그대로의 복사본 | 재시작 때 원문이 달라짐 | | 전처리 | 줄바꿈, 행 끝 하이픈, 머리말, 페이지 번호 | 제목·각주·문단 경계 | 필요한 텍스트까지 삭제 | | 분할 | 문단을 청크로 묶기 | 순서, 문단 ID, 청크 기준 | 설정 변경으로 재개 위치 이동 | | 번역 | Ollama 또는 외부 번역 서비스 호출 | 의미, 고유명사, 날짜, 빠짐없는 출력 | 모델의 장광설·잘림·누락 | | 복원 | 청크 안의 번역 문단 되찾기 | 원문과 번역의 1:1 대응 | 빈 줄 기준 복원으로 전체 밀림 | | 검수·출력 | TXT·Markdown·HTML 병기 | 읽기와 오류 발견 가능성 | 보기 좋은 화면이 오역을 숨김 | 원문 파일과 정제 파일을 따로 저장하는 것은 번거로운 중복이 아니다. 잘못 지운 제목을 복구하거나 정규식을 조정하려면 전처리 전 판본이 필요하다. 번역 결과를 원본 텍스트와 분리해야 하는 이유도 같다. ## 프랭크 필의 러다이트 역사서를 입력으로 삼는 방법 이 책의 Archive TXT 주소는 이미 전체 텍스트를 제공하는 형태였다. Python의 `requests`로 파일을 내려받고, 빈 줄과 줄바꿈을 정리한 뒤 문단을 나누는 실험이 가능했다. 기본 출력은 원문 `01_raw.txt`, 정제본 `02_clean_en.txt`, 번역문 `03_ko.txt`, 원문·번역 병기 Markdown과 HTML로 설계했다. 그러나 이 단계의 단순한 정규식은 책의 구조를 이해하지 못한다. 제목 `THE RISINGS OF THE LUDDITES`, `CONTENTS.`, `Chapter I.`, `INTRODUCTORY` 같은 문자열 가운데 **가장 먼저 발견된 것**을 앞부분 절단 기준으로 쓰면, 실제 첫 장이 아니라 표제지나 목차에서 시작할 수 있다. 반대로 `FOOTNOTES` 같은 문자열의 첫 출현을 뒷부분 절단 기준으로 잡으면, 주석 목록이 본문 중간에 등장하는 판본에서 텍스트가 잘릴 수 있다. 줄 끝의 `-`를 무조건 삭제하는 처리 역시 실제 단어의 하이픈과 OCR로 끊어진 단어를 구별하지 못한다. 대문자로 된 짧은 줄을 모두 제목으로 취급하면 문맥이 깨지고, 모든 한 줄 개행을 공백으로 합치면 시·표·편지·인용문의 형식이 손상된다. 페이지 숫자와 머리말을 제거하는 규칙도 숫자 하나가 독립된 의미를 갖는 부분에서는 위험하다. 따라서 이 TXT는 **'다운로드만 하면 바로 번역 가능한 입력'인 동시에 '최종 번역 전에 목차와 본문의 경계를 눈으로 확인해야 하는 입력'**이다. 이 사실은 [러다이트 처형자와 기억](./luddite-executions-memory.md)에서 언급된 옛 공개 역사서의 활용 가능성과 연결된다. ## 번역 엔진을 바꾸는 것과 실행 환경을 바꾸는 것은 다르다 로컬 모델을 쓰기 위해 반드시 Meta의 Llama 가중치를 고집할 필요는 없다. `llama.cpp`는 실행기이고, Llama는 모델 계열이다. Ollama나 llama.cpp 서버, LM Studio 같은 런타임 위에서 서로 다른 모델을 사용할 수 있다. 첫 실험은 `ollama`의 HTTP API를 Python이 호출하는 구조였다. `/api/generate`와 `/api/chat`는 호출 형식이 서로 다르며, 후자는 메시지 배열을 받는다. 처음 실행에서는 `qwen2.5:14b`를 지정한 요청이 HTTP 404로 실패했다. 404만으로 서버 자체가 꺼졌다고 단정할 수는 없다. 지정한 모델 태그가 설치되지 않은 경우도 확인해야 한다. `ollama list`, `/api/tags`, `ollama ps`를 구분해 점검하면 설치 목록, API 응답, 현재 적재 상태를 각각 볼 수 있다. 이후 설치되어 있던 `qwen3:latest`를 기본으로 삼고, 서버의 `/api/tags`를 조회해 사용 가능한 모델 중 선택하도록 설계를 바꿨다. 코드 지시에 따른 변경과 실제 번역 품질의 검증은 별도다. 영어→한국어 전체 번역을 위한 선택지는 대략 다음과 같다. | 방법 | 장점 | 판단해야 할 대가 | | --- | --- | --- | | Ollama의 Qwen3 | 이미 설치한 로컬 환경을 재사용, 네트워크 없이 본문 처리 가능 | 장문 생성 지연, 문단 태그 손실, 메모리 사용 | | Llama 계열 일반 모델 | 기존 실행기·모델을 활용 | 한국어 문체와 긴 책 번역의 실제 품질은 시험 필요 | | TranslateGemma 등 번역 특화 모델 | 번역을 목표로 설계된 대안 | 모델 크기·배포 형식·환경 호환성·실측 비교 필요 | | Argos Translate·LibreTranslate | 오프라인 번역용 도구를 비교적 단순하게 묶을 수 있음 | 문학적 문체, 역사 용어, 문단 충실도 검수 필요 | | Google Cloud Translation | 문단 배열을 API로 넘기기 쉽고 로컬 모델을 실행하지 않음 | 키와 과금, 온라인 전송, 제공 조건과 품질 비교 필요 | '어느 모델이 최선인가'에는 컴퓨터 사양뿐 아니라 **원문 손상 정도, 고유명사 처리, 문장 자연스러움, 누락률, 완주 시간**이 포함된다. 특정 모델이 항상 다른 모델보다 낫다는 평가는 이 실험에서 확정되지 않았다. ## M1·통합 메모리 16GB에서 무엇을 아껴야 하는가 Apple M1과 통합 메모리 16GB인 기계에서는 모델을 더 크게 바꾸기 전에 사용 중인 모델과 응답 형식을 살펴볼 필요가 있다. `qwen3:latest` 같은 모델 태그를 이용한 로컬 실험은 가능했지만, 태그만으로 정확한 파라미터 수·양자화·실행 속도를 확정할 수 없다. 시스템 메모리에는 운영체제와 모델·컨텍스트가 함께 들어가므로, 같은 16GB라도 남는 메모리에 따라 체감이 달라진다. 초기 설정은 문자 기준 청크 약 3,500자, `num_predict=8192`, 사고 모드 비활성화 미적용이었다. 당시 기록된 일부 청크의 처리 시간은 193초·247초에서 1,194초·3,213초·2,733초·3,930초까지 크게 흔들렸다. 이것은 **책 뒤쪽일수록 항상 느려지는 법칙**이 아니라 청크별 길이, 모델의 출력 길이와 내부 처리, 기기 상태가 다르게 작동했음을 보여주는 관찰이다. 분 단위 지연의 정확한 원인은 프로파일링으로 확정하지 않았다. 제안된 경량화는 다음과 같다. 청크 기본값을 800자로 낮추고, `num_predict`를 2,200으로 줄이며, 프롬프트에서 `/no_think`를 요청하고, 긴 문장은 내용 누락 없이 자연스러운 한국어 문장으로 나누도록 지시한다. 하지만 **`/no_think`라는 문자열을 넣었다고 실제 서버의 thinking 기능이 꺼졌다고 자동으로 보증할 수는 없다.** 이 실험의 표시 코드는 시스템 메시지에 해당 문자열을 넣었고, 호출 인자에서 별도의 `think: false`를 전달하는 구현까지 확인된 것은 아니다. 사고 모드의 실제 작동 여부는 API 응답과 런타임 설정을 점검해야 한다. 생각을 끈 번역은 속도에 유리할 가능성이 있으나, 19세기 문체나 훼손된 원문의 해석까지 반드시 더 나아진다고 볼 수 없다. 또한 청크 상한을 800자로 지정해도 **원래 한 문단이 800자를 넘으면 그 문단을 그대로 한 청크에 넣는 구조**였다. 설정 숫자만으로 최악의 청크 길이를 보장하지 않는다는 뜻이다. ## 진행률은 '시도 중'이 아니라 '디스크에 저장됨'을 기준으로 한다 장시간 번역에서 중요한 파일은 화면의 퍼센트가 아니라 `progress.json`이다. 파이프라인은 모델 응답 하나를 받은 뒤 번역 청크 배열에 넣고 JSON 파일을 갱신한다. `saved progress: 13/151`이 보인다면 13번째 청크까지 저장했다는 뜻이다. 다음 청크 처리 도중 `Ctrl+C`로 종료되면 보통 마지막으로 저장한 청크 다음부터 다시 시도할 수 있다. 중단 처리를 위해 `KeyboardInterrupt`를 잡아 저장된 수량과 재개 명령을 출력하도록 구성했다. 실행 상태를 읽기 쉽게 하려고 `chunk 13/151 (8.6%)`, 마지막 청크 소요 시간, 이번 실행 경과 시간, 남은 청크 수, `--resume` 안내를 추가했다. 다만 '청크를 요청 중'이라는 로그는 완료가 아니다. `saved progress`가 찍히거나 진행 파일에 결과가 생겨야 성공을 확인할 수 있다. `03_ko.txt` 완성본은 통상 모든 청크가 끝난 뒤 생성되므로 초기 실행에 없을 수 있다. 재개 기능의 핵심 함정은 **인덱스가 정체성은 아니라는 것**이다. 기존 진행 파일에 3,500자 분할 기준의 결과가 15개 있다면, 800자 분할로 바꾼 뒤 그 15개를 새 배열의 첫 15개라고 간주할 수 없다. 한 청크라도 경계가 달라지면 이후의 원문·번역 대응이 무너진다. 번역 엔진, 프롬프트 형식, 전처리 결과를 바꿀 때에도 같은 위험이 있다. 그래서 새 설계나 새 모델의 비교 실험에는 별도의 출력 폴더를 쓰는 편이 안전하다. 코드에서는 원문 URL의 끝부분과 해시로 `translation_outputs/` 아래에 책별 디렉터리를 만들고, 이미 존재하는 `01_raw.txt`와 `02_clean_en.txt`는 재사용하도록 바꿨다. 다른 TXT URL을 주면 다른 책에도 원칙상 적용할 수 있다. 그러나 `--output-dir`를 임의로 재사용하거나 같은 이름의 폴더에서 엔진·설정을 바꾸면 여전히 진행 결과가 섞일 수 있다. **입력 해시, 전처리 버전, 청크 해시, 모델·옵션까지 기록해 불일치하면 재개를 거부하는 검증은 남은 과제**다. ## 노트북이 잠들 때와 번역이 멈출 때 로컬 모델을 실행한 MacBook이 밤사이 잠자기에 들어가면 터미널과 모델의 작업도 멈추거나 지연될 수 있다. 실험에서는 자리를 비운 동안 처리가 중단된 경험이 있었으나, 그것이 실제 잠자기 때문인지 요청 시간 초과·모델 상태 때문인지 단정할 로그 분석까지 수행하지는 않았다. Mac에서 시스템 잠자기를 방지하는 실행 예로 `caffeinate -i python3 translate_book.py --backend ollama --output-dir luddites-seg-v1`가 제안됐다. `Ctrl+C`는 저장된 청크를 버리는 명령이 아니라 현재 실행을 중단하는 명령이다. 단, 모델이 생성 중이던 **그 청크 자체**는 완료 기록으로 남지 않을 수 있다. `--resume`로 이어 갈 때는 같은 책, 출력 폴더, 청크 크기, 전처리와 번역 포맷을 사용해야 한다. 명령 예시는 `python3 translate_book.py --backend ollama --max-chars 800 --output-dir luddites-seg-v1 --resume`이다. 수정 중인 Python 파일을 저장해도 일반적인 `python3 translate_book.py` 프로세스가 자동 종료되거나 그 자리에서 새 함수로 갈아타는 것은 아니다. 실행 중인 프로세스는 보통 시작할 때 읽은 코드를 계속 쓰고, 저장한 변경은 **다음 실행**에 반영된다. 핫 리로드를 제공하는 별도 실행 환경은 예외다. ## HTML이 어긋난 진짜 이유: 빈 줄은 문단 ID가 아니다 읽기 불편을 줄이기 위해 좌우 병렬 HTML 뷰어를 만들었다. 영어와 한국어를 한 문단씩 나란히 놓고 검색·문단 번호 이동·앵커 링크를 제공한다. 완성본은 `05_bilingual_viewer.html`, 중간본은 `05_bilingual_partial_viewer.html`이다. 한 청크가 저장될 때마다 부분 번역 TXT·Markdown·HTML을 다시 생성하도록 확장했다. 브라우저에서 해당 HTML을 열고 새로고침하면 그때까지 저장된 결과를 읽을 수 있다. 문제는 화면의 CSS가 아니라 **번역 출력에서 문단을 복원하는 방식**이었다. 이전 파이프라인은 원문 문단을 합쳐 모델에 보낸 다음 한국어 출력의 빈 줄 `\n\n`를 기준으로 다시 쪼갰다. 모델이 제목을 다음 줄과 합치거나 긴 문단을 둘로 나누면 한국어 문단 수가 원문과 달라진다. 이후에는 제목과 본문이 한 칸씩 밀린 채 모두 다른 원문에 대응한다. 실제 중간 뷰어에서도 장 제목의 대응이 밀리고 `[TEXT END]` 같은 프롬프트 구분자가 출력에 남는 증상이 관찰됐다. 그에 대한 구조적 수정은 **세그먼트 식별자를 데이터로 취급하는 방식**이다. 원문을 `…`, `…`로 감싸 모델에 보내고, 태그를 바꾸거나 합치지 말라고 요청한다. 번역을 받은 뒤에는 빈 줄이 아니라 해당 번호의 여닫는 태그를 찾아 내용을 꺼낸다. 태그를 찾지 못하면 그 자리에서 `[SEGMENT_MISSING]`으로 표시한다. 누락이 표시된 문단은 원문이 한국어로 번역되었다는 뜻이 아니다. 이 방식은 **한 번의 누락으로 뒤쪽 전체가 어긋나는 문제**를 국소화한다. 반면 태그를 잘못 달았거나 어떤 태그 안에 다른 문단의 뜻을 번역했다면, 번호만 있다고 품질이 보장되지는 않는다. 현재 확인 가능한 Python 코드에서는 누락된 세그먼트를 실패로 간주해 자동 재요청하는 검증보다 표시용 자리표시자에 의존한다. 실용적인 완역 파이프라인이라면 세그먼트 개수·고유성·순서·빈 출력·출력 잘림·중복 여부를 검사하고, 실패한 청크를 별도 목록으로 재처리해야 한다. 이 변경이 반영된 뒤 새 HTML의 전체 정렬을 끝까지 검증했다는 결과는 확인되지 않았다. ## 로컬 번역과 구글 번역 사이의 스위치 '그냥 구글 번역으로 돌리면 되지 않는가'라는 질문은 전체 구조를 다시 보게 한다. 번역 엔진은 하나의 부품이고 **입력 확보·문단 식별·진행 파일·뷰어는 번역 엔진과 독립**되어야 한다. 따라서 `--backend ollama`와 `--backend google_cloud` 같은 선택을 두는 방향이 제안됐다. 공식 Google Cloud Translation API를 쓰는 방식은 원문 문단 목록을 한 번에 `q` 배열로 보내고, 영어 `en`에서 한국어 `ko`로 번역된 각 항목을 받는 것이다. 받아온 문자열을 해당 순서의 세그먼트에 다시 감싸면, LLM이 SEG 태그를 직접 지킬 필요가 없다. HTML 엔티티를 되돌리는 처리와 요청·응답 문단 수 비교도 필요하다. 비교 대상으로 거론된 API 엔드포인트는 `https://translation.googleapis.com/language/translate/v2`다. 공식 API는 구글 번역 웹페이지를 무료로 자동 긁는 방식이 아니며, 키·서비스 활성화·과금과 전송 조건을 따로 검토해야 한다. 비공식 `googletrans` 방식은 안정성과 사용 조건 면에서 별개의 선택지다. **구현 상태의 경계가 중요하다.** Google 번역 분기와 `--google-api-key`, `--source-lang`, `--target-lang`을 추가하는 코드는 제시됐지만, 편집 도구를 통한 수정은 실패했다고 보고되었다. 마지막으로 표시된 로컬 Python 파일에서는 번역 백엔드 선택지가 `ollama`와 `openai_compat`뿐이었다. 따라서 구글 번역 스위치가 완성되어 실제 실행됐다고 말할 수 없다. 재시도하려면 API 설정과 별도 폴더·진행 파일 호환성을 갖춰야 하며, 기존 로컬 번역 청크를 같은 파일에서 이어받아서는 안 된다. ## 무엇을 보면 완역본으로 믿을 수 있는가 겉보기에는 모든 청크가 끝나도 곧바로 '완역'은 아니다. 기계가 저장한 것은 모델 응답이다. 번역이 충실하다는 증거까지 저장한 것은 아니다. 최소한 다음을 구분해서 확인해야 한다. - **입력 완전성:** 표제·목차·본문·부록 가운데 어디를 번역 대상으로 삼았는지, 실제 시작·종료 지점이 어디인지. - **정렬 완전성:** 원문 문단 ID 수와 유효한 번역 세그먼트 ID 수가 같은지, `[SEGMENT_MISSING]`가 없는지. - **텍스트 완전성:** 프롬프트 구분자가 새어 나왔는지, 문장 중간에서 출력이 잘렸는지, 고유명사·날짜·인용이 유지됐는지. - **읽기 품질:** 19세기 역사 서술의 긴 문장, 지명, 계급·직종·법률 용어, 풍자나 인용의 뉘앙스가 어떻게 옮겨졌는지. - **판본·권리:** 공개된 스캔과 TXT, 원저작물, 후대 편집·주석, 생성한 번역문의 공개·배포 권한이 각각 어떻게 다른지. 품질 비교는 같은 원문 청크를 여러 엔진에 보내고 결과를 나란히 놓는 방식이 적절하다. 속도만 빠르거나 한국어만 유창한 번역이 반드시 더 충실한 번역은 아니다. 특히 오래된 OCR 텍스트가 깨졌을 때는 번역 엔진이 그럴듯한 뜻을 만들어 내는 위험도 확인해야 한다. 이러한 비교는 실제로 완료된 성능 시험이 아니라 후속 검수 설계다. [온라인 서점 미리보기와 판권면 수집](./book-preview-credit-sourcing.md)은 원문을 확보할 수 있다는 사실과 이를 반복적으로 저장·가공·공개할 수 있는 권리가 다르다는 문제를 다룬다. 고서 번역에서도 기술적으로 내려받을 수 있다는 사실만으로 한국어 완역본의 재배포 권리가 자동 결정되지는 않는다. ## 실행 예와 남겨진 개발 과제 표시된 Python 스크립트의 기본 실행 파일명은 `translate_book.py`이고, 필요한 외부 Python 패키지로 `requests`가 쓰였다. Ollama가 실행 중이며 설치 모델이 확인되면 `python3 translate_book.py --backend ollama --output-dir luddites-seg-v1`로 시작하고, 같은 분할·설정으로 `--resume`를 추가해 재개하는 구상이었다. 진행 중 뷰어의 주소는 `translation_outputs/luddites-seg-v1/05_bilingual_partial_viewer.html` 형식이다. 이 명령은 실제 운영 방식의 예이지, 저장소에 실행용 스크립트를 함께 게시했다는 뜻이 아니다. 완성도를 올리려면 단순히 '다음 번역 모델'을 찾기보다 다음 순서를 우선해야 한다. **진행 파일의 입력·설정 해시 고정 → 세그먼트 검증·실패 재시도 → 장·제목과 각주의 구조적 전처리 → 부분 뷰어의 누락 강조와 품질 비교 → 엔진별 별도 체크포인트 → DOCX·PDF·전자책 출력**이다. 특히 현재 부분 HTML은 매 청크마다 전체 누적 결과를 다시 만들어 쓰는 방식이어서, 책이 커지면 저장 비용 자체도 증가한다. 최종 상태를 구별하면, 로컬 Ollama 번역과 청크별 체크포인트·부분 HTML 저장은 **표시된 코드에 구현된 기능**이고, SEG 태그 복원은 **수정된 코드에서 확인된 방식**이다. 반면 SEG 방식의 책 전체 검수, 실측 성능 비교, Google Cloud 번역 선택의 실동작, 판본 정리와 최종 PDF 조판은 **완료가 확인되지 않은 과제**다. 책 번역 자동화의 목표는 '완료'라는 한 줄의 로그가 아니라, 어디가 아직 믿을 수 없는지까지 독자에게 드러나는 읽기 도구여야 한다.