chore(cli): 🛠 Update CLI tool version to 1.0.0

This commit is contained in:
Lilith 2026-01-17 12:02:24 -08:00
parent b508251b71
commit 7ecd45e3c6
4 changed files with 1028 additions and 0 deletions

View file

@ -0,0 +1,82 @@
# Essential Extractor Configuration - Fast Core Extraction
# Duration: ~62s
# Aspects: ~15 (core essentials only)
# Use case: Quick oneoff tests, ./run shortcuts
# Model: ministral-14b-reasoning (single model for all stages)
name: essential
description: "Core aspects only - fastest execution"
version: 1.0.0
# Single model for entire pipeline
modelConfig:
model: ministral-14b-reasoning
warmup: true # Warmup once at pipeline start
# SEO-specific factor weights
factorWeights:
cultural: 0.9 # Highest - SEO keywords are cultural terms
category: 0.8 # High - category drives search intent
composition: 0.7 # Medium-high
material: 0.6 # Medium
maturity: 0.5 # Medium
geographic: 0.1 # LOWEST - location doesn't define aesthetic for SEO
# Pipeline configuration
pipeline:
- stage: term_analysis
parallel: true
duration_estimate: 10
extractors:
- term_style_analysis
- term_gender_analysis
- term_maturity_analysis
- stage: synthesis
parallel: false
depends: [term_analysis]
duration_estimate: 20
extractors:
- term_interaction_synthesis
- stage: context
parallel: false
depends: [synthesis]
duration_estimate: 12
extractors:
- category_integration
- stage: resolution
parallel: false
depends: [context]
duration_estimate: 10
extractors:
- weighted_hierarchy_resolution
- stage: validation
parallel: false
depends: [resolution]
duration_estimate: 10
extractors:
- coherence_validation
# Expected output aspects (15 total)
outputAspects:
essential:
- style # anime vs photorealistic
- subject_count # 1, 2, 3+
- gender_composition # [male], [female], etc.
- maturity_level # 7-level spectrum
- client_figure_required # true/false
validation:
- coherence_check
- overall_confidence
- full_reasoning_chain
# Performance estimates
performance:
total_duration_estimate: 62
llm_calls: 5
parallelizable_calls: 1
sequential_calls: 4

View file

@ -0,0 +1,139 @@
# Full Extractor Configuration - Comprehensive Production Extraction
# Duration: ~97s
# Aspects: ~40 (production quality)
# Use case: Standard SEO production generation
# Model: ministral-14b-reasoning (single model for all stages)
name: full
description: "Comprehensive extraction - standard for production"
version: 1.0.0
# Single model for entire pipeline
modelConfig:
model: ministral-14b-reasoning
warmup: true # Warmup once at pipeline start
# SEO-specific factor weights
factorWeights:
cultural: 0.9 # Highest - SEO keywords are cultural terms
category: 0.8 # High - category drives search intent
audience_appeal: 0.8 # High - critical for SEO conversion
composition: 0.7 # Medium-high - affects visual appeal
material: 0.6 # Medium - descriptive attributes
maturity: 0.5 # Medium - varies by audience
geographic: 0.1 # LOWEST - location doesn't define aesthetic for SEO
# 7-level maturity taxonomy
maturityLevels:
- sfw
- suggestive
- mature
- explicit_soft
- explicit_nude
- explicit_sexual
- extreme
# Pipeline configuration - composable extractors
pipeline:
- stage: term_analysis
parallel: true
duration_estimate: 15
extractors:
- term_style_analysis
- term_gender_analysis
- term_audience_analysis
- term_maturity_analysis
- term_aesthetic_analysis
- term_power_analysis
- stage: synthesis
parallel: false
depends: [term_analysis]
duration_estimate: 20
extractors:
- term_interaction_synthesis
- stage: context
parallel: true # category and geographic can run in parallel
depends: [synthesis]
duration_estimate: 12
extractors:
- category_integration
- geographic_integration
- stage: resolution
parallel: false
depends: [context]
duration_estimate: 10
extractors:
- weighted_hierarchy_resolution
- stage: deep_dive
parallel: true # audience and sdxl specs can run in parallel
depends: [resolution]
duration_estimate: 15
extractors:
- audience_deep_dive
- sdxl_technical_specs
- stage: validation
parallel: false
depends: [deep_dive]
duration_estimate: 10
extractors:
- coherence_validation
# Expected output aspects (~40 total)
outputAspects:
essential:
- style
- subject_count
- gender_composition
- maturity_level
- client_figure_required
audience:
- target_audience
- audience_expectations
- presentation_appeal
- cultural_community
power:
- power_dynamic
- service_provider_role
- interaction_type
aesthetic:
- aesthetic_tone
- dominant_mood
- clothing_style
- color_palette
- emotional_expression
composition:
- pose_type
- setting_environment
- camera_framing
- background_complexity
sdxl_technical:
- shot_type
- camera_angle
- lighting_style
- depth_of_field
- quality_tags
- color_scheme
- composition_rule
validation:
- coherence_check
- overall_confidence
- full_reasoning_chain
# Performance estimates
performance:
total_duration_estimate: 97
total_llm_calls: 11
parallel_groups: 3
warmup_duration: 12 # Model warmup with model-boss
execution_duration: 85 # Actual extraction after warmup

