voice: quieter thinking tone, log cleanup, and small ui polish
- Add scripts/lower-thinking-tone.mjs to scale the thinking-tone PCM amplitude in-place; apply it at 0.1 so the cue is a soft indicator instead of triggering the VAD via mic feedback. - Drop the "Received first voice_audio_chunk" log that re-fires every summary window because the chunk counter is reset. - Match Spoke message icon and label sizing to the standard tool-call badge. - Append a spoken-input instruction so voice replies route through the speak tool.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env node
|
||||
// Scale the amplitude of the thinking-tone PCM16 base64 in-place.
|
||||
// Usage: node scripts/lower-thinking-tone.mjs <gain>
|
||||
// Example: node scripts/lower-thinking-tone.mjs 0.3 (≈ -10 dB)
|
||||
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const target = resolve(here, "../src/utils/thinking-tone.native-pcm.ts");
|
||||
|
||||
const gainArg = process.argv[2];
|
||||
if (!gainArg) {
|
||||
console.error("Usage: node scripts/lower-thinking-tone.mjs <gain>");
|
||||
process.exit(1);
|
||||
}
|
||||
const gain = Number(gainArg);
|
||||
if (!Number.isFinite(gain) || gain < 0) {
|
||||
console.error(`Invalid gain: ${gainArg}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const source = readFileSync(target, "utf8");
|
||||
const match = source.match(/"([A-Za-z0-9+/=]+)"/);
|
||||
if (!match) {
|
||||
console.error("Could not find base64 string in target file.");
|
||||
process.exit(1);
|
||||
}
|
||||
const originalBase64 = match[1];
|
||||
|
||||
const buf = Buffer.from(originalBase64, "base64");
|
||||
if (buf.byteLength % 2 !== 0) {
|
||||
console.error(`PCM16 buffer has odd length ${buf.byteLength}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let peakBefore = 0;
|
||||
let peakAfter = 0;
|
||||
const out = Buffer.alloc(buf.byteLength);
|
||||
for (let i = 0; i < buf.byteLength; i += 2) {
|
||||
const sample = buf.readInt16LE(i);
|
||||
peakBefore = Math.max(peakBefore, Math.abs(sample));
|
||||
const scaled = Math.max(-32768, Math.min(32767, Math.round(sample * gain)));
|
||||
peakAfter = Math.max(peakAfter, Math.abs(scaled));
|
||||
out.writeInt16LE(scaled, i);
|
||||
}
|
||||
|
||||
const newBase64 = out.toString("base64");
|
||||
const updated = source.replace(originalBase64, newBase64);
|
||||
writeFileSync(target, updated);
|
||||
|
||||
console.log(`Scaled thinking tone by ${gain}.`);
|
||||
console.log(` samples: ${buf.byteLength / 2}`);
|
||||
console.log(` peak abs: ${peakBefore} -> ${peakAfter}`);
|
||||
console.log(` base64 len ${originalBase64.length} -> ${newBase64.length}`);
|
||||
console.log(` wrote ${target}`);
|
||||
@@ -1740,8 +1740,8 @@ const speakMessageStylesheet = StyleSheet.create((theme) => ({
|
||||
},
|
||||
headerLabel: {
|
||||
fontFamily: Fonts.sans,
|
||||
fontSize: 12,
|
||||
fontWeight: "500",
|
||||
fontSize: theme.fontSize.base,
|
||||
fontWeight: theme.fontWeight.normal,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
text: {
|
||||
@@ -1769,7 +1769,7 @@ export const SpeakMessage = memo(function SpeakMessage({
|
||||
return (
|
||||
<View testID="speak-message" style={containerStyle}>
|
||||
<View style={speakMessageStylesheet.header}>
|
||||
<ThemedMicVocal size={14} uniProps={foregroundMutedColorMapping} />
|
||||
<ThemedMicVocal size={12} uniProps={foregroundMutedColorMapping} />
|
||||
<Text style={speakMessageStylesheet.headerLabel}>Spoke</Text>
|
||||
</View>
|
||||
<Text style={speakMessageStylesheet.text}>{message}</Text>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -7546,15 +7546,6 @@ export class Session {
|
||||
const chunkBytes = Buffer.byteLength(msg.audio, "base64");
|
||||
this.voiceInputChunkCount += 1;
|
||||
this.voiceInputBytes += chunkBytes;
|
||||
if (this.voiceInputChunkCount === 1) {
|
||||
this.sessionLogger.info(
|
||||
{
|
||||
format: chunkFormat,
|
||||
audioBytes: chunkBytes,
|
||||
},
|
||||
"Received first voice_audio_chunk for active voice mode",
|
||||
);
|
||||
}
|
||||
const now = Date.now();
|
||||
if (this.voiceInputChunkCount % 50 === 0 || now - this.voiceInputWindowStartedAt >= 1000) {
|
||||
this.sessionLogger.info(
|
||||
|
||||
@@ -54,7 +54,7 @@ export function buildVoiceModeSystemPrompt(existing: string | undefined, enabled
|
||||
}
|
||||
|
||||
export function wrapSpokenInput(text: string): string {
|
||||
return `<spoken-input>\n${text}\n</spoken-input>`;
|
||||
return `<spoken-input>\n${text}\n</spoken-input>\n<instruction>This message was spoken by the user. Respond using the speak tool only, not normal messages, because the user may not be looking at the chat.</instruction>`;
|
||||
}
|
||||
|
||||
export function buildVoiceAgentMcpServerConfig(params: {
|
||||
|
||||
Reference in New Issue
Block a user