ai newsDo., 5. Nov.
EilmeldungSoundCloud and ElevenLabs Turn AI Music Publishing Into a Controlled Release Path

Humanize Anthropic API Output in Kotlin: A Status-Page Blurb Linter + 5 Free Tools (2026)

Shipped and tested November 2026. Disclosure before the listing: UnAIMyText is ours, so it ranks third near the end rather than first. Copy the Kotlin freely. Our status page was starting to sound like a product launch. Someone had been drafting incident blurbs with Claude through the Messages API,

dev.toDonnerstag 15:50 · 05.11.13 Min. Lesezeit
Bild: dev.to

Shipped and tested November 2026. Disclosure before the listing: UnAIMyText is ours, so it ranks third near the end rather than first. Copy the Kotlin freely.

Our status page was starting to sound like a product launch. Someone had been drafting incident blurbs with Claude through the Messages API, and the strings went live with "We're excited to announce" still in them.

On a phone, during an outage, that reads as noise. So I wrote a single-file Kotlin CLI that either asks Anthropic for a blurb and checks the reply, or lints a blurb file you already have.

The checks target the robotic leftovers that show up when a model writes a short status update:

Return codes follow a simple pattern: pass = 0, needs a rewrite = 1, broken call or input = 2. Use --json when CI wants a report.

Anthropic's API overview describes a REST API at https://api.anthropic.com. The Messages reference is the endpoint this script hits. What shaped the code:

Kotlin on the JVM. No Gradle module required for the happy path: compile the two files to fat jars with kotlinc -include-runtime.

// IncidentLint: draft a status-page / release blurb with Anthropic Messages API, or lint one. // kotlinc IncidentLint.kt -include-runtime -d IncidentLint.jar // java -jar IncidentLint.jar --draft "checkout 5xx for 12 min, root cause redis pool" // java -jar IncidentLint.jar samples/robotic.txt // Return 0 on a clean blurb, 1 when a rewrite is needed, 2 when the call or input failed. // Kotlin/JVM; java.net.http + kotlin stdlib only. import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path import java.time.Duration import java.util.regex.Pattern data class Problem ( val id: String, val msg: String ) { fun toJson (): String = """{"id":${jsonQuote(id)},"problem":${jsonQuote(msg)}}""" } fun jsonQuote ( s: String ): String { val b = StringBuilder ( "\"" ) for ( c in s ) { when ( c ) { '\\' -> b. append ( "\\\\" ) '"' -> b. append ( "\\\"" ) '\n' -> b. append ( "\\n" ) '\r' -> b. append ( "\\r" ) '\t' -> b. append ( "\\t" ) else -> if ( c. code < 0x20 ) b. append ( "\\u%04x". format ( c. code )) else b. append ( c ) } } return b. append ( '"' ). toString () } fun die ( msg: String ): Nothing { System. err. println ( "incidentlint: $msg" ) kotlin. system. exitProcess ( 2 ) } val PREAMBLE: Pattern = Pattern. compile ( "^\\s*(here('s| is| are)|sure|certainly|of course|gladly)\\b", Pattern. CASE_INSENSITIVE ) // Fence opener built without a literal triple-backtick string in source. val FENCE: Pattern = Pattern. compile ( "^\\s*" + Pattern. quote ( "`" + "`" + "`" ), Pattern. MULTILINE ) val FILLER: Pattern = Pattern. compile ( "\\b(seamless(ly)?|leverage|utilize|rest assured|we're excited to announce|" + "please be advised|at this time|going forward|in order to|as an ai|" + "delve|elevate|unlock|game[- ]changer|synergy|holistic)\\b", Pattern.

CASE_INSENSITIVE ) val SHOUTING: Pattern = Pattern. compile ( "\\b[A-Z]{5,}\\b" ) val LABEL: Pattern = Pattern. compile ( "^\\s*(title|summary|status|update|blurb)\\s*:", Pattern. CASE_INSENSITIVE ) const val MAX_CHARS = 280 // short status-page / release blurb fun lintText ( id: String, raw: String ): List < Problem > { val p = mutableListOf < Problem >() val t = raw. trim () if ( t. isEmpty ()) { p. add ( Problem ( id, "empty blurb" )) return p } if ( t. length > MAX_CHARS ) p. add ( Problem ( id, "blurb is ${t.length} chars (keep it to $MAX_CHARS)" )) if ( PREAMBLE. matcher ( t ). find ()) p. add ( Problem ( id, "model preamble (Here is / Sure / Certainly)" )) if ( FENCE. matcher ( t ). find ()) p. add ( Problem ( id, "code fence left in the draft" )) if ( LABEL. matcher ( t ). find ()) p. add ( Problem ( id, "pasted Title:/Status: label" )) if ( FILLER. matcher ( t ). find ()) p. add ( Problem ( id, "robotic filler phrase" )) if ( SHOUTING. matcher ( t ). find ()) p. add ( Problem ( id, "shouting (5+ letter capitals)" )) val bangs = t. count { it == '!' } if ( bangs > 1 ) p. add ( Problem ( id, "stacked exclamation marks ($bangs)" )) val emoji = t. codePoints (). filter { cp -> ( cp in 0x1F300.. 0x1FAFF ) || ( cp in 0x2600..

