commit fb72d26ef9437a9b4ca256cdbd8cf14770421d25 Author: Johannes Reichhardt Date: Tue Mar 31 15:32:46 2026 -0500 Initial commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..90d944e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +__pycache__/ +*.py[cod] +*$py.class +.venv/ +venv/ +ENV/ +.env +.git/ +.gitignore +.dockerignore +Dockerfile +.env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b9e9e9d --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +GOOGLE_API_KEY=your_gemini_api_key_here +FLASK_SECRET_KEY=something_random_and_secure +FLASK_DEBUG=True +PORT=5000 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..717c745 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +# Use a lightweight Python base image +FROM python:3.12-slim + +# Set working directory +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy application files +COPY . . + +# Set environment variables (defaults) +ENV PORT=5000 +ENV FLASK_DEBUG=False +ENV GEMINI_API_KEY="" +ENV FLASK_SECRET_KEY="production-secret-key-change-me" + +# Expose the application port +EXPOSE 5000 + +# Run the Flask app +CMD ["python", "app.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..96f373d --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# txt2md + +A minimalist Flask application to convert raw text into high-quality Markdown documents. + +## Features + +- **Smart Processing:** Automatically structures your raw input with headings, lists, and formatting. +- **Modern UI:** Clean, responsive interface for effortless writing. +- **Portable:** Easy to run locally or as a Docker container. + +## Local Setup + +1. **Clone the repository:** + ```bash + git clone https://github.com/joreichhardt/txt2md.git + cd txt2md + ``` + +2. **Install dependencies:** + ```bash + pip install -r requirements.txt + ``` + +3. **Configuration:** + Set the following environment variables: + - `AI_API_KEY`: Your API key for processing. + - `FLASK_SECRET_KEY`: A random string for session security. + +4. **Run:** + ```bash + python app.py + ``` + +## Docker Deployment + +### Build +```bash +docker build -t txt2md:latest . +``` + +### Run +```bash +docker run -p 5000:5000 \ + -e AI_API_KEY="your-key" \ + -e FLASK_SECRET_KEY="your-secret" \ + txt2md:latest +``` diff --git a/__pycache__/app.cpython-312.pyc b/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..2b9251c Binary files /dev/null and b/__pycache__/app.cpython-312.pyc differ diff --git a/app.py b/app.py new file mode 100644 index 0000000..255f17c --- /dev/null +++ b/app.py @@ -0,0 +1,46 @@ +import os +from flask import Flask, render_template, request, flash +import google.generativeai as genai +from markdown import markdown +from dotenv import load_dotenv + +load_dotenv() + +app = Flask(__name__) +app.secret_key = os.environ.get("FLASK_SECRET_KEY", "prod-secret-7721") + +# API Configuration +api_key = os.environ.get("AI_API_KEY") +if api_key: + genai.configure(api_key=api_key) + model = genai.GenerativeModel('gemini-1.5-flash') +else: + model = None + +@app.route("/", methods=["GET", "POST"]) +def index(): + converted_html = None + original_text = "" + + if request.method == "POST": + original_text = request.form.get("text", "") + + if not api_key: + flash("AI_API_KEY is not set. Please check your environment configuration.", "error") + elif not original_text.strip(): + flash("Please enter some text to process.", "warning") + else: + try: + # Content processing logic + prompt = f"Convert the following text to high-quality Markdown. Use appropriate structure, headings, and formatting. Return only the markdown content:\n\n{original_text}" + response = model.generate_content(prompt) + markdown_content = response.text + converted_html = markdown(markdown_content, extensions=['extra', 'codehilite']) + except Exception as e: + flash(f"Error during processing: {str(e)}", "error") + + return render_template("index.html", original_text=original_text, converted_html=converted_html) + +if __name__ == "__main__": + debug_mode = os.environ.get("FLASK_DEBUG", "False").lower() == "true" + app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000)), debug=debug_mode) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3616bb9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +google-generativeai +python-dotenv +markdown diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..bf4a376 --- /dev/null +++ b/static/style.css @@ -0,0 +1,137 @@ +:root { + --primary-color: #3498db; + --secondary-color: #2c3e50; + --background-color: #f8f9fa; + --text-color: #333; + --border-color: #ddd; + --alert-error: #e74c3c; + --alert-warning: #f39c12; + --alert-info: #3498db; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + background-color: var(--background-color); + color: var(--text-color); + line-height: 1.6; + margin: 0; + padding: 0; +} + +.container { + max-width: 1200px; + margin: 2rem auto; + padding: 0 1.5rem; +} + +header { + text-align: center; + margin-bottom: 3rem; +} + +header h1 { + font-size: 2.5rem; + color: var(--secondary-color); + margin-bottom: 0.5rem; +} + +header p { + color: #666; + font-size: 1.1rem; +} + +.editor-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; +} + +section h2 { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--secondary-color); + border-bottom: 2px solid var(--primary-color); + padding-bottom: 0.5rem; +} + +textarea { + width: 100%; + height: 400px; + padding: 1rem; + border: 1px solid var(--border-color); + border-radius: 8px; + font-size: 1rem; + font-family: inherit; + resize: vertical; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +textarea:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2); +} + +button { + display: block; + width: 100%; + padding: 0.75rem; + margin-top: 1rem; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: background-color 0.3s ease; +} + +button:hover { + background-color: #2980b9; +} + +.preview { + background-color: white; + padding: 1rem; + border: 1px solid var(--border-color); + border-radius: 8px; + height: 400px; + overflow-y: auto; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); +} + +.preview .placeholder { + color: #999; + font-style: italic; + text-align: center; + margin-top: 2rem; +} + +.flashes { + margin-bottom: 1.5rem; +} + +.alert { + padding: 0.75rem 1rem; + border-radius: 6px; + margin-bottom: 0.5rem; + font-weight: 500; +} + +.alert.error { background-color: #fadbd8; color: var(--alert-error); border: 1px solid #f5b7b1; } +.alert.warning { background-color: #fef5e7; color: var(--alert-warning); border: 1px solid #f9e79f; } +.alert.info { background-color: #ebf5fb; color: var(--alert-info); border: 1px solid #d6eaf8; } + +/* Markdown Preview Styles */ +.preview h1 { font-size: 1.5rem; margin-top: 0; } +.preview h2 { font-size: 1.3rem; } +.preview blockquote { border-left: 4px solid #eee; padding-left: 1rem; color: #666; margin-left: 0; } +.preview pre { background-color: #f4f4f4; padding: 1rem; border-radius: 4px; overflow-x: auto; } +.preview code { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; background-color: #f4f4f4; padding: 0.2rem 0.4rem; border-radius: 3px; } + +@media (max-width: 768px) { + .editor-grid { + grid-template-columns: 1fr; + } +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..fd817f3 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,48 @@ + + + + + + txt2md + + + +
+
+

txt2md

+

Convert your raw notes into high-quality Markdown.

+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + +
+
+

Raw Input

+
+ + +
+
+ +
+

Formatted Output

+
+ {% if converted_html %} + {{ converted_html | safe }} + {% else %} +

Results will be displayed here.

+ {% endif %} +
+
+
+
+ +