첫 AI 앱 만들기: AI 회의록 정리

첫 AI 앱 만들기: AI 회의록 정리 (Building Your First AI App)

기본을 배웠고 제공자 생태계도 이해했으니, 이제 실제로 무언가 만들어 볼게요: 오디오 파일을 받아 회의록(스크립트)을 만들고, 요약과 실행 항목을 생성해 주는 AI 미팅 노트 앱이에요.

이 프로젝트는 단일 앱 안에서 여러 전문화된 제공자를 사용하는 실전 AI 오케스트레이션을 보여줘요.

프로젝트 개요

우리 앱은 다음을 할 거예요:

  1. 웹 인터페이스에서 마이크 입력으로 오디오 받기
  2. 빠른 음성→텍스트 모델로 음성 전사(transcribe)
  3. 강력한 언어 모델로 요약 생성
  4. 공유하기 쉽게 웹에 배포

테크 스택: HTML/JavaScript (UI) + Inference Providers (AI)

UI는 단순하고 범용적으로 유지하려고 HTML과 JavaScript를 쓸 거예요. 더 성숙한 예시를 보고 싶다면 Hugging Face JS spaces 페이지를 확인해 보세요.

Step 1: 인증 설정

코딩을 시작하기 전에 CLI로 Hugging Face에 인증하세요:

pip install huggingface_hub
hf auth login

프롬프트가 나오면 Hugging Face 토큰을 붙여 넣으세요. 그러면 모든 inference 호출에 인증이 자동 처리돼요. 토큰은 설정 페이지에서 생성할 수 있어요.

앱에서는 토큰을 환경변수로 설정할 수 있어요.

export HF_TOKEN="your_token_here"
// Add your token at the top of your script
const HF_TOKEN = process.env.HF_TOKEN;

⚠️ 앱을 Hugging Face Spaces에 배포할 때는 토큰을 **시크릿(secret)**으로 추가해야 해요. 이건 코드에 토큰을 노출하지 않으면서 안전하게 다루는 방법이에요.

Step 2: 사용자 인터페이스 만들기

Gradio로 간단한 웹 인터페이스를 만들어 볼게요:

import gradio as gr
from huggingface_hub import InferenceClient

def process_meeting_audio(audio_file):
    """Process uploaded audio file and return transcript + summary"""
    if audio_file is None:
        return "Please upload an audio file.", ""

    # We'll implement the AI logic next
    return "Transcript will appear here...", "Summary will appear here..."

# Create the Gradio interface
app = gr.Interface(
    fn=process_meeting_audio,
    inputs=gr.Audio(label="Upload Meeting Audio", type="filepath"),
    outputs=[
        gr.Textbox(label="Transcript", lines=10),
        gr.Textbox(label="Summary & Action Items", lines=8)
    ],
    title="🎤 AI Meeting Notes",
    description="Upload an audio file to get an instant transcript and summary with action items."
)

if __name__ == "__main__":
    app.launch()

Gradio의 gr.Audio 컴포넌트로 오디오 파일 업로드나 마이크 입력을 처리해요. 출력은 두 가지 — 전사본과 실행 항목이 포함된 요약 — 로 단순하게 유지했어요.

JavaScript로는 네이티브 파일 업로드와 간단한 로딩 상태가 있는 깔끔한 HTML 인터페이스를 만들 거예요:

<body>
  <h1>🎤 AI Meeting Notes</h1>

  <div class="upload" onclick="document.getElementById('file').click()">
    <input type="file" id="file" accept="audio/*" />
    <p>Upload audio file</p>
    <button type="button">Choose File</button>
  </div>

  <div class="loading" id="loading">Processing...</div>

  <div class="results" id="results">
    <div class="result">
      <h3>📝 Transcript</h3>
      <div id="transcript"></div>
    </div>
    <div class="result">
      <h3>📋 Summary</h3>
      <div id="summary"></div>
    </div>
  </div>
</body>

이렇게 하면 전사본·요약을 위한 스타일된 결과 섹션이 있는 깔끔한 드래그앤드롭 인터페이스가 만들어져요.