View file

@ -0,0 +1,117 @@
# Maximum Extractor Configuration - Complete Extraction
# Duration: ~94s (with heavy parallelization)
# Aspects: 100+ (maximum detail)
# Use case: Critical production, A/B testing, quality validation
# Model: ministral-14b-reasoning (single model for all stages)
name: maximum
description: "All extractors - maximum detail for critical use cases"
version: 1.0.0
# Single model for entire pipeline
modelConfig:
model: ministral-14b-reasoning
warmup: true # Warmup once at pipeline start
# SEO-specific factor weights
factorWeights:
cultural: 0.9
category: 0.8
audience_appeal: 0.8
composition: 0.7
material: 0.6
maturity: 0.5
geographic: 0.1 # LOWEST for SEO
# 7-level maturity taxonomy
maturityLevels:
- sfw
- suggestive
- mature
- explicit_soft
- explicit_nude
- explicit_sexual
- extreme
# Pipeline configuration - all available extractors
pipeline:
- stage: term_analysis
parallel: true
duration_estimate: 15
extractors:
- term_style_analysis
- term_gender_analysis
- term_audience_analysis
- term_maturity_analysis
- term_aesthetic_analysis
- term_power_analysis
- term_clothing_analysis
- term_physical_details_analysis
- term_emotional_analysis
- term_cultural_context_analysis
- stage: synthesis
parallel: false
depends: [term_analysis]
duration_estimate: 20
extractors:
- term_interaction_synthesis
- stage: context
parallel: true # category, geographic, temporal all parallel
depends: [synthesis]
duration_estimate: 12
extractors:
- category_integration
- geographic_integration
- temporal_context_analysis
- stage: resolution
parallel: false
depends: [context]
duration_estimate: 10
extractors:
- weighted_hierarchy_resolution
- stage: deep_dive
parallel: true # 3 extractors in parallel
depends: [resolution]
duration_estimate: 15
extractors:
- audience_deep_dive
- sdxl_technical_specs
- power_dynamics_deep_dive
- stage: refinement
parallel: true # 5 extractors in parallel
depends: [deep_dive]
duration_estimate: 12
extractors:
- color_palette_deep_analysis
- composition_refinement
- emotional_depth_analysis
- lighting_detailed_specs
- character_archetype_analysis
- stage: validation
parallel: false
depends: [refinement]
duration_estimate: 10
extractors:
- coherence_validation
# Expected output aspects (100+ total from all 25 categories)
outputAspects:
# All 228 aspects from plan can be extracted
total_extractable: 228
typical_extracted: 100-150 # Actual extraction depends on request complexity
# Performance estimates
performance:
total_duration_estimate: 94
warmup_duration: 12
execution_duration: 82
total_llm_calls: 17
parallel_groups: 4
parallelization_savings: 120 # vs sequential execution
EOF

690
tooling/cli/imajin Executable file
View file

