chore(sdd): handoff snapshot — ledger, briefs, reports, review packages + HANDOFF.md for cross-machine continuation
This commit is contained in:
1 parent
acfba19fc2
commit
b934571b97
25 files changed
+17590
No files matched your search
@@ -0,0 +1,178 @@
|
||||
# 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 }
|
||||
|
||||
Reference in new issue
Block a user