멘토링 내용 요약하는 프로그램 만들기

황토소금·2024년 11월 27일

TIL

목록 보기
48/49

들어가며

중학생을 멘토링하고 있는데 멘토링 보고서를 작성해야 한다. 그런데 보고서를 작성하기 너무 귀찮아서 자동화해보려고 한다.

현재 줌으로 진행해서 멘토링 내용은 동영상 파일로 존재한다.

초안 작성하기

cursor가 초안 작성하기에는 너무 훌륭한 것 같다.

로컬에 있는 mp4 파일을 읽어서 openAI api를 사용해서 해당 동영상 내용을 요약하는 간단한 파이썬 프로그램 작성하고 싶어

로 질문을 시작했다.

cursor가 짜준 전체적인 흐름은 다음과 같다.

  1. select_video_file - 비디오 파일을 선택한다.
  2. extract_audio(video_path, audio_path) - 비디오 파일에서 오디오를 추출한다.
  3. transcribe_audio(audio_path) - 오디오를 텍스트로 추출한다.
  4. summarize_text(transcript) - 텍스트를 요약한다.

괜찮은 것 같아서 추가 질문으로 좀 더 가다듬었다.

초안 가다듬기

#1

우선 파일 경로를 내가 입력해야 한다는 점이 불편해서 이 점을 개선해보려고 했다.

  1. 파일 선택은 gui로 finder 가 나와서 선택하면 어떨까.
    그런데 cursor는 tk라는 것을 사용해서 finder를 열려고 했는데 정확히 동작하지 않고, 나는 tk를 처음 보아서 이 방법은 접었다.
  2. 그래서 그냥 터미널에서 cli로 파일 탐색하듯이 고르면 괜찮지 않을까
    이번엔 cursor가 잘 만들어주었다. inquirer라는 라이브러리를 사용했는데 괜찮게 동작했다.

#2

그래서 이제 실행해보았는데 중간에 이런 에러가 발생했다.

You tried to access openai.Audio, but this is no longer supported in openai>=1.0.0 - see the README at https://github.com/openai/openai-python for the API.
You can run openai migrate to automatically upgrade your codebase to use the 1.0.0 interface.
Alternatively, you can pin your installation to the old version, e.g. pip install openai==0.28
A detailed migration guide is available here: https://github.com/openai/openai-python/discussions/742

초안에서 openai.Audio를 사용했는데 이게 deprecated된 것이다.
로그에서 알려주는 깃헙 링크로 들어가서 문서를 읽어보았다.

읽어보니 일단 migrate를 하라고 한다.
하라는 대로

openai migrate

를 실행하니...

Downloading Grit CLI from https://github.com/getgrit/gritql/releases/latest/download/marzano-aarch64-apple-darwin.tar.gz
Error: Failed to download Grit CLI from https://github.com/getgrit/gritql/releases/latest/download/marzano-aarch64-apple-darwin.tar.gz

이렇게 Grit CLI를 다운에 실패했다는 에러가 발생했다.

해당 에러에 관해
약 한달 전에 이슈가 올라온게 있었고 Grit 다운로드를 수동으로 하는 방법이 커멘트로 있었다.
https://github.com/openai/openai-python/issues/1838

솔루션만 떼오면 다음과 같았다!
1. curl -fsSL https://docs.grit.io/install | bash
2. source $HOME/.grit/bin/env
3. grit apply openai

사실 이 이슈까지 보니까 내 코드는 양이 그렇게 많지 않은데 change log 보고 바뀐 부분만 바꿔줘도 될 것 같긴 한데 이미 찾았으니까 migrate 써보려고 했다.

그런데 grit apply openai까지 하니까 후회한게 grit이 venv까지 다 검사를 해서

그래서 엄청나게 오래 걸렸다.
venv 파일을 다음부터는 내 로컬 저장소의 루트에 .venv 안에 둔다거나 해야겠다.

#3

migrate 다 하고 다시 실행해보았는데

transcript = client.audio.transcribe(file=audio_file,
             ^^^^^^^^^^^^^^^^^^^^^^^

AttributeError: 'Audio' object has no attribute 'transcribe'

migrate가 되었는데도 audio object가 transcribe 속성이 없다고 한다.

그래서 내부 코드를 찾아보았다.

class OpenAI(SyncAPIClient):
    ...
    audio: resources.Audio
    ...
	def __init__(
    ...
    ) -> None:
    ...
    self.audio = resources.Audio(self)

OpenAI라는 클래스에서 auodio는 resources.Audio라는 클래스의 인스턴스이다.
그럼 여기서 resources.Audio를 보면

class Audio(SyncAPIResource):
    @cached_property
    def transcriptions(self) -> Transcriptions:
        return Transcriptions(self._client)

    @cached_property
    def translations(self) -> Translations:
        return Translations(self._client)

    @cached_property
    def speech(self) -> Speech:
        return Speech(self._client)

    @cached_property
    def with_raw_response(self) -> AudioWithRawResponse:
        """
        This property can be used as a prefix for any HTTP method call to return the
        the raw response object instead of the parsed content.

        For more information, see https://www.github.com/openai/openai-python#accessing-raw-response-data-eg-headers
        """
        return AudioWithRawResponse(self)

    @cached_property
    def with_streaming_response(self) -> AudioWithStreamingResponse:
        """
        An alternative to `.with_raw_response` that doesn't eagerly read the response body.

        For more information, see https://www.github.com/openai/openai-python#with_streaming_response
        """
        return AudioWithStreamingResponse(self)

transcribe 메서드는 없고 대신 transcriptioins 메서드가 있는 것을 확인

transcriptioinsTranscriptions 함수에 self._client를 인자로 주고 냅다 리턴하는 것을 확인

이렇게 되면 transcriptions를 실행해도 audio file을 인자로 받지 않아서 어떻게 해야하는지 순간 당황

그래서 Transcriptions 함수를 보니까

class Transcriptions(SyncAPIResource):
    ...

    @overload
    def create(
        self,
        *,
        file: FileTypes,
        ...

create메서드에 file을 인자로 받는 것을 확인
file의 타입이 FileTypes인데 이걸 찾아보니

FileTypes = Union[
    # file (or bytes)
    FileContent,
    # (filename, file (or bytes))
    Tuple[Optional[str], FileContent],
    # (filename, file (or bytes), content_type)
    Tuple[Optional[str], FileContent, Optional[str]],
    # (filename, file (or bytes), content_type, headers)
    Tuple[Optional[str], FileContent, Optional[str], Mapping[str, str]],
]

다음과 같이 uion으로 4가지 경우를 받을 수 있게 했다
그래서 file에다가 fileContent를 그대로 주면 되는 것을 확인 -> 해당 사항 반영

그리고 그 외에

        model: Union[str, AudioModel],
        response_format: Union[Literal["json"], NotGiven] = NOT_GIVEN,
        language: str | NotGiven = NOT_GIVEN,
        prompt: str | NotGiven = NOT_GIVEN,
        temperature: float | NotGiven = NOT_GIVEN,
        timestamp_granularities: List[Literal["word", "segment"]] | NotGiven = NOT_GIVEN,
        # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
        # The extra values given here take precedence over values defined on the client or passed to this method.
        extra_headers: Headers | None = None,
        extra_query: Query | None = None,
        extra_body: Body | None = None,
        timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,

create 메서드에 모델, 언어 등을 넣어주면 되는 것을 확인

수정한 코드

    with open(audio_path, "rb") as audio_file:
        transcript = client.audio.transcriptions.create(file=audio_file,
        model="whisper-1",
        language="ko")

#4

그리고 다시 실행해보니...

openai.RateLimitError: Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.', 'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}}

You exceeded your current quota 라고해서 돈을 좀 채워넣었다.

#5

그리고 이제 다시 실행을 해보았는데

File "/summy.py", line 62, in transcribe_audio
transcript = client.audio.transcriptions.create(file=audio_file,
model="whisper-1",
language="ko")
...
openai.APIStatusError: Error code: 413 - {'error': {'message': '413: Maximum content size limit (26214400) exceeded (26277055 bytes read)', 'type': 'server_error', 'param': None, 'code': None}}

라는 에러가 발생했다.
transcription 과정에서 사이즈 리밋이 약 26메가 정도인데 동영상 속 음성을 추출한 mp3가 이것보다 큰 것이 문제

def split_audio(audio_path, max_size_bytes=25000000):  # 25MB보다 약간 작게 설정
    """오디오 파일을 청크로 분할"""
    audio = AudioSegment.from_mp3(audio_path)
    chunks = []
    
    # 1ms당 바이트 크기 계산 (근사치)
    bytes_per_ms = os.path.getsize(audio_path) / len(audio)
    
    # 청크의 길이(ms) 계산
    chunk_length_ms = int(max_size_bytes / bytes_per_ms)
    
    # 전체 길이를 chunk_length_ms로 나누어 청크 개수 계산
    total_chunks = math.ceil(len(audio) / chunk_length_ms)
    
    for i in range(total_chunks):
        start_time = i * chunk_length_ms
        end_time = min((i + 1) * chunk_length_ms, len(audio))
        chunk = audio[start_time:end_time]
        ...
        chunks.append(chunk_path)
    
    return chunks

