Vibe scripting with AI and Python
- Authors

- Name
- Matt Morris
One of the greatest benefits of AI is its ability to help you write code, which doesn't just include large applications. It can also help you write scripts for smaller tasks, a great way for someone new to programming to get started and create something useful quickly.
In this post, I share a few examples of how I use AI to help me write Python scripts for a variety of purposes that could be approachable for anyone willing to take a few steps to set up a development environment and install any necessary packages (AI can help with that as well).
These are the kinds of capabilities that can be used on their own, extended to agents, or included in an automated workflow.
Examples
Caption file generator script
I work with video a lot and we always need caption files for the videos we produce. These are generally available but sometimes not (or they are available in a different format than we need).
Previously this would have required finding a team that can convert the file or scrambling to find a tool that can do it for free (usually with AdWare). Now, it's incredibly easy to use AI to generate a script for you that can create a caption file in whatever format you need. You just need a Python development environment set up and the necessary packages installed.
The script uses the OpenAI Whisper model model to transcribe the audio from a video file and output it as a caption file in the desired format.
You run it from the command line, passing in the video file as an argument, and it will generate a caption file in the same directory.
import whisper
import sys
import os
def write_srt(segments, output_path):
with open(output_path, "w", encoding="utf-8") as f:
for i, seg in enumerate(segments, start=1):
start = seg["start"]
end = seg["end"]
text = seg["text"].strip()
def ts(t):
hrs = int(t // 3600)
mins = int((t % 3600) // 60)
secs = int(t % 60)
ms = int((t - int(t)) * 1000)
return f"{hrs:02}:{mins:02}:{secs:02},{ms:03}"
f.write(f"{i}\n")
f.write(f"{ts(start)} --> {ts(end)}\n")
f.write(f"{text}\n\n")
def main():
if len(sys.argv) < 2:
print("Usage: python mp4_to_srt.py input.mp4")
sys.exit(1)
input_file = sys.argv[1]
output_file = os.path.splitext(input_file)[0] + ".srt"
print("Loading Whisper model…")
model = whisper.load_model("medium") # or "small", "large", etc.
print("Transcribing…")
result = model.transcribe(input_file)
print("Writing SRT…")
write_srt(result["segments"], output_file)
print(f"Done! SRT saved to {output_file}")
if __name__ == "__main__":
main()
Example Usage
I used the viral "I'm not a cat" video as the input for this example:
Here's the script output in SRT format:
1
00:00:00,000 --> 00:00:10,779
Mr. Ponton, I believe you have a filter turned on in the video settings.
2
00:00:10,779 --> 00:00:12,880
You might want to take a look.
3
00:00:12,880 --> 00:00:16,640
We're trying to, can you hear me, Judge?
4
00:00:16,640 --> 00:00:17,679
I can hear you.
5
00:00:17,679 --> 00:00:19,679
I think it's a filter.
6
00:00:19,679 --> 00:00:22,440
It is and I don't know how to remove it.
7
00:00:22,440 --> 00:00:23,719
I've got my assistant here.
8
00:00:23,719 --> 00:00:29,400
She's trying to, but I'm prepared to go forward with it.
9
00:00:29,959 --> 00:00:30,759
I'm here live.
10
00:00:30,759 --> 00:00:32,159
That's not, I'm not a cat.
11
00:00:35,679 --> 00:00:37,519
I can, I can see that.
12
00:00:37,960 --> 00:00:42,119
I think if you click the up arrow next to the.
Web scraper script
I work with web data a lot and sometimes need to scrape data from websites. This can be a tedious task, especially when the website is dynamic and requires interaction to load all the data.
This example uses Playwright to scrape the Apple Retail Success Stories page. It connects to a running instance of Chrome with remote debugging enabled, navigates to the page, scrolls to load all the content, and extracts the relevant information.
from playwright.sync_api import sync_playwright, Error as PlaywrightError
from urllib.parse import urljoin
import sys
import time
BASE_URL = "https://www.apple.com/business/enterprise/success-stories/retail/"
CDP_URL = "http://127.0.0.1:9223"
def scrape_retail():
with sync_playwright() as p:
# Connect to the already-running real Chrome instance
print("Connecting to Chrome debugging instance on port 9223...")
try:
browser = p.chromium.connect_over_cdp(CDP_URL)
except PlaywrightError as e:
if "ECONNREFUSED" in str(e):
print(
"ERROR: Could not reach Chrome's debugging port at "
f"{CDP_URL}. Chrome isn't running with remote debugging enabled.\n"
"Close all Chrome windows, then start it with:\n\n"
' & "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" '
'--remote-debugging-port=9223 --user-data-dir="C:\\chrome-debug-profile"\n'
)
sys.exit(1)
raise
# Diagnostic: show contexts returned by Chrome
print("Connected contexts:", browser.contexts)
# If no contexts exist, Chrome debugging is NOT being used
if not browser.contexts:
print("ERROR: No contexts found. Playwright did NOT attach to Chrome.")
return []
context = browser.contexts[0]
page = context.new_page()
page.goto(BASE_URL, wait_until="domcontentloaded")
page.wait_for_selector(".tile", timeout=20000)
# Scroll slowly to trigger lazy loading + hydration
for _ in range(25):
page.mouse.wheel(0, 600)
time.sleep(0.3)
# Give React time to finish hydration
page.wait_for_timeout(2000)
tiles = page.query_selector_all(".tile")
print("Tile elements found (includes hidden duplicates):", len(tiles))
stories_by_url = {}
for tile in tiles:
link = tile.query_selector("a")
if not link:
continue
href = link.get_attribute("href")
if not href:
continue
full_url = urljoin(BASE_URL, href)
title_el = tile.query_selector("h3")
if title_el:
title = title_el.inner_text().strip()
else:
credit_el = tile.query_selector(".quote-credit")
title = credit_el.inner_text().strip() if credit_el else None
desc_el = tile.query_selector("blockquote p")
desc = desc_el.inner_text().strip() if desc_el else None
image = None
img_el = tile.query_selector("img")
if img_el:
image = img_el.get_attribute("src") or img_el.get_attribute("data-src")
else:
icon_el = tile.query_selector(".image-icon")
if icon_el:
bg = icon_el.evaluate("el => getComputedStyle(el).backgroundImage")
if bg and bg.startswith("url("):
image = bg[4:-1].strip("'\"")
if image:
image = urljoin(BASE_URL, image)
stories_by_url[full_url] = {
"title": title,
"description": desc,
"url": full_url,
"image": image
}
print("Unique stories after dedup:", len(stories_by_url))
page.close()
return list(stories_by_url.values())
if __name__ == "__main__":
data = scrape_retail()
for story in data:
print(story)
Example output
You could adapt this to output to a spreadsheet or whatever format you need to, but for this example, I just printed the results to the console. The output is a list of dictionaries, each containing the title, description, URL, and image (if available) of each unique story found on the page.
The script can also be adapted to scrape other pages or websites by changing the BASE_URL and adjusting the selectors as needed.
Semantic search script
Python is excellent at processing text data, and AI can help you do it even better. One of the most useful applications of AI is semantic search, which allows you to search for information based on meaning rather than just keywords.
The script I generated uses the sentence-transformers library to create a semantic search mini-engine that can index documents and perform semantic queries against them. The script can load documents from a folder, extract text from various file formats (plain text, markdown, HTML, Word, PDF, PowerPoint, Excel), and build embeddings for the documents using a transformer model. It also provides a fallback option using TF-IDF and SVD if the transformer model is not available.
"""
Semantic Search Mini-Engine
============================
Loads documents and lets you run semantic queries against them from an
interactive CLI. By default it loads the placeholder docs in data/ (see
generate_data.py + data/manifest.json), but you can point it at any folder:
python3 semantic_search.py --dir "/path/to/other/folder"
If that folder has its own manifest.json (id/filename/title/category/date, like
data/manifest.json), it's used for metadata. Otherwise the script walks the
folder recursively and indexes every file it can extract text from — plain
text/markdown/csv/html plus Word (.docx), PDF (.pdf), PowerPoint (.pptx), and
Excel (.xlsx). Old binary Office formats (.doc/.ppt/.xls) aren't supported.
Embedding backend:
- Preferred: sentence-transformers ("all-MiniLM-L6-v2"), a real local transformer
embedding model. Used automatically if the package is installed and the model
can be downloaded (first run only; it's cached under ~/.cache afterwards).
- Fallback: TF-IDF + Truncated SVD (a.k.a. Latent Semantic Analysis). Pure
scikit-learn, no model download required, so it always works offline. The
engine picks this automatically if sentence-transformers isn't available.
Usage:
python3 semantic_search.py [--dir FOLDER] [--ext .txt,.md,.docx,...] [--top-k 5]
Requirements: see requirements.txt
"""
import argparse
import json
import os
import re
import sys
from html.parser import HTMLParser
import numpy as np
DEFAULT_DIR = "data"
# Folders that are never worth walking into.
SKIP_DIR_NAMES = {
".git", ".svn", ".hg", "__pycache__", "node_modules", ".venv", "venv",
"$RECYCLE.BIN", "System Volume Information",
}
# Skip cloud-sync/backup dirs and anything hidden (dotfolders).
def _should_skip_dir(name):
return name in SKIP_DIR_NAMES or name.startswith(".")
MAX_FILE_SIZE_BYTES = 25 * 1024 * 1024 # skip anything bigger than this
TOP_K_DEFAULT = 5
class _HTMLTextExtractor(HTMLParser):
def __init__(self):
super().__init__()
self.chunks = []
def handle_data(self, data):
self.chunks.append(data)
def text(self):
return " ".join(self.chunks)
def _extract_plain(path):
with open(path, encoding="utf-8", errors="ignore") as f:
return f.read()
def _extract_html(path):
raw = _extract_plain(path)
parser = _HTMLTextExtractor()
parser.feed(raw)
return re.sub(r"\s+", " ", parser.text())
def _extract_docx(path):
import docx
d = docx.Document(path)
parts = [p.text for p in d.paragraphs]
for table in d.tables:
for row in table.rows:
parts.append(" | ".join(cell.text for cell in row.cells))
return "\n".join(parts)
def _extract_pdf(path):
from pypdf import PdfReader
reader = PdfReader(path)
return "\n".join((page.extract_text() or "") for page in reader.pages)
def _extract_pptx(path):
from pptx import Presentation
prs = Presentation(path)
parts = []
for slide in prs.slides:
for shape in slide.shapes:
if shape.has_text_frame:
parts.append(shape.text_frame.text)
if shape.has_table:
for row in shape.table.rows:
parts.append(" | ".join(cell.text for cell in row.cells))
return "\n".join(parts)
def _extract_xlsx(path):
import openpyxl
wb = openpyxl.load_workbook(path, data_only=True, read_only=True)
parts = []
for ws in wb.worksheets:
for row in ws.iter_rows(values_only=True):
cells = [str(c) for c in row if c is not None]
if cells:
parts.append(" | ".join(cells))
return "\n".join(parts)
EXTRACTORS = {
".txt": _extract_plain,
".md": _extract_plain,
".csv": _extract_plain,
".log": _extract_plain,
".json": _extract_plain,
".html": _extract_html,
".htm": _extract_html,
".docx": _extract_docx,
".pdf": _extract_pdf,
".pptx": _extract_pptx,
".xlsx": _extract_xlsx,
}
DEFAULT_EXTENSIONS = tuple(EXTRACTORS.keys())
def load_documents(root_dir, extensions=DEFAULT_EXTENSIONS, verbose=True):
"""Load metadata + text content for every document under root_dir.
Uses root_dir/manifest.json if present (same format as data/manifest.json).
Otherwise walks root_dir recursively and indexes every file matching
`extensions`, extracting text with the format-appropriate extractor.
"""
if not os.path.isdir(root_dir):
sys.exit(f"'{root_dir}' isn't a folder I can find.")
manifest_path = os.path.join(root_dir, "manifest.json")
if os.path.exists(manifest_path):
with open(manifest_path) as f:
manifest = json.load(f)
docs = []
for entry in manifest:
path = os.path.join(root_dir, entry["filename"])
docs.append({**entry, "text": _extract_plain(path)})
return docs
# No manifest — walk the folder and extract text from matching files.
docs = []
doc_id = 1
skipped = [] # (rel_path, reason)
for dirpath, dirnames, filenames in os.walk(root_dir):
dirnames[:] = [d for d in dirnames if not _should_skip_dir(d)]
for filename in sorted(filenames):
ext = os.path.splitext(filename)[1].lower()
if ext not in extensions:
continue
path = os.path.join(dirpath, filename)
rel_path = os.path.relpath(path, root_dir)
try:
if os.path.getsize(path) > MAX_FILE_SIZE_BYTES:
skipped.append((rel_path, "over size limit"))
continue
except OSError as e:
skipped.append((rel_path, f"can't stat file ({e.__class__.__name__})"))
continue
extractor = EXTRACTORS.get(ext, _extract_plain)
try:
text = extractor(path)
except Exception as e:
skipped.append((rel_path, f"{e.__class__.__name__}: {e}"))
continue
if not text or not text.strip():
skipped.append((rel_path, "no extractable text (empty/scanned/image-only)"))
continue
docs.append(
{
"id": doc_id,
"filename": rel_path,
"title": os.path.splitext(filename)[0],
"category": os.path.dirname(rel_path) or "uncategorized",
"date": "",
"text": text,
}
)
doc_id += 1
if verbose and skipped:
print(f"(skipped {len(skipped)} files)")
for rel_path, reason in skipped[:30]:
print(f" - {rel_path}: {reason}")
if len(skipped) > 30:
print(f" ...and {len(skipped) - 30} more")
if not docs:
sys.exit(
f"No files with a supported extension found under '{root_dir}'. "
f"Supported: {', '.join(sorted(EXTRACTORS))}. Pass --ext to narrow/widen the list."
)
return docs
class EmbeddingBackend:
"""Turns a list of strings into vectors. Tries sentence-transformers first,
falls back to TF-IDF + SVD (LSA) if that's unavailable or can't reach the
internet to download its model."""
def __init__(self):
self.name = None
self._model = None
self._vectorizer = None
self._svd = None
self._init_backend()
def _init_backend(self):
try:
from sentence_transformers import SentenceTransformer
self._model = SentenceTransformer("all-MiniLM-L6-v2")
self.name = "sentence-transformers/all-MiniLM-L6-v2"
return
except Exception:
pass # not installed, or couldn't download the model — fall back
self.name = "tfidf-svd (fallback, no model download required)"
def fit_transform(self, texts):
"""Fit the backend on the corpus and return document vectors."""
if self._model is not None:
return self._model.encode(texts, normalize_embeddings=True)
from sklearn.feature_extraction.text import TfidfVectorizer
# max_df/min_df filtering only makes sense with a reasonably sized
# corpus — with very few documents they can conflict (e.g. max_df's
# document count works out to less than min_df's) and scikit-learn
# raises. Relax them below a threshold instead of crashing.
if len(texts) >= 5:
vectorizer_kwargs = {"max_df": 0.9}
else:
vectorizer_kwargs = {}
self._vectorizer = TfidfVectorizer(stop_words="english", **vectorizer_kwargs)
tfidf = self._vectorizer.fit_transform(texts)
max_components = min(tfidf.shape[0], tfidf.shape[1]) - 1
if max_components < 2:
# Too little data/vocabulary for a meaningful SVD — search directly
# on the (normalized) TF-IDF vectors instead.
self._svd = None
return self._normalize(tfidf.toarray())
from sklearn.decomposition import TruncatedSVD
n_components = min(100, max_components)
self._svd = TruncatedSVD(n_components=n_components, random_state=42)
vectors = self._svd.fit_transform(tfidf)
return self._normalize(vectors)
def transform(self, texts):
"""Embed new text (e.g. a query) using the already-fitted backend."""
if self._model is not None:
return self._model.encode(texts, normalize_embeddings=True)
tfidf = self._vectorizer.transform(texts)
if self._svd is None:
return self._normalize(tfidf.toarray())
vectors = self._svd.transform(tfidf)
return self._normalize(vectors)
@staticmethod
def _normalize(vectors):
vectors = np.asarray(vectors, dtype=np.float32)
norms = np.linalg.norm(vectors, axis=1, keepdims=True)
norms[norms == 0] = 1.0
return vectors / norms
class SemanticSearchEngine:
def __init__(self, docs):
self.docs = docs
self.backend = EmbeddingBackend()
texts = [d["text"] for d in docs]
self.doc_vectors = self.backend.fit_transform(texts)
def search(self, query, top_k=TOP_K_DEFAULT):
query_vector = self.backend.transform([query])[0]
scores = self.doc_vectors @ query_vector
ranked_idx = np.argsort(-scores)[:top_k]
return [(self.docs[i], float(scores[i])) for i in ranked_idx]
def snippet(text, length=140):
flat = " ".join(text.split())
return flat if len(flat) <= length else flat[:length].rstrip() + "..."
def print_results(results):
if not results:
print(" No results.")
return
for rank, (doc, score) in enumerate(results, start=1):
meta = doc["category"]
if doc.get("date"):
meta += f", {doc['date']}"
print(f" {rank}. [{score:.3f}] {doc['title']} ({meta}) — {doc['filename']}")
print(f" {snippet(doc['text'])}")
def parse_args():
parser = argparse.ArgumentParser(description="Semantic Search Mini-Engine")
parser.add_argument(
"--dir",
default=DEFAULT_DIR,
help=f"Folder to search (default: {DEFAULT_DIR})",
)
parser.add_argument(
"--ext",
default=",".join(DEFAULT_EXTENSIONS),
help="Comma-separated file extensions to index when no manifest.json "
f"is present (default: all supported types — {','.join(DEFAULT_EXTENSIONS)})",
)
parser.add_argument(
"--top-k",
type=int,
default=TOP_K_DEFAULT,
help=f"Number of results to show per query (default: {TOP_K_DEFAULT})",
)
return parser.parse_args()
def run_cli():
args = parse_args()
extensions = tuple(e.strip().lower() for e in args.ext.split(","))
docs = load_documents(args.dir, extensions)
print(f"Loaded {len(docs)} documents from {args.dir}/")
engine = SemanticSearchEngine(docs)
print(f"Embedding backend: {engine.backend.name}")
print('\nType a query and press Enter. Type "quit" or "exit" to leave.\n')
while True:
try:
query = input("Search> ").strip()
except (EOFError, KeyboardInterrupt):
print()
break
if not query:
continue
if query.lower() in {"quit", "exit"}:
break
results = engine.search(query, top_k=args.top_k)
print_results(results)
print()
if __name__ == "__main__":
run_cli()
Example output
I used AI to create some placeholder documents and then ran the semantic search script against them. Here's an example of what the output looks like when you run a query: