Gå til hovedinnhold

Voice profiles

Where terminology ensures you use the right words, a voice profile describes how you say them: the personality, formality, and writing patterns that make content recognizable. neokapi captures a voice as a machine-readable profile and runs it as one checkset over the same content-verification engine that powers terminology, do-not-translate, and placeholder integrity: every checker emits the same findings into the same Block annotation, so voice is one check among many. The Go library lives in core/profile/.

Used this way, a voice profile keeps an AI assistant on-voice the way a test keeps code correct: load the profile into context (or expose it over MCP) so generated copy is on-voice from the first draft, then check anything that drifts and carry the same voice through every translation. The findings (the specific terms and rules that broke) are the substance; the 0–100 roll-up is a convenience, reliable only when calibrated against a labeled set.

Voice profiles with the CLI

The kapi voice command group works against a profile from a built-in starter pack (--pack), the local voice store (--profile), or a standalone git-shareable YAML file (--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

What voice checks assess

The default vocabulary and pattern checks assess encoded rules. kapi check --voice adds advisory similarity to profile examples through the kapi-check plugin; it does not request an LLM judgment. kapi voice check --ai and the raw voice-check tool explicitly request semantic model review.

A clean rule result means no findings in those rules. It does not establish that a service description, promise or factual claim is correct. The canonical check report identifies analyses that ran, were not requested or cannot be assessed by exact rules. Reports without execution metadata have unreported coverage. Applicable prose guidance is recorded as unsupported by deterministic checks; an encoded prohibited expression can produce a rule finding.

Requested analysis failures are operation errors. Missing profiles, unavailable plugins, provider errors, timeout and cancellation cannot yield a new successful score. The LLM tool validates structured findings locally: malformed JSON, missing or null findings, invalid fields and invalid finding values are errors. Only an explicit empty findings array is a completed clean model response. Empty content skips model analysis and emits no score.

Voice profiles

A profile captures tone, style, and vocabulary as rules:

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

Profiles support locale overrides (e.g. formal and third-person POV for ja), channel overrides (e.g. casual, frequent humor for social_media) and persona overrides (an individual author's voice). Channel and persona overrides replace whole Tone/Style sections, and their vocabulary can only tighten the profile's; locale overrides merge individual fields.

Three rule fields do most of the work beyond the example above:

  • A vocabulary entry is a term rule: term, replacement, severity, and optionally forms (the inflections the exact matcher also recognises, which kapi voice expand fills in), case_sensitive, scope (prose, code, heading), do_not_translate, and a concept_id tying the rule to a concept in the terms store. The same shape is what a flow step accepts under term_rules: for term-check, translate, recycle, dnt-check and pseudo-translate, so a rule authored in the profile and a rule handed to a tool read identically.
  • A style.prohibited_patterns or required_patterns entry is a pattern: regex, description, severity, and optionally a rate (max matches per per_words, default 1,000) that turns a ban into a ceiling, and a scope.
  • min_score is the compliance bar a block must reach to count as compliant in roll-ups; unset, one critical vocabulary hit already drops a block below it.

Every field is listed in the voice profile reference.

Compliance scoring

Compliance is scored 0–100 across five dimensions: Tone, Style, Vocabulary, Clarity, and overall voice compliance. Each finding reduces the score by its severity weight:

SeverityWeightExample
Neutral0Informational note
Minor1Slight tone inconsistency
Major5Wrong term used
Critical25Competitor term used

Starter packs

Built-in packs provide ready-to-use starting points (professional-b2b, friendly-dtc, technical-docs, marketing-blog, and customer-support), each with tone settings, style rules, vocabulary constraints, and before/after examples to customize.

Pipeline integration

The voice-check tool runs in the pipeline alongside other tools:

recyclechanterm-checkchantranslateLLMchanvoice-checkLLMchanqaLLM

It uses an LLM to analyze content against the profile and attaches compliance scores and findings to each Block as annotations. The faster, rule-based voice-vocab-check tool checks forbidden and competitor terms without LLM calls. Voice vocabulary also flows through the ordinary terminology tools: term-check and dnt-check are the registered tools, and term-check takes the profile's rules as term_rules:, so a forbidden or competitor term fires there too, and voice guardrails and terminology share one enforcement path. The term-lookup and term-enforce stages in terms/tool.go are library code rather than recipe names: the runner appends them behind any tool whose schema requires terms, and neither appears in kapi tools.

MCP integration

AI agents reach voice checking through the kapi mcp server:

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

Agents can score content for voice compliance with the voice_check MCP tool and rewrite off-voice copy with voice_rewrite, which lists under skipped the terms it matched and could not replace. The guide itself is read rather than called: kapi voice guide prints it, and the context://<path> resource returns it for the point a file sits at, with the terms bound there. Server deployments can expose an HTTP MCP endpoint so agents consume profiles and scoring without a local CLI process.

Go library

Store

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, and the other unqualified types are declared in the profile package; model.LocaleID is the BCP-47 locale type from github.com/neokapi/neokapi/core/model.

The framework ships a SQLite backend (voice/sqlite.go) built on the shared core/storage migration system, with JSON columns for the complex tone/style/vocabulary fields. Inside a project the voice tables live in the shared .kapi/work/store.db; a standalone store is voice.db. The interface is designed for extension: server deployments can add a scope-partitioned PostgreSQL backend.

Scoring and resolution

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", "", "")

Pipeline tools

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)

Starter packs

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()

Packs are YAML files embedded via go:embed; each returns a *profile.VoiceProfile ready to use or customize.

Content model integration

VoiceAnnotation is a registered payload (voice) stored as a block-scoped annotation (F-02), the counterpart to positional overlays like term and entity. It is reached through the block's Anno/SetAnno helpers and registered for wire/store rehydration via 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" }

Profiles serialize as both JSON and YAML, so they can be authored by hand or constructed programmatically as a *profile.VoiceProfile.