그래서 추출된 mp3가 맥시멈 사이즈를 넘기지 않도록 25메가 크기만큼 chunk로 쪼개고

    chunks = split_audio(audio_path)
    
    # 각 청크의 텍스트를 저장할 리스트
    transcripts = []
    
    try:
        # 각 청크를 개별적으로 처리
        for chunk_path in chunks:
            ...
                transcripts.append(transcript.text)
        
        # 모든 텍스트를 하나로 합침
        return " ".join(transcripts)

각각 transcription하는 함수를 만들었다.

그리고 하는 김에 text summarize할 때도 맥시멈 인풋 토큰보다 텍스트가 크면 chunk로 쪼개서 요약하게 했다.

#6

드디어 동작은 잘 했지만...
결과가 심상치 않다.

=== 요약 결과 ===
이 텍스트는 kakaotalk 플러스친구에서 제공한 자막과 협찬에 대한 내용으로 시작한 뒤, 수학 문제를 푸는 대화로 이어집니다. 문제 번호 522번부터 시작하여 여러 수학 문제를 함께 풀면서 이야기하며 닮음, 비율 등 수학적 개념을 설명합니다. 또한, 숙제를 위한 문제 유형에 대한 언급과 시험 준비에 대한 대화가 포함되어 있습니다. 전체적으로 수학 문제 풀이와 관련된 학습 내용을 담고 있습니다.

output이 너무 구리다

그래서 프롬프팅을 조금 해주었다.

  1. 역할부여 : 당신은 전문적인 교육 멘토링 보고서 작성자입니다. 멘토링 세션의 녹음 내용을 명확하고 구조화된 보고서로 작성해주세요.
  2. 구조화 : 다음 형식으로 작성해주시되, 각 섹션을 상세하게 작성해주세요
  3. 예시 : 예시: 요약: 도형의 닮음 단원 진도 나갔습니다.

이정도 프롬프팅을 시도해보았다.

messages=[
                {
                    "role": "system",
                    "content": """당신은 전문적인 교육 멘토링 보고서 작성자입니다. 멘토링 세션의 녹음 내용을 명확하고 구조화된 보고서로 작성해주세요.

                    다음 형식으로 작성해주시되, 각 섹션을 상세하게 작성해주세요:

                    요약: [전체 멘토링 세션의 핵심 내용을 한 문장으로 간단명료하게 작성]

                    진행도: [
                    - 완료한 강의 번호
                    - 학습한 단원명
                    - 진행한 문제 유형이나 교재 정보
                    ]

                    멘토링 내용: [
                    - 학습한 주요 개념 설명
                    - 특별히 중점을 둔 학습 방법이나 전략
                    - 학생의 이해도 향상을 위해 사용한 구체적인 예시나 설명
                    - 발생한 어려움과 그 해결 방법
                    ]

                    시간별 진행 내용: [
                    구체적인 시간대별 활동을 다음과 같이 기록
                    XX분~XX분: [진행한 활동]
                    ]

                    예시:
                    요약: 도형의 닮음 단원 진도 나갔습니다.
                    진행도: 33강 ~ 36강, RPM 도형의 닮음 대표 유형 문제
                    멘토링 내용: 도형의 닮음은 도형의 합동에서 확장된 개념입니다. 그래서 도형의 합동을 바탕으로 닮음 문제를 풀었습니다. 이번 시간엔 문제를 풀며 특히 개념이 헷갈릴 때 어떻게 대처할 수 있는지에 대해 다뤘습니다...
                    시간별 진행 내용:
                    00분~05분: 근황 토크
                    05분~55분: 문제풀이
                    55분~60분: 다음 숙제 안내"""
                },
                {
                    "role": "user",
                    "content": chunk
                }
            ],

chunk를 나눌 때는 이렇게