이제 앱은 huggingface.jsInferenceClient를 써서 전사·요약 함수를 호출할 수 있어요.

import { InferenceClient } from "https://esm.sh/@huggingface/inference";

// Access the token from Hugging Face Spaces secrets
const HF_TOKEN = window.huggingface?.variables?.HF_TOKEN;
// Or if you're running locally, you can set it as an environment variable
// const HF_TOKEN = process.env.HF_TOKEN;

document.getElementById("file").onchange = async (e) => {
  if (!e.target.files[0]) return;

  const file = e.target.files[0];

  show(document.getElementById("loading"));
  hide(document.getElementById("results"), document.getElementById("error"));

  try {
    const transcript = await transcribe(file);
    const summary = await summarize(transcript);

    document.getElementById("transcript").textContent = transcript;
    document.getElementById("summary").textContent = summary;

    hide(document.getElementById("loading"));
    show(document.getElementById("results"));
  } catch (error) {
    hide(document.getElementById("loading"));
    showError(`Error: ${error.message}`);
  }
};

transcribesummarize 함수도 구현해야 해요.

Step 3: 음성 전사 추가하기

이제 OpenAI의 whisper-large-v3 모델로 빠르고 안정적인 음성 처리를 하는 전사를 구현할게요.

💡 auto 제공자를 써서 모델에 사용 가능한 첫 번째 제공자를 자동 선택하게 할 거예요. 제공자 우선순위 목록은 Inference Providers 페이지에서 정의할 수 있어요.

def transcribe_audio(audio_file_path):
    """Transcribe audio using fal.ai for speed"""
    client = InferenceClient(provider="auto")

    # Pass the file path directly - the client handles file reading
    transcript = client.automatic_speech_recognition(
        audio=audio_file_path,
        model="openai/whisper-large-v3"
    )

    return transcript.text

JavaScript 버전:

import { InferenceClient } from "https://esm.sh/@huggingface/inference";

async function transcribe(file) {
  const client = new InferenceClient(HF_TOKEN);

  const output = await client.automaticSpeechRecognition({
    data: file,
    model: "openai/whisper-large-v3-turbo",
    provider: "auto",
  });

  return output.text || output || "Transcription completed";
}

Step 4: AI 요약 추가하기

다음으로 DeepSeek의 deepseek-ai/DeepSeek-R1-0528 같은 강력한 언어 모델을 Inference Provider를 통해 쓸 거예요. 채팅 완성의 기본 정책인 :fastest는 이 모델에 가장 좋은 성능의 제공자를 자동 선택해요. 출력이 요약 + 실행 항목 + 결정 사항 형식이 되도록 커스텀 프롬프트를 정의할 거예요:

def generate_summary(transcript):
    """Generate summary using an Inference Provider"""
    client = InferenceClient(provider="auto")

    prompt = f"""
    Analyze this meeting transcript and provide:
    1. A concise summary of key points
    2. Action items with responsible parties
    3. Important decisions made

    Transcript: {transcript}

    Format with clear sections:
    ## Summary
    ## Action Items
    ## Decisions Made
    """

    response = client.chat.completions.create(
        model="deepseek-ai/DeepSeek-R1-0528:fastest",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=1000
    )

    return response.choices[0].message.content

JavaScript 버전:

async function summarize(transcript) {
  const client = new InferenceClient(HF_TOKEN);

  const prompt = `Analyze this meeting transcript and provide:
    1. A concise summary of key points
    2. Action items with responsible parties
    3. Important decisions made

    Transcript: ${transcript}

    Format with clear sections:
    ## Summary
    ## Action Items  
    ## Decisions Made`;

  const response = await client.chatCompletion(
    {
      model: "deepseek-ai/DeepSeek-R1-0528:fastest",
      messages: [
        {
          role: "user",
          content: prompt,
        },
      ],
      max_tokens: 1000,
    },
    {
      provider: "auto",
    }
  );

  return (
    response.choices?.[0]?.message?.content ||
    response ||
    "No summary available"
  );
}

Step 5: Hugging Face Spaces에 배포하기

배포하려면 app.py 파일을 만들고 Hugging Face Spaces에 업로드해야 해요.

완전한 app.py (Gradio 버전):

import gradio as gr
from huggingface_hub import InferenceClient

def transcribe_audio(audio_file_path):
    """Transcribe audio using an Inference Provider"""
    client = InferenceClient(provider="auto")

    # Pass the file path directly - the client handles file reading
    transcript = client.automatic_speech_recognition(
        audio=audio_file_path, model="openai/whisper-large-v3"
    )

    return transcript.text

def generate_summary(transcript):
    """Generate summary using an Inference Provider"""
    client = InferenceClient(provider="auto")

    prompt = f"""
    Analyze this meeting transcript and provide:
    1. A concise summary of key points
    2. Action items with responsible parties
    3. Important decisions made

    Transcript: {transcript}

    Format with clear sections:
    ## Summary
    ## Action Items
    ## Decisions Made
    """

    response = client.chat.completions.create(
        model="deepseek-ai/DeepSeek-R1-0528:fastest",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=1000,
    )

    return response.choices[0].message.content

def process_meeting_audio(audio_file):
    """Main processing function"""
    if audio_file is None:
        return "Please upload an audio file.", ""

    try:
        # Step 1: Transcribe
        transcript = transcribe_audio(audio_file)

        # Step 2: Summarize
        summary = generate_summary(transcript)

        return transcript, summary

    except Exception as e:
        return f"Error processing audio: {str(e)}", ""

# Create Gradio interface
app = gr.Interface(
    fn=process_meeting_audio,
    inputs=gr.Audio(label="Upload Meeting Audio", type="filepath"),
    outputs=[
        gr.Textbox(label="Transcript", lines=10),
        gr.Textbox(label="Summary & Action Items", lines=8),
    ],
    title="🎤 AI Meeting Notes",
    description="Upload audio to get instant transcripts and summaries.",
)

if __name__ == "__main__":
    app.launch()

앱은 포트 7860에서 실행되고 이런 모습이에요.

배포 단계:

  1. 새 Space 만들기: huggingface.co/new-space로 이동
  2. Gradio SDK 선택 후 public으로 만들기
  3. 파일 업로드: app.py 업로드
  4. 토큰 추가: Space 설정에서 HF_TOKEN을 시크릿으로 추가(설정에서 얻기)
  5. 런칭: 앱이 https://huggingface.co/spaces/your-username/your-space-name에서 라이브 됨

참고: 로컬에서는 CLI 인증을 썼지만, Spaces는 배포 환경에서 시크릿으로 토큰을 제공해야 해요.

