PSC Coach is a full-stack Mandarin speaking practice system. The project is not a single web app binary: it is a static frontend, a FastAPI backend, an async worker, a PostgreSQL database, a Redis-backed queue, and an S3-compatible object store, plus a small but important set of repo-local source files used at startup to seed content.
This README is deployment-first. It explains what has to exist for the full system to boot, what can run locally with Docker Compose today, and what needs to be packaged for a real production deployment.
Use this section if you want a clean, reviewer-friendly setup from zero.
- Docker Desktop with Compose v2
- Python 3.11
ffmpeg(needed by worker audio normalization)- Optional: Conda (if you prefer
environment.yml)
From the repo root:
cp .env.example .envThen review at least:
OBJECT_STORE_PUBLIC_ENDPOINT(for local Docker, keephttp://localhost:9000)STT_PROVIDER(autofor local bootstrap, real provider for production)REFERENCE_AUDIO_MODEandREFERENCE_AUDIO_LOCAL_ROOT
This project requires local runtime source data under ALL-SRC/ and ALL-SRC-EXTRACTED/. A code-only checkout is not enough.
Minimum required files:
ALL-SRC/99_indices_and_design/cloud_inclusion_manifest.csv
ALL-SRC/00_cloud_core_content/stage30_s3_usage_and_oral_builder/s3_task_spec_from_briefing.csv
ALL-SRC/00_cloud_core_content/stage40_s4_reading_fluency_bank/2026_reading_passages_50/普通话测试50篇朗读范文短文.docx
ALL-SRC/00_cloud_core_content/stage50_s5_topic_expression_bank/2026_topics_100_300/命题说话题目汇总.docx
ALL-SRC-EXTRACTED/stage10/parser/stage10_paper_section_segmentation.csv
ALL-SRC-EXTRACTED/stage20/cleaned/polyphone_hand_cleaned.csv
Without these files, API startup/import-time seeding can fail.
make up
make up-app
make healthOpen the UI:
http://localhost:8512
Run end-to-end smoke test:
python3 tools/smoke_user_flow.pymake up
python3 -m pip install -r requirements-dev.txt
uvicorn apps.api.app.main:app --reload --port 8000
python -m worker.main
cd apps/web && python3 -m http.server 4173In this mode, web UI is at http://localhost:4173.
- Commit code/config/docs only.
- Do not commit large media archives from
ALL-SRC/orALL-SRC-EXTRACTED/; they are gitignored by design. - For deployment, package only the minimum runtime data subset (Section 5), plus optional reference audio if needed.
At runtime the project is split into these pieces:
Browser
-> static frontend from apps/web
-> calls FastAPI
-> uploads audio directly to S3-compatible object storage using presigned URLs
FastAPI API (apps/api/app/main.py)
-> initializes schema + runs migrations + seeds runtime content at startup
-> reads/writes PostgreSQL
-> enqueues analysis jobs into Redis
-> generates presigned object-store URLs
Worker (worker/main.py)
-> consumes Redis queue
-> downloads uploaded audio from object storage
-> normalizes audio with ffmpeg
-> runs STT/scoring pipeline
-> writes review results back to PostgreSQL
PostgreSQL
-> source of truth for content, attempts, reviews, user progress
Redis
-> job queue, retry queue, DLQ tracking
S3-compatible object store
-> uploaded attempt audio
-> optional reference audio if you choose object-store mode
Repo-local runtime source bundle
-> ALL-SRC/
-> ALL-SRC-EXTRACTED/
-> required for import-time manifest loading and startup seeding
These are the constraints that matter before you pick a host.
apps/webis plain HTML/CSS/JS.- There is no Node build step in this repo.
- Routes are hash-based (
#/home,#/path, and so on), so a static host does not need SPA rewrite rules for the current route model.
- The API is a long-running FastAPI service.
- The worker is a separate long-running process.
- The worker depends on Redis, object storage, and ffmpeg.
- A frontend-only host is not enough for the full stack.
The code imports and seeds against files under ALL-SRC/ and ALL-SRC-EXTRACTED/.
Two deployment-critical consequences:
apps/api/app/source_registry.pyreadsALL-SRC/99_indices_and_design/cloud_inclusion_manifest.csvduring import. If that file is missing, the API can fail before the app is usable.db.init_db()callsseed_runtime_content()on startup, and that seeding logic reads staged source documents and extracted CSV artifacts from the repo tree.
Because both ALL-SRC/ and ALL-SRC-EXTRACTED/ are gitignored, a code-only deploy from Git is incomplete unless you explicitly package the runtime data subset too.
During database initialization the API also generates coverage/demo artifacts under:
artifacts/coverage/artifacts/demo/
That means the API container/file system cannot be strictly read-only in its current form unless you change the code or mount writable storage for those paths.
GET /healthz returns {"status": "ok"}. It confirms the API process is up, but it does not continuously probe PostgreSQL, Redis, or object storage. For real validation you should also hit content/home endpoints and run the smoke workflow described later in this document.
Only part of the repo matters for deployment:
apps/
api/
app/
sql/
web/
worker/
shared/
infra/docker/Dockerfile.dev
docker-compose.yml
Makefile
requirements.txt
requirements-dev.txt
.env.example
tools/smoke_user_flow.py
ALL-SRC/ # gitignored, but needed at runtime
ALL-SRC-EXTRACTED/ # gitignored, but needed at runtime
Useful service-specific docs already in the repo:
apps/api/README.mdworker/README.mddeploy.md
For full-project deployment, this README should be the starting point.
- Python 3.11
- PostgreSQL deployment (local Compose uses Postgres 16)
- Redis deployment (local Compose uses Redis 7)
- S3-compatible object storage
ffmpegavailable in the API/worker runtime image
- One API process:
uvicorn apps.api.app.main:app --host 0.0.0.0 --port 8000 - One worker process:
python -m worker.main - One static web host serving
apps/web/
At minimum, allow writes to:
artifacts/coverage/artifacts/demo/- normal temp space used by Python/ffmpeg, typically
/tmp
- local S4 reference audio directory for first release
- generated S1 reference audio directory
- local faster-whisper model cache if using a local STT provider
- Vosk model directory if using
STT_PROVIDER=vosk_local
If you want the full project to seed and run correctly, package the following files/directories with the deployable artifact.
These are the important minimum paths to include:
ALL-SRC/99_indices_and_design/cloud_inclusion_manifest.csv
ALL-SRC/00_cloud_core_content/stage30_s3_usage_and_oral_builder/README.md
ALL-SRC/00_cloud_core_content/stage30_s3_usage_and_oral_builder/s3_task_spec_from_briefing.csv
ALL-SRC/00_cloud_core_content/stage40_s4_reading_fluency_bank/2026_reading_passages_50/普通话测试50篇朗读范文短文.docx
ALL-SRC/00_cloud_core_content/stage50_s5_topic_expression_bank/2026_topics_100_300/命题说话题目汇总.docx
ALL-SRC/01_cloud_optional_reference/stage20_s1_s2_supporting_docs/other_word_materials/普通话水平测试字表.doc
ALL-SRC/01_cloud_optional_reference/stage20_s1_s2_supporting_docs/other_word_materials/普通话常用平翘舌音字表.doc
ALL-SRC/01_cloud_optional_reference/stage20_s1_s2_supporting_docs/other_word_materials/普通话水平测试难点音字表.doc
ALL-SRC/01_cloud_optional_reference/stage20_s1_s2_supporting_docs/other_word_materials/普通话音变(轻声、儿化).doc
ALL-SRC/01_cloud_optional_reference/stage20_s1_s2_supporting_docs/other_word_materials/普通话音调(阴、阳、上、去).doc
ALL-SRC/01_cloud_optional_reference/stage20_phonetics_docs_for_tagging/psc_improvement_guides_and_audio/普通话水平考试常用儿化音表.docx
ALL-SRC/01_cloud_optional_reference/stage20_phonetics_docs_for_tagging/psc_improvement_guides_and_audio/普通话考试易错字词.doc
ALL-SRC-EXTRACTED/stage10/parser/stage10_paper_section_segmentation.csv
ALL-SRC-EXTRACTED/stage20/cleaned/polyphone_hand_cleaned.csv
If these files are missing, expect one or more of the following:
- import-time failure in source registry
- empty or incomplete seeded content
- broken guidebook/content metadata
- startup errors during DB initialization
Include these too if you want reference audio to work immediately without separately uploading media into object storage:
ALL-SRC/01_cloud_optional_reference/stage40_s4_reference_audio_sample/reading_work_50_audio/
ALL-SRC-EXTRACTED/stage20/support/s1_reference_audio_generated/
For a first release, REFERENCE_AUDIO_MODE=local is the simplest option because it avoids pre-populating object storage with reference media.
Use the top-level .env.example as the main template for full-stack deployment. Do not treat infra/.env.example as the canonical full-project env file; the top-level template matches the current app config and compose setup.
| Variable | Required | Purpose |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL DSN used by API and worker |
REDIS_URL |
Yes | Redis DSN used for queue operations |
OBJECT_STORE_ENDPOINT |
Yes | Internal S3-compatible endpoint reachable by API and worker |
OBJECT_STORE_PUBLIC_ENDPOINT |
Usually | Browser-reachable S3-compatible endpoint for presigned upload/download URLs |
OBJECT_STORE_BUCKET |
Yes | Bucket name for uploaded audio and optional reference media |
OBJECT_STORE_ACCESS_KEY |
Yes | S3 credential |
OBJECT_STORE_SECRET_KEY |
Yes | S3 credential |
OBJECT_STORE_REGION |
No | Defaults to us-east-1 |
UPLOAD_URL_EXPIRY_SECONDS |
No | Presigned upload TTL; defaults to 900 |
CORS_ALLOW_ORIGINS |
Yes in production | Allowed browser origins; do not leave * in production |
| Variable | Required | Purpose |
|---|---|---|
QUEUE_NAME |
No | Redis list for new jobs |
QUEUE_RETRY_ZSET |
No | Retry schedule set |
QUEUE_DLQ_NAME |
No | Dead-letter queue |
QUEUE_RETRY_COUNT_HASH |
No | Retry counter hash |
WORKER_MAX_RETRIES |
No | Defaults to 3 |
WORKER_RETRY_BACKOFF_SECONDS |
No | Defaults to 3 |
| Variable | Required | Purpose |
|---|---|---|
REFERENCE_AUDIO_MODE |
No | auto, local, object_store, or none |
REFERENCE_AUDIO_LOCAL_ROOT |
Required for local mode |
Root directory for local reference audio |
REFERENCE_AUDIO_URL_EXPIRY_SECONDS |
No | Signed GET URL expiry for reference audio |
Recommended first-release setting:
REFERENCE_AUDIO_MODE=local
REFERENCE_AUDIO_LOCAL_ROOT=/workspace/ALL-SRC/01_cloud_optional_reference/stage40_s4_reference_audio_sample/reading_work_50_audioSTT_PROVIDER currently supports these values:
autofaster_whisper_locallocalvosk_localiflytek_iatiflytek_rtasriflytek_isenonemock
Important related variables:
LOCAL_STT_MODELLOCAL_STT_DEVICELOCAL_STT_COMPUTE_TYPELOCAL_STT_MODEL_DIRVOSK_MODEL_PATHSTT_LANGUAGESTT_TIMEOUT_SECONDS- all
IFLYTEK_*credentials/settings shown in.env.example
Production guidance:
- use a real provider (
iflytek_*, local whisper, or Vosk) - set
STT_ALLOW_MOCK_FALLBACK=false - keep
mockfor tests/demo only
These should stay off in production:
STT_ALLOW_MOCK_FALLBACK=false
PSC_DEMO_DIRECT_ANALYZE=false
PSC_DEMO_FORCE_FALLBACK_REVIEW=falseThis is the fastest way to boot the entire project on one machine using the files already in the repo.
docker-compose.yml provides:
postgresredisminiominio-initto create the bucketapiworkerwebdevtesttest-smoke
The app services use the existing infra/docker/Dockerfile.dev image.
From the repo root:
cp .env.example .envThen review at least these values:
OBJECT_STORE_PUBLIC_ENDPOINTSTT_PROVIDERREFERENCE_AUDIO_MODEREFERENCE_AUDIO_LOCAL_ROOT- any
IFLYTEK_*credentials if using iFLYTEK
make upThis starts the core dependencies defined without profiles:
- PostgreSQL
- Redis
- MinIO
- MinIO bucket initialization
make up-appor:
make demoThat starts:
- API on
http://localhost:8000 - worker in the background
- static frontend on
http://localhost:8512
Default local ports:
| Service | URL / Port |
|---|---|
| Frontend UI | http://localhost:8512 |
| API | http://localhost:8000 |
| PostgreSQL | localhost:5432 |
| Redis | localhost:6379 |
| MinIO API | http://localhost:9000 |
| MinIO Console | http://localhost:9001 |
Check health and smoke flow:
make health
python3 tools/smoke_user_flow.pyOr use the make target:
make smoke-workflowIf the smoke script passes, you have verified:
- API boot
- content/home endpoints
- attempt creation
- presigned upload URL generation
- audio upload
- queue submission
- worker processing
- review payload generation
make ps
make logs
make logs-app
make test-docker
make qa
make downThis is useful if you want Docker only for infra but run Python directly on the host.
make upUse either pip or conda:
python3 -m pip install -r requirements-dev.txtor:
make conda-env
conda activate psc-coachAPI:
uvicorn apps.api.app.main:app --reload --port 8000Worker:
python -m worker.mainWeb:
cd apps/web
python3 -m http.server 4173In this mode the browser-facing UI will be at http://localhost:4173 unless you proxy it.
The project deploys cleanly if you think in terms of three runtime workloads and three managed dependencies:
- Static frontend serving
apps/web - API container running
uvicorn apps.api.app.main:app --host 0.0.0.0 --port $PORT - Worker container running
python -m worker.main
- PostgreSQL
- Redis or Valkey-compatible Redis API
- S3-compatible object storage
- package the runtime data subset from Section 5 into the image/bundle
- do not rely on a raw Git clone alone
- keep API and worker on the same env set and the same code/data bundle
- make object storage reachable both internally and from the browser
- serve the web app separately as static assets
The repo currently ships infra/docker/Dockerfile.dev, not a dedicated production Dockerfile. You can still use it for a first working deploy if you accept a larger-than-ideal image.
From the repo root or from a trimmed deployment bundle:
docker build -f infra/docker/Dockerfile.dev -t psc-coach:latest .Make sure the build context already contains:
- runtime code (
apps/,worker/,shared/) - SQL files
.envor injected environment at runtime- the required
ALL-SRC/andALL-SRC-EXTRACTED/subset
Example:
docker run --rm \
--name psc-api \
-p 8000:8000 \
--env-file .env \
psc-coach:latest \
uvicorn apps.api.app.main:app --host 0.0.0.0 --port 8000Example:
docker run --rm \
--name psc-worker \
--env-file .env \
psc-coach:latest \
python -m worker.mainThe API and worker must both be able to reach the same:
- PostgreSQL database
- Redis instance
- object storage bucket
- packaged runtime source bundle from Section 5
Also keep the runtime filesystem writable for artifacts/coverage and artifacts/demo, or mount writable storage for those paths.
Any static host can serve apps/web/. For a simple containerized variant:
docker run --rm \
--name psc-web \
-p 8512:4173 \
-v "$PWD/apps/web:/workspace/apps/web:ro" \
-w /workspace/apps/web \
python:3.11-slim \
python3 -m http.server 4173 --bind 0.0.0.0For a real production deployment, a CDN/static-site host is a better fit than leaving the frontend on python -m http.server.
There is no frontend build pipeline in this repo right now. Deploy the contents of apps/web/ as static files.
Because the app uses hash routes, the current frontend does not require SPA rewrite rules for route resolution.
apps/web/js/api.js looks for window.PSC_API_BASE first. If your frontend and API are on different origins, set that explicitly before loading app.js.
Example:
<script>
window.PSC_API_BASE = "https://api.example.com";
</script>
<script type="module" src="./js/app.js"></script>Without that override, the frontend probes:
- same-origin
- same host on port
8000 http://localhost:8000http://127.0.0.1:8000
That works for local development, but not for most split production deployments.
The object store is not optional for the full workflow.
It is used for:
- audio upload URLs returned by
POST /v1/attempts/{attempt_id}/upload-url - worker-side audio download and processing
- optional reference-audio download URLs in object-store mode
Use both endpoints correctly:
OBJECT_STORE_ENDPOINT: internal endpoint reachable from API and workerOBJECT_STORE_PUBLIC_ENDPOINT: endpoint reachable from the browser for presigned URLs
Local Docker example:
- internal:
http://minio:9000 - public:
http://localhost:9000
If OBJECT_STORE_PUBLIC_ENDPOINT is wrong, uploads can fail in the browser even though the API itself can talk to the object store.
Local Compose creates the bucket automatically through minio-init.
In production you must ensure the bucket already exists or provision it before the app starts.
On API startup the app does all of the following:
- reads base schema SQL
- applies migrations from
apps/api/sql/migrations/ - seeds static SQL content
- seeds runtime content from repo-local source files
- generates coverage/demo artifacts
- upserts the default user
Operational consequences:
- the first boot can take longer than a simple stateless service
- DB credentials must be valid before the API can become ready
- the runtime source bundle must already be present at startup
- the API needs a writable filesystem for artifact generation
The code uses advisory locks and a mutex around initialization, which helps when more than one process starts at once, but you should still treat first boot as a stateful initialization phase rather than a purely disposable stateless startup.
For a straightforward first release, these settings are the safest defaults:
CORS_ALLOW_ORIGINS=https://your-frontend-origin.example
REFERENCE_AUDIO_MODE=local
REFERENCE_AUDIO_LOCAL_ROOT=/workspace/ALL-SRC/01_cloud_optional_reference/stage40_s4_reference_audio_sample/reading_work_50_audio
STT_PROVIDER=iflytek_iat
STT_ALLOW_MOCK_FALLBACK=false
PSC_DEMO_DIRECT_ANALYZE=false
PSC_DEMO_FORCE_FALLBACK_REVIEW=false
WORKER_MAX_RETRIES=3
WORKER_RETRY_BACKOFF_SECONDS=3If you are not ready to use a live STT provider yet:
STT_PROVIDER=autoornonewill still let the system run- review quality becomes more fallback-driven and less production-grade
Run these checks in order after every deployment.
curl -sSf https://api.example.com/healthz
curl -sSf "https://api.example.com/v1/home?section=S1&mode=auto&pattern=psc&demo_mode=true"
curl -sSf "https://api.example.com/v1/path?section=S1"
curl -sSf "https://api.example.com/v1/content?limit=1&demo_mode=true"Run the built-in smoke script against the deployed API:
API_BASE_URL=https://api.example.com python3 tools/smoke_user_flow.pyThis is the most useful deploy validation currently in the repo.
Confirm all of the following:
- the frontend boots without API bootstrap errors
- the home screen loads real data
- a recording can be created and uploaded
- attempt status progresses from
createdtoqueuedtoprocessingtodone - the review screen renders a completed payload
Cause:
- required
ALL-SRCorALL-SRC-EXTRACTEDfiles were not packaged
Fix:
- include the minimum runtime data bundle from Section 5
Cause:
OBJECT_STORE_PUBLIC_ENDPOINTpoints to an internal hostname such asminio:9000
Fix:
- set
OBJECT_STORE_PUBLIC_ENDPOINTto a browser-reachable URL
Cause:
- the frontend could not discover the API origin
- or one of the boot probe endpoints failed
Fix:
- explicitly set
window.PSC_API_BASE - verify
/healthz,/v1/home,/v1/path, and/v1/content
Cause:
- worker is not running
- API and worker are pointed at different Redis instances or queue names
Fix:
- confirm
python -m worker.mainis running - confirm
REDIS_URL,QUEUE_NAME, and retry/DLQ vars match between API and worker
Common causes:
- object store credentials wrong
- uploaded object not reachable by worker
ffmpegmissing- STT provider unavailable or misconfigured
Fix:
- inspect worker logs first
- verify bucket/object existence
- verify
ffmpegis installed in the runtime image - verify STT settings and credentials
Cause:
REFERENCE_AUDIO_MODEresolved tonone- local reference directory is absent
- object-store mode is enabled but reference files were never uploaded
Fix:
- use
REFERENCE_AUDIO_MODE=localfor first release - ensure
REFERENCE_AUDIO_LOCAL_ROOTexists in the container
Cause:
- startup artifact generation writes under
artifacts/coverageandartifacts/demo
Fix:
- allow writes there
- or patch the application before attempting read-only container execution
Before calling the deployment production-ready, verify this list:
- set a real
CORS_ALLOW_ORIGINSvalue - keep all demo toggles off
- keep
STT_ALLOW_MOCK_FALLBACK=false - use managed secrets, not committed
.envfiles - ensure PostgreSQL, Redis, and object-store data are persistent
- terminate TLS in front of the API and static frontend
- monitor worker failures and DLQ growth
- back up PostgreSQL and object storage
- package only the required runtime source subset, not the full local archive unless needed
If you only need the shortest route to a working full-stack deployment:
- Copy
.env.exampleto.env. - Ensure the required
ALL-SRCandALL-SRC-EXTRACTEDruntime subset exists locally. - Run
make up. - Run
make up-app. - Open
http://localhost:8512. - Run
python3 tools/smoke_user_flow.py.
If you need a real production deployment:
- Package the runtime source subset into your deployable bundle/image.
- Provision PostgreSQL, Redis, and S3-compatible storage.
- Run one API container and one worker container with the same env settings.
- Serve
apps/web/as a static site. - Set
window.PSC_API_BASEif the frontend and API are on different origins. - Run the smoke script against the deployed API.
apps/api/README.mdworker/README.mddeploy.md