CW QSO Trainer is an AI-assisted command-line trainer for CW contest copy practice.
Author: ENDOH-R
It runs in the terminal, generates contest-style CW exchanges, accepts typed answers, scores them locally, adapts WPM during practice, saves sessions, and provides post-session review with teacher feedback.
AI is optional and used only for teacher feedback when enabled. CW generation, expected answers, answer normalization, scoring, adaptive difficulty, session persistence, and achievement evaluation are local deterministic behavior.
Implemented CLI commands include cwqso contest, cwqso review,
cwqso sessions, cwqso doctor, cwqso achievements, and
cwqso collection.
- CLI commands for contest practice, review, sessions, diagnostics, achievements, and achievement collection.
- Contest-style copy prompts such as
JA1ABC TEST. - Local deterministic scoring with exact answers, CW shorthand equivalence, and weak-copy partial credit.
- Adaptive WPM based on correct and wrong streaks.
- Session persistence as JSON.
- Offline deterministic teacher feedback.
- Optional OpenAI-compatible AI teacher feedback.
- Achievement display and ASCII-art collection.
- English and Japanese output for implemented localized commands.
- Callsign profiles:
jp,us,uk,dx.
Python 3.11 or newer is required.
From a local checkout:
git clone https://github.com/ENDOH-R/CW-QSO-trainer.git
cd CW-QSO-trainer
uv run cwqso --helpFor editable development install:
uv venv --python=3.11
source .venv/bin/activate
uv pip install -e ".[dev]"For audio playback dependencies:
uv pip install -e ".[dev,audio]"Contest mode continues even if audio playback is unavailable. It prints an audio error and still accepts typed answers.
uv run cwqso contest
uv run cwqso contest --questions 10
uv run cwqso review
uv run cwqso achievements
uv run cwqso doctorSee all commands:
uv run cwqso --helpImplemented commands:
contest
review
sessions
doctor
achievements
collection
Start a training session:
uv run cwqso contestUseful options:
uv run cwqso contest --wpm 24 --freq 700 --volume 0.3
uv run cwqso contest --seed 42 --questions 10
uv run cwqso contest --adaptive
uv run cwqso contest --no-adaptive
uv run cwqso contest --show-answer
uv run cwqso contest --callsign-profile jp
uv run cwqso contest --contest-profile cqww
uv run cwqso contest --message-style realistic --my-call N0CALL
uv run cwqso contest --max-repeats 1
uv run cwqso contest --cw-shorthand standard
uv run cwqso contest --operating-mode run
uv run cwqso contest --operator-style fast
uv run cwqso contest --qrn 0.1 --qsb 0.2 --qrm 0.1
uv run cwqso contest --noise-profile normal --adaptive-noise
uv run cwqso contest --head-copy --reveal-answer wrong --answer-timeout 12
uv run cwqso contest --head-copy-preset hardCurrent contest options are --wpm, --freq, --volume, --seed,
--questions, --adaptive / --no-adaptive, --show-answer,
--callsign-profile, --qrn, --qsb, --qrm, --noise-profile,
--adaptive-noise / --no-adaptive-noise, --head-copy / --no-head-copy,
--reveal-answer, --answer-timeout, --head-copy-preset,
--contest-profile, --message-style, --max-repeats, --my-call,
--cw-shorthand, --operating-mode, and --operator-style.
Default settings:
- WPM:
20 - Tone:
600Hz - Volume:
0.2 - Questions:
5 - Adaptive difficulty: enabled
- Show answer: disabled
- Callsign profile:
jpby default;cqwwandarrl_dxuseuswhen--callsign-profileis omitted - Contest profile:
generic_serial - Message style:
simple - My callsign:
N0CALL - Max repeats per copy step:
2 - QRN band noise:
0.0(disabled) - QSB fading:
0.0(disabled) - QRM interferer:
0.0(disabled) - Noise profile: disabled
- Adaptive noise: disabled
- Head-copy mode: disabled
- Reveal answer:
always - Answer timeout:
0.0(disabled) - Head-copy preset: disabled
- CW shorthand:
contest - Operating mode:
sp - Operator style:
standard
Contest mode uses a two-step copy flow. First, copy the other station callsign:
RX CALL > JA1ABC TEST
YOU CALL > JA1ABC
Then copy the contest exchange:
RX EXCHANGE > 5NN 001
YOU EXCHANGE > 599 001
Operators may begin typing their answer while CW audio is still playing. The audio playback runs asynchronously in the background, allowing immediate input without waiting for the audio to finish.
Use --message-style realistic for more contest-like RX text while keeping the
required answers unchanged:
RX CALL > CQ TEST JA1ABC
YOU CALL > JA1ABC
RX EXCHANGE > N0CALL 5NN 001 TU
YOU EXCHANGE > 599 001
simple is the default message style. realistic affects visible RX text and
CW playback text only; users still type just the callsign in the CALL step and
just the exchange in the EXCHANGE step. It does not change scoring or saved
session JSON.
Use --my-call CALLSIGN to set your station identity for realistic exchange
messages. The default is N0CALL. In realistic contest operation, the other
station sends back your callsign plus the exchange:
RX EXCHANGE > N0CALL 5NN 001 TU
YOU EXCHANGE > 599 001
--my-call does not change the required answer, scoring, or saved session JSON.
It only changes the realistic EXCHANGE payload. Simple message style keeps
RX EXCHANGE > 5NN 001 with the default contest shorthand.
During either copy step, enter AGN, ?, AGN?, or RPT to request a repeat.
Repeat commands are case-insensitive. A repeat replays and redisplays the same
CALL or EXCHANGE message, keeps the current question active, and does not score
immediately.
RX CALL > CQ TEST JA1ABC
YOU CALL > AGN
RX CALL > CQ TEST JA1ABC
YOU CALL > JA1ABC
Use --max-repeats INTEGER to limit repeat requests per copy step. The default
is 2. Exceeding the limit treats the question as incorrect using the normal
wrong-answer flow. Repeat counts are runtime-only and are not stored in session
JSON.
Use --head-copy to hide the visible RX payloads and practice listening from
CW audio memory. The prompts show RX CALL > [CW AUDIO] and
RX EXCHANGE > [CW AUDIO], audio playback still sends the selected simple or
realistic message text, and scoring stays unchanged. --head-copy cannot be
combined with --show-answer. It does not change the saved session JSON schema.
In head-copy mode, operators may begin typing their answer while CW audio is still playing. The audio playback runs asynchronously in the background, allowing immediate input without waiting for the audio to finish.
Available contest profiles:
generic_serial 5NN 001
cqww 5NN 25
all_ja 5NN 0601
arrl_dx 5NN 100
generic_serial uses RST + serial number, cqww uses RST + CQ zone, all_ja
uses RST + prefecture / area code, and arrl_dx uses RST + power. The saved
session keeps the same combined schema fields; expected values reflect the
rendered exchange, such as JA1ABC 5NN 001 with the default shorthand.
Use --reveal-answer always|wrong|never to control when Expected: is shown
after each answer. always reveals after every question, wrong reveals only
after incorrect answers, and never suppresses post-result expected-answer
reveal. This is runtime-only and does not change scoring or saved session JSON.
Use --answer-timeout SECONDS to limit answer entry time. 0.0 disables the
timeout. On timeout, the answer is treated as incorrect with an empty stored
answer and score 0, then the session continues. No timeout metadata is added
to saved session JSON.
Use --head-copy-preset normal|hard for convenient head-copy practice presets:
normal: Equivalent to--head-copy --reveal-answer wrong --answer-timeout 12hard: Equivalent to--head-copy --reveal-answer never --answer-timeout 8 --noise-profile normal --adaptive-noise
Use --cw-shorthand standard|contest to control CW contest shorthand notation.
In contest mode (default), common CW contest shorthand is used:
599becomes5NN9becomesN(in RST portion only)0becomesT(in RST portion only)
Examples:
599 001becomes5NN 001599 25becomes5NN 25599 0601becomes5NN 0601599 100becomes5NN 100
Shorthand is applied only to the RST portion of exchanged information. Exchange payload fields (serial numbers, zones, etc.) remain in their canonical numeric form. Scoring normalization accepts both forms, so users can enter either the shorthand form or the standard form as their answer.
Use --operating-mode sp|run to control contest operating mode. The implemented
answer flow is still two-step in both modes: type the generated other-station
callsign at YOU CALL >, then type only the exchange at YOU EXCHANGE >.
sp(default): Search & Pounce style RX payloads.run: Running-station style RX payloads using--my-call.
In SP mode:
- CALL step (simple):
<OTHER_CALL> TEST - CALL step (realistic):
CQ TEST <OTHER_CALL> - EXCHANGE step (simple):
<EXCHANGE> - EXCHANGE step (realistic):
<MY_CALL> <EXCHANGE> TU
In RUN mode:
- CALL step (simple):
<MY_CALL> TEST - CALL step (realistic):
CQ TEST <MY_CALL> - EXCHANGE step (simple):
<OTHER_CALL> <EXCHANGE> - EXCHANGE step (realistic):
<OTHER_CALL> <MY_CALL> <EXCHANGE> TU
Use --operator-style standard|fast|verbose|aggressive to control operator pacing style.
This option only affects realistic message style; simple message style ignores it.
standard:CQ TEST <CALL>/<MY_CALL> <EXCHANGE> TUfast:TEST <CALL>/<MY_CALL> <EXCHANGE> TUverbose:CQ CQ TEST DE <CALL>/<MY_CALL> <EXCHANGE> BKaggressive:CQ <CALL>/<EXCHANGE> TU
Partial copy / fill requests are supported during contest operation. When you're unsure about part of a callsign or exchange, you can request a repeat by including a question mark (?) at the beginning or end of your input:
- CALL step:
?ABC,ABC?,?OQI,JR?,7L1?,CALL? - EXCHANGE step:
001?,5NN?,25?,0601?,100?,NR?,EXCH?
These inputs are treated as repeat requests and do not score immediately. Standard
repeat commands (AGN, ?, AGN?, RPT) replay the full message, while partial
copy requests provide more realistic responses:
- Partial copy requests during the CALL step (
?ABC,ABC?, etc.) replay only the callsign portion - Partial copy requests during the EXCHANGE step (
001?,5NN?, etc.) replay only the exchange portion
This behavior works with both standard repeat commands and respects the --max-repeats limit.
Weak-copy / uncertain answers with embedded question marks are also supported:
- CALL step:
JR?OQI,J?9OQI,JA7??? - EXCHANGE step:
5NN 0?1,5NN ??1,5NN ???
These inputs are treated as actual answers (not repeat requests) and are scored with confidence-aware partial credit. They do not trigger a replay of the payload.
Confidence-aware scoring is supported for weak-copy answers. The question mark (?)
acts as a wildcard that matches exactly one character, with partial credit based on
confidence:
- Exact match: 100 points
- Partial match with wildcards: max(50, round(100 * known_characters / total_characters)) points
- No match, or a scored weak-copy answer with no known copied characters: 0 points
Examples:
Expected: JR9OQI
Answer: JR?OQI (1 unknown character)
Score: ~83 points (5 known / 6 total characters)
Expected: 5NN 001
Answer: 5NN 0?1 (1 unknown character)
Score: ~83 points (5 known / 6 total characters)
Expected: 5NN 001
Answer: 5NN ??1 (2 unknown characters)
Score: ~67 points (4 known / 6 total characters)
Expected: JR9OQI
Answer: JR?XXX (does not match)
Score: 0 points
Expected: 5NN 001
Answer: 5NN ??? (exchange value unknown, RST copied)
Score: 50 points
The wildcard matching is exact - each ? must match exactly one character, and the
overall length must match. For example:
JR?XXXdoes not matchJR9OQIbecause characters outside?positions differJA7???does not matchJR9OQI(content mismatch after matching wildcards)
Spaces do not count toward confidence scoring.
Confidence feedback is displayed after scoring weak-copy answers:
Exact answer:
Expected: JR9OQI
Answer: JR9OQI
Output:
RESULT: OK SCORE: +100
(no confidence message)
Weak-copy partial success:
Expected: JR9OQI
Answer: JR?OQI
Output:
RESULT: OK SCORE: +83 Confidence: weak-copy (1 uncertain character) Hint: callsign nearly complete
Expected: 5NN 001
Answer: 5NN ??1
Output:
RESULT: OK SCORE: +67 Confidence: weak-copy (2 uncertain characters) Hint: exchange uncertainty
Weak-copy failure:
Expected: JR9OQI
Answer: JR?XXX
Output:
RESULT: NG SCORE: +0 Confidence: weak-copy mismatch Hint: re-request recommended
Exchange value unknown:
Expected: 5NN 001
Answer: 5NN ???
Output:
RESULT: OK SCORE: +50 Confidence: weak-copy (3 uncertain characters) Hint: exchange uncertainty
Inputs made only of question marks, such as ??? or ??????, are repeat
requests. They replay the current payload and are not scored as weak-copy
answers.
Head-copy presets fill in settings that are still at their built-in defaults.
For example, --head-copy-preset hard --answer-timeout 20 uses the hard preset
but keeps a 20-second timeout. The hard preset enables adaptive noise. Preset
settings are runtime-only and do not change scoring or saved session JSON.
Scoring uses confidence-aware partial credit for weak-copy answers. Leading/trailing whitespace is stripped, repeated whitespace is collapsed, and letters are compared case-insensitively.
- Exact answers score
100points - Weak-copy answers with wildcards score partial credit based on confidence
- Wrong answers score
0points
Answers with any score above 0 are counted as correct for statistics.
Adaptive difficulty rules:
- 3 consecutive correct answers: WPM increases by 2.
- 2 consecutive wrong answers: WPM decreases by 1.
- WPM does not go below 10.
Contest sessions are saved automatically at the end of the run.
Use --qrn 0.0..1.0 to add optional white-noise QRN to playback audio only.
QRN does not affect scoring, callsign generation, adaptive logic, or saved
session data.
Use --qsb 0.0..1.0 to add optional signal fading to playback audio only. QSB
can be combined with QRN and does not affect scoring, callsign generation,
adaptive logic, or saved session data.
Use --qrm 0.0..1.0 to add one simple CW-like interfering signal to playback
audio only. QRM can be combined with QRN and QSB and does not affect scoring,
callsign generation, adaptive logic, or saved session data.
Use --noise-profile easy|normal|hard for fixed QRN/QSB/QRM bundles:
easy QRN=0.05 QSB=0.10 QRM=0.00
normal QRN=0.10 QSB=0.20 QRM=0.10
hard QRN=0.20 QSB=0.40 QRM=0.30
Explicit --qrn, --qsb, or --qrm values override the profile values.
Use --adaptive-noise to adjust the noise level during a contest session.
Adaptive noise levels are off, easy, normal, and hard. It starts from
the selected --noise-profile, or off when no profile is selected. Three
consecutive correct answers increase noise difficulty by one level; two
consecutive wrong answers decrease it by one level. Explicit --qrn, --qsb,
or --qrm values stay fixed while the other noise values adapt. Adaptive noise
does not change the saved session JSON schema.
Contest mode supports:
uv run cwqso contest --callsign-profile jp
uv run cwqso contest --callsign-profile us
uv run cwqso contest --callsign-profile uk
uv run cwqso contest --callsign-profile dxProfile summary:
jp: Japanese-style callsigns such asJA1ABC,JH7XYZ,7K3AAA.us: US-style callsigns such asK1ABC,W6XYZ,AA1A.uk: UK-style callsigns such asG0ABC,M0ABC,2E0ABC.dx: General DX-style callsigns such asDL1ABC,F5XYZ,VK2XYZ.
The generator is intended for realistic contest practice, not full regulatory
validation. See CALLSIGN.md for the generation policy.
Sessions are saved under:
$XDG_DATA_HOME/cwqso/sessions
If XDG_DATA_HOME is not set, the fallback path is:
~/.local/share/cwqso/sessions
List saved sessions:
uv run cwqso sessionsSession IDs are shown newest first.
Review the latest saved session:
uv run cwqso reviewReview a specific session:
uv run cwqso review --session SESSION_IDOther review options:
uv run cwqso review --latest
uv run cwqso review --no-teacher
uv run cwqso review --aiOffline deterministic teacher feedback is the default. It does not require a network connection or credentials.
AI teacher feedback is opt-in with --ai. It sends a structured summary of the
saved session to an OpenAI-compatible /chat/completions API and reads
choices[0].message.content. When the configured endpoint is an external API,
the saved session content is sent to that external service. Extra response
fields are tolerated. On timeout, connection failure, HTTP error, invalid JSON,
or missing/invalid content, CW QSO Trainer prints a fallback warning and returns
offline feedback.
AI configuration can be set using environment variables or a .env file:
# Using a .env file (recommended)
cp .env.example .env
# Edit .env with your configuration
# Or using environment variables directly
export CWQSO_AI_BASE_URL="http://127.0.0.1:11434/v1"
export CWQSO_AI_MODEL="gpt-oss:20b"
export CWQSO_AI_TIMEOUT=120Configuration priority:
- Shell environment variables
- Variables from
.envfile - Built-in defaults
The .env file is loaded automatically when present. An .env.example template is provided. Note that .env should not be committed to version control.
Example commands:
uv run cwqso doctor --check-ai
uv run cwqso review --aiSupported AI environment variables:
CWQSO_AI_BASE_URL
CWQSO_AI_MODEL
CWQSO_AI_API_KEY
CWQSO_AI_TIMEOUT
These can be set either as regular environment variables or in a .env file.
Defaults:
CWQSO_AI_BASE_URL=http://127.0.0.1:11434/v1
CWQSO_AI_MODEL=qwen3:latest
CWQSO_AI_TIMEOUT=60
Example using environment variables:
export CWQSO_AI_BASE_URL="http://127.0.0.1:11434/v1"
export CWQSO_AI_MODEL="gpt-oss:20b"
export CWQSO_AI_TIMEOUT=120
uv run cwqso doctor --check-ai
uv run cwqso review --aiExample using .env file:
cp .env.example .env
# Edit .env with your configuration
uv run cwqso doctor --check-ai
uv run cwqso review --aiThe API key is sent as a bearer token. Do not commit API keys.
CWQSO_AI_TIMEOUT is optional and sets the chat completion request timeout in
seconds. It accepts integer or floating-point values and defaults to 60 seconds.
Increase it for local Ollama models or slower OpenAI-compatible APIs.
The AI client is compatible with OpenAI-compatible APIs including Ollama. For Ollama, ensure the model is running and use the /v1 endpoint:
export CWQSO_AI_BASE_URL=http://127.0.0.1:11434/v1
export CWQSO_AI_MODEL=gpt-oss:20b
# No API key needed for local Ollama
uv run cwqso doctor --check-ai
uv run cwqso review --aiExample configurations:
Local Ollama:
CWQSO_AI_BASE_URL=http://127.0.0.1:11434/v1
CWQSO_AI_MODEL=gpt-oss:20b
CWQSO_AI_TIMEOUT=120Sakura AI Engine:
CWQSO_AI_BASE_URL=https://api.ai.sakura.ad.jp/v1
CWQSO_AI_MODEL=gpt-oss-120b
CWQSO_AI_API_KEY=YOUR_API_KEY_HERE
CWQSO_AI_TIMEOUT=60Show unlocked achievements:
uv run cwqso achievementsShow all achievements:
uv run cwqso achievements --allShow one achievement with ASCII art:
uv run cwqso achievements --show PERFECT_COPYView the ASCII-art collection:
uv run cwqso collection
uv run cwqso collection --all
uv run cwqso collection --locked
uv run cwqso collection --title PERFECT_COPYImplemented achievement IDs:
FIRST_QSO
PERFECT_COPY
TEN_SESSIONS
ADAPTIVE_SURVIVOR
AI_TRAINEE
Achievement IDs and ASCII art are not translated. Titles and descriptions are localized for English and Japanese output.
Show environment diagnostics:
uv run cwqso doctorThe doctor command reports Python/package/license information, session storage path, AI base URL, AI model, whether an API key is set, and AI connectivity status.
By default, connectivity is not checked. To check the configured AI backend:
uv run cwqso doctor --check-aiThis sends a GET request to:
<CWQSO_AI_BASE_URL>/models
It also performs a lightweight inference test with a simple /chat/completions
prompt. The output shows both connectivity and inference status:
Connectivity: ok
Inference: ok
Connectivity can report ok or failed. Inference can report ok, failed,
timeout, or an invalid-response status depending on the backend response.
The actual API key value is never printed.
Supported languages:
en
ja
Language selection priority:
- CLI option
--lang - Environment variable
CWQSO_LANG - Fallback
en
Examples:
uv run cwqso --lang ja doctor
CWQSO_LANG=ja uv run cwqso review
uv run cwqso --lang ja contest --questions 1Localized command output is implemented for:
doctorreviewcontestachievementscollection
Callsigns, RST, serial numbers, session IDs, achievement IDs, ASCII art, and raw AI output are not translated.
Run tests:
uv run pytest -qUseful project documents:
AGENTS.md: repository guidance for coding agents and contributors.SPEC.md: current implementation specification.CALLSIGN.md: callsign generation policy.
Contributor constraints:
- Do not make AI mandatory.
- Do not let AI decide official scores.
- Do not hard-code API keys.
- Keep normal training usable without network access.
- Add or update tests when changing scoring, adaptive logic, storage, AI review, achievements, i18n, or callsign generation.
CW QSO Trainer is licensed under:
GPL-3.0-or-later
See LICENSE for the full license text.