Answers confirmed:
All three paths run the same brain. The only difference is what happens at the output step.
The one question this prototype needs to answer:
"Does the legal analysis logic work accurately enough to be useful in a real session — and what's the fastest way to find out?"
Everything else (speaker ID, dashboards, archive systems) is downstream of this.
Switching between mic and livestream is a single function swap. The rest of the pipeline is unchanged.
The idea: Python + Streamlit running entirely on a laptop in the committee room. No server, no deploy, no accounts. One terminal command → browser tab opens.
Who accesses it: The laptop screen directly. Optionally expose to the MP's phone via ngrok — one extra terminal command, zero code change.
Build sequence:
Stack:
Python 3.11+
├── sounddevice # mic capture
├── openai # Whisper API
├── google-generativeai # Gemini API
└── streamlit # UI
Core code sketch:
import sounddevice as sd
import tempfile, wave, time
from openai import OpenAI
import google.generativeai as genai
CHUNK_SECONDS = 30
SAMPLE_RATE = 16000
openai_client = OpenAI()
genai.configure(api_key="YOUR_GEMINI_KEY")
model = genai.GenerativeModel("gemini-1.5-pro")
SYSTEM_PROMPT = """
คุณคือ Smart MP Advisor ผู้เชี่ยวชาญด้านข้อบังคับการประชุมรัฐสภาและรัฐธรรมนูญ
[paste Smart MP Advisor instructions here]
วิเคราะห์ข้อความต่อไปนี้ และตอบกลับในรูปแบบ:
สถานะ: NORMAL หรือ AT_RISK
ข้อบังคับที่เกี่ยวข้อง: (ถ้ามี)
เหตุผล: (สั้นๆ)
"""
def record_chunk():
audio = sd.rec(int(CHUNK_SECONDS * SAMPLE_RATE),
samplerate=SAMPLE_RATE, channels=1, dtype='int16')
sd.wait()
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
with wave.open(f.name, 'wb') as wf:
wf.setnchannels(1); wf.setsampwidth(2); wf.setframerate(SAMPLE_RATE)
wf.writeframes(audio.tobytes())
return f.name
def transcribe(path):
with open(path, "rb") as f:
return openai_client.audio.transcriptions.create(
model="whisper-1", file=f, language="th"
).text
def analyze(transcript):
return model.generate_content(
f"{SYSTEM_PROMPT}\n\nข้อความ: {transcript}"
).text
while True:
path = record_chunk()
transcript = transcribe(path)
analysis = analyze(transcript)
print(f"[{time.strftime('%H:%M:%S')}] {transcript}")
print(f"→ {analysis}\n")
time.sleep(1)
For phone access (zero code change):
# Terminal 1
streamlit run app.py
# Terminal 2
ngrok http 8501
Pros: Fastest to build. Easiest to iterate. Everything visible on screen. Cons: Laptop must stay open. Phone access needs ngrok (one extra step).
The idea: Same Python backend. Output is pushed as a LINE message to the MP's phone. No new interface — the alert arrives where the MP is already looking.
Why this fits: LINE penetration in Thai political circles is near 100%. Zero setup required on the MP's side.
Build sequence:
Two LINE options:
| Option | Setup time | Cost | Best for |
|---|---|---|---|
| LINE Notify | 5 min, personal token | Free | 1 recipient, fastest test |
| LINE Messaging API | 30 min, channel setup | Free tier | Multiple recipients, richer format |
Core push function:
import requests
LINE_NOTIFY_TOKEN = "YOUR_TOKEN"
def push_to_line(status: str, transcript: str, article: str, reason: str):
icon = "🟢" if status == "NORMAL" else "🔴"
msg = f"""
{icon} {status}
──────────────────
📝 {transcript[:150]}{'...' if len(transcript) > 150 else ''}
⚖️ {article if article else '-'}
💬 {reason}
""".strip()
requests.post(
"https://notify-api.line.me/api/notify",
headers={"Authorization": f"Bearer {LINE_NOTIFY_TOKEN}"},
data={"message": msg}
)
What the MP sees:
🔴 AT_RISK
──────────────────
📝 "ท่านประธานที่หน้าตาดีที่นั่งอยู่..."
⚖️ ข้อบังคับการประชุม ข้อ 45
💬 อาจเข้าข่ายการใช้ถ้อยคำเสียดสี
Pros: Zero new interface for the MP. Most socially invisible. Fastest to working demo. Cons: LINE Notify deprecated end-2025 (Messaging API is the upgrade path). No running log view.
The idea: Build the pipeline as an n8n workflow. Entire logic visible and editable on the n8n canvas. No Python to maintain beyond the 10-line audio recorder.
Why this fits: n8n is already running in this environment. The workflow can be handed off to, or modified by, non-developers.
Build sequence:
Companion recorder (10 lines — run once, leave running):
# recorder.py
import sounddevice as sd, wave, time
while True:
audio = sd.rec(30 * 16000, samplerate=16000, channels=1, dtype='int16')
sd.wait()
with wave.open("/tmp/chunk.wav", "wb") as f:
f.setnchannels(1); f.setsampwidth(2); f.setframerate(16000)
f.writeframes(audio.tobytes())
time.sleep(1)
n8n Gemini HTTP node config:
{
"method": "POST",
"url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro:generateContent",
"headers": { "x-goog-api-key": "{{ $env.GEMINI_KEY }}" },
"body": {
"contents": [{
"parts": [{ "text": "[Smart MP Advisor system prompt]\n\nข้อความ: {{ $json.transcript }}" }]
}]
}
}
Pros: Entire logic visible on canvas. Easy to hand off. Logging and multi-output trivial to add. Cons: Audio capture still needs the 10-line Python companion.
| Path A — Streamlit | Path B — LINE Bot | Path C — n8n | |
|---|---|---|---|
| Build time | 2–3 days | 1–2 days | 2–3 days |
| Code required | Medium (Python + Streamlit) | Low (Python + API call) | Minimal (10-line recorder) |
| Infrastructure | Laptop only | Laptop + LINE account | Laptop + n8n instance |
| MP experience | Watch screen / open URL | Check LINE (already open) | Check LINE / email |
| Visual running log | ✅ Yes | ❌ Push per alert | ❌ Push per alert |
| Modifiable by non-dev | ❌ No | ❌ No | ✅ Yes (n8n canvas) |
| Livestream input | ✅ Easy swap | ✅ Easy swap | ✅ Easy swap |
| Multi-recipient | Via ngrok URL share | ✅ Native (LINE group) | ✅ Native (multiple nodes) |
| Auto-logging | Manual | Manual | ✅ Google Sheets node |
| Best for | Demos + iteration | Deployment + daily use | Handoff + auditability |
All three paths use the same Whisper + Gemini + Smart MP Advisor core. Switching between paths costs 1 day, not 1 week.
Given the confirmed answers — MP checks their phone, n8n already running, no live dashboard needed:
Path A becomes relevant only if a visual running log proves necessary after the first test.
def capture_livestream_chunk(url, duration=30, output="/tmp/chunk.wav"):
subprocess.run([
"ffmpeg", "-y", "-i", url,
"-t", str(duration), "-ar", "16000", "-ac", "1", output
], capture_output=True)
Replace record_chunk() with capture_livestream_chunk(url) in any path. Everything downstream is unchanged.
The prototype is done when all four are true:
Everything beyond that is the next sprint.