#!/usr/bin/env bash # diariz-yt — send a YouTube (or any yt-dlp-supported) link to Diariz for transcription + diarization. # # diariz-yt [--title "Custom title"] [--keep] # # Diariz has no URL ingest of its own (files only), so this fetches the audio and uploads it as a file: # 1. yt-dlp pulls the best audio-only stream, natively (no transcode): YouTube's Opus/WebM, else M4A. Both are # formats Diariz accepts (its upload sniffs the bytes: wav/mp3/flac/ogg/webm, and m4a with UPLOAD_ALLOW_AAC). # Anything else is converted to Ogg/Opus with ffmpeg. # 2. ffprobe reads the duration (Diariz wants durationMs). # 3. POST /api/recordings (multipart, source=Upload) with the vaulted personal API token `diariz/api-token`. The # recording lands in Prime's personal room, and Diariz transcribes, diarizes and summarizes it on its own. # # Runs on nh3-dev: NH3's egress is residential, which YouTube does not bot-flag the way it flags colo IPs. Set # DIARIZ_YT_PROXY (e.g. socks5h://100.64.0.1:1080, the nh3-scale residential proxy) to route yt-dlp through a proxy # from anywhere else. Provenance is kept in the title: " — <channel> [YouTube <id>]". set -euo pipefail BASE=${DIARIZ_URL:-https://diariz.nh3.phasefinal.com} usage() { sed -n '2,4p' "$0" | sed 's/^# \{0,1\}//'; exit 2; } url="" title="" keep=0 while [ $# -gt 0 ]; do case $1 in --title) title=${2:?--title needs a value}; shift 2 ;; --keep) keep=1; shift ;; -h|--help) usage ;; -*) echo "unknown option: $1" >&2; usage ;; *) [ -z "$url" ] || { echo "one URL at a time" >&2; exit 2; }; url=$1; shift ;; esac done [ -n "$url" ] || usage for t in yt-dlp ffprobe ffmpeg curl python3 secret; do command -v "$t" >/dev/null || { echo "missing tool: $t" >&2; exit 3; }; done work=$(mktemp -d "${TMPDIR:-/tmp}/diariz-yt.XXXXXX") [ "$keep" = 1 ] || trap 'rm -rf "$work"' EXIT proxy=(); [ -n "${DIARIZ_YT_PROXY:-}" ] && proxy=(--proxy "$DIARIZ_YT_PROXY") echo "fetching audio: $url" >&2 yt-dlp "${proxy[@]}" --no-playlist --no-progress -q \ -f 'bestaudio[ext=webm]/bestaudio[ext=m4a]/bestaudio' \ -o "$work/%(id)s.%(ext)s" --write-info-json "$url" info=$(ls "$work"/*.info.json 2>/dev/null | head -1); [ -n "$info" ] || { echo "yt-dlp wrote no metadata" >&2; exit 4; } audio=$(ls "$work"/* | grep -v '\.info\.json$' | head -1); [ -s "$audio" ] || { echo "yt-dlp produced no audio" >&2; exit 4; } case ${audio##*.} in webm|m4a|ogg|opus|mp3|wav|flac) ;; *) echo "converting ${audio##*.} to Ogg/Opus" >&2 ffmpeg -hide_banner -loglevel error -i "$audio" -vn -c:a libopus -b:a 96k "$work/audio.ogg" && audio=$work/audio.ogg ;; esac # A bare .opus is Ogg inside; rename so the multipart filename matches what Diariz sniffs. [ "${audio##*.}" = opus ] && { mv "$audio" "${audio%.opus}.ogg"; audio=${audio%.opus}.ogg; } secs=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$audio") dur_ms=$(python3 -c "import sys; print(int(float(sys.argv[1]) * 1000))" "$secs") if [ -z "$title" ]; then title=$(python3 -c " import json, sys d = json.load(open(sys.argv[1])) t = (d.get('title') or 'Untitled').strip(); ch = (d.get('channel') or d.get('uploader') or '').strip() src = d.get('extractor_key') or 'web'; vid = d.get('id') or '' print((t + (' — ' + ch if ch else '') + f' [{src} {vid}]')[:250])" "$info") fi bytes=$(stat -c %s "$audio") echo "uploading \"$title\" ($(python3 -c "print(round($secs/60, 1))") min, $((bytes / 1024 / 1024)) MiB)" >&2 token=$(secret get diariz/api-token) resp=$(curl -sS --fail-with-body -m 900 -X POST "$BASE/api/recordings" \ -H @<(printf 'Authorization: Bearer %s\n' "$token") \ -F "title=$title" -F "durationMs=$dur_ms" -F "source=Upload" \ -F "audio=@$audio;filename=$(basename "$audio")") || { echo "upload failed: $resp" >&2; exit 5; } unset token python3 -c " import json, sys r = json.loads(sys.argv[1]); print('queued recording', r.get('id'), 'status', r.get('status')) print('open:', sys.argv[2])" "$resp" "$BASE"