Implement unified story generation flow

This commit is contained in:
2026-06-18 14:48:27 +08:00
parent 0ccfd00a23
commit 7ebdfb2582
27 changed files with 1323 additions and 215 deletions

View File

@@ -5,13 +5,23 @@ APP_URL="${APP_URL:-http://localhost:52080}"
BACKEND_URL="${BACKEND_URL:-http://localhost:52000}"
ADMIN_BACKEND_URL="${ADMIN_BACKEND_URL:-http://localhost:52800}"
ADMIN_AUTH="${ADMIN_AUTH:-admin:admin}"
DEV_SIGNIN_URL="${DEV_SIGNIN_URL:-$APP_URL/auth/dev/signin}"
SMOKE_AUDIO="${SMOKE_AUDIO:-0}"
SMOKE_VOICE="${SMOKE_VOICE:-0}"
SMOKE_REAL_ASR="${SMOKE_REAL_ASR:-0}"
REAL_ASR_AUDIO_FILE="${REAL_ASR_AUDIO_FILE:-}"
REAL_ASR_EXPECTED_TEXT="${REAL_ASR_EXPECTED_TEXT:-小熊和星星一起找家}"
REAL_ASR_DURATION_MS="${REAL_ASR_DURATION_MS:-2200}"
if [[ "$SMOKE_REAL_ASR" == "1" ]]; then
SMOKE_VOICE=1
fi
COOKIE_JAR="$(mktemp "${TMPDIR:-/tmp}/dreamweaver-cookie.XXXXXX")"
VOICE_SMOKE_AUDIO="$(mktemp "${TMPDIR:-/tmp}/dreamweaver-voice-audio.XXXXXX")"
REAL_ASR_SMOKE_AUDIO="${TMPDIR:-/tmp}/dreamweaver-real-asr-audio.$$.$RANDOM.m4a"
cleanup() {
rm -f "$COOKIE_JAR" "$VOICE_SMOKE_AUDIO"
rm -f "$COOKIE_JAR" "$VOICE_SMOKE_AUDIO" "$REAL_ASR_SMOKE_AUDIO" "$REAL_ASR_SMOKE_AUDIO.caf"
}
trap cleanup EXIT
@@ -57,6 +67,78 @@ assert_jq() {
fi
}
curl_form_capture() {
local body_file="$1"
local status_file="$2"
local url="$3"
shift 3
local http_code
if http_code="$(curl -sS -b "$COOKIE_JAR" -o "$body_file" -w '%{http_code}' "$@" "$url")"; then
printf '%s' "$http_code" > "$status_file"
return 0
fi
printf '%s' "${http_code:-curl_failed}" > "$status_file"
return 1
}
print_json_or_raw() {
local body_file="$1"
if jq '.' "$body_file" >&2 2>/dev/null; then
return 0
fi
cat "$body_file" >&2
}
print_real_asr_diagnostics() {
local session_id="$1"
local body_file="$2"
echo "Real ASR smoke failed." >&2
echo "Required backend env: ASR_PROVIDERS=[\"openai_asr\"] or [\"openai_asr\", \"demo\"], OPENAI_API_KEY, optional OPENAI_API_BASE, VOICE_TRANSCRIPTION_MODEL, VOICE_TRANSCRIPTION_LANGUAGE." >&2
echo "Upload response:" >&2
print_json_or_raw "$body_file"
if [[ -n "$session_id" && "$session_id" != "null" ]]; then
echo "Voice session events:" >&2
if voice_diag_json="$(get_json "$APP_URL/api/voice-sessions/$session_id" 2>/dev/null)"; then
echo "$voice_diag_json" | jq '{id,status,last_error,events:[.events[] | {event_type,status,message,event_metadata}]}' >&2
fi
fi
echo "Admin ASR analytics:" >&2
if admin_asr_json="$(curl -fsS -u "$ADMIN_AUTH" "$ADMIN_BACKEND_URL/admin/providers/analytics?days=7&capability=asr" 2>/dev/null)"; then
echo "$admin_asr_json" | jq '{capability,total_calls,successful_calls,failed_calls,voice_session_count,voice_turn_count,by_provider,failure_reasons}' >&2
fi
echo "If provider rows were changed in Admin, POST /admin/providers/reload and restart the API container/process before rerunning this smoke." >&2
}
ensure_real_asr_audio() {
if [[ -n "$REAL_ASR_AUDIO_FILE" ]]; then
if [[ ! -f "$REAL_ASR_AUDIO_FILE" ]]; then
echo "REAL_ASR_AUDIO_FILE does not exist: $REAL_ASR_AUDIO_FILE" >&2
exit 1
fi
printf '%s\n' "$REAL_ASR_AUDIO_FILE"
return 0
fi
if command -v say >/dev/null 2>&1 && command -v afconvert >/dev/null 2>&1; then
if ! say -v Tingting -o "$REAL_ASR_SMOKE_AUDIO.caf" "$REAL_ASR_EXPECTED_TEXT" 2>/dev/null; then
say -o "$REAL_ASR_SMOKE_AUDIO.caf" "$REAL_ASR_EXPECTED_TEXT"
fi
afconvert -f m4af -d aac "$REAL_ASR_SMOKE_AUDIO.caf" "$REAL_ASR_SMOKE_AUDIO" >/dev/null
rm -f "$REAL_ASR_SMOKE_AUDIO.caf"
printf '%s\n' "$REAL_ASR_SMOKE_AUDIO"
return 0
fi
echo "SMOKE_REAL_ASR=1 requires REAL_ASR_AUDIO_FILE, or macOS say + afconvert to synthesize a short sample." >&2
exit 1
}
wait_for_job_story() {
local job_id="$1"
local attempts="${2:-60}"
@@ -88,7 +170,7 @@ curl -fsS "$BACKEND_URL/health" | jq -e '.status == "ok"' >/dev/null
curl -fsS "$ADMIN_BACKEND_URL/health" | jq -e '.status == "ok"' >/dev/null
say "Logging in with dev auth"
curl -fsS -c "$COOKIE_JAR" -o /dev/null -L "$APP_URL/auth/dev/signin"
curl -fsS -c "$COOKIE_JAR" -o /dev/null -L "$DEV_SIGNIN_URL"
session_json="$(get_json "$APP_URL/auth/session")"
assert_jq "$session_json" '.user.id == "github:dev_user_001"' "dev session should be active"
@@ -211,6 +293,48 @@ if [[ "$SMOKE_VOICE" == "1" ]]; then
voice_waiting_analytics_json="$(get_json "$APP_URL/api/voice-sessions/analytics?days=7&session_status=waiting_user")"
assert_jq "$voice_waiting_analytics_json" '.session_status == "waiting_user" and .total_sessions >= 1' "voice analytics should filter by session status"
if [[ "$SMOKE_REAL_ASR" == "1" ]]; then
say "Submitting voice uploaded turn with real OpenAI ASR"
real_asr_audio_path="$(ensure_real_asr_audio)"
real_asr_body="$(mktemp "${TMPDIR:-/tmp}/dreamweaver-real-asr-body.XXXXXX")"
real_asr_status_file="$(mktemp "${TMPDIR:-/tmp}/dreamweaver-real-asr-status.XXXXXX")"
if ! curl_form_capture "$real_asr_body" "$real_asr_status_file" "$APP_URL/api/voice-sessions/$voice_session_id/turns" \
-F "audio_file=@${real_asr_audio_path};filename=real-asr.m4a;type=audio/mp4" \
-F "duration_ms=${REAL_ASR_DURATION_MS}"; then
print_real_asr_diagnostics "$voice_session_id" "$real_asr_body"
rm -f "$real_asr_body" "$real_asr_status_file"
exit 1
fi
real_asr_status="$(cat "$real_asr_status_file")"
if [[ "$real_asr_status" != "202" ]]; then
echo "Unexpected real ASR upload HTTP status: $real_asr_status" >&2
print_real_asr_diagnostics "$voice_session_id" "$real_asr_body"
rm -f "$real_asr_body" "$real_asr_status_file"
exit 1
fi
real_asr_upload_json="$(cat "$real_asr_body")"
rm -f "$real_asr_body" "$real_asr_status_file"
real_asr_turn_id="$(jq -r '.turn_id' <<<"$real_asr_upload_json")"
assert_jq "$real_asr_upload_json" '.status != "failed" and .transcription_provider == "openai_asr"' "real ASR upload turn should use openai_asr"
real_asr_detail_json="$(get_json "$APP_URL/api/voice-sessions/$voice_session_id/turns/$real_asr_turn_id")"
assert_jq "$real_asr_detail_json" '.transcription_provider == "openai_asr"' "real ASR turn detail should keep openai_asr provider"
assert_jq "$real_asr_detail_json" '.user_transcript != null and (.user_transcript | length) > 0' "real ASR turn should expose a non-empty transcript"
assert_jq "$real_asr_detail_json" '.assistant_text != null and .assistant_text != ""' "real ASR turn should continue the narrative"
echo "$real_asr_detail_json" | jq '{id,status,transcription_provider,user_transcript,detected_intent,requires_confirmation,assistant_audio_ready,assistant_text}'
voice_openai_asr_analytics_json="$(get_json "$APP_URL/api/voice-sessions/analytics?days=7&provider=openai_asr")"
assert_jq "$voice_openai_asr_analytics_json" '.provider == "openai_asr" and .uploaded_audio_turns >= 1 and (.transcription_provider_counts.openai_asr >= 1)' "voice analytics should filter real ASR provider"
admin_asr_analytics_json="$(curl -fsS -u "$ADMIN_AUTH" "$ADMIN_BACKEND_URL/admin/providers/analytics?days=7&capability=asr")"
assert_jq "$admin_asr_analytics_json" '.capability == "asr" and .successful_calls >= 1 and ([.by_provider[].adapter] | index("openai_asr")) != null' "admin ASR analytics should include openai_asr"
echo "$admin_asr_analytics_json" | jq '{capability,total_calls,successful_calls,failed_calls,voice_session_count,voice_turn_count,by_provider,failure_reasons}'
else
say "Skipping real ASR smoke; set SMOKE_REAL_ASR=1 with backend OPENAI_API_KEY and ASR_PROVIDERS=[\"openai_asr\", \"demo\"]"
fi
say "Finalizing voice session into story"
voice_finalize_json="$(post_json "$APP_URL/api/voice-sessions/$voice_session_id/finalize" '{
"save_story": true,