Darkwood Blog Blog
  • Articles
  • Watch
  • Releases
  • Creators
en
  • de
  • fr
Login
  • Blog
  • Articles
  • Watch
  • Releases
  • Creators

๐Ÿ“š When AI Becomes a Library: The Nolife Local Experiment

on August 23, 2026

Log in to add a reaction to this post

๐Ÿš€ 1

Most of the AI development I see still orbits the same center of gravity: ever larger general-purpose language models. That reflex is understandable. A model that can write poetry, reason about contracts, generate PHP, summarize documents, and answer questions feels like a universal tool. When every problem looks vaguely โ€œAI-shaped,โ€ the temptation is to send it to the same endpoint.

But many application problems do not need a model capable of all of that. They need one model that performs one computational task extremely well.

Nolife Local is an experiment around that alternative. Instead of sending an image to a general-purpose multimodal LLM, I integrated a specialized vision model directly into a Symfony application and made it part of the applicationโ€™s normal execution pipeline. The concrete case study is Depth Anything 3: monocular depth estimation, running locally on Apple Silicon through PHP FFI, orchestrated with Darkwood Flow, and built with a coding agent in a loop that slowly changed shape as the project grew.

Early on, that loop looked familiar:

idea โ†’ ask agent to implement โ†’ inspect result

By the end it looked more like:

observe โ†’ collect evidence โ†’ formulate problem โ†’ define constraints
โ†’ let agent investigate โ†’ implement โ†’ measure โ†’ validate

The second loop is slower to start and much more reliable. The article follows both the runtime architecture and that shift in how the work itself was defined.

This is not an argument that LLMs are bad, that local AI is always better, or that small models replace general ones. The stronger conclusion is simpler:

The task should determine the model and the architecture.

General-purpose models remain excellent when the problem requires broad semantic reasoning. Specialized models become interesting when the desired output is itself specialized computation.

Not every AI problem is an LLM problem

The default developer reflex in 2026 is familiar:

problem โ†’ large model โ†’ prompt โ†’ parse text

It works often enough that we stop asking whether it is the right abstraction. For many tasks it is. For others, it is like using a spreadsheet to edit pixels: possible in theory, expensive in practice, and the output shape is wrong.

What I wanted to see was the opposite shape:

problem โ†’ specialized model โ†’ dense numerical result โ†’ application code

Depth estimation is a good stress test for that idea. The desired product is not a sentence about space. It is a field of numbers โ€” one value per pixel โ€” that can become a colormap, a camera estimate, or a 3D reconstruction. Language is a poor carrier for that result. A model trained to emit depth is a better one.

That distinction matters before any talk of PHP, FFI, or Symfony. If the computation itself is specialized, the model should be specialized too. The same instinct would later apply to how I used the coding agent: broad prompts produce broad (and often wrong) work. Narrow problems produce narrower, inspectable changes.

Depth from one image

Monocular depth estimation answers a deceptively simple question: given a single RGB photograph, can we estimate how far things are from the camera?

single RGB image
        โ†“
Depth Anything 3
        โ†“
dense geometric estimation

Humans do this constantly. A photograph of mountains already feels three-dimensional. Software has to recover that structure without stereo pairs, LiDAR, or a second viewpoint. The model must reason about occlusion, texture gradients, perspective, and object boundaries โ€” but the result is numeric, not narrative.

Depth Anything 3 (ByteDance) is a family of ViT-based models with a DualDPT head. The LocalAI teamโ€™s depth-anything.cpp port runs them locally through ggml. No Python at inference time. No PyTorch runtime. No cloud vision API.

In Nolife Local we use DA3-BASE. Through the native C API, one dense inference returns:

  • a depth map (width ร— height floats)
  • a confidence map (same resolution, when present)
  • camera extrinsics (3ร—4)
  • camera intrinsics (3ร—3)
  • a flag indicating whether the depth is metric

For DA3-BASE, that flag is false. The depth values are relative, not meters. On our reference photograph the observed range was roughly 0.43โ€“3.47 in relative units. Treating those numbers as distances in meters would be a factual error. Metric depth exists in other DA3 variants (nested / mono paths); this demo does not use them.

From those outputs the native library can also build a glTF/GLB point cloud by back-projecting valid pixels into world coordinates. That is how the Symfony demo ends up with a 3D viewer, not by inventing geometry in PHP.

The research-to-runtime path looks like this:

ByteDance PyTorch checkpoint
  โ†“  convert once to GGUF (Python, offline)
GGUF weights on disk
  โ†“  ModelLoader (C++)
ggml compute graph
  โ†“  Metal / CPU / other backends
depth + confidence + camera
  โ†“  optional GLB export
application artifacts

We did not train Depth Anything 3. We did not fine-tune it, apply LoRA, or distill a student network. Specialization here means choosing an existing purpose-trained model and integrating it into a specialized local execution pipeline. Quantization (q4_k versus f32) is a separate dimension: a runtime/model-representation choice, not training.

Before writing PHP, the first work was understanding what the native stack actually did โ€” which C API functions existed, what memory contracts they implied, and what outputs were real rather than assumed from model names. That investigation became the first โ€œissueโ€ in the project: understand inference before binding it. Skipping that step would have been the agent equivalent of choosing a general model because it sounds capable.

Specialized model versus general multimodal model

The contrast is not merely about size.

General multimodal path:

image
  โ†“
large general model
  โ†“
semantic interpretation
  โ†“
text / structured answer

Specialized depth path:

image
  โ†“
DA3
  โ†“
dense numerical representation
  โ†“
depth / confidence / camera / geometry

A multimodal LLM can discuss spatial relationships in language. That is a real capability, and for some products it is exactly what you want. It is not the same as emitting a deterministic 504ร—280 float field, offline, from a ~99 MB GGUF, with structured camera parameters and a GLB export path.

The difference is the nature of the computation. Once the output is a dense geometric estimate, the model stops looking like a chatbot and starts looking like an application primitive: something you call, measure, cache, and compose.

That is the architectural shift Nolife Local explores at runtime. A parallel shift would appear in development: stop asking for general improvement; ask for the specific computation or integration step the application actually needs.

The unusual part: PHP

Then comes the deliberately awkward constraint.

The goal was not:

Call a Python microservice from Symfony.

Nor:

Send the image to an AI API.

The experiment asked:

How far can we integrate the native model directly into a Symfony application?

That leads to PHP FFI.

PHP does not execute the neural network kernels itself. Claiming otherwise would be marketing, not engineering. The ownership boundary in the final application is approximately:

Browser
   โ†“
Symfony
   โ†“
Darkwood Flow
   โ†“
NativeDepthBridge
   โ†“
PHP FFI
   โ†“
libdepthanything
   โ†“
ggml / Metal
   โ†“
Depth Anything 3

Native results then return to PHP for application-level work.

PHP owns HTTP and upload handling, validation, orchestration, Flow step timing, native lifecycle coordination, result representation, GD colormap rendering, Twig / Symfony UX, CLI tooling, observability surfaces, and artifact management under var/inference/.

Native code owns preprocessing (resize, normalize, patch alignment), tensor operations, the ViT backbone, DualDPT depth/confidence heads, camera estimation, Metal execution, and native GLB reconstruction.

Symfony remains a web framework. Depth Anything remains a native vision engine. FFI is the seam between them.

Several tempting directions were deliberately not taken: pure PHP tensor inference, a Messenger worker farm, DDD layering, CQRS, or โ€œwhile weโ€™re hereโ€ Linux porting. None of those solved the current problem. They would have expanded scope because an agent could generate them, not because the experiment required them. Scope, I learned, has to be chosen by the human and written down before the agent starts typing.

PHP FFI as the boundary

FFI matters because it lets PHP own the lifecycle of a native context instead of shelling out to a CLI and hoping for the best.

The first useful question was not how much of the C++ code I could rewrite in PHP. It was where the useful boundary actually was. The brief for the FFI bridge was narrow on purpose:

Use da_capi_depth_dense, not a CLI wrapper.
Honor da_capi_free_floats for native buffers.
Respect that da_ctx is not thread-safe.
Produce an observable CLI or HTTP result.

That is already an issue in miniature: problem, constraints, expected outcome.

NativeDepthBridge loads libdepthanything.dylib, binds the C API from config/da_capi.h, and exposes a narrow surface to the rest of the application: load context, run dense inference, export GLB from cache, free buffers.

A focused excerpt of the boundary looks like this:

$rc = $ffi->da_capi_depth_dense(
    $this->ctx,
    $imagePath,
    FFI::addr($outH),
    FFI::addr($outW),
    FFI::addr($outDepthPtr),
    FFI::addr($outConfPtr),
    FFI::addr($outSkyPtr),
    $ext,
    $intr,
    FFI::addr($isMetric),
);

$depthPtr = $this->ffi->cast('float*', $outDepthPtr);
$depth = $this->copyFloatsFromPtr($depthPtr, $count);

The pattern is deliberately boring:

PHP
โ†’ native function
โ†’ native result pointer
โ†’ copy into PHP arrays
โ†’ da_capi_free_floats

Resource lifetime is part of the story. The bridge keeps a da_ctx while the model path stays unchanged, frees it on destruction or model swap, and serializes access with a file lock because the native context is not thread-safe. Float buffers returned by the C API are copied into PHP arrays and then freed immediately.

The brief was incomplete. During implementation, casting a large float* to float[141120] segfaulted on PHP 8.5. Indexed reads ($ptr[$i]) worked. That finding had to be added to the working context โ€” observed behavior over a plausible explanation. Agents are very good at sounding confident about memory layout. Reproduction won.

Later we stress-tested that lifetime inside one PHP process:

php -d ffi.enable=1 bin/console app:depth-stress \
    -i public/samples/mountains.jpg \
    -r 10

Ten full pipeline runs in-process kept GLB export on the cached path (~20 ms each) and did not require reloading the model between iterations. That is the kind of validation that turns โ€œFFI works onceโ€ into โ€œFFI works as application infrastructure.โ€

Making it a Symfony feature

Once the bridge exists, the native model becomes an ordinary Symfony feature rather than a mysterious external service.

The user flow is:

upload image
โ†“
validate
โ†“
run local inference
โ†“
extract depth / confidence / camera
โ†“
generate visualization (PHP GD turbo colormap)
โ†“
reuse native result for GLB
โ†“
build result
โ†“
render Symfony UX

The upload page is intentionally small: drop an image, choose q4_k or f32, submit. While the request runs, the UI shows a processing state. A few seconds later the result page shows the original photograph beside the depth colormap, a confidence panel, camera summaries, pipeline timings, and a <model-viewer> for the GLB.

No custom WebGL engine. No Messenger workers. No Python sidecar. The Symfony application hosts the interaction; the native library performs the expensive computation; the browser only visualizes.

CLI tooling follows the same idea. Diagnostics treat the model like any other dependency:

FFI                OK
GD                 OK
Native library     OK
q4_k model         OK
f32 model          OK
Inference dir      OK

READY

That output comes from php bin/console app:depth-check. The model is no longer an opaque AI endpoint. It is application infrastructure with a readiness check.

CLI inference prints the same pipeline story the web UI shows:

OK id=โ€ฆ 504x336 infer=3179.4ms โ€ฆ
  validate: 3.49ms
  infer: 3180.28ms
  colormap: 75.52ms
  glb: 19.65ms
  result: 0.00ms

The important line for readers of benchmarks is that infer= is the Flow infer step, not a hidden synonym for โ€œwhole request.โ€ Once that vocabulary is shared between CLI, web, and agent briefs, arguments about latency become much less mystical.

Orchestrating inference with Darkwood Flow

After the first working version, orchestration lived inside one service method: validate, infer, colormap, export, assemble. The pipeline was real, but implicit. Timings were either missing or collapsed into a single number that mixed very different kinds of work.

I introduced Darkwood Flow not as product placement, but because an expensive local model needs the same engineering properties as any other expensive computation: explicit steps, timings, failure handling, and a place to look when something feels wrong.

The final pipeline is:

Validate
   โ†“
Infer
   โ†“
Colormap
   โ†“
Export scene
   โ†“
Build result

Flow sits above the FFI bridge. NativeDepthBridge remains a narrow primitive. Flow owns sequencing and per-step wall-clock measurements.

Making the pipeline explicit turned out to matter more than making it clever. The integration iteration started with a small, well-scoped observation โ€” orchestration was monolithic โ€” and a concrete desired outcome: per-step timings in CLI and, later, in the web UI. That is issue-shaped work even before anyone names the pattern.

Flow did not make inference faster. It made the expensive steps visible. Visibility is a prerequisite for honest problem statements.

The benchmark that did not make sense

Early on, Symfony inference looked roughly like this:

~2.6 seconds

That matched the warm native numbers closely enough that I trusted it. The demo felt โ€œabout three seconds.โ€ Then Flow made every step visible, and the arithmetic stopped adding up.

A warm run after Flow integration looked more like:

Step ms
Validate ~3
InferDepth ~2639
Colormap ~86
ExportScene ~2535
Total ~5260

The InferDepth step alone still looked like the earlier ~2.6 s story. The full request the user waited for was closer to five seconds.

ExportScene was not โ€œwriting a GLB.โ€ It was paying for another native inference.

At that moment, the valuable contribution was no longer โ€œplease optimize the PHP code.โ€ It was a precisely formulated engineering problem:

Observation:
The complete pipeline takes much longer than infer-only.

Evidence:
Flow timings show another expensive native operation during GLB export.
InferDepth โ‰ˆ 2.6 s. ExportScene โ‰ˆ 2.5 s.

Constraint:
Preserve depth, confidence, camera, and GLB output.

Desired result:
One native inference per request.

Non-goal:
Do not solve this by adding concurrency or extra architecture layers.

Compare that to an early prompt like โ€œimprove performance.โ€ The second brief is narrower and far more powerful. The agent does not need more freedom. It needs better context.

Finding the second inference

The call path before the fix was:

InferDepth
  โ†’ da_capi_depth_dense()
  โ†’ full ViT + DPT inference

ExportScene
  โ†’ da_capi_export_glb()
  โ†’ full ViT + DPT inference again
  โ†’ write GLB

da_capi_depth_dense() and da_capi_export_glb() were independent entry points. The export path prepared depth, confidence, pose, and RGB by running depth_pose_native() again. There was no cache between them. From the applicationโ€™s point of view we already โ€œhadโ€ the depth result; from the native APIโ€™s point of view, GLB export did not know that.

This is the kind of bug that looks like a performance problem and is actually an architecture problem. Without step timings, the second pass hides inside a single user-facing wait. With step timings, it becomes obvious:

InferDepth โ‰ˆ 2.6 s
ExportScene โ‰ˆ 2.5 s

Two almost equal expensive steps are rarely a coincidence.

The investigation followed evidence, not speculation. I did not start by blaming Metal, quantization, PHP overhead, or memory bandwidth. I followed the timings to the call path, then read the native export preparation code. That discipline mattered because a coding agent can generate equally plausible wrong explanations at high speed. Observed behavior, logs, timings, code paths, and reproduction should outrank narrative.

One inference, several outputs

The fix was not asynchronous workers, Messenger, or a broader rewrite. It was reuse.

Conceptually:

BEFORE

image
 โ”œโ”€โ”€ infer โ†’ depth
 โ””โ”€โ”€ infer again โ†’ GLB
AFTER

image
   โ†“
one inference
   โ†“
native result
   โ”œโ”€โ”€ depth
   โ”œโ”€โ”€ confidence
   โ”œโ”€โ”€ camera
   โ””โ”€โ”€ GLB

On the native side this became ABI v11: da_capi_export_glb_cached(). Dense inference populates an export cache on the context; GLB writing consumes that cache without a second depth_pose_native(). On the PHP side, ExportScene calls exportGlbCached().

The decisive signal was the GLB step, not a contested millisecond on InferDepth.

Before (warm Flow run that exposed the bug):

Step ms
Validate ~3
InferDepth ~2639
Colormap ~86
ExportScene ~2535
Total ~5260

After (experiments/004-pipeline-single-pass, mountains.jpg โ†’ 504ร—336, q4_k, warm, Apple M4 / Metal):

Step Median (ms)
Validate ~3
InferDepth ~3165
Colormap ~88
ExportScene (cached) ~19
Total ~3274

Native inferences per request: 2 โ†’ 1. ExportScene collapsed from roughly another full inference to a ~20 ms write from cache. InferDepth is higher in the โ€œafterโ€ table because that measurement uses the larger demo-sample tensor (504ร—336); the optimization is the deleted second pass, not a claim that InferDepth itself got faster.

The resulting code change was relatively small. Identifying exactly which work was duplicated was the valuable part. Observability found the duplicated work. The performance improvement came from deleting it.

That lesson is sharper than โ€œadd caching somewhere.โ€ The expensive computation already produced every tensor the GLB exporter needed. The architecture just failed to notice.

What the measurements actually say

All numbers below were measured on Apple M4, macOS arm64, Metal. They are not universal Depth Anything 3 benchmarks. Different processed tensor sizes mean different workloads.

Measurement before optimization became a recurring rule. Not โ€œPHP is slow,โ€ but: native infer, Flow/PHP steps, GLB export, full pipeline โ€” each with its boundary named.

Resolution is part of the benchmark

DA3 resizes so the longest side is about 504 pixels, with dimensions aligned to patch size 14. Source aspect ratio therefore changes the processed tensor:

Source Processed Warm infer-only median (q4_k)
1280ร—720 reference photo 504ร—280 ~2495 ms
1024ร—680 demo samples 504ร—336 ~3.0โ€“3.2 s

Those figures are not contradictory. 504ร—336 has roughly 20% more pixels than 504ร—280. Comparing them without naming the processed resolution is how fake regressions are invented.

When one run looked like ~2495 ms and another like ~3175 ms, the right next step was not another optimization prompt. It was a better problem statement: same hardware, same variant, different processed dimensions. Once the numbers disagreed, investigation beat speculation.

Warm infer-only at 504ร—280

From experiments/003-warm-q4k-f32 (app:depth-bench, warmup 1, repeat 10, same PHP process):

Variant Median (ms) Model size
q4_k 2494.9 ~99 MB
f32 2495.1 ~393 MB

Cold start is a different phenomenon

The first q4_k load in a process measured about 6.9 seconds in the reference CLI experiment, including Metal shader compilation. Warm loads afterward are tens or low hundreds of milliseconds. Mixing cold start into a โ€œlatencyโ€ table without labeling it produces nonsense.

Flow / PHP overhead

Using same-run step timings on a warm full pipeline (experiments/005-flow-overhead, mountains.jpg):

validate   ~3.5 ms
colormap   ~90 ms
glb        ~20 ms   (cached)
result     ~0 ms

Non-native Flow/PHP steps summed to roughly 114 ms. In this experiment, the orchestration side accounted for about 0.1 seconds of the request. Neural inference dominated.

Flow did not make Depth Anything faster. It made the double-pass bug discoverable, and afterward it made the remaining overhead honest.

CLI reference (native da3-cli, 504ร—280)

From experiments/001-reference-cpp:

Variant Model size Load Infer median Peak RSS
q4_k ~99 MB ~6869 ms cold* 2589 ms ~457 MB
f32 ~393 MB ~194 ms warm 2580 ms ~1028 MB

* Cold q4_k load includes first-run Metal library init.

q4_k versus f32

Quantization is often sold as a free speedup. This experiment did not support that story on this hardware.

The question was not โ€œwhich should feel faster?โ€ It was a controlled comparison: same process, same input, warmup then ten repeats, processed resolution recorded. Issue 009 captured that brief before implementation.

At 504ร—280 warm inference, q4_k and f32 medians were effectively identical (~2495 ms). What changed dramatically was storage and memory footprint: roughly 99 MB versus 393 MB on disk, and much higher peak RSS for f32 in the CLI reference run.

The supported conclusion is therefore not โ€œq4_k is four times faster.โ€ It wasnโ€™t.

The supported conclusion is closer to:

Quantization dramatically reduced model storage in this experiment without producing a corresponding latency improvement on this particular Apple M4 / Metal workload.

I did not claim an accuracy winner. Without ground-truth depth for the demo photos, a โ€œwhich is betterโ€ metric would be theater. The practical choice for the demo default is q4_k because the file is smaller and the warm latency matched f32 here. A depth-delta command without ground truth was explicitly rejected from scope for the same reason.

What โ€œlocalโ€ means

Local inference and a fully offline application are related claims, not identical ones.

After install, Nolife Local performs no remote AI API call. Inference is FFI โ†’ libdepthanything โ†’ GGUF on disk โ†’ Metal. That part was true early.

The stricter demo-runtime claim took more work. The result page originally loaded Googleโ€™s <model-viewer> from a CDN while inference stayed local. That is a credibility trap: the AI is local, the page is not. We vendored public/vendor/model-viewer.min.js, removed the CDN reference from the demo path, and verified with HTTP checks and HTML inspection โ€” observed behavior again, not assumption.

Verified after that change:

Claim Result
Network required for inference No
Network required for demo runtime (result page) No

Setup still needs the network once: Composer packages, model download, native build. FrankenPHP hot-reload CDN scripts appear only when that optional env flag is set; the publication demo uses the built-in PHP server without it.

So the honest phrase is not โ€œAI in the cloud.โ€ It is also not โ€œthe entire universe is offline forever.โ€ It is:

After install, this demo can run with networking disabled. The model is a local library.

AI models as application infrastructure

Once a specialized model behaves like a library, the surrounding tooling starts to look familiar to any Symfony developer.

php bin/console app:depth-check
php -d ffi.enable=1 bin/console app:depth-infer -i public/samples/mountains.jpg -m q4_k
php -d ffi.enable=1 bin/console app:depth-bench -i public/samples/mountains.jpg -w 1 -r 5 --json
php -d ffi.enable=1 bin/console app:depth-stress -i public/samples/mountains.jpg -r 10
php bin/console app:depth-cleanup --keep=20

Warmup exists for demos. Cleanup exists because every run creates artifacts. Stress exists because native cache lifetime across repeated in-process calls is part of the contract. Metadata is written beside the images. Pipeline totals are computed once in BuildResult, not re-summed differently in Twig and CLI.

The surprising feeling, after enough of this, is that Depth Anything stops reading as โ€œan AI featureโ€ and starts reading as โ€œa native dependency with diagnostics.โ€

That is the point.

Context as an engineering artifact

By this point I had stopped treating the coding agent as something I asked to โ€œimprove the project.โ€ Each iteration started with a smaller observed problem, evidence, constraints, and a success condition. Implementation became the last step rather than the first.

That is what I mean by issue-first development โ€” not project-management theater, but a compressed brief the agent can actually use.

An issue can become a compressed representation of everything the agent needs to understand about a problem. Not just โ€œsomething is broken,โ€ but:

what happened
where it happened
environment
evidence
why it matters
constraints
expected behavior
what not to change

For Nolife Local, that context eventually included source code, the native API surface, model variant, processed resolution, Flow timings, benchmark methodology, error output, git history, known limitations, expected results, and explicit non-goals. The combination worked much better than a vague implementation request.

The projectโ€™s issue files mirror that shape. Issue 007 for the double inference. Issue 009 for q4_k versus f32. Issue 003 for the FFI bridge, updated when PHP 8.5 disagreed with the first memory assumption. ITERATIONS.md records the same loop in chronological form: observation, problem selected, change, validation, measurement, next candidate.

observe โ†’ issue โ†’ agent โ†’ implementation โ†’ measure โ†’ observe

Issue-first does not mean handing ownership to the agent.

Human: chooses the problem, defines intent, sets constraints, decides scope, evaluates trade-offs, validates results.

Agent: inspects, implements, measures, documents, iterates within the brief.

The agent can generate large amounts of code. That does not make it responsible for deciding what the project should become. A submitted patch can still contain insight, architecture understanding, and API design experience. Issue-first does not make code worthless. It makes problem understanding relatively more valuable when implementation is cheap to produce.

Specialize the model, specialize the context

There is a parallel here, and I treat it as an interpretation from the project rather than a universal law.

For computation:

general model
        โ†“
specialized task model
        โ†“
more constrained computation

For development:

general prompt
        โ†“
well-defined issue
        โ†“
more constrained implementation

The model side says: do not use the most general model when the task is already well defined. The agent side says: do not give the most general prompt when the engineering problem can be precisely defined. In both cases, reducing the search space can improve the result.

Specialize the model when the computation is specialized. Specialize the context when an agent is about to implement the next change.

What the experiment taught me

A few surprises, synthesized rather than re-tabled:

PHP FFI was sufficient for the integration boundary. The expensive part was not PHP orchestration. Observability exposed duplicate inference more effectively than intuition did. Processed resolution explained apparently inconsistent benchmarks. q4_k saved substantial disk space without an obvious warm-latency win on this Metal workload. And after enough tooling, a vision model can feel more like a native library than an AI service.

On the process side: implementation got cheaper; understanding the real problem did not. Configuration, environment, logs, measurements, and reproduction mattered as much as code. Agent-generated speculation was never equivalent to observed evidence.

Darkwood Flowโ€™s strongest claim in this project is not that every Symfony application needs Flow. It is that once AI models become normal application components, they need the same properties as any expensive computation: orchestration, timings, failure handling, lifecycle management, and observability. In Nolife Local, that visibility revealed duplicated native work. That is concrete evidence, not a slogan.

Limitations

This experiment has a defined scope.

It was validated on macOS arm64 / Metal only. There is no Linux verification here, no Windows verification, and no production PHP-FPM deployment benchmark. We did not train or fine-tune Depth Anything 3. Performance numbers are hardware-specific. The demo implements one specialized model, not a multi-model production mesh. GLB quality was validated for the publication samples; absolute geometric accuracy against ground-truth depth was not the goal.

Those are not apologies. They are the edges of the claim โ€” chosen constraints, not missing ambition.

Conclusion

I set out to ask a practical question: instead of sending an image to a general-purpose multimodal LLM, what happens if a specialized vision model becomes part of a Symfony applicationโ€™s normal pipeline?

What happened is that Depth Anything 3 behaved less like an โ€œAI productโ€ and more like a library: load it, call it, free it, measure it, diagnose it, reuse its results. PHP did not become an ML framework. It orchestrated a native specialized component through FFI, made the pipeline observable with Flow, and turned a hidden double inference into a single-pass architecture.

The broader change may be this: as AI produces more implementation, engineering moves further toward choosing the right computation, defining the right problem, and validating the result.

Three layers stack in this experiment:

Depth Anything 3     โ†’ specializes computation
Darkwood Flow        โ†’ makes computation observable and composable
Issue-first briefs   โ†’ specialize the context given to the coding agent

The conclusion is not โ€œstop using LLMs.โ€

It is closer to:

Stop assuming every AI-shaped problem should be solved by the same kind of model โ€” or the same kind of prompt.

A modern AI application can combine an LLM, a vision model, an embedding model, a speech model, a depth model, ordinary application code, and native libraries. Each should perform the work it is good at. Symfony does not need to reimplement ggml. It needs to be capable of hosting the boundary where those components meet.

Historically, open-source contribution often meant find a problem, write a patch, submit a pull request. Coding agents weaken the assumption that writing the patch is necessarily the scarce part. A future contribution can also be extremely valuable when someone provides precise reproduction, environment details, logs, configuration, use case, expected behavior, and constraints โ€” because those things allow a maintainer and an agent to implement the correct fix. Pull requests are not obsolete. Neither is a well-written issue. When implementation can be generated quickly, the interesting scarcity shifts toward intent and evidence.

Nolife Local is one version of that architecture:

Symfony + PHP + FFI + Darkwood Flow + specialized native model

with no remote inference API required after install.

Resources

Upstream and related projects:

  • depth-anything.cpp

  • Source code: https://github.com/matyo91/nolife-local

  • Slides: https://github.com/matyo91/slidewire

Log in to add a reaction to this post

๐Ÿš€ 1

Site

  • Sitemap
  • Contact
  • Legal mentions

Network

  • Hello
  • Blog
  • Apps
  • Photos

Social

Darkwood 2026, all rights reserved