# --- 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(
"""
"""
)