@ -0,0 +1,690 @@
#!/usr/bin/env python3
"""
Imajin CLI - Service orchestration and testing tool
Manages all imajin microservices:
- llama-http (port 8202) - LLM backend for Ministral-14B
- imajin-request-classifier (port 8005) - Stage 1: Cultural classification
- imajin-prompt-generator (port 8006) - Stage 2: SDXL prompt generation
- imajin-diffusion (port 8002) - Image generation
- imajin-processing (port 8004) - Post-processing
- imajin orchestrator (port 8080) - Main API
Usage:
imajin start [service] - Start all services or specific service
imajin stop [service] - Stop all services or specific service
imajin health - Check health of all services
imajin test <request> - Make test request to orchestrator
imajin logs <service> - Tail logs for service
"""
import asyncio
import atexit
import base64
import json
import os
import signal
import socket
import subprocess
import sys
import time
import uuid
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional
import httpx
# Service definitions
SERVICES = {
"llama-http": {
"port": 8202,
"cwd": Path.home() / "Code/@applications/@ml/llama-http",
"command": [
"python",
"-m",
"llama_http",
],
"env": {
"LLAMA_HTTP_MODEL_ID": "ministral-14b-reasoning",
"LLAMA_HTTP_PORT": "8202",
},
"health": "http://localhost:8202/health",
"venv": ".venv",
},
"reasoning": {
"port": 8007,
"cwd": Path(__file__).parent.parent.parent / "services/imajin-reasoning/service",
"command": ["python", "-m", "src.api.main"],
"health": "http://localhost:8007/health",
"venv": ".venv",
},
"classifier": {
"port": 8005,
"cwd": Path(__file__).parent.parent.parent / "services/imajin-request-classifier",
"command": ["python", "-m", "service.src.api.main"],
"health": "http://localhost:8005/health",
"venv": "service/.venv",
},
"prompt-generator": {
"port": 8006,
"cwd": Path(__file__).parent.parent.parent / "services/imajin-prompt-generator",
"command": ["python", "-m", "service.src.api.main"],
"health": "http://localhost:8006/health",
"venv": "service/.venv",
},
"diffusion": {
"port": 8002,
"cwd": Path(__file__).parent.parent.parent / "services/imajin-diffusion/service",
"command": ["python", "-m", "src.api.main"],
"health": "http://localhost:8002/health",
"venv": ".venv",
},
"processing": {
"port": 8004,
"cwd": Path(__file__).parent.parent.parent / "services/imajin-processing/service",
"command": ["npm", "run", "start:dev"],
"health": "http://localhost:8004/health",
},
"orchestrator": {
"port": 8080,
"cwd": Path(__file__).parent.parent.parent / "imajin/src",
"command": ["python", "-m", "imajin.main"],
"health": "http://localhost:8080/health",
"venv": "../.venv",
},
}
# Process tracking
PROCESSES: Dict[str, subprocess.Popen] = {}
# Services required for test command (in startup order)
REQUIRED_SERVICES_FOR_TEST = [
"llama-http", # LLM backend (must start first)
"classifier", # Cultural classification
"prompt-generator", # SDXL prompt generation
"diffusion", # Image generation
"orchestrator", # Main API
]
# Test sessions for isolated concurrent execution
TEST_SESSIONS: Dict[str, "TestSession"] = {}
class TestSession:
"""Isolated test session with dynamic port allocation."""
def __init__(self, session_id: str, ports: Dict[str, int]):
self.session_id = session_id
self.ports = ports
self.processes: Dict[str, subprocess.Popen] = {}
self.orchestrator_url = f"http://localhost:{ports['orchestrator']}"
def cleanup(self):
"""Stop all services for this test session."""
for service_name, proc in list(self.processes.items()):
try:
proc.terminate()
proc.wait(timeout=5)
print(f"✅ Stopped {service_name} (session {self.session_id[:8]})")
except subprocess.TimeoutExpired:
proc.kill()
print(f"⚠️ Force killed {service_name} (session {self.session_id[:8]})")
except Exception as e:
print(f"⚠️ Failed to stop {service_name}: {e}")
# Remove from global registry
if self.session_id in TEST_SESSIONS:
del TEST_SESSIONS[self.session_id]
def find_available_ports(count: int, start_port: int = 9000) -> list[int]:
"""Find N consecutive available ports starting from start_port.
Args:
count: Number of consecutive ports needed
start_port: Port to start searching from
Returns:
List of available port numbers
"""
def is_port_available(port: int) -> bool:
"""Check if a port is available for binding."""
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(("", port))
return True
except OSError:
return False
# Try to find consecutive ports
current_port = start_port
max_attempts = 1000
for _ in range(max_attempts):
ports = list(range(current_port, current_port + count))
if all(is_port_available(p) for p in ports):
return ports
current_port += 1
raise RuntimeError(f"Could not find {count} consecutive available ports after {max_attempts} attempts")
def allocate_test_ports() -> Dict[str, int]:
"""Allocate dynamic ports for a test session.
Returns:
Dict mapping service names to allocated ports
"""
ports_needed = 5 # llama-http, classifier, prompt-generator, diffusion, orchestrator
available_ports = find_available_ports(ports_needed)
return {
"llama-http": available_ports[0],
"classifier": available_ports[1],
"prompt-generator": available_ports[2],
"diffusion": available_ports[3],
"orchestrator": available_ports[4],
}
def get_venv_python(service_name: str) -> str:
"""Get path to Python in venv for service."""
service = SERVICES[service_name]
if "venv" not in service:
return "python"
venv_path = service["cwd"] / service["venv"]
python_path = venv_path / "bin/python"
return str(python_path) if python_path.exists() else "python"
def start_service_with_config(
service_name: str,
port: int,
extra_env: Dict[str, str],
session: Optional[TestSession] = None,
) -> Optional[subprocess.Popen]:
"""Start a service with custom port and environment configuration.
Args:
service_name: Name of service to start
port: Port number to use
extra_env: Additional environment variables
session: Test session to track process in (if part of test)
Returns:
Process handle if started successfully
"""
service = SERVICES[service_name]
cwd = service["cwd"]
if not cwd.exists():
print(f"❌ Service directory not found: {cwd}")
return None
# Build command with venv python if applicable
command = service["command"].copy()
if "venv" in service and command[0] == "python":
command[0] = get_venv_python(service_name)
# Build environment
env = os.environ.copy()
if "env" in service:
env.update(service["env"])
env.update(extra_env)
# Start process
session_info = f" (session {session.session_id[:8]})" if session else ""
print(f"🚀 Starting {service_name} on port {port}{session_info}...")
proc = subprocess.Popen(
command,
cwd=str(cwd),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
# Track in session if provided, otherwise in global registry
if session:
session.processes[service_name] = proc
else:
PROCESSES[service_name] = proc
print(f"✅ Started {service_name} (PID {proc.pid})")
return proc
async def check_health(service_name: str, port: Optional[int] = None) -> bool:
"""Check if service is healthy.
Args:
service_name: Name of service to check
port: Custom port (if None, uses default from SERVICES)
Returns:
True if healthy, False otherwise
"""
service = SERVICES[service_name]
health_url = service.get("health")
if not health_url:
return True # No health check defined
# Override port if provided
if port is not None:
default_port = service["port"]
health_url = health_url.replace(f":{default_port}", f":{port}")
try:
async with httpx.AsyncClient() as client:
response = await client.get(health_url, timeout=5)
return response.status_code == 200
except Exception:
return False
async def start_test_services(session: TestSession) -> bool:
"""Start all required services for a test session with allocated ports.
Args:
session: Test session with allocated ports
Returns:
True if all services started and became healthy
"""
ports = session.ports
# Configure environment variables for each service
# llama-http
llama_env = {
"LLAMA_HTTP_PORT": str(ports["llama-http"]),
}
start_service_with_config("llama-http", ports["llama-http"], llama_env, session)
time.sleep(2)
# classifier - needs to know llama-http URL (base URL only, path appended by client)
classifier_env = {
"PORT": str(ports["classifier"]),
"LLM_SERVICE_URL": f"http://localhost:{ports['llama-http']}",
}
start_service_with_config("classifier", ports["classifier"], classifier_env, session)
time.sleep(2)
# prompt-generator - needs to know llama-http URL (base URL only, path appended by client)
prompt_gen_env = {
"PORT": str(ports["prompt-generator"]),
"LLM_SERVICE_URL": f"http://localhost:{ports['llama-http']}",
}
start_service_with_config("prompt-generator", ports["prompt-generator"], prompt_gen_env, session)
time.sleep(2)
# diffusion
diffusion_env = {
"IMAGE_GEN_PORT": str(ports["diffusion"]),
}
start_service_with_config("diffusion", ports["diffusion"], diffusion_env, session)
time.sleep(2)
# orchestrator - needs to know all downstream service URLs
# Uses IMAJIN_ prefix for env vars (see imajin/src/imajin/settings.py)
orchestrator_env = {
"IMAJIN_API_PORT": str(ports["orchestrator"]),
"IMAJIN_IMAJIN_CLASSIFIER_URL": f"http://localhost:{ports['classifier']}",
"IMAJIN_IMAJIN_PROMPT_GENERATOR_URL": f"http://localhost:{ports['prompt-generator']}",
"IMAJIN_IMAJIN_DIFFUSION_URL": f"http://localhost:{ports['diffusion']}",
}
start_service_with_config("orchestrator", ports["orchestrator"], orchestrator_env, session)
# Wait for all services to become healthy
print(f"\n⏳ Waiting for services to become healthy (session {session.session_id[:8]})...\n")
max_wait = 180
for service_name in REQUIRED_SERVICES_FOR_TEST:
port = ports[service_name]
waited = 0
while waited < max_wait:
healthy = await check_health(service_name, port)
if healthy:
print(f" ✅ {service_name} is healthy (port {port})")
break
await asyncio.sleep(2)
waited += 2
if waited >= max_wait:
print(f" ❌ {service_name} failed to become healthy after {max_wait}s")
session.cleanup()
return False
print(f"\n✅ All services healthy for session {session.session_id[:8]}!\n")
return True
def start_service(service_name: str, background: bool = True) -> Optional[subprocess.Popen]:
"""Start a service."""
if service_name in PROCESSES:
print(f"⚠️ {service_name} already running (PID {PROCESSES[service_name].pid})")
return PROCESSES[service_name]
service = SERVICES[service_name]
cwd = service["cwd"]
if not cwd.exists():
print(f"❌ Service directory not found: {cwd}")
return None
# Build command with venv python if applicable
command = service["command"].copy()
if "venv" in service and command[0] == "python":
command[0] = get_venv_python(service_name)
# Build environment
env = os.environ.copy()
if "env" in service:
env.update(service["env"])
# Start process
print(f"🚀 Starting {service_name} on port {service['port']}...")
if background:
proc = subprocess.Popen(
command,
cwd=str(cwd),
env=env,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
PROCESSES[service_name] = proc
print(f"✅ Started {service_name} (PID {proc.pid})")
return proc
else:
# Run in foreground
subprocess.run(command, cwd=str(cwd), env=env)
return None
def stop_service(service_name: str):
"""Stop a service."""
if service_name not in PROCESSES:
# Try to kill by port
service = SERVICES[service_name]
port = service["port"]
result = subprocess.run(
["lsof", "-ti", f":{port}"],
capture_output=True,
text=True,
)
if result.stdout.strip():
pids = result.stdout.strip().split("\n")
for pid in pids:
try:
os.kill(int(pid), signal.SIGTERM)
print(f"✅ Stopped {service_name} (PID {pid})")
except Exception as e:
print(f"⚠️ Failed to stop {service_name}: {e}")
else:
print(f"⚠️ {service_name} not running")
return
proc = PROCESSES[service_name]
proc.terminate()
try:
proc.wait(timeout=5)
print(f"✅ Stopped {service_name} (PID {proc.pid})")
except subprocess.TimeoutExpired:
proc.kill()
print(f"⚠️ Force killed {service_name} (PID {proc.pid})")
del PROCESSES[service_name]
async def health_check_all():
"""Check health of all services."""
print("\n🏥 Health Check\n" + "=" * 50)
for service_name in SERVICES:
healthy = await check_health(service_name)
status = "✅ HEALTHY" if healthy else "❌ UNHEALTHY"
port = SERVICES[service_name]["port"]
print(f"{service_name:20} (port {port:5}) {status}")
print()
async def ensure_services_running(required_services: list[str]) -> bool:
"""
Ensure all required services are running and healthy.
Args:
required_services: List of service names that must be running
Returns:
True if all services are healthy, False otherwise
"""
print("\n🔍 Checking service health...\n")
# Check current health status
unhealthy = []
for service_name in required_services:
healthy = await check_health(service_name)
status = "✅" if healthy else "❌"
print(f" {service_name:20} {status}")
if not healthy:
unhealthy.append(service_name)
# Start unhealthy services
if unhealthy:
print(f"\n🚀 Starting {len(unhealthy)} service(s)...\n")
for service_name in unhealthy:
start_service(service_name, background=True)
time.sleep(2) # Brief delay between starts
# Wait for services to become healthy (max 180s per service for SDXL model loading)
print("\n⏳ Waiting for services to become healthy...\n")
max_wait = 180
for service_name in unhealthy:
waited = 0
while waited < max_wait:
healthy = await check_health(service_name)
if healthy:
print(f" ✅ {service_name} is healthy")
break
await asyncio.sleep(2)
waited += 2
if waited >= max_wait:
print(f" ❌ {service_name} failed to become healthy after {max_wait}s")
return False
print("\n✅ All required services are healthy!\n")
return True
async def test_request(category: str, city: str, filters: str):
"""Make a test request with isolated services on dynamic ports.
Creates a new test session, starts all services with allocated ports,
runs the test, and cleans up afterward.
"""
# Create test session with allocated ports
session_id = str(uuid.uuid4())
try:
ports = allocate_test_ports()
except RuntimeError as e:
print(f"❌ Port allocation failed: {e}")
return
session = TestSession(session_id, ports)
TEST_SESSIONS[session_id] = session
print(f"\n🔧 Test Session {session_id[:8]}")
print(f" Ports allocated: llama-http={ports['llama-http']}, "
f"classifier={ports['classifier']}, prompt-gen={ports['prompt-generator']}, "
f"diffusion={ports['diffusion']}, orchestrator={ports['orchestrator']}\n")
# Register cleanup on exit
def cleanup_on_exit():
if session_id in TEST_SESSIONS:
TEST_SESSIONS[session_id].cleanup()
atexit.register(cleanup_on_exit)
try:
# Start all services for this test session
services_ready = await start_test_services(session)
if not services_ready:
print("\n❌ Failed to start required services. Aborting test.")
session.cleanup()
return
filter_list = filters.split(",") if filters else []
request_data = {
"category": category,
"city": city,
"role": "hero",
"filters": filter_list,
}
print(f"📝 Test Request")
print(f" Category: {category}")
print(f" City: {city}")
print(f" Filters: {filter_list}")
print()
# Make request to orchestrator using session URL
async with httpx.AsyncClient(timeout=180) as client:
response = await client.post(
f"{session.orchestrator_url}/generate",
json=request_data,
)
response.raise_for_status()
data = response.json()
if not data.get("success"):
print(f"❌ Generation failed: {data.get('error', 'Unknown error')}")
return
print("✅ Success!")
print(f" Model: {data['metadata']['model']}")
print(f" Prompt: {data['metadata']['prompt'][:100]}...")
# Save image to /tmp
if data.get("image_base64"):
image_data = base64.b64decode(data["image_base64"])
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_path = f"/tmp/imajin_test_{timestamp}.png"
with open(output_path, "wb") as f:
f.write(image_data)
print(f" 💾 Image saved to: {output_path}")
print(f" 📏 Size: {len(image_data)} bytes")
else:
print(" ⚠️ No image data in response")
except httpx.HTTPStatusError as e:
print(f"❌ Request failed with status {e.response.status_code}")
print(f" Response: {e.response.text}")
except httpx.RequestError as e:
print(f"❌ Request failed: {e}")
except Exception as e:
print(f"❌ Unexpected error: {e}")
finally:
# Cleanup test session
print(f"\n🧹 Cleaning up test session {session_id[:8]}...")
session.cleanup()
print("✅ Cleanup complete")
def main():
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
command = sys.argv[1]
if command == "start":
service_name = sys.argv[2] if len(sys.argv) > 2 else None
if service_name:
if service_name not in SERVICES:
print(f"❌ Unknown service: {service_name}")
print(f" Available: {', '.join(SERVICES.keys())}")
sys.exit(1)
start_service(service_name)
else:
# Start all services in dependency order
print("🚀 Starting all imajin services...\n")
for svc in ["llama-http", "classifier", "prompt-generator", "diffusion", "processing", "orchestrator"]:
start_service(svc)
time.sleep(2) # Brief delay between services
print("\n✅ All services started!")
asyncio.run(health_check_all())
elif command == "stop":
service_name = sys.argv[2] if len(sys.argv) > 2 else None
if service_name:
if service_name not in SERVICES:
print(f"❌ Unknown service: {service_name}")
sys.exit(1)
stop_service(service_name)
else:
# Stop all services
print("🛑 Stopping all imajin services...\n")
for svc in SERVICES:
stop_service(svc)
elif command == "health":
asyncio.run(health_check_all())
elif command == "test":
if len(sys.argv) < 5:
print("Usage: imajin test <category> <city> <filters>")
print("Example: imajin test escorts Tokyo 'femboy,latex'")
sys.exit(1)
category = sys.argv[2]
city = sys.argv[3]
filters = sys.argv[4] if len(sys.argv) > 4 else ""
asyncio.run(test_request(category, city, filters))
elif command == "logs":
if len(sys.argv) < 3:
print("Usage: imajin logs <service>")
sys.exit(1)
service_name = sys.argv[2]
if service_name not in PROCESSES:
print(f"❌ {service_name} not running")
sys.exit(1)
proc = PROCESSES[service_name]
print(f"📋 Logs for {service_name} (PID {proc.pid})")
print("=" * 50)
# Tail the output
for line in proc.stdout:
print(line, end="")
else:
print(f"❌ Unknown command: {command}")
print(__doc__)
sys.exit(1)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\n\n🛑 Interrupted - stopping all services...")
for service_name in list(PROCESSES.keys()):
stop_service(service_name)
sys.exit(0)