messages=[
            {
                "role": "system",
                "content": """당신은 전문적인 교육 멘토링 보고서 작성자입니다. 멘토링 세션의 녹음 내용을 명확하고 구조화된 보고서로 작성해주세요.

                다음 형식으로 작성해주시되, 각 섹션을 상세하게 작성해주세요:

                요약: [전체 멘토링 세션의 핵심 내용을 한 문장으로 간단명료하게 작성]

                진행도: [
                - 완료한 강의 번호
                - 학습한 단원명
                - 진행한 문제 유형이나 교재 정보
                ]

                멘토링 내용: [
                - 학습한 주요 개념 설명
                - 특별히 중점을 둔 학습 방법이나 전략
                - 학생의 이해도 향상을 위해 사용한 구체적인 예시나 설명
                - 발생한 어려움과 그 해결 방법
                ]

                시간별 진행 내용: [
                구체적인 시간대별 활동을 다음과 같이 기록
                XX분~XX분: [진행한 활동]
                ]

                예시:
                요약: 도형의 닮음 단원 진도 나갔습니다.
                진행도: 33강 ~ 36강, RPM 도형의 닮음 대표 유형 문제
                멘토링 내용: 도형의 닮음은 도형의 합동에서 확장된 개념입니다. 그래서 도형의 합동을 바탕으로 닮음 문제를 풀었습니다. 이번 시간엔 문제를 풀며 특히 개념이 헷갈릴 때 어떻게 대처할 수 있는지에 대해 다뤘습니다...
                시간별 진행 내용:
                00분~05분: 근황 토크
                05분~55분: 문제풀이
                55분~60분: 다음 숙제 안내"""
            },
            {
                "role": "user",
                "content": final_text
            }
        ],

그랬더니

=== 요약 결과 ===
요약: 오늘 멘토링 세션에서는 닮음과 비율을 주제로 문제를 여러 개 풀면서 이해를 심화했습니다.
진행도:

  • 완료한 강의 번호: 522번 ~ 535번
  • 학습한 단원명: 닮음과 비율
  • 진행한 문제 유형이나 교재 정보: RPM 교재의 닮음 문제 (522번, 523번, 526번, 527번, 530번, 531번, 535번)
    멘토링 내용:
  • 오늘의 주요 개념은 삼각형의 닮음과 그에 따른 비율 계산이었습니다. 학생은 닮음의 기본 개념을 이해하고, 다양한 문제를 통해 그 적용 방법을 연습했습니다.
  • 특히, 문제를 풀 때는 각 대칭 변의 비율을 명확히 기억하고 적용하는 것이 중요하다는 점을 강조했습니다.
  • 구체적인 예시로는 "522번 문제"에서 삼각형의 닮음 비율을 a:b = 2:1로 설정하며 잘못된 비율로 접근할 수 있는 경우(예: 작은 삼각형과 큰 삼각형의 비율 혼동)를 설명했습니다.
  • 학생은 비율의 개념에서 약간의 혼란이 있었으나, 함께 문제를 풀면서 자연스럽게 해결되었습니다. 또한, 평행선과 관련된 조건이 추가될 때 발생할 수 있는 문제들을 함께 다뤘습니다.
    시간별 진행 내용:
  • 00분~05분: 근황 토크
  • 05분~20분: 522번 문제 풀이
  • 20분~40분: 523번, 526번 문제 풀이
  • 40분~50분: 527번, 530번, 531번 연속 문제 풀이
  • 50분~60분: 535번 문제 풀이 및 다음 숙제 안내
    이번 멘토링에서는 학생이 다양한 문제를 풀어보며 개념을 깊게 이해할 수 있도록 도왔습니다. 다음 세션에서는 추가 문제 풀이를 통해 학생의 이해도를 더욱 높이고, 실제 시험 대비를 할 수 있도록 할 예정입니다.

어느정도 괜찮은 결과가 나왔다.

결과

https://github.com/byungchanKo99/summy

멘토링 보고서 쓰기 싫어서 만들어보았는데 나름 재밌다.
실질적으로 중요한 로직들은 전부 cursor가 작성했고 문제 발생하는 부분 원인 찾고 해결하는데만 신경썼다.
약 3시간~4시간 정도 쓴 것 같다.
그리고 비용은 한 1시간짜리 동영상 요약을 5~6번 정도 한 것 같은데 1.5달러 정도 나간 것 같다.

재밌었음

더 발전시킬 부분

  1. 프롬프팅 다양화 - 몇가지 옵션을 정해두고 선택하면 좋을듯. 혹은 직접 입력
  2. 프롬프트를 직접 입력한다면 해당 입력을 바탕으로 프롬프트 고도화한 후 요약하는 로직을 넣으면 좋겠음
  3. 비디오에서 오디오 추출하고 텍스트로 변환하는 과정이 생각보다 엄청 오래걸림
  4. 파일 선택할 때 cli가 좀 구림

2025.01.09 추가
ModuleNotFoundError: No module named 'audioop' 라는 에러가 발생. 검색해보니 레딧에 나온대로 따라서 해당 라이브러리를 설치하니 해결됨.
https://www.reddit.com/r/discordbots/comments/1grhj51/heres_my_code_but_when_i_try_to_run_it_i_have_the/

profile
안녕하세요, 반갑습니다.

0개의 댓글