Spaces:
Running
Running
Download solar_eval/pipelines/rules.py from dev-strender/proofread-demo: direct link, hf CLI and curl.
- Browser
- Download file 19.5 kB
-
https://huggingface.co/spaces/dev-strender/proofread-demo/resolve/main/solar_eval/pipelines/rules.py
- Command line
-
hf download hf://spaces/dev-strender/proofread-demo/solar_eval/pipelines/rules.py
-
curl -L -o rules.py https://huggingface.co/spaces/dev-strender/proofread-demo/resolve/main/solar_eval/pipelines/rules.py
19.5 kB
| """multi_step ํ์ดํ๋ผ์ธ์ rule ์คํ ๊ตฌํ์ฒด. | |
| `multi_step.py` ๊ฐ 518์ค์ด๋ผ (`.claude/rules/code-style.md` ์ 400์ค ๊ถ์ฅ์ ์ | |
| ๋์ด) ๋ฃฐ ๊ตฌํ๋ง ๋ฐ๋ก ๋ฝ์๋๋ค. vocabulary/post_process/correction_filter ๋ | |
| ๋์์ ๋ฐ๊พธ์ง ์๊ณ `MultiStepPipeline._apply_*` ๋ฉ์๋์์ ๊ทธ๋๋ก ์ฎ๊ฒจ์จ | |
| ๊ฒ์ด๋ค โ ์์ ํ ์คํธ ๋ณํ ๋ก์ง์ด๋ผ `self` ์ํ๊ฐ ํ์ ์๋ค(`apply_vocabulary` | |
| ๋ง ์์ธ โ ๋ฐ์ดํฐ์ ์์ ๋ก๋ํ ์ดํ ์บ์๋ฅผ ์ธ์๋ก ๋ฐ๊ณ ๊ฐฑ์ ๋ ์บ์๋ฅผ ๋๋๋ ค์ค๋ค). | |
| `apply_judge_decisions`/`apply_change_rate_guard` ๋ v24 ๊ณ์ด ํฌํ ๋จ๊ณ(ยง5-F)์์ | |
| ์ถ๊ฐ๋๋ค. ์ ์๋ `EvalSample.artifacts` ๋ฅผ ์ฝ์ด์ผ ํด์(judge ์คํ ์ ๊ตฌ์กฐํ ๊ฒฐ์ , | |
| self-consistency ์ corrections ๋ชฉ๋ก) `sample` ์ ์ธ์๋ก ๋ฐ๋๋ค โ ๋ค๋ฅธ rule | |
| ํจ์๋ค๊ณผ ์๊ทธ๋์ฒ๊ฐ ๋ค๋ฅธ ์ ์ผํ ์ด์ ๋ค. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import re | |
| from pathlib import Path | |
| from typing import TYPE_CHECKING, Any | |
| from solar_eval.models.sample import EvalSample | |
| from solar_eval.pipelines.errors import KnowledgeAssetError | |
| if TYPE_CHECKING: | |
| from solar_eval.core.dataset_loader import DatasetLoader | |
| logger = logging.getLogger(__name__) | |
| def apply_vocabulary( | |
| text: str, | |
| config: dict[str, Any], | |
| vocabulary: list[dict[str, str]] | None, | |
| dataset_loader: DatasetLoader | None, | |
| config_dir: Path | None = None, | |
| ) -> tuple[str, list[dict[str, str]] | None]: | |
| """`vocabulary_sub`/`vocabulary_reapply` rule_type ๊ตฌํ. | |
| ํธ์ถ์(`VocabularyStep`)๊ฐ run ์๋ช ์บ์๋ฅผ ๋ค๊ณ ์์ผ๋ฏ๋ก, ์ต์ด ํธ์ถ ์ ๋ก๋ํ | |
| ์ดํ ๋ชฉ๋ก์ ๋ฐํ๊ฐ์ผ๋ก ๋๋๋ ค์ค ์บ์๋ก ์ ์ฅํ๊ฒ ํ๋ค. | |
| Args: | |
| config_dir: ๋ ํฌ์ config ์ ๋ณธ ๋๋ ํ ๋ฆฌ(`03-evaluation`). ์นํ ์ฌ์ ์ | |
| ๋ฐ์ดํฐ๊ฐ ์๋๋ผ **์ค์ **์ด๋ผ(๋ฐ๋๋ฉด ๊ฐ์ ๋ฌธ์ ๋ฅผ ๋ค๋ฅด๊ฒ ํผ๋ค) ์ฌ๊ธฐ๋ฅผ | |
| ๋จผ์ ์ฐพ๋๋ค. ๋ฐ์ดํฐ ๋ฃจํธ ๊ธฐ์ค ๋ ๊ฑฐ์ ์ ์ธ๋ ๊ณ์ ์ง์ํ๋ค. | |
| Returns: | |
| (์นํ๋ text, ๊ฐฑ์ ๋ vocabulary ์บ์) ํํ. | |
| Raises: | |
| KnowledgeAssetError: `vocab_dataset` ์ ์ ์ธํ๋๋ฐ ๋ชป ์ฝ์ ๋. | |
| """ | |
| if vocabulary is None: | |
| vocabulary = _load_vocabulary(config, dataset_loader, config_dir) | |
| for row in vocabulary: | |
| wrong = row.get("wrong", row.get("before", "")) | |
| correct = row.get("correct", row.get("after", "")) | |
| if wrong and correct: | |
| text = text.replace(wrong, correct) | |
| return text, vocabulary | |
| def _load_vocabulary( | |
| config: dict[str, Any], | |
| dataset_loader: DatasetLoader | None, | |
| config_dir: Path | None, | |
| ) -> list[dict[str, str]]: | |
| """์นํ ์ฌ์ ์ ๋ก๋ํ๋ค โ config ์ ๋ณธ ์ฐ์ , ๋ฐ์ดํฐ ๋ฃจํธ ํด๋ฐฑ. | |
| ์ ์ธ์ด ์์ผ๋ฉด ๋น ๋ชฉ๋ก(์ ์์ ์ธ no-op)์ด๊ณ , ์ ์ธํ๋๋ฐ ๋ชป ์ฝ์ผ๋ฉด ์์ธ๋ค. | |
| """ | |
| vocab_path = config.get("vocab_dataset", "") | |
| if not vocab_path: | |
| return [] | |
| searched: list[str] = [] | |
| # 1. config ์ ๋ณธ (๋ ํฌ์ 03-evaluation/) โ ์นํ ์ฌ์ ์ ์ค์ ์ด๋ค. | |
| if config_dir is not None: | |
| candidate = Path(config_dir) / vocab_path | |
| searched.append(str(candidate)) | |
| if candidate.is_file() and dataset_loader is not None: | |
| return dataset_loader.load_csv(str(config_dir), vocab_path, source="local") | |
| # 2. ๋ ๊ฑฐ์ ์ ์ธ (repo/source ๋ก ๋ฐ์ดํฐ ๋ฃจํธ๋ HF ๋ฅผ ๊ฐ๋ฆฌํค๋ ๊ฒฝ์ฐ) | |
| repo = config.get("repo", "") | |
| source = config.get("source", "huggingface") | |
| if dataset_loader is not None: | |
| searched.append(f"{source}:{repo}/{vocab_path}" if repo else f"{source}:{vocab_path}") | |
| try: | |
| return dataset_loader.load_csv(repo, vocab_path, source=source) | |
| except Exception as e: # noqa: BLE001 โ ์์ธ์ KnowledgeAssetError ์ ์ค์ด ๋ค์ ๋์ง๋ค | |
| logger.debug("vocabulary load failed (%s): %s", vocab_path, e) | |
| else: | |
| searched.append("(dataset_loader ์์)") | |
| raise KnowledgeAssetError(vocab_path, searched) | |
| def apply_pronoun_postprocess(text: str, replacements: dict[str, str]) -> str: | |
| """๋๋ช ์ฌ/์งํจ ์นํ + ํ๊ตญ์ด ์กฐ์ฌ ๊ต์ . | |
| ๋จ์ด๋ฅผ ์นํํ ๋(์: "์ด์ฌ๋ช ๋ํ" โ "์ด์ฌ๋ช ๋ํต๋ น") ๋ค๋ฐ๋ฅด๋ ์กฐ์ฌ๋ | |
| ์ ๋จ์ด์ ๋ฐ์นจ์ ๋ง์ถฐ ๋ฐ๊ฟ์ผ ํ๋ค. | |
| """ | |
| # ๋ชจ์์ผ๋ก ๋๋๋ ์กฐ์ฌ โ ์์์ผ๋ก ๋๋๋ ๋จ์ด ๋ค์ ๋ถ๋ ๋์ ์กฐ์ฌ | |
| correction_map = { | |
| "๋": "์", | |
| "๊ฐ": "์ด", | |
| "๋ฅผ": "์", | |
| "์": "๊ณผ", | |
| "๋ก": "์ผ๋ก", | |
| "์ฌ": "์ด์ฌ", | |
| "๋ผ": "์ด๋ผ", | |
| "๋": "์ด๋", | |
| "๋ค": "์ด๋ค", | |
| "์๋ค": "์ด์๋ค", | |
| "๋ผ๋ฉด": "์ด๋ผ๋ฉด", | |
| "๋ผ์": "์ด๋ผ์", | |
| "๋ก๋ถํฐ": "์ผ๋ก๋ถํฐ", | |
| } | |
| all_particles = ( | |
| list(correction_map.keys()) + list(correction_map.values()) + ["๋ง", "๋", "๊ป์"] | |
| ) | |
| particle_pattern = "|".join(re.escape(p) for p in sorted(all_particles, key=len, reverse=True)) | |
| for old, new in replacements.items(): | |
| regex = re.compile(f"({re.escape(old)})({particle_pattern})?") | |
| def replacer(m: re.Match, new_word: str = new) -> str: | |
| particle = m.group(2) | |
| if particle: | |
| return new_word + correction_map.get(particle, particle) | |
| return new_word | |
| text = regex.sub(replacer, text) | |
| return text | |
| def apply_currency_compact(text: str) -> str: | |
| """ํตํ ํํ์์ ์ซ์์ ๋จ์ ์ฌ์ด ๊ณต๋ฐฑ์ ์ ๊ฑฐํ๋ค.""" | |
| _unit = r"(์|๋ช |๊ฐ|๋|์์|๋ฌ๋ฌ|์ ๋ก|์|ํค|๊ถ|๊ฑด|ํ|์ฅ|๋ง๋ฆฌ|๊ทธ๋ฃจ|์ก์ด|ํ)" | |
| _tail = r"(?=[\s.,!?()\"\'๊ฐ-ํฃ]|$)" | |
| # "3์ต 5000๋ง ์" โ "3์ต5000๋ง์" | |
| text = re.sub( | |
| r"(\d+(?:\.\d+)?)(๋ง|์ต|์กฐ)\s*(\d+)\s*" + _unit + _tail, | |
| r"\1\2\3\4", | |
| text, | |
| ) | |
| # "3์ต ์" โ "3์ต์" | |
| text = re.sub( | |
| r"(\d+(?:\.\d+)?)(๋ง|์ต|์กฐ)\s*" + _unit + _tail, | |
| r"\1\2\3", | |
| text, | |
| ) | |
| return text | |
| def apply_comma_removal(text: str) -> str: | |
| """์ซ์ ์ฌ์ด์ ์ฒ๋จ์ ์ฝค๋ง๋ฅผ ์ ๊ฑฐํ๋ค (1,000 โ 1000).""" | |
| return re.sub(r"(?<=\d),(?=\d)", "", text) | |
| def apply_correction_filter(text: str, config: dict[str, Any], original: str = "") -> str: | |
| """Filter corrections by comparing with original text. | |
| Keeps only 'safe' corrections (spacing changes, small character diffs). | |
| Reverts larger changes that are likely unnecessary expression substitutions. | |
| Config options: | |
| max_char_diff: Maximum character differences to allow (default: 2) | |
| allow_spacing: Keep spacing-only changes (default: true) | |
| """ | |
| from solar_eval.evaluators.scoring import compute_lcs | |
| if not original: | |
| return text | |
| max_cd = config.get("max_char_diff", 2) | |
| allow_spacing = config.get("allow_spacing", True) | |
| orig_tokens = original.split() | |
| pred_tokens = text.split() | |
| if orig_tokens == pred_tokens: | |
| return text | |
| lcs_pairs = compute_lcs(orig_tokens, pred_tokens) | |
| def is_safe(o_text: str, c_text: str) -> bool: | |
| if allow_spacing and o_text.replace(" ", "") == c_text.replace(" ", ""): | |
| return True | |
| if abs(len(o_text) - len(c_text)) <= 1: | |
| d = sum(1 for a, b in zip(o_text, c_text) if a != b) | |
| d += abs(len(o_text) - len(c_text)) | |
| if d <= max_cd: | |
| return True | |
| return False | |
| result_tokens: list[str] = [] | |
| prev_orig = -1 | |
| prev_pred = -1 | |
| for oi, pi in lcs_pairs: | |
| orig_gap = orig_tokens[prev_orig + 1 : oi] | |
| pred_gap = pred_tokens[prev_pred + 1 : pi] | |
| if orig_gap or pred_gap: | |
| o_text = " ".join(orig_gap) | |
| p_text = " ".join(pred_gap) | |
| if o_text == p_text or is_safe(o_text, p_text): | |
| result_tokens.extend(pred_gap) | |
| else: | |
| result_tokens.extend(orig_gap) | |
| result_tokens.append(orig_tokens[oi]) | |
| prev_orig = oi | |
| prev_pred = pi | |
| orig_tail = orig_tokens[prev_orig + 1 :] | |
| pred_tail = pred_tokens[prev_pred + 1 :] | |
| if orig_tail or pred_tail: | |
| o_text = " ".join(orig_tail) | |
| p_text = " ".join(pred_tail) | |
| if o_text == p_text or is_safe(o_text, p_text): | |
| result_tokens.extend(pred_tail) | |
| else: | |
| result_tokens.extend(orig_tail) | |
| return " ".join(result_tokens) | |
| def apply_judge_decisions(text: str, config: dict[str, Any], sample: EvalSample) -> str: | |
| """`tool_calling_judge` ์ verdict="revert" ํ์ ์ ํ ์คํธ์ ๋๋๋ฆฐ๋ค. | |
| `judge_step`(๊ธฐ๋ณธ "tool_calling_judge")์ ํ์ ๋ชฉ๋ก๊ณผ `corrections_step` | |
| (๊ธฐ๋ณธ "basic_correction")์ corrections ๋ชฉ๋ก์ `sample.artifacts` ์์ ์ฝ๋๋ค | |
| โ ์ด ๋ artifact ๋ ๊ฐ๊ฐ `pipelines/judge.py::run_tool_calling_judge` ์ | |
| `MultiStepPipeline` ์ self-consistency ๊ฒฝ๋ก๊ฐ ์ฑ์ด๋ค. ์ด๋ ํ์ชฝ์ด๋ผ๋ ๋น์ด | |
| ์์ผ๋ฉด(judge ์คํ ์ด ์์๊ฑฐ๋ corrections ๊ฐ ํ๋๋ ์์๋ ๊ฒฝ์ฐ) ์กฐ์ฉํ | |
| ์๋ฌธ(text)์ ๊ทธ๋๋ก ๋๋ ค์ค๋ค โ revert ํ ๋์ ์์ฒด๊ฐ ์๋ ์ ์ ์ํ๋ค. | |
| HF ๊ตฌํ(`huggingface/pipelines_v24.py::apply_judge_decisions_for_bulk`)์ | |
| ํฌํ . bulk ๋จ์์๋ ์๋ณธ๊ณผ ๋ฌ๋ฆฌ ์ฌ๊ธฐ์๋ ๋ฌธ์ ์ ์ฒด text ์ ์ ์ฉํ๋ค | |
| (ยง5-F ์ค๊ณ ๊ฒฐ์ โ ์ด ์์ง์ tool_calling_judge/basic_correction ์ ์ด๋ฏธ | |
| ๋ฌธ์ ๋จ์๋ก ์ฒ๋ฆฌํ๋ฏ๋ก bulk ์ ๋ ฌ ๋ฌธ์ ์์ฒด๊ฐ ์๋ค). | |
| """ | |
| judge_step = config.get("judge_step", "tool_calling_judge") | |
| corrections_step = config.get("corrections_step", "basic_correction") | |
| decisions = sample.artifacts.get("judge_decisions", {}).get(judge_step, []) | |
| corrections = sample.artifacts.get("corrections", {}).get(corrections_step, []) | |
| if not decisions or not corrections: | |
| return text | |
| revert_pairs: list[tuple[str, str]] = [] | |
| for d in decisions: | |
| if not isinstance(d, dict) or d.get("verdict") != "revert": | |
| continue | |
| idx = d.get("correction_index") | |
| if not isinstance(idx, int) or idx < 0 or idx >= len(corrections): | |
| continue | |
| entry = corrections[idx] | |
| if not isinstance(entry, dict): | |
| continue | |
| corrected = str(entry.get("corrected", "")) | |
| original = str(entry.get("original", "")) | |
| if corrected and original and corrected != original: | |
| revert_pairs.append((corrected, original)) | |
| # ๊ธด ๊ฒ๋ถํฐ ๋๋๋ ค์ผ ์งง์ ๊ต์ ์ด ๊ธด ๊ต์ ์ ๋ถ๋ถ ๋ฌธ์์ด์ผ ๋ ์๋ชป ๋๋๋ฆฌ์ง ์๋๋ค. | |
| revert_pairs.sort(key=lambda p: -len(p[0])) | |
| for corrected, original in revert_pairs: | |
| if corrected in text: | |
| text = text.replace(corrected, original, 1) | |
| return text | |
| def apply_change_rate_guard(text: str, original: str, config: dict[str, Any]) -> str: | |
| """์๋ฌธ ๋๋น ํ ํฐ ๋ณ๊ฒฝ ๋น์จ์ด `max_change_rate` ๋ฅผ ๋์ผ๋ฉด ์๋ฌธ์ผ๋ก ๋๋๋ฆฌ๋ ์์ ๋ง. | |
| v17 ์ธ์ฌ์ดํธ ๋ฌธ์(`insights/260504_solar-pro2_essentials.md`)๊ฐ "change_rate_guard | |
| (rule, bulk-level safety net, max_change_rate: 0.45)" ๋ผ๊ณ ๋ง ์ค๋ช ํ๊ณ , ์ค์ | |
| ๊ตฌํ์ HF ์์นด์ด๋ธ(`huggingface/pipelines.py`/`pipelines_v24.py`) ์ด๋์๋ ์๋ค | |
| (๋ ํฌ ์ ์ฒด ๊ฒ์์ผ๋ก ํ์ธ). ๊ทธ๋์ ์ด ํจ์๋ ๊ทธ ์ค๋ช ๋ง ๋ณด๊ณ ์๋ก ์์ฑํ ๊ฒ์ด๋ค | |
| โ v17 ์์น ์ฌํ์ ์ฝ์ํ์ง ์๋๋ค๋ ์ ์ ๋ก ํฌํ ๋์์ ํฌํจ๋๋ค | |
| (`.agents/01-plans/harness/2026-08-09-chosun-proofread-evalsample-migration.md` ยง9-3). | |
| ๋ณ๊ฒฝ ๋น์จ์ `evaluators/scoring.compute_lcs` ๋ก ์๋ฌธยทํ์ฌ ํ ์คํธ์ ํ ํฐ ๋จ์ | |
| LCS ๋ฅผ ๊ตฌํด `1 - |LCS| / |์๋ฌธ ํ ํฐ|` ์ผ๋ก ์ ์ํ๋ค โ ์ด ํ๋ก์ ํธ์ ๋ค๋ฅธ ๋ชจ๋ | |
| ๊ต์ด diff ๋ก์ง(`compute_proofread_metrics`, `apply_correction_filter`)๊ณผ ๊ฐ์ | |
| ์ฒ๋๋ฅผ ์ด๋ค. | |
| """ | |
| from solar_eval.evaluators.scoring import compute_lcs | |
| max_rate = config.get("max_change_rate", 0.45) | |
| orig_tokens = original.split() | |
| if not orig_tokens: | |
| return text | |
| text_tokens = text.split() | |
| lcs_pairs = compute_lcs(orig_tokens, text_tokens) | |
| change_rate = 1 - (len(lcs_pairs) / len(orig_tokens)) | |
| if change_rate > max_rate: | |
| logger.warning( | |
| "[change_rate_guard] change_rate=%.3f > max=%.3f, reverting to original", | |
| change_rate, | |
| max_rate, | |
| ) | |
| return original | |
| return text | |
| def apply_unit_unicode(text: str) -> str: | |
| """๋จ์ ํ ์คํธ๋ฅผ ์ ๋์ฝ๋ ๊ฒฐํฉ ๋ฌธ์๋ก ๋ณํํ๋ค.""" | |
| replacements = { | |
| "mmยฒ": "ใ", | |
| "cmยฒ": "ใ ", | |
| "mยฒ": "ใก", | |
| "kmยฒ": "ใข", | |
| "mm": "ใ", | |
| "cm": "ใ", | |
| "km": "ใ", | |
| "mg": "ใ", | |
| "kg": "ใ", | |
| "ml": "ใ", | |
| "dl": "ใ", | |
| "cc": "ใ", | |
| } | |
| for k, v in replacements.items(): | |
| text = text.replace(k, v) | |
| return text | |
| def load_compound_whitelist(config: dict[str, Any], config_dir: Path | None) -> set[str]: | |
| """๋ณตํฉ์ด ํ์ดํธ๋ฆฌ์คํธ(ํญ ๊ตฌ๋ถ ``ํ์ ์ด\\t๋น๋``)๋ฅผ ๋ก๋ํ๋ค. | |
| ์ ์ธ์ด ์์ผ๋ฉด ๋น set(์ ์์ ์ธ no-op)์ด๊ณ , ์ ์ธํ๋๋ฐ ๋ชป ์ฝ์ผ๋ฉด ์์ธ๋ค โ | |
| `KnowledgeAssetError` ๊ณ์ฝ(`errors.py`)์ ์นํ ์ฌ์ ๊ณผ ๋์ผํ๊ฒ ๋ฐ๋ฅธ๋ค. | |
| ํ์ดํธ๋ฆฌ์คํธ๋ ์ฌ๋์ด ํธ์งํ๋ ์ง์ ์์ฐ์ด๋ผ config ์ ๋ณธ(`03-evaluation/`) | |
| ๊ธฐ์ค ์๋๊ฒฝ๋ก๋ง ์ง์ํ๋ค. | |
| """ | |
| wl_path = config.get("whitelist_path", "") | |
| if not wl_path: | |
| return set() | |
| searched: list[str] = [] | |
| if config_dir is not None: | |
| candidate = Path(config_dir) / wl_path | |
| searched.append(str(candidate)) | |
| if candidate.is_file(): | |
| entries: set[str] = set() | |
| for line in candidate.read_text(encoding="utf-8").splitlines(): | |
| form = line.split("\t")[0].strip() | |
| if form: | |
| entries.add(form) | |
| return entries | |
| else: | |
| searched.append("(config_dir ์์)") | |
| raise KnowledgeAssetError(wl_path, searched) | |
| def _has_whitelist_prefix(joined: str, split_at: int, whitelist: set[str]) -> bool: | |
| """๊ฒฐํฉํ์ ์ ๋(๋ถ๋ฆฌ์ ๋๋จธ ~ ์ ์ฒด)๊ฐ ํ์ดํธ๋ฆฌ์คํธ ํ์ ์ด์ธ์ง. | |
| ์กฐ์ฌยท์ด๋ฏธ๊ฐ ๋ถ์ ์ด์ ("์ผ์ฑ"+"์ ์๋")๋ ํ์ ์ด("์ผ์ฑ์ ์")๋ก ์ก๊ธฐ ์ํด | |
| ์ ํ ์ผ์น๊ฐ ์๋๋ผ ์ ๋ ์ผ์น๋ฅผ ๋ณธ๋ค. ๋ถ๋ฆฌ์ (`split_at`) ์์ชฝ์์ ๋๋๋ | |
| ์ ๋๋ ์ฒซ ์ด์ ๋ง์ผ๋ก ์ฑ๋ฆฝํ๋ฏ๋ก ํ๋ณด์์ ๋บ๋ค. | |
| """ | |
| return any(joined[:k] in whitelist for k in range(split_at + 1, len(joined) + 1)) | |
| def apply_compound_whitelist_restore(text: str, original: str, whitelist: set[str]) -> str: | |
| """`compound_whitelist_restore` rule_type ๊ตฌํ (v19/v32 ๊ณ๋ณด์ ํ ์์ง ํฌํ ). | |
| ํ์ดํ๋ผ์ธ(FTยทstep1/2/3)์ด ๊ฐ๋ผ๋์ ํ์ดํธ๋ฆฌ์คํธ ๋ณตํฉ์ด๋ฅผ ๋ค์ ๋ถ์ธ๋ค. | |
| ์ธ์ ์ด์ ์์ ๊ฒฐํฉ์ด (1) ํ์ดํธ๋ฆฌ์คํธ ํ์ ์ด๋ฅผ ์ ๋๋ก ๊ฐ๊ณ โ ๋ถ๋ฆฌ์ ์ด | |
| ํ์ ์ด ์์ชฝ โ (2) ์๋ฌธ์ ๊ทธ๋๋ก ์กด์ฌํ ๋๋ง ๋ถ์ธ๋ค. ์กฐ๊ฑด (2) ๋๋ฌธ์ ์ด | |
| ๋ฃฐ์ ํ์ดํ๋ผ์ธ์ด ๋ง๋ ๋ถ๋ฆฌ๋ฅผ ๋๋๋ฆด ๋ฟ, ์ ๊ต์ ์ ๋ง๋ค์ง ์๋๋ค. | |
| 2026-09-02 ์ฌ์ ์ค์ธก(combo ์ฒด์ธ ์ถ๋ ฅ ์): paragraph +2.91 f1 | |
| (redundant 408โ342), ci-v0 +0.43, ci-v2/ci-v1/yul-ryul ๋ฌด๋ณํ. | |
| """ | |
| if not whitelist: | |
| return text | |
| restored_lines: list[str] = [] | |
| for line in text.split("\n"): | |
| tokens = line.split() | |
| out: list[str] = [] | |
| i = 0 | |
| while i < len(tokens): | |
| if i + 1 < len(tokens): | |
| joined = tokens[i] + tokens[i + 1] | |
| if joined in original and _has_whitelist_prefix(joined, len(tokens[i]), whitelist): | |
| out.append(joined) | |
| i += 2 | |
| continue | |
| out.append(tokens[i]) | |
| i += 1 | |
| restored_lines.append(" ".join(out)) | |
| return "\n".join(restored_lines) | |
| #: ์จ/๋ฅ ์ ๊ทํ๊ฐ "๋ช ์ฌ + ์กฐ์ฌ" ๋ฌธ๋งฅ์ผ๋ก ์ธ์ ํ๋ ์ด์ ๊ผฌ๋ฆฌ(ํ๊ธ ์์ฌ) ๋ชฉ๋ก. | |
| #: ์ด ๋ชฉ๋ก ๋ฐ์ ํ๊ธ์ด ์ด์ด์ง๋ฉด(์: ์ธ๋ช '๊น์จํฌ๊ฐ'์ 'ํฌ๊ฐ') ๊ฑด๋๋ฆฌ์ง ์๋๋ค. | |
| _YUL_RYUL_JOSA = frozenset( | |
| { | |
| "์", | |
| "๋", | |
| "์ด", | |
| "๊ฐ", | |
| "์", | |
| "๋ฅผ", | |
| "์", | |
| "์", | |
| "๋", | |
| "๋ง", | |
| "๊ณผ", | |
| "์", | |
| "๋ก", | |
| "์ผ๋ก", | |
| "๋ณด๋ค", | |
| "๋ถํฐ", | |
| "๊น์ง", | |
| "์กฐ์ฐจ", | |
| "๋ง์ ", | |
| "๋ฐ์", | |
| "์ฒ๋ผ", | |
| "๋๋ก", | |
| "๋งํผ", | |
| "๋ฟ", | |
| "๋", | |
| "์ด๋", | |
| "๋", | |
| "์ด๋", | |
| "๋ผ", | |
| "์ด๋ผ", | |
| "๋ผ๊ณ ", | |
| "์ด๋ผ๊ณ ", | |
| "๋ผ๋", | |
| "์ด๋ผ๋", | |
| "๋ผ๋ฉฐ", | |
| "์ด๋ผ๋ฉฐ", | |
| "๋ฉฐ", | |
| "์ด๋ฉฐ", | |
| "๊ณ ", | |
| "์ด๊ณ ", | |
| "๋ค", | |
| "์ด๋ค", | |
| "์ ๋๋ค", | |
| "์์", | |
| "์๊ฒ", | |
| "์๋", | |
| "์๋", | |
| "์์๋", | |
| "์์๋", | |
| "๋ก๋", | |
| "์ผ๋ก๋", | |
| "๋ก๋", | |
| "์ผ๋ก๋", | |
| "๋ก์", | |
| "์ผ๋ก์", | |
| "๋ก์จ", | |
| "์ผ๋ก์จ", | |
| "์์", | |
| "๊ณผ์", | |
| "๊ณผ๋", | |
| "์๋", | |
| "์์", | |
| "์", | |
| "๋๋น", | |
| } | |
| ) | |
| def apply_yul_ryul_normalize(text: str, exceptions: tuple[str, ...] = ()) -> str: | |
| """`yul_ryul_normalize` rule_type ๊ตฌํ โ ์จ/๋ฅ ๋ฐ์นจ ๊ท์น์ ๊ฒฐ์ ๋ก ์ ์ ์ฉ. | |
| ํ๊ธ ๋ง์ถค๋ฒ ์ 11ํญ: ์ ์์ ์ ๋ฐ์นจ์ด ์๊ฑฐ๋ ใด ๋ฐ์นจ์ด๋ฉด '์จ'(๋น์จยทํ์จยท | |
| ์ ํ์จ), ๊ทธ ์ธ ๋ฐ์นจ์ด๋ฉด '๋ฅ '(๋ฒ๋ฅ ยทํ๋ฅ ยท์ถ์๋ฅ ). ๋ฐ์นจ์ ์ ๋์ฝ๋ ์ฐ์ฐ์ผ๋ก | |
| ํ์ ๋๋ฏ๋ก LLM ์์ด ์ต์ข ํ ์คํธ์์ ๊ฐ์ ํ ์ ์๋ค. | |
| ์ธ๋ช ์คํ ๋ฐฉ์ง 2์ค ๊ฐ๋: | |
| 1. ์ด์ ๊ผฌ๋ฆฌ ๊ฒ์ฌ โ ์จ/๋ฅ ๋ค์ ํ๊ธ ์์ฌ๊ฐ ์กฐ์ฌ ๋ชฉ๋ก(`_YUL_RYUL_JOSA`)์ | |
| ์์ผ๋ฉด ์ ๋ฏธ์ฌ ์ฉ๋ฒ์ด ์๋ ๊ฒ์ผ๋ก ๋ณด๊ณ ๊ฑด๋๋ด๋ค (์ฌ์ ์ค์ธก์ ์ ์ผํ ์คํ | |
| '๊น์จํฌ๊ฐ'โ'๊น๋ฅ ํฌ๊ฐ' ๊ฐ ์ฌ๊ธฐ์ ๊ฑธ๋ฌ์ง๋ค). | |
| 2. `exceptions` โ ์ด์ ์ ํฌํจ๋๋ฉด ๋ฌด์กฐ๊ฑด ๊ฑด๋๋ฐ๋ ๋ฌธ์์ด ๋ชฉ๋ก (์จ/๋ฅ ๋ก | |
| ๋๋๋ ์ธ๋ช '๊น์์จ' ๋ฅ๋ฅผ ์ํ ํ . ๊ธฐ๋ณธ ๋น์ด ์์). | |
| 2026-09-02 ์ฌ์ ์ค์ธก(v33/combo ์ถ๋ ฅ ์, ์กฐ์ฌ ๊ฐ๋ ํฌํจ): yul-ryul ๋์นจ 2๊ฑด | |
| ์ ๋ ํ์(+3.07), ci-v2 +0.59, ci-v1 +0.53, paragraphยทci-v0 ๋ณ๊ฒฝ 0๊ฑด. | |
| """ | |
| out = list(text) | |
| n = len(text) | |
| for i, ch in enumerate(text): | |
| if ch not in ("์จ", "๋ฅ "): | |
| continue | |
| prev = text[i - 1] if i > 0 else "" | |
| if not ("๊ฐ" <= prev <= "ํฃ"): | |
| continue | |
| jong = (ord(prev) - 0xAC00) % 28 | |
| want = "์จ" if jong in (0, 4) else "๋ฅ " # ๋ฐ์นจ ์์(0)/ใด(4) โ ์จ | |
| if want == ch: | |
| continue | |
| j = i + 1 | |
| while j < n and not text[j].isspace(): | |
| j += 1 | |
| residue = "".join(c for c in text[i + 1 : j] if "๊ฐ" <= c <= "ํฃ") | |
| if residue and residue not in _YUL_RYUL_JOSA: | |
| continue | |
| if exceptions: | |
| k = i | |
| while k > 0 and not text[k - 1].isspace(): | |
| k -= 1 | |
| token = text[k:j] | |
| if any(e in token for e in exceptions): | |
| continue | |
| out[i] = want | |
| return "".join(out) | |