מאמר טכני

RapidOCR in-process ב-HotPDF: OCR של DLL מקורי ב-Delphi

HotPDF הופך עמודי PDF סרוקים לניתנים לחיפוש עם RapidOCR בתוך התהליך דרך HPDFCreateRapidOCRDLLOCREngine, מפעל שנוסף ב-v2.774.0 שטוען את HotPDFRapidOCR.dll, מחזיק את מודלי ה-detection, סיווג הזווית וה-recognition של ONNX תושבי זיכרון, ומחזיר IHPDFOCREngine. את המנוע הזה מעבירים אל THotPDF.ApplyLoadedOCRTextLayer, שמרנדר כל עמוד, מריץ inferencing של CPU בלי Python או תהליך בן, וקובע שכבת טקסט Unicode בלתי נראית

המניע הוא עלות לעמוד. מתאם ה-process של RapidOCR שיצא קודם, HPDFCreateRapidOCREngine, מריץ worker של Python עבור כל קריאת Recognize, וה-worker הזה מייבא את הראנטיים שלו וטוען את מודלי ה-ONNX שלו לפני שהוא קורא פיקסל בודד. על ארכיון בן 500 עמודים מס ההפעלה הזה חוזר 500 פעמים, והפצה פירושה לשלוח סביבת Python לצד קובץ הפעלה של Delphi. ה-DLL המקורי טוען את המודלים פעם אחת, בעת יצירת המנוע, וההפצה מתכווצת ל-DLL, לקבצי המודל שלו ולמילון תווים. מה שמוותרים עליו בתמורה הוא היכולת להרוג מזהה שנתקע, ורוב ההנדסה במתאם הזה עוסקת בלחיות עם זה ביושר

איך הופכים PDF סרוק לניתן לחיפוש עם ה-DLL של RapidOCR?

יצירת PDF ניתן לחיפוש עם ה-DLL המקורי של RapidOCR דורשת קריאת מפעל אחת ואת קריאת ה-ApplyLoadedOCRTextLayer שכל מנוע OCR של HotPDF משתמש בה. המפעל יושב ביחידת ה-HPDFRapidOCRRecognition ומאמת בחפזון: ה-DLL וספריית המודלים חייבים להתקיים, כל קובץ מודל ומילון חייב להיפתר, גרסת ה-ABI חייבת להיות 1, וכל ה-exports הנדרשים חייבים להימצא לפני שמודל כלשהו מאותחל. טעויות הגדרות מעלות EArgumentException; מודל שנכשל בטעינה מעלה EInvalidOperation שנושא את טקסט הדיאגנוסטיקה שה-DLL כתב

רצף האימות של מפעל ה-DLL של RapidOCR ב-HotPDF עבור HPDFCreateRapidOCRDLLOCREngine: נתיבים וקבצי מודל חייבים להתקיים, HPDFRapidOCRAbiVersion חייב להחזיר 1, ה-exports הנדרשים חייבים להיפתר, ו-HPDFRapidOCRCreate חייב לאתחל את המודלים, כש-EArgumentException או EInvalidOperation מועלים בחפזון לפני שרינדור זיהוי כלשהו רץ, והאחרון נושא את טקסט הדיאגנוסטיקה המקורי
האימות חפוז בכוונה: בעיות הגדרות מעלות חריגה לפני שמודל כלשהו מאותחל, כך שנתיב או ABI פגומים לעולם לא מגיעים אל דדליין זיהוי
uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // המודלים נטענים כאן, מחוץ לכל דדליין זיהוי.
  // שמות מודל יחסיים ב-THPDFRapidOCRDLLOptions.Default נפתרים
  // ביחס לספריית המודלים.
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models');
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    if Doc.LoadFromFile(SourceFile) < 1 then
      raise Exception.Create('Cannot load ' + SourceFile);
    Options := THPDFOCRTextLayerOptions.Default;  // 300 DPI, MinimumConfidence 0.5
    // רשימת עמודים ריקה פירושה כל עמוד; עמודים שכבר יש להם טקסט מדולגים
    if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
      raise Exception.Create(string(Info.Diagnostic));
    Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
      ' lines accepted, ', Info.DroppedWordCount, ' dropped');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

THPDFRapidOCRDLLOptions.Default מציין את ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx ו-ppocr_keys_v1.txt, עם thread אחד של CPU, מגבלת קלט של 16,777,216 פיקסלים ודדליין זיהוי של 60,000 ms. מאז v2.775.0, THPDFRapidOCRDLLOptions.ForLanguage מחליף במודל זיהוי ומילון תואמים עבור סינית מסורתית, רוסית, יפנית, ערבית ופרופילים נוספים; למה המודל והמילון חייבים להשתנות יחד מכוסה ב-מודלי RapidOCR רב-לשוניים ומילוני CTC ב-HotPDF. המנוע מדווח על עצמו בתור RapidOCR (native DLL) ב-Info.EngineName, מה שמשאיר את הלוגים חד-משמעיים לצד מתאם ה-process של Tesseract OCR החיצוני ו-מנוע ה-OCR מבוסס התאמת תבניות המובנה

למה ה-C ABI מדבר רק int32_t ובייטים של UTF-8?

ה-ABI של HotPDFRapidOCR.dll משתמש רק בשלמים ברוחב קבוע, במצביעים גולמיים ובאורכי בייטים מפורשים כי Delphi, C++Builder ו-Free Pascal לא חולקים עם MSVC דבר מעבר לקונבנציית הקריאה של C. ל-std::string, ל-std::vector או לחריגה של C++ יש פריסה ומודל פריקה ששייכים למהדר אחד ולספריית ראנטיים אחת. תנו לאחד מהם לחצות את הגבול והכישלון יהיה stack מושחת או בלוק heap ששוחרר על ידי ה-allocator הלא נכון, לא שגיאה נקייה

גרסת ABI 1 נאמנה אפוא לרשימה קצרה של כללים. כל export הוא cdecl ומחזיר status של int32_t, כאשר 1 פירושו הצלחה ו-0 כישלון. כל פונקציה שעלולה להיכשל מקבלת חוצץ דיאגנוסטיקה בבעלות הקורא ואת קיבולתו בבייטים; ה-DLL כותב הודעת UTF-8 המסתיימת ב-NUL וקטועה כדי להיכנס, והמתאם מפענח אותה עם מסיים קשיח בבייט האחרון של חוצץ 4,096 הבייטים שלו. גוף כל export עטוף ב-try עם גם catch (const std::exception &) וגם catch (...), כך ששגיאת ONNX Runtime, אסרט של OpenCV או מילון לא תקין הופכים ל-status 0 בתוספת טקסט, ולעולם לא לחריגה שבורחת אל קוד Pascal

Exportתפקידמתי המתאם פותר אותו
HPDFRapidOCRAbiVersionמחזיר 1; כל ערך אחר נדחהראשון, לפני כל דבר אחר
HPDFRapidOCRCreateטוען detection, classification אופציונלי, מודלי recognition ואת המילוןבמפעל
HPDFRapidOCRRecognizeמריץ bitmap אחד ופולט callback אחד לכל שורת טקסטבמפעל
HPDFRapidOCRDestroyמשחרר את מופע המודלבמפעל
HPDFRapidOCRSetReadingDirectionסדר שורות מימין לשמאל אופציונלי, נוסף ב-v2.775.0רק כש-RightToLeft מוגדר

ה-export האופציונלי נפתר בעצלות בכוונה: DLL של v2.774.0 שחסר אותו עדיין משרת בקשות משמאל לימין. ה-DLL נטען עם LoadLibraryEx עם דגלי חיפוש שמכסים את תיקיית ה-DLL עצמה בתוספת הספריות הבטוחות כברירת מחדל, כך שתלות ONNX Runtime או OpenCV שמוצבות לצד HotPDFRapidOCR.dll נמצאות בלי לגעת ב-PATH. נתיבי מודל ומילון נעים בתור UTF-8 וה-DLL ממיר אותם עם MultiByteToWideChar במצב מחמיר לפני פתיחת קבצים דרך APIs של תווים רחבים, כך שספריית מודלים תחת שם משתמש סיני או קירילי עובדת במקום להתרחב בייט בבייט אל גיבוב

כלל אחד חי בבנייה ולא בכותרת. ה-DLL מקשר סטטית את ONNX Runtime ו-OpenCV, והגדרת ה-CMake כברירת מחדל משתמשת ב-CRT הסטטי של release (/MT). ספריות סטטיות שהודרו מול /MD המעורבות בתוך DLL של /MT מניבות שגיאות קישור במקרה הטוב ושני heap בלתי תלויים במקרה הרע, ולכן הספריות שמסופקות חייבות להתאים לכל מצב CRT שה-DLL משתמש בו

מה קורה בין TBitmap לשורת טקסט?

HotPDF מוסר ל-DLL snapshot עצמאי top-down ב-BGR של העמוד המרונדר, וה-DLL משיב callback אחד לכל שורת טקסט שזוהתה עם טקסט UTF-8 בהשאלה שהמתאם חייב להעתיק לפני החזרה

ב-Delphi המתאם משייך את bitmap העמוד ל-TBitmap פרטי, כופה pf24bit, וקורא שורות עם GetDIBits בשימוש ב-biHeight שלילי, מה שמניב שורות top-down מרופדות ליישור של ארבעה בייטים; ה-stride הזה מועבר במפורש. ב-FPC הקריאה עוברת דרך CreateIntfImage, כי כתיבת scanline של LCL יכולה לעדכן את התמונה הגולמית בלי לרענן את ה-handle של ה-GDI. ה-bitmap של הקורא לעולם לא משתנה, ותקציב הפיקסלים (MaxPixels, 16,777,216 כברירת מחדל וניתן להגדרה עד 67,108,864) ומגבלת 32,767 הפיקסלים לממד נבדקים לפני שחוצץ ה-snapshot מוקצה

צינור ה-DLL של RapidOCR ב-HotPDF מ-bitmap לשכבת טקסט: המתאם מצלם snapshot של העמוד בתור top-down pf24bit BGR, ה-DLL מרפד, מאתר, מסדר ומזהה חיתוכים, מספק callback אחד לכל שורה עם טקסט UTF-8 בהשאלה, box ו-confidence, והמתאם מאמת כל שורה לפני קביעת שכבת הטקסט
פיקסלים חוצים את ה-ABI פעם אחת בתור snapshot, שורות חוזרות callback אחד בכל פעם, ושום דבר לא מגיע אל השכבה הניתנת לחיפוש עד שכל בדיקה עוברת

בתוך ה-DLL ה-snapshot מרופד ב-50 פיקסלים לבנים, אזורי טקסט מזוהים עם צלע מקסימלית של 1,024 פיקסלים, תיבות מסודרות לשורות אופקיות, וכל חיתוך מסובב באופן אופציונלי על ידי מסווג הזוויות לפני הזיהוי. כל שורת טקסט עוברת אז callback שמקבל const char*, מספר בייטים, תיבת מספרים שלמים בפיקסלים של התמונה המקורית, ואת ממוצע ביטחון התווים. מצביע הטקסט תקף רק במהלך ה-callback, ולכן המתאם מעתיק אותו מיד, והוא מחמיר לגבי מה שהוא מקבל:

  • UTF-8 מפוענח עם MB_ERR_INVALID_CHARS; רצף פגום מכשיל את העמוד במקום לייצר תווי החלפה בשכבה ניתנת לחיפוש
  • תווי בקרה מסוג C0 ו-C1 נדחים, ושורות של רווח בלבד מדולגות
  • התיבה חייבת לשכון בתוך ה-bitmap והביטחון חייב להיות ערך סופי בין 0 ל-1
  • הטקסט נספר מול MaxTextCodeUnits של הבקשה עם תקרה קשיחה של 1,048,576 יחידות UTF-16 לקריאה, ותווים ממישור משלים עולים שתי יחידות
  • כל חריגה של Pascal בתוך ה-callback נתפסת שם, נשמרת והופכת להחזרת 0, מה שגורם ל-DLL לעצור ולדווח כישלון; ההודעה השמורה הופכת אז לדיאגנוסטיקה

שתי השלכות משנות עבור כיוונון. ראשונה, יחידת הפלט היא שורה ולא מילה: כל שורה צורכת slot אחד של MaxWords, Info.AcceptedWordCount ו-Info.DroppedWordCount סופרים שורות, והדגשת חיפוש משתרעת על תיבת השורה. שנייה, MinimumConfidence (0.5 כברירת מחדל) מושווה מול ממוצע ביטחון התווים של השורה, כך ששורה עם תו אחד בלתי קריא בין עשרים נקיים בדרך כלל שורדת. ה-DLL לא מספק בסיס, ולכן צינור שכבת הטקסט מעריך אחד מתוך התיבה. עמוד ריק מצליח עם אפס שורות, וכל כישלון מנקה תוצאות חלקיות כך שהקביעה המרובת עמודים נשארת הכול-או-כלום

בעלות על מודלים ו-thread safety

כל מנוע DLL של RapidOCR בבעלותו מופע מודל אחד בדיוק לכל אורך חייו, וקריאות Recognize על אותו מנוע ממוספרות על ידי critical section. החזקת הממשק IHPDFOCREngine היא מה שמשאיר את המודלים חמים, ולכן התבנית הנכונה לעבודה אצוותית היא ליצור את המנוע פעם אחת ולעשות בו שימוש חוזר על פני מסמכים

procedure OcrBatch(const Files: TStrings; const OutputDir: string);
var
  Models: THPDFRapidOCRDLLOptions;
  Engine: IHPDFOCREngine;
  Doc: THotPDF;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
  I: Integer;
begin
  Models := THPDFRapidOCRDLLOptions.Default;
  Models.UseAngleClassifier := False;    // סריקות ישרות: לא נטען מודל classifier
  Models.Threads := 4;                   // 1..64, חסום במספר המעבדים הלוגיים
  Models.TimeoutMilliseconds := 120000;  // לכל קריאת Recognize, בשיתוף פעולה
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
  Options := THPDFOCRTextLayerOptions.Default;
  for I := 0 to Files.Count - 1 do
  begin
    Doc := THotPDF.Create(nil);
    try
      Doc.AutoLaunch := False;
      if (Doc.LoadFromFile(Files[I]) > 0) and
        Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
        Doc.SaveLoadedDocument(IncludeTrailingPathDelimiter(OutputDir) +
          ExtractFileName(Files[I]))
      else
        Writeln(Files[I], ': ', string(Info.Diagnostic));
    finally
      Doc.Free;
    end;
  end;
end;  // ההפניה האחרונה משוחררת: המודלים נהרסים, ואז ה-DLL נפרק

ערך ה-Threads קובע הן את מספרי ה-threads של ה-intra-op והן של ה-inter-op בכל סשן ONNX, וה-DLL לוחץ אותו אל מספר המעבדים הפעיל. שני threads שחולקים מנוע אחד לא רצים במקביל; השני ממתין לנעילה. ההמתנה הזאת אינה EnterCriticalSection עיוורת: המתאם קורא ל-TryEnterCriticalSection כל 25 ms ובודק את token הביטול ואת הדדליין בין ניסיונות, כך שבקשה בתור עדיין יכולה להתבטל או לפוג. אם צריך מקביליות אמיתית, יוצרים מנוע אחד לכל worker ומקבלים שכל מנוע מחזיק עותק משלו של המודלים בזיכרון

סדר הפירוק קבוע על ידי ה-destructor של המנוע: HPDFRapidOCRDestroy משחרר את מופע המודל קודם, ואז FreeLibrary פורק את ה-DLL. בצד המקורי, אתחול המודלים זהיר באותה מידה; כשמודל הזיהוי נכשל אחרי שסשני ה-detector וה-classifier כבר נבנו, אותם סשנים משוחררים לפני שהשגיאה מדווחת, ומניין המחלקות במילון נבדק מול הפלט של המודל במהלך האתחול ולא בעמוד הראשון

למה קריאת OCR מקורית לא יכולה ליהרג באמצע inference?

קריאת RapidOCR מקורית לא יכולה ליהרג באמצע inference כי היא רצה על ה-thread שלכם, בתוך התהליך שלכם, באמצע סשן ONNX Runtime שאינו מקבל הפרעה. הביטול במתאם ה-DLL של HotPDF הוא אפוא שיתופי: ה-DLL קורא ל-callback של abort לפני ואחרי ה-detection, אחרי ה-classification, ואחרי כל שורה שזוהתה, ועוצר בנקודת הביקורת הראשונה שבה ה-callback מחזיר 0. Run של ONNX שכבר התחיל יסיים קודם

החלופות גרועות מהמתנה. TerminateThread היה משאיר את נעילת ה-heap של ה-CRT, את מאגר ה-threads של ONNX Runtime ואת כל state של OpenCV במצב שבו הם מצאו את עצמם, ומרעיל את שאר התהליך. FreeLibrary בזמן שקריאה עדיין רצה פורק קוד שנמצא על המחסנית. אף אחד מהם לא יכול להיעשות בטוח, ולכן המתאם לעולם לא מנסה אותם. הדדליין ב-TimeoutMilliseconds הוא אפוא דדליין שיתופי, ודדליין שפג מתגלה בתור שגיאת מנוע עם דיאגנוסטיקת פקיעה, בזמן ש-token שבוטל מתגלה בתור otlsCancelled:

// ה-Token נוצר על ידי הקורא ומשותף עם ה-thread של ה-UI,
// שקורא ל-Token.Cancel כשהמשתמש לוחץ על עצור
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
  case Info.Status of
    otlsCancelled:
      // מוחזר בגבול השלב או השורה הבא; המסמך ללא שינוי
      Writeln('Cancelled');
    otlsEngineError:
      // כולל פקיעת דדליין שיתופי ודיאגנוסטיקות מקוריות
      Writeln('Engine: ', string(Info.Diagnostic));
    otlsBudgetExceeded:
      Writeln('Budget: ', string(Info.Diagnostic));
  else
    Writeln(string(Info.Diagnostic));
  end;

זהו הפשרה המרכזית בין מתאמי ה-process של HotPDF לבין ה-DLL בתוך התהליך, ואף צד לא מנצח בכל שורה:

פשרות מתאמי ה-OCR של HotPDF: מתאמי process מריצים worker וטוענים מודלים בכל עמוד אבל ניתנים להריגה ומכילים קריסות, בזמן שה-DLL של RapidOCR בתוך התהליך טוען מודלים פעם אחת, עוצר רק בנקודות ביקורת שיתופיות, חולק את מרחב הכתובות, ומופץ בתור DLL עם המודלים והמילון שלו
בוחרים לפי עומס עבודה: אפליקציית שולחן עבודה של עמוד בכל פעם מרוויחה מה-DLL החם, בזמן ששרת שבולע סריקות לא מהימנות כדאי שישלם עבור חומת ה-process
  • עלות הפעלה: מתאמי ה-Tesseract וה-Python RapidOCR משגרים תהליך וטוענים מודלים עבור כל עמוד; ה-DLL טוען מודלים פעם אחת לכל מנוע
  • עצירה: תהליך בן ניתן לחיסול מיידי, וה-worker של Python רץ בתוך Job Object של kill-on-close כך שכל עץ התהליכים שלו הולך איתו; ה-DLL יכול לעצור רק בגבולות שלב ושורה
  • כליאת תקלות: קריסה ב-tesseract.exe מכשילה עמוד אחד; access violation בתוך ה-DLL מוריד את התהליך שלכם
  • הפצה: מתאמי process זקוקים לתוכנית מותקנת או לסביבת Python; ה-DLL זקוק לעצמו, למודליו ולמילונו, מותאמים ל-bitness של האפליקציה
  • זיכרון: מתאמי process משחררים הכול כשהבן יוצא; מנוע DLL משאיר את המודלים שלו תושבי זיכרון עד ששוחררה ההפניה האחרונה לממשק

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

בנייה והפצה של HotPDFRapidOCR.dll

HotPDFRapidOCR.dll נבנה מקוד המקור של C++ ב-Native/RapidOCR עם MSVC, C++17, Windows SDK ו-CMake 3.20 ומעלה, בשימוש בסקריפט עזר שמקבל את ספריות ה-network sources המקוריות, את ONNX Runtime ואת OpenCV בתוספת פלטפורמת Win32 או Win64. בנו את שניהם אם אתם מפיצים את שניהם, כי אפליקציית Delphi בת 32 ביט לא יכולה לטעון DLL בן 64 ביט, והספריות הסטטיות שמספקים חייבות להתאים לארכיטקטורת היעד וגם למצב ה-CRT

צד המודלים מחזיק במגבלות תאימות משלו. ה-detector הוא DB text detector; המזהה מקבל מודלי CTC בפריסת NCHW עם גובה קלט קבוע של 32 או 48, ומשתמש ב-48 עבור מודלים בגובה דינמי. ONNX Runtime הסטטי המצורף אינו יכול לטעון מודלים שנשמרו עם גרסת IR חדשה יותר, ולכן exports של PP-OCRv5 אחרונים נכשלים באתחול עם דיאגנוסטיקה במקום להיטען חלקית. המילון חייב להיות UTF-8 ללא BOM, בדיוק בסדר התווים של המודל, ומניין המחלקות שלו חייב להתאים לפלט המודל; סיומות שורה של CRLF מתקבלות. הזיהוי אופליין: ה-DLL לעולם לא מוריד מודל חסר

עזר זריז

  • מפעל: HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options]) ב-HPDFRapidOCRRecognition, זמין מאז v2.774.0 ב-Delphi, C++Builder ו-builds של FPC/Lazarus ל-Windows
  • משאירים את ה-IHPDFOCREngine המוחזר חי על פני עמודים ומסמכים; שחרורו הורס את המודלים ופורק את ה-DLL
  • מנוע אחד מריץ זיהוי אחד בכל רגע; יוצרים כמה מנועים עבור workers מקביליים ומתקצבים זיכרון עבור כל עותק מודל
  • הפלט הוא רשומה אחת לכל שורת טקסט עם ממוצע ביטחון תווים, מסונן על ידי THPDFOCRTextLayerOptions.MinimumConfidence
  • הביטול ו-TimeoutMilliseconds שיתופיים; ריצת ONNX בעיצומה תמיד מסתיימת
  • מתאימים את bitness של ה-DLL לאפליקציה ואת מצב ה-CRT של ספריות ONNX Runtime ו-OpenCV הסטטיות אל ה-DLL
  • בוחרים פרופיל שפה לכל מנוע עם THPDFRapidOCRDLLOptions.ForLanguage (v2.775.0); מנוע אחד אינו מאתר שפות בעצמו

מתאם ה-RapidOCR המקורי, מתאמי ה-OCR מבוססי ה-process, מנוע הרינדור לעמודים שמזין אותם וכותב שכבת הטקסט הבלתי נראית של Unicode מגיעים כולם יחד ב-HotPDF, רכיב PDF מקורי ל-VCL עבור Delphi ו-C++Builder. אם אפליקציית לכידת או ארכוב מסמכים שלכם זקוקה לפלט ניתן לחיפוש בלי ראנטיים Python על מכונת היעד, HotPDF Delphi PDF component מספק את כל הצינור, ונותר להפיץ רק את ה-DLL ואת המודלים שלו