feat(android): add native display and embedded announcements
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# Embedded announcement audio evidence
|
||||
|
||||
## Current accepted source
|
||||
|
||||
Android version 1.5.1 reuses the queue announcement WAV fragments that already existed in this repository's browser implementation. They were first added in commit `6d911271e6bf5b0120edc04c91506077239e96e5`, moved byte-for-byte into `web/src/assets/queue-voice/` in commit `4405bf96f7ad8029fbf62741d92ddaab7b99c096`, and later removed from the browser bundle in commit `5ad8bfcd0377e6dfb8c703d0834d248633a5b939` when that implementation changed to `speechSynthesis`.
|
||||
|
||||
The user confirmed that the project already had these WAV assets; that evidence redirected the fix to reuse the project's own prior assets instead of generating another voice. Repository history does not record the original generator, speaker, recording author, or a separate license/NOTICE. Therefore this record treats them as project-owned legacy assets in scope for this repair; it does not claim that they are open source or independently cleared for third-party redistribution.
|
||||
|
||||
## Android mapping
|
||||
|
||||
| Android resource | Historical source / operation | Spoken content |
|
||||
| --- | --- | --- |
|
||||
| `voice_prompt.wav` | `please.wav` | 请 |
|
||||
| `voice_digit_0.wav` ... `voice_digit_9.wav` | corresponding `digit-0.wav` ... `digit-9.wav` | 零 ... 九 |
|
||||
| `voice_range.wav` | PCM concatenation of `number.wav` then `through.wav` | 号至 |
|
||||
| `voice_suffix.wav` | PCM concatenation of `number.wav` then `entrance.wav` | 号,前往入口 |
|
||||
|
||||
The two concatenated resources contain one RIFF header and the source PCM samples in the listed order. Android creates its repeat and batch gaps, so the historical `gap.wav` is not packaged.
|
||||
|
||||
## Format and integrity
|
||||
|
||||
All 13 Android resources parse as RIFF PCM format tag 1, 16-bit, mono, 16 kHz, with aligned sample data. `EmbeddedVoiceAssetsTest` locks the exact resource set and validates every committed file through the production WAV parser.
|
||||
|
||||
| Resource | SHA-256 |
|
||||
| --- | --- |
|
||||
| `voice_digit_0.wav` | `DA7E001528495F75473F99A6BBC275B9C4B7E5FC0888FA9495F86E16231DDC61` |
|
||||
| `voice_digit_1.wav` | `2248F327AC9386ECEADB98A4A24F857C218596406EE519F268D67FEEF785971D` |
|
||||
| `voice_digit_2.wav` | `DFCD91AA933F78E5CC4BB18B84A9F00D5365C7992B15EB7948960BFBE4505EE6` |
|
||||
| `voice_digit_3.wav` | `98FD18056AF18D3B951E6777D5CA2EE7971005F9FBFD5054E8DADDDA4913778F` |
|
||||
| `voice_digit_4.wav` | `073B17DCC81F416799644A34A75723CE1B75A8D90D06679D4F0E6F96DB9CBBFE` |
|
||||
| `voice_digit_5.wav` | `80DFA4AD87E752CE23F49B123D91E9E06F691267CBD3ECA94FF12213C73739D7` |
|
||||
| `voice_digit_6.wav` | `131A747A9D0C8825F8C69907E19E9FB83935D2EB6FEB1137F6EF0311F96F197E` |
|
||||
| `voice_digit_7.wav` | `7DAF943934080733D7F6704CE58FCADC736965A578DC0A091CBFDCCB8E4B9BE4` |
|
||||
| `voice_digit_8.wav` | `1406CF912AB2756A6A6600749767FFA0181832DADB28FA0CD81FBB0F1BE47A48` |
|
||||
| `voice_digit_9.wav` | `BE72EC45438D4D2835E11B91380715CE36650CC465D5EE5465B540B1B8686F8F` |
|
||||
| `voice_prompt.wav` | `C066CAF5107994D4B7F7E96E6818AF54E8BB785914FD5881FA223C18C3CF9BDE` |
|
||||
| `voice_range.wav` | `9018BF8EF743F6A6FC2323BDE7313CCFE12D5E14A19207E87C2EE3B3A19D9C7B` |
|
||||
| `voice_suffix.wav` | `010CF52AE95B0F541A105DB43741832A75FB670A3D02A72A032D94D9AC95182E` |
|
||||
|
||||
## Semantic diagnosis and acceptance
|
||||
|
||||
The 1.5.0 eSpeak NG `cmn` assets were rejected after the target user reported that they did not sound Chinese. Reproduction showed correct byte order and repetition, but two Chinese recognizers returned unrelated text at approximately 0.001-0.002 confidence. Direct unfragmented synthesis from the same engine also failed, confirming the source voice mapping rather than Android composition as the cause.
|
||||
|
||||
The restored legacy assets were evaluated in the actual Android token order:
|
||||
|
||||
- `00017`, one pass: `请 + 0 + 0 + 0 + 1 + 7 + 号 + 前往入口`.
|
||||
- `00017-00020`, one pass: `请 + 0 + 0 + 0 + 1 + 7 + 号至 + 0 + 0 + 0 + 2 + 0 + 号 + 前往入口`.
|
||||
- Android repeats the pass three times with two 350 ms zero-PCM gaps.
|
||||
|
||||
Vosk small-cn 0.22 recovered the complete target phoneme sequence and sentence structure from final Android-composed samples, with average word confidence approximately 0.84-0.89. Its text used homophones/segmentation such as `一期` for `一七` and `好` for `号`, while all three repeats remained stable. Windows zh-CN SAPI also recovered the sentence anchors but was less accurate on isolated digit fragments. A digit calibration sequence recovered 零 through 九 in order. These are calibrated automated red-flag checks, not a substitute for human acceptance.
|
||||
|
||||
The final 1.5.1 single-number preview produced from the committed Android resources is 11.516125 seconds and has SHA-256 `9FFDA2837EC40DAF4AC626FF093DDC64223A6F9E352FA3F8E198A48C97C285D3`.
|
||||
|
||||
## Verification and remaining risk
|
||||
|
||||
- `gradlew.bat clean testDebugUnitTest lintDebug assembleDebug`: PASS; 53 JVM tests, no failures/errors/skips; lint has zero errors and 23 warnings.
|
||||
- APK version: versionCode 8, versionName 1.5.1.
|
||||
- APK contains exactly the 13 required `res/raw/voice_*.wav` resources.
|
||||
- APK SHA-256: `5CB13105178EA5630EA124D0BE98A189E1B7F0557B2672C8CFAD3B6002FCBBEB`.
|
||||
|
||||
Remaining acceptance: listen to representative single and range announcements on the target television and confirm every digit. The historical clips have uneven source loudness; they were kept byte-exact rather than normalized because processing would invalidate their prior use and requires renewed listening/ASR review.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Evidence: Public display API contract
|
||||
|
||||
- Date: 2026-08-11
|
||||
- Status: Verified
|
||||
- Scope: Native Android overview and project-detail implementation
|
||||
- Confidence: High
|
||||
- Sources: `server/internal/httpapi/public.go`, related Go tests, and live HTTPS responses from `queue.nianxx.cn`
|
||||
- Last verified: 2026-08-11
|
||||
- Stale trigger: public display routes or DTOs change, or production is deployed from a server revision that differs from this repository
|
||||
|
||||
## Confirmed endpoints
|
||||
|
||||
- `GET /api/display/overview` returns `{ projects: [...], server_time }` without authentication.
|
||||
- `GET /api/display/{identifier}/snapshot` accepts a normalized project code or a 40-128 character display token.
|
||||
- Invalid or unknown identifiers return `DISPLAY_NOT_FOUND` with HTTP 404.
|
||||
|
||||
## Confirmed overview project fields
|
||||
|
||||
- `id`, `code`, `name`, `status`
|
||||
- `waiting_count`, `waiting_ticket_count`, `waiting_people_count`
|
||||
- `issued_ticket_count`, nullable `latest_ticket_number`, `experienced_people`
|
||||
- nullable `current_batch`, `estimated_wait`, `last_updated_at`
|
||||
|
||||
## Confirmed snapshot fields
|
||||
|
||||
- Project: `project_name`, `status`, nested `project`
|
||||
- Queue: `revision`, the three waiting counts, issued/latest/experienced metrics
|
||||
- Calls: nullable `current_batch`, `recent_batches`
|
||||
- Timing: `estimated_wait`, `server_time`, `last_updated_at`
|
||||
|
||||
Each call batch exposes `batch_number`, `sequence`, `status`, `call_mode`, requested/ticket/people counts, `called_at`, and tickets. Each ticket exposes public display number fields, status, and party size; no personal visitor fields are present.
|
||||
|
||||
## Implementation consequence
|
||||
|
||||
The native detail screen can render and decide announcements from one snapshot request every three seconds. A second announcement-only request is unnecessary. The first successful snapshot after entering or resuming a project must establish a baseline and must not replay the current batch.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Native range-display truncation evidence
|
||||
|
||||
## Reported symptom
|
||||
|
||||
On the target 1920x1080 television, a batch that called tickets `00010` and `00011` rendered the main current-call field as only `00010 至`; the latest-ticket metric at the right still showed `00011`.
|
||||
|
||||
## Boundary verification
|
||||
|
||||
The production public snapshot was read on 2026-08-12 from `GET https://queue.nianxx.cn/api/display/YYHHC/snapshot`. It contained:
|
||||
|
||||
- `latest_ticket_number: "00011"`
|
||||
- `current_batch.ticket_count: 2`
|
||||
- `current_batch.tickets[0].display_number: "00010"`
|
||||
- `current_batch.tickets[1].display_number: "00011"`
|
||||
|
||||
`DisplayFormatting.currentBatchNumbers` therefore produces the complete string `00010 至 00011`. This rules out the server response, JSON parser, and range formatter as the cause.
|
||||
|
||||
## Root cause
|
||||
|
||||
The original native current-number `TextView` started at 74sp, allowed one line, and called `NativeDisplayUi.autoSize`. That helper only activated Android platform auto-size on API 26 and later, although the APK supports API 23. On API 23-25, long range text stayed at 74sp and could be silently clipped by the half-width current-call panel. Platform behavior and display density also made the guarantee device-dependent on newer releases.
|
||||
|
||||
An API-25 Robolectric regression using a full `ProjectDisplayView`, 1920x1080 exact layout, and the real two-ticket snapshot was run before the fix. Its red signal confirmed the legacy view retained the initial 74sp rather than applying a fitting policy.
|
||||
|
||||
## Fix and invariant
|
||||
|
||||
`SingleLineFitTextView` now owns current-call fitting on API 23 and later. On text, size, or padding changes it:
|
||||
|
||||
- computes actual content width and height after compound padding;
|
||||
- measures the real text and font metrics with a copy of the active `TextPaint`;
|
||||
- binary-searches for the largest whole-sp size that fits both dimensions;
|
||||
- enforces a single line and never ellipsizes;
|
||||
- falls below the normal 30sp readability floor only when necessary to preserve the complete ticket range.
|
||||
|
||||
Short single numbers still receive the largest fitting size, up to 74sp.
|
||||
|
||||
## Verification
|
||||
|
||||
- `gradlew.bat clean testDebugUnitTest lintDebug assembleDebug`: PASS.
|
||||
- JVM tests: 56 across 12 suites, 0 failures, 0 errors, 0 skipped.
|
||||
- Lint: 0 errors, 24 warnings.
|
||||
- `ProjectDisplayViewTest` locks the full `00010 至 00011` string, one-line behavior, and no ellipsis on API 25.
|
||||
- A narrow ASCII control asserts the fitted `TextPaint` width and font height are within actual content bounds; deterministic sizing tests call the production two-stage decision and lock largest-fitting selection, full-size selection, below-floor fallback, and the 1sp terminal boundary.
|
||||
- APK: versionCode 9, versionName 1.5.2; Android Debug v1/v2 signatures verified.
|
||||
- Build and deliverable SHA-256: `0D56C10B8CE706893F3EA8E59B21329847517F56F7347ABEA127AB30C6C71D77`.
|
||||
|
||||
## Remaining acceptance
|
||||
|
||||
Robolectric's host font metrics are not the target television's font metrics. The production code uses the device's real `TextPaint`, but final acceptance still requires installing 1.5.2 on the target television and visually confirming both endpoints for `00010 至 00011` and at least one wider representative range.
|
||||
Reference in New Issue
Block a user