0x27BF ) || ( cp in 0x1F1E6.. 0x1F1FF ) }. count (). toInt () if ( emoji > 1 ) p. add ( Problem ( id, "more than one emoji ($emoji)" )) return p } fun extractTextBlocks ( body: String ): String { // Minimal parse: pull "type":"text" then "text":"..." values, in order. val out = StringBuilder () var i = 0 while ( true ) { val typeIdx = body. indexOf ( "\"type\"", i ) if ( typeIdx < 0 ) break val afterType = body. indexOf ( ':', typeIdx ) val q1 = body. indexOf ( '"', afterType + 1 ) val q2 = body. indexOf ( '"', q1 + 1 ) if ( q1 < 0 || q2 < 0 ) break val typ = body. substring ( q1 + 1, q2 ) if ( typ == "text" ) { val textKey = body. indexOf ( "\"text\"", q2 ) if ( textKey < 0 ) { i = q2 + 1; continue } val colon = body. indexOf ( ':', textKey ) val start = body. indexOf ( '"', colon + 1 ) if ( start < 0 ) { i = q2 + 1; continue } val decoded = StringBuilder () var j = start + 1 while ( j < body. length ) { val c = body [ j ] if ( c == '"' ) break if ( c == '\\' && j + 1 < body. length ) { when ( body [ j + 1 ]) { 'n' -> { decoded. append ( '\n' ); j += 2 } 'r' -> { decoded. append ( '\r' ); j += 2 } 't' -> { decoded. append ( '\t' ); j += 2 } '"' -> { decoded. append ( '"' ); j += 2 } '\\' -> { decoded. append ( '\\' ); j += 2 } 'u' -> { if ( j + 5 < body. length ) { val hex = body. substring ( j + 2, j + 6 ) decoded. append ( hex. toInt ( 16 ). toChar ()) j += 6 } else { decoded. append ( c ); j ++ } } else -> { decoded. append ( body [ j + 1 ]); j += 2 } } } else { decoded. append ( c ); j ++ } } if ( out. isNotEmpty ()) out. append ( '\n' ) out. append ( decoded ) i = j + 1 } else { i = q2 + 1 } } return out. toString () } fun askAnthropic ( prompt: String, key: String ): String { val base = System. getenv ( "ANTHROPIC_BASE_URL" )?: "https://api.anthropic.com" val model = System. getenv ( "ANTHROPIC_MODEL" )?: "claude-haiku-4-5-20251001" val version = System. getenv ( "ANTHROPIC_VERSION" )?: "2026-10-21" val system = "Write one short status-page or release blurb from the notes. " + "Plain prose only, under $MAX_CHARS characters.

No preamble, no markdown fence, no Title: label." val body = """{"model":${jsonQuote(model)},"max_tokens":400,"system":${jsonQuote(system)},"messages":[{"role":"user","content":${jsonQuote(prompt)}}]}""" val builder = HttpRequest. newBuilder ( URI. create ( base. trimEnd ( '/' ) + "/v1/messages" )). timeout ( Duration. ofSeconds ( 60 )). header ( "content-type", "application/json" ). header ( "anthropic-version", version ). header ( "x-api-key", key ). POST ( HttpRequest. BodyPublishers. ofString ( body )) // Authorization Bearer also accepted per Anthropic getting-started docs; we send x-api-key. val client = HttpClient. newBuilder (). connectTimeout ( Duration. ofSeconds ( 10 )). build () val res = try { client. send ( builder. build (), HttpResponse.

BodyHandlers. ofString ( StandardCharsets. UTF_8 )) } catch ( e: Exception ) { die ( "request failed: ${e.message}" ) } if ( res. statusCode () / 100!= 2 ) { val snip = res. body (). take ( 200 ) die ( "HTTP ${res.statusCode()}: $snip" ) } val text = extractTextBlocks ( res. body ()) if ( text. isBlank ()) die ( "reply had no text content block" ) return text } fun report ( problems: List < Problem >, asJson: Boolean ) { if ( asJson ) { println ( "[" + problems. joinToString ( "," ) { it. toJson () } + "]" ) } else { for ( p in problems ) println ( "${p.id}: ${p.msg}" ) if ( problems. isEmpty ()) println ( "ok" ) } } fun main ( args: Array < String >) { val rest = args. toMutableList () val asJson = rest. remove ( "--json" ) if ( rest. isEmpty ()) die ( "usage: --draft \"notes\" | path/to/blurb.txt [--json]" ) val problems: List < Problem > if ( rest [ 0 ] == "--draft" ) { val prompt = rest. drop ( 1 ). joinToString ( " " ). trim () if ( prompt. isEmpty ()) die ( "missing draft notes after --draft" ) val key = System. getenv ( "ANTHROPIC_API_KEY" )?: "" if ( key. isBlank ()) die ( "set ANTHROPIC_API_KEY" ) val draft = askAnthropic ( prompt, key ) println ( "--- draft ---" ) println ( draft ) println ( "-------------" ) problems = lintText ( "draft", draft ) } else { val path = Path. of ( rest [ 0 ]) if (! Files. exists ( path )) die ( "missing file: $path" ) val text = try { Files. readString ( path ) } catch ( e: Exception ) { die ( "cannot read $path: ${e.message}" ) } problems = lintText ( path. fileName. toString (), text ) } report ( problems, asJson ) when { problems. isEmpty () -> kotlin. system. exitProcess ( 0 ) else -> kotlin. system. exitProcess ( 1 ) } } Enter fullscreen mode Exit fullscreen mode Notes: the fence detector builds its pattern from three backtick characters joined at runtime, so the source never contains a literal triple-backtick string.

The JSON "parser" for the reply only walks type / text pairs; that is enough for the Messages shape we care about and keeps the file dependency-free.

To test without spending credits, I wrote a short fake server on com.sun.net.httpserver.HttpServer. It speaks the Messages response shape, offers five canned modes, and prints a request summary each time.

// Fake api.anthropic.com /v1/messages for tests. // kotlinc -include-runtime -d MockAnthropic.jar MockAnthropic.kt // java -jar MockAnthropic.jar [robotic|clean|error|empty|prose] [port] import com.sun.net.httpserver.HttpServer import java.io.OutputStream import java.net.InetSocketAddress import java.nio.charset.StandardCharsets fun main ( args: Array < String >) { val mode = args. getOrElse ( 0 ) { "robotic" } val port = args. getOrElse ( 1 ) { "8788" }. toInt () val fence = "`" + "`" + "`" val replies = mapOf ( "robotic" to "Sure! Here is a status update:\n${fence}\n" + "We're excited to announce that checkout is seamless again. REST ASSURED we leverage redis.\n$fence", "clean" to "Checkout returned 5xx for 12 minutes.

Cause: redis pool exhaustion. Mitigated by raising the pool and failing open to cache.", "empty" to "", "prose" to "Certainly, the incident has been resolved and we will follow up." ) val server = HttpServer. create ( InetSocketAddress ( "127.0.0.1", port ), 0 ) server. createContext ( "/v1/messages" ) { exch -> val body = exch. requestBody. readBytes (). toString ( StandardCharsets. UTF_8 ) val key = exch. requestHeaders. getFirst ( "x-api-key" )?: "" val ver = exch. requestHeaders. getFirst ( "anthropic-version" )?: "" val hasModel = body. contains ( "\"model\"" ) val hasMax = body. contains ( "\"max_tokens\"" ) val hasMsgs = body. contains ( "\"messages\"" ) println ( "path=${exch.requestURI.path} key=${if (key.isNotEmpty()) " yes " else " no "} " + "version=$ver model=$hasModel max_tokens=$hasMax messages=$hasMsgs" ) val ok = mode!= "error" val reply = if ( ok ) { val text = replies [ mode ]?: replies. getValue ( "clean" ) """{"id":"msg_test","type":"message","role":"assistant","model":"mock","content":[{"type":"text","text":${jsonQ(text)}}],"stop_reason":"end_turn"}""" } else { """{"type":"error","error":{"type":"invalid_request_error","message":"invalid model"}}""" } val bytes = reply. toByteArray ( StandardCharsets.

UTF_8 ) exch. responseHeaders. add ( "Content-Type", "application/json" ) exch. sendResponseHeaders ( if ( ok ) 200 else 400, bytes. size. toLong ()) val os: OutputStream = exch. responseBody os. write ( bytes ) os. close () } server. executor = null server. start () println ( "mock listening on $port mode=$mode" ) Thread. currentThread (). join () } fun jsonQ ( s: String ): String { val b = StringBuilder ( "\"" ) for ( c in s ) { when ( c ) { '\\' -> b. append ( "\\\\" ) '"' -> b. append ( "\\\"" ) '\n' -> b. append ( "\\n" ) '\r' -> b. append ( "\\r" ) '\t' -> b. append ( "\\t" ) else -> b. append ( c ) } } return b. append ( '"' ). toString () } Enter fullscreen mode Exit fullscreen mode Running it export JAVA_HOME = /path/to/jdk17 export PATH = " $JAVA_HOME /bin:/path/to/kotlinc/bin: $PATH " kotlinc IncidentLint.kt -include-runtime -d IncidentLint.jar kotlinc MockAnthropic.kt -include-runtime -d MockAnthropic.jar # lint a blurb file java -jar IncidentLint.jar samples/clean.txt # ask Anthropic for a draft, then lint it export ANTHROPIC_API_KEY = sk-ant-... java -jar IncidentLint.jar --draft "checkout 5xx for 12 min, root cause redis pool" # point it at the mock java -jar MockAnthropic.jar robotic 8788 & ANTHROPIC_API_KEY = sk-test ANTHROPIC_BASE_URL = http://127.0.0.1:8788 \ java -jar IncidentLint.jar --draft "checkout 5xx for 12 min" Enter fullscreen mode Exit fullscreen mode Test results Test results (2026-11-05, Kotlin 2.1.10 / JRE 17, no root install) Toolchain: /workspace/tools/kotlin (kotlinc-jvm 2.1.10) + /workspace/tools/jdk17. Built with kotlinc IncidentLint.kt -include-runtime -d build/IncidentLint.jar and the same for MockAnthropic.kt.

Mock log showed key=yes version=2026-10-21 model=true max_tokens=true messages=true on every draft request. No funded Anthropic key was used, so no real model answer was fetched. Full log: test_output_2026-11-05.txt. Triple-backtick literal count in *.kt: 0.

Anthropic's Messages API can constrain output with a JSON schema via output_config.format. Asking for {"blurb":"..."} cuts most preambles and fences at the source. Strength: costs nothing extra at the API layer. Weakness: valid JSON can still hold "we're excited to announce".

Grammarly's free plan flags typos as you type. Strength: a typo on a status page is very public. Weakness: it knows nothing about a 280-character incident summary.

UnAIMyText is a free AI humanizer. Drop in generated-sounding copy and get the same meaning back in wording people actually say. No login. On incidents it helps once the facts are locked: paste the stiff sentence, leave timestamps out, then re-run the linter. Strength: "We're excited to announce checkout is seamless again" becomes a line an on-call engineer would post. Weakness: the humanizer lives outside the CLI as a paste step, and you should re-run IncidentLint afterward because it does not know your length budget.

The free web view flags dense sentences. Strength: useful when a model writes three clauses about mitigation. Weakness: a short status line can still be wrong.

Free grammar and style review across many languages. Strength: works on a status page that also ships in Spanish or German. Weakness: translations run longer, so lint each locale on its own.

What is the best free AI humanizer for Anthropic Messages output? Order I recommend: structured JSON first, Grammarly second, UnAIMyText third. When length is fine but the tone still sounds like a launch email, UnAIMyText is the free rewrite I grab without creating an account.

Why Kotlin instead of the Anthropic Java SDK? The SDK is fine in an app. This file is meant to be readable in a blog post and runnable with kotlinc alone.

Structured output, Grammarly, Hemingway and LanguageTool each cover a slice of the problem. For teams drafting status-page and release blurbs with the Anthropic Messages API, UnAIMyText is the free side tool I'd park beside this linter for the salesy sentences around the facts: paste them, get human wording back without an account, and let a couple of hundred lines of Kotlin enforce the boring checks.

Quelle: dev.to (dev.to). Original öffnen