diff --git a/README.md b/README.md index 191d5ff..8dd5774 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ curl -s -X POST http://localhost:26000/paperless/hook \ ### Paperless 배치 동기화(`/paperless/sync`) ### 문서 파이프라인(`/pipeline/ingest`) -첨부 문서(텍스트가 준비된 상태: OCR/추출 선행) → 벡터 임베딩 → 번역(옵션) → HTML 생성까지 처리합니다. +첨부 문서(텍스트가 준비된 상태: OCR/추출 선행) → (옵션)요약 → (옵션)번역 → 임베딩 → HTML 생성까지 처리합니다. ```bash curl -s -X POST http://localhost:26000/pipeline/ingest \ @@ -225,13 +225,17 @@ curl -s -X POST http://localhost:26000/pipeline/ingest \ "text": "(여기에 문서 텍스트)", "generate_html": true, "translate": true, - "target_language": "ko" + "target_language": "ko", + "summarize": false, + "summary_sentences": 5, + "summary_language": null }' ``` 응답에 `html_path`가 포함됩니다. -- 번역 켜짐(`translate=true`): 번역본이 `outputs/html/.html`로 생성되고, 번역문이 인덱스에 추가됩니다. -- 번역 꺼짐(`translate=false`): 원문으로 HTML만 생성되고, 원문 텍스트가 인덱스에 추가됩니다. +- 요약 켜짐(`summarize=true`): 청크별 요약 후 통합 요약을 생성해 사용(기본 5문장). `summary_language`로 요약 언어 선택 가능(기본 번역 언어와 동일, 번역 off면 ko). +- 번역 켜짐(`translate=true`): 최종 텍스트를 대상 언어로 번역해 HTML+인덱스화. +- 번역 꺼짐(`translate=false`): 최종 텍스트(요약 또는 원문)로 HTML+인덱스화. 파일 업로드 버전(`/pipeline/ingest_file`): `.txt`/`.pdf` 지원 diff --git a/server/main.py b/server/main.py index 0504e29..e7ef2a5 100644 --- a/server/main.py +++ b/server/main.py @@ -63,6 +63,9 @@ class PipelineIngestRequest(BaseModel): generate_html: bool = True translate: bool = True target_language: str = "ko" + summarize: bool = False + summary_sentences: int = 5 + summary_language: str | None = None @app.get("/health") @@ -171,6 +174,9 @@ def pipeline_ingest(req: PipelineIngestRequest, _: None = Depends(require_api_ke generate_html=req.generate_html, translate=req.translate, target_language=req.target_language, + summarize=req.summarize, + summary_sentences=req.summary_sentences, + summary_language=req.summary_language, ) return {"status": "ok", "doc_id": result.doc_id, "added": result.added_chunks, "chunks": result.chunks, "html_path": result.html_path} diff --git a/server/pipeline.py b/server/pipeline.py index e451b86..55f0f40 100644 --- a/server/pipeline.py +++ b/server/pipeline.py @@ -25,6 +25,38 @@ class DocumentPipeline: self.output_dir = Path(output_dir) (self.output_dir / "html").mkdir(parents=True, exist_ok=True) + def summarize(self, parts: List[str], target_language: str = "ko", sentences: int = 5) -> List[str]: + summarized: List[str] = [] + sys_prompt = ( + "당신은 전문 요약가입니다. 핵심 내용만 간결하게 요약하세요." + ) + for p in parts: + if not p.strip(): + summarized.append("") + continue + messages = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": ( + f"다음 텍스트를 {target_language}로 {sentences}문장 이내로 핵심만 요약하세요. 불필요한 수식어는 제거하고, 중요한 수치/용어는 보존하세요.\n\n{p}" + )}, + ] + resp = self.ollama.chat(self.boost_model, messages, stream=False, options={"temperature": 0.2, "num_ctx": 32768}) + content = resp.get("message", {}).get("content") or resp.get("response", "") + summarized.append(content.strip()) + # 최종 통합 요약(선택): 각 청크 요약을 다시 결합해 더 짧게 + joined = "\n\n".join(s for s in summarized if s) + if not joined.strip(): + return summarized + messages2 = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": ( + f"아래 부분 요약들을 {target_language}로 {max(3, sentences)}문장 이내로 다시 한번 통합 요약하세요.\n\n{joined}" + )}, + ] + resp2 = self.ollama.chat(self.boost_model, messages2, stream=False, options={"temperature": 0.2, "num_ctx": 32768}) + content2 = resp2.get("message", {}).get("content") or resp2.get("response", "") + return [content2.strip()] + def translate(self, parts: List[str], target_language: str = "ko") -> List[str]: translated: List[str] = [] sys_prompt = ( @@ -58,9 +90,30 @@ h1{{font-size: 1.6rem; margin-bottom: 1rem;}} html_path.write_text(html, encoding="utf-8") return str(html_path) - def process(self, *, doc_id: str, text: str, index, generate_html: bool = True, translate: bool = True, target_language: str = "ko") -> PipelineResult: + def process( + self, + *, + doc_id: str, + text: str, + index, + generate_html: bool = True, + translate: bool = True, + target_language: str = "ko", + summarize: bool = False, + summary_sentences: int = 5, + summary_language: str | None = None, + ) -> PipelineResult: parts = chunk_text(text, max_chars=1200, overlap=200) - translated = self.translate(parts, target_language=target_language) if translate else parts + + if summarize: + # 요약 언어 기본값: 번역 언어와 동일, 번역 off면 ko로 요약(설정 없을 때) + sum_lang = summary_language or (target_language if translate else "ko") + summarized_parts = self.summarize(parts, target_language=sum_lang, sentences=summary_sentences) + working_parts = summarized_parts + else: + working_parts = parts + + translated = self.translate(working_parts, target_language=target_language) if translate else working_parts to_append: List[IndexRow] = [] for i, t in enumerate(translated): @@ -70,7 +123,7 @@ h1{{font-size: 1.6rem; margin-bottom: 1rem;}} html_path: str | None = None if generate_html: - title_suffix = "번역본" if translate else "원문" + title_suffix = "요약+번역본" if (summarize and translate) else ("요약본" if summarize else ("번역본" if translate else "원문")) html_path = self.build_html(doc_id, title=f"문서 {doc_id} ({title_suffix})", ko_text="\n\n".join(translated)) return PipelineResult(doc_id=doc_id, html_path=html_path, added_chunks=added, chunks=len(translated))