mydocker/camera-llm/camera-api/app/query_api.py

144 lines
5.9 KiB
Python

import os
from datetime import datetime, timedelta
from fastapi import FastAPI, Query
from fastapi.responses import HTMLResponse
import ollama
import chromadb
OLLAMA_URL = os.getenv("OLLAMA_URL", "http://ollama:11434")
CHROMA_URL = os.getenv("CHROMA_URL", "http://chromadb:8000")
ollama_client = ollama.Client(host=OLLAMA_URL)
chroma_host = CHROMA_URL.replace("http://", "").split(":")[0]
chroma_port = int(CHROMA_URL.split(":")[-1])
chroma = chromadb.HttpClient(host=chroma_host, port=chroma_port)
collection = chroma.get_or_create_collection("camera_frames")
app = FastAPI(title="Camera Memory API")
HTML_PAGE = """
<!DOCTYPE html>
<html>
<head>
<title>Camera Memory</title>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; background: #1a1a2e; color: #eee; }
h1 { color: #00d9ff; }
.search-box { display: flex; gap: 10px; margin-bottom: 20px; }
input[type="text"] { flex: 1; padding: 12px; font-size: 16px; border: none; border-radius: 8px; background: #16213e; color: #eee; }
button { padding: 12px 24px; font-size: 16px; background: #00d9ff; color: #1a1a2e; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; }
button:hover { background: #00b4d8; }
button:disabled { background: #555; cursor: wait; }
.answer { background: #16213e; padding: 20px; border-radius: 8px; margin-bottom: 20px; line-height: 1.6; }
.sources { font-size: 14px; color: #888; }
.source { background: #0f1729; padding: 10px; margin: 5px 0; border-radius: 4px; border-left: 3px solid #00d9ff; }
.source-time { color: #00d9ff; font-weight: bold; }
.stats { color: #666; font-size: 14px; margin-bottom: 20px; }
.loading { color: #00d9ff; }
</style>
</head>
<body>
<h1>📷 Camera Memory</h1>
<div class="stats" id="stats">Loading stats...</div>
<div class="search-box">
<input type="text" id="question" placeholder="Zapytaj np. 'kiedy była kobieta?' lub 'czy było czerwone auto?'" onkeypress="if(event.key==='Enter')ask()">
<button onclick="ask()" id="btn">Zapytaj</button>
</div>
<div id="result"></div>
<script>
async function loadStats() {
try {
const r = await fetch('/stats');
const data = await r.json();
document.getElementById('stats').innerHTML = `📊 Zapisanych klatek: <strong>${data.total_frames}</strong>`;
} catch(e) {
document.getElementById('stats').innerHTML = '❌ Błąd połączenia';
}
}
async function ask() {
const q = document.getElementById('question').value.trim();
if (!q) return;
const btn = document.getElementById('btn');
const result = document.getElementById('result');
btn.disabled = true;
btn.textContent = '';
result.innerHTML = '<div class="loading">Szukam w pamięci kamer...</div>';
try {
const r = await fetch('/ask?q=' + encodeURIComponent(q));
const data = await r.json();
let html = '<div class="answer">' + data.answer.replace(/\\n/g, '<br>') + '</div>';
if (data.sources && data.sources.length > 0) {
html += '<div class="sources"><strong>Źródła:</strong>';
for (const s of data.sources) {
html += `<div class="source"><span class="source-time">${s.time}</span> [${s.camera}]<br>${s.description}</div>`;
}
html += '</div>';
}
result.innerHTML = html;
} catch(e) {
result.innerHTML = '<div class="answer">❌ Błąd: ' + e.message + '</div>';
}
btn.disabled = false;
btn.textContent = 'Zapytaj';
}
loadStats();
setInterval(loadStats, 30000);
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
def home():
return HTML_PAGE
@app.get("/ask")
def ask(q: str = Query(..., description="Pytanie")):
q_emb = ollama_client.embeddings(model='nomic-embed-text', prompt=q)
results = collection.query(query_embeddings=[q_emb['embedding']], n_results=20, include=["documents", "metadatas"])
if not results['documents'][0]:
return {"answer": "Brak danych w pamięci kamer.", "sources": []}
context = "\n".join([f"[{m['datetime']} - {m['camera']}]: {doc}" for doc, m in zip(results['documents'][0], results['metadatas'][0])])
res = ollama_client.chat(
model='mistral',
messages=[
{'role': 'system', 'content': 'Odpowiadasz po polsku na podstawie obserwacji z kamer. Podawaj konkretne czasy. Bądź zwięzły.'},
{'role': 'user', 'content': f"Obserwacje z kamer:\n{context}\n\nPytanie: {q}"}
]
)
return {
"answer": res['message']['content'],
"sources": [{"time": m['datetime'], "camera": m['camera'], "description": doc[:100]} for doc, m in zip(results['documents'][0][:5], results['metadatas'][0][:5])]
}
@app.get("/recent")
def recent(camera: str = None, hours: int = 1):
cutoff = (datetime.now() - timedelta(hours=hours)).timestamp()
where = {"timestamp": {"$gt": cutoff}}
if camera:
where = {"$and": [where, {"camera": camera}]}
results = collection.get(where=where, include=["documents", "metadatas"])
return [{"time": m['datetime'], "camera": m['camera'], "description": doc} for doc, m in zip(results['documents'], results['metadatas'])]
@app.get("/stats")
def stats():
return {"total_frames": collection.count()}
@app.get("/health")
def health():
return {"status": "ok"}