JavaScript 배포용으로는 간단한 정적 HTML 파일을 만듭니다. 완전한 index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>🎤 AI Meeting Notes</title>
    <style>
      body {
        font-family: system-ui;
        max-width: 600px;
        margin: 50px auto;
        padding: 20px;
      }
      .upload {
        border: 2px dashed #ccc;
        padding: 40px;
        text-align: center;
        margin: 20px 0;
        cursor: pointer;
      }
      .upload:hover {
        border-color: #007bff;
      }
      button {
        background: #007bff;
        color: white;
        border: none;
        padding: 10px 20px;
        border-radius: 4px;
        cursor: pointer;
      }
      .loading {
        display: none;
        text-align: center;
        margin: 20px 0;
      }
      .results {
        display: none;
        margin-top: 20px;
      }
      .result {
        background: #f5f5f5;
        padding: 15px;
        margin: 10px 0;
        border-radius: 4px;
      }
      .error {
        color: red;
        background: #ffe6e6;
        padding: 15px;
        border-radius: 4px;
        display: none;
      }
      input[type="file"] {
        display: none;
      }
    </style>
  </head>
  <body>
    <h1>🎤 AI Meeting Notes</h1>

    <div class="upload" onclick="document.getElementById('file').click()">
      <input type="file" id="file" accept="audio/*" />
      <p>Upload audio file</p>
      <button type="button">Choose File</button>
    </div>

    <div class="loading" id="loading">Processing...</div>
    <div class="error" id="error"></div>

    <div class="results" id="results">
      <div class="result">
        <h3>📝 Transcript</h3>
        <div id="transcript"></div>
      </div>
      <div class="result">
        <h3>📋 Summary</h3>
        <div id="summary"></div>
      </div>
    </div>

    <script type="module">
      import { InferenceClient } from "https://esm.sh/@huggingface/inference";

      // Access the token from Hugging Face Spaces secrets
      const HF_TOKEN = window.huggingface?.variables?.HF_TOKEN;

      // Add error handling for missing token
      if (!HF_TOKEN) {
        showError("HF_TOKEN not configured. Please add it in Space settings.");
        return;
      }

      document.getElementById("file").onchange = async (e) => {
        if (!e.target.files[0]) return;

        const file = e.target.files[0];

        show(document.getElementById("loading"));
        hide(
          document.getElementById("results"),
          document.getElementById("error")
        );

        try {
          console.log("🎤 Starting transcription...");
          const transcript = await transcribe(file);
          console.log(
            "✅ Transcription completed:",
            transcript.substring(0, 100) + "..."
          );

          console.log("🖊️ Starting summarization...");
          const summary = await summarize(transcript);
          console.log(
            "✅ Summary completed:",
            summary.substring(0, 100) + "..."
          );

          document.getElementById("transcript").textContent = transcript;
          document.getElementById("summary").textContent = summary;

          hide(document.getElementById("loading"));
          show(document.getElementById("results"));
        } catch (error) {
          hide(document.getElementById("loading"));
          showError(`Error: ${error.message}`);
        }
      };

      async function transcribe(file) {
        const client = new InferenceClient(HF_TOKEN);

        const output = await client.automaticSpeechRecognition({
          data: file,
          model: "openai/whisper-large-v3-turbo",
          provider: "auto",
        });

        return output.text || output || "Transcription completed";
      }

      async function summarize(transcript) {
        const client = new InferenceClient(HF_TOKEN);

        const prompt = `Analyze this meeting transcript and provide:
            1. A concise summary of key points
            2. Action items with responsible parties
            3. Important decisions made

            Transcript: ${transcript}

            Format with clear sections:
            ## Summary
            ## Action Items  
            ## Decisions Made`;

        const response = await client.chatCompletion(
          {
            model: "deepseek-ai/DeepSeek-R1-0528:fastest",
            messages: [
              {
                role: "user",
                content: prompt,
              },
            ],
            max_tokens: 1000,
          },
          {
            provider: "auto",
          }
        );

        return (
          response.choices?.[0]?.message?.content ||
          response ||
          "No summary available"
        );
      }

      const show = (el) => (el.style.display = "block");
      const hide = (...els) => els.forEach((el) => (el.style.display = "none"));
      const showError = (msg) => {
        const error = document.getElementById("error");
        error.innerHTML = msg;
        show(error);
      };
    </script>
  </body>
</html>

배포 단계:

  1. 새 Space 만들기: huggingface.co/new-space로 이동
  2. Static SDK 선택 후 public으로 만들기
  3. 파일 업로드: index.html 업로드
  4. 토큰을 시크릿으로 추가: Space 설정에서 HF_TOKENSecret으로 추가
  5. 런칭: 앱이 https://huggingface.co/spaces/your-username/your-space-name에서 라이브 됨

참고: 토큰은 Hugging Face Spaces가 안전하게 관리하고 window.huggingface.variables.HF_TOKEN으로 접근합니다.

다음 단계

축하해요! 실무 작업을 처리하고, 전문적인 인터페이스를 제공하고, 자동 스케일링되고, 비용 효율적인 프로덕션 레디 AI 애플리케이션을 만들었어요. 더 많은 제공자를 탐색하려면 Inference Providers 페이지를 확인하세요. 다음 단계 아이디어:

  • 프롬프트 개선: 사용 사례에 맞게 다른 프롬프트를 시도
  • 다른 모델 시도: 다양한 음성·텍스트 모델 실험
  • 성능 비교: 제공자 간 속도 vs 정확도 벤치마크

더 알아보기 (Learn more)

출처: 공식문서