Phase 5: voice consistency pass
Tier-1 voice-consistency pass: whole-document LLM review surfacing passages that read tonally out of place (formal/over-polished/paraphrased-too-closely), as honey-decorated `voice` flags with no correction (awareness-only). - internal/llm/voice.go: RunVoice sends the whole document (no TruncateDoc), MaxTokens 2048, 20s per-doc floor (VoiceInterval). Standalone voice prompt in prompts.go (not bundled with the grammar checkpoint, per spec). - internal/suggestions: POST /api/docs/:id/voice. replacePending is now family-scoped (pendingScope) so grammar and voice never clobber each other's pending flags; both passes return the unified pending set. check/voice share one runPass helper. TestVoicePassCoexists covers both directions. - Frontend: api.voiceDoc, useCheckpoint voicing/runVoice, honey "Check my voice" toolbar pill, breathing honey dot in StatusBar. Claude-Session: https://claude.ai/code/session_016Yr6jELuRc7hyzYLccQKZd
This commit is contained in:
@@ -33,6 +33,44 @@ func CheckpointMessages(contentText string) []Message {
|
||||
}
|
||||
}
|
||||
|
||||
// voiceSystemPrompt drives the Tier-1 voice-consistency pass. It is a distinct
|
||||
// pass from the grammar checkpoint (spec: "do not bundle them") — the model
|
||||
// reads the whole document to learn the writer's natural voice, then flags
|
||||
// passages that read as tonally out of place. `replacement` is null: these are
|
||||
// awareness-only, with no correction to apply.
|
||||
const voiceSystemPrompt = `You are a warm, encouraging writing assistant helping someone who speaks English as a second language. ` +
|
||||
`You are reviewing a COMPLETE document for VOICE CONSISTENCY only — not grammar.
|
||||
|
||||
Read the whole document to learn the writer's natural voice, then identify any passages (2 or more sentences) ` +
|
||||
`that feel tonally inconsistent with the surrounding writing — unusually formal, unusually polished, or phrased ` +
|
||||
`in a way that differs from the writer's established voice elsewhere in the document. These often signal text ` +
|
||||
`that was paraphrased too closely from another source. Do not flag the first paragraph (there is no baseline yet). ` +
|
||||
`Do not flag grammar or spelling mistakes — only voice.
|
||||
|
||||
Respond ONLY with valid JSON. No preamble, no markdown fences. Format:
|
||||
{
|
||||
"suggestions": [
|
||||
{
|
||||
"original": "exact passage from the document that feels inconsistent",
|
||||
"replacement": null,
|
||||
"explanation": "friendly one-sentence note, e.g. 'This passage sounds more formal than the rest of your writing — worth reviewing.'",
|
||||
"type": "voice"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
If the voice is consistent throughout, return: {"suggestions": []}`
|
||||
|
||||
// VoiceMessages builds the message array for a voice-consistency pass. Unlike
|
||||
// the checkpoint, the caller passes the WHOLE document (no truncation) — voice
|
||||
// consistency is judged against the established voice everywhere else.
|
||||
func VoiceMessages(contentText string) []Message {
|
||||
return []Message{
|
||||
{Role: "system", Content: voiceSystemPrompt},
|
||||
{Role: "user", Content: contentText},
|
||||
}
|
||||
}
|
||||
|
||||
// askPetalSystemTemplate is the Ask Petal tutor prompt. The suggestion context
|
||||
// is interpolated in; the user's own messages are appended after this system
|
||||
// turn by the caller.
|
||||
|
||||
32
internal/llm/voice.go
Normal file
32
internal/llm/voice.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// VoiceInterval is the minimum gap between voice-consistency passes for one
|
||||
// document. The pass is whole-document and slow, and it runs on an explicit
|
||||
// user action rather than a typing cadence, so this floor only guards the
|
||||
// inference endpoint against the button being mashed.
|
||||
const VoiceInterval = 20 * time.Second
|
||||
|
||||
// RunVoice sends the WHOLE document — deliberately NOT TruncateDoc'd, since
|
||||
// voice consistency is judged against the established voice everywhere else —
|
||||
// for a Tier-1 voice pass and parses the JSON result. It reuses the checkpoint's
|
||||
// tolerant parser and a larger token budget, since one pass may flag several
|
||||
// passages. Each flag carries a null replacement (awareness-only).
|
||||
func RunVoice(ctx context.Context, client LLMClient, contentText string) ([]RawSuggestion, error) {
|
||||
raw, err := client.Complete(ctx, CompletionRequest{
|
||||
Messages: VoiceMessages(contentText),
|
||||
MaxTokens: 2048,
|
||||
Temperature: 0.3,
|
||||
RepetitionPenalty: 1.15,
|
||||
TopP: 0.9,
|
||||
Stop: []string{"```", "\n\n\n\n"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ParseCheckpoint(raw)
|
||||
}
|
||||
Reference in New Issue
Block a user