diff --git a/.task/checksum/build-frontend--DEV--RUNNER-npm- b/.task/checksum/build-frontend--DEV--RUNNER-npm- index dacfa89..b280382 100644 --- a/.task/checksum/build-frontend--DEV--RUNNER-npm- +++ b/.task/checksum/build-frontend--DEV--RUNNER-npm- @@ -1 +1 @@ -d098e6fd836c808a8a51f1d4f66e26d9 +19019f00457c1fd7dea1654686ad406d diff --git a/.task/checksum/windows-common-generate-bindings b/.task/checksum/windows-common-generate-bindings index c1583a3..ce53944 100644 --- a/.task/checksum/windows-common-generate-bindings +++ b/.task/checksum/windows-common-generate-bindings @@ -1 +1 @@ -595353d17d943f79f25c683007ba302c +b4345212d1427ec0e476a5f95c172c6e diff --git a/README.md b/README.md index 2b1454e..eb3a912 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ # opic +Wails 3 + Vue TypeScript 기반 AI OPIc 말하기 연습 앱입니다. + +## AI 공급자 + +설정에서 OpenAI 또는 Google Gemini를 선택할 수 있습니다. 키는 앱 프로세스 메모리에만 보관됩니다. + +```powershell +$env:OPENAI_API_KEY="sk-..." +$env:GEMINI_API_KEY="AIza..." +``` + +Gemini는 평가, 음성 전사, 질문 TTS에 사용할 모델을 각각 여러 개 지정할 수 있습니다. 위에서부터 호출하며 HTTP 오류, 기능 미지원, 빈 응답, 잘못된 평가 JSON, 전사 또는 오디오 누락 시 다음 모델을 자동으로 시도합니다. + +기본 Gemini 우선순위: + +- 평가·전사: `gemini-3.5-flash` → `gemini-2.5-flash` → `gemini-2.5-flash-lite` +- TTS: `gemini-3.1-flash-tts-preview` → `gemini-2.5-flash-preview-tts` + +## 빌드 + +```powershell +cd frontend +npm install +cd .. +wails3 generate bindings -clean -ts -i +wails3 build +``` diff --git a/docs/PLAN.md b/docs/PLAN.md index 4d25b2a..583fe48 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -85,7 +85,8 @@ Realtime 모델이 대화를 자연스럽게 진행하더라도 시험 문항 ## 7. 인증과 보안 - ChatGPT 웹 로그인/구독은 API 인증으로 재사용하지 않는다. -- 개인용 MVP: 사용자가 Platform API 키를 입력하며 메모리에만 유지한다. +- 개인용 MVP: 사용자가 OpenAI Platform 또는 Google Gemini API 키를 입력하며 메모리에만 유지한다. +- Gemini는 기능별 모델 우선순위를 두고 오류 또는 검증 불가능한 응답이면 다음 모델로 폴백한다. - 배포용: 앱에 공용 API 키를 포함하지 않는다. 자체 백엔드에서 사용자 로그인, 사용량 제한, ephemeral Realtime 인증을 처리한다. - 장기 로컬 저장이 필요하면 Windows Credential Manager/macOS Keychain/libsecret을 사용한다. - 오디오는 기본적으로 평가 후 폐기하고, 사용자가 명시적으로 저장을 선택한 경우에만 로컬 암호화 저장한다. @@ -129,4 +130,3 @@ Realtime 모델이 대화를 자연스럽게 진행하더라도 시험 문항 - 25MB 오디오 업로드 제한 전 사전 차단 - 세션 완료 전 보고서 접근 차단 - 난이도별 대표 transcript를 사용한 평가 회귀 테스트 - diff --git a/frontend/bindings/opiccoach/appservice.ts b/frontend/bindings/opiccoach/appservice.ts index d3dad1f..82e88b1 100644 --- a/frontend/bindings/opiccoach/appservice.ts +++ b/frontend/bindings/opiccoach/appservice.ts @@ -13,6 +13,14 @@ export function Configure(request: $models.ConfigureRequest): $CancellablePromis return $Call.ByID(1065098514, request); } +export function FinalizeSession(sessionID: string): $CancellablePromise<$models.ExamReport> { + return $Call.ByID(3685135372, sessionID); +} + +export function GenerateSpeech(text: string): $CancellablePromise<$models.SpeechResponse> { + return $Call.ByID(2776553183, text); +} + export function GetReport(sessionID: string): $CancellablePromise<$models.ExamReport> { return $Call.ByID(704064164, sessionID); } diff --git a/frontend/bindings/opiccoach/index.ts b/frontend/bindings/opiccoach/index.ts index 4441aff..fd95b88 100644 --- a/frontend/bindings/opiccoach/index.ts +++ b/frontend/bindings/opiccoach/index.ts @@ -13,8 +13,10 @@ export type { ConfigureRequest, ExamConfig, ExamReport, + GradePrediction, OverallReport, Question, + SpeechResponse, StartSessionResponse, SubmitAnswerRequest, SubmitAnswerResponse diff --git a/frontend/bindings/opiccoach/models.ts b/frontend/bindings/opiccoach/models.ts index d4ec7e6..d5a6e33 100644 --- a/frontend/bindings/opiccoach/models.ts +++ b/frontend/bindings/opiccoach/models.ts @@ -23,19 +23,36 @@ export interface AnswerRecord { } export interface AppSettings { + "provider": string; "hasApiKey": boolean; + "hasOpenAIKey": boolean; + "hasGeminiKey": boolean; "demoMode": boolean; "evaluationModel": string; "transcribeModel": string; "realtimeModel": string; + "speechModel": string; + "speechVoice": string; + "geminiEvaluationModels": string[] | null; + "geminiTranscriptionModels": string[] | null; + "geminiSpeechModels": string[] | null; + "geminiSpeechVoice": string; } export interface ConfigureRequest { + "provider": string; "apiKey": string; + "geminiApiKey": string; "demoMode": boolean; "evaluationModel": string; "transcribeModel": string; "realtimeModel": string; + "speechModel": string; + "speechVoice": string; + "geminiEvaluationModels": string[] | null; + "geminiTranscriptionModels": string[] | null; + "geminiSpeechModels": string[] | null; + "geminiSpeechVoice": string; } export interface ExamConfig { @@ -49,15 +66,30 @@ export interface ExamReport { "config": ExamConfig; "overall": OverallReport; "answers": AnswerRecord[] | null; + "answeredCount": number; + "totalCount": number; + "partial": boolean; "generatedAt": time$0.Time; } +export interface GradePrediction { + "grade": string; + "probability": number; + "status": string; + "description": string; +} + export interface OverallReport { "score": number; "estimatedBand": string; "summary": string; "strengths": string[] | null; + "weaknesses": string[] | null; "priorities": string[] | null; + "targetGrade": string; + "targetStatus": string; + "targetProbability": number; + "gradePredictions": GradePrediction[] | null; } export interface Question { @@ -68,6 +100,11 @@ export interface Question { "intent": string; } +export interface SpeechResponse { + "audioBase64": string; + "mimeType": string; +} + export interface StartSessionResponse { "sessionId": string; "question": Question; diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6aa6e8b..c668eaa 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,7 +1,7 @@