feat(aurora-smart): add dual-stack orchestration with policy, audit, and UI toggle

This commit is contained in:
Apple
2026-03-01 06:21:17 -08:00
parent 5b4c4f92ba
commit 1ea4464838
2 changed files with 918 additions and 5 deletions

View File

@@ -253,6 +253,23 @@ _aurora_live_samples: Dict[str, collections.deque] = {}
_aurora_live_last: Dict[str, Dict[str, Any]] = {}
_aurora_live_last_loaded = False
_aurora_live_last_path = (AURORA_DATA_DIR.parent / "sofiia-console-cache" / "aurora_live_last.json")
_aurora_smart_runs: Dict[str, Dict[str, Any]] = {}
_aurora_smart_runs_loaded = False
_aurora_smart_runs_path = (AURORA_DATA_DIR.parent / "sofiia-console-cache" / "aurora_smart_runs.json")
_aurora_smart_policy: Dict[str, Any] = {
"updated_at": None,
"strategies": {
"local_only": {"count": 0, "avg_score": 0.0, "wins": 0, "losses": 0},
"local_then_kling": {"count": 0, "avg_score": 0.0, "wins": 0, "losses": 0},
},
}
_aurora_smart_policy_loaded = False
_aurora_smart_policy_path = (AURORA_DATA_DIR.parent / "sofiia-console-cache" / "aurora_smart_policy.json")
_AURORA_SMART_MAX_RUNS = max(20, int(os.getenv("AURORA_SMART_MAX_RUNS", "200")))
_AURORA_SMART_LOCAL_POLL_SEC = max(2.0, float(os.getenv("AURORA_SMART_LOCAL_POLL_SEC", "3.0")))
_AURORA_SMART_KLING_POLL_SEC = max(3.0, float(os.getenv("AURORA_SMART_KLING_POLL_SEC", "6.0")))
_AURORA_SMART_LOCAL_MAX_SEC = max(60.0, float(os.getenv("AURORA_SMART_LOCAL_MAX_SEC", "10800")))
_AURORA_SMART_KLING_MAX_SEC = max(60.0, float(os.getenv("AURORA_SMART_KLING_MAX_SEC", "3600")))
MEDIA_COMFY_AGENT_URL = os.getenv(
"MEDIA_COMFY_AGENT_URL",
"http://comfy-agent:8880" if _is_container_runtime() else "http://127.0.0.1:8880",
@@ -352,6 +369,10 @@ async def lifespan(app_: Any):
task = asyncio.create_task(_nodes_poll_loop())
logger.info("Nodes poll loop started (interval=%ds)", _NODES_POLL_INTERVAL)
try:
_smart_resume_active_monitors()
except Exception as e:
logger.warning("aurora smart monitor resume failed: %s", e)
yield
task.cancel()
try:
@@ -972,6 +993,505 @@ def _aurora_persist_live_last_to_disk() -> None:
logger.debug("aurora live-last persist failed: %s", e)
def _smart_now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def _smart_is_terminal(status: Any) -> bool:
return str(status or "").lower() in {"completed", "failed", "cancelled"}
def _smart_media_type(file_name: str, content_type: str) -> str:
name = str(file_name or "").lower()
ctype = str(content_type or "").lower()
video_ext = (".mp4", ".avi", ".mov", ".mkv", ".webm")
audio_ext = (".mp3", ".wav", ".flac", ".m4a", ".aac", ".ogg")
image_ext = (".jpg", ".jpeg", ".png", ".webp", ".tif", ".tiff", ".bmp")
if ctype.startswith("video/") or name.endswith(video_ext):
return "video"
if ctype.startswith("audio/") or name.endswith(audio_ext):
return "audio"
if ctype.startswith("image/") or name.endswith(image_ext):
return "photo"
return "unknown"
def _smart_trim_runs() -> None:
if len(_aurora_smart_runs) <= _AURORA_SMART_MAX_RUNS:
return
ordered = sorted(
_aurora_smart_runs.items(),
key=lambda kv: str((kv[1] or {}).get("created_at") or ""),
reverse=True,
)
keep = dict(ordered[:_AURORA_SMART_MAX_RUNS])
_aurora_smart_runs.clear()
_aurora_smart_runs.update(keep)
def _smart_load_runs_from_disk() -> None:
global _aurora_smart_runs_loaded
if _aurora_smart_runs_loaded:
return
_aurora_smart_runs_loaded = True
try:
if not _aurora_smart_runs_path.exists():
return
payload = json.loads(_aurora_smart_runs_path.read_text(encoding="utf-8"))
if isinstance(payload, dict):
runs = payload.get("runs")
else:
runs = payload
if isinstance(runs, dict):
for run_id, run in runs.items():
if isinstance(run_id, str) and isinstance(run, dict):
_aurora_smart_runs[run_id] = run
_smart_trim_runs()
except Exception as exc:
logger.debug("aurora smart-runs load failed: %s", exc)
def _smart_persist_runs() -> None:
try:
_smart_trim_runs()
_aurora_smart_runs_path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"updated_at": _smart_now_iso(),
"runs": _aurora_smart_runs,
}
_aurora_smart_runs_path.write_text(
json.dumps(payload, ensure_ascii=False, separators=(",", ":")),
encoding="utf-8",
)
except Exception as exc:
logger.debug("aurora smart-runs persist failed: %s", exc)
def _smart_load_policy_from_disk() -> None:
global _aurora_smart_policy_loaded
if _aurora_smart_policy_loaded:
return
_aurora_smart_policy_loaded = True
try:
if not _aurora_smart_policy_path.exists():
return
payload = json.loads(_aurora_smart_policy_path.read_text(encoding="utf-8"))
if isinstance(payload, dict):
strategies = payload.get("strategies")
if isinstance(strategies, dict):
_aurora_smart_policy["strategies"] = strategies
_aurora_smart_policy["updated_at"] = payload.get("updated_at")
except Exception as exc:
logger.debug("aurora smart-policy load failed: %s", exc)
def _smart_persist_policy() -> None:
try:
_aurora_smart_policy["updated_at"] = _smart_now_iso()
_aurora_smart_policy_path.parent.mkdir(parents=True, exist_ok=True)
_aurora_smart_policy_path.write_text(
json.dumps(_aurora_smart_policy, ensure_ascii=False, separators=(",", ":")),
encoding="utf-8",
)
except Exception as exc:
logger.debug("aurora smart-policy persist failed: %s", exc)
def _smart_strategy_stats(strategy: str) -> Dict[str, Any]:
_smart_load_policy_from_disk()
strategies = _aurora_smart_policy.setdefault("strategies", {})
stats = strategies.get(strategy)
if not isinstance(stats, dict):
stats = {"count": 0, "avg_score": 0.0, "wins": 0, "losses": 0}
strategies[strategy] = stats
return stats
def _smart_update_strategy_score(strategy: str, score: float) -> None:
stats = _smart_strategy_stats(strategy)
try:
count = int(stats.get("count") or 0) + 1
avg = float(stats.get("avg_score") or 0.0)
stats["avg_score"] = round(((avg * (count - 1)) + float(score)) / max(1, count), 4)
stats["count"] = count
_smart_persist_policy()
except Exception:
return
def _smart_update_strategy_outcome(strategy: str, success: bool) -> None:
stats = _smart_strategy_stats(strategy)
key = "wins" if success else "losses"
stats[key] = int(stats.get(key) or 0) + 1
_smart_persist_policy()
def _smart_new_run_id() -> str:
stamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
return f"smart_{stamp}_{uuid.uuid4().hex[:6]}"
def _smart_append_audit(run: Dict[str, Any], event: str, detail: Optional[Dict[str, Any]] = None) -> None:
audit = run.setdefault("audit", [])
if not isinstance(audit, list):
audit = []
run["audit"] = audit
item: Dict[str, Any] = {"ts": _smart_now_iso(), "event": str(event)}
if isinstance(detail, dict) and detail:
item["detail"] = detail
audit.append(item)
if len(audit) > 200:
del audit[:-200]
run["updated_at"] = item["ts"]
def _smart_analysis_features(analysis: Optional[Dict[str, Any]]) -> Dict[str, Any]:
if not isinstance(analysis, dict):
return {
"faces": 0,
"plates": 0,
"noise": "unknown",
"blur": "unknown",
"quality_score": 0.0,
}
faces = len(analysis.get("faces") or []) if isinstance(analysis.get("faces"), list) else 0
plates = len(analysis.get("license_plates") or []) if isinstance(analysis.get("license_plates"), list) else 0
qa = analysis.get("quality_analysis") if isinstance(analysis.get("quality_analysis"), dict) else {}
noise = str(qa.get("noise_level") or "unknown").lower()
blur = str(qa.get("blur_level") or "unknown").lower()
score = 0.0
score += min(2.0, faces * 0.2)
score += min(2.0, plates * 0.4)
if noise in {"high", "very_high"}:
score += 1.0
if blur in {"high", "very_high"}:
score += 1.0
return {
"faces": faces,
"plates": plates,
"noise": noise,
"blur": blur,
"quality_score": round(score, 3),
}
def _smart_decide_strategy(
*,
media_type: str,
mode: str,
requested_strategy: str,
prefer_quality: bool,
budget_tier: str,
analysis: Optional[Dict[str, Any]],
learning_enabled: bool,
) -> Dict[str, Any]:
strategy = str(requested_strategy or "auto").strip().lower()
valid = {"auto", "local_only", "local_then_kling"}
if strategy not in valid:
strategy = "auto"
features = _smart_analysis_features(analysis)
reasons: List[str] = []
score = 0.0
if media_type != "video":
chosen = "local_only"
reasons.append("non-video media -> local stack only")
return {"strategy": chosen, "reasons": reasons, "score": 0.0, "features": features}
if strategy in {"local_only", "local_then_kling"}:
reasons.append(f"explicit strategy={strategy}")
return {"strategy": strategy, "reasons": reasons, "score": features["quality_score"], "features": features}
score += float(features["quality_score"])
if prefer_quality:
score += 1.3
reasons.append("prefer_quality=true")
if str(mode).lower() == "forensic":
score += 0.8
reasons.append("forensic mode")
budget_norm = str(budget_tier or "normal").strip().lower()
if budget_norm == "low":
score -= 1.4
reasons.append("budget_tier=low")
elif budget_norm == "high":
score += 0.6
reasons.append("budget_tier=high")
if learning_enabled:
stats = _smart_strategy_stats("local_then_kling")
wins = int(stats.get("wins") or 0)
losses = int(stats.get("losses") or 0)
total = wins + losses
if total >= 6:
success_ratio = wins / max(1, total)
if success_ratio >= 0.65:
score += 0.5
reasons.append(f"learned success ratio {success_ratio:.2f}")
elif success_ratio <= 0.35:
score -= 0.7
reasons.append(f"learned low success ratio {success_ratio:.2f}")
chosen = "local_then_kling" if score >= 2.1 else "local_only"
if not reasons:
reasons.append("default heuristic")
return {"strategy": chosen, "reasons": reasons, "score": round(score, 3), "features": features}
def _smart_compact_result(result_payload: Dict[str, Any]) -> Dict[str, Any]:
payload = {}
if not isinstance(result_payload, dict):
return payload
payload["mode"] = result_payload.get("mode")
payload["media_type"] = result_payload.get("media_type")
payload["digital_signature"] = result_payload.get("digital_signature")
output_files = result_payload.get("output_files")
if isinstance(output_files, list):
payload["output_files"] = output_files[:8]
q = result_payload.get("quality_report")
if isinstance(q, dict):
payload["quality_report"] = q
return payload
async def _smart_fetch_run_status(run_id: str) -> Optional[Dict[str, Any]]:
_smart_load_runs_from_disk()
run = _aurora_smart_runs.get(run_id)
if not isinstance(run, dict):
return None
return run
async def _smart_monitor_run(run_id: str) -> None:
run = await _smart_fetch_run_status(run_id)
if not run:
return
local = run.get("local") if isinstance(run.get("local"), dict) else {}
local_job_id = str(local.get("job_id") or "")
if not local_job_id:
_smart_append_audit(run, "monitor.error", {"reason": "missing local job id"})
run["status"] = "failed"
run["phase"] = "failed"
_smart_persist_runs()
return
start = time.monotonic()
while time.monotonic() - start <= _AURORA_SMART_LOCAL_MAX_SEC:
try:
st = await _aurora_request_json(
"GET",
f"/api/aurora/status/{quote(local_job_id, safe='')}",
timeout=20.0,
retries=2,
retry_backoff_sec=0.25,
)
except Exception as exc:
_smart_append_audit(run, "local.status.error", {"error": str(exc)[:220]})
await asyncio.sleep(_AURORA_SMART_LOCAL_POLL_SEC)
continue
status = str(st.get("status") or "").lower()
if status in {"queued", "processing"}:
run["phase"] = "local_processing"
run["status"] = "processing"
elif status == "completed":
run["phase"] = "local_completed"
run["status"] = "processing"
else:
run["phase"] = f"local_{status or 'unknown'}"
run["status"] = status
run["local"] = {
**local,
"job_id": local_job_id,
"status": status,
"progress": st.get("progress"),
"current_stage": st.get("current_stage"),
"eta_seconds": st.get("eta_seconds"),
"live_fps": st.get("live_fps"),
"error_message": st.get("error_message"),
"updated_at": _smart_now_iso(),
}
_smart_persist_runs()
if status in {"queued", "processing"}:
await asyncio.sleep(_AURORA_SMART_LOCAL_POLL_SEC)
continue
if status != "completed":
run["status"] = "failed"
run["phase"] = "local_failed"
_smart_append_audit(
run,
"local.failed",
{"status": status, "error": str(st.get("error_message") or "")[:220]},
)
_smart_update_strategy_outcome(str(run.get("policy", {}).get("strategy") or "local_only"), False)
_smart_persist_runs()
return
_smart_append_audit(run, "local.completed", {"job_id": local_job_id})
break
else:
run["status"] = "failed"
run["phase"] = "local_timeout"
_smart_append_audit(run, "local.timeout", {"max_sec": _AURORA_SMART_LOCAL_MAX_SEC})
_smart_update_strategy_outcome(str(run.get("policy", {}).get("strategy") or "local_only"), False)
_smart_persist_runs()
return
try:
local_result = await _aurora_request_json(
"GET",
f"/api/aurora/result/{quote(local_job_id, safe='')}",
timeout=30.0,
retries=2,
retry_backoff_sec=0.25,
)
except Exception as exc:
run["status"] = "failed"
run["phase"] = "local_result_error"
_smart_append_audit(run, "local.result.error", {"error": str(exc)[:240]})
_smart_update_strategy_outcome(str(run.get("policy", {}).get("strategy") or "local_only"), False)
_smart_persist_runs()
return
run.setdefault("local", {})
if isinstance(run["local"], dict):
run["local"]["result"] = _smart_compact_result(local_result)
run["local"]["result_ready"] = True
run["selected_stack"] = "local"
policy = run.get("policy") if isinstance(run.get("policy"), dict) else {}
strategy = str(policy.get("strategy") or "local_only")
media_type = str(run.get("media_type") or "")
kling = run.get("kling") if isinstance(run.get("kling"), dict) else {}
if strategy != "local_then_kling" or media_type != "video":
run["status"] = "completed"
run["phase"] = "completed"
_smart_append_audit(run, "smart.completed", {"selected_stack": "local", "reason": "strategy local_only or non-video"})
_smart_update_strategy_outcome(strategy, True)
_smart_persist_runs()
return
run["phase"] = "kling_submitting"
run["status"] = "processing"
_smart_append_audit(run, "kling.submit.start")
_smart_persist_runs()
try:
submit = await _aurora_request_json(
"POST",
"/api/aurora/kling/enhance",
data={
"job_id": local_job_id,
"prompt": str(kling.get("prompt") or "enhance video quality, improve sharpness and clarity"),
"negative_prompt": str(kling.get("negative_prompt") or "noise, blur, artifacts, distortion"),
"mode": str(kling.get("mode") or "pro"),
"duration": str(kling.get("duration") or "5"),
"cfg_scale": str(kling.get("cfg_scale") if kling.get("cfg_scale") is not None else "0.5"),
},
timeout=120.0,
retries=1,
retry_backoff_sec=0.25,
)
except Exception as exc:
run["kling"] = {
**kling,
"status": "failed",
"error": str(exc)[:320],
}
run["status"] = "completed"
run["phase"] = "completed_with_kling_failure"
run["selected_stack"] = "local"
_smart_append_audit(run, "kling.submit.error", {"error": str(exc)[:220]})
_smart_update_strategy_outcome(strategy, False)
_smart_persist_runs()
return
task_id = str(submit.get("kling_task_id") or "")
run["kling"] = {
**kling,
"task_id": task_id,
"status": str(submit.get("status") or "submitted").lower(),
"submitted_at": _smart_now_iso(),
}
_smart_append_audit(run, "kling.submitted", {"task_id": task_id})
_smart_persist_runs()
k_start = time.monotonic()
while time.monotonic() - k_start <= _AURORA_SMART_KLING_MAX_SEC:
try:
kst = await _aurora_request_json(
"GET",
f"/api/aurora/kling/status/{quote(local_job_id, safe='')}",
timeout=30.0,
retries=1,
retry_backoff_sec=0.2,
)
except Exception as exc:
_smart_append_audit(run, "kling.status.error", {"error": str(exc)[:220]})
await asyncio.sleep(_AURORA_SMART_KLING_POLL_SEC)
continue
k_status = str(kst.get("status") or "").lower()
k_url = kst.get("kling_result_url")
run["phase"] = "kling_processing"
run["kling"] = {
**(run.get("kling") if isinstance(run.get("kling"), dict) else {}),
"status": k_status,
"result_url": k_url,
"last_polled_at": _smart_now_iso(),
}
_smart_persist_runs()
if k_status in {"submitted", "queued", "running", "processing", "pending"}:
await asyncio.sleep(_AURORA_SMART_KLING_POLL_SEC)
continue
if k_status in {"succeed", "completed", "success"} and k_url:
run["status"] = "completed"
run["phase"] = "completed"
run["selected_stack"] = "kling"
_smart_append_audit(run, "smart.completed", {"selected_stack": "kling", "task_id": task_id})
_smart_update_strategy_outcome(strategy, True)
_smart_persist_runs()
return
run["status"] = "completed"
run["phase"] = "completed_with_kling_failure"
run["selected_stack"] = "local"
_smart_append_audit(
run,
"kling.terminal.non_success",
{"status": k_status, "task_id": task_id},
)
_smart_update_strategy_outcome(strategy, False)
_smart_persist_runs()
return
run["status"] = "completed"
run["phase"] = "completed_with_kling_timeout"
run["selected_stack"] = "local"
_smart_append_audit(run, "kling.timeout", {"max_sec": _AURORA_SMART_KLING_MAX_SEC})
_smart_update_strategy_outcome(strategy, False)
_smart_persist_runs()
def _smart_resume_active_monitors() -> None:
_smart_load_runs_from_disk()
for run_id, run in list(_aurora_smart_runs.items()):
if not isinstance(run, dict):
continue
if _smart_is_terminal(run.get("status")):
continue
try:
asyncio.create_task(_smart_monitor_run(run_id))
except Exception:
continue
@app.get("/api/aurora/health")
async def api_aurora_health() -> Dict[str, Any]:
return await _aurora_request_json("GET", "/health", timeout=10.0)
@@ -1012,6 +1532,241 @@ async def api_aurora_upload(
return payload
@app.post("/api/aurora/process-smart")
async def api_aurora_process_smart(
file: UploadFile = File(...),
mode: str = Form("tactical"),
priority: str = Form("balanced"),
export_options: str = Form(""),
strategy: str = Form("auto"),
prefer_quality: bool = Form(True),
budget_tier: str = Form("normal"),
learning_enabled: bool = Form(True),
kling_prompt: str = Form("enhance video quality, improve sharpness and clarity"),
kling_negative_prompt: str = Form("noise, blur, artifacts, distortion"),
kling_mode: str = Form("pro"),
kling_duration: str = Form("5"),
kling_cfg_scale: float = Form(0.5),
) -> Dict[str, Any]:
_smart_load_runs_from_disk()
_smart_load_policy_from_disk()
file_name = file.filename or "upload.bin"
content_type = file.content_type or "application/octet-stream"
media_type = _smart_media_type(file_name, content_type)
analysis: Optional[Dict[str, Any]] = None
if media_type in {"video", "photo"}:
try:
await file.seek(0)
files = {"file": (file_name, file.file, content_type)}
analysis = await _aurora_request_json(
"POST",
"/api/aurora/analyze",
files=files,
timeout=120.0,
retries=1,
retry_backoff_sec=0.25,
)
except Exception as exc:
analysis = None
logger.warning("smart-process analyze skipped: %s", str(exc)[:220])
policy = _smart_decide_strategy(
media_type=media_type,
mode=mode,
requested_strategy=strategy,
prefer_quality=bool(prefer_quality),
budget_tier=budget_tier,
analysis=analysis,
learning_enabled=bool(learning_enabled),
)
chosen_strategy = str(policy.get("strategy") or "local_only")
policy.setdefault("requested_strategy", str(strategy or "auto"))
policy["learning_enabled"] = bool(learning_enabled)
policy["budget_tier"] = str(budget_tier or "normal")
await file.seek(0)
files = {"file": (file_name, file.file, content_type)}
local_payload = await _aurora_request_json(
"POST",
"/api/aurora/upload",
files=files,
data={
"mode": mode,
"priority": priority,
"export_options": export_options,
},
timeout=120.0,
)
local_job_id = str(local_payload.get("job_id") or "")
if not local_job_id:
raise HTTPException(status_code=502, detail="Smart process failed: local job_id missing")
run_id = _smart_new_run_id()
now = _smart_now_iso()
run: Dict[str, Any] = {
"run_id": run_id,
"created_at": now,
"updated_at": now,
"status": "processing",
"phase": "local_processing",
"media_type": media_type,
"selected_stack": None,
"requested": {
"mode": mode,
"priority": priority,
"export_options": export_options,
"strategy": strategy,
"prefer_quality": bool(prefer_quality),
"budget_tier": budget_tier,
"learning_enabled": bool(learning_enabled),
},
"policy": policy,
"analysis_summary": _smart_analysis_features(analysis),
"analysis": analysis if isinstance(analysis, dict) else None,
"local": {
"job_id": local_job_id,
"status": "queued",
"submit_payload": {
"status_url": f"/api/aurora/status/{quote(local_job_id, safe='')}",
"result_url": f"/api/aurora/result/{quote(local_job_id, safe='')}",
},
},
"kling": {
"enabled": chosen_strategy == "local_then_kling" and media_type == "video",
"status": "pending",
"prompt": kling_prompt,
"negative_prompt": kling_negative_prompt,
"mode": kling_mode,
"duration": kling_duration,
"cfg_scale": kling_cfg_scale,
},
"audit": [],
}
_smart_append_audit(
run,
"smart.submitted",
{
"local_job_id": local_job_id,
"media_type": media_type,
"strategy": chosen_strategy,
"score": policy.get("score"),
},
)
_aurora_smart_runs[run_id] = run
_smart_persist_runs()
try:
asyncio.create_task(_smart_monitor_run(run_id))
except Exception as exc:
_smart_append_audit(run, "monitor.spawn.error", {"error": str(exc)[:220]})
_smart_persist_runs()
return {
"smart_run_id": run_id,
"status": run.get("status"),
"phase": run.get("phase"),
"media_type": media_type,
"local_job_id": local_job_id,
"policy": policy,
"smart_status_url": f"/api/aurora/process-smart/{quote(run_id, safe='')}",
"local_status_url": f"/api/aurora/status/{quote(local_job_id, safe='')}",
"local_result_url": f"/api/aurora/result/{quote(local_job_id, safe='')}",
}
@app.get("/api/aurora/process-smart")
async def api_aurora_process_smart_list(
limit: int = Query(default=20, ge=1, le=200),
status: Optional[str] = Query(default=None),
) -> Dict[str, Any]:
_smart_load_runs_from_disk()
requested = str(status or "").strip().lower()
rows = []
for run in _aurora_smart_runs.values():
if not isinstance(run, dict):
continue
run_status = str(run.get("status") or "")
if requested and run_status.lower() != requested:
continue
local = run.get("local") if isinstance(run.get("local"), dict) else {}
kling = run.get("kling") if isinstance(run.get("kling"), dict) else {}
rows.append(
{
"run_id": run.get("run_id"),
"status": run_status,
"phase": run.get("phase"),
"media_type": run.get("media_type"),
"strategy": (run.get("policy") or {}).get("strategy") if isinstance(run.get("policy"), dict) else None,
"selected_stack": run.get("selected_stack"),
"created_at": run.get("created_at"),
"updated_at": run.get("updated_at"),
"local_job_id": local.get("job_id"),
"local_status": local.get("status"),
"kling_status": kling.get("status"),
}
)
rows.sort(key=lambda x: str(x.get("created_at") or ""), reverse=True)
return {"runs": rows[:limit], "count": min(limit, len(rows)), "total": len(rows)}
@app.get("/api/aurora/process-smart/{run_id}")
async def api_aurora_process_smart_status(run_id: str) -> Dict[str, Any]:
run = await _smart_fetch_run_status(run_id)
if not run:
raise HTTPException(status_code=404, detail="smart run not found")
return run
@app.post("/api/aurora/process-smart/{run_id}/feedback")
async def api_aurora_process_smart_feedback(
run_id: str,
payload: Optional[Dict[str, Any]] = Body(default=None),
) -> Dict[str, Any]:
run = await _smart_fetch_run_status(run_id)
if not run:
raise HTTPException(status_code=404, detail="smart run not found")
body = payload if isinstance(payload, dict) else {}
score_raw = body.get("score")
score: Optional[float] = None
try:
if score_raw is not None:
score = float(score_raw)
except Exception:
score = None
selected_stack = str(body.get("selected_stack") or "").strip().lower() or None
notes = str(body.get("notes") or "").strip()
feedback = {
"ts": _smart_now_iso(),
"score": score,
"selected_stack": selected_stack,
"notes": notes[:1000] if notes else None,
}
run["feedback"] = feedback
strategy = str((run.get("policy") or {}).get("strategy") or "local_only")
if score is not None:
score = max(1.0, min(5.0, score))
_smart_update_strategy_score(strategy, score)
if selected_stack in {"local", "kling"}:
run["selected_stack"] = selected_stack
_smart_append_audit(run, "feedback.received", {"score": score, "selected_stack": selected_stack})
_smart_persist_runs()
return {
"ok": True,
"run_id": run_id,
"feedback": feedback,
"policy": _aurora_smart_policy,
}
@app.get("/api/aurora/process-smart/policy/stats")
async def api_aurora_process_smart_policy_stats() -> Dict[str, Any]:
_smart_load_policy_from_disk()
return _aurora_smart_policy
@app.post("/api/aurora/analyze")
async def api_aurora_analyze(file: UploadFile = File(...)) -> Dict[str, Any]:
await file.seek(0)

View File

@@ -836,6 +836,35 @@
</div>
</details>
<div style="margin-top:10px; border:1px solid rgba(255,255,255,0.08); border-radius:8px; padding:8px; background:var(--bg2);">
<div class="aurora-note" style="margin-top:0;">Smart Orchestrator (Dual Stack)</div>
<label class="aurora-checkline" style="margin-top:5px;">
<input type="checkbox" id="auroraSmartEnabled" checked>
Auto (Aurora + Kling when needed)
</label>
<div class="aurora-export-grid" style="margin-top:8px;">
<label>Strategy
<select id="auroraSmartStrategy">
<option value="auto" selected>Auto</option>
<option value="local_only">Local only</option>
<option value="local_then_kling">Local then Kling</option>
</select>
</label>
<label>Budget
<select id="auroraSmartBudget">
<option value="low">Low</option>
<option value="normal" selected>Normal</option>
<option value="high">High</option>
</select>
</label>
</div>
<label class="aurora-checkline" style="margin-top:6px;">
<input type="checkbox" id="auroraSmartPreferQuality" checked>
Prefer quality over speed
</label>
<div id="auroraSmartHint" class="aurora-note">policy: standby</div>
</div>
<div style="display:flex; gap:8px; margin-top:12px;">
<button id="auroraAnalyzeBtn" class="btn btn-ghost" onclick="auroraAnalyze()" disabled>🔍 Аналіз</button>
<button id="auroraAudioProcessBtn" class="btn btn-ghost" style="display:none;" onclick="auroraStartAudio()">🎧 Audio process</button>
@@ -847,6 +876,8 @@
<div class="aurora-card">
<div class="aurora-title">Job Status</div>
<div class="aurora-kv"><span class="k">Job ID</span><span class="v" id="auroraJobId"></span></div>
<div class="aurora-kv"><span class="k">Smart Run</span><span class="v" id="auroraSmartRunId"></span></div>
<div class="aurora-kv"><span class="k">Smart Policy</span><span class="v" id="auroraSmartPolicy"></span></div>
<div class="aurora-kv"><span class="k">Статус</span><span class="v" id="auroraJobStatus">idle</span></div>
<div class="aurora-kv"><span class="k">Етап</span><span class="v" id="auroraJobStage"></span></div>
<div class="aurora-kv"><span class="k">Черга</span><span class="v" id="auroraQueuePos"></span></div>
@@ -2018,6 +2049,8 @@ let currentAudio = null;
let auroraMode = 'tactical';
let auroraSelectedFile = null;
let auroraJobId = null;
let auroraSmartRunId = null;
let auroraSmartStatusCache = null;
let auroraPollTimer = null;
let auroraResultCache = null;
let auroraAnalysisCache = null;
@@ -2035,8 +2068,10 @@ let auroraChatBusy = false;
let auroraFolderPath = null;
const AURORA_MAX_TRANSIENT_ERRORS = 12;
const AURORA_ACTIVE_JOB_KEY = 'aurora_active_job_id';
const AURORA_SMART_RUN_KEY = 'aurora_smart_run_id';
const AURORA_TIMING_CACHE_PREFIX = 'aurora_timing_cache_v1:';
try { auroraJobId = localStorage.getItem(AURORA_ACTIVE_JOB_KEY) || null; } catch (_) {}
try { auroraSmartRunId = localStorage.getItem(AURORA_SMART_RUN_KEY) || null; } catch (_) {}
let mediaTabBootstrapped = false;
let aistalkTabBootstrapped = false;
let aistalkRunId = null;
@@ -2214,6 +2249,38 @@ function auroraPersistActiveJob() {
} catch (_) {}
}
function auroraPersistSmartRun() {
try {
if (auroraSmartRunId) localStorage.setItem(AURORA_SMART_RUN_KEY, auroraSmartRunId);
else localStorage.removeItem(AURORA_SMART_RUN_KEY);
} catch (_) {}
}
function auroraSetSmartRunId(runId) {
const normalized = String(runId || '').trim();
auroraSmartRunId = normalized || null;
const el = document.getElementById('auroraSmartRunId');
if (el) el.textContent = auroraSmartRunId || '—';
auroraPersistSmartRun();
}
function auroraSetSmartPolicyText(text) {
const el = document.getElementById('auroraSmartPolicy');
const hint = document.getElementById('auroraSmartHint');
const msg = String(text || '').trim() || '—';
if (el) el.textContent = msg;
if (hint) hint.textContent = `policy: ${msg}`;
}
function auroraSmartConfig() {
return {
enabled: Boolean(document.getElementById('auroraSmartEnabled')?.checked),
strategy: document.getElementById('auroraSmartStrategy')?.value || 'auto',
budget_tier: document.getElementById('auroraSmartBudget')?.value || 'normal',
prefer_quality: Boolean(document.getElementById('auroraSmartPreferQuality')?.checked),
};
}
function auroraTimingCacheKey(jobId) {
const id = String(jobId || '').trim();
return id ? `${AURORA_TIMING_CACHE_PREFIX}${id}` : '';
@@ -2301,6 +2368,9 @@ function auroraSetSelectedFile(file) {
auroraSuggestedPriority = 'balanced';
auroraSuggestedExport = {};
auroraPresetMode = 'balanced';
auroraSetSmartRunId(null);
auroraSmartStatusCache = null;
auroraSetSmartPolicyText('standby');
auroraResetAnalysisControls();
auroraUpdateQueuePosition(null);
auroraUpdateStorage(null);
@@ -2474,6 +2544,9 @@ function auroraSelectJob(jobId) {
const id = String(jobId || '').trim();
if (!id) return;
auroraSetActiveJobId(id);
auroraSetSmartRunId(null);
auroraSmartStatusCache = null;
auroraSetSmartPolicyText('manual open');
auroraStatusCache = null;
auroraResultCache = null;
auroraPollErrorCount = 0;
@@ -2491,6 +2564,9 @@ function auroraSelectJob(jobId) {
function auroraClearActiveJob() {
auroraStopPolling();
auroraSetActiveJobId(null);
auroraSetSmartRunId(null);
auroraSmartStatusCache = null;
auroraSetSmartPolicyText('—');
auroraStatusCache = null;
auroraResultCache = null;
auroraLastProgress = 0;
@@ -3165,6 +3241,9 @@ async function auroraReprocess(options) {
}
const data = await r.json();
auroraSetActiveJobId(data.job_id);
auroraSetSmartRunId(null);
auroraSmartStatusCache = null;
auroraSetSmartPolicyText('audio local');
auroraStatusCache = null;
auroraResultCache = null;
auroraPollErrorCount = 0;
@@ -3301,6 +3380,9 @@ async function auroraStartAudio() {
}
const data = await r.json();
auroraSetActiveJobId(data.job_id);
auroraSetSmartRunId(null);
auroraSmartStatusCache = null;
auroraSetSmartPolicyText('reprocess local');
auroraStatusCache = null;
auroraResultCache = null;
auroraPollErrorCount = 0;
@@ -3331,6 +3413,29 @@ function auroraStopPolling() {
auroraPollInFlight = false;
}
async function auroraPollSmartStatus({ quiet = true } = {}) {
if (!auroraSmartRunId) return null;
try {
const r = await fetch(`${API}/api/aurora/process-smart/${encodeURIComponent(auroraSmartRunId)}`);
if (r.status === 404) {
auroraSetSmartRunId(null);
auroraSmartStatusCache = null;
auroraSetSmartPolicyText('—');
return null;
}
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const smart = await r.json();
auroraSmartStatusCache = smart;
const strategy = smart?.policy?.strategy || 'auto';
const phase = smart?.phase || smart?.status || '—';
auroraSetSmartPolicyText(`${strategy} · ${phase}`);
return smart;
} catch (e) {
if (!quiet) console.warn('aurora smart status error:', e);
return auroraSmartStatusCache;
}
}
async function auroraPollStatus() {
if (!auroraJobId || auroraPollInFlight) return;
auroraPollInFlight = true;
@@ -3346,6 +3451,7 @@ async function auroraPollStatus() {
}
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const st = await r.json();
const smart = await auroraPollSmartStatus({ quiet: true });
auroraStatusCache = st;
auroraPollErrorCount = 0;
const cachedTiming = auroraGetPersistedTiming(auroraJobId) || {};
@@ -3369,10 +3475,24 @@ async function auroraPollStatus() {
const reBtn = document.getElementById('auroraReprocessBtn');
if (reBtn) reBtn.disabled = !(st.status === 'completed' || st.status === 'failed' || st.status === 'cancelled');
if (st.status === 'completed') {
const smartActive = smart && !['completed', 'failed', 'cancelled'].includes(String(smart.status || '').toLowerCase());
if (smartActive) {
if (!auroraResultCache) {
await auroraLoadResult(auroraJobId);
}
const kStat = smart?.kling?.status ? ` · Kling ${smart.kling.status}` : '';
auroraSetProgress(99, 'processing', `smart orchestration (${smart.phase || 'running'}${kStat})`);
const cancelBtn = document.getElementById('auroraCancelBtn');
if (cancelBtn) cancelBtn.style.display = 'none';
return;
}
auroraStopPolling();
await auroraLoadResult(auroraJobId);
const cancelBtn = document.getElementById('auroraCancelBtn');
if (cancelBtn) cancelBtn.style.display = 'none';
if (smart && smart.selected_stack) {
auroraChatAdd('assistant', `Smart run завершено. Selected stack: ${smart.selected_stack}.`);
}
await auroraRefreshJobs();
} else if (st.status === 'failed' || st.status === 'cancelled') {
auroraStopPolling();
@@ -3406,20 +3526,29 @@ async function auroraStart() {
return;
}
const analysisControls = auroraCollectAnalysisControls();
const smartCfg = auroraSmartConfig();
const fd = new FormData();
fd.append('file', auroraSelectedFile);
fd.append('mode', auroraMode);
fd.append('priority', analysisControls.priority || auroraSuggestedPriority || 'balanced');
const uiExport = auroraCollectExportOptions();
const analysisExport = auroraBuildAnalysisExportHints(analysisControls);
fd.append('export_options', JSON.stringify({ ...auroraSuggestedExport, ...uiExport, ...analysisExport }));
const mergedExport = { ...auroraSuggestedExport, ...uiExport, ...analysisExport };
fd.append('export_options', JSON.stringify(mergedExport));
if (smartCfg.enabled) {
fd.append('strategy', smartCfg.strategy || 'auto');
fd.append('prefer_quality', smartCfg.prefer_quality ? 'true' : 'false');
fd.append('budget_tier', smartCfg.budget_tier || 'normal');
fd.append('learning_enabled', 'true');
}
_auroraLogLines = [];
const startBtn = document.getElementById('auroraStartBtn');
const quickStartBtn = document.getElementById('auroraStartFromAnalysisBtn');
if (startBtn) startBtn.disabled = true;
if (quickStartBtn) quickStartBtn.disabled = true;
try {
const r = await fetch(`${API}/api/aurora/upload`, {
const endpoint = smartCfg.enabled ? '/api/aurora/process-smart' : '/api/aurora/upload';
const r = await fetch(`${API}${endpoint}`, {
method: 'POST',
body: fd,
});
@@ -3428,7 +3557,23 @@ async function auroraStart() {
throw new Error(body || `HTTP ${r.status}`);
}
const data = await r.json();
auroraSetActiveJobId(data.job_id);
const localJobId = data.local_job_id || data.job_id;
if (!localJobId) {
throw new Error('job_id missing in response');
}
auroraSetActiveJobId(localJobId);
if (smartCfg.enabled) {
auroraSetSmartRunId(data.smart_run_id || null);
const policyStrategy = data?.policy?.strategy || smartCfg.strategy || 'auto';
const policyScore = Number(data?.policy?.score);
const scoreTxt = Number.isFinite(policyScore) ? ` (${policyScore.toFixed(2)})` : '';
auroraSetSmartPolicyText(`${policyStrategy}${scoreTxt}`);
auroraChatAdd('assistant', `Smart run ${data.smart_run_id || '—'}: strategy=${policyStrategy}`);
} else {
auroraSetSmartRunId(null);
auroraSmartStatusCache = null;
auroraSetSmartPolicyText('manual local');
}
auroraStatusCache = null;
auroraResultCache = null;
auroraPollErrorCount = 0;
@@ -3441,7 +3586,7 @@ async function auroraStart() {
document.getElementById('auroraResultCard').style.display = 'none';
const reBtn = document.getElementById('auroraReprocessBtn');
if (reBtn) reBtn.disabled = true;
auroraSetProgress(1, 'processing', 'dispatching');
auroraSetProgress(1, 'processing', smartCfg.enabled ? 'dispatching smart orchestration' : 'dispatching');
const cancelBtn = document.getElementById('auroraCancelBtn');
if (cancelBtn) cancelBtn.style.display = 'inline-block';
auroraStopPolling();
@@ -3449,7 +3594,7 @@ async function auroraStart() {
await auroraPollStatus();
await auroraRefreshJobs();
} catch (e) {
alert(`Aurora upload error: ${e.message || e}`);
alert(`Aurora start error: ${e.message || e}`);
auroraSetProgress(0, 'failed', 'upload_error');
} finally {
if (startBtn) startBtn.disabled = !auroraSelectedFile;
@@ -3805,6 +3950,10 @@ function auroraInitTab() {
auroraBindDropzone();
auroraRefreshHealth();
auroraUpdatePriorityLabel();
auroraSetSmartRunId(auroraSmartRunId);
if (!auroraSmartRunId) {
auroraSetSmartPolicyText('standby');
}
const quickStartBtn = document.getElementById('auroraStartFromAnalysisBtn');
if (quickStartBtn) quickStartBtn.disabled = !auroraSelectedFile;
if (!auroraTabBootstrapped) {
@@ -3827,6 +3976,15 @@ function auroraInitTab() {
}
auroraUpdateQueuePosition((auroraStatusCache || {}).queue_position || null);
auroraUpdateStorage((auroraStatusCache || {}).storage || null);
if (auroraSmartRunId) {
auroraPollSmartStatus({ quiet: true }).then((smart) => {
if (!smart || typeof smart !== 'object') return;
const localJob = smart?.local?.job_id || null;
if (!auroraJobId && localJob) {
auroraSetActiveJobId(localJob);
}
}).catch(() => {});
}
auroraRefreshJobs();
if (auroraJobId && !auroraPollTimer) {
auroraSetProgress(Math.max(1, auroraLastProgress || 0), 'processing', 'restoring previous job...');