Publish & audio

Moving a finished draft into the learner-facing catalogue, and narrating it · ← platform hub

In one sentence

Authoring and learning are two different services with two different databases. Publishing is the moment a draft crosses from one to the other: authoring-v2 pushes the finished module into micro-learning over HTTP, and from then on the learner-facing copy is the one that matters.

This is the only place in the platform where a user-visible action depends on a synchronous call between two services — if micro-learning is down, publishing fails immediately rather than queueing.

Publishing

sequenceDiagram
  autonumber
  participant A as Author
  participant AV2 as authoring-v2
  participant UA as users-auth
  participant ML as micro-learning
  A->>AV2: publish-module tool
  AV2->>UA: debit credits
  AV2->>ML: POST /v1/internal/modules/ingest
  ML-->>AV2: module id + version
  Note over ML: module visible to learners
  ML-->>ML: tts.audio.requested per screen
  1. The author runs the publish-module tool. authoring-v2 validates the draft, then calls micro-learning's ingest endpoint — authoring-service-v2/app/core/http.py:56.
  2. micro-learning creates the module, its sections, lessons, screens and blocks, and returns the new identifiers. Publishing is versioned: republishing produces a new version rather than mutating the live one, and learners mid-module are not disturbed.
  3. Unpublish and delete follow the same shape, addressed by root id rather than module id — http.py:197, :217, :235 — because a module's identity survives across versions.
  4. Credits are debited at publish as well as at run start; see credits & billing.

Translation

Translation is retrofitted onto an already-published module rather than being part of the first publish. authoring-v2 publishes authoring.content.translate.requested (app/services/publish_service.py:376 and app/tools/handlers/translate_handlers.py:97); content-worker consumes it, translates the content, and writes the result back to micro-learning directly over HTTP rather than routing it through authoring-v2 — authoring-content-worker/app/clients/ml_client.py. It is the one place a pipeline worker talks to a service other than authoring-v2 and asset-manager.

Narration audio

sequenceDiagram
  autonumber
  participant ML as micro-learning
  participant TTS as tts worker
  participant MOD as Modal chatterbox
  participant AM as asset-manager
  ML-->>TTS: tts.audio.requested
  TTS->>MOD: synthesise speech
  MOD-->>TTS: audio
  TTS->>AM: upload file
  TTS->>ML: write lesson_screen_audio row
  Note over TTS,ML: same database, no completion event

Two things ask for audio: publishing a module can pre-warm narration for every screen (micro-learning-service-v2/app/services/screen_audio_service.py:108), and a reconciler sweeps for screens whose audio is missing or stale (app/services/audio_reconciler_scheduler.py:127). Both publish tts.audio.requested.

The TTS worker is a separate Deployment with a separate image — it carries ffmpeg and a torch-sized dependency set — but it is not a separate service: its code lives in micro-learning-service-v2/app/tts_worker/ and it writes straight into micro-learning's lesson_screen_audio table with SELECT … FOR UPDATE SKIP LOCKED. There is no completion event; the row changing state is the completion signal. That is the tightest coupling on the platform and the reason the two were merged into one codebase.

Where it breaks today

IssueDetail
Publishing is a blocking cross-service callIf micro-learning is unavailable, publish fails in the author's face. There is no retry queue. Deliberate — a half-published module would be worse — but worth knowing.
Audio has no failure signalBecause completion is a database row rather than an event, a synthesis failure surfaces only through the reconciler's next sweep. See micro-learning async contracts.
Three audio operations are unreachable/v1/internal/audio/reconcile, /retry-failed and /prewarm/{module_id} have no in-repo caller and no ingress rule — the work runs on in-process timers instead. See the micro-learning verdicts.
Publish events are published but unreadtraining.module.published is mapped by feed-service, but feed's event processor is never started, so nothing consumes it. See feed-service.