144 lines
5.9 KiB
Python
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"}
|