fix(provider): Chain.Speak läuft im Direkt-Endgame nur einmal durch die Kette
Speak rief bei ErrNoByteOutput SpeakDirect auf, das einen eigenen vollständigen walk machte: fehlgeschlagene Cloud-Stufen wurden zweimal angefragt (doppelte POSTs auf bezahlten APIs), Fehlschlag-Zeilen verdoppelten sich im Log, die Worst-Case-Latenz verdoppelte sich, und Bytes aus einer transienten Erholung im zweiten Durchlauf hätten einen Fehler statt Wiedergabe produziert. Speak entscheidet jetzt aus EINEM walk-Ergebnis (Bytes → Player, nur-Direkt-Stufe → SpeakDirect direkt daran, sonst gebündelter Stufen-Fehler aus dem geteilten Helper walkResult.noTierErr). Synthesize/SpeakDirect behalten ihre öffentlichen Verträge; null-Player verwirft tonlos mit Exit 0 wie zuvor. Test: TestChainDirektEndgameEinDurchlauf pinnt Cloud-Stufe genau 1× angefragt, jede Fehlschlag-Zeile genau 1×, Direktansage kommt zustande.
This commit is contained in:
1 parent
8351d40717
commit
3b44605234
2 files changed
+78
-14
No files matched your search
+25
-14
@@ -205,8 +205,7 @@ func (c *Chain) Synthesize(ctx context.Context, text, lang string) ([]byte, erro
|
||||
}
|
||||
return nil, fmt.Errorf("%w (Stufe %s spricht nur direkt)", ErrNoByteOutput, r.directName)
|
||||
}
|
||||
return nil, fmt.Errorf("kette: keine Stufe konnte sprechen (nicht verfügbar: %s; fehlgeschlagen: %s)",
|
||||
joinOr(r.skipped, "keine"), joinOr(r.failed, "keine"))
|
||||
return nil, r.noTierErr()
|
||||
}
|
||||
|
||||
// SpeakDirect implementiert DirectSpeaker: Direktansage über die Stufe,
|
||||
@@ -225,34 +224,46 @@ func (c *Chain) SpeakDirect(ctx context.Context, text, lang string) error {
|
||||
joinOr(r.skipped, "keine"), joinOr(r.failed, "keine"))
|
||||
}
|
||||
|
||||
// Speak ist die komplette Ansage: Synthese mit Fallback → Wiedergabe über
|
||||
// den Player; fällt die Kette auf die Direkt-Stufe zurück, entscheidet der
|
||||
// Player-Name über Ton (alles außer „null") oder tonloses Verwerfen (null:
|
||||
// CI/Server bleiben fehler- und tonfrei, der Verwurf wird protokolliert).
|
||||
// Nur ein Fehler aller Stufen oder eine fehlgeschlagene Wiedergabe sind
|
||||
// Fehler.
|
||||
// Speak ist die komplette Ansage in EINEM Ketten-Durchlauf: Synthese mit
|
||||
// Fallback → Wiedergabe über den Player; fällt die Kette auf die Direkt-
|
||||
// Stufe zurück, entscheidet der Player-Name über Ton (alles außer „null")
|
||||
// oder tonloses Verwerfen (null: CI/Server bleiben fehler- und tonfrei,
|
||||
// der Verwurf wird protokolliert). Der Durchlauf findet bewusst genau
|
||||
// einmal statt: fehlgeschlagene Cloud-Stufen werden nicht ein zweites Mal
|
||||
// angefragt (keine doppelten POSTs auf bezahlten APIs, keine doppelten
|
||||
// Protokoll-Zeilen), und Bytes aus diesem Durchlauf werden abgespielt,
|
||||
// statt sie wegzuwerfen und neu zu laufen. Nur ein Fehler aller Stufen
|
||||
// oder eine fehlgeschlagene Wiedergabe sind Fehler.
|
||||
func (c *Chain) Speak(ctx context.Context, text, lang string) error {
|
||||
data, err := c.Synthesize(ctx, text, lang)
|
||||
r := c.walk(ctx, text, lang)
|
||||
switch {
|
||||
case err == nil:
|
||||
case r.data != nil:
|
||||
if c.cfg.Player == nil {
|
||||
return fmt.Errorf("kette: kein Player konfiguriert — Bytes können nicht abgespielt werden")
|
||||
}
|
||||
if err := c.cfg.Player.Play(ctx, data); err != nil {
|
||||
if err := c.cfg.Player.Play(ctx, r.data); err != nil {
|
||||
return fmt.Errorf("kette: Wiedergabe über %s fehlgeschlagen: %w", c.cfg.Player.Name(), err)
|
||||
}
|
||||
return nil
|
||||
case errors.Is(err, ErrNoByteOutput):
|
||||
case r.direct != nil:
|
||||
if c.cfg.Player != nil && c.cfg.Player.Name() == NullPlayerName {
|
||||
chainLogf(c.cfg, "kette: Direktansage verworfen (audio.sink=null)\n")
|
||||
return nil
|
||||
}
|
||||
return c.SpeakDirect(ctx, text, lang)
|
||||
return r.direct.SpeakDirect(ctx, text, lang)
|
||||
default:
|
||||
return err
|
||||
return r.noTierErr()
|
||||
}
|
||||
}
|
||||
|
||||
// noTierErr bündelt den Ende-ohne-Ergebnis-Fehler eines Durchlaufs: keine
|
||||
// Stufe lieferte Bytes oder Direktansage — übersprungene (nicht verfügbar)
|
||||
// und fehlgeschlagene Stufen werden genannt, nie still.
|
||||
func (r walkResult) noTierErr() error {
|
||||
return fmt.Errorf("kette: keine Stufe konnte sprechen (nicht verfügbar: %s; fehlgeschlagen: %s)",
|
||||
joinOr(r.skipped, "keine"), joinOr(r.failed, "keine"))
|
||||
}
|
||||
|
||||
// chainLogf schreibt eine Protokoll-Zeile, wenn ein Log gesetzt ist.
|
||||
func chainLogf(cfg ChainConfig, format string, args ...any) {
|
||||
if cfg.Log != nil {
|
||||
|
||||
Reference in new issue
Block a user