dev-strender Claude Opus 4.7 (1M context) commited on
Commit
725b08e
·
1 Parent(s): c540d93

fix: preserve bulk boundary \n + substring-count dedupe + 30-char prefix

Browse files

- bulk join "" → "\n" to keep paragraph boundaries across bulks
- apply_paragraph_dedupe: substring-count input to tolerate LLM paragraph restructuring
- prefix_len default 80 → 30 to catch corrected+original echo pairs
- temperature 0 → 0.0001 to loosen greedy decoding bias

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. pipelines.py +21 -11
  2. postprocess.py +14 -10
pipelines.py CHANGED
@@ -416,7 +416,11 @@ def call_llm(
416
  system_prompt: str,
417
  user_content: str,
418
  model: str = "solar-pro2",
419
- temperature: float = 0.0,
 
 
 
 
420
  reasoning_effort: str | None = None,
421
  max_tokens: int | None = None,
422
  response_format: dict | None = None,
@@ -804,12 +808,14 @@ def apply_rule(
804
  )
805
  elif rule == "paragraph_dedupe":
806
  config = step.get("config", {})
807
- return apply_paragraph_dedupe(
808
- text,
809
- original_text,
810
- min_len=config.get("min_len", 40),
811
- prefix_len=config.get("prefix_len", 80),
812
- )
 
 
813
 
814
  return text
815
 
@@ -1068,10 +1074,14 @@ def run_pipeline(
1068
  processed = process_bulks_parallel(
1069
  bulks, original_bulks, step, model, prompts, client
1070
  )
1071
- # Defensive: coerce any non-string bulk result before joining
1072
- # so the next step (which may call re.sub) never sees a
1073
- # non-string input.
1074
- text = "".join(str(p) if not isinstance(p, str) else p for p in processed)
 
 
 
 
1075
  elif step["type"] == "specialist":
1076
  text = run_specialist(step, text, original_text, prompts, model, client)
1077
  # If a step produced an unusably short output (< 10% of input), the
 
416
  system_prompt: str,
417
  user_content: str,
418
  model: str = "solar-pro2",
419
+ # Upstage 서빙 스택이 temperature=0 에서 greedy 디코딩 경로로 들어가는데,
420
+ # 이 경로가 특정 입력(truncated article 등)에서 학습 데이터의 "재시작" 패턴을
421
+ # 재현성 높게 재현하는 바이어스가 관찰됨. 0.0001 로 sampling 경로로 살짝
422
+ # 밀어 넣어 bias 를 흔들되, argmax 확률 비중은 거의 그대로 유지.
423
+ temperature: float = 0.0001,
424
  reasoning_effort: str | None = None,
425
  max_tokens: int | None = None,
426
  response_format: dict | None = None,
 
808
  )
809
  elif rule == "paragraph_dedupe":
810
  config = step.get("config", {})
811
+ # prefix_len 기본값은 apply_paragraph_dedupe 의 기본값(30)을 따른다.
812
+ # 명시적으로 config 에 지정된 경우만 override.
813
+ kwargs: dict = {}
814
+ if "min_len" in config:
815
+ kwargs["min_len"] = config["min_len"]
816
+ if "prefix_len" in config:
817
+ kwargs["prefix_len"] = config["prefix_len"]
818
+ return apply_paragraph_dedupe(text, original_text, **kwargs)
819
 
820
  return text
821
 
 
1074
  processed = process_bulks_parallel(
1075
  bulks, original_bulks, step, model, prompts, client
1076
  )
1077
+ # Bulk 경계는 원본에서 \n 분리돼 있었으므로 합칠 때도 \n 로
1078
+ # 연결한다. "".join 으로 붙이면 인접 bulk 문단이 줄로
1079
+ # 뭉개지고, 이후 split_into_bulks 재실행 시 문단 수가 줄어
1080
+ # step1 <원문>↔<교열_모델_수정결과> 정렬이 어긋나며 구조
1081
+ # 보존 규칙까지 깨진다 (참조: run.py:326 "\n".join).
1082
+ text = "\n".join(
1083
+ str(p) if not isinstance(p, str) else p for p in processed
1084
+ )
1085
  elif step["type"] == "specialist":
1086
  text = run_specialist(step, text, original_text, prompts, model, client)
1087
  # If a step produced an unusably short output (< 10% of input), the
postprocess.py CHANGED
@@ -380,7 +380,7 @@ def apply_paragraph_dedupe(
380
  text: str,
381
  original: str,
382
  min_len: int = 40,
383
- prefix_len: int = 80,
384
  ) -> str:
385
  """LLM이 뱉은 중복 문단을 제거한다.
386
 
@@ -411,13 +411,12 @@ def apply_paragraph_dedupe(
411
  if not text:
412
  return text
413
 
414
- in_paras_norm = [_normalize_paragraph(p) for p in _split_output_paragraphs(original or "")]
415
- in_exact: Counter[str] = Counter()
416
- in_prefix: Counter[str] = Counter()
417
- for norm in in_paras_norm:
418
- if len(norm) >= min_len:
419
- in_exact[norm] += 1
420
- in_prefix[norm[:prefix_len]] += 1
421
 
422
  out_paras = _split_output_paragraphs(text)
423
  out_exact_seen: Counter[str] = Counter()
@@ -435,12 +434,17 @@ def apply_paragraph_dedupe(
435
  out_exact_seen[norm] += 1
436
  out_prefix_seen[prefix] += 1
437
 
 
 
 
 
 
438
  exact_dup = (
439
- out_exact_seen[norm] > in_exact.get(norm, 0)
440
  and out_exact_seen[norm] >= 2
441
  )
442
  near_dup = (
443
- out_prefix_seen[prefix] > in_prefix.get(prefix, 0)
444
  and out_prefix_seen[prefix] >= 2
445
  )
446
 
 
380
  text: str,
381
  original: str,
382
  min_len: int = 40,
383
+ prefix_len: int = 30,
384
  ) -> str:
385
  """LLM이 뱉은 중복 문단을 제거한다.
386
 
 
411
  if not text:
412
  return text
413
 
414
+ # Input "normalized 전체 문자열" 두고 substring count 를 쓴다.
415
+ # 이전 구현은 input 을 문단 Counter 로 셌는데, LLM 이 문단 경계를 재구조화
416
+ # (예: 줄바꿈 없이 중복 문장이 들어있던 한 문단을 여러 문단으로 쪼갬) 하면
417
+ # output 문단이 input 문단과 exact match 가 안 되어 input_count=0 으로 잡혀,
418
+ # 정당한 중복 (저자/원본이 의도한 반복) 까지 drop 되는 버그가 있었다.
419
+ in_norm = _normalize_paragraph(original or "")
 
420
 
421
  out_paras = _split_output_paragraphs(text)
422
  out_exact_seen: Counter[str] = Counter()
 
434
  out_exact_seen[norm] += 1
435
  out_prefix_seen[prefix] += 1
436
 
437
+ # input 문자열 전체에서 해당 문단(또는 prefix)이 몇 번 substring 으로
438
+ # 등장하는지 집계. output_count 가 input_count 를 초과할 때만 drop.
439
+ in_exact_count = in_norm.count(norm) if in_norm else 0
440
+ in_prefix_count = in_norm.count(prefix) if in_norm else 0
441
+
442
  exact_dup = (
443
+ out_exact_seen[norm] > in_exact_count
444
  and out_exact_seen[norm] >= 2
445
  )
446
  near_dup = (
447
+ out_prefix_seen[prefix] > in_prefix_count
448
  and out_prefix_seen[prefix] >= 2
449
  )
450