From e861578e4e8034192192b177f560539ce5b296da Mon Sep 17 00:00:00 2001
From: Remi <73385395+itsrubberduck@users.noreply.github.com>
Date: Thu, 18 Sep 2025 23:55:13 +0200
Subject: [PATCH] Translate learn page and add audio speed control
---
app/pages/learn.vue | 286 ++++++++++++++++++++--------------
server/api/learn/state.put.ts | 5 +
server/models/LearnProfile.ts | 1 +
shared/learn/config.ts | 2 +
4 files changed, 179 insertions(+), 115 deletions(-)
diff --git a/app/pages/learn.vue b/app/pages/learn.vue
index 8c7359c..494c2a9 100644
--- a/app/pages/learn.vue
+++ b/app/pages/learn.vue
@@ -10,7 +10,7 @@
OpenSquawk
|
- Lernwelt
+ Training
@@ -20,22 +20,34 @@
mdi-radio-handheld
- L {{ cfg.radioLevel }}
-
+ Level {{ cfg.radioLevel }}
+
+
+
+ mdi-speedometer
+ Speed {{ cfg.audioSpeed.toFixed(1) }}x
+
-
@@ -98,7 +110,7 @@
Mission Hub
-
Starte mit ICAO Alphabet & Zahlen, dann Basics, Ground …
+
Start with the ICAO alphabet & numbers, then basics, ground, and beyond.
@@ -128,7 +140,7 @@
mdi-skip-next
- Weiter
+ Continue
@@ -137,7 +149,7 @@
-
+
@@ -168,7 +180,7 @@
{{ l.title }}
- {{ bestScore(current.id, l.id) ? bestScore(current.id, l.id) + '%' : 'neu' }}
+ {{ bestScore(current.id, l.id) ? bestScore(current.id, l.id) + '%' : 'new' }}
{{ l.desc }}
@@ -186,12 +198,12 @@
{{ scenario.radioCall }}
-
Flugplatz
+
Airport
{{ scenario.airport.icao }}
{{ scenario.airport.name }}
-
Frequenzen
+
Frequencies
- Audio Challenge aktiv – zuerst anhören.
+ Audio Challenge active – listen first.
@@ -245,11 +257,11 @@
@click="revealAudioContent"
>
mdi-eye
- Anzeigen
+ Show
mdi-dice-5
- Würfeln
+ Roll
@@ -272,7 +284,7 @@
-
Deine Readback
+
Your Readback
@@ -294,7 +306,7 @@
mdi-check
mdi-alert
- Soll: {{ fieldExpectedValue(segment.key) }}
+ Expected: {{ fieldExpectedValue(segment.key) }}
@@ -303,7 +315,7 @@
mdi-check
- Prüfen
+ Check
mdi-eraser
@@ -311,20 +323,29 @@
mdi-auto-fix
- Autotext
+ Autofill
+
+
+ mdi-arrow-right-bold
+ {{ nextLesson ? 'Next Lesson' : 'Back to Hub' }}
{{ result.score }}%
- Felder korrekt: {{ result.hits }}/{{ activeLesson.fields.length }} · Ähnlichkeit: {{ Math.round(result.sim * 100) }}%
+ Fields correct: {{ result.hits }}/{{ activeLesson.fields.length }} · Similarity: {{ Math.round(result.sim * 100) }}%
{{ field.label }}
{{ field.answer || '—' }}
-
Soll: {{ field.expected }}
+
Expected: {{ field.expected }}
@@ -332,8 +353,8 @@
-
- Gesamtfortschritt
+
+ Overall Progress
@@ -353,13 +374,13 @@
-
ATC Einstellungen
+
ATC Settings
Browser-TTS (Web Speech)
- Läuft offline, startet schneller und spart unsere API-Anfragen.
+ Works offline, starts faster, and saves our API calls.
@@ -367,38 +388,53 @@
Audio Challenge
- Blendet Zieltext & Infos, bis du sie manuell einblendest.
+ Hides target text & details until you reveal them manually.
- Funk-Level (1..5)
+ Radio level (1..5)
+
+
+ ATC audio speed
+ Fine-tune the playback speed for ATC voice lines.
+
+
+
+
Voice
-
Probe-TTS
+
Test TTS
mdi-volume-high
- Probe
+ Test
mdi-stop
- Stopp
+ Stop
- Schließen
+ Close
@@ -1014,17 +1050,17 @@ const modules = ref
([
lessons: [
{
id: 'icao-alphabet',
- title: 'ICAO Alphabet & Zahlen',
- desc: 'Buchstaben und Ziffern sauber buchstabieren',
+ title: 'ICAO Alphabet & Numbers',
+ desc: 'Spell letters and digits clearly',
keywords: ['Alphabet', 'Numbers', 'Normalize'],
hints: [
- 'Buchstabiere jeden Buchstaben mit dem ICAO-Namen (z. B. Delta, Lima, Hotel).',
- 'Ziffern im Funk: tree, fower, fife, niner.'
+ 'Spell each letter with the ICAO name (e.g. Delta, Lima, Hotel).',
+ 'Digits on the radio: tree, fower, fife, niner.'
],
fields: [
{
key: 'letters',
- label: 'Buchstaben',
+ label: 'Letters',
expected: scenario => scenario.callsignNato,
placeholder: 'Delta Lima Hotel',
width: 'xl',
@@ -1032,7 +1068,7 @@ const modules = ref([
},
{
key: 'digits',
- label: 'Zahlen',
+ label: 'Digits',
expected: scenario => scenario.flightNumberWords,
placeholder: 'one two three',
width: 'lg',
@@ -1040,7 +1076,7 @@ const modules = ref([
}
],
readback: [
- { type: 'text', text: 'Rufezeichen: ' },
+ { type: 'text', text: 'Callsign: ' },
{ type: 'field', key: 'letters', width: 'lg' },
{ type: 'text', text: ' · ' },
{ type: 'field', key: 'digits', width: 'md' }
@@ -1048,21 +1084,21 @@ const modules = ref([
defaultFrequency: 'DEL',
phrase: scenario => `${scenario.callsignNato} ${scenario.flightNumberWords}`,
info: scenario => [
- `Kennzeichen: ${scenario.callsign}`,
- `Airline Call: ${scenario.radioCall}`,
+ `Registration: ${scenario.callsign}`,
+ `Airline call: ${scenario.radioCall}`,
`ICAO: ${scenario.callsignNato}`,
- `Flightnummer gesprochen: ${scenario.flightNumberWords}`
+ `Flight number spoken: ${scenario.flightNumberWords}`
],
generate: createBaseScenario
},
{
id: 'atis',
- title: 'ATIS verstehen',
- desc: 'Kennung und Kerndaten aus der ATIS ziehen',
- keywords: ['ATIS', 'Wetter'],
+ title: 'Understand ATIS',
+ desc: 'Extract the identifier and core data from the ATIS',
+ keywords: ['ATIS', 'Weather'],
hints: [
- 'ATIS-Kennung als einzelnen Buchstaben merken.',
- 'Reihenfolge: Runway – Wind – Sicht – Temperatur – Taupunkt – QNH.'
+ 'Remember the ATIS designator as a single letter.',
+ 'Order: runway – wind – visibility – temperature – dew point – QNH.'
],
fields: [
{
@@ -1074,7 +1110,7 @@ const modules = ref([
scenario.atisCode.toLowerCase(),
`Information ${scenario.atisCode}`
],
- placeholder: 'Buchstabe',
+ placeholder: 'Letter',
width: 'xs',
threshold: 0.9
},
@@ -1098,21 +1134,21 @@ const modules = ref([
},
{
key: 'atis-visibility',
- label: 'Sicht',
+ label: 'Visibility',
expected: scenario => scenario.atisSummary.visibility,
alternatives: scenario => [scenario.visibility, scenario.visibilityWords],
width: 'sm'
},
{
key: 'atis-temp',
- label: 'Temperatur',
+ label: 'Temperature',
expected: scenario => scenario.atisSummary.temperature,
alternatives: scenario => [scenario.temperatureWords],
width: 'sm'
},
{
key: 'atis-dew',
- label: 'Taupunkt',
+ label: 'Dew point',
expected: scenario => scenario.atisSummary.dewpoint,
alternatives: scenario => [scenario.dewpointWords],
width: 'sm'
@@ -1144,19 +1180,19 @@ const modules = ref([
defaultFrequency: 'ATIS',
phrase: scenario => scenario.atisText,
info: () => [
- 'Notiere Kennung, Runway, Wind, Sichtweite, Temperatur, Taupunkt und QNH.',
- 'Sicht: vier Ziffern oder 9999 für ≥10 km · QNH als vierstellige Zahl.'
+ 'Take note of the designator, runway, wind, visibility, temperature, dew point, and QNH.',
+ 'Visibility: four digits or 9999 for ≥10 km · QNH as a four-digit number.'
],
generate: createBaseScenario
},
{
id: 'metar',
- title: 'METAR entschlüsseln',
- desc: 'Rohe METAR-Werte extrahieren',
+ title: 'Decode METAR',
+ desc: 'Extract raw METAR values.',
keywords: ['METAR', 'Weather'],
hints: [
- 'METAR blockweise lesen: Wind – Sicht – Wolken – Temperatur – QNH.',
- 'Der Temperaturblock hat die Form 18/10, Minuswerte beginnen mit M.'
+ 'Read METARs in blocks: wind – visibility – clouds – temperature – QNH.',
+ 'The temperature block looks like 18/10, negative values start with M.'
],
fields: [
{
@@ -1172,7 +1208,7 @@ const modules = ref([
},
{
key: 'metar-vis',
- label: 'Sicht',
+ label: 'Visibility',
expected: scenario => scenario.metarSegments.visibility,
alternatives: scenario => [scenario.visibility],
placeholder: '9999',
@@ -1199,9 +1235,9 @@ const modules = ref([
readback: [
{ type: 'text', text: 'Wind ' },
{ type: 'field', key: 'metar-wind', width: 'md' },
- { type: 'text', text: ', Sicht ' },
+ { type: 'text', text: ', visibility ' },
{ type: 'field', key: 'metar-vis', width: 'sm' },
- { type: 'text', text: ', Temperatur ' },
+ { type: 'text', text: ', temperature ' },
{ type: 'field', key: 'metar-temp', width: 'md' },
{ type: 'text', text: ', QNH ' },
{ type: 'field', key: 'metar-qnh', width: 'sm' }
@@ -1210,18 +1246,18 @@ const modules = ref([
phrase: scenario => scenario.metar,
info: scenario => [
`METAR: ${scenario.metar}`,
- `Interpretation: Wind ${scenario.metarSegments.wind}, Sicht ${scenario.visibilityWords}, Temperatur ${scenario.temperature}°C, QNH ${scenario.qnh}`
+ `Interpretation: Wind ${scenario.metarSegments.wind}, visibility ${scenario.visibilityWords}, temperature ${scenario.temperature}°C, QNH ${scenario.qnh}`
],
generate: createBaseScenario
},
{
id: 'radio-check',
title: 'Radio Check',
- desc: 'Lesbarkeit melden',
+ desc: 'Report readability',
keywords: ['Ground', 'Comms'],
hints: [
- 'Antwort besteht aus "Readability" plus Zahl.',
- 'Beende den Check immer mit deinem Callsign.'
+ 'Reply with "Readability" plus the number.',
+ 'Always end the check with your callsign.'
],
fields: [
{
@@ -1238,7 +1274,7 @@ const modules = ref([
},
{
key: 'rc-readability',
- label: 'Lesbarkeit',
+ label: 'Readability',
expected: scenario => scenario.readabilityWord,
alternatives: scenario => [scenario.readability.toString(), scenario.readabilityWord],
placeholder: 'five',
@@ -1254,8 +1290,8 @@ const modules = ref([
defaultFrequency: 'GND',
phrase: scenario => `${scenario.airport.name} Ground, ${scenario.radioCall}, radio check on ${scenario.groundFreq}.`,
info: scenario => [
- `Frequenz: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`,
- `Erwartete Antwort: Readability ${scenario.readability} (${scenario.readabilityWord})`
+ `Frequency: ${scenario.groundFreq} (${scenario.frequencyWords.GND})`,
+ `Expected answer: readability ${scenario.readability} (${scenario.readabilityWord})`
],
generate: createBaseScenario
}
@@ -1264,17 +1300,17 @@ const modules = ref([
{
id: 'arc',
title: 'ARC Decision Tree',
- subtitle: 'Vom Clearance Call bis Departure',
+ subtitle: 'From the clearance call to departure',
art: gradientArt(['#f97316', '#fb923c', '#0f172a']),
lessons: [
{
id: 'clearance-contact',
- title: 'Delivery: Erstkontakt',
- desc: 'Clearance Delivery kontaktieren',
+ title: 'Delivery: Initial contact',
+ desc: 'Call clearance delivery',
keywords: ['Delivery', 'Clearance'],
hints: [
- 'Reihenfolge: Einheit, Callsign, ATIS, Ziel, Stand, Request.',
- 'ATIS als einzelner Buchstabe sprechen.'
+ 'Order: unit, callsign, ATIS, destination, stand, request.',
+ 'Say the ATIS as a single letter.'
],
fields: [
{
@@ -1319,7 +1355,7 @@ const modules = ref([
phrase: scenario => `${scenario.airport.city} Delivery, ${scenario.radioCall}, information ${scenario.atisCode}, IFR to ${scenario.destination.city}, stand ${scenario.stand}, request clearance.`,
info: scenario => [
`ATIS ${scenario.atisCode}`,
- `Ziel: ${scenario.destination.city} (${scenario.destination.icao})`,
+ `Destination: ${scenario.destination.city} (${scenario.destination.icao})`,
`Stand: ${scenario.stand}`
],
generate: createBaseScenario
@@ -1327,11 +1363,11 @@ const modules = ref([
{
id: 'clearance-readback',
title: 'Clearance Readback',
- desc: 'Freigabe komplett zurücklesen',
+ desc: 'Read back the clearance completely',
keywords: ['Delivery', 'Readback'],
hints: [
- 'Reihenfolge merken: Ziel – SID – Runway – Altitude – Squawk.',
- 'Zahlen bei Altitude und Squawk sauber ausschreiben.'
+ 'Remember the sequence: destination – SID – runway – altitude – squawk.',
+ 'Spell out the altitude and squawk digits clearly.'
],
fields: [
{
@@ -1393,11 +1429,11 @@ const modules = ref([
{
id: 'pushback',
title: 'Push & Start Readback',
- desc: 'Pushback-Freigabe bestätigen',
+ desc: 'Confirm the pushback clearance',
keywords: ['Ground', 'Pushback'],
hints: [
- 'Bahnrichtung und QNH übernehmen, Callsign ans Ende.',
- 'QNH darf als Zahl oder "QNH xxxx" stehen.'
+ 'Repeat the runway direction and QNH, callsign at the end.',
+ 'You may say QNH as a number or as "QNH xxxx".'
],
fields: [
{
@@ -1445,11 +1481,11 @@ const modules = ref([
{
id: 'taxi',
title: 'Taxi Readback',
- desc: 'Taxi-Freigabe mit Hold Short',
+ desc: 'Read back the taxi clearance with the hold short instruction',
keywords: ['Ground', 'Taxi'],
hints: [
- 'Route wie gehört wiederholen, inklusive Hold Short.',
- 'Am Ende wieder Callsign.'
+ 'Repeat the route as given, including the hold short.',
+ 'Finish with the callsign again.'
],
fields: [
{
@@ -1498,19 +1534,19 @@ const modules = ref([
defaultFrequency: 'GND',
phrase: scenario => `${scenario.radioCall}, taxi to runway ${scenario.runway} via ${scenario.taxiRoute}, hold short runway ${scenario.runway}.`,
info: scenario => [
- `Taxi-Route: ${scenario.taxiRoute}`,
- `Hold Short: ${scenario.runway}`
+ `Taxi route: ${scenario.taxiRoute}`,
+ `Hold short: ${scenario.runway}`
],
generate: createBaseScenario
},
{
id: 'lineup',
title: 'Line-up Clearance',
- desc: 'Aufrollen und warten bestätigen',
+ desc: 'Confirm line up and wait',
keywords: ['Tower', 'Line Up'],
hints: [
- 'Runway wiederholen, dann "line up and wait".',
- 'Callsign am Ende setzen.'
+ 'Repeat the runway, then say "line up and wait".',
+ 'Place the callsign at the end.'
],
fields: [
{
@@ -1542,18 +1578,18 @@ const modules = ref([
phrase: scenario => `${scenario.radioCall}, line up and wait runway ${scenario.runway}.`,
info: scenario => [
`Tower: ${scenario.towerFreq} (${scenario.frequencyWords.TWR})`,
- `Line-up: runway ${scenario.runway}`
+ `Line up: runway ${scenario.runway}`
],
generate: createBaseScenario
},
{
id: 'takeoff',
title: 'Takeoff Clearance',
- desc: 'Takeoff-Freigabe bestätigen',
+ desc: 'Acknowledge the takeoff clearance',
keywords: ['Tower', 'Departure'],
hints: [
- 'Reihenfolge: Runway – cleared for takeoff – Callsign.',
- 'Windangabe kann entfallen, wenn nicht gefordert.'
+ 'Order: runway – cleared for takeoff – callsign.',
+ 'Wind call can be omitted if not stated.'
],
fields: [
{
@@ -1592,16 +1628,16 @@ const modules = ref([
{
id: 'departure-handoff',
title: 'Departure Handoff',
- desc: 'Auf Departure wechseln',
+ desc: 'Switch to departure',
keywords: ['Departure', 'Handoff'],
hints: [
- 'Frequenz exakt wiederholen, optional als Zahl oder gesprochen.',
- 'Callsign am Ende anhängen.'
+ 'Repeat the frequency exactly, either as digits or spoken.',
+ 'Add the callsign at the end.'
],
fields: [
{
key: 'dep-freq',
- label: 'Frequenz',
+ label: 'Frequency',
expected: scenario => scenario.departureFreq,
alternatives: scenario => [
scenario.departureFreq,
@@ -1632,18 +1668,18 @@ const modules = ref([
phrase: scenario => `${scenario.radioCall}, contact departure ${scenario.departureFreq}.`,
info: scenario => [
`Departure: ${scenario.departureFreq} (${scenario.frequencyWords.DEP})`,
- `Tower Handoff nach Start.`
+ `Tower handoff after departure.`
],
generate: createBaseScenario
},
{
id: 'departure-checkin',
title: 'Departure Check-in',
- desc: 'Erstmeldung bei Departure',
+ desc: 'First call with departure',
keywords: ['Departure', 'Check-in'],
hints: [
- 'Einheit ansprechen, dann Callsign nennen.',
- 'Altitude und SID exakt wie gehört wiederholen.'
+ 'Address the unit, then state the callsign.',
+ 'Repeat the altitude and SID exactly as given.'
],
fields: [
{
@@ -1694,11 +1730,11 @@ const modules = ref([
{
id: 'climb-instruction',
title: 'Climb & Direct',
- desc: 'Steigauftrag komplett zurücklesen',
+ desc: 'Read back the climb instruction completely',
keywords: ['Departure', 'Climb'],
hints: [
- 'Mit "Climb" beginnen, dann Höhe und gegebenenfalls Direct.',
- 'Callsign am Ende wiederholen.'
+ 'Start with "Climb", then the altitude and any direct.',
+ 'Repeat the callsign at the end.'
],
fields: [
{
@@ -1749,16 +1785,16 @@ const modules = ref([
{
id: 'center-handoff',
title: 'Center Handoff',
- desc: 'Übergabe an Center bestätigen',
+ desc: 'Acknowledge the handoff to center',
keywords: ['Center', 'Handoff'],
hints: [
- 'Frequenz exakt wiedergeben (mit oder ohne Dezimalpunkt).',
- 'Callsign am Ende platzieren.'
+ 'Repeat the frequency exactly (with or without the decimal).',
+ 'Place the callsign at the end.'
],
fields: [
{
key: 'ctr-freq',
- label: 'Frequenz',
+ label: 'Frequency',
expected: scenario => scenario.centerFreq,
alternatives: scenario => [
scenario.centerFreq,
@@ -1789,7 +1825,7 @@ const modules = ref([
phrase: scenario => `${scenario.radioCall}, contact center ${scenario.centerFreq}.`,
info: scenario => [
`Center: ${scenario.centerFreq} (${scenario.frequencyWords.CTR})`,
- `Nächste Einheit: Center`
+ `Next unit: Center`
],
generate: createBaseScenario
}
@@ -1934,6 +1970,7 @@ if (isClient) {
resetAudioReveal()
})
watch(() => cfg.value.radioLevel, markConfigDirty)
+ watch(() => cfg.value.audioSpeed, markConfigDirty)
watch(() => cfg.value.voice, markConfigDirty)
}
@@ -2029,6 +2066,12 @@ function fieldExpectedValue(key: string): string {
const targetPhrase = computed(() => (activeLesson.value && scenario.value ? activeLesson.value.phrase(scenario.value) : ''))
const lessonInfo = computed(() => (activeLesson.value && scenario.value ? activeLesson.value.info(scenario.value) : []))
+const nextLesson = computed(() => {
+ if (!current.value || !activeLesson.value) return null
+ const lessons = current.value.lessons
+ const index = lessons.findIndex(lesson => lesson.id === activeLesson.value?.id)
+ return index >= 0 && index < lessons.length - 1 ? lessons[index + 1] : null
+})
watch(activeLesson, lesson => {
if (lesson) {
@@ -2158,6 +2201,18 @@ function selectLesson(lesson: Lesson) {
activeLesson.value = lesson
}
+function goToNextLesson() {
+ stopAudio()
+ if (nextLesson.value) {
+ activeLesson.value = nextLesson.value
+ return
+ }
+ panel.value = 'hub'
+ activeLesson.value = null
+ current.value = null
+ result.value = null
+}
+
function bestScore(modId: string, lesId: string) {
return progress.value[modId]?.[lesId]?.best || 0
}
@@ -2185,9 +2240,9 @@ function avgScore(modId: string) {
}
const dailies = ref([
- { id: 'd1', title: '3 Readbacks ≥80%', sub: 'Belohnung: +50 XP', reward: 50 },
- { id: 'd2', title: '1 Modul starten', sub: 'Belohnung: +20 XP', reward: 20 },
- { id: 'd3', title: 'Zielphrase abspielen', sub: 'Belohnung: +10 XP', reward: 10 }
+ { id: 'd1', title: '3 readbacks ≥80%', sub: 'Reward: +50 XP', reward: 50 },
+ { id: 'd2', title: 'Start one module', sub: 'Reward: +20 XP', reward: 20 },
+ { id: 'd3', title: 'Play a target phrase', sub: 'Reward: +10 XP', reward: 10 }
])
function startDaily(daily: { id: string; reward: number; title: string }) {
@@ -2280,7 +2335,8 @@ async function say(text: string) {
if (!trimmed) return
const rateBase = 0.9 + (cfg.value.radioLevel - 3) * 0.12
- const normalizedRate = Math.min(1.5, Math.max(0.6, rateBase))
+ const rateAdjusted = rateBase * (cfg.value.audioSpeed || 1)
+ const normalizedRate = Math.min(1.6, Math.max(0.6, rateAdjusted))
const hasBrowserTts = cfg.value.tts && typeof window !== 'undefined' && 'speechSynthesis' in window
diff --git a/server/api/learn/state.put.ts b/server/api/learn/state.put.ts
index 3927181..7223ef7 100644
--- a/server/api/learn/state.put.ts
+++ b/server/api/learn/state.put.ts
@@ -64,6 +64,11 @@ function sanitizeConfig(input: Partial | undefined): LearnConfig |
config.radioLevel = Math.min(5, Math.max(1, level))
}
+ if (typeof input.audioSpeed === 'number' && Number.isFinite(input.audioSpeed)) {
+ const rounded = Math.round(input.audioSpeed * 10) / 10
+ config.audioSpeed = Math.min(1.3, Math.max(0.7, rounded))
+ }
+
if (typeof input.voice === 'string') {
config.voice = input.voice.slice(0, 120)
}
diff --git a/server/models/LearnProfile.ts b/server/models/LearnProfile.ts
index 3c60165..22c9154 100644
--- a/server/models/LearnProfile.ts
+++ b/server/models/LearnProfile.ts
@@ -20,6 +20,7 @@ const learnProfileSchema = new mongoose.Schema(
radioLevel: { type: Number, default: 4, min: 1, max: 5 },
voice: { type: String, default: '', trim: true },
audioChallenge: { type: Boolean, default: false },
+ audioSpeed: { type: Number, default: 1, min: 0.5, max: 2 },
},
},
{
diff --git a/shared/learn/config.ts b/shared/learn/config.ts
index c924e8b..cf445cc 100644
--- a/shared/learn/config.ts
+++ b/shared/learn/config.ts
@@ -10,6 +10,7 @@ export interface LearnConfig {
radioLevel: number
voice: string
audioChallenge: boolean
+ audioSpeed: number
}
export interface LearnState {
@@ -23,6 +24,7 @@ export const LEARN_CONFIG_DEFAULTS: LearnConfig = {
radioLevel: 4,
voice: '',
audioChallenge: false,
+ audioSpeed: 1,
}
export function createDefaultLearnConfig(): LearnConfig {