음성 재생과 운영 배포 정리
This commit is contained in:
parent
8ed185ce6c
commit
ac7db95542
1020 changed files with 46863 additions and 2175 deletions
|
|
@ -41,6 +41,9 @@ interface VoiceUiProbeState {
|
|||
recorderStops: number;
|
||||
trackStops: number;
|
||||
audioPlays: number;
|
||||
audioContextResumes: number;
|
||||
audioBufferStarts: number;
|
||||
mediaPlayRejections: number;
|
||||
messages: VoiceUiProbeMessage[];
|
||||
closeEvents: number[];
|
||||
}
|
||||
|
|
@ -206,6 +209,7 @@ async function startApi({
|
|||
ENGINE_CONNECT_TIMEOUT: "2",
|
||||
OPENAI_API_KEY: "e2e-fake-key",
|
||||
OPENAI_BASE_URL: `${openAIBaseURL}/v1`,
|
||||
VIGNETTE_VOICE_POC_SAMPLE_TTS: "false",
|
||||
FRONTEND_BASE_URL: frontendBaseURL,
|
||||
CORS_ORIGINS: JSON.stringify([frontendBaseURL]),
|
||||
},
|
||||
|
|
@ -356,8 +360,11 @@ async function probeVoiceCascade(page: Page, apiBaseURL: string, sessionId: stri
|
|||
);
|
||||
}
|
||||
|
||||
async function installSyntheticVoiceCapture(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
async function installSyntheticVoiceCapture(
|
||||
page: Page,
|
||||
options: { blockMediaElementPlayback?: boolean } = {},
|
||||
): Promise<void> {
|
||||
await page.addInitScript((opts) => {
|
||||
type ProbeMessage = {
|
||||
direction: "sent" | "received";
|
||||
kind: "text" | "binary";
|
||||
|
|
@ -370,6 +377,9 @@ async function installSyntheticVoiceCapture(page: Page): Promise<void> {
|
|||
recorderStops: number;
|
||||
trackStops: number;
|
||||
audioPlays: number;
|
||||
audioContextResumes: number;
|
||||
audioBufferStarts: number;
|
||||
mediaPlayRejections: number;
|
||||
messages: ProbeMessage[];
|
||||
closeEvents: number[];
|
||||
};
|
||||
|
|
@ -380,6 +390,9 @@ async function installSyntheticVoiceCapture(page: Page): Promise<void> {
|
|||
recorderStops: 0,
|
||||
trackStops: 0,
|
||||
audioPlays: 0,
|
||||
audioContextResumes: 0,
|
||||
audioBufferStarts: 0,
|
||||
mediaPlayRejections: 0,
|
||||
messages: [],
|
||||
closeEvents: [],
|
||||
};
|
||||
|
|
@ -507,14 +520,93 @@ async function installSyntheticVoiceCapture(page: Page): Promise<void> {
|
|||
value: ProbeWebSocket,
|
||||
});
|
||||
|
||||
class FakeAnalyser {
|
||||
fftSize = 1024;
|
||||
|
||||
connect() {
|
||||
return this;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getFloatTimeDomainData(buf: Float32Array) {
|
||||
for (let i = 0; i < buf.length; i += 1) buf[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeBufferSource {
|
||||
buffer: unknown = null;
|
||||
onended: ((event: Event) => void) | null = null;
|
||||
|
||||
connect() {
|
||||
return this;
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
start() {
|
||||
probe.audioBufferStarts += 1;
|
||||
window.setTimeout(() => {
|
||||
this.onended?.(new Event("ended"));
|
||||
}, 120);
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.onended = null;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeAudioContext {
|
||||
state = "running";
|
||||
destination = {};
|
||||
|
||||
async resume() {
|
||||
probe.audioContextResumes += 1;
|
||||
this.state = "running";
|
||||
}
|
||||
|
||||
async close() {
|
||||
this.state = "closed";
|
||||
}
|
||||
|
||||
async decodeAudioData(_data: ArrayBuffer) {
|
||||
return { duration: 0.12 };
|
||||
}
|
||||
|
||||
createAnalyser() {
|
||||
return new FakeAnalyser();
|
||||
}
|
||||
|
||||
createBufferSource() {
|
||||
return new FakeBufferSource();
|
||||
}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, "AudioContext", {
|
||||
configurable: true,
|
||||
value: FakeAudioContext,
|
||||
});
|
||||
Object.defineProperty(window, "webkitAudioContext", {
|
||||
configurable: true,
|
||||
value: FakeAudioContext,
|
||||
});
|
||||
|
||||
HTMLMediaElement.prototype.play = function patchedPlay() {
|
||||
if (opts.blockMediaElementPlayback) {
|
||||
probe.mediaPlayRejections += 1;
|
||||
return Promise.reject(new DOMException("Synthetic autoplay block", "NotAllowedError"));
|
||||
}
|
||||
probe.audioPlays += 1;
|
||||
window.setTimeout(() => {
|
||||
this.dispatchEvent(new Event("ended"));
|
||||
}, 120);
|
||||
return Promise.resolve();
|
||||
};
|
||||
});
|
||||
}, options);
|
||||
}
|
||||
|
||||
async function readVoiceUiProbe(page: Page): Promise<VoiceUiProbeState> {
|
||||
|
|
@ -562,6 +654,33 @@ test.describe("voice cascade success path", () => {
|
|||
if (!login.ok) {
|
||||
return { ok: false, step: "login", status: login.status, body: loginBody };
|
||||
}
|
||||
const onboarding = await fetch(`${apiBase}/users/me/onboarding`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
legal_name: "Voice Success",
|
||||
affiliation: "한신대학교",
|
||||
department: "상담심리학과",
|
||||
grade_level: "3학년",
|
||||
phone: "010-5555-5555",
|
||||
contact_address: "경기도 오산시 한신대학교",
|
||||
nickname: "Voice Success",
|
||||
self_introduction: "음성 캐스케이드 성공 경로 검증용 사용자입니다.",
|
||||
avatar_url: "",
|
||||
terms_accepted: true,
|
||||
privacy_accepted: true,
|
||||
}),
|
||||
});
|
||||
const onboardingBody = await onboarding.text();
|
||||
if (!onboarding.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
step: "onboarding",
|
||||
status: onboarding.status,
|
||||
body: onboardingBody,
|
||||
};
|
||||
}
|
||||
const me = await fetch(`${apiBase}/auth/me`, { credentials: "include" });
|
||||
const meBody = await me.text();
|
||||
if (!me.ok) {
|
||||
|
|
@ -613,7 +732,6 @@ test.describe("voice cascade success path", () => {
|
|||
expect.objectContaining({ type: "transcript", text: "요즘 잠을 잘 못 자요." }),
|
||||
expect.objectContaining({ type: "reply", text: "괜찮아요. 천천히 말해볼게요." }),
|
||||
expect.objectContaining({ type: "state", state: "speaking" }),
|
||||
expect.objectContaining({ type: "tts_chunk", seq: 0 }),
|
||||
expect.objectContaining({ type: "tts_end" }),
|
||||
expect.objectContaining({ type: "state", state: "idle" }),
|
||||
]),
|
||||
|
|
@ -646,7 +764,7 @@ test.describe("voice cascade success path", () => {
|
|||
}
|
||||
});
|
||||
|
||||
await installSyntheticVoiceCapture(page);
|
||||
await installSyntheticVoiceCapture(page, { blockMediaElementPlayback: true });
|
||||
|
||||
const openai = await startFakeOpenAI();
|
||||
const engine = await startFakeEngine();
|
||||
|
|
@ -703,6 +821,33 @@ test.describe("voice cascade success path", () => {
|
|||
if (!login.ok) {
|
||||
return { ok: false, step: "login", status: login.status, body: loginBody };
|
||||
}
|
||||
const onboarding = await fetch(`${apiBase}/users/me/onboarding`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
legal_name: "Voice UI",
|
||||
affiliation: "한신대학교",
|
||||
department: "상담심리학과",
|
||||
grade_level: "3학년",
|
||||
phone: "010-6666-6666",
|
||||
contact_address: "경기도 오산시 한신대학교",
|
||||
nickname: "Voice UI",
|
||||
self_introduction: "브라우저 음성 UI 검증용 사용자입니다.",
|
||||
avatar_url: "",
|
||||
terms_accepted: true,
|
||||
privacy_accepted: true,
|
||||
}),
|
||||
});
|
||||
const onboardingBody = await onboarding.text();
|
||||
if (!onboarding.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
step: "onboarding",
|
||||
status: onboarding.status,
|
||||
body: onboardingBody,
|
||||
};
|
||||
}
|
||||
const me = await fetch(`${apiBase}/auth/me`, { credentials: "include" });
|
||||
const meBody = await me.text();
|
||||
if (!me.ok) {
|
||||
|
|
@ -794,7 +939,10 @@ test.describe("voice cascade success path", () => {
|
|||
await expect(page.locator(".sx-utt").filter({ hasText: String(reply) })).toBeVisible();
|
||||
|
||||
const probe = await readVoiceUiProbe(page);
|
||||
expect(probe.audioPlays).toBeGreaterThan(0);
|
||||
expect(probe.audioContextResumes).toBeGreaterThan(0);
|
||||
expect(probe.audioBufferStarts).toBeGreaterThan(0);
|
||||
expect(probe.audioPlays).toBe(0);
|
||||
expect(probe.mediaPlayRejections).toBe(0);
|
||||
expect(probe.messages.some((message) => message.direction === "received" && message.kind === "binary")).toBe(
|
||||
true,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue