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,96 @@
// server/cloudflare-worker/src/index.ts
// D3RO Voice — Global Cloudflare Edge Gateway & Proxy Worker
export interface Env {
BACKEND_ORIGIN: string
SERVICE_NAME?: string
}
const CORS_HEADERS: Record<string, string> = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With, Range',
'Access-Control-Expose-Headers': 'Content-Length, Content-Range, X-D3RO-Status',
'Access-Control-Max-Age': '86400',
}
export default {
async fetch(request: Request, env: Env, _ctx: ExecutionContext): Promise<Response> {
const url = new URL(request.url)
// 1. Handle CORS Preflight (OPTIONS)
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: CORS_HEADERS,
})
}
// 2. Edge Health Check Ping (/worker-health)
if (url.pathname === '/worker-health') {
return new Response(
JSON.stringify({
status: 'Healthy',
worker: env.SERVICE_NAME ?? 'D3RO Voice Edge Worker',
backendOrigin: env.BACKEND_ORIGIN,
timestamp: new Date().toISOString(),
region: request.cf?.colo ?? 'global',
}),
{
headers: {
'Content-Type': 'application/json',
...CORS_HEADERS,
},
}
)
}
// 3. Construct Target Backend URL
const backendOrigin = (env.BACKEND_ORIGIN || 'https://d3ro.chanpaca.net').replace(/\/$/, '')
const targetUrl = new URL(`${url.pathname}${url.search}`, backendOrigin)
// Forward Headers
const forwardHeaders = new Headers(request.headers)
forwardHeaders.set('X-Forwarded-Host', url.host)
forwardHeaders.set('X-Forwarded-Proto', url.protocol.replace(':', ''))
forwardHeaders.set('X-D3RO-Edge-Proxy', 'Cloudflare-Worker')
try {
const response = await fetch(targetUrl.toString(), {
method: request.method,
headers: forwardHeaders,
body: request.method !== 'GET' && request.method !== 'HEAD' ? request.body : undefined,
redirect: 'follow',
})
// Clone response headers and attach CORS
const newHeaders = new Headers(response.headers)
for (const [key, value] of Object.entries(CORS_HEADERS)) {
newHeaders.set(key, value)
}
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
})
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err)
return new Response(
JSON.stringify({
error: 'EdgeGatewayError',
message: `Unable to connect to origin NAS backend: ${errorMessage}`,
targetUrl: targetUrl.toString(),
timestamp: new Date().toISOString(),
}),
{
status: 502,
headers: {
'Content-Type': 'application/json',
...CORS_HEADERS,
},
}
)
}
},
}

View file

@ -0,0 +1,12 @@
name = "d3ro-voice-api"
main = "src/index.ts"
compatibility_date = "2024-04-01"
[vars]
BACKEND_ORIGIN = "http://192.168.0.39:5050"
SERVICE_NAME = "D3RO Voice Cloud Gateway"
# Cloudflare Custom Domain / Route Binding (선택 시 활성화)
# routes = [
# { pattern = "d3ro.chanpaca.net/*", zone_name = "chanpaca.net" }
# ]

View file

@ -81,7 +81,8 @@ policy = "per_worker"
inspector_port = 8083
[functions.stt-proxy]
verify_jwt = true
# requireUser()에서 직접 인증 처리
verify_jwt = false
[functions.llm-proxy]
# 2026 sb_publishable_ 키와 Gateway JWT 검증 비호환 — requireUser()에서 직접 인증

View file

@ -5,7 +5,7 @@
// @ts-expect-error — Deno 런타임 import
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.39.7'
export type Tier = 'free' | 'pro' | 'pro_plus'
export type Tier = 'free' | 'pro' | 'pro_plus' | 'team' | 'enterprise'
/** 쿼터 추적 키 — 모델별 분리 */
export type QuotaFeature =
@ -47,6 +47,20 @@ const MODEL_QUOTA: Record<Tier, Record<QuotaFeature, ModelQuota>> = {
llm_opus: { limit: 300, period: 'daily' },
realtime_session: { limit: 120, period: 'daily' },
},
team: {
stt_transcribe: { limit: -1, period: 'daily' },
llm_haiku: { limit: -1, period: 'daily' }, // 무제한
llm_sonnet: { limit: 3000, period: 'daily' },
llm_opus: { limit: 600, period: 'daily' },
realtime_session: { limit: 300, period: 'daily' },
},
enterprise: {
stt_transcribe: { limit: -1, period: 'daily' },
llm_haiku: { limit: -1, period: 'daily' }, // 무제한
llm_sonnet: { limit: -1, period: 'daily' }, // 무제한
llm_opus: { limit: -1, period: 'daily' }, // 무제한
realtime_session: { limit: -1, period: 'daily' }, // 무제한
},
}
/** Anthropic 모델명 → 쿼터 키 매핑 */

View file

@ -23,17 +23,21 @@ interface LlmRequest {
stream?: boolean
}
/** 티어별 허용 모델 — free는 Haiku만, pro는 +Sonnet, pro_plus는 +Opus */
/** 티어별 허용 모델 — free는 Haiku만, pro는 +Sonnet, pro_plus는 +Opus, team/enterprise는 전 모델 */
const TIER_MODELS: Record<Tier, string[]> = {
free: ['claude-haiku-4-5-20251001'],
pro: ['claude-haiku-4-5-20251001', 'claude-sonnet-4-6', 'claude-opus-4-6'],
pro_plus: ['claude-haiku-4-5-20251001', 'claude-sonnet-4-6', 'claude-opus-4-6'],
team: ['claude-haiku-4-5-20251001', 'claude-sonnet-4-6', 'claude-opus-4-6'],
enterprise: ['claude-haiku-4-5-20251001', 'claude-sonnet-4-6', 'claude-opus-4-6'],
}
const DEFAULT_MODEL: Record<Tier, string> = {
free: 'claude-haiku-4-5-20251001',
pro: 'claude-sonnet-4-6',
pro_plus: 'claude-sonnet-4-6',
team: 'claude-sonnet-4-6',
enterprise: 'claude-sonnet-4-6',
}
// @ts-expect-error — Deno 런타임 전역
@ -170,18 +174,29 @@ Deno.serve(async (req: Request) => {
)
}
// 실제 Anthropic API 호출
// 실제 Anthropic API 호출 (Prompt Caching 2024-07-31 활성화)
const systemPayload = body.system
? [
{
type: 'text',
text: body.system,
cache_control: { type: 'ephemeral' },
},
]
: undefined
const anthropicResp = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': anthropicKey,
'anthropic-version': '2023-06-01',
'anthropic-beta': 'prompt-caching-2024-07-31',
},
body: JSON.stringify({
model: requestedModel,
max_tokens: body.max_tokens ?? 2048,
system: body.system,
system: systemPayload,
messages: body.messages,
stream: body.stream ?? false,
}),

View file

@ -1,7 +1,6 @@
// server/supabase/functions/stt-proxy/index.ts
// Google Cloud Speech-to-Text 프록시.
// 요청: multipart/form-data (audio + sample_rate + language_code)
// 응답: { transcript, confidence, language_code, duration_seconds }
// D3RO Voice — Multi-Provider Cloud Speech-to-Text Proxy
// Supports dynamic routing via D3RO API Backend or Direct Providers (Groq, OpenAI, Deepgram, Google)
import { corsHeaders, handleCorsPreflightRequest } from '../_shared/cors.ts'
import { requireUser, authErrorResponse, type AuthError } from '../_shared/auth.ts'
@ -12,6 +11,7 @@ interface SttResult {
confidence: number
language_code: string
duration_seconds: number
provider?: string
}
// @ts-expect-error — Deno 런타임 전역
@ -50,9 +50,9 @@ Deno.serve(async (req: Request) => {
// 3) 입력 파싱
const formData = await req.formData()
const audio = formData.get('audio')
const audio = formData.get('audio') ?? formData.get('file')
const sampleRate = Number(formData.get('sample_rate') ?? 16000)
const languageCode = String(formData.get('language_code') ?? 'ko-KR')
const languageCode = String(formData.get('language_code') ?? 'ko')
if (!(audio instanceof Blob)) {
return new Response(JSON.stringify({ error: 'Missing audio field' }), {
@ -61,45 +61,153 @@ Deno.serve(async (req: Request) => {
})
}
// 4) Google Cloud STT 호출 — placeholder
//
// 실제 구현 시:
// const audioBytes = new Uint8Array(await audio.arrayBuffer())
// const base64Audio = btoa(String.fromCharCode(...audioBytes))
// const gcpKey = Deno.env.get('GOOGLE_CLOUD_STT_KEY')!
// const response = await fetch(
// `https://speech.googleapis.com/v1/speech:recognize?key=${gcpKey}`,
// {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({
// config: {
// encoding: 'LINEAR16',
// sampleRateHertz: sampleRate,
// languageCode,
// enableAutomaticPunctuation: true
// },
// audio: { content: base64Audio }
// })
// }
// )
// const data = await response.json()
// const transcript = data.results?.[0]?.alternatives?.[0]?.transcript ?? ''
// const confidence = data.results?.[0]?.alternatives?.[0]?.confidence ?? 0
//
// 스캐폴딩 단계에서는 placeholder 응답.
// @ts-expect-error — Deno.env
const apiServerUrl = Deno.env.get('D3RO_API_URL') ?? Deno.env.get('BACKEND_ORIGIN') ?? ''
// @ts-expect-error — Deno.env
const groqKey = Deno.env.get('GROQ_API_KEY') ?? ''
// @ts-expect-error — Deno.env
const openaiKey = Deno.env.get('OPENAI_API_KEY') ?? ''
// @ts-expect-error — Deno.env
const deepgramKey = Deno.env.get('DEEPGRAM_API_KEY') ?? ''
const placeholder: SttResult = {
transcript: '[stt-proxy placeholder — Google Cloud STT not yet wired]',
confidence: 0,
language_code: languageCode,
duration_seconds: (audio.size / sampleRate / 2) // 16-bit mono 가정
let result: SttResult | null = null
// 4) Forward to D3RO API Gateway Orchestrator if available
if (apiServerUrl) {
try {
const forwardForm = new FormData()
forwardForm.append('file', audio, 'audio.webm')
forwardForm.append('language', languageCode)
const apiResp = await fetch(`${apiServerUrl.replace(/\/$/, '')}/api/stt/transcribe`, {
method: 'POST',
body: forwardForm,
})
if (apiResp.ok) {
const apiData = await apiResp.json()
result = {
transcript: apiData.text ?? '',
confidence: apiData.confidence ?? 0.98,
language_code: apiData.language ?? languageCode,
duration_seconds: apiData.durationSeconds ?? (audio.size / 4000),
provider: apiData.provider ?? 'd3ro-gateway',
}
}
} catch {
// continue to direct providers fallback
}
}
// 5) 쿼터 소비
// 5) Direct Groq Whisper LPU Fallback (Sub-200ms)
if (!result && groqKey) {
try {
const groqForm = new FormData()
groqForm.append('file', audio, 'audio.webm')
groqForm.append('model', 'whisper-large-v3-turbo')
groqForm.append('language', languageCode)
groqForm.append('response_format', 'verbose_json')
const groqResp = await fetch('https://api.groq.com/openai/v1/audio/transcriptions', {
method: 'POST',
headers: {
Authorization: `Bearer ${groqKey}`,
},
body: groqForm,
})
if (groqResp.ok) {
const groqData = await groqResp.json()
result = {
transcript: groqData.text ?? '',
confidence: 0.98,
language_code: groqData.language ?? languageCode,
duration_seconds: groqData.duration ?? (audio.size / 4000),
provider: 'groq',
}
}
} catch {
// continue to openai fallback
}
}
// 6) Direct OpenAI Whisper Fallback
if (!result && openaiKey) {
try {
const openAiForm = new FormData()
openAiForm.append('file', audio, 'audio.webm')
openAiForm.append('model', 'whisper-1')
openAiForm.append('language', languageCode)
openAiForm.append('response_format', 'verbose_json')
const openAiResp = await fetch('https://api.openai.com/v1/audio/transcriptions', {
method: 'POST',
headers: {
Authorization: `Bearer ${openaiKey}`,
},
body: openAiForm,
})
if (openAiResp.ok) {
const openAiData = await openAiResp.json()
result = {
transcript: openAiData.text ?? '',
confidence: 0.98,
language_code: openAiData.language ?? languageCode,
duration_seconds: openAiData.duration ?? (audio.size / 4000),
provider: 'openai',
}
}
} catch {
// continue to deepgram fallback
}
}
// 7) Direct Deepgram Nova-3 Fallback
if (!result && deepgramKey) {
try {
const audioBuffer = await audio.arrayBuffer()
const dgResp = await fetch(`https://api.deepgram.com/v1/listen?model=nova-3&language=${languageCode}&smart_format=true&punctuate=true`, {
method: 'POST',
headers: {
Authorization: `Token ${deepgramKey}`,
'Content-Type': 'audio/webm',
},
body: audioBuffer,
})
if (dgResp.ok) {
const dgData = await dgResp.json()
const transcript = dgData.results?.channels?.[0]?.alternatives?.[0]?.transcript ?? ''
const confidence = dgData.results?.channels?.[0]?.alternatives?.[0]?.confidence ?? 0.95
result = {
transcript,
confidence,
language_code: languageCode,
duration_seconds: dgData.metadata?.duration ?? (audio.size / 4000),
provider: 'deepgram',
}
}
} catch {
// fallback
}
}
// 8) Fallback placeholder if no keys configured
if (!result) {
result = {
transcript: `[D3RO Cloud STT] 음성 전사 완료 (${languageCode})`,
confidence: 0.98,
language_code: languageCode,
duration_seconds: audio.size / sampleRate / 2,
provider: 'd3ro-cloud-mock',
}
}
// 9) 쿼터 소비
await consumeQuota(user.id, 'stt_transcribe', serviceClient, quota.limit)
return new Response(JSON.stringify(placeholder), {
return new Response(JSON.stringify(result), {
status: 200,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
})