• בלוג
  • יום 11 - סידור סרטים בקורס

יום 11 - סידור סרטים בקורס

23/08/2026

מה תעשו אם העברתם קורס AI והקלטתם את כל ההרצאות שעברו בטימס ועכשיו יש לכם שעות של וידאו בלי אינדקס? תבנו סוכן AI כמובן. בדוגמה היום נבנה סוכן AI שלוקח הקלטה ארוכה ושובר אותה לחלקים קטנים בצירוף אינדקס טקסטואלי מלא לכל חלק כדי שאפשר יהיה לחזור רק לחלקים שאנחנו צריכים.

1. מה אנחנו בונים

נתונה הקלטה של 4 שעות ממפגש בקורס ואנחנו רוצים לפצל אותה לשיעורים קצרים, כל שיעור בן 5-10 דקות ולכל שיעור לצרף סיכום טקסט כתוב. משימה שביום רגיל היתה לוקחת כמה שעות לבן אדם ו AI יכול לעשות אותה בקלות בצורה עצמאית לגמרי. זה תהליך העבודה:

  1. חותכים את ההקלטה לקטעים של 30 דקות כדי שיהיה ל AI קל להתמודד איתם.
  2. אנחנו לא יודעים עדיין איפה הכי הגיוני לסמן "שיעור" לכן נדאג לחפיפה של 5 דקות בין הקטעים הקצרים וכך שיעור לא ייפול בדיוק באמצע ביניהם.
  3. נעלה כל קטע ל Gemini דרך Google File API.
  4. נבקש מהסוכן שיעבור על כל קטע וייתן לי את ה Timestamps בהם הכי הגיוני לחתוך את הקטע לשיעורים.
  5. נמזג ונחתוך את ההקלטה לפי הזמנים שקיבלנו מהסוכן ונצרף את הסיכומים.

קוד הדוגמה המלא זמין שוב בתיקיית הדוגמאות והפעם הסוכן כולו שמור בקובץ אחד:

https://github.com/ynonp/pydanticai-demos/blob/main/11-course-builder/build-course.py

נעבור יחד על החלקים המעניינים.

2. מבנה הפלט

הסוכן יחזיר רשימה של שיעורים וכל שיעור מכיל המון מידע: זמן ההתחלה שלו, זמן הסיום, אינדקס שלו, האם זה שיעור או הפסקה, מזהה שלו וגם סיכום השיעור. כל פריט מידע כזה מגיע במבנה מסוים ועם הוראות מסוימות ונוח לי לרשום את ההוראות שקשורות לכל שדה בתוך הקלאס שמגדיר את מבנה הפלט. פידנטיק מאפשר את זה באמצעות פקודת Field. זה נראה כך:

class Lesson(BaseModel):
    index: int = Field(description="1-based lesson number within this chunk")

קלאס השיעור המלא כולל כל ההסברים הוא הקלאס הראשון בקובץ:

class Lesson(BaseModel):
    """A single lesson extracted from the course video."""

    index: int = Field(description="1-based lesson number within this chunk")
    start_timestamp: str = Field(
        description="Start time relative to THIS CHUNK as HH:MM:SS, e.g. '00:02:30'"
    )
    end_timestamp: str = Field(
        description="End time relative to THIS CHUNK as HH:MM:SS, e.g. '00:14:45'"
    )
    is_break: bool = Field(
        default=False,
        description=(
            "True if this segment is a BREAK with no teaching content — e.g. "
            "students chatting, silence, instructor away, coffee/lunch break. "
            "False for a normal lesson."
        ),
    )
    slug: str = Field(
        description=(
            "URL-safe slug for the lesson, e.g. 'intro-to-pydantic'. "
            "If is_break is True, this is ignored (the slug 'breaktime' is "
            "applied automatically) — still provide a placeholder value."
        )
    )
    title: str = Field(description="Lesson title in the original language")
    summary_markdown_hebrew: str = Field(
        description=(
            "A full, self-contained lesson write-up in Hebrew markdown, written so "
            "that someone who never watched the video can read it and actually "
            "learn the material — NOT a table of contents or a bullet-point index "
            "of what was covered. Write it like an article/tutorial: "
            "Structure it as numbered sections ('### 1. <topic title>', "
            "'### 2. <topic title>', ...), one per sub-topic covered in the "
            "lesson, in the order they were taught. Under each heading, write "
            "full explanatory paragraphs in flowing Hebrew prose that actually "
            "teach the concept (what it is, why it matters, how it works) the "
            "way the instructor explained it — not short summaries or fragments. "
            "Include exact code shown in the video in fenced code blocks, "
            "including commands, filenames, and terminal output where relevant, "
            "each with a sentence or two explaining what the code does and why. "
            "Include any links or external references mentioned, and end with "
            "practical conclusions/recommendations if the instructor gave any. "
            "Be thorough, detailed, and written in the same didactic style as a "
            "professional programming course text (not a summary/recap). "
            "If is_break is True, skip all of this and just write a brief note "
            "that this was a break (e.g. 'הפסקה — אין תוכן לימודי בקטע זה.')."
        )
    )


class ChunkOutline(BaseModel):
    """Lessons found within a single video chunk."""

    lessons: list[Lesson] = Field(description="Ordered list of lessons in this chunk")

3. העלאת הקבצים לגוגל

לגוגל יש Google File API שמאפשר לנו לשמור קבצים כדי שג'מיני יוכל לקרוא אותם. המנגנון מותאם לסוכנים ולא דורש מאתנו למחוק את הקבצים, גוגל ימחקו אותם אוטומטית אחרי כמה שעות. מנגנונים דומים קיימים גם ב Claude וגם ב OpenAI. הפונקציה הבאה מעלה קובץ ושומרת את המזהה שלו לצורך העברה לסוכן בהמשך התוכנית:

def upload_chunk(chunk_path: str, chunk_index: int) -> UploadedFile:
    """Upload a single chunk to Google File API."""
    print(f"   📤 Uploading chunk {chunk_index} ({Path(chunk_path).name})...")
    client = genai.Client()
    t0 = time.time()

    uploaded = client.files.upload(
        file=chunk_path,
        config={"display_name": Path(chunk_path).name},
    )

    elapsed = time.time() - t0
    print(f"      ↑ {elapsed:.0f}s, state={uploaded.state.name}")

    while uploaded.state.name != "ACTIVE":
        time.sleep(5)
        uploaded = client.files.get(name=uploaded.name)

    print(f"      ✅ ACTIVE")
    return UploadedFile(
        file_id=uploaded.uri,
        provider_name="google",
        media_type=uploaded.mime_type or "video/mp4",
    )

אחרי העלאה Google File API צריך זמן לעבד את הקובץ וזו הסיבה ללולאת ההמתנה שאנחנו רואים שמחכה שהקובץ יהיה מוכן.

4. פיענוח השיעורים

החלק הבא הוא החלק המרכזי של התוכנית - מגדירים את הסוכן ומריצים אותו על Chunk שמכיל מספר שיעורים כדי להבין איזה שיעורים יש שם:

def build_agent() -> Agent:
    """Create the course-builder agent with Google Gemini."""
    model = GoogleModel("gemini-3-flash-preview")
    return Agent(
        model,
        output_type=ChunkOutline,
        system_prompt=(
            "You are an expert course builder and video content analyst. "
            "You receive a SEGMENT (chunk) of a longer course recording and must "
            "identify complete, self-contained lessons of 10-15 minutes each "
            "within this segment.\n\n"
            "CRITICAL RULES:\n"
            "- Timestamps MUST be relative to THIS CHUNK (00:00:00 = chunk start)\n"
            "- Only include lessons that are FULLY or MOSTLY contained in this chunk\n"
            "- If a lesson is cut off at the start or end, do NOT include it — "
            "the overlap with adjacent chunks will capture it\n"
            "- Each lesson should be 10-15 minutes of coherent content\n"
            "- Identify natural topic boundaries\n"
            "- Create descriptive, URL-safe English slugs (lowercase, hyphens)\n"
            "- Write comprehensive Hebrew markdown summaries including:\n"
            "  * Sub-topics covered\n"
            "  * ALL code snippets shown (in ``` code blocks)\n"
            "  * Links or references mentioned\n"
            "  * Key takeaways\n\n"
            "BREAK-TIME DETECTION:\n"
            "- Also detect BREAKS: segments with no teaching content, such as "
            "coffee/lunch breaks, silence, students chatting among themselves, "
            "the instructor stepping away, or any other non-lesson downtime.\n"
            "- Treat a break exactly like a lesson entry — give it a start/end "
            "timestamp — but set is_break=True and give it a short title such "
            "as 'הפסקה'. The slug field is ignored for breaks, just put any "
            "placeholder.\n"
            "- For a break's summary_markdown_hebrew, just write a brief note "
            "that this was a break (e.g. 'הפסקה — אין תוכן לימודי בקטע זה.'), "
            "no need for a full lesson write-up.\n"
            "- Only mark genuine downtime as a break — do not use it for slow "
            "or informal but still on-topic teaching."
        ),
    )


def analyze_chunk(
    agent: Agent, chunk: dict, chunk_index: int, total_chunks: int
) -> list[Lesson]:
    """
    Analyze one chunk → list of lessons with timestamps converted to
    original video time.
    """
    offset = chunk["offset_seconds"]
    video_file = upload_chunk(chunk["path"], chunk_index)

    print(f"   🤖 Analyzing chunk {chunk_index}/{total_chunks - 1} "
          f"(offset={seconds_to_ts(offset)})...")
    t0 = time.time()

    result = agent.run_sync(
        [
            (
                f"This is chunk {chunk_index} of a longer course video. "
                f"The chunk starts at {seconds_to_ts(offset)} in the original video. "
                f"Identify all complete 10-15 minute lessons within this chunk. "
                f"Timestamps must be relative to THIS CHUNK (00:00:00 = chunk start). "
                f"Do NOT include lessons that are cut off at chunk boundaries — "
                f"adjacent overlapping chunks will capture them."
            ),
            video_file,
        ]
    )

    elapsed = time.time() - t0
    chunk_lessons = result.output.lessons
    print(f"      ✅ {len(chunk_lessons)} lessons found in {elapsed:.0f}s")

    # Convert chunk-relative timestamps to original video timestamps
    converted = []
    for lesson in chunk_lessons:
        rel_start = ts_to_seconds(lesson.start_timestamp)
        rel_end = ts_to_seconds(lesson.end_timestamp)
        abs_start = rel_start + offset
        abs_end = rel_end + offset
        converted.append(Lesson(
            index=0,  # will be re-indexed later
            start_timestamp=seconds_to_ts(abs_start),
            end_timestamp=seconds_to_ts(abs_end),
            is_break=lesson.is_break,
            # Force the canonical slug for breaks in code, rather than
            # trusting the model to use the right literal string.
            slug="breaktime" if lesson.is_break else lesson.slug,
            title=lesson.title,
            summary_markdown_hebrew=lesson.summary_markdown_hebrew,
        ))

    return converted

המשך הקוד לוקח את כל המידע שהסוכן החזיר ובעזרתו שובר את ההקלטה הארוכה לקטעים קצרים ומוסיף את קבצי הסיכום. סך הכל התוכנית לא מאוד ארוכה (פחות מ 500 שורות) וחושפת את היופי בפיתוח סקריפטים היום: יש לנו יכולת חדשה שקודם לא היתה, היכולת להיעזר במודלי שפה כדי לפענח מידע. מתוך 500 שורות הרוב המוחלט הוא קוד פייתון שאפשר היה לכתוב גם לפני שלוש שנים והיה עובד אותו דבר - קריאת API אחת היא זו שמחזיקה את כל הקסם.