Šķîþ ţö ḿàîñ çöñţéñţ

Ṽöîçé þŕöƒîļéš

Ŵĥéŕé ţéŕḿîñöļöĝý éñšüŕéš ýöü üšé ţĥé ŕîĝĥţ ŵöŕđš, à ṽöîçé þŕöƒîļé đéšçŕîƃéš ĥöŵ ýöü šàý ţĥéḿ: ţĥé þéŕšöñàļîţý, ƒöŕḿàļîţý, àñđ ŵŕîţîñĝ þàţţéŕñš ţĥàţ ḿàķé çöñţéñţ ŕéçöĝñîžàƃļé. ñéöķàþî çàþţüŕéš à ṽöîçé àš à ḿàçĥîñé-ŕéàđàƃļé þŕöƒîļé àñđ ŕüñš îţ àš öñé çĥéçķšéţ öṽéŕ ţĥé šàḿé çöñţéñţ-ṽéŕîƒîçàţîöñ éñĝîñé ţĥàţ þöŵéŕš ţéŕḿîñöļöĝý, đö-ñöţ-ţŕàñšļàţé, àñđ þļàçéĥöļđéŕ îñţéĝŕîţý: éṽéŕý çĥéçķéŕ éḿîţš ţĥé šàḿé ƒîñđîñĝš îñţö ţĥé šàḿé Block àññöţàţîöñ, šö ṽöîçé îš öñé çĥéçķ àḿöñĝ ḿàñý. Ţĥé Ĝö ļîƃŕàŕý ļîṽéš îñ core/profile/.

Üšéđ ţĥîš ŵàý, à ṽöîçé þŕöƒîļé ķééþš àñ ÀÎ àššîšţàñţ öñ-ṽöîçé ţĥé ŵàý à ţéšţ ķééþš çöđé çöŕŕéçţ: ļöàđ ţĥé þŕöƒîļé îñţö çöñţéẋţ (öŕ éẋþöšé îţ öṽéŕ ḾÇÞ) šö ĝéñéŕàţéđ çöþý îš öñ-ṽöîçé ƒŕöḿ ţĥé ƒîŕšţ đŕàƒţ, ţĥéñ çĥéçķ àñýţĥîñĝ ţĥàţ đŕîƒţš àñđ çàŕŕý ţĥé šàḿé ṽöîçé ţĥŕöüĝĥ éṽéŕý ţŕàñšļàţîöñ. Ţĥé ƒîñđîñĝš (ţĥé šþéçîƒîç ţéŕḿš àñđ ŕüļéš ţĥàţ ƃŕöķé) àŕé ţĥé šüƃšţàñçé; ţĥé 0–100 ŕöļļ-üþ îš à çöñṽéñîéñçé, ŕéļîàƃļé öñļý ŵĥéñ çàļîƃŕàţéđ àĝàîñšţ à ļàƃéļéđ šéţ.

Ṽöîçé þŕöƒîļéš ŵîţĥ ţĥé ÇĻÎ​

Ţĥé kapi voice çöḿḿàñđ ĝŕöüþ ŵöŕķš àĝàîñšţ à þŕöƒîļé ƒŕöḿ à ƃüîļţ-îñ šţàŕţéŕ þàçķ (--pack), ţĥé ļöçàļ ṽöîçé šţöŕé (--profile), öŕ à šţàñđàļöñé ĝîţ-šĥàŕéàƃļé ÝÀḾĻ ƒîļé (--profile-file):

# Print the rendered guide (paste into an assistant, or pipe to a file)
kapi voice show --pack friendly-dtc

# Score text: file argument, --input-text, or stdin. Exits 3 when a finding
# fails, so it gates CI. The default pass is rule-based and offline; --ai adds
# an LLM analysis of tone, style, and clarity, whose findings report.
kapi voice check --profile-file voice.yaml release-notes.md

# Rewrite off-voice content: deterministic substitution of the terms the
# profile's file carries, no model. A rule with no replacement is reported
# under "skipped" rather than applied.
kapi voice rewrite --profile-file voice.yaml --input-text "Leverage our solution"

# Ask a model for the inflected forms of each term, written as a diff
kapi terms expand --locale nb

# Manage profiles in the local store
kapi voice profiles

Ŵĥàţ ṽöîçé çĥéçķš àššéšš​

Ţĥé đéƒàüļţ ŵöŕđ-ŕüļé àñđ þàţţéŕñ çĥéçķš àššéšš éñçöđéđ ŕüļéš. kapi check --voice àđđš àđṽîšöŕý šîḿîļàŕîţý ţö þŕöƒîļé éẋàḿþļéš ţĥŕöüĝĥ ţĥé kapi-check þļüĝîñ, àñđ ŕéǫüéšţš ñö ĻĻḾ ĵüđĝḿéñţ. kapi voice check --ai àñđ ţĥé ŕàŵ voice-check ţööļ éẋþļîçîţļý ŕéǫüéšţ šéḿàñţîç ḿöđéļ ŕéṽîéŵ.

À çļéàñ ŕüļé ŕéšüļţ ḿéàñš ţĥöšé ŕüļéš ŕàîšéđ ñö ƒîñđîñĝ. Ŵĥéţĥéŕ à šéŕṽîçé đéšçŕîþţîöñ, þŕöḿîšé öŕ ƒàçţüàļ çļàîḿ îš çöŕŕéçţ šţàýš üñàššéššéđ. Ţĥé çàñöñîçàļ çĥéçķ ŕéþöŕţ îđéñţîƒîéš àñàļýšéš ţĥàţ ŕàñ, ŵéŕé ñöţ ŕéǫüéšţéđ öŕ çàññöţ ƃé àššéššéđ ƃý éẋàçţ ŕüļéš. Ŕéþöŕţš ŵîţĥöüţ éẋéçüţîöñ ḿéţàđàţà ĥàṽé üñŕéþöŕţéđ çöṽéŕàĝé. Àþþļîçàƃļé þŕöšé ĝüîđàñçé îš ŕéçöŕđéđ àš üñšüþþöŕţéđ ƃý đéţéŕḿîñîšţîç çĥéçķš; àñ éñçöđéđ þŕöĥîƃîţéđ éẋþŕéššîöñ çàñ þŕöđüçé à ŕüļé ƒîñđîñĝ.

Ŕéǫüéšţéđ àñàļýšîš ƒàîļüŕéš àŕé öþéŕàţîöñ éŕŕöŕš. Ḿîššîñĝ þŕöƒîļéš, üñàṽàîļàƃļé þļüĝîñš, þŕöṽîđéŕ éŕŕöŕš, ţîḿéöüţ àñđ çàñçéļļàţîöñ çàññöţ ýîéļđ à ñéŵ šüççéššƒüļ šçöŕé. Ţĥé ĻĻḾ ţööļ ṽàļîđàţéš šţŕüçţüŕéđ ƒîñđîñĝš ļöçàļļý: ḿàļƒöŕḿéđ ĴŠÖÑ, ḿîššîñĝ öŕ ñüļļ ƒîñđîñĝš, îñṽàļîđ ƒîéļđš àñđ îñṽàļîđ ƒîñđîñĝ ṽàļüéš àŕé éŕŕöŕš. Öñļý àñ éẋþļîçîţ éḿþţý ƒîñđîñĝš àŕŕàý îš à çöḿþļéţéđ çļéàñ ḿöđéļ ŕéšþöñšé. Éḿþţý çöñţéñţ šķîþš ḿöđéļ àñàļýšîš àñđ éḿîţš ñö šçöŕé.

Ŵĥàţ à þŕöƒîļé ĥöļđš​

À þŕöƒîļé çàþţüŕéš ţöñé, šţýļé ḿéàšüŕéš àñđ þàţţéŕñ ŕüļéš. Ŵöŕđ ŕüļéš ("ŵŕîţé ţĥîš, ñöţ ţĥàţ") àŕé ţéŕḿš, àñđ à ṽöîçé ƒîļé ḿàý çàŕŕý ţĥéḿ ƃéšîđé ţĥé ṽöîçé üñđéŕ à ţöþ-ļéṽéļ terms: ļîšţ:

name: "Acme Corp"
description: "Professional yet approachable B2B SaaS voice"

tone:
personality: [knowledgeable, helpful, confident]
formality: neutral
emotion: warm
humor: light

style:
active_voice: true
sentence_length: medium
person_pov: second # "you" / "your"
contractions: sometimes

terms:
- replacement: "workspace"
note: "Use instead of 'account' or 'organization'"
- term: "leverage"
replacement: "use"
advisory: true
- term: "Slack"
replacement: "messaging platform"
competitor: true

examples:
- before: "Users can leverage the platform to achieve synergy."
after: "Your team can use the workspace to collaborate more effectively."
explanation: "Active voice, preferred terms, removed jargon"
category: style

Þŕöƒîļéš šüþþöŕţ ļöçàļé öṽéŕŕîđéš (é.ĝ. formal àñđ ţĥîŕđ-þéŕšöñ ÞÖṼ ƒöŕ ja), çĥàññéļ öṽéŕŕîđéš (é.ĝ. çàšüàļ, ƒŕéǫüéñţ ĥüḿöŕ ƒöŕ social_media) àñđ þéŕšöñà öṽéŕŕîđéš (àñ îñđîṽîđüàļ àüţĥöŕ'š ṽöîçé). Çĥàññéļ àñđ þéŕšöñà öṽéŕŕîđéš ŕéþļàçé ŵĥöļé Ţöñé/Šţýļé šéçţîöñš; ļöçàļé öṽéŕŕîđéš ḿéŕĝé îñđîṽîđüàļ ƒîéļđš. Ñö öṽéŕŕîđé çàŕŕîéš ŵöŕđ ŕüļéš.

Ţĥŕéé ŕüļé ƒîéļđš đö ḿöšţ öƒ ţĥé ŵöŕķ ƃéýöñđ ţĥé éẋàḿþļé àƃöṽé:

  • À terms: éñţŕý îš à ţéŕḿ ŕüļé: term, replacement, note, advisory, competitor ƒöŕ à ŕîṽàļ'š ñàḿé, àñđ öþţîöñàļļý forms (ţĥé îñƒļéçţîöñš ţĥé éẋàçţ ḿàţçĥéŕ àļšö ŕéçöĝñîšéš), case_sensitive àñđ scope (prose, code, heading). À ŕüļé ŵîţĥ öñļý à replacement ñàḿéš à þŕéƒéŕŕéđ ƒöŕḿ àñđ ŕéĵéçţš ñöţĥîñĝ. kapi voice import àñđ kapi store import ḿöṽé à ƒîļé'š terms: îñţö ţĥé þŕöĵéçţ'š ţéŕḿš šţöŕé àñđ ŕéþöŕţ ĥöŵ ḿàñý; à ƒîļé ţĥàţ ļîšţš îţš ŵöŕđš üñđéŕ vocabulary: îš çöñṽéŕţéđ ţĥé šàḿé ŵàý. Ţĥé šàḿé šĥàþé îš ŵĥàţ à ƒļöŵ šţéþ àççéþţš üñđéŕ term_rules: ƒöŕ term-check, translate, recycle, dnt-check àñđ pseudo-translate. À üšé öƒ ţĥé ţéŕḿ ƒàîļš à çĥéçķ üñļéšš ţĥé ŕüļé îš ḿàŕķéđ advisory: true, ŵĥîçĥ ḿàķéš îţ ŕéþöŕţ. À ŕüļé ŵĥöšé replacement îš çàþîţàļîšéđ, šüçĥ àš à þŕöđüçţ ñàḿé (term: QuickCast, replacement: Quickcast), ḿàţçĥéš çàšé-šéñšîţîṽéļý, šö à ļöŵéŕ-çàšé ÜŔĻ šļüĝ quickcast îš ļéƒţ àļöñé; šö đöéš à ŕüļé ŵĥöšé term đéŕš ƒŕöḿ îţš replacement öñļý îñ çàšé. Àñý öţĥéŕ ŕüļé ḿàţçĥéš ŕéĝàŕđļéšš öƒ çàšé, àñđ case_sensitive: true öŕ false šéţš îţ éîţĥéŕ ŵàý.
  • À style.prohibited_patterns öŕ required_patterns éñţŕý îš à þàţţéŕñ: regex, description, fails, advisory, àñđ öþţîöñàļļý à rate (max ḿàţçĥéš þéŕ per_words, đéƒàüļţ 1,000) ţĥàţ ţüŕñš à ƃàñ îñţö à çéîļîñĝ, àñđ à scope. À þàţţéŕñ ŕéþöŕţš üñţîļ îţ îš ḿàŕķéđ fails: true, ŵĥîçĥ ḿàķéš à ḿàţçĥ ƒàîļ à çĥéçķ; advisory: true ķééþš îţ ŕéþöŕţîñĝ éîţĥéŕ ŵàý.
  • min_score îš ţĥé çöḿþļîàñçé ƃàŕ à ƃļöçķ ḿüšţ ŕéàçĥ ţö çöüñţ àš çöḿþļîàñţ îñ ŕöļļ-üþš; üñšéţ, öñé ƒàîļîñĝ ƒîñđîñĝ àļŕéàđý đŕöþš à ƃļöçķ ƃéļöŵ îţ.

Éṽéŕý ƒîéļđ îš ļîšţéđ îñ ţĥé ṽöîçé þŕöƒîļé ŕéƒéŕéñçé.

Çöḿþļîàñçé šçöŕîñĝ​

Çöḿþļîàñçé îš šçöŕéđ 0–100 àçŕöšš ƒîṽé đîḿéñšîöñš: Ţöñé, Šţýļé, Ṽöçàƃüļàŕý (ţĥé ŵöŕđ-ŕüļé ƒîñđîñĝš), Çļàŕîţý, àñđ öṽéŕàļļ ṽöîçé çöḿþļîàñçé. Éàçĥ ƒîñđîñĝ ŕéđüçéš ţĥé šçöŕé ƃý à ŵéîĝĥţ šéţ ƃý ŵĥàţ îţ đöéš ţö à çĥéçķ:

ƑîñđîñĝŴéîĝĥţÉẋàḿþļé
Ƒàîļš25Ƒöŕƃîđđéñ öŕ çöḿþéţîţöŕ ţéŕḿ
Ŕéþöŕţš1Àđṽîšöŕý ŕüļé, ţöñé ŕéàđîñĝ
Ŕàîšéđ ƃý à šüĝĝéšţéđ ŕüļé0À þŕöþöšéđ ŕüļé ñöţ ýéţ çöñƒîŕḿéđ

Ţĥé šçöŕé îš ŕéþöŕţéđ ƃéšîđé ţĥé ƒîñđîñĝš. Ŵĥéţĥéŕ à çĥéçķ ƒàîļš đéþéñđš öñļý öñ ŵĥéţĥéŕ à ƒîñđîñĝ ƒàîļš.

Šţàŕţéŕ þàçķš​

Ƃüîļţ-îñ þàçķš þŕöṽîđé ŕéàđý-ţö-üšé šţàŕţîñĝ þöîñţš (professional-b2b, friendly-dtc, technical-docs, marketing-blog, àñđ customer-support), éàçĥ ŵîţĥ ţöñé šéţţîñĝš, šţýļé ŕüļéš, ƃéƒöŕé/àƒţéŕ éẋàḿþļéš ţö çüšţöḿîžé, àñđ ţéŕḿš üñđéŕ terms:. Ŵĥéŕé à þàçķ îš ƃöüñđ àš ţĥé ṽöîçé, îţš ţéŕḿš àþþļý ƃéšîđé ţĥé þŕöĵéçţ'š öŵñ, àñđ à ƒîñđîñĝ ţĥéý ŕàîšé ñàḿéš ţĥé þàçķ (pack technical-docs).

Þîþéļîñé îñţéĝŕàţîöñ​

Ţĥé voice-check ţööļ ŕüñš îñ ţĥé þîþéļîñé àļöñĝšîđé öţĥéŕ ţööļš:

recyclechanterm-checkchantranslateLLMchanvoice-checkLLMchanqaLLM

Îţ üšéš àñ ĻĻḾ ţö àñàļýžé çöñţéñţ àĝàîñšţ ţĥé þŕöƒîļé àñđ àţţàçĥéš çöḿþļîàñçé šçöŕéš àñđ ƒîñđîñĝš ţö éàçĥ Ƃļöçķ àš àññöţàţîöñš. Ţĥé ƒàšţéŕ, ŕüļé-ƃàšéđ voice-vocab-check ţööļ çĥéçķš ţĥé ṽöîçé'š þàţţéŕñš àñđ ţĥé ƒöŕƃîđđéñ, çöḿþéţîţöŕ àñđ ŕéţîŕéđ ţéŕḿš ţĥàţ ĝöṽéŕñ ţĥé ţéẋţ ŵîţĥöüţ ĻĻḾ çàļļš. Ŵöŕđ ŕüļéš àŕé ţéŕḿš, šö ţĥéý àļšö ƒļöŵ ţĥŕöüĝĥ ţĥé öŕđîñàŕý ţéŕḿîñöļöĝý ţööļš: term-check àñđ dnt-check ţàķé ţĥéḿ àš term_rules:, àñđ ṽöîçé ĝüàŕđŕàîļš àñđ ţéŕḿîñöļöĝý šĥàŕé öñé éñƒöŕçéḿéñţ þàţĥ. Ţĥé term-lookup àñđ term-enforce šţàĝéš îñ terms/tool.go àŕé ļîƃŕàŕý çöđé ŕàţĥéŕ ţĥàñ ŕéçîþé ñàḿéš: ţĥé ŕüññéŕ àþþéñđš ţĥéḿ ƃéĥîñđ àñý ţööļ ŵĥöšé šçĥéḿà ŕéǫüîŕéš ţéŕḿš, àñđ ñéîţĥéŕ àþþéàŕš îñ kapi tools.

ḾÇÞ îñţéĝŕàţîöñ​

ÀÎ àĝéñţš ŕéàçĥ ṽöîçé çĥéçķîñĝ ţĥŕöüĝĥ ţĥé kapi mcp šéŕṽéŕ:

{
"mcpServers": {
"kapi": {
"command": "kapi",
"args": ["mcp"]
}
}
}

Àĝéñţš çàñ šçöŕé çöñţéñţ ƒöŕ ṽöîçé çöḿþļîàñçé ŵîţĥ ţĥé voice_check ḾÇÞ ţööļ àñđ ŕéŵŕîţé öƒƒ-ṽöîçé çöþý ŵîţĥ voice_rewrite, ŵĥîçĥ ļîšţš üñđéŕ skipped ţĥé ţéŕḿš îţ ḿàţçĥéđ àñđ çöüļđ ñöţ ŕéþļàçé. Àñ àĝéñţ ŕéàđš ţĥé ṽöîçé îţšéļƒ ƒŕöḿ ţĥé šéçţîöñ ķàþî ŵŕîţéš îñţö ţĥé þŕöĵéçţ'š AGENTS.md àñđ CLAUDE.md. Ƒöŕ ţĥé ƒüļļ ṽöîçé öƒ öñé ƒîļé, kapi context <path> þŕîñţš îţ ŵîţĥ ţĥé ţéŕḿš ţĥàţ àþþļý ţĥéŕé, àñđ ţĥé context_read ţööļ àñđ ţĥé context://<path> ŕéšöüŕçé ŕéţüŕñ ţĥé šàḿé àñšŵéŕ. Šéŕṽéŕ đéþļöýḿéñţš çàñ éẋþöšé àñ ĤŢŢÞ ḾÇÞ éñđþöîñţ šö àĝéñţš çöñšüḿé þŕöƒîļéš àñđ šçöŕîñĝ ŵîţĥöüţ à ļöçàļ ÇĻÎ þŕöçéšš.

Ĝö ļîƃŕàŕý​

Šţöŕé​

type Store interface {
// Profile CRUD; scope is the storing host's partition key (empty for the local CLI)
CreateProfile(ctx context.Context, profile *VoiceProfile) error
GetProfile(ctx context.Context, id string) (*VoiceProfile, error)
UpdateProfile(ctx context.Context, profile *VoiceProfile) error
DeleteProfile(ctx context.Context, id string) error
ListProfiles(ctx context.Context, scope string) ([]*VoiceProfile, error)

// Version history and named tags
ListProfileVersions(ctx context.Context, profileID string) ([]*ProfileVersion, error)
GetProfileVersion(ctx context.Context, profileID string, version int) (*ProfileVersion, error)
GetProfileAtTag(ctx context.Context, profileID, tagName string) (*VoiceProfile, error)
CreateProfileTag(ctx context.Context, tag *ProfileTag) error
ListProfileTags(ctx context.Context, profileID string) ([]*ProfileTag, error)
DeleteProfileTag(ctx context.Context, profileID, tagName string) error

// Scores
StoreScore(ctx context.Context, score *StoredScore) error
GetScores(ctx context.Context, projectID string, locale model.LocaleID) ([]*StoredScore, error)
GetScoreTrends(ctx context.Context, projectID string, days int) ([]*ScoreTrend, error)
GetScoresByStream(ctx context.Context, projectID, stream string) ([]*StoredScore, error)

// Corrections and the candidate rules derived from them
StoreCorrection(ctx context.Context, correction *Correction) error
GetSuggestedRules(ctx context.Context, scope string, minCount int) ([]*SuggestedRule, error)
RecordRuleDecision(ctx context.Context, d *RuleDecision) error
GetRuleDecision(ctx context.Context, profileID, term string) (*RuleDecision, error)
ListRuleDecisions(ctx context.Context, profileID string) ([]*RuleDecision, error)

Close() error
}

StoredScore, ScoreTrend, àñđ ţĥé öţĥéŕ üñǫüàļîƒîéđ ţýþéš àŕé đéçļàŕéđ îñ ţĥé profile þàçķàĝé; model.LocaleID îš ţĥé ƂÇÞ-47 ļöçàļé ţýþé ƒŕöḿ github.com/neokapi/neokapi/core/model.

Ţĥé ƒŕàḿéŵöŕķ šĥîþš à ŠǪĻîţé ƃàçķéñđ (voice/sqlite.go) ƃüîļţ öñ ţĥé šĥàŕéđ core/storage ḿîĝŕàţîöñ šýšţéḿ, ŵîţĥ ĴŠÖÑ çöļüḿñš ƒöŕ ţĥé çöḿþļéẋ ţöñé àñđ šţýļé ƒîéļđš. Îñšîđé à þŕöĵéçţ ţĥé ṽöîçé ţàƃļéš ļîṽé îñ ţĥé þŕöĵéçţ'š öŵñ šţöŕé; à šţàñđàļöñé šţöŕé îš voice.db. Šéŕṽéŕ đéþļöýḿéñţš çàñ àđđ à šçöþé-þàŕţîţîöñéđ ÞöšţĝŕéŠǪĻ ƃàçķéñđ ƃéĥîñđ ţĥé šàḿé îñţéŕƒàçé.

Šçöŕîñĝ àñđ ŕéšöļüţîöñ​

import "github.com/neokapi/neokapi/core/profile"

findings := []profile.VoiceFinding{
{Dimension: profile.DimensionVocabulary, Severity: profile.SeverityMajor,
Message: "Forbidden term: leverage", Suggestion: "use"},
{Dimension: profile.DimensionTone, Severity: profile.SeverityMinor,
Message: "Tone is too formal for this profile"},
}
score := profile.CalculateScore(findings) // score.Overall = 94 (100 - 5 - 1)

// ResolveProfile layers locale, then channel, then persona overrides on a base
// profile
resolved := profile.ResolveProfile(base, "ja", "", "")

Þîþéļîñé ţööļš​

import (
aitool "github.com/neokapi/neokapi/core/ai/tools"
"github.com/neokapi/neokapi/core/profile"
"github.com/neokapi/neokapi/core/tools"
)

// LLM-based: structured findings scored via CalculateScore, attached as a
// VoiceAnnotation plus voice-score / voice-findings properties
checkTool := aitool.NewVoiceCheckTool(llmProvider, profile)

// Rule-based: word rules and prohibited patterns, no LLM calls
vocabTool := tools.NewVoiceVocabCheckTool(profile, terminology)

Šţàŕţéŕ þàçķš​

import "github.com/neokapi/neokapi/core/profile/packs"

names, _ := packs.List() // the five built-in pack names
profile, _ := packs.Load("professional-b2b")
all, _ := packs.LoadAll()

Þàçķš àŕé ÝÀḾĻ ƒîļéš éḿƃéđđéđ ṽîà go:embed; éàçĥ ŕéţüŕñš à *profile.VoiceProfile ŕéàđý ţö üšé öŕ çüšţöḿîžé, çàŕŕýîñĝ ţĥé þàçķ'š ţéŕḿš (CarriedTerms, ƒŕöḿ pack <name>).

Çöñţéñţ ḿöđéļ îñţéĝŕàţîöñ​

VoiceAnnotation îš à ŕéĝîšţéŕéđ þàýļöàđ (voice) šţöŕéđ àš à ƃļöçķ-šçöþéđ àññöţàţîöñ (Ƒ-02), ţĥé çöüñţéŕþàŕţ ţö þöšîţîöñàļ öṽéŕļàýš ļîķé term àñđ entity. Îţ îš ŕéàçĥéđ ţĥŕöüĝĥ ţĥé ƃļöçķ'š Anno/SetAnno ĥéļþéŕš àñđ ŕéĝîšţéŕéđ ƒöŕ ŵîŕé/šţöŕé ŕéĥýđŕàţîöñ ṽîà model.RegisterPayload:

type VoiceAnnotation struct {
ProfileID string `json:"profile_id"`
Score int `json:"score"` // 0-100 overall
Findings []VoiceFinding `json:"findings"`
Position model.Anchor `json:"position"`
}

func (a *VoiceAnnotation) AnnotationType() string { return "voice" }

Þŕöƒîļéš šéŕîàļîžé àš ƃöţĥ ĴŠÖÑ àñđ ÝÀḾĻ, šö ţĥéý çàñ ƃé àüţĥöŕéđ ƃý ĥàñđ öŕ çöñšţŕüçţéđ þŕöĝŕàḿḿàţîçàļļý àš à *profile.VoiceProfile.

Ŕéļàţéđ​