홈랩 AI 서버 구축 가이드 3편: ComfyUI·FLUX 이미지 및 음성 AI 멀티모달

편수3편 / 5편
난이도⭐⭐ 중급
다루는 내용ComfyUI · FLUX · Whisper · Kokoro TTS · LLaVA · OCR · 회의록 자동화
섹션 수12개 섹션
🎨 ComfyUI · A1111 ⚡ FLUX.1 🎤 Whisper STT 🗣️ Kokoro TTS 👁️ Vision · OCR 회의록 자동화 · LoRA · ControlNet
3편은 텍스트를 넘어 이미지·음성·문서까지 처리하는 멀티모달 AI 서버를 구축합니다. ComfyUI·A1111·FLUX.1으로 프라이빗 이미지 생성 서버를 세우고, Whisper STT와 Kokoro TTS로 음성 AI 어시스턴트 파이프라인을 구성합니다. LLaVA Vision으로 이미지를 이해하고, PaddleOCR로 문서를 디지털화하며, 회의록을 자동으로 만드는 n8n 자동화까지 — 완전한 멀티모달 홈랩 AI를 한 편에 완성합니다.
SECTION01

이미지 생성 엔진 비교 & 선택 가이드

로컬 AI 이미지 생성 생태계는 크게 두 가지 엔진으로 나뉩니다. ComfyUI는 노드 기반 워크플로우로 최고의 유연성을 제공하고, Automatic1111 (A1111)은 직관적인 UI로 빠른 생성에 최적화됩니다. 두 가지를 같은 서버에 설치해 용도에 따라 선택하는 것이 가장 현명합니다.

🆚 ComfyUI vs A1111 상세 비교

항목ComfyUIA1111 (Automatic1111)
인터페이스노드 그래프 (고급)탭형 UI (직관적)
FLUX.1 지원🟢 최고 (공식 우선 지원)🟡 플러그인 필요
워크플로우 재현성🟢 완벽 (JSON 저장)🟡 일부 설정만
API 연동🟢 강력한 REST API🟢 REST API 지원
확장 플러그인🟡 커스텀 노드🟢 방대한 Extensions
VRAM 효율🟢 최적화 우수🟡 보통
초보자 접근성🔴 학습 곡선 있음🟢 쉬움
배치 처리🟢 워크플로우 배치🟡 기본 배치
추천 용도고급 워크플로우, API 자동화빠른 실험, 다양한 모델 탐색

🎨 VRAM별 추천 이미지 모델

VRAM추천 모델해상도특징
6~8GBSDXL Turbo, SD 1.5512~768px빠른 생성. 품질 제한적
10~12GBSDXL, FLUX.1 Schnell (양자화)1024px고품질. FLUX는 FP8 양자화 필요
16~24GBFLUX.1 Dev, SD3.5 Large1024~2048px최고 품질. 상세한 표현 가능
24GB+FLUX.1 Dev FP16, SVD (비디오)2048px+무제한 수준의 품질과 해상도
SECTION02

ComfyUI Docker 설치 & 완전 설정

ComfyUI를 Docker로 설치하면 모델 파일과 워크플로우를 볼륨으로 영속화할 수 있고, 커스텀 노드 관리도 깔끔하게 됩니다. GPU 접근을 위해 NVIDIA Container Toolkit이 미리 설치되어 있어야 합니다.

yaml — ~/ai-server/image-gen/docker-compose.yml
services:

  # ── ComfyUI ────────────────────────────────────
  comfyui:
    image: yanwk/comfyui-boot:cu124
    container_name: comfyui
    restart: unless-stopped
    ports:
      - "8188:8188"
    volumes:
      - comfyui_models:/root/ComfyUI/models        # 모델 파일 영속화
      - comfyui_output:/root/ComfyUI/output         # 생성 이미지
      - comfyui_custom_nodes:/root/ComfyUI/custom_nodes
      - ~/ai-server/data/models:/shared_models:ro   # A1111과 모델 공유
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
      - CLI_ARGS=--listen 0.0.0.0 --port 8188      # 외부 접근 허용
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    networks:
      - ai_net

  # ── Automatic1111 (A1111) ───────────────────────
  a1111:
    image: universalml/stable-diffusion-webui:latest
    container_name: a1111
    restart: unless-stopped
    ports:
      - "7860:7860"
    volumes:
      - a1111_models:/stable-diffusion-webui/models
      - a1111_output:/stable-diffusion-webui/outputs
      - a1111_extensions:/stable-diffusion-webui/extensions
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
      - CLI_ARGS=--listen --api --xformers --enable-insecure-extension-access
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    networks:
      - ai_net

volumes:
  comfyui_models:
  comfyui_output:
  comfyui_custom_nodes:
  a1111_models:
  a1111_output:
  a1111_extensions:

networks:
  ai_net:
    external: true
    name: ai_network
bash — ComfyUI 커스텀 노드 설치 (필수 노드)
# ComfyUI Manager 설치 (노드 관리 UI)
docker exec -it comfyui bash -c "
cd /root/ComfyUI/custom_nodes && \
git clone https://github.com/ltdrdata/ComfyUI-Manager.git && \
pip install -r ComfyUI-Manager/requirements.txt
"

# ComfyUI 재시작
docker restart comfyui

# 접속 후 Manager에서 추가 설치 권장 노드
# - ComfyUI-Impact-Pack (고급 Detailer)
# - comfyui_controlnet_aux (ControlNet 전처리기)
# - ComfyUI-KJNodes (유용한 유틸리티)
# - was-node-suite-comfyui (이미지 처리 노드)
SECTION03

A1111 핵심 확장 & 설정 최적화

A1111은 설치 후 확장 플러그인 구성이 핵심입니다. Extensions 탭에서 URL로 직접 설치하거나 Extensions 목록에서 검색해 설치합니다.

🔌 필수 Extensions 목록

Extension기능설치 URL
ControlNet포즈·엣지·깊이 기반 이미지 제어mikubill/sd-webui-controlnet
Ultimate SD Upscale타일 기반 고해상도 업스케일링Coyote-A/ultimate-upscale-for-automatic1111
ADetailer얼굴·손 자동 수정 (Inpainting)Bing-su/adetailer
Infinite Zoom줌인/줌아웃 영상 생성v8hid/infinite-zoom-automatic1111-webui
Prompt All-in-One한국어 프롬프트 번역 지원Physton/sd-webui-prompt-all-in-one
Regional Prompter이미지 영역별 다른 프롬프트 적용hako-mikan/sd-webui-regional-prompter

⚡ A1111 성능 최적화 launch 옵션

bash — A1111 최적화 CLI 옵션 (RTX 3090 기준)
# webui-user.sh 또는 Docker environment의 CLI_ARGS
CLI_ARGS="
  --listen                    # 외부 접근 허용
  --api                       # REST API 활성화
  --xformers                  # 메모리 최적화 (VRAM 20~30% 절감)
  --opt-sdp-attention         # SDP attention 최적화
  --opt-channelslast          # 채널 순서 최적화
  --no-half-vae               # VAE 품질 유지 (권장)
  --medvram                   # 12GB VRAM 이하에서 권장
  --skip-torch-cuda-test      # 시작 시간 단축
  --enable-insecure-extension-access  # Extension 설치 허용
"
SECTION04

FLUX.1 모델 완전 정복

FLUX.1은 2024년 Black Forest Labs가 출시한 차세대 이미지 생성 모델로, 사실적인 표현력과 텍스트 렌더링에서 SDXL을 크게 앞섭니다. 특히 손가락, 글씨, 복잡한 구도 처리가 혁신적으로 개선됐습니다.

🔥 FLUX.1 버전별 비교

버전용도라이선스VRAM스텝
FLUX.1-Schnell빠른 생성 (1~4스텝)Apache 2.0 (무료 상업용)12GB+4
FLUX.1-Dev고품질 생성비상업용 연구16GB+20~30
FLUX.1-Pro최고 품질 API 서비스상업용 유료API자동
FLUX.1-Fill인페인팅 특화비상업용16GB+30
bash — FLUX.1 모델 다운로드 (HuggingFace)
# HuggingFace CLI 설치
pip install huggingface_hub

# FLUX.1-Schnell (무료 상업용, 12GB VRAM)
huggingface-cli download black-forest-labs/FLUX.1-schnell \
  --local-dir ~/ai-server/data/models/FLUX/schnell

# FLUX.1-Dev (비상업 고품질, 16GB VRAM)
huggingface-cli download black-forest-labs/FLUX.1-dev \
  --local-dir ~/ai-server/data/models/FLUX/dev

# GGUF 양자화 버전 (12GB VRAM에서 Dev 품질)
huggingface-cli download city96/FLUX.1-dev-gguf \
  flux1-dev-Q4_K_S.gguf \
  --local-dir ~/ai-server/data/models/FLUX/gguf

✍️ FLUX.1 프롬프트 작성 가이드

💡
FLUX는 자연어 프롬프트에 최적화 — 키워드 나열 방식 비효율

SDXL/SD1.5는 “masterpiece, 8k, ultra realistic” 같은 키워드 나열이 효과적이지만, FLUX.1은 자연어 문장 형태로 상세하게 묘사하는 것이 훨씬 좋은 결과를 냅니다. “A professional portrait photo of a Korean woman in her 30s, wearing a white linen shirt, sitting in a bright modern cafe, shallow depth of field, natural window light” 처럼 시각적 상황을 완전한 문장으로 작성하세요.

SECTION05

이미지 생성 API 서버화 & Open WebUI 연동

ComfyUI와 A1111 모두 REST API를 제공합니다. 이를 Open WebUI에 연결하면 채팅에서 “이 콘셉트를 이미지로 만들어줘”라고 입력하는 것만으로 AI가 프롬프트를 생성하고 이미지까지 자동 생성합니다.

🔗 Open WebUI 이미지 생성 연동 설정

1
Open WebUI Admin Panel → Settings → Images
Image Generation Engine → AUTOMATIC1111 또는 ComfyUI 선택
2
API URL 입력
A1111: http://a1111:7860 / ComfyUI: http://comfyui:8188 (Docker 네트워크 내부 주소)
3
Save 후 채팅 테스트
채팅창에서 🖼️ 아이콘 클릭 → 이미지 생성 모드 활성화 → 프롬프트 입력

🐍 Python으로 A1111 API 직접 호출

python — A1111 REST API 이미지 생성
import requests
import base64
from PIL import Image
from io import BytesIO

A1111_URL = "http://localhost:7860"

def generate_image(prompt, negative_prompt="", steps=20, width=1024, height=1024):
    payload = {
        "prompt": prompt,
        "negative_prompt": negative_prompt,
        "steps": steps,
        "width": width,
        "height": height,
        "cfg_scale": 7,
        "sampler_name": "DPM++ 2M Karras",
        "batch_size": 1,
    }
    response = requests.post(
        f"{A1111_URL}/sdapi/v1/txt2img",
        json=payload,
        timeout=120
    )
    result = response.json()
    image_data = base64.b64decode(result["images"][0])
    image = Image.open(BytesIO(image_data))
    return image

