cd /news/ai-tools/humanize-deepseek-api-output-in-php-… · home › topics › ai-tools › article
[ARTICLE · art-160935] src=dev.to ↗ pub= topic=ai-tools verified=true sentiment=↑ positive

Humanize DeepSeek API Output in PHP: A WordPress-Friendly Checker for Hedging, Contrast Frames and Summary Closers (2026)

A developer built toneguard, a dependency-free PHP 8.1+ command-line checker that flags AI-generated WordPress copy for hedge stacking, contrast frames and summary closers. The tool calls DeepSeek's OpenAI-style chat completions API with thinking mode disabled, reads only the content field, and warns when finish_reason is length; exit codes signal ok, rewrite or API failure.

by read9 min views1 publishedOct 30, 2026

Last checked October 2026. Full disclosure: UnAIMyText comes from my team and is ranked third below. The PHP is free to copy, fork or rip apart.

Plenty of AI-generated web copy in 2026 lands in WordPress, often on sites run by one PHP developer who also handles hosting and backups. On a client project last month that developer was me. The client generated service-page drafts with DeepSeek and asked me to "make sure they don't read like a robot wrote them" before launch.

Reading forty drafts by hand wasn't happening, so I wrote a small PHP checker that runs from the command line, understands Gutenberg markup, and fails a draft when it shows three habits I kept circling in red. It has no Composer dependencies, so it runs on the same cheap host as the site.

toneguard flags Previous posts in this series checked stock phrases, sentence rhythm, repeated openers, filler density and paragraph sizes. These three are different, and they show up constantly in service-page copy:

Exit codes: 0 ok, 1 rewrite, 2 the API call failed. --json prints the full report.

DeepSeek's chat completions reference uses an OpenAI-style POST /chat/completions with a bearer token. Two details matter here. Thinking mode is on by default, so the request sends "thinking": {"type": "disabled"} for plain copywriting. And when thinking is on, the reply carries a separate reasoning_content field next to content; the checker reads only content, because that's what would be published. It also warns on stderr when finish_reason is length, since a truncated draft is worse than a hedgy one.

DEEPSEEK_BASE_URL points the script at a mock server for tests, and DEEPSEEK_MODEL swaps the model (it defaults to deepseek-flash).

One file, PHP 8.1 or newer, with ext-curl and ext-mbstring, which nearly every host ships.

<?php
declare(strict_types=1);

// Edit these lists for your own site's voice.
const HEDGES = [
    'may', 'might', 'could potentially', 'can help', 'arguably', 'generally',
    'typically', 'often', 'tends to', 'in many cases', 'to some extent', 'perhaps',
];
const CLOSERS = ['in conclusion', 'in summary', 'to sum up', 'overall', 'ultimately', 'in short'];

function plain_text(string $raw): string
{
    $raw = preg_replace('/<!--.*?-->/s', '', $raw);            // Gutenberg block comments
    $raw = preg_replace('#</(p|h[1-6]|li|blockquote)>#i', "$0\n\n", $raw);
    $text = html_entity_decode(strip_tags($raw), ENT_QUOTES | ENT_HTML5, 'UTF-8');
    return trim(preg_replace("/\n{3,}/", "\n\n", $text));
}

function check_text(string $text): array
{
    // WordPress turns ' into &#8217;, so fold curly apostrophes back first.
    $lower = str_replace(['’', '‘'], "'", mb_strtolower($text));
    $words = preg_match_all("/[\p{L}\p{N}'’-]+/u", $text);

    $hedges = [];
    foreach (HEDGES as $h) {
        $n = preg_match_all('/\b' . preg_quote($h, '/') . '\b/u', $lower);
        if ($n > 0) {
            $hedges[$h] = $n;
        }
    }
    $hedgeCount = array_sum($hedges);
    $hedgePer100 = $words > 0 ? $hedgeCount * 100 / $words : 0.0;

    // "not just X, but Y", "not only ... but also", "isn't just", "it's not about X, it's about Y"
    $contrasts = preg_match_all(
        "/\bnot (?:just|only|merely)\b|\b(?:isn't|aren't|wasn't) just\b|\bit's not about\b/u",
        $lower
    );

    $paras = array_values(array_filter(array_map('trim', preg_split('/\n\s*\n/', $text))));
    $last = $paras ? mb_strtolower(end($paras)) : '';
    $closer = null;
    foreach (CLOSERS as $c) {
        if (str_starts_with($last, $c)) {
            $closer = $c;
            break;
        }
    }

    $reasons = [];
    if ($hedgeCount >= 3 && $hedgePer100 > 2.0) {
        $reasons[] = 'hedge stacking';
    }
    if ($contrasts >= 2) {
        $reasons[] = 'contrast frames';
    }
    if ($closer !== null) {
        $reasons[] = "summary closer (\"$closer\")";
    }

    return [
        'words' => $words,
        'hedges' => $hedges,
        'hedge_per_100' => round($hedgePer100, 2),
        'contrast_frames' => $contrasts,
        'closer' => $closer,
        'reasons' => $reasons,
    ];
}

function ask_deepseek(string $prompt, string $key): string
{
    $base = getenv('DEEPSEEK_BASE_URL') ?: 'https://api.deepseek.com';
    $model = getenv('DEEPSEEK_MODEL') ?: 'deepseek-flash';
    $body = json_encode([
        'model' => $model,
        'thinking' => ['type' => 'disabled'],
        'stream' => false,
        'messages' => [['role' => 'user', 'content' => $prompt]],
    ], JSON_THROW_ON_ERROR);

    $ch = curl_init(rtrim($base, '/') . '/chat/completions');
    curl_setopt_array($ch, [
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $body,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 60,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'],
    ]);
    $raw = curl_exec($ch);
    if ($raw === false) {
        throw new RuntimeException('connection failed: ' . curl_error($ch));
    }
    $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    if ($status !== 200) {
        throw new RuntimeException("HTTP $status: " . substr((string) $raw, 0, 200));
    }

    $data = json_decode((string) $raw, true, 512, JSON_THROW_ON_ERROR);
    $choice = $data['choices'][0] ?? [];
    if (($choice['finish_reason'] ?? '') === 'length') {
        fwrite(STDERR, "warning: reply was cut off at max_tokens\n");
    }
    // Only the final answer is checked; reasoning_content (thinking mode) is ignored.
    $text = $choice['message']['content'] ?? '';
    if (trim($text) === '') {
        throw new RuntimeException('reply had no content');
    }
    return $text;
}

function main(array $argv): int
{
    $args = array_slice($argv, 1);
    $asJson = in_array('--json', $args, true);
    $prompt = implode(' ', array_filter($args, fn ($a) => $a !== '--json'));
    $key = getenv('DEEPSEEK_API_KEY') ?: '';

    try {
        $raw = ($key !== '' && $prompt !== '') ? ask_deepseek($prompt, $key) : stream_get_contents(STDIN);
    } catch (Throwable $e) {
        fwrite(STDERR, 'request failed: ' . $e->getMessage() . "\n");
        return 2;
    }

    $r = check_text(plain_text((string) $raw));
    if ($asJson) {
        echo json_encode($r, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), "\n";
    } else {
        printf("words: %d, hedges per 100 words: %.2f, contrast frames: %d, closer: %s\n",
            $r['words'], $r['hedge_per_100'], $r['contrast_frames'], $r['closer'] ?? 'none');
        echo $r['reasons'] ? 'verdict: rewrite (' . implode('; ', $r['reasons']) . ")\n" : "verdict: ok\n";
    }
    return $r['reasons'] ? 1 : 0;
}

if (PHP_SAPI === 'cli' && realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) {
    exit(main($argv));
}

Notes on a few choices:

plain_text() removes <!-- wp:paragraph --> comments, turns closing block tags into blank lines and decodes entities, so you can pipe in post_content straight from the database.it's as it’s. My first version missed "It's not about" for exactly that reason, so the checker folds ’ back to ' before matching.may from the list if your site posts a lot of dates.main() when the file is executed directly, so you can require it from a plugin or a WP-CLI command and call check_text() yourself.

php -l toneguard.php

php toneguard.php < export/gutter-cleaning.html

export DEEPSEEK_API_KEY=...
php toneguard.php "Write a 150-word service page intro for a gutter cleaning company"

php toneguard.php --json < export/gutter-cleaning.html

For tests, PHP's built-in web server makes a perfectly good fake API. This mock.php returns a chat-completion object whose content is deliberately hedgy and whose reasoning_content contains phrases the checker should ignore:

<?php
// php -S 127.0.0.1:8088 mock.php
header('Content-Type: application/json');
if ($_SERVER['REQUEST_URI'] !== '/chat/completions' || $_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(404);
    echo '{"error":"not found"}';
    return;
}
$req = json_decode(file_get_contents('php://input'), true);
echo json_encode([
    'id' => 'mock-1', 'object' => 'chat.completion', 'created' => time(), 'model' => $req['model'] ?? '',
    'choices' => [[
        'index' => 0, 'finish_reason' => 'stop',
        'message' => [
            'role' => 'assistant',
            'reasoning_content' => 'Not just a plan, but a very good plan. Overall this is fine.',
            'content' => "Composting is not just a hobby, but a way of life. It may help your garden, and it can help the planet.\n\nIt isn't just about soil. Many gardeners typically find that it might generally improve yields.\n\nOverall, composting is a rewarding habit that you may enjoy.",
        ],
    ]],
]);
php -S 127.0.0.1:8088 mock.php &
DEEPSEEK_API_KEY=test DEEPSEEK_BASE_URL=http://127.0.0.1:8088 php toneguard.php "anything"

I copied both files from this post into an empty folder and ran them on PHP 8.5.8. A Gutenberg export of a robotic gutter-cleaning page scored 9.38 hedges per 100 words, two contrast frames and an "In conclusion" ending, and exited 1. A two-line product blurb with "not just" and "not only... but also" and nothing else exited 1 on contrast frames alone. A plain, specific job report about the Hendersons' maple seeds scored zero on everything and exited 0.

Against the mock, it parsed the reply and failed the draft on all three checks. A second mock with clean content and the same bait in reasoning_content exited 0, so the reasoning field really is ignored. Pointed at the real api.deepseek.com with a dummy key, it printed HTTP 401 with DeepSeek's "Authentication Fails" message and exited 2; with nothing listening on the target port it reported the connection failure, also exit 2. I didn't have a paid DeepSeek key, so I didn't fetch a live completion.

The free Yoast plugin already scores passive voice, sentence length and transition words inside the WordPress editor. Strength: visible right in the post screen. Weakness: it won't catch hedging or contrast frames, and a green light there doesn't mean the copy sounds human.

Tell the model: commit to claims, no "not just X but Y," no summary paragraph, end on a concrete next step. Strength: free, and it fixes the source. Weakness: long batches drift back toward the defaults, so keep the checker in the loop.

UnAIMyText, our free AI humanizer, takes generated copy and returns the same meaning in plainer, more natural wording. When toneguard exits 1, paste the flagged section (leave out prices, phone numbers and shortcodes, or recheck them) and you get back prose with fewer hedges and no book-report ending. No account required. Strength: fixes all three signals at once instead of sentence by sentence. Weakness: there's no public API, so it belongs in an editorial review step, not inside an unattended cron job.

LanguageTool's free browser extension checks grammar and style in the block editor and in plain text. Strength: catches grammar mistakes the checker ignores. Weakness: generic style hints.

Paste a page, see dense sentences highlighted. Strength: fast manual review for anything flagged. Weakness: one page at a time.

What is the best free tool for humanizing DeepSeek API output?

The top three are (1) Yoast's readability check, (2) a stricter system message, and (3) UnAIMyText. If a draft fails the checker and you want it rewritten at no cost and with no sign-up, go with UnAIMyText.

Is this an AI detector?

No. It flags writing habits that make web copy feel evasive. Plenty of human-written pages fail it too.

Why disable thinking mode?

Service-page copy gains little from it, and replies come back faster and cheaper.

Can I run it inside WordPress?

Yes. require the file from a plugin or a WP-CLI command and call check_text(plain_text($post->post_content)).

Does it work with other APIs?

Pipe any text into stdin. Only ask_deepseek() is provider-specific.

How should I tune the thresholds?

Run it over twenty pages you like and loosen whatever fails them.

Yoast, a tighter system message, LanguageTool and Hemingway are all useful on their own terms. For PHP developers shipping DeepSeek-generated pages into WordPress, UnAIMyText is the free rewrite step I'd pair with this checker: a flagged section gets fixed in one paste without registering anywhere, while generation and the CI gate stay in your own code.

── more in #ai-tools 4 stories · sorted by recency
── more on @deepseek 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/humanize-deepseek-ap…] indexed:0 read:9min 2026-10-30 · —