Skip to content

ENDOH-R/CW-QSO-trainer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CW QSO Trainer

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.

Features

  • 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.

Installation

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 --help

For 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.

Quick Start

uv run cwqso contest
uv run cwqso contest --questions 10
uv run cwqso review
uv run cwqso achievements
uv run cwqso doctor

See all commands:

uv run cwqso --help

Implemented commands:

contest
review
sessions
doctor
achievements
collection

Contest Mode

Start a training session:

uv run cwqso contest

Useful 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 hard

Current 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: 600 Hz
  • Volume: 0.2
  • Questions: 5
  • Adaptive difficulty: enabled
  • Show answer: disabled
  • Callsign profile: jp by default; cqww and arrl_dx use us when --callsign-profile is 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 12
  • hard: 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:

  • 599 becomes 5NN
  • 9 becomes N (in RST portion only)
  • 0 becomes T (in RST portion only)

Examples:

  • 599 001 becomes 5NN 001
  • 599 25 becomes 5NN 25
  • 599 0601 becomes 5NN 0601
  • 599 100 becomes 5NN 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> TU
  • fast: TEST <CALL> / <MY_CALL> <EXCHANGE> TU
  • verbose: CQ CQ TEST DE <CALL> / <MY_CALL> <EXCHANGE> BK
  • aggressive: 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?XXX does not match JR9OQI because characters outside ? positions differ
  • JA7??? does not match JR9OQI (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 100 points
  • Weak-copy answers with wildcards score partial credit based on confidence
  • Wrong answers score 0 points

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.

Callsign Profiles

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 dx

Profile summary:

  • jp: Japanese-style callsigns such as JA1ABC, JH7XYZ, 7K3AAA.
  • us: US-style callsigns such as K1ABC, W6XYZ, AA1A.
  • uk: UK-style callsigns such as G0ABC, M0ABC, 2E0ABC.
  • dx: General DX-style callsigns such as DL1ABC, F5XYZ, VK2XYZ.

The generator is intended for realistic contest practice, not full regulatory validation. See CALLSIGN.md for the generation policy.

Sessions

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 sessions

Session IDs are shown newest first.

Review and AI Teacher

Review the latest saved session:

uv run cwqso review

Review a specific session:

uv run cwqso review --session SESSION_ID

Other review options:

uv run cwqso review --latest
uv run cwqso review --no-teacher
uv run cwqso review --ai

Offline 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=120

Configuration priority:

  1. Shell environment variables
  2. Variables from .env file
  3. 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 --ai

Supported 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 --ai

Example using .env file:

cp .env.example .env
# Edit .env with your configuration
uv run cwqso doctor --check-ai
uv run cwqso review --ai

The 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 --ai

Example configurations:

Local Ollama:

CWQSO_AI_BASE_URL=http://127.0.0.1:11434/v1
CWQSO_AI_MODEL=gpt-oss:20b
CWQSO_AI_TIMEOUT=120

Sakura 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=60

Achievements and Collection

Show unlocked achievements:

uv run cwqso achievements

Show all achievements:

uv run cwqso achievements --all

Show one achievement with ASCII art:

uv run cwqso achievements --show PERFECT_COPY

View the ASCII-art collection:

uv run cwqso collection
uv run cwqso collection --all
uv run cwqso collection --locked
uv run cwqso collection --title PERFECT_COPY

Implemented 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.

Doctor

Show environment diagnostics:

uv run cwqso doctor

The 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-ai

This 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.

Internationalization

Supported languages:

en
ja

Language selection priority:

  1. CLI option --lang
  2. Environment variable CWQSO_LANG
  3. Fallback en

Examples:

uv run cwqso --lang ja doctor
CWQSO_LANG=ja uv run cwqso review
uv run cwqso --lang ja contest --questions 1

Localized command output is implemented for:

  • doctor
  • review
  • contest
  • achievements
  • collection

Callsigns, RST, serial numbers, session IDs, achievement IDs, ASCII art, and raw AI output are not translated.

Development

Run tests:

uv run pytest -q

Useful 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.

License

CW QSO Trainer is licensed under:

GPL-3.0-or-later

See LICENSE for the full license text.

About

AI-assisted CLI/TUI trainer for CW QSO and contest practice with head-copy, realistic contest flow, and AI review.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages