Files
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

569 lines
20 KiB
Python

#!/usr/bin/env python3
"""N-gram diversification — rewrite over-represented n-grams in the assistant
streams (`thought` and `text`) of `expectedResponse`.
Driven by `data/synthesized/review/ngrams/diversification_candidates.json`
produced by `scripts/analyze_ngrams.py`. A candidate has `record_pct > 5%`,
`gini > 0.7`, `n >= 4`. Static paraphrase tables cover the worst offenders
(n8n template tail, nemotron tool-call thought boilerplate); other
candidates with no static rule are reported as flagged-for-manual-review.
Behavior contract
-----------------
* Read-only on user_input. Only `assistant_thought` and `assistant_text`
native JSON fields are rewritten — the user side is conditioning, not target.
* Deterministic per record: seed = `roomName + agentId`. Re-running on the
same input produces the same output.
* native JSON-validity preserving: rewrites operate on the inner string of the
`text:`/`thought:` field. The shape of the native JSON document is untouched.
An acceptance check round-trip-decodes a 100-record sample at the end.
* Replacements only happen when the paraphrase is at least 3 tokens shorter
than the original n-gram. Empty pool / equal-length pool entries are
skipped, leaving the original verbatim.
* Per-ngram + per-source replacement counts land in
`data/synthesized/review/ngrams/diversification_applied.json`.
CLI
---
python scripts/transform_ngram_diversify.py \\
--input data/final/train.jsonl \\
--output data/intermediate/train_ngram_diversified.jsonl \\
--candidates data/synthesized/review/ngrams/diversification_candidates.json \\
[--dry-run] [--max-records N]
"""
from __future__ import annotations
import argparse
import hashlib
import json
import re
import sys
import time
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
# ---------------------------------------------------------------------------
# Paraphrase tables
# ---------------------------------------------------------------------------
#
# (A) Static n8n boilerplate. The n8n-mega-workflows + n8n-workflows-templates
# family emits a verbatim template tail
# "... nodes connect any required credentials then confirm to deploy."
# in 12.6% of records. Hard-coded paraphrases are appropriate here because
# the source phrasing is already mechanical.
#
# (B) Thought-leak patterns. Nemotron-rl-tool-use + dolci-instruct + openclaw
# repeatedly produce phrases like "call the tool to satisfy the request".
# Replaced with a small, more natural pool. Sampled uniformly per record.
N8N_TEMPLATE_PARAPHRASES: dict[str, list[str]] = {
# 5-gram and 4-gram supersets — match longest first (see PARAPHRASE_ORDER)
"nodes connect any required credentials then confirm to deploy": [
"wire up the credentials and deploy",
"set credentials and deploy",
"add credentials, then deploy",
"configure the credential bindings and ship",
"fill in credentials and deploy",
"plug in the credentials and run",
],
"connect any required credentials then confirm to deploy": [
"wire up credentials and deploy",
"set credentials, then deploy",
"add credentials and ship",
"configure credentials and run",
"fill in credentials and deploy",
"plug in credentials and run",
],
"connect any required credentials then confirm": [
"wire up credentials, then confirm",
"set credentials, then confirm",
"add credentials and confirm",
],
"any required credentials then confirm to deploy": [
"credentials, then deploy",
"credentials and ship",
"credentials, then run",
],
"required credentials then confirm to deploy": [
"credentials, then deploy",
"credentials and run",
"credentials, then ship",
],
"credentials then confirm to deploy": [
"credentials and deploy",
"credentials, then deploy",
"credentials and ship",
],
"nodes connect any required credentials": [
"wire up the credentials",
"set the credentials",
"configure credentials",
],
"connect any required credentials": [
"wire up credentials",
"set credentials",
"configure credentials",
"add credentials",
"plug in credentials",
],
"any required credentials then confirm": [
"credentials, then confirm",
"credentials and confirm",
],
"required credentials then confirm": [
"credentials, then confirm",
"credentials and confirm",
],
"credentials then confirm to": [
"credentials, then",
"credentials and",
],
"then confirm to deploy": [
"and deploy",
"then deploy",
"and ship",
"then run",
"and launch",
],
}
THOUGHT_LEAK_PARAPHRASES: dict[str, list[str]] = {
"call the tool to satisfy the request": [
"use the tool",
"invoke the matching tool",
"run the right tool",
"fire the tool",
],
"tool to satisfy the request": [
"tool for the ask",
"tool for this",
"right tool",
],
"to satisfy the request": [
"for the ask",
"for this",
"for the user",
],
"call the tool to": [
# 4 tokens -> 2 tokens. Saves >=3 tokens? 4 - 2 = 2. Skipped by guard.
# Kept here so flagged-as-manual is accurate; guard rejects at runtime.
"use the",
"fire the",
],
"the user s request": [
"the request",
"the ask",
"what was asked",
],
}
# Order matters: we apply longest patterns first so a 5-gram beats its 4-gram
# subset to the same span. Built from the union of both tables.
PARAPHRASE_TABLE: dict[str, list[str]] = {
**N8N_TEMPLATE_PARAPHRASES,
**THOUGHT_LEAK_PARAPHRASES,
}
PARAPHRASE_ORDER: list[str] = sorted(
PARAPHRASE_TABLE.keys(),
key=lambda k: (-len(k.split()), -len(k)),
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
MIN_TOKEN_SAVINGS = 3 # required shortening to accept a paraphrase
def _tokens(s: str) -> list[str]:
return re.findall(r"[A-Za-z0-9']+", s)
def _ngram_to_regex(ng: str) -> re.Pattern[str]:
"""Word-boundary, case-insensitive, whitespace-flexible match."""
parts = [re.escape(t) for t in ng.split()]
pat = r"\b" + r"\s+".join(parts) + r"\b"
return re.compile(pat, re.IGNORECASE)
def _stable_choice(seed_key: str, choices: list[str]) -> str:
h = int(hashlib.md5(seed_key.encode("utf-8")).hexdigest()[:8], 16)
return choices[h % len(choices)]
def _record_seed(rec: dict) -> str:
"""Deterministic seed per record. Falls back to JSON hash when ids absent."""
rn = rec.get("roomName") or ""
aid = rec.get("agentId") or ""
if rn or aid:
return f"{rn}|{aid}"
# last-ditch: hash the canonical message + first 200 chars of expectedResponse
cm = (rec.get("currentMessage") or {}).get("content") or ""
er = rec.get("expectedResponse") or ""
return hashlib.md5((cm[:200] + er[:200]).encode("utf-8")).hexdigest()
# ---------------------------------------------------------------------------
# Candidate ingestion
# ---------------------------------------------------------------------------
def _load_candidates(path: Path) -> tuple[list[dict], dict[str, list[str]]]:
"""Return (raw_candidates, applicable_paraphrases) keyed by ngram lowercase.
`applicable_paraphrases` only contains candidates that:
* appear in `assistant_thought` or `assistant_text` streams,
* have at least one paraphrase that beats the MIN_TOKEN_SAVINGS guard.
Candidates that don't qualify are still returned in `raw_candidates` so the
caller can flag them for manual review.
"""
if not path.exists():
raise SystemExit(
f"missing candidates file: {path}\n"
"Run scripts/analyze_ngrams.py first."
)
raw = json.loads(path.read_text())
if not isinstance(raw, list):
raise SystemExit(f"unexpected candidates shape: {type(raw)}")
applicable: dict[str, list[str]] = {}
for entry in raw:
ng = (entry.get("ngram") or "").lower().strip()
stream = entry.get("stream", "")
if stream not in ("assistant_thought", "assistant_text"):
continue
pool = PARAPHRASE_TABLE.get(ng, [])
if not pool:
continue
n_tokens = len(ng.split())
keep = [p for p in pool if n_tokens - len(_tokens(p)) >= MIN_TOKEN_SAVINGS]
if not keep:
continue
applicable[ng] = keep
return raw, applicable
def _flag_unmatched(raw: list[dict]) -> list[dict]:
"""Return candidates that have no static paraphrase rule + are eligible
in the assistant streams. These need manual authoring."""
flagged: list[dict] = []
for entry in raw:
ng = (entry.get("ngram") or "").lower().strip()
stream = entry.get("stream", "")
if stream not in ("assistant_thought", "assistant_text"):
continue
if ng in PARAPHRASE_TABLE:
# Still flag if all pool entries failed the savings guard.
n_tokens = len(ng.split())
pool = PARAPHRASE_TABLE[ng]
keep = [p for p in pool if n_tokens - len(_tokens(p)) >= MIN_TOKEN_SAVINGS]
if keep:
continue
flagged.append({
"ngram": entry.get("ngram"),
"stream": stream,
"n": entry.get("n"),
"record_pct": entry.get("record_pct"),
"gini": entry.get("gini"),
"top_sources": entry.get("top_sources", [])[:3],
"reason": (
"no static paraphrase rule"
if ng not in PARAPHRASE_TABLE
else "all pool entries fail >=3-token savings guard"
),
})
return flagged
# ---------------------------------------------------------------------------
# String rewriter
# ---------------------------------------------------------------------------
class Rewriter:
"""Stateful per-stream rewriter. Tracks per-ngram replacement counts."""
def __init__(self, applicable: dict[str, list[str]], stream: str) -> None:
self.stream = stream
self.pools: dict[str, list[str]] = applicable
self.compiled: list[tuple[str, re.Pattern[str]]] = [
(ng, _ngram_to_regex(ng))
for ng in PARAPHRASE_ORDER
if ng in applicable
]
self.replacements: Counter = Counter()
def rewrite(self, text: str, *, seed: str) -> tuple[str, int]:
if not isinstance(text, str) or not text:
return text, 0
out = text
local_hits = 0
for ng, pat in self.compiled:
pool = self.pools.get(ng)
if not pool:
continue
def _replace(match: re.Match, *, _ng: str = ng, _pool: list[str] = pool) -> str:
nonlocal local_hits
key = f"{seed}|{self.stream}|{_ng}|{match.start()}"
choice = _stable_choice(key, _pool)
# Defensive: re-check the savings guard at runtime.
if len(_tokens(_ng)) - len(_tokens(choice)) < MIN_TOKEN_SAVINGS:
return match.group(0)
self.replacements[_ng] += 1
local_hits += 1
# Preserve a leading capital if the original started with one.
orig = match.group(0)
if orig[:1].isupper() and choice[:1].islower():
choice = choice[:1].upper() + choice[1:]
return choice
out = pat.sub(_replace, out)
return out, local_hits
# ---------------------------------------------------------------------------
# native JSON field substitution
# ---------------------------------------------------------------------------
NATIVE_JSON_THOUGHT_QUOTED = re.compile(
r'(^|\n)(\s*thought:\s*)("(?:[^"\\]|\\.)*")(\s*(?=\n|$))',
re.DOTALL,
)
NATIVE_JSON_THOUGHT_UNQUOTED = re.compile(
r'(^|\n)(\s*thought:\s*)([^"\n][^\n]*)(?=\n|$)',
)
NATIVE_JSON_TEXT_QUOTED = re.compile(
r'(^|\n)(\s*text:\s*)("(?:[^"\\]|\\.)*")(\s*(?=\n|$))',
re.DOTALL,
)
NATIVE_JSON_TEXT_UNQUOTED = re.compile(
r'(^|\n)(\s*text:\s*)([^"\n][^\n]*)(?=\n|$)',
)
def _sub_quoted(payload: str, regex: re.Pattern[str], rewriter: Rewriter, seed: str) -> tuple[str, int]:
hits = 0
def _r(m: re.Match) -> str:
nonlocal hits
prefix, key, quoted, suffix = m.groups()
try:
inner = json.loads(quoted)
except json.JSONDecodeError:
return m.group(0)
new_inner, n = rewriter.rewrite(inner, seed=seed)
if n == 0 or new_inner == inner:
return m.group(0)
hits += n
return f"{prefix}{key}{json.dumps(new_inner, ensure_ascii=False)}{suffix}"
return regex.sub(_r, payload), hits
def _sub_unquoted(payload: str, regex: re.Pattern[str], rewriter: Rewriter, seed: str) -> tuple[str, int]:
hits = 0
def _r(m: re.Match) -> str:
nonlocal hits
prefix, key, value = m.groups()
new_value, n = rewriter.rewrite(value, seed=seed)
if n == 0 or new_value == value:
return m.group(0)
hits += n
# Promote to a quoted form if the new value contains native JSON-special chars.
if any(c in new_value for c in '"\n\\,'):
return f"{prefix}{key}{json.dumps(new_value, ensure_ascii=False)}"
return f"{prefix}{key}{new_value}"
return regex.sub(_r, payload), hits
def diversify_record(
rec: dict,
*,
text_rw: Rewriter,
thought_rw: Rewriter,
per_source_hits: dict[str, Counter],
) -> tuple[dict, int]:
er = rec.get("expectedResponse")
if not isinstance(er, str) or not er:
return rec, 0
seed = _record_seed(rec)
new_er = er
total_hits = 0
new_er, h1 = _sub_quoted(new_er, NATIVE_JSON_THOUGHT_QUOTED, thought_rw, seed)
new_er, h2 = _sub_unquoted(new_er, NATIVE_JSON_THOUGHT_UNQUOTED, thought_rw, seed)
new_er, h3 = _sub_quoted(new_er, NATIVE_JSON_TEXT_QUOTED, text_rw, seed)
new_er, h4 = _sub_unquoted(new_er, NATIVE_JSON_TEXT_UNQUOTED, text_rw, seed)
total_hits = h1 + h2 + h3 + h4
if total_hits and new_er != er:
rec["expectedResponse"] = new_er
src = (rec.get("metadata") or {}).get("source_dataset") or "unknown"
per_source_hits[src]["records"] += 1
per_source_hits[src]["replacements"] += total_hits
return rec, total_hits
# ---------------------------------------------------------------------------
# Acceptance check (skipped: native JSON decoder removed in native v5)
# ---------------------------------------------------------------------------
def _acceptance_check(sample: list[str]) -> dict:
"""Acceptance check is skipped — native JSON decoder is not available in native v5."""
return {"checked": 0, "ok": 0, "failed": 0, "skipped": True}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--input", default="data/final/train.jsonl")
p.add_argument(
"--output",
default="data/intermediate/train_ngram_diversified.jsonl",
)
p.add_argument(
"--candidates",
default="data/synthesized/review/ngrams/diversification_candidates.json",
)
p.add_argument(
"--summary",
default="data/synthesized/review/ngrams/diversification_applied.json",
)
p.add_argument("--dry-run", action="store_true")
p.add_argument("--max-records", type=int, default=0)
args = p.parse_args()
in_path = Path(args.input).resolve() if Path(args.input).is_absolute() else (ROOT / args.input)
out_path = Path(args.output).resolve() if Path(args.output).is_absolute() else (ROOT / args.output)
cand_path = Path(args.candidates).resolve() if Path(args.candidates).is_absolute() else (ROOT / args.candidates)
summary_path = Path(args.summary).resolve() if Path(args.summary).is_absolute() else (ROOT / args.summary)
raw_cands, applicable = _load_candidates(cand_path)
flagged = _flag_unmatched(raw_cands)
print(
f"[ngram-diversify] candidates loaded: {len(raw_cands)}; "
f"applicable (with shorter paraphrase): {len(applicable)}; "
f"flagged for manual review: {len(flagged)}",
file=sys.stderr,
)
if args.dry_run:
# Project hit count from candidate record_count totals.
projected = 0
for entry in raw_cands:
ng = (entry.get("ngram") or "").lower().strip()
if ng in applicable:
projected += int(entry.get("record_count") or 0)
print(
json.dumps(
{
"dry_run": True,
"candidate_set_size": len(raw_cands),
"applicable_size": len(applicable),
"flagged_for_manual_review": len(flagged),
"projected_record_replacements_upper_bound": projected,
"applicable_ngrams": sorted(applicable.keys()),
"flagged_ngrams_first_10": [f["ngram"] for f in flagged[:10]],
},
indent=2,
)
)
return 0
if not in_path.exists():
print(f"missing input: {in_path}", file=sys.stderr)
return 2
out_path.parent.mkdir(parents=True, exist_ok=True)
summary_path.parent.mkdir(parents=True, exist_ok=True)
text_rw = Rewriter(applicable, stream="assistant_text")
thought_rw = Rewriter(applicable, stream="assistant_thought")
per_source_hits: dict[str, Counter] = defaultdict(Counter)
stats = {
"total": 0,
"decode_errors": 0,
"records_changed": 0,
"records_skipped": 0,
"total_replacements": 0,
}
t0 = time.time()
last_print = t0
with in_path.open("r", encoding="utf-8") as fin, out_path.open("w", encoding="utf-8") as fout:
for line in fin:
stats["total"] += 1
try:
rec = json.loads(line)
except json.JSONDecodeError:
stats["decode_errors"] += 1
fout.write(line)
continue
rec, hits = diversify_record(
rec,
text_rw=text_rw,
thought_rw=thought_rw,
per_source_hits=per_source_hits,
)
if hits:
stats["records_changed"] += 1
stats["total_replacements"] += hits
else:
stats["records_skipped"] += 1
fout.write(json.dumps(rec, ensure_ascii=False) + "\n")
now = time.time()
if now - last_print > 5:
rate = stats["total"] / max(1e-6, now - t0)
print(
f"[ngram-diversify] {stats['total']:>8d} "
f"changed={stats['records_changed']:>7d} "
f"replacements={stats['total_replacements']:>7d} "
f"{rate:.0f} rec/s",
file=sys.stderr,
)
last_print = now
if args.max_records and stats["total"] >= args.max_records:
break
elapsed = round(time.time() - t0, 1)
summary = {
"input": str(in_path),
"output": str(out_path),
"candidates_path": str(cand_path),
"elapsed_sec": elapsed,
"totals": stats,
"per_ngram_replacements": {
"assistant_text": dict(text_rw.replacements),
"assistant_thought": dict(thought_rw.replacements),
},
"per_source_replacements": {
src: {"records": c["records"], "replacements": c["replacements"]}
for src, c in sorted(
per_source_hits.items(),
key=lambda kv: kv[1]["replacements"],
reverse=True,
)
},
"applicable_ngrams": sorted(applicable.keys()),
"flagged_for_manual_review": flagged,
}
summary_path.write_text(json.dumps(summary, indent=2, ensure_ascii=False))
print(json.dumps(summary["totals"], indent=2), file=sys.stderr)
print(f"[ngram-diversify] summary -> {summary_path}", file=sys.stderr)
print(f"[ngram-diversify] output -> {out_path}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())