Two builds that solve the same problem in different clothes: something unstructured — a sentence spoken in a field, a scattered set of supplier websites — has to end up as clean, dated rows in a spreadsheet, without anyone typing it in. Here is how each one works, and the code underneath.
← Back to marinaermina.comA voice note from the field becomes a dated row in a Google Sheet, in seconds, with my computer switched off.
My husband does farm work all day and needed to record what he'd done — task, time spent, date. Writing it up later meant it got forgotten or mis-remembered.
An earlier attempt in Make.com only ran when my laptop was open, so in practice it didn't run at all.
A Telegram bot living in Google Cloud Run. He sends a voice note or a typed message; Whisper transcribes it; an AI step pulls out task · time spent · date; a row is appended to the sheet and he gets a confirmation back.
He can speak normally — "spent about half an hour feeding the sheep yesterday" resolves to the right date and duration. No format to remember, no app to learn.
| Date | Task type | Time spent |
|---|---|---|
| 2026-08-24 | Feeding sheep | 30 min |
| 2026-08-24 | Fence repair — north field | 1 h 45 min |
| 2026-08-25 | Moving hay bales | 2 h |
A voice note needs transcription. If that call fails — say the API key runs out of credit — the bot should tell him, not silently swallow the message. Failure has to be visible to the person in the field.
# Telegram calls this endpoint every time he sends a message. @app.route("/webhook", methods=["POST"]) def webhook(): # VOICE needs OpenAI Whisper. If that's down (e.g. no credit), # we DON'T crash — we tell him to type it instead. ...
The date is the subtle part. "Yesterday" has to become a real date, and it has to be written YYYY-MM-DD — because that is the format that sorts chronologically when the sheet re-sorts itself.
def extract_fields(text, today_iso): """Pull task type, time spent and date out of natural speech. The date is returned as a YYYY-MM-DD string — that format sorts chronologically when the sheet is sorted. """ ... response = openai_client.chat.completions.create(...)
A logbook people trust is one that never rewrites itself. New rows are added and the sheet is re-sorted by date, but existing rows are never edited or moved.
# Existing rows are never edited, moved, or re-sorted.
gc = gspread.authorize(credentials)
Ten searches in English and Icelandic, read and sorted into a single comparable sheet.
Before positioning a compostable-cup line, I needed to know who already supplied eco cups in Iceland, whether they printed logos, and what they charged.
Doing that by hand means the same afternoon of searching every time you want a refresh — so in practice it gets done once and goes stale.
A Python script that runs ten searches — five in English, five in Icelandic — de-duplicates by URL, opens each result, reads its meta description, and classifies whether the page mentions pricing, logo printing, and whether the supplier is Icelandic or importing.
The result is written to a Google Sheet through a service account, with a frozen, formatted header row, ready to re-run whenever the picture needs refreshing.
| Company Name | Website | Description | Price / Pricing Info | Logo Printing | Country / Region | Last Updated |
|---|---|---|---|---|---|---|
| Example Umbúðir ehf. | example.is | Disposable packaging supplier — cups, lids, napkins | Mentioned on site — check website | Yes | Iceland / Nordic | 2026-08-26 |
| Nordic Eco Supply | example.com | Compostable cups, PLA-lined, wholesale quantities | Contact for pricing | Yes | International (ships to Iceland) | 2026-08-26 |
| Green Cup Co. | example.co.uk | Custom printed paper cups for cafés | Mentioned on site — check website | Not specified | International (ships to Iceland) | 2026-08-26 |
This is the part that matters most and is easiest to skip. An Icelandic supplier describes itself in Icelandic — search only in English and you miss the local market, which is exactly the market in question.
SEARCH_QUERIES = [
"biodegradable paper cups distributor Iceland",
"compostable paper cups supplier Iceland",
"eco-friendly disposable cups Iceland",
"sustainable packaging cups Iceland supplier",
"environmentally friendly paper cups Iceland buy",
"umhverfisvænar pappírsbikara Ísland",
"lífrænar einnota bíkara Ísland dreifingaraðili",
"pappírsbikar lógó prentun Ísland",
"custom printed paper cups Iceland supplier",
"wholesale paper cups Iceland",
]
Ten overlapping queries return the same supplier repeatedly. De-duplicating by URL as results arrive is what makes the output a comparison table rather than a pile of links. The pause between searches is courtesy to the search engine.
def search_distributors(): seen_urls = set() results = [] with DDGS() as ddgs: for query in SEARCH_QUERIES: hits = ddgs.text(query, max_results=6) for hit in hits: url = hit.get("href", "") if url and url not in seen_urls: seen_urls.add(url) results.append(hit) time.sleep(1.5)
A search snippet is often marketing filler. Opening the page and reading its meta description gives a truer one-line summary, which then gets scanned for pricing and logo-printing signals so the columns are comparable across suppliers.
PRICE_KEYWORDS = ["price", "cost", "isk", "kr.", "per unit", "per case", ...] LOGO_KEYWORDS = ["logo", "print", "custom print", "branded", "branding", ...] def scrape_meta_description(url: str) -> str: resp = requests.get(url, headers={...}, timeout=10) if resp.status_code == 200: soup = BeautifulSoup(resp.text, "html.parser") tag = soup.find("meta", attrs={"name": "description"})
Writing to a Google Sheet needs a service-account key. It's resolved at runtime from a folder that's git-ignored, never hard-coded — so the script can be shown to anyone without showing them the credentials.
_CREDENTIAL_CANDIDATES = [
os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"),
os.path.join(PROJECT_DIR, "secrets", CREDENTIALS_FILENAME),
...
]