• בלוג
  • כמה שכבות של בעיות

כמה שכבות של בעיות

18/05/2026

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

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

https://github.com/ynonp/langlets-rails/blob/c846dfe220fa9e25c1f54b3b3b22aaff1a463592/app/jobs/resynctimestampsjob.rb

class ResyncTimestampsJob < ApplicationJob
  queue_as :default

  def perform(course_id, json_data)
    course = Course.find(course_id)
    medium = course.medium

    Rails.logger.info "Starting ResyncTimestampsJob for course #{course.id} (#{course.slug})"

    phrases = medium.phrases.ordered_by_timestamp.to_a

    if phrases.length != json_data.length
      raise "Phrase count mismatch: medium has #{phrases.length} phrases, JSON has #{json_data.length} entries"
    end

    # First pass: build a hash of phrase.id => new timestamp
    updates = {}
    json_data.each_with_index do |entry, index|
      phrase = phrases[index]
      new_timestamp = entry["timestamp"]

      if new_timestamp.blank?
        raise "Missing timestamp at JSON index #{index}"
      end

      updates[phrase.id] = new_timestamp
      Rails.logger.info "Mapping phrase #{phrase.id} ('#{phrase.text_l1}') => #{new_timestamp}"
    end

    # Second pass: apply all updates
    updates.each do |phrase_id, new_timestamp|
      phrase = Phrase.find(phrase_id)
      old_timestamp = phrase.timestamp
      phrase.update!(timestamp: new_timestamp)
      Rails.logger.info "Updated phrase #{phrase_id} timestamp: #{old_timestamp} => #{new_timestamp}"
    end

    # Update lesson timestamps based on first and last phrase in each lesson
    course.lessons.includes(:activities).find_each do |lesson|
      lesson_phrase_ids = lesson.activities.joins(:phrases).pluck("phrases.id").uniq
      lesson_phrases = phrases.select { |p| lesson_phrase_ids.include?(p.id) }.sort_by(&:timestamp)

      if lesson_phrases.any?
        first_timestamp = lesson_phrases.first.timestamp
        last_timestamp = lesson_phrases.last.timestamp

        lesson.update!(start_timestamp: first_timestamp, end_timestamp: last_timestamp)
        Rails.logger.info "Updated lesson #{lesson.id} timestamps: #{first_timestamp} => #{last_timestamp}"
      end
    end

    Rails.logger.info "ResyncTimestampsJob completed for course #{course.id}. Updated #{updates.count} phrases."

  rescue => e
    Rails.logger.error "ResyncTimestampsJob failed for course #{course_id}: #{e.message}"
    Rails.logger.error e.backtrace.join("\n")
    raise e
  end
end

הקוד גרוע מהרבה בחינות ובדיוק מתאים לתבנית של כמה שכבות של באגים. בקצרה:

  1. כל הטקסט של הוידאו נטען לזכרון בשורה 10 (הופכת את כל השורות למערך).

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

  3. אחרי זה מעדכנים את כל הטקסטים לזמן החדש שלהם.

  4. בסיום רצים על השיעורים בקורס ומעדכנים את זמני ההתחלה והסיום שלהם כדי להתאים לזמנים החדשים מהקובץ.

למה AI הסתבך עם זה? אני לא יודע, אבל אני חושד שהקוד כולל כמה תבניות לא נכונות והיה קשה לפתור את הסבך. הבעיות המרכזיות בקוד:

  1. הוא ארוך מדי. אני יודע זה לא באג אבל זה חשוב, כי כשקוד ארוך מדי קשה להבין אותו.

  2. הוא עושה עבודה מיותרת - טוען לזכרון את כל הטקסטים ואז בונה את המילון, כל זה מבלבל את ה AI.

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

אבל כל פעם שביקשתי מ AI לתקן את 3 הוא הסתבך והקוד שנוצר לא עבד או לא עבד תמיד.

הפתרון כמובן היה לארגן מחדש את הקוד. ככה זה נראה אחרי שפירקתי אותו:

https://github.com/ynonp/langlets-rails/blob/main/app/jobs/resynctimestampsjob.rb

class ResyncTimestampsJob < ApplicationJob
  queue_as :default

  def perform(course_id, json_data)
    course = Course.find(course_id)
    medium = course.medium

    Rails.logger.info "Starting ResyncTimestampsJob for course #{course.id} (#{course.slug})"

    phrases = medium.phrases.ordered_by_timestamp.to_a
    verify_json_data!(json_data, phrases)

    # Update all phrase timestamps in a single pass
    json_data.zip(phrases).each do |entry, phrase|
      phrase.update!(timestamp: entry["timestamp"])
    end

    # Update lesson timestamps based on first and last phrase in each lesson
    course.sync_lesson_timestamps

    Rails.logger.info "ResyncTimestampsJob completed for course #{course.id}. Updated #{phrases.length} phrases."

  rescue => e
    Rails.logger.error "ResyncTimestampsJob failed for course #{course_id}: #{e.message}"
    Rails.logger.error e.backtrace.join("\n")
    raise e
  end

  private

  # Expected json_data structure:
  #   [{ "timestamp" => "00:01:23.456", ... }, ...]
  # One entry per phrase, ordered to match medium.phrases.ordered_by_timestamp.
  def verify_json_data!(json_data, phrases)
    if phrases.length != json_data.length
      raise "Phrase count mismatch: medium has #{phrases.length} phrases, JSON has #{json_data.length} entries"
    end

    missing = json_data.each_with_index.select { |entry, _| entry["timestamp"].blank? }
    if missing.any?
      raise "Missing timestamps at JSON indices: #{missing.map(&:last).join(', ')}"
    end
  end
end

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

  def sync_lesson_timestamps
    ordered_lessons = lessons.includes(activities: { activity_phrases: :phrase }).sort_by(&:order)

    # Precompute sorted unique phrases per lesson to avoid N+1 inside the loop
    lesson_phrases_map = ordered_lessons.to_h do |lesson|
      phrases = lesson.activities.flat_map { |a| a.activity_phrases.map(&:phrase) }.uniq.sort_by(&:timestamp)
      [lesson, phrases]
    end

    ordered_lessons.each_with_index do |lesson, index|
      lesson_phrases = lesson_phrases_map[lesson]
      next if lesson_phrases.empty?

      start_ts = lesson_phrases.first.timestamp

      end_ts = if (next_lesson = ordered_lessons[index + 1])
        next_phrases = lesson_phrases_map[next_lesson]
        next_phrases.any? ? next_phrases.first.timestamp : lesson_phrases.last.timestamp
      else
        Phrase.to_string_timestamp(lesson_phrases.last.timestamp_seconds + 5)
      end

      lesson.update!(start_timestamp: start_ts, end_timestamp: end_ts)
    end
  end

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

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