feat: complete release preparation, 10+ ad mediation, CI/CD, and docker deployment
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
Some checks failed
CI Pipeline / Code Quality & Typecheck (push) Waiting to run
CI Pipeline / Test Suite (macos-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (ubuntu-latest) (push) Blocked by required conditions
CI Pipeline / Test Suite (windows-latest) (push) Blocked by required conditions
CI Pipeline / Build Validation (admin) (push) Blocked by required conditions
CI Pipeline / Build Validation (desktop) (push) Blocked by required conditions
Deploy Landing Page / deploy (push) Blocked by required conditions
Deploy Landing Page / build (push) Waiting to run
Release & Packaging Pipeline / Build & Publish Admin Docker Image (push) Failing after 8s
Release & Code Signing CA Pipeline / build-and-sign-windows (push) Failing after 1m51s
Build macOS / Build & Package (macOS) (push) Failing after 4s
Build macOS / Build & Package (macOS)-1 (push) Failing after 5s
Release & Code Signing CA Pipeline / build-and-sign-macos (push) Failing after 3s
Release & Packaging Pipeline / Package macOS Desktop App (push) Failing after 4s
Release & Packaging Pipeline / Package Windows Desktop App (push) Failing after 2m28s
Release & Packaging Pipeline / Publish Official GitHub Release (push) Has been skipped
This commit is contained in:
parent
5cd1de6859
commit
708e20f747
406 changed files with 42464 additions and 6199 deletions
108
packages/api-client/__tests__/transcribe.test.ts
Normal file
108
packages/api-client/__tests__/transcribe.test.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// packages/api-client/__tests__/transcribe.test.ts
|
||||
// transcribeAudio 함수 단위 및 통합 인터페이스 테스트
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { transcribeAudio } from '../src/transcribe'
|
||||
|
||||
describe('transcribeAudio', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('Blob 입력 시 multipart/form-data로 /api/stt/transcribe를 호출하여 전사 결과를 반환한다', async () => {
|
||||
const mockAudioBlob = new Blob(['mock-audio-content'], { type: 'audio/webm' })
|
||||
const mockResponse = {
|
||||
text: '안녕하세요, 클라우드 음성 전사 테스트입니다.',
|
||||
confidence: 0.99,
|
||||
language: 'ko',
|
||||
durationSeconds: 2.5,
|
||||
provider: 'groq',
|
||||
modelId: 'whisper-large-v3-turbo',
|
||||
latencyMs: 145,
|
||||
cost: 0.000021,
|
||||
}
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => mockResponse,
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const result = await transcribeAudio({
|
||||
audio: mockAudioBlob,
|
||||
language: 'ko',
|
||||
apiBaseUrl: 'http://localhost:5000',
|
||||
token: 'test-token',
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const [url, options] = fetchMock.mock.calls[0]
|
||||
expect(url).toBe('http://localhost:5000/api/stt/transcribe')
|
||||
expect(options.method).toBe('POST')
|
||||
expect(options.headers['Authorization']).toBe('Bearer test-token')
|
||||
expect(options.body).toBeInstanceOf(FormData)
|
||||
|
||||
expect(result.text).toBe('안녕하세요, 클라우드 음성 전사 테스트입니다.')
|
||||
expect(result.provider).toBe('groq')
|
||||
expect(result.durationSeconds).toBe(2.5)
|
||||
expect(result.latencyMs).toBe(145)
|
||||
})
|
||||
|
||||
it('Base64 문자열 입력 시 JSON 페이로드로 전송한다', async () => {
|
||||
const mockBase64 = 'UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA='
|
||||
const mockResponse = {
|
||||
text: 'Base64 전사 성공',
|
||||
confidence: 0.98,
|
||||
language: 'ko',
|
||||
durationSeconds: 1.0,
|
||||
provider: 'openai',
|
||||
modelId: 'whisper-1',
|
||||
latencyMs: 320,
|
||||
cost: 0.0001,
|
||||
}
|
||||
|
||||
const fetchMock = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => mockResponse,
|
||||
})
|
||||
vi.stubGlobal('fetch', fetchMock)
|
||||
|
||||
const result = await transcribeAudio({
|
||||
audio: mockBase64,
|
||||
language: 'ko',
|
||||
prompt: '의료 용어 가이드',
|
||||
apiBaseUrl: 'http://localhost:5000',
|
||||
})
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||
const [url, options] = fetchMock.mock.calls[0]
|
||||
expect(url).toBe('http://localhost:5000/api/stt/transcribe')
|
||||
expect(options.headers['Content-Type']).toBe('application/json')
|
||||
const body = JSON.parse(options.body)
|
||||
expect(body.audioBase64).toBe(mockBase64)
|
||||
expect(body.language).toBe('ko')
|
||||
expect(body.initialPrompt).toBe('의료 용어 가이드')
|
||||
|
||||
expect(result.text).toBe('Base64 전사 성공')
|
||||
})
|
||||
|
||||
it('서버 응답이 4xx/5xx 실패 시 에러를 throw한다', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
text: async () => 'Internal STT Proxy Failure',
|
||||
})
|
||||
)
|
||||
|
||||
await expect(
|
||||
transcribeAudio({
|
||||
audio: new Blob(['data']),
|
||||
apiBaseUrl: 'http://localhost:5000',
|
||||
})
|
||||
).rejects.toThrow('STT transcription failed (500): Internal STT Proxy Failure')
|
||||
})
|
||||
})
|
||||
|
|
@ -15,5 +15,6 @@ export * from './auth'
|
|||
export * from './meetings'
|
||||
export * from './history'
|
||||
export * from './usage'
|
||||
export * from './transcribe'
|
||||
export * from './supabase-browser'
|
||||
export * from './supabase-server'
|
||||
|
|
|
|||
91
packages/api-client/src/transcribe.ts
Normal file
91
packages/api-client/src/transcribe.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
// packages/api-client/src/transcribe.ts
|
||||
// D3RO Cloud STT Transcription Unified Client
|
||||
|
||||
export interface TranscribeAudioParams {
|
||||
audio: Blob | ArrayBuffer | Uint8Array | string // Blob, Binary, or Base64 string
|
||||
language?: string
|
||||
prompt?: string
|
||||
model?: string
|
||||
provider?: string
|
||||
apiBaseUrl?: string
|
||||
token?: string
|
||||
}
|
||||
|
||||
export interface TranscribeAudioResult {
|
||||
text: string
|
||||
confidence: number
|
||||
language: string
|
||||
durationSeconds: number
|
||||
provider: string
|
||||
modelId: string
|
||||
latencyMs: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 전사(STT) API를 호출합니다.
|
||||
* 사용자는 클라우드 서비스를 통해 자연스럽게 음성을 전사할 수 있으며,
|
||||
* 관리자에서 설정된 기본 프로바이더(Groq, OpenAI, Deepgram, Google 등)를 통해 자동으로 처리됩니다.
|
||||
*/
|
||||
export async function transcribeAudio(params: TranscribeAudioParams): Promise<TranscribeAudioResult> {
|
||||
const apiBase = (params.apiBaseUrl || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000').replace(/\/$/, '')
|
||||
const url = `${apiBase}/api/stt/transcribe`
|
||||
|
||||
const headers: Record<string, string> = {}
|
||||
if (params.token) {
|
||||
headers['Authorization'] = `Bearer ${params.token}`
|
||||
}
|
||||
|
||||
// 1. If audio is Blob or ArrayBuffer, send via FormData
|
||||
if (params.audio instanceof Blob) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', params.audio, 'recording.webm')
|
||||
if (params.language) formData.append('language', params.language)
|
||||
if (params.prompt) formData.append('prompt', params.prompt)
|
||||
if (params.model) formData.append('model', params.model)
|
||||
if (params.provider) formData.append('provider', params.provider)
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const errText = await res.text()
|
||||
throw new Error(`STT transcription failed (${res.status}): ${errText}`)
|
||||
}
|
||||
|
||||
return (await res.json()) as TranscribeAudioResult
|
||||
} else if (params.audio instanceof ArrayBuffer || params.audio instanceof Uint8Array) {
|
||||
const blob = new Blob([params.audio as BlobPart], { type: 'audio/webm' })
|
||||
return transcribeAudio({ ...params, audio: blob })
|
||||
} else if (typeof params.audio === 'string') {
|
||||
// 2. If audio is Base64 string, send JSON payload
|
||||
const payload = {
|
||||
audioBase64: params.audio,
|
||||
language: params.language || 'ko',
|
||||
initialPrompt: params.prompt,
|
||||
modelId: params.model,
|
||||
provider: params.provider,
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
const errText = await res.text()
|
||||
throw new Error(`STT transcription failed (${res.status}): ${errText}`)
|
||||
}
|
||||
|
||||
return (await res.json()) as TranscribeAudioResult
|
||||
} else {
|
||||
throw new Error('Unsupported audio payload format.')
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue