Skip to content
Open
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
93 changes: 93 additions & 0 deletions viet-font-app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Vietnamese Font Generator

AI-powered tool for adding Vietnamese diacritical marks to existing fonts, following [Google Fonts diacritics guidelines](https://googlefonts.github.io/gf-guide/diacritics.html).

## Features

- **AI Font Analysis**: Upload sample images and let AI (Gemini or ChatGPT) analyze the font style
- **Automatic Glyph Generation**: AI generates Vietnamese diacritical marks matching the font's style
- **Composite Glyphs**: When possible, creates composite glyphs from existing base characters and marks
- **Google Fonts Compliant**: Follows proper anchor-based mark positioning guidelines
- **TTF Export**: Downloads the Vietnamized font as a standard TrueType font file

## Vietnamese Character Support

The tool generates all Vietnamese-specific characters including:
- **Base characters**: Ă, Â, Đ, Ê, Ô, Ơ, Ư (and lowercase)
- **Tone marks**: Acute (sắc), Grave (huyền), Hook above (hỏi), Tilde (ngã), Dot below (nặng)
- **Combined characters**: All combinations of base + tone marks (134+ characters total)

## Quick Start

### Requirements

- Python 3.9+
- A Gemini API key or OpenAI API key

### Installation

```bash
cd viet-font-app
pip install -r requirements.txt
```

### Running

```bash
uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload
```

Then open http://localhost:8000 in your browser.

### Usage

1. **Select AI Provider**: Choose between Google Gemini or OpenAI ChatGPT
2. **Enter API Key**: Provide your API key for the selected provider
3. **Upload Font**: Upload your base font file (.ttf, .otf, .woff, .woff2)
4. **Upload Sample Images** (optional): Add images showing the font style for better AI analysis
5. **Generate**: Click "Generate Vietnamese Font" and wait for processing
6. **Download**: Download the Vietnamized font as a .ttf file

## How It Works

1. **Font Analysis**: The tool reads the uploaded font and identifies which Vietnamese characters are missing
2. **Style Analysis**: If sample images are provided, AI analyzes the font's visual characteristics (weight, contrast, terminals, construction)
3. **Composite Generation**: For characters that can be built from existing glyphs (base + combining marks), composite glyphs are created
4. **AI Glyph Generation**: For missing characters, AI generates SVG path data matching the analyzed style
5. **Font Assembly**: Generated glyphs are added to the font using fonttools, following Google Fonts diacritics guidelines
6. **Export**: The modified font is saved as a TrueType (.ttf) file

## Technical Details

### Diacritics Guidelines (Google Fonts)

Following the [Google Fonts diacritics guide](https://googlefonts.github.io/gf-guide/diacritics.html):

- **Anchor-based positioning**: Uses `top`, `bottom`, and `top_viet` anchors
- **Combining marks**: Zero-width marks with proper Unicode codepoints
- **Stacked diacritics**: Vietnamese-specific stacked mark handling
- **GDEF classification**: Proper mark glyph classification (class 3)
- **Case variants**: Separate `.case` marks for capital letters

### Tech Stack

- **Backend**: FastAPI + fonttools + Pillow
- **Frontend**: Vanilla HTML/CSS/JS
- **AI**: Google Gemini 2.0 Flash / OpenAI GPT-4o
- **Font Processing**: fonttools library

## API Endpoints

| Method | Path | Description |
|--------|------|-------------|
| GET | `/` | Frontend UI |
| POST | `/api/analyze-font` | Analyze font for missing Vietnamese chars |
| POST | `/api/generate-font` | Generate Vietnamese font (direct download) |
| POST | `/api/generate-font-stream` | Generate font with detailed results JSON |
| GET | `/api/download/{session_id}` | Download previously generated font |
| DELETE | `/api/cleanup/{session_id}` | Clean up temporary files |
| GET | `/api/health` | Health check |

## License

This tool is part of the FontForge project. See the main [LICENSE](../LICENSE) file.
Empty file.
214 changes: 214 additions & 0 deletions viet-font-app/backend/ai_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
"""AI client module for font style analysis and glyph generation.

Supports both Google Gemini and OpenAI (ChatGPT) APIs.
Uses vision capabilities to analyze font styles from images
and generate SVG path data for Vietnamese diacritical marks.
"""

import base64
import json
import logging
import re
from pathlib import Path

logger = logging.getLogger(__name__)

FONT_ANALYSIS_PROMPT = """You are a professional font designer and typographer.
Analyze this font sample image and describe the font's visual characteristics in detail:
- Stroke weight and contrast (thin/regular/bold, monolinear/high contrast)
- Serif style (sans-serif, serif, slab-serif, etc.)
- Letter construction (geometric, humanist, transitional, etc.)
- Stroke terminals (round, square, teardrop, etc.)
- Overall proportions and x-height
- Any distinctive stylistic features

Provide your analysis as a structured JSON object with these fields:
{
"weight": "thin|light|regular|medium|bold|black",
"contrast": "monolinear|low|medium|high",
"serif_style": "sans-serif|serif|slab-serif|decorative",
"construction": "geometric|humanist|transitional|modern|decorative",
"terminals": "round|square|teardrop|ball|flat",
"x_height": "small|medium|large",
"style_notes": "any additional distinctive features"
}

Return ONLY the JSON object, no other text."""

GLYPH_GENERATION_PROMPT = """You are a professional font designer.
Based on this font style analysis: {style_analysis}

Generate SVG path data for the Vietnamese diacritical mark: "{mark_name}"
This mark will be used as a combining diacritical mark in the font.

The mark should:
- Match the style described in the analysis (weight, contrast, terminals)
- Follow Google Fonts diacritics guidelines
- Be designed at a UPM (units per em) of {upm}
- Have appropriate proportions for the font's x-height

For reference, the font's ascender is {ascender} and descender is {descender}.
The base glyph width is approximately {avg_width}.

Return ONLY a JSON object with this structure:
{{
"svg_path": "M ... Z",
"width": <number>,
"height": <number>,
"offset_x": <number>,
"offset_y": <number>,
"description": "brief description of the mark design"
}}

The svg_path should be a valid SVG path data string using absolute coordinates.
Width and height are the bounding box of the mark.
offset_x and offset_y are the recommended positioning offsets from the base glyph's anchor point.

Return ONLY the JSON object, no other text."""

FULL_CHAR_GENERATION_PROMPT = """You are a professional font designer.
Based on this font style analysis: {style_analysis}

Generate an SVG path for the Vietnamese character "{char}" (U+{codepoint:04X}).
This character is composed of base "{base}" with the following marks: {marks}.

The design should:
- Match the style described (weight, contrast, terminals, construction)
- Follow Google Fonts Vietnamese diacritics guidelines
- Use proper mark positioning with appropriate spacing
- Be designed at UPM of {upm}
- Diacritics should be coherent and harmonious with the base letter
- Marks should be at consistent distance from base letter
- Symmetric marks centered on optical center of base

Font metrics: ascender={ascender}, descender={descender}, avg_width={avg_width}

Return ONLY a JSON object:
{{
"svg_path": "M ... Z",
"width": <advance_width>,
"lsb": <left_side_bearing>,
"components": [
{{"name": "base_or_mark_name", "svg_path": "M...Z", "offset_x": 0, "offset_y": 0}}
],
"description": "brief description"
}}

Return ONLY the JSON object."""


def _extract_json(text: str) -> dict:
"""Extract JSON from AI response text, handling markdown code blocks."""
text = text.strip()
json_match = re.search(r'```(?:json)?\s*([\s\S]*?)```', text)
if json_match:
text = json_match.group(1).strip()
try:
return json.loads(text)
except json.JSONDecodeError:
json_match = re.search(r'\{[\s\S]*\}', text)
if json_match:
return json.loads(json_match.group(0))
raise ValueError(f"Could not parse JSON from AI response: {text[:200]}")


def _encode_image(image_path: str) -> str:
"""Encode an image file to base64."""
with open(image_path, "rb") as f:
return base64.b64encode(f.read()).decode("utf-8")


def _get_image_mime(image_path: str) -> str:
"""Get MIME type from file extension."""
ext = Path(image_path).suffix.lower()
mime_map = {
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".gif": "image/gif",
".webp": "image/webp",
".bmp": "image/bmp",
}
return mime_map.get(ext, "image/png")


class GeminiClient:
"""Client for Google Gemini API."""

def __init__(self, api_key: str):
import google.generativeai as genai
genai.configure(api_key=api_key)
self.model = genai.GenerativeModel("gemini-2.5-pro-preview-05-06")

def analyze_font_style(self, image_paths: list[str]) -> dict:
"""Analyze font style from sample images."""
import google.generativeai as genai
parts = [FONT_ANALYSIS_PROMPT]
for path in image_paths:
img_data = _encode_image(path)
mime = _get_image_mime(path)
parts.append({
"inline_data": {
"mime_type": mime,
"data": img_data,
}
})
response = self.model.generate_content(parts)
return _extract_json(response.text)

def generate_glyph(self, prompt: str) -> dict:
"""Generate glyph data from a text prompt."""
response = self.model.generate_content(prompt)
return _extract_json(response.text)


class OpenAIClient:
"""Client for OpenAI ChatGPT API."""

def __init__(self, api_key: str):
from openai import OpenAI
self.client = OpenAI(api_key=api_key)
self.model = "gpt-4o"

def analyze_font_style(self, image_paths: list[str]) -> dict:
"""Analyze font style from sample images."""
messages = [{"role": "system", "content": "You are a professional font designer."}]
content = [{"type": "text", "text": FONT_ANALYSIS_PROMPT}]
for path in image_paths:
img_data = _encode_image(path)
mime = _get_image_mime(path)
content.append({
"type": "image_url",
"image_url": {
"url": f"data:{mime};base64,{img_data}",
},
})
messages.append({"role": "user", "content": content})
response = self.client.chat.completions.create(
model=self.model,
messages=messages,
max_tokens=2000,
)
return _extract_json(response.choices[0].message.content)

def generate_glyph(self, prompt: str) -> dict:
"""Generate glyph data from a text prompt."""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "You are a professional font designer."},
{"role": "user", "content": prompt},
],
max_tokens=4000,
)
return _extract_json(response.choices[0].message.content)


def create_ai_client(provider: str, api_key: str):
"""Factory function to create the appropriate AI client."""
if provider == "gemini":
return GeminiClient(api_key)
elif provider == "openai":
return OpenAIClient(api_key)
else:
raise ValueError(f"Unsupported AI provider: {provider}. Use 'gemini' or 'openai'.")
Loading