mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-01 06:06:05 +08:00
Enhance admin transmissions with detailed LLM and TTS context
This commit is contained in:
@@ -729,9 +729,9 @@
|
||||
<v-expand-transition>
|
||||
<div
|
||||
v-if="expandedLog === entry.id"
|
||||
class="space-y-3 rounded-2xl border border-cyan-400/30 bg-cyan-500/10 p-4 text-xs text-white/80"
|
||||
class="space-y-4 rounded-2xl border border-cyan-400/30 bg-cyan-500/10 p-4 text-xs text-white/80"
|
||||
>
|
||||
<div v-if="entry.metadata?.decision" class="space-y-2">
|
||||
<div v-if="entry.metadata?.decision" class="space-y-3">
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-200/80">LLM Decision Summary</p>
|
||||
<div class="grid gap-2 md:grid-cols-2">
|
||||
<div class="rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
@@ -752,6 +752,159 @@
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<template v-for="usage in [buildLlmUsage(entry)]" :key="`${entry.id}-usage`">
|
||||
<div
|
||||
v-if="usage"
|
||||
class="space-y-2 rounded-xl border border-white/10 bg-black/30 p-3 text-[11px] text-white/70"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-200/80">LLM usage</p>
|
||||
<p class="text-sm font-semibold text-white">{{ usage.method }}</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<v-chip size="x-small" color="cyan" variant="outlined">
|
||||
Auto decision: {{ usage.autoDecide === false ? 'Disabled' : 'Enabled' }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="cyan" variant="outlined">
|
||||
OpenAI: {{ usage.openaiUsed ? 'Used' : 'Not used' }}
|
||||
</v-chip>
|
||||
<v-chip v-if="usage.openaiUsed" size="x-small" color="cyan" variant="outlined">
|
||||
Calls: {{ usage.callCount }}
|
||||
</v-chip>
|
||||
<v-chip v-if="usage.fallbackUsed" size="x-small" color="orange" variant="tonal">
|
||||
Fallback triggered
|
||||
</v-chip>
|
||||
</div>
|
||||
<p v-if="usage.reason" class="text-white/60">{{ usage.reason }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="entry.metadata?.context" class="space-y-3">
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-200/80">State context snapshot</p>
|
||||
<div class="space-y-3 rounded-xl border border-white/10 bg-black/30 p-3">
|
||||
<div class="flex flex-col gap-2 md:flex-row md:items-start md:justify-between">
|
||||
<div>
|
||||
<p class="font-mono text-sm text-white">
|
||||
{{ entry.metadata.context.stateId || 'Unknown state' }}
|
||||
</p>
|
||||
<p v-if="entry.metadata.context.state?.name" class="text-[11px] text-white/60">
|
||||
{{ entry.metadata.context.state?.name }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 text-[11px] text-white/60">
|
||||
<v-chip v-if="entry.metadata.context.state?.role" size="x-small" color="cyan" variant="outlined">
|
||||
Role: {{ entry.metadata.context.state?.role }}
|
||||
</v-chip>
|
||||
<v-chip v-if="entry.metadata.context.state?.phase" size="x-small" color="cyan" variant="text">
|
||||
Phase: {{ entry.metadata.context.state?.phase }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="entry.metadata.context.state?.say_tpl"
|
||||
class="rounded-lg border border-white/10 bg-black/40 p-3"
|
||||
>
|
||||
<p class="text-[11px] text-white/50">State Say Template</p>
|
||||
<p class="font-mono text-sm">{{ entry.metadata.context.state?.say_tpl }}</p>
|
||||
</div>
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<p class="text-[11px] text-white/50">Next transitions</p>
|
||||
<ul
|
||||
v-if="transitionList(entry.metadata.context.state?.next).length"
|
||||
class="list-inside list-disc space-y-1 text-[11px] text-white/70"
|
||||
>
|
||||
<li
|
||||
v-for="(transition, index) in transitionList(entry.metadata.context.state?.next)"
|
||||
:key="`next-${index}`"
|
||||
>
|
||||
{{ describeTransition(transition) }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="text-[11px] text-white/50">—</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[11px] text-white/50">OK transitions</p>
|
||||
<ul
|
||||
v-if="transitionList(entry.metadata.context.state?.ok_next).length"
|
||||
class="list-inside list-disc space-y-1 text-[11px] text-white/70"
|
||||
>
|
||||
<li
|
||||
v-for="(transition, index) in transitionList(entry.metadata.context.state?.ok_next)"
|
||||
:key="`ok-${index}`"
|
||||
>
|
||||
{{ describeTransition(transition) }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="text-[11px] text-white/50">—</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-[11px] text-white/50">Bad transitions</p>
|
||||
<ul
|
||||
v-if="transitionList(entry.metadata.context.state?.bad_next).length"
|
||||
class="list-inside list-disc space-y-1 text-[11px] text-white/70"
|
||||
>
|
||||
<li
|
||||
v-for="(transition, index) in transitionList(entry.metadata.context.state?.bad_next)"
|
||||
:key="`bad-${index}`"
|
||||
>
|
||||
{{ describeTransition(transition) }}
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="text-[11px] text-white/50">—</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="entry.metadata.context.candidates?.length" class="space-y-2">
|
||||
<p class="text-[11px] text-white/50 uppercase tracking-[0.3em]">Candidates</p>
|
||||
<div
|
||||
v-for="(candidate, index) in entry.metadata.context.candidates"
|
||||
:key="candidate.id || index"
|
||||
class="space-y-2 rounded-lg border border-white/10 bg-black/40 p-3"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-mono text-sm text-white">{{ candidate.id || 'unknown' }}</span>
|
||||
<v-chip
|
||||
v-if="candidate.id && candidate.id === entry.metadata?.decision?.next_state"
|
||||
size="x-small"
|
||||
color="cyan"
|
||||
variant="flat"
|
||||
>
|
||||
Selected
|
||||
</v-chip>
|
||||
</div>
|
||||
<p v-if="candidate.state?.name" class="text-[11px] text-white/60">
|
||||
{{ candidate.state?.name }}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-2 text-[11px] text-white/60">
|
||||
<v-chip v-if="candidate.state?.role" size="x-small" color="cyan" variant="outlined">
|
||||
Role: {{ candidate.state?.role }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="candidate.state?.requires_atc_reply"
|
||||
size="x-small"
|
||||
color="cyan"
|
||||
variant="tonal"
|
||||
>
|
||||
Requires ATC reply
|
||||
</v-chip>
|
||||
</div>
|
||||
<p
|
||||
v-if="candidate.state?.say_tpl"
|
||||
class="rounded-lg border border-white/10 bg-black/60 p-2 font-mono text-[11px] text-white"
|
||||
>
|
||||
{{ candidate.state?.say_tpl }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div v-if="entry.metadata.context.variables">
|
||||
<p class="text-[11px] text-white/50">Variables snapshot</p>
|
||||
<pre class="trace-json">{{ formatJson(entry.metadata.context.variables) }}</pre>
|
||||
</div>
|
||||
<div v-if="entry.metadata.context.flags">
|
||||
<p class="text-[11px] text-white/50">Flags snapshot</p>
|
||||
<pre class="trace-json">{{ formatJson(entry.metadata.context.flags) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="entry.metadata?.decisionTrace?.calls?.length" class="space-y-3">
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-200/80">OpenAI Decision Calls</p>
|
||||
<div
|
||||
@@ -803,6 +956,62 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
<template v-for="usage in [buildLlmUsage(entry)]" :key="`${entry.id}-usage-empty`">
|
||||
<div
|
||||
v-if="usage"
|
||||
class="space-y-2 rounded-xl border border-white/10 bg-black/30 p-3 text-[11px] text-white/70"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-200/80">OpenAI Decision Calls</p>
|
||||
<p>No OpenAI call was recorded for this transmission.</p>
|
||||
<p v-if="usage.reason" class="text-white/60">{{ usage.reason }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="entry.channel === 'say'" class="space-y-2">
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-200/80">Say endpoint invocation</p>
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div class="rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
<p class="text-[11px] text-white/50">Voice</p>
|
||||
<p class="font-mono text-sm text-white">{{ entry.metadata?.voice || '—' }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
<p class="text-[11px] text-white/50">Signal level</p>
|
||||
<p class="font-mono text-sm text-white">{{ entry.metadata?.level ?? '—' }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
<p class="text-[11px] text-white/50">Speech speed</p>
|
||||
<p class="font-mono text-sm text-white">{{ entry.metadata?.speed ?? '—' }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
<p class="text-[11px] text-white/50">Radio quality</p>
|
||||
<p class="font-mono text-sm text-white">{{ entry.metadata?.radioQuality || '—' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div class="rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
<p class="text-[11px] text-white/50">TTS provider</p>
|
||||
<p class="font-mono text-sm text-white">{{ entry.metadata?.tts?.provider || '—' }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
<p class="text-[11px] text-white/50">Model</p>
|
||||
<p class="font-mono text-sm text-white">{{ entry.metadata?.tts?.model || '—' }}</p>
|
||||
</div>
|
||||
<div class="rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
<p class="text-[11px] text-white/50">Format</p>
|
||||
<p class="font-mono text-sm text-white">
|
||||
{{ entry.metadata?.tts?.format || '—' }}
|
||||
<span v-if="entry.metadata?.tts?.extension">
|
||||
({{ entry.metadata?.tts?.extension }})
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="entry.metadata?.tag" class="rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
<p class="text-[11px] text-white/50">Tag</p>
|
||||
<p class="font-mono text-sm text-white">{{ entry.metadata?.tag }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-200/80">Metadata</p>
|
||||
<pre class="trace-json">{{ formatJson(entry.metadata) }}</pre>
|
||||
@@ -980,6 +1189,78 @@ interface TransmissionUser {
|
||||
role: string
|
||||
}
|
||||
|
||||
interface DecisionTraceCall {
|
||||
stage: 'readback-check' | 'decision'
|
||||
request: Record<string, any>
|
||||
response?: any
|
||||
rawResponseText?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface DecisionTraceMetadata {
|
||||
calls?: DecisionTraceCall[]
|
||||
fallback?: { used?: boolean; reason?: string; selected?: string }
|
||||
}
|
||||
|
||||
interface CandidateSnapshot {
|
||||
id?: string
|
||||
state?: Record<string, any>
|
||||
}
|
||||
|
||||
interface TransmissionContextSnapshot {
|
||||
stateId?: string
|
||||
state?: Record<string, any>
|
||||
candidates?: CandidateSnapshot[]
|
||||
selectedCandidate?: CandidateSnapshot
|
||||
variables?: Record<string, any>
|
||||
flags?: Record<string, any>
|
||||
}
|
||||
|
||||
interface LlmUsageMetadata {
|
||||
autoDecide?: boolean
|
||||
openaiUsed?: boolean
|
||||
callCount?: number
|
||||
fallbackUsed?: boolean
|
||||
strategy?: 'manual' | 'openai' | 'heuristic' | 'fallback'
|
||||
reason?: string
|
||||
}
|
||||
|
||||
interface TransmissionMetadata {
|
||||
moduleId?: string
|
||||
lessonId?: string
|
||||
autoDecide?: boolean
|
||||
decision?: {
|
||||
next_state?: string
|
||||
controller_say_tpl?: string
|
||||
off_schema?: boolean
|
||||
radio_check?: boolean
|
||||
}
|
||||
decisionTrace?: DecisionTraceMetadata
|
||||
context?: TransmissionContextSnapshot
|
||||
llm?: LlmUsageMetadata
|
||||
tts?: {
|
||||
provider?: string
|
||||
model?: string
|
||||
format?: string
|
||||
extension?: string
|
||||
}
|
||||
voice?: string
|
||||
level?: number
|
||||
speed?: number
|
||||
tag?: string | null
|
||||
radioQuality?: string
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
interface LlmUsageSummary {
|
||||
method: string
|
||||
openaiUsed: boolean
|
||||
callCount: number
|
||||
fallbackUsed: boolean
|
||||
autoDecide?: boolean
|
||||
reason?: string
|
||||
}
|
||||
|
||||
interface TransmissionEntry {
|
||||
id: string
|
||||
role: string
|
||||
@@ -988,7 +1269,7 @@ interface TransmissionEntry {
|
||||
text: string
|
||||
normalized?: string
|
||||
createdAt: string
|
||||
metadata?: Record<string, any>
|
||||
metadata?: TransmissionMetadata
|
||||
user?: TransmissionUser
|
||||
}
|
||||
|
||||
@@ -1203,6 +1484,92 @@ function formatJson(value?: any) {
|
||||
}
|
||||
}
|
||||
|
||||
function transitionList(value: any) {
|
||||
return Array.isArray(value) ? value : []
|
||||
}
|
||||
|
||||
function describeTransition(transition: any) {
|
||||
if (!transition) return '—'
|
||||
if (typeof transition === 'string') return transition
|
||||
if (typeof transition !== 'object') return String(transition)
|
||||
|
||||
const destination = transition.to || transition.id || '—'
|
||||
const details: string[] = []
|
||||
|
||||
if (transition.when) details.push(`when: ${transition.when}`)
|
||||
if (transition.action) details.push(`action: ${transition.action}`)
|
||||
if (transition.intent) details.push(`intent: ${transition.intent}`)
|
||||
if (transition.auto) details.push(`auto: ${transition.auto}`)
|
||||
if (transition.say_tpl || transition.controller_say_tpl) details.push('say')
|
||||
if (transition.note) details.push(`note: ${transition.note}`)
|
||||
if (transition.condition) details.push(`cond: ${transition.condition}`)
|
||||
|
||||
return details.length ? `${destination} (${details.join(', ')})` : destination
|
||||
}
|
||||
|
||||
function buildLlmUsage(entry: TransmissionEntry): LlmUsageSummary | null {
|
||||
const metadata = entry.metadata
|
||||
if (!metadata) return null
|
||||
|
||||
const hasDecisionData =
|
||||
metadata.autoDecide !== undefined ||
|
||||
Boolean(metadata.llm) ||
|
||||
Boolean(metadata.decisionTrace?.calls?.length) ||
|
||||
Boolean(metadata.decisionTrace?.fallback?.used)
|
||||
|
||||
if (!hasDecisionData) return null
|
||||
|
||||
const autoDecide = metadata.llm?.autoDecide ?? metadata.autoDecide
|
||||
const callCount = metadata.llm?.callCount ?? metadata.decisionTrace?.calls?.length ?? 0
|
||||
const fallbackUsed = metadata.llm?.fallbackUsed ?? Boolean(metadata.decisionTrace?.fallback?.used)
|
||||
const openaiUsed = metadata.llm?.openaiUsed ?? callCount > 0
|
||||
|
||||
const strategy =
|
||||
metadata.llm?.strategy ||
|
||||
(!autoDecide
|
||||
? 'manual'
|
||||
: openaiUsed
|
||||
? 'openai'
|
||||
: fallbackUsed
|
||||
? 'fallback'
|
||||
: 'heuristic')
|
||||
|
||||
let method: string
|
||||
switch (strategy) {
|
||||
case 'openai':
|
||||
method = `OpenAI decision (${callCount} ${callCount === 1 ? 'call' : 'calls'})`
|
||||
break
|
||||
case 'fallback':
|
||||
method = 'Fallback decision after OpenAI error'
|
||||
break
|
||||
case 'manual':
|
||||
method = 'Manual routing (auto decision disabled)'
|
||||
break
|
||||
default:
|
||||
method = 'Heuristic decision (no OpenAI call)'
|
||||
break
|
||||
}
|
||||
|
||||
const reason =
|
||||
metadata.llm?.reason ||
|
||||
(strategy === 'openai'
|
||||
? `Decision derived from OpenAI with ${callCount} ${callCount === 1 ? 'call' : 'calls'}.`
|
||||
: strategy === 'fallback'
|
||||
? metadata.decisionTrace?.fallback?.reason || 'Fallback executed because OpenAI response could not be used.'
|
||||
: strategy === 'manual'
|
||||
? 'Automatic decision was disabled for this transmission.'
|
||||
: 'Rules and heuristics resolved the decision without contacting OpenAI.')
|
||||
|
||||
return {
|
||||
method,
|
||||
openaiUsed,
|
||||
callCount,
|
||||
fallbackUsed,
|
||||
autoDecide,
|
||||
reason,
|
||||
}
|
||||
}
|
||||
|
||||
function isExpired(expiresAt?: string) {
|
||||
if (!expiresAt) return false
|
||||
const date = new Date(expiresAt)
|
||||
|
||||
@@ -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) => {
|
||||
const body = await readBody<PTTRequest>(event);
|
||||
|
||||
@@ -161,6 +174,64 @@ export default defineEventHandler(async (event) => {
|
||||
|
||||
try {
|
||||
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({
|
||||
user: user?._id,
|
||||
role: "pilot",
|
||||
@@ -173,6 +244,15 @@ export default defineEventHandler(async (event) => {
|
||||
decision,
|
||||
decisionTrace: decisionResult?.trace,
|
||||
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) {
|
||||
|
||||
@@ -159,6 +159,7 @@ export default defineEventHandler(async (event) => {
|
||||
let audioBuffer: Buffer;
|
||||
let modelUsed: string;
|
||||
let actualMime = mime;
|
||||
let ttsProvider: 'openai' | 'speaches' | 'piper' = 'openai';
|
||||
|
||||
if (useSpeaches) {
|
||||
// Speaches (prefer compact: MP3, otherwise FLAC/WAV/PCM)
|
||||
@@ -171,12 +172,14 @@ export default defineEventHandler(async (event) => {
|
||||
modelUsed = model;
|
||||
// Server returns the correct format according to response_format
|
||||
actualMime = fmtToMime(fmt);
|
||||
ttsProvider = 'speaches';
|
||||
} else if (usePiper) {
|
||||
// Local Piper
|
||||
audioBuffer = await piperTTS(normalized, voice, runtimeConfig.piperPort);
|
||||
modelUsed = "piper-local";
|
||||
// Piper returns WAV
|
||||
actualMime = "audio/wav";
|
||||
ttsProvider = 'piper';
|
||||
} else {
|
||||
// OpenAI (fallback)
|
||||
const tts = await normalize.audio.speech.create({
|
||||
@@ -189,6 +192,7 @@ export default defineEventHandler(async (event) => {
|
||||
audioBuffer = Buffer.from(await tts.arrayBuffer());
|
||||
modelUsed = TTS_MODEL;
|
||||
actualMime = "audio/wav";
|
||||
ttsProvider = 'openai';
|
||||
}
|
||||
|
||||
// Optional persistence
|
||||
@@ -208,7 +212,8 @@ export default defineEventHandler(async (event) => {
|
||||
lessonId: body?.lessonId || null,
|
||||
files: { audio: fileOut },
|
||||
model: modelUsed,
|
||||
format: actualMime
|
||||
format: actualMime,
|
||||
ttsProvider
|
||||
};
|
||||
|
||||
// await writeFile(fileJson, JSON.stringify(meta, null, 2), "utf-8");
|
||||
@@ -229,6 +234,12 @@ export default defineEventHandler(async (event) => {
|
||||
lessonId: body?.lessonId || null,
|
||||
tag: body?.tag || null,
|
||||
radioQuality: radioQuality.description,
|
||||
tts: {
|
||||
provider: ttsProvider,
|
||||
model: modelUsed,
|
||||
format: actualMime,
|
||||
extension: ext
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (logError) {
|
||||
|
||||
Reference in New Issue
Block a user