# 사용 예시
img = generate_image(
    prompt="A futuristic AI server room with glowing blue lights, cinematic, 8k",
    negative_prompt="blurry, low quality, text",
    steps=30,
    width=1024,
    height=1024
)
img.save("output.png")
print("이미지 생성 완료!")
SECTION06

고급 기능 — LoRA · ControlNet · 업스케일 · 비디오

🎭 LoRA — 특정 스타일/인물 파인튜닝 적용

LoRA(Low-Rank Adaptation)는 특정 화풍, 캐릭터, 제품을 AI가 학습한 작은 모델 파일입니다. 기본 모델에 LoRA를 추가하면 특정 스타일을 즉시 적용할 수 있습니다.

📂 LoRA 파일 위치
A1111: /stable-diffusion-webui/models/Lora/
ComfyUI: /ComfyUI/models/loras/
✍️ LoRA 프롬프트 적용
프롬프트에 <lora:lora_filename:0.8> 추가. 숫자는 강도(0~1). CivitAI에서 다양한 LoRA 무료 다운로드 가능.

🎮 ControlNet — 포즈·구도 정밀 제어

ControlNet 유형입력효과활용
OpenPose포즈 스켈레톤인물 포즈 정확 제어인물 사진, 캐릭터
Canny엣지 맵구도·형태 유지제품 사진 리스타일
Depth깊이 맵원근감 보존인테리어, 건축
Lineart선화선화 채색일러스트, 애니
InPaint마스크 영역특정 부분만 변경배경 교체, 수정

🔍 Real-ESRGAN 업스케일링

bash — Real-ESRGAN Docker 업스케일러
# 512px 이미지를 4K로 업스케일 (Docker)
docker run --rm --gpus all \
  -v ~/ai-server/data/outputs:/input \
  -v ~/ai-server/data/upscaled:/output \
  xinntao/realesrgan:latest \
  -i /input/image.png \
  -o /output/upscaled.png \
  -n RealESRGAN_x4plus \
  -s 4                       # 4배 업스케일
✅ 이미지 생성 자동화 파이프라인 추천 조합
  • 블로그 썸네일 자동 생성 — n8n이 새 포스트 발행 시 → A1111 API로 썸네일 자동 생성 → WordPress 업로드
  • 제품 사진 배경 교체 — ControlNet Canny로 제품 형태 유지 → 배경만 교체 → 쇼핑몰 자동 업로드
  • 소셜미디어 콘텐츠 배치 생성 — ComfyUI API + n8n 워크플로우로 10~50장 배치 생성
  • Open WebUI 채팅 → 이미지 생성 자동화 — LLM이 프롬프트 최적화 → A1111로 생성 → 채팅창 표시
SECTIONA1

Whisper 음성 인식 서버 완전 구축

OpenAI Whisper는 99개 언어를 지원하는 최고 수준의 오픈소스 STT(Speech-to-Text) 모델입니다. faster-whisper는 CTranslate2 기반으로 원본보다 최대 4배 빠르면서 VRAM도 절반만 사용합니다. AI 서버에 설치하면 회의 녹음 파일을 올리는 것만으로 수 분 만에 완전한 한국어 자막이 완성됩니다.

🎙️ Whisper 모델 선택 가이드

모델VRAM속도한국어 정확도추천 용도
tiny~1GB⚡⚡⚡⚡⚡🟡 보통실시간 자막 (저지연 우선)
base~1GB⚡⚡⚡⚡🟡 양호빠른 받아쓰기
small~2GB⚡⚡⚡🟢 좋음일상 대화, 팟캐스트
medium~5GB⚡⚡🟢 우수한국어 추천 — 균형
large-v3~10GB🟢 최고회의록·전문 음성 최고 품질
large-v3-turbo~6GB⚡⚡🟢 최고large 품질 + 속도 개선판
yaml — Whisper API 서버 (faster-whisper + REST API)
services:
  whisper-api:
    image: fedirz/faster-whisper-server:latest-cuda
    container_name: whisper_api
    restart: unless-stopped
    ports:
      - "8000:8000"
    volumes:
      - whisper_models:/root/.cache/huggingface
    environment:
      - WHISPER__MODEL=large-v3-turbo    # 모델 선택
      - WHISPER__INFERENCE_DEVICE=cuda   # GPU 사용
      - WHISPER__COMPUTE_TYPE=float16    # FP16으로 VRAM 절약
      - WHISPER__LANGUAGE=ko             # 한국어 고정 (정확도↑)
      - WHISPER__BEAM_SIZE=5
      - WHISPER__VAD_FILTER=true         # 무음 구간 자동 제거
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    networks:
      - ai_net

volumes:
  whisper_models:

networks:
  ai_net:
    external: true
    name: ai_network

🐍 Whisper API 활용 — 파일 변환 & 실시간 스트리밍

python — Whisper REST API 활용 완전 예제
import httpx
import asyncio
from pathlib import Path

WHISPER_URL = "http://192.168.1.253:8000"

## ── 파일 음성 인식 ────────────────────────────────
async def transcribe_file(audio_path: str) -> dict:
    """오디오 파일 전체를 텍스트로 변환"""
    async with httpx.AsyncClient(timeout=300) as client:
        with open(audio_path, "rb") as f:
            response = await client.post(
                f"{WHISPER_URL}/v1/audio/transcriptions",
                files={"file": (Path(audio_path).name, f, "audio/mpeg")},
                data={
                    "model":           "large-v3-turbo",
                    "language":        "ko",
                    "response_format": "verbose_json",  # 타임스탬프 포함
                    "timestamp_granularities": ["segment", "word"],
                }
            )
    return response.json()

