Sign inStart creating

Addendum A1 — ArcReel & ecosystem review

Twelve features from cineforge-feature-addendum-A1.md, all built and verified.

Licence note. ArcReel is AGPL-3.0. Its ideas and workflows informed this work; none of its code was read, copied or adapted. Every file here was written from the addendum's behavioural description. Features and workflows are not copyrightable; source is.

Status

#FeatureStateWhere it lives
A1.1Book-to-Film adaptationBuiltStudio → project → Adapt
A1.2Grid storyboard generationBuiltStudio → project → Shots, per scene
A1.3Narration ModeBuiltStudio → project → Narration
A1.3Product-Ad ModeBuiltStudio → project → Product ad
A1.4Reference-to-video (Quick Shot)BuiltShot inspector
A1.5Narrative clue trackingBuiltStudio → project → Clues
A1.6NLE interchange exportBuiltStudio → project → Export
A1.7BYOK & custom providersBuiltSettings → Providers
A1.7Multi-key poolsBuiltAdmin → API keys
A1.8Resumable pipelinesBuiltStudio → project → Runs
A1.9Agent Platform APIBuiltSettings → API keys
A1.10Sandboxed agent runtimePartial — see belowAdmin → Agent security
A1.11Candidate model adaptersBuiltStudio → Models
A1.12Pose library + plan exportBuiltShot inspector; Export

A1.10 is the one honest "partial". Application-level containment — tool allowlist, project confinement, egress allowlist, out-of-band spend caps, prompt-injection fencing — is complete and enforced. OS-level process isolation (container, seccomp, network namespace) is infrastructure this build cannot provide for itself. Admin → Agent security reports the gap rather than letting the presence of the module imply a containment it does not have.


Design decisions worth knowing

Every extracted detail carries its source quote (A1.1)

"Tall, with a burn scar down his left forearm" is a useful character note if the book says so and a fabrication if it does not — and once it is in a Bible the two are indistinguishable, after which it propagates into the reference pack and every shot of that character.

So a SourceExtraction without a supporting passage is not a valid extraction. The quote and its character offset are stored beside the claim, the UI shows them, and rows below 60% confidence are never pre-selected. When a language model is available its quotes are verified against the source text before being accepted; a paraphrase presented as a quotation is dropped and the structural evidence used instead.

Planning is incremental (A1.1)

Episodes are proposed five at a time. Confirm, adjust, or give a one-sentence note and the batch re-plans. Nothing becomes a Project until confirmed.

Planning a 200k-word novel in one pass would be faster and much worse: if the pacing is wrong you find out at episode three, after every downstream decision was built on it.

Grid storyboards are a consistency mechanism, not just a saving (A1.2)

Nine panels from one call is ~9× cheaper on a pass every shot goes through. The larger win is that the model makes one decision about the coat, the light and the room, then draws nine panels under it. Nine separate calls make that decision nine times, and drift between storyboard frames is drift the scene inherits.

Panel geometry is deterministic and always recorded exactly. Writing separate image files needs FFmpeg; without it panels display correctly everywhere but cannot anchor a video generation — and that use is refused with the reason rather than sending the whole contact sheet to a provider.

Narration runs the pipeline backwards (A1.3)

Voice first, measure, then generate picture to the measured length. In a narrated piece the narrator's track is the spine — it cannot be trimmed without cutting words. Generate picture first and every segment needs re-timing.

measuredDurationSec is nullable and never receives a fallback estimate. A word-count guess is out by 15–20%, and 20% of a 40-second segment is eight seconds of black. Illustration is refused until a real measurement exists.

A product is not a character (A1.3)

An audience forgives a slightly different jawline. Nobody forgives a logo that changed. So a strict-fidelity prop is anchored into every shot, requested at 0.95 identity lock — higher than any character — and never described in words. Adjectives compete with the reference images for authority and the model splits the difference.

Lighting normalization is reported as not performed unless a backend exists, because reference photos shot under different light are the main cause of colour drift, and claiming a normalization that did not happen would let a creator skip the one manual step that would have fixed it.

Quick Shot disqualifies rather than deprioritises (A1.4)

A model without a reference-native mode does not do this job badly — it does a different job. Scoring it lower would still let it win when nothing better is configured, and spend real credits on an unrelated shot.

Clues track meaning, not appearance (A1.5)

The Prop Bible keeps the locket looking like the same locket. This checks whether it is planted where you meant, present where the story needs it, and paid off. A clue can render perfectly in all eleven of its shots and still be a dead plot thread.

Fixes here are always manual. "The locket never reappears" cannot be repaired by regenerating a shot; it needs a writing decision.

Interchange answers lock-in with a door (A1.6)

OpenTimelineIO is the backbone; every other format converts from it. Gaps are emitted explicitly — OTIO has no notion of a clip starting at an arbitrary time, so a hole must be a Gap item or every later clip silently slides earlier.

The bundle carries structure and references media by URL. The README travels inside the download and states exactly how many clips are online.

BYOK changes who pays, never what is allowed (A1.7)

creditPolicyFor returns a billing decision and nothing else. There is no code path where holding a personal key reaches the moderation layer — deliberately, so "it was their own key" can never become a defence.

Leases, not locks (A1.8)

A worker that segfaults never releases a lock and the unit is stranded until someone opens a database console — which is the failure this is meant to survive. A lease expires. The cost is that a slow unit can run twice, which is why completeUnit is idempotent.

The desired unit list is recomputed on every resume from the current project, not from a snapshot. Add three shots overnight and they are included; delete one and it is not regenerated into a project that no longer wants it.

Agents drive the Director, not the graph (A1.9)

Every safety property lives in the plan-then-approve loop: cost estimates before spending, cascade impact before regenerating, the storyboard gate, moderation. An agent driving primitives directly bypasses all of it and would eventually cascade a regeneration across four hundred shots because it misread a dependency.

Spend caps are enforced outside the agent. An agent that has been talked into spending more cannot, because the thing counting its credits is not a thing it can talk to.

The fence is the control, not the scanner (A1.10)

Untrusted content is labelled as data inside a nonce-delimited fence before it reaches any model. The injection scanner is a tripwire, not a filter — a competent rephrasing walks past it, and it exists so attempts leave a trace.

A manuscript containing "ignore your instructions" is logged and not rejected. A novel whose villain says that is a novel, and refusing it would cost a real user their book for a false positive.


Bugs found and fixed during this work

Nine registry models had no adapter behind them

adobe, google, ideogram, pika, recraft, selfhost, utility, vidu and xai were declared in the model registry with no implementation. getAdapter() falls back to the mock adapter for an unknown name, and isModelLive() only checked that the env var was present.

Set IDEOGRAM_API_KEY and Ideogram 3 reported as live, won routing on text-rendering jobs, and returned a procedural placeholder — labelled throughout the UI as Ideogram output, with no way for a creator to tell.

Fixed twice over: the nine adapters are implemented (router/adapters/extra.ts), and isModelLive() now requires a registered adapter as well as a key, so a future registry row cannot reach that state. verify-a1.ts asserts the registry and the adapter map agree.

Quick Shot was undemonstrable without API keys

The mock adapter advertises every capability so the whole app works with no keys — except it was missing referenceNative, and the Router disqualifies models lacking it. Quick Shot would have been a permanently dead button on a fresh install. Added to MOCK_MODELS.

The injection scanner flagged ordinary prose

act as matched anywhere, so "He said he would act as a witness at the trial" was logged as an attack. Tightened to require an imperative position — sentence start, addressing the reader. False positives are the expensive failure here: they would have put a security-log entry on every legal thriller anyone uploaded.

The reachability checker was right by accident

ProjectTabs builds links as const href = ... and renders <Link href={href}>. No pattern in the checker could see that — a slug is not a path. It reported /studio/[projectId]/export as unreachable while the tab sat in the nav, and the other tabs only passed because their paths happened to appear in unrelated revalidatePath() calls. A checker that gives the right answer for the wrong reason is worse than one that fails. It now understands slug-based tab arrays.

npm run smoke pointed at the wrong server

smoke-routes.ts defaulted to port 3000 while every other check script defaulted to 3100. One stage of the suite silently tested a different server and reported every page as a 404 while the app was working perfectly.

The features page warned on every render

The <> fragment returned from .map() carried no key — it was on the inner <tr>, which satisfies nothing. Pre-existing; fixed with a keyed <Fragment>.

An audit assertion hardcoded its own answer

verify-pipeline.ts asserted exactly 11 continuity categories. A1.5 added a twelfth and the count was correct — the assertion was stale. Now compared against CONTINUITY_CATEGORIES.length, so adding a category is a one-line change in one place.


Verification

npm run verify        # 1167 assertions, 11 suites — includes verify-a1 (340)
npm run smoke         # against a running server: routes, content, mobile, agent API
npm run verify:a1     # the A1 suite alone
npm run check:agent   # the live agent API alone

verify-a1.ts covers all twelve features against the real database and real services. Highlights of what it actually asserts:

  • Extraction quotes are genuinely present in the source text.
  • Episode slices are contiguous — no text is dropped between them.
  • A re-run of the extraction pass preserves decisions already made.
  • Grid panel geometry is deterministic, non-overlapping, and covers the image.
  • An uncropped grid panel is refused as a video frame anchor.
  • Narration refuses to illustrate before a duration is measured; a zero

measurement is rejected rather than stored.

  • A shot's duration equals the measured voiceover length.
  • Product prompts contain no colour or shape adjectives.
  • Two workers never lease the same pipeline unit; an expired lease is reclaimed.
  • Completing a unit twice does not double-count.
  • A resume picks up units added since the run started and skips ones removed.
  • A spend cap refusal is a 402, not a 500 — an agent retries a 500 forever.
  • evil-fal.run is refused: suffix matching is on a dot boundary.
  • Content cannot break out of its own injection fence.
  • Every registry model has a real adapter.
  • No help article links to a slug that does not exist.

What none of it proves: that a provider returns good pictures. No test can, and claiming otherwise is the failure mode this codebase is built to avoid.