Merge branch 'master' of ssh://cckb9998.synology.me:30022/ckh08045/HUTAMS_AUDIO
This commit is contained in:
commit
a4aba7aae1
|
|
@ -3,7 +3,7 @@ __pycache__/
|
||||||
*.gguf
|
*.gguf
|
||||||
*.bin
|
*.bin
|
||||||
*.pt
|
*.pt
|
||||||
models/
|
/models/
|
||||||
whisper.db
|
whisper.db
|
||||||
data/audio/
|
data/audio/
|
||||||
data/samples/
|
data/samples/
|
||||||
|
|
@ -11,4 +11,6 @@ data/samples/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.whl
|
*.whl
|
||||||
*.log
|
*.log
|
||||||
|
/llm_weights/
|
||||||
|
/whisper_weights/
|
||||||
|
|
@ -9,9 +9,10 @@
|
||||||
```bash
|
```bash
|
||||||
uv sync
|
uv sync
|
||||||
```
|
```
|
||||||
- **[주의] 대용량 언어모델 수동 배치**: `Qwen3.5-0.8B-Q4_K_M.gguf` (또는 지정된 모델 버전)와 같은 대형 `.gguf` 파일들은 `.gitignore` 처리되어 Git에서 제외되므로 개별적으로 다운로드 받아 프로젝트 **루트 폴더**에 배치해야 합니다.
|
- **[주의] 대용량 언어모델 수동 배치**: `Qwen3.5-0.8B-Q4_K_M.gguf` (또는 지정된 모델 버전)와 같은 대형 `.gguf` 파일들은 `.gitignore` 처리되어 Git에서 제외되므로 개별적으로 다운로드 받아 프로젝트 내 **`llm_weights/`** 폴더 안에 배치해야 합니다. (동봉된 `setup.ps1` 스크립트를 관리자 권한으로 실행하면 모델 다운로드 및 폴더 생성까지 자동으로 처리됩니다).
|
||||||
- 최초 기동 시 오디오 저장 폴더(`data/audio/`, `data/samples/`) 및 로컬 DB 파일(`whisper.db`)은 시스템 내에 자동으로 초기화됩니다.
|
- 최초 기동 시 오디오 저장 폴더(`data/audio/`, `data/samples/`) 및 로컬 DB 파일(`whisper.db`)은 시스템 내에 자동으로 초기화됩니다.
|
||||||
|
|
||||||
|
- whisper_weights 폴더와 llm_weights 폴더는 `.gitignore` 처리되어 Git에서 제외되므로 개별적으로 다운로드 받아 프로젝트 내 **`whisper_weights/`** 폴더와 **`llm_weights/`** 폴더 안에 배치해야 합니다.
|
||||||
2. **서버 실행**:
|
2. **서버 실행**:
|
||||||
`run_server.ps1` 또는 아래 구문을 사용합니다.
|
`run_server.ps1` 또는 아래 구문을 사용합니다.
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@ class Settings(BaseSettings):
|
||||||
# CPU/내장그래픽 구동을 위해 한국어 인식률이 극대화된 모델 지정
|
# CPU/내장그래픽 구동을 위해 한국어 인식률이 극대화된 모델 지정
|
||||||
WHISPER_MODEL_NAME: str = "large-v3-turbo"
|
WHISPER_MODEL_NAME: str = "large-v3-turbo"
|
||||||
|
|
||||||
|
# 로컬 캐시 경로 (비워두면 HuggingFace 전역 캐시 사용, 설정하면 내부 폴더 사용)
|
||||||
|
WHISPER_MODEL_PATH: str = "./whisper_weights"
|
||||||
|
|
||||||
# 로컬 LLM GGUF 모델 파일 경로 (.env에서 오버라이드 가능)
|
# 로컬 LLM GGUF 모델 파일 경로 (.env에서 오버라이드 가능)
|
||||||
LLM_MODEL_PATH: str = ""
|
LLM_MODEL_PATH: str = ""
|
||||||
LLM_ENABLED: bool = False # False면 LLM 분석 스킵 (모델 미설정 시 안전 기본값)
|
LLM_ENABLED: bool = False # False면 LLM 분석 스킵 (모델 미설정 시 안전 기본값)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
"""
|
||||||
|
STT 이력 조회용 Pydantic 응답 스키마 모듈.
|
||||||
|
ORM 모델(app/db/models.py)과 분리하여 API 계층의 직렬화 규격을 관리합니다.
|
||||||
|
"""
|
||||||
|
from pydantic import BaseModel, Field, ConfigDict
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class SegmentSchema(BaseModel):
|
||||||
|
"""TranscriptionSegment ORM → JSON 직렬화 스키마"""
|
||||||
|
model_config = ConfigDict(from_attributes=True) # Pydantic v2: ORM 객체 직렬화 허용
|
||||||
|
|
||||||
|
id: int
|
||||||
|
start_sec: float
|
||||||
|
end_sec: float
|
||||||
|
text: Optional[str]
|
||||||
|
speaker: Optional[str]
|
||||||
|
absolute_start_time: Optional[str]
|
||||||
|
absolute_end_time: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class RecordListResponse(BaseModel):
|
||||||
|
"""TranscriptionRecord ORM → JSON 직렬화 스키마 (세그먼트 포함)"""
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
filename: str
|
||||||
|
full_text: Optional[str]
|
||||||
|
language: Optional[str]
|
||||||
|
base_datetime: Optional[datetime]
|
||||||
|
processing_time_sec: Optional[float]
|
||||||
|
audio_duration_sec: Optional[float]
|
||||||
|
peak_memory_mb: Optional[float]
|
||||||
|
process_speed_x: Optional[float]
|
||||||
|
# LLM 메타데이터
|
||||||
|
title: Optional[str] = None
|
||||||
|
summary: Optional[str] = None
|
||||||
|
keywords: Optional[str] = None
|
||||||
|
urgency: Optional[str] = None
|
||||||
|
created_at: datetime
|
||||||
|
segments: List[SegmentSchema] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class RecordPageResponse(BaseModel):
|
||||||
|
"""페이징이 적용된 목록 조회 응답 래퍼"""
|
||||||
|
total: int = Field(..., description="필터 조건에 해당하는 전체 레코드 수")
|
||||||
|
skip: int = Field(..., description="현재 오프셋")
|
||||||
|
limit: int = Field(..., description="현재 페이지 크기")
|
||||||
|
records: List[RecordListResponse] = Field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
|
class DailySegmentResponse(BaseModel):
|
||||||
|
"""일자별 채팅 뷰용 세그먼트 평면 응답 스키마"""
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int
|
||||||
|
record_id: int
|
||||||
|
start_sec: float
|
||||||
|
end_sec: float
|
||||||
|
text: Optional[str]
|
||||||
|
speaker: Optional[str]
|
||||||
|
is_reviewed: bool = False
|
||||||
|
absolute_start_time: Optional[str] = Field(None, description="ISO 8601 발화 절대 시작 시간")
|
||||||
|
absolute_end_time: Optional[str] = Field(None, description="ISO 8601 발화 절대 종료 시간")
|
||||||
|
|
||||||
|
|
||||||
|
class DailySegmentPage(BaseModel):
|
||||||
|
"""Cursor 기반 페이지네이션 래퍼 — COUNT(*) 없이 인덱스만 사용."""
|
||||||
|
items: List[DailySegmentResponse] = Field(..., description="세그먼트 목록")
|
||||||
|
next_cursor: Optional[int] = Field(None, description="다음 페이지 시작 커서 (마지막 item의 id). None이면 마지막 페이지.")
|
||||||
|
has_more: bool = Field(..., description="다음 페이지 존재 여부")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
from typing import Optional, List
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class STTSegment(BaseModel):
|
||||||
|
"""발화 단위(Segment) 상세 데이터 모델"""
|
||||||
|
start_sec: float = Field(..., description="발화 시작 시간 (초)")
|
||||||
|
end_sec: float = Field(..., description="발화 종료 시간 (초)")
|
||||||
|
text: str = Field(..., description="교정이 완료된 텍스트")
|
||||||
|
speaker: Optional[str] = Field(default=None, description="휴리스틱 발화자 식별 결과")
|
||||||
|
absolute_start_time: Optional[str] = Field(default=None, description="ISO 8601 발화 시작 절대 시간")
|
||||||
|
absolute_end_time: Optional[str] = Field(default=None, description="ISO 8601 발화 종료 절대 시간")
|
||||||
|
audio_path: Optional[str] = Field(default=None, description="로컬 파일 경로 (Opus 변환본)")
|
||||||
|
id: Optional[int] = Field(default=None, description="DB segment ID (저장 후 매핑됨)")
|
||||||
|
|
||||||
|
class STTRequest(BaseModel):
|
||||||
|
"""STT 처리를 요청하기 위한 데이터 모델"""
|
||||||
|
audio_file_path: str = Field(..., description="변환할 .wav 등 오디오 파일의 절대/상대 경로")
|
||||||
|
language: str = Field(default="ko", description="오디오의 주 언어 코드 (기본값: 한국어)")
|
||||||
|
base_datetime: Optional[datetime] = Field(default=None, description="녹음 시작 절대 시간 (Line-In 동기화용)")
|
||||||
|
|
||||||
|
class STTResponse(BaseModel):
|
||||||
|
"""STT 처리 결과를 반환하기 위한 데이터 모델"""
|
||||||
|
text: str = Field(..., description="변환이 완료된 전체 문자열 텍스트")
|
||||||
|
language: str = Field(..., description="인식된/지정된 언어 코드")
|
||||||
|
segments: List[STTSegment] = Field(default_factory=list, description="개별 발화 단위 데이터 리스트")
|
||||||
|
processing_time_sec: Optional[float] = Field(default=None, description="STT 처리에 소요된 시간 (초)")
|
||||||
|
|
||||||
|
# 벤치마킹 지표
|
||||||
|
load_time_sec: Optional[float] = Field(default=None, description="모델 로드 시간 (초)")
|
||||||
|
audio_duration_sec: Optional[float] = Field(default=None, description="원본 오디오 총 길이 (초)")
|
||||||
|
peak_memory_mb: Optional[float] = Field(default=None, description="변환 중 발생한 최대 메모리 점유율 (MB)")
|
||||||
|
process_speed_x: Optional[float] = Field(default=None, description="오디오 길이 대비 처리 속도 비율 (배수)")
|
||||||
|
|
||||||
|
# 룰 기반 실시간 분석 결과
|
||||||
|
urgency: Optional[str] = Field(default="일반", description="긴급도 판별 결과")
|
||||||
|
train_number: Optional[str] = Field(default=None, description="추출된 열차 번호")
|
||||||
|
|
@ -35,8 +35,16 @@ class WhisperSTTService:
|
||||||
|
|
||||||
if self._model is None:
|
if self._model is None:
|
||||||
try:
|
try:
|
||||||
# CPU 타겟, int8 양자화로 모델 객체 생성. (최초 시도시 모델 자동 다운로드됨)
|
# CPU 타겟, int8 양자화로 모델 객체 생성.
|
||||||
self._model = WhisperModel(self.model_name, device="cpu", compute_type="int8")
|
# WHISPER_MODEL_PATH가 설정되어 있으면 해당 폴더를 다운로드 루트로 사용 (퀔야 3기동 후 오프라인 가능)
|
||||||
|
from app.core.config import settings
|
||||||
|
download_root = settings.WHISPER_MODEL_PATH if settings.WHISPER_MODEL_PATH else None
|
||||||
|
self._model = WhisperModel(
|
||||||
|
self.model_name,
|
||||||
|
device="cpu",
|
||||||
|
compute_type="int8",
|
||||||
|
download_root=download_root
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ModelNotFoundError(f"Whisper 모델 로드 실패 ({self.model_name}): {str(e)}")
|
raise ModelNotFoundError(f"Whisper 모델 로드 실패 ({self.model_name}): {str(e)}")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
HUTAMS 프로젝트 최초 설정 스크립트.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
1. uv 동기화(의존성 라이브러리 설치)
|
||||||
|
2. llm_weights, whisper_weights 폴더 생성
|
||||||
|
3. 대용량 언어모델(.gguf) 다운로드 (존재하지 않을 경우)
|
||||||
|
4. Whisper STT 모델 다운로드 (존재하지 않을 경우)
|
||||||
|
#>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
Write-Host "=====================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " HUTAMS STT 프로젝트 초기화 스크립트 (setup.ps1) " -ForegroundColor Cyan
|
||||||
|
Write-Host "=====================================================" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# ── 1. uv 패키지 설치 ────────────────────────────────────────────────────────
|
||||||
|
Write-Host "`n[1/4] uv sync로 파이썬 패키지 설치 중..." -ForegroundColor Yellow
|
||||||
|
try {
|
||||||
|
uv sync
|
||||||
|
Write-Host "패키지 설치 완료!" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "uv 실행 실패. https://docs.astral.sh/uv/ 에서 uv를 설치 후 재시도해주세요." -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 2. 폴더 생성 ─────────────────────────────────────────────────────────────
|
||||||
|
Write-Host "`n[2/4] 모델 폴더 생성 중..." -ForegroundColor Yellow
|
||||||
|
New-Item -ItemType Directory -Force -Path "llm_weights" | Out-Null
|
||||||
|
New-Item -ItemType Directory -Force -Path "whisper_weights" | Out-Null
|
||||||
|
New-Item -ItemType Directory -Force -Path "data/audio" | Out-Null
|
||||||
|
New-Item -ItemType Directory -Force -Path "data/samples" | Out-Null
|
||||||
|
Write-Host "폴더 생성 완료!" -ForegroundColor Green
|
||||||
|
|
||||||
|
# ── 3. LLM 모델(.gguf) 다운로드 ──────────────────────────────────────────────
|
||||||
|
$LlmModelName = "Qwen3.5-0.8B-Q4_K_M.gguf"
|
||||||
|
$LlmModelPath = "llm_weights\$LlmModelName"
|
||||||
|
|
||||||
|
# [⚠️ 실제 사용 중인 모델 URL로 교체하세요]
|
||||||
|
$LlmDownloadUrl = "https://huggingface.co/unsloth/Qwen3.5-0.8B-GGUF/resolve/main/Qwen3.5-0.8B-Q4_K_M.gguf?download=true"
|
||||||
|
|
||||||
|
# $LlmModelName2 = "Qwen3.5-2B-Q4_K_M.gguf"
|
||||||
|
# $LlmDownloadUrl2 = "https://huggingface.co/unsloth/Qwen3.5-2B-GGUF/resolve/main/Qwen3.5-2B-Q4_K_M.gguf?download=true"# 모델이 실제로는 Qwen3.5-0.8B 이므로 실제 유효한 HuggingFace URL
|
||||||
|
|
||||||
|
Write-Host "`n[3/4] LLM 모델($LlmModelName) 확인 중..." -ForegroundColor Yellow
|
||||||
|
if (Test-Path -Path $LlmModelPath) {
|
||||||
|
Write-Host "이미 존재합니다. 다운로드를 건너뜁니다." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "다운로드 시작 (용량이 크므로 시간이 걸릴 수 있습니다)..." -ForegroundColor Yellow
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -Uri $LlmDownloadUrl -OutFile $LlmModelPath
|
||||||
|
Write-Host "LLM 모델 다운로드 완료!" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "[오류] LLM 모델 다운로드 실패. 아래 URL에서 수동으로 받아 'llm_weights/' 폴더에 배치하세요:" -ForegroundColor Red
|
||||||
|
Write-Host " → $LlmDownloadUrl" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 4. Whisper STT 모델 다운로드 ─────────────────────────────────────────────
|
||||||
|
$WhisperDir = "whisper_weights"
|
||||||
|
$WhisperModelName = "large-v3-turbo"
|
||||||
|
$WhisperHfRepoId = "mobiuslabsgmbh/faster-whisper-large-v3-turbo"
|
||||||
|
|
||||||
|
Write-Host "`n[4/4] Whisper STT 모델($WhisperModelName) 다운로드 중..." -ForegroundColor Yellow
|
||||||
|
# whisper_weights/ 안에 이미 모델 폴더가 있으면 건너뜀
|
||||||
|
$WhisperFlag = Get-ChildItem -Path $WhisperDir -ErrorAction SilentlyContinue | Measure-Object
|
||||||
|
if ($WhisperFlag.Count -gt 0) {
|
||||||
|
Write-Host "이미 whisper_weights 폴더에 파일이 존재합니다. 건너뜁니다." -ForegroundColor Green
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "uv run을 통해 Whisper 모델을 프로젝트 내부 캐시로 다운로드합니다 (최초 1회)..." -ForegroundColor Yellow
|
||||||
|
try {
|
||||||
|
# 환경변수로 다운로드 경로를 설정하고 서버를 즉시 로드(초기화) 후 종료
|
||||||
|
$env:WHISPER_MODEL_PATH = (Resolve-Path $WhisperDir).Path
|
||||||
|
uv run python -c "
|
||||||
|
from faster_whisper import WhisperModel
|
||||||
|
import os
|
||||||
|
model_path = os.environ.get('WHISPER_MODEL_PATH', './whisper_weights')
|
||||||
|
print(f'Whisper 모델 다운로드 위치: {model_path}')
|
||||||
|
m = WhisperModel('$WhisperModelName', device='cpu', compute_type='int8', download_root=model_path)
|
||||||
|
print('다운로드 완료!')
|
||||||
|
"
|
||||||
|
Write-Host "Whisper STT 모델 다운로드 완료!" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Host "[오류] Whisper 모델 다운로드 실패. 서버 첫 기동 시 자동으로 다운로드를 시도합니다." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 완료 ──────────────────────────────────────────────────────────────────────
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=====================================================" -ForegroundColor Cyan
|
||||||
|
Write-Host " 모든 설정이 완료되었습니다! " -ForegroundColor Cyan
|
||||||
|
Write-Host " run_server.ps1을 실행하여 관제 서버를 시작하세요. " -ForegroundColor Cyan
|
||||||
|
Write-Host "=====================================================" -ForegroundColor Cyan
|
||||||
Loading…
Reference in New Issue