- Posted on
- • Artificial Intelligence
Artificial Intelligence Open Source Project Ideas
- Author
-
-
- User
- linuxbash
- Posts by this author
- Posts by this author
-
4 Practical AI Open‑Source Project Ideas You Can Build From Your Linux Terminal
If you’ve ever thought “I want to learn AI, but I don’t know what to build,” this is your sign to start. You don’t need a GPU cluster or a megacorp budget. With a Linux shell, a few open libraries, and some focused, real‑world problems, you can ship useful AI tools that run locally, teach you the fundamentals, and look great in a portfolio.
This article gives you four project ideas you can implement as open‑source, command‑line tools. Each idea includes why it matters, how it works, distro‑friendly setup, and a minimal working example you can extend.
Why these projects are worth your time
You’ll learn by shipping: model selection, data handling, packaging, and CLI UX.
They’re practical: solve clear problems (transcribe, dedupe, detect, monitor).
They’re portable: run on CPUs and common distros without cloud lock‑in.
They’re maintainable: simple, sharp tools you can grow into larger systems.
Prerequisites (apt, dnf, zypper)
The following system packages cover most examples below.
- Debian/Ubuntu (apt):
sudo apt update
sudo apt install -y git python3 python3-pip python3-venv build-essential cmake pkg-config curl jq fzf alsa-utils
- Fedora/RHEL (dnf):
sudo dnf install -y git python3 python3-pip python3-virtualenv @development-tools cmake pkgconfig curl jq fzf alsa-utils
- openSUSE (zypper):
sudo zypper refresh
sudo zypper install -y git python3 python3-pip python3-virtualenv gcc make cmake pkg-config curl jq fzf alsa-utils
Python virtual environment notes:
Preferred:
python3 -m venv .venv && source .venv/bin/activateIf
venvis unavailable, use:python3 -m virtualenv .venv && source .venv/bin/activate
Project 1: whisp.sh — Offline Terminal Transcriber with whisper.cpp
Turn your mic and terminal into a fast transcription station for meetings, notes, or captions. It’s private, fast, and works on CPUs.
What you’ll build:
- A Bash wrapper that records audio, runs whisper.cpp locally, and prints text to stdout.
Install and build whisper.cpp:
git clone https://github.com/ggerganov/whisper.cpp.git
cd whisper.cpp
make -j
bash ./models/download-ggml-model.sh base.en
Test a 10‑second mic capture and transcription:
arecord -d 10 -f cd -t wav -r 16000 -c 1 /tmp/clip.wav
./main -m ./models/ggml-base.en.bin -f /tmp/clip.wav
Create a reusable script:
cat > ~/bin/whisp.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DUR="${1:-10}"
OUT="${2:-/tmp/clip.wav}"
MODEL="${MODEL:-$HOME/whisper.cpp/models/ggml-base.en.bin}"
BIN="${BIN:-$HOME/whisper.cpp/main}"
echo "[*] Recording ${DUR}s to ${OUT}..."
arecord -d "$DUR" -f cd -t wav -r 16000 -c 1 "$OUT" >/dev/null 2>&1
echo "[*] Transcribing..."
"$BIN" -m "$MODEL" -f "$OUT" -otxt -of /tmp/whisper_out >/dev/null 2>&1
cat /tmp/whisper_out.txt
EOF
chmod +x ~/bin/whisp.sh
Usage:
whisp.sh 15
Ideas to extend:
Add
fzfprompt to pick audio files and batch transcribe.Pipe output to a notes app or a git‑tracked knowledge base.
Support multiple models (
tiny.en,small.en) based on CPU time/accuracy.
Project 2: dedupe — Fast Dataset/Text Deduplicator (MinHash + LSH)
If you work with text datasets, duplicates inflate training costs and bias evaluation. Build a CLI that removes near‑duplicates using MinHash and Locality‑Sensitive Hashing.
Create a new repo and environment:
mkdir -p ~/code/dedupe && cd ~/code/dedupe
python3 -m venv .venv || python3 -m virtualenv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install datasketch tqdm
Minimal dedupe CLI:
cat > dedupe.py <<'EOF'
#!/usr/bin/env python3
import sys, argparse, hashlib
from datasketch import MinHash, MinHashLSH
from tqdm import tqdm
def shingle(text, k=5):
text = text.strip().lower()
return { text[i:i+k] for i in range(max(0, len(text)-k+1)) }
def mhash(text, num_perm=128):
mh = MinHash(num_perm=num_perm)
for s in shingle(text):
mh.update(s.encode("utf-8"))
return mh
def main():
ap = argparse.ArgumentParser(description="Near-duplicate remover using MinHash+LSH")
ap.add_argument("infile", help="Input text file (one doc per line)")
ap.add_argument("--out", default="deduped.txt", help="Output file")
ap.add_argument("--threshold", type=float, default=0.85, help="Jaccard threshold")
ap.add_argument("--num-perm", type=int, default=128)
args = ap.parse_args()
lsh = MinHashLSH(threshold=args.threshold, num_perm=args.num_perm)
kept, seen_keys = [], set()
with open(args.infile, "r", encoding="utf-8", errors="ignore") as f:
lines = f.readlines()
for line in tqdm(lines, desc="Indexing"):
# fast exact-dup guard:
h = hashlib.sha1(line.strip().encode("utf-8")).hexdigest()
if h in seen_keys:
continue
seen_keys.add(h)
mh = mhash(line, num_perm=args.num_perm)
near = lsh.query(mh)
if not near:
key = f"k{len(kept)}"
lsh.insert(key, mh)
kept.append(line)
with open(args.out, "w", encoding="utf-8") as w:
w.writelines(kept)
print(f"Input lines: {len(lines)} -> Unique-ish: {len(kept)} (threshold={args.threshold})")
if __name__ == "__main__":
main()
EOF
chmod +x dedupe.py
Run:
./dedupe.py data.txt --threshold 0.9 --out clean.txt
Ideas to extend:
Accept JSONL with a field selector.
Emit a mapping of canonical → duplicates.
Parallelize hashing; add Bloom filters for memory.
Project 3: detect — CPU‑friendly Object Detection with ONNX Runtime
Build an image detection CLI that runs YOLOv5n (nano) ONNX models on CPU. Perfect for quick automation (batch scans, alerts, sorting photos).
Set up environment:
mkdir -p ~/code/detect && cd ~/code/detect
python3 -m venv .venv || python3 -m virtualenv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install onnxruntime opencv-python numpy requests
Download a pre‑trained model:
wget -O yolov5n.onnx https://github.com/ultralytics/yolov5/releases/download/v7.0/yolov5n.onnx
Minimal detector (prints JSON per image):
cat > detect.py <<'EOF'
#!/usr/bin/env python3
import sys, json, argparse, cv2, numpy as np, onnxruntime as ort
COCO80 = ["person","bicycle","car","motorcycle","airplane","bus","train","truck",
"boat","traffic light","fire hydrant","stop sign","parking meter","bench","bird",
"cat","dog","horse","sheep","cow","elephant","bear","zebra","giraffe","backpack",
"umbrella","handbag","tie","suitcase","frisbee","skis","snowboard","sports ball",
"kite","baseball bat","baseball glove","skateboard","surfboard","tennis racket",
"bottle","wine glass","cup","fork","knife","spoon","bowl","banana","apple",
"sandwich","orange","broccoli","carrot","hot dog","pizza","donut","cake","chair",
"couch","potted plant","bed","dining table","toilet","tv","laptop","mouse",
"remote","keyboard","cell phone","microwave","oven","toaster","sink","refrigerator",
"book","clock","vase","scissors","teddy bear","hair drier","toothbrush"]
def letterbox(img, new_shape=(640,640), color=(114,114,114)):
h, w = img.shape[:2]
r = min(new_shape[0]/h, new_shape[1]/w)
nh, nw = int(round(h*r)), int(round(w*r))
dh, dw = new_shape[0]-nh, new_shape[1]-nw
top, bottom = dh//2, dh-dh//2
left, right = dw//2, dw-dw//2
img = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR)
img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
return img, r, (left, top)
def nms(boxes, scores, iou_thres=0.45):
idxs = np.argsort(scores)[::-1]
keep = []
while len(idxs):
i = idxs[0]
keep.append(i)
if len(idxs) == 1: break
rest = idxs[1:]
iou = bbox_iou(boxes[i], boxes[rest])
idxs = rest[iou <= iou_thres]
return keep
def bbox_iou(box, boxes):
# boxes: [N,4]
x1 = np.maximum(box[0], boxes[:,0])
y1 = np.maximum(box[1], boxes[:,1])
x2 = np.minimum(box[2], boxes[:,2])
y2 = np.minimum(box[3], boxes[:,3])
inter = np.maximum(0, x2-x1) * np.maximum(0, y2-y1)
uni = (box[2]-box[0])*(box[3]-box[1]) + (boxes[:,2]-boxes[:,0])*(boxes[:,3]-boxes[:,1]) - inter
return inter / (uni + 1e-6)
def infer(sess, img_path, conf_thres=0.25, iou_thres=0.45):
img0 = cv2.imread(img_path)
if img0 is None:
raise RuntimeError(f"Could not read image: {img_path}")
img, r, (dx, dy) = letterbox(img0, (640,640))
x = img[:, :, ::-1].transpose(2,0,1).astype(np.float32) / 255.0
x = np.expand_dims(x, 0)
out = sess.run(None, {sess.get_inputs()[0].name: x})[0] # [1,25200,85]
out = out[0]
boxes = out[:, :4]
scores = out[:, 4:5] * out[:, 5:]
cls = np.argmax(scores, axis=1)
conf = scores[np.arange(scores.shape[0]), cls]
m = conf > conf_thres
boxes, conf, cls = boxes[m], conf[m], cls[m]
# xywh -> xyxy in original image scale
if boxes.size:
xy = boxes[:, :2]; wh = boxes[:, 2:]
xyxy = np.empty_like(boxes)
xyxy[:,0] = xy[:,0] - wh[:,0]/2
xyxy[:,1] = xy[:,1] - wh[:,1]/2
xyxy[:,2] = xy[:,0] + wh[:,0]/2
xyxy[:,3] = xy[:,1] + wh[:,1]/2
# undo letterbox
xyxy[:,[0,2]] -= dx
xyxy[:,[1,3]] -= dy
xyxy /= r
# clip
h, w = img0.shape[:2]
xyxy[:,[0,2]] = np.clip(xyxy[:,[0,2]], 0, w)
xyxy[:,[1,3]] = np.clip(xyxy[:,[1,3]], 0, h)
else:
xyxy = boxes
keep = nms(xyxy, conf, iou_thres=iou_thres) if len(conf) else []
dets = []
for i in keep:
dets.append({
"class_id": int(cls[i]),
"class_name": COCO80[int(cls[i])] if int(cls[i]) < len(COCO80) else str(int(cls[i])),
"conf": float(conf[i]),
"xyxy": [float(x) for x in xyxy[i].tolist()]
})
return dets
def main():
ap = argparse.ArgumentParser(description="YOLOv5n ONNX CPU detector")
ap.add_argument("images", nargs="+")
ap.add_argument("--model", default="yolov5n.onnx")
ap.add_argument("--conf", type=float, default=0.25)
ap.add_argument("--iou", type=float, default=0.45)
args = ap.parse_args()
sess = ort.InferenceSession(args.model, providers=["CPUExecutionProvider"])
for p in args.images:
dets = infer(sess, p, args.conf, args.iou)
print(json.dumps({"image": p, "detections": dets}, ensure_ascii=False))
if __name__ == "__main__":
main()
EOF
chmod +x detect.py
Run:
./detect.py sample.jpg | jq .
Ideas to extend:
Output annotated images; add a
--saveflag.Batch over a directory; export CSV.
Build a udev/systemd pair to watch a folder and auto‑label new images.
Project 4: logad — Linux Log Anomaly Detector (Isolation Forest)
System logs are noisy. This tool learns “normal” patterns from journalctl and flags unusual lines in real time.
Environment:
mkdir -p ~/code/logad && cd ~/code/logad
python3 -m venv .venv || python3 -m virtualenv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install scikit-learn numpy tqdm
CLI that “fits” on historical logs and “watches” live logs:
cat > logad.py <<'EOF'
#!/usr/bin/env python3
import argparse, os, sys, pickle, subprocess, re, time
import numpy as np
from sklearn.ensemble import IsolationForest
from tqdm import tqdm
MODEL_PATH = "logad_model.pkl"
DIM = 1024 # hashed feature dimension
TOKEN_SPLIT = re.compile(r"[^\w:/\.-]+")
def featurize(line, dim=DIM):
vec = np.zeros(dim, dtype=np.float32)
for tok in TOKEN_SPLIT.split(line.lower()):
if not tok: continue
h = hash(tok) % dim
vec[h] += 1.0
# simple length/entropy-ish signals
vec[0] = min(len(line) / 2000.0, 1.0)
return vec
def read_journal(n=50000):
cmd = ["journalctl", "-o", "short-iso", "-n", str(n)]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
for line in p.stdout:
yield line.strip()
def fit_model(n=50000):
X = []
for line in tqdm(read_journal(n), total=n, desc="Reading logs"):
X.append(featurize(line))
X = np.vstack(X) if X else np.zeros((0, DIM), dtype=np.float32)
if len(X) == 0:
print("No logs read; aborting.", file=sys.stderr); sys.exit(1)
clf = IsolationForest(n_estimators=200, contamination="auto", random_state=42)
clf.fit(X)
with open(MODEL_PATH, "wb") as f:
pickle.dump(clf, f)
print(f"Saved model to {MODEL_PATH} with {len(X)} samples.")
def watch(threshold=-0.25):
if not os.path.exists(MODEL_PATH):
print("Model not found. Run: logad.py fit", file=sys.stderr); sys.exit(1)
with open(MODEL_PATH, "rb") as f:
clf = pickle.load(f)
# follow journal in real time
cmd = ["journalctl", "-o", "short-iso", "-f", "-n", "0"]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
for line in iter(p.stdout.readline, ""):
x = featurize(line).reshape(1, -1)
score = clf.score_samples(x)[0] # lower = more anomalous
if score < threshold:
print(f"[ANOMALY score={score:.3f}] {line}", flush=True)
def main():
ap = argparse.ArgumentParser(description="Log anomaly detector (Isolation Forest)")
sub = ap.add_subparsers(dest="cmd", required=True)
ap_fit = sub.add_parser("fit", help="Train on recent logs")
ap_fit.add_argument("--n", type=int, default=50000)
ap_watch = sub.add_parser("watch", help="Stream and flag anomalies")
ap_watch.add_argument("--threshold", type=float, default=-0.25)
args = ap.parse_args()
if args.cmd == "fit":
fit_model(args.n)
elif args.cmd == "watch":
watch(args.threshold)
if __name__ == "__main__":
main()
EOF
chmod +x logad.py
Train and watch:
./logad.py fit --n 75000
sudo ./logad.py watch --threshold -0.3
Ideas to extend:
Whitelist noisy units; add
--unit sshd.service.Emit JSON to stdout and integrate with
jq/fzf.Package as a systemd service with rate‑limited notifications.
Tips for shipping your projects
Start with a clean CLI:
--help, clear defaults, sensible exit codes.Add a README with examples, benchmarks, and screenshots or asciinema.
Include a small test dataset and a simple
make testtarget.Provide packages or a one‑liner installer script.
License it early (MIT/Apache‑2.0) and write CONTRIBUTING.md.
Conclusion and next steps
AI doesn’t have to be a black box or a cloud bill. These four projects are concrete, useful, and approachable from a Linux terminal:
whisp.sh: private, offline transcription
dedupe: cleaner datasets with near‑dup detection
detect: CPU object detection for quick automation
logad: anomaly alerts from your own logs
Pick one, fork the snippets above, and ship a v0.1 this week. When you do, write about what you learned and link your repo. If you want a review, open an issue with your project URL and I’ll give feedback on performance, packaging, and UX.
Happy hacking—and may your prompts, hashes, and NMS always converge.