converter / app.py
usmannawaz's picture
Update app.py
887ffba verified
Raw
History Blame Contribute Delete
16 kB
import tempfile
import zipfile
from pathlib import Path
import gradio as gr
import spaces
REPLACEMENTS1 = {
'': 'и',
'': '҃',
'': '҃',
'': 'ч',
'': 'Ѥ',
'': 'н',
'': '҇',
'': '҃',
'': '~',
'': 'ⷦ҇',
'': ' ⷮ',
'': '҆̀',
'': '҆̀',
'': 'ⷹ',
'': 'ч',
'': 'ⷹ',
'': ':',
'': 'Чⷹ',
'': 'о',
'': 'с',
'': 'е',
'': '͠',
'': '·̀',
'': '·̀',
'ⷭⷭ': '҇',
'': '҇',
'': '҆',
'': 'ⷩ',
'': 'ꙶ',
'': 'оу',
'': 'ꙁ',
'': 'ⷿ',
'': 'ⷿ',
'': 'ѧ',
' ': 'с',
'': 'ѱ',
'': 'Ѥ',
'': 'р҃',
'꙳': 'у',
'꙯': '҃',
'': '͡',
'': '͠',
'ъ': 'уъ',
'': 'у',
'ⷣ': 'ⷣ͡',
'ⷮ': 'ⷣ͡',
'ⷯ': '̈͠',
'': 'ꙩ́',
'ѧ': 'уѧ',
'': '҆',
'': 'ꙺ',
'': 'Ю',
'': 'ꙻ',
'ⷤ': '',
}
REPLACEMENTS2 = {
'0': '́',
'1': '́',
'2': '̀',
'3': '҆',
'4': '҆́',
'5': '҆̀',
'6': '̑',
# 'б': 'Б',
'7': '҃',
'8': 'ⸯ',
'9': 'Җ҃',
'™': '҃'
}
REPLACEMENTS3 = {
'': ' ',
'': 'А',
'': 'Б',
'': 'В',
'': 'Г',
'': 'Д',
'': 'Е',
'': 'Ꙁ',
'': 'И',
'': 'І',
'': 'К',
'': 'Л',
'': 'М',
'': 'Н',
'': 'О',
'': 'П',
'': 'Р',
'': 'С',
'': 'Т',
'': 'ОУ',
'': 'Х',
'': 'Ч',
'': 'Ъ',
'': 'Ѣ',
'': 'ꙗ',
'': 'а',
'': 'б',
'': 'в',
'': 'г',
'': 'д',
'': 'е',
'': 'ж',
'': 'ꙅ',
'': 'ꙁ',
'': 'и',
'': 'І',
'': 'к',
'': 'л',
'': 'м',
'': 'н',
'': 'о',
'': 'п',
'': 'р',
'': 'с',
'': 'т',
'': 'оу',
'': 'оу',
'': 'ф',
'': 'х',
'': 'ѡ',
'': 'ѡт',
'': 'ц',
'': 'ч',
'': 'ш',
'': 'шт',
'': 'щ',
'': 'ъ',
'': 'ꙑ',
'': 'ы',
'': 'ь',
'': 'ѣ',
'': 'ю',
'': 'ꙗ',
'': 'ѥ',
'': 'ѧ',
'': 'ѩ',
'': 'ѫ',
'': 'ѭ',
'': 'ѳ',
'': 'у',
'': 'ⷡ',
'': 'ⷣ',
'': 'ⷷ',
'': 'ⷩ',
'': 'ⷪ',
'': 'ⷭ',
'': 'ⷰ',
'': 'ⷽ',
'': 'ʾ',
'': '̑',
'': '̾',
'': '҃',
'': '꙯',
'': '̀',
'': '̅',
'': '҇',
'': '҇',
'': '҃',
'': '·',
'': '⁘',
'': '⁖',
'': ':·',
'': '⁘',
'': '҃',
'': '⊂',
}
REPLACEMENTS4 = {
'1': '̋',
'2': '̀',
'3': '҆',
'7': '҃',
'9': 'ж҃',
'0': '́о',
'l': 'л҃',
'†': 'а̑',
'‡': 'І̑',
'‰': 'Ѧ̑',
'‹': 'І҃',
'№': 'л҃',
'D': 'д ⷭ҇',
'#': '̑',
'¦': 'х҃',
'¤': '҂',
'™': 'т҃',
'§': 'Ч҃',
}
REPLACEMENTS5 = {
'A': 'а̀',
'B': 'ѣ̑',
'C': 'ⷭ҇',
'D': 'дⷭ҇',
'E': 'ѐ',
'F': 'ѳ',
'G': 'г҃',
'H': 'ѡ́',
'I': 'І',
'J': 'і̀',
'K': 'Ꙗ҆',
'L': 'лⷣ',
'M': 'Ѷ',
'N': 'Ѻ҆',
'O': 'Ѻ',
'P': 'Ѱ',
'Q': 'Ѽ҃',
'R': 'р҃',
'S': 'ѧ̀',
'T': 'Ѿ',
'U': 'Оу',
'V': 'Ѵ',
'W': 'Ѡ',
'X': 'Ѯ',
'Y': 'ꙋ̀',
'Z': 'Ѧ',
'a': 'а́',
'b': 'ⷪ҇',
'c': 'ⷭ҇',
'd': 'ⷣ',
'e': 'е́',
'f': 'ѳ',
'g': 'ⷢ҇',
'h': 'ы́',
'i': 'і',
'j': 'і́',
'k': 'ꙗ҆',
'l': 'л҃',
'm': 'ѷ',
'n': 'ѻ҆',
'o': 'ѻ',
'p': 'ѱ',
'q': 'ѽ',
'r': 'рⷭ҇',
's': 'ѧ́',
't': 'ѿ',
'u': 'Оу',
'v': 'ѵ',
'w': 'ѡ',
'x': 'ѯ',
'y': 'ꙋ́',
'z': 'ѧ',
'±': 'Ꙗ҆̀',
'}': 'и҃',
'4': '҆́',
'5': '҆̀',
'6': '̑',
'8': '̾',
'1': '́',
'2': '̀',
'3': '̑',
'7': '҃',
'9': 'ж҃',
'0': 'о́',
'™': 'т҃',
'†': 'а̑',
'‡': 'І̑',
'‰': 'Ѧ̑',
'‹': 'І҃',
'№': 'л҃',
'#': '̑',
'¦': '҃х',
'¤': '҂',
'§': 'Ч҃',
}
DATABASE_MAPPINGS = {
"Cyrillomethodiana": REPLACEMENTS1,
"Азбука веры": REPLACEMENTS2,
"Manuscripts.ru": REPLACEMENTS3,
"Православие и мир": REPLACEMENTS4,
"Минея": REPLACEMENTS5,
}
FONT_DIR = Path("fonts")
SUPPORTED_FONT_EXTENSIONS = {".otf", ".ttf", ".woff", ".woff2"}
if FONT_DIR.exists():
gr.set_static_paths(paths=[FONT_DIR])
def discover_fonts():
"""Return {display_name: CSS font-family} for local font files."""
fonts = {"Default Unicode": "Default Unicode"}
if not FONT_DIR.exists():
return fonts
for font_path in sorted(FONT_DIR.iterdir()):
if (
font_path.is_file()
and font_path.suffix.lower() in SUPPORTED_FONT_EXTENSIONS
):
display_name = font_path.stem.replace("_", " ")
fonts[display_name] = font_path.stem
return fonts
AVAILABLE_FONTS = discover_fonts()
def font_format(extension):
formats = {
".otf": "opentype",
".ttf": "truetype",
".woff": "woff",
".woff2": "woff2",
}
return formats[extension.lower()]
def build_font_face_css():
rules = []
if not FONT_DIR.exists():
return ""
for font_path in sorted(FONT_DIR.iterdir()):
if (
not font_path.is_file()
or font_path.suffix.lower() not in SUPPORTED_FONT_EXTENSIONS
):
continue
family = font_path.stem
file_format = font_format(font_path.suffix)
rules.append(
f"""
@font-face {{
font-family: "{family}";
src: url("/gradio_api/file=fonts/{font_path.name}")
format("{file_format}");
font-style: normal;
font-weight: normal;
font-display: swap;
}}
"""
)
return "\n".join(rules)
def build_css():
font_faces = build_font_face_css()
return f"""
{font_faces}
body {{
background: linear-gradient(
135deg,
#eaf3ff 0%,
#ffffff 48%,
#dbeafe 100%
);
}}
.gradio-container {{
max-width: 1000px !important;
width: 80% !important;
margin: auto !important;
font-family: Arial, Helvetica, sans-serif !important;
}}
#main-card {{
background: #ffffff;
border: 1px solid #bfdbfe;
border-radius: 26px;
padding: 30px;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.16);
}}
#title {{
text-align: center;
color: #020617;
font-size: 2.4rem;
font-weight: 900;
margin-bottom: 0.25rem;
}}
#subtitle {{
text-align: center;
color: #1e40af;
font-size: 1.05rem;
line-height: 1.55;
margin-bottom: 1.6rem;
}}
textarea,
input,
select {{
border-radius: 16px !important;
border: 1.5px solid #2563eb !important;
background: #ffffff !important;
color: #020617 !important;
}}
label {{
color: #020617 !important;
font-weight: 800 !important;
}}
button {{
background: linear-gradient(
90deg,
#020617,
#1d4ed8
) !important;
color: #ffffff !important;
border: none !important;
border-radius: 16px !important;
padding: 13px 24px !important;
font-weight: 900 !important;
}}
#output-box textarea {{
background: #f8fbff !important;
font-family: "Noto Sans", "DejaVu Sans", Arial, sans-serif !important;
font-size: 25px !important;
line-height: 1.8 !important;
}}
/* Hide Gradio footer: Use via API, Built with Gradio, Settings */
footer,
.gradio-container > footer,
.api,
.settings,
[class*="footer"],
a[href*="gradio.app"],
button[aria-label="Settings"],
button[aria-label="Use via API"],
button[aria-label*="API"],
button[aria-label*="Settings"] {{
display: none !important;
}}
"""
CUSTOM_CSS = build_css()
FONT_CHANGE_JS = r"""
(fontName) => {
const fallback = '"Noto Sans", "DejaVu Sans", Arial, sans-serif';
const selectedFont =
fontName && fontName !== "Default Unicode"
? `"${fontName}", ${fallback}`
: fallback;
const applyFont = () => {
const outputBox = document.querySelector(
"#output-box textarea"
);
if (outputBox) {
outputBox.style.setProperty(
"font-family",
selectedFont,
"important"
);
outputBox.style.setProperty(
"font-size",
"25px",
"important"
);
outputBox.style.setProperty(
"line-height",
"1.8",
"important"
);
}
};
applyFont();
setTimeout(applyFont, 100);
setTimeout(applyFont, 400);
return fontName;
}
"""
def apply_mapping(text, mapping):
ordered_mapping = sorted(
mapping.items(),
key=lambda item: len(item[0]),
reverse=True,
)
for old_char, new_char in ordered_mapping:
text = text.replace(old_char, new_char)
return text
@spaces.GPU(duration=60)
def convert_text(input_text, database_name):
input_text = str(input_text)
if not input_text.strip():
return "", "Please paste text to process."
mapping = DATABASE_MAPPINGS.get(database_name)
if mapping is None:
return "", "Please select a valid database."
converted_text = apply_mapping(input_text, mapping)
return converted_text, f"Converted using {database_name}."
def clear_text():
return "", "", ""
@spaces.GPU(duration=60)
def convert_txt_files(uploaded_files, database_name):
if not uploaded_files:
return None, "Please upload at least one TXT file."
mapping = DATABASE_MAPPINGS.get(database_name)
if mapping is None:
return None, "Please select a valid database."
temp_directory = Path(
tempfile.mkdtemp(prefix="ocs_converter_")
)
output_directory = temp_directory / "converted"
output_directory.mkdir(parents=True, exist_ok=True)
converted_count = 0
skipped_files = []
for uploaded_file in uploaded_files:
input_path = Path(uploaded_file)
if input_path.suffix.lower() != ".txt":
skipped_files.append(input_path.name)
continue
try:
input_text = input_path.read_text(encoding="utf-8")
except UnicodeDecodeError:
skipped_files.append(input_path.name)
continue
converted_text = apply_mapping(
input_text,
mapping,
)
output_path = output_directory / input_path.name
output_path.write_text(
converted_text,
encoding="utf-8",
)
converted_count += 1
if converted_count == 0:
return None, "No UTF-8 TXT files could be processed."
zip_path = temp_directory / "converted_text_files.zip"
with zipfile.ZipFile(
zip_path,
mode="w",
compression=zipfile.ZIP_DEFLATED,
) as archive:
for output_path in output_directory.glob("*.txt"):
archive.write(
output_path,
arcname=output_path.name,
)
message = (
f"Converted {converted_count} file(s) "
f"using {database_name}."
)
if skipped_files:
message += " Skipped: " + ", ".join(skipped_files)
return str(zip_path), message
with gr.Blocks(
title="Converter for Old Church Slavonic Demo",
) as demo:
with gr.Column(elem_id="main-card"):
gr.Markdown(
"# Converter for Old Church Slavonic Demo",
elem_id="title",
)
gr.Markdown(
"Select a database and convert "
"Old Church Slavonic text.",
elem_id="subtitle",
)
database_input = gr.Dropdown(
label="Database",
choices=list(DATABASE_MAPPINGS.keys()),
value="Cyrillomethodiana",
interactive=True,
)
font_input = gr.Dropdown(
label="Output font",
choices=list(AVAILABLE_FONTS.keys()),
value="Default Unicode",
interactive=True,
info=(
" "
),
)
font_state = gr.Textbox(
value="Default Unicode",
visible=False,
)
with gr.Tab("Text conversion"):
input_text = gr.Textbox(
label="Input text",
lines=7,
placeholder="Paste text here...",
)
with gr.Row():
convert_button = gr.Button(
"Convert text",
variant="primary",
)
clear_button = gr.Button("Clear")
output_text = gr.Textbox(
label="Converted text",
lines=8,
interactive=False,
elem_id="output-box",
)
text_status = gr.Textbox(
label="Status",
lines=1,
interactive=False,
)
with gr.Tab("TXT file conversion"):
file_input = gr.File(
label="Upload UTF-8 TXT files",
file_types=[".txt"],
file_count="multiple",
type="filepath",
)
file_convert_button = gr.Button(
"Convert TXT files",
variant="primary",
)
file_output = gr.File(
label="Download converted ZIP",
interactive=False,
)
file_status = gr.Textbox(
label="Status",
lines=3,
interactive=False,
)
font_input.change(
fn=None,
inputs=[font_input],
outputs=[font_state],
js=FONT_CHANGE_JS,
)
convert_button.click(
fn=convert_text,
inputs=[
input_text,
database_input,
],
outputs=[
output_text,
text_status,
],
).then(
fn=None,
inputs=[font_input],
outputs=[font_state],
js=FONT_CHANGE_JS,
)
input_text.submit(
fn=convert_text,
inputs=[
input_text,
database_input,
],
outputs=[
output_text,
text_status,
],
).then(
fn=None,
inputs=[font_input],
outputs=[font_state],
js=FONT_CHANGE_JS,
)
clear_button.click(
fn=clear_text,
inputs=[],
outputs=[
input_text,
output_text,
text_status,
],
)
file_convert_button.click(
fn=convert_txt_files,
inputs=[
file_input,
database_input,
],
outputs=[
file_output,
file_status,
],
)
demo.load(
fn=None,
inputs=[font_input],
outputs=[font_state],
js=FONT_CHANGE_JS,
)
demo.queue()
demo.launch(
server_name="0.0.0.0",
server_port=7860,
ssr_mode=False,
theme=gr.themes.Soft(
primary_hue="blue",
secondary_hue="sky",
neutral_hue="slate",
),
css=CUSTOM_CSS,
)