parakeet

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

README

go-parakeet

github.com/androiddrew/go-parakeet (package parakeet) transcribes speech with NVIDIA Parakeet TDT 0.6B v3 through ONNX Runtime. It accepts 16 kHz mono float32 PCM and produces text and token emission timestamps in 25 languages, with punctuation and capitalization. Audio decoding, resampling and any CLI or serving layer belong to the application.

You need an installed ONNX Runtime and three model downloads: the preprocessor plus an FP32 (2.5 GB) or INT8 (670 MB) encoder and decoder; see ASSETS.md for links and checksums. The vocabulary is built in.

import (
    "context"
    "errors"
    parakeet "github.com/androiddrew/go-parakeet"
)

func transcribe(ctx context.Context, samples []float32) (result parakeet.Result, err error) {
    engine, err := parakeet.New(parakeet.Config{
        ORTLibrary:       "libonnxruntime.so", // or an explicit installed-library path
        PreprocessorPath: "/assets/parakeet/nemo128.onnx",
        EncoderPath:      "/assets/parakeet/encoder-model.onnx",
        DecoderPath:      "/assets/parakeet/decoder_joint-model.onnx",
        Provider:         parakeet.CPU, // or parakeet.CUDA
        Threads:          1,
    })
    if err != nil { return result, err }
    defer func() { err = errors.Join(err, engine.Close()) }()
    return engine.Transcribe(ctx, parakeet.Request{
        Samples: samples, SampleRate: parakeet.SampleRate,
    })
}

Reuse an initialized engine across requests. For INT8, use encoder-model.int8.onnx and decoder_joint-model.int8.onnx. FP32's encoder-model.onnx.data must be beside its graph.

API and guarantees

  • Transcribe returns the complete text, tokens and finalized chunks. It returns no partial transcript on error.
  • TranscribeStream is an alternative to Transcribe: it delivers finalized chunks to a callback without accumulating a whole-recording result.
  • Result.Tokens retains raw SentencePiece IDs/pieces, recording-relative emission frames/timestamps, and predicted frame advances. These are model emission positions, not exact word boundaries, and can change with chunk settings.
  • The embedded Summary reports duration, decoded windows, delivered chunks, finalized source coverage, effective window sizes and stage Timings.
  • ModelInfo returns inspected tensor metadata for all three graphs. Initialization reports loading times and the loaded native runtime version.
  • Results own their Go buffers. Input samples must not be mutated during a call.
  • Input must be at least 20 ms of finite samples at SampleRate; otherwise ErrInvalidAudio. Samples are not rescaled, clipped, mixed or resampled.
  • VocabPath overrides the embedded vocabulary only for a custom export.

Long audio

ChunkOptions{} automatically uses 30-second windows with 4-second overlap. Audio fitting one window runs as a single pass. Customize the latency/context tradeoff per request:

result, err := engine.Transcribe(ctx, parakeet.Request{
    Samples: samples, SampleRate: parakeet.SampleRate,
    Chunking: parakeet.ChunkOptions{
        Window:  30 * time.Second,
        Overlap: 4 * time.Second,
    },
})
  • Window: 2–300 seconds; zero selects DefaultWindow (30 s).
  • Overlap: positive, at most half the window; zero selects min(DefaultOverlap, Window/4).
  • Durations round down to the 80 ms encoder grid. A positive overlap must round to at least one frame. Effective sizes are reported as sample counts in Summary.
  • SinglePass: true disables windowing and rejects input exceeding MaxWindowSamples (300 seconds) with ErrAudioTooLong. Window/Overlap must then be zero. The cap is not a memory guarantee on every GPU.
  • Invalid options return ErrInvalidChunking.

Each window is preprocessed independently and starts with fresh decoder state. Seam reconciliation aligns whole SentencePiece words using timestamps and a bounded word-alignment search. It prefers quiet regions for boundaries and falls back to timestamp ownership when alignment is ambiguous; ChunkResult.Seam reports SeamAligned or SeamTimestamp (empty for the first chunk). The unresolved overlap and a 640 ms timestamp-drift margin are retained until the next window is decoded. Small backwards timestamp differences at a splice are clamped, retaining words and nondecreasing times.

