A poster brief can specify the correct copy while an exported image contains a missing accent, a different character, or an extra space. When a reviewer types what they see into a form, the review tool can compare that transcription with the approved string and point to the first difference.
This Node.js check accepts two strings for one labeled text region. It distinguishes an exact match, a match after Unicode normalization, and a difference that needs review.
1. Capture one region at a time
Give each region a stable ID in your review record, such as headline or price-line. Store the approved copy and the observed transcription beside it. Compare each pair independently so an extra line in one region cannot shift the results for other regions.
Preserve accents, punctuation, case and whitespace in the transcription. Record unknown characters as a review problem before running the check. An empty observed string can represent a region you confirmed was blank.
2. Start with a fixture that has two kinds of change
Save this invented example as transcription.json. The expected string contains a precomposed é. In the observed string, the escape sequence represents a combining acute accent after e. The last phrase also changes 週 to 周.
{
"expected": "Café · 週末",
"observed": "Cafe\u0301 · 周末"
}
JSON.parse decodes the escape into the actual combining mark. Keep the original input strings in the review record so another reviewer can inspect that representation later.
3. Compare exact strings before normalizing
Try raw equality first. If the strings differ, normalize both to NFC and compare again. NFC makes canonically equivalent spellings comparable while retaining compatibility differences such as fullwidth Latin letters. This policy also preserves case and spacing. MDN String normalization.
Use grapheme segmentation to report a position in the normalized text. A joined emoji can span several code points, and this approach counts it as a single segment. The position is one-based in logical string order. For right-to-left copy, it does not identify a physical location in the image. MDN Intl.Segmenter.
4. Implement the comparison and its command line
Save this as poster-copy.mjs. The script requires Node.js 24.20.0 for the example and rejects non-string input and unpaired surrogates. It reports the first differing grapheme after normalization.
import { readFileSync } from "node:fs";
const graphemes = new Intl.Segmenter("en", { granularity: "grapheme" });
export function compareTranscriptions(expected, observed) {
if (typeof expected !== "string" || typeof observed !== "string" ||
!expected.isWellFormed() || !observed.isWellFormed()) {
throw new Error("expected and observed must be well-formed strings");
}
if (expected === observed) return { status: "exact" };
const approved = expected.normalize("NFC");
const captured = observed.normalize("NFC");
if (approved === captured) return { status: "canonical-match" };
const split = (text) =>
Array.from(graphemes.segment(text), ({ segment }) => segment);
const left = split(approved);
const right = split(captured);
const length = Math.max(left.length, right.length);
for (let index = 0; index < length; index += 1) {
if (left[index] !== right[index]) {
return {
status: "different",
position: index + 1,
expected: left[index] ?? null,
observed: right[index] ?? null,
};
}
}
throw new Error("could not locate a difference");
}
if (import.meta.main) {
try {
if (process.argv.length !== 3) {
throw new Error("Usage: node poster-copy.mjs transcription.json");
}
const record = JSON.parse(readFileSync(process.argv[2], "utf8"));
const result = compareTranscriptions(record?.expected, record?.observed);
console.log(JSON.stringify(result, null, 2));
process.exitCode = result.status === "different" ? 1 : 0;
} catch (error) {
console.error(error.message);
process.exitCode = 2;
}
}
An observed or expected value of null in a difference report means that side ended before that position. The report marks the first point to inspect. A single insertion can shift every subsequent segment, so this first position is only a starting point for review.
5. Run both outcomes
Make a copy named equivalent.json and change the observed 周 back to 週 while leaving the escape sequence in place. Run the two files, then run the script without a file to test the invalid-input path.
node poster-copy.mjs transcription.json
node poster-copy.mjs equivalent.json
node poster-copy.mjs
The first command returns exit 1 and prints the difference below. The normalized headline prefix occupies seven graphemes, so the character change is at position eight.
{
"status": "different",
"position": 8,
"expected": "週",
"observed": "周"
}
The second command returns exit 0 with canonical-match. Raw identical strings also return exit 0 with exact. Malformed JSON, missing fields, or a missing path return exit 2 and an error message.
6. Return to the image for the visual decision
Show the approved and observed strings beside the region ID in the review interface. Use the first differing position to direct attention to the relevant text and let the reviewer check the export at its intended size.
Glyph shape, layout, clipping and visual legibility still require the image. This tool operates on the two submitted strings and depends on the accuracy of the transcription.
AI-assisted writing and code drafting. Verification used synthetic strings and local JSON on Node.js 24.20.0. No image was generated or processed, and no OCR or model performance was tested.
This tutorial develops a developer review workflow from the PixMind article’s discussion of multilingual text in images. PixMind source article.
About the author
- The PixMind team shares practical AI image and video workflows. PixMind brings text-to-image, reference-based creation, editing, and video tools into one workspace. Website: https://www.pixmind.io/
Comments
Join the discussionNo comments yet. Be the first to comment.