## ── 실시간 스트리밍 STT (마이크 입력) ───────────
async def realtime_stt():
    """마이크 실시간 입력을 스트리밍으로 전사"""
    import sounddevice as sd
    import numpy as np

    SAMPLE_RATE = 16000
    CHUNK_SIZE  = 1024
    BUFFER_SEC  = 3      # 3초 단위로 전송

    buffer = []
    async with httpx.AsyncClient(timeout=30) as client:
        def callback(indata, frames, time, status):
            buffer.extend(indata[:, 0].tolist())

        with sd.InputStream(samplerate=SAMPLE_RATE,
                            channels=1, callback=callback):
            print("🎙️ 실시간 STT 시작... (Ctrl+C로 종료)")
            while True:
                if len(buffer) >= SAMPLE_RATE * BUFFER_SEC:
                    audio_chunk = np.array(buffer[:SAMPLE_RATE * BUFFER_SEC],
                                          dtype=np.float32)
                    buffer = buffer[SAMPLE_RATE * BUFFER_SEC:]

                    # WAV 형식으로 변환 후 전송
                    import io, wave, struct
                    wav_buffer = io.BytesIO()
                    with wave.open(wav_buffer, 'wb') as wf:
                        wf.setnchannels(1)
                        wf.setsampwidth(2)
                        wf.setframerate(SAMPLE_RATE)
                        wf.writeframes(
                            struct.pack(f'{len(audio_chunk)}h',
                                       *[int(s * 32767) for s in audio_chunk])
                        )
                    wav_buffer.seek(0)

                    resp = await client.post(
                        f"{WHISPER_URL}/v1/audio/transcriptions",
                        files={"file": ("chunk.wav", wav_buffer, "audio/wav")},
                        data={"model": "large-v3-turbo", "language": "ko"}
                    )
                    text = resp.json().get("text", "").strip()
                    if text:
                        print(f"📝 {text}")

                await asyncio.sleep(0.1)

## ── 사용 예시 ─────────────────────────────────────
result = asyncio.run(transcribe_file("meeting_recording.mp3"))
print(f"전사 완료: {len(result['segments'])} 세그먼트")
for seg in result["segments"]:
    print(f"[{seg['start']:.1f}s ~ {seg['end']:.1f}s] {seg['text']}")

🔗 Open WebUI Whisper 연동 설정

1
Open WebUI Admin → Settings → Audio
STT Engine: OpenAI → API Base URL: http://whisper-api:8000/v1 → API Key: any (인증 없음)
2
모델 이름 입력
STT Model: large-v3-turbo → Save
3
채팅창 마이크 버튼 테스트
🎙️ 버튼 클릭 → 말하기 → 자동으로 텍스트 변환됨. 한국어로 말하면 한국어로 인식.
SECTIONA2

Kokoro TTS — 프라이빗 음성 합성 서버

Kokoro는 2024년 말 등장한 오픈소스 TTS 모델로, 82M 파라미터라는 초경량 구조에도 불구하고 ElevenLabs, Google TTS에 필적하는 자연스러운 음성을 생성합니다. 단 2GB VRAM으로도 실시간 스트리밍이 가능합니다.

yaml — Kokoro TTS API 서버
services:
  kokoro-tts:
    image: ghcr.io/remsky/kokoro-fastapi-cpu:latest  # CPU 버전 (GPU도 있음)
    container_name: kokoro_tts
    restart: unless-stopped
    ports:
      - "8880:8880"
    volumes:
      - kokoro_models:/app/models
    environment:
      - KOKORO_DEFAULT_VOICE=af_sarah   # 기본 성우
      - KOKORO_DEFAULT_SPEED=1.0
    networks:
      - ai_net

volumes:
  kokoro_models:

networks:
  ai_net:
    external: true
    name: ai_network
python — Kokoro TTS API 활용 + 스트리밍 재생
import httpx
import asyncio
import pyaudio  # pip install pyaudio

KOKORO_URL = "http://192.168.1.253:8880"

# 사용 가능한 성우 목록
VOICES = {
    "af_sarah":  "여성 (부드럽고 자연스러움)",
    "af_bella":  "여성 (밝고 활기참)",
    "am_adam":   "남성 (깊고 차분함)",
    "am_michael":"남성 (전문적, 뉴스 앵커 스타일)",
    "bf_emma":   "영국식 여성",
    "bm_george": "영국식 남성",
}

async def text_to_speech(
    text: str,
    voice: str = "af_sarah",
    speed: float = 1.0,
    output_file: str = None
) -> bytes:
    """텍스트를 음성으로 변환 (OpenAI 호환 API)"""
    async with httpx.AsyncClient(timeout=60) as client:
        response = await client.post(
            f"{KOKORO_URL}/v1/audio/speech",
            json={
                "model":  "kokoro",
                "input":  text,
                "voice":  voice,
                "speed":  speed,
                "response_format": "mp3",
            }
        )

    if output_file:
        with open(output_file, "wb") as f:
            f.write(response.content)
        print(f"✅ 저장 완료: {output_file}")
    return response.content

async def tts_streaming_playback(text: str, voice: str = "af_sarah"):
    """스트리밍으로 TTS 생성하면서 실시간 재생"""
    p       = pyaudio.PyAudio()
    stream  = None

    async with httpx.AsyncClient(timeout=60) as client:
        async with client.stream(
            "POST",
            f"{KOKORO_URL}/v1/audio/speech",
            json={
                "model":  "kokoro",
                "input":  text,
                "voice":  voice,
                "response_format": "pcm",  # 재생용 RAW PCM
            }
        ) as response:
            stream = p.open(
                format=pyaudio.paInt16,
                channels=1,
                rate=24000,
                output=True
            )
            async for chunk in response.aiter_bytes(chunk_size=4096):
                stream.write(chunk)

    if stream:
        stream.stop_stream()
        stream.close()
    p.terminate()

# 사용 예시
asyncio.run(text_to_speech(
    text="안녕하세요. AI 서버에서 생성된 음성입니다.",
    voice="af_sarah",
    output_file="output.mp3"
))

# Open WebUI 연동: Admin → Settings → Audio
# TTS Engine: OpenAI → Base URL: http://kokoro-tts:8880/v1
# TTS Model: kokoro → TTS Voice: af_sarah
SECTIONA3

실시간 음성 AI 어시스턴트 파이프라인

STT → LLM → TTS를 하나의 파이프라인으로 연결하면 마이크로 말하면 AI가 음성으로 응답하는 완전한 음성 어시스턴트가 완성됩니다. 핵심은 각 단계의 지연을 최소화하는 스트리밍 아키텍처입니다.

python — STT → LLM → TTS 실시간 파이프라인 (지연 최소화)
import asyncio
import httpx
import sounddevice as sd
import numpy as np
import pyaudio
import webrtcvad  # pip install webrtcvad — 음성 감지(VAD)

WHISPER_URL = "http://192.168.1.253:8000"
OLLAMA_URL  = "http://192.168.1.253:11434"
KOKORO_URL  = "http://192.168.1.253:8880"

SAMPLE_RATE = 16000
VAD_MODE    = 2  # 0~3, 높을수록 공격적 음성 감지
SILENCE_MS  = 800  # 800ms 침묵이면 발화 종료로 판단

class VoiceAssistant:
    def __init__(self):
        self.vad     = webrtcvad.Vad(VAD_MODE)
        self.history = []  # 대화 히스토리

    async def run(self):
        print("🎙️ 음성 AI 어시스턴트 시작 (말을 시작하면 자동 감지)")
        while True:
            # 1. 음성 감지 및 녹음
            audio_data = await self._record_until_silence()
            if audio_data is None:
                continue

            # 2. STT (Whisper)
            print("🔄 음성 인식 중...")
            user_text = await self._stt(audio_data)
            if not user_text.strip():
                continue
            print(f"👤 사용자: {user_text}")

            # 3. LLM 스트리밍 응답 (첫 문장 나오면 즉시 TTS 시작)
            print("🤖 AI 응답 생성 중...")
            await self._llm_and_tts(user_text)

    async def _record_until_silence(self) -> np.ndarray | None:
        """VAD로 발화 감지 후 침묵까지 녹음"""
        FRAME_DURATION_MS = 30  # 30ms 프레임
        FRAME_SIZE        = int(SAMPLE_RATE * FRAME_DURATION_MS / 1000)

        recording   = False
        audio_buffer = []
        silent_frames = 0
        MAX_SILENCE  = int(SILENCE_MS / FRAME_DURATION_MS)

        with sd.InputStream(samplerate=SAMPLE_RATE, channels=1,
                           dtype='int16', blocksize=FRAME_SIZE) as stream:
            while True:
                frame, _ = stream.read(FRAME_SIZE)
                is_speech = self.vad.is_speech(
                    frame.tobytes(), SAMPLE_RATE
                )

                if is_speech and not recording:
                    print("🎤 발화 감지...")
                    recording = True
                    audio_buffer = [frame]
                elif recording:
                    audio_buffer.append(frame)
                    if not is_speech:
                        silent_frames += 1
                        if silent_frames >= MAX_SILENCE:
                            return np.concatenate(audio_buffer).flatten()
                    else:
                        silent_frames = 0

    async def _stt(self, audio: np.ndarray) -> str:
        import io, wave, struct
        wav_buf = io.BytesIO()
        with wave.open(wav_buf, 'wb') as wf:
            wf.setnchannels(1)
            wf.setsampwidth(2)
            wf.setframerate(SAMPLE_RATE)
            wf.writeframes(audio.tobytes())
        wav_buf.seek(0)

        async with httpx.AsyncClient(timeout=60) as c:
            resp = await c.post(
                f"{WHISPER_URL}/v1/audio/transcriptions",
                files={"file": ("audio.wav", wav_buf, "audio/wav")},
                data={"model": "large-v3-turbo", "language": "ko"}
            )
        return resp.json().get("text", "")

    async def _llm_and_tts(self, user_text: str):
        """LLM 스트리밍 응답 → 문장 단위로 즉시 TTS 변환"""
        self.history.append({"role": "user", "content": user_text})

        sentence_buffer = ""
        full_response   = ""

        async with httpx.AsyncClient(timeout=120) as client:
            async with client.stream(
                "POST", f"{OLLAMA_URL}/api/chat",
                json={
                    "model":    "qwen2.5:7b",
                    "messages": self.history,
                    "stream":   True,
                    "options":  {"temperature": 0.7, "num_predict": 300}
                }
            ) as response:
                import json
                async for line in response.aiter_lines():
                    if not line:
                        continue
                    data  = json.loads(line)
                    token = data.get("message", {}).get("content", "")
                    sentence_buffer += token
                    full_response   += token

                    # 문장 구분자에서 TTS 즉시 실행 (지연 최소화)
                    for punct in [".", "!", "?", "。", "!", "?"]:
                        if punct in sentence_buffer:
                            parts = sentence_buffer.split(punct, 1)
                            sentence = parts[0] + punct
                            sentence_buffer = parts[1] if len(parts) > 1 else ""
                            if sentence.strip():
                                print(f"🔊 {sentence}")
                                await self._speak(sentence)
                            break

                # 남은 버퍼 처리
                if sentence_buffer.strip():
                    await self._speak(sentence_buffer)

        self.history.append({"role": "assistant", "content": full_response})

    async def _speak(self, text: str):
        """텍스트를 즉시 TTS로 변환 후 재생"""
        async with httpx.AsyncClient(timeout=30) as c:
            resp = await c.post(
                f"{KOKORO_URL}/v1/audio/speech",
                json={"model": "kokoro", "input": text,
                      "voice": "af_sarah", "response_format": "pcm"}
            )

        p      = pyaudio.PyAudio()
        stream = p.open(format=pyaudio.paInt16, channels=1,
                       rate=24000, output=True)
        stream.write(resp.content)
        stream.stop_stream()
        stream.close()
        p.terminate()

# 실행
asyncio.run(VoiceAssistant().run())
SECTIONA4

멀티모달 Vision AI 완전 정복

Ollama를 통해 실행되는 Gemma3, LLaVA, MiniCPM-V 같은 비전 모델은 이미지를 입력받아 내용을 설명하고, 질문에 답하고, 분석합니다. 사진만 올리면 자동으로 태그를 생성하거나, 화이트보드 사진을 텍스트로 변환하거나, 제품 이미지를 분석해 스펙을 추출하는 자동화가 가능합니다.

🔍 로컬 Vision 모델 비교

모델VRAM이미지 이해한국어특징
gemma3:12b10GB🟢 우수🟡 양호Ollama 기본 Vision. 균형 잡힌 성능
llava:13b10GB🟢 우수🟡 보통최초 오픈소스 Vision LLM. 안정적
llava:34b24GB🟢 최고🟡 보통GPT-4V에 근접. RTX 3090 필요
minicpm-v:8b6GB🟢 우수🟢 우수한국어 포함 다국어 최강 경량 모델
moondream:1.8b2GB🟡 보통🔴 약함초경량. 간단한 이미지 설명용
python — Vision AI 활용 완전 예제 (이미지 분석 + 자동화)
import httpx
import base64
import asyncio
from pathlib import Path

OLLAMA_URL = "http://192.168.1.253:11434"

def image_to_base64(image_path: str) -> str:
    with open(image_path, "rb") as f:
        return base64.b64encode(f.read()).decode("utf-8")

async def analyze_image(image_path: str, prompt: str,
                         model: str = "gemma3:12b") -> str:
    """이미지 분석 기본 함수"""
    img_b64 = image_to_base64(image_path)

    async with httpx.AsyncClient(timeout=120) as client:
        resp = await client.post(
            f"{OLLAMA_URL}/api/chat",
            json={
                "model": model,
                "messages": [{
                    "role":    "user",
                    "content": prompt,
                    "images":  [img_b64],
                }],
                "stream": False,
                "options": {"temperature": 0.1}
            }
        )
    return resp.json()["message"]["content"]

## ── 실전 활용 함수들 ──────────────────────────────

async def extract_text_from_image(image_path: str) -> str:
    """이미지에서 텍스트 추출 (화이트보드, 문서, 간판 등)"""
    return await analyze_image(
        image_path,
        """이미지에서 모든 텍스트를 정확하게 추출해줘.
        원본 형식과 줄 구분을 최대한 유지해서 출력해.
        텍스트만 출력하고 설명은 하지 마."""
    )

async def analyze_product(image_path: str) -> dict:
    """제품 이미지에서 스펙 정보 추출"""
    result = await analyze_image(
        image_path,
        """이 제품 이미지를 분석해서 JSON 형식으로 출력해줘:
        {
          "product_name": "제품명",
          "category": "카테고리",
          "visible_specs": ["스펙1", "스펙2"],
          "brand": "브랜드",
          "estimated_price_range": "가격대",
          "description": "간단한 설명"
        }
        JSON만 출력, 다른 텍스트 없이."""
    )
    import json
    try:
        return json.loads(result)
    except:
        return {"raw": result}

async def auto_tag_image(image_path: str) -> list[str]:
    """이미지 자동 태그 생성 (사진 라이브러리 자동화)"""
    result = await analyze_image(
        image_path,
        """이 이미지에 적합한 태그를 10~15개 생성해줘.
        쉼표로 구분해서 한 줄로 출력해. 예: 자연, 산, 일몰, 여행, 풍경
        태그만 출력해."""
    )
    return [tag.strip() for tag in result.split(",") if tag.strip()]

async def check_receipt(image_path: str) -> dict:
    """영수증 이미지에서 구매 정보 추출"""
    result = await analyze_image(
        image_path,
        """이 영수증을 분석해서 다음 JSON 형식으로 추출해줘:
        {
          "store_name": "가게명",
          "date": "날짜 (YYYY-MM-DD)",
          "items": [{"name": "항목명", "price": 숫자, "quantity": 숫자}],
          "total": 합계숫자,
          "payment_method": "결제수단"
        }"""
    )
    import json
    try:
        return json.loads(result)
    except:
        return {"raw": result}

## ── 폴더 전체 자동 분류 파이프라인 ───────────────
async def auto_classify_photos(folder: str):
    """폴더 내 모든 이미지 자동 분류 및 태그 생성"""
    image_extensions = {".jpg", ".jpeg", ".png", ".webp", ".heic"}
    results = []

    for img_path in Path(folder).iterdir():
        if img_path.suffix.lower() not in image_extensions:
            continue

        print(f"처리 중: {img_path.name}")
        tags = await auto_tag_image(str(img_path))
        results.append({
            "file":     img_path.name,
            "tags":     tags,
            "category": tags[0] if tags else "기타"
        })

    # 결과를 JSON으로 저장
    import json
    with open(f"{folder}/image_catalog.json", "w", encoding="utf-8") as f:
        json.dump(results, f, ensure_ascii=False, indent=2)
    print(f"✅ {len(results)}개 이미지 분류 완료")
    return results

# 사용 예시
asyncio.run(auto_classify_photos("/home/user/photos/2026"))
SECTIONA5

문서 OCR 자동화 파이프라인

스캔된 PDF, 사진으로 찍은 문서, 이미지 형태의 청구서를 AI가 자동으로 텍스트화하고 RAG 지식베이스에 인덱싱하는 파이프라인을 구축합니다. Vision LLM을 활용한 방식과 전통적인 OCR 엔진을 비교하고 최적 조합을 사용합니다.

OCR 방법속도정확도한국어적합 문서
Vision LLM (Gemma3)🟡 느림🟢 최고🟢 우수복잡한 레이아웃, 손글씨, 표
PaddleOCR🟢 빠름🟢 우수🟢 우수인쇄체 문서, 대량 처리
docTR🟢 빠름🟡 양호🟡 보통영수증, 청구서 등 표준 양식
Tesseract🟢 매우 빠름🟡 보통🔴 약함간단한 영문 텍스트
python — OCR 자동화 파이프라인 (PDF + 이미지 → RAG 자동 인덱싱)
from paddleocr import PaddleOCR     # pip install paddlepaddle paddleocr
from pdf2image import convert_from_path  # pip install pdf2image
from qdrant_client import QdrantClient
from sentence_transformers import SentenceTransformer
from pathlib import Path
import asyncio, uuid

# OCR 엔진 초기화 (한국어 + 영어)
ocr = PaddleOCR(
    use_angle_cls=True,
    lang='korean',
    use_gpu=True,
    show_log=False
)

embedder = SentenceTransformer("BAAI/bge-m3")
qdrant   = QdrantClient(host="192.168.1.253", port=6333,
                        api_key="your_api_key")

def ocr_image(image_path: str) -> str:
    """이미지에서 텍스트 추출 (PaddleOCR)"""
    result = ocr.ocr(image_path, cls=True)
    if not result or not result[0]:
        return ""
    lines = []
    for line in result[0]:
        text, confidence = line[1]
        if confidence > 0.7:  # 신뢰도 70% 이상만 포함
            lines.append(text)
    return "\n".join(lines)

def ocr_pdf(pdf_path: str) -> str:
    """PDF → 이미지 변환 → OCR"""
    images  = convert_from_path(pdf_path, dpi=300)
    all_text = []
    for i, img in enumerate(images):
        # PIL Image → 임시 파일 저장 → OCR
        tmp_path = f"/tmp/pdf_page_{i}.png"
        img.save(tmp_path, "PNG")
        page_text = ocr_image(tmp_path)
        if page_text:
            all_text.append(f"[페이지 {i+1}]\n{page_text}")
    return "\n\n".join(all_text)

async def ocr_and_index(file_path: str, category: str = "documents"):
    """OCR 추출 후 Qdrant 자동 인덱싱"""
    path = Path(file_path)
    print(f"📄 처리 중: {path.name}")

    # 1. OCR 추출
    if path.suffix.lower() == ".pdf":
        text = ocr_pdf(file_path)
    else:
        text = ocr_image(file_path)

    if not text.strip():
        print(f"⚠️ 텍스트 추출 실패: {path.name}")
        return

    # 2. 청킹 (OCR 텍스트는 고정 크기 청킹)
    chunk_size = 500
    chunks = [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)
              if text[i:i+chunk_size].strip()]

    # 3. 임베딩 및 Qdrant 삽입
    from qdrant_client.models import PointStruct
    vectors = embedder.encode(chunks, normalize_embeddings=True).tolist()

    points = [
        PointStruct(
            id=str(uuid.uuid4()),
            vector=vectors[i],
            payload={
                "content":     chunks[i],
                "source":      path.name,
                "source_path": str(file_path),
                "category":    category,
                "chunk_index": i,
                "total_chunks": len(chunks),
                "ocr_extracted": True,
            }
        )
        for i in range(len(chunks))
    ]
    qdrant.upsert(collection_name="ai_knowledge_base", points=points)
    print(f"✅ 인덱싱 완료: {path.name} ({len(chunks)} 청크)")

async def watch_and_index_folder(watch_dir: str):
    """폴더 감시 → 새 파일 자동 OCR + 인덱싱"""
    from watchdog.observers import Observer
    from watchdog.events import FileSystemEventHandler

    class Handler(FileSystemEventHandler):
        def on_created(self, event):
            if event.is_directory:
                return
            path = Path(event.src_path)
            if path.suffix.lower() in {".pdf", ".jpg", ".jpeg", ".png"}:
                asyncio.run(ocr_and_index(str(path)))

    observer = Observer()
    observer.schedule(Handler(), watch_dir, recursive=False)
    observer.start()
    print(f"👁️ 폴더 감시 시작: {watch_dir}")
    try:
        while True:
            await asyncio.sleep(1)
    except KeyboardInterrupt:
        observer.stop()

# 기존 폴더 전체 일괄 인덱싱
async def batch_index_folder(folder: str):
    supported = {".pdf", ".jpg", ".jpeg", ".png", ".tiff"}
    files = [f for f in Path(folder).iterdir()
             if f.suffix.lower() in supported]
    print(f"총 {len(files)}개 파일 처리 시작")
    for f in files:
        await ocr_and_index(str(f))

asyncio.run(batch_index_folder("/home/user/documents/scan"))
SECTIONA6

회의록 자동 생성 완전 자동화

회의 녹음 파일을 AI 서버에 업로드하는 것만으로 전사(Transcript) → 화자 분리 → 핵심 요약 → 액션 아이템 추출 → Notion/Obsidian 자동 저장까지 완전 자동화됩니다.

python — 회의록 자동 생성 완전 파이프라인
import httpx, asyncio, json
from datetime import datetime

