ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
"""Fish Audio S1 zero-shot cloning execution layer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from voice_library import DEFAULT_MANIFEST, load_voice_library, profile_key
|
||||
|
||||
MODEL = "s1"
|
||||
|
||||
|
||||
def _session():
|
||||
from fish_audio_sdk import Session
|
||||
|
||||
key = os.getenv("FISH_API_KEY")
|
||||
if not key:
|
||||
raise RuntimeError("Fish S1 synthesis requires FISH_API_KEY")
|
||||
return Session(key)
|
||||
|
||||
|
||||
def synth_speech(
|
||||
text: str,
|
||||
emotion: str,
|
||||
speed: str,
|
||||
style: str,
|
||||
emphasis: bool,
|
||||
out_path: str | Path,
|
||||
*,
|
||||
voice_library: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""Clone from the selected real reference clip using Fish S1."""
|
||||
from fish_audio_sdk import Prosody, ReferenceAudio, TTSRequest
|
||||
|
||||
key = profile_key(emotion, speed, style)
|
||||
profile = voice_library["profiles"][key]
|
||||
reference_path = Path(profile["absolute_path"])
|
||||
# S1 supports native parentheses markers, including real non-verbal sounds.
|
||||
fish_text = f"(emphasis){text}" if emphasis else text
|
||||
request = TTSRequest(
|
||||
text=fish_text,
|
||||
references=[ReferenceAudio(audio=reference_path.read_bytes(), text=profile["transcript"])],
|
||||
format="mp3",
|
||||
prosody=Prosody(speed=1.0, volume=0),
|
||||
)
|
||||
Path(out_path).write_bytes(b"".join(_session().tts(request, backend=MODEL)))
|
||||
return {
|
||||
"model": MODEL,
|
||||
"provider": "Fish Audio",
|
||||
"profile": key,
|
||||
"reference_path": reference_path.name,
|
||||
"reference_sha256": profile["sha256"],
|
||||
"fish_text": fish_text,
|
||||
}
|
||||
|
||||
|
||||
def synth_direct_reference(text: str, reference_id: str, out_path: str | Path) -> dict[str, Any]:
|
||||
"""Fish S1 without the 24-clip control library (configuration A)."""
|
||||
from fish_audio_sdk import TTSRequest
|
||||
|
||||
request = TTSRequest(text=text, reference_id=reference_id, format="mp3")
|
||||
Path(out_path).write_bytes(b"".join(_session().tts(request, backend=MODEL)))
|
||||
return {"provider": "Fish Audio", "model": MODEL, "reference_id": reference_id}
|
||||
|
||||
|
||||
def make_silence(ms: int, out_path: str | Path) -> None:
|
||||
subprocess.run(
|
||||
["ffmpeg", "-y", "-loglevel", "error", "-f", "lavfi", "-i", "anullsrc=r=44100:cl=mono",
|
||||
"-t", f"{ms / 1000:.3f}", "-q:a", "9", str(out_path)],
|
||||
check=True,
|
||||
)
|
||||
|
||||
|
||||
def concat_mp3(parts: list[Path], out_path: str | Path) -> None:
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as handle:
|
||||
for part in parts:
|
||||
escaped = part.resolve().as_posix().replace("'", "'\\''")
|
||||
handle.write(f"file '{escaped}'\n")
|
||||
list_path = handle.name
|
||||
try:
|
||||
subprocess.run(
|
||||
["ffmpeg", "-y", "-loglevel", "error", "-f", "concat", "-safe", "0", "-i", list_path,
|
||||
"-ar", "44100", "-ac", "1", "-b:a", "128k", str(out_path)],
|
||||
check=True,
|
||||
)
|
||||
finally:
|
||||
os.unlink(list_path)
|
||||
|
||||
|
||||
def synthesize_segments(
|
||||
segments,
|
||||
out_path,
|
||||
workdir,
|
||||
*,
|
||||
manifest_path: str | Path = DEFAULT_MANIFEST,
|
||||
):
|
||||
if not segments:
|
||||
raise ValueError("No speech segments to synthesize")
|
||||
library = load_voice_library(manifest_path)
|
||||
workdir = Path(workdir)
|
||||
workdir.mkdir(parents=True, exist_ok=True)
|
||||
parts, info = [], []
|
||||
for index, segment in enumerate(segments):
|
||||
path = workdir / f"segment_{index:02d}.mp3"
|
||||
if segment["type"] == "silence":
|
||||
make_silence(segment["ms"], path)
|
||||
meta = {"type": "silence", "ms": segment["ms"]}
|
||||
else:
|
||||
meta = synth_speech(
|
||||
segment["text"], segment["emotion"], segment["speed"], segment["style"],
|
||||
segment.get("emphasis", False), path, voice_library=library,
|
||||
)
|
||||
meta.update(type="speech", text=segment["text"])
|
||||
parts.append(path)
|
||||
info.append(meta)
|
||||
Path(out_path).parent.mkdir(parents=True, exist_ok=True)
|
||||
if len(parts) == 1:
|
||||
Path(out_path).write_bytes(parts[0].read_bytes())
|
||||
else:
|
||||
concat_mp3(parts, out_path)
|
||||
return info
|
||||
Reference in New Issue
Block a user