We are going to build a single-file desktop app with Python and tkinter that reformats raw meeting transcripts into structured Markdown notes. It is useful for anyone who ends up with messy bullet dumps after a call and needs clean minutes without pasting data into a browser tab. I chose Oxlo.ai for the backend because flat per-request pricing keeps the cost predictable even with long transcripts, and you can start on the Free tier at https://oxlo.ai/pricing.
What you'll need
- Python 3.10 or newer installed locally.
- The OpenAI SDK:
pip install openai. - An Oxlo.ai API key from https://portal.oxlo.ai. The Free tier gives you 60 requests per day, which is plenty for testing.
Step 1: Scaffold the tkinter window
I start with a minimal window that has two scrolling text boxes and a single button. This gives us a place to paste raw notes and view the formatted result.
import tkinter as tk
from tkinter import scrolledtext
class NotesFormatter:
def __init__(self, root):
self.root = root
self.root.title("Meeting Notes Formatter")
self.root.geometry("900x700")
tk.Label(root, text="Raw Notes").pack(anchor="w", padx=10, pady=(10, 0))
self.input_box = scrolledtext.ScrolledText(root, height=14, wrap=tk.WORD)
self.input_box.pack(fill="both", expand=True, padx=10, pady=5)
self.fmt_btn = tk.Button(root, text="Format with Oxlo.ai", command=self.format_notes)
self.fmt_btn.pack(pady=5)
tk.Label(root, text="Formatted Output").pack(anchor="w", padx=10)
self.output_box = scrolledtext.ScrolledText(root, height=14, wrap=tk.WORD)
self.output_box.pack(fill="both", expand=True, padx=10, pady=5)
def format_notes(self):
pass
if __name__ == "__main__":
app = tk.Tk()
NotesFormatter(app)
app.mainloop()
Step 2: Write the system prompt
The system prompt is the only formatting guardrail we need. It keeps the model focused on Markdown structure and prevents it from hallucinating details that are not in the transcript.
SYSTEM_PROMPT = (
"You are a precise meeting notes formatter. "
"Take raw, messy transcript text and convert it into clean Markdown. "
"Use a ## heading for the meeting title inferred from the content, "
"bullet points for discussion topics, and a checklist for action items. "
"Only use information explicitly present in the input. "
"If there are no action items, omit that section entirely."
)
Step 3: Initialize the Oxlo.ai client
I initialize the Oxlo.ai client using the standard OpenAI SDK pattern. The base URL points to Oxlo.ai and the API key authenticates every request.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
Step 4: Connect the button to the LLM
Now I will replace the empty format_notes method so it sends the input to Llama 3.3 70B and renders the response in the output box. This is the core integration that turns the GUI into a useful tool.
import tkinter as tk
from tkinter import scrolledtext
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
SYSTEM_PROMPT = (
"You are a precise meeting notes formatter. "
"Take raw, messy transcript text and convert it into clean Markdown. "
"Use a ## heading for the meeting title inferred from the content, "
"bullet points for discussion topics, and a checklist for action items. "
"Only use information explicitly present in the input. "
"If there are no action items, omit that section entirely."
)
class NotesFormatter:
def __init__(self, root):
self.root = root
self.root.title("Meeting Notes Formatter")
self.root.geometry("900x700")
tk.Label(root, text="Raw Notes").pack(anchor="w", padx=10, pady=(10, 0))
self.input_box = scrolledtext.ScrolledText(root, height=14, wrap=tk.WORD)
self.input_box.pack(fill="both", expand=True, padx=10, pady=5)
self.fmt_btn = tk.Button(root, text="Format with Oxlo.ai", command=self.format_notes)
self.fmt_btn.pack(pady=5)
tk.Label(root, text="Formatted Output").pack(anchor="w", padx=10)
self.output_box = scrolledtext.ScrolledText(root, height=14, wrap=tk.WORD)
self.output_box.pack(fill="both", expand=True, padx=10, pady=5)
def format_notes(self):
raw = self.input_box.get("1.0", tk.END).strip()
if not raw:
return
try:
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": raw},
],
)
formatted = response.choices[0].message.content
self.output_box.delete("1.0", tk.END)
self.output_box.insert("1.0", formatted)
except Exception as e:
self.output_box.delete("1.0", tk.END)
self.output_box.insert("1.0", f"Error: {e}")
if __name__ == "__main__":
app = tk.Tk()
NotesFormatter(app)
app.mainloop()
Step 5: Polish with clipboard and error handling
Finally, I will add a Copy Output button and a status label so the app feels finished. The status line gives immediate feedback while Oxlo.ai is processing the text.
import tkinter as tk
from tkinter import scrolledtext
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
SYSTEM_PROMPT = (
"You are a precise meeting notes formatter. "
"Take raw, messy transcript text and convert it into clean Markdown. "
"Use a ## heading for the meeting title inferred from the content, "
"bullet points for discussion topics, and a checklist for action items. "
"Only use information explicitly present in the input. "
"If there are no action items, omit that section entirely."
)
class NotesFormatter:
def __init__(self, root):
self.root = root
self.root.title("Meeting Notes Formatter")
self.root.geometry("900x700")
tk.Label(root, text="Raw Notes").pack(anchor="w", padx=10, pady=(10, 0))
self.input_box = scrolledtext.ScrolledText(root, height=14, wrap=tk.WORD)
self.input_box.pack(fill="both", expand=True, padx=10, pady=5)
btn_frame = tk.Frame(root)
btn_frame.pack(fill="x", padx=10, pady=5)
self.fmt_btn = tk.Button(
btn_frame, text="Format with Oxlo.ai", command=self.format_notes
)
self.fmt_btn.pack(side="left", padx=(0, 5))
self.copy_btn = tk.Button(
btn_frame, text="Copy Output", command=self.copy_output
)
self.copy_btn.pack(side="left")
self.status_lbl = tk.Label(btn_frame, text="Ready", anchor="e")
self.status_lbl.pack(side="right", fill="x", expand=True)
tk.Label(root, text="Formatted Output").pack(anchor="w", padx=10)
self.output_box = scrolledtext.ScrolledText(root, height=14, wrap=tk.WORD)
self.output_box.pack(fill="both", expand=True, padx=10, pady=5)
def format_notes(self):
raw = self.input_box.get("1.0", tk.END).strip()
if not raw:
self.status_lbl.config(text="Input is empty")
return
self.status_lbl.config(text="Calling Oxlo.ai...")
self.root.update_idletasks()
try:
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": raw},
],
)
formatted = response.choices[0].message.content
self.output_box.delete("1.0", tk.END)
self.output_box.insert("1.0", formatted)
self.status_lbl.config(text="Done")
except Exception as e:
self.output_box.delete("1.0", tk.END)
self.output_box.insert("1.0", f"Error: {e}")
self.status_lbl.config(text="Failed")
def copy_output(self):
out = self.output_box.get("1.0", tk.END).strip()
self.root.clipboard_clear()
self.root.clipboard_append(out)
self.status_lbl.config(text="Copied to clipboard")
if __name__ == "__main__":
app = tk.Tk()
NotesFormatter(app)
app.mainloop()
Run it
Save the final script as notes_formatter.py, replace YOUR_OXLO_API_KEY with your key from https://portal.oxlo.ai, and run python notes_formatter.py. Paste this raw text into the input box and click Format with Oxlo.ai:
attendees: sarah, mike, jen
sarah said the q2 roadmap is delayed by two weeks because the api migration took longer than expected. mike to follow up with devops on staging env. jen mentioned we need to update the docs before next release. next meeting june 10.
On my machine, Llama 3.3 70B returns something like this in under a second:
## Q2 Roadmap Discussion
- The Q2 roadmap is delayed by two weeks because the API migration took longer than expected.
- Documentation needs to be updated before the next release.
### Action Items
- [ ] Mike: Follow up with DevOps on staging environment.
Next steps
Add a drag-and-drop handler so users can drop raw .txt files onto the window instead of pasting. If you want to stay on the Free tier, swap the model string to deepseek-v3.2. For whiteboard photos or screenshots, switch to kimi-k2.6 and pass image bytes into the messages payload using Oxlo.ai vision support.
Top comments (0)