179 lines
10 KiB
Diff
179 lines
10 KiB
Diff
# Review package: 8351d40..HEAD
|
||
|
||
## Commits
|
||
3b44605 fix(provider): Chain.Speak läuft im Direkt-Endgame nur einmal durch die Kette
|
||
|
||
## Files changed
|
||
internal/provider/chain.go | 39 +++++++++++++++++++-----------
|
||
internal/provider/chain_test.go | 53 +++++++++++++++++++++++++++++++++++++++++
|
||
2 files changed, 78 insertions(+), 14 deletions(-)
|
||
|
||
## Diff
|
||
internal/provider/chain.go --- 1/2 --- Go
|
||
205 } 205 }
|
||
206 return nil, fmt.Errorf("%w 206 return nil, fmt.Errorf("%w
|
||
... (Stufe %s spricht nur direkt)", Err ... (Stufe %s spricht nur direkt)", Err
|
||
... NoByteOutput, r.directName) ... NoByteOutput, r.directName)
|
||
207 } 207 }
|
||
208 return nil, fmt.Errorf("kette: 208 return nil, r.noTierErr()
|
||
... keine Stufe konnte sprechen (nicht ...
|
||
... verfügbar: %s; fehlgeschlagen: %s)" ...
|
||
... , ...
|
||
209 joinOr(r.skipped, "keine"), ...
|
||
... joinOr(r.failed, "keine")) ...
|
||
210 } 209 }
|
||
211 210
|
||
212 // SpeakDirect implementiert Direct 211 // SpeakDirect implementiert Direct
|
||
... Speaker: Direktansage über die Stuf ... Speaker: Direktansage über die Stuf
|
||
... e, ... e,
|
||
|
||
internal/provider/chain.go --- 2/2 --- Go
|
||
225 joinOr(r.skipped, "keine"), 224 joinOr(r.skipped, "keine"),
|
||
... joinOr(r.failed, "keine")) ... joinOr(r.failed, "keine"))
|
||
226 } 225 }
|
||
227 226
|
||
228 // Speak ist die komplette Ansage: 227 // Speak ist die komplette Ansage i
|
||
... Synthese mit Fallback → Wiedergabe ... n EINEM Ketten-Durchlauf: Synthese
|
||
... über ... mit
|
||
229 // den Player; fällt die Kette auf 228 // Fallback → Wiedergabe über den P
|
||
... die Direkt-Stufe zurück, entscheide ... layer; fällt die Kette auf die Dire
|
||
... t der ... kt-
|
||
230 // Player-Name über Ton (alles auße 229 // Stufe zurück, entscheidet der Pl
|
||
... r „null") oder tonloses Verwerfen ( ... ayer-Name über Ton (alles außer „nu
|
||
... null: ... ll")
|
||
231 // CI/Server bleiben fehler- und to 230 // oder tonloses Verwerfen (null: C
|
||
... nfrei, der Verwurf wird protokollie ... I/Server bleiben fehler- und tonfre
|
||
... rt). ... i,
|
||
... 231 // der Verwurf wird protokolliert).
|
||
... ... Der Durchlauf findet bewusst genau
|
||
... 232 // einmal statt: fehlgeschlagene Cl
|
||
... ... oud-Stufen werden nicht ein zweites
|
||
... ... Mal
|
||
... 233 // angefragt (keine doppelten POSTs
|
||
... ... auf bezahlten APIs, keine doppelte
|
||
... ... n
|
||
... 234 // Protokoll-Zeilen), und Bytes aus
|
||
... ... diesem Durchlauf werden abgespielt
|
||
... ... ,
|
||
... 235 // statt sie wegzuwerfen und neu zu
|
||
... ... laufen. Nur ein Fehler aller Stufe
|
||
... ... n
|
||
232 // Nur ein Fehler aller Stufen oder 236 // oder eine fehlgeschlagene Wieder
|
||
... eine fehlgeschlagene Wiedergabe si ... gabe sind Fehler.
|
||
... nd ...
|
||
233 // Fehler. ...
|
||
234 func (c *Chain) Speak(ctx context.C 237 func (c *Chain) Speak(ctx context.C
|
||
... ontext, text, lang string) error { ... ontext, text, lang string) error {
|
||
235 data, err := c.Synthesize(ctx, 238 r := c.walk(ctx, text, lang)
|
||
... text, lang) ...
|
||
236 switch { 239 switch {
|
||
237 case err == nil: 240 case r.data != nil:
|
||
238 if c.cfg.Player == nil { 241 if c.cfg.Player == nil {
|
||
239 return fmt.Errorf("kett 242 return fmt.Errorf("kett
|
||
... e: kein Player konfiguriert — Bytes ... e: kein Player konfiguriert — Bytes
|
||
... können nicht abgespielt werden") ... können nicht abgespielt werden")
|
||
240 } 243 }
|
||
241 if err := c.cfg.Player.Play 244 if err := c.cfg.Player.Play
|
||
... (ctx, data); err != nil { ... (ctx, r.data); err != nil {
|
||
242 return fmt.Errorf("kett 245 return fmt.Errorf("kett
|
||
... e: Wiedergabe über %s fehlgeschlage ... e: Wiedergabe über %s fehlgeschlage
|
||
... n: %w", c.cfg.Player.Name(), err) ... n: %w", c.cfg.Player.Name(), err)
|
||
243 } 246 }
|
||
244 return nil 247 return nil
|
||
245 case errors.Is(err, ErrNoByteOu 248 case r.direct != nil:
|
||
... tput): ...
|
||
246 if c.cfg.Player != nil && c 249 if c.cfg.Player != nil && c
|
||
... .cfg.Player.Name() == NullPlayerNam ... .cfg.Player.Name() == NullPlayerNam
|
||
... e { ... e {
|
||
247 chainLogf(c.cfg, "kette 250 chainLogf(c.cfg, "kette
|
||
... : Direktansage verworfen (audio.sin ... : Direktansage verworfen (audio.sin
|
||
... k=null)\n") ... k=null)\n")
|
||
248 return nil 251 return nil
|
||
249 } 252 }
|
||
250 return c.SpeakDirect(ctx, t 253 return r.direct.SpeakDirect
|
||
... ext, lang) ... (ctx, text, lang)
|
||
251 default: 254 default:
|
||
252 return err 255 return r.noTierErr()
|
||
253 } 256 }
|
||
254 } 257 }
|
||
... 258
|
||
... 259 // noTierErr bündelt den Ende-ohne-
|
||
... ... Ergebnis-Fehler eines Durchlaufs: k
|
||
... ... eine
|
||
... 260 // Stufe lieferte Bytes oder Direkt
|
||
... ... ansage — übersprungene (nicht verfü
|
||
... ... gbar)
|
||
... 261 // und fehlgeschlagene Stufen werde
|
||
... ... n genannt, nie still.
|
||
... 262 func (r walkResult) noTierErr() err
|
||
... ... or {
|
||
... 263 return fmt.Errorf("kette: keine
|
||
... ... Stufe konnte sprechen (nicht verfü
|
||
... ... gbar: %s; fehlgeschlagen: %s)",
|
||
... 264 joinOr(r.skipped, "keine"),
|
||
... ... joinOr(r.failed, "keine"))
|
||
... 265 }
|
||
255 266
|
||
256 // chainLogf schreibt eine Protokol 267 // chainLogf schreibt eine Protokol
|
||
... l-Zeile, wenn ein Log gesetzt ist. ... l-Zeile, wenn ein Log gesetzt ist.
|
||
257 func chainLogf(cfg ChainConfig, for 268 func chainLogf(cfg ChainConfig, for
|
||
... mat string, args ...any) { ... mat string, args ...any) {
|
||
|
||
internal/provider/chain_test.go --- Go
|
||
341 341 }
|
||
342 342 }
|
||
343
|
||
344 // TestChainDirektEndgameEinDurchlauf (Review-Fix): Im DirectSpeaker-
|
||
345 // Endgame läuft die Kette genau EINMAL — die fehlgeschlagene Cloud-Stufe
|
||
346 // wird nur einmal angefragt (keine doppelten POSTs auf bezahlten APIs,
|
||
347 // keine verdoppelte Latenz), jede Stufe protokolliert ihren Fehlschlag
|
||
348 // genau einmal, und die Direktansage kommt zustande.
|
||
349 func TestChainDirektEndgameEinDurchlauf(t *testing.T) {
|
||
350 eleven, elHits := countingTTSServer(t, 500, []byte("boom"))
|
||
351 // elevenlabs: Key + Fake (500). openai: Endpunkt, aber KEIN Key →
|
||
352 // übersprungen. edge: toter Loopback-Port. osbuiltin: PATH nur mit
|
||
353 // fake spd-say (kein espeak) → keine Bytes, Direktansage.
|
||
354 dir := t.TempDir()
|
||
355 seen := filepath.Join(dir, "seen.txt")
|
||
356 fakeProcBin(t, dir, "spd-say", `printf '%s\n' "$*" > "`+seen+`"`)
|
||
357 t.Setenv("PATH", dir)
|
||
358
|
||
359 var log bytes.Buffer
|
||
360 ch, err := NewChain(ChainConfig{
|
||
361 Getenv: chainEnv(map[string]string{
|
||
362 EnvElevenLabsKey: "k",
|
||
363 EnvElevenLabsEndpoint: eleven,
|
||
364 EnvOpenAIEndpoint: eleven, // ohne Key ohnehin übersprungen
|
||
365 EnvEdgeEndpoint: "ws://127.0.0.1:1/",
|
||
366 }),
|
||
367 Player: &fakePlayer{name: "oto"},
|
||
368 Log: &log,
|
||
369 })
|
||
370 if err != nil {
|
||
371 t.Fatalf("NewChain: %v", err)
|
||
372 }
|
||
373
|
||
374 if err := ch.Speak(context.Background(), "Hallo Welt", "de"); err != nil {
|
||
375 t.Fatalf("Speak (Direkt-Endgame): %v", err)
|
||
376 }
|
||
377 if got := elHits.Load(); got != 1 {
|
||
378 t.Errorf("elevenlabs wurde %d× angefragt, want genau 1 (kein zweiter Durchlauf)", got)
|
||
379 }
|
||
380 for _, tier := range []string{"elevenlabs", "edge"} {
|
||
381 if got := strings.Count(log.String(), "Stufe "+tier+" fehlgeschlagen"); got != 1 {
|
||
382 t.Errorf("„Stufe %s fehlgeschlagen“ steht %d× im Log, want genau 1:\n%s", tier, got, log.String())
|
||
383 }
|
||
384 }
|
||
385 if got := strings.Count(log.String(), "Stufe openai übersprungen"); got != 1 {
|
||
386 t.Errorf("openai-Skip steht %d× im Log, want genau 1", got)
|
||
387 }
|
||
388 args, err := os.ReadFile(seen)
|
||
389 if err != nil {
|
||
390 t.Fatalf("fake spd-say nicht gerufen: %v", err)
|
||
391 }
|
||
392 if got := strings.TrimSpace(string(args)); got != "-w -l de Hallo Welt" {
|
||
393 t.Errorf("spd-say ARGS = %q, want %q", got, "-w -l de Hallo Welt")
|
||
394 }
|
||
395 }
|
||
|