Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ jobs:
path: _site
retention-days: 7

# Publishes to GitHub Pages. Requires a one-time repo setting:
# Settings -> Pages -> Build and deployment -> Source = "GitHub Actions".
# Until that is set this job (only) fails; `test` above stays green.
# Publishes to GitHub Pages. The `github-pages` environment only permits
# deployments from the default branch, so this job runs on `main` only
# (on feature branches it is skipped, not failed). Requires the one-time
# repo setting: Settings -> Pages -> Source = "GitHub Actions".
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: github-pages
Expand Down
41 changes: 23 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
# Humanizer-tester

[![Try it live](https://img.shields.io/badge/Try%20it%20live-Humanizer%20web%20app-3b82f6?style=for-the-badge)](https://carlosa49.github.io/Humanizer-tester/)
[![Tests](https://img.shields.io/badge/tests-41%20passing-22c55e?style=flat-square)](tests/)
[![Runs in browser](https://img.shields.io/badge/runtime-Pyodide%20(no%20server)-9aa7b4?style=flat-square)](docs/)
[![Open the app](https://img.shields.io/badge/▶%20Open%20the%20app-Tap%20here-3b82f6?style=for-the-badge)](https://carlosa49.github.io/Humanizer-tester/)
[![Install on iPhone](https://img.shields.io/badge/Install%20on%20iPhone-Add%20to%20Home%20Screen-000000?style=for-the-badge&logo=apple)](https://carlosa49.github.io/Humanizer-tester/)

> **▶ Live app: https://carlosa49.github.io/Humanizer-tester/**
# 📲 Get the app

An **AI Humanizer**: it rewrites machine-sounding text into a chosen human
**tone** while measuring and improving the three signals AI-text detectors
care about most — **perplexity**, **burstiness**, and **lexical diversity**.
**Open this link on your phone, then add it to your Home Screen — it becomes an app.**

Pure Python, **standard library only** (no models, no network, no installs).
### 👉 https://carlosa49.github.io/Humanizer-tester/

### Try it in your browser
| Device | How to install (takes 5 seconds) |
|---|---|
| **iPhone / iPad** | Open the link in **Safari** → tap the **Share** button → **Add to Home Screen** → **Add**. |
| **Android** | Open the link in **Chrome** → menu **⋮** → **Install app**. |
| **Computer** | Open in **Chrome/Edge** → click the **Install** icon in the address bar. |

**▶ https://carlosa49.github.io/Humanizer-tester/**
You get a real app icon, it opens full‑screen, and it **works offline** after
the first open. Delete it like any app and re‑add it from the link anytime.
Nothing is uploaded — it all runs on your device. *(No App Store needed; a
native App Store build is also scaffolded in [`ios/`](ios/).)*

A full web app that runs the humanizer **entirely in your browser** (Python
via Pyodide/WASM) — no server, nothing uploaded. First visit downloads a
~6 MB runtime, then it's instant. Source for the app lives in [`docs/`](docs/)
and is deployed by [`.github/workflows/pages.yml`](.github/workflows/pages.yml).
---

> One-time repo setting to publish it: **Settings → Pages → Build and
> deployment → Source = "GitHub Actions"**. The workflow then deploys on
> every push and prints the live URL in its run summary.
An **AI Humanizer**: it rewrites machine-sounding text into a chosen human
**tone** while measuring and improving the three signals AI-text detectors
care about most — **perplexity**, **burstiness**, and **lexical diversity**.

Pure Python, **standard library only** (no models, no network, no installs).

## What it does

Expand Down Expand Up @@ -140,9 +143,11 @@ humanizer/
cli.py command-line interface
tests/ unittest suites (test_humanizer, test_extended)
examples/ per-tone sample inputs
docs/ in-browser web app (Pyodide): index.html, app.js, styles.css
docs/ installable web app (PWA): index.html, app.js, styles.css,
manifest.webmanifest, sw.js (offline), icons/
web/build_site.py bundles docs/ + live humanizer/ into _site for Pages
.github/workflows/ CI: runs tests, builds & deploys the web app to Pages
ios/ App Store wrapper scaffold (SwiftUI WKWebView) + plan
```

> Note: this nudges text toward human-typical statistics; it is a writing
Expand Down
6 changes: 6 additions & 0 deletions docs/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ function wireUI() {
runBtn.textContent = "Humanize";
}

if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker.register("./sw.js").catch(() => {});
Comment on lines +155 to +157

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register the service worker before loading Pyodide

Because registration is deferred until window.load, and index.html loads the CDN pyodide.js script before app.js, the service worker cannot see or cache that required runtime during a user's first visit. In a fresh install, going offline immediately after the first successful open can leave the PWA unable to reload because the top-level Pyodide script was never placed in the app cache; register earlier or explicitly precache the runtime URLs before advertising first-open offline support.

Useful? React with 👍 / 👎.

});
}

(async () => {
try {
await loadHumanizer();
Expand Down
Binary file added docs/icons/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/icons/icon-192.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/icons/icon-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/icons/icon-maskable-512.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>AI Humanizer — perplexity, burstiness &amp; lexical, by tone</title>
<meta name="description" content="Rewrite machine-sounding text into a human tone. Tunes perplexity, burstiness and lexical diversity. Runs entirely in your browser." />
<link rel="manifest" href="./manifest.webmanifest" />
<meta name="theme-color" content="#0e1117" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Humanizer" />
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
Expand Down
17 changes: 17 additions & 0 deletions docs/manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "AI Humanizer",
"short_name": "Humanizer",
"description": "Rewrite machine-sounding text into a human tone — perplexity, burstiness and lexical, by tone. Runs entirely on your device.",
"id": "./",
"start_url": "./",
"scope": "./",
"display": "standalone",
"orientation": "any",
"background_color": "#0e1117",
"theme_color": "#0e1117",
"icons": [
{ "src": "./icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "./icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "./icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
]
}
71 changes: 71 additions & 0 deletions docs/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* Service worker: makes the app installable and work offline.
Strategy: precache the app shell + the humanizer package, then
cache-first with network fallback for everything else (incl. the
Pyodide runtime from the CDN, cached opportunistically on first use). */

const CACHE = "humanizer-v1";
const SHELL = [
"./",
"./index.html",
"./styles.css",
"./app.js",
"./manifest.webmanifest",
"./icons/icon-192.png",
"./icons/icon-512.png",
"./icons/icon-maskable-512.png",
"./icons/apple-touch-icon.png",
];

self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE);
await cache.addAll(SHELL).catch(() => {});
// Also precache the Python package files listed in the manifest.
try {
const list = await (await fetch("./humanizer/__files__.json", { cache: "no-cache" })).json();
await cache.addAll(
["./humanizer/__files__.json", ...list.map((f) => "./humanizer/" + f)]
);
} catch (e) {}
self.skipWaiting();
})()
);
});

self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
const keys = await caches.keys();
await Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)));
await self.clients.claim();
})()
);
});

self.addEventListener("fetch", (event) => {
const req = event.request;
if (req.method !== "GET") return;
event.respondWith(
(async () => {
const cached = await caches.match(req);
if (cached) return cached;
Comment on lines +51 to +52

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Revalidate cached assets after deployments

With a fixed humanizer-v1 cache and this cache-first return, anyone who has opened the PWA keeps receiving the old app.js, humanizer/__files__.json, and package files after a later GitHub Pages deploy unless sw.js/CACHE is also changed. That means future dictionary or core fixes can be invisible to installed users even though the README/iOS wrapper says site updates are picked up automatically; use revisioned cache names/generated manifests or a network-first/stale-while-revalidate path for these app assets.

Useful? React with 👍 / 👎.

try {
const res = await fetch(req);
// Cache successful or opaque (cross-origin CDN) responses.
if (res && (res.ok || res.type === "opaque")) {
const cache = await caches.open(CACHE);
cache.put(req, res.clone()).catch(() => {});
}
return res;
} catch (e) {
// Offline and not cached: fall back to the app shell for navigations.
if (req.mode === "navigate") {
const shell = await caches.match("./index.html");
if (shell) return shell;
}
throw e;
}
})()
);
});
23 changes: 22 additions & 1 deletion humanizer/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,31 @@
_AN_EXCEPTIONS = ("hour", "honest", "honour", "honor", "heir")
_A_EXCEPTIONS = ("uni", "use", "user", "one", "once", "euro", "ufo")
_ARTICLE_RE = re.compile(r"\b([Aa])(n?)\s+([A-Za-z]+)")
# A run of two or more articles (e.g. "the the", "the a") collapses to the
# last one — the article the substituted phrase brought with it.
_DUP_ARTICLE_RE = re.compile(
r"\b((?:an?|the)\s+){2,}(?=[A-Za-z])", re.IGNORECASE
)


def _fix_articles(text: str) -> str:
"""Repair a/an after lexical substitution changes the following word."""
"""Repair article glitches introduced by lexical substitution.

Lexical replacements can be article-led phrases (e.g. ``plan`` ->
``"the strategy"``). When the original word already had an article this
yields a doubled article (``the the strategy``); collapse those, keeping
the last article and its capitalization, then fix ``a``/``an`` agreement.
"""

def _collapse(m: re.Match) -> str:
run = m.group(0)
last = m.group(1)
# Preserve a leading capital (e.g. sentence start "The the ...").
if run[:1].isupper():
last = last[:1].upper() + last[1:]
return last

text = _DUP_ARTICLE_RE.sub(_collapse, text)

def repl(m: re.Match) -> str:
art, n, word = m.group(1), m.group(2), m.group(3)
Expand Down
22 changes: 11 additions & 11 deletions humanizer/dictionaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
'drew to a close',
'came to an end'],
'finish': ['complete', 'wrap up', 'round off', 'conclude', 'close out'],
'change': ['shift', 'transition', 'adjustment', 'alteration', 'transformation'],
'change': ['alter', 'adjust', 'modify', 'revise', 'shift', 'tweak', 'rework', 'reshape'],
'changed': ['shifted', 'altered', 'adjusted', 'modified', 'reshaped'],
'improve': ['sharpen', 'strengthen', 'refine', 'upgrade', 'boost', 'enhance'],
'improved': ['sharpened', 'strengthened', 'refined', 'upgraded', 'boosted'],
Expand Down Expand Up @@ -1063,7 +1063,7 @@
'options': ['choices', 'alternatives', 'possibilities', 'routes', 'avenues', 'ways forward'],
'decision': ['choice', 'call', 'ruling', 'verdict', 'determination'],
'decisions': ['choices', 'calls', 'rulings', 'verdicts', 'determinations', 'rulings made'],
'changes': ['shifts', 'transitions', 'adjustments', 'alterations', 'overhauls', 'tweaks'],
'changes': ['alters', 'adjusts', 'modifies', 'revises', 'shifts', 'reworks', 'reshapes', 'tweaks'],
'future': ['years ahead',
'time to come',
'road ahead',
Expand Down Expand Up @@ -1294,7 +1294,7 @@
'support': ['corroborate', 'buttress', 'underpin', 'substantiate'],
'increase': ['augment', 'amplify', 'elevate', 'intensify'],
'decrease': ['attenuate', 'diminish', 'abate', 'curtail'],
'change': ['modification', 'alteration', 'perturbation', 'variation'],
'change': ['alter', 'modify', 'revise', 'recalibrate', 'adjust'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve noun-safe replacements for change

The lexical pass does not do POS tagging, so this verb-only override also fires when change is a noun; for example, academic tone can turn We need a change into we demand a reshape/a revise. The previous values were noun phrases, so this introduces ungrammatical output for common noun contexts while fixing imperative verb contexts; keeping alternatives that work as both nouns and verbs or handling noun forms separately would avoid trading one regression for another.

Useful? React with 👍 / 👎.

'cause': ['give rise to', 'engender', 'precipitate', 'induce'],
'affect': ['bear upon', 'modulate', 'influence', 'condition'],
'effect': ['effect size', 'consequence', 'ramification', 'outcome'],
Expand Down Expand Up @@ -1580,7 +1580,7 @@
'great': ['awesome', 'killer', 'amazing', 'epic'],
'easy': ['a breeze', 'no sweat', 'simple', 'easy-peasy'],
'hard': ['tough', 'tricky', 'a pain', 'rough'],
'change': ['switch-up', 'shake-up', 'tweak', 'shift'],
'change': ['switch up', 'shake up', 'tweak', 'mix up', 'redo'],
'increase': ['bump up', 'boost', 'crank up', 'ramp up'],
'decrease': ['cut down', 'trim', 'knock back', 'scale back'],
'problem': ['snag', 'hiccup', 'headache', 'issue'],
Expand Down Expand Up @@ -1808,7 +1808,7 @@
'great': ['outstanding', 'exceptional', 'first-rate', 'stellar'],
'easy': ['simple', 'effortless', 'well within reach', 'clear-cut'],
'hard': ['demanding', 'no small task', 'a real test', 'tough'],
'change': ['a decisive shift', 'transformation', 'overhaul', 'pivot'],
'change': ['overhaul', 'revamp', 'transform', 'redefine', 'reshape'],
'increase': ['drive up', 'scale up', 'accelerate', 'push higher'],
'decrease': ['slash', 'cut decisively', 'drive down', 'reduce hard'],
'problem': ['obstacle', 'hurdle to clear', 'challenge', 'roadblock'],
Expand Down Expand Up @@ -2036,7 +2036,7 @@
'great': ['wonderful', 'lovely', 'heartening', 'uplifting'],
'easy': ['gentle', 'manageable', 'doable', 'light'],
'hard': ["hard, and that's okay", 'tough', 'heavy', 'trying'],
'change': ['a gentle shift', 'a transition', 'a new chapter', 'growth'],
'change': ['gently adjust', 'rework', 'reshape', 'revisit', 'soften'],
'increase': ['gently grow', 'build up', 'nurture upward', 'lift'],
'decrease': ['gently reduce', 'ease back', 'lighten', 'soften'],
'problem': ['struggle', 'hardship', 'rough patch', 'difficulty'],
Expand Down Expand Up @@ -2264,7 +2264,7 @@
'great': ['fantastic', 'wonderful', 'brilliant', 'terrific'],
'easy': ['simple', 'a breeze', 'no trouble', 'easy-peasy'],
'hard': ['tricky', 'tough', 'a bit of a challenge', 'fiddly'],
'change': ['a switch', 'a tweak', 'a fresh take', 'an update'],
'change': ['switch up', 'tweak', 'update', 'tidy up', 'rework'],
'increase': ['boost', 'bump up', 'grow', 'build up'],
'decrease': ['cut back', 'trim', 'scale back', 'ease off'],
'problem': ['snag', 'hiccup', 'little issue', 'bump'],
Expand Down Expand Up @@ -2492,7 +2492,7 @@
'great': ['remarkable', 'game-changing', 'outstanding', 'powerful'],
'easy': ['effortless', 'simple', 'frictionless', 'painless'],
'hard': ['a real challenge worth facing', 'tough but worth it'],
'change': ['a transformation', 'a breakthrough shift', 'a turning point'],
'change': ['transform', 'overhaul', 'revolutionize', 'reinvent', 'supercharge'],
'increase': ['maximize', 'skyrocket', 'supercharge', 'multiply'],
'decrease': ['slash', 'eliminate the waste', 'cut sharply', 'wipe out'],
'problem': ['pain point', 'costly issue', 'barrier', 'weakness'],
Expand Down Expand Up @@ -2720,7 +2720,7 @@
'great': ['excellent', 'outstanding', 'high-caliber', 'exceptional'],
'easy': ['straightforward', 'low-effort', 'streamlined', 'manageable'],
'hard': ['complex', 'demanding', 'challenging', 'involved'],
'change': ['a transition', 'a transformation', 'a realignment', 'a shift'],
'change': ['adjust', 'realign', 'revise', 'restructure', 'recalibrate'],
'increase': ['increase', 'scale', 'expand', 'grow'],
'decrease': ['reduce', 'scale back', 'contract', 'trim'],
'problem': ['issue', 'risk', 'blocker', 'challenge'],
Expand Down Expand Up @@ -2948,7 +2948,7 @@
'great': ['magnificent', 'glorious', 'splendid', 'wondrous'],
'easy': ['effortless', 'smooth', 'untroubled', 'light'],
'hard': ['grueling', 'arduous', 'punishing', 'uphill'],
'change': ['a turning point', 'an upheaval', 'a sea change', 'a twist'],
'change': ['upend', 'transform', 'reshape', 'unravel', 'rewrite'],
'increase': ['swell', 'mount', 'surge', 'climb'],
'decrease': ['dwindle', 'ebb', 'wane', 'shrink away'],
'problem': ['trouble', 'predicament', 'plight', 'thorn'],
Expand Down Expand Up @@ -3176,7 +3176,7 @@
'great': ['objectively flawless', 'stupendous, allegedly', 'epic'],
'easy': ['deceptively simple', 'suspiciously easy', "child's play"],
'hard': ['heroically difficult', 'comically hard', 'an ordeal'],
'change': ['a plot twist', 'an unexpected pivot', 'a curveball'],
'change': ['shake up', 'flip', 'reinvent', 'spin', 'overhaul'],
'increase': ['inflate dramatically', 'balloon', 'spike, naturally'],
'decrease': ['nosedive', 'plummet, dramatically', 'evaporate'],
'problem': ['calamity', 'fiasco', 'debacle', 'kerfuffle'],
Expand Down
Loading
Loading