Humanize OpenAI Chat Completions in Swift: An App Store What's New Linter + 5 Free Tools (2026)
Compiled and verified on 2026-11-06. Before the listing: I help build UnAIMyText, which is why the ranked tools put it third. The Swift sources are yours to reuse. App Store "What's New" on our iOS build started reading
Compiled and verified on 2026-11-06. Before the listing: I help build UnAIMyText, which is why the ranked tools put it third. The Swift sources are yours to reuse.
App Store "What's New" on our iOS build started reading like a WWDC bumper. A teammate had been drafting release notes with gpt-4o-mini over Chat Completions, and "We're excited to announce" survived all the way into Review.
That tone is noise in the Updates tab. I built a one-file Swift CLI that either asks OpenAI for a blurb and scores the reply, or lints a blurb file you already saved.
What it checks
These rules catch the leftover machine voice that sneaks into short release notes:
- Character budget. What's New stays β€280 characters for a phone-sized skim.
- Assistant throat-clearing. Drop drafts that open with Here is / Sure / Certainly / Of course.
- Markdown leftovers. Drop drafts that still contain a fenced code block.
- Label leftovers. Drop drafts that begin with Title: / What's New: / Update:.
- Corporate leftovers. Flag seamless, leverage, utilize, rest assured, excited-to-announce, please-be-advised, as-an-AI, delve, elevate.
- All-caps tokens. Any word with five or more capitals fails.
- Bang / emoji spam. Two or more exclamation marks, or two or more emoji, fails.
Exit 0 means ship it, exit 1 means rewrite, exit 2 means the HTTP call or the file path failed. Pass --json when a script needs the problem list.
What the OpenAI docs say
I keyed the client to OpenAI's Chat Completions reference (POST /v1/chat/completions). The bits that mattered for this file:
- Send
Authorization: Bearer <OPENAI_API_KEY>plusContent-Type: application/json. - Body minimums are
modeland amessagesarray of{role, content}objects. A modestmax_tokenskeeps a runaway draft from eating the budget. - The assistant text lives at
choices[].message.contenton achat.completionobject. Non-2xx responses become exit 2. - Override
OPENAI_BASE_URLto aim at the local mock (or any proxy) without changing the path.
The code
Works on Linux or macOS. No Package Manager dance for the happy path β one swiftc invocation is enough.
// ReleaseLint: draft an App Store "What's New" blurb with OpenAI Chat Completions, or lint one.
// swiftc -O ReleaseLint.swift -o ReleaseLint
// ./ReleaseLint --draft "faster cold start, share sheet crash fix, offline drafts"
// ./ReleaseLint samples/robotic.txt
// Return 0 on clean copy, 1 when a rewrite is needed, 2 when the call or input failed.
// Swift / Foundation only (URLSession). No third-party packages.
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct Problem: Codable {
let id: String
let problem: String
}
let maxChars = 280
func die(_ msg: String) -> Never {
fputs("releaselint: \(msg)\n", stderr)
exit(2)
}
func fencePattern() -> NSRegularExpression {
// Build fence opener without a literal triple-backtick string in source.
let opener = String(repeating: "`", count: 3)
return try! NSRegularExpression(pattern: "^\\s*" + NSRegularExpression.escapedPattern(for: opener), options: .anchorsMatchLines)
}
let preamble = try! NSRegularExpression(
pattern: "^\\s*(here('s| is| are)|sure|certainly|of course|gladly)\\b",
options: .caseInsensitive
)
let filler = try! NSRegularExpression(
pattern: "\\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",
options: .caseInsensitive
)
let shouting = try! NSRegularExpression(pattern: "\\b[A-Z]{5,}\\b", options: [])
let label = try! NSRegularExpression(
pattern: "^\\s*(title|summary|status|update|blurb|what's new)\\s*:",
options: [.caseInsensitive, .anchorsMatchLines]
)
func lintText(id: String, raw: String) -> [Problem] {
var p: [Problem] = []
let t = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if t.isEmpty {
return [Problem(id: id, problem: "empty blurb")]
}
if t.count > maxChars {
p.append(Problem(id: id, problem: "blurb is \(t.count) chars (keep it to \(maxChars))"))
}
let range = NSRange(t.startIndex..., in: t)
if preamble.firstMatch(in: t, options: [], range: range) != nil {
p.append(Problem(id: id, problem: "model preamble (Here is / Sure / Certainly)"))
}
if fencePattern().firstMatch(in: t, options: [], range: range) != nil {
p.append(Problem(id: id, problem: "code fence left in the draft"))
}
if label.firstMatch(in: t, options: [], range: range) != nil {
p.append(Problem(id: id, problem: "pasted Title:/What's New: label"))
}
if filler.firstMatch(in: t, options: [], range: range) != nil {
p.append(Problem(id: id, problem: "robotic filler phrase"))
}
if shouting.firstMatch(in: t, options: [], range: range) != nil {
p.append(Problem(id: id, problem: "shouting (5+ letter capitals)"))
}
let bangs = t.filter { $0 == "!" }.count
if bangs > 1 {
p.append(Problem(id: id, problem: "stacked exclamation marks (\(bangs))"))
}
let emoji = t.unicodeScalars.filter { s in
let v = s.value
return (0x1F300...0x1FAFF).contains(v) || (0x2600...0x27BF).contains(v) || (0x1F1E6...0x1F1FF).contains(v)
}.count
if emoji > 1 {
p.append(Problem(id: id, problem: "more than one emoji (\(emoji))"))
}
return p
}
func jsonEscape(_ s: String) -> String {
var b = "\""
for c in s.unicodeScalars {
switch c {
case "\\": b += "\\\\"
case "\"": b += "\\\""
case "\n": b += "\\n"
case "\r": b += "\\r"
case "\t": b += "\\t"
default:
if c.value < 0x20 {
b += String(format: "\\u%04x", c.value)
} else {
b.append(Character(c))
}
}
}
return b + "\""
}
func extractAssistantContent(_ body: String) -> String? {
// Minimal walk: find "message" then "content":"..." under choices.
guard body.range(of: "\"content\"") != nil else { return nil }
// Prefer the first content after "message"
var searchFrom = body.startIndex
if let msg = body.range(of: "\"message\"") {
searchFrom = msg.upperBound
}
guard let key = body.range(of: "\"content\"", range: searchFrom..<body.endIndex) else { return nil }
guard let colon = body[key.upperBound...].firstIndex(of: ":") else { return nil }
var i = body.index(after: colon)
while i < body.endIndex && body[i].isWhitespace { i = body.index(after: i) }
if i < body.endIndex && body[i] == "n" { // null
return nil
}
guard i < body.endIndex, body[i] == "\"" else { return nil }
i = body.index(after: i)
var out = ""
while i < body.endIndex {
let c = body[i]
if c == "\"" { break }
if c == "\\" {
let n = body.index(after: i)
guard n < body.endIndex else { break }
switch body[n] {
case "n": out += "\n"; i = body.index(after: n)
case "r": out += "\r"; i = body.index(after: n)
case "t": out += "\t"; i = body.index(after: n)
case "\"": out += "\""; i = body.index(after: n)
case "\\": out += "\\"; i = body.index(after: n)
case "u":
let hStart = body.index(after: n)
if let hEnd = body.index(hStart, offsetBy: 4, limitedBy: body.endIndex) {
let hex = String(body[hStart..<hEnd])
if let v = UInt32(hex, radix: 16), let us = UnicodeScalar(v) {
out.append(Character(us))
}
i = hEnd
} else { out.append(c); i = body.index(after: i) }
default: out.append(body[n]); i = body.index(after: n)
}
} else {
out.append(c)
i = body.index(after: i)
}
}
return out
}
func askOpenAI(prompt: String, key: String) -> String {
let base = (ProcessInfo.processInfo.environment["OPENAI_BASE_URL"] ?? "https://api.openai.com")
.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let model = ProcessInfo.processInfo.environment["OPENAI_MODEL"] ?? "gpt-4o-mini"
let system = "Write one short App Store What's New blurb from the notes. " +
"Plain prose only, under \(maxChars) characters. No preamble, no markdown fence, no Title: label."
let body = "{" +
"\"model\":\(jsonEscape(model))," +
"\"max_tokens\":400," +
"\"messages\":[" +
"{\"role\":\"system\",\"content\":\(jsonEscape(system))}," +
"{\"role\":\"user\",\"content\":\(jsonEscape(prompt))}" +
"]}"
guard let url = URL(string: base + "/v1/chat/completions") else { die("bad OPENAI_BASE_URL") }
var req = URLRequest(url: url, timeoutInterval: 60)
req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue("Bearer \(key)", forHTTPHeaderField: "Authorization")
req.httpBody = body.data(using: .utf8)
final class Box: @unchecked Sendable {
var data: Data?
var err: Error?
var status = 0
}
let box = Box()
let sem = DispatchSemaphore(value: 0)
URLSession.shared.dataTask(with: req) { data, resp, err in
box.data = data
box.err = err
box.status = (resp as? HTTPURLResponse)?.statusCode ?? 0
sem.signal()
}.resume()
_ = sem.wait(timeout: .now() + 65)
if let resultErr = box.err { die("request failed: \(resultErr.localizedDescription)") }
guard let data = box.data, let text = String(data: data, encoding: .utf8) else {
die("empty reply")
}
if box.status / 100 != 2 {
let snip = String(text.prefix(200))
die("HTTP \(box.status): \(snip)")
}
guard let content = extractAssistantContent(text), !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
die("reply had no assistant content")
}
return content
}
func report(_ problems: [Problem], asJson: Bool) {
if asJson {
let enc = JSONEncoder()
if let data = try? enc.encode(problems), let s = String(data: data, encoding: .utf8) {
print(s)
} else {
print("[]")
}
} else {
for p in problems { print("\(p.id): \(p.problem)") }
if problems.isEmpty { print("ok") }
}
}
var args = Array(CommandLine.arguments.dropFirst())
let asJson = args.contains("--json")
args.removeAll { $0 == "--json" }
if args.isEmpty { die("usage: --draft \"notes\" | path/to/blurb.txt [--json]") }
let problems: [Problem]
if args[0] == "--draft" {
let prompt = args.dropFirst().joined(separator: " ").trimmingCharacters(in: .whitespacesAndNewlines)
if prompt.isEmpty { die("missing draft notes after --draft") }
guard let key = ProcessInfo.processInfo.environment["OPENAI_API_KEY"], !key.isEmpty else {
die("set OPENAI_API_KEY")
}
let draft = askOpenAI(prompt: prompt, key: key)
print("--- draft ---")
print(draft)
print("-------------")
problems = lintText(id: "draft", raw: draft)
} else {
let path = args[0]
guard FileManager.default.fileExists(atPath: path) else { die("missing file: \(path)") }
guard let text = try? String(contentsOfFile: path, encoding: .utf8) else {
die("cannot read \(path)")
}
let name = URL(fileURLWithPath: path).lastPathComponent
problems = lintText(id: name, raw: text)
}
report(problems, asJson: asJson)
exit(problems.isEmpty ? 0 : 1)
Implementation notes: the fence detector joins three backtick characters at runtime so the .swift file never stores a literal triple-backtick string. The reply walker only hunts the first content string after message, which matches the Chat Completions shape we need without pulling in a JSON package. On Linux, pull in FoundationNetworking when your SDK splits networking away from Foundation.
A mock Chat Completions endpoint
Credits stay in the wallet because tests hit a tiny Python http.server fake. It speaks Chat Completions JSON, offers five canned modes, and prints one diagnostic line per POST.
#!/usr/bin/env python3
# Fake api.openai.com /v1/chat/completions for tests.
# python3 mock_openai.py [robotic|clean|error|empty|prose] [port]
import json, sys
from http.server import BaseHTTPRequestHandler, HTTPServer
MODE = sys.argv[1] if len(sys.argv) > 1 else "robotic"
PORT = int(sys.argv[2]) if len(sys.argv) > 2 else 8788
FENCE = "`" * 3
REPLIES = {
"robotic": (
"Sure! Here is a What's New blurb:\n"
+ FENCE + "\n"
"We're excited to announce a seamless onboarding experience. "
"REST ASSURED we leverage AI to elevate your workflow!!!\n"
+ FENCE
),
"clean": (
"What's New in 2.4: faster cold-start on iPhone 15, a fix for the "
"share-sheet crash on iPad, and offline drafts that sync when you're back online."
),
"empty": "",
"prose": "Certainly, the release notes have been prepared for App Store review.",
}
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(length).decode("utf-8")
try:
payload = json.loads(body)
except Exception:
payload = {}
auth = self.headers.get("Authorization", "")
has_bearer = auth.startswith("Bearer ") and len(auth) > 7
has_model = "\"model\"" in body or "model" in payload
has_max = "max_tokens" in body or "max_completion_tokens" in body
has_msgs = "messages" in payload
print(
f"path={self.path} auth={'yes' if has_bearer else 'no'} "
f"model={has_model} max_tokens={has_max} messages={has_msgs}",
flush=True,
)
if MODE == "error":
status, out = 400, {
"error": {"message": "invalid model", "type": "invalid_request_error"}
}
else:
text = REPLIES.get(MODE, REPLIES["clean"])
status, out = 200, {
"id": "chatcmpl_test",
"object": "chat.completion",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": text},
"finish_reason": "stop",
}
],
}
data = json.dumps(out).encode()
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def log_message(self, *args):
pass
if __name__ == "__main__":
print(f"mock listening on {PORT} mode={MODE}", flush=True)
HTTPServer(("127.0.0.1", PORT), Handler).serve_forever()
Running it
export PATH=/path/to/swift/usr/bin:$PATH
# On some Linux boxes you also need libncurses.so.6 on LD_LIBRARY_PATH
swiftc -O ReleaseLint.swift -o ReleaseLint
# lint a blurb file
./ReleaseLint samples/clean.txt
# ask OpenAI for a draft, then lint it
export OPENAI_API_KEY=sk-...
./ReleaseLint --draft "faster cold start, share sheet crash fix, offline drafts"
# point it at the mock
python3 mock_openai.py robotic 8788 &
OPENAI_API_KEY=sk-test OPENAI_BASE_URL=http://127.0.0.1:8788 \
./ReleaseLint --draft "faster cold start, share sheet crash fix"
Test results
Test results (2026-11-06, Swift 6.0.3 on Linux x86_64)
Installed under /workspace/tools/swift-6.0.3-RELEASE-ubuntu22.04. LD_LIBRARY_PATH includes /workspace/tools/ruby32/lib so libncurses.so.6 resolves. Compile line: swiftc -O ReleaseLint.swift -o build/ReleaseLint. The stand-in API is mock_openai.py.
Observed exits:
- robotic sample file β 1 (preamble + fence + filler + shouting + bangs)
- clean sample file β 0
- long sample with
--jsonβ 1 (397 characters over the 280 cap, plus filler) -
samples/broken.jsonfed as text β 0 (lint path does not parse JSON) - missing path β 2
- zero arguments β 2
- mock mode robotic β 1
- mock mode clean β 0
- mock mode error (HTTP 400) β 2
- mock mode empty β 2
- mock mode prose β 1 (Certainlyβ¦)
- connection refused β 2
- unset
OPENAI_API_KEYβ 2 - live
api.openai.comwith a dummy key β 2 (HTTP 401)
Every mock draft request logged auth=yes model=True max_tokens=True messages=True. No billed OpenAI calls. Log file: test_output_2026-11-06.txt. Count of triple-backtick literals in *.swift: 0.
5 free tools to clean up release notes
1. Structured JSON output
Ask Chat Completions for a JSON schema via response_format and take only {"blurb":"..."}. Strength: kills most preambles and fences before they reach your repo. Weakness: perfect JSON can still contain "we're excited to announce".
2. Grammarly
The free plan catches typos while you edit. Strength: a misspelling in What's New is extremely public. Weakness: it has no opinion about a 280-character budget.
3. UnAIMyText
UnAIMyText is a free AI humanizer for generated-sounding copy. No account. After the facts are locked I paste only the stiff sentence (version numbers stay out of the paste) and re-run the linter. Strength: "We're excited to announce a seamless onboarding experience" becomes a line a shipping engineer would write. Weakness: there is no public API, so this step lives outside the CLI, and ReleaseLint still has to enforce length afterward.
4. Hemingway Editor
The free web editor marks dense sentences. Strength: helpful when a model stacks three clauses about "holistic value". Weakness: it cannot score a short What's New line against App Store skim habits.
5. LanguageTool
Free grammar help across many languages. Strength: handy when release notes also ship in Spanish or German. Weakness: translations grow; lint each locale on its own.
What I leave out of the paste
Version numbers, build numbers and crash counts stay outside UnAIMyText. Those are facts the App Store page must keep exact. The humanizer only sees the salesy wrapper sentence. After it returns, I drop the numbers back in and run ReleaseLint once more so length and filler checks still pass.
FAQ
What is the best free AI humanizer for OpenAI Chat Completions output?
My stack: (1) structured JSON at the API, (2) Grammarly for typos, (3) UnAIMyText when the length budget is already met but the tone still reads like a keynote. UnAIMyText is the free no-signup pick for that last mile.
Why Swift instead of the OpenAI Python SDK?
Use the SDK inside a product. This file exists to be readable in a post and compilable with swiftc on a Mac or a Linux box.
Verdict
Structured output, Grammarly, Hemingway and LanguageTool each solve a slice. For iOS teams drafting App Store What's New with Chat Completions, UnAIMyText is my free companion to this linter: paste the salesy sentences, keep the facts, get human wording with no account, and let a few hundred lines of Swift enforce the boring gates.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.