diff --git a/desktop/src/App.tsx b/desktop/src/App.tsx index b77e3e1..0f0fbce 100644 --- a/desktop/src/App.tsx +++ b/desktop/src/App.tsx @@ -1995,14 +1995,14 @@ function App() { setAgentStates(INITIAL_AGENT_STATES); updateWorkflowMessage(workflowId, { status: "blocked", - phase: "Transcription approval required", + phase: "Local catalog required", progress: 100, - detail: result.message || "ReelBrain cannot begin grounded editing until a transcript is authorized.", + detail: result.message || "ReelBrain cannot begin grounded editing until a local catalog is available.", videoChanged: false, }); setMessages((current) => [ ...current, - { id: crypto.randomUUID(), role: "system", text: result.message || "Transcription approval is required before fan-out.", time: nowLabel() }, + { id: crypto.randomUUID(), role: "system", text: result.message || "A local transcript and candidate catalog are required before fan-out.", time: nowLabel() }, ]); } else { updateWorkflowMessage(workflowId, { diff --git a/desktop/src/services/reelbrain.ts b/desktop/src/services/reelbrain.ts index 3c8f0c9..2d08809 100644 --- a/desktop/src/services/reelbrain.ts +++ b/desktop/src/services/reelbrain.ts @@ -59,6 +59,8 @@ export async function startEditorialFanout(input: { agentResults: (raw.agentResults ?? raw.agent_results) as unknown[] | undefined, requiresCreatorApproval: (raw.requiresCreatorApproval ?? raw.requires_creator_approval) as boolean | undefined, requiredEffect: (raw.requiredEffect ?? raw.required_effect) as string | undefined, + catalogSearchPath: (raw.catalogSearchPath ?? raw.catalog_search_path) as string | undefined, + requiredCatalogFiles: (raw.requiredCatalogFiles ?? raw.required_catalog_files) as string[] | undefined, message: raw.message as string | undefined, }; } diff --git a/desktop/src/types.ts b/desktop/src/types.ts index 5d53a05..0022310 100644 --- a/desktop/src/types.ts +++ b/desktop/src/types.ts @@ -257,6 +257,8 @@ export type FanoutResult = { agentResults?: unknown[]; requiresCreatorApproval?: boolean; requiredEffect?: string; + catalogSearchPath?: string; + requiredCatalogFiles?: string[]; message?: string; }; diff --git a/reelbrain/fanout.py b/reelbrain/fanout.py index bf56c3c..2e3abaf 100644 --- a/reelbrain/fanout.py +++ b/reelbrain/fanout.py @@ -223,13 +223,36 @@ def plan(self, request: dict[str, object]) -> dict[str, object]: raise FanoutError("source_digest_mismatch") catalog = self._find_catalog(source_sha256) if catalog is None: + catalog_search_path = ( + self.workspace + / ".reelbrain" + / "dogfood" + / "*" + / "run" + / "source_inventory.json" + ) + required_catalog_files = [ + "source_inventory.json", + "/editorial_plan.json", + "/bilingual_transcript.json", + ] return { "status": "TRANSCRIPT_REQUIRED", "source_path": str(source_path), "source_sha256": source_sha256, "requires_creator_approval": True, "required_effect": "transcribe_and_build_candidate_catalog", - "message": "This source has no canonical transcript/catalog yet. Approve transcription before starting the editorial agents.", + "catalog_search_path": str(catalog_search_path), + "required_catalog_files": required_catalog_files, + "message": ( + "No canonical catalog matched this source SHA-256. ReelBrain searched " + f"{catalog_search_path} and requires source_inventory.json plus " + "/editorial_plan.json and " + "/bilingual_transcript.json. Files elsewhere are not " + "imported automatically. No agents ran, no video changed, and no " + "provider spend was authorized. Prepare or import the catalog into " + "the app workspace, then choose New edit again." + ), } memory = DesktopMemoryService(self.workspace, creator_id).inspect() current_steering = str(request.get("current_steering") or "").strip() or None diff --git a/tests/test_desktop_runtime.py b/tests/test_desktop_runtime.py index 638504c..5ea7d26 100644 --- a/tests/test_desktop_runtime.py +++ b/tests/test_desktop_runtime.py @@ -442,6 +442,18 @@ def test_unprepared_source_requires_transcription_approval(tmp_path: Path) -> No ) assert result["status"] == "TRANSCRIPT_REQUIRED" assert result["requires_creator_approval"] is True + assert result["catalog_search_path"] == str( + tmp_path / ".reelbrain" / "dogfood" / "*" / "run" / "source_inventory.json" + ) + assert result["required_catalog_files"] == [ + "source_inventory.json", + "/editorial_plan.json", + "/bilingual_transcript.json", + ] + assert "Files elsewhere are not imported automatically" in result["message"] + assert "No agents ran, no video changed, and no provider spend was authorized" in result[ + "message" + ] def test_creator_review_actions_never_imply_publish_ready(tmp_path: Path) -> None: