Capstone consumer interface composing every piece shipped today: fetch_audio.sh → audio_to_manifest → pick_context.sh → emotional_speech_n.sh (N-seed picker, decoder route) Caches fetch + manifest by URL hash so re-runs with the same --workdir skip the slow steps. Defaults to the Phase 9 recipe: target=decoder, scale=1.0, layers [2,3], cfg=linear:3.0:1.0:25, 5-seed roll with the lowest-WER winner picked. End-to-end smoke test (cached Carlini source, prompt "Today I want to share..."): picker auto-selected: nicholas_carlini...spk0.0078.wav (10.78 s) seed 42 (winner): cos 0.974, WER 0.143 ⭐ "But Jason, today I want to share something with you that I h" seed 100: cos 0.986, WER 0.286 "It ties upon a share something with you that I have been thi" seed 7: cos 0.862, WER 1.000 "Let me think, let him out." The auto-picker chose spk0 (Carlini himself) where manual selection earlier in the day grabbed spk1 (the announcer) — so the automated pipeline is also a slight context-selection improvement. Three sub-second-WER results recorded over the day: - WER 0.071 Amini imperative prompt (manual) - WER 0.125 Amini original prompt (manual) - WER 0.143 Carlini auto-picked spk0 (this commit, end-to-end) Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
155 lines
5.0 KiB
Bash
Executable File
155 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# End-to-end: URL + target text + target emotion → emotional voice clone.
|
|
#
|
|
# Pipeline:
|
|
# 1. fetch_audio.sh (yt-dlp + ffmpeg → 24 kHz WAV)
|
|
# 2. examples/audio_to_manifest (diarize + transcribe → manifest.jsonl)
|
|
# 3. pick_context.sh (rank clips by suitability)
|
|
# 4. emotional_speech_n.sh (N-seed picker over the decoder route)
|
|
#
|
|
# Recipe defaults (Phase 9 findings):
|
|
# - target=decoder, scale=1.0, layers [2,3]
|
|
# - cfg_schedule=linear:3.0:1.0:25
|
|
# - 5-seed roll, lowest-WER picked
|
|
#
|
|
# All intermediate state caches by URL hash, so re-running with the same
|
|
# --workdir reuses fetch + manifest. Emotional generation is the only
|
|
# non-cached step (it depends on text + emotion + seeds, not URL).
|
|
#
|
|
# Usage:
|
|
# scripts/emotional_clone.sh \
|
|
# --url "https://www.youtube.com/watch?v=..." \
|
|
# --text "What you want said." \
|
|
# --emotion happy \
|
|
# --steering-dir /tmp/ravdess_dec_steering \
|
|
# --out /tmp/cloned_emotional.wav \
|
|
# [--workdir /tmp/voice_clone] # default: /tmp/voice_clone_<ts>
|
|
# [--seeds 7,42,100,123,256]
|
|
# [--target decoder] # or backbone
|
|
# [--wavlm-sv /tmp/wavlm_sv.safetensors]
|
|
|
|
set -euo pipefail
|
|
|
|
URL=""
|
|
TEXT=""
|
|
EMOTION=""
|
|
STEERING_DIR=""
|
|
OUT=""
|
|
WORKDIR=""
|
|
SEEDS="7,42,100,123,256"
|
|
TARGET="decoder"
|
|
WAVLM_SV="/tmp/wavlm_sv.safetensors"
|
|
KEEP_ALL=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--url) URL="$2"; shift 2 ;;
|
|
--text) TEXT="$2"; shift 2 ;;
|
|
--emotion) EMOTION="$2"; shift 2 ;;
|
|
--steering-dir) STEERING_DIR="$2"; shift 2 ;;
|
|
--out) OUT="$2"; shift 2 ;;
|
|
--workdir) WORKDIR="$2"; shift 2 ;;
|
|
--seeds) SEEDS="$2"; shift 2 ;;
|
|
--target) TARGET="$2"; shift 2 ;;
|
|
--wavlm-sv) WAVLM_SV="$2"; shift 2 ;;
|
|
--keep-all) KEEP_ALL=true; shift 1 ;;
|
|
-h|--help) sed -n '2,32p' "$0"; exit 0 ;;
|
|
*) echo "unknown arg: $1" >&2; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
for v in URL TEXT EMOTION STEERING_DIR OUT; do
|
|
if [[ -z "${!v}" ]]; then
|
|
echo "error: --${v,,} is unset" >&2
|
|
echo "usage: $0 --url U --text T --emotion E --steering-dir D --out O" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
if [[ ! -f "$WAVLM_SV" ]]; then
|
|
echo "error: wavlm-sv weights not found at $WAVLM_SV" >&2
|
|
exit 1
|
|
fi
|
|
if [[ ! -d "$STEERING_DIR" ]]; then
|
|
echo "error: steering dir not found: $STEERING_DIR" >&2
|
|
exit 1
|
|
fi
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
WORKSPACE_DIR="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
|
A2M_BIN="$WORKSPACE_DIR/target/release/examples/audio_to_manifest"
|
|
N_SCRIPT="$SCRIPT_DIR/emotional_speech_n.sh"
|
|
for f in "$A2M_BIN" "$N_SCRIPT"; do
|
|
if [[ ! -x "$f" ]]; then
|
|
echo "error: missing $f" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
if [[ -z "$WORKDIR" ]]; then
|
|
WORKDIR="/tmp/emotional_clone_$(date +%s)"
|
|
fi
|
|
mkdir -p "$WORKDIR"
|
|
echo "→ workdir: $WORKDIR"
|
|
|
|
URL_HASH="$(echo -n "$URL" | shasum | awk '{print $1}' | head -c 12)"
|
|
|
|
# 1. Fetch audio (cached by URL hash).
|
|
FETCH_DIR="$WORKDIR/fetch_$URL_HASH"
|
|
if [[ ! -d "$FETCH_DIR" ]] || ! ls "$FETCH_DIR"/*.wav >/dev/null 2>&1; then
|
|
mkdir -p "$FETCH_DIR"
|
|
echo "→ step 1/4: fetching audio"
|
|
"$SCRIPT_DIR/fetch_audio.sh" "$URL" "$FETCH_DIR"
|
|
else
|
|
echo "→ step 1/4: reusing cached fetch in $FETCH_DIR"
|
|
fi
|
|
SRC_WAV="$(ls "$FETCH_DIR"/*.wav | head -n1)"
|
|
echo " source: $(basename "$SRC_WAV")"
|
|
|
|
# 2. Build manifest (cached).
|
|
MANIFEST_DIR="$WORKDIR/manifest_$URL_HASH"
|
|
MANIFEST="$MANIFEST_DIR/manifest.jsonl"
|
|
if [[ ! -f "$MANIFEST" ]]; then
|
|
echo "→ step 2/4: diarize + transcribe → manifest"
|
|
"$A2M_BIN" \
|
|
--in "$SRC_WAV" \
|
|
--out-dir "$MANIFEST_DIR" \
|
|
--wavlm-sv-weights "$WAVLM_SV"
|
|
else
|
|
echo "→ step 2/4: reusing manifest at $MANIFEST"
|
|
fi
|
|
|
|
# 3. Rank + pick top-scoring context clip.
|
|
echo "→ step 3/4: ranking context clips"
|
|
RANKED="$WORKDIR/ranked_$URL_HASH.tsv"
|
|
"$SCRIPT_DIR/pick_context.sh" "$MANIFEST" 999 2>/dev/null > "$RANKED"
|
|
PICKED="$(tail -n +2 "$RANKED" | sort -t $'\t' -k2,2gr | awk 'NR==1')"
|
|
if [[ -z "$PICKED" ]]; then
|
|
echo "no scorable clips in manifest — diarization may have failed." >&2
|
|
exit 2
|
|
fi
|
|
CTX_WAV="$(echo "$PICKED" | awk -F'\t' '{print $6}')"
|
|
CTX_TEXT="$(echo "$PICKED" | awk -F'\t' '{print $7}' | sed 's/^← *//; s/^[[:space:]]*//')"
|
|
PICKED_SCORE="$(echo "$PICKED" | awk -F'\t' '{print $2}')"
|
|
PICKED_DUR="$(echo "$PICKED" | awk -F'\t' '{print $3}')"
|
|
echo " picked (score=$PICKED_SCORE, dur=${PICKED_DUR}s): $(basename "$CTX_WAV")"
|
|
|
|
# 4. Roll N seeds through emotional_speech_n.sh; copy the winner to --out.
|
|
echo "→ step 4/4: rolling $(echo "$SEEDS" | tr ',' '\n' | wc -l | tr -d ' ') seeds for emotion='$EMOTION' on target=$TARGET"
|
|
EXTRA=()
|
|
[[ "$KEEP_ALL" == true ]] && EXTRA+=("--keep-all")
|
|
"$N_SCRIPT" \
|
|
--text "$TEXT" \
|
|
--context-wav "$CTX_WAV" \
|
|
--context-text "$CTX_TEXT" \
|
|
--emotion "$EMOTION" \
|
|
--steering-dir "$STEERING_DIR" \
|
|
--target "$TARGET" \
|
|
--seeds "$SEEDS" \
|
|
--wavlm-sv "$WAVLM_SV" \
|
|
--out "$OUT" \
|
|
${EXTRA[@]+"${EXTRA[@]}"}
|
|
|
|
echo
|
|
echo "✓ wrote $OUT"
|