[AI] 搭配AI Vision進一步補強OCR結果

  • 5
  • 0
  • AI
  • 2026-04-29

使用Azure Document Intelligence將文件轉Markdown

由於OCR產出的markdown是夾帶HTML, 

而且無法處理圖表, OCR僅只有把上面的數據提取出來, 你看不出折線圖和直方圖長怎樣,

但若單用AI Vision直接處理圖片上資訊, 當遇到密密麻麻的表格時, 錯誤率又很高,

所以可以先作OCR, 再以AI Vision將圖表轉成表格, 同時將PDF裡表格的跨欄及跨列取消掉, 以便將來進一步結構化

Prompt如下:

🧰角色設定
你是專業的財務報告文件分析助手,負責解析圖片中的文字、表格與圖表,並修正與優化OCR產出的Markdown結果

🧬極重要核心原則
完整擷取圖片資訊,不得遺漏,但不處理浮水印和印章
同一項目的字串併成一格或一句
表格中欄位名稱(如金額)與數值必須嚴格對齊在同一欄

🧭表格處理規則
colspan/rowspan一律拆開儲存格,內容重複填入每一格,以維持對齊
將空白的欄名或列名,依內容補上名稱
科目代碼與科目名稱需拆為兩欄,並補上欄名
有些表格(如權益變動表)欄位多,空值多,請以專業判斷對齊欄位與每列數值
td內只有-時,依情境改為空值或0
數值前後有小括號()代表負數:改為數值前加-

👁️模糊文字處理方式
依下表輸出並加上刪除線
| OCR 狀態 | AI 視覺可辨識文字 | AI 視覺無法辨識文字 |
|---|---|---|
| 無OCR結果 | 採AI視覺辨識結果 | 輸出:??? |
| OCR結果不完整、數字位數不合理、誤辨為文字或出現亂碼 | 以AI視覺修正OCR結果 | 原OCR結果 |

📊圖表(折線圖/長條圖):判斷顏色&圖例&數值對應關係,以表格形式輸出並對齊
📄頁碼處理:如~ 3 ~、~4-1~ 於最後一行輸出為斜體字Page.3、Page.4-1
🧨輸出限制:不要任何說明或註解,僅輸出結構化Markdown最終結果

每頁pdf轉圖片後搭配OCR傳給AI Vision程式如下:

using System.Text;
using System.Text.Json;

namespace Abbee
{
    #region recode
    internal record Message(string role, string content);

    internal record PromptRequest<T>(IList<T> messages, double temperature, string model);

    internal record ImageUrl(string url);

    internal record VisionContentPart(string type, string? text = null, ImageUrl? image_url = null);

    internal record VisionMessage(string role, List<VisionContentPart> content);
    #endregion

    internal class LLM
    {
        private const string aiDir = @"D:\Abbee\財報簡報\sample\ai";
        private static string prompt;
        static LLM()
        {
            prompt = File.ReadAllText(Path.Combine(aiDir, "prompt.txt"));
        }

        #region prompt
        public static PromptRequest<VisionMessage> BuildVisionPrompt(string base64Image, string ocr, string model)
        {
            var userContent = new List<VisionContentPart>
            {
                new("text", text: ocr),
                new("image_url", image_url: new ImageUrl($"data:image/jpeg;base64,{base64Image}"))
            };
            return new PromptRequest<VisionMessage>(
                new List<VisionMessage>
                {
                    new("system", new List<VisionContentPart>
                                 {new("text", text: prompt)}),
                    new("user", userContent)
                },
                0.5,
                model
            );
        }
        #endregion

        #region call api
        public static async Task<string> CallAsync<T>(HttpClient client, PromptRequest<T> prompt, int tryTimes, string url)
        {
            for (int i = tryTimes; i > 0; i--)
            {
                try
                {
                    string json = JsonSerializer.Serialize(prompt);
                    using (StringContent content = new StringContent(json, Encoding.UTF8, "application/json"))
                    {
                        HttpResponseMessage response = await client.PostAsync(url, content);
                        string result = await response.Content.ReadAsStringAsync();
                        if (!response.IsSuccessStatusCode)
                            throw new Exception($"HTTP Error: {result}\n{response.RequestMessage}");
                        if (string.IsNullOrEmpty(result))
                            throw new Exception(result);
                        response.EnsureSuccessStatusCode();
                        return GetAiContent(result);
                    }
                }
                catch
                {
                    if (i == 1) throw;
                }
            }
            return null;
        }

        public static string GetAiContent(string aiJson)
        {
            try
            {
                using var doc = JsonDocument.Parse(aiJson);
                return doc.RootElement
                    .GetProperty("choices")[0]
                    .GetProperty("message")
                    .GetProperty("content")
                    .GetString() ?? string.Empty;
            }
            catch
            {
                return string.Empty;
            }
        }

        public static async Task GetVisionMD(HttpClient client, string ocrDir, string aiDir, int tryTimes, string aiUrl, string model)
        {
            if (!Directory.Exists(ocrDir))
            {
                Console.WriteLine($"找不到來源: {ocrDir}");
                return;
            }
            Console.WriteLine($"開始呼叫 AI 轉MD, 讀取來源: {ocrDir}");
            var files = Directory.GetFiles(ocrDir, "*.md");
            Directory.CreateDirectory(aiDir);
            foreach (var f in files)
            {
                try
                {
                    var ocrText = await File.ReadAllTextAsync(f);
                    var imgPath = Path.ChangeExtension(f, ".png");
                    string fileName = Path.Combine(aiDir, Path.GetFileName(f)); ;
                    Console.WriteLine($"AI 正在處理 {Path.GetFileName(f)}...");
                    if (!File.Exists(fileName))
                    {
                        for (int i = tryTimes; i > 0; i--)
                            try
                            {
                                if (!File.Exists(imgPath))
                                    throw new FileNotFoundException($"找不到影像檔案: {imgPath}");

                                byte[] imageBytes = File.ReadAllBytes(imgPath);
                                string base64Image = Convert.ToBase64String(imageBytes);
                                string aiContentText = await CallAsync(client, BuildVisionPrompt(base64Image, ocrText, model), tryTimes, aiUrl);
                                File.WriteAllTextAsync(fileName, aiContentText.Trim());
                            }
                            catch
                            {
                                if (i == 1) throw;
                            }
                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"  {Path.GetFileNameWithoutExtension(f)} AI 存檔失敗: {ex}");
                    return;
                }
            }
            Console.WriteLine($"多模態分析完成");
        }
        #endregion
    }
}

Taiwan is a country. 臺灣是我的國家