feat: implement TTS, Document processing, and Memory Service /facts API

- TTS: xtts-v2 integration with voice cloning support
- Document: docling integration for PDF/DOCX/PPTX processing
- Memory Service: added /facts/upsert, /facts/{key}, /facts endpoints
- Added required dependencies (TTS, docling)
This commit is contained in:
Apple
2026-01-17 08:16:37 -08:00
parent a9fcadc6e2
commit 5290287058
121 changed files with 17071 additions and 436 deletions

View File

@@ -0,0 +1,126 @@
import { useEffect, useState } from 'react';
import { AlertCircle, CheckCircle2 } from 'lucide-react';
interface ImageGenHealth {
status: string;
model_loaded: boolean;
model_id: string;
device: string;
dtype: string;
}
interface ImageGenInfo {
model_id: string;
device: string;
dtype: string;
pipeline_loaded: boolean;
load_error?: string | null;
}
const getImageGenUrl = (nodeId?: string): string => {
if (!nodeId) {
return import.meta.env.VITE_IMAGE_GEN_URL || 'http://localhost:8892';
}
if (nodeId === 'node-1' || nodeId === 'node-1-hetzner-gex44' || nodeId.includes('node-1')) {
return import.meta.env.VITE_IMAGE_GEN_NODE1_URL || 'http://144.76.224.179:8892';
}
if (nodeId === 'node-3' || nodeId.includes('node-3')) {
return import.meta.env.VITE_IMAGE_GEN_NODE3_URL || 'http://80.77.35.151:8892';
}
return import.meta.env.VITE_IMAGE_GEN_URL || 'http://localhost:8892';
};
export function ImageGenStatusCard({ nodeId }: { nodeId?: string }) {
const [health, setHealth] = useState<ImageGenHealth | null>(null);
const [info, setInfo] = useState<ImageGenInfo | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const imageGenUrl = getImageGenUrl(nodeId);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const healthRes = await fetch(`${imageGenUrl}/health`, { mode: 'cors' });
if (!healthRes.ok) {
throw new Error(`Health check failed: ${healthRes.status}`);
}
const healthData = (await healthRes.json()) as ImageGenHealth;
setHealth(healthData);
const infoRes = await fetch(`${imageGenUrl}/info`, { mode: 'cors' });
if (infoRes.ok) {
const infoData = (await infoRes.json()) as ImageGenInfo;
setInfo(infoData);
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, [imageGenUrl]);
if (loading) {
return <div className="text-sm text-gray-500">Завантаження статусу Image Gen...</div>;
}
if (error || !health) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-start gap-2">
<AlertCircle className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" />
<div className="flex-1">
<h4 className="font-semibold text-red-900 mb-1">Image Gen Service недоступний</h4>
<p className="text-sm text-red-700 mb-2">{error || 'Немає даних'}</p>
<div className="text-xs text-red-600">
<p><strong>URL:</strong> {imageGenUrl}</p>
<p><strong>Node ID:</strong> {nodeId || 'N/A'}</p>
</div>
</div>
</div>
</div>
);
}
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-gray-900">🎨 Image Gen (FLUX)</h3>
<span className={`px-3 py-1 rounded-full text-sm font-semibold ${
health.status === 'ok' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
}`}>
{health.status === 'ok' ? 'Healthy' : 'Error'}
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm text-gray-600">
<div>
<div className="text-gray-500">Модель</div>
<div className="font-semibold text-gray-900">{info?.model_id || health.model_id}</div>
</div>
<div>
<div className="text-gray-500">Device / Dtype</div>
<div className="font-semibold text-gray-900">{health.device} / {health.dtype}</div>
</div>
<div className="flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-green-600" />
<span>{health.model_loaded ? 'Модель завантажена' : 'Модель ще вантажиться'}</span>
</div>
{info?.load_error && (
<div className="text-red-600">{info.load_error}</div>
)}
</div>
<div className="mt-4 text-xs text-gray-500">
Endpoint: <code className="text-xs">{imageGenUrl}</code>
</div>
</div>
);
}

View File

