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

This commit is contained in:
Yun Chan 2026-08-20 11:12:05 +09:00
parent 5cd1de6859
commit 708e20f747
406 changed files with 42464 additions and 6199 deletions

View 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')
})
})

View file

@ -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'

View 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.')
}
}