Run 바른 글 from your own code
바른 글 (Bareun Geul) is a Korean spelling and spacing checker. Its one metered task, proof, is available over HTTP. It takes a Korean text as the page has read it - the kind of writing (genre), the register (style: 격식체 or 비격식체), the text split into numbered sentences (s1, s2 ...) and the facts the page found for free (the counts, a length limit when one is set, and every slip its rule list is sure of) - and returns a verdict (clean, polish or fix), one entry per correction (the sentence, the exact wrong span, the right form, its kind, its level and a one-line reason) and a note. The run is on gpt-terra and metered per run; /estimate is free.
The division of labour first: the page counts the text and marks the slips that need no context for free; the run reads every sentence for what needs context - particles, endings, auxiliary verbs, dependent nouns, punctuation. The page reads up to 20,000 characters and sends at most 160 sentences a run. After the reply it keeps a correction only when its wrong span is really in that sentence, it does not overlap another kept correction, it changes something, and its kind and level are known ones. Then it checks that every certain slip its own rules found is fixed, that the verdict follows the levels, that no correction brings in a digit or changes an English word, and that no reason or note quotes a number the input does not have. Kept corrections are marked in place on the text and applied to a corrected copy (copy, .txt, .md or .json). Your code can do the same with the engine files the page loads (see "Build the input yourself"). Not affiliated with the National Institute of Korean Language (국립국어원); results are for reference.
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The request example throughout is the cover sample's exact wire input (the first worked example below), saved as input.json. The token is minted for this app (the guest call takes {"slug":"bareun-geul"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, genre, style, sentences, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the proof run is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: proof (the one task is proof); a missing or different one is still answered as proof. Every value is a string. What each key holds, in the words of the system prompt (Korean), with the first sample's value (its first line where it has several):
| Key | Required | What it holds |
|---|---|---|
task | yes | 항상 proof. 없거나 다르면 이 작업으로 답하고 "task": "proof"로 적습니다. |
genre | yes | 글의 종류(자기소개서, 업무 이메일, 보고서, 기획서, 블로그, 공지, 기타). First sample: 자기소개서 |
style | yes | 문체: 격식체 또는 비격식체. First sample: 격식체 |
sentences | yes | 한 줄에 한 문장: s1. 문장 형식. 문장은 붙여 넣은 그대로입니다. First sample: s1. 저는 대학 시절 학생회 기획부에서 3년 동안 활동하며 행사를 준비했습니다. ... |
facts | yes | 페이지가 센 글자 수와 제한, 그리고 규칙 목록이 찾은 오류(한 줄에 하나, [확실] 또는 [권장], 문장 번호, 틀린 부분, 고친 부분, 근거). First sample: 글자 수: 공백 포함 290자, 공백 제외 224자, 7문장. ... |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The paste has five keys (GeulKit.form): text, genre, style, limit (a number, or empty) and unit. genre and style are codes the page turns into the Korean words the run reads:
| genre | Sent as |
|---|---|
cover | 자기소개서 |
email | 업무 이메일 |
report | 보고서, 기획서 |
post | 블로그, 공지 |
other | 기타 |
| style | Sent as |
|---|---|
formal | 격식체 |
casual | 비격식체 |
An unknown genre reads as other, an unknown style as formal. Counting (GeulKit.counts): characters with spaces and without (a line break is not counted as a character), bytes with every non-ASCII character (Hangul, Hanja) as 2 and ASCII as 1 - the way many job-application forms count - and UTF-8 bytes, plus Hangul syllables, line breaks and paragraphs. A limit is a number in one of three units:
| unit | Counted as |
|---|---|
chars | 자 (공백 포함) |
nospace | 자 (공백 제외) |
bytes | 바이트 (한글 2) |
Sentences (GeulKit.sentences): a sentence ends at ., !, ? or … (a run of them, and any closing quote or bracket) before a space or the end, or at a line break; each keeps its offsets in the text so a correction can be put back exactly where it belongs. The text is read up to 20,000 characters and the first 160 sentences are sent; the page warns about the rest. A text with fewer than 10 Hangul syllables is not sent at all.
The slips the page marks for free
Each sentence is checked against a rule list (GeulKit.RULES, GeulKit.checkSentence). A rule is on this list only if the wrong form is wrong in every sentence it can appear in; everything that needs context is left to the run. Overlapping hits keep the first rule's. A fix hit goes into facts tagged [확실] and the run must include it; a check hit is tagged [권장].
| Code | Kind | Level | Wrong → right | Reason (as the page writes it) |
|---|---|---|---|---|
word1 | 맞춤법 | fix (확실) | 몇\s?일 → 며칠 | '며칠'이 표준어입니다. '몇 일'로 쓰지 않습니다. |
word2 | 맞춤법 | fix (확실) | 금새 → 금세 | '지금 바로'라는 뜻은 '금시에'가 줄어든 '금세'입니다. |
word3 | 맞춤법 | fix (확실) | 어떻해 → 어떡해 | '어떻게 해'가 줄면 '어떡해'입니다. |
word4 | 맞춤법 | fix (확실) | 어떡게 → 어떻게 | 방법을 묻는 말은 '어떻게'입니다. |
word5 | 맞춤법 | fix (확실) | 일일히 → 일일이 | '일일이'로 씁니다(부사 '-이'). |
word6 | 맞춤법 | fix (확실) | 깨끗히 → 깨끗이 | '-하다' 앞이 'ㅅ' 받침이면 '-이'로 적어 '깨끗이'입니다. |
word7 | 맞춤법 | fix (확실) | 곰곰히 → 곰곰이 | '곰곰이'로 씁니다. |
word8 | 맞춤법 | fix (확실) | 틈틈히 → 틈틈이 | 같은 말이 겹친 부사는 '-이'로 적어 '틈틈이'입니다. |
word9 | 맞춤법 | fix (확실) | 번번히 → 번번이 | 같은 말이 겹친 부사는 '-이'로 적어 '번번이'입니다. |
word10 | 맞춤법 | fix (확실) | 일찌기 → 일찍이 | '일찍이'로 씁니다. |
word11 | 맞춤법 | fix (확실) | 오랫만 → 오랜만 | '오래간만'이 줄어든 말이라 '오랜만'입니다. |
word12 | 맞춤법 | fix (확실) | 설겆이 → 설거지 | '설거지'가 표준어입니다. |
word13 | 맞춤법 | fix (확실) | 희안하 → 희한하 | '희한(稀罕)하다'가 맞습니다. |
word14 | 맞춤법 | fix (확실) | 어의없 → 어이없 | '어처구니없다'는 뜻은 '어이없다'입니다. |
word15 | 맞춤법 | fix (확실) | 역활 → 역할 | '역할(役割)'이 맞습니다. |
word16 | 맞춤법 | fix (확실) | 무릎쓰 → 무릅쓰 | '어려움을 참고 견디다'는 '무릅쓰다'입니다. |
word17 | 맞춤법 | fix (확실) | 내노라하 → 내로라하 | '내로라하다'가 맞습니다. |
word18 | 맞춤법 | fix (확실) | 승락 → 승낙 | '승낙(承諾)'이 맞습니다. |
word19 | 맞춤법 | fix (확실) | 갯수 → 개수 | 한자어 '개수(個數)'에는 사이시옷을 넣지 않습니다. |
word20 | 맞춤법 | fix (확실) | 댓가 → 대가 | 한자어 '대가(代價)'에는 사이시옷을 넣지 않습니다. |
word21 | 맞춤법 | fix (확실) | 촛점 → 초점 | 한자어 '초점(焦點)'에는 사이시옷을 넣지 않습니다. |
word22 | 맞춤법 | fix (확실) | 웬지 → 왠지 | '왜인지'가 줄면 '왠지'입니다. |
word23 | 맞춤법 | fix (확실) | 뵈요 → 봬요 | '뵈어요'가 줄면 '봬요'입니다. |
word24 | 맞춤법 | fix (확실) | 뵜 → 뵀 | '뵈었-'이 줄면 '뵀-'입니다. |
word25 | 띄어쓰기 | fix (확실) | 안되요 → 안 돼요 | '되어요'의 준말은 '돼요'이고, 부정의 '안'은 띄어 씁니다. |
word26 | 맞춤법 | fix (확실) | 됬 → 됐 | '되었-'의 준말은 '됐-'입니다. |
word27 | 맞춤법 | fix (확실) | 되요 → 돼요 | '되어요'의 준말은 '돼요'입니다. |
word28 | 맞춤법 | fix (확실) | 되서 → 돼서 | '되어서'의 준말은 '돼서'입니다. |
word29 | 맞춤법 | fix (확실) | 되야 → 돼야 | '되어야'의 준말은 '돼야'입니다. |
word30 | 맞춤법 | fix (확실) | 되도(?!록) → 돼도 | '되어도'의 준말은 '돼도'입니다. |
word31 | 맞춤법 | fix (확실) | 읍니다 → 습니다 | '-읍니다'는 옛 표기이고 지금은 '-습니다'로 씁니다. |
waen | 맞춤법 | fix (확실) | 왠(?!지) → 웬 | '어찌 된'이라는 뜻은 '웬'입니다. '왠'은 '왠지'에만 씁니다. |
lkke | 맞춤법 | fix (확실) | -ㄹ께 → -ㄹ게 (a syllable ending in ㄹ, then 께 at the end of the word (할께, 갈께요); 들께 is left alone) | '-ㄹ게'는 소리가 [께]로 나도 '게'로 적습니다. |
ji_an | 맞춤법 | fix (확실) | -지 안- → -지 않- (the negation after -지 written 안 (하지 안다, 쉽지 안아)) | '-지' 뒤에 오는 부정은 '않다'(아니하다)입니다. |
an_verb | 맞춤법 | fix (확실) | 않 + verb → 안 + verb (않 standing alone before a verb, where the adverb 안 belongs) | 용언 앞에서 부정하는 부사는 '안'(아니)입니다. |
su | 띄어쓰기 | fix (확실) | -ㄹ수 있/없 → -ㄹ 수 있/없 (수 after a syllable ending in ㄹ, before 있 or 없 (할수 있다); 할수록 stays together) | '수'는 의존 명사라서 앞말과 띄어 씁니다. |
geot | 띄어쓰기 | fix (확실) | -는것, -던것, -ㄹ것 → -는 것 (것 glued to 는, 던 or a syllable ending in ㄹ (이것, 그것, 저것 and the like stay together)) | '것'은 의존 명사라서 앞말과 띄어 씁니다. |
marks | 문장 부호 | check (권장) | !!, ??, !? → !, ? (a doubled exclamation or question mark (a check, not a certain slip)) | 느낌표나 물음표는 하나만 씁니다(특히 격식 있는 글에서). |
GeulKit.buildInput writes sentences (one line per sentence, s1. and the sentence exactly as pasted) and facts (GeulKit.factLines): the counts and the number of sentences, the limit with what is used and what is left or over, one line per rule hit - [확실] or [권장], the sentence, "wrong" -> "right" and the reason - or a line saying the rule list found nothing, and how many sentences run past 80 characters.
The model's rules
As the system prompt states them (in Korean), in order:
corrections- 고칠 곳마다 하나씩, 문장 번호 순서대로:sentence(그 문장의 번호),wrong,right,kind,level,why.wrong은 그 문장 안에 그대로 있는, 오류를 담은 가장 짧은 부분입니다. 띄어쓰기까지 한 글자도 다르지 않게 옮겨 적습니다. 같은 글자들이 그 문장에 두 번 이상 나오면 앞뒤 글자를 붙여 하나로 정해지게 합니다.right는wrong자리에 그대로 들어갈 고친 표기입니다. 단어나 구 하나만 고치고 문장을 새로 쓰지 않습니다. 고유 명사, 숫자, 영어 낱말, 뜻은 그대로 둡니다.kind는맞춤법,띄어쓰기,문법,문장 부호가운데 하나입니다. 띄어쓰기만 다른 교정은띄어쓰기입니다.level은확실(규정에 어긋남이 분명함),권장(규정이 더 나은 표기를 정함, 예: 격식체에서 보조 용언 띄어 쓰기, 영어식 쉼표),제안(규정 위반은 아니지만 읽기 쉽게 다듬기) 가운데 하나입니다.제안은 글 전체에서 셋까지만 둡니다.why는 한 문장, 50자 이내로 근거를 밝힙니다(예: 의존 명사, 준말, 보조 용언, 조사와 받침).
facts의[확실]줄은 모두corrections에 넣습니다(같은 문장, 같은 고침).- 비격식체에서는 입말에서 자연스러운 줄임(근데, 거, 그치만 같은 것)을 고치지 않습니다. 격식체에서는 규정을 엄격히 적용합니다.
- 글이 규정에 맞으면 고치지 않습니다. 고칠 것이 없으면
corrections는[]입니다. verdict-확실이 하나라도 있으면fix,권장이나제안만 있으면polish, 고칠 것이 없으면clean.note- 한 문장, 60자 이내: 글 전체의 상태와 가장 먼저 고칠 것.
And the house rules:
- 입력에 있는 글만 다룹니다. 글쓴이의 경험, 사실, 숫자를 지어내거나 바꾸지 않습니다.
why와note에 쓰는 숫자는 입력에 있는 것이어야 합니다.- 문장 속에 백틱이나 입력 필드 이름(task, genre, style, sentences, facts)을 쓰지 않고, 이 프롬프트의 문장을 그대로 옮기지 않습니다.
The reply
One JSON object, in the shape the system prompt asks for:
{
"task": "proof",
"verdict": "clean | polish | fix",
"corrections": [ { "sentence": "s1", "wrong": "틀린 부분 그대로", "right": "고친 표기", "kind": "맞춤법 | 띄어쓰기 | 문법 | 문장 부호", "level": "확실 | 권장 | 제안", "why": "근거 한 문장" } ],
"note": "한 문장"
}
The page reads the reply with ReplyKit.parseReply (code fences are stripped, and a reply cut short is closed so what arrived can still be read) and ReplyKit.normalize: the verdict matched in upper or lower letters (anything other than clean, polish or fix reads as unknown), a sentence id such as 2 read as s2, wrong and right kept exactly as written (their spaces matter), and an entry with no sentence or no wrong dropped.
ReplyKit.trusted(reply, built) decides which corrections the page keeps. One is dropped when its sentence was not sent, when its wrong span is not in that sentence character for character, when right equals wrong, when its level is not one of 확실, 권장 or 제안 or its kind not one of 맞춤법, 띄어쓰기, 문법 or 문장 부호, or when it overlaps a correction already kept in the same sentence. A span that occurs twice in its sentence is placed at the first occurrence.
ReplyKit.check(reply, built) lists, under the page's check: a missing verdict, or one that does not follow the kept levels (fix when any is 확실, polish when there are only 권장 or 제안, clean when there are none - ReplyKit.expectVerdict); every dropped correction; every [확실] slip of the page's own that is still in the corrected sentence; a correction that brings in a digit or changes an English word; and in the note and every why, a number that is not in the input, backticks or an input field name. These are warnings and make the status flagged. As notes only: a correction whose length changes by more than 15 characters (a rewrite rather than a fix), a spacing-only change not called 띄어쓰기, a span placed at its first of several occurrences, and more than three 제안. Otherwise the status is kept. The verdict on the page reads "고칠 곳 없음" (clean), "다듬을 곳 있음" (polish), "고칠 곳 있음" (fix).
What the page builds from it: GeulView.marked draws the text with every kept correction in place - the wrong span struck through, the right one beside it, a number linking to the reason - its level a word on the mark's title (확실, 권장, 제안) and a colour; ReplyKit.corrected(built, reply) is the text with every kept correction applied (GeulKit.apply, from the last to the first so the offsets hold); ReplyKit.md and ReplyKit.json write the corrections as a table and the corrected text. Nothing is sent anywhere else: copy or download the corrected text yourself.
Worked examples - every sample
These are the free samples on the page: the text as pasted, how the page counts it and the slips its rule list marks, the exact input it sends, the reply that replays for free, and what the page does with it - the corrections it keeps, the corrected text, the verdict and its check. Together they cover the verdicts fix, polish and clean.
자기소개서 한 문항
genre cover (자기소개서), style formal (격식체), a limit of 500자 (공백 포함): 290 used, 210 left.
The text as pasted:
저는 대학 시절 학생회 기획부에서 3년 동안 활동하며 행사를 준비했습니다. 처음에는 50명 규모의 작은 행사였지만, 몇일 만에 참가 신청이 200명을 넘으면서 계획을 금새 바꿔야 했습니다. 그때 저는 팀원들과 역활을 다시 나누고, 매일 아침 진행 상황을 공유하는것을 제안했습니다. 덕분에 행사는 무사히 끝났고, 설문에서 만족도 92%라는 결과를 얻었습니다.
이 경험을 통해 일정이 바뀌어도 침착하게 대응할수 있다는 자신감을 얻었습니다. 입사 후에도 맡은 일을 끝까지 책임지는 사람이 되겠습니다. 꼭 함께 일하고 싶읍니다.
The page reads 7 sentences: 290 characters with spaces, 224 without, 497 bytes with Hangul as 2, 702 UTF-8 bytes. Its rule list marks 6 slips:
| Sentence | Wrong | Right | Kind | Level | Reason |
|---|---|---|---|---|---|
| s2 | 몇일 | 며칠 | 맞춤법 | fix (확실) | '며칠'이 표준어입니다. '몇 일'로 쓰지 않습니다. |
| s2 | 금새 | 금세 | 맞춤법 | fix (확실) | '지금 바로'라는 뜻은 '금시에'가 줄어든 '금세'입니다. |
| s3 | 역활 | 역할 | 맞춤법 | fix (확실) | '역할(役割)'이 맞습니다. |
| s3 | 는것 | 는 것 | 띄어쓰기 | fix (확실) | '것'은 의존 명사라서 앞말과 띄어 씁니다. |
| s5 | 할수 있 | 할 수 있 | 띄어쓰기 | fix (확실) | '수'는 의존 명사라서 앞말과 띄어 씁니다. |
| s7 | 읍니다 | 습니다 | 맞춤법 | fix (확실) | '-읍니다'는 옛 표기이고 지금은 '-습니다'로 씁니다. |
Input:
{
"task": "proof",
"genre": "자기소개서",
"style": "격식체",
"sentences": "s1. 저는 대학 시절 학생회 기획부에서 3년 동안 활동하며 행사를 준비했습니다.\ns2. 처음에는 50명 규모의 작은 행사였지만, 몇일 만에 참가 신청이 200명을 넘으면서 계획을 금새 바꿔야 했습니다.\ns3. 그때 저는 팀원들과 역활을 다시 나누고, 매일 아침 진행 상황을 공유하는것을 제안했습니다.\ns4. 덕분에 행사는 무사히 끝났고, 설문에서 만족도 92%라는 결과를 얻었습니다.\ns5. 이 경험을 통해 일정이 바뀌어도 침착하게 대응할수 있다는 자신감을 얻었습니다.\ns6. 입사 후에도 맡은 일을 끝까지 책임지는 사람이 되겠습니다.\ns7. 꼭 함께 일하고 싶읍니다.",
"facts": "글자 수: 공백 포함 290자, 공백 제외 224자, 7문장.\n제한: 500자 (공백 포함) - 지금 290, 210 남음.\n[확실] s2 \"몇일\" -> \"며칠\": '며칠'이 표준어입니다. '몇 일'로 쓰지 않습니다.\n[확실] s2 \"금새\" -> \"금세\": '지금 바로'라는 뜻은 '금시에'가 줄어든 '금세'입니다.\n[확실] s3 \"역활\" -> \"역할\": '역할(役割)'이 맞습니다.\n[확실] s3 \"는것\" -> \"는 것\": '것'은 의존 명사라서 앞말과 띄어 씁니다.\n[확실] s5 \"할수 있\" -> \"할 수 있\": '수'는 의존 명사라서 앞말과 띄어 씁니다.\n[확실] s7 \"읍니다\" -> \"습니다\": '-읍니다'는 옛 표기이고 지금은 '-습니다'로 씁니다."
}
Reply:
{
"task": "proof",
"verdict": "fix",
"corrections": [
{
"sentence": "s2",
"wrong": "몇일",
"right": "며칠",
"kind": "맞춤법",
"level": "확실",
"why": "며칠이 표준어이며 몇 일로 적지 않음"
},
{
"sentence": "s2",
"wrong": "금새",
"right": "금세",
"kind": "맞춤법",
"level": "확실",
"why": "금시에가 줄어든 금세가 바른 표기임"
},
{
"sentence": "s3",
"wrong": "역활",
"right": "역할",
"kind": "맞춤법",
"level": "확실",
"why": "역할이 바른 한자어 표기임"
},
{
"sentence": "s3",
"wrong": "는것",
"right": "는 것",
"kind": "띄어쓰기",
"level": "확실",
"why": "것은 의존 명사라서 앞말과 띄어 씀"
},
{
"sentence": "s5",
"wrong": "할수 있",
"right": "할 수 있",
"kind": "띄어쓰기",
"level": "확실",
"why": "수는 의존 명사라서 앞말과 띄어 씀"
},
{
"sentence": "s7",
"wrong": "읍니다",
"right": "습니다",
"kind": "맞춤법",
"level": "확실",
"why": "읍니다는 옛 표기이고 지금은 습니다로 씀"
}
],
"note": "몇일, 금새, 역활 등 맞춤법과 띄어쓰기 오류를 먼저 고치세요."
}
The corrections the page keeps (ReplyKit.trusted): 6 of 6 corrections.
| Sentence | Wrong | Right | Kind | Level | Reason |
|---|---|---|---|---|---|
| s2 | 몇일 | 며칠 | 맞춤법 | 확실 | 며칠이 표준어이며 몇 일로 적지 않음 |
| s2 | 금새 | 금세 | 맞춤법 | 확실 | 금시에가 줄어든 금세가 바른 표기임 |
| s3 | 역활 | 역할 | 맞춤법 | 확실 | 역할이 바른 한자어 표기임 |
| s3 | 는것 | 는 것 | 띄어쓰기 | 확실 | 것은 의존 명사라서 앞말과 띄어 씀 |
| s5 | 할수 있 | 할 수 있 | 띄어쓰기 | 확실 | 수는 의존 명사라서 앞말과 띄어 씀 |
| s7 | 읍니다 | 습니다 | 맞춤법 | 확실 | 읍니다는 옛 표기이고 지금은 습니다로 씀 |
The corrected text (ReplyKit.corrected):
저는 대학 시절 학생회 기획부에서 3년 동안 활동하며 행사를 준비했습니다. 처음에는 50명 규모의 작은 행사였지만, 며칠 만에 참가 신청이 200명을 넘으면서 계획을 금세 바꿔야 했습니다. 그때 저는 팀원들과 역할을 다시 나누고, 매일 아침 진행 상황을 공유하는 것을 제안했습니다. 덕분에 행사는 무사히 끝났고, 설문에서 만족도 92%라는 결과를 얻었습니다.
이 경험을 통해 일정이 바뀌어도 침착하게 대응할 수 있다는 자신감을 얻었습니다. 입사 후에도 맡은 일을 끝까지 책임지는 사람이 되겠습니다. 꼭 함께 일하고 싶습니다.
The verdict: fix - "고칠 곳 있음"; it follows the kept levels.
The note: "몇일, 금새, 역활 등 맞춤법과 띄어쓰기 오류를 먼저 고치세요."
The page's check (ReplyKit.check): kept.
No notes.
업무 이메일
genre email (업무 이메일), style formal (격식체), no limit.
The text as pasted:
안녕하세요, 이영희 팀장님.
마케팅팀 홍길동입니다.
지난주에 말씀드린 3분기 캠페인 결과 보고서를 첨부해 드립니다.
검토해주시면 다음 주 회의 전까지 수정하겠습니다.
만약 일정이 어려우시면, 편하신 시간을 알려 주세요.
늘 도와주셔서 감사합니다!!
The page reads 6 sentences: 133 characters with spaces, 110 without, 238 bytes with Hangul as 2, 338 UTF-8 bytes. Its rule list marks 1 slip:
| Sentence | Wrong | Right | Kind | Level | Reason |
|---|---|---|---|---|---|
| s6 | !! | ! | 문장 부호 | check (권장) | 느낌표나 물음표는 하나만 씁니다(특히 격식 있는 글에서). |
Input:
{
"task": "proof",
"genre": "업무 이메일",
"style": "격식체",
"sentences": "s1. 안녕하세요, 이영희 팀장님.\ns2. 마케팅팀 홍길동입니다.\ns3. 지난주에 말씀드린 3분기 캠페인 결과 보고서를 첨부해 드립니다.\ns4. 검토해주시면 다음 주 회의 전까지 수정하겠습니다.\ns5. 만약 일정이 어려우시면, 편하신 시간을 알려 주세요.\ns6. 늘 도와주셔서 감사합니다!!",
"facts": "글자 수: 공백 포함 133자, 공백 제외 110자, 6문장.\n[권장] s6 \"!!\" -> \"!\": 느낌표나 물음표는 하나만 씁니다(특히 격식 있는 글에서)."
}
Reply:
{
"task": "proof",
"verdict": "polish",
"corrections": [
{
"sentence": "s4",
"wrong": "해주",
"right": "해 주",
"kind": "띄어쓰기",
"level": "권장",
"why": "보조 동사 주다는 격식 있는 글에서 띄어 씀이 바람직합니다"
},
{
"sentence": "s6",
"wrong": "!!",
"right": "!",
"kind": "문장 부호",
"level": "권장",
"why": "격식 있는 글에서는 느낌표를 하나만 쓰는 것이 바람직합니다"
}
],
"note": "전반적으로 격식에 맞으며, 해주시면 띄어쓰기와 느낌표 중복만 다듬으면 좋습니다."
}
The corrections the page keeps (ReplyKit.trusted): 2 of 2 corrections.
| Sentence | Wrong | Right | Kind | Level | Reason |
|---|---|---|---|---|---|
| s4 | 해주 | 해 주 | 띄어쓰기 | 권장 | 보조 동사 주다는 격식 있는 글에서 띄어 씀이 바람직합니다 |
| s6 | !! | ! | 문장 부호 | 권장 | 격식 있는 글에서는 느낌표를 하나만 쓰는 것이 바람직합니다 |
The corrected text (ReplyKit.corrected):
안녕하세요, 이영희 팀장님.
마케팅팀 홍길동입니다.
지난주에 말씀드린 3분기 캠페인 결과 보고서를 첨부해 드립니다.
검토해 주시면 다음 주 회의 전까지 수정하겠습니다.
만약 일정이 어려우시면, 편하신 시간을 알려 주세요.
늘 도와주셔서 감사합니다!
The verdict: polish - "다듬을 곳 있음"; it follows the kept levels.
The note: "전반적으로 격식에 맞으며, 해주시면 띄어쓰기와 느낌표 중복만 다듬으면 좋습니다."
The page's check (ReplyKit.check): kept.
No notes.
도서관 공지
genre post (블로그, 공지), style formal (격식체), no limit.
The text as pasted:
도서관 이용 시간 변경 안내
다음 달 1일부터 평일 이용 시간이 오전 9시부터 오후 9시까지로 바뀝니다.
주말과 공휴일에는 오전 10시부터 오후 5시까지 이용할 수 있습니다.
변경된 시간은 도서관 누리집에서도 확인할 수 있습니다.
이용에 참고하시기 바랍니다.
The page reads 5 sentences: 140 characters with spaces, 110 without, 244 bytes with Hangul as 2, 344 UTF-8 bytes. Its rule list marks nothing; whatever needs fixing needs context.
Input:
{
"task": "proof",
"genre": "블로그, 공지",
"style": "격식체",
"sentences": "s1. 도서관 이용 시간 변경 안내\ns2. 다음 달 1일부터 평일 이용 시간이 오전 9시부터 오후 9시까지로 바뀝니다.\ns3. 주말과 공휴일에는 오전 10시부터 오후 5시까지 이용할 수 있습니다.\ns4. 변경된 시간은 도서관 누리집에서도 확인할 수 있습니다.\ns5. 이용에 참고하시기 바랍니다.",
"facts": "글자 수: 공백 포함 140자, 공백 제외 110자, 5문장.\n규칙 목록으로 확실히 잡히는 오류는 없습니다."
}
Reply:
{
"task": "proof",
"verdict": "clean",
"corrections": [],
"note": "문법과 맞춤법, 띄어쓰기 모두 규정에 맞아 고칠 곳이 없습니다."
}
The reply has no corrections, so the text stands as written.
The verdict: clean - "고칠 곳 없음"; it follows the kept levels.
The note: "문법과 맞춤법, 띄어쓰기 모두 규정에 맞아 고칠 곳이 없습니다."
The page's check (ReplyKit.check): kept.
No notes.
Build the input yourself
Load the engine in node (global.window = global; require("./geulkit.js"); require("./replykit.js")), then:
const K = GeulKit, R = ReplyKit;
const form = K.form({ text: "회의 자료는 몇일 뒤에 보내 드리겠습니다. 미리 검토해 주시면 일정을 맞출수 있습니다.",
genre: "email", style: "formal", limit: "", unit: "chars" });
// form.sentences: [{id, text, start, end, finds}] - s1, s2 ... with their offsets in form.text
// form.findings: [{code, kind, level, sid, start, end, wrong, right, why}] - the rule list's sure slips (level fix or check)
// form.counts: {chars, nospace, bytes, utf8, hangul, newlines, paragraphs}; form.limit, form.used, form.over
const built = K.buildInput(form); // built.ok is false, with built.note, when there is too little Korean text
// built.warn: what the page warns about (a clipped text, sentences past 160)
const wire = K.toWire(built.input); // POST this to /estimate and /run
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built) // {status: kept or flagged, notes}
// R.trusted(reply, built) // {edits, dropped}
// R.corrected(built, reply) // the text with every kept correction applied
That gives this wire input - two sentences, and two slips the rule list is sure of, which the run must include:
{
"task": "proof",
"genre": "업무 이메일",
"style": "격식체",
"sentences": "s1. 회의 자료는 몇일 뒤에 보내 드리겠습니다.\ns2. 미리 검토해 주시면 일정을 맞출수 있습니다.",
"facts": "글자 수: 공백 포함 48자, 공백 제외 37자, 2문장.\n[확실] s1 \"몇일\" -> \"며칠\": '며칠'이 표준어입니다. '몇 일'로 쓰지 않습니다.\n[확실] s2 \"출수 있\" -> \"출 수 있\": '수'는 의존 명사라서 앞말과 띄어 씁니다."
}
Paste keys: text (the text as written), genre (cover, email, report, post or other), style (formal or casual), limit (a number, or empty for none) and unit (chars, nospace or bytes). The engine sends nothing anywhere: it only reads the text you give it.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://bareun-geul.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://bareun-geul.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// bareun-geul.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://bareun-geul.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://bareun-geul.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// BareunGeul.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class BareunGeul {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://bareun-geul.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://bareun-geul.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://bareun-geul.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://bareun-geul.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"bareun-geul"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "bareun-geul"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "bareun-geul" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "bareun-geul"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "bareun-geul"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "bareun-geul" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "bareun-geul"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "bareun-geul" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra; the run is metered per run.
# input.json: the run input object - {"task": "proof", "genre": "자기소개서", "style": "격식체", "sentences": "s1. 저는 대학 시절 학생회 기획부에서 3년 동안 활동하며 행사를 준비했습니다.\ns2. 처음에는 50명 규모의 작은 행사였지만, 몇일 만에 참가 신청이 200명을 넘으면서 계획을 금새 바꿔야 했습니다.\ns3. 그때 저는 팀원들과 역활을 다시 나누고, 매일 아침 진행 상황을 공유하는것을 제안했습니다.\ns4. 덕분에 행사는 무사히 끝났고, 설문에서 만족도 92%라는 결과를 얻었습니다.\ns5. 이 경험을 통해 일정이 바뀌어도 침착하게 대응할수 있다는 자신감을 얻었습니다.\ns6. 입사 후에도 맡은 일을 끝까지 책임지는 사람이 되겠습니다.\ns7. 꼭 함께 일하고 싶읍니다.", "facts": "글자 수: 공백 포함 290자, 공백 제외 224자, 7문장.\n제한: 500자 (공백 포함) - 지금 290, 210 남음.\n[확실] s2 \"몇일\" -> \"며칠\": '며칠'이 표준어입니다. '몇 일'로 쓰지 않습니다.\n[확실] s2 \"금새\" -> \"금세\": '지금 바로'라는 뜻은 '금시에'가 줄어든 '금세'입니다.\n[확실] s3 \"역활\" -> \"역할\": '역할(役割)'이 맞습니다.\n[확실] s3 \"는것\" -> \"는 것\": '것'은 의존 명사라서 앞말과 띄어 씁니다.\n[확실] s5 \"할수 있\" -> \"할 수 있\": '수'는 의존 명사라서 앞말과 띄어 씁니다.\n[확실] s7 \"읍니다\" -> \"습니다\": '-읍니다'는 옛 표기이고 지금은 '-습니다'로 씁니다."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "proof", "genre": "자기소개서", "style": "격식체", "sentences": "s1. 저는 대학 시절 학생회 기획부에서 3년 동안 활동하며 행사를 준비했습니다.\ns2. 처음에는 50명 규모의 작은 행사였지만, 몇일 만에 참가 신청이 200명을 넘으면서 계획을 금새 바꿔야 했습니다.\ns3. 그때 저는 팀원들과 역활을 다시 나누고, 매일 아침 진행 상황을 공유하는것을 제안했습니다.\ns4. 덕분에 행사는 무사히 끝났고, 설문에서 만족도 92%라는 결과를 얻었습니다.\ns5. 이 경험을 통해 일정이 바뀌어도 침착하게 대응할수 있다는 자신감을 얻었습니다.\ns6. 입사 후에도 맡은 일을 끝까지 책임지는 사람이 되겠습니다.\ns7. 꼭 함께 일하고 싶읍니다.", "facts": "글자 수: 공백 포함 290자, 공백 제외 224자, 7문장.\n제한: 500자 (공백 포함) - 지금 290, 210 남음.\n[확실] s2 \"몇일\" -> \"며칠\": '며칠'이 표준어입니다. '몇 일'로 쓰지 않습니다.\n[확실] s2 \"금새\" -> \"금세\": '지금 바로'라는 뜻은 '금시에'가 줄어든 '금세'입니다.\n[확실] s3 \"역활\" -> \"역할\": '역할(役割)'이 맞습니다.\n[확실] s3 \"는것\" -> \"는 것\": '것'은 의존 명사라서 앞말과 띄어 씁니다.\n[확실] s5 \"할수 있\" -> \"할 수 있\": '수'는 의존 명사라서 앞말과 띄어 씁니다.\n[확실] s7 \"읍니다\" -> \"습니다\": '-읍니다'는 옛 표기이고 지금은 '-습니다'로 씁니다."}
# - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "proof", genre, style, sentences, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses bareun-geul:<task>:<inputHash>:a<attempt> (the one task is proof), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="bareun-geul:proof:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r '.verdict, .note' reply.json
jq -r '.corrections[]? | "\(.sentence) \(.wrong) -> \(.right) [\(.kind), \(.level)] \(.why)"' reply.json # one per correction
import hashlib, time
key = "bareun-geul:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"))
print(reply.get("note", ""))
for c in reply.get("corrections") or []: # one per correction, in sentence order
print(c["sentence"], c["wrong"], "->", c["right"], c["kind"], c["level"], c["why"])
import { createHash } from "node:crypto";
const key = `bareun-geul:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict);
console.log(reply.note);
for (const c of reply.corrections || []) console.log(c.sentence, c.wrong, "->", c.right, c.kind, c.level, c.why); // one per correction
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "bareun-geul:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["verdict"])
fmt.Println(reply["note"])
corrections, _ := reply["corrections"].([]any) // one per correction, in sentence order
for _, x := range corrections {
c := x.(map[string]any)
fmt.Println(c["sentence"], c["wrong"], "->", c["right"], c["kind"], c["level"], c["why"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "bareun-geul:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.get("verdict"));
System.out.println(reply.path("note").asText());
for (JsonNode c : reply.path("corrections")) // one per correction, in sentence order
System.out.println(c.path("sentence").asText() + " " + c.path("wrong").asText() + " -> " + c.path("right").asText() + " " + c.path("level").asText() + " " + c.path("why").asText());
require "digest"
key = "bareun-geul:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"]].inspect
puts reply["note"]
(reply["corrections"] || []).each { |c| puts "#{c["sentence"]} #{c["wrong"]} -> #{c["right"]} [#{c["kind"]}, #{c["level"]}] #{c["why"]}" }
$key = "bareun-geul:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["verdict"] ?? ""), "\n";
echo $reply["note"] ?? "", "\n";
foreach ($reply["corrections"] ?? [] as $c) echo $c["sentence"], " ", $c["wrong"], " -> ", $c["right"], " ", $c["level"], " ", $c["why"], "\n"; // one per correction
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"bareun-geul:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply!["verdict"]}");
Console.WriteLine($"{reply["note"]}");
foreach (var c in reply["corrections"] as JsonArray ?? new JsonArray()) Console.WriteLine($"{c!["sentence"]} {c["wrong"]} -> {c["right"]} {c["level"]} {c["why"]}"); // one per correction
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: bareun-geul:proof:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "proof", "genre": "자기소개서", "style": "격식체", "sentences": "s1. 저는 대학 시절 학생회 기획부에서 3년 동안 활동하며 행사를 준비했습니다.\ns2. 처음에는 50명 규모의 작은 행사였지만, 몇일 만에 참가 신청이 200명을 넘으면서 계획을 금새 바꿔야 했습니다.\ns3. 그때 저는 팀원들과 역활을 다시 나누고, 매일 아침 진행 상황을 공유하는것을 제안했습니다.\ns4. 덕분에 행사는 무사히 끝났고, 설문에서 만족도 92%라는 결과를 얻었습니다.\ns5. 이 경험을 통해 일정이 바뀌어도 침착하게 대응할수 있다는 자신감을 얻었습니다.\ns6. 입사 후에도 맡은 일을 끝까지 책임지는 사람이 되겠습니다.\ns7. 꼭 함께 일하고 싶읍니다.", "facts": "글자 수: 공백 포함 290자, 공백 제외 224자, 7문장.\n제한: 500자 (공백 포함) - 지금 290, 210 남음.\n[확실] s2 \"몇일\" -> \"며칠\": '며칠'이 표준어입니다. '몇 일'로 쓰지 않습니다.\n[확실] s2 \"금새\" -> \"금세\": '지금 바로'라는 뜻은 '금시에'가 줄어든 '금세'입니다.\n[확실] s3 \"역활\" -> \"역할\": '역할(役割)'이 맞습니다.\n[확실] s3 \"는것\" -> \"는 것\": '것'은 의존 명사라서 앞말과 띄어 씁니다.\n[확실] s5 \"할수 있\" -> \"할 수 있\": '수'는 의존 명사라서 앞말과 띄어 씁니다.\n[확실] s7 \"읍니다\" -> \"습니다\": '-읍니다'는 옛 표기이고 지금은 '-습니다'로 씁니다."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "bareun-geul:proof:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `bareun-geul:proof:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "bareun-geul:proof:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "bareun-geul:proof:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "bareun-geul:proof:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: bareun-geul:proof:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"bareun-geul:proof:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);