Skip to content

dictation_history

dictation_history

A local, bounded history of dictations.

Two things make this worth having: you can recover a sentence the paste lost (wrong window focused, app ate it), and you can see what the recogniser actually heard — which is how you find out that "small" fixes the proper nouns "base" was mangling.

Design constraints that follow from the privacy contract:

  • Local only. A JSONL file under the Diapason config dir. Nothing here is ever sent anywhere; the outbound ratchet has no reason to see this module.
  • Opt-out and bounded. Off by default is wrong (an invisible history is the point of a history), but unbounded growth is a liability: a dictation tool used all day would accumulate every sentence you ever spoke. It keeps the last N entries, N being small enough to stay a convenience rather than an archive.
  • Content lives here, so nowhere else. Entries hold the transcript in clear — that IS the feature — which is precisely why the log must not. redact stays the rule for logging; this file is the one sanctioned place the text is written down.

Classes

DictationEntry dataclass

DictationEntry(
    text: str,
    timestamp: float,
    duration_s: float = 0.0,
    app: str = "",
    model: str = "",
    chars: int = 0,
    pid: int = 0,
)

One completed dictation.

Functions:

load_history

load_history(
    path: str | Path | None = None,
    *,
    limit: Optional[int] = None,
) -> List[DictationEntry]

Return entries, newest first. A corrupt line is skipped, not fatal.

Source code in src/diapason/desktop/dictation_history.py
def load_history(
    path: str | Path | None = None, *, limit: Optional[int] = None
) -> List[DictationEntry]:
    """Return entries, newest first. A corrupt line is skipped, not fatal."""
    p = _resolve(path)
    if not p.is_file():
        return []
    entries: List[DictationEntry] = []
    try:
        for line in p.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if not line:
                continue
            try:
                data = json.loads(line)
                entries.append(DictationEntry(**data))
            except (json.JSONDecodeError, TypeError):
                # One bad line must not destroy the whole history.
                continue
    except OSError:
        logger.debug("could not read dictation history", exc_info=True)
        return []
    entries.reverse()  # newest first
    return entries[:limit] if limit else entries

append_entry

append_entry(
    entry: DictationEntry,
    *,
    path: str | Path | None = None,
    max_entries: int = DEFAULT_MAX_ENTRIES,
) -> None

Append one entry, trimming the file to max_entries.

Never raises: losing a history line must not break a dictation that otherwise succeeded.

Source code in src/diapason/desktop/dictation_history.py
def append_entry(
    entry: DictationEntry,
    *,
    path: str | Path | None = None,
    max_entries: int = DEFAULT_MAX_ENTRIES,
) -> None:
    """Append one entry, trimming the file to ``max_entries``.

    Never raises: losing a history line must not break a dictation that
    otherwise succeeded.
    """
    p = _resolve(path)
    try:
        p.parent.mkdir(parents=True, exist_ok=True)
        with open(p, "a", encoding="utf-8") as fh:
            fh.write(json.dumps(asdict(entry), ensure_ascii=False) + "\n")
        _trim(p, max_entries)
    except OSError:
        logger.debug("could not append to dictation history", exc_info=True)

clear_history

clear_history(path: str | Path | None = None) -> bool

Delete the history file. Returns True if one existed.

Source code in src/diapason/desktop/dictation_history.py
def clear_history(path: str | Path | None = None) -> bool:
    """Delete the history file. Returns True if one existed."""
    p = _resolve(path)
    if p.exists():
        try:
            p.unlink()
            return True
        except OSError:
            return False
    return False

stats

stats(entries: Iterable[DictationEntry]) -> dict

Aggregate a few honest numbers for a dashboard.

Words-per-minute is deliberately absent: it would need a speaking-time denominator this data cannot support, and a made-up productivity figure is worse than none.

Source code in src/diapason/desktop/dictation_history.py
def stats(entries: Iterable[DictationEntry]) -> dict:
    """Aggregate a few honest numbers for a dashboard.

    Words-per-minute is deliberately absent: it would need a speaking-time
    denominator this data cannot support, and a made-up productivity figure is
    worse than none.
    """
    items = list(entries)
    total_chars = sum(e.chars for e in items)
    total_seconds = sum(e.duration_s for e in items)
    return {
        "count": len(items),
        "total_chars": total_chars,
        "total_seconds": round(total_seconds, 1),
        "avg_chars": round(total_chars / len(items), 1) if items else 0.0,
    }