Conversation
…t-ID A user reported Spanish audio coming back as English. The report predates the Deepgram cutover (#38); the current auto path (detect_language=true) already keeps the spoken language, and Deepgram's `multi` mode was rejected because it wrecks Korean and Chinese (ADR-004 has the probe table). So this change adds the proof and hardening rather than a provider change: - backend/tests/fixtures/audio/: short synthetic clips (macOS voices) for es-MX, es-ES, en-US, ko, zh and an English-then-Spanish mix. - tests/test_language_detection.py: recorded-response tests pinning that auto asks for detection and stores whatever Deepgram detects, for every offered language; Spanish text is stored verbatim. - tests/live/: real fixtures through real Deepgram, `pytest -m live` (excluded from the default run via pytest.ini). - frontend/e2e/tests/multilingual-upload.spec.js: uploads the Spanish and the mixed clip through the real UI and asserts Spanish text plus a Spanish badge (opt-in with CLIO_LIVE_E2E=1). - NoteDetailPage: the language badge now shows "Spanish" instead of "es", carries a lang attribute and a test id; the option list is hoisted so the badge and the re-transcribe dialog share it. Found while running the browser proof: the frontend sends X-Request-ID on note creation (#41) but CORS_ALLOW_HEADERS did not list it, so on any cross-origin deployment (dev stack, CI e2e) the browser dropped the POST after preflight. Added the header and tests/test_cors_preflight.py. Production is same-origin and was not affected.
…language Review finding: the badge text is an English label (Spanish), so lang="es" declared the wrong language for that content. The detected code is now exposed as data-language for tests and tooling; the e2e assertion follows.
Prove spoken-language transcription end to end; fix CORS for X-Request-ID
The CI workflow pinned Python 3.9 while the backend ships on 3.12 (backend/Dockerfile). Test collection died on PEP 604 syntax, so pytest ran nothing and reported 31% coverage against a 60% floor; mypy never reached apps/ at all. Every backend job now runs 3.12, the runtime the code targets. Backend: - mypy.ini python_version 3.9 -> 3.12. At 3.9 mypy aborts inside site-packages (anyio uses match statements) before checking a single project file. - backend-typecheck gains DEBUG/SECRET_KEY. The django-stubs plugin imports config.settings, which refuses to load without them; the job failed with ImproperlyConfigured before mypy produced any output. - FolderSerializer installs its user-scoped `parent` field through get_fields() instead of a class attribute. DRF's own Field.parent owns that name on the base class, so the declarative form reads as an override of an unrelated attribute. Same field map, same IDOR scoping. - VoiceNoteListSerializer.folder gets the element-type annotation PrimaryKeyRelatedField needs when it is read-only. Frontend: `npm run build` under CI=true treats eslint warnings as errors, and there were 17 of them (npm run lint saw only 12 -- its glob is not recursive). All fixed in the source, none suppressed: - Dropped imports and bindings nothing read: DocumentTextIcon, MicrophoneIcon (x2), ExclamationTriangleIcon, PerformanceIndicator and its never-rendered showPerformanceIndicator prop, getPerformanceStatusColor, and four leftovers from the disabled performance manager in useAudioRecorder. - RegisterPage's two href="#" anchors became plain text. The app has no /terms or /privacy route, and LoginPage already states the same notice as text. - speech.d.ts declares SpeechRecognitionConstructor instead of redeclaring the SpeechRecognition name as both interface and var. - PerformanceManager reads its CPU-benchmark accumulator so the timed loop cannot be optimised away, and drops an unused getCurrentTier() call. - startRecording's dependency array is now accurate: state.recordingTime and updateAudioLevel move behind refs. Adding recordingTime directly would rebuild the callback every second, and updateAudioLevel is declared below it. The ref also fixes the diagnostic that always logged a duration of 0. - MicrophonePermission's mount probe declares its dependency; the callback stays stable via a ref because RecordPage passes a fresh handler identity each render, which would otherwise loop. Triggers now include dev on push and pull_request, so PRs into dev get checks. playwright.config.js ignores production-sweep.spec.js everywhere. It drives https://clio.chadacus.dev and registers real accounts there; the e2e job would have run it against production.
…der boundary New apps.translations module: NoteTranslation rows per (note, target language), a TranslationProvider protocol with a Claude implementation using structured output, a Celery task that validates the provider output against the unit contract before storing text and per-segment translations, and a REST adapter at /api/notes/<id>/translations/ (GET reports whether the server can translate; POST dispatches or returns the stored row). The transcript is never modified. Frontend: TranslationPanel on the note page with disabled, empty, pending, failed, and completed states, including translated speaker turns aligned by segment id; shared transcript helpers moved to utils/transcript.ts. Governance: capability contract, deterministic eval suite (marker ai_eval, fails closed without ANTHROPIC_API_KEY), ADR-005, .claude/verification.json, README env and testing notes. Compose files pass the new env through.
comprehensive-harness.spec.js expected an h1 of "Record Voice Note". No such
string exists in frontend/src -- RecordPage.tsx renders "New Voice Note"
(line 168) -- so the assertion failed against a correct page. The two sibling
assertions in the same block ("Ready to record" from RecorderControls,
"Recording Tips" from RecordPage) do match the source and are unchanged.
production-sweep.spec.js carries the same stale assertion but is excluded from
automated runs, so it is left alone.
The e2e suite's registration and login tests share the anon bucket (60/minute) with the health check, so the health spec failed with 429 once the whole suite ran in one worker. A liveness probe must never be throttled; uptime checks hit it too. Adds a test that pins the exemption.
conftest disables throttling for the suite, so the first version passed with or without the exemption. Restore the production throttle on the base view for this test, tighten the anon rate, and prove with a control request that the throttle bites before asserting the probe stays 200.
… volume mount - Plumb THROTTLE_ANON_RATE through docker-compose.yml and raise it for the CI e2e job only; one runner address sends the suite's anonymous traffic inside the production 60/minute window. Production default unchanged. - celery now waits for the backend to be healthy: both images carry /app/media and both mounted the empty media_files volume at once, which failed with 'mkdir ...: file exists' on the second CI run. - playwright.config.js comment now says what testIgnore really does. - Folder update path gets the IDOR regression test review asked for.
…the provider boundary - Re-transcription and a manual transcript edit now call the translations module's public invalidate_translations_for_note; stale rows were shown under a fresh header and their per-turn view broke on the new segment ids. - The Claude provider uses messages.create with a JSON-schema output_config and validates the text itself, so truncation (stop_reason max_tokens) and malformed output become controlled failures instead of a pydantic exception escaping the boundary; failure reasons never carry model output. - Retry policy: transient provider failures (rate limit, 5xx, connection) are retried with the row left pending; refusals, truncation, schema and contract violations, and configuration errors fail fast. Unexpected exceptions are logged by type only. - Compose passes CLIO_TRANSLATION_EFFORT and CLIO_TRANSLATION_TIMEOUT_SECONDS. - TranslationPanel renders a load-error state with Retry instead of vanishing. - Tests: invalidation on retranscribe/edit, segments untouched, retry policy, provider boundary with a faked SDK client; Playwright load-error case. - Contract and ADR updated with invalidation and retry policy.
The response interceptor retried every 401 through /auth/refresh/, including the refresh call's own 401, so a visitor with no session looped profile -> refresh -> refresh ... and never left the loading state. Locally the anon throttle (60/minute) turned the loop into a 429 within a second, which is why the redirect-to-login tests passed; the raised CI throttle exposed it. The refresh request is now excluded from the retry path, so its 401 rejects and the existing redirect runs.
ci: run the pipeline on 3.12, fix the gates it was hiding, and cover dev
…schema definition - Task writes are conditional UPDATEs on the row pk; zero rows means the note was re-transcribed or edited while the provider call was in flight and the result (or failure) is discarded. A plain save() re-inserted the stale row. - OUTPUT_SCHEMA is derived from _TranslationOut (refs inlined, titles dropped, objects closed) and a test binds it to the SDK's own transform of the model. - Unexpected task exceptions are logged by type without a traceback, and the test observes the real log records through caplog's handler.
QuerySet.update() bypasses auto_now, so the row's updated_at froze at creation; the panel orders translations by it and it is provenance for the artifact.
Translate a note's transcript into a chosen language (Claude provider, stored beside the transcript)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Release: spoken-language transcription, CI repair, transcript translation
dev→main. Human gate #1: this merge needs Chad's click. The Linode deploy (/root/clio,docker-compose.prod.yml) follows this merge.What
devcarries beyondmaindetect_language); CORS fix forX-Request-IDdev; health probe exempt from the anonymous throttle; e2e stack deterministic; 401 refresh loop fix; folder PATCH IDOR testapps.translations) behind a Claude provider boundary; invalidation on re-transcription and transcript edit; capability contract, eval suite, ADR-00564 files changed, +2605 / −112.
Production impact
ANTHROPIC_API_KEYis set for thebackendandcelerycontainers. Without the key the endpoint answers 503 and the UI hides the control.translations.0001_initial(runs on container start).pytest evals/translation -m ai_evalpasses with a live key.Deploy steps after merge