mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-11 20:15:32 +08:00
Enhance admin transmissions with detailed LLM and TTS context
This commit is contained in:
@@ -88,6 +88,19 @@ async function convertToWav(inputPath: string, outputPath: string) {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeClone<T>(value: T): T | undefined {
|
||||||
|
if (value === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(JSON.stringify(value));
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("Failed to clone value for transmission metadata", err);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const body = await readBody<PTTRequest>(event);
|
const body = await readBody<PTTRequest>(event);
|
||||||
|
|
||||||
@@ -161,6 +174,64 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await getUserFromEvent(event)
|
const user = await getUserFromEvent(event)
|
||||||
|
|
||||||
|
const llmCallCount = decisionResult?.trace?.calls?.length || 0;
|
||||||
|
const fallbackUsed = Boolean(decisionResult?.trace?.fallback?.used);
|
||||||
|
|
||||||
|
let llmStrategy: 'manual' | 'openai' | 'heuristic' | 'fallback' = 'manual';
|
||||||
|
if (shouldAutoDecide) {
|
||||||
|
if (llmCallCount > 0) {
|
||||||
|
llmStrategy = 'openai';
|
||||||
|
} else if (fallbackUsed) {
|
||||||
|
llmStrategy = 'fallback';
|
||||||
|
} else {
|
||||||
|
llmStrategy = 'heuristic';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const llmUsage = {
|
||||||
|
autoDecide: shouldAutoDecide,
|
||||||
|
openaiUsed: llmStrategy === 'openai',
|
||||||
|
callCount: llmCallCount,
|
||||||
|
fallbackUsed,
|
||||||
|
strategy: llmStrategy,
|
||||||
|
reason:
|
||||||
|
llmStrategy === 'manual'
|
||||||
|
? 'Automatic decision disabled in request.'
|
||||||
|
: llmStrategy === 'openai'
|
||||||
|
? `Decision derived from OpenAI with ${llmCallCount} call(s).`
|
||||||
|
: llmStrategy === 'fallback'
|
||||||
|
? (decisionResult?.trace?.fallback?.reason || 'Fallback triggered after OpenAI failure.')
|
||||||
|
: 'Decision resolved locally without calling OpenAI.'
|
||||||
|
};
|
||||||
|
|
||||||
|
const contextState = safeClone(body.context.state);
|
||||||
|
if (contextState && typeof contextState === 'object' && contextState !== null) {
|
||||||
|
const stateRecord = contextState as Record<string, any>;
|
||||||
|
if (!('id' in stateRecord)) {
|
||||||
|
stateRecord.id = body.context.state_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextCandidates = Array.isArray(body.context.candidates)
|
||||||
|
? body.context.candidates.map(candidate => {
|
||||||
|
const candidateState = safeClone(candidate.state);
|
||||||
|
if (candidateState && typeof candidateState === 'object' && candidateState !== null) {
|
||||||
|
const candidateRecord = candidateState as Record<string, any>;
|
||||||
|
if (!('id' in candidateRecord)) {
|
||||||
|
candidateRecord.id = candidate.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: candidate.id,
|
||||||
|
state: candidateState
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const selectedCandidate = contextCandidates?.find(c => c.id === decision?.next_state);
|
||||||
|
|
||||||
await TransmissionLog.create({
|
await TransmissionLog.create({
|
||||||
user: user?._id,
|
user: user?._id,
|
||||||
role: "pilot",
|
role: "pilot",
|
||||||
@@ -173,6 +244,15 @@ export default defineEventHandler(async (event) => {
|
|||||||
decision,
|
decision,
|
||||||
decisionTrace: decisionResult?.trace,
|
decisionTrace: decisionResult?.trace,
|
||||||
autoDecide: shouldAutoDecide,
|
autoDecide: shouldAutoDecide,
|
||||||
|
llm: llmUsage,
|
||||||
|
context: {
|
||||||
|
stateId: body.context.state_id,
|
||||||
|
state: contextState,
|
||||||
|
candidates: contextCandidates,
|
||||||
|
selectedCandidate,
|
||||||
|
variables: safeClone(body.context.variables),
|
||||||
|
flags: safeClone(body.context.flags)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (logError) {
|
} catch (logError) {
|
||||||
|
|||||||
@@ -159,6 +159,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
let audioBuffer: Buffer;
|
let audioBuffer: Buffer;
|
||||||
let modelUsed: string;
|
let modelUsed: string;
|
||||||
let actualMime = mime;
|
let actualMime = mime;
|
||||||
|
let ttsProvider: 'openai' | 'speaches' | 'piper' = 'openai';
|
||||||
|
|
||||||
if (useSpeaches) {
|
if (useSpeaches) {
|
||||||
// Speaches (prefer compact: MP3, otherwise FLAC/WAV/PCM)
|
// Speaches (prefer compact: MP3, otherwise FLAC/WAV/PCM)
|
||||||
@@ -171,12 +172,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
modelUsed = model;
|
modelUsed = model;
|
||||||
// Server returns the correct format according to response_format
|
// Server returns the correct format according to response_format
|
||||||
actualMime = fmtToMime(fmt);
|
actualMime = fmtToMime(fmt);
|
||||||
|
ttsProvider = 'speaches';
|
||||||
} else if (usePiper) {
|
} else if (usePiper) {
|
||||||
// Local Piper
|
// Local Piper
|
||||||
audioBuffer = await piperTTS(normalized, voice, runtimeConfig.piperPort);
|
audioBuffer = await piperTTS(normalized, voice, runtimeConfig.piperPort);
|
||||||
modelUsed = "piper-local";
|
modelUsed = "piper-local";
|
||||||
// Piper returns WAV
|
// Piper returns WAV
|
||||||
actualMime = "audio/wav";
|
actualMime = "audio/wav";
|
||||||
|
ttsProvider = 'piper';
|
||||||
} else {
|
} else {
|
||||||
// OpenAI (fallback)
|
// OpenAI (fallback)
|
||||||
const tts = await normalize.audio.speech.create({
|
const tts = await normalize.audio.speech.create({
|
||||||
@@ -189,6 +192,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
audioBuffer = Buffer.from(await tts.arrayBuffer());
|
audioBuffer = Buffer.from(await tts.arrayBuffer());
|
||||||
modelUsed = TTS_MODEL;
|
modelUsed = TTS_MODEL;
|
||||||
actualMime = "audio/wav";
|
actualMime = "audio/wav";
|
||||||
|
ttsProvider = 'openai';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional persistence
|
// Optional persistence
|
||||||
@@ -208,7 +212,8 @@ export default defineEventHandler(async (event) => {
|
|||||||
lessonId: body?.lessonId || null,
|
lessonId: body?.lessonId || null,
|
||||||
files: { audio: fileOut },
|
files: { audio: fileOut },
|
||||||
model: modelUsed,
|
model: modelUsed,
|
||||||
format: actualMime
|
format: actualMime,
|
||||||
|
ttsProvider
|
||||||
};
|
};
|
||||||
|
|
||||||
// await writeFile(fileJson, JSON.stringify(meta, null, 2), "utf-8");
|
// await writeFile(fileJson, JSON.stringify(meta, null, 2), "utf-8");
|
||||||
@@ -229,6 +234,12 @@ export default defineEventHandler(async (event) => {
|
|||||||
lessonId: body?.lessonId || null,
|
lessonId: body?.lessonId || null,
|
||||||
tag: body?.tag || null,
|
tag: body?.tag || null,
|
||||||
radioQuality: radioQuality.description,
|
radioQuality: radioQuality.description,
|
||||||
|
tts: {
|
||||||
|
provider: ttsProvider,
|
||||||
|
model: modelUsed,
|
||||||
|
format: actualMime,
|
||||||
|
extension: ext
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} catch (logError) {
|
} catch (logError) {
|
||||||
|
|||||||
Reference in New Issue
Block a user