@@ -205,6 +205,8 @@ export function DagiMonitorPage() {
{ name: 'NATS JetStream', url: 'http://144.76.224.179:8222/varz', type: 'message-broker', port: 4222, description: 'Message broker for async communication' },
{ name: 'Swapper Node1', url: 'http://144.76.224.179:8890/health', type: 'service', port: 8890, description: 'LLM routing service on Node1' },
{ name: 'Swapper Node2', url: 'http://localhost:8890/health', type: 'service', port: 8890, description: 'LLM routing service on Node2' },
{ name: 'Image Gen Node1', url: 'http://144.76.224.179:8892/health', type: 'image-gen', port: 8892, description: 'FLUX image generation on Node1' },
{ name: 'Image Gen Node3', url: 'http://80.77.35.151:8892/health', type: 'image-gen', port: 8892, description: 'FLUX image generation on Node3' },
{ name: 'DAGI Router Node1', url: 'http://144.76.224.179:9102/health', type: 'router', port: 9102, description: 'DAGI Router on Node1' },
{ name: 'DAGI Router Node2', url: 'http://localhost:9102/health', type: 'router', port: 9102, description: 'DAGI Router on Node2' },
{ name: 'Main API', url: `${API_BASE_URL}/health`, type: 'api', description: 'Main MicroDAO API' },

View File

@@ -1,5 +1,5 @@
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, Server, Activity, Cpu, HardDrive, Network, Users, Settings, BarChart3, Plug, RefreshCw, CheckCircle2, XCircle, AlertCircle, Filter, Play, Loader2, Wrench, Download, Bot, Database, AlertTriangle, PlusCircle, Boxes, Shield } from 'lucide-react';
import { ArrowLeft, Server, Activity, Cpu, HardDrive, Network, Users, Settings, BarChart3, Plug, RefreshCw, CheckCircle2, XCircle, AlertCircle, Filter, Play, Loader2, Wrench, Download, Bot, Database, AlertTriangle, PlusCircle, Boxes, Shield, Zap } from 'lucide-react';
import { useState, useEffect } from 'react';
import { useQuery } from '@tanstack/react-query';
import { apiGet } from '../api/client';
@@ -8,6 +8,7 @@ import { getNode1Agents, type Node1Agent } from '../api/node1Agents';
import { deployAgentToNode2, deployAllAgentsToNode2, checkNode2AgentsDeployment } from '../api/node2Deployment';
import { SwapperStatusCard, SwapperMetricsSummary } from '../components/swapper/SwapperComponents';
import { SwapperDetailedMetrics } from '../components/swapper/SwapperDetailedMetrics';
import { ImageGenStatusCard } from '../components/image-gen/ImageGenStatusCard';
import { getNodeInventory, type NodeInventory } from '../api/nodeInventory';
import { NodeMonitorChat } from '../components/monitor/NodeMonitorChat';
import '../styles/swapper.css';
@@ -66,6 +67,14 @@ interface NodeDetails {
const GRAFANA_URL = import.meta.env.VITE_GRAFANA_URL || 'http://localhost:3000';
const PROMETHEUS_URL = import.meta.env.VITE_PROMETHEUS_URL || 'http://localhost:9090';
const formatServiceName = (name: string) => {
return name
.replace('dagi-', '')
.replace('swapper-service', 'Swapper Service')
.replace('image-gen-service', 'Image Gen Service')
.replace(/-/g, ' ');
};
export function NodeCabinetPage() {
const { nodeId } = useParams<{ nodeId: string }>();
const navigate = useNavigate();
@@ -151,7 +160,7 @@ export function NodeCabinetPage() {
const port = portStr.includes(':') ? portStr.split(':')[0] : portStr;
const portNum = parseInt(port) || 0;
services.push({
name: container.name.replace('dagi-', '').replace('swapper-service', 'Swapper Service').replace(/-/g, ' '),
name: formatServiceName(container.name),
status: 'running',
port: portNum,
url: isNode1
@@ -166,7 +175,7 @@ export function NodeCabinetPage() {
const port = portStr.includes(':') ? portStr.split(':')[0] : portStr;
const portNum = parseInt(port) || 0;
services.push({
name: container.name.replace('dagi-', '').replace(/-/g, ' '),
name: formatServiceName(container.name),
status: 'running',
port: portNum,
url: isNode1
@@ -181,7 +190,7 @@ export function NodeCabinetPage() {
const port = portStr.includes(':') ? portStr.split(':')[0] : portStr;
const portNum = parseInt(port) || 0;
services.push({
name: container.name.replace('dagi-', '').replace(/-/g, ' '),
name: formatServiceName(container.name),
status: container.state === 'restarting' ? 'restarting' : 'unhealthy',
port: portNum,
url: isNode1
@@ -193,6 +202,7 @@ export function NodeCabinetPage() {
// Fallback дані
services.push(
{ name: 'Swapper Service', status: 'running', port: 8890, url: isNode1 ? 'http://144.76.224.179:8890' : 'http://192.168.1.244:8890' },
{ name: 'Image Gen Service', status: 'running', port: 8892, url: isNode1 ? 'http://144.76.224.179:8892' : 'http://80.77.35.151:8892' },
{ name: 'Node Registry', status: 'running', port: 9205, url: isNode1 ? 'http://144.76.224.179:9205' : 'http://192.168.1.244:9205' },
{ name: 'NATS JetStream', status: 'running', port: 4222, url: 'nats://localhost:4222' }
);
@@ -604,6 +614,7 @@ export function NodeCabinetPage() {
{ name: 'Node Registry', icon: Database },
{ name: 'NATS JetStream', icon: Network },
{ name: 'Swapper Service', icon: RefreshCw },
{ name: 'Image Gen', icon: Zap },
{ name: 'Ollama', icon: Bot },
].map((service) => {
const s = nodeDetails.services?.find(s => s.name.includes(service.name) || (service.name === 'Ollama' && s.name.includes('ollama')));
@@ -945,6 +956,19 @@ export function NodeCabinetPage() {
</div>
</div>
{/* Image Generation Service */}
<div className="bg-white rounded-lg shadow">
<div className="p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900">🎨 Image Gen (FLUX)</h2>
<p className="text-sm text-gray-500 mt-1">
Генерація зображень для {nodeDetails.node_name} (FLUX.2 Klein 4B Base)
</p>
</div>
<div className="p-6">
<ImageGenStatusCard nodeId={nodeId} />
</div>
</div>
{/* Інші сервіси ноди */}
<div className="bg-white rounded-lg shadow">
<div className="p-6 border-b border-gray-200">