Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ __pycache__/
*.db-journal
.env
output/
youtube
youtube/channels/
agent/active_project.json

# Generated AI tool configs (run: python setup.py)
Expand Down
6 changes: 3 additions & 3 deletions agent/api/active_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
def _read_state() -> dict | None:
if _STATE_FILE.exists():
try:
with open(_STATE_FILE) as f:
with open(_STATE_FILE, encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, OSError) as e:
logger.warning("Corrupt active_project.json, clearing: %s", e)
Expand All @@ -28,10 +28,10 @@ def _read_state() -> dict | None:

def _write_state(data: dict):
"""Atomic write — temp file + os.replace to avoid partial reads."""
content = json.dumps(data, indent=2) + "\n"
content = json.dumps(data, indent=2, ensure_ascii=False) + "\n"
fd, tmp = tempfile.mkstemp(dir=_STATE_FILE.parent, suffix=".tmp")
try:
os.write(fd, content.encode())
os.write(fd, content.encode("utf-8"))
os.close(fd)
os.replace(tmp, _STATE_FILE)
except BaseException:
Expand Down
78 changes: 67 additions & 11 deletions agent/api/flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from fastapi import APIRouter, File, Form, HTTPException, Response, UploadFile
from pydantic import BaseModel, Field
from typing import Literal, Optional
from typing import Any, Literal, Optional

from agent.config import (
FLOW_PROJECT_ID, FLOW_ALLOW_DEGRADED,
Expand All @@ -24,6 +24,12 @@
router = APIRouter(prefix="/flow", tags=["flow"])


def _safe_status_code(status: Any, default: int = 502) -> int:
if isinstance(status, int) and 400 <= status <= 599:
return status
return default


class GenerateImageRequest(BaseModel):
prompt: str
project_id: str = ""
Expand Down Expand Up @@ -236,7 +242,7 @@ async def generate_image(body: GenerateImageRequest):
data["character_media_ids"] = refs or None
result = await client.generate_images(**data)
if result.get("error") or (isinstance(result.get("status"), int) and result["status"] >= 400):
raise HTTPException(result.get("status", 502), result.get("error", result.get("data")))
raise HTTPException(_safe_status_code(result.get("status")), result.get("error", result.get("data")))
return result.get("data", result)


Expand Down Expand Up @@ -286,7 +292,7 @@ async def generate_video(body: GenerateVideoRequest):
result = await client.generate_video(**payload)

if result.get("error") or (isinstance(result.get("status"), int) and result["status"] >= 400):
raise HTTPException(result.get("status", 502), result.get("error", result.get("data")))
raise HTTPException(_safe_status_code(result.get("status")), result.get("error", result.get("data")))
return result.get("data", result)


Expand Down Expand Up @@ -324,7 +330,7 @@ async def generate_video_refs(body: GenerateVideoRefsRequest):
result = await client.generate_video_from_references(**payload)

if result.get("error") or (isinstance(result.get("status"), int) and result["status"] >= 400):
raise HTTPException(result.get("status", 502), result.get("error", result.get("data")))
raise HTTPException(_safe_status_code(result.get("status")), result.get("error", result.get("data")))
return result.get("data", result)


Expand All @@ -348,7 +354,7 @@ async def generate_video_omni_text(body: GenerateOmniFlashTextVideoRequest):
isinstance(result.get("status"), int) and result["status"] >= 400
):
raise HTTPException(
result.get("status", 502),
_safe_status_code(result.get("status")),
result.get("error", result.get("data")),
)
return result.get("data", result)
Expand All @@ -372,7 +378,7 @@ async def generate_video_omni(body: GenerateOmniFlashVideoRequest):
except ValueError as exc:
raise HTTPException(400, str(exc)) from exc
if result.get("error") or (isinstance(result.get("status"), int) and result["status"] >= 400):
raise HTTPException(result.get("status", 502), result.get("error", result.get("data")))
raise HTTPException(_safe_status_code(result.get("status")), result.get("error", result.get("data")))
return result.get("data", result)


Expand All @@ -384,7 +390,7 @@ async def upscale_video(body: UpscaleVideoRequest):
raise HTTPException(503, "Extension not connected")
result = await client.upscale_video(**body.model_dump())
if result.get("error") or (isinstance(result.get("status"), int) and result["status"] >= 400):
raise HTTPException(result.get("status", 502), result.get("error", result.get("data")))
raise HTTPException(_safe_status_code(result.get("status")), result.get("error", result.get("data")))
return result.get("data", result)


