From 8dcc0af15d393701988a048b7f7aab2bba4be68a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B2=BD=EB=AF=BC?= Date: Thu, 19 Mar 2026 09:48:25 +0900 Subject: [PATCH] Update step6_corpus.py --- .../converters/pipeline/step6_corpus.py | 212 +----------------- 1 file changed, 1 insertion(+), 211 deletions(-) diff --git a/03.Code/업로드용/converters/pipeline/step6_corpus.py b/03.Code/업로드용/converters/pipeline/step6_corpus.py index 1ca3d7e..f17a509 100644 --- a/03.Code/업로드용/converters/pipeline/step6_corpus.py +++ b/03.Code/업로드용/converters/pipeline/step6_corpus.py @@ -1,211 +1 @@ -# -*- coding: utf-8 -*- -from dotenv import load_dotenv -load_dotenv() -""" -make_corpus_v2.py - -기능: -- output/rag/*_chunks.json 내의 모든 요약문들을 모음 -- AI가 전체 도메인 목적(측량+지리정보)에 맞게 중복 제거 및 핵심 내용 추출(Compacting) -- 결과는 context/corpus.txt로 저장되며, 이는 전체 보고서의 밑바탕(Corpus)이 됨 -- chunk_and_summary.py 실행 후 생성된 *_chunks.json 파일이 있어야 함. -- domain_prompt.txt가 존재해야 함. -""" -import os -import sys -import json -from pathlib import Path -from datetime import datetime -from openai import OpenAI - -# ===== OpenAI 설정 ===== -OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY") -GPT_MODEL = "gpt-5-2025-08-07" -client = OpenAI(api_key=OPENAI_API_KEY) - -# ===== 추출 설정 ===== -BATCH_SIZE = 80 # 한 번에 처리할 요약문 개수 (API 입력 한계 고려) -MAX_CHARS_PER_BATCH = 1500 # 추출 결과 글자 수 제한 -MAX_FINAL_CHARS = 12000 # 최종 corpus 글자 수 - - -def log(msg: str): - print(msg, flush=True) - with (LOG_DIR / "make_corpus_log.txt").open("a", encoding="utf-8") as f: - f.write(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}\n") - - -def load_domain_prompt() -> str: - p = CONTEXT_DIR / "domain_prompt.txt" - if not p.exists(): - log("domain_prompt.txt가 없습니다. 먼저 step1_domainprompt.py를 실행하십시오.") - sys.exit(1) - return p.read_text(encoding="utf-8", errors="ignore").strip() - - -def load_all_summaries() -> list: - """모든 청크의 요약문을 수집""" - summaries = [] - rag_files = sorted(RAG_DIR.glob("*_chunks.json")) - - if not rag_files: - log("RAG 파일이 없습니다. 먼저 chunk_and_summary.py를 실행하십시오.") - sys.exit(1) - - for f in rag_files: - try: - units = json.loads(f.read_text(encoding="utf-8", errors="ignore")) - except Exception as e: - log(f"[WARN] RAG 로드 실패: {f.name} | {e}") - continue - - for u in units: - summ = (u.get("summary") or "").strip() - source = (u.get("source") or "").strip() - keywords = (u.get("keywords") or "") - - if summ: - # 출처 정보 포함 - entry = f"[{source}] {summ}" - if keywords: - entry += f" (키워드: {keywords})" - summaries.append(entry) - - return summaries - - -def compress_batch(domain_prompt: str, batch: list, batch_num: int, total_batches: int) -> str: - """배치 단위로 요약문들을 AI가 압축""" - - batch_text = "\n".join([f"{i+1}. {s}" for i, s in enumerate(batch)]) - - prompt = f""" -다음은 문서에서 추출된 요약들입니다. (배치 {batch_num}/{total_batches}) -도메인 프롬프트를 참고하여, 이 문서들의 핵심 지식과 사실 정보들을 압축하여 정리하십시오. - -규칙: -1) 중복되거나 유사한 내용은 하나로 통합하며 사실 관계를 명확히 함 -2) domain_prompt에 있는 주요 전문 용어나 기준(측량 기법 등)은 반드시 포함 -3) 수치 데이터(정확도, 기준점 번호 등)는 보존 -4) 과거 이력/처리 단계별 특징 등 정보성 있는 내용 중심 -5) 결과는 한글로 작성하며 글자 수는 {MAX_CHARS_PER_BATCH}자 이내로 제한 - -추출된 요약들: -{batch_text} - -출력은 다른 설명 없이 정리된 텍스트만 작성하십시오. -""" - - try: - resp = client.chat.completions.create( - model=GPT_MODEL, - messages=[ - {"role": "system", "content": domain_prompt + "\n\n당신은 문서 요약들을 주제별로 압축 정리하는 전문가입니다."}, - {"role": "user", "content": prompt} - ] - ) - result = resp.choices[0].message.content.strip() - log(f" 배치 {batch_num} 압축 완료 ({len(result)}자)") - return result - except Exception as e: - log(f"[ERROR] 배치 압축 실패: {e}") - # 실패 시 원본의 일부라도 반환 - return "\n".join(batch[:10]) - - -def merge_compressed_parts(domain_prompt: str, parts: list) -> str: - """압축된 배치 결과물들을 최종적으로 통합""" - - if len(parts) == 1: - return parts[0] - - all_parts = "\n\n---\n\n".join([f"[파트 {i+1}]\n{p}" for i, p in enumerate(parts)]) - - prompt = f""" -다음은 여러 파트로 나누어 압축된 문서 요약 결과들입니다. -이를 도메인 지식 기반의 통합 코퍼스(Corpus)로 만드십시오. - -통합 기준: -1) 도메인 전문가가 참고할 수 있는 백과사전식 기술 정보 중심 -2) domain_prompt에 부합하는 목적과 업무 흐름이 보이도록 구성 -3) 중복된 기술 설명은 최신/최고 사양 기준으로 정리 -4) 결과물은 총 {MAX_FINAL_CHARS}자 이내로 구성 - -출력은 주제별로 일목요연하게 정리된 최종 코퍼스 텍스트만 출력하십시오. -""" - - try: - resp = client.chat.completions.create( - model=GPT_MODEL, - messages=[ - {"role": "system", "content": domain_prompt + "\n\n당신은 전체 기술 코퍼스를 설계하는 정보 아키텍트입니다."}, - {"role": "user", "content": prompt} - ] - ) - return resp.choices[0].message.content.strip() - except Exception as e: - log(f"[ERROR] 최종 통합 실패: {e}") - return "\n\n".join(parts) - - -def main(input_dir, output_dir): - global DATA_ROOT, OUTPUT_ROOT, RAG_DIR, CONTEXT_DIR, LOG_DIR - DATA_ROOT = Path(input_dir) - OUTPUT_ROOT = Path(output_dir) - RAG_DIR = OUTPUT_ROOT / "rag" - CONTEXT_DIR = OUTPUT_ROOT / "context" - LOG_DIR = OUTPUT_ROOT / "logs" - for d in [RAG_DIR, CONTEXT_DIR, LOG_DIR]: - d.mkdir(parents=True, exist_ok=True) - log("=" * 60) - log("Corpus 생성 작업 시작 (AI 압축 방식)") - log("=" * 60) - - # 도메인 프롬프트 로드 - domain_prompt = load_domain_prompt() - log(f"도메인 프롬프트 로드 완료 ({len(domain_prompt)}자)") - - # 모든 요약문 수집 - summaries = load_all_summaries() - if not summaries: - log("요약 데이터가 없습니다. 먼저 chunk_and_summary.py를 실행하십시오.") - sys.exit(1) - - log(f"총 요약문 개수: {len(summaries)}개") - - # 1단계: 배치 압축 - compressed_parts = [] - total_batches = (len(summaries) + BATCH_SIZE - 1) // BATCH_SIZE - log(f"\n배치 압축 시작 ({BATCH_SIZE}개씩 {total_batches}개 배치)...") - - for i in range(total_batches): - batch = summaries[i*BATCH_SIZE : (i+1)*BATCH_SIZE] - part = compress_batch(domain_prompt, batch, i+1, total_batches) - compressed_parts.append(part) - - # 2단계: 최종 통합 - log(f"\n최종 통합 시작 ({len(compressed_parts)}개 파트)...") - final_corpus = merge_compressed_parts(domain_prompt, compressed_parts) - - # 저장 - out_path = CONTEXT_DIR / "corpus.txt" - out_path.write_text(final_corpus, encoding="utf-8") - - # 통계 출력 - raw_corpus_len = sum(len(s) for s in summaries) - log("\n" + "=" * 60) - log("Corpus 생성 완료!") - log("=" * 60) - log(f"전체 요약문: {len(summaries)}개 ({raw_corpus_len}자)") - log(f"최종 Corpus: {len(final_corpus)}자") - log(f"압축률: {100 - (len(final_corpus) / raw_corpus_len * 100):.1f}%") - log(f"\n결과 저장 위치:") - log(f" - 원본 백업: {CONTEXT_DIR / 'corpus_raw.txt'}") - log(f" - 최종 Corpus: {out_path}") - - # 원본 백업 (디버깅용) - (CONTEXT_DIR / "corpus_raw.txt").write_text("\n".join(summaries), encoding="utf-8") - - -if __name__ == "__main__": - main() +IyAtKi0gY29kaW5nOiB1dGYtOCAtKi0NCmZyb20gZG90ZW52IGltcG9ydCBsb2FkX2RvdGVudg0KbG9hZF9kb3RlbnYoKQ0KIiIiDQptYWtlX2NvcnB1c192Mi5weQ0KDQrquLDriqU6DQotIG91dHB1dC9yYWcvKl9jaHVua3MuanNvbiDsl5DshJwg66qo65OgIOyyre2BrOydmCBzdW1tYXJ566W8IOuqqOyVhA0KLSBBSeqwgCBDRUwg66qp7KCBKOq1kOycoSvsnpDsgqzshpTro6jshZgg7ZmN67O0KeyXkCDrp57qsowg7JWV7LaVIOyGLEmI66m0DQotIOykkeyalO2VnCDqsbQgW+2VteyLrF0g7ZGc7IucDQotIOqysOqzvOulvCBvdXRwdXQvY29udGV4dC9jb3JwdXMudHh0IOuhnCDsoIDsnqUNCg0K7KCE7KCcOg0KLSBjaHVua19hbmRfc3VtbWFyeS5weSDsi6Ttlokg7ZuEICpfY2h1bmtzLmpzb24g65Ok7J20IOyhtOyerO2VtOyVvCDtlZzri6QuDQotIGRvbWFpbl9wcm9tcHQudHh06rCAIOyhtOyerO2VtOyVvCDtlZzri6QuDQoiIiINCg0KaW1wb3J0IG9zDQppbXBvcnQgc3lzDQppbXBvcnQganNvbg0KZnJvbSBwYXRobGliIGltcG9ydCBQYXRoDQpmcm9tIGRhdGV0aW1lIGltcG9ydCBkYXRldGltZQ0KZnJvbSBvcGVuYWkgaW1wb3J0IE9wZW5BSQ0KDQojID09PT09IE9wZW5BSSDshKTsoJUgPT09PT0NCk9QRU5BSV9BUElfS0VZID0gb3MuZW52aXJvbi5nZXQoIk9QRU5BSV9BUElfS0VZIikNCkdQVF9NT0RFTCAgICAgID0gImdwdC01LTIwMjUtMDgtMDciDQoNCmNsaWVudCA9IE9wZW5BSShhcGlfa2V5PU9QRU5BSV9BUElfS0VZKQ0KDQojID09PT09IOyVley2lSDshKTsoJUgPT09PT0NCkJBVENIX1NJWkUgPSA4MCAgIyDtlZwg67KI7JeQIOyymOumrO2VoCDsmpTslb0g6rCc7IiYDQpNQVhfQ0hBUlNfUEVSX0JBVENIID0gMzAwMCAgIyDrsLDsuZjri7kg7JWV7LaVIOqysOqzvCDquIDsnpDsiJgNCk1BWF9GSU5BTF9DSEFSUyA9IDgwMDAgICMg7LWc7KKFIGNvcnB1cyDquIDsnpDsiJgNCg0KDQpkZWYgbG9nKG1zZzogc3RyKToNCiAgICBwcmludChtc2csIGZsdXNoPVRydWUpDQogICAgd2l0aCAoTE9HX0RJUiAvICJtYWtlX2NvcnB1c19sb2cudHh0Iikub3BlbigiYSIsIGVuY29kaW5nPSJ1dGYtOCIpIGFzIGY6DQogICAgICAgIGYud3JpdGUoZiJbe2RhdGV0aW1lLm5vdygpLnN0cmZ0aW1lKCclSDolTTolUycpfV0ge21zZ31cbiIpDQoNCg0KZGVmIGxvYWRfZG9tYWluX3Byb21wdCgpIC0+IHN0cjoNCiAgICBwID0gQ09OVEVYVF9ESVIgLyAiZG9tYWluX3Byb21wdC50eHQiDQogICAgaWYgbm90IHAuZXhpc3RzKCk6DQogICAgICAgIGxvZygiZG9tYWluX3Byb21wdC50eHTqsIAg7JeG7Iq164uI64ukLiDrqLzsoIAgc3RlcDHsnYQg7Iuk7ZaJ7ZW07JW8IO2VqeuLiOuLpC4iKQ0KICAgICAgICBzeXMuZXhpdCgxKQ0KICAgIHJldHVybiBwLnJlYWRfdGV4dChlbmNvZGluZz0idXRmLTgiLCBlcnJvcnM9Imlnbm9yZSIpLnN0cmlwKCkNCg0KDQpkZWYgbG9hZF9hbGxfc3VtbWFyaWVzKCkgLT4gbGlzdDoNCiAgICAiIiLrqqjrk6Ag7LKt7YGs7J2YIHN1bW1hcnkgKyDstpzsspgg7KCV67O0IOyImOynkSIiIg0KICAgIHN1bW1hcmllcyA9IFtdDQogICAgcmFnX2ZpbGVzID0gc29ydGVkKFJBR19ESVIuZ2xvYigiKl9jaHVua3MuanNvbiIpKQ0KICAgIA0KICAgIGlmIG5vdCByYWdfZmlsZXM6DQogICAgICAgIGxvZygiUkFHIO2MjOydvCgqX2NodW5rcy5qc29uKeydtCDsl4bsirXri4jri6QuIOuovOyggCBjaHVua19hbmRfc3VtbWFyeS5weeulvCDsi6TtlontlbTslbwg7ZWp64uI64ukLiIpDQogICAgICAgIHN5cy5leGl0KDEpDQoNCiAgICBmb3IgZiBpbiByYWdfZmlsZXM6DQogICAgICAgIHRyeToNCiAgICAgICAgICAgIHVuaXRzID0ganNvbi5sb2FkcyhmLnJlYWRfdGV4dChlbmNvZGluZz0idXRmLTgiLCBlcnJvcnM9Imlnbm9yZSIpKQ0KICAgICAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgICAgICBsb2coZiJbV0FSTl0gUkFHIO2MjOydvCDsnb3quLAg7Iuk7YyoOiB7Zi5uYW1lfSB8IHtlfSIpDQogICAgICAgICAgICBjb250aW51ZQ0KDQogICAgICAgIGZvciB1IGluIHVuaXRzOg0KICAgICAgICAgICAgc3VtbSA9ICh1LmdldCgic3VtbWFyeSIpIG9yICIiKS5zdHJpcCgpDQogICAgICAgICAgICBzb3VyY2UgPSAodS5nZXQoInNvdXJjZSIpIG9yICIiKS5zdHJpcCgpDQogICAgICAgICAgICBrZXl3b3JkcyA9ICh1LmdldCgia2V5d29yZHMiKSBvciAiIikNCiAgICAgICAgICAgIA0KICAgICAgICAgICAgaWYgc3VtbToNCiAgICAgICAgICAgICAgICAjIOy2nOyymOyZgCDtgqTsm4zrk5wg7Y+s7ZWoDQogICAgICAgICAgICAgICAgZW50cnkgPSBmIlt7c291cmNlfV0ge3N1bW19Ig0KICAgICAgICAgICAgICAgIGlmIGtleXdvcmRzOg0KICAgICAgICAgICAgICAgICAgICBlbnRyeSArPSBmIiAo7YKk7JuM65OcOiB7a2V5d29yZHN9KSINCiAgICAgICAgICAgICAgICBzdW1tYXJpZXMuYXBwZW5kKGVudHJ5KQ0KDQogICAgcmV0dXJuIHN1bW1hcmllcw0KDQoNCmRlZiBjb21wcmVzc19iYXRjaChkb21haW5fcHJvbXB0OiBzdHIsIGJhdGNoOiBsaXN0LCBiYXRjaF9udW06IGludCwgdG90YWxfYmF0Y2hlczogaW50KSAtPiBzdHI6DQogICAgIiIi67Cw7LmYIOuLqOychOuhnCDsmpTslb3rk6TsnYQgQUnqsIAg7JWV7LaVIiIiDQogICAgDQogICAgYmF0Y2hfdGV4dCA9ICJcbiIuam9pbih[ZiJ7aSsxfS4ge3N9IiBmb3IgaSwgcyBpbiBlbnVtZXJhdGUoYmF0Y2gpXSkNCiAgICANCiAgICBwcm9tcHQgPSBmIiIiDQrslYTrnpjripQg66y47ISc7JeQ7IScIOy2lOy2nO2VnCDsmpTslb0ge2xlbihiYXRjaCl96rCc7J2064ukLiAo67Cw7LmYIHtiYXRjaF9udW19L3t0b3RhbF9iYXRjaGVzfSkNCg0KW+yalOyVvSDrqqnroZ1dDQp7YmF0Y2hfdGV4dH0NCg0K64uk7J2MIOq4sOykgOycvOuhnCDsnbQg7JqU7JW965Ok7J2EIOyVley2lSDsoJXrpqztlZjrnbw6DQoNCjEpIOykkeuztS/snKDsgqwg64K07JqpOiDtlZjrgpjroZwg7Ya17ZWp7ZWY65CYLCDsl6zrn6wg66y47ISc7JeQ7IScIOyWuOq4ieuQmOuptCAiKE7tmowg7Ja46riJKSIg7ZGc7IucDQoyKSBkb21haW5fcHJvbXB07JeQIOuqheyLnOuQnCDtlbXsi6wg7IaU66Oo7IWYL+yLnOyKpO2FnDog67CY65Oc7IucIOuztOyhtO2VmOqzoCBb7IaU66Oo7IWYXSDtkZzsi5wNCjMpIGRvbWFpbl9wcm9tcHTsnZgg66qp7KCB7JeQIOykkeyalO2VnCDrgrTsmqkg7Jqw7ISgIOuztOyhtDoNCiAgIC0g7ZW064u5IOu2hOyVvOydmCDquLDstIgg6rCc64WQDQogICAtIOq4sOyhtCDrsKnsi53snZgg7ZWc6rOE7KCQ6rO8IOusuOygnOygkA0KICAgLSDsg4jroZzsmrQg6riw7IigL+uwqeyLneydmCDsnqXsoJANCjQpIOuLqOyInCDrgpjsl7Qv7KCI7LCo66eMIOyeiOuKlCDrgrTsmqk6IOqzvOqwkO2eiCDstpXslb0NCjUpIO2drOq3gO2VmOyngOunjCDtlbXsi6zsoIHsnbgg7J247IKs7J207Yq4OiBb7ZW17IusXSDtkZzsi5wNCg0K7Lac66ClIO2YleyLnToNCi0g7KO87KCc67OE66GcIOq3uOujue2VkQ0KLSDqsIEg7ZWt66qp7J2AIDF+MuusuOyepeycvOuhnCDqsITqsrDtlZjqsowNCi0g7KCE7LK0IHtNQVhfQ0hBUlNfUEVSX0JBVENIfeyekCDsnbTrgrQNCi0g66eI7YGs64uk7Jq0IOyXhuydtCDsiJzsiJgg7YWN7Iqk7Yq466GcDQoiIiINCiAgICANCiAgICB0cnk6DQogICAgICAgIHJlc3AgPSBjbGllbnQuY2hhdC5jb21wbGV0aW9ucy5jcmVhdGUoDQogICAgICAgICAgICBtb2RlbD1HUFRfTU9ERUwsDQogICAgICAgICAgICBtZXNzYWdlcz1bDQogICAgICAgICAgICAgICAgeyJyb2xlIjogInN5c3RlbSIsICJjb250ZW50IjogZG9tYWluX3Byb21wdCArICJcblxu64SI64qUIOusuOyEnCDsmpTslb3snYQg7KO87KCc67OE66GcIOyVley2lSDsoJXrpqztlZjripQg7KCE66y46rCA7J2064ukLiJ9LA0KICAgICAgICAgICAgICAgIHsicm9sZSI6ICJ1c2VyIiwgImNvbnRlbnQiOiBwcm9tcHR9DQogICAgICAgICAgICBdDQogICAgICAgICkNCiAgICAgICAgcmVzdWx0ID0gcmVzcC5jaG9pY2VzWzBdLm1lc3NhZ2UuY29udGVudC5zdHJpcCgpDQogICAgICAgIGxvZyhmIiAgICDrsLDsuZgge2JhdGNoX251bX0ve3RvdGFsX2JhdGNoZXN9IOyVley2lSDsmYTro4wgKHtsZW4ocmVzdWx0KX3snpApIikNCiAgICAgICAgcmV0dXJuIHJlc3VsdA0KICAgIGV4Y2VwdCBFeGNlcHRpb24gYXMgZToNCiAgICAgICAgbG9nKGYiW0VSUk9SXSDrsLDsuZgge2JhdGNoX251bX0g7JWV7LaVIOyLpO2MqDoge2V9IikNCiAgICAgICAgIyDsi6TtjKg7IucIOybkOuzuCDsnbzrtoAg67CY7ZmYDQogICAgICAgIHJldHVybiAiXG4iLmpvaW4oYmF0Y2hbOjEwXSkNCg0KDQpkZWYgbWVyZ2VfY29tcHJlc3NlZF9wYXJ0cyhkb21haW5fcHJvbXB0OiBzdHIsIHBhcnRzOiBsaXN0KSAtPiBzdHI6DQogICAgIiIi67Cw7LmY67OEIOyVley2lSDqsrDqs7zrpbwg7LWc7KKFIO2Gte2VqSIiIg0KICAgIA0KICAgIGlmIGxlbihwYXJ0cykgPT0gMToNCiAgICAgICAgcmV0dXJuIHBhcnRzWzBdDQogICAgDQogICAgYWxsX3BhcnRzID0gIlxuXG4tLS1cblxuIi5qb2luKFtmIlvtjIztirgge2krMX1dXG57cH0iIGZvciBpLCBwIGluIGVudW1lcmF0ZShwYXJ0cyldKQ0KICAgIA0KICAgIHByb21wdCA9IGYiIiINCuyVhOuemOuKlCDrjIDrn4nsnZgg66y47IScIOyalOyVveydhCDrsLDsuZjrs4TroZwg7JWV7LaV7ZWcIOqysOqzvOydtOuLpC4NCuydtOqyg+ydhCDstZzsooUgY29ycHVz66GcIO2Gte2Vqe2VmOudvC4NCg0KW+uwsOy5mOuzhCDslZXstpUg6rKw6rO8XQ0Ke2FsbF9wYXJ0c30NCg0K7Ya17ZWpIOq4sOykgDoNCjEpIO2MjO2KuCDqsIQg7KSR67O1IOuCtOyaqSDsoJzqsbAg67CPIO2Gte2VqQ0KMikgZG9tYWluX3Byb21wdOyXkCDrqoXsi5zrkJwg66qp7KCB6rO8IO2dkOumhOyXkCDrp57qsowg7J6s6rWs7ISxDQozKSBb7IaU66Oo7IWYXSwgW+2VteyLrF0sIChO7ZqMIOyWuOq4iSkg7ZGc7Iuc64qUIOycoOyngA0KNCkg7KCE7LK0IHtNQVhfRklOQUxfQ0hBUlNfUEVSX0JBVENIfeyekCDsnbTrgrQNCg0K7Lac66ClOiDyjvOygnOuzhOuhnCDsoJXrpqzrkJwg7LWc7KKFIGNvcnB1cyAo66eI7YGs64uk7Jq0IOyXhuydtCkNCiIiIg0KICAgIA0KICAgIHRyeToNCiAgICAgICAgcmVzcCA9IGNsaWVudC5jaGF0LmNvbXBsZXRpb25zLmNyZWF0ZSgNCiAgICAgICAgICAgIG1vZGVsPUdQVF9NT0RFTCwNCiAgICAgICAgICAgIG1lc3NhZ2VzPVsNCiAgICAgICAgICAgICAgICB7InJvbGUiOiAic3lzdGVtIiwgImNvbnRlbnQiOiBkb21haW5fcHJvbXB0ICsgIlxuXG7rhIjripQgQ0VMIOq1kOycoSDsvZjthZDsuKAg6riw7ZqN7J2EIOychO2VnCBjb3JwdXPrpbwg7ISk6rOE7ZWY64qUIOyghOusuOqwgOydtOuLpC4ifSwNCiAgICAgICAgICAgICAgICB7InJvbGUiOiAidXNlciIsICJjb250ZW50IjogcHJvbXB0fQ0KICAgICAgICAgICAgXQ0KICAgICAgICApDQogICAgICAgIHJldHVybiByZXNwLmNob2ljZXNbMF0ubWVzc2FnZS5jb250ZW50LnN0cmlwKCkNCiAgICBleGNlcHQgRXhjZXB0aW9uIGFzIGU6DQogICAgICAgIGxvZyhmIltFUlJPUl0g7LWc7KKFIO2Gte2VqSDsi6TtjKg6IHtlfSIpDQogICAgICAgIHJldHVybiAiXG5cbiIuam9pbihwYXJ0cykNCg0KDQpkZWYgbWFpbihpbnB1dF9kaXIsIG91dHB1dF9kaXIpOg0KICAgIGdsb2JhbCBEQVRBX1JPT1QsIE9VVFBVVF9ST09ULCBSQUdfRElSLCBDT05URVhUX0RJUiwgTE9HX0RJUg0KICAgIERBVEFfUk9PVCAgID0gUGF0aChpbnB1dF9kaXIpDQogICAgT1VUUFVUX1JPT1QgPSBQYXRoKG91dHB1dF9kaXIpDQogICAgUkFHX0RJUiAgICAgPSBPVVRQVVRfUk9PVCAvICJyYWciDQogICAgQ09OVEVYVF9ESVIgPSBPVVRQVVRfUk9PVCAvICJjb250ZXh0Ig0KICAgIExPR19ESVIgICAgID0gT1VUUFVUX1JPT1QgLyAibG9ncyINCiAgICBmb3IgZCBpbiBbUkFHX0RJUiwgQ09OVEVYVF9ESVIsIExPR19ESVJdOg0KICAgICAgICBkLm1rZGlyKHBhcmVudHM9VHJ1ZSwgZXhpc3Rfb2s9VHJ1ZSkNCiAgICBsb2coIj0iICogNjApDQogICAgbG9nKCJjb3JwdXMg7IOd7ISxIOy5nOyekCAoQUkg7JWV7LaVIOuyhOyghCkiKQ0KICAgIGxvZygiPSIgKiA2MCkNCg0KICAgICMgZG9tYWluX3Byb21wdCDroZzrk5wNCiAgICBkb21haW5fcHJvbXB0ID0gbG9hZF9kb3RhaW5fcHJvbXB0KCkNCiAgICBsb2coZiJkb21haW5fcHJvbXB0IOuhnOuTnCDsmYTro4wgKHtsZW4oZG9tYWluX3Byb21wdCl97J6Q64uk7J2064ukKSIpDQoNCiAgICAjIHN1bW1hcnmrl6Qg66qo7Jy86riwDQogICAgc3VtbWFyaWVzID0gbG9hZF9hbGxfc3VtbWFyaWVzKCkNCiAgICBpZiBub3Qgc3VtbWFyaWVzOg0KICAgICAgICBsb2coIuyYpCDtmZXsi53tlaAg7JqU7JW97J20IOyXhuqyvOuCmCDsl4bsirXri4jri6QuIikNCiAgICAgICAgc3lzLmV4aXQoMSkNCg0KICAgIGxvZyhmIuS9lCDsmpTslb0g67Cw7LmYIOyekeyXhSDsi5zsnpQuIOyalOyVvCDqsJzsi6Q6IHtsZW4oc3VtbWFyaWVzKX3qsJwiKQ0KDQogICAgIyDsnbTrtoAg7KCA7J6lIChrsLHslYUpDQogICAgcmF3X2NvcnB1cyA9ICJcbiIuam9pbihzdW1tYXJpZXMpDQogICAgcmF3X3BhdGggPSBDT05URVhUX0RJUiAvICJjb3JwdXNfcmF3LnR4dCINCiAgICByYXdfcGF0aC53cml0ZV90ZXh0KHJhd19jb3JwdXMsIGVuY29kaW5nPSJ1dGYtOCIpDQogICAgbG9nKGYi7JuQ67O0IGNvcnB1cyDrsoHslYUpOiB7cmF3X3BhdGh9ICh7bGVuKHJhd19jb3JwdXMpfX3snpApIikNCg0KICAgICMg67Cw7LmYIOyVley2lQ0KICAgIHRvdGFsX2JhdGNoZXMgPSAobGVuKHN1bW1hcmllcykgKyBCQVRDSF9TSVpFIC0gMSkgLy8gQkFUQ0hfU0laRQ0KICAgIGxvZyhmIlxu67Cw7LmYIOyVley2lCDsi5zsnpEgKHtCQVRDSF9TSVpFfWrqsJzsi6QsIOyjvCB7dG90YWxfYmF0Y2hlc33rsrIpIikNCg0KICAgIGNvbXByZXNzZWRfcGFydHMgPSBbXQ0KICAgIGZvciBpIGluIHJhbmdlKDAsIGxlbihzdW1tYXJpZXMpLCBCQVRDSF9TSVpFKToNCiAgICAgICAgYmF0Y2ggPSBzdW1tYXJpZXNbaTppK0JBVENIX1NJWkVdDQogICAgICAgIGJhdGNoX251bSA9IChpIC8vIEJBVENIX1NJWkUpICsgMQ0KDQogICAgICAgIGNvbXByZXNzZWQgPSBjb21wcmVzc19iYXRjaChkb21haW5fcHJvbXB0LCBiYXRjaCwgYmF0Y2hfbnVtLCB0b3RhbF9iYXRjaGVzKQ0KICAgICAgICBjb21wcmVzc2VkX3BhcnRzLmFwcGVuZChjb21wcmVzc2VkKQ0KDQogICAgIy stZooo7Yte2VqQ0KICAgIGxvZyhmIlxu7LWc7KKFIO2Gte2VqSDsi5zsnpEgKHtsZW4oY29tcHJlc3NlZF9wYXJ0cyl96rCcIO2MjO2KruInKQ0KICAgIGZpbmFsX2NvcnB1cyA9IG1lcmdlX2Nvcm1wcmVzc2VkX3BhcnRzKGRvbWFpbl9wcm9tcHQsIGNvbXByZXNzZWRfcGFydHMpDQoNCiAgICAjIOyGjeuToCDsoIDsnqUNCiAgICBvdXRfcGF0aCA9IENPTlRFWFRfRElSIC8gImNvcnB1cy50eHQiDQogICAgb3V0X3BhdGgud3JpdGVfdGV4dChmaW5hbF9jb3JwdXMsIGVuY29kaW5nPSJ1dGYtOCIpDQoNCiAgICAjIO2GteqzhA0KICAgIGxvZyhmIlxuIiArICI9IiAqIDYwKQ0KICAgIGxvZygiY29ycHVzIOyDneyFvCDsmYTro4whIikNCiAgICBsb2coIj0iICogNjApDQogICAgbG9nKGYi7KO8IOyalOyVvCDqsJzsi6Q6IHtsZW4oc3VtbWFyaWVzKX3qsJwgKHtsZW4ocmF3X2NvcnB1cyl97J6QKSIpDQogICAgbG9nKGYi7JWV7LaVIGNvcnB1czoge2xlbihmaW5hbF9jb3JwdXMpfX3snpApIikNCiAgICBsb2coZirslVfntpXri6ToIHsxMDAgLSAobGVuKGZpbmFsX2NvcnB1cykgLyBsZW4ocmF3X2NvcnB1cykgKiAxMDApOi4xZn0lIikNCiAgICBsb2coZlxu7KCA7J6lIOyXhuuovDoiKQ0KICAgIGxvZyhmIiAgLSAm07O0OiB7cmF3X3BhdGh9IikNCiAgICBsb2coZiIgIC0g7JWV7LaVOiB7b3V0X3BhdGh9IikNCg0KDQppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOg0KICAgIG1haW4oKQ0K \ No newline at end of file