Skip to content

Commit d91956a

Browse files
feat: add TOPIK track labels
1 parent 964596e commit d91956a

3 files changed

Lines changed: 51 additions & 12 deletions

File tree

src/nokaman/cli.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
from pathlib import Path
5+
from typing import Optional
56

67
import typer
78
from rich.console import Console
@@ -30,6 +31,10 @@
3031
console = Console()
3132

3233

34+
def _print_json(data: object) -> None:
35+
console.print_json(data=data, ensure_ascii=True)
36+
37+
3338
@app.command("version")
3439
def version_cmd() -> None:
3540
console.print(f"NokaMan {__version__}")
@@ -50,7 +55,7 @@ def stats_cmd() -> None:
5055
skill = parts[1] if len(parts) > 1 else "?"
5156
by_lang[lang] += 1
5257
by_skill[skill] += 1
53-
console.print_json(
58+
_print_json(
5459
data={
5560
"version": __version__,
5661
"n_samples": len(list_sample_files()),
@@ -65,7 +70,7 @@ def stats_cmd() -> None:
6570
def demo_cmd(lang: str = typer.Option("en", "--lang", "-l")) -> None:
6671
"""Full multi-skill demo for a language (end-to-end runnable)."""
6772
result = evaluate_demo(lang)
68-
console.print_json(data=result)
73+
_print_json(data=result)
6974
OUT_DIR.mkdir(parents=True, exist_ok=True)
7075
path = OUT_DIR / f"demo_{lang}.json"
7176
path.write_text(json.dumps(result, indent=2) + "\n", encoding="utf-8")
@@ -85,7 +90,7 @@ def languages_list() -> None:
8590

8691

8792
@rubrics_app.command("list")
88-
def rubrics_list(lang: str | None = typer.Option(None, "--lang", "-l")) -> None:
93+
def rubrics_list(lang: Optional[str] = typer.Option(None, "--lang", "-l")) -> None:
8994
files = list_rubric_files()
9095
if lang:
9196
files = [p for p in files if p.stem == lang.strip().lower()]
@@ -105,8 +110,8 @@ def rubrics_list(lang: str | None = typer.Option(None, "--lang", "-l")) -> None:
105110
@eval_app.command("text")
106111
def eval_text(
107112
lang: str = typer.Option("en", "--lang", "-l"),
108-
text: str | None = typer.Option(None, "--text", "-t"),
109-
file: Path | None = typer.Option(None, "--file", "-f", exists=True, dir_okay=False),
113+
text: Optional[str] = typer.Option(None, "--text", "-t"),
114+
file: Optional[Path] = typer.Option(None, "--file", "-f", exists=True, dir_okay=False),
110115
skill: str = typer.Option("writing", "--skill", "-s"),
111116
) -> None:
112117
if file is not None:
@@ -116,12 +121,12 @@ def eval_text(
116121
else:
117122
console.print("[red]Provide --text or --file[/red]")
118123
raise typer.Exit(code=1)
119-
console.print_json(data=result)
124+
_print_json(data=result)
120125

121126

122127
@eval_app.command("demo")
123128
def eval_demo(lang: str = typer.Option("en", "--lang", "-l")) -> None:
124-
console.print_json(data=evaluate_demo(lang))
129+
_print_json(data=evaluate_demo(lang))
125130

126131

127132
@eval_app.command("samples")
@@ -150,7 +155,7 @@ def eval_samples() -> None:
150155

151156
@eval_app.command("batch")
152157
def eval_batch(
153-
out: Path | None = typer.Option(None, "--out", "-o"),
158+
out: Optional[Path] = typer.Option(None, "--out", "-o"),
154159
table: bool = typer.Option(True, "--table/--json-only"),
155160
) -> None:
156161
report = batch_evaluate()
@@ -188,7 +193,7 @@ def eval_summary() -> None:
188193
lang = stem.split("_")[0] if "_" in stem else "?"
189194
by_lang[lang] = by_lang.get(lang, 0) + 1
190195
report = batch_evaluate()
191-
console.print_json(
196+
_print_json(
192197
data={
193198
"version": __version__,
194199
"n_samples": len(files),
@@ -205,7 +210,7 @@ def eval_placement(
205210
answer: list[str] = typer.Option(..., "--answer", "-a", help="Repeat for multiple answers"),
206211
) -> None:
207212
result = placement_test(lang, answer)
208-
console.print_json(data=result)
213+
_print_json(data=result)
209214

210215

211216
@train_app.command("toy")

src/nokaman/models/toy.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,17 @@ def _framework_bands(language: str, cefr: str, score: float) -> dict:
123123
c = (cefr or "A1").upper()
124124
# rough ladders for demo UX
125125
jlpt = {"A1": "N5", "A2": "N4", "B1": "N3", "B2": "N2", "C1": "N1", "C2": "N1+"}.get(c, "N5")
126-
topik = {"A1": "1", "A2": "2", "B1": "3", "B2": "4", "C1": "5", "C2": "6"}.get(c, "1")
126+
topik = _topik_band(c)
127127
hsk = {"A1": "1", "A2": "2", "B1": "3", "B2": "4", "C1": "5", "C2": "6"}.get(c, "1")
128128
ielts = round(3.0 + (score / 100.0) * 6.0, 1) # ~3.0–9.0
129129
toeic = int(200 + score * 7) # ~200–900
130130
out = {"cefr": c}
131131
if lang == "ja":
132132
out["jlpt"] = jlpt
133133
if lang == "ko":
134-
out["topik"] = topik
134+
out["topik"] = topik["level"]
135+
out["topik_track"] = topik["track"]
136+
out["topik_label"] = topik["label"]
135137
if lang == "zh":
136138
out["hsk"] = hsk
137139
if lang == "en":
@@ -140,6 +142,19 @@ def _framework_bands(language: str, cefr: str, score: float) -> dict:
140142
return out
141143

142144

145+
def _topik_band(cefr: str) -> dict[str, str]:
146+
level = {"A1": "1", "A2": "2", "B1": "3", "B2": "4", "C1": "5", "C2": "6"}.get(
147+
cefr,
148+
"1",
149+
)
150+
track = "TOPIK I" if level in {"1", "2"} else "TOPIK II"
151+
return {
152+
"level": level,
153+
"track": track,
154+
"label": f"{track} Level {level}",
155+
}
156+
157+
143158
def _script_bonus(text: str, language: str) -> float:
144159
if not text:
145160
return 0.0

tests/test_framework_bands.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,22 @@ def test_demo_includes_frameworks() -> None:
1515
d = evaluate_demo("ja")
1616
assert "framework_bands" in d
1717
assert d["framework_bands"].get("jlpt")
18+
19+
20+
def test_ko_framework_bands_include_topik_track() -> None:
21+
r = ToyAbilityModel("ko").score_text(
22+
"I study Korean every morning and write short practice notes.",
23+
skill="writing",
24+
)
25+
bands = r["framework_bands"]
26+
assert bands["topik"] in {"1", "2", "3", "4", "5", "6"}
27+
assert bands["topik_track"] in {"TOPIK I", "TOPIK II"}
28+
assert bands["topik_label"] == f"{bands['topik_track']} Level {bands['topik']}"
29+
30+
31+
def test_ko_demo_includes_topik_field() -> None:
32+
d = evaluate_demo("ko")
33+
bands = d["framework_bands"]
34+
assert bands.get("topik")
35+
assert bands.get("topik_track")
36+
assert bands.get("topik_label")

0 commit comments

Comments
 (0)