Germ

Back
Germ is the result of my impressive inability to learn German. I had (very) slowly wrapped my head around the fundamentals of the language, and wanted to start immersing myself in some authentic German content.
The problem was that my German wasn't good enough to read anything interesting, like news articles or blog posts. At the same time, I think you'll agree that life is too short to trudge through textbook passages about Ingrid asking for directions or Wolfgang checking into a hotel. I basically wanted to find content that I was keen to read in English, but then somehow be able to read it in German instead.
Enter Germ, a humble little command line tool that takes one argument: the url of an article you want to read. It then creates a variation of the article that's designed for language learning and sends it to your Kindle, so you have some educational content waiting for you when you tuck yourself into bed that night.

This very page, getting run through Germ...

Germ end result

...and the end result of that process.

Here's the repo, in case you'd like to check it out play around with it yourself: https://github.com/andrewerlanger/germ

Behind the scenes, Germ:

  • Converts the article to clean markdown using Jina
  • Sends the markdown to Gemini, which:
    • Translates the content to German at your specified proficiency (B1 in my case)
    • Adds a glossary of key terms to aid comprehension
  • Generates a language-learning version of the article, complete German text, English translations and a glossary of any useful terms encountered along the way
  • Sends the finished e-book to your Kindle
# frozen_string_literal: true

require "faraday"

module Germ
  class Conductor
    def self.call(url)
      new(url).call
    end

    def initialize(url)
      @url = url
    end

    def call
      puts "🌐 fetching article content..."
      content = fetch_content

      puts "📝 building prompt..."
      user_prompt = build_prompt(content)

      puts "⚡ generating xhtml response..."
      xhtml = generate_xhtml(user_prompt)

      puts "📚 generating ebook..."
      result = Ebook.call(xhtml, @url)
      title = result[:title]
      filename = result[:filename]

      puts "📧 sending to kindle..."
      Kindle.call(title, filename)

      puts "🚮 deleting file..."
      File.delete(filename)

      puts "😎 done!"
    end

    private

    def fetch_content
      url = @url.start_with?("http") ? @url : "https://r.jina.ai/#{@url}"
      Faraday.get(url).body
    end

    def build_prompt(content)
      example = File.read(File.expand_path("../../example.html", __dir__))
      user_prompt = File.read(File.expand_path("../../user_prompt.md", __dir__))
      user_prompt = user_prompt.gsub("{{ CONTENT }}", content)
      user_prompt = user_prompt.gsub("{{ EXAMPLE }}", example)
    end

    def generate_xhtml(user_prompt)
      response = Gemini.call(user_prompt)
      response.split("```xhtml", 2).last.split("```", 2).first
    end
  end
end

The main thrust of the logic.

In case you were wondering, my German is still pretty terrible. But it's notably less terrible than it used to be, and I mostly have Germ to thank for that 🦠♥️