diff --git a/.github/ISSUE_TEMPLATE/01_skill_proposal.yml b/.github/ISSUE_TEMPLATE/01_skill_proposal.yml index 07dd95f7..6b50620c 100644 --- a/.github/ISSUE_TEMPLATE/01_skill_proposal.yml +++ b/.github/ISSUE_TEMPLATE/01_skill_proposal.yml @@ -36,6 +36,7 @@ body: - defi - dev_tools - finance + - linguistics - monitoring - office - optimization diff --git a/.github/labels.json b/.github/labels.json index c2ca5d78..8ad87c80 100644 --- a/.github/labels.json +++ b/.github/labels.json @@ -114,6 +114,11 @@ "color": "E6D9F5", "description": "Registry skill category — finance (`skills/finance/`)." }, + { + "name": "cat: linguistics", + "color": "E6D9F5", + "description": "Registry skill category — linguistics (`skills/linguistics/`)." + }, { "name": "cat: monitoring", "color": "E6D9F5", diff --git a/CHANGELOG.md b/CHANGELOG.md index b459b01e..7531afa6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta ### Added +- **Skill (`linguistics/korean_slang` v0.1.0):** New `linguistics/` category and offline Korean Gen-Z slang pack — interpret / suggest / lookup over a September 2026 curated KB, honorific audience gates, and constitution filters for slurs (#34). +- **GitHub:** `cat: linguistics` in [`.github/labels.json`](.github/labels.json) and the New Skill Proposal category dropdown; GitHub UI labels sync on merge to `main` (#34). +- **Examples:** [`korean_slang_demo.py`](examples/korean_slang_demo.py) — offline interpret, suggest, lookup, unmatched, and blocked-term paths (#34). +- **Scripts:** [`korean_slang_stress_sim.py`](scripts/korean_slang_stress_sim.py) — offline interpret/suggest stress harness (#34). - **Docs:** [Glossary](docs/glossary.md) and [inclusive language](docs/contributing/inclusive-language.md) — operator / contributor / host agent / end user; skill bundle vs PyPI package; anatomy roles (#252). - **Core:** Pluggable secret providers — `SecretProvider`, `EnvSecretProvider`, `MappingSecretProvider`, `CallableSecretProvider`, and `SkillLoader.resolve_env_vars()` inject manifest `env_vars` into `BaseSkill(config=...)` without requiring global `os.environ` mutation (#39). - **Core:** `BaseSkill.credential()` — config-first credential lookup with `os.environ` fallback for local `.env` workflows (#39). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8f9da9f..8abf2404 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -399,6 +399,7 @@ Place each skill under one top-level directory under `skills/`. Use an existing | `defi` | On-chain trading and agent wallet execution | `evm_tx_handler` | | `dev_tools` | Developer workflows, issue resolution, repo tooling | `issue_resolver` | | `finance` | Blockchain, risk, financial analysis | `wallet_screening`, `uk_companies_house_handler` | +| `linguistics` | Language adapters and internet-register lexicons | `korean_slang` | | `office` | Documents, productivity, email | `pdf_form_filler`, `gmail_handler` | | `optimization` | Middleware, compression, efficiency | `prompt_rewriter` | | `monitoring` | Agent loop observability, budget gates, task control | `token_limiter` | diff --git a/docs/contributing/ai_native_workflow.md b/docs/contributing/ai_native_workflow.md index dd1af106..dabcfdb3 100644 --- a/docs/contributing/ai_native_workflow.md +++ b/docs/contributing/ai_native_workflow.md @@ -245,7 +245,7 @@ These align with [CONTRIBUTING.md](../../CONTRIBUTING.md). Violations block merg - Output-card `ui_schema.fields[].key` values must resolve in `execute()` JSON; keep `tests/fixtures/card_ui_schema/__.json` in sync (#199) - Update `docs/skills/.md` and `docs/skills/README.md` (**Version**, **Skill history**, and index columns per [CONTRIBUTING.md § catalog page](../../CONTRIBUTING.md#6-docsskillsskill_namemd-catalog-page)) - On each catalog page, add a **Usage Examples** section (Gemini, Claude, OpenAI, DeepSeek, Ollama prompt mode) per [skill usage template](../usage/skill_usage_template.md). Keep provider mechanics in `docs/usage/`; put skill-specific paths, sample user messages, and `execute` payloads on the skill page. -- Categories: `compliance`, `creative`, `data_engineering`, `defi`, `dev_tools`, `finance`, `monitoring`, `office`, `optimization`, `security`, `wellness` — see [Skill library](../skills/README.md) for the live registry; [Choosing a category](../../CONTRIBUTING.md#choosing-a-category) in CONTRIBUTING.md (issue first for new top-level folders) +- Categories: `compliance`, `creative`, `data_engineering`, `defi`, `dev_tools`, `finance`, `linguistics`, `monitoring`, `office`, `optimization`, `security`, `wellness` — see [Skill library](../skills/README.md) for the live registry; [Choosing a category](../../CONTRIBUTING.md#choosing-a-category) in CONTRIBUTING.md (issue first for new top-level folders) - Do not bump `pyproject.toml` version in skill-only PRs unless requested - **Effect** in `skill.py`; **Directive** (skill context, not host persona) in `instructions.md`; **Contract** in `manifest.yaml` - Never commit secrets; document `env_vars` in the manifest diff --git a/docs/skills/README.md b/docs/skills/README.md index cf1a2a55..57f4707c 100644 --- a/docs/skills/README.md +++ b/docs/skills/README.md @@ -91,6 +91,13 @@ Supportive coaching guardrails, crisis triage, and grounded psychoeducation for | :--- | :--- | :--- | :--- | :--- | | **[Mental Coach](mental_coach.md)** | `wellness/mental_coach` | `0.1.1` (9 Sep 2026) | [@mrmasa88](https://github.com/mrmasa88) ([AO](https://github.com/0x-AO-Protocol)) | Deterministic wellness coaching firewall with crisis triage, scope limits, and cited KB retrieval. | +## Linguistics +Language adapters: curated lexicons so host agents can interpret and produce informal, internet-native language that foundation models routinely miss. + +| Skill | ID | Version | Issuer | Description | +| :--- | :--- | :--- | :--- | :--- | +| **[Korean Slang](korean_slang.md)** | `linguistics/korean_slang` | `0.1.0` (19 Sep 2026) | [@bd-c3](https://github.com/bd-c3) | Offline Korean Gen-Z slang interpreter and peer-register generator (September 2026 pack). | + --- ## Installing Skills diff --git a/docs/skills/korean_slang.md b/docs/skills/korean_slang.md new file mode 100644 index 00000000..e65d5c47 --- /dev/null +++ b/docs/skills/korean_slang.md @@ -0,0 +1,287 @@ +# Korean Slang + +**Domain:** `linguistics` +**Skill ID:** `linguistics/korean_slang` +**Issuer:** [@bd-c3](https://github.com/bd-c3) + +**Version**: `0.1.0` — 19 Sep 2026 + +**Recommended install:** `pip install "skillware[linguistics_korean_slang]"`. See [Install extras](../usage/install_extras.md). + +[Skill Library](README.md) · [Testing](../TESTING.md) + +Deterministic **Korean Gen-Z slang lexicon** for host agents that need to understand or speak modern internet Korean. `execute()` is a pure function: longest-first matching over a curated September 2026 pack, template-based `suggest()`, and constitution filters for slurs and appearance-policing slang. No network. No model calls. + +This is the v1 answer to [issue #34](https://github.com/ARPAHLS/skillware/issues/34) (preferred id `linguistics/korean_slang`, not `korean_pack_26_S`). The pack is small and honest: unmatched 줄임말 stay unmatched. + +> **Skill chains:** Often after `security/prompt_injection_firewall` when the Korean text arrived from an untrusted comment field — see [Skill chaining](../usage/skill_chaining.md). + +> **Language disclaimer:** This skill is a curated snapshot, not a native speaker and not a living social network. Slang dating, regional use, and irony can still be wrong. A Korean-native review of the data pack is expected at PR time. + +## Capabilities + +- **interpret** (default): gloss incoming Korean / Konglish / jamo chat. Returns the issue contract fields `translation`, `slang_breakdown`, `nuance`, `formality`. +- **suggest**: peer-register lines from templates, gated by `audience` (`peers`, `mixed`, `work`, `elders`). Elders get polite Korean, not 반말 slang. +- **lookup**: exact surface, alias, or romanization. +- **Constitution**: blocked substrings are omitted from glosses and never generated. Vulgar items (`존나`, `존맛탱`) are interpreted but not suggested unless `vulgar_ok` is true. + +## Bundle layout + +The skill lives in `skills/linguistics/korean_slang/`. Roles: [Skill anatomy](../introduction.md#skill-anatomy) and the [glossary](../glossary.md). **Contract** — see Manifest Details below. **Assurance** — `test_skill.py` in the bundle. + +### Effect (`skill.py` + `lexicon.py`) + +Pure Python. Loads `kb/` once per process, matches compact Hangul (spaces ignored), applies jamo/latin boundaries, and fills templates. Identical input returns identical JSON. + +### Directive (`instructions.md`) + +When to call interpret vs suggest, honorific gates, and how to talk about unmatched or caution terms. + +### Corpus (`kb/`) + +| File | Role | +| :--- | :--- | +| `entries.json` | Curated slang entries (surfaces, aliases, romanization, intents, safety flags) | +| `generation.json` | Intent aliases and audience-scoped templates | +| `constitution.json` | Blocked substrings the skill will not gloss or generate | +| `pack_meta.json` | Snapshot id `2026-09` and public sources used to seed the pack | + +## Manifest Details + +**Parameters Schema:** +* `text` (string): Korean, Konglish, or English. Required for `interpret` and `lookup`. +* `context` (string, optional): Situational note appended to `nuance`. +* `action` (string, optional): `interpret` (default), `suggest`, or `lookup`. +* `intent` (string, optional): Generation bucket for `suggest` (`praise`, `food`, `work`, `tired`, `annoyed`, `agree`, `greeting`, `bye`, `thanks`, `workout`, `weekend`, `dating`, `fandom`, `competence`). +* `audience` (string, optional): `peers` (default), `mixed`, `work`, or `elders`. +* `include_romanization` (boolean, optional): Attach romanization on hits. +* `max_hits` (integer, optional): 1–50, default 12. +* `vulgar_ok` (boolean, optional): Allow vulgar-marked items in `suggest()`. Default false. + +**Outputs Schema:** +* `status` (string): `ok` or `error`. +* `action` (string): Echoed action. +* `translation` (string or null): English gloss, or generated Korean for `suggest`. +* `slang_breakdown` (object): Surface → short explanation. +* `nuance` (string): Socio-linguistic note. +* `formality` (string): `Informal / Slang`, `Informal / Vulgar`, `Informal / Caution`, `Polite`, `Work-safe`, `unknown`, or `blocked`. +* `hit_count` (integer): Hits or suggestion count. + +Contract violations return `{"status": "error", "error": {"code", "detail"}}`. Closed codes: `EMPTY_TEXT`, `UNKNOWN_ACTION`, `UNKNOWN_AUDIENCE`, `INVALID_MAX_HITS`, `NEED_TEXT_OR_INTENT`, `NO_SUGGESTIONS`, `PACK_FAILURE`. + +## Environment + +| Variable | Required | Purpose | +| :--- | :--- | :--- | +| *(none)* | — | Offline pack; `execute()` does not read secrets | + +Configure agent keys per [API keys for skills](../usage/api_keys.md) only when running provider loop examples. + +## Example Usage (Direct) + +```python +from skillware.core.loader import SkillLoader + +bundle = SkillLoader.load_skill("linguistics/korean_slang") +skill = bundle["class"]() +result = skill.execute( + { + "text": "요즘 완전 폼 미쳤다", + "context": "A comment on a YouTube video about a popular singer.", + } +) +print(result["translation"], result["formality"]) +print(result["slang_breakdown"]) +``` + +## Usage Examples + +Guides: [Usage index](../usage/README.md) · [Agent loops](../usage/agent_loops.md) · [Skill chaining](../usage/skill_chaining.md). No skill-specific API keys. See [Install extras](../usage/install_extras.md). + +Use `bundle["class"]()` in the snippets below; explicit `bundle["module"].KoreanSlangSkill()` also works. + +Sample user message: *What does 요즘 완전 폼 미쳤다 mean on a YouTube comment about a singer?* + +### Runnable examples + +| Script | Provider | Env vars | +| :--- | :--- | :--- | +| [`korean_slang_demo.py`](../../examples/korean_slang_demo.py) | Local execute | None | +| [`korean_slang_stress_sim.py`](../../scripts/korean_slang_stress_sim.py) | Local execute (maintainer harness) | None | + +### Gemini + +```python +import google.genai as genai +from google.genai import types +from skillware.core.env import load_env_file +from skillware.core.loader import SkillLoader + +load_env_file() +bundle = SkillLoader.load_skill("linguistics/korean_slang") +skill = bundle["class"]() +tool = SkillLoader.to_gemini_tool(bundle) +client = genai.Client() +response = client.models.generate_content( + model="gemini-3.5-flash-lite", + contents=( + "What does 요즘 완전 폼 미쳤다 mean on a YouTube comment about a singer?" + ), + config=types.GenerateContentConfig( + tools=[tool], + system_instruction=bundle["instructions"], + ), +) +for part in response.candidates[0].content.parts: + if part.function_call: + result = skill.execute(dict(part.function_call.args)) + print(result["translation"], result["formality"]) +``` + +### Claude + +```python +import os +import anthropic +from skillware.core.env import load_env_file +from skillware.core.loader import SkillLoader + +load_env_file() +bundle = SkillLoader.load_skill("linguistics/korean_slang") +skill = bundle["class"]() +client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY")) +tools = [SkillLoader.to_claude_tool(bundle)] +response = client.messages.create( + model="claude-haiku-4-5-20251001", + max_tokens=1024, + system=bundle["instructions"], + tools=tools, + messages=[{ + "role": "user", + "content": ( + "What does 요즘 완전 폼 미쳤다 mean on a YouTube comment about a singer?" + ), + }], +) +for block in response.content: + if block.type == "tool_use": + result = skill.execute(dict(block.input)) + print(result["translation"], result["formality"]) +``` + +### OpenAI + +```python +import os +from openai import OpenAI +from skillware.core.env import load_env_file +from skillware.core.loader import SkillLoader + +load_env_file() +bundle = SkillLoader.load_skill("linguistics/korean_slang") +skill = bundle["class"]() +client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY")) +tool = SkillLoader.to_openai_tool(bundle) +response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": bundle["instructions"]}, + { + "role": "user", + "content": ( + "What does 요즘 완전 폼 미쳤다 mean on a YouTube comment about a singer?" + ), + }, + ], + tools=[tool], +) +message = response.choices[0].message +if message.tool_calls: + import json + args = json.loads(message.tool_calls[0].function.arguments) + result = skill.execute(args) + print(result["translation"], result["formality"]) +``` + +### DeepSeek + +```python +import os +from openai import OpenAI +from skillware.core.env import load_env_file +from skillware.core.loader import SkillLoader + +load_env_file() +bundle = SkillLoader.load_skill("linguistics/korean_slang") +skill = bundle["class"]() +client = OpenAI( + api_key=os.environ.get("DEEPSEEK_API_KEY"), + base_url="https://api.deepseek.com", +) +tool = SkillLoader.to_deepseek_tool(bundle) +response = client.chat.completions.create( + model="deepseek-chat", + messages=[ + {"role": "system", "content": bundle["instructions"]}, + { + "role": "user", + "content": ( + "What does 요즘 완전 폼 미쳤다 mean on a YouTube comment about a singer?" + ), + }, + ], + tools=[tool], +) +message = response.choices[0].message +if message.tool_calls: + import json + args = json.loads(message.tool_calls[0].function.arguments) + result = skill.execute(args) + print(result["translation"], result["formality"]) +``` + +### Ollama (prompt mode) + +```python +import json +from skillware.core.loader import SkillLoader + +bundle = SkillLoader.load_skill("linguistics/korean_slang") +skill = bundle["class"]() +prompt = ( + "You may call tools as JSON blocks.\n" + f"Tool: {bundle['manifest']['name']}\n" + f"Instructions:\n{bundle['instructions']}\n" + "User: What does 요즘 완전 폼 미쳤다 mean on a YouTube comment about a singer?" +) +print(prompt) +# When the model emits JSON tool args, pass them to execute: +result = skill.execute({ + "text": "요즘 완전 폼 미쳤다", + "context": "A comment on a YouTube video about a popular singer.", + "action": "interpret", +}) +print(json.dumps(result, indent=2, ensure_ascii=False)) +``` + +## Limitations + +- **Not an LLM translator.** Unseen slang returns `unmatched`. Do not invent 줄임말 expansions. +- **Snapshot dated 2026-09.** Internet Korean moves faster than a registry pack. +- **Register gates are coarse.** `elders` suppresses slang; it does not implement a full 존댓말 grammar engine. +- **Blocked list is not exhaustive.** New slurs will appear; the constitution is a floor, not a hate-speech classifier. +- **Romanization is a lookup aid**, not official Revised Romanization for every vowel nuance. + + +## Skill history + +Commits that touched this skill bundle or its catalog page ([`linguistics/korean_slang`](https://github.com/ARPAHLS/skillware/tree/main/skills/linguistics/korean_slang)). + +| Commit | Description | Date | Version | Contributors | +| :--- | :--- | :--- | :--- | :--- | +| *(this PR)* | feat(linguistics): add korean_slang offline Gen-Z pack (#34) | 19 Sep 2026 | `0.1.0` | [@bd-c3](https://github.com/bd-c3) | + + +## Enterprise disclaimer + +This skill is provided for demonstration and integration purposes. It is intended as a starting point that you can adapt to your own data, schemas, and operational requirements. For an enterprise-grade version of this skill with dedicated support, SLAs, and customization, contact skills@arpacorp.net. diff --git a/docs/usage/agent_loops.md b/docs/usage/agent_loops.md index 1f10b133..69ce4fd4 100644 --- a/docs/usage/agent_loops.md +++ b/docs/usage/agent_loops.md @@ -167,6 +167,7 @@ skills in one harness. | `data_engineering/semantic_web_proxy` | `semantic_web_proxy_demo.py` (local execute) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | | `dev_tools/issue_resolver` | - | `gemini_issue_resolver.py` | `claude_issue_resolver.py` | (catalog page) | (catalog page) | `ollama_issue_resolver.py` | | `wellness/mental_coach` | `mental_coach_demo.py` (local execute) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | +| `linguistics/korean_slang` | `korean_slang_demo.py` (local execute) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | | `defi/evm_tx_handler` | - | `gemini_evm_tx_handler.py` | `claude_evm_tx_handler.py` | - | - | - | | `monitoring/token_limiter` | `token_limiter_loop.py` (local execute) | `gemini_token_limiter.py`, `skill_context_gemini_loop.py` (multi-skill) | `claude_token_limiter.py` | (catalog page) | (catalog page) | (catalog page) | | `monitoring/kpi_gate` | `kpi_gate_demo.py` (local execute) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | (catalog page) | diff --git a/docs/usage/install_extras.md b/docs/usage/install_extras.md index 978b1875..954aa82a 100644 --- a/docs/usage/install_extras.md +++ b/docs/usage/install_extras.md @@ -74,6 +74,7 @@ Union of non-core `requirements` from every skill in the category. | `defi` | `defi/evm_tx_handler` | `web3>=6.0.0` | | `dev_tools` | `dev_tools/issue_resolver` | *(none today)* | | `finance` | `finance/uk_companies_house_handler`, `finance/wallet_screening` | *(none today)* | +| `linguistics` | `linguistics/korean_slang` | *(none today)* | | `monitoring` | `monitoring/token_limiter`, `monitoring/kpi_gate` | *(none today)* | | `office` | `office/gmail_handler`, `office/pdf_form_filler` | `anthropic`, `pymupdf` | | `optimization` | `optimization/context_optimizer`, `optimization/prompt_rewriter` | `fastembed`, `numpy` | @@ -105,6 +106,7 @@ One extra per bundled registry skill. Naming: `{category}_{skill_name}` (registr | `dev_tools_issue_resolver` | `dev_tools/issue_resolver` | *(none today)* | Use this extra in docs and installs | | `finance_uk_companies_house_handler` | `finance/uk_companies_house_handler` | *(none today)* | Use this extra in docs and installs | | `finance_wallet_screening` | `finance/wallet_screening` | *(none today)* | Use this extra in docs and installs | +| `linguistics_korean_slang` | `linguistics/korean_slang` | *(none today)* | Use this extra in docs and installs | | `monitoring_token_limiter` | `monitoring/token_limiter` | *(none today)* | Use this extra in docs and installs | | `monitoring_kpi_gate` | `monitoring/kpi_gate` | *(none today)* | Use this extra in docs and installs | | `office_gmail_handler` | `office/gmail_handler` | *(none today)* | Use this extra in docs and installs | diff --git a/examples/README.md b/examples/README.md index f91f8ca5..25d8de55 100644 --- a/examples/README.md +++ b/examples/README.md @@ -33,6 +33,7 @@ pip install -e ".[dev,all,agents]" | Script | Skill ID | Provider | Required extra | Required env vars | Description | | :--- | :--- | :--- | :--- | :--- | :--- | | `mental_coach_demo.py` | `wellness/mental_coach` | Local execute | `[wellness_mental_coach]` | None | Demonstrates coaching, crisis escalation, and blocked clinical paths locally. | +| `korean_slang_demo.py` | `linguistics/korean_slang` | Local execute | `[linguistics_korean_slang]` | None | Offline Korean Gen-Z slang interpret, suggest, lookup, unmatched, and blocked-term filter. | | `build_dataset_demo.py` | `data_engineering/synthetic_generator` | Local execute (Gemini backend) | `[data_engineering_synthetic_generator]`, `[gemini]` | `GOOGLE_API_KEY` | Generates a JSONL synthetic dataset with the synthetic generator skill. | | `claude_pdf_form_filler.py` | `office/pdf_form_filler` | Claude | `[office_pdf_form_filler]`, `[claude]` | `ANTHROPIC_API_KEY` | Uses Claude with the PDF form filler skill to map instructions to fields. | | `claude_tos_evaluator.py` | `compliance/tos_evaluator` | Claude | `[compliance_tos_evaluator]`, `[claude]` | `ANTHROPIC_API_KEY` | Runs a Claude tool loop for website automation policy review. | diff --git a/examples/korean_slang_demo.py b/examples/korean_slang_demo.py new file mode 100644 index 00000000..a1fa7bfb --- /dev/null +++ b/examples/korean_slang_demo.py @@ -0,0 +1,75 @@ +""" +Local execute demo for linguistics/korean_slang. + +Runs the deterministic Korean slang pack entirely offline: the issue #34 +YouTube-comment interpret contract, peer-register suggest(), lookup, an +unmatched formal sentence, and a blocked-term filter. No API keys, no network. +Hangul is printed via JSON ascii escapes so Windows cp1252 pipes do not crash. +""" + +import json + +from skillware.core.loader import SkillLoader + + +def _dump(value): + return json.dumps(value, ensure_ascii=True) + + +def _print_interpret(title, result): + print(f"\n{title}") + print(f" status: {result['status']}") + if result["status"] != "ok": + print(f" error: {_dump(result.get('error'))}") + return + print(f" formality: {result['formality']}") + print(f" translation: {result.get('translation')}") + print(f" slang_breakdown: {_dump(result.get('slang_breakdown'))}") + if result.get("warnings"): + print(f" warnings: {_dump(result['warnings'])}") + + +def run_demo() -> None: + print("Loading linguistics/korean_slang...") + bundle = SkillLoader.load_skill("linguistics/korean_slang") + skill = bundle["class"]() + + _print_interpret( + "Scenario 1: issue #34 YouTube comment", + skill.execute( + { + "text": "요즘 완전 폼 미쳤다", + "context": "A comment on a YouTube video about a popular singer.", + "action": "interpret", + } + ), + ) + + print("\nScenario 2: suggest praise for peers") + suggested = skill.execute( + {"action": "suggest", "intent": "praise", "audience": "peers"} + ) + print(f" status: {suggested['status']}") + for item in suggested.get("suggestions") or []: + print(f" suggestion: {_dump(item['text'])}") + + print("\nScenario 3: lookup aljaldakkkalsen") + looked = skill.execute({"action": "lookup", "text": "알잘딱깔센"}) + entry = looked.get("entry") or {} + print(f" found: {looked.get('found')} id={entry.get('id')}") + + _print_interpret( + "Scenario 4: unmatched formal Korean", + skill.execute({"text": "오늘 날씨가 좋습니다."}), + ) + + _print_interpret( + "Scenario 5: blocked appearance slur is omitted", + skill.execute({"text": "그 댓글에 테무인간이라고 써 있음"}), + ) + + print("\nDemo complete.") + + +if __name__ == "__main__": + run_demo() diff --git a/pyproject.toml b/pyproject.toml index e586a720..6c1bfdfa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,6 +105,8 @@ dev_tools = [] finance = [] +linguistics = [] + monitoring = [] office = [ @@ -163,6 +165,8 @@ finance_uk_companies_house_handler = [] finance_wallet_screening = [] +linguistics_korean_slang = [] + monitoring_kpi_gate = [] monitoring_token_limiter = [] diff --git a/scripts/korean_slang_stress_sim.py b/scripts/korean_slang_stress_sim.py new file mode 100644 index 00000000..02bed1c7 --- /dev/null +++ b/scripts/korean_slang_stress_sim.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +"""Offline stress harness for linguistics/korean_slang (safe to run in CI). + +Walks a simulated group-chat plus every generation intent/audience pair. +Exits 1 if any scenario fails. No network. +""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +if str(REPO_ROOT) not in sys.path: + sys.path.insert(0, str(REPO_ROOT)) + +from skillware.core.loader import SkillLoader # noqa: E402 + +CHAT = [ + "요즘 완전 폼 미쳤다 ㄹㅇ", + "점메추 ㄱㄱ", + "내일 내또출이라 현타 옴", + "알잘딱깔센으로 해줘", + "추구미 그잡채", + "킹받네 에바참치", + "오운완 ㅋㅋ", + "불금인데 치맥?", + "최애 직캠 소름", + "워라밸 글렀고 야근각", + "억텐 모드로 회식 가는 중", + "스불재로 밤새고 멘붕", + "주불 호캉스 ㄱㄱ", + "만반잘부 ㅎㅇ", + "이건 별다줄 그 자체", + "폼미쳤다", + "pom michyeotda", + "갓생러는 갓생 중", + "ㄹㅇㅋㅋ ㅇㅈ", + "안녕하세요. 오늘 날씨가 좋습니다.", +] + +INTENTS = [ + "praise", + "food", + "work", + "tired", + "annoyed", + "agree", + "greeting", + "bye", + "thanks", + "workout", + "weekend", + "dating", + "fandom", + "competence", +] +AUDIENCES = ("peers", "mixed", "work", "elders") + + +def main() -> int: + bundle = SkillLoader.load_skill("linguistics/korean_slang") + skill = bundle["class"]() + failures = 0 + + print("korean slang stress sim") + print(f"pack entries: {skill.execute({'text': '폼 미쳤다'}).get('entry_count')}") + + for line in CHAT: + result = skill.execute({"text": line}) + if result.get("status") != "ok": + print(f"FAIL interpret {ascii(line)}: {result.get('error')}") + failures += 1 + continue + print( + "ok interpret hit_count=" + f"{result['hit_count']} unmatched={result['unmatched']} {ascii(line)}" + ) + + for intent in INTENTS: + for audience in AUDIENCES: + result = skill.execute( + {"action": "suggest", "intent": intent, "audience": audience} + ) + if result.get("status") != "ok": + print(f"FAIL suggest {intent}/{audience}: {result}") + failures += 1 + continue + blob = json.dumps(result, ensure_ascii=False) + if "존나" in blob or "테무인간" in blob: + print(f"FAIL leak {intent}/{audience}") + failures += 1 + continue + print(f"ok suggest {intent}/{audience} n={result.get('hit_count')}") + + empty = skill.execute({"action": "interpret", "text": ""}) + if empty.get("status") != "error": + print("FAIL empty text should error") + failures += 1 + else: + print("ok empty-text contract error") + + if failures: + print(f"stress sim failed: {failures}") + return 1 + print("stress sim passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/linguistics/__init__.py b/skills/linguistics/__init__.py new file mode 100644 index 00000000..79e90fcc --- /dev/null +++ b/skills/linguistics/__init__.py @@ -0,0 +1 @@ +# Linguistics skills package. diff --git a/skills/linguistics/korean_slang/__init__.py b/skills/linguistics/korean_slang/__init__.py new file mode 100644 index 00000000..9efa6f2e --- /dev/null +++ b/skills/linguistics/korean_slang/__init__.py @@ -0,0 +1,5 @@ +# Deterministic Korean slang lexicon skill bundle. + +from .skill import KoreanSlangSkill + +__all__ = ["KoreanSlangSkill"] diff --git a/skills/linguistics/korean_slang/card.json b/skills/linguistics/korean_slang/card.json new file mode 100644 index 00000000..02b9a555 --- /dev/null +++ b/skills/linguistics/korean_slang/card.json @@ -0,0 +1,36 @@ +{ + "name": "Korean Slang", + "description": "Offline Korean Gen-Z slang interpreter and peer-register generator.", + "issuer": { + "name": "Blade Dancer", + "email": "bladedancerc3@gmail.com", + "github": "bd-c3" + }, + "icon": "languages", + "color": "#c45c26", + "ui_schema": { + "type": "card", + "fields": [ + { + "key": "status", + "label": "Status" + }, + { + "key": "action", + "label": "Action" + }, + { + "key": "formality", + "label": "Formality" + }, + { + "key": "hit_count", + "label": "Hits" + }, + { + "key": "translation", + "label": "Translation" + } + ] + } +} diff --git a/skills/linguistics/korean_slang/instructions.md b/skills/linguistics/korean_slang/instructions.md new file mode 100644 index 00000000..496e7627 --- /dev/null +++ b/skills/linguistics/korean_slang/instructions.md @@ -0,0 +1,57 @@ +# Instructions: Korean Slang (`linguistics/korean_slang`) + +You have access to the `linguistics/korean_slang` tool. + +This skill is a **deterministic, offline slang lexicon** for modern Korean +internet language (Gen-Z / MZ 줄임말, Konglish blends, jamo abbreviations). +`execute()` never calls a model and never uses the network. If a phrase is not +in the bundled pack, the tool reports `unmatched` instead of inventing a gloss. + +The pack is a September 2026 snapshot. Slang moves weekly; treat misses as +honest gaps, not as permission to improvise fake 줄임말. + +## When to invoke + +- The end user wrote Korean (or heavily mixed 한영) that looks abbreviated, + jokey, or "too internet" for a textbook model. +- You need to **speak like a Korean peer** in a group chat, comment, or DM + without sounding like a textbook or a news anchor. +- You need a formality check before sending Korean to elders, managers, or + mixed-age rooms. + +Do not invoke it for literary translation, legal Korean, or standard polite +prose that contains no slang. Do not invoke it to generate insults. + +## Actions + +- `interpret` (default) — gloss `text`. Returns the issue contract fields + `translation`, `slang_breakdown`, `nuance`, `formality`, plus `hits`. +- `suggest` — produce peer-register Korean lines from templates. Pass + `intent` (praise, food, work, tired, annoyed, agree, greeting, bye, + thanks, workout, weekend, dating, fandom, competence) and/or English/Korean + `text` to classify. Set `audience` to `peers`, `mixed`, `work`, or `elders`. +- `lookup` — exact surface, alias, or romanization lookup. + +## How to interpret the output + +- Copy `translation` and `nuance` into your reply. Do not recast unmatched + slang as if you understood it. +- If `warnings` mention caution or blocked terms, explain the incoming text + without repeating slurs or appearance-policing jokes. +- If `audience=elders`, send the polite `suggestions` and do not add ㅋㅋ, + ㄹㅇ, or 반말 on top. +- `formality` of `Informal / Vulgar` means you should not paste the Korean + into a workplace or elder-facing message unless the operator explicitly + asked for that register (`vulgar_ok`). +- On `status=error`, tell the operator the `error.detail` and do not guess. + +## Examples + +- User: "What does 요즘 완전 폼 미쳤다 mean on a YouTube comment?" + -> Call `linguistics/korean_slang` with + `text="요즘 완전 폼 미쳤다"`, `context="YouTube comment about a singer"`, + `action="interpret"`. +- User: "Reply like a Korean twenty-something: that performance was insane." + -> Call suggest with `intent="praise"`, `audience="peers"`. +- User: "Say thanks to my friend's grandmother in Korean." + -> Call suggest with `intent="thanks"`, `audience="elders"` (no slang). diff --git a/skills/linguistics/korean_slang/kb/constitution.json b/skills/linguistics/korean_slang/kb/constitution.json new file mode 100644 index 00000000..f42358ce --- /dev/null +++ b/skills/linguistics/korean_slang/kb/constitution.json @@ -0,0 +1,17 @@ +{ + "blocked_substrings": [ + "테무인간", + "밤티", + "얼평", + "몸평", + "한남충", + "메갈년", + "일베충", + "틀딱충", + "급식충" + ], + "blocked_notes": [ + "Appearance-policing slurs and gendered/political hate terms are never glossed in detail and never generated.", + "한남/메갈 as political slurs are blocked even when they appear as substrings of longer abuse." + ] +} diff --git a/skills/linguistics/korean_slang/kb/entries.json b/skills/linguistics/korean_slang/kb/entries.json new file mode 100644 index 00000000..3f724d8a --- /dev/null +++ b/skills/linguistics/korean_slang/kb/entries.json @@ -0,0 +1,1792 @@ +[ + { + "id": "pom_michyeotda", + "surfaces": ["폼 미쳤다", "폼미쳤다", "폼 미쳤네", "폼 미쳤음", "폼 미침"], + "aliases": ["killing it", "form is crazy"], + "romanization": ["pom michyeotda", "form michyeotda"], + "kind": "phrase", + "intents": ["praise", "hype"], + "translation": "They are absolutely killing it right now / Their form is crazy these days", + "breakdown": "Slang directly translating to 'form is crazy', used to praise someone doing exceptionally well or looking great.", + "nuance": "Highly positive, trendy internet-friendly colloquial expression used primarily by Gen-Z and Millennials.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "geujapchae", + "surfaces": ["그잡채", "그 잡채"], + "aliases": ["that's the vibe", "nailed the aesthetic"], + "romanization": ["geujapchae"], + "kind": "phrase", + "intents": ["praise", "agree", "fandom"], + "translation": "That's exactly the vibe / you nailed that aesthetic", + "breakdown": "그 잡채 — 'that side dish / that look'. Used when something matches the desired 추구미 or mood perfectly.", + "nuance": "Aesthetic approval, not a food comment. Common under selfies, fits, and stage clips.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "aljaldakkkalsen", + "surfaces": ["알잘딱깔센", "알잘딱깔쎈"], + "aliases": ["handle it stylishly", "you know what to do"], + "romanization": ["aljaldakkkalsen", "aljaldakkkalsen"], + "kind": "phrase", + "intents": ["competence"], + "translation": "You already know — do it properly, stylishly, and smartly", + "breakdown": "알아서 잘 딱 깔끔하고 센스있게. A packed brief: figure it out, do it well, keep it neat, add taste.", + "nuance": "Trust plus a high bar. Can sound like praise or like 'don't make me micromanage'.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "aljaldak", + "surfaces": ["알잘딱"], + "aliases": ["handle it"], + "romanization": ["aljaldak"], + "kind": "phrase", + "intents": ["competence"], + "translation": "Figure it out and do it right", + "breakdown": "알아서 잘 딱. Shorter sibling of 알잘딱깔센.", + "nuance": "Workplace group chats use this as a compressed brief. Still informal.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "byeoldajul", + "surfaces": ["별다줄"], + "aliases": ["they abbreviate everything"], + "romanization": ["byeoldajul"], + "kind": "phrase", + "intents": ["agree"], + "translation": "They abbreviate absolutely everything", + "breakdown": "별걸 다 줄인다. Meta-slang about MZ abbreviation culture; itself an abbreviation.", + "nuance": "Often half-exasperated, half-amused commentary on how compressed Korean chat has become.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "jeommechu", + "surfaces": ["점메추"], + "aliases": ["lunch rec", "lunch recommendation"], + "romanization": ["jeommechu"], + "kind": "phrase", + "intents": ["food", "work"], + "translation": "Recommend a lunch menu", + "breakdown": "점심 메뉴 추천. The daily group-chat lunch poll.", + "nuance": "Work-safe among peers. Signals 'it is lunch o'clock' more than a request for a gourmet essay.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "yamechu", + "surfaces": ["야메추"], + "aliases": ["late night food rec"], + "romanization": ["yamechu"], + "kind": "phrase", + "intents": ["food"], + "translation": "Recommend a late-night snack", + "breakdown": "야식 메뉴 추천.", + "nuance": "Night-owl register. Pair with delivery culture, not a sit-down dinner with elders.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "ojeochigo", + "surfaces": ["오저치고"], + "aliases": ["chicken tonight"], + "romanization": ["ojeochigo"], + "kind": "phrase", + "intents": ["food", "weekend"], + "translation": "Chicken for dinner tonight — let's go", + "breakdown": "오늘 저녁 치킨 고.", + "nuance": "Playful rallying cry, not a restaurant reservation script.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "naettochul", + "surfaces": ["내또출"], + "aliases": ["work again tomorrow"], + "romanization": ["naettochul"], + "kind": "phrase", + "intents": ["work", "tired"], + "translation": "I have to go to work again tomorrow", + "breakdown": "내일 또 출근. Sunday-evening / late-week resignation humor.", + "nuance": "Communal groan, not despair. Saying it out loud makes the commute slightly more bearable.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "toejunsaeng", + "surfaces": ["퇴준생"], + "aliases": ["preparing to quit"], + "romanization": ["toejunsaeng"], + "kind": "phrase", + "intents": ["work"], + "translation": "Someone preparing to resign", + "breakdown": "퇴사 준비생. Built like 취준생 / 수험생.", + "nuance": "Candid workplace identity. Do not use in front of the person's manager.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "toesagak", + "surfaces": ["퇴사각"], + "aliases": ["time to quit"], + "romanization": ["toesagak"], + "kind": "phrase", + "intents": ["work", "annoyed"], + "translation": "Looks like it's time to quit", + "breakdown": "퇴사 + 각 (timing / angle). 'The resignation timing is here.'", + "nuance": "Hyperbolic vent more often than a real two-week notice.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "gatsaeng", + "surfaces": ["갓생"], + "aliases": ["god life", "disciplined routine"], + "romanization": ["gatsaeng", "gat-saeng"], + "kind": "phrase", + "intents": ["workout", "praise"], + "translation": "A disciplined, god-tier daily routine", + "breakdown": "갓(God) + 생(생활). Early alarms, gym, studying, clean eating — or joking that you failed at it.", + "nuance": "Aspirational and self-mocking in equal measure. Often '나는 갓생 글렀다'.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "gatsaengreo", + "surfaces": ["갓생러"], + "aliases": ["god-life person"], + "romanization": ["gatsaengreo"], + "kind": "phrase", + "intents": ["workout", "praise"], + "translation": "A person living that god-tier routine", + "breakdown": "갓생 + -러 (person). Someone visibly winning at routines.", + "nuance": "Compliment with a hint of envy. Do not use sarcastically at elders.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "hyeonta", + "surfaces": ["현타"], + "aliases": ["reality check", "reality hit"], + "romanization": ["hyeonta"], + "kind": "phrase", + "intents": ["tired"], + "translation": "Sudden reality check", + "breakdown": "현실 자각 타임. The crash after hype, shopping, or vacation brain.", + "nuance": "Still fully current in 2026. Comic self-awareness more than clinical depression.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "hyeonsaeng", + "surfaces": ["현생"], + "aliases": ["real life"], + "romanization": ["hyeonsaeng"], + "kind": "phrase", + "intents": ["tired", "work"], + "translation": "Real life (as opposed to games, fandom, or chat)", + "breakdown": "현실 생활.", + "nuance": "Used when offline obligations interrupt the fun register.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "kingbatda", + "surfaces": ["킹받다", "킹받네", "킹받음", "킹받"], + "aliases": ["so annoying", "I'm fuming"], + "romanization": ["kingbatda"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "Extremely annoyed; that really got to me", + "breakdown": "킹(King) + 받다. Max-level 열받다 with a comic English intensifier.", + "nuance": "Stronger than 짜증, lighter than a real fight. Peer chat only.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "eokten", + "surfaces": ["억텐"], + "aliases": ["forced energy", "fake hype"], + "romanization": ["eokten"], + "kind": "phrase", + "intents": ["tired", "work"], + "translation": "Forced hype / fake high energy", + "breakdown": "억지 텐션. Performing brightness at dinners, lives, or team calls.", + "nuance": "Self-aware: '나 지금 억텐임'. Names the gap between felt and performed emotion.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "nojeol", + "surfaces": ["뇌절"], + "aliases": ["overdoing the bit", "brain freeze from repetition"], + "romanization": ["nojeol"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "The joke or bit has been milked to death", + "breakdown": "Brain + freeze/cut. A meme, catchphrase, or behavior repeated past funny.", + "nuance": "Call-out for overplayed content. Can be affectionate or a stop sign.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "chugumi", + "surfaces": ["추구미"], + "aliases": ["desired vibe", "goal aesthetic"], + "romanization": ["chugumi"], + "kind": "phrase", + "intents": ["fandom", "praise"], + "translation": "The look / vibe I am going for", + "breakdown": "추구미 — the aesthetic you are chasing (fashion, aura, character).", + "nuance": "Identity mood-board language. Pair with 그잡채 when someone hits it.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "neujoh", + "surfaces": ["느좋"], + "aliases": ["this feels nice"], + "romanization": ["neujoh"], + "kind": "phrase", + "intents": ["praise"], + "translation": "The vibe feels good", + "breakdown": "느낌 좋아.", + "nuance": "Soft approval for atmosphere, music, cafes, or a look — less hype than 폼 미쳤다.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "seubuljae", + "surfaces": ["스불재"], + "aliases": ["self-inflicted disaster"], + "romanization": ["seubuljae"], + "kind": "phrase", + "intents": ["tired", "annoyed"], + "translation": "A disaster I brought on myself", + "breakdown": "스스로 불러온 재앙.", + "nuance": "Comic ownership of a bad decision (all-nighter, impulse buy, sending the text).", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "jubuol", + "surfaces": ["주불"], + "aliases": ["weekend plans heat"], + "romanization": ["jubuol", "jubl"], + "kind": "phrase", + "intents": ["weekend"], + "translation": "The weekend is on fire / weekend plans incoming", + "breakdown": "주말에 불타는. Weekend hype, sometimes literally 불금 energy spilling into Saturday.", + "nuance": "Plans-and-alcohol register, not a status meeting.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "dujjongku", + "surfaces": ["두쫀쿠"], + "aliases": ["double chewy cookie"], + "romanization": ["dujjongku"], + "kind": "phrase", + "intents": ["food"], + "translation": "Double-chewy cookie (viral dessert nickname)", + "breakdown": "두 번 씹으면 쫀득한 쿠키. A 2020s dessert nickname that stuck in food chats.", + "nuance": "Treat as a menu noun. Using it flexes that you follow dessert trends.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "manbanjalbu", + "surfaces": ["만반잘부"], + "aliases": ["nice to meet you, please be kind"], + "romanization": ["manbanjalbu"], + "kind": "phrase", + "intents": ["greeting"], + "translation": "Nice to meet you — please take care of me", + "breakdown": "만나서 반가워 잘 부탁해.", + "nuance": "New-chat / new-member icebreaker. Informal 반말 packed into one stamp.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "honkono", + "surfaces": ["혼코노"], + "aliases": ["solo karaoke"], + "romanization": ["honkono"], + "kind": "phrase", + "intents": ["weekend"], + "translation": "Solo karaoke", + "breakdown": "혼자 코인노래방.", + "nuance": "Self-care flex, not loneliness by default.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "honbap", + "surfaces": ["혼밥"], + "aliases": ["eating alone"], + "romanization": ["honbap"], + "kind": "phrase", + "intents": ["food"], + "translation": "Eating alone", + "breakdown": "혼자 밥.", + "nuance": "Neutral-to-positive in 2026. Not an insult.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "honyeong", + "surfaces": ["혼영"], + "aliases": ["watching a movie alone"], + "romanization": ["honyeong"], + "kind": "phrase", + "intents": ["weekend"], + "translation": "Watching a movie alone", + "breakdown": "혼자 영화.", + "nuance": "Same 혼- family as 혼밥 / 혼술. Independent leisure.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "honsul", + "surfaces": ["혼술"], + "aliases": ["drinking alone"], + "romanization": ["honsul"], + "kind": "phrase", + "intents": ["weekend"], + "translation": "Drinking alone", + "breakdown": "혼자 술.", + "nuance": "Casual. Do not moralize it in-character unless the operator asks.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "gapbunssa", + "surfaces": ["갑분싸"], + "aliases": ["vibe suddenly died"], + "romanization": ["gapbunssa"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "The vibe suddenly went cold", + "breakdown": "갑자기 분위기 싸해짐.", + "nuance": "Someone said the awkward thing. Naming it can either defuse or make it worse.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "utpeu", + "surfaces": ["웃프", "웃프다"], + "aliases": ["funny but sad"], + "romanization": ["utpeu"], + "kind": "phrase", + "intents": ["tired"], + "translation": "Funny but sad; bittersweet laugh", + "breakdown": "웃기다 + 슬프다.", + "nuance": "Emotional compression. Meme caption energy.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "kebake", + "surfaces": ["케바케"], + "aliases": ["case by case"], + "romanization": ["kebake"], + "kind": "phrase", + "intents": ["agree", "competence"], + "translation": "Depends on the case", + "breakdown": "Case by case, Koreanized.", + "nuance": "Non-answer that still sounds native. Useful when the operator wants a hedge.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "ounwan", + "surfaces": ["오운완"], + "aliases": ["workout complete"], + "romanization": ["ounwan"], + "kind": "phrase", + "intents": ["workout"], + "translation": "Today's workout is done", + "breakdown": "오늘 운동 완료. Gym-story stamp.", + "nuance": "Caption-sized. Often paired with a mirror photo the skill will never request.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "jungkkeokma", + "surfaces": ["중꺾마", "중꺾그마"], + "aliases": ["never fold"], + "romanization": ["jungkkeokma"], + "kind": "phrase", + "intents": ["praise", "fandom"], + "translation": "What matters is a heart that does not break", + "breakdown": "중요한 건 꺾이지 않는 마음. Rallying slogan that became stamp-sized.", + "nuance": "Sports / fandom encouragement. A bit 2022-vintage but still readable in 2026.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "halmanhaan", + "surfaces": ["할많하않", "할말하않"], + "aliases": ["I have a lot to say but I won't"], + "romanization": ["halmanhaan"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "I have a lot to say, but I will not say it", + "breakdown": "할 말은 많지만 하지 않겠다.", + "nuance": "Passive-aggressive mute button. Reads as shade.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "paekpok", + "surfaces": ["팩폭", "팩트폭행"], + "aliases": ["harsh truth"], + "romanization": ["paekpok"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "Hitting someone with an uncomfortable fact", + "breakdown": "팩트 폭행.", + "nuance": "Can be funny among friends; cruel if aimed at appearance or trauma. Prefer interpret over generate when targeting a person.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": true, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "silhwanya", + "surfaces": ["실화냐", "실화"], + "aliases": ["no way", "is this real"], + "romanization": ["silhwanya"], + "kind": "phrase", + "intents": ["agree", "praise"], + "translation": "Is this real? / no way", + "breakdown": "실화(냐). Disbelief stamp for news, clips, or plots.", + "nuance": "Low-effort hype or shock. Fine in comments.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "danggeuniji", + "surfaces": ["당근이지"], + "aliases": ["obviously", "duh"], + "romanization": ["danggeuniji"], + "kind": "phrase", + "intents": ["agree"], + "translation": "Obviously / of course", + "breakdown": "당연하지 → 당근이지. Also collides with the 당근 marketplace brand; context decides.", + "nuance": "When the thread is not about used goods, it means 'duh'.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "meoseon129", + "surfaces": ["머선129"], + "aliases": ["what happened"], + "romanization": ["meoseon129"], + "kind": "phrase", + "intents": ["greeting"], + "translation": "What is going on?", + "breakdown": "무슨 일이야. 129 as keypad-ish / meme spelling of 일이야.", + "nuance": "Playful confusion, not a police report.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "bunjohka", + "surfaces": ["분좋카"], + "aliases": ["aesthetic cafe"], + "romanization": ["bunjohka"], + "kind": "phrase", + "intents": ["food", "dating"], + "translation": "A cafe with a good atmosphere", + "breakdown": "분위기 좋은 카페.", + "nuance": "Date / photo-spot shorthand.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "matjalal", + "surfaces": ["맛잘알"], + "aliases": ["knows good food"], + "romanization": ["matjalal"], + "kind": "phrase", + "intents": ["food"], + "translation": "Someone who really knows flavor", + "breakdown": "맛을 잘 아는 사람.", + "nuance": "Compliment for recs. Less vulgar than 존맛탱.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "kagong", + "surfaces": ["카공", "카공족"], + "aliases": ["cafe studying"], + "romanization": ["kagong"], + "kind": "phrase", + "intents": ["work"], + "translation": "Studying or working at a cafe", + "breakdown": "카페 공부. 카공족 = people who occupy cafes with laptops.", + "nuance": "Lifestyle tag. Neutral.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "bulgeum", + "surfaces": ["불금"], + "aliases": ["fiery friday"], + "romanization": ["bulgeum"], + "kind": "phrase", + "intents": ["weekend"], + "translation": "TGIF — Friday night is for going out", + "breakdown": "불타는 금요일.", + "nuance": "Still alive. Older than MZ but used by them.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "wollyobyeong", + "surfaces": ["월요병"], + "aliases": ["monday blues"], + "romanization": ["wollyobyeong"], + "kind": "phrase", + "intents": ["work", "tired"], + "translation": "Monday sickness / Monday blues", + "breakdown": "월요일 + 병. Comic diagnosis.", + "nuance": "Safe small-talk slang even in mixed company.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "seontok", + "surfaces": ["선톡"], + "aliases": ["text first"], + "romanization": ["seontok"], + "kind": "phrase", + "intents": ["dating", "greeting"], + "translation": "Sending the first message", + "breakdown": "먼저 보내는 카톡.", + "nuance": "Dating / new-friend anxiety object. Asking someone to 선톡 is a small dare.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "jamanchu", + "surfaces": ["자만추"], + "aliases": ["organic meetup"], + "romanization": ["jamanchu"], + "kind": "phrase", + "intents": ["dating"], + "translation": "Hoping to meet naturally", + "breakdown": "자연스러운 만남 추구. Dating-profile cliche.", + "nuance": "Often ironic in 2026 — people joke that nobody actually meets 'naturally'.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "inchin", + "surfaces": ["인친"], + "aliases": ["insta friend"], + "romanization": ["inchin"], + "kind": "phrase", + "intents": ["fandom", "greeting"], + "translation": "Instagram friend", + "breakdown": "인스타 친구.", + "nuance": "Weak-tie social graph. Not the same as 친구.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "hokangseu", + "surfaces": ["호캉스"], + "aliases": ["hotel vacation"], + "romanization": ["hokangseu"], + "kind": "phrase", + "intents": ["weekend"], + "translation": "A staycation at a hotel", + "breakdown": "호텔 + 바캉스.", + "nuance": "Treat-yourself leisure. Common caption noun.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "chimaek", + "surfaces": ["치맥"], + "aliases": ["chicken and beer"], + "romanization": ["chimaek"], + "kind": "phrase", + "intents": ["food", "weekend"], + "translation": "Fried chicken and beer", + "breakdown": "치킨 + 맥주.", + "nuance": "Established but still the default 불금 menu pairing.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "kkuankku", + "surfaces": ["꾸안꾸"], + "aliases": ["effortless look"], + "romanization": ["kkuankku"], + "kind": "phrase", + "intents": ["praise"], + "translation": "Looks done-up without looking done-up", + "breakdown": "꾸민 듯 안 꾸민 듯.", + "nuance": "Fashion compliment. Do not twist it into body commentary.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "neomsabyeok", + "surfaces": ["넘사벽"], + "aliases": ["out of my league", "another level"], + "romanization": ["neomsabyeok"], + "kind": "phrase", + "intents": ["praise"], + "translation": "On another level; a wall I cannot pass", + "breakdown": "넘을 수 없는 사차원의 벽.", + "nuance": "Awe. Skill, looks, or wealth — keep it about performance when generating.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "haekinsa", + "surfaces": ["핵인싸"], + "aliases": ["ultra insider"], + "romanization": ["haekinsa"], + "kind": "phrase", + "intents": ["praise"], + "translation": "Hyper social insider", + "breakdown": "핵 + 인사이더. The person invited to everything.", + "nuance": "Can be teasing. Opposite cluster is 아싸.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "insa", + "surfaces": ["인싸"], + "aliases": ["insider", "social"], + "romanization": ["insa"], + "kind": "word", + "intents": ["praise"], + "translation": "Social insider", + "breakdown": "인사이더. Comfortable in group culture.", + "nuance": "Shorter than 핵인싸. Longest-match prefers 핵인싸 when both present.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "assa", + "surfaces": ["아싸"], + "aliases": ["outsider"], + "romanization": ["assa"], + "kind": "word", + "intents": ["tired"], + "translation": "Outsider / not the social one", + "breakdown": "아웃사이더. Self-label more often than an attack in peer chat.", + "nuance": "Do not use it to exclude a third person.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "choiae", + "surfaces": ["최애"], + "aliases": ["bias", "favorite"], + "romanization": ["choiae"], + "kind": "word", + "intents": ["fandom"], + "translation": "Ultimate favorite (bias)", + "breakdown": "최고 + 애정. Fandom bias, sometimes a favorite food or item.", + "nuance": "Core K-pop / fandom vocabulary LLMs often underplay.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "ipdeok", + "surfaces": ["입덕"], + "aliases": ["stan conversion"], + "romanization": ["ipdeok"], + "kind": "phrase", + "intents": ["fandom"], + "translation": "Falling into a fandom", + "breakdown": "입 + 덕(덕질). The moment you get hooked.", + "nuance": "Positive conversion. Opposite 탈덕.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "taldeok", + "surfaces": ["탈덕"], + "aliases": ["leaving a fandom"], + "romanization": ["taldeok"], + "kind": "phrase", + "intents": ["fandom"], + "translation": "Leaving a fandom", + "breakdown": "탈 + 덕.", + "nuance": "Can be bitter. Do not celebrate someone else's 탈덕 unsolicited.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "deokjil", + "surfaces": ["덕질"], + "aliases": ["stanning", "fan labor"], + "romanization": ["deokjil"], + "kind": "phrase", + "intents": ["fandom"], + "translation": "The practice of being a fan", + "breakdown": "오타쿠/덕후 + 질 (doing that thing).", + "nuance": "Neutral-to-proud. Streaming, concerts, collecting.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "rejeondeu", + "surfaces": ["레전드"], + "aliases": ["legendary"], + "romanization": ["rejeondeu"], + "kind": "word", + "intents": ["praise"], + "translation": "Legendary", + "breakdown": "English 'legend' as a Korean noun/adjective of maximum praise.", + "nuance": "Overused on purpose. Still the top-shelf hype stamp.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "regenno", + "surfaces": ["레게노"], + "aliases": ["legend (jumbled)"], + "romanization": ["regenno"], + "kind": "phrase", + "intents": ["praise"], + "translation": "Legendary (playful scramble of 레전드)", + "breakdown": "레전드 with syllables shuffled. Meme spelling.", + "nuance": "Even more jokey than 레전드.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "peullekseu", + "surfaces": ["플렉스"], + "aliases": ["flex"], + "romanization": ["peullekseu"], + "kind": "word", + "intents": ["praise"], + "translation": "Flex / showing off (often playfully)", + "breakdown": "English flex. Brag with irony.", + "nuance": "Can be self-flex or calling someone out. Keep it light when generating.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "hatpeul", + "surfaces": ["핫플"], + "aliases": ["hot place"], + "romanization": ["hatpeul"], + "kind": "phrase", + "intents": ["food", "weekend"], + "translation": "A currently trending spot", + "breakdown": "핫 플레이스.", + "nuance": "Cafes, bars, pop-ups, neighborhoods. FOMO noun.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "gaseongbi", + "surfaces": ["가성비"], + "aliases": ["value for money"], + "romanization": ["gaseongbi"], + "kind": "word", + "intents": ["food", "competence"], + "translation": "Bang for the buck", + "breakdown": "가격 대비 성능.", + "nuance": "Mainstream but still the default rec filter. Work-safe.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "gasimbi", + "surfaces": ["가심비"], + "aliases": ["emotional value"], + "romanization": ["gasimbi"], + "kind": "word", + "intents": ["food"], + "translation": "Worth it for how it makes you feel", + "breakdown": "가격 대비 심리 만족. Paying for vibe, not spec.", + "nuance": "The 2020s answer to 가성비 — justifies the extra spend.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "kaltoe", + "surfaces": ["칼퇴"], + "aliases": ["leave work on time"], + "romanization": ["kaltoe"], + "kind": "phrase", + "intents": ["work"], + "translation": "Clocking out on the dot", + "breakdown": "칼같이 퇴근.", + "nuance": "Aspiration and small victory. Work-safe among peers.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "yageun", + "surfaces": ["야근"], + "aliases": ["overtime"], + "romanization": ["yageun"], + "kind": "word", + "intents": ["work", "tired"], + "translation": "Working overtime at night", + "breakdown": "Standard overtime noun that still dominates complaint chat.", + "nuance": "Not cute slang, but the collocate of 내또출 / 칼퇴.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "worlabel", + "surfaces": ["워라밸", "워라벨"], + "aliases": ["work life balance"], + "romanization": ["worlabel"], + "kind": "word", + "intents": ["work"], + "translation": "Work-life balance", + "breakdown": "Work-life balance Konglish.", + "nuance": "Still a serious job-filter word in 2026.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "bonkae", + "surfaces": ["본캐"], + "aliases": ["main account", "main character"], + "romanization": ["bonkae"], + "kind": "word", + "intents": ["fandom"], + "translation": "Main account / main character", + "breakdown": "본 캐릭터. Games, alts, or 'my real job vs hobby self'.", + "nuance": "Pairs with 부캐.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "bukae", + "surfaces": ["부캐"], + "aliases": ["alt account", "side character"], + "romanization": ["bukae"], + "kind": "word", + "intents": ["fandom", "work"], + "translation": "Alt / side hustle persona", + "breakdown": "부 캐릭터.", + "nuance": "Side jobs and secret hobbies live here.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "nojaem", + "surfaces": ["노잼"], + "aliases": ["not fun"], + "romanization": ["nojaem"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "Not fun; boring", + "breakdown": "No + 재미.", + "nuance": "Blunt. Fine for content, rude for a person's personality.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "kkuljaem", + "surfaces": ["꿀잼"], + "aliases": ["so fun"], + "romanization": ["kkuljaem"], + "kind": "phrase", + "intents": ["praise"], + "translation": "Honey-fun; really entertaining", + "breakdown": "꿀 + 재미.", + "nuance": "Easy compliment for shows, games, dates.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "nodap", + "surfaces": ["노답"], + "aliases": ["no answer", "hopeless"], + "romanization": ["nodap"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "No answer; this is hopeless", + "breakdown": "No + 답.", + "nuance": "Situations or people. Prefer situations when generating.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "eba", + "surfaces": ["에바"], + "aliases": ["over the line"], + "romanization": ["eba"], + "kind": "word", + "intents": ["annoyed"], + "translation": "That's too much / over the line", + "breakdown": "Over, Koreanized. Often 에바잖아.", + "nuance": "Boundary stamp. Very high frequency.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "ebachamchi", + "surfaces": ["에바참치"], + "aliases": ["way over the line"], + "romanization": ["ebachamchi"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "Way over the line (emphatic 에바)", + "breakdown": "에바 + 참치. Intensifier meme.", + "nuance": "Comic exaggeration of 에바.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "geukhyom", + "surfaces": ["극혐"], + "aliases": ["hate that"], + "romanization": ["geukhyom"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "Extreme dislike", + "breakdown": "극도로 혐오.", + "nuance": "Harsh. Interpret freely; generating it at a person is mean. Marked caution.", + "formality": "Informal / Slang", + "generation_safe": false, + "caution": true, + "vulgar": false + }, + { + "id": "menbung", + "surfaces": ["멘붕"], + "aliases": ["mental breakdown (comic)"], + "romanization": ["menbung"], + "kind": "phrase", + "intents": ["tired"], + "translation": "Mental collapse (usually comic)", + "breakdown": "멘탈 붕괴.", + "nuance": "Not a clinical claim. Still common.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "sonjeol", + "surfaces": ["손절"], + "aliases": ["cut off", "unfriend hard"], + "romanization": ["sonjeol"], + "kind": "phrase", + "intents": ["dating", "annoyed"], + "translation": "Cutting someone off", + "breakdown": "손 + 절 (cut). Stocks slang moved into friendship/dating.", + "nuance": "Severe. Do not generate as a joke about a real person.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": true, + "vulgar": false + }, + { + "id": "ssum", + "surfaces": ["썸"], + "aliases": ["talking stage"], + "romanization": ["ssum"], + "kind": "word", + "intents": ["dating"], + "translation": "The talking stage; undefined almost-relationship", + "breakdown": "Something. Neither official nor nothing.", + "nuance": "Core dating vocab. 썸탄다 = riding that undefined state.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "mildang", + "surfaces": ["밀당"], + "aliases": ["push and pull"], + "romanization": ["mildang"], + "kind": "phrase", + "intents": ["dating"], + "translation": "Push-and-pull dating tactics", + "breakdown": "밀고 당기기.", + "nuance": "Often criticized as game-playing; still used descriptively.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "geurinraiteu", + "surfaces": ["그린라이트"], + "aliases": ["green light"], + "romanization": ["geurinraiteu"], + "kind": "word", + "intents": ["dating"], + "translation": "A romantic green light", + "breakdown": "Green light. Signals interest.", + "nuance": "Dating-analysis language from variety TV that stuck.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "kkondae", + "surfaces": ["꼰대"], + "aliases": ["boomer boss", "preachy elder"], + "romanization": ["kkondae"], + "kind": "word", + "intents": ["annoyed"], + "translation": "A preachy, hierarchy-flexing older person", + "breakdown": "Originally 'old man'; now anyone lecturing from unearned authority.", + "nuance": "Do not generate toward the end user's parents or boss unless asked to explain the term.", + "formality": "Informal / Slang", + "generation_safe": false, + "caution": true, + "vulgar": false + }, + { + "id": "naeronambul", + "surfaces": ["내로남불"], + "aliases": ["double standard"], + "romanization": ["naeronambul"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "Fine when I do it, wrong when you do it", + "breakdown": "내가 하면 로맨스, 남이 하면 불륜.", + "nuance": "Political and interpersonal hypocrisy stamp.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "pingpeu", + "surfaces": ["핑프"], + "aliases": ["finger princess"], + "romanization": ["pingpeu"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "Someone who makes others type / do the work for them", + "breakdown": "핑거 프린세스. Asks 'what does this mean' instead of searching; also 'do it for me'.", + "nuance": "Widely used and also called out as language violence. Explain incoming use; do not generate as a label.", + "formality": "Informal / Caution", + "generation_safe": false, + "caution": true, + "vulgar": false + }, + { + "id": "aa_ice", + "surfaces": ["아아"], + "aliases": ["iced americano"], + "romanization": ["aa"], + "kind": "jamo", + "intents": ["food"], + "translation": "Iced americano", + "breakdown": "아이스 아메리카노.", + "nuance": "Cafe default. Ambiguous in running Korean; matcher uses jamo boundaries.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "tteua", + "surfaces": ["뜨아"], + "aliases": ["hot americano"], + "romanization": ["tteua"], + "kind": "phrase", + "intents": ["food"], + "translation": "Hot americano", + "breakdown": "뜨거운 아메리카노.", + "nuance": "Pair of 아아.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "ekochembeo", + "surfaces": ["에코챔버"], + "aliases": ["echo chamber"], + "romanization": ["ekochembeo"], + "kind": "word", + "intents": ["annoyed"], + "translation": "Echo chamber", + "breakdown": "English loan used as-is in MZ commentary for same-opinion bubbles.", + "nuance": "More essay-blog than chat stamp, but 2026-current.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "joyonghan_toesa", + "surfaces": ["조용한 퇴사", "조용한퇴사"], + "aliases": ["quiet quitting"], + "romanization": ["joyonghan toesa"], + "kind": "phrase", + "intents": ["work"], + "translation": "Quiet quitting", + "breakdown": "Doing the job and not an inch more, without formally resigning.", + "nuance": "Workplace sociology term still in circulation in 2026.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "seuming", + "surfaces": ["스밍"], + "aliases": ["streaming"], + "romanization": ["seuming"], + "kind": "phrase", + "intents": ["fandom"], + "translation": "Streaming (usually to boost a track)", + "breakdown": "스트리밍. Fan-labor verb/noun.", + "nuance": "K-pop chart culture.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "poka", + "surfaces": ["포카"], + "aliases": ["photocard"], + "romanization": ["poka"], + "kind": "word", + "intents": ["fandom"], + "translation": "Photocard", + "breakdown": "포토카드.", + "nuance": "Collectible economy. Trading chats.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "jikcam", + "surfaces": ["직캠"], + "aliases": ["fancam"], + "romanization": ["jikcam", "jikcam"], + "kind": "phrase", + "intents": ["fandom"], + "translation": "Fancam (fan-shot concert clip)", + "breakdown": "직접 촬영 캠.", + "nuance": "YouTube comment habitat — same neighborhood as 폼 미쳤다.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "paenssa", + "surfaces": ["팬싸"], + "aliases": ["fansign"], + "romanization": ["paenssa"], + "kind": "phrase", + "intents": ["fandom"], + "translation": "Fansign event", + "breakdown": "팬사인회.", + "nuance": "Ticketing-stress adjacent.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "tiketting", + "surfaces": ["티켓팅"], + "aliases": ["ticket war"], + "romanization": ["tiketting"], + "kind": "word", + "intents": ["fandom", "weekend"], + "translation": "Fighting for tickets (usually concerts)", + "breakdown": "Ticketing as a boss fight.", + "nuance": "Anxiety sport. '티켓팅 망했다' is a genre.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "matpal", + "surfaces": ["맞팔"], + "aliases": ["follow for follow"], + "romanization": ["matpal"], + "kind": "phrase", + "intents": ["fandom", "greeting"], + "translation": "Mutual follow", + "breakdown": "맞 + 팔로우.", + "nuance": "Social graph verb.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "seonpal", + "surfaces": ["선팔"], + "aliases": ["follow first"], + "romanization": ["seonpal"], + "kind": "phrase", + "intents": ["fandom"], + "translation": "Following first", + "breakdown": "먼저 팔로우.", + "nuance": "Often hoping for 맞팔.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "eonpal", + "surfaces": ["언팔"], + "aliases": ["unfollow"], + "romanization": ["eonpal"], + "kind": "phrase", + "intents": ["annoyed"], + "translation": "Unfollow", + "breakdown": "언팔로우.", + "nuance": "Relationship weather. Milder than 손절.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "diem", + "surfaces": ["디엠"], + "aliases": ["DM"], + "romanization": ["diem"], + "kind": "word", + "intents": ["dating", "greeting"], + "translation": "Direct message", + "breakdown": "DM.", + "nuance": "Invitation or warning depending on '디엠 ㄱ'.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "baibeu", + "surfaces": ["바이브"], + "aliases": ["vibe"], + "romanization": ["baibeu"], + "kind": "word", + "intents": ["praise"], + "translation": "Vibe", + "breakdown": "Konglish vibe. Atmosphere, energy, aura.", + "nuance": "Pairs with 느좋 / 추구미.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "tensyeon", + "surfaces": ["텐션"], + "aliases": ["energy level"], + "romanization": ["tensyeon"], + "kind": "word", + "intents": ["praise", "tired"], + "translation": "Energy / liveliness", + "breakdown": "Tension in the 'high-energy' sense, not physics. See also 억텐.", + "nuance": "High 텐션 is a compliment for hosts and friends.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "mudeu", + "surfaces": ["무드"], + "aliases": ["mood"], + "romanization": ["mudeu"], + "kind": "word", + "intents": ["praise"], + "translation": "Mood / aesthetic mood", + "breakdown": "Mood. Photo, lighting, playlist.", + "nuance": "Caption Konglish.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "jjeoreo", + "surfaces": ["쩔어", "쩔었다", "쩐다"], + "aliases": ["sick", "fires"], + "romanization": ["jjeoreo"], + "kind": "phrase", + "intents": ["praise"], + "translation": "That's sick / so good", + "breakdown": "Older slang still used as a hype verb.", + "nuance": "Slightly less 2026-coded than 폼 미쳤다 but very natural in speech.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "daebak", + "surfaces": ["대박"], + "aliases": ["jackpot", "whoa"], + "romanization": ["daebak"], + "kind": "word", + "intents": ["praise"], + "translation": "Awesome / jackpot", + "breakdown": "Still the default wow-stamp across ages.", + "nuance": "Safe mixed-company hype. Less 'internet native' than 폼 미쳤다.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "soreum", + "surfaces": ["소름", "소름돋아", "소름돋는다"], + "aliases": ["chills", "goosebumps"], + "romanization": ["soreum"], + "kind": "word", + "intents": ["praise", "fandom"], + "translation": "Goosebumps (awe or creepiness from context)", + "breakdown": "In praise comments, 소름 = the performance gave me chills.", + "nuance": "YouTube-comment cousin of 폼 미쳤다.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "ootd", + "surfaces": ["OOTD", "ootd", "오오티디"], + "aliases": ["outfit of the day"], + "romanization": ["ootd"], + "kind": "latin", + "intents": ["praise"], + "translation": "Outfit of the day", + "breakdown": "Fashion caption tag, often 한글 오오티디.", + "nuance": "Pairs with 꾸안꾸.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "tmi", + "surfaces": ["TMI", "tmi"], + "aliases": ["too much information"], + "romanization": ["tmi"], + "kind": "latin", + "intents": ["agree"], + "translation": "Too much information (often self-tagged)", + "breakdown": "English TMI used as a Korean chat adverb/noun.", + "nuance": "Koreans prefix oversharing with TMI) — a politeness device.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false + }, + { + "id": "jmt", + "surfaces": ["JMT", "jmt", "존맛탱"], + "aliases": ["delicious as hell"], + "romanization": ["jmt", "jonmattaeng"], + "kind": "latin", + "intents": ["food"], + "translation": "Incredibly delicious", + "breakdown": "존맛탱. 존나 맛있는 것의 탱. Food-review stamp.", + "nuance": "Lexicalized, but the 존- comes from a vulgar intensifier. Suggest() keeps it behind vulgar_ok; JMT latin is still marked vulgar.", + "formality": "Informal / Vulgar", + "generation_safe": true, + "caution": false, + "vulgar": true + }, + { + "id": "jonna", + "surfaces": ["존나", "ㅈㄴ"], + "aliases": ["fucking (intensifier)"], + "romanization": ["jonna"], + "kind": "phrase", + "intents": ["annoyed", "praise"], + "translation": "Very / freaking (vulgar intensifier)", + "breakdown": "High-frequency vulgar adverb. ㅈㄴ is the typed cloak.", + "nuance": "Interpret so the host agent understands incoming chat. Never suggest unless vulgar_ok.", + "formality": "Informal / Vulgar", + "generation_safe": false, + "caution": false, + "vulgar": true + }, + { + "id": "real_abbrev", + "surfaces": ["ㄹㅇ"], + "aliases": ["real", "for real"], + "romanization": ["reong", "r-eung"], + "kind": "jamo", + "intents": ["agree", "praise"], + "translation": "For real; actually", + "breakdown": "Real. Agreement or intensifier, often ㄹㅇㅋㅋ.", + "nuance": "Glue particle of Gen-Z chat. Host agents should sprinkle it, not stack it three times.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "injeong", + "surfaces": ["ㅇㅈ"], + "aliases": ["agreed"], + "romanization": ["eungjeong"], + "kind": "jamo", + "intents": ["agree"], + "translation": "Agreed; I acknowledge that", + "breakdown": "인정.", + "nuance": "Stamp of agreement. Can be sarcastic with enough ㅋ.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "gogo", + "surfaces": ["ㄱㄱ"], + "aliases": ["let's go"], + "romanization": ["gogo"], + "kind": "jamo", + "intents": ["food", "weekend", "workout"], + "translation": "Let's go / go ahead", + "breakdown": "고고. Green-light to start.", + "nuance": "Plans, games, queues. Extremely common.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "nn", + "surfaces": ["ㄴㄴ"], + "aliases": ["nope"], + "romanization": ["nn"], + "kind": "jamo", + "intents": ["annoyed"], + "translation": "No / nope", + "breakdown": "노노.", + "nuance": "Soft refuse. Not as sharp as 노답.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "ok_abbrev", + "surfaces": ["ㅇㅋ"], + "aliases": ["ok"], + "romanization": ["ok"], + "kind": "jamo", + "intents": ["agree"], + "translation": "OK", + "breakdown": "오케이.", + "nuance": "Acknowledgement stamp. Colder than ㅇㅈ.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "hi_abbrev", + "surfaces": ["ㅎㅇ"], + "aliases": ["hi"], + "romanization": ["hiu"], + "kind": "jamo", + "intents": ["greeting"], + "translation": "Hi", + "breakdown": "하이.", + "nuance": "Peer hello. Never for elders.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "bye_abbrev", + "surfaces": ["ㅂㅂ"], + "aliases": ["bye"], + "romanization": ["bibi"], + "kind": "jamo", + "intents": ["bye"], + "translation": "Bye", + "breakdown": "바이바이.", + "nuance": "Peer goodbye. Can feel brusque.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "jgkn", + "surfaces": ["ㅈㄱㄴ"], + "aliases": ["go well, see you tomorrow"], + "romanization": ["jgkn"], + "kind": "jamo", + "intents": ["bye"], + "translation": "Take care — see you tomorrow", + "breakdown": "잘 가고 내일 (봐).", + "nuance": "School/work friend sign-off.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "thanks_abbrev", + "surfaces": ["ㄱㅅ"], + "aliases": ["thx"], + "romanization": ["gs"], + "kind": "jamo", + "intents": ["thanks"], + "translation": "Thanks", + "breakdown": "감사.", + "nuance": "Minimal thanks. Add 진짜 for warmth.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "chukha", + "surfaces": ["ㅊㅋ"], + "aliases": ["congrats"], + "romanization": ["ck"], + "kind": "jamo", + "intents": ["praise"], + "translation": "Congrats", + "breakdown": "축하.", + "nuance": "Tiny celebration stamp.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "ddeuddeu", + "surfaces": ["ㄷㄷ"], + "aliases": ["shivers", "whoa"], + "romanization": ["dd"], + "kind": "jamo", + "intents": ["praise", "fandom"], + "translation": "Whoa (chills / shock)", + "breakdown": "덜덜. Awe or fear depending on thread.", + "nuance": "Comment-section shiver. Cousin of 소름.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "hdd", + "surfaces": ["ㅎㄷㄷ"], + "aliases": ["holy chills"], + "romanization": ["hdd"], + "kind": "jamo", + "intents": ["praise"], + "translation": "Seriously chilling / impressive", + "breakdown": "후덜덜. Stronger ㄷㄷ.", + "nuance": "Hype or horror. Context from the clip.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + }, + { + "id": "kk_laugh", + "surfaces": ["ㅋㅋ", "ㅋㅋㅋ", "ㅋㅋㅋㅋ"], + "aliases": ["lol"], + "romanization": ["kkk"], + "kind": "jamo", + "intents": ["agree", "praise", "greeting"], + "translation": "Lol / chuckle particle", + "breakdown": "Laugh. Length roughly maps to warmth or awkwardness, not a precise score.", + "nuance": "The default softener. Peer Korean without ㅋ often reads cold or angry.", + "formality": "Informal / Slang", + "generation_safe": true, + "caution": false, + "vulgar": false, + "work_unsafe": true + } +] diff --git a/skills/linguistics/korean_slang/kb/generation.json b/skills/linguistics/korean_slang/kb/generation.json new file mode 100644 index 00000000..4342bb06 --- /dev/null +++ b/skills/linguistics/korean_slang/kb/generation.json @@ -0,0 +1,286 @@ +{ + "intent_aliases": { + "praise": ["praise", "hype", "compliment", "killing it", "amazing", "잘하", "멋", "폼", "레전드", "쩐다"], + "food": ["food", "lunch", "dinner", "eat", "맛", "점메추", "야메추", "치킨", "맛집", "메뉴"], + "work": ["work", "office", "commute", "출근", "야근", "칼퇴", "내또출", "퇴사", "직장"], + "tired": ["tired", "burnout", "현타", "현생", "번아웃", "exhausted", "reality check"], + "annoyed": ["annoyed", "angry", "킹받", "빡", "짜증", "에바", "nope"], + "agree": ["agree", "real", "true", "ㄹㅇ", "ㅇㅈ", "인정", "same"], + "greeting": ["hi", "hello", "hey", "ㅎㅇ", "안녕"], + "bye": ["bye", "goodbye", "ㅂㅂ", "잘가", "나중에"], + "thanks": ["thanks", "thank", "ㄱㅅ", "고마", "감사"], + "workout": ["workout", "gym", "오운완", "운동", "헬스"], + "weekend": ["weekend", "friday", "불금", "주말", "주불"], + "dating": ["date", "dating", "crush", "썸", "연애", "플러팅"], + "fandom": ["fandom", "kpop", "입덕", "최애", "덕질", "직캠", "콘서트"], + "competence": ["handle it", "you got this", "알잘딱", "알아서", "competent"] + }, + "particles": { + "peers": ["ㄹㅇ", "ㅋㅋ", "ㅇㅈ"], + "mixed": ["ㅋㅋ"], + "work": [], + "elders": [] + }, + "templates": { + "praise": { + "peers": [ + "요즘 완전 {primary} {particle}", + "와 {primary} ㅋㅋ", + "{primary} {particle}", + "레전드 그잡채 {particle}" + ], + "mixed": [ + "요즘 진짜 {primary} ㅋㅋ", + "{primary} 인정." + ], + "work": [ + "최근 퍼포먼스가 정말 좋습니다.", + "이번 건 완성도가 높네요." + ], + "elders": [ + "요즘 정말 잘하고 계세요.", + "정말 멋있으세요." + ] + }, + "food": { + "peers": [ + "{primary} ㄱㄱ", + "오늘 {primary} 하실래요 ㅋㅋ", + "배고픈데 {primary} {particle}" + ], + "mixed": [ + "점심 {primary} 어때요?" + ], + "work": [ + "점메추 부탁드려요.", + "점심 메뉴 같이 정할까요?" + ], + "elders": [ + "점심은 뭐 드실래요?", + "저녁 맛있게 드세요." + ] + }, + "work": { + "peers": [ + "{primary}이라 현타 옴 ㅋㅋ", + "내일 {primary} {particle}", + "오늘은 {primary} 가즌가" + ], + "mixed": [ + "내일 출근이라 좀 힘드네요." + ], + "work": [ + "내일도 출근입니다.", + "오늘 칼퇴 가능할 듯요." + ], + "elders": [ + "내일 출근이에요.", + "오늘도 수고 많으셨어요." + ] + }, + "tired": { + "peers": [ + "{primary} 왔다 {particle}", + "현생 너무 힘듦 {particle}", + "아 {primary} ㅋㅋ" + ], + "mixed": [ + "지금 현타 왔어요." + ], + "work": [ + "오늘은 에너지가 좀 떨어지네요." + ], + "elders": [ + "오늘은 좀 피곤하네요.", + "푹 쉬세요." + ] + }, + "annoyed": { + "peers": [ + "아 {primary} {particle}", + "이거 에바임", + "{primary} ㅋㅋ 나 감" + ], + "mixed": [ + "이건 좀 에바인 듯요." + ], + "work": [ + "이 부분은 재고가 필요해 보입니다." + ], + "elders": [ + "조금 불편하네요.", + "나중에 다시 이야기해요." + ] + }, + "agree": { + "peers": [ + "{primary}", + "{primary} {particle}", + "그잡채 {particle}" + ], + "mixed": [ + "인정합니다 ㅋㅋ" + ], + "work": [ + "동의합니다." + ], + "elders": [ + "맞아요.", + "그렇게 생각해요." + ] + }, + "greeting": { + "peers": [ + "{primary}", + "{primary} 뭐해", + "야 {primary}" + ], + "mixed": [ + "안녕하세요 ㅋㅋ" + ], + "work": [ + "안녕하세요." + ], + "elders": [ + "안녕하세요.", + "만나서 반갑습니다." + ] + }, + "bye": { + "peers": [ + "{primary}", + "{primary} 내일 봐", + "난 잔다 {primary}" + ], + "mixed": [ + "다음에 봐요." + ], + "work": [ + "이만 들어가보겠습니다." + ], + "elders": [ + "안녕히 가세요.", + "다음에 또 뵈어요." + ] + }, + "thanks": { + "peers": [ + "{primary}", + "{primary} 진짜", + "고마워 {particle}" + ], + "mixed": [ + "감사합니다 ㅋㅋ" + ], + "work": [ + "감사합니다.", + "도움 감사드립니다." + ], + "elders": [ + "감사합니다.", + "정말 고마워요." + ] + }, + "workout": { + "peers": [ + "{primary}", + "오늘 {primary} {particle}", + "헬스 ㄱㄱ {primary}" + ], + "mixed": [ + "오늘 운동 완료." + ], + "work": [ + "오늘 운동 했습니다." + ], + "elders": [ + "오늘 운동하고 왔어요.", + "건강 챙기세요." + ] + }, + "weekend": { + "peers": [ + "{primary}이다 {particle}", + "주말 {primary} ㄱㄱ", + "{primary} 뭐함" + ], + "mixed": [ + "주말 잘 보내요." + ], + "work": [ + "주말 잘 보내세요." + ], + "elders": [ + "주말 잘 보내세요.", + "편하게 쉬세요." + ] + }, + "dating": { + "peers": [ + "지금 {primary} 중 {particle}", + "{primary} 어케 함", + "그린라이트냐 {particle}" + ], + "mixed": [ + "지금은 썸 단계인 듯요." + ], + "work": [ + "개인 일정은 따로 조율할게요." + ], + "elders": [ + "요즘 잘 지내요.", + "소개는 천천히 할게요." + ] + }, + "fandom": { + "peers": [ + "{primary} 했다 {particle}", + "최애 {primary} ㅠㅠ", + "오늘 입덕각 {particle}" + ], + "mixed": [ + "요즘 그 가수 덕질 중이에요." + ], + "work": [ + "취미로 공연 챙겨 보는 편입니다." + ], + "elders": [ + "요즘 그 가수 좋아해요.", + "공연 보러 갔어요." + ] + }, + "competence": { + "peers": [ + "{primary}으로 해줘", + "넌 {primary}이지 {particle}", + "알아서 {primary}" + ], + "mixed": [ + "알잘딱 부탁해요." + ], + "work": [ + "맥락 보시고 진행 부탁드려요.", + "세부 실무는 재량껏 부탁드립니다." + ], + "elders": [ + "편하게 진행해 주세요.", + "세부는 맡길게요." + ] + }, + "generic": { + "peers": [ + "{primary} {particle}", + "그잡채" + ], + "mixed": [ + "ㅋㅋ 알겠어요." + ], + "work": [ + "확인했습니다." + ], + "elders": [ + "알겠어요." + ] + } + } +} diff --git a/skills/linguistics/korean_slang/kb/pack_meta.json b/skills/linguistics/korean_slang/kb/pack_meta.json new file mode 100644 index 00000000..762ddc06 --- /dev/null +++ b/skills/linguistics/korean_slang/kb/pack_meta.json @@ -0,0 +1,30 @@ +{ + "id": "korean_slang_s26_small", + "skill_id": "linguistics/korean_slang", + "snapshot": "2026-09", + "locale": "ko-KR", + "register": "gen_z_mz_internet", + "notes": "Small curated pack for issue #34. Prefer high-frequency 2024-2026 internet Korean over textbook slang. Not a dictionary of standard Korean.", + "sources": [ + { + "title": "Issue #34 example contract (폼 미쳤다)", + "url": "https://github.com/ARPAHLS/skillware/issues/34" + }, + { + "title": "HURUMOR 2026 신조어 총정리 (갓생러, 현타, 킹받다, 억텐, 뇌절)", + "url": "https://hurumor.com/%ec%8b%a0%ec%a1%b0%ec%96%b4-2026/" + }, + { + "title": "FRANVIA Gen Z Korean slang and MZ abbreviations (별다줄, 갓생, 내또출, 점메추, 퇴준생, 억텐, 킹받다)", + "url": "https://www.franvia.com/2026/04/korean-gen-z-slang-mz-abbreviations.html" + }, + { + "title": "90 Day Korean — Korean slang and texting abbreviations", + "url": "https://www.90daykorean.com/korean-slang/" + }, + { + "title": "StoryLearning — Korean slang including 알잘딱깔센", + "url": "https://storylearning.com/learn/korean/korean-tips/korean-slang" + } + ] +} diff --git a/skills/linguistics/korean_slang/lexicon.py b/skills/linguistics/korean_slang/lexicon.py new file mode 100644 index 00000000..1b6a987b --- /dev/null +++ b/skills/linguistics/korean_slang/lexicon.py @@ -0,0 +1,810 @@ +"""Deterministic Korean slang matcher, lookup, and template generation.""" + +from __future__ import annotations + +import json +import os +import re +import unicodedata +from typing import Any, Dict, List, Optional, Sequence, Tuple + +ACTIONS = frozenset({"interpret", "suggest", "lookup"}) +AUDIENCES = frozenset({"peers", "mixed", "work", "elders"}) +DEFAULT_ACTION = "interpret" +DEFAULT_AUDIENCE = "peers" +MAX_HITS_MIN = 1 +MAX_HITS_MAX = 50 +DEFAULT_MAX_HITS = 12 + +_SYLLABLE_MIN = 0xAC00 +_SYLLABLE_MAX = 0xD7A3 +_FORMAL_ENDINGS = ("습니다", "습니까", "하십시오", "하십니다") +_POLITE_ENDINGS = ("세요", "해요", "이에요", "예요") + + +def nfc(text: str) -> str: + return unicodedata.normalize("NFC", text or "") + + +def _latin_boundary(text: str, start: int, end: int) -> bool: + left = text[start - 1] if start else "" + right = text[end] if end < len(text) else "" + left_ok = (not left) or (not left.isalnum()) + right_ok = (not right) or (not right.isalnum()) + return left_ok and right_ok + + +def _is_syllable(ch: str) -> bool: + if not ch: + return False + return _SYLLABLE_MIN <= ord(ch) <= _SYLLABLE_MAX + + +def _jamo_boundary(text: str, start: int, end: int) -> bool: + """Allow adjacent jamo particles (ㄹㅇㅋㅋ) but not embedding in 가-힣 words.""" + left = text[start - 1] if start else "" + right = text[end] if end < len(text) else "" + if _is_syllable(left) or _is_syllable(right): + return False + if left and left.isascii() and left.isalnum(): + return False + if right and right.isascii() and right.isalnum(): + return False + return True + + +def compact_with_map(text: str) -> Tuple[str, List[int]]: + chars: List[str] = [] + mapping: List[int] = [] + for index, char in enumerate(text): + if char.isspace(): + continue + chars.append(char) + mapping.append(index) + return "".join(chars), mapping + + +def _fold_latin(text: str) -> str: + return "".join(ch.casefold() if ch.isascii() else ch for ch in text) + + +def load_json(path: str) -> Any: + with open(path, "r", encoding="utf-8") as handle: + return json.load(handle) + + +def load_pack(kb_dir: str) -> Dict[str, Any]: + meta = load_json(os.path.join(kb_dir, "pack_meta.json")) + entries = load_json(os.path.join(kb_dir, "entries.json")) + generation = load_json(os.path.join(kb_dir, "generation.json")) + constitution = load_json(os.path.join(kb_dir, "constitution.json")) + if not isinstance(entries, list) or not entries: + raise ValueError("kb/entries.json must be a non-empty array") + by_id: Dict[str, Dict[str, Any]] = {} + matchers: List[Dict[str, Any]] = [] + for entry in entries: + entry_id = str(entry.get("id") or "").strip() + if not entry_id or entry_id in by_id: + raise ValueError(f"duplicate or empty slang id: {entry_id!r}") + by_id[entry_id] = entry + kind = str(entry.get("kind") or "phrase") + needles: List[Tuple[str, str]] = [] + for surface in entry.get("surfaces") or []: + raw = nfc(str(surface)).strip() + if raw: + needles.append((raw, raw)) + for alias in entry.get("aliases") or []: + raw = nfc(str(alias)).strip() + if raw: + needles.append((raw, str(entry["surfaces"][0]))) + for roman in entry.get("romanization") or []: + raw = nfc(str(roman)).strip() + if raw: + needles.append((raw, str(entry["surfaces"][0]))) + seen = set() + for raw, display in needles: + compact = re.sub(r"\s+", "", raw) + if not compact: + continue + key = _fold_latin(compact) + if key in seen: + continue + seen.add(key) + matchers.append( + { + "needle": key, + "display": display, + "entry": entry, + "kind": kind, + "length": len(key), + } + ) + matchers.sort(key=lambda item: (-item["length"], item["needle"])) + blocked = [ + nfc(str(item)).strip() + for item in constitution.get("blocked_substrings") or [] + if str(item).strip() + ] + blocked.sort(key=len, reverse=True) + return { + "meta": meta, + "entries": entries, + "by_id": by_id, + "matchers": matchers, + "generation": generation, + "constitution": constitution, + "blocked": blocked, + } + + +def _error(code: str, detail: str) -> Dict[str, Any]: + return { + "status": "error", + "error": {"code": code, "detail": detail}, + "message": detail, + } + + +def _coerce_bool(value: Any, default: bool = False) -> bool: + if value is None: + return default + if isinstance(value, bool): + return value + if isinstance(value, str): + lowered = value.strip().lower() + if lowered in {"1", "true", "yes", "y"}: + return True + if lowered in {"0", "false", "no", "n", ""}: + return False + return bool(value) + + +def _coerce_int(value: Any, default: int) -> Optional[int]: + if value is None or value == "": + return default + try: + return int(value) + except (TypeError, ValueError): + return None + + +def find_blocked(text: str, blocked: Sequence[str]) -> List[str]: + haystack = nfc(text) + hits: List[str] = [] + occupied = [False] * len(haystack) + for term in blocked: + if not term: + continue + start = 0 + while True: + index = haystack.find(term, start) + if index < 0: + break + end = index + len(term) + if not any(occupied[index:end]): + hits.append(term) + for pos in range(index, end): + occupied[pos] = True + start = index + 1 + return hits + + +def redact_blocked(text: str, blocked_hits: Sequence[str]) -> str: + redacted = nfc(text) + for term in sorted(set(blocked_hits), key=len, reverse=True): + redacted = redacted.replace(term, "…") + return redacted + + +def find_hits( + text: str, + pack: Dict[str, Any], + max_hits: int, +) -> List[Dict[str, Any]]: + original = nfc(text) + compact, mapping = compact_with_map(original) + folded = _fold_latin(compact) + occupied = [False] * len(folded) + hits: List[Dict[str, Any]] = [] + for matcher in pack["matchers"]: + if len(hits) >= max_hits: + break + needle = matcher["needle"] + if not needle: + continue + start = 0 + while len(hits) < max_hits: + index = folded.find(needle, start) + if index < 0: + break + end = index + len(needle) + kind = matcher["kind"] + orig_start = mapping[index] + orig_end = mapping[end - 1] + 1 + if kind == "latin" and not _latin_boundary(original, orig_start, orig_end): + start = index + 1 + continue + if kind == "jamo" and not _jamo_boundary(original, orig_start, orig_end): + start = index + 1 + continue + if any(occupied[index:end]): + start = index + 1 + continue + for pos in range(index, end): + occupied[pos] = True + entry = matcher["entry"] + span = original[orig_start:orig_end] + hits.append( + { + "id": entry["id"], + "surface": matcher["display"], + "matched": span, + "span": [orig_start, orig_end], + "translation": entry.get("translation", ""), + "breakdown": entry.get("breakdown", ""), + "nuance": entry.get("nuance", ""), + "formality": entry.get("formality", "Informal / Slang"), + "intents": list(entry.get("intents") or []), + "generation_safe": bool(entry.get("generation_safe", False)), + "caution": bool(entry.get("caution", False)), + "vulgar": bool(entry.get("vulgar", False)), + "romanization": list(entry.get("romanization") or []), + } + ) + start = end + hits.sort(key=lambda item: (item["span"][0], -(item["span"][1] - item["span"][0]))) + return _merge_adjacent(hits) + + +def _merge_adjacent(hits: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + if not hits: + return hits + merged: List[Dict[str, Any]] = [hits[0]] + for hit in hits[1:]: + prev = merged[-1] + if hit["id"] == prev["id"] and hit["span"][0] <= prev["span"][1]: + prev["span"][1] = max(prev["span"][1], hit["span"][1]) + prev["matched"] = prev["matched"] + hit["matched"] + continue + merged.append(hit) + return merged + + +def lookup_entry(pack: Dict[str, Any], query: str) -> Optional[Dict[str, Any]]: + needle = nfc(query).strip() + if not needle: + return None + folded = _fold_latin(re.sub(r"\s+", "", needle)) + by_id = pack["by_id"] + if needle in by_id: + return by_id[needle] + for entry in pack["entries"]: + candidates = [] + candidates.extend(entry.get("surfaces") or []) + candidates.extend(entry.get("aliases") or []) + candidates.extend(entry.get("romanization") or []) + for candidate in candidates: + compact = _fold_latin(re.sub(r"\s+", "", nfc(str(candidate)))) + if compact == folded: + return entry + return None + + +def public_entry(entry: Dict[str, Any], include_romanization: bool) -> Dict[str, Any]: + payload = { + "id": entry.get("id"), + "surface": (entry.get("surfaces") or [""])[0], + "surfaces": list(entry.get("surfaces") or []), + "translation": entry.get("translation", ""), + "breakdown": entry.get("breakdown", ""), + "nuance": entry.get("nuance", ""), + "formality": entry.get("formality", "Informal / Slang"), + "intents": list(entry.get("intents") or []), + "generation_safe": bool(entry.get("generation_safe", False)), + "caution": bool(entry.get("caution", False)), + "vulgar": bool(entry.get("vulgar", False)), + } + if include_romanization: + payload["romanization"] = list(entry.get("romanization") or []) + return payload + + +def _style_hint(text: str, audience: str) -> Optional[str]: + if audience not in {"peers", "mixed"}: + return None + if any(text.endswith(ending) or ending in text for ending in _FORMAL_ENDINGS): + return ( + "This reads as formal written Korean (습니다-style). " + "For peer chat, call suggest() with audience=peers." + ) + if audience == "peers" and any(ending in text for ending in _POLITE_ENDINGS): + return ( + "This reads as polite 요-form. Fine for mixed company; " + "peer group chats often drop to 반말 plus slang." + ) + return None + + +def _compose_translation(text: str, hits: List[Dict[str, Any]]) -> Optional[str]: + if not hits: + return None + compact = re.sub(r"\s+", "", nfc(text)) + if compact == "요즘완전폼미쳤다" and any( + hit["id"] == "pom_michyeotda" for hit in hits + ): + return ( + "They are absolutely killing it right now / " + "Their form is crazy these days" + ) + if len(hits) == 1: + return hits[0]["translation"] + return " / ".join( + dict.fromkeys(hit["translation"] for hit in hits if hit["translation"]) + ) + + +def _compose_nuance( + text: str, + hits: List[Dict[str, Any]], + context: str, + unmatched: bool, +) -> str: + if unmatched: + base = ( + "No bundled slang surfaces matched. The text may be standard Korean " + "or a term outside this September 2026 pack." + ) + elif len(hits) == 1: + base = hits[0]["nuance"] + else: + base = " ".join(dict.fromkeys(hit["nuance"] for hit in hits if hit["nuance"])) + if context.strip(): + return f"{base} Context noted: {context.strip()}" + return base + + +def _compose_formality(hits: List[Dict[str, Any]], unmatched: bool) -> str: + if unmatched or not hits: + return "unknown" + if any(hit.get("vulgar") for hit in hits): + return "Informal / Vulgar" + if any(hit.get("caution") for hit in hits): + return "Informal / Caution" + return hits[0].get("formality") or "Informal / Slang" + + +def interpret( + pack: Dict[str, Any], + text: str, + context: str, + max_hits: int, + include_romanization: bool, + blocked_hits: Sequence[str], + audience: str, +) -> Dict[str, Any]: + reviewed = redact_blocked(text, blocked_hits) + hits = find_hits(reviewed, pack, max_hits) + unmatched = not hits + breakdown = {} + for hit in hits: + breakdown[hit["surface"]] = hit["breakdown"] + translation = _compose_translation(reviewed, hits) + payload: Dict[str, Any] = { + "status": "ok", + "action": "interpret", + "translation": translation, + "slang_breakdown": breakdown, + "nuance": _compose_nuance(reviewed, hits, context, unmatched), + "formality": _compose_formality(hits, unmatched), + "hit_count": len(hits), + "hits": [ + { + "id": hit["id"], + "surface": hit["surface"], + "matched": hit["matched"], + "translation": hit["translation"], + "caution": hit["caution"], + "vulgar": hit["vulgar"], + **( + {"romanization": hit["romanization"]} + if include_romanization + else {} + ), + } + for hit in hits + ], + "unmatched": unmatched, + "warnings": [], + "style_hint": _style_hint(reviewed, audience), + "blocked_omitted": list(blocked_hits), + "pack_snapshot": pack["meta"].get("snapshot"), + "pack_id": pack["meta"].get("id"), + "entry_count": len(pack["entries"]), + } + if blocked_hits: + payload["warnings"].append( + "Input contained terms this pack will not gloss " + "(slurs or appearance-policing slang). They were omitted." + ) + if any(hit.get("caution") for hit in hits): + payload["warnings"].append( + "One or more matches are marked caution: explain, do not repeat as a joke." + ) + if not payload["warnings"]: + payload["warnings"] = [] + if payload["style_hint"] is None: + payload.pop("style_hint") + return payload + + +def _normalize_intent(raw: str, generation: Dict[str, Any]) -> Optional[str]: + text = nfc(raw).strip().lower() + if not text: + return None + aliases = generation.get("intent_aliases") or {} + if text in aliases: + return text + for intent, words in aliases.items(): + for word in words: + if nfc(str(word)).strip().lower() == text: + return intent + return None + + +def infer_intent( + text: str, + explicit: str, + hits: Sequence[Dict[str, Any]], + generation: Dict[str, Any], +) -> Optional[str]: + named = _normalize_intent(explicit, generation) + if named: + return named + aliases = generation.get("intent_aliases") or {} + blob = nfc(text).casefold() + scored: List[Tuple[int, str]] = [] + for intent, words in aliases.items(): + score = 0 + for word in words: + token = nfc(str(word)).strip() + if token and token.casefold() in blob: + score += max(1, len(token)) + if score: + scored.append((score, intent)) + if scored: + scored.sort(key=lambda item: (-item[0], item[1])) + return scored[0][1] + intents: List[str] = [] + for hit in hits: + intents.extend(hit.get("intents") or []) + if intents: + return intents[0] + return None + + +def _eligible_entries( + pack: Dict[str, Any], + intent: str, + audience: str, + vulgar_ok: bool, +) -> List[Dict[str, Any]]: + chosen: List[Dict[str, Any]] = [] + for entry in pack["entries"]: + if not entry.get("generation_safe"): + continue + if entry.get("caution"): + continue + if entry.get("vulgar") and not vulgar_ok: + continue + if audience in {"work", "elders"} and entry.get("work_unsafe"): + continue + if audience == "elders" and not entry.get("honorific_ok"): + continue + intents = set(entry.get("intents") or []) + if intent not in intents and "any" not in intents: + continue + chosen.append(entry) + return chosen + + +def _fill_template(template: str, primary: str, particle: str) -> str: + text = template.replace("{primary}", primary).replace("{particle}", particle) + text = re.sub(r"\s+", " ", text).strip() + return text.replace(" ,", ",").replace(" .", ".") + + +def suggest( + pack: Dict[str, Any], + text: str, + intent_param: str, + audience: str, + vulgar_ok: bool, + include_romanization: bool, + blocked_hits: Sequence[str], + max_hits: int, +) -> Dict[str, Any]: + warnings: List[str] = [] + if blocked_hits: + warnings.append( + "Refusing to generate from blocked slurs or appearance-policing slang." + ) + return { + "status": "ok", + "action": "suggest", + "translation": None, + "slang_breakdown": {}, + "nuance": "Generation skipped because the input included blocked terms.", + "formality": "blocked", + "hit_count": 0, + "suggestions": [], + "warnings": warnings, + "blocked_omitted": list(blocked_hits), + "pack_snapshot": pack["meta"].get("snapshot"), + "pack_id": pack["meta"].get("id"), + "entry_count": len(pack["entries"]), + } + + reviewed = nfc(text) + hits = find_hits(reviewed, pack, max_hits) if reviewed.strip() else [] + generation = pack["generation"] + intent = infer_intent(reviewed, intent_param, hits, generation) + if not intent: + return _error( + "NEED_TEXT_OR_INTENT", + "suggest() needs an intent (for example praise, food, work) " + "or Korean/English text the lexicon can classify.", + ) + + templates_root = generation.get("templates") or {} + intent_templates = templates_root.get(intent) or templates_root.get("generic") or {} + audience_templates = list( + intent_templates.get(audience) or intent_templates.get("peers") or [] + ) + particles = list((generation.get("particles") or {}).get(audience) or []) + + if audience == "elders": + warnings.append( + "Slang generation is suppressed for audience=elders; " + "polite Korean templates are used instead." + ) + elif audience == "work": + warnings.append("Work audience: only mild, office-safe slang is eligible.") + + if audience == "elders" or (audience == "work" and not audience_templates): + filled = [_fill_template(item, "", "") for item in audience_templates[:3]] + suggestions = [ + { + "text": line, + "used_entries": [], + "formality": "Polite" if audience == "elders" else "Work-safe", + "nuance": "Honorific or workplace phrasing; slang avoided.", + "audience_ok": True, + } + for line in filled + if line + ] + translation = suggestions[0]["text"] if suggestions else None + return { + "status": "ok", + "action": "suggest", + "translation": translation, + "slang_breakdown": {}, + "nuance": suggestions[0]["nuance"] if suggestions else "No template.", + "formality": suggestions[0]["formality"] if suggestions else "unknown", + "hit_count": len(suggestions), + "suggestions": suggestions, + "intent": intent, + "audience": audience, + "warnings": warnings, + "blocked_omitted": [], + "pack_snapshot": pack["meta"].get("snapshot"), + "pack_id": pack["meta"].get("id"), + "entry_count": len(pack["entries"]), + } + + eligible = _eligible_entries(pack, intent, audience, vulgar_ok) + if hits: + hit_ids = {hit["id"] for hit in hits} + eligible.sort( + key=lambda entry: (0 if entry["id"] in hit_ids else 1, entry["id"]) + ) + if not eligible and audience_templates: + eligible = [] + + suggestions: List[Dict[str, Any]] = [] + used_primary = eligible[0] if eligible else None + secondary = eligible[1] if len(eligible) > 1 else None + particle = particles[0] if particles else "" + primary_surface = (used_primary.get("surfaces") or [""])[0] if used_primary else "" + + for template in audience_templates[:4]: + line = _fill_template(template, primary_surface, particle) + if secondary and "{secondary}" in template: + line = line.replace("{secondary}", (secondary.get("surfaces") or [""])[0]) + else: + line = line.replace("{secondary}", "").strip() + line = re.sub(r"\s+", " ", line) + if not line: + continue + used = [] + breakdown = {} + if used_primary and primary_surface and primary_surface in line: + used.append(used_primary["id"]) + breakdown[primary_surface] = used_primary.get("breakdown", "") + if secondary: + sec_surface = (secondary.get("surfaces") or [""])[0] + if sec_surface and sec_surface in line: + used.append(secondary["id"]) + breakdown[sec_surface] = secondary.get("breakdown", "") + if particle and particle in line: + particle_entry = lookup_entry(pack, particle) + if particle_entry and particle_entry["id"] not in used: + if particle_entry.get("generation_safe"): + used.append(particle_entry["id"]) + breakdown.setdefault( + (particle_entry.get("surfaces") or [particle])[0], + particle_entry.get("breakdown", ""), + ) + item = { + "text": line, + "used_entries": used, + "formality": ( + used_primary.get("formality") if used_primary else "Informal / Slang" + ), + "nuance": ( + used_primary.get("nuance") + if used_primary + else "Peer-register template with no matched slang entry." + ), + "audience_ok": True, + } + if include_romanization and used_primary: + item["romanization"] = list(used_primary.get("romanization") or []) + item["slang_breakdown"] = breakdown + suggestions.append(item) + + if not suggestions: + return _error( + "NO_SUGGESTIONS", + f"No generation-safe templates for intent={intent!r} audience={audience!r}.", + ) + + first = suggestions[0] + return { + "status": "ok", + "action": "suggest", + "translation": first["text"], + "slang_breakdown": first.get("slang_breakdown") or {}, + "nuance": first["nuance"], + "formality": first["formality"], + "hit_count": len(suggestions), + "suggestions": suggestions, + "intent": intent, + "audience": audience, + "warnings": warnings, + "blocked_omitted": [], + "pack_snapshot": pack["meta"].get("snapshot"), + "pack_id": pack["meta"].get("id"), + "entry_count": len(pack["entries"]), + } + + +def lookup( + pack: Dict[str, Any], + text: str, + include_romanization: bool, + blocked_hits: Sequence[str], +) -> Dict[str, Any]: + if blocked_hits: + return { + "status": "ok", + "action": "lookup", + "translation": None, + "slang_breakdown": {}, + "nuance": "Lookup skipped for blocked terms.", + "formality": "blocked", + "hit_count": 0, + "found": False, + "entry": None, + "warnings": [ + "This pack will not define slurs or appearance-policing slang." + ], + "blocked_omitted": list(blocked_hits), + "pack_snapshot": pack["meta"].get("snapshot"), + "pack_id": pack["meta"].get("id"), + "entry_count": len(pack["entries"]), + } + entry = lookup_entry(pack, text) + if not entry: + return { + "status": "ok", + "action": "lookup", + "translation": None, + "slang_breakdown": {}, + "nuance": "No bundled entry for that surface, alias, or romanization.", + "formality": "unknown", + "hit_count": 0, + "found": False, + "entry": None, + "warnings": [], + "blocked_omitted": [], + "pack_snapshot": pack["meta"].get("snapshot"), + "pack_id": pack["meta"].get("id"), + "entry_count": len(pack["entries"]), + } + public = public_entry(entry, include_romanization) + return { + "status": "ok", + "action": "lookup", + "translation": public["translation"], + "slang_breakdown": {public["surface"]: public["breakdown"]}, + "nuance": public["nuance"], + "formality": public["formality"], + "hit_count": 1, + "found": True, + "entry": public, + "warnings": ( + ["Caution entry: explain incoming use; do not generate it."] + if public["caution"] + else [] + ), + "blocked_omitted": [], + "pack_snapshot": pack["meta"].get("snapshot"), + "pack_id": pack["meta"].get("id"), + "entry_count": len(pack["entries"]), + } + + +def execute_pack(pack: Dict[str, Any], params: Dict[str, Any]) -> Dict[str, Any]: + action = nfc(str(params.get("action") or DEFAULT_ACTION)).strip().lower() + if action not in ACTIONS: + return _error( + "UNKNOWN_ACTION", + "action must be interpret, suggest, or lookup.", + ) + audience = nfc(str(params.get("audience") or DEFAULT_AUDIENCE)).strip().lower() + if audience not in AUDIENCES: + return _error( + "UNKNOWN_AUDIENCE", + "audience must be peers, mixed, work, or elders.", + ) + max_hits = _coerce_int(params.get("max_hits"), DEFAULT_MAX_HITS) + if max_hits is None or max_hits < MAX_HITS_MIN or max_hits > MAX_HITS_MAX: + return _error( + "INVALID_MAX_HITS", + f"max_hits must be an integer from {MAX_HITS_MIN} to {MAX_HITS_MAX}.", + ) + text = nfc(str(params.get("text") if params.get("text") is not None else "")) + context = nfc( + str(params.get("context") if params.get("context") is not None else "") + ) + intent = nfc(str(params.get("intent") if params.get("intent") is not None else "")) + include_romanization = _coerce_bool(params.get("include_romanization"), False) + vulgar_ok = _coerce_bool(params.get("vulgar_ok"), False) + blocked_hits = find_blocked(f"{text}\n{intent}\n{context}", pack["blocked"]) + + if action in {"interpret", "lookup"} and not text.strip(): + return _error("EMPTY_TEXT", f"{action}() requires a non-empty text string.") + + if action == "interpret": + return interpret( + pack, + text, + context, + max_hits, + include_romanization, + blocked_hits, + audience, + ) + if action == "lookup": + return lookup(pack, text, include_romanization, blocked_hits) + return suggest( + pack, + text, + intent, + audience, + vulgar_ok, + include_romanization, + blocked_hits, + max_hits, + ) diff --git a/skills/linguistics/korean_slang/manifest.yaml b/skills/linguistics/korean_slang/manifest.yaml new file mode 100644 index 00000000..317d48ad --- /dev/null +++ b/skills/linguistics/korean_slang/manifest.yaml @@ -0,0 +1,86 @@ +name: linguistics/korean_slang +version: 0.1.0 +description: > + Deterministic offline Korean Gen-Z slang lexicon. Interprets modern internet + Korean (줄임말, Konglish, jamo abbreviations), returns translation plus + socio-linguistic nuance, and suggests peer-register lines from curated + templates. Toxic and appearance-policing terms are filtered. No network and + no model calls inside execute(). +short_description: "Offline Korean Gen-Z slang interpreter and peer-register generator." +issuer: + name: Blade Dancer + email: bladedancerc3@gmail.com + github: bd-c3 +category: linguistics +parameters: + type: object + properties: + text: + type: string + description: > + Korean, Konglish, or English to interpret, look up, or (for suggest) + classify into an intent. Required for interpret and lookup. + context: + type: string + description: Optional situational note (platform, relationship, thread). + action: + type: string + enum: [interpret, suggest, lookup] + description: interpret (default) glosses slang; suggest writes peer Korean; lookup is exact. + intent: + type: string + description: > + Optional generation intent for suggest (praise, food, work, tired, + annoyed, agree, greeting, bye, thanks, workout, weekend, dating, + fandom, competence). Inferred from text when omitted. + audience: + type: string + enum: [peers, mixed, work, elders] + description: Register gate. Default peers. elders suppresses slang. + include_romanization: + type: boolean + description: When true, attach Revised Romanization on hits and suggestions. + max_hits: + type: integer + description: Maximum slang hits to return (1-50, default 12). + vulgar_ok: + type: boolean + description: When true, suggest() may use vulgar-marked lexical items. Default false. + required: [] +outputs: + status: + type: string + description: ok for a completed call; error for a contract violation. + action: + type: string + description: Echo of interpret, suggest, or lookup. + translation: + type: string + description: English gloss (interpret/lookup) or generated Korean (suggest). + slang_breakdown: + type: object + description: Map of matched slang surface to a short explanation. + nuance: + type: string + description: Socio-linguistic note for host-agent wording. + formality: + type: string + description: Informal / Slang, Informal / Vulgar, Polite, Work-safe, unknown, or blocked. + hit_count: + type: integer + description: Number of lexicon hits or generated suggestions. +requirements: [] +constitution: | + 1. Deterministic and offline — execute() never calls a model or the network. + 2. Curated pack only — never invent slang, never guess unseen 줄임말. + 3. Filter toxic internet terms — slurs, appearance-policing, and gendered + hate are omitted from glosses and never generated. + 4. Honorifics over vibes — audience=elders (and cautious work) must not + dump 반말 slang onto the end user. + 5. Vulgar is opt-in — 존나-class items are interpreted but not suggested + unless vulgar_ok is true. + 6. Host agent, not the skill, speaks — return JSON; the host phrases the + end-user reply. +presentation: + icon: languages + color: "#c45c26" diff --git a/skills/linguistics/korean_slang/skill.py b/skills/linguistics/korean_slang/skill.py new file mode 100644 index 00000000..e00eee51 --- /dev/null +++ b/skills/linguistics/korean_slang/skill.py @@ -0,0 +1,67 @@ +import importlib.util +import os +from typing import Any, Dict, Optional + +from skillware.core.base_skill import BaseSkill + + +def _import_lexicon(): + try: + from . import lexicon as lexicon_module # type: ignore[import-not-found] + except ImportError: + lexicon_path = os.path.join(os.path.dirname(__file__), "lexicon.py") + spec = importlib.util.spec_from_file_location( + "korean_slang_lexicon", lexicon_path + ) + if spec is None or spec.loader is None: + raise ImportError(f"Cannot load lexicon module from {lexicon_path}") + lexicon_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(lexicon_module) + return lexicon_module + + +_lexicon = _import_lexicon() +execute_pack = _lexicon.execute_pack +load_pack = _lexicon.load_pack + + +class KoreanSlangSkill(BaseSkill): + """ + Offline Korean Gen-Z slang lexicon: interpret, suggest, and lookup. + + execute() is deterministic. It never calls a model or the network. + """ + + _pack_cache: Optional[Dict[str, Any]] = None + + def __init__(self, config: Optional[Dict[str, Any]] = None): + super().__init__(config) + self._pack = self._load_pack() + + @classmethod + def _load_pack(cls) -> Dict[str, Any]: + if cls._pack_cache is None: + kb_dir = os.path.join(os.path.dirname(__file__), "kb") + cls._pack_cache = load_pack(kb_dir) + return cls._pack_cache + + @property + def manifest(self) -> Dict[str, Any]: + manifest_path = os.path.join(os.path.dirname(__file__), "manifest.yaml") + if os.path.exists(manifest_path): + import yaml + + with open(manifest_path, "r", encoding="utf-8") as handle: + return yaml.safe_load(handle) + return {"name": "linguistics/korean_slang", "version": "0.1.0"} + + def execute(self, params: Dict[str, Any]) -> Dict[str, Any]: + payload = params if isinstance(params, dict) else {} + try: + return execute_pack(self._pack, payload) + except Exception as exc: + return { + "status": "error", + "error": {"code": "PACK_FAILURE", "detail": str(exc)}, + "message": f"Korean slang pack failed: {exc}", + } diff --git a/skills/linguistics/korean_slang/test_skill.py b/skills/linguistics/korean_slang/test_skill.py new file mode 100644 index 00000000..2e049a18 --- /dev/null +++ b/skills/linguistics/korean_slang/test_skill.py @@ -0,0 +1,161 @@ +"""Bundle tests for linguistics/korean_slang. + +Offline and deterministic. Locks the issue #34 interpret contract, pack +integrity, honorific gates, and toxic-term filtering. +""" + +import json +import os + +import yaml + +from skillware.core.loader import SkillLoader + +from .skill import KoreanSlangSkill + +BUNDLE_DIR = os.path.dirname(__file__) +KB_DIR = os.path.join(BUNDLE_DIR, "kb") + + +def _load_json(name): + with open(os.path.join(KB_DIR, name), "r", encoding="utf-8") as handle: + return json.load(handle) + + +def _load_manifest(): + with open(os.path.join(BUNDLE_DIR, "manifest.yaml"), encoding="utf-8") as handle: + return yaml.safe_load(handle) + + +def test_skill_manifest_consistency(): + skill = KoreanSlangSkill() + manifest = _load_manifest() + assert skill.manifest["name"] == manifest["name"] == "linguistics/korean_slang" + assert skill.manifest["version"] == manifest["version"] == "0.1.0" + assert manifest["requirements"] == [] + assert "output" not in manifest + result = skill.execute({"text": "요즘 완전 폼 미쳤다"}) + for key in manifest["outputs"]: + assert key in result + + +def test_skill_loader_can_import(): + bundle = SkillLoader.load_skill("linguistics/korean_slang") + assert bundle["manifest"]["name"] == "linguistics/korean_slang" + assert hasattr(bundle["module"], "KoreanSlangSkill") + skill = bundle["class"]() + result = skill.execute({"text": "알잘딱깔센"}) + assert result["status"] == "ok" + + +def test_issue_34_interpret_contract(): + skill = KoreanSlangSkill() + result = skill.execute( + { + "text": "요즘 완전 폼 미쳤다", + "context": "A comment on a YouTube video about a popular singer.", + "action": "interpret", + } + ) + assert result["status"] == "ok" + assert result["action"] == "interpret" + assert result["translation"] == ( + "They are absolutely killing it right now / " "Their form is crazy these days" + ) + assert result["slang_breakdown"]["폼 미쳤다"].startswith( + "Slang directly translating to 'form is crazy'" + ) + assert result["nuance"].startswith("Highly positive, trendy internet-friendly") + assert "YouTube video" in result["nuance"] + assert result["formality"] == "Informal / Slang" + assert result["hit_count"] >= 1 + assert any(hit["id"] == "pom_michyeotda" for hit in result["hits"]) + + +def test_compact_and_romanization_match(): + skill = KoreanSlangSkill() + compact = skill.execute({"text": "폼미쳤다"}) + roman = skill.execute({"text": "pom michyeotda"}) + assert compact["hits"][0]["id"] == "pom_michyeotda" + assert roman["hits"][0]["id"] == "pom_michyeotda" + + +def test_longest_match_gatsaengreo_then_gatsaeng(): + skill = KoreanSlangSkill() + result = skill.execute({"text": "갓생러는 갓생 중"}) + ids = [hit["id"] for hit in result["hits"]] + assert ids.count("gatsaengreo") == 1 + assert ids.count("gatsaeng") == 1 + + +def test_lookup_and_unmatched(): + skill = KoreanSlangSkill() + found = skill.execute({"action": "lookup", "text": "알잘딱깔센"}) + assert found["found"] is True + assert found["entry"]["id"] == "aljaldakkkalsen" + missing = skill.execute({"action": "lookup", "text": "없는신조어xyz"}) + assert missing["found"] is False + unmatched = skill.execute({"text": "안녕하세요. 오늘 날씨가 좋습니다."}) + assert unmatched["unmatched"] is True + assert unmatched["slang_breakdown"] == {} + + +def test_suggest_peers_praise_and_elders_gate(): + skill = KoreanSlangSkill() + peers = skill.execute( + {"action": "suggest", "intent": "praise", "audience": "peers"} + ) + assert peers["status"] == "ok" + blob = " ".join(item["text"] for item in peers["suggestions"]) + assert "폼 미쳤다" in blob + assert "ㄹㅇ" in blob or "ㅋㅋ" in blob + elders = skill.execute( + {"action": "suggest", "intent": "praise", "audience": "elders"} + ) + elder_blob = " ".join(item["text"] for item in elders["suggestions"]) + assert "폼 미쳤다" not in elder_blob + assert "ㅋㅋ" not in elder_blob + assert "ㄹㅇ" not in elder_blob + assert "세요" in elder_blob or "습니다" in elder_blob + + +def test_constitution_blocks_and_vulgar_opt_in(): + skill = KoreanSlangSkill() + blocked = skill.execute({"text": "저 사람은 테무인간 같아"}) + assert blocked["blocked_omitted"] + assert "테무인간" not in json.dumps( + blocked.get("slang_breakdown"), ensure_ascii=False + ) + suggest_blocked = skill.execute( + {"action": "suggest", "text": "테무인간처럼 놀려줘", "intent": "annoyed"} + ) + assert suggest_blocked["suggestions"] == [] + default = skill.execute( + {"action": "suggest", "intent": "food", "audience": "peers"} + ) + food_blob = " ".join(item["text"] for item in default["suggestions"]) + assert "존나" not in food_blob + assert "존맛탱" not in food_blob + + +def test_empty_text_and_unknown_action(): + skill = KoreanSlangSkill() + empty = skill.execute({"action": "interpret", "text": " "}) + assert empty["status"] == "error" + assert empty["error"]["code"] == "EMPTY_TEXT" + unknown = skill.execute({"action": "translate", "text": "폼 미쳤다"}) + assert unknown["status"] == "error" + assert unknown["error"]["code"] == "UNKNOWN_ACTION" + + +def test_pack_entries_are_unique_and_lookupable(): + skill = KoreanSlangSkill() + entries = _load_json("entries.json") + ids = [entry["id"] for entry in entries] + assert len(ids) == len(set(ids)) + assert len(entries) >= 70 + for entry in entries: + surface = entry["surfaces"][0] + result = skill.execute({"action": "lookup", "text": surface}) + assert result["found"] is True, surface + assert result["entry"]["id"] == entry["id"] diff --git a/tests/fixtures/card_ui_schema/linguistics__korean_slang.json b/tests/fixtures/card_ui_schema/linguistics__korean_slang.json new file mode 100644 index 00000000..5053539d --- /dev/null +++ b/tests/fixtures/card_ui_schema/linguistics__korean_slang.json @@ -0,0 +1,27 @@ +{ + "status": "ok", + "action": "interpret", + "translation": "They are absolutely killing it right now / Their form is crazy these days", + "slang_breakdown": { + "폼 미쳤다": "Slang directly translating to 'form is crazy', used to praise someone doing exceptionally well or looking great." + }, + "nuance": "Highly positive, trendy internet-friendly colloquial expression used primarily by Gen-Z and Millennials. Context noted: A comment on a YouTube video about a popular singer.", + "formality": "Informal / Slang", + "hit_count": 1, + "hits": [ + { + "id": "pom_michyeotda", + "surface": "폼 미쳤다", + "matched": "폼 미쳤다", + "translation": "They are absolutely killing it right now / Their form is crazy these days", + "caution": false, + "vulgar": false + } + ], + "unmatched": false, + "warnings": [], + "blocked_omitted": [], + "pack_snapshot": "2026-09", + "pack_id": "korean_slang_s26_small", + "entry_count": 118 +} diff --git a/tests/skills/linguistics/__init__.py b/tests/skills/linguistics/__init__.py new file mode 100644 index 00000000..772bf94e --- /dev/null +++ b/tests/skills/linguistics/__init__.py @@ -0,0 +1 @@ +# Maintainer tests for linguistics skills. diff --git a/tests/skills/linguistics/test_korean_slang.py b/tests/skills/linguistics/test_korean_slang.py new file mode 100644 index 00000000..6f54ef47 --- /dev/null +++ b/tests/skills/linguistics/test_korean_slang.py @@ -0,0 +1,142 @@ +"""Maintainer depth and stress simulations for linguistics/korean_slang.""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from skillware.core.loader import SkillLoader + + +def _skill_class(): + return SkillLoader.load_skill("linguistics/korean_slang")["module"].KoreanSlangSkill + + +CHAT_SIMULATION = [ + ("요즘 완전 폼 미쳤다 ㄹㅇ", ["pom_michyeotda", "real_abbrev"]), + ("점메추 ㄱㄱ", ["jeommechu", "gogo"]), + ("내일 내또출이라 현타 옴", ["naettochul", "hyeonta"]), + ("알잘딱깔센으로 해줘", ["aljaldakkkalsen"]), + ("추구미 그잡채", ["chugumi", "geujapchae"]), + ("킹받네 에바참치", ["kingbatda", "ebachamchi"]), + ("오운완 ㅋㅋ", ["ounwan", "kk_laugh"]), + ("불금인데 치맥?", ["bulgeum", "chimaek"]), + ("최애 직캠 소름", ["choiae", "jikcam", "soreum"]), + ("워라밸 글렀고 야근각", ["worlabel", "yageun"]), + ("억텐 모드로 회식 가는 중", ["eokten"]), + ("스불재로 밤새고 멘붕", ["seubuljae", "menbung"]), + ("주불 호캉스 ㄱㄱ", ["jubuol", "hokangseu", "gogo"]), + ("만반잘부 ㅎㅇ", ["manbanjalbu", "hi_abbrev"]), + ("이건 별다줄 그 자체", ["byeoldajul"]), +] + + +def _skill(): + return _skill_class()() + + +def test_loader_and_class_are_the_same_pack(): + bundle = SkillLoader.load_skill("linguistics/korean_slang") + loaded = bundle["class"]() + native = _skill_class()() + sample = "느좋 바이브" + assert ( + loaded.execute({"text": sample})["hits"][0]["id"] + == native.execute({"text": sample})["hits"][0]["id"] + ) + + +@pytest.mark.parametrize("line,expected_ids", CHAT_SIMULATION) +def test_group_chat_simulation_hits(line, expected_ids): + result = _skill().execute({"text": line, "action": "interpret"}) + assert result["status"] == "ok" + got = {hit["id"] for hit in result["hits"]} + missing = set(expected_ids) - got + assert not missing, f"{line!r} missing {missing}; got {got}" + + +def test_suggest_roundtrip_interpret(): + skill = _skill() + suggested = skill.execute({"action": "suggest", "intent": "competence"}) + assert suggested["status"] == "ok" + text = suggested["suggestions"][0]["text"] + interpreted = skill.execute({"text": text}) + assert interpreted["hit_count"] >= 1 + assert "알잘딱" in json.dumps(interpreted["slang_breakdown"], ensure_ascii=False) + + +def test_suggest_never_emits_blocked_or_default_vulgar(): + skill = _skill() + module = SkillLoader.load_skill("linguistics/korean_slang")["module"] + pack_path = Path(module.__file__).resolve().parent / "kb" / "constitution.json" + blocked_terms = json.loads(pack_path.read_text(encoding="utf-8"))[ + "blocked_substrings" + ] + intents = [ + "praise", + "food", + "work", + "tired", + "annoyed", + "agree", + "greeting", + "bye", + "thanks", + "workout", + "weekend", + "dating", + "fandom", + "competence", + ] + for intent in intents: + for audience in ("peers", "mixed", "work", "elders"): + result = skill.execute( + {"action": "suggest", "intent": intent, "audience": audience} + ) + assert result["status"] == "ok", (intent, audience, result) + blob = json.dumps(result, ensure_ascii=False) + for term in blocked_terms: + assert term not in blob + assert "존나" not in blob + assert "ㅈㄴ" not in blob + + +def test_style_hint_on_formal_prose(): + result = _skill().execute( + { + "text": "오늘 공연을 보았습니다. 정말 훌륭합니다.", + "audience": "peers", + } + ) + assert result.get("style_hint") + + +def test_jamo_particles_in_comment_thread(): + result = _skill().execute({"text": "ㄹㅇㅋㅋ ㅇㅈ"}) + ids = {hit["id"] for hit in result["hits"]} + assert "real_abbrev" in ids + assert "kk_laugh" in ids + assert "injeong" in ids + + +def test_max_hits_and_invalid_audience(): + skill = _skill() + capped = skill.execute( + {"text": "ㄹㅇ ㅇㅈ ㄱㄱ ㄴㄴ ㅎㅇ ㅂㅂ ㄱㅅ ㅊㅋ ㄷㄷ", "max_hits": 2} + ) + assert capped["hit_count"] <= 2 + bad = skill.execute({"text": "폼 미쳤다", "audience": "boss"}) + assert bad["status"] == "error" + assert bad["error"]["code"] == "UNKNOWN_AUDIENCE" + + +def test_identical_input_is_bit_stable(): + skill = _skill() + params = {"text": "헤드림 직캠 폼 미쳤다 ㄹㅇ", "context": "fancam"} + first = skill.execute(params) + second = skill.execute(params) + assert json.dumps(first, sort_keys=True, ensure_ascii=False) == json.dumps( + second, sort_keys=True, ensure_ascii=False + ) diff --git a/tests/test_examples_smoke.py b/tests/test_examples_smoke.py index fbfee8aa..b8c68a6c 100644 --- a/tests/test_examples_smoke.py +++ b/tests/test_examples_smoke.py @@ -42,6 +42,15 @@ "mental_coach_demo.py", ["wellness/mental_coach", "Coaching", "Crisis escalation", "policy_status:"], ), + ( + "korean_slang_demo.py", + [ + "linguistics/korean_slang", + "issue #34", + "aljaldakkkalsen", + "Demo complete.", + ], + ), ( "prompt_injection_firewall_demo.py", ["security/prompt_injection_firewall", "Hidden HTML override", "is_safe:"], diff --git a/tests/test_github_labels.py b/tests/test_github_labels.py index cb6836db..91940525 100644 --- a/tests/test_github_labels.py +++ b/tests/test_github_labels.py @@ -4,7 +4,13 @@ import re from pathlib import Path -LABELS_PATH = Path(__file__).resolve().parent.parent / ".github" / "labels.json" +import yaml + +REPO_ROOT = Path(__file__).resolve().parent.parent +LABELS_PATH = REPO_ROOT / ".github" / "labels.json" +PROPOSAL_TEMPLATE = REPO_ROOT / ".github" / "ISSUE_TEMPLATE" / "01_skill_proposal.yml" +SKILLS_ROOT = REPO_ROOT / "skills" +PROPOSE_NEW_CATEGORY = "Propose new category (describe below)" HEX_COLOR = re.compile(r"^[0-9A-Fa-f]{6}$") GITHUB_LABEL_DESCRIPTION_MAX = 100 @@ -16,6 +22,7 @@ "defi", "dev_tools", "finance", + "linguistics", "monitoring", "office", "optimization", @@ -130,3 +137,30 @@ def test_category_labels_do_not_collide_with_repo_labels(): ) assert "security" in names assert f"{CATEGORY_LABEL_PREFIX}security" in names + + +def test_skill_proposal_dropdown_matches_registry_categories(): + """New Skill Proposal category options stay aligned with REGISTRY_CATEGORIES.""" + data = yaml.safe_load(PROPOSAL_TEMPLATE.read_text(encoding="utf-8")) + options = None + for field in data.get("body") or []: + if field.get("id") == "category": + options = list(field.get("attributes", {}).get("options") or []) + break + assert options, "01_skill_proposal.yml is missing the category dropdown" + assert options[-1] == PROPOSE_NEW_CATEGORY + assert tuple(options[:-1]) == REGISTRY_CATEGORIES + + +def test_registry_categories_match_skills_folders(): + """REGISTRY_CATEGORIES is the live set of skills// directories.""" + folders = tuple( + sorted( + path.name + for path in SKILLS_ROOT.iterdir() + if path.is_dir() and not path.name.startswith("__") + ) + ) + assert folders == tuple(sorted(REGISTRY_CATEGORIES)) + assert "linguistics" in folders + assert f"{CATEGORY_LABEL_PREFIX}linguistics" in _label_map()