A pathological unbroken hypothesis that cannot be finalized within two windows' maximum token budget returns ErrUnstableSeam. This keeps memory bounded and avoids silently cutting an unresolved word; already delivered chunks remain valid. Increasing window context or splitting the input at pauses can help.

Windowing bounds intermediate inference memory. The caller still holds the complete PCM input, and Transcribe retains the complete output. Smaller windows change acoustic context and per-window normalization and can change recognition. Keep the default windows for INT8 unless shorter windows are validated on your inputs: with 8-second windows the quantized model omitted speech in testing.

Streaming text

Use TranscribeStream to consume stable text before the complete recording finishes. Each callback receives one finalized chunk; text is never revised.

// output is an application-supplied io.Writer.
summary, err := engine.TranscribeStream(ctx, parakeet.Request{
    Samples: samples, SampleRate: parakeet.SampleRate,
}, func(chunk parakeet.ChunkResult) error {
    // Text may begin with a space; concatenate verbatim.
    _, err := io.WriteString(output, chunk.Text)
    return err
})
// Handle err even if some chunks were already delivered. summary excludes callback time.
  • Concatenating chunk.Text verbatim equals a successful Transcribe result with the same settings.
  • Each chunk owns its token slice, which remains valid after further inference, errors and engine closure.
  • StartSample/EndSample describe contiguous finalized source spans. They are progress boundaries, not word alignment. Empty-text chunks can report progress through silence.
  • Streaming waits for a complete encoder window before delivering that window's stable prefix. Audio is supplied up front; this is not a live microphone API.

The callback is synchronous and applies backpressure. Queue text for a separate consumer to overlap it with the next window. The engine creates no background goroutines and retains no aggregate output in streaming mode.

Errors and lifetime: a callback error stops further inference; it is wrapped with the chunk index and supports errors.Is/errors.As. Previously delivered data remains valid on any error. Summary includes completed work and timings on error; DeliveredChunks counts callbacks that returned nil and FinalizedSeconds ends at the last such chunk. Context cancellation is checked between native runs and around callbacks, including after the final callback. Native inference cannot be interrupted mid-run. A blocking callback must observe the context itself.

Requests and Close remain serialized for the entire stream, including callbacks. Do not call back into the same engine from a callback; that can deadlock. Close the engine after streaming returns. See ExampleEngine_TranscribeStream for a complete producer/consumer example.

ONNX Runtime ownership

ONNX Runtime has one environment per process, shared by every model in it.

  • Set ORTLibrary (simplest): the engine initializes the environment through github.com/androiddrew/ortenv on first use. The environment stays loaded until process exit, so closing the last engine never unloads the runtime. Every component using ortenv in the process must use the same library selector, and nothing may destroy the environment ortenv owns.
  • Leave ORTLibrary empty when your application already manages ONNX Runtime itself (directly through onnxruntime_go or through ortenv). Initialize the environment before New and destroy it only after Close; otherwise New returns ErrNotInitialized:
ort.SetSharedLibraryPath("libonnxruntime.so")
if err := ort.InitializeEnvironment(); err != nil { return err }
defer ort.DestroyEnvironment() // runs after engine.Close

engine, err := parakeet.New(parakeet.Config{
    PreprocessorPath: preprocessorPath, EncoderPath: encoderPath, DecoderPath: decoderPath,
})
if err != nil { return err }
defer engine.Close()

This makes go-parakeet usable in the same process as go-kokoro and go-g2p.

Providers and lifetime

Requires Go 1.25 or newer with CGO. Tested on Linux/amd64, Go 1.25.0 and 1.26.4, with ORT 1.22.0 (official release) and 1.22.2 (source build) on CPU, and ORT 1.22.2 with CUDA 12.0/cuDNN 9.8 for GPU tests. These describe the known tested configuration. Native runtime installation is user-managed and must provide the resolved binding's C API; there is no exact version-string restriction. CUDA/cuDNN requirements follow the selected native runtime/provider build.

Preprocessing always runs on CPU. The encoder and decoder use the requested provider; CUDA setup errors propagate, with no CPU fallback. Provider: parakeet.CUDA uses an explicit device ID, cudnn_conv_algo_search=DEFAULT, use_tf32=0 and exact-request arena growth. Some encoder operators remain on CPU under CUDA; Verbose exposes node placement. CUDA is tested with FP32 assets. Each session uses one intra/inter-op thread by default, sequential graph execution and full optimization. Set Threads for intra-op parallelism.

Close every engine. Requests and Close on an engine are serialized; use separate engines for parallel work. Metadata inspection currently creates a temporary session before each inference session, so loading timings include two graph loads per model. Requests reuse their sessions and decoder tensors.

On a small FLEURS sample, Greek had materially higher word error than English, French, German, Spanish and Russian, especially with INT8. Validate accuracy on your own audio.

Development

go test ./...
go vet ./...
go build ./cmd/inference-probe

Set PARAKEET_TEST_ASSETS to the directory holding the model files, and PARAKEET_TEST_ORT to an ONNX Runtime library, then run go test -race ./.... Unit tests need neither. PARAKEET_TEST_PRECISION=int8 selects the INT8 assets and baselines (default fp32). Once native testing is enabled, incomplete configuration fails instead of skipping. Tests that load ONNX Runtime each run in a fresh subprocess because ortenv keeps the environment loaded until process exit. The fixtures in testdata/ hold the exact audio, tensors and token decisions expected from a Python reference; see testdata/README.md. CI runs everything against the official ONNX Runtime 1.22.0 release with FP32 assets. INT8 parity baselines were recorded on an AVX2 CPU; quantized kernels differ by CPU feature set, so INT8 tensor parity can fail on other hardware (for example AVX-512 VNNI) even when transcription is unaffected.

PARAKEET_TEST_LONG=1 additionally transcribes a recording over 300 seconds. CUDA tests need both the cuda build tag and PARAKEET_TEST_CUDA_ORT (FP32):

PARAKEET_TEST_CUDA_ORT=/path/to/cuda/libonnxruntime.so \
PARAKEET_TEST_ASSETS=/assets/parakeet \
    go test -race -tags=cuda -count=1 -run '^TestCUDA' .

cmd/inference-probe reads little-endian 16 kHz mono float32 PCM and prints JSON metadata, timings, text and tokens. --window, --overlap and --single-pass control windowing; --stream writes type: "chunk" JSON lines followed by a final type: "done" summary. It is an optional diagnostic tool.

See ASSETS.md, THIRD_PARTY.md, LICENSE and NOTICE.

Documentation

Overview

Package parakeet transcribes 16 kHz mono PCM using Parakeet TDT v3 and ONNX Runtime.

Example

Transcribe a recording. The model files are downloads (see ASSETS.md); the vocabulary is built in. Long recordings are windowed automatically.

package main

import (
	"context"
	"fmt"
	"log"

	parakeet "github.com/androiddrew/go-parakeet"
)

// loadSamples stands in for the application's audio decoding. The engine
// requires 16 kHz mono float32 PCM and performs no resampling.
func loadSamples() []float32 { return make([]float32, parakeet.SampleRate) }

func main() {
	engine, err := parakeet.New(parakeet.Config{
		ORTLibrary:       "libonnxruntime.so", // OS loader name or a path to the library
		PreprocessorPath: "assets/parakeet/nemo128.onnx",
		EncoderPath:      "assets/parakeet/encoder-model.onnx",
		DecoderPath:      "assets/parakeet/decoder_joint-model.onnx",
	})
	if err != nil {
		log.Fatal(err)
	}
	defer engine.Close()

	result, err := engine.Transcribe(context.Background(), parakeet.Request{
		Samples: loadSamples(), SampleRate: parakeet.SampleRate,
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(result.Text)
}
Example (CallerOwnedEnvironment)

An application that already initializes ONNX Runtime itself leaves ORTLibrary empty. The engine then uses that environment and never destroys it.

package main

import (
	"fmt"
	"log"

	parakeet "github.com/androiddrew/go-parakeet"
	ort "github.com/yalue/onnxruntime_go"
)

func main() {
	ort.SetSharedLibraryPath("libonnxruntime.so")
	if err := ort.InitializeEnvironment(); err != nil {
		log.Fatal(err)
	}
	defer ort.DestroyEnvironment() // runs after engine.Close

	engine, err := parakeet.New(parakeet.Config{
		PreprocessorPath: "assets/parakeet/nemo128.onnx",
		EncoderPath:      "assets/parakeet/encoder-model.onnx",
		DecoderPath:      "assets/parakeet/decoder_joint-model.onnx",
		Provider:         parakeet.CUDA, // requires an ONNX Runtime build with CUDA
	})
	if err != nil {
		log.Fatal(err)
	}
	defer engine.Close()
	fmt.Println(engine.Initialization().RuntimeVersion)
}

Index

Examples

Constants

View Source
const (
	DefaultWindow  = 30 * time.Second
	DefaultOverlap = 4 * time.Second
)

Default long-audio windowing, used when ChunkOptions durations are zero.

View Source
const MaxWindowSamples = 300 * SampleRate

MaxWindowSamples bounds each native pass, not the complete recording. It is not a memory guarantee on every GPU or an exact exported-model limit.

View Source
const SampleRate = 16000

SampleRate is the required input rate in Hz. Input is mono.

Variables

View Source
var (
	// ErrNotInitialized is returned by New when ORTLibrary is empty and the
	// caller has not initialized the ONNX Runtime environment.
	ErrNotInitialized = ortruntime.ErrNotInitialized
	// ErrClosed is returned by requests on a closed engine.
	ErrClosed = errors.New("parakeet engine is closed")
	// ErrInvalidAudio reports a wrong sample rate, too few samples or a
	// nonfinite sample.
	ErrInvalidAudio = errors.New("invalid audio")
	// ErrAudioTooLong is returned when SinglePass input exceeds MaxWindowSamples.
	ErrAudioTooLong = errors.New("audio exceeds the single-pass limit")
	// ErrInvalidChunking reports ChunkOptions outside their documented bounds.
	ErrInvalidChunking = errors.New("invalid chunking options")
	// ErrUnstableSeam is returned when an unbroken hypothesis cannot be
	// finalized within two windows' token budget. Delivered chunks remain valid.
	ErrUnstableSeam = errors.New("overlap cannot be finalized within bounded context")
)

Functions

This section is empty.

Types

type ChunkOptions

type ChunkOptions struct {
	Window     time.Duration `json:"window"`
	Overlap    time.Duration `json:"overlap"`
	SinglePass bool          `json:"single_pass"`
}

ChunkOptions controls complete-audio windowing. Zero values enable automatic chunking with 30-second windows and up to 4 seconds of overlap. Window is 2..300 seconds; Overlap is at most half the window. Durations round down to the 80 ms encoder grid. Zero Overlap selects min(4 seconds, Window/4). A positive overlap must round to at least one frame. SinglePass disables windowing and rejects audio beyond MaxWindowSamples; its durations must be zero.

type ChunkResult

type ChunkResult struct {
	Index       int     `json:"index"`
	StartSample int     `json:"start_sample"`
	EndSample   int     `json:"end_sample"`
	Text        string  `json:"text"`
	Tokens      []Token `json:"tokens"`
	Seam        Seam    `json:"seam,omitempty"`
}

ChunkResult is finalized text and owned tokens. Text may start with a space; concatenate it verbatim. The half-open sample span records finalized source coverage, not precise word alignment or the encoder's overlapping window. Seam is empty for the first window.

type Config

type Config struct {
	// ORTLibrary is an ONNX Runtime library path or OS-loader name. When set, the
	// engine initializes the environment through ortenv, which retains it until
	// process exit. When empty, the caller owns the environment and must
	// initialize it before New and destroy it only after Close.
	ORTLibrary       string
	PreprocessorPath string // nemo128.onnx
	EncoderPath      string // encoder-model.onnx (with .onnx.data beside it) or encoder-model.int8.onnx
	DecoderPath      string // decoder_joint-model.onnx or decoder_joint-model.int8.onnx
	// VocabPath overrides the embedded Parakeet TDT v3 vocabulary. Usually empty.
	VocabPath string
	Provider  Provider // empty selects CPU
	DeviceID  int      // CUDA device
	Threads   int      // intra-op threads per session; zero selects 1
	Verbose   bool     // write ORT node-placement diagnostics to stderr
}

Config locates the model assets and configures the sessions.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine owns three model sessions. Requests and Close are serialized. Construct with New; the zero value is not an initialized engine.

func New

func New(c Config) (_ *Engine, err error)

New validates assets and tensor metadata and eagerly creates the sessions. Close is mandatory. Concurrent calls on one engine are serialized, including Close.

func (*Engine) Close

func (e *Engine) Close() error

Close releases all owned native objects. It is idempotent. The ONNX Runtime environment stays initialized for other engines and later calls to New.

func (*Engine) Initialization

func (e *Engine) Initialization() Initialization

Initialization returns how long New spent in each loading stage.

func (*Engine) ModelInfo

func (e *Engine) ModelInfo() ModelInfo

ModelInfo returns an owned metadata copy. Negative dimensions are dynamic.

func (*Engine) Transcribe

func (e *Engine) Transcribe(ctx context.Context, request Request) (Result, error)

Transcribe collects finalized chunks into an owned aggregate result. Long recordings are windowed by default. On error it returns no partial transcript.

func (*Engine) TranscribeStream

func (e *Engine) TranscribeStream(ctx context.Context, request Request, yield func(ChunkResult) error) (Summary, error)

TranscribeStream delivers finalized, caller-owned chunks to yield, in order. Text is final and concatenates verbatim to Transcribe's result. Empty text chunks can report progress through silence. The library retains no aggregate output.

A callback error stops inference; errors.Is/errors.As preserve its identity. Previously delivered chunks survive all errors and engine closure. Summary counts callbacks that returned nil, even if cancellation follows the last one. Callbacks must observe ctx themselves when blocking, and must not call this engine's inference methods or Close. Requests and Close stay serialized during callbacks. Native Run cannot be interrupted mid-call. The complete input audio is supplied up front and must remain unmodified until this method returns.

Example

Stream finalized text into a bounded queue so a consumer can forward it while the next window is decoded.

package main

import (
	"context"
	"fmt"
	"log"

	parakeet "github.com/androiddrew/go-parakeet"
)

// loadSamples stands in for the application's audio decoding. The engine
// requires 16 kHz mono float32 PCM and performs no resampling.
func loadSamples() []float32 { return make([]float32, parakeet.SampleRate) }

func main() {
	engine, err := parakeet.New(parakeet.Config{
		ORTLibrary:       "libonnxruntime.so",
		PreprocessorPath: "assets/parakeet/nemo128.onnx",
		EncoderPath:      "assets/parakeet/encoder-model.int8.onnx",
		DecoderPath:      "assets/parakeet/decoder_joint-model.int8.onnx",
	})
	if err != nil {
		log.Fatal(err)
	}
	defer engine.Close()
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	text := make(chan string, 2)
	done := make(chan error, 1)
	go func() {
		_, err := engine.TranscribeStream(ctx, parakeet.Request{
			Samples: loadSamples(), SampleRate: parakeet.SampleRate,
		}, func(chunk parakeet.ChunkResult) error {
			select {
			case text <- chunk.Text:
				return nil
			case <-ctx.Done():
				return ctx.Err()
			}
		})
		close(text)
		done <- err
	}()
	for t := range text {
		// Forward to the application's display or transport. Text may begin
		// with a space; concatenate chunks verbatim. On a consumer failure,
		// cancel() and wait for done before closing the engine.
		fmt.Print(t)
	}
	if err := <-done; err != nil {
		log.Print(err) // earlier chunks may already have been delivered
	}
}

type GraphInfo

type GraphInfo struct {
	Inputs  []TensorInfo `json:"inputs"`
	Outputs []TensorInfo `json:"outputs"`
}

GraphInfo describes one ONNX graph's inputs and outputs.

type Initialization

type Initialization struct {
	RuntimeVersion string  `json:"runtime_version"`
	RuntimeSeconds float64 `json:"runtime_initialization_seconds"`
	VocabSeconds   float64 `json:"vocabulary_loading_seconds"`
	ModelSeconds   float64 `json:"model_load_seconds"`
}

Initialization is a diagnostic breakdown of New's loading time. ModelSeconds includes metadata inspection, which loads temporary sessions with this binding version, as well as the retained inference sessions. RuntimeVersion is the loaded native ONNX Runtime version.

type ModelInfo

type ModelInfo struct {
	Preprocessor GraphInfo `json:"preprocessor"`
	Encoder      GraphInfo `json:"encoder"`
	Decoder      GraphInfo `json:"decoder"`
}

ModelInfo describes the three loaded graphs.

type Provider

type Provider string

Provider selects the ONNX Runtime execution provider for the encoder and decoder. Preprocessing always runs on CPU.

const (
	CPU  Provider = "cpu"
	CUDA Provider = "cuda" // appended explicitly; there is no CPU retry on failure
)

Supported providers.

type Request

type Request struct {
	Samples    []float32    `json:"-"`
	SampleRate int          `json:"sample_rate"`
	Chunking   ChunkOptions `json:"chunking"`
}

Request supplies already-decoded mono PCM. Samples must remain unmodified until transcription returns. Values must be finite, conventionally in [-1, 1]. No implicit gain, clipping, channel conversion, or resampling is performed.

type Result

type Result struct {
	Text   string        `json:"text"`
	Tokens []Token       `json:"tokens"`
	Chunks []ChunkResult `json:"chunks"`
	Summary
}

Result collects the same finalized chunks emitted by TranscribeStream.

type Seam

type Seam string

Seam reports how a chunk was joined to the previous window's hypothesis.

const (
	SeamAligned   Seam = "aligned"   // matching words anchored the splice
	SeamTimestamp Seam = "timestamp" // no confident match; emission time decided ownership
)

Seam methods.

type Summary

type Summary struct {
	AudioSeconds     float64 `json:"audio_seconds"`
	ProcessedSeconds float64 `json:"processed_seconds"`
	FinalizedSeconds float64 `json:"finalized_seconds"`
	Windows          int     `json:"windows"`
	DeliveredChunks  int     `json:"delivered_chunks"`
	WindowSamples    int     `json:"window_samples"`
	OverlapSamples   int     `json:"overlap_samples"`
	Timings          Timings `json:"timings"`
}

Summary has constant size. Windows counts completed decodes; DeliveredChunks counts callbacks that returned nil. On error it includes completed work and timings (including work in a failed window), excluding time in callbacks. ProcessedSeconds is the furthest decoded sample; FinalizedSeconds is the end of the last successfully delivered chunk. Neither implies recognition accuracy.

type TensorInfo

type TensorInfo struct {
	Name  string  `json:"name"`
	Shape []int64 `json:"shape"`
	Type  string  `json:"type"`
}

TensorInfo describes one model input or output.

type Timings

type Timings struct {
	ValidationSeconds    float64 `json:"validation_seconds"`
	PreprocessingSeconds float64 `json:"preprocessing_seconds"`
	EncoderSeconds       float64 `json:"encoder_seconds"`
	DecoderSeconds       float64 `json:"decoder_seconds"`
	AssemblySeconds      float64 `json:"assembly_seconds"`
}

Timings is a diagnostic breakdown of one request into disjoint stages, summed over all windows. TranscribeStream excludes time spent in the callback.

type Token

type Token struct {
	ID           int     `json:"id"`
	Piece        string  `json:"piece"`
	Frame        int     `json:"frame"`
	StartSeconds float64 `json:"start_seconds"`
	Advance      int     `json:"advance"` // predicted encoder-frame advance, not word duration
}

Token records a nonblank decoder emission. Piece is the raw SentencePiece entry. Frame and StartSeconds are recording-relative emission positions, not aligned word boundaries. Small backwards shifts at seams are clamped to keep the assembled sequence nondecreasing.

Directories

Path Synopsis
cmd
inference-probe command
inference-probe runs a local model on little-endian float32 mono PCM.
inference-probe runs a local model on little-endian float32 mono PCM.
internal
assets
Package assets validates the supported Parakeet export's vocabulary.
Package assets validates the supported Parakeet export's vocabulary.
chunking
Package chunking plans frame-aligned, overlapping PCM windows without retaining a per-recording list of windows.
Package chunking plans frame-aligned, overlapping PCM windows without retaining a per-recording list of windows.
decoder
Package decoder implements Parakeet's greedy token-and-duration state machine.
Package decoder implements Parakeet's greedy token-and-duration state machine.
ortruntime
Package ortruntime decides who owns the process-global ONNX Runtime environment.
Package ortruntime decides who owns the process-global ONNX Runtime environment.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL