Skip to content

feat(workflows): make the whisper-to-notes template real - #2502

Open
Hoang130203 wants to merge 1 commit into
Osmantic:mainfrom
Hoang130203:feat/n8n-whisper-notes-workflow
Open

feat(workflows): make the whisper-to-notes template real#2502
Hoang130203 wants to merge 1 commit into
Osmantic:mainfrom
Hoang130203:feat/n8n-whisper-notes-workflow

Conversation

@Hoang130203

Copy link
Copy Markdown

Summary

config/n8n/whisper-to-notes.json was a placeholder — manualTrigger + sticky
note + "connections": {} — behind a catalog card advertising "Transcribe and
summarize voice notes"
.

Webhook POST /webhook/ods-notes  (multipart recording)
  -> Recording Attached? (IF)   false -> Return 400
  -> whisper:8000      /v1/audio/transcriptions
  -> llama-server:8080 /v1/chat/completions   (structure the transcript)
  -> Parse Notes (Code)                        (defensive JSON parse)
  -> Return Notes
curl -X POST http://localhost:5678/webhook/ods-notes \
  -F 'file=@standup.m4a' -F 'title=Monday standup'

Returns summary, decisions, action_items ({owner, task}),
open_questionsplus the full transcript, so nothing is lost to
summarisation.

The part worth reviewing: JSON from a model is not JSON

The system prompt pins an exact schema and forbids code fences, but local
models at Tier 0–2 do not always comply. The Code node therefore parses
defensively rather than trusting it:

const stripped = raw.replace(/^\s*```(?:json)?/i, '').replace(/```\s*$/, '').trim();
const start = stripped.indexOf('{');
const end = stripped.lastIndexOf('}');
...
try { notes = JSON.parse(stripped.slice(start, end + 1)); } catch (e) { notes = null; }

and on failure returns parsed: false with raw_reply alongside the
transcript. A model that answers in prose costs you the structure, not the
transcription
— which is the expensive half and the part that cannot be
redone without the audio.

Other choices:

  • The prompt says use only what the transcript states, and write
    "unassigned" rather than invent an owner for an action item.
  • The transcript is clamped to 24000 characters before the summarisation call,
    so a long recording cannot overrun the context window and lose the whole run.
  • Whisper gets a 600s timeout (recordings are long); the summarisation call
    gets 300s.
  • Both hops use in-network addresses (whisper:8000, llama-server:8080), so
    neither the audio nor the transcript leaves ods-network.

AI Assistance

AI assisted with drafting the node graph, the schema prompt, and this
description. I verified both endpoints against their manifests, checked the
Code node body parses as JavaScript, and validated the file against the real
node package before pushing.

Release Lane

  • Stable hotfix targeting release/2.6.x
  • Mainline change targeting main
  • Next-minor work targeting the next feature/minor release
  • Not sure; reviewer should help classify

Stable hotfix reason:

n/a

Changed Surface

  • Docs only
  • Tests only
  • Dashboard UI
  • Dashboard API / host agent
  • Installer / bootstrap / lifecycle
  • Docker Compose / service manifests
  • Model routing / Hermes / capabilities
  • Network exposure / auth / proxy
  • Dependencies / runtime wiring

(One JSON file under config/n8n/. An import payload for n8n; no ODS code
executes it. The catalog entry is unchanged.)

Risk And Validation

  • Risk level: Low
  • Validation run:
    • git diff --check
    • Markdown/link sanity for docs
    • Focused tests listed below
    • Dashboard lint/test/build
    • Extension audit / compose validation
    • Release-grade fleet or scoped hardware validation
    • Stable-lane patch validation, if targeting release/2.6.x

Commands/results:

# Validated against the exact node package ODS ships (n8n 2.6.4 -> 2.6.2).

$ node verify.js whisper-to-notes.json
n8n-nodes-base version: 2.6.2
node types loaded: 417
  checked whisper-to-notes.json: 8 nodes

ALL WORKFLOWS VALID

# The Code node body is real JavaScript, not a string that only looks like it:
$ node -e "new Function(require('./whisper-to-notes.json').nodes
             .find(n => n.type === 'n8n-nodes-base.code').parameters.jsCode)"
Code node body parses as JS

Caveat: Docker is not running on my dev host, so I could not post a real
recording through the two-hop chain. Static validation proves the file imports
and every parameter and version is real. The fragile part — the model's JSON
compliance — is precisely what the defensive parser handles, and that parser is
plain JavaScript I checked separately. Happy to get a live run before merge.

Operational Change Check

An import payload for n8n. Nothing in the installer, compose stack, ods-cli,
or dashboard-api executes it. No existing install changes until a user imports
it.

  • This is not an operational change.
  • This is an operational change and validation is recorded above.
  • This is an operational change and validation is intentionally deferred for:

Notes For Reviewers

parsed: false is a returned field, not an error. I chose to return 200
with the transcript and the raw reply rather than 500, on the grounds that the
caller wants their transcript either way. If you would rather it be a 422 so
callers can retry the structuring step, that is a one-node change.

STT model default is Systran/faster-whisper-base, matching
scripts/validate-models.py. On an NVIDIA install the tier map pins
deepdml/faster-whisper-large-v3-turbo-ct2; the caller can override with
-F 'stt_model=...'. Wiring the configured value in would need
AUDIO_STT_MODEL in n8n's environment, which compose.yaml does not pass —
same note as #2498.

Part of the series making the 18 stub templates real: #2496, #2497, #2498,
#2499, #2500, #2501.

config/n8n/whisper-to-notes.json was a manualTrigger plus a sticky note
saying "Customize the nodes below to match your setup", with
"connections": {} — an empty canvas behind a catalog card advertising
"Transcribe and summarize voice notes".

Now: POST a recording -> whisper transcribes -> llama-server turns the
transcript into structured notes -> JSON with summary, decisions,
action_items and open_questions, plus the full transcript so nothing is
lost to summarisation.

    curl -X POST http://localhost:5678/webhook/ods-notes \
      -F 'file=@standup.m4a' -F 'title=Monday standup'

The model is asked for JSON, and a Code node parses it defensively: it
strips a ``` fence if one is there, slices from the first { to the last
}, and falls back to returning the raw reply with parsed=false. A model
that answers in prose costs you the structure, not the transcription —
which is the expensive half.

The system prompt pins the exact schema, tells the model to use only
what the transcript says, and to write "unassigned" rather than invent an
owner for an action item.

Transcript is clamped to 24000 characters before the summarisation call
so a long recording cannot overrun the context window.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant