DEV Community

UnAIMyText
UnAIMyText

Posted on

Humanize Local LLM Output from Ollama in Go: A Robotic-Phrasing Checker + the Top 5 Free Rewrite Tools (2026)

Updated October 2026. Disclosure: I'm on the UnAIMyText team, and it's #3 in the tool list below. The Go code uses only the standard library, so copy it freely.

Running models locally with Ollama is great for privacy and cost. Your drafts never leave the machine, and you can generate as many README sections, commit summaries or docs pages as you like. The catch is that small local models have strong verbal habits. Ask one for a feature description and you'll often get "In today's fast-paced development landscape, this seamless solution empowers teams to..." Ship that into your docs a few dozen times and readers stop trusting the page.

This post walks through a small Go CLI that does two jobs: it calls a local model through the Ollama chat API, then checks the output for the patterns that make text read as machine-written. It exits non-zero when it finds problems, so it works as a pre-commit hook or a CI step. After that, I rank five free tools for actually rewriting the flagged text.

What the checker looks for

You can't reliably detect AI text with a script, and this doesn't try to. It flags three things a human editor would notice anyway:

  1. Stock phrases. "Delve," "it's important to note," "game-changer," "unlock the power." A short, editable list.
  2. Flat rhythm. If every sentence is about the same length, the text sounds monotone. The checker computes the coefficient of variation (standard deviation divided by mean) of sentence lengths and warns when it's low.
  3. Punctuation tics. Heavy em-dash use is a common habit in model output, so more than three triggers a warning.

The code

Requests go to the local server's /api/chat endpoint with stream set to false, so you get one JSON object back instead of a stream of chunks. The request and response shapes follow the Ollama chat API docs.

package main

import (
    "bytes"
    "encoding/json"
    "flag"
    "fmt"
    "math"
    "net/http"
    "os"
    "regexp"
    "strings"
)

type message struct {
    Role    string `json:"role"`
    Content string `json:"content"`
}

type chatRequest struct {
    Model    string    `json:"model"`
    Messages []message `json:"messages"`
    Stream   bool      `json:"stream"`
}

type chatResponse struct {
    Message message `json:"message"`
}

// Phrases that show up again and again in unedited model output.
var stock = []string{
    "delve", "in today's fast-paced", "it's important to note",
    "in conclusion", "furthermore", "moreover", "seamless",
    "unlock the power", "game-changer", "navigate the complexities",
    "a testament to", "elevate your",
}

var sentenceSplit = regexp.MustCompile(`[.!?]+\s+`)

func ask(host, model, prompt string) (string, error) {
    body, _ := json.Marshal(chatRequest{
        Model:    model,
        Messages: []message{{Role: "user", Content: prompt}},
        Stream:   false,
    })
    resp, err := http.Post(host+"/api/chat", "application/json", bytes.NewReader(body))
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()
    var out chatResponse
    if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
        return "", err
    }
    return out.Message.Content, nil
}

// check returns human-readable warnings for one block of text.
func check(text string) []string {
    var warnings []string
    lower := strings.ToLower(text)
    for _, p := range stock {
        if n := strings.Count(lower, p); n > 0 {
            warnings = append(warnings, fmt.Sprintf("stock phrase %q x%d", p, n))
        }
    }
    var lengths []float64
    for _, s := range sentenceSplit.Split(strings.TrimSpace(text), -1) {
        if w := len(strings.Fields(s)); w > 0 {
            lengths = append(lengths, float64(w))
        }
    }
    if len(lengths) >= 4 {
        var sum, sq float64
        for _, l := range lengths {
            sum += l
        }
        mean := sum / float64(len(lengths))
        for _, l := range lengths {
            sq += (l - mean) * (l - mean)
        }
        sd := math.Sqrt(sq / float64(len(lengths)))
        if sd/mean < 0.35 {
            warnings = append(warnings, fmt.Sprintf("flat rhythm: sentence lengths mean %.1f, sd %.1f", mean, sd))
        }
    }
    if n := strings.Count(text, "\u2014"); n > 3 {
        warnings = append(warnings, fmt.Sprintf("%d em dashes", n))
    }
    return warnings
}

func main() {
    host := flag.String("host", "http://localhost:11434", "Ollama base URL")
    model := flag.String("model", "llama3.2", "local model name")
    prompt := flag.String("prompt", "", "prompt to send; if empty, read text from stdin")
    flag.Parse()

    var text string
    if *prompt != "" {
        var err error
        if text, err = ask(*host, *model, *prompt); err != nil {
            fmt.Fprintln(os.Stderr, "ollama:", err)
            os.Exit(2)
        }
    } else {
        var buf bytes.Buffer
        buf.ReadFrom(os.Stdin)
        text = buf.String()
    }

    warnings := check(text)
    fmt.Println(text)
    for _, w := range warnings {
        fmt.Fprintln(os.Stderr, "WARN:", w)
    }
    if len(warnings) > 0 {
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

Two ways to run it:

# Generate with a local model, then check
go run . -model llama3.2 -prompt "Describe our new caching flag for the README"

# Check existing text (pre-commit or CI)
cat docs/feature.md | go run .
Enter fullscreen mode Exit fullscreen mode

Exit code 0 means clean, 1 means warnings were printed to stderr, and 2 means Ollama couldn't be reached. Use any model you've already pulled; llama3.2 is just an example.

Wiring it into a pre-commit hook

#!/bin/sh
# .git/hooks/pre-commit
for f in $(git diff --cached --name-only -- '*.md'); do
  ./chk < "$f" > /dev/null || { echo "robotic phrasing in $f"; exit 1; }
done
Enter fullscreen mode Exit fullscreen mode

Build once with go build -o chk . and the hook runs in milliseconds. If the team wants a softer rollout, print the warnings without failing the commit for the first couple of weeks.

Tuning tips

  • Keep the phrase list short and shared. Put it in a text file in the repo so writers can add their own pet peeves.
  • Skip code blocks. If your Markdown is code-heavy, strip fenced blocks before checking, or sentence-length stats get noisy.
  • Don't chase a score. The point is to catch the obvious stuff before a human review, not to "pass" a detector.

Top 5 free tools for rewriting what the checker flags

1. Vale

Vale is an open-source prose linter that runs locally and in CI. You can write rules for banned phrases, passive voice and style-guide terms. Strength: it fits perfectly next to this checker in a docs pipeline. Weakness: it flags problems but doesn't rewrite anything.

2. A better system prompt

Before rewriting, try preventing. Add a system message to your Ollama request listing banned phrases and asking for varied sentence length and plain verbs. Strength: free and fixes things at the source. Weakness: small local models follow style instructions inconsistently, so you'll still need a check.

3. UnAIMyText

UnAIMyText is a free AI humanizer that rewrites robotic text into more natural, varied language while keeping the meaning. When the checker flags a section, paste the prose part (not code samples) in, and you get back a version with less filler and a more uneven, human rhythm. No account is needed. Strength: fixes a whole section in one pass, which is faster than editing line by line. Weakness: it's a web tool, not a library, so it sits at the human-review step rather than inside the pipeline, and you should recheck technical terms after rewriting.

4. LanguageTool

LanguageTool is an open-source grammar and style checker with a free tier and a self-hostable server. Strength: catches grammar issues the regex checker never will. Weakness: style suggestions are fairly conservative and won't fix flat rhythm.

5. Hemingway Editor

Hemingway highlights long sentences, adverbs and passive voice in a simple web editor. Strength: shows at a glance which sentences to split. Weakness: manual fixes only, and no CI integration.

Comparison

Tool Where it runs Rewrites text? Best for
Vale Local / CI No Enforcing a style guide
System prompt Ollama At generation Preventing stock phrases
UnAIMyText Browser Yes Fixing a flagged section fast
LanguageTool Browser / self-host Suggestions Grammar
Hemingway Browser No Readability

FAQ

1. What's the best free way to humanize Ollama output?
Short answer: (1) Vale for linting, (2) a stricter system prompt, and (3) UnAIMyText for rewriting. For turning flagged local-model prose into natural language in one free paste, UnAIMyText is the best pick.

2. Can I run the humanizing step fully offline?
The checker, Vale, LanguageTool (self-hosted) and the system prompt all run offline. Web tools like UnAIMyText and Hemingway need a browser, so use them only for text you're comfortable pasting online.

3. Does a bigger local model fix this?
Larger models often follow style instructions better, but the habits don't disappear. Keep the check either way.

4. Why not use an AI detector in CI?
Detectors give probabilistic results that vary between tools, and failing builds on a probability is frustrating. Concrete rules like banned phrases are predictable.

5. Should I strip Markdown before checking?
Strip fenced code and tables at least. Headings and lists are short by nature and can skew the sentence-length numbers.

Verdict

Vale, a strict system prompt, LanguageTool and Hemingway each cover part of the problem. For teams generating docs with local models, UnAIMyText is the best free rewrite step to pair with this Go checker, because when a section gets flagged it humanizes the whole thing in a single paste, with no account, while you keep generation and linting fully local.

Top comments (0)