Expand Down Expand Up @@ -418,7 +424,7 @@ async def check_status(body: CheckStatusRequest):
if result.get("error"):
raise HTTPException(502, result["error"])
if isinstance(result.get("status"), int) and result["status"] >= 400:
raise HTTPException(result["status"], result.get("data", "Flow polling failed"))
raise HTTPException(_safe_status_code(result["status"]), result.get("data", "Flow polling failed"))
return result.get("data", result)


Expand Down Expand Up @@ -488,7 +494,7 @@ async def get_media(media_id: str):
raise HTTPException(502, result["error"])
status = result.get("status", 200)
if isinstance(status, int) and status >= 400:
raise HTTPException(status, result.get("data", "Media not found"))
raise HTTPException(_safe_status_code(status, 404), result.get("data", "Media not found"))
return result.get("data", result)


Expand All @@ -510,7 +516,7 @@ async def edit_image(body: EditImageRequest):
seed=body.seed,
)
if result.get("error") or (isinstance(result.get("status"), int) and result["status"] >= 400):
raise HTTPException(result.get("status", 502), result.get("error", result.get("data")))
raise HTTPException(_safe_status_code(result.get("status")), result.get("error", result.get("data")))
return result.get("data", result)


Expand All @@ -530,7 +536,7 @@ async def export_image(body: UpscaleImageRequest):
resolution=body.quality.upper(),
)
if result.get("error") or (isinstance(result.get("status"), int) and result["status"] >= 400):
raise HTTPException(result.get("status", 502), result.get("error", result.get("data")))
raise HTTPException(_safe_status_code(result.get("status")), result.get("error", result.get("data")))
data = result.get("data", result)
try:
content = base64.b64decode(data["encodedImage"], validate=True)
Expand Down Expand Up @@ -700,3 +706,53 @@ async def upload_image_file(
mime_type=resolved_mime,
file_name=resolved_name,
)


@router.get("/debug-cache")
async def debug_cache():
client = get_flow_client()
return {
"operation_media": client._operation_media,
"operation_projects": client._operation_projects,
"operation_polls": client._operation_polls,
}


@router.get("/debug-listing/{operation_id}")
async def debug_listing(operation_id: str, project_id: str = "594758cc-11f5-4f92-8b3c-1213686591f4"):
import agent.services.flow_batch as fb
client = get_flow_client()
if not client.connected:
raise HTTPException(503, "Extension not connected")
result = await client.batch_rpc(
fb.RPC_PROJECT_MEDIA, fb.project_media_request(project_id),
match=operation_id, timeout=30,
)
raw = result.get("data") or ""
mid = fb.find_media_id_in_text(raw, operation_id)
return {
"operation_id": operation_id,
"matched_media_id": mid,
"raw_preview": raw[:1000],
}


@router.get("/debug-project-media")
async def debug_project_media(project_id: str = "594758cc-11f5-4f92-8b3c-1213686591f4"):
import re
import agent.services.flow_batch as fb
client = get_flow_client()
if not client.connected:
raise HTTPException(503, "Extension not connected")
result = await client.batch_rpc(
fb.RPC_PROJECT_MEDIA, fb.project_media_request(project_id),
match=None, timeout=60,
)
raw = result.get("data") or ""
uuids = list(set(re.findall(r'[0-9a-fA-F-]{36}', raw)))
return {
"raw_length": len(raw),
"found_uuids_count": len(uuids),
"uuids": uuids,
"sample": raw[:2000]
}
4 changes: 2 additions & 2 deletions agent/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@


def _read_models() -> dict:
with open(_MODELS_FILE) as f:
with open(_MODELS_FILE, encoding="utf-8") as f:
return json.load(f)


def _write_models(data: dict):
with open(_MODELS_FILE, "w") as f:
with open(_MODELS_FILE, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")

Expand Down
4 changes: 2 additions & 2 deletions agent/api/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,9 @@ async def get_output_dir(pid: str):
}
meta_path = output_dir / "meta.json"
if meta_path.exists():
existing = json.loads(meta_path.read_text())
existing = json.loads(meta_path.read_text(encoding="utf-8"))
meta["created_at"] = existing.get("created_at", now)
meta_path.write_text(json.dumps(meta, indent=2, ensure_ascii=False))
meta_path.write_text(json.dumps(meta, indent=2, ensure_ascii=False), encoding="utf-8")

return {"slug": slug, "path": f"output/{slug}", "meta": meta}

Expand Down
6 changes: 3 additions & 3 deletions agent/api/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ async def get_media_provider_status():


def _read() -> dict:
"""Read providers.json and hot-reload it into `config.CLI_PROVIDERS`.
"""Read providers.json from disk and refresh config.CLI_PROVIDERS in place.

The file is the source of truth, and it is editable by hand (the skills
do), so re-reading it here is what makes a hand edit take effect without a
restart — and what stops a GET describing a state the worker is not in.
"""
with open(_PROVIDERS_FILE) as f:
with open(_PROVIDERS_FILE, encoding="utf-8") as f:
data = json.load(f)
data.setdefault("active", "claude")
data.setdefault("roles", {})
Expand All @@ -86,7 +86,7 @@ def _write(data: dict):
"""
tmp = _PROVIDERS_FILE.with_suffix(".json.tmp")
try:
with open(tmp, "w") as f:
with open(tmp, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
f.write("\n")
f.flush()
Expand Down
5 changes: 4 additions & 1 deletion agent/api/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ async def create(body: RequestCreate):
data = body.model_dump(exclude_none=True)
data["req_type"] = data.pop("type")

# Reject if there's already an active request for the same scene + type
scene_id = data.get("scene_id")
req_type = data.get("req_type")
orient = data.get("orientation")
if scene_id and req_type:
existing = await crud.list_requests(scene_id=scene_id)
active = [r for r in existing
if r.get("type") == req_type
and (not orient or r.get("orientation") == orient)
and r.get("status") in ("PENDING", "PROCESSING")]
if active:
raise HTTPException(
Expand Down Expand Up @@ -79,11 +80,13 @@ async def create_batch(body: BatchRequestCreate):
scene_id = data.get("scene_id")
character_id = data.get("character_id")
req_type = data.get("req_type")
orient = data.get("orientation")
# Idempotent: skip if active request already exists
if scene_id and req_type:
existing = await crud.list_requests(scene_id=scene_id)
active = [r for r in existing
if r.get("type") == req_type
and (not orient or r.get("orientation") == orient)
and r.get("status") in ("PENDING", "PROCESSING")]
if active:
results.append(active[0])
Expand Down
4 changes: 2 additions & 2 deletions agent/api/tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,13 +357,13 @@ async def delete_voice_template(name: str):

def _load_templates_meta() -> dict:
if TEMPLATES_META.exists():
return json.loads(TEMPLATES_META.read_text())
return json.loads(TEMPLATES_META.read_text(encoding="utf-8"))
return {}


def _save_templates_meta(meta: dict):
TEMPLATES_META.parent.mkdir(parents=True, exist_ok=True)
TEMPLATES_META.write_text(json.dumps(meta, indent=2, ensure_ascii=False))
TEMPLATES_META.write_text(json.dumps(meta, indent=2, ensure_ascii=False), encoding="utf-8")


def _wav_duration(path: str) -> float | None:
Expand Down
6 changes: 3 additions & 3 deletions agent/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@

# ─── Model Keys (loaded from models.json for easy updates) ──
_MODELS_FILE = Path(__file__).parent / "models.json"
with open(_MODELS_FILE) as _f:
with open(_MODELS_FILE, encoding="utf-8") as _f:
_MODELS = json.load(_f)

VIDEO_MODELS = _MODELS["video_models"]
Expand Down Expand Up @@ -142,7 +142,7 @@

# ─── CLI Providers (video review vision analysis) ────────────
_PROVIDERS_FILE = Path(__file__).parent / "providers.json"
with open(_PROVIDERS_FILE) as _pvf:
with open(_PROVIDERS_FILE, encoding="utf-8") as _pvf:
CLI_PROVIDERS = json.load(_pvf) # mutable dict, hot-reloaded like VIDEO_MODELS
REVIEW_CLI_TIMEOUT_S = float(os.environ.get("REVIEW_CLI_TIMEOUT_S", "120"))

Expand All @@ -156,7 +156,7 @@ def _load_suno_key() -> str:
if channels_dir.exists():
for rules_file in channels_dir.glob("*/channel_rules.json"):
try:
rules = json.loads(rules_file.read_text())
rules = json.loads(rules_file.read_text(encoding="utf-8"))
key = rules.get("api_keys", {}).get("suno", "")
if key:
return key
Expand Down
Loading