font-manager / app.py
yukee1992's picture
Update app.py
7446357 verified
import gradio as gr
import os
import shutil
import tempfile
import json
from datetime import datetime
from huggingface_hub import HfApi, upload_file, list_repo_files, hf_hub_download
from PIL import Image, ImageFont, ImageDraw
import requests
import traceback
print("=" * 60)
print("🚀 FONT MANAGER FOR HF DATASET")
print("=" * 60)
# =============================================
# CONFIGURATION
# =============================================
HF_TOKEN = os.environ.get("HF_TOKEN")
HF_USERNAME = "yukee1992"
FONTS_DATASET = f"{HF_USERNAME}/video-fonts"
PREVIEW_DIR = "/tmp/font_previews"
print(f"📦 Fonts Dataset: {FONTS_DATASET}")
print(f"🔑 HF Token: {'✅ Set' if HF_TOKEN else '❌ Missing'}")
# Create preview directory
os.makedirs(PREVIEW_DIR, exist_ok=True)
# Initialize HF API
api = HfApi(token=HF_TOKEN)
# =============================================
# DATASET FUNCTIONS
# =============================================
def ensure_dataset_exists():
"""Create dataset if it doesn't exist"""
try:
api.dataset_info(FONTS_DATASET)
print(f"✅ Dataset {FONTS_DATASET} exists")
except Exception:
print(f"📦 Creating dataset: {FONTS_DATASET}")
api.create_repo(
repo_id=FONTS_DATASET,
repo_type="dataset",
private=False,
exist_ok=True
)
print(f"✅ Created dataset")
def get_font_list():
"""Get list of all fonts in dataset"""
try:
files = api.list_repo_files(repo_id=FONTS_DATASET, repo_type="dataset")
fonts = []
categories = set()
for file in files:
if file.endswith(('.ttf', '.otf')):
# Parse category from path
parts = file.split('/')
category = parts[0] if len(parts) > 1 else "uncategorized"
categories.add(category)
fonts.append({
"path": file,
"name": os.path.basename(file),
"category": category,
"url": f"https://huggingface.co/datasets/{FONTS_DATASET}/resolve/main/{file}"
})
return fonts, sorted(list(categories))
except Exception as e:
print(f"❌ Error listing fonts: {e}")
return [], []
# =============================================
# FIXED UPLOAD FUNCTION
# =============================================
def upload_font(file_obj, category, font_name, description):
"""Upload a font to the dataset - FIXED for both file objects and paths"""
try:
# Check if file is provided
if file_obj is None:
return "❌ Please select a font file to upload"
# Check if font name is provided
if not font_name or font_name.strip() == "":
return "❌ Please enter a font name"
print(f"📤 Uploading font: {font_name}")
print(f" Category: {category}")
print(f" File object type: {type(file_obj)}")
# Handle different input types
temp_file_path = None
original_filename = "font.ttf"
if hasattr(file_obj, 'read'):
# It's a file-like object (Gradio file object)
print(" Type: File-like object")
with tempfile.NamedTemporaryFile(delete=False, suffix='.ttf') as tmp:
shutil.copyfileobj(file_obj, tmp)
temp_file_path = tmp.name
original_filename = getattr(file_obj, 'name', 'font.ttf')
elif isinstance(file_obj, str):
# It's a string (file path)
print(" Type: String path")
if os.path.exists(file_obj):
temp_file_path = file_obj
original_filename = os.path.basename(file_obj)
else:
return f"❌ File not found at path: {file_obj}"
else:
return f"❌ Unexpected file type: {type(file_obj)}"
print(f" Original filename: {original_filename}")
print(f" Temp path: {temp_file_path}")
# Determine file extension from original filename
if original_filename.endswith('.otf'):
ext = '.otf'
else:
ext = '.ttf'
# Create safe filename
safe_name = font_name.replace(' ', '-').lower()
# Remove any non-alphanumeric characters except hyphen
safe_name = ''.join(c for c in safe_name if c.isalnum() or c == '-')
filename = f"{safe_name}{ext}"
# Path in dataset: category/filename
dataset_path = f"{category}/{filename}"
print(f" Dataset path: {dataset_path}")
# Upload to dataset
api.upload_file(
path_or_fileobj=temp_file_path,
path_in_repo=dataset_path,
repo_id=FONTS_DATASET,
repo_type="dataset"
)
print(f"✅ File uploaded successfully")
# Update metadata
try:
update_font_metadata(category, filename, font_name, description)
except Exception as e:
print(f"⚠️ Metadata update failed: {e}")
# Clean up temp file if we created one
if temp_file_path and hasattr(file_obj, 'read') and os.path.exists(temp_file_path):
os.unlink(temp_file_path)
return f"✅ Successfully uploaded {font_name} to {category}/"
except Exception as e:
print(f"❌ Upload error: {str(e)}")
traceback.print_exc()
return f"❌ Upload failed: {str(e)}"
def update_font_metadata(category, filename, font_name, description):
"""Update the metadata JSON file for the category"""
try:
metadata_path = f"{category}/metadata.json"
existing_metadata = []
# Try to download existing metadata
try:
metadata_file = hf_hub_download(
repo_id=FONTS_DATASET,
filename=metadata_path,
repo_type="dataset",
token=HF_TOKEN
)
with open(metadata_file, 'r', encoding='utf-8') as f:
existing_metadata = json.load(f)
except:
existing_metadata = []
# Add new font metadata
existing_metadata.append({
"name": font_name,
"file": filename,
"description": description,
"category": category,
"uploaded_at": datetime.now().isoformat()
})
# Save updated metadata
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json', encoding='utf-8') as f:
json.dump(existing_metadata, f, indent=2, ensure_ascii=False)
meta_path = f.name
# Upload metadata
api.upload_file(
path_or_fileobj=meta_path,
path_in_repo=metadata_path,
repo_id=FONTS_DATASET,
repo_type="dataset"
)
os.unlink(meta_path)
print(f"✅ Metadata updated for {category}")
except Exception as e:
print(f"⚠️ Metadata error: {e}")
raise
def delete_font(font_path):
"""Delete a font from the dataset"""
try:
api.delete_file(
path_in_repo=font_path,
repo_id=FONTS_DATASET,
repo_type="dataset"
)
return f"✅ Deleted: {font_path}"
except Exception as e:
return f"❌ Delete failed: {str(e)}"
def generate_preview(font_url, text="ABCDEFGHIJKLMNOPQRSTUVWXYZ\nabcdefghijklmnopqrstuvwxyz\n0123456789\n中文字体测试", font_size=36):
"""Generate a preview image for a font"""
try:
# Download font file
response = requests.get(font_url)
if response.status_code != 200:
return None
font_path = os.path.join(PREVIEW_DIR, f"temp_{datetime.now().timestamp()}.ttf")
with open(font_path, 'wb') as f:
f.write(response.content)
# Create preview image
img = Image.new('RGB', (800, 400), color='white')
d = ImageDraw.Draw(img)
try:
font = ImageFont.truetype(font_path, font_size)
except:
font = ImageFont.load_default()
# Draw text
d.text((50, 50), text, fill='black', font=font)
# Save preview
preview_path = os.path.join(PREVIEW_DIR, f"preview_{datetime.now().timestamp()}.png")
img.save(preview_path)
# Clean up font file
os.unlink(font_path)
return preview_path
except Exception as e:
print(f"❌ Preview failed: {e}")
return None
# =============================================
# GRADIO INTERFACE
# =============================================
# Ensure dataset exists
ensure_dataset_exists()
# Get initial font list
fonts, categories = get_font_list()
with gr.Blocks(title="Font Manager", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🎨 Font Manager for Video Projects")
gr.Markdown(f"### Dataset: `{FONTS_DATASET}`")
with gr.Tabs():
# =====================================
# TAB 1: UPLOAD FONTS
# =====================================
with gr.TabItem("📤 Upload Fonts"):
gr.Markdown("### Upload New Fonts")
with gr.Row():
with gr.Column(scale=1):
font_file = gr.File(
label="Select Font File",
file_types=[".ttf", ".otf"],
file_count="single"
)
font_name = gr.Textbox(
label="Font Name",
placeholder="e.g., TikTok Sans Bold",
info="Enter a descriptive name for the font"
)
font_category = gr.Dropdown(
label="Category",
choices=["english", "chinese", "japanese", "korean", "display", "handwriting", "other"],
value="english"
)
font_description = gr.Textbox(
label="Description (Optional)",
placeholder="Describe the font style...",
lines=2
)
with gr.Row():
upload_btn = gr.Button("🚀 Upload Font", variant="primary")
clear_btn = gr.Button("🗑️ Clear", variant="secondary")
upload_output = gr.Textbox(label="Upload Status", interactive=False)
with gr.Column(scale=1):
gr.Markdown("### Preview Area")
preview_image = gr.Image(label="Font Preview", height=300)
preview_text = gr.Textbox(
label="Preview Text",
value="ABCDEFGHIJKLMNOPQRSTUVWXYZ\nabcdefghijklmnopqrstuvwxyz\n0123456789\n中文字体测试",
lines=3
)
preview_size = gr.Slider(
label="Preview Size",
minimum=12,
maximum=72,
value=36,
step=2
)
preview_btn = gr.Button("👁️ Preview Font")
# Preview function
def update_preview(file, text, size):
if file is None:
return None
try:
# Handle different file types
if hasattr(file, 'read'):
# It's a file-like object
with tempfile.NamedTemporaryFile(delete=False, suffix='.ttf') as tmp:
shutil.copyfileobj(file, tmp)
tmp_path = tmp.name
elif isinstance(file, str) and os.path.exists(file):
# It's a file path
tmp_path = file
else:
return None
# Create preview image
img = Image.new('RGB', (800, 400), color='white')
d = ImageDraw.Draw(img)
try:
font = ImageFont.truetype(tmp_path, size)
except:
font = ImageFont.load_default()
d.text((50, 50), text, fill='black', font=font)
preview_path = os.path.join(PREVIEW_DIR, f"preview_{datetime.now().timestamp()}.png")
img.save(preview_path)
# Clean up temp file if we created one
if hasattr(file, 'read') and os.path.exists(tmp_path):
os.unlink(tmp_path)
return preview_path
except Exception as e:
print(f"Preview error: {e}")
return None
preview_btn.click(
fn=update_preview,
inputs=[font_file, preview_text, preview_size],
outputs=[preview_image]
)
# Upload function
upload_btn.click(
fn=upload_font,
inputs=[font_file, font_category, font_name, font_description],
outputs=[upload_output]
).then(
fn=lambda: (None, "", "", "", None),
inputs=[],
outputs=[font_file, font_name, font_description, upload_output, preview_image]
)
# Clear button
clear_btn.click(
fn=lambda: (None, "", "", "", None),
inputs=[],
outputs=[font_file, font_name, font_description, upload_output, preview_image]
)
# =====================================
# TAB 2: BROWSE FONTS
# =====================================
with gr.TabItem("🔍 Browse Fonts"):
gr.Markdown("### Font Library")
refresh_btn = gr.Button("🔄 Refresh List", variant="secondary")
category_filter_dropdown = gr.Dropdown(
label="Filter by Category",
choices=["all"] + categories,
value="all"
)
font_gallery = gr.Gallery(
label="Fonts",
columns=4,
object_fit="contain",
height="auto"
)
with gr.Row():
selected_font = gr.Dropdown(
label="Select Font",
choices=[f["name"] for f in fonts],
interactive=True
)
delete_btn = gr.Button("🗑️ Delete Selected Font", variant="stop")
delete_output = gr.Textbox(label="Delete Status", interactive=False)
font_preview_big = gr.Image(label="Font Preview", height=300)
font_info = gr.JSON(label="Font Details")
def load_font_gallery(filter_cat):
fonts, _ = get_font_list()
images = []
for font in fonts:
if filter_cat != "all" and font["category"] != filter_cat:
continue
preview = generate_preview(font["url"], font_size=24)
if preview:
images.append(preview)
return images
def show_font_details(font_name):
fonts, _ = get_font_list()
for font in fonts:
if font["name"] == font_name:
preview = generate_preview(font["url"], font_size=36)
return preview, font
return None, {}
def delete_selected_font(font_name):
fonts, _ = get_font_list()
for font in fonts:
if font["name"] == font_name:
return delete_font(font["path"])
return "❌ Font not found"
def update_font_dropdown():
fonts, _ = get_font_list()
return gr.Dropdown(choices=[f["name"] for f in fonts])
refresh_btn.click(
fn=load_font_gallery,
inputs=[category_filter_dropdown],
outputs=[font_gallery]
).then(
fn=update_font_dropdown,
outputs=[selected_font]
)
category_filter_dropdown.change(
fn=load_font_gallery,
inputs=[category_filter_dropdown],
outputs=[font_gallery]
)
selected_font.change(
fn=show_font_details,
inputs=[selected_font],
outputs=[font_preview_big, font_info]
)
delete_btn.click(
fn=delete_selected_font,
inputs=[selected_font],
outputs=[delete_output]
).then(
fn=load_font_gallery,
inputs=[category_filter_dropdown],
outputs=[font_gallery]
).then(
fn=update_font_dropdown,
outputs=[selected_font]
)
# =====================================
# TAB 3: CATEGORY MANAGEMENT
# =====================================
with gr.TabItem("📁 Categories"):
gr.Markdown("### Manage Font Categories")
def get_categories():
_, cats = get_font_list()
return {"categories": cats}
current_categories = gr.JSON(
label="Current Categories",
value=get_categories()
)
with gr.Row():
new_category = gr.Textbox(
label="New Category Name",
placeholder="e.g., handwriting, decorative"
)
add_category_btn = gr.Button("➕ Add Category", variant="primary")
add_category_output = gr.Textbox(label="Result", interactive=False)
def add_category(cat_name):
if not cat_name:
return "❌ Please enter a category name"
try:
placeholder_path = f"{cat_name}/.keep"
with tempfile.NamedTemporaryFile(mode='w', delete=False) as f:
f.write("# This directory exists")
temp_path = f.name
api.upload_file(
path_or_fileobj=temp_path,
path_in_repo=placeholder_path,
repo_id=FONTS_DATASET,
repo_type="dataset"
)
os.unlink(temp_path)
return f"✅ Category '{cat_name}' created"
except Exception as e:
return f"❌ Failed: {str(e)}"
add_category_btn.click(
fn=add_category,
inputs=[new_category],
outputs=[add_category_output]
).then(
fn=None,
inputs=None,
outputs=None,
js="() => { setTimeout(() => window.location.reload(), 2000); }"
)
# =====================================
# TAB 4: DATASET INFO
# =====================================
with gr.TabItem("ℹ️ Dataset Info"):
gr.Markdown("### Dataset Information")
def get_dataset_stats():
fonts, cats = get_font_list()
total_size = 0
category_counts = {}
for cat in cats:
category_counts[cat] = len([f for f in fonts if f["category"] == cat])
for font in fonts:
try:
response = requests.head(font["url"])
total_size += int(response.headers.get('content-length', 0))
except:
pass
return {
"total_fonts": len(fonts),
"categories": cats,
"fonts_per_category": category_counts,
"total_size_mb": round(total_size / (1024 * 1024), 2),
"dataset_url": f"https://huggingface.co/datasets/{FONTS_DATASET}"
}
stats_btn = gr.Button("📊 Refresh Stats")
stats_display = gr.JSON(label="Dataset Statistics")
stats_btn.click(
fn=get_dataset_stats,
inputs=[],
outputs=[stats_display]
)
gr.Markdown("### Quick Links")
gr.Markdown(f"- [View Dataset on HF](https://huggingface.co/datasets/{FONTS_DATASET})")
# =========================================
# INITIAL LOAD
# =========================================
demo.load(
fn=lambda: load_font_gallery("all"),
inputs=[],
outputs=[font_gallery]
)
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860)