<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Tighnari</title>
    <description>The latest articles on Forem by Tighnari (@tighnarizerda).</description>
    <link>https://forem.com/tighnarizerda</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3769876%2F5f7f014e-d372-479b-91f1-b5524c3ee901.jpg</url>
      <title>Forem: Tighnari</title>
      <link>https://forem.com/tighnarizerda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tighnarizerda"/>
    <language>en</language>
    <item>
      <title>From IMG_4382.jpg to Invoice_Acme_2024-03.pdf: Building a Content-Aware Renaming Pipeline</title>
      <dc:creator>Tighnari</dc:creator>
      <pubDate>Mon, 23 Feb 2026 08:30:20 +0000</pubDate>
      <link>https://forem.com/tighnarizerda/from-img4382jpg-to-invoiceacme2024-03pdf-building-a-content-aware-renaming-pipeline-2ll3</link>
      <guid>https://forem.com/tighnarizerda/from-img4382jpg-to-invoiceacme2024-03pdf-building-a-content-aware-renaming-pipeline-2ll3</guid>
      <description>&lt;p&gt;Plug in a flatbed scanner and watch what happens to your filenames. Every document gets named &lt;code&gt;Scan0047.pdf&lt;/code&gt;. Photos leave the camera as &lt;code&gt;IMG_4382.jpg&lt;/code&gt;. Screenshots pile up as &lt;code&gt;Screenshot 2024-03-14 at 09.42.17.png&lt;/code&gt;. Within a week, a Downloads folder turns into a graveyard of meaningless names attached to files that might be anything.&lt;/p&gt;

&lt;p&gt;The naive fix is a renaming rule. "Anything prefixed with &lt;code&gt;Scan&lt;/code&gt; goes into &lt;code&gt;/documents/scans/&lt;/code&gt;." That works until your scanner firmware updates and starts outputting &lt;code&gt;IMG&lt;/code&gt; prefixes. Or until you add a second scanner. Rule-based approaches collapse because they operate on filenames, and filenames carry exactly zero semantic information about what's inside the file.&lt;/p&gt;

&lt;p&gt;This post walks through the engineering approach we use to solve this: a content-aware renaming pipeline that reads the document, understands what it is, and generates a meaningful name from the content itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why filename metadata is a dead end
&lt;/h2&gt;

&lt;p&gt;Before getting into the solution, it helps to be precise about why this problem is stubborn.&lt;/p&gt;

&lt;p&gt;Modern file systems give you: filename, creation date, modification date, file size, MIME type. None of those fields tells you whether a PDF is a tax return, an NDA, or a pizza receipt. MIME type gets you &lt;code&gt;application/pdf&lt;/code&gt;. That's the same for all three.&lt;/p&gt;

&lt;p&gt;For images, EXIF data adds GPS coordinates, camera model, and focal length. Useful for photographers. Not useful when you're trying to identify a photo of a whiteboard with meeting notes on it.&lt;/p&gt;

&lt;p&gt;The only reliable signal about what a file contains is the file contents. Which means you have to read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The OCR pipeline
&lt;/h2&gt;

&lt;p&gt;If your input is a native PDF with embedded text (something exported from Word or Google Docs), text extraction is straightforward. &lt;code&gt;pdfplumber&lt;/code&gt;, &lt;code&gt;PyMuPDF&lt;/code&gt;, and &lt;code&gt;pdfminer&lt;/code&gt; all get you there in a few lines. The hard cases are scanned documents and images.&lt;/p&gt;

&lt;p&gt;For those, you need OCR. And before you run OCR, you need to preprocess the image. Raw scanner output is noisy in ways that destroy recognition accuracy.&lt;/p&gt;

&lt;p&gt;Preprocessing steps that actually matter: convert to grayscale, deskew to correct page rotation (scanners are never perfectly aligned), binarize to black and white to remove noise and uneven lighting, then strip borders and scanner artifacts.&lt;/p&gt;

&lt;p&gt;The deskew step gets underestimated. A 2-degree tilt can visibly cut Tesseract word-level accuracy on dense text. Here is the preprocessing function we use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import cv2
import numpy as np

