mirror of
https://github.com/Anil-matcha/Open-Generative-AI.git
synced 2026-05-07 01:17:18 +00:00
Bundles three changes that were originally landed as stacked PRs into peer branches and never reached main. Combined here as a single PR targeting main. worker/index.js (new) --------------------- Long-lived Node process that: - Polls Postgres every 2s for batches in status='running'. - Claims queued jobs with SELECT ... FOR UPDATE SKIP LOCKED (single worker today, ready for N-worker scale tomorrow with no further code change). - Re-uploads local-only trainer images (/api/uploads/...) to MuAPI's /upload_file once and caches the resulting CDN URL on the trainer row so future jobs reuse it. - Submits POST /api/v1/<batch.model> with prompt, images_list, aspect_ratio, duration, quality. - Polls /api/v1/predictions/:id/result, extracts videoUrl from outputs[0]/url/output.url/video_url on success. - Backoff: min(10 * 3^retries, 300)s. After retries >= 3, status= 'failed', waiting for manual Retry from the UI. - Recovery on boot: re-queues anything left in 'submitting' from a crashed run; 'polling' jobs keep their muapiRequestId. Configuration: MUAPI_API_KEY (required to do work), MUAPI_BASE_URL (default https://api.muapi.ai), WORKER_TICK_MS (default 2000), UPLOAD_DIR (default /data/uploads). worker/package.json: { "type": "module" } so plain Node 20 runs the ESM imports. Dockerfile: copy /app/worker into the runner stage. docker-compose.override.yml: replace placeholder with the real 'npx prisma generate && node worker/index.js'. lib/promptTemplate.js (new) + worker integration ------------------------------------------------ renderPrompt({trainer, studio, job}) wraps each row's raw practice description in a fixed narrative template that pins identity, biomechanics, environment, lighting, clothing, expression, video style, and a list of forbidden behaviours (no camera movement, no cuts, no limb warping, no pose distortion, etc). duration, aspect_ratio, and quality remain SEPARATE fields in the MuAPI request body; they are never injected into the prompt text. lib/csvParser.js: stops appending 'Start position: ... Camera: ...' into the stored prompt. Just stores the raw practice description; startPosition + cameraAngle remain available as their own columns for the renderer to consume. components/SectionSwitcher.jsx (new) ------------------------------------ Two-pill switcher (Studio · Batch) rendered in the top-left of every shell. Active pill is yellow on black. Click writes the choice to localStorage so the home hub can highlight 'Last used'. StandaloneShell, BatchShell, BatchDetail headers all updated to host the switcher next to the logo. The redundant '← Studio' link in BatchShell's right-side actions is removed since the switcher replaces it. app/page.js + components/HomeHub.jsx ------------------------------------ / used to redirect to /studio. Now serves a hub with two cards: Batch (CSV-driven automation) and Studio (one-off generations). The card the user last opened gets a yellow accent + 'Last used' tag from localStorage. Stub-package copy ----------------- packages/ai-agent and packages/workflow-ui description strings and runtime 'feature unavailable' notices: removed organization-specific phrasing in favour of generic 'this fork' wording.
107 lines
3.7 KiB
JavaScript
107 lines
3.7 KiB
JavaScript
'use client';
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import ApiKeyModal from '@/components/ApiKeyModal';
|
|
import SectionSwitcher from '@/components/SectionSwitcher';
|
|
import TrainersTab from './TrainersTab';
|
|
import StudiosTab from './StudiosTab';
|
|
import BatchesTab from './BatchesTab';
|
|
|
|
const STORAGE_KEY = 'muapi_key';
|
|
|
|
const TABS = [
|
|
{ id: 'batches', label: 'Batches' },
|
|
{ id: 'trainers', label: 'Trainers' },
|
|
{ id: 'studios', label: 'Studios' },
|
|
];
|
|
|
|
export default function BatchShell() {
|
|
const [apiKey, setApiKey] = useState(null);
|
|
const [hasMounted, setHasMounted] = useState(false);
|
|
const [activeTab, setActiveTab] = useState('batches');
|
|
|
|
useEffect(() => {
|
|
setHasMounted(true);
|
|
const stored = typeof window !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null;
|
|
if (stored) {
|
|
setApiKey(stored);
|
|
document.cookie = `muapi_key=${stored}; path=/; max-age=31536000; SameSite=Lax`;
|
|
}
|
|
}, []);
|
|
|
|
const handleKeySave = (key) => {
|
|
localStorage.setItem(STORAGE_KEY, key);
|
|
document.cookie = `muapi_key=${key}; path=/; max-age=31536000; SameSite=Lax`;
|
|
setApiKey(key);
|
|
};
|
|
|
|
const handleKeyChange = () => {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
document.cookie = 'muapi_key=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
|
setApiKey(null);
|
|
};
|
|
|
|
if (!hasMounted) {
|
|
return (
|
|
<div className="min-h-screen bg-[#050505] flex items-center justify-center">
|
|
<div className="animate-spin text-[#d9ff00] text-3xl">◌</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!apiKey) {
|
|
return <ApiKeyModal onSave={handleKeySave} />;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#030303] text-white flex flex-col">
|
|
<header className="flex-shrink-0 h-14 border-b border-white/[0.03] flex items-center justify-between px-6 bg-black/20 backdrop-blur-md">
|
|
<div className="flex items-center gap-3">
|
|
<a href="/" className="flex items-center gap-3 hover:opacity-80 transition-opacity" title="Home">
|
|
<div className="w-8 h-8 bg-white rounded-lg flex items-center justify-center">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="black" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
|
</svg>
|
|
</div>
|
|
<span className="text-sm font-bold tracking-tight hidden sm:block">OpenGenerativeAI</span>
|
|
</a>
|
|
<SectionSwitcher active="batch" />
|
|
</div>
|
|
|
|
<nav className="flex items-center gap-6">
|
|
{TABS.map((tab) => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`relative py-4 text-[13px] font-medium transition-all whitespace-nowrap px-1 ${
|
|
activeTab === tab.id ? 'text-[#d9ff00]' : 'text-white/50 hover:text-white'
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
{activeTab === tab.id && (
|
|
<div className="absolute bottom-0 left-0 right-0 h-[2px] bg-[#d9ff00] rounded-full" />
|
|
)}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="flex items-center gap-3">
|
|
<button
|
|
onClick={handleKeyChange}
|
|
className="text-[11px] text-white/40 hover:text-red-400 transition-colors"
|
|
>
|
|
Change key
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="flex-1 overflow-y-auto px-6 py-8">
|
|
<div className="max-w-6xl mx-auto">
|
|
{activeTab === 'batches' && <BatchesTab apiKey={apiKey} />}
|
|
{activeTab === 'trainers' && <TrainersTab apiKey={apiKey} />}
|
|
{activeTab === 'studios' && <StudiosTab apiKey={apiKey} />}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|