# --- Import necessary libraries --- import streamlit as st import os import sys import time import io # Needed for handling file streams in memory from pathlib import Path try: import google.generativeai as genai from google.api_core import exceptions as google_exceptions except ImportError: st.error("Error: google-generativeai library not found. Please install it: `pip install google-generativeai`") st.stop() # --- Configuration --- # GEMINI_API_KEY is handled via Streamlit secrets MODEL_NAME = "gemini-1.5-pro" # Or "gemini-1.5-flash-latest" etc. SAFETY_SETTINGS = [ {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_MEDIUM_AND_ABOVE"}, ] API_CALL_DELAY = 0.5 # Optional delay in seconds between API calls LANGUAGES = ["russian", "romanian", "english", "german", "french", "spanish"] # --- Default File Configuration --- DEFAULT_TXT_PATH = Path(__file__).parent / "default_pharma.txt" # <--- CHANGE FILENAME IF NEEDED DEFAULT_TXT_LANGUAGE = "russian" # <--- CHANGE LANGUAGE IF NEEDED # --- Core Functions --- def load_css(file_name): """Loads a CSS file and injects it into the Streamlit app.""" try: css_path = Path(__file__).parent / file_name with open(css_path) as f: st.markdown(f'', unsafe_allow_html=True) except FileNotFoundError: st.error(f"CSS file not found: {file_name}. Make sure it's in the same directory as app.py.") except Exception as e: st.error(f"Error loading CSS file {file_name}: {e}") # Global variable to hold the configured model gemini_model = None def configure_gemini(): """Configures the Gemini client using Streamlit secrets.""" global gemini_model api_key = st.secrets.get("GOOGLE_API_KEY") if not api_key: st.error("Error: GOOGLE_API_KEY not found in Streamlit secrets.") st.info("Please add your Gemini API Key to the Streamlit secrets manager.") gemini_model = None return False try: genai.configure(api_key=api_key) gemini_model = genai.GenerativeModel(MODEL_NAME, safety_settings=SAFETY_SETTINGS) # st.sidebar.success("Gemini client configured successfully.") # Optional feedback return True except Exception as e: st.error(f"Error configuring Gemini: {e}") gemini_model = None # Ensure model is None if config fails return False def extract_text_from_txt(txt_file_obj): """Reads text content from a TXT file object (BytesIO or file).""" try: # Read as bytes first, then decode smartly content_bytes = txt_file_obj.read() try: # Try UTF-8 first text = content_bytes.decode('utf-8') except UnicodeDecodeError: try: # Fallback to latin-1 (or cp1252 for Windows files) text = content_bytes.decode('latin-1') st.warning("Decoded TXT file using 'latin-1'. Some characters might be misinterpreted if the encoding is different.") except Exception as decode_err: st.error(f"Error decoding TXT file: {decode_err}. Please ensure it's UTF-8 or Latin-1 encoded.") return None st.info(f"Successfully read text file.") return text except Exception as e: st.error(f"An error occurred reading the TXT file: {e}") return None def translate_text_gemini(text, source_lang, target_lang, log_prefix="Text block"): """Translates text using the Gemini API.""" global gemini_model if gemini_model is None: st.error("Gemini model not configured. Cannot translate.") return None # Indicate failure if not text or not text.strip(): st.warning(f"{log_prefix}: Input text is empty or whitespace only. Skipping translation.") return "" # Nothing to translate prompt = f"""Translate the following text from {source_lang} to {target_lang}. Preserve paragraph breaks where appropriate. Output *only* the translated text, without any introductory phrases like "Here is the translation:", or any explanations or markdown formatting. If the input text is empty or nonsensical for translation, output nothing. Text to translate: --- {text} --- Translation:""" try: # Optional: Add delay between calls if API_CALL_DELAY > 0: time.sleep(API_CALL_DELAY) response = gemini_model.generate_content(prompt) # Robust check for content translated_text = "" if response.parts: translated_text = "".join(part.text for part in response.parts).strip() elif hasattr(response, 'text'): # Fallback for simpler response structures translated_text = response.text.strip() # Handle potential blocking or empty responses if not translated_text: if response.prompt_feedback and response.prompt_feedback.block_reason: st.warning(f"{log_prefix}: Translation blocked. Reason: {response.prompt_feedback.block_reason}") return f"[Translation blocked on {log_prefix}: {response.prompt_feedback.block_reason}]" else: finish_reason = response.candidates[0].finish_reason if response.candidates else 'UNKNOWN' if finish_reason == 'STOP': if text.strip(): # Only warn if input wasn't just whitespace/empty st.warning(f"{log_prefix}: Received no translated content (finish reason STOP). Original text might have been empty or untranslatable.") return "" # Return empty if no content and no blocking else: st.warning(f"{log_prefix}: Received empty response from API. Finish Reason: {finish_reason}, Feedback: {response.prompt_feedback}") return f"[Translation failed on {log_prefix}: Empty API response]" return translated_text except google_exceptions.ResourceExhausted as e: st.error(f"{log_prefix}: Error: Gemini API quota exceeded: {e}. Consider increasing API_CALL_DELAY or checking your quota.") return f"[Translation failed on {log_prefix}: Quota Exceeded - {e}]" except google_exceptions.InvalidArgument as e: st.error(f"{log_prefix}: Error: Invalid argument passed to Gemini API: {e}") return f"[Translation failed on {log_prefix}: Invalid Argument - {e}]" except Exception as e: st.error(f"{log_prefix}: Error during Gemini API call: {e}") return f"[Translation failed on {log_prefix}: {e}]" def create_txt_from_text(translated_text): """Creates a TXT file content in memory.""" try: txt_buffer = io.StringIO() txt_buffer.write(translated_text) txt_buffer.seek(0) # We need BytesIO for download button, so encode it txt_bytes_buffer = io.BytesIO(txt_buffer.getvalue().encode('utf-8')) st.info("Translated TXT file content prepared.") return txt_bytes_buffer except Exception as e: st.error(f"Error creating output TXT: {e}") return None # --- Load CSS and Fonts --- st.markdown(""" """, unsafe_allow_html=True) load_css("style.css") # --- Streamlit App UI --- st.title("📄 TXT Document Translator") # Configure Gemini (attempt on each run, handles secret check) gemini_configured = configure_gemini() # --- File Input Options --- st.sidebar.subheader("📁 Input File") # Check if default language is valid if DEFAULT_TXT_LANGUAGE not in LANGUAGES: st.sidebar.error(f"Configuration Error: Default language '{DEFAULT_TXT_LANGUAGE}' is not in the available LANGUAGES list.") use_default_disabled = True default_label = f"Default TXT option disabled (invalid language)" else: use_default_disabled = False default_label = f"Use default {DEFAULT_TXT_LANGUAGE.capitalize()} TXT file ({DEFAULT_TXT_PATH.name})" use_default = st.sidebar.checkbox( default_label, value=False, key="use_default_cb", disabled=use_default_disabled ) uploaded_file = None source_lang_selected = None if use_default and not use_default_disabled: if not DEFAULT_TXT_PATH.exists(): st.sidebar.error(f"Default TXT file '{DEFAULT_TXT_PATH.name}' not found in the app directory!") # Keep source_lang_selected as None to prevent translation attempt else: st.sidebar.info(f"Using default file: `{DEFAULT_TXT_PATH.name}`") source_lang_selected = DEFAULT_TXT_LANGUAGE # Set source language automatically st.sidebar.markdown(f"*(Source Language: **{source_lang_selected.capitalize()}**)*") else: uploaded_file = st.sidebar.file_uploader( "Or, upload your TXT file", type=["txt"], accept_multiple_files=False, key="file_uploader_txt" ) if uploaded_file: # Dropdown for source language ONLY if uploading st.sidebar.markdown("👇 Select the **source** language of your uploaded file:") source_lang_selected = st.sidebar.selectbox( "Source Language", options=[""] + LANGUAGES, # Add empty option for prompt index=0, # Default to empty key="source_lang_uploader" ) if not source_lang_selected: st.sidebar.warning("Please select the source language of your document.") elif not use_default: st.sidebar.info("Select the default option or upload a TXT file.") st.sidebar.markdown("---") # Separator # --- Target Language Selection --- st.sidebar.subheader("🎯 Target Language") target_lang_selected = None # Ensure a source is defined before showing target selection if source_lang_selected: # Filter out the selected source language for the target options available_target_langs = [lang for lang in LANGUAGES if lang != source_lang_selected] if available_target_langs: target_lang_selected = st.sidebar.selectbox( "Translate To", options=[""] + available_target_langs, # Exclude source lang index=0, # Default to empty key="target_lang", help="Select the language you want to translate the document into." ) if not target_lang_selected: st.sidebar.warning("Please select the target language.") else: st.sidebar.warning("No other languages available for translation target.") elif use_default and not DEFAULT_TXT_PATH.exists(): st.sidebar.info("Cannot select target language: Default file missing.") elif use_default_disabled: st.sidebar.info("Cannot select target language: Default file configuration error.") elif uploaded_file and not source_lang_selected: st.sidebar.info("Select the source language first.") else: st.sidebar.info("Select or upload a file and its source language first.") st.sidebar.markdown("---") # Separator # --- Translate Button --- # Determine if input conditions are met input_ready = (use_default and source_lang_selected and DEFAULT_TXT_PATH.exists()) or \ (uploaded_file and source_lang_selected) can_translate = gemini_configured and input_ready and target_lang_selected translate_button = st.sidebar.button("Translate Document", disabled=not can_translate) # Provide feedback on why the button might be disabled if not gemini_configured: st.sidebar.error("Translation disabled: Gemini not configured (check API key in secrets).") elif not input_ready: if use_default and not DEFAULT_TXT_PATH.exists(): st.sidebar.markdown("_(Cannot translate: Default file is missing)_") elif use_default and not source_lang_selected: # Should not happen if lang is valid, but for safety st.sidebar.markdown("_(Cannot translate: Default file language error)_") elif not use_default and not uploaded_file: st.sidebar.markdown("_(Upload a TXT file or select default to enable translation)_") elif not use_default and uploaded_file and not source_lang_selected: st.sidebar.markdown("_(Select source language to enable translation)_") elif not target_lang_selected: st.sidebar.markdown("_(Select target language to enable translation)_") # --- Main Area for Processing and Results --- if translate_button and can_translate: # Double check conditions st.subheader("🚀 Translation Progress") output_buffer = None output_filename = "translation_failed.txt" # Default filename input_data = None input_filename_for_output = "default" # Default for output name generation with st.spinner("Processing... Please wait."): # 1. Get Input Data if use_default: try: with open(DEFAULT_TXT_PATH, "rb") as f: input_data = io.BytesIO(f.read()) input_filename_for_output = DEFAULT_TXT_PATH.name st.write(f"Processing default file: {input_filename_for_output} (TXT)") except Exception as e: st.error(f"Error reading default TXT file '{DEFAULT_TXT_PATH.name}': {e}") st.stop() # Stop processing if default file cannot be read elif uploaded_file: input_data = io.BytesIO(uploaded_file.getvalue()) # Use BytesIO for consistency input_filename_for_output = uploaded_file.name st.write(f"Processing uploaded file: {input_filename_for_output} (TXT)") else: # This case should ideally be prevented by button logic, but as a safeguard: st.error("No input file specified!") st.stop() # Basic validation passed in UI, but double-check core requirements if not input_data or not source_lang_selected or not target_lang_selected: st.error("Internal Error: Missing required input (file, source language, or target language) despite button being enabled.") st.stop() if source_lang_selected == target_lang_selected: st.error("Source and Target languages cannot be the same.") st.stop() # --- Start TXT Processing --- st.markdown("---") st.write("**Step 1: Reading Text...**") # Pass the BytesIO object directly to the function original_text = extract_text_from_txt(input_data) if original_text is not None: st.markdown("---") st.write(f"**Step 2: Translating text from {source_lang_selected} to {target_lang_selected}...**") status_text_txt = st.empty() status_text_txt.text("Sending text to translation API...") # Create a meaningful log prefix log_prefix = f"TXT ({Path(input_filename_for_output).name})" translated_text = translate_text_gemini(original_text, source_lang_selected, target_lang_selected, log_prefix=log_prefix) status_text_txt.text("Translation received.") if translated_text is not None and not translated_text.startswith("[Translation"): # Check success st.markdown("---") st.write("**Step 3: Creating Translated TXT file...**") output_buffer = create_txt_from_text(translated_text) if output_buffer: # Generate filename based on original (default or uploaded) output_filename = f"{Path(input_filename_for_output).stem}_translated_{target_lang_selected}.txt" st.success("✅ Translation and TXT creation successful!") # else: Error handled in create_txt_from_text elif translated_text is not None and translated_text.startswith("[Translation"): # Display specific error from API call if it was returned st.error(f"Translation step failed. Reason: {translated_text}") else: # translated_text is None or some other issue st.error("Translation failed. Cannot create TXT file.") else: st.error("Reading/Decoding TXT file failed. Cannot proceed.") # --- Offer Download --- if output_buffer: st.markdown("---") st.subheader("📥 Download Result") st.download_button( label=f"Download {output_filename}", data=output_buffer, file_name=output_filename, mime="text/plain", # Correct mime type for TXT ) # Display a snippet of the translation (optional) try: output_buffer.seek(0) snippet = output_buffer.read(1000).decode('utf-8', errors='ignore') # Read more for TXT st.text_area("Translation Snippet:", snippet + ("..." if len(snippet) == 1000 else ""), height=200) except Exception as e: st.warning(f"Could not display snippet: {e}") # --- Initial Instructions --- # Show instructions if Gemini is configured but no action has been taken yet elif gemini_configured and not input_ready: st.markdown( f""" ## How to Use: 1. **Choose Input:** * Check the box in the sidebar to use the **default {DEFAULT_TXT_LANGUAGE.capitalize()} TXT file** (`{DEFAULT_TXT_PATH.name}`). * Or, **upload** your own TXT file using the uploader below the checkbox. 2. **Select Languages:** * If uploading, select the **source language** of your file. (The source language is set automatically for the default file). * Select the **target language** you want to translate to. 3. **Translate:** Click the "Translate Document" button in the sidebar (it will be enabled once steps 1/2 & 3 are complete). 4. **Download:** Once processed, a download button for the translated TXT file will appear, along with a preview snippet. """ ) elif not gemini_configured: # Optional: Add a message here if Gemini isn't configured, though the error message is already shown in the sidebar. st.info("Please configure the Gemini API Key in Streamlit secrets to enable translation.") import streamlit.components.v1 as components components.html( """ """ )