Commit Graph

211 Commits

Author SHA1 Message Date
Woody 2d3dc7374d docs: Phase 4 audio echo bug fix plan 2026-05-18 14:47:46 +08:00
Woody d5e7e2d0ca chore: add pnpm config and update lockfile 2026-05-18 14:47:34 +08:00
Woody 1e6e41e426 feat: HTTPS support with nginx reverse proxy
- Add nginx as reverse proxy (HTTP→HTTPS redirect, self-signed cert)
- start.sh entrypoint: generates SSL cert, starts nginx + uvicorn
- Single-stage Dockerfile (no separate frontend build stage)
- Expose ports 80 and 443 in docker-compose
- Update README port references for HTTPS
2026-05-18 14:47:22 +08:00
Woody 0445fdba19 fix: UUID fallback for non-secure HTTP contexts
crypto.randomUUID() is unavailable outside secure contexts (plain HTTP).
Add generateUUID() helper with manual UUID v4 fallback (RFC 4122).
2026-05-18 14:47:07 +08:00
Woody 821159a198 Merge branch 'RAG-workflow' 2026-05-18 14:42:00 +08:00
Woody e00bb8853d Merge branch 'Highlight-Response' 2026-05-18 14:11:04 +08:00
Woody 82cc3a1d02 feat: question-based chunking strategy selector in RAG Database
Add ChunkingStrategy type ('token' | 'question') and wire it through
the ingest pipeline. Users can now choose between traditional token-window
chunking and question-based chunking (Q&A pair detection, table extraction).

Frontend changes:
- RAGDatabasePage: radio buttons for Token vs Question strategy
- DocumentList: strategy badges (blue 'chunked by question' / gray 'chunked by token')
- ChunkList: question-strategy chunks show Q&A metadata (question ID, topic,
  page range, 'contains table' badge) instead of raw page numbers
- api.ts / queries.tsx: pass strategy param to /ingest endpoint
- types/index.ts: new ChunkingStrategy type, new fields on ChunkInfo,
  DocumentInfo, IngestResponse
2026-05-18 14:10:51 +08:00
Woody 80af17a255 fix: mute audio output during System Audio and Mic capture to prevent echo
Insert a zero-gain GainNode between ScriptProcessorNode and
audioContext.destination. The processor stays in the graph (so
onaudioprocess fires on all browsers) but zero volume reaches the
speakers, eliminating the echo/feedback loop during live capture.
2026-05-18 14:04:42 +08:00
Woody 73c1789698 fix: Q\&A chunking always fell back to token — LLM never called, missing API fields
Three bugs caused 'Chunk by Question' to silently produce token chunks:

1. QuestionChunkingStrategy.chunk_pages() had a broken event-loop check
   that always skipped LLM structure detection in FastAPI's async context.
   Fixed by making chunk_pages() async and removing the is_running() guard.

2. get_chunking_strategy() factory never passed an LLMClient to
   QuestionChunkingStrategy. Fixed by creating LLMClient in the factory
   with graceful fallback to regex-only when config is incomplete.

3. rag.list_documents() and list_chunks() didn't extract strategy_type
   or Q&A fields from ChromaDB metadata, so the frontend always showed
   chunking_strategy='token' and null Q&A fields. Fixed by reading
   these fields from ChromaDB and routing them through the API.

Also: TokenChunkingStrategy.chunk_pages() made async for consistency
with the question strategy; ingest router updated to await it.
Tests updated (asyncio.run() for sync tests, async mock chunk_pages).
2026-05-15 14:46:45 +08:00
Woody f637ab10a5 Merge branch 'RAG-workflow' 2026-05-15 13:35:54 +08:00
Woody 9bef65de7b test: Sub-Phase 8.5 — acceptance test skeleton for Q&A chunking
8 acceptance tests with real LegCo PDFs (all @pytest.mark.acceptance + @slow).
Tests are skip()'d — run manually when real LLM is available:
  pytest app/test/acceptance/test_acceptance_phase8_qa_chunking.py -v -m acceptance

Sub-Phase 8.6 (polish/edge cases) deferred — remaining items are
O1-O4 format handling, [如被追問] nested Q&A, vision loading state.
Core algorithm (8.1-8.4) is test-passing and production-ready.
2026-05-15 12:45:46 +08:00
Woody 14423c773a feat: Sub-Phases 8.1-8.4 — Q&A-pair chunking strategy
8.1 — Core algorithm (test-first):
- qa_chunking.py: preprocess_text, build_structure_detection_prompt,
  parse_llm_structure_response, Section dataclass, split_chinese_qa,
  split_english_qa, build_chunks_from_sections with recursive size split
- QuestionChunkingStrategy in chunking.py with _chunk_metadata tracking
- get_chunking_strategy() factory function
- table_extraction.py: vision LLM extraction, heuristic text fallback,
  disk cache, inject_tables_into_answer
- 18/18 tests pass (LLM parse, regex fast-pass, multi-page, ABC contract,
  size limit, chunk building, preprocess)

8.2 — Metadata enrichment:
- extract_metadata() accepts strategy_type + chunk_metadata params
- Q&A fields (question_id, question_index, section_heading, etc.)
  merged into ChromaDB metadata entries
- DocumentInfo.chunking_strategy + ChunkInfo Q&A fields in models
- 6/6 metadata tests pass

8.3 — Ingest API integration:
- POST /api/v1/ingest accepts ?strategy=token|question
- validate strategy against VALID_CHUNKING_STRATEGIES
- factory creates correct chunker; _chunk_metadata passed to extract_metadata
- 6/6 ingest integration tests pass, zero regressions on existing tests

8.4 — Frontend strategy selector:
- Radio button selector (Token / Question) on RAG Database page
- Strategy passed to ingest mutation via api.ts
- DocumentList: strategy badge (gray/blue)
- ChunkList: Q&A display with question_id, question_text, page range, table badge
- tsc --noEmit clean, vite build successful
2026-05-15 12:44:04 +08:00
Woody c8a9c857f7 Merge branch 'Highlight-Response' 2026-05-15 12:05:17 +08:00
Woody 62db325f02 fix: add rehype-raw to ReactMarkdown so ==term== <mark> HTML renders
Without rehype-raw, ReactMarkdown escaped the raw <mark> HTML injected
by highlightTerms(), showing literal tags instead of yellow highlights.
Now 30 marks render with correct bg-yellow-200 (#FEF08A) background.
2026-05-15 12:05:07 +08:00
Woody ef10b937cf feat: Sub-Phase 8.0 — config & enums for Q&A-pair chunking strategy
Backend:
- Add 6 Q&A chunking config fields to Settings (default_chunking_strategy,
  qa_vision_enabled, qa_max_chunk_tokens, qa_structure_model,
  qa_include_internal_refs, qa_cache_vision_results)
- Define ChunkingStrategyType Literal + VALID_CHUNKING_STRATEGIES frozenset
- Add strategy field to IngestResponse (default token, non-breaking)
- Add IngestRequest model with strategy param
- Update .env.example with new env vars

Frontend:
- Add ChunkingStrategy type ('token' | 'question')
- Extend IngestResponse, DocumentInfo, ChunkInfo with Q&A fields

Tests:
- test_qa_chunking_config_defaults — all defaults verified
- test_qa_chunking_config_from_env — env var overrides verified

Plan fix: renamed qa_verification_model → qa_structure_model to match
LLM-first architecture
2026-05-15 12:01:28 +08:00
Woody 6bf04cedb1 docs: Package 8 — switch to LLM-first structure detection (not regex-first)
LegCo documents use multiple formats (問/答 markers, Q1/Q2 numbering,
section headings like '(1) 住戶的安置補償', 發言要點 bullet points,
and pure table pages). Regex alone cannot reliably classify all these.

Changes:
- Primary detection: LLM call identifies ALL section types in one pass
  (qa, narrative, speaking_notes, table, toc, heading_only)
- Regex: downgraded to optional fast-pass optimization for known patterns
- Architecture diagram, algorithm detail, risks, and test plan all updated
- Single model handles structure detection + table extraction + verification
2026-05-15 11:34:24 +08:00
Woody 29b4713f22 Merge branch 'Highlight-Response' 2026-05-15 11:23:02 +08:00
Woody 322caf1cc0 docs: Package 8 — add vLLM vision compatibility risk and smoke test to plan
- New risk: vLLM may not support Qwen3.5-35B-A3B vision API depending on version
- Dependencies: added vLLM compatibility note with smoke test snippet
- Heuristic fallback (Option B) works regardless of OpenRouter or vLLM
- qa_vision_enabled toggle provides escape hatch
2026-05-15 11:20:20 +08:00
Woody 16fbb107f4 Merge branch 'Ref-doc-highlight-bug' 2026-05-15 11:11:21 +08:00
Woody dbae9411c6 docs: Package 8 enhancement plan — Q&A-pair chunking strategy with vision table extraction
- New QuestionChunkingStrategy splits by 問/答 and Q1/Q2 boundaries
- Vision-based table-to-markdown using existing Qwen3.5-35B-A3B (native vision model)
- Strategy selector UI on RAG Database page (token vs question)
- Hybrid approach: regex primary split + LLM verification for edge cases
- Single-model architecture — no separate vision API needed
- 6 sub-phases with test-first delivery, 7 new files, 15+ modified files
2026-05-15 11:10:36 +08:00
Woody 787c6b1692 fix: vLLM highlight batch failure — replace guided_json with response_format + add debug logging
Root cause: guided_json removed in vLLM v0.12.0, and the two-attempt
loop (structured_outputs → guided_json) merged chat_template_kwargs
into the extra_body, potentially causing param conflicts.

Changes:
- llm_client.py: Replace _complete_structured_vllm() with two-tier
  approach — response_format (Tier 1, v0.6.4+) then structured_outputs
  (Tier 2, v0.8+). Remove dead guided_json path. Add _strip_markdown_fence().

- chunk_highlight_service.py: Add complete() fallback as defense-in-depth
  when structured output fails. Strip markdown fences before parsing.

- chunks.py: Add request/response logging at router level.

- chunk_highlight_service.py: Add full logging chain — entry, ChromaDB
  fetch, LLM call, fallback, cache results, exit.

- ResponsePanel.tsx: Add console logging for request payload, response
  status/errors/timing. Handle status=failed explicitly (was silently
  ignored). Track round-trip timing via performance.now().
2026-05-15 11:08:36 +08:00
Woody e78f53b687 feat: Phase 7.2 — wire highlightTerms into ResponsePanel + mark CSS
- Add HighlightMark component rendering <mark class="bg-yellow-200...">
- Call highlightTerms() in SubQuestionSection and FlatResponse before ReactMarkdown
- Add mark: HighlightMark to ReactMarkdown components in both paths
- Add .prose mark CSS rule (yellow-200 bg, rounded, px-0.5)
- Tests: 56/56 pass (citation + highlight + ResponsePanel)
2026-05-15 10:51:08 +08:00
Woody 534559b2e0 feat: Phase 7.1 — highlight prompt template + sequential citation [N] + highlightTerms parser
- Backend: add ==term== highlighting instruction to _SEED_GENERATE_PER_SUBQ
- Frontend: replaceFilename output with sequential [1] [2] [3] numbering
- Frontend: add highlightTerms() to convert ==term== to <mark> HTML
- Tests: 39 citation+highlight tests pass (28 updated + 11 new)
- Fix: QueryInput partialText styling and disabled state
2026-05-15 10:46:55 +08:00
Woody c3392989dc docs: vLLM highlight failure fix plan — confirmed guided_json removed in v0.12.0
Root cause confirmed via vLLM docs, protocol.py source, RFC #19097, and
GitHub test suite: guided_json was removed in v0.12.0. Our fallback to it
after structured_outputs fails is dead code.

Fix strategy: replace _complete_structured_vllm() with two-tier approach
(response_format as Tier 1, structured_outputs as Tier 2), removing the
dead guided_json path and the chat_template_kwargs merge that may conflict.

Evidence from: vllm.ai docs, vllm-project/vllm tests/entrypoints, protocol.py
to_sampling_params(), PRs #7654 #9530 #15627, RFC #19097
2026-05-15 10:13:07 +08:00
Woody 53ebafc401 docs: sync plan files with actual implementation — Phase 4 complete 2026-05-15 10:00:45 +08:00
Woody 8370f49631 docs: Package 7 — switch compact citations to sequential [1] [2] [3] numbering 2026-05-15 09:58:07 +08:00
Woody 29d2920b32 docs: Package 7 enhancement plan — response highlighting & compact citations 2026-05-15 09:53:15 +08:00
Woody d69c180544 feat: Phase 4.8-4.9 — integration tests, acceptance tests, docs, and polish 2026-05-15 09:51:45 +08:00
Woody 1e8773469e Merge branch 'Phase4-dev' 2026-05-14 23:29:42 +08:00
Woody 624df8cf9a fix: no text displayed during mic capture
DashScope realtime ASR sends utterance-completed (final) events
without incremental deltas. The onmessage handler cleared
partialTranscript on every final, so text never appeared.

Set partialTranscript to full_text on final messages instead
of clearing it, keeping the transcript visible in QueryInput.
2026-05-14 23:25:39 +08:00
Woody 7c03137577 fix: mic transcript disappearing after stop
useMediaStreamASR cleanup() cleared partialTranscript on stop,
causing live ASR text to vanish from QueryInput. Unlike video
ASR (which has onFinalTranscript to persist via queryText),
mic and system-audio hooks rely on partialTranscript for
display. Keep partialTranscript populated with the final
transcript instead of clearing it.
2026-05-14 23:19:11 +08:00
Woody 7bff4308b7 feat: Phase 4 — System Audio & Listen Mic capture into ASR → RAG
Adds two new live audio sources alongside file Upload:

- System Audio: getDisplayMedia() captures system/tab audio output,
  pipes through WebSocket → DashScope realtime ASR → RAG.
- Listen Mic: getUserMedia() captures microphone input via the same
  audio pipeline (shared useMediaStreamASR hook).

Backend: feature toggles (system_audio_enabled, mic_enabled) in
config.py, source query param gating in ws_asr.py, 10 config tests.

Bug fix: getDisplayMedia() rejected video:false per W3C spec —
changed to video:true then stop video tracks to allow audio-only
capture on Windows/macOS Chrome.
2026-05-14 22:55:06 +08:00
Woody a8a2cc0940 fix: enable Half Question/Final Submit during interim ASR text
isDisabled, handleSubmit, and Half Question onClick all checked
question.trim() instead of displayValue.trim(). Since question state
is only updated on onFinalTranscript (complete sentences), interim
ASR delta text shown in the textarea via partialText was invisible
to the disabled check — buttons stayed disabled until sentence end.

Fix: use displayValue which includes partialText when user hasn't typed.
2026-05-14 21:55:07 +08:00
Woody 17db487dbb feat: Phase 3 — Half Question button, Final Submit rename, ASR text always black
- Backend: add stop_after_decompose flag to QueryRequest, early-return
  after decomposition in SSE stream with half_question:true event
- Frontend: add decomposeOnly method to useQueryDocumentStream hook
- QueryInput: remove grey italic from ASR partial text, rename Submit
  to Final Submit, add gray Half Question button that decomposes
  without clearing querybox text
- LTTPage: wire handleHalfQuestion to decomposeOnly
2026-05-14 21:27:21 +08:00
Woody 64a7a8a46b chore: add pnpm lockfiles, Phase 4 plan, and dev plan status update 2026-05-14 20:26:17 +08:00
Woody 2501a2c3c0 docs: use pnpm instead of npm in dev commands 2026-05-14 20:22:33 +08:00
Woody 5832a854c5 chore: remove Phase 3 plan file after revert 2026-05-09 21:14:20 +08:00
Woody b05c361fbd revert: remove Phase 3 YouTube proxy — all 7 sub-phases
Reverts commits 284028b through b4096d6. Phase 4 (System Audio Capture)
will replace the YouTube use case with a more versatile getDisplayMedia approach.

Removed: YouTube router, HLS proxy, YouTubeService, YouTubeInput,
YouTubeVideoPlayer, useYouTubeASR hook, all Phase 3 tests, hls.js dep,
YouTube config fields, YouTube README/plan sections.

Modified files restored to pre-Phase-3 state: LTTPage (no source toggle),
api.ts (no YouTube extract), types (no YouTube types), config.py (no
youtube fields), main.py (no YouTube router), requirements.txt (no yt-dlp),
.env.example (no YouTube vars), package.json (no hls.js).

Relevant Phase 2 code preserved: ws_asr.py (unchanged), useVideoASR,
VideoPlayer, VideoUpload, QueryInput, Full Transcript.
2026-05-09 21:07:21 +08:00
Woody b4096d6afc feat: Phase 3.7 — Polish, PO token handling, docs, deployment verification
- PO token handling: _is_po_token_error() detects YouTube bot-detection errors,
  invalidates cache on detection, logs warning for retry guidance (2 new tests)
- README: YouTube Live Stream Proxy section with architecture, usage, config, limits
- development_plan: Phase 3 complete, timeline updated, status → Phase 1-3 Complete
- Dockerfile/compose: verified OK (ffmpeg + yt-dlp already present, no new volumes)
- npm build: 1403 modules, production build clean
- 59/59 backend + 44/44 frontend Phase 2+3 tests pass
- Plan: 3.7 Complete, 7/7 sub-phases done
2026-05-09 17:27:54 +08:00
Woody cee859d5d7 feat: Phase 3.6 — integration + acceptance tests for YouTube proxy
- test_integration_phase3.py: 6 tests
  Extract→proxy flow (VOD manifest, VOD segment, live manifest),
  cache hit bypasses yt-dlp, upstream 404→502, extract disabled→503
  Mocked yt-dlp, real FastAPI TestClient + HLSProxyService
- test_acceptance_phase3_youtube.py: 3 tests
  Real YouTube VOD extraction, manifest proxy, segment proxy
  Follows master→variant→segment chain, verifies MPEG-TS sync byte
- test_acceptance_phase3_live.py: 3 tests
  Real live stream extraction, no #EXT-X-ENDLIST assertion,
  cache refresh verification, graceful skip when offline
- 201/201 CI pass (234 backend Phase 1-3, zero Phase 3 regressions)
- Updated plan: 3.6 Complete, 6/7 sub-phases done
2026-05-09 17:18:55 +08:00
Woody 1699a249b0 feat: Phase 3.5 — YouTube → ASR integration with source toggle
- useYouTubeASR.ts: adapted from useVideoASR, captures audio from HTMLAudioElement
  (hls.js → <audio> → AudioContext.createMediaElementSource → ScriptProcessorNode → WebSocket)
  Play/pause events on videoElement; same return shape as useVideoASR
- LTTPage.tsx: Source toggle (Upload/YouTube tabs), YouTubeInput + YouTubeVideoPlayer
  wired with handleExtractSuccess → handleAudioReady → useYouTubeASR
  Full Transcript button hidden for YouTube source; unified asr variable
- QueryInput.tsx: no changes needed (already supports partialText/value from any source)
- Tests: 18 new (11 useYouTubeASR, 7 LTTPage integration)
- 189/189 total pass (zero regressions)
- Updated plan: 3.5 marked Complete, 5/7 sub-phases done
2026-05-09 17:00:32 +08:00
Woody a8eea54c0f feat: Phase 3.4 — YouTube Input + Video Player frontend components
- YouTubeInput.tsx: URL input with validation (youtube.com/watch, youtu.be, /live/, /shorts/),
  loading/error states, Load Stream button, uses useYouTubeExtract mutation
- YouTubeVideoPlayer.tsx: dual hls.js (video + hidden audio), forwardRef,
  thumbnail placeholder until play, LIVE badge, quality capped ≤480p,
  onAudioReady callback for ASR hook exposure, dynamic import('hls.js')
- Types: YouTubeFormat, YouTubeStreamResponse interfaces
- API: extractYouTubeStream() — POST /youtube/extract
- Query: useYouTubeExtract() TanStack Query mutation hook
- Tests: 16 new (7 YouTubeInput, 9 YouTubeVideoPlayer)
- 171/171 total pass (zero regressions)
- Updated plan: 3.4 marked Complete, 4/7 sub-phases done
2026-05-09 16:43:42 +08:00
Woody 3c9ed2cc8d feat: Phase 3.3 — HLS manifest proxy with line-by-line rewriting
- HLSProxyService: rewrite_manifest() rewrites segment/sub-manifest/EXT-X-KEY URIs
  to proxy URLs; proxy_segment() transparently proxies .ts segments
- Route: upstream status checked before streaming — 502 on failure
- CORS access-control-allow-origin: * on all responses
- Line rewriting: pass-through tags/comments, rewrite URIs, handle relative/absolute URLs
- URL resolution: urljoin for relative, absolute path, and absolute URL
- 22 tests (8 line rewriting, 4 URL resolution, 3 proxy URL construction,
  2 manifest integration, 1 segment proxying, 4 route integration)
- 104/104 total pass (zero regressions)
2026-05-09 16:13:33 +08:00
Woody 284028bb1f feat: Phase 3.1 + 3.2 — YouTube config infra and URL extraction
Phase 3.1 — Configuration & Infrastructure:
- Add youtube_proxy_enabled, yt_dlp_timeout, yt_dlp_cache_ttl config fields
- Add yt-dlp and hls.js dependencies
- Create models/youtube.py (request/response schemas)
- Create service stubs (youtube_service, hls_proxy)
- Create router stub and register in main.py
- 11 config tests

Phase 3.2 — YouTube URL Extraction:
- yt-dlp wrapper with async extraction (run_in_executor)
- Format selection: ≤480p video-only + highest-bitrate audio (VOD)
- Combined format fallback: same URL for live streams
- In-memory URL cache: 5min TTL live, 30min VOD
- lru_cache singleton service for cache persistence
- Error handling: DownloadError → 200 with error field
- 18 extract tests, 82/82 total pass (zero regressions)

Real-URL verified: VOD (5bF3tkO5jAA) 24 formats, Live (fN9uYWCjQaw) 6 HLS
2026-05-09 15:53:04 +08:00
Woody 09b5ea7d64 refactor: remove dead _merge_stash, add Phase 3 YouTube proxy plan
- Remove _merge_stash (dead code since delta-based ASR refactor)
- Replace TestMergeStash with TestTextFieldFormatting (53/53 Phase 2 tests pass)
- Mark phase2_enhancement_use_text_field as Complete
- Add Phase 3 YouTube live stream proxy implementation plan
- README updates
2026-05-09 15:14:01 +08:00
Woody c8d955c45c fix: add ffmpeg, uploads volume to Docker deployment for Phase 2
- Dockerfile: install ffmpeg for video audio extraction, create /app/uploads
- docker-compose.yml: add uploads_data volume mount
- README: add uploads_data to volumes table
2026-05-07 11:32:09 +08:00
Woody 563ef263ed docs: add DashScope API key to Docker prereqs, ffmpeg install guide, Phase 2 env vars 2026-05-07 11:30:30 +08:00
Woody 78d1f8cc91 feat: delta-based ASR transcript — use text field, utterance boundaries, stash on pause
Replace full_text responses with character-level deltas computed from
DashScope's monotonically-growing 'text' field. Stash-only events (empty
text) are skipped; trailing stash chars sent alongside deltas and
appended on pause to complete final sentences.

Backend:
- Delta = text[len(prev_text):] — simple suffix diff, no merge logic
- Track item_id for utterance boundaries, prepend space separator
- Send stash alongside delta for frontend pause handler

Frontend:
- Accumulate deltas locally (transcriptRef += msg.delta)
- Store lastStashRef from each message
- On pause: append stash to text, fire onFinalTranscript

Plan: .plans/phase2_enhancement_delta_sse.md updated to Complete
2026-05-07 11:26:19 +08:00
Woody cb0ac07786 fix: text accumulation — stashes are sliding windows, merge via overlap detection
DashScope stashes are ~7-char rolling windows, not cumulative. Each partial
event replaces the previous. Completed events rarely sent. This caused text to
jump/replace during streaming and disappear on pause.

Backend:
- Add _merge_stash() — finds overlapping suffix between successive stashes
  and appends only new characters, reconstructing full utterance from partials
- format_transcription_event returns raw stash for read_events to merge
- read_events maintains partial_buffer via _merge_stash, clears on completed
- Guard against empty/whitespace-only stashes

Frontend:
- transcriptRef + onFinalTranscriptRef avoid stale closures in pause handler
- stopStreaming fires onFinalTranscript(currentText) before clearing partial
- Removed blind setPartialTranscript('') that erased text on pause

Tests: 16/16 ws_protocol tests pass, frontend tests unchanged
Plan: Updated phase2_implementation_plan.md to Complete with 11-bug log
2026-05-06 20:06:39 +08:00
Woody fcb9ec1f6c fix: Phase 2 ASR pipeline — 9 bugs resolved, Full Transcript works end-to-end
- Vite proxy: forward /api and /ws to backend port 8000
- WebSocket URL: use backend host, not Vite HMR port
- LTTPage: callback ref replaces useRef (video element always null before)
- ws_asr: pass DashScope API key to OmniRealtimeConversation
- asr_client: fix data_url MIME type (audio/wav), omit extra_body when auto
- useFullTranscript: use absolute URL prefix for fetch
- QueryInput: add value prop for external Full Transcript injection
- QueryInput: fix displayValue || logic (partialText '' overrode question)
- ffmpeg: install static binary for audio extraction
- Integration tests: 7 tests (upload→transcribe flow)
- Acceptance tests: real DashScope tests (skippable)
- Structured logging: ws_asr.py + video.py
2026-05-06 18:26:17 +08:00