LessonTime.AI Notes

Short, practical notes on machine learning and Python — the things worth writing down after you hit them the first time.

Notes: Transcribe Unlimited

We built Notes to solve a very ordinary problem: speech-to-text should feel simple. No subscriptions. No minute caps. No signup friction. Just open the app, speak, and keep going.

That idea is surprisingly rare in voice transcription apps. Most tools charge by usage, gate a free plan behind a tiny quota, or send audio to a server you don't control. Notes takes a different approach: the model runs on the device, and the transcript stays in your own app storage.

On-device transcription has a few nice side effects. It is private by default, it works without a live internet connection after the model is downloaded, and it removes the constantly looming question of how many minutes are left. If the app is installed, the experience is effectively unlimited.

For everyday work, that matters a lot. Lectures, interviews, ideas during a walk, meeting notes, language practice, or a rough draft of a lesson plan — all of them are easier to capture when there is no friction between thought and text. A single mic button and a live transcript is enough for a lot of real life.

Unlimited transcription is not just about larger quotas. It is about making the tool feel trustworthy: one less thing to think about when you are trying to capture what matters.

Put your model in eval mode

Dropout makes a model non-deterministic on purpose: during training it zeroes a random subset of activations on every forward pass. That randomness is exactly what you don't want at inference time, when the same input should always give the same answer.

Before you run predictions, switch the model over:

model.eval()

with torch.no_grad():
    predictions = model(inputs)

model.eval() does two things: it turns dropout off, and it makes batch-norm layers use their stored running statistics instead of the current batch's statistics. torch.no_grad() is a separate concern — it stops PyTorch building the autograd graph, which saves memory and time. You usually want both.

Call model.train() again before you resume training. Forgetting to switch back is a quiet bug: the loss keeps going down, just not as well as it should.

Basic usage of Matplotlib

Matplotlib is Python's workhorse plotting library. It covers everything from a throwaway line chart in a notebook to a figure you'd put in a paper, and it shows up in data analysis, scientific work, and machine learning alike.

Here is the shortest useful example — a line plot:

import matplotlib.pyplot as plt

x_values = [1, 2, 3, 4, 5]
y_values = [2, 4, 6, 8, 10]

plt.plot(x_values, y_values)

plt.xlabel("X values")
plt.ylabel("Y values")
plt.title("Line plot example")

plt.show()

plot() draws the data, xlabel(), ylabel() and title() label it, and show() opens the window. In a Jupyter notebook you can leave show() out — the figure renders on its own.

Scatter plots take the same shape, with per-point colours and a marker style:

x_values = [1, 2, 3, 4, 5]
y_values = [2, 4, 6, 8, 10]

plt.scatter(
    x_values,
    y_values,
    color=["red", "green", "blue", "orange", "purple"],
    marker="s",
)

plt.xlabel("X values")
plt.ylabel("Y values")
plt.title("Scatter plot example")

plt.show()

Once a figure has more than one thing in it, move to the object-oriented interface: fig, ax = plt.subplots(), then ax.plot(...) and ax.set_xlabel(...). The plt. shortcuts all act on whichever figure happens to be current, which gets confusing fast with subplots.

Between the two interfaces and a deep set of styling options, Matplotlib will take you from a quick sanity-check plot to a publication figure without switching libraries.

Python lambda functions

A lambda is a small, unnamed function written as a single expression. It's useful when you need a function once, in the place you're already standing — as an argument to another function, or inside a comprehension.

double = lambda x: x * 2
print(double(5))
# 10

That example is the clearest way to show what a lambda does, but PEP 8 asks you not to write it that way in real code. If the function needs a name, use def double(x): return x * 2 — you get a proper name in tracebacks for free.

Where lambdas actually earn their place is as a throwaway argument. Passing one to map():

numbers = [1, 2, 3, 4, 5]
squared_numbers = map(lambda x: x ** 2, numbers)
print(list(squared_numbers))
# [1, 4, 9, 16, 25]

map() applies the lambda to each element and returns a lazy iterator, so list() is what forces the work to happen. The same pattern works with filter(), with sorted(..., key=...), and with functools.reduce() — note that reduce moved into functools in Python 3 and is no longer a builtin.

One caveat worth knowing: for map and filter specifically, a list comprehension is usually the more readable option. [x ** 2 for x in numbers] says the same thing with less machinery. Save lambdas for key= arguments and callbacks, where there's nowhere natural to put a def.

Useful applications of AI in educational technology

A running list of the places machine learning is genuinely earning its keep in education, rather than being bolted on:

  • Intelligent tutoring systems that adapt to a student's individual strengths and gaps.
  • Automated grading for assignments and tests, giving teachers their marking hours back.
  • Adaptive learning platforms that adjust the difficulty of material based on how a student is doing.
  • Natural language processing for feedback on student writing and speaking.
  • Virtual assistants that answer questions outside class hours.
  • Early-warning models that spot patterns in behaviour and performance so teachers can step in sooner.
  • Educational games and simulations that make practice something students actually want to do.
  • Computer vision for engagement and attention signals — powerful, and the one on this list that needs the most careful thought about consent and privacy.
  • Predictive analytics that forecast future performance from past work.
  • Recommender systems that surface resources and activities matched to a student's interests.

List comprehensions

A list comprehension builds a new list from an existing iterable in a single expression. It replaces the loop-append pattern, and once your eye is used to it, it reads more directly than the loop it replaces.

original_list = [1, 2, 3, 4, 5]
new_list = [x ** 2 for x in original_list if x % 2 == 0]
print(new_list)
# [4, 16]

Read it left to right: take x from original_list, keep it only if x % 2 == 0, then square it. The filter runs before the operation, so only the even numbers get squared.

Comprehensions aren't limited to flat lists of numbers. Combining two lists with zip() gives you a list of tuples:

names = ["John", "Jane", "Bob"]
ages = [23, 29, 35]
tuple_list = [(name, age) for name, age in zip(names, ages)]
print(tuple_list)
# [('John', 23), ('Jane', 29), ('Bob', 35)]

The same syntax gives you dict and set comprehensions with different brackets — {name: age for name, age in zip(names, ages)} — and swapping the square brackets for round ones gives a generator expression, which produces values one at a time instead of building the whole list in memory. Reach for that when the input is large and you only need to iterate once.

Keep them to one level. Once a comprehension has two for clauses and a condition, a plain loop is the kinder thing to leave for whoever reads it next — including you.