pi-voice-input 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/extensions/voice-input.ts +258 -61
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@ import { randomUUID } from "node:crypto";
|
|
|
6
6
|
import {
|
|
7
7
|
chmodSync,
|
|
8
8
|
existsSync,
|
|
9
|
+
lstatSync,
|
|
9
10
|
mkdirSync,
|
|
10
11
|
mkdtempSync,
|
|
11
12
|
readFileSync,
|
|
@@ -24,6 +25,25 @@ const CONFIG_PATH = path.join(homedir(), ".pi", "agent", "voice-input.config.jso
|
|
|
24
25
|
const VOLC_API_KEY_URL = "https://console.volcengine.com/speech/new/setting/apikeys?projectName=default";
|
|
25
26
|
const DEFAULT_SHORTCUT = Key.ctrlShift("r");
|
|
26
27
|
const DEFAULT_POSTPROCESS_MODEL = "";
|
|
28
|
+
const DEFAULT_POSTPROCESS_CONTEXT_TOKENS = 20000;
|
|
29
|
+
const POSTPROCESS_GIT_LOG_LIMIT = 10;
|
|
30
|
+
const POSTPROCESS_DIRECTORY_DEPTH = 2;
|
|
31
|
+
const POSTPROCESS_DIRECTORY_TOKENS = 20000;
|
|
32
|
+
const SKIPPED_DIRECTORY_ENTRIES = new Set([
|
|
33
|
+
".git",
|
|
34
|
+
".hg",
|
|
35
|
+
".svn",
|
|
36
|
+
"node_modules",
|
|
37
|
+
"dist",
|
|
38
|
+
"build",
|
|
39
|
+
"coverage",
|
|
40
|
+
".next",
|
|
41
|
+
".nuxt",
|
|
42
|
+
".turbo",
|
|
43
|
+
".cache",
|
|
44
|
+
"logs",
|
|
45
|
+
"recordings",
|
|
46
|
+
]);
|
|
27
47
|
const POSTPROCESS_SYSTEM_PROMPT = `You are the speech-recognition postprocessor for the pi voice input extension. Your only job is to polish the raw ASR text into text that the plugin can paste verbatim at the current cursor position in the pi editor.
|
|
28
48
|
|
|
29
49
|
Interaction contract:
|
|
@@ -88,7 +108,7 @@ type VoiceConfig = {
|
|
|
88
108
|
postprocessModel: string;
|
|
89
109
|
postprocessTimeoutMs: number;
|
|
90
110
|
postprocessMaxTokens: number;
|
|
91
|
-
|
|
111
|
+
postprocessContextTokens: number;
|
|
92
112
|
duckSystemVolume: boolean;
|
|
93
113
|
duckSystemVolumeFactor: number;
|
|
94
114
|
duckSystemVolumeFadeMs: number;
|
|
@@ -222,7 +242,7 @@ function getConfig(): VoiceConfig {
|
|
|
222
242
|
postprocessModel: polishModel,
|
|
223
243
|
postprocessTimeoutMs: 30000,
|
|
224
244
|
postprocessMaxTokens: 2048,
|
|
225
|
-
|
|
245
|
+
postprocessContextTokens: DEFAULT_POSTPROCESS_CONTEXT_TOKENS,
|
|
226
246
|
duckSystemVolume: fileConfig.duckSystemVolume,
|
|
227
247
|
duckSystemVolumeFactor: fileConfig.duckSystemVolumeFactor,
|
|
228
248
|
duckSystemVolumeFadeMs: fileConfig.duckSystemVolumeFadeMs,
|
|
@@ -956,16 +976,77 @@ async function transcribePcm(pcm: Buffer, durationMs: number, config: VoiceConfi
|
|
|
956
976
|
};
|
|
957
977
|
}
|
|
958
978
|
|
|
959
|
-
function
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
979
|
+
function estimateTokens(text: string): number {
|
|
980
|
+
let tokens = 0;
|
|
981
|
+
let asciiRun = 0;
|
|
982
|
+
const flushAscii = () => {
|
|
983
|
+
if (asciiRun > 0) {
|
|
984
|
+
tokens += Math.ceil(asciiRun / 4);
|
|
985
|
+
asciiRun = 0;
|
|
986
|
+
}
|
|
987
|
+
};
|
|
988
|
+
|
|
989
|
+
for (const char of text) {
|
|
990
|
+
if (/\s/u.test(char)) {
|
|
991
|
+
flushAscii();
|
|
992
|
+
} else if (/[^\x00-\x7F]/u.test(char)) {
|
|
993
|
+
flushAscii();
|
|
994
|
+
tokens += 1;
|
|
995
|
+
} else {
|
|
996
|
+
asciiRun += 1;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
flushAscii();
|
|
1000
|
+
return tokens;
|
|
963
1001
|
}
|
|
964
1002
|
|
|
965
|
-
function
|
|
966
|
-
if (
|
|
967
|
-
if (text
|
|
968
|
-
|
|
1003
|
+
function takeTokensFromStart(text: string, maxTokens: number): string {
|
|
1004
|
+
if (maxTokens <= 0 || !text) return "";
|
|
1005
|
+
if (estimateTokens(text) <= maxTokens) return text;
|
|
1006
|
+
|
|
1007
|
+
let low = 0;
|
|
1008
|
+
let high = text.length;
|
|
1009
|
+
while (low < high) {
|
|
1010
|
+
const mid = Math.ceil((low + high) / 2);
|
|
1011
|
+
if (estimateTokens(text.slice(0, mid)) <= maxTokens) low = mid;
|
|
1012
|
+
else high = mid - 1;
|
|
1013
|
+
}
|
|
1014
|
+
return text.slice(0, low);
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function takeTokensFromEnd(text: string, maxTokens: number): string {
|
|
1018
|
+
if (maxTokens <= 0 || !text) return "";
|
|
1019
|
+
if (estimateTokens(text) <= maxTokens) return text;
|
|
1020
|
+
|
|
1021
|
+
let low = 0;
|
|
1022
|
+
let high = text.length;
|
|
1023
|
+
while (low < high) {
|
|
1024
|
+
const mid = Math.ceil((low + high) / 2);
|
|
1025
|
+
if (estimateTokens(text.slice(text.length - mid)) <= maxTokens) low = mid;
|
|
1026
|
+
else high = mid - 1;
|
|
1027
|
+
}
|
|
1028
|
+
return text.slice(text.length - low);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function limitTokensFromStart(text: string, maxTokens: number, marker = "\n…(truncated to fit token budget)"): string {
|
|
1032
|
+
if (maxTokens <= 0) return "";
|
|
1033
|
+
if (estimateTokens(text) <= maxTokens) return text;
|
|
1034
|
+
const markerTokens = estimateTokens(marker);
|
|
1035
|
+
return `${takeTokensFromStart(text, Math.max(0, maxTokens - markerTokens)).trimEnd()}${marker}`;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function limitTokensFromEnd(text: string, maxTokens: number, marker = "…(older context omitted to fit token budget)\n"): string {
|
|
1039
|
+
if (maxTokens <= 0) return "";
|
|
1040
|
+
if (estimateTokens(text) <= maxTokens) return text;
|
|
1041
|
+
const markerTokens = estimateTokens(marker);
|
|
1042
|
+
return `${marker}${takeTokensFromEnd(text, Math.max(0, maxTokens - markerTokens)).trimStart()}`;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function redactSensitiveText(text: string): string {
|
|
1046
|
+
return text
|
|
1047
|
+
.replace(/(\bVOLC_API_KEY\s*=\s*)[^\s"']+/giu, "$1[redacted]")
|
|
1048
|
+
.replace(/((?:\b|["'])(?:volcApiKey|api[_-]?key|apikey|access[_-]?token|secret|password)(?:\b|["'])\s*[:=]\s*["']?)[^"'\s,}]+/giu, "$1[redacted]")
|
|
1049
|
+
.replace(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/giu, "[redacted-uuid]");
|
|
969
1050
|
}
|
|
970
1051
|
|
|
971
1052
|
function textFromContent(content: unknown): string {
|
|
@@ -982,27 +1063,163 @@ function textFromContent(content: unknown): string {
|
|
|
982
1063
|
.join("\n");
|
|
983
1064
|
}
|
|
984
1065
|
|
|
985
|
-
function getEditorContext(ctx: ExtensionContext
|
|
986
|
-
if (maxChars <= 0) return "";
|
|
1066
|
+
function getEditorContext(ctx: ExtensionContext): string {
|
|
987
1067
|
try {
|
|
988
|
-
return
|
|
1068
|
+
return redactSensitiveText(ctx.ui.getEditorText()).trim();
|
|
989
1069
|
} catch {
|
|
990
1070
|
return "";
|
|
991
1071
|
}
|
|
992
1072
|
}
|
|
993
1073
|
|
|
994
|
-
function getRecentSessionContext(ctx: ExtensionContext
|
|
995
|
-
if (maxChars <= 0) return "";
|
|
1074
|
+
function getRecentSessionContext(ctx: ExtensionContext): string {
|
|
996
1075
|
const lines: string[] = [];
|
|
997
1076
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
998
1077
|
if (entry.type !== "message") continue;
|
|
999
1078
|
const message = entry.message as { role?: unknown; content?: unknown };
|
|
1000
1079
|
if (message.role !== "user" && message.role !== "assistant") continue;
|
|
1001
|
-
const text = textFromContent(message.content)
|
|
1080
|
+
const text = redactSensitiveText(textFromContent(message.content)).trim();
|
|
1002
1081
|
if (!text) continue;
|
|
1003
|
-
lines.push(`${message.role}: ${
|
|
1082
|
+
lines.push(`${message.role}: ${text}`);
|
|
1083
|
+
}
|
|
1084
|
+
return lines.join("\n\n");
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function buildConversationContext(ctx: ExtensionContext, maxTokens: number): { editorContext: string; sessionContext: string } {
|
|
1088
|
+
if (maxTokens <= 0) return { editorContext: "", sessionContext: "" };
|
|
1089
|
+
|
|
1090
|
+
const editorContext = getEditorContext(ctx);
|
|
1091
|
+
const sessionContext = getRecentSessionContext(ctx);
|
|
1092
|
+
const editorSection = [
|
|
1093
|
+
"--- Context: current unsent editor draft (context only; do not output wholesale) ---",
|
|
1094
|
+
editorContext || "(empty)",
|
|
1095
|
+
].join("\n");
|
|
1096
|
+
const sessionHeader = "--- Context: recent conversation ---\n";
|
|
1097
|
+
const totalTokens = estimateTokens([editorSection, sessionHeader, sessionContext || "(empty)"].join("\n\n"));
|
|
1098
|
+
if (totalTokens <= maxTokens) return { editorContext, sessionContext };
|
|
1099
|
+
|
|
1100
|
+
const editorTokenBudget = Math.min(5000, Math.floor(maxTokens / 4));
|
|
1101
|
+
const limitedEditor = editorContext ? limitTokensFromEnd(editorContext, editorTokenBudget) : "";
|
|
1102
|
+
const fixedTokens = estimateTokens(
|
|
1103
|
+
[
|
|
1104
|
+
"--- Context: current unsent editor draft (context only; do not output wholesale) ---",
|
|
1105
|
+
limitedEditor || "(empty)",
|
|
1106
|
+
sessionHeader,
|
|
1107
|
+
].join("\n\n"),
|
|
1108
|
+
);
|
|
1109
|
+
const sessionBudget = Math.max(0, maxTokens - fixedTokens);
|
|
1110
|
+
return {
|
|
1111
|
+
editorContext: limitedEditor,
|
|
1112
|
+
sessionContext: sessionContext ? limitTokensFromEnd(sessionContext, sessionBudget) : "",
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function commandOutputInDir(cwd: string, command: string, args: string[], timeoutMs = 2500): string {
|
|
1117
|
+
const result = spawnSync(command, args, { cwd, encoding: "utf8", timeout: timeoutMs, maxBuffer: 1024 * 1024 * 8 });
|
|
1118
|
+
if (result.status !== 0) return "";
|
|
1119
|
+
return (result.stdout || "").trim();
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function findGitRoot(startDir: string): string | null {
|
|
1123
|
+
const root = commandOutputInDir(startDir, "git", ["rev-parse", "--show-toplevel"], 1500);
|
|
1124
|
+
return root || null;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function buildGitContext(cwd: string): string {
|
|
1128
|
+
const gitRoot = findGitRoot(cwd);
|
|
1129
|
+
if (!gitRoot) return "(not a git repository)";
|
|
1130
|
+
|
|
1131
|
+
const recentLog = commandOutputInDir(
|
|
1132
|
+
gitRoot,
|
|
1133
|
+
"git",
|
|
1134
|
+
["log", `-${POSTPROCESS_GIT_LOG_LIMIT}`, "--date=short", "--pretty=format:%h %ad %an %s"],
|
|
1135
|
+
2500,
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
return [
|
|
1139
|
+
`Repository root: ${gitRoot}`,
|
|
1140
|
+
`Recent commits (up to ${POSTPROCESS_GIT_LOG_LIMIT}):`,
|
|
1141
|
+
recentLog ? redactSensitiveText(recentLog) : "(no commits yet)",
|
|
1142
|
+
].join("\n");
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function shouldSkipDirectoryEntry(name: string): boolean {
|
|
1146
|
+
return SKIPPED_DIRECTORY_ENTRIES.has(name);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function safeReadDir(dir: string): string[] {
|
|
1150
|
+
try {
|
|
1151
|
+
return readdirSync(dir).sort((a, b) => a.localeCompare(b));
|
|
1152
|
+
} catch {
|
|
1153
|
+
return [];
|
|
1004
1154
|
}
|
|
1005
|
-
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
function formatDirectoryEntry(fullPath: string, name: string, isCwd = false): string {
|
|
1158
|
+
try {
|
|
1159
|
+
const stat = lstatSync(fullPath);
|
|
1160
|
+
const suffix = stat.isDirectory() ? "/" : stat.isSymbolicLink() ? "@" : "";
|
|
1161
|
+
return `${name}${suffix}${isCwd ? " <-- current" : ""}`;
|
|
1162
|
+
} catch {
|
|
1163
|
+
return `${name}${isCwd ? " <-- current" : ""}`;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function buildTreeLines(dir: string, depth: number, prefix = "", state = { entries: 0, omitted: 0 }): string[] {
|
|
1168
|
+
if (depth < 0) return [];
|
|
1169
|
+
const names = safeReadDir(dir).filter((name) => !shouldSkipDirectoryEntry(name));
|
|
1170
|
+
const visibleNames = names.slice(0, 120);
|
|
1171
|
+
state.omitted += Math.max(0, names.length - visibleNames.length);
|
|
1172
|
+
const lines: string[] = [];
|
|
1173
|
+
|
|
1174
|
+
visibleNames.forEach((name, index) => {
|
|
1175
|
+
const fullPath = path.join(dir, name);
|
|
1176
|
+
const isLast = index === visibleNames.length - 1;
|
|
1177
|
+
const connector = isLast ? "└── " : "├── ";
|
|
1178
|
+
const childPrefix = `${prefix}${isLast ? " " : "│ "}`;
|
|
1179
|
+
const label = formatDirectoryEntry(fullPath, name);
|
|
1180
|
+
lines.push(`${prefix}${connector}${label}`);
|
|
1181
|
+
state.entries += 1;
|
|
1182
|
+
|
|
1183
|
+
try {
|
|
1184
|
+
const stat = lstatSync(fullPath);
|
|
1185
|
+
if (stat.isDirectory() && !stat.isSymbolicLink() && depth > 0) {
|
|
1186
|
+
lines.push(...buildTreeLines(fullPath, depth - 1, childPrefix, state));
|
|
1187
|
+
}
|
|
1188
|
+
} catch {
|
|
1189
|
+
// ignore unreadable entries
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
return lines;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function buildDirectoryContext(cwd: string, maxTokens: number): string {
|
|
1197
|
+
if (maxTokens <= 0) return "";
|
|
1198
|
+
const parent = path.dirname(cwd);
|
|
1199
|
+
const isRootDirectory = path.resolve(parent) === path.resolve(cwd);
|
|
1200
|
+
const cwdName = path.basename(cwd) || cwd;
|
|
1201
|
+
const parentEntries = isRootDirectory
|
|
1202
|
+
? []
|
|
1203
|
+
: safeReadDir(parent)
|
|
1204
|
+
.filter((name) => !shouldSkipDirectoryEntry(name))
|
|
1205
|
+
.slice(0, 160)
|
|
1206
|
+
.map((name) => formatDirectoryEntry(path.join(parent, name), name, path.resolve(parent, name) === path.resolve(cwd)));
|
|
1207
|
+
const state = { entries: 0, omitted: 0 };
|
|
1208
|
+
const cwdTree = buildTreeLines(cwd, POSTPROCESS_DIRECTORY_DEPTH - 1, "", state);
|
|
1209
|
+
const text = [
|
|
1210
|
+
`Current directory: ${cwd}`,
|
|
1211
|
+
isRootDirectory ? "Parent directory: (current directory is filesystem root; no parent directory)" : `Parent directory: ${parent}`,
|
|
1212
|
+
"Parent entries (one level up):",
|
|
1213
|
+
isRootDirectory ? "- (none; current directory is filesystem root)" : parentEntries.length ? parentEntries.map((entry) => `- ${entry}`).join("\n") : "- (empty or unreadable)",
|
|
1214
|
+
"",
|
|
1215
|
+
`Current directory tree (${cwdName}/, ${POSTPROCESS_DIRECTORY_DEPTH} levels deep):`,
|
|
1216
|
+
`${cwdName}/`,
|
|
1217
|
+
cwdTree.length ? cwdTree.join("\n") : "└── (empty or no readable child entries)",
|
|
1218
|
+
state.omitted > 0 ? `…(${state.omitted} entries omitted)` : "",
|
|
1219
|
+
]
|
|
1220
|
+
.filter(Boolean)
|
|
1221
|
+
.join("\n");
|
|
1222
|
+
return limitTokensFromStart(text, maxTokens);
|
|
1006
1223
|
}
|
|
1007
1224
|
|
|
1008
1225
|
function simplifyModelReference(value: string): string {
|
|
@@ -1091,46 +1308,22 @@ function cleanPostprocessOutput(output: string): string {
|
|
|
1091
1308
|
return text;
|
|
1092
1309
|
}
|
|
1093
1310
|
|
|
1094
|
-
const EXPLICIT_ENGLISH_MULTILINE_PATTERN =
|
|
1095
|
-
/\b(?:new\s*line|newline|line break|next line|new paragraph|paragraph break|carriage return|press enter|separate lines?|multi[- ]line|multiple lines)\b/i;
|
|
1096
|
-
const EXPLICIT_CHINESE_MULTILINE_PATTERN = /(?:换行|新的一行|另起一行|下一行|回车|分行|多行|逐行|每行|空一行|新段落|另起一段|分段)/u;
|
|
1097
|
-
const CJK_LIKE_PATTERN = /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}]/u;
|
|
1098
|
-
const CJK_PUNCTUATION_PATTERN = /[,。!?、;:()《》「」『』“”‘’]/u;
|
|
1099
|
-
const CLOSING_PUNCTUATION_PATTERN = /^[,.;:!?,。!?、;:))\]}》」』”’]/u;
|
|
1100
|
-
const OPENING_PUNCTUATION_PATTERN = /[(([{\[《「『“‘]$/u;
|
|
1101
|
-
|
|
1102
1311
|
function rawTextRequestsMultiline(rawText: string): boolean {
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
function lineBreakJoiner(left: string, right: string): string {
|
|
1110
|
-
if (!left || !right) return "";
|
|
1111
|
-
if (CLOSING_PUNCTUATION_PATTERN.test(right) || OPENING_PUNCTUATION_PATTERN.test(left)) return "";
|
|
1112
|
-
if (CJK_PUNCTUATION_PATTERN.test(left) || CJK_PUNCTUATION_PATTERN.test(right)) return "";
|
|
1113
|
-
if (CJK_LIKE_PATTERN.test(left) && CJK_LIKE_PATTERN.test(right)) return "";
|
|
1114
|
-
return " ";
|
|
1312
|
+
return (
|
|
1313
|
+
/\r|\n/.test(rawText) ||
|
|
1314
|
+
/\b(?:new\s*line|newline|line break|next line|new paragraph|paragraph break|carriage return|press enter|separate lines?|multi[- ]line|multiple lines)\b/i.test(rawText) ||
|
|
1315
|
+
/(?:换行|新的一行|另起一行|下一行|回车|分行|多行|逐行|每行|空一行|新段落|另起一段|分段)/u.test(rawText)
|
|
1316
|
+
);
|
|
1115
1317
|
}
|
|
1116
1318
|
|
|
1117
1319
|
function collapseUnexpectedLineBreaks(text: string): string {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
.replace(/[ \t\f\v]*\n+[ \t\f\v]*/g,
|
|
1121
|
-
const left = source.slice(0, offset).replace(/[ \t\f\v]+$/g, "").at(-1) ?? "";
|
|
1122
|
-
const right = source.slice(offset + match.length).replace(/^[ \t\f\v]+/g, "").at(0) ?? "";
|
|
1123
|
-
return lineBreakJoiner(left, right);
|
|
1124
|
-
})
|
|
1320
|
+
return text
|
|
1321
|
+
.replace(/\r\n?/g, "\n")
|
|
1322
|
+
.replace(/[ \t\f\v]*\n+[ \t\f\v]*/g, " ")
|
|
1125
1323
|
.replace(/[ \t\f\v]{2,}/g, " ")
|
|
1126
1324
|
.trim();
|
|
1127
1325
|
}
|
|
1128
1326
|
|
|
1129
|
-
function normalizeRawTextForPostprocess(rawText: string): string {
|
|
1130
|
-
const raw = rawText.trim();
|
|
1131
|
-
return rawTextRequestsMultiline(raw) ? raw : collapseUnexpectedLineBreaks(raw);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
1327
|
function preserveExpectedPostprocessLayout(rawText: string, output: string): string {
|
|
1135
1328
|
if (rawTextRequestsMultiline(rawText)) return output.trim();
|
|
1136
1329
|
return collapseUnexpectedLineBreaks(output);
|
|
@@ -1169,9 +1362,10 @@ function getFullEditorText(ctx: ExtensionContext): string {
|
|
|
1169
1362
|
}
|
|
1170
1363
|
|
|
1171
1364
|
function buildPostprocessPrompt(ctx: ExtensionContext, rawText: string, config: VoiceConfig): string {
|
|
1172
|
-
const
|
|
1173
|
-
const
|
|
1174
|
-
const
|
|
1365
|
+
const { editorContext, sessionContext } = buildConversationContext(ctx, config.postprocessContextTokens);
|
|
1366
|
+
const cwd = process.cwd();
|
|
1367
|
+
const gitContext = buildGitContext(cwd);
|
|
1368
|
+
const directoryContext = buildDirectoryContext(cwd, POSTPROCESS_DIRECTORY_TOKENS);
|
|
1175
1369
|
|
|
1176
1370
|
return [
|
|
1177
1371
|
"Polish only the raw ASR text below, using context only when it helps disambiguate the user's intent.",
|
|
@@ -1184,15 +1378,22 @@ function buildPostprocessPrompt(ctx: ExtensionContext, rawText: string, config:
|
|
|
1184
1378
|
"The true cursor position is not marked in the draft shown here; the pi editor owns the actual insertion point. Do not guess the cursor and synthesize a full surrounding sentence.",
|
|
1185
1379
|
"Preserve layout: if the raw ASR text is one line, output one line unless the user explicitly dictated line breaks or another multiline layout.",
|
|
1186
1380
|
"If the raw speech is an inline insertion, continuation, a few words, or a phrase, output only the newly spoken words or phrase.",
|
|
1381
|
+
"Use the git history and directory structure only as reference context for project names, files, APIs, and intent; never summarize them in the output.",
|
|
1187
1382
|
"Example: draft is `Please make this function async and [cursor].`, raw speech is `add error handling`, correct output is `add error handling`, not `Please make this function async and add error handling.`.",
|
|
1188
1383
|
"Example: draft is `This variable name is [cursor]unclear`, raw speech is `still`, correct output is `still`, not `This variable name is still unclear`.",
|
|
1189
1384
|
"",
|
|
1190
1385
|
"--- Context: current unsent editor draft (context only; do not output wholesale) ---",
|
|
1191
|
-
editorContext
|
|
1386
|
+
editorContext || "(empty)",
|
|
1192
1387
|
"",
|
|
1193
|
-
"--- Context: recent conversation ---",
|
|
1388
|
+
"--- Context: recent conversation (recent tail, capped at 20k estimated tokens with the editor draft) ---",
|
|
1194
1389
|
sessionContext || "(empty)",
|
|
1195
1390
|
"",
|
|
1391
|
+
"--- Context: git history (latest commit summaries only) ---",
|
|
1392
|
+
gitContext || "(empty)",
|
|
1393
|
+
"",
|
|
1394
|
+
"--- Context: directory structure (parent one level; current directory two levels deep) ---",
|
|
1395
|
+
directoryContext || "(empty)",
|
|
1396
|
+
"",
|
|
1196
1397
|
"--- Raw ASR text ---",
|
|
1197
1398
|
rawText.trim(),
|
|
1198
1399
|
].join("\n");
|
|
@@ -1217,7 +1418,7 @@ async function postprocessTranscript(ctx: ExtensionContext, rawText: string, con
|
|
|
1217
1418
|
messages: [
|
|
1218
1419
|
{
|
|
1219
1420
|
role: "user",
|
|
1220
|
-
content: buildPostprocessPrompt(ctx,
|
|
1421
|
+
content: buildPostprocessPrompt(ctx, raw, config),
|
|
1221
1422
|
timestamp: Date.now(),
|
|
1222
1423
|
},
|
|
1223
1424
|
],
|
|
@@ -1394,7 +1595,6 @@ async function stopRecording(ctx: ExtensionContext, transcribe = true) {
|
|
|
1394
1595
|
|
|
1395
1596
|
let finalText = result.text;
|
|
1396
1597
|
let postprocessMs = 0;
|
|
1397
|
-
let postprocessSucceeded = false;
|
|
1398
1598
|
let postprocessUsed = false;
|
|
1399
1599
|
if (config.postprocessEnabled) {
|
|
1400
1600
|
ctx.ui.setStatus("voice-input", ctx.ui.theme.fg("warning", "● polishing"));
|
|
@@ -1402,7 +1602,7 @@ async function stopRecording(ctx: ExtensionContext, transcribe = true) {
|
|
|
1402
1602
|
try {
|
|
1403
1603
|
finalText = await postprocessTranscript(ctx, result.text, config);
|
|
1404
1604
|
postprocessMs = Date.now() - postprocessStart;
|
|
1405
|
-
|
|
1605
|
+
postprocessUsed = finalText.trim() !== result.text.trim();
|
|
1406
1606
|
} catch (error) {
|
|
1407
1607
|
postprocessMs = Date.now() - postprocessStart;
|
|
1408
1608
|
ctx.ui.notify(
|
|
@@ -1412,9 +1612,6 @@ async function stopRecording(ctx: ExtensionContext, transcribe = true) {
|
|
|
1412
1612
|
}
|
|
1413
1613
|
}
|
|
1414
1614
|
|
|
1415
|
-
finalText = preserveExpectedPostprocessLayout(result.text, finalText);
|
|
1416
|
-
postprocessUsed = postprocessSucceeded && finalText.trim() !== result.text.trim();
|
|
1417
|
-
|
|
1418
1615
|
ctx.ui.setStatus("voice-input", undefined);
|
|
1419
1616
|
insertIntoEditor(ctx, finalText);
|
|
1420
1617
|
ctx.ui.notify(
|