Skip to content

Design: architecture overview

Capacity values on this page are historical

The capacity 1 figures below record the original single-image-model build. They are not current guidance and must not be copied. Capacity is now set per model in the registry, and the live account runs real capacities. Capacity 1 is not a cost control: it measures at roughly one request per minute while GlobalStandard still bills per token. See the deployment guide and as-built. Corrected 2026-08-02.

Scope: Azure AI Foundry

This page describes the Azure AI Foundry target, the hosted-cloud target of ADR-0011. Foundry Local and Azure Local Foundry differ from it in models, features, identity, cost, and operations. Compare all three on Deployment targets.

  • Status: draft for review
  • Date: 2026-07-11
  • Author: foundry-architect (Fable)
  • Grounded in: ADR-0002 (image model), ADR-0003 (voice model), ADR-0004 (topology and region), ADR-0008 (publish-pipeline integration); supporting references to ADR-0001, ADR-0005, ADR-0006, ADR-0007 where a flow crosses their territory
  • Companion docs: resource-topology-and-caf-naming.md (names and tags), reliability-and-operations.md (Reliability and Operational Excellence), performance-efficiency.md (Performance Efficiency)

This document describes the general methodology end to end in text: how to stand up one shared Azure AI Foundry account to serve image and voice generation for one or more downstream products, and how to wire that account into a publish-time pipeline safely. The Lucid phase draws it; nothing here depends on a diagram existing first. It designs only what the ADRs decided. Where an ADR is silent, the gap is called out in the final section rather than filled in. A concrete, deployed instance of this exact methodology is restated in full at the end, in the closing "Worked example" appendix.


1. Solution in one paragraph

One shared Azure AI Foundry account of kind AIServices (<prefix>-<initiative>-<env>-<region>-01, SKU S0, single region, resource group rg-<initiative>-<env>-<region>-01) serves two Microsoft first-party preview models for one or more downstream consumers (in this methodology, "brands," "products," or "sites" - any distinct published output that needs generated art and narration). MAI-Image-2.5 is hosted as a Global Standard model deployment named mai-image-25; MAI-Voice-2 needs no deployment and is served through the same account's Azure Speech surface by SSML voice name. Nothing is synthesized at runtime: a publish-time Node pipeline running on a developer workstation pre-renders per-voice audio variants and scene art, then uploads them to per-consumer object storage (this methodology's reference implementation uses Cloudflare R2 buckets for audio and covers, and a static site origin for scene art) under immutable content-hashed keys. The downstream apps and sites consume those static assets; no Azure key ever reaches a worker or a browser. Cost is capped by a synchronous pipeline budget guard, backstopped by the Azure budget budget-<initiative>-<env>-<region>-01 and the subscription spending limit, with secrets held by name in a reused platform Key Vault.

2. Component inventory

2.1 Azure estate (new, this initiative)

ComponentCanonical nameWhat it isADR
Resource grouprg-<initiative>-<env>-<region>-01Single scope for the initiative's Azure resources, budget, and tags, one regionADR-0004, ADR-0006
Foundry account<prefix>-<initiative>-<env>-<region>-01Microsoft.CognitiveServices/accounts, kind AIServices, SKU S0, custom subdomain enabled; hosts the image deployment and serves the Speech surfaceADR-0004
Model deploymentmai-image-25MAI-Image-2.5 (Preview), model format Microsoft, SKU GlobalStandard, capacity 1; version re-queried at deploy timeADR-0002, ADR-0004
Foundry projectproj-<initiative>-media-01Optional Foundry account project, created for the playground voice audition and the auto-applied project cost tag; no runtime call depends on itADR-0004 follow-up, ADR-0003 follow-up, ADR-0006
Budgetbudget-<initiative>-<env>-<region>-01Cost Management monthly budget, capped USD, scoped to the resource group, notify-only backstopADR-0006
Model gateway (optional, off by default)app-gw-<initiative>-<env>-<region>-01 on asp-<initiative>-<env>-<region>-01App Service (Linux, B1) running a request shim in front of the account. Repairs requests a client sends that a model refuses, and holds the account key so editor configuration never carries it. Deploy only when a tool you cannot configure meets a model that will not budgesee model gateway

2.2 Azure estate (existing, reused or untouched)

ComponentNameRole in this designADR
Key Vault (REUSE, do not create)a platform Key Vault, by name onlyHolds the one stored secret by name; endpoint and region entries for one-stop retrievalADR-0005
Existing Speech account (UNTOUCHED, if one already exists), by name onlykind SpeechServices, F0, its own resource groupKeeps serving any pre-existing narration and read-along track for downstream consumers; deliberately not reused, upgraded, or migratedADR-0004
Subscriptionby name onlyHome of everything above; monthly credit or budget with a spending limit ONADR-0001, ADR-0006

2.3 Non-Azure estate (existing, integrated)

ComponentWhereRole
Publish pipelinetools/publish.mjs, tools/tts.mjs, tools/stitch.mjs, tools/r2-upload.mjs in each downstream consumer's repoPublish-time pre-render of chapter/content JSON, narrator audio, and (new) MAI listen-voice variants; runs on a developer workstation, outside Azure
Image tool (NEW)consumer site repo, tools/mai-image.mjsCalls the generations and edits endpoints, writes PNGs and the provenance index
Prompt librarya read-only prompt/resource repoPrompt source, keyed by scene; trademarked terms genericized per ADR-0007
Content storageobject storage (reference implementation: Cloudflare R2), per-consumer bucketsImmutable content-hashed audio, chapter/content JSON, covers; manifest.json uploaded last
Downstream appsoffline-first PWAs (reference implementation: Vite + Preact, Cloudflare Worker with D1 + R2)Offline-first consumption; never synthesize, never hold a key
Marketing/static siteseach consumer's public siteHosts scene art that content JSON hotlinks; covers are the only art copied into object storage

3. The two model surfaces on one account

Both modalities ride the single AIServices account. This is the load-bearing topology decision of ADR-0004 (WAF Cost Optimization and Operational Excellence: one resource means one budget scope, one identity surface, one secret set).

3.1 Image: MAI-Image-2.5 via the mai-image-25 deployment

  • Endpoints on the account's custom subdomain: https://<prefix>-<initiative>-<env>-<region>-01.services.ai.azure.com/mai/v1/images/generations (JSON) and .../mai/v1/images/edits (multipart). The request model parameter is the deployment name mai-image-25, which stays stable even when the underlying Preview model version is redeployed.
  • The documented parameter surface is only model, prompt, image (edits only), width, height. No seed, mask, negative prompt, or candidate count exists, so style matching uses exactly two arms: prompt engineering on generations, and edits as a pseudo style reference. Candidate selection is a human review step.
  • Canvas standardized at 1248x832 (a clean 3:2 at 1,038,336 pixels, under the 1,048,576-pixel cap). Larger legacy canvases that exceed the cap are not reproducible.
  • Auth is Microsoft Entra ID, keyless: DefaultAzureCredential with scope https://cognitiveservices.azure.com/.default, resolving to the az login user on a workstation. WAF Security: zero stored secret on the image path.

3.2 Voice: MAI-Voice-2 via the Speech surface, no deployment

  • There is no deployment step and no Foundry-project dependency. A voice is selected per call by the SSML <voice name> attribute against the Speech synthesis endpoint; the key path uses the regional endpoint https://<region>.tts.speech.microsoft.com/cognitiveservices/v1.
  • Owner-locked voice set, identical across downstream consumers, listen-only in v1: a small fixed set of named voices, one rendered with an excited style via an mstts:express-as wrapper (the one real tts.mjs change). Read-along stays on each consumer's existing narrator because WordBoundary support for MAI-Voice-2 is unconfirmed. WAF Reliability: the word-sync highlight never leaves the proven track.
  • Narrators stay where they are, each served by any existing pre-existing Speech account. The new account serves only MAI listen variants and image calls, so the pipeline carries a deliberate two-resource key split: one credential pair for the narrator (old resource) and one for the variants (new resource).
  • Speech auth on the new account is key-based for now, with the key sourced from the platform Key Vault and injected via gitignored .dev.vars; the custom subdomain is kept enabled so a later Entra-for-Speech move stays cheap.

4. End-to-end flows

Flow A: voice variant publish (per consumer, per content unit)

  1. An operator runs node tools/publish.mjs --brand <consumer> --voices <all|slug[,slug]> in the consumer's repo (default none, so today's behavior is unchanged).
  2. The pipeline parses source content into speakable blocks, then, for each requested listen voice, checks the voice-aware variantHash (content plus voice id plus style) against tools/.state/<consumer>.json. Unchanged variants are skipped; only missing or stale variants render (WAF Cost Optimization and Performance Efficiency).
  3. Before any metered call, a ledger guard runs: state.maiLedger[month] = { chars, estUsd } with estUsd = chars x rate / 1,000,000; the run aborts if a configured monthly budget cap would be exceeded. This is the synchronous, pre-spend cap. WAF Cost Optimization.
  4. tts.mjs synthesizes block by block against the new account, wrapping the expressive voice's blocks in mstts:express-as style="excited". Blocks respect the 10-minute-per-request audio cap and the 64 KB SSML limit; stitch.mjs joins block MP3s with ffmpeg on the publish machine.
  5. r2-upload.mjs uploads each variant to an immutable content-hashed key with a one-year immutable cache profile, retrying with backoff.
  6. The manifest gains an additive, optional audioVariants array per content unit (schemaVersion stays stable; old clients ignore it) and is uploaded last with a short cache profile, so downstream apps never see a manifest that points at missing objects. WAF Reliability.
  7. Variants carry empty word arrays; the narrator's audio and timings fields are untouched, so read-along keeps working everywhere.

Flow B: image generation (one consumer pilots first, per ADR-0002 pilot scope)

  1. An operator runs node tools/mai-image.mjs in the piloting consumer's site repo with a scene id, an arm (--gen or --edit <sourcePng>), and the default 1248x832 canvas.
  2. The tool reads the scene's prompt from the prompt library, keeps whatever house illustration framing that consumer uses, and genericizes trademarked terms.
  3. It acquires an Entra bearer token via DefaultAzureCredential and calls generations or edits with model: "mai-image-25", pacing to the tier rate limit (10 requests per minute, Tier 5) and retrying 429s with backoff.
  4. The first calls double as the cost probe: the tool reads any token usage off the live response (tokens-per-image is unpublished) before any batch is authorized.
  5. The returned PNG is written to the consumer's image directory (or covers directory), and a record is appended to a committed provenance index (generator, modelVersion, endpoint, promptHash, promptRef, width, height, sourceImage, createdAt, tool; seed recorded as none because the API has no seed).
  6. Publication is asymmetric by design: scene art is hotlinked from the marketing origin inside content JSON, so a site deploy makes regenerated scenes live in the downstream app with no republish; covers are content-hashed into object storage, so a changed cover re-uploads on the next publish run.

Flow C: downstream consumption (runtime, no Azure dependency)

  1. A downstream app fetches manifest.json (short cache) from its consumer's object storage bucket, then content JSON and audio by immutable hashed URL.
  2. In listen mode the app plays the selected variant track; whenever read-along is active it always loads the narrator's audio plus timings, keeping the highlight on the proven WordBoundary track.
  3. Scene art loads from the marketing origin; covers load from object storage. Offline-first downloads fetch the selected variant. No call in this flow touches Azure, so a preview-model outage never reaches a downstream user. WAF Reliability.
  4. Downstream apps show a user-facing disclosure that illustrations and some narration are AI-generated, where the content is aimed at a sensitive audience.

Flow D: cost and governance (continuous)

  1. The pipeline ledger is the authoritative per-consumer and per-model record and the only synchronous cap (Flow A step 3).
  2. budget-<initiative>-<env>-<region>-01 (resource-group scope, capped USD monthly, actual thresholds at 50, 75, 90, 100 percent plus a forecasted alert at 100 percent) notifies the owner through one action group; it detects after the spend, roughly daily.
  3. The subscription spending limit stays ON as the invoice-side hard stop for a credit subscription, where one applies; tags (initiative=<initiative>, env=<env>, owner, costCenter) are applied before any backfill so Cost Analysis can isolate the initiative; the per-consumer split lives only in the ledger. WAF Cost Optimization and Operational Excellence.

5. Trust boundaries and secret posture

  • Boundary 1, developer workstation to Azure: Entra user token for image (no stored secret); vault-sourced key for Speech (named secret in the platform Key Vault, injected via gitignored .dev.vars). Names only in git, values only in the vault or the CI secret store. WAF Security.
  • Boundary 2, pipeline to object storage: provider credentials for uploads, already established by the existing pipeline; out of Azure scope.
  • Boundary 3, runtime: none. Workers and browsers hold no Azure credentials and make no Azure calls. WAF Security and Reliability.
  • Least-privilege roles on the account scope: pipeline identity holds Cognitive Services User (image data plane) plus Cognitive Services Speech User (TTS data plane); Cognitive Services Contributor is held by a human for the one-time deployment only.
  • Network posture: public network access stays enabled because the pipeline runs outside Azure; an optional service-level IP allowlist is the only proportionate hardening.

6. WAF pillar map (why each major choice is shaped this way)

ChoicePillarRationale
One shared AIServices account for both modalities and every downstream consumerCost Optimization, Operational ExcellenceOne budget scope, one role surface, one secret set (ADR-0004)
Publish-time pre-render, never runtime synthesisReliability, Cost Optimization, Performance EfficiencyPreview outages never reach users; spend is bounded per publish run; downstream apps get CDN-cached static assets (ADR-0008)
Narrator stays on the existing Speech account; MAI variants on the new accountReliabilityBlast-radius isolation: the proven read-along track has no dependency on the preview stack (ADR-0004)
Listen-only MAI voices in v1ReliabilityWordBoundary is unconfirmed for MAI-Voice-2; the highlight never gambles on it (ADR-0003)
Version re-query at deploy, periodic re-check, deployment name without versionOperational ExcellencePreview version churn is absorbed without caller changes (ADR-0002)
Entra keyless for image, vault-sourced key for SpeechSecurityZero stored secret where cheap; the one stored secret is vault-held, gitignored, rotatable (ADR-0005)
Ledger guard as the real cap, budget as backstop, spending limit ONCost OptimizationThe only pre-spend stop is client-side; Azure budgets are notify-only and daily (ADR-0006)
Immutable content-hashed keys, manifest lastReliability, Performance EfficiencyIdempotent re-runs, resumable backfill, no dangling references, one-year edge cache (ADR-0008)
Default guardrails, genericized trademarks, house illustration framing, provenance index, user disclosureSecurity (responsible AI), Operational ExcellenceLow-friction posture fit for sensitive-audience content; auditability closes the unknown-generator gap (ADR-0007)

7. Explicit non-goals (decided against in the ADRs)

  • No on-demand or worker-proxied synthesis (long-form content exceeds the 10-minute cap; Workers cannot run ffmpeg; keys would become runtime secrets).
  • No reuse or upgrade of an existing pre-Foundry Speech account, and no second region or split resource.
  • No MAI-Image-2 or MAI-Image-2e deployments (both retire on a fixed date; neither has the edits endpoint).
  • No private endpoints or disabled public network access (the pipeline runs outside Azure).
  • No batch or Long Audio TTS path (it stores script and audio; the real-time path stores nothing).
  • No bulk generation inside this plan: infrastructure is deployed and verified ready to generate, then holds for the owner (MASTER-PLAN scope).

8. Build order dependencies

  1. Any drifted publish.mjs copies across downstream consumer repos are reconciled first, in a standalone commit per repo. This blocks all MAI pipeline work.
  2. Azure resources are created in dependency order (group, tags, account, deployment, roles, budget) with the budget in place before any spend; every resource-creating call is owner-gated (ADR-0006, MASTER-PLAN guardrail). The exact sequence belongs to the implementation guide.
  3. A short voice spike runs against the new account before variant code lands: WordBoundary behavior, output format acceptance, the exact voice identifiers, and the expressive style token.

9. ADR gaps surfaced by this design

  • Environment token: ADR-0004's illustrative naming shape and ADR-0006's tag table used a placeholder environment token as an example. The canonical set fixed in this design phase standardizes on a concrete environment token (for example prod) once a real deployment is scoped, because the account produces production-serving assets. ADR-0004 explicitly deferred final strings to the design phase, so this is a finalization, not a new decision.
  • Foundry project: ADR-0004 left creating the optional project open. This design creates proj-<initiative>-media-01 for the playground audition (the ADR-0003 voice-token confirmations) and the auto-applied project cost tag. No API flow depends on it.
  • Runtime telemetry: no ADR decides diagnostic settings, a Log Analytics workspace, or Azure Monitor metric alerts (for example 429 rates) on the account. The decided monitoring is cost-only plus pipeline logs. Recorded as a gap in reliability-and-operations.md, not filled in here.
  • CI identity: ADR-0005 defers the exact OIDC workload-identity federation setup and role-assignment scripts to the design phase's identity-and-security doc, which is outside this four-document batch. The roles themselves are decided and summarized in section 5.

Sources

  • docs/adr/ADR-0001-target-tenant.md through docs/adr/ADR-0008-publish-pipeline-integration.md (all decisions restated here)
  • docs/research/SPIKE-06-pipeline-integration.md (as-is pipeline mechanics, object storage key layout, hotlink behavior)
  • docs/research/SPIKE-02-voice-model.md (voice access pattern, SSML and endpoint shapes)
  • ai/verification/environment-readiness.md (subscription, region, quota facts)
  • ai/MASTER-PLAN.md (scope boundary: deploy and verify, then hold)

<!-- safety-scan-worked-example:start -->

Worked example: Brand A / Brand B

This methodology is deployed and running in production today for two publishing brands, Brand A and Brand B, proving the pattern above holds up outside the abstract.

Azure estate (new):

  • Resource group: rg-<workload>-<env>-<region>-01 (East US)
  • Foundry account: aif-<workload>-<env>-<region>-01, kind AIServices, SKU S0
  • Model deployment: mai-image-25 (MAI-Image-2.5, Preview, GlobalStandard, capacity 1; re-queried 2026-06-02)
  • Foundry project: proj-<workload>-media-01
  • Budget: budget-<workload>-<env>-<region>-01, 100 USD monthly, resource-group scope

Azure estate (existing, reused or untouched):

  • Key Vault: kv-<workload>-<env>-01 (reused, holds <workload>-speech-key)
  • Existing Speech account: the legacy narrator Speech resource (kind SpeechServices, F0, its own resource group), untouched, keeps serving both brands' narrator and read-along track
  • Subscription: the MVP credit subscription, spending limit ON

Endpoints:

  • Image: https://aif-<workload>-<env>-<region>-01.services.ai.azure.com/mai/v1/images/generations and .../mai/v1/images/edits
  • Voice: https://eastus.tts.speech.microsoft.com/cognitiveservices/v1

Voice set (owner-locked, listen-only in v1, identical for both brands): Harper (en-US), Lisa (en-AU, exact identifier confirmed at spike time), Ethan (en-US, rendered with the excited style via mstts:express-as).

Narrators (unchanged, on the legacy narrator Speech resource): Brand A uses en-US-AndrewMultilingualNeural; Brand B uses en-GB-Ryan:DragonHDLatestNeural. The pipeline carries a deliberate two-resource key split: AZURE_SPEECH_* (narrator, old resource) and MAI_SPEECH_* (variants, new resource).

Non-Azure estate:

  • Publish pipeline: tools/publish.mjs, tools/tts.mjs, tools/stitch.mjs, tools/r2-upload.mjs in Brand B's publish pipeline and Brand A's publish pipeline
  • Image tool: Brand A's reader-app repo's tools/mai-image.mjs
  • Prompt library: the studio prompt repo (Illustration_Prompts_All_Stories.md, Branding_Illustration_Prompts.md, Character_Bible.md)
  • Content storage: Cloudflare R2, per-brand buckets <brand>-content
  • Reader apps: the reader apps at Brand A's reader app and Brand B's reader app
  • Marketing sites: Brand A's site (hosts scene art hotlinked by chapter JSON) and Brand B's site

Pilot scope: Image generation (Flow B) pilots on Brand A first, per ADR-0002.

Secrets: the one stored secret, <workload>-speech-key, lives in kv-<workload>-<env>-01 and is injected as MAI_SPEECH_KEY via gitignored .dev.vars. No value appears in this repo.

Tags applied: initiative=<workload>, env=prod, plus owner and costCenter.

<!-- safety-scan-worked-example:end -->