Ṽöîçé þŕöƒîļéš
Ŵĥéŕé ţéŕḿîñöļöĝý éñšüŕéš ýöü üšé ţĥé ŕîĝĥţ ŵöŕđš,
à ṽöîçé þŕöƒîļé đéšçŕîƃéš ĥöŵ ýöü šàý ţĥéḿ: ţĥé þéŕšöñàļîţý, ƒöŕḿàļîţý, àñđ
ŵŕîţîñĝ þàţţéŕñš ţĥàţ ḿàķé çöñţéñţ ŕéçöĝñîžàƃļé. neokapi çàþţüŕéš à ṽöîçé
àš à ḿàçĥîñé-ŕéàđàƃļé þŕöƒîļé àñđ ŕüñš îţ àš öñé çĥéçķšéţ öṽéŕ ţĥé šàḿé
çöñţéñţ-ṽéŕîƒîçàţîöñ éñĝîñé ţĥàţ þöŵéŕš ţéŕḿîñöļöĝý,
đö-ñöţ-ţŕàñšļàţé, àñđ þļàçéĥöļđéŕ îñţéĝŕîţý: éṽéŕý çĥéçķéŕ éḿîţš ţĥé šàḿé
ƒîñđîñĝš îñţö ţĥé šàḿé 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 guide --pack friendly-dtc
# Score text: file argument, --input-text, or stdin. --min-score gates CI (exit 3).
# The default pass is rule-based and offline; --ai adds an LLM analysis of tone,
# style, and clarity.
kapi voice check --profile-file voice.yaml --min-score 80 release-notes.md
# Rewrite off-voice content: deterministic vocabulary substitution, 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 vocabulary term, written as a diff
kapi voice expand --profile-file voice.yaml --language nb
# Manage profiles in the local store
kapi voice profiles
Ŵĥàţ ṽöîçé çĥéçķš àššéšš
Ţĥé đéƒàüļţ ṽöçàƃüļàŕý àñđ þàţţéŕñ çĥéçķš àššéšš éñçöđéđ ŕüļéš. kapi check --voice àđđš àđṽîšöŕý šîḿîļàŕîţý ţö þŕöƒîļé éẋàḿþļéš ţĥŕöüĝĥ ţĥé kapi-check
þļüĝîñ; îţ đöéš ñöţ ŕéǫüéšţ àñ ĻĻḾ ĵüđĝḿéñţ. kapi voice check --ai àñđ ţĥé ŕàŵ
voice-check ţööļ éẋþļîçîţļý ŕéǫüéšţ šéḿàñţîç ḿöđéļ ŕéṽîéŵ.
À çļéàñ ŕüļé ŕéšüļţ ḿéàñš ñö ƒîñđîñĝš îñ ţĥöšé ŕüļéš. Îţ đöéš ñöţ éšţàƃļîšĥ ţĥàţ à šéŕṽîçé đéšçŕîþţîöñ, þŕöḿîšé öŕ ƒàçţüàļ çļàîḿ îš çöŕŕéçţ. Ţĥé çàñöñîçàļ çĥéçķ ŕéþöŕţ îđéñţîƒîéš àñàļýšéš ţĥàţ ŕàñ, ŵéŕé ñöţ ŕéǫüéšţéđ öŕ çàññöţ ƃé àššéššéđ ƃý éẋàçţ ŕüļéš. Ŕéþöŕţš ŵîţĥöüţ éẋéçüţîöñ ḿéţàđàţà ĥàṽé üñŕéþöŕţéđ çöṽéŕàĝé. Àþþļîçàƃļé þŕöšé ĝüîđàñçé îš ŕéçöŕđéđ àš üñšüþþöŕţéđ ƃý đéţéŕḿîñîšţîç çĥéçķš; àñ éñçöđéđ þŕöĥîƃîţéđ éẋþŕéššîöñ çàñ þŕöđüçé à ŕüļé ƒîñđîñĝ.
Ŕéǫüéšţéđ àñàļýšîš ƒàîļüŕéš àŕé öþéŕàţîöñ éŕŕöŕš. Ḿîššîñĝ þŕöƒîļéš, üñàṽàîļàƃļé þļüĝîñš, þŕöṽîđéŕ éŕŕöŕš, ţîḿéöüţ àñđ çàñçéļļàţîöñ çàññöţ ýîéļđ à ñéŵ šüççéššƒüļ šçöŕé. Ţĥé ĻĻḾ ţööļ ṽàļîđàţéš šţŕüçţüŕéđ ƒîñđîñĝš ļöçàļļý: ḿàļƒöŕḿéđ ĴŠÖÑ, ḿîššîñĝ öŕ ñüļļ ƒîñđîñĝš, îñṽàļîđ ƒîéļđš àñđ îñṽàļîđ ƒîñđîñĝ ṽàļüéš àŕé éŕŕöŕš. Öñļý àñ éẋþļîçîţ éḿþţý ƒîñđîñĝš àŕŕàý îš à çöḿþļéţéđ çļéàñ ḿöđéļ ŕéšþöñšé. Éḿþţý çöñţéñţ šķîþš ḿöđéļ àñàļýšîš àñđ éḿîţš ñö šçöŕé.
Ṽöîçé þŕöƒîļéš
À þŕöƒîļé çàþţüŕéš ţöñé, šţýļé, àñđ ṽöçàƃüļàŕý àš ŕüļéš:
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
vocabulary:
preferred_terms:
- term: "workspace"
note: "Use instead of 'account' or 'organization'"
forbidden_terms:
- term: "leverage"
replacement: "use"
severity: minor
competitor_terms:
- term: "Slack"
replacement: "messaging platform"
severity: critical
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) àñđ þéŕšöñà öṽéŕŕîđéš (àñ îñđîṽîđüàļ àüţĥöŕ'š ṽöîçé).
Çĥàññéļ àñđ þéŕšöñà öṽéŕŕîđéš ŕéþļàçé ŵĥöļé Ţöñé/Šţýļé šéçţîöñš, àñđ ţĥéîŕ
ṽöçàƃüļàŕý çàñ öñļý ţîĝĥţéñ ţĥé þŕöƒîļé'š; ļöçàļé öṽéŕŕîđéš ḿéŕĝé îñđîṽîđüàļ
ƒîéļđš.
Ţĥŕéé ŕüļé ƒîéļđš đö ḿöšţ öƒ ţĥé ŵöŕķ ƃéýöñđ ţĥé éẋàḿþļé àƃöṽé:
- À ṽöçàƃüļàŕý éñţŕý îš à ţéŕḿ ŕüļé:
term,replacement,severity, àñđ öþţîöñàļļýforms(ţĥé îñƒļéçţîöñš ţĥé éẋàçţ ḿàţçĥéŕ àļšö ŕéçöĝñîšéš, ŵĥîçĥkapi voice expandƒîļļš îñ),case_sensitive,scope(prose,code,heading),do_not_translate, àñđ àconcept_idţýîñĝ ţĥé ŕüļé ţö à çöñçéþţ îñ ţĥé ţéŕḿš šţöŕé. Ţĥé šàḿé šĥàþé îš ŵĥàţ à ƒļöŵ šţéþ àççéþţš üñđéŕterm_rules:ƒöŕterm-check,translate,recycle,dnt-checkàñđpseudo-translate, šö à ŕüļé àüţĥöŕéđ îñ ţĥé þŕöƒîļé àñđ à ŕüļé ĥàñđéđ ţö à ţööļ ŕéàđ îđéñţîçàļļý. - À
style.prohibited_patternsöŕrequired_patternséñţŕý îš à þàţţéŕñ:regex,description,severity, àñđ öþţîöñàļļý àrate(maxḿàţçĥéš þéŕper_words, đéƒàüļţ 1,000) ţĥàţ ţüŕñš à ƃàñ îñţö à çéîļîñĝ, àñđ àscope. min_scoreîš ţĥé çöḿþļîàñçé ƃàŕ à ƃļöçķ ḿüšţ ŕéàçĥ ţö çöüñţ àš çöḿþļîàñţ îñ ŕöļļ-üþš; üñšéţ, öñé çŕîţîçàļ ṽöçàƃüļàŕý ĥîţ àļŕéàđý đŕöþš à ƃļöçķ ƃéļöŵ îţ.
Éṽéŕý ƒîéļđ îš ļîšţéđ îñ ţĥé ṽöîçé þŕöƒîļé ŕéƒéŕéñçé.
Çöḿþļîàñçé šçöŕîñĝ
Çöḿþļîàñçé îš šçöŕéđ 0–100 àçŕöšš ƒîṽé đîḿéñšîöñš: Ţöñé, Šţýļé, Ṽöçàƃüļàŕý, Çļàŕîţý, àñđ öṽéŕàļļ ṽöîçé çöḿþļîàñçé. Éàçĥ ƒîñđîñĝ ŕéđüçéš ţĥé šçöŕé ƃý îţš šéṽéŕîţý ŵéîĝĥţ:
| Šéṽéŕîţý | Ŵéîĝĥţ | Éẋàḿþļé |
|---|---|---|
Neutral | 0 | Îñƒöŕḿàţîöñàļ ñöţé |
Minor | 1 | Šļîĝĥţ ţöñé îñçöñšîšţéñçý |
Major | 5 | Ŵŕöñĝ ţéŕḿ üšéđ |
Critical | 25 | Çöḿþéţîţöŕ ţéŕḿ üšéđ |
Šţàŕţéŕ þàçķš
Ƃüîļţ-îñ þàçķš þŕöṽîđé ŕéàđý-ţö-üšé šţàŕţîñĝ þöîñţš (professional-b2b,
friendly-dtc, technical-docs, marketing-blog, àñđ customer-support),
éàçĥ ŵîţĥ ţöñé šéţţîñĝš, šţýļé ŕüļéš, ṽöçàƃüļàŕý çöñšţŕàîñţš, àñđ ƃéƒöŕé/àƒţéŕ
éẋàḿþļéš ţö çüšţöḿîžé.
Þîþéļîñé îñţéĝŕàţîöñ
Ţĥé voice-check ţööļ ŕüñš îñ ţĥé þîþéļîñé àļöñĝšîđé öţĥéŕ ţööļš:
Îţ üšéš àñ ĻĻḾ ţö àñàļýžé çöñţéñţ àĝàîñšţ ţĥé þŕöƒîļé àñđ àţţàçĥéš çöḿþļîàñçé
šçöŕéš àñđ ƒîñđîñĝš ţö éàçĥ Ƃļöçķ àš àññöţàţîöñš. Ţĥé ƒàšţéŕ, ŕüļé-ƃàšéđ
voice-vocab-check ţööļ çĥéçķš ƒöŕƃîđđéñ àñđ çöḿþéţîţöŕ ţéŕḿš ŵîţĥöüţ ĻĻḾ
çàļļš. Ṽöîçé ṽöçàƃüļàŕý àļšö ƒļöŵš ţĥŕöüĝĥ ţĥé öŕđîñàŕý ţéŕḿîñöļöĝý ţööļš:
term-check àñđ dnt-check àŕé ţĥé ŕéĝîšţéŕéđ ţööļš, àñđ term-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
ţĥé ţéŕḿš îţ ḿàţçĥéđ àñđ çöüļđ ñöţ ŕéþļàçé. Ţĥé ĝüîđé îţšéļƒ îš ŕéàđ
ŕàţĥéŕ ţĥàñ çàļļéđ: kapi voice guide þŕîñţš îţ, àñđ ţĥé 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 ḿîĝŕàţîöñ šýšţéḿ, ŵîţĥ ĴŠÖÑ çöļüḿñš ƒöŕ ţĥé çöḿþļéẋ
ţöñé/šţýļé/ṽöçàƃüļàŕý ƒîéļđš. Îñšîđé à þŕöĵéçţ ţĥé ṽöîçé ţàƃļéš ļîṽé îñ ţĥé
šĥàŕéđ .kapi/work/store.db; à šţàñđàļöñé šţöŕé îš 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; a persona's vocabulary can only tighten the profile's
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: fast forbidden/competitor-term enforcement, 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 ŕéàđý ţö üšé öŕ çüšţöḿîžé.
Çöñţéñţ ḿöđéļ îñţéĝŕàţîöñ
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.