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: {
|
headerLabel: {
|
||||||
fontFamily: Fonts.sans,
|
fontFamily: Fonts.sans,
|
||||||
fontSize: 12,
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: "500",
|
fontWeight: theme.fontWeight.normal,
|
||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
@@ -1769,7 +1769,7 @@ export const SpeakMessage = memo(function SpeakMessage({
|
|||||||
return (
|
return (
|
||||||
<View testID="speak-message" style={containerStyle}>
|
<View testID="speak-message" style={containerStyle}>
|
||||||
<View style={speakMessageStylesheet.header}>
|
<View style={speakMessageStylesheet.header}>
|
||||||
<ThemedMicVocal size={14} uniProps={foregroundMutedColorMapping} />
|
<ThemedMicVocal size={12} uniProps={foregroundMutedColorMapping} />
|
||||||
<Text style={speakMessageStylesheet.headerLabel}>Spoke</Text>
|
<Text style={speakMessageStylesheet.headerLabel}>Spoke</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={speakMessageStylesheet.text}>{message}</Text>
|
<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");
|
const chunkBytes = Buffer.byteLength(msg.audio, "base64");
|
||||||
this.voiceInputChunkCount += 1;
|
this.voiceInputChunkCount += 1;
|
||||||
this.voiceInputBytes += chunkBytes;
|
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();
|
const now = Date.now();
|
||||||
if (this.voiceInputChunkCount % 50 === 0 || now - this.voiceInputWindowStartedAt >= 1000) {
|
if (this.voiceInputChunkCount % 50 === 0 || now - this.voiceInputWindowStartedAt >= 1000) {
|
||||||
this.sessionLogger.info(
|
this.sessionLogger.info(
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function buildVoiceModeSystemPrompt(existing: string | undefined, enabled
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function wrapSpokenInput(text: string): string {
|
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: {
|
export function buildVoiceAgentMcpServerConfig(params: {
|
||||||
|
|||||||
Reference in New Issue
Block a user