WHISPER_URL = "http://192.168.1.253:8000"
OLLAMA_URL  = "http://192.168.1.253:11434"

async def generate_meeting_notes(audio_file: str,
                                  meeting_title: str = None) -> dict:
    """
    완전한 회의록 자동 생성 파이프라인
    반환: {transcript, summary, action_items, key_decisions, participants}
    """
    print(f"1️⃣ 음성 인식 시작: {audio_file}")

    # Step 1: Whisper로 전사 (타임스탬프 포함)
    async with httpx.AsyncClient(timeout=600) as client:
        with open(audio_file, "rb") as f:
            resp = await client.post(
                f"{WHISPER_URL}/v1/audio/transcriptions",
                files={"file": (audio_file, f, "audio/mpeg")},
                data={
                    "model":           "large-v3-turbo",
                    "language":        "ko",
                    "response_format": "verbose_json",
                }
            )
    transcript_data = resp.json()
    full_text = " ".join([s["text"] for s in transcript_data["segments"]])
    print(f"✅ 전사 완료: {len(transcript_data['segments'])} 세그먼트")

    # Step 2: LLM으로 구조화된 회의록 생성
    print("2️⃣ AI 회의록 분석 중...")
    analysis_prompt = f"""다음은 회의 녹취록입니다. 아래 JSON 형식으로 정리해줘.
모든 내용은 한국어로 작성하고, JSON만 출력해.

녹취록:
{full_text[:4000]}  

출력 형식:
{{
  "meeting_title": "회의 제목 (맥락에서 추론)",
  "duration_summary": "회의 시간대 요약",
  "participants_inferred": ["추론된 참석자 역할 (이름을 알 수 없으면 역할로)"],
  "executive_summary": "3~5문장 핵심 요약",
  "key_discussions": [
    {{"topic": "토픽", "summary": "논의 내용", "outcome": "결론"}}
  ],
  "action_items": [
    {{"task": "할 일", "assignee": "담당자", "deadline": "기한", "priority": "High/Mid/Low"}}
  ],
  "key_decisions": ["결정사항1", "결정사항2"],
  "next_meeting": "다음 회의 일정 (언급된 경우)",
  "unresolved_issues": ["미해결 사항"]
}}"""

    async with httpx.AsyncClient(timeout=120) as client:
        resp = await client.post(
            f"{OLLAMA_URL}/api/generate",
            json={
                "model":  "qwen2.5:14b",
                "prompt": analysis_prompt,
                "stream": False,
                "options": {"temperature": 0.1, "num_predict": 2000}
            }
        )

    raw_result = resp.json()["response"]
    # JSON 파싱
    import re
    json_match = re.search(r'\{.*\}', raw_result, re.DOTALL)
    meeting_notes = json.loads(json_match.group()) if json_match else {"raw": raw_result}

    # Step 3: 마크다운 회의록 생성
    date_str = datetime.now().strftime("%Y-%m-%d %H:%M")
    md = f"""# {meeting_notes.get('meeting_title', meeting_title or '회의록')}
    
**날짜:** {date_str}  
**참석자:** {', '.join(meeting_notes.get('participants_inferred', ['미확인']))}

---

## 📋 핵심 요약
{meeting_notes.get('executive_summary', '')}

## 💬 주요 논의사항
"""
    for d in meeting_notes.get("key_discussions", []):
        md += f"\n### {d.get('topic', '')}\n"
        md += f"- **논의:** {d.get('summary', '')}\n"
        md += f"- **결론:** {d.get('outcome', '')}\n"

    md += "\n## ✅ 액션 아이템\n"
    for item in meeting_notes.get("action_items", []):
        priority_emoji = {"High": "🔴", "Mid": "🟡", "Low": "🟢"}.get(
            item.get("priority", "Mid"), "⚪"
        )
        md += (f"- {priority_emoji} **{item.get('task', '')}** "
               f"— {item.get('assignee', '미배정')} "
               f"({item.get('deadline', '기한 미정')})\n")

    md += f"\n## 🎯 주요 결정사항\n"
    for dec in meeting_notes.get("key_decisions", []):
        md += f"- {dec}\n"

    md += f"\n---\n\n
\n📝 전체 녹취록 (클릭하여 펼치기)\n\n" md += full_text md += "\n
" # 파일 저장 output_file = f"meeting_{datetime.now().strftime('%Y%m%d_%H%M')}.md" with open(output_file, "w", encoding="utf-8") as f: f.write(md) print(f"✅ 회의록 저장: {output_file}") return {"markdown": md, "structured": meeting_notes, "transcript": full_text} # 실행 result = asyncio.run(generate_meeting_notes( "team_meeting_20260524.mp3", "2026년 5월 팀 주간 회의" )) print(result["markdown"][:500])
✅ 추가편 A 구축으로 가능해지는 것들
  • Open WebUI 채팅창에서 마이크로 말하기 → Whisper 인식 → Qwen 답변 → Kokoro 음성 응답
  • 회의 파일 업로드 → 자동 전사·요약·액션 아이템 → Notion 자동 저장
  • 스캔 문서 폴더 → 자동 OCR → Qdrant 인덱싱 → 즉시 RAG 검색 가능
  • 제품 사진 찍기 → Vision AI 분석 → 스펙·가격·태그 자동 추출
  • 영수증 사진 → 금액·항목 자동 추출 → 가계부 자동 기록
← 이전 편
2편 — Linux 기본설정·보안·네트워크·백업
다음 편 →
4편 — 자동화·Open WebUI·실전 워크플로우

Related Stories

Leave A Reply

Please enter your comment!
Please enter your name here

Stay on op - Ge the daily news in your inbox