Spaces:
Running on Zero
Running on Zero
| 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 | |
| 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 "", "", "" | |
| 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, | |
| ) |