def preprocess_for_ocr(image_path: str) -&amp;gt; np.ndarray:
    img = cv2.imread(image_path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    # Deskew via Hough line detection
    edges = cv2.Canny(gray, 50, 150, apertureSize=3)
    lines = cv2.HoughLines(edges, 1, np.pi / 180, 200)

    if lines is not None:
        angles = [line[0][1] for line in lines]
        median_angle = np.median(angles) - np.pi / 2
        (h, w) = gray.shape
        center = (w // 2, h // 2)
        M = cv2.getRotationMatrix2D(center, np.degrees(median_angle), 1.0)
        gray = cv2.warpAffine(
            gray, M, (w, h),
            flags=cv2.INTER_CUBIC,
            borderMode=cv2.BORDER_REPLICATE
        )

    # Adaptive threshold handles uneven lighting across the scan
    binary = cv2.adaptiveThreshold(
        gray, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY, 11, 2
    )
    return binary
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the image is clean, you run OCR and capture confidence scores alongside the text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import pytesseract
from dataclasses import dataclass, field

@dataclass
class OCRResult:
    text: str
    confidence: float
    word_boxes: list[dict] = field(default_factory=list)

def extract_text(preprocessed_img: np.ndarray, lang: str = "eng") -&amp;gt; OCRResult:
    data = pytesseract.image_to_data(
        preprocessed_img,
        lang=lang,
        output_type=pytesseract.Output.DICT,
        config="--psm 3"  # Auto page segmentation
    )

    words = []
    confidences = []

    for i, word in enumerate(data["text"]):
        if word.strip() and int(data["conf"][i]) &amp;gt; 0:
            words.append(word)
            confidences.append(int(data["conf"][i]))

    avg_confidence = sum(confidences) / len(confidences) if confidences else 0

    return OCRResult(
        text=" ".join(words),
        confidence=avg_confidence / 100,
        word_boxes=[
            {
                "text": data["text"][i],
                "conf": data["conf"][i],
                "x": data["left"][i],
                "y": data["top"][i],
            }
            for i in range(len(data["text"]))
            if data["text"][i].strip()
        ],
    )
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The confidence score is your branching point. We trust Tesseract above 0.85. Below 0.60, we route to a cloud OCR API such as Google Document AI or AWS Textract. In between, we apply additional preprocessing passes before making the call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where vision models change things
&lt;/h2&gt;

&lt;p&gt;OCR gives you text. Vision models give you understanding. That distinction is worth sitting with for a moment.&lt;/p&gt;

&lt;p&gt;If you feed a scanned receipt to Tesseract, you get a blob of text: prices, a merchant name, some line items, a date. What Tesseract does not tell you is "this is a receipt." You have to figure that out from the extracted text itself, which means writing heuristics that are fragile and specific.&lt;/p&gt;

&lt;p&gt;Vision models look at the whole document image and build a representation that fuses visual layout with content semantics. They can classify a document as a receipt, invoice, contract, or driver's license before reading a single word, because the visual structure of these document types is distinct. A receipt looks like a receipt. An invoice has a specific spatial layout with a header, line-item table, and totals block.&lt;/p&gt;

&lt;p&gt;That classification matters a lot for renaming. If you know the document type upfront, you know which entities to prioritize. Receipts: merchant name, date, total. Invoices: vendor, invoice number, due date. Contracts: parties, effective date, agreement type. Different types need different templates.&lt;/p&gt;

&lt;p&gt;We run classification on the raw image in parallel with OCR, then merge the results:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import anthropic
import base64
import json
from pathlib import Path

DOCUMENT_TYPES = [
    "invoice", "receipt", "contract", "tax_form", "bank_statement",
    "id_document", "medical_record", "letter", "form", "report", "other",
]

def classify_document(image_path: str) -&amp;gt; dict:
    client = anthropic.Anthropic()

    image_data = base64.standard_b64encode(
        Path(image_path).read_bytes()
    ).decode("utf-8")

    suffix = Path(image_path).suffix.lower()
    media_type_map = {
        ".jpg": "image/jpeg",
        ".jpeg": "image/jpeg",
        ".png": "image/png",
        ".webp": "image/webp",
        ".pdf": "image/jpeg",  # PDFs rendered to JPEG before this step
    }
    media_type = media_type_map.get(suffix, "image/jpeg")

    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=256,
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "image",
                        "source": {
                            "type": "base64",
                            "media_type": media_type,
                            "data": image_data,
                        },
                    },
                    {
                        "type": "text",
                        "text": (
                            "Classify this document. Reply with JSON only:\n"
                            '{"type": "&amp;lt;one of the DOCUMENT_TYPES list&amp;gt;",'
                            '"confidence": &amp;lt;0.0-1.0&amp;gt;,'
                            '"language": "&amp;lt;ISO 639-1 code&amp;gt;"}'
                        ),
                    },
                ],
            }
        ],
    )

    return json.loads(response.content[0].text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The classification result feeds directly into entity extraction. You get the document type, you select the right template, and you extract the right fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  From extracted text to a meaningful filename
&lt;/h2&gt;

&lt;p&gt;This is the part that looks simple and isn't. Once you have OCR text and a document type, you run type-specific regex patterns against the text and compose a filename from whatever entities resolve successfully. The key engineering decision is building a fallback chain so you always get something useful, even when entity extraction partially fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import re
from datetime import datetime

EXTRACTION_TEMPLATES = {
    "invoice": {
        "patterns": {
            "vendor": r"(?:from|vendor|bill from|billed by)[:\s]+([A-Z][A-Za-z\s&amp;amp;,\.]+?)(?:\n|,|\d)",
            "invoice_number": r"(?:invoice\s*#?|inv\.?\s*#?)[:\s]*([A-Z0-9\-]+)",
            "date": r"(?:date|issued)[:\s]*(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4}|\w+\s+\d{1,2},?\s*\d{4})",
        },
        "template": "{vendor}_{date}_Invoice_{invoice_number}",
        "fallback": "Invoice_{date}",
    },
    "receipt": {
        "patterns": {
            "merchant": r"^([A-Z][A-Za-z\s&amp;amp;]+?)(?:\n|LLC|Inc|Corp|Ltd)",
            "date": r"(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})",
        },
        "template": "Receipt_{merchant}_{date}",
        "fallback": "Receipt_{date}",
    },
    "contract": {
        "patterns": {
            "parties": r"(?:between|by and between)\s+(.+?)\s+and\s+(.+?)(?:\n|,|\()",
            "date": r"(?:dated|effective|as of)[:\s]*(\w+\s+\d{1,2},?\s*\d{4}|\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})",
            "type": r"(non-disclosure|employment|service|licensing|partnership)\s+agreement",
        },
        "template": "{type}_Agreement_{parties}_{date}",
        "fallback": "Contract_{date}",
    },
}

def normalize_date(raw_date: str) -&amp;gt; str:
    formats = ["%m/%d/%Y", "%m-%d-%Y", "%B %d, %Y", "%b %d %Y", "%m/%d/%y"]
    for fmt in formats:
        try:
            return datetime.strptime(raw_date.strip(), fmt).strftime("%Y-%m-%d")
        except ValueError:
            continue
    return raw_date.replace("/", "-").replace(" ", "-")

def sanitize_filename(name: str) -&amp;gt; str:
    name = re.sub(r'[&amp;lt;&amp;gt;:"/\\|?*\x00-\x1f]', "", name)
    name = re.sub(r"\s+", "_", name.strip())
    name = re.sub(r"_+", "_", name)
    return name[:200]

def generate_filename(ocr_text: str, doc_type: str, original_ext: str) -&amp;gt; str:
    config = EXTRACTION_TEMPLATES.get(doc_type, {})

    if not config:
        first_line = next(
            (line.strip() for line in ocr_text.split("\n") if len(line.strip()) &amp;gt; 5),
            "document",
        )
        return sanitize_filename(first_line[:50]) + original_ext

    extracted = {}
    for field_name, pattern in config["patterns"].items():
        match = re.search(pattern, ocr_text, re.IGNORECASE | re.MULTILINE)
        if match:
            value = match.group(1).strip()
            if field_name == "date":
                value = normalize_date(value)
            extracted[field_name] = sanitize_filename(value)

    try:
        name = config["template"].format(**extracted)
    except KeyError:
        try:
            name = config["fallback"].format(**extracted)
        except KeyError:
            name = f"{doc_type}_{datetime.now().strftime('%Y-%m-%d')}"

    return sanitize_filename(name) + original_ext
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always have a fallback. If entity extraction fails on a low-quality scan, you want something better than &lt;code&gt;Scan0047.pdf&lt;/code&gt; even if you cannot get &lt;code&gt;Acme_Inc_2024-03-14_Invoice_INV-4421.pdf&lt;/code&gt;. The fallback chain gives you graceful degradation at every level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pipeline architecture
&lt;/h2&gt;

&lt;p&gt;Here is the full flow as it runs in production at &lt;a href="https://renamer.ai/" rel="noopener noreferrer"&gt;&lt;strong&gt;renamer.ai&lt;/strong&gt;&lt;/a&gt;, where this exact pipeline handles batch renaming across web, Windows, and Mac:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;File Input
    |
    v
Format Detection
    +-- Native PDF/DOCX ---------&amp;gt; Text Extraction (skip OCR)
    |                                       |
    +-- Image / Scanned PDF --&amp;gt; Preprocessing Pipeline
                                            |
                               OCR Engine (Tesseract)
                                            |
                         +-----------------+------------------+
                  conf &amp;gt;= 0.85        0.60-0.85          conf &amp;lt; 0.60
                         |               |                   |
                   Use result    Extra preprocessing    Cloud OCR API
                         |               |                   |
                         +---------------+-------------------+
                                         |
                              Vision Classification
                              (document type + language)
                                         |
                              Entity Extraction
                              (type-specific patterns)
                                         |
                              Name Generation
                              (template -&amp;gt; fallback chain)
                                         |
                              Conflict Resolution
                              (append _v2, _v3 if name exists)
                                         |
                              Rename + Audit Log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Conflict Resolution step is easy to skip in early development and painful to bolt on later. When you process 500 files in a batch, you will hit cases where two invoices from the same vendor on the same date generate identical filenames. You need deterministic tie-breaking before any writes touch disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling the hard cases
&lt;/h2&gt;

&lt;p&gt;Blurry or low-resolution scans are the most common problem. If Tesseract confidence drops below 0.40, no preprocessing pass will recover it. Our fix: upscale with a super-resolution model (Real-ESRGAN works well for document scans) before the preprocessing step when initial confidence is low. It adds latency but meaningfully improves accuracy on fax-quality documents.&lt;/p&gt;

&lt;p&gt;Handwritten notes are a different problem. Standard Tesseract handles handwriting poorly. We route these to a dedicated model such as Google Document AI's handwriting mode or a fine-tuned TrOCR. These paths are slower and more expensive, so you want to detect handwriting early and only invoke them when you need to. Visual classifiers can distinguish handwritten from printed documents reliably before any OCR runs.&lt;/p&gt;

&lt;p&gt;Mixed-language documents come up more often than you'd expect. A contract with English headers and Spanish body text is common in cross-border work. Tesseract handles multi-language extraction with &lt;code&gt;--lang eng+spa&lt;/code&gt;, but your entity extraction patterns also need to be language-aware. Detect the primary language at the classification stage and select the matching pattern set.&lt;/p&gt;

&lt;p&gt;Photos that aren't documents will always make it into your pipeline. Vision models handle this cleanly: they recognize that a photograph of a coastline is not a document and return a flag rather than a confused filename.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance at scale
&lt;/h2&gt;

&lt;p&gt;If you're processing thousands of files, throughput becomes the binding constraint. A few things that actually move the needle:&lt;/p&gt;

&lt;p&gt;Run OCR and vision classification in parallel using asyncio with a thread pool executor. OCR is CPU-bound. Classification API calls are I/O-bound. Mixing both in the same pipeline makes parallelism tricky; separating them into their own executor pools helps.&lt;/p&gt;

&lt;p&gt;Cache document type classifications. If you're processing 200 files from the same client folder, a local cache keyed on image embeddings avoids redundant API calls for visually similar document layouts.&lt;/p&gt;

&lt;p&gt;For large PDFs, only process the first two or three pages for naming. The information you need is almost always in the header section of page one. Processing a 200-page contract end to end is wasteful.&lt;/p&gt;

&lt;p&gt;One non-obvious bottleneck: filesystem stat() calls during conflict resolution. If you're checking filenames against disk for every file in a batch, that adds up. Build an in-memory name registry at the start of the job and check against that instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the failure modes actually look like
&lt;/h2&gt;

&lt;p&gt;The hardest case is a document that gives you text but the wrong text. A photocopy of a photocopy of a fax from 1994 might OCR to mostly garbage with a confidence score of 0.72 -- high enough that you skip the cloud fallback, low enough that extracted entities are wrong. Your pipeline produces a confidently wrong filename: &lt;code&gt;Smith_Associates_1994-03-15_Invoice_INV-872X.pdf&lt;/code&gt; when the vendor is &lt;code&gt;Smithfield &amp;amp; Associates&lt;/code&gt; and the invoice number ends in &lt;code&gt;4&lt;/code&gt;, not &lt;code&gt;X&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is why audit logs are non-negotiable. Every rename operation should write the original filename, the generated name, confidence scores, and extracted entities to a log. When the pipeline gets something wrong, you need to trace exactly which step failed.&lt;/p&gt;

&lt;p&gt;If you want to see this pipeline running on real documents, &lt;a href="https://renamer.ai/" rel="noopener noreferrer"&gt;&lt;strong&gt;renamer.ai&lt;/strong&gt;&lt;/a&gt; handles batch renaming across web, Windows, and Mac and lets you review generated names before anything commits to disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The rough priority order if you're building this from scratch:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Native PDF and DOCX text extraction first -- immediate wins, no OCR complexity&lt;/li&gt;
&lt;li&gt;Tesseract for scanned documents, with confidence thresholds&lt;/li&gt;
&lt;li&gt;Document type classification (even a rule-based classifier improves entity extraction significantly)&lt;/li&gt;
&lt;li&gt;Entity extraction templates for your top three to five document types&lt;/li&gt;
&lt;li&gt;Cloud OCR fallback for low-confidence results&lt;/li&gt;
&lt;li&gt;Vision model classification for ambiguous cases&lt;/li&gt;
&lt;li&gt;Batch throughput optimization once accuracy is solid&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not start at step 7. Optimizing a pipeline that generates wrong filenames just produces wrong filenames faster. Get accuracy right first, then make it fast.&lt;/p&gt;

&lt;p&gt;The core flow -- preprocess, OCR, classify, extract, template, fallback -- is stable enough to build on incrementally. Each stage can be improved in isolation without requiring rewrites downstream. That is the version worth building.&lt;/p&gt;

</description>
      <category>python</category>
      <category>machinelearning</category>
      <category>ai</category>
      <category>devops</category>
    </item>
    <item>
      <title>How AI Generates Brand Names: The Real Pipeline</title>
      <dc:creator>Tighnari</dc:creator>
      <pubDate>Fri, 13 Feb 2026 01:52:39 +0000</pubDate>
      <link>https://forem.com/tighnarizerda/how-ai-generates-brand-names-the-real-pipeline-5h7m</link>
      <guid>https://forem.com/tighnarizerda/how-ai-generates-brand-names-the-real-pipeline-5h7m</guid>
      <description>&lt;p&gt;I spent three weeks trying to name a side project last year. Three weeks. I had a spreadsheet with 200 entries, half of them portmanteaus that sounded like prescription medications. That's when I got curious about how AI name generators actually work under the hood.&lt;/p&gt;

&lt;p&gt;Turns out the problem is far more interesting than "just ask GPT for a name." Let me walk you through the real engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why Name Generation Is Deceptively Hard
&lt;/h3&gt;

&lt;p&gt;Think of a good brand name off the top of your head. Got one? Now check if the .com is available. It's not. That, in miniature, is the whole problem.&lt;/p&gt;

&lt;p&gt;But it goes deeper than domain squatting. A name generator has to satisfy constraints that fight each other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phonetic quality:&lt;/strong&gt; The name needs to be pronounceable, memorable, and pleasant to say. "Spotify" rolls off the tongue. "Qwrtyp" does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic relevance:&lt;/strong&gt; It should hint at what the product does, or at least not contradict it.&lt;/li&gt;
&lt;li&gt;Uniqueness: It can't sound like an existing trademark. Call your fintech startup "Paypel" and see what happens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-language safety:&lt;/strong&gt; "Nova" means "doesn't go" in Spanish (the Chevy Nova legend is actually a myth, but the concern is real). "Siri" means something unfortunate in Georgian.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain availability:&lt;/strong&gt; There are roughly 350 million registered domains. Your perfect five-letter .com is taken.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The combinatorial space of possible names is enormous. English has about 44 phonemes. A two-syllable name is 4-6 phonemes, giving you millions of combinations. Most of them are garbage. The engineering challenge is generating candidates that land in the narrow band between "that sounds like a real word" and "that's already trademarked."&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 1: Markov Chains (The Simple Baseline)
&lt;/h3&gt;

&lt;p&gt;The oldest trick in the book. Train a character-level Markov chain on a corpus of existing brand names, then sample from it. Each character prediction depends only on the previous N characters.&lt;/p&gt;

&lt;p&gt;Here's a minimal implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from collections import defaultdict
import random

def build_chain(names, order=3):
    chain = defaultdict(list)
    for name in names:
        padded = "^" * order + name.lower() + "$"
        for i in range(len(padded) - order):
            key = padded[i:i + order]
            chain[key].append(padded[i + order])
    return chain

def generate(chain, order=3, max_len=12):
    result = ""
    key = "^" * order
    for _ in range(max_len):
        if key not in chain:
            break
        next_char = random.choice(chain[key])
        if next_char == "$":
            break
        result += next_char
        key = key[1:] + next_char
    return result.capitalize()

# Train on real brand names
brands = ["spotify", "shopify", "stripe", "slack", "notion",
          "figma", "vercel", "linear", "retool", "supabase"]
chain = build_chain(brands, order=2)

for _ in range(5):
    print(generate(chain, order=2))
python

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run that and you'll get output like "Slace", "Notify", "Supa". Some are surprisingly good. Most are not.&lt;/p&gt;

&lt;p&gt;The fatal flaw? Markov chains have no understanding of what makes a name good. They learn character co-occurrence patterns, nothing more. Set the order too low and you get random nonsense. Set it too high and you just recombine chunks of your training data. There's a sweet spot around order 2-3 for brand names, but even then you're playing a numbers game where maybe 1 in 50 outputs is usable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 2: Neural Language Models
&lt;/h3&gt;

&lt;p&gt;RNNs and LSTMs were the first real upgrade. Instead of a fixed context window, recurrent models maintain a hidden state that (theoretically) captures long-range dependencies across the entire name.&lt;/p&gt;

&lt;p&gt;You train a character-level LSTM on your brand name corpus, and it learns subtler phonotactic patterns. It picks up that "str-" is a strong opening cluster in English, that names rarely end in "-gk", and that doubled vowels like "oo" give a name a friendly feel (Google, Yahoo, Voodoo).&lt;/p&gt;

&lt;p&gt;The practical difference from Markov chains: LSTMs generate names that sound more like real words because they're better at learning the statistical structure of English phonology. The tradeoff is training time, model complexity, and the need for a larger corpus. For a hobby project, Markov chains are fine. For a production system, you want something with more capacity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 3: Transformer-Based Generation
&lt;/h3&gt;

&lt;p&gt;This is where things get interesting. Fine-tune a GPT-style model on brand names, and you unlock something Markov chains and LSTMs can't do: conditional generation.&lt;/p&gt;

&lt;p&gt;Want a name that sounds techy? Playful? Premium? You can encode those attributes into your prompt or training data and the model learns to steer its outputs. Here's where temperature and sampling strategy become critical:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import numpy as np

def sample_with_temperature(logits, temperature=1.0, top_k=10):
    """
    Lower temperature = more conservative, predictable names
    Higher temperature = more creative, risky names
    top_k limits sampling to the K most likely next tokens
    """
    # Apply temperature scaling
    scaled = logits / temperature

    # Top-k filtering
    if top_k &amp;gt; 0:
        threshold = np.sort(scaled)[-top_k]
        scaled[scaled &amp;lt; threshold] = -np.inf

    # Convert to probabilities
    probs = np.exp(scaled) / np.sum(np.exp(scaled[scaled &amp;gt; -np.inf]))

    return np.random.choice(len(probs), p=probs)

# In practice:
# temperature=0.3 → safe names like "Bluecore", "Datafy"  
# temperature=0.7 → balanced like "Zentiq", "Colvara"
# temperature=1.2 → wild like "Xyphora", "Quenbi"
python

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Temperature is the single most important hyperparameter in name generation. Too low and every output sounds like every other B2B SaaS startup. Too high and you get names that look like someone fell asleep on a keyboard. Most production systems let users control this indirectly through a "creativity slider" or style selector.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach 4: Generator-Discriminator Architectures
&lt;/h3&gt;

&lt;p&gt;Here's a pattern borrowed from GANs but adapted for discrete text. One model generates name candidates. A separate model scores them. The scorer is trained on human preference data, and you use its signal to improve the generator over time.&lt;/p&gt;

&lt;p&gt;The scorer typically evaluates multiple dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phonetic quality&lt;/strong&gt; (does it sound good?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic fit&lt;/strong&gt; (does it match the industry?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memorability&lt;/strong&gt; (how easy is it to recall after one exposure?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual balance&lt;/strong&gt; (does it look good written down?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is closer to how RLHF works for chat models, but applied to a much narrower domain. The advantage is that your generator keeps improving as you collect more human feedback. The downside: you need that feedback data, and collecting it is slow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phonetic Scoring: The Secret Weapon
&lt;/h3&gt;

&lt;p&gt;Ask any naming professional what separates forgettable names from sticky ones, and the answer usually involves sound symbolism. This isn't mysticism. It's backed by decades of linguistics research.&lt;/p&gt;

&lt;p&gt;Certain sound patterns trigger consistent associations across languages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Front vowels (like /i/ in "sweet") feel small, fast, light&lt;/li&gt;
&lt;li&gt;Back vowels (like /u/ in "brute") feel large, heavy, powerful&lt;/li&gt;
&lt;li&gt;Plosive consonants (/b/, /k/, /t/) feel strong and decisive&lt;/li&gt;
&lt;li&gt;Fricatives (/f/, /s/, /v/) feel soft and sophisticated&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can encode this into a scoring function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PHONETIC_FEATURES = {
    'b': {'strength': 0.8, 'softness': 0.1, 'energy': 0.7},
    'k': {'strength': 0.9, 'softness': 0.0, 'energy': 0.8},
    's': {'strength': 0.2, 'softness': 0.9, 'energy': 0.4},
    'f': {'strength': 0.1, 'softness': 0.8, 'energy': 0.3},
    'i': {'brightness': 0.9, 'warmth': 0.3, 'weight': 0.1},
    'o': {'brightness': 0.4, 'warmth': 0.8, 'weight': 0.7},
}

def score_name_phonetics(name, target_profile):
    """
    Score how well a name's phonetic features match
    a desired brand personality profile.

    target_profile example: {'strength': 0.7, 'softness': 0.3}
    """
    scores = []
    for char in name.lower():
        if char in PHONETIC_FEATURES:
            features = PHONETIC_FEATURES[char]
            for trait, target_val in target_profile.items():
                if trait in features:
                    diff = abs(features[trait] - target_val)
                    scores.append(1.0 - diff)

    return sum(scores) / len(scores) if scores else 0.0

# "Kraft" scores high on strength. "Silvia" scores high on softness.
print(score_name_phonetics("kraft", {"strength": 0.8}))   # ~0.87
print(score_name_phonetics("silvia", {"softness": 0.8}))  # ~0.85
python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is simplified, obviously. Production systems use IPA transcription, syllable stress patterns, and cross-language phoneme databases. But the core idea holds: you can computationally score how a name feels before any human ever reads it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Filtering Pipeline
&lt;/h3&gt;

&lt;p&gt;Raw generation is maybe 20% of the work. The real engineering is in filtering. A production name generator pushes candidates through a pipeline like this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 1:&lt;/strong&gt; Linguistic filtering. Check for profanity (including in other languages), slang, or unfortunate double meanings. This is harder than it sounds. "Therapist" contains an unfortunate substring. "Pen Island" is a classic. Your filter needs to catch both exact matches and embedded patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 2:&lt;/strong&gt; Domain availability. DNS lookups are fast but rate-limited. Most systems check multiple TLDs (.com, .io, .co, .ai) and surface available options. Some use WHOIS APIs or registrar APIs like Namecheap or GoDaddy to check in bulk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 3:&lt;/strong&gt; Trademark screening. The USPTO has a free API (TESS), and EUIPO has one for European marks. You're looking for exact matches and phonetic similarity. "Gogle" would fail even though it's spelled differently. Levenshtein distance and phonetic hashing (Soundex, Metaphone) handle the fuzzy matching.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 4:&lt;/strong&gt; Phonetic deduplication. If your generator produced "Zentiq" and "Zentik", you probably only want to show one. Metaphone encoding or phonetic distance scoring collapses these into equivalence classes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stage 5:&lt;/strong&gt; Human scoring. The best systems incorporate user feedback loops. Every name a user saves, dismisses, or edits becomes training data for the next iteration.&lt;/p&gt;

&lt;h3&gt;
  
  
  How It All Fits Together
&lt;/h3&gt;

&lt;p&gt;In a production system, these components form a pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User specifies keywords, industry, and style preferences&lt;/li&gt;
&lt;li&gt;Multiple generators run in parallel (transformer, Markov, phonetic assembly)&lt;/li&gt;
&lt;li&gt;Candidates merge into a pool (typically 500-2,000 raw names)&lt;/li&gt;
&lt;li&gt;The filtering pipeline removes roughly 90% of candidates&lt;/li&gt;
&lt;li&gt;A ranking model scores survivors on phonetic quality, semantic relevance, and user preference alignment&lt;/li&gt;
&lt;li&gt;The top 20-50 names reach the user, with domain availability and basic trademark status attached&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tools like &lt;strong&gt;&lt;a href="https://nametastic.com/" rel="noopener noreferrer"&gt;nametastic.com&lt;/a&gt;&lt;/strong&gt; combine several of these techniques in their pipeline. If you've used any AI name generator recently, you've seen this architecture in action even if the specific model choices vary between products.&lt;/p&gt;

&lt;p&gt;The interesting engineering challenge isn't any single component. It's the orchestration. How do you balance generation diversity against quality? How aggressively should you filter? Too aggressive and you show 5 boring safe names. Too loose and you bury the gems in noise.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's Coming Next
&lt;/h3&gt;

&lt;p&gt;Two emerging approaches are worth watching:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reinforcement learning from human preferences.&lt;/strong&gt; Instead of training a separate scorer, you fine-tune the generator directly on preference data. Every time a user picks "Zentiq" over "Blandco", that signal flows back into the model. This is the same idea behind RLHF in ChatGPT, applied to a constrained generation task. The smaller output space (short strings vs. paragraphs) actually makes this more tractable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Diffusion models for discrete text.&lt;/strong&gt; Models like D3PM and MDLM are adapting the diffusion framework from image generation to text. Instead of denoising a blurry image, you iteratively refine a corrupted token sequence into a clean name. Early results are promising for short text generation because the fixed-length output structure maps well to the diffusion paradigm. This is still research-stage, but the name generation use case is almost tailor-made for it.&lt;/p&gt;

&lt;p&gt;And honestly? The biggest unsolved problem is cultural sensitivity at scale. You can check a name against five or ten languages. But there are 7,000 languages worldwide, and a name that's perfect in English might be offensive in a language your product expands into three years later. No model handles that well yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Wrapping Up
&lt;/h3&gt;

&lt;p&gt;If you're thinking about building a name generator yourself, start with a Markov chain. Seriously. Get the filtering pipeline right first, because that's where the real value lives. Then swap in increasingly sophisticated generators as your needs grow.&lt;/p&gt;

&lt;p&gt;The gap between a basic Markov chain generator and a production system like &lt;strong&gt;&lt;a href="https://nametastic.com/" rel="noopener noreferrer"&gt;nametastic.com&lt;/a&gt;&lt;/strong&gt; isn't just the model. It's the filtering, the phonetic scoring, the domain checking, and the preference learning loop that turns user behavior into better outputs over time.&lt;/p&gt;

&lt;p&gt;For the ML engineers in the room: this is a weirdly satisfying problem space. The outputs are short enough to iterate fast, the evaluation is immediate (does this name sound good?), and you get to combine NLP, information retrieval, and human-computer interaction in a single system.&lt;/p&gt;

&lt;p&gt;And if nothing else, you'll never look at a startup name the same way again.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>nlp</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
