Generate a concise debrief for a single race and persist it on the
session in the helmlog UI as a moment, so the crew can see it next time
they open the session detail page.
-
Resolve the race. If no ID was given, find the latest race:
SELECT id, name, date, start_utc, end_utc, vakaros_session_id
FROM races
WHERE session_type='race' AND end_utc IS NOT NULL AND start_utc != end_utc
ORDER BY start_utc DESC LIMIT 1;
Confirm with the user before proceeding (Debriefing race {id}: {name} on {date}. Continue?).
-
Run the analyses for this race only. Use scripts/analysis/
helpers but constrain to the single race window
(start_utc → end_utc, or post-gun if the Vakaros gun event exists).
Compute:
- Finish: Corvo's place + points from
race_results joined via
local_session_id. If no place is recorded yet, say so.
- Conditions: mean TWS, TWS range, mean TWA upwind / downwind.
- Heel: mean abs heel (from
attitudes) — note if not present.
- VMG percentages: % of seconds at ≥90% best-VMG (vs the
season's polar baseline).
- Shifts: good/bad/neutral tack count from the maneuvers table
using the 90–150s pre/post window approach in
scripts/analysis/full_analysis.py.
- Start: dist@gun, SOG@gun, TTL, favored end, what end Corvo
went to, tack at gun. Use the helpers in
scripts/analysis/start_quality.py.
- OCS: if the start analysis flags this race as OCS-returned,
call it out and apply the
ocs tag (see /ocs-check).
- Crew weight: lookup default crew (1,070 lb on Corvo as of May
2026) — note if a per-race override exists in
crew_defaults.
-
Pull and analyze the audio transcripts. Each race has audio in
one or more audio_sessions rows linked by race_id:
session_type='race' — the on-water mic capture (helm + crew chatter)
session_type='debrief' — post-race recorded debrief (only present
for some races)
Both can have multiple sibling rows when more than one mic was
running (different capture_group_id per session_type, ordered by
capture_ordinal). For each audio_session, check transcripts.status:
SELECT a.id, a.session_type, a.capture_ordinal, t.id AS transcript_id,
t.status, length(t.text) AS text_len
FROM audio_sessions a
LEFT JOIN transcripts t ON t.audio_session_id = a.id
WHERE a.race_id = {race_id}
ORDER BY a.session_type, a.capture_ordinal;
Status handling:
What to look for in the transcript:
- Tactical calls vs reactions. Sailing language is brief —
"pressure right!", "two boat lengths", "tack", "hold". A good
race has crisp anticipatory calls; a bad race has lots of
reactive "watch out!" or "we got rolled" comments.
- Specific moments worth flagging back into the analysis:
- Mark roundings (counted)
- Tacks/jibes (count vs the maneuvers table — should match)
- OCS recall calls ("come back!", "we're over!") — cross-reference
with the OCS detection
- Pressure/wind callouts ("big puff coming", "lift on the right")
- Boat-on-boat ("Moose to leeward", "got rolled by LiftOff")
- Sail trim ("more vang", "kite collapse", "we're stalled")
- Tone trajectory. Compare debrief audio (post-race calm
reflection) to race audio (in-the-moment reactions). The debrief
usually surfaces what went wrong in the team's own words —
prioritize quoting the crew's own language over your own
speculation.
- Segment-level speaker tracking.
segments_json has
speaker labels (or anonymized IDs); per-position quotes are more
useful than aggregate. If position_name is set on
transcript_segments, group by position (helm/main/pit/bow/tac).
Add a TRANSCRIPT section to the debrief synthesized in step 4
(not as a transcription dump — as a curated 3–6 bullet list of
notable callouts with timestamps, drawn from the actual transcript
text). Always cite a specific quote and timestamp.
-
Synthesize the debrief. A short markdown summary, ~250–400
words, with this structure:
## Result
{place + points + boat conditions in one sentence}
## Start
- Distance from line at gun: {x} m
- SOG at gun: {y} kt
- Time to clear line: {z} s {(OCS — returned)}
- Favored end: {boat/pin/square}, bias {n°}
- We went to: {boat/mid/pin} — {got favored ✓ / wrong end ✗}
- Tack at gun: {stbd/port}
## Speed (post-gun)
- Upwind VMG: {p}% of best-ever in similar conditions
- Downwind VMG: {q}% of best-ever
- Mean heel: {h}° (or "not recorded")
## Shifts
- Tacked on header: {g}, on lift: {b}, neutral: {n}
## From the transcript
{3-6 bullets of notable callouts with timestamps and speakers,
drawn from the actual transcript text. Always quote — don't
paraphrase. Example:
- 02:14:55 (helm): "we're getting rolled by Moose"
- 02:18:30 (tac): "pressure on the right, hold this tack"
If the transcript is in progress: "Transcript still processing
({status}); re-run /debrief in a few minutes for transcript notes."
If no audio captured for this race: "No race audio recorded."}
## What we did well / what to look at
{1-2 bullets each, drawn from BOTH the data AND the transcript —
be specific, cite numbers, tie to the season patterns where
relevant (e.g., "we sailed 135° TWA in 7 kt — the season optimum
is 145°, the same gap we've seen all spring") and to the crew's
own words from the transcript ("the helm called out 'we're slow'
at 02:30 — the data shows we were 0.5 kt below polar at that
moment")}
-
Attach to the session. Insert the debrief as a moment on the
session via direct SQL on the live DB (the API requires a session
cookie that's awkward to wire up from the Mac).
BEGIN;
INSERT INTO moments (
session_id, subject, anchor_kind, anchor_t_start,
resolved, source, created_by, created_at, updated_at
)
VALUES (
{race_id},
'Post-race debrief — {short summary, e.g. "5th, OCS recovery, 65% UP"}',
'timestamp',
'{gun_iso}',
0, 'auto', 1,
'{now_iso}', '{now_iso}'
);
INSERT INTO comments (moment_id, author, body, created_at)
VALUES (last_insert_rowid(), 1, '{markdown body}', '{now_iso}');
COMMIT;
The full markdown debrief goes in the comment body (rich text
supported); the moment subject stays a one-line summary so the
moment list is scannable.
-
Report the URL. Tell the user the moment was created and give
them the deep link:
https://corvo105.helmlog.org/session/{race_id}/{slug}?moment={moment_id}
(or just /session/{id}/{slug} if the ?moment= deep-link param
doesn't exist on the session detail page).