TRD: OmniAvatar Render — Technical Design¶
Status: Approved Date: 2026-07-16 Related: PRD.md, PLAN.md, ROADMAP.md
Package Layout¶
omniavatar-core/ # interfaces only, stdlib deps
├── live/ # RENAMED from avatar/ (real-time sessions)
│ ├── provider.go # live.Provider, live.SessionConfig
│ ├── session.go # live.Session, live.SessionCallbacks, live.Metrics
│ ├── audio.go # live.AudioDestination, live.AudioConfig
│ └── errors.go # live sentinel errors, live.ProviderError
├── render/ # NEW (batch generation)
│ ├── provider.go # render.Provider, render.AudioUploader
│ ├── job.go # render.Job, render.JobState, render.JobStatus, render.Wait
│ ├── request.go # render.GenerateRequest, render.Background
│ └── errors.go # render sentinel errors, render.ProviderError
└── registry/
└── registry.go # ProviderConfig + options (shared),
# LiveProviderFactory, RenderProviderFactory
omniavatar/ # provider implementations
├── registry.go # live + render global registries (symmetric API)
├── token.go # LiveKit token generation (unchanged)
├── start_options.go # LiveKitStartOptions (unchanged)
└── providers/
├── heygen/ # live provider + render.go (heygen-go/video)
├── tavus/ # live provider + render.go (tavus-go)
├── bithuman/ # live provider + render.go (bithuman-go), AudioUploader
└── all/ # unchanged (imports the three provider packages)
Part 1: Rename avatar/ → live/¶
Mechanical rename with semantic cleanups:
git mv avatar live;package avatar→package livein all files.- Error string prefixes
"avatar: ..."→"live: ...";ProviderError.Error()prefix"avatar/"→"live/"(Go convention: error strings identify the package). - All type and function names inside the package are unchanged (
Provider,Session,AudioDestination,SessionConfig,DefaultAudioConfig, ...). Call sites change fromavatar.Xtolive.X. registry.ProviderFactory→registry.LiveProviderFactory(making room forRenderProviderFactory).- Downstream (
omniavatar): imports, plus registry API renames for symmetry (see Part 3).
Part 2: render/ Package Interfaces¶
Provider¶
// Package render provides provider-agnostic interfaces for asynchronous
// (batch) avatar video generation: submit narration audio or a script,
// poll for completion, download a talking-head video.
//
// This is the offline counterpart to package live, which handles
// real-time streaming avatar sessions.
package render
// Provider generates avatar videos asynchronously.
type Provider interface {
// Name returns the provider name (e.g., "heygen", "tavus", "bithuman").
Name() string
// Generate submits a video generation job. It returns as soon as the
// provider accepts the job; use Status or Wait to track completion.
Generate(ctx context.Context, req GenerateRequest) (*Job, error)
// Status returns the current status of a job.
Status(ctx context.Context, jobID string) (*JobStatus, error)
// Download streams the completed video to dst.
// Returns ErrJobNotCompleted if the job has not completed successfully.
Download(ctx context.Context, jobID string, dst io.Writer) error
}
// AudioUploader is an optional capability for providers that can host
// local audio files. Callers should feature-detect:
//
// if up, ok := provider.(render.AudioUploader); ok {
// url, err = up.UploadAudio(ctx, "narration.mp3", f)
// }
//
// Providers without hosting support do not implement this interface;
// callers must supply a publicly fetchable GenerateRequest.AudioURL.
type AudioUploader interface {
// UploadAudio uploads audio content and returns a URL usable as
// GenerateRequest.AudioURL with the same provider.
UploadAudio(ctx context.Context, filename string, r io.Reader) (string, error)
}
Request¶
// GenerateRequest describes an avatar video generation job.
//
// Exactly one of AudioURL or Script must be set. AudioURL is the design
// center: it drives lip-sync from existing narration audio so the avatar
// matches the authoritative audio track exactly.
type GenerateRequest struct {
// AvatarID identifies the presenter with the provider.
// HeyGen: avatar_id; Tavus: replica_id; bitHuman: agent_id.
AvatarID string
// AudioURL is a fetchable URL to narration audio (.mp3/.wav).
AudioURL string
// Script is text for provider TTS (secondary path). Providers that
// require a voice read it from Extensions (e.g., "voice_id").
Script string
// Width, Height are the requested output dimensions (optional;
// provider defaults apply when zero).
Width, Height int
// Background requests a background treatment (optional, best-effort;
// see provider mapping table).
Background *Background
// Title is a human-readable job/video name (optional).
Title string
// Extensions holds provider-specific options
// (e.g., "voice_id", "avatar_style", "test", "fast").
Extensions map[string]any
}
// Background describes the requested video background.
type Background struct {
// Type is "color", "image", or "video".
Type string
// Value is a hex color or URL depending on Type.
Value string
}
Job and State¶
// JobState is the normalized lifecycle state of a generation job.
type JobState string
const (
JobStatePending JobState = "pending"
JobStateProcessing JobState = "processing"
JobStateCompleted JobState = "completed"
JobStateFailed JobState = "failed"
)
// Terminal reports whether the state is final.
func (s JobState) Terminal() bool
// Job identifies a submitted generation job.
type Job struct {
ID string // provider job/video ID
Provider string // provider name
}
// JobStatus is a point-in-time snapshot of a job.
type JobStatus struct {
ID string
State JobState
RawStatus string // provider-native status string, for logging/debugging
VideoURL string // set when State == JobStateCompleted
ThumbnailURL string // preview image, when the provider reports one (HeyGen)
Duration float64 // video duration in seconds, when reported
ErrorCode string // provider error code, when State == JobStateFailed
ErrorMsg string // provider error message, when State == JobStateFailed
}
// Wait polls Status until the job reaches a terminal state, the context
// is cancelled, or an error occurs. Interval <= 0 defaults to 3s.
// A JobStateFailed result returns the final status AND an error wrapping
// ErrJobFailed.
func Wait(ctx context.Context, p Provider, jobID string, interval time.Duration) (*JobStatus, error)
Errors¶
var (
// ErrAudioUploadUnsupported: provider cannot host audio; supply AudioURL.
ErrAudioUploadUnsupported = errors.New("render: audio upload unsupported")
// ErrInvalidRequest: request failed validation before submission
// (e.g., neither or both of AudioURL/Script set, missing AvatarID).
ErrInvalidRequest = errors.New("render: invalid request")
// ErrJobNotFound: the provider does not recognize the job ID.
ErrJobNotFound = errors.New("render: job not found")
// ErrJobFailed: the job reached JobStateFailed.
ErrJobFailed = errors.New("render: job failed")
// ErrJobNotCompleted: Download called before successful completion.
ErrJobNotCompleted = errors.New("render: job not completed")
)
// ProviderError mirrors live.ProviderError with a "render/" prefix.
type ProviderError struct {
Provider string
Op string
Err error
}
Part 3: Registry Symmetry¶
omniavatar-core/registry keeps the shared ProviderConfig / ProviderOption / With* helpers and gains one factory type per surface:
type LiveProviderFactory func(config ProviderConfig) (live.Provider, error)
type RenderProviderFactory func(config ProviderConfig) (render.Provider, error)
omniavatar (root package) hosts two independent registries with a symmetric API. The old *AvatarProvider* names are removed (breaking, v0.x):
| Old (removed) | Live surface | Render surface |
|---|---|---|
RegisterAvatarProvider |
RegisterLiveProvider |
RegisterRenderProvider |
GetAvatarProvider |
GetLiveProvider |
GetRenderProvider |
ListAvatarProviders |
ListLiveProviders |
ListRenderProviders |
HasAvatarProvider |
HasLiveProvider |
HasRenderProvider |
GetAvatarProviderPriority |
GetLiveProviderPriority |
GetRenderProviderPriority |
PriorityThin / PriorityThick apply to both registries. Provider packages register both surfaces in their register.go init().
Part 4: Provider Mappings¶
Normalized state mapping¶
render.JobState |
HeyGen | Tavus | bitHuman |
|---|---|---|---|
pending |
pending |
queued |
pending |
processing |
processing |
generating |
processing |
completed |
completed |
ready |
completed |
failed |
failed |
error, deleted |
failed |
RawStatus always carries the provider-native string.
HeyGen (providers/heygen/render.go)¶
- SDK:
heygen-goheygen.NewClient+video.NewClient(base URLapi.heygen.com; note this is the HeyGen API key, distinct from the LiveAvatar key used by the live provider). - Generate: one
video.VideoInputwithCharacter{Type: "avatar"|"talking_photo", AvatarID|TalkingPhotoID, AvatarStyle}andVoiceInput{Type: "audio", AudioURL}(orType: "text", InputText, VoiceID).Dimensionfrom Width/Height;Backgroundmaps natively (color/image/video);Testfrom extension. - Status:
GetStatus(videoID)→Video{Status, VideoURL, Duration, Error}. - Download: HTTP GET of
VideoURL(time-limited signed URL) streamed todst; status is re-fetched immediately before download. - Implements
AudioUploaderviaheygen-go/asset(upload.heygen.com/v1/asset, raw-body POST with the file's MIME type; MP3/audio/mpegis the documented audio asset type). The upload host is separate from the API host and overridable via theupload_base_urlextension. - Extensions:
talking_photo_id,avatar_style,voice_id,test,upload_base_url.
Tavus (providers/tavus/render.go)¶
- SDK:
tavus-go(ogen-generated).CreateVideo(&api.CreateVideoRequest{ReplicaID, AudioURL|Script, VideoName, ...})→CreateVideoResponse{VideoID};GetVideo(api.GetVideoParams{...})→Video{Status, DownloadURL, StreamURL, HostedURL}. AvatarIDmaps toreplica_id.- Background:
background_url(website recording) /background_source_url(video file) viaBackground{Type: "video"|"url"}best-effort; no color backgrounds. - Download: prefer
DownloadURL, fall back toStreamURL. AudioUploader: not implemented (Tavus has no upload API). Callers getAudioURL-only behavior; the capability check fails cleanly.- Extensions:
fast,transparent_background,callback_url. - ogen note: responses are sum types; implementations type-switch on the success variant and convert typed error variants into
render.ProviderErrorwrapping core sentinels.
bitHuman (providers/bithuman/render.go)¶
- SDK:
bithuman-go(ogen-generated).CreateVideo(&api.CreateVideoRequest{AgentID, AudioURL|Text+VoiceID})→VideoJob;GetVideo(api.GetVideoParams{...})→VideoJob{Status, VideoURL, DurationSeconds, Error}. AvatarIDmaps toagent_id.- Implements
AudioUploader:UploadFile(&api.UploadFileRequest{Base64, Filename, ContentType})→File{URL}. This provider validates the capability interface design. - Width/Height/Background: not supported by the API; documented as ignored.
- Extensions:
voice_id.
Part 5: Consumer Flow (videoascode, informative)¶
p, err := omniavatar.GetRenderProvider("bithuman",
omniavatar.WithAPIKey(os.Getenv("BITHUMAN_API_KEY")))
audioURL := cfg.AudioURL
if up, ok := p.(render.AudioUploader); ok && audioURL == "" {
audioURL, err = up.UploadAudio(ctx, "narration.mp3", f)
}
job, err := p.Generate(ctx, render.GenerateRequest{
AvatarID: cfg.AvatarID,
AudioURL: audioURL,
})
status, err := render.Wait(ctx, p, job.ID, 5*time.Second)
err = p.Download(ctx, job.ID, presenterFile)
Circle masking and overlay composition remain in videoascode (FFmpeg), per the internal ideation notes (IDEATION_CHAT_PRESENTATION.md, untracked).
Testing¶
omniavatar-core/render: unit tests forJobState.Terminal,Wait(fake provider: completion, failure, context cancellation, default interval), request validation helper if exported.omniavatar: registry tests for both surfaces (registration, priority override, get/list/has); per-provider state-mapping unit tests. Live API calls are out of scope for unit tests.
Breaking Changes (release notes)¶
omniavatar-corev0.2.0: packageavatarrenamed tolive;registry.ProviderFactoryrenamed toregistry.LiveProviderFactory; new packagerender.omniavatarv0.2.0: all*AvatarProvider*registry functions renamed to*LiveProvider*; new*RenderProvider*registry API; new render providers for heygen, tavus, bithuman.