Ken-AI-Co-Listener / pipeline.py
Zheng, Zaoyi
Fix: remove dead profiles import that breaks fresh clones
cfc74e7
Raw
History Blame Contribute Delete
5.11 kB
import json
import sys
import os
import concurrent.futures
from pathlib import Path
from openai import OpenAI
from faster_whisper import WhisperModel
from triggers import TRIGGER_PROMPTS
AMD_ENDPOINT = os.environ.get("AMD_ENDPOINT", "http://134.199.198.41:8000/v1")
MODEL_NAME = os.environ.get("MODEL_NAME", "Qwen/Qwen3-14B")
client = OpenAI(base_url=AMD_ENDPOINT, api_key="not-required")
def transcribe(audio_path: str) -> list[dict]:
model = WhisperModel("base", device="cpu", compute_type="int8")
segments, _ = model.transcribe(audio_path, beam_size=3, language="en")
results = []
for seg in segments:
text = seg.text.strip()
if len(text) > 10:
results.append({
"start": round(seg.start, 2),
"end": round(seg.end, 2),
"text": text,
})
return results
def merge_segments(segments: list[dict], max_chars: int = 300) -> list[dict]:
"""Merge short segments into chunks to reduce LLM calls."""
merged = []
current = None
for seg in segments:
if current is None:
current = {**seg}
elif len(current["text"]) + len(seg["text"]) < max_chars:
current["text"] += " " + seg["text"]
current["end"] = seg["end"]
else:
merged.append(current)
current = {**seg}
if current:
merged.append(current)
return merged
def run_trigger(trigger_name: str, prompt_template: str, segment: dict, profile: dict) -> dict | None:
prompt = prompt_template.format(
name=profile.get("name", ""),
situation=profile.get("situation", ""),
knowledge_level=profile.get("knowledge_level", "intermediate"),
concerns=", ".join(profile.get("concerns", [])),
segment_text=segment["text"],
)
try:
response = client.chat.completions.create(
model=MODEL_NAME,
messages=[{"role": "user", "content": prompt}],
max_tokens=500,
temperature=0.3,
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
)
content = response.choices[0].message.content.strip()
if "</think>" in content:
content = content.split("</think>")[-1].strip()
if content.startswith("```"):
content = content.split("\n", 1)[1].rsplit("```", 1)[0].strip()
result = json.loads(content)
if result.get("triggered"):
return {
"trigger": trigger_name,
"timestamp": segment["end"],
"end": segment["end"],
"segment_text": segment["text"],
"data": result,
}
except (json.JSONDecodeError, Exception) as e:
print(f" [{trigger_name}] parse error on segment @{segment['start']}s: {e}", file=sys.stderr)
return None
def process_segment_triggers(args: tuple) -> list[dict]:
"""Process one trigger on one segment (for parallel execution)."""
trigger_name, template, segment, profile = args
result = run_trigger(trigger_name, template, segment, profile)
return [result] if result else []
def run_pipeline(audio_path: str, profile: dict, max_workers: int = 8) -> dict:
print(f"[1/3] Transcribing: {audio_path}")
segments = transcribe(audio_path)
print(f" → {len(segments)} raw segments")
chunks = merge_segments(segments)
print(f" → merged into {len(chunks)} chunks")
print(f"[2/3] Running triggers ({len(chunks)} chunks × 4 triggers = {len(chunks)*4} calls, {max_workers} workers)...")
all_tasks = []
for chunk in chunks:
for trigger_name, template in TRIGGER_PROMPTS.items():
all_tasks.append((trigger_name, template, chunk, profile))
all_cards = []
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(process_segment_triggers, task) for task in all_tasks]
for future in concurrent.futures.as_completed(futures):
all_cards.extend(future.result())
all_cards.sort(key=lambda c: c["timestamp"])
print(f"[3/3] Done. {len(all_cards)} cards generated.")
return {
"profile": profile["name"],
"audio_file": audio_path,
"total_segments": len(segments),
"total_chunks": len(chunks),
"total_cards": len(all_cards),
"cards": all_cards,
"transcript": segments,
}
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python pipeline.py <audio_file> [situation]")
sys.exit(1)
audio_file = sys.argv[1]
situation = sys.argv[2] if len(sys.argv) > 2 else "General professional context"
profile = {
"name": "User",
"situation": situation,
"knowledge_level": "intermediate",
"concerns": [situation],
}
result = run_pipeline(audio_file, profile)
output_path = Path(audio_file).stem + f"_{profile_name}_cards.json"
with open(output_path, "w") as f:
json.dump(result, f, indent=2)
print(f"Output saved to: {output_path}")