Skip to content

About

eval English speech

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

OScanner-Eng

A local web app for generating English speaking questions from a candidate profile, recording the answer with the browser camera, and saving the video plus metadata on the server.

Run

npm install
npm start

Open http://localhost:3199.

Configuration

Copy .env.example to .env, then fill in the required credentials. Use .env.local for local overrides and .env.prod for production overrides; these three environment files are gitignored, while .env.example is safe to commit.

The app reads the internal OpenAI-compatible model gateway settings from .env:

INTERNAL_LLM_API_KEY=...
INTERNAL_LLM_CHAT_COMPLETIONS_URL=https://api.example.com/v1/chat/completions
INTERNAL_LLM_TRANSCRIPTIONS_URL=https://api.example.com/v1/audio/transcriptions
INTERNAL_LLM_QUESTION_MODEL=glm
INTERNAL_LLM_TRANSCRIBE_MODEL=qwen-asr
INTERNAL_LLM_EVAL_MODEL=qwen
EVAL_MAX_FRAMES=24
EVAL_REQUEST_TIMEOUT_MS=100000

# OpenRouter fallback for question generation, scoring, comment moderation,
# and ASR transcription when the internal gateway is unavailable. The
# transcription fallback uses OpenRouter's POST /api/v1/audio/transcriptions
# endpoint with a Qwen3 ASR model (same language coverage as qwen-asr).
OPENROUTER_API_KEY=...
OPENROUTER_CHAT_COMPLETIONS_URL=https://openrouter.ai/api/v1/chat/completions
OPENROUTER_EVAL_MODEL=z-ai/glm-5.3-flash
OPENROUTER_TRANSCRIBE_MODEL=qwen/qwen3-asr-1.7b

Everyone can generate questions, save recordings, and view their own history as a guest. DingTalk login is optional; configure it with:

DINGTALK_UNIFIED_APP_ID=...
DINGTALK_CORP_ID=...
DINGTALK_CLIENT_ID=...
DINGTALK_CLIENT_SECRET=...
APP_BASE_URL=http://localhost:3199
SESSION_SECRET=...
COOKIE_SECURE=false
PARTNER_API_KEY=replace-with-a-long-random-secret

The DingTalk app callback URL must match APP_BASE_URL plus /auth/dingtalk/callback. DINGTALK_CORP_ID enables automatic H5 passwordless sign-in when the page is opened inside DingTalk. The configured 端内免登地址 is an allowlist; the browser must still call DingTalk's requestAuthCode JSAPI and the server must exchange that one-time code. Outside DingTalk, the existing nonce-protected OAuth flow is available as an optional sign-in instead.

Guest access uses a server-issued signed HttpOnly englisheval_guest cookie with a renewable 180-day lifetime. Set SESSION_SECRET even without DingTalk configuration (the existing DINGTALK_CLIENT_SECRET fallback remains supported). No IP hash or localStorage credential is used. Cookie loss or expiry creates a new guest identity. GET /api/me returns the effective user and identityType (guest or dingtalk). Guest openId values are synthetic guest:<UUID> ownership keys. DingTalk sessions take priority; signing out restores the browser's separate guest history. Records, consent, and pending jobs are never merged. Guest videos are private and excluded from partner exports; weekly scores use generated aliases. All learners share the same queue limits. Clearing cookies can create another identity; global queue and worker limits remain the resource protection.

Learner API mutations require X-Expected-Owner matching /api/me's user.openId. Missing or mismatched values return 409 with code: IDENTITY_CHANGED before upload processing. Reads also reject a mismatched header when provided. Refresh the session and reset identity-bound UI before retrying; never replay a previous owner's upload under the new identity. Admin routes still require DingTalk and the admin token; partner routes still require their bearer token. In production, APP_BASE_URL must use HTTPS and COOKIE_SECURE must be true. Local HTTP development may set COOKIE_SECURE=false.

Recording protection and production maintenance use:

RECORDING_RETENTION_DAYS=0
BACKUP_RETENTION_DAYS=30
BACKUP_AGE_RECIPIENT=<public-age-recipient>
RECORDING_BACKUP_ENABLED=true

RECORDING_RETENTION_DAYS=0 disables live-recording expiration, so videos and their extracted evaluation artifacts are preserved indefinitely. Encrypted backup snapshots expire independently according to BACKUP_RETENTION_DAYS (30 days by default).

RECORDING_BACKUP_ENABLED=false turns off the encrypted backup and generates the maintenance unit without its service stop/start steps: the nightly timer then runs only recording retention and the orphan sweep with the application and worker fully up, and deployment becomes the only source of restarts. The age command and BACKUP_AGE_RECIPIENT are required only while the backup is enabled; switching either way takes effect on the next ./deploy.sh, which regenerates the unit.

The browser never receives API keys. Question generation goes through POST /api/generate-question.

Core authentication, question, and answer workflow

This workflow is a core application contract and should be preserved when the authentication, question-generation, recording, or evaluation code changes.

  1. The browser checks GET /api/me. Inside DingTalk it automatically requests a one-time H5 auth code and posts it to POST /auth/dingtalk/in-app. In a regular browser, visitors continue with a signed guest cookie and may choose GET /auth/dingtalk with a safe local redirect path. In-app sign-in is bounded and falls back to guest access on failure.
  2. The server creates a nonce-protected OAuth state and redirects to DingTalk. At /auth/dingtalk/callback, it exchanges the authorization code, obtains the user's openId, optionally enriches the session with organization fields (userId, jobNumber, email, and orgEmail), and sets a signed, HTTP-only session cookie. Authentication can continue if optional organization enrichment fails, but it cannot continue without openId.
  3. Before question generation, the effective user must accept both current privacy acknowledgements. Consent is versioned and persisted by openId.
  4. The browser submits the candidate profile to POST /api/generate-question. The server replaces the submitted name with the DingTalk name or generated guest alias, asks the configured internal chat model for a structured interview-style question, and persists the generated question with its owner and profile. If model generation fails, a persisted fallback question is returned with the error.
  5. After a short preparation countdown, the browser records camera and microphone together with MediaRecorder. Recording stops when the user selects Finish and save, when the three-minute-thirty-second limit is reached, or when a required media device is interrupted. An interrupted incomplete recording is not uploaded.
  6. The browser uploads the finalized recording and its questionId to POST /api/save-answer. The server rejects a missing question or a question not owned by the authenticated user's openId. Supported browser formats are validated and normalized to MP4 before durable storage.
  7. Once the video is safely stored, the server runs the speech-evaluation workflow described below. Evaluation failure does not discard the answer: the recording and a failed evaluation status are still persisted.
  8. The recording metadata, evaluation, and owned question reference are appended to recordings/metadata.jsonl. History and video requests are always filtered by the effective user's ownership key.

In compact form:

Guest cookie or DingTalk OAuth -> effective owner -> versioned privacy consent
    -> generate and persist owned question -> prepare -> record answer
    -> verify owned question -> normalize and save video -> evaluate
    -> persist result -> show owner-filtered history

Weekly game and the private make-up page

/game always serves the current week: POST /api/game/question resolves the week from the clock (currentChallenge) and the selected class group (?group=g1|g2). The public leaderboard can display any of the last ten weeks, so a past week is answerable only through /game/first-lesson — a private make-up page pinned to group 2's lesson 1 (weekly-2026-09-28-g2, "Explaining an experimental figure"). It is deliberately absent from every menu and serves the app shell to the allow-listed session and to anonymous visitors. A visitor without a session gets a blank page and the sign-in modal — the link is pasted around directly, so a silent bounce to /leaderboard read as a broken link — while a signed-in session that is not allow-listed is still redirected to /leaderboard. The lesson itself paints only once /api/me reports catchupAccess, and every API the page calls is gated on its own.

Access follows the teacher-console contract: CATCHUP_OPEN_IDS (comma-separated openIds) is authoritative once non-empty, and until then the built-in names 张鳐 and 吴衍标 grant access while each login logs [catchup-enroll] with the openId to enroll. The challengeId parameter on GET /api/game/challenge and POST /api/game/question is rejected for every other session, and the learner's answer is stored under the pinned challenge id, which is what puts his score on the group 2 lesson 1 board.

Audio, transcript, and evaluation workflow

Qwen ASR is the transcription component; it does not assign evaluation scores. The scoring model evaluates the transcript together with derived audio metrics, the question and rubric, and sampled video frames.

Recorded video
    -> validate media; limit recorded answers to 3:30 and standalone uploads to 2:00
    -> FFmpeg extracts mono 16 kHz audio
    -> reject missing, silent, or extremely quiet audio
    -> Qwen ASR transcribes English speech to text
       (long audio is processed in ordered chunks)
    -> FFmpeg derives duration, silence, pause, word-count, and speaking-rate metrics
    -> FFmpeg samples a video frame about every five seconds, up to EVAL_MAX_FRAMES
    -> GLM-5.3-Flash receives the transcript, audio metrics, question/profile, rubric, and frames
    -> normalize and persist JSON scores, feedback, strengths, and improvements

The evaluator therefore uses different evidence for different dimensions:

  • Grammar, vocabulary, coherence, and task relevance primarily use the transcript.
  • Fluency and pacing use the transcript plus speaking-rate and pause metrics.
  • Visual delivery uses sampled video frames. It is excluded and reweighted when usable audio exists but no video picture is available.
  • Pronunciation/intelligibility is currently inferred from transcription reliability and intelligibility clues. The scoring model does not receive the original audio, so this is not a phoneme-level acoustic pronunciation test.

INTERNAL_LLM_API_KEY is required for both transcription and evaluation. Evaluation uses INTERNAL_LLM_EVAL_MODEL (default qwen) through the internal chat gateway; question generation remains on INTERNAL_LLM_QUESTION_MODEL (default glm). Qwen ASR requests explicitly use English and retry transient failures. Audio longer than the configured chunk size (30 seconds by default, capped at 40) is split and transcribed in order; known Qwen input-format failures trigger WAV/MP3 format fallback.

If the internal gateway fails during question generation, evaluation scoring, or comment moderation, the same request is retried once through OpenRouter (OPENROUTER_API_KEY required; scoring uses OPENROUTER_EVAL_MODEL, default z-ai/glm-5.3-flash, and question/moderation fall back to OPENROUTER_QUESTION_MODEL/OPENROUTER_MODERATION_MODEL when set). The evaluation result records whichever model actually produced it.

Capacity and Background Evaluation

Node.js 22.16 or newer is required. npm run dev starts the web process and evaluation worker; production uses separate englisheval and englisheval-worker systemd services. npm start starts only the web process; npm run worker starts only the worker.

The default capacity is 50 admitted sessions and 200 waiting-room positions, with one outstanding session per effective owner openId. Recording happens in the browser. Upload transfers are limited to four. Generic defaults are four processing pipelines, CPU-derived FFmpeg concurrency, and two ASR/scoring calls each. The six-CPU production profile uses eight pipelines, three FFmpeg processes, three ASR slots and four scoring slots. Shared model budgets can reduce actual request concurrency. Question generation stays at two slots and is reused within an admission. These are single-server limits; do not start additional worker instances.

Clients accept privacy terms, POST /api/admission, heartbeat every 20 seconds, and wait for state: admitted before generating a question. Idle pre-upload reservations expire after three minutes. Obtain a grant from POST /api/admission/upload-grant before uploading. Both upload endpoints now require X-Submission-Id (UUID v4), X-Upload-Grant, and, for recorded answers, X-Question-Id. They return HTTP 202 with a job ID and statusUrl; poll that URL until completed, failed, or canceled. Reusing a submission ID returns its existing job. Acknowledgement means the original upload and job are durable; media validation and MP4 conversion occur asynchronously.

SQLite lives at recordings/queue.sqlite, with pending originals under recordings/pending/. These files, WAL files, and artifacts are private persistent data and are included in encrypted recording backups. Existing JSONL records are preserved; only the web process projects worker results into them. The worker checkpoints stages and request results, renews leases, and recovers after restart. Discard creates a durable tombstone and removes media after the worker has stopped. Never delete the queue database to clear a backlog. DATA_DIR selects isolated storage for tests and staging; do not change it on the production service.

The browser retains unfinished uploads in owner-scoped IndexedDB. Successful acknowledgement or explicit discard removes that local copy. Queued evaluations survive closing the browser. History is paginated using limit (maximum 100) and offset. Wait estimates use recent measured stage durations and current pipeline occupancy; they are ranges, not completion guarantees. Cold starts show "Calculating estimate" and service outages show a delay.

Admission pauses under worker outages, model cooldowns, sustained available RAM below 512 MiB, or insufficient space for 512 MiB per reservation plus 5 GiB spare. Already admitted users retain their upload allocation. Monitor /api/health and the administrator's Evaluation capacity section. Administrators can pause new admissions there; accepted jobs continue processing. Operational telemetry records durations, retries, upstream status, and returned usage/cost fields without transcripts, credentials, or identity fields.

Deployment initially holds admissions when creating a new queue. Verify health, perform the isolated benchmark, and retain a tested queue-compatible release for rollback before running node scripts/queue-control.js resume from the production release with its original environment. pause, status, and checkpoint are also available. Roll back web and worker together; never roll back to a release without queue support once submissions have been accepted. Maintenance stops both services before copying SQLite and resumes both afterward.

Run node scripts/load-benchmark.js --users=50 --arrivals=100 --duration=120 --restart for isolated synthetic identities, real FFmpeg media processing, deterministic model responses, and forced worker recovery. --users=10 and --users=30 cover smaller bursts. --fixture=/absolute/video.mp4 --real-upstreams --users=1 measures actual configured model services using the explicit fixture; it uses temporary storage and never reads production recordings. --keep-data retains the isolated test results. Do not load-test the live production application or user database.

The media worker uses FFprobe JSON to select streams. One FFmpeg invocation produces the durable MP4, mono 16 kHz audio, volume/pause measurements and frames. Compliant H.264 (8-bit 4:2:0, square pixels, up to 1280x720, known bitrate up to 2 Mbps, supported profile/level, no rotation or required trimming) is remuxed without video re-encoding. Other video is normalized with H.264. Frames span the whole answer, approximately every five seconds, subject to EVAL_MAX_FRAMES (at most 24). Artifact failures fall back to saving the MP4; old checkpoints and missing artifacts can resume from that recording.

FFMPEG_CONCURRENCY accepts 1-4. When unset, CPU affinity and Linux cgroup v2 quotas select 1-2 slots. WORKER_CONCURRENCY accepts 1-16 (default 4). Model semaphores use MODEL_*_CONCURRENT, matching the durable model guard. The queue estimator uses the worker's reported media, model and pipeline limits. All decoder, filter and encoder outputs remain single-threaded.

The production web and worker units load the non-secret, versioned ops/englisheval-capacity.env profile. On the six-CPU / 12 GB shared host the worker gets CPUQuota=400%, MemoryHigh=3G, and MemoryMax=4G, leaving capacity for the web process and other applications. The profile sets media/pipeline/ASR/ scoring limits to 3/8/3/4. RPM and TPM budgets remain unchanged; four scoring slots do not guarantee four simultaneous provider requests. Existing .env.prod values explicitly loaded by the app take precedence. Deployment and rollback switch code and systemd units together without modifying shared credentials.

Admin capacity metrics show P50/P90, sample counts, duration class and pipeline version. Stage samples measure active resource time; telemetry also records wall time and resource waits. New and legacy pipeline samples are kept separate. node scripts/stage-report.js --database=/absolute/queue.sqlite --hours=168 reads aggregate timings without opening user recordings or changing the database. Only the latest 30 samples per stage/class/version are retained; report sample size and workload when interpreting percentiles.

On the production host, run this from the validated code directory:

TEST_ROOT=/path/to/validated/code bash scripts/production-load-check.sh \
  --users=50 --arrivals=100 --duration=120 \
  --ffmpeg-concurrency=1 --restart --keep-data

The wrapper defaults to constraining the test process group to one CPU and 1400 MiB, uses isolated temporary data, and stops if real queued work arrives, health checks fail, or memory/disk margins shrink. Use --webm --size=1280x720 for camera-like transcoding, or --fixture=/absolute/synthetic-spoken.mp4 --real-upstreams for real model measurements. Never use a user's recording as a test fixture. --keep-data retains report.json with the synthetic test results.

For the six-CPU profile, add --capacity-profile and set BENCHMARK_CPU_QUOTA=400% BENCHMARK_MEMORY_MIB=4096 when running the wrapper. Its high-memory mode stops below 2 GiB available host memory. Benchmark reports separate local semaphore peaks from sampled actual in-flight model requests.

--resume-data=/tmp/englisheval-load-... --keep-data resumes an interrupted synthetic member benchmark. It verifies the temporary path and synthetic owners, preserves existing recordings/checkpoints, and can revalidate a cached scoring response after a parser fix. Resume reports are marked explicitly; their elapsed time is the resumed portion, while stage measurements preserve already recorded resource time. Never use this option with the production database.

Run node scripts/full-browser-check.js --full-duration with Playwright installed for desktop/mobile recording, three-minute-thirty-second auto-stop, upload, scoring, playback, share-image export, game, history, feedback and comments. Model responses are deterministic in this browser test. The other browser scripts cover queue recovery, guest/DingTalk identity changes and administrator monitoring.

Partner API

The read-only partner API exposes DingTalk user identity fields and speaking-assessment scores without exposing recorded videos, video paths, filenames, extracted audio, sampled frames, transcripts, raw audio metrics, improved-answer text, or internal model metadata. Configure a separate integration secret as PARTNER_API_KEY; do not reuse the DingTalk app secret or session secret. Generate a strong key with openssl rand -hex 32, and expose the API only over HTTPS or a trusted private network.

Swagger UI is available at /api-docs, and the OpenAPI 3.0 specification is available at /openapi.yaml.

curl \
  -H "Authorization: Bearer $PARTNER_API_KEY" \
  "http://localhost:3199/api/v1/users?jobNumber=EMPLOYEE_NUMBER"

curl \
  -H "Authorization: Bearer $PARTNER_API_KEY" \
  "http://localhost:3199/api/v1/users/DINGTALK_USER_ID"

curl \
  -H "Authorization: Bearer $PARTNER_API_KEY" \
  "http://localhost:3199/api/v1/rubrics"

GET /api/v1/users supports exact, case-insensitive filters for openId, userId, jobNumber (or its job_number alias), email, and orgEmail, plus limit (maximum 200) and offset. User responses include both jobNumber and job_number with the same value. Evaluations are sorted newest-first, users are sorted by their latest evaluation, and pagination.total is calculated before pagination. GET /api/v1/users/{userId} returns one exact, case-sensitive DingTalk organization user ID match. GET /api/v1/rubrics returns the active versioned scoring standard, formula, score bands, weights, evidence, and interpretation guidance. Each evaluation response includes rubricId and rubricVersion so consumers can join scores to the correct standard. Existing records created before organization enrichment may have empty identity fields, and standalone speech uploads return an empty questionId because they are not backed by a generated question.

The partner endpoints also expose 28 deterministic roster-backed users, each with one completed mock evaluation, for integration testing. Their jobNumber values come from the roster's 工号 field, their name values come from 姓名, and generated API identifiers start with mock_. Only those two roster fields are retained. The mocks are generated in memory and do not modify recording or question metadata.

Answer evaluation runs after Finish and save according to the audio, transcript, and evaluation workflow above. The active rubric is:

Dimension Weight
Pronunciation / intelligibility 20%
Fluency 10%
Grammar 20%
Vocabulary 15%
Coherence / task relevance 25%
Visual delivery 10%

Persistent storage and ownership

Generated questions are appended to questions/metadata.jsonl with the DingTalk OAuth openId and an organization user snapshot containing userId, jobNumber, email, and orgEmail when the organization contact API returns them. These fields are stored both at the record's top level and in its nested user object. Answer attempts are appended to recordings/metadata.jsonl with the same user fields plus the owned questionId. Attempts without a video are retained with hasVideo: false and a skipped evaluation; uploaded answer videos are stored in recordings/ and use hasVideo: true.

The JSONL files and the video/artifact directories live on the server filesystem, so they survive application restarts. Privacy acknowledgements are stored separately in consents/metadata.jsonl by DingTalk openId and policy version. Experience ratings are stored in ratings/metadata.jsonl with only the DingTalk user snapshot, 1–5 score, selected reason tags, outcome, and timestamp; they do not contain the user's question, answer, transcript, recording, or evaluation. The current per-user leaderboard alias and actual-name/alias choice are stored in recordings/leaderboard-identities.jsonl, so changing either updates all leaderboard views without rewriting answer records. In production, these records live in raw persistent directories under /srv/englisheval/shared. If the app will run on multiple instances, migrate these records to a shared database/object store rather than relying on instance-local files.

History and private video endpoints always filter by the effective DingTalk or guest ownership key. Guest videos are excluded from the public standalone gallery and its video/poster endpoints. The recordings directory is not publicly served. Application startup never deletes or migrates persistent records; any future migration must be run explicitly with a verified backup.

Standalone uploads are private by default, including existing records without explicit sharing consent. A DingTalk user can separately opt in to public video, title, score, and feedback sharing for an individual upload. Only records with publiclyShared: true can appear in the gallery or its media endpoints; guest uploads remain private.

Run npm test for API, media, queue, recovery, and security regression checks. For browser integration checks, install Chromium with npx playwright install chromium and run npm run test:browser. The browser checks start npm run dev on an isolated port with temporary data, synthetic identities and local model responses. Use node scripts/full-browser-check.js --full-duration to also wait for the real three-minute-thirty-second browser recording cutoff. Additional access and draft-restoration checks are available through node scripts/access-browser-check.js and node scripts/guest-browser-check.js.

The production service applies a 0077 umask. Persistent directories are mode 0700, and data files are mode 0600, limiting access to the service account. The daily recording-maintenance unit comes in two shapes, selected at deploy time by RECORDING_BACKUP_ENABLED. With the backup enabled (the default) it briefly stops the application, creates an encrypted backup, skips live-recording deletion when retention is disabled, and starts the application again. With the backup disabled it runs recording retention and the orphan upload sweep only, without stopping anything.

openId remains the ownership field. Browser OAuth stores the application-scoped openId returned directly by DingTalk. H5 in-app login returns the organization's unionId and userId instead; the server reuses a matching historical OAuth openId when one exists, otherwise it derives a deterministic, app-scoped opaque inapp_... ownership value without exposing the raw organization identifiers. At sign-in, the server queries DingTalk user details and normalizes DingTalk's snake_case/camelCase response fields as userId, jobNumber, email, and orgEmail. This enrichment requires the application's organization-contact permission; if the lookup is unavailable, authentication continues with empty organization fields and the server logs a warning.

Browser support for direct MP4 recording varies. The app asks for MP4 first and records WebM when the browser does not support MP4 through MediaRecorder. Recordings are finalized as a single browser blob because timed MP4 chunks are not reliably concatenable across implementations. The server validates and transcodes every accepted upload to MP4 before storage.

Extracted audio and sampled frames are stored under recordings/artifacts/.

Encrypted recording backups

Backups use age public-key encryption. Generate the key on a trusted administrator workstation, not on production:

age-keygen -o englisheval-backup-identity.txt

Keep that identity file offline and outside this repository. Copy only the printed public recipient into production's original /srv/englisheval/shared/.env as BACKUP_AGE_RECIPIENT. Never copy the identity file or a local .env to production.

Production must have the age command installed when the recording backup is enabled. Deployment refuses to proceed without age, an explicit non-negative retention setting, and a native age recipient; with RECORDING_BACKUP_ENABLED=false only the retention setting is required. The maintenance timer runs daily around 03:15 and, with the backup enabled, writes encrypted snapshots to /srv/englisheval/backups. It never creates a plaintext archive.

To test recovery, restore into a new empty staging directory rather than over the live persistent data:

./scripts/restore-recordings-backup.sh \
  recordings-YYYYMMDDTHHMMSSZ.tar.gz.age \
  /secure/offline/englisheval-backup-identity.txt \
  /tmp/englisheval-restore-test

Verify the restored metadata and videos before considering any manual recovery. The restore script deliberately refuses to write into a non-empty directory.

Live recordings are stored as raw files in /srv/englisheval/shared/recordings. They are protected by service-account ownership and restrictive filesystem permissions, but they are not encrypted at rest. The encrypted backup archives remain the recovery mechanism for this directory.

Deploy

The included script deploys versioned releases over SSH, installs a systemd service, and keeps .env, .env.prod, recordings/, questions/, comments/, consents/, ratings/, invitations/, analytics/, and user-figures/ in shared persistent storage on the target host. Every directory that src/config.js creates must be in that shared list and in the ReadWritePaths of each systemd unit that loads it; otherwise ProtectSystem=strict makes the release tree read-only and the service fails to start. Local-only working state (.workbuddy/, output/, tmp/, downloads/, backups/, and the offline age/LUKS key material) is excluded from the release. .env contains settings shared by development and production, while .env.local and .env.prod override environment-specific values. Deployment never copies .env.local.

For the initial deployment and data migration:

./deploy.sh --migrate-data

For later code-only deployments:

./deploy.sh

The deployment script (deploy.sh), server configuration (ops/), and production validation scripts are private local files excluded from Git. Obtain them from your deployment administrator before using the commands above. Deployment destinations and paths are configured in those local files. The production APP_BASE_URL is read from the existing shared .env.prod. Plain HTTP deployments must set COOKIE_SECURE=false; HTTPS deployments should set it to true. Override TARGET, REMOTE_ROOT, APP_PORT, or SERVICE_NAME in the command environment. The migration option deliberately refuses to overwrite non-empty remote data.

Resource alerts

Production also runs englisheval-monitor.service, independently sampling disk, memory, CPU, queue and service health every 30 seconds. The EnglishEval robot sends in-app DING to the configured administrator on incidents, severity escalations and recovery, without periodic reminders. Configure notifications in shared monitor.env; retain application credentials in the original shared environment. The admin dashboard includes monitor health and delivery status. See resource alert operations and deployment validation.

About

eval English speech

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages