m3ta-chironandClaude Fable 5.1 2e2bc27acf docs: talk-Skill mit Opt-in-Etikette, AGENTS-Snippet, README Installation/Release/Toast
skills/talk/SKILL.md: wann sprechen (nur auf Trigger in der Session oder
Standing-Order in der AGENTS.md, session-sticky bis Widerruf), wann nicht,
was eine gute Ansage ist (Status · nächster Schritt · wofür der Nutzer
gebraucht wird; Beispiele gut/schlecht), Umgang mit muted/discarded/
--now/Prioritäten, Pitfalls. AGENTS-snippet.md: der Block für Projekt-
AGENTS.md.

README: Installation (Release-Asset mit Prüfsumme, Nix-Flake, go install),
Skill-Abschnitt, Toast als echte Desktop-Benachrichtigung, Release-Ablauf
und nix build in „Entwicklung". Spec-Nachtrag: Release ohne GoReleaser,
Notifications ohne beeep.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 10:37:46 +02:00

talk

Status: WIP — aktive Entwicklung (talk 2.0). Spezifikation mit Entscheidungs-Nachtrag: docs/spec.md

talk macht Skripte und KI-Agenten hörbar: Ein Aufruf — talk "Build fertig" — spricht eine kurze Nachricht über eine TTS-Provider-Kette (ElevenLabs → OpenAI → Edge-TTS → OS-Stimme), serialisiert durch einen Hintergrund-Daemon, den talk beim ersten Aufruf selbst startet. Ein statisches Binary, cross-platform (Windows/macOS/Linux, amd64/arm64), keine Runtime-Abhängigkeiten.

Befehle

Befehl Zweck
talk "Text" Ansage — Abkürzung für talk say, non-blocking
talk say "Text" Ansage (Text als Argument oder von stdin)
talk wrap -- <Befehl> Befehl ausführen, Erfolg/Misserfolg + Dauer ansagen
talk mute / talk unmute Ansagen dauerhaft stummschalten / Stummschaltung aufheben
talk status Daemon-Status (Queue, Mute, Provider, Version)
talk cancel laufende und geplante Ansagen abbrechen
talk daemon run|start|stop Daemon im Vordergrund betreiben / im Hintergrund starten / stoppen (normalerweise nicht nötig, siehe unten)
talk voices bekannte Stimmen je Provider/Sprache, Default markiert (--provider, --lang, --json)
talk test Testsatz sprechen; zeigt Provider und Zeiten (--lang, --provider, --voice)
talk doctor Selbstdiagnose: Config, Daemon, Audio, Provider, Mute, Gesetze (--probe, --json)
talk setup Ersteinrichtung in wenigen Fragen: Stimmen probehören, Ruhezeit, config.toml

Say-Flags (gelten auch für talk "Text"): --wait, --now, --priority, --voice, --lang, --provider.

Installation

Release-Binary (Linux, macOS, Windows; amd64/arm64): von den Releases das passende talk_<os>_<arch> laden, Prüfsumme gegen checksums.txt vergleichen, als talk in den PATH legen. Keine weiteren Abhängigkeiten.

curl -LO https://code.m3ta.dev/m3tam3re/talk/releases/latest/download/talk_linux_amd64
sha256sum -c --ignore-missing <(curl -sL https://code.m3ta.dev/m3tam3re/talk/releases/latest/download/checksums.txt)
install -m 755 talk_linux_amd64 ~/.local/bin/talk

Nix (Flake): nix run git+https://code.m3ta.dev/m3tam3re/talk -- doctor oder in der Konfiguration inputs.talk.url = "git+https://code.m3ta.dev/m3tam3re/talk"; und inputs.talk.packages.${system}.talk bzw. inputs.talk.overlays.default. Das Paket bringt espeak-ng (OS-Stimme) und libnotify (Toast) mit.

Aus dem Quelltext: go install code.m3ta.dev/m3tam3re/talk/cmd/talk@latest.

Erste Schritte

talk doctor            # Was ist da, was fehlt? Eine Zeile pro Punkt.
talk test              # Hörst du etwas? Welche Stimme war das?
talk setup             # Stimme aussuchen, Ruhezeit, fertig.
talk "Hallo Welt"      # Ab jetzt: Skripte und Agenten rufen genau das.

Ohne jeden Key funktioniert talk sofort: Es nutzt die kostenlose Edge- Stimme von Microsoft (Internet nötig) und sonst die Stimme des Betriebssystems. Wer eine Premium-Stimme will, setzt TALK_ELEVENLABS_KEY oder TALK_OPENAI_KEY als Umgebungsvariable — Keys gehören nie in die config.toml.

Diagnose und Einrichtung

  • talk doctor berichtet zeilenweise mit ✔ (ok), ⚠ (Hinweis), ✘ (Fehler): Config-Datei, Daemon (läuft / startet beim nächsten Sprechbefehl / fremder Dienst auf dem Port), Audio-Ausgabe (oto initialisierbar? null = kein Ton), jede Provider-Stufe (Key vorhanden, Endpunkt, Default-Stimme; OS-Stimme: welcher Mechanismus gefunden wurde), Mute, Gesetze inkl. aktiver Ruhezeit. Exit 0 ohne ✘, sonst 1; ein nicht laufender Daemon ist ⚠. --probe macht echte Synthese-Proben (Netz), --json liefert die Liste maschinenlesbar. Auf Windows-Konsolen stehen [OK]/[WARN]/[FAIL] statt der Symbole.
  • talk test spricht einen festen Testsatz direkt in diesem Prozess (nicht über den Daemon; Mute und Ruhezeit gelten hier nicht, ein aktives Mute wird genannt) und zeigt, welche Stufe gesprochen hat und wie lange Synthese und Wiedergabe gedauert haben.
  • talk voices listet den kuratierten Stimmen-Katalog (ElevenLabs- Premade, OpenAI, Edge-Neural je Locale) als Tabelle; * markiert die Stimme, die talk ohne --voice nimmt (config voice.<provider>.<lang> oder Repo-Default). Config-Einträge außerhalb des Katalogs erscheinen als eigene Zeile. Jede Voice-ID des Providers funktioniert auch ohne Katalog-Eintrag.
  • talk setup fragt im Terminal: Keys (nur Hinweis — ohne Key läuft die Edge-Stimme), Stimme für Deutsch und Englisch der aktiven Stufe (Nummer aus dem Katalog oder Voice-ID, probehören, übernehmen), Ruhezeit. Dann schreibt es die config.toml — eine bestehende Datei wird zusammengeführt (andere Einstellungen bleiben) und als .bak gesichert — und spricht einen Testsatz. q bricht jederzeit ab, geschrieben wird erst ganz am Ende. Ohne Terminal (Pipe, Agent) stellt setup keine Fragen und zeigt nur die wirksame Konfiguration.

Daemon

Es gibt genau ein Binary. Der Daemon ist talk daemon run — derselbe Code, im Hintergrund. Er besitzt die Ansage-Queue und spricht seriell ab — genau eine Ansage gleichzeitig, Reihenfolge now > high > normal > low (FIFO je Level). Der HTTP-Server lauscht nur auf 127.0.0.1; Port: Default 7800, config [server] port, Env TALK_PORT (schlagen den Default).

Autostart: Läuft kein Daemon, startet talk say (und talk wrap) ihn beim ersten Sprechbefehl selbst — losgelöst vom Terminal, mit Protokoll in daemon.log und Prozess-ID in daemon.pid im Config-Dir; stderr meldet einmalig „Daemon gestartet". Der Daemon läuft dann weiter (Leerlauf kostet nichts Nennenswertes), bis talk daemon stop, Abmelden oder Neustart. Parallele erste Aufrufe sind harmlos: Verliert einer das Port-Rennen, benutzt er den Daemon des anderen. Wer den Daemon selbst verwaltet (systemd, launchd, Task Scheduler mit talk daemon run), schaltet den Autostart ab:

[daemon]
autostart = false   # Env TALK_AUTOSTART=false

Steuerbefehle (mute, unmute, status, cancel) starten den Daemon bewusst nicht: talk status sagt dann „läuft nicht" (Exit 1), talk mute schreibt die Mute-Datei direkt.

Endpunkt Wirkung
POST /speak {text, priority?, provider?, voice?, lang?, now?} → 202 {"id":"…"} (+ "truncated":true, wenn auf max_text gekürzt); bei Mute: 202 {"id":"","muted":true}; bei Gesetz-Verwurf: 202 {"id":"","discarded":"rate_limit"|"quiet_hours"} (+ "toast":true im Toast-Modus) — verworfen, dokumentiertes Verhalten
OPTIONS /speak CORS-Preflight → 204 mit Freigabe-Headern, wenn die Origin in server.allowed_origins steht (siehe unten); sonst 204 ohne CORS-Header
GET /status {"version", "queue", "queue_ids", "current", "muted", "provider", "rate_limit": {"interval", "allow_high_burst", "next_slot"}, "quiet_hours": {"window", "active", "mode", "until"}, "limits": {"max_text", "max_age"}, "mcp": true, "cors": [origins]}
POST /cancel bricht die laufende Ansage ab + leert die Queue → 200 {"cancelled":n}
POST /mute / POST /unmute persistenter Mute-State → 200 {"muted":bool}
POST /shutdown fährt den Daemon herunter (talk daemon stop) → 200 {"ok":true}
POST /mcp MCP (Streamable HTTP): JSON-RPC initialize, notifications/initialized, ping, tools/list, tools/call — ein Tool speak (siehe unten)
GET /mcp 405 — der Daemon bietet keine server-initiierten Streams an
  • CLI-Verhalten: talk say (und talk "Text") erkennt den Daemon via GET /status. Läuft er: enqueue + sofortige Rückkehr (non-blocking, stdout erhält die Item-ID); --wait pollt bis zur Fertigstellung; --now springt vor die Warteschlange; bei Mute sagt die CLI muted, bei Gesetz-Verwurf discarded: <grund>. talk mute/unmute/status/ cancel sind Daemon-Aufrufe mit plain Output.
  • Mute ist persistent (Datei mute im Config-Dir, Inhalt true/ false) und überlebt Daemon-Restarts. talk mute ohne laufenden Daemon schreibt die Datei direkt — der Daemon liest sie bei jeder Ansage neu, der Zustand greift also auch offline.
  • Direkt-Pfad: Ist kein Daemon erreichbar und keiner startbar (Autostart aus, Port fremd belegt, Start gescheitert), spricht talk direkt im eigenen Prozess — blockierend, ohne Warteschlange (parallele Aufrufe können überlappen), mit Hinweis und Grund auf stderr. Mute wird auch hier respektiert. Nichts bricht still.

Rate-Limit + Ruhezeiten (Task 05)

Der Daemon erzwingt das Anti-Spam-Gesetz als Backstop unter allem Prompt-Verhalten. Rate-Limit: Jede gestartete Ansage bucht ein Zeitfenster — Default 1 Ansage / 30 s. Bis zum Fenster-Ablauf gilt:

  • priority=low wird verworfen (schon beim Eintritt, die 202-Antwort nennt discarded: "rate_limit"); war das Fenster beim Eintritt noch frei, entscheidet der Worker später.
  • normal parkt in der Queue und spielt zum freien Slot (in /status als rate_limit.next_slot sichtbar).
  • priority=high darf mit queue.allow_high_burst = true (Default) bewusst verstoßen; ohne Burst-Recht wartet high wie normal.
  • now:true umgeht auch das Rate-Limit — der dokumentierte Ausbruch für Dringendes (startet trotzdem ein neues Fenster).

Ruhezeiten: quiet_hours = "18:00-08:00" (lokale Zeit, überspannt Mitternacht; leer/off = aus — bewusst kein Default-Fenster, Ruhezeiten sind Opt-in wie Mute). In der Ruhezeit verfällt Audio, auch für high (Burst ist ein Rate-Recht, kein Ruhe-Recht):

  • quiet_mode = "discard" (Default): still verwerfen.
  • quiet_mode = "toast": Desktop-Benachrichtigung statt Audio — über die Bordmittel des Systems (Linux notify-send, macOS Notification Center, Windows Toast), best effort: fehlt das Werkzeug, bleibt es bei der Protokoll-Zeile, talk doctor zeigt es unter „Toast".
  • now:true bricht auch die Ruhezeit (bewusster Ausbruch).

Länge und Alter: Zwei weitere Gesetze gegen das, was Prompts gern tun — die Langantwort vorlesen und Ansagen stapeln:

  • max_text (Default 400 Zeichen, etwa zwei bis drei Sätze): Längere Texte werden beim Eintritt an einer Wortgrenze gekürzt und so gesprochen; die Antwort meldet "truncated": true, die CLI warnt auf stderr, das MCP-Ergebnis sagt es im Text. Der Nutzer hört den Anfang statt zehn Minuten Vortrag, das API-Budget bleibt begrenzt.
  • max_age (Default 2 Minuten): Was länger in der Queue parkt (Rate- Limit, Stau), ist keine Nachricht mehr und wird beim Fälligwerden verworfen (discarded: "expired" im Protokoll; die 202-Antwort beim Eintritt bleibt eine Zusage, denn beim Eintritt ist nichts veraltet). now:true wartet nie lange und ist ausgenommen.

Rangordnung: manuelles Mute gewinnt immer > now > veraltet (max_age)

Ruhezeit > Rate-Limit; max_text greift beim Eintritt auf alles. Konfiguration (config.toml, Env schlägt Datei):

[queue]
rate_limit = "30s"        # Dauer; "0s" = unbegrenzt (TALK_RATE_LIMIT)
allow_high_burst = true   # high darf verstoßen (TALK_ALLOW_HIGH_BURST)
max_text = 400            # Zeichen; 0 = unbegrenzt (TALK_MAX_TEXT)
max_age = "2m"            # Wartezeit; "0s" = unbegrenzt (TALK_MAX_AGE)

quiet_hours = "18:00-08:00"  # leer = aus; Env TALK_QUIET_HOURS ("off" = aus)
quiet_mode = "discard"       # discard | toast (TALK_QUIET_MODE)

talk status zeigt den Gesetz-State: rate-limit: 30s (high-burst erlaubt), nächster slot …, ruhezeit: 18:00-08:00 (modus discard, aktiv bis 08:00) und limits: max. 400 Zeichen, max. Wartezeit 2m0s.

CORS-Tür + MCP-Endpoint (Task 06)

CORS gilt bewusst nur für POST /speak — genau die eine Tür, die eine spätere Browser-Anbindung (z. B. LibreChat) braucht; Steuer-Endpunkte bleiben browserseitig unerreicht. Die Erlaubnis ist eine exakte Origin- Liste; "*" ist erlaubt, aber nicht Default; leere Liste (Default) = gar keine CORS-Header = der Browser blockt:

[server]
allowed_origins = ["https://chat.az-gruppe.com"]  # Env TALK_ALLOWED_ORIGINS
  • OPTIONS /speak (Preflight) von gelisteter Origin → 204 mit Access-Control-Allow-Origin (die Origin bzw. *), -Methods: POST, OPTIONS, -Headers: Content-Type, Max-Age: 600.
  • Echter POST /speak mit gelisteter Origin trägt denselben ACAO-Header.
  • Die Tür ist serverseitig zu, nicht nur browserseitig. CORS allein verhindert nur das Lesen der Antwort; einen POST mit text/plain-Body oder einen Formular-POST schickt jede Webseite ohne Preflight ab. Deshalb lehnt der Daemon jede Anfrage mit fremdem Origin-Header mit 403 ab, die nicht POST /speak von einer gelisteten Origin ist — also ungelistete Origins auf /speak, jeden Browser-Zugriff auf /status, /mute, /unmute, /cancel und /mcp, auch von gelisteten Origins. Nur der Preflight OPTIONS /speak bleibt für ungelistete Origins ein 204 ohne CORS-Header (ohne Wirkung; der Browser verwirft den geplanten POST). CLI, curl und MCP-Clients senden keinen Origin-Header und sind nicht betroffen.

MCP (POST /mcp, Streamable HTTP): schlanke eigene Implementierung des JSON-RPC-Subsets ohne SDK — initialize (angefragte protocolVersion wird geechot; Ergebnis protocolVersion, capabilities: {tools:{}}, serverInfo), notifications/initialized (Notification → 202 ohne Body), ping, tools/list, tools/call. Der Server ist zustandslos (keine Session-IDs) und pusht nichts (daher GET /mcp → 405). Ein Tool:

{"name": "speak",
 "inputSchema": {"type": "object",
   "properties": {"text": {"type": "string"},
                  "priority": {"enum": ["low","normal","high"]},
                  "provider": {"type": "string"},
                  "lang": {"enum": ["de","en"]},
                  "now": {"type": "boolean"}},
   "required": ["text"]}}

tools/call fährt exakt denselben Aufnahme-Pfad wie POST /speak (inklusive Mute, Rate-Limit, Ruhezeiten). Ergebnis ist immer Text-Content: "queued: <id>" bei Aufnahme; muted/discarded: <grund> mit Erklärung — bewusst isError: false, denn ein Verwurf ist das korrekte dokumentierte Ergebnis, kein Protokollfehler. Validierungsfehler (fehlender Text, ungültige Priorität, unbekanntes Tool) sind dagegen JSON-RPC-Fehler (-32602). Handshake als curl-Sequenz:

curl -s http://127.0.0.1:7800/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
curl -s http://127.0.0.1:7800/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}'
curl -s http://127.0.0.1:7800/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"speak","arguments":{"text":"Build fertig"}}}'

Provider-Kette (Task 03)

Ohne Pin läuft die Vier-Stufen-Fallback-Kette ElevenLabs → OpenAI TTS → Edge-TTS → OS built-in: Bei fehlendem Key, HTTP-Fehler, Timeout oder Netz-Problem fällt die Kette automatisch auf die nächste Stufe; jeder Fallback wird auf stderr protokolliert (nie still). Pinnen per --provider=<name> oder provider = "<name>" in der config.toml.

Stufe Key (Env) Default-Endpunkt Audio
elevenlabs TALK_ELEVENLABS_KEY https://api.elevenlabs.io mp3
openai TALK_OPENAI_KEY https://api.openai.com mp3
edge — (kostenlos) wss://speech.platform.bing.com/... mp3
osbuiltin — lokale OS-TTS WAV/PCM

Keys laufen ausschließlich über Env-Variablen (nie in die config.toml). Endpunkte sind konfigurierbar — Datei [providers.<name>] endpoint = "…" bzw. Env TALK_ELEVENLABS_ENDPOINT / TALK_OPENAI_ENDPOINT / TALK_EDGE_ENDPOINT (Env schlägt Datei; so zeigen Tests/CI die Kette auf Fake-Server). Das OpenAI-Modell ist konfigurierbar ([providers.openai] model, Default tts-1).

Stimmen: neutralen Repo-Defaults je Provider+Sprache (ElevenLabs „Rachel", OpenAI „alloy", Edge de-DE-KatjaNeural/en-US-AriaNeural) kann man pro Sprache überordnen — config.toml:

[voice.elevenlabs]
de = "21m00Tcm4TlvDq8ikWAM"   # eigene Stimme je Sprache
en = "21m00Tcm4TlvDq8ikWAM"

[voice.edge]
de = "de-DE-KatjaNeural"

Rangfolge: --voice-Flag > voice.<provider>.<lang> > Repo-Default. MP3 wird von der Binärdatei selbst dekodiert (reiner Go-Dekoder, kein ffmpeg, CGO-frei).

OS-TTS auf Linux (Dev-Abhängigkeit espeak)

Der OS-Provider osbuiltin nutzt auf Linux zwei Wege:

  • Audio-Bytes (Wiedergabe über den eingebauten oto-Sink): benötigt espeak (oder espeak-ng). Auf NixOS als Dev-Abhängigkeit: nix shell nixpkgs#espeak-ng.
  • Direktansage ohne espeak: spd-say (speech-dispatcher) — spielt selbst über den Daemon ab, kann aber nach spd-say --help keine Bytes in Dateien/Pipes liefern. talk nutzt diesen Weg automatisch, wenn kein espeak vorhanden ist.

TALK_AUDIO_SINK=null (oder audio.sink = "null" in der config.toml) schaltet jede Tongabe ab — für CI und Server.

talk wrap — Befehle annoncieren (Task 07)

talk wrap -- <Befehl> [Argumente …] führt den Befehl aus und macht das Ergebnis hörbar: stdin/stdout/stderr werden durchgereicht — stdout gehört allein dem Befehl, Informationszeilen von talk (z. B. die Item-ID der Ansage) gehen auf stderr, damit Pipes und Redirects sauber bleiben. Die Wandzeit wird gemessen und nach dem Ende des Befehls angesagt:

  • Erfolg: „sleep erfolgreich. Dauer: 2 Sekunden."
  • Misserfolg: „sh fehlgeschlagen, Code 2." (ohne Dauer)
talk wrap -- sleep 2
talk wrap --wait -- make test
  • -- schirmt den Befehl komplett ab: Das erste allein stehende -- beendet die talk-Flag-Auswertung — alles danach geht unverändert an den Befehl. talk wrap -- foo --bar reicht --bar an foo weiter, statt es als talk-Flag zu fehlzuinterpretieren. talk-Flags für die Ansage stehen deshalb davor (talk wrap --wait -- …); ohne -- gelten die üblichen Regeln (Flags überall, auch nach Positionalargumenten).
  • Label der Ansage: wrap.label in der config.toml (Env TALK_WRAP_LABEL schlägt die Datei) oder der Basisname des Befehls (/usr/bin/make → „make"). Die Sprache erkennt langdetect am Label (de/en, deutscher Default); --lang überschreibt.
  • Exit-Code-Politik: wrap exitet immer mit dem Code des Befehls — scheitert die Ansage selbst, bleibt der Befehls-Code trotzdem (nur eine Warning auf stderr; talk-Fehler ≠ Befehls-Fehler). Nicht startbare Befehle (Binary nicht gefunden) zählen als 127 (Shell-Konvention), Signaltode portabel als 1. Benutzungsfehler von talk selbst (kein Befehl, ungültige Flags) scheitern vor dem Start des Befehls.
  • Ansageweg: derselbe wie say — Daemon (enqueue, non-blocking; --wait respektiert; Autostart), ohne Daemon der Direkt-Pfad. wrap erbt die say-Flags (--wait, --now, --priority, --voice, --lang, --provider) und reicht sie an die Ansage durch.
[wrap]
label = "Deployment"   # Ansage-Label statt Befehls-Basisname (TALK_WRAP_LABEL)

Für Agenten: Skill und Standing-Order

skills/talk/SKILL.md ist die Etikette als Skill-Artefakt: striktes Opt-in (Sprechen nur auf Aufforderung in der Session oder Standing-Order in der AGENTS.md, session-sticky bis Widerruf), Inhalt = Status · nächster Schritt · wofür der Nutzer gebraucht wird, nie die Langantwort, muted/discarded respektieren. Der Drei-Zeilen-Block für Projekt-AGENTS.md steht in skills/talk/AGENTS-snippet.md. Der Daemon erzwingt Rate-Limit, Längen- und Alters-Grenze, Mute und Ruhezeit unabhängig davon, was im Prompt steht.

Entwicklung

go build ./...   # bauen
go test ./...    # Tests
go vet ./...     # statische Analyse
gofmt -l .       # Formatierung prüfen (leerer Output = ok)
nix develop      # devShell mit go, gopls, espeak-ng, libnotify

CI: .gitea/workflows/test.yml (Gitea Actions) läuft vet, gofmt-Check, Tests und Build bei jedem Push und Pull Request.

Release: Tag v* pushen → .gitea/workflows/release.yml testet, baut mit scripts/release.sh die sechs Targets (statisch, CGO_ENABLED=0, Version per ldflags) und legt ein Gitea-Release mit talk_<os>_<arch>[.exe] und checksums.txt an. Lokal: scripts/release.sh 1.2.3 → dist/. Bewusst ohne GoReleaser: ein Shell-Skript über go build ist dasselbe Artefakt, lokal wie in CI, ohne zusätzliches Werkzeug. nix build .#talk baut das Nix-Paket (nach Änderungen an go.mod den vendorHash in flake.nix nachziehen — nix build nennt den erwarteten Wert).

Lizenz

MIT — siehe LICENSE.

S
Description
No description provided
Readme MIT
816 KiB
0 Stars 1 Watchers 0 Forks
talk v0.1.1
Latest
2026-10-25 12:59:59 +01:00
Languages
Go 99.3%
Nix 0.5%
Shell 0.2%