Don’t Send the Whole Camera: Build One-Shot Visual Context for a Tencent RTC Voice Companion A developer demonstrates a one-shot visual context system for Tencent RTC voice companions, using TypeScript and Gemini, to avoid continuous camera access. The approach limits visual input to approved frames, reducing privacy risks and data costs while ensuring the model answers from current context. A multimodal voice companion creates an awkward product tension: users want to ask “What am I looking at?” without granting an AI system indefinite access to their camera. The easiest implementation—forwarding frames continuously—also creates hidden costs. It increases data transfer and model work, makes visual context harder to reproduce, and leaves users unsure when the companion is actually observing them. It can also produce a subtler correctness bug: the model answers from an old frame while speaking as if it can see the present. A better default for many companion experiences is one-shot visual context : This tutorial builds that control layer in TypeScript. Tencent RTC supplies the real-time conversational setting, while Gemini sits behind an application-owned multimodal model port. We will not treat the model as the camera controller, consent authority, speech recognizer, or media transport. Multimodal capability does not automatically justify continuous vision. Choose the smallest visual scope that supports the task. | User task | Visual policy | Trade-off | |---|---|---| | Identify an object or read a label | One approved frame | Low exposure, but the user may need to recapture | | Compare two arrangements | Two explicitly labelled frames | More application state and UI work | | Explain ongoing movement | Time-bounded video may be necessary | Higher privacy, bandwidth, and moderation cost | | General voice companionship | Camera off by default | The companion cannot answer visual questions until invited | A still frame is the wrong abstraction for motion. If someone asks whether their exercise form remains correct over ten seconds, do not send one image and let the model imply that it observed the full movement. The demonstrated capability is narrower: a multimodal model can receive a bounded text-and-image request. The hype-shaped interpretation—that it continuously understands the user’s environment—is a product decision your application should not silently make. A production conversational pipeline should remain separable: php Microphone - RTC/media transport - speech recognition - application turn coordinator - approved visual snapshot - Gemini/model adapter - output moderation - speech synthesis - RTC/media transport - user Tencent Conversational AI is documented as a real-time voice interaction scenario that can work with multiple LLM providers. Its overview is the appropriate starting point for the voice architecture: Tencent RTC also documents LLM configuration, including OpenAI-compatible model connections, agent platforms such as Dify or Coze, and request identifiers used for routing and observability: Do not infer from voice connectivity that a selected model route accepts images. Validate multimodal support for the exact provider and model configuration you operate. A text-only route should fail as text-only, not quietly discard the image and produce a confident answer. The visual permission and the voice turn are related, but they are not the same state. Our snapshot can be: php off - capturing - ready - consumed off - capturing - unavailable capturing/ready/requesting - off, when the user withdraws it A monotonically increasing visualEpoch invalidates asynchronous work. Every capture and withdrawal advances the epoch. A callback may mutate state only if it still belongs to the current epoch. The important invariants are: mkdir one-shot-visual-companion cd one-shot-visual-companion npm init -y npm install --save-dev typescript tsx @types/node npx tsc --init mkdir src Add these scripts to package.json : { "scripts": { "check": "tsc --noEmit", "test": "tsx --test src/ .test.ts" } } Use a strict TypeScript configuration: { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "noUncheckedIndexedAccess": true } } Create src/visual-turn.ts : js import { randomUUID } from 'node:crypto'; export type Snapshot = { id: string; capturedAt: number; mimeType: 'image/jpeg' | 'image/png'; bytes: Uint8Array; }; type VisualState = | { kind: 'off' } | { kind: 'capturing'; epoch: number } | { kind: 'ready'; epoch: number; snapshot: Snapshot } | { kind: 'unavailable'; epoch: number; reason: string }; export interface CameraPort { captureOneFrame : Promise