{"id":8078,"date":"2026-03-25T13:51:55","date_gmt":"2026-03-25T05:51:55","guid":{"rendered":"https:\/\/kyle.ai\/blog\/?p=8078"},"modified":"2026-04-03T15:25:48","modified_gmt":"2026-04-03T07:25:48","slug":"%e4%bd%bf%e7%94%a8-golang-%e6%8f%90%e5%8f%96%e8%a7%86%e9%a2%91%e6%96%87%e4%bb%b6%e4%b8%ad%e7%9a%84%e5%ad%97%e5%b9%95%e6%96%87%e4%bb%b6","status":"publish","type":"post","link":"https:\/\/kyle.ai\/blog\/8078.html","title":{"rendered":"\u4f7f\u7528 Golang \u63d0\u53d6\u89c6\u9891\u6587\u4ef6\u4e2d\u7684\u5b57\u5e55\u6587\u4ef6"},"content":{"rendered":"\n<p>\u672c\u6765\u662f\u60f3\u7814\u7a76\u4e00\u4e0b\uff0c\u80fd\u4e0d\u80fd\u53ea\u8bfb\u53d6\u89c6\u9891\u6587\u4ef6\u7684\u524d\u9762\u5f88\u5c0f\u4e00\u90e8\u5206\u6570\u636e\uff0c\u4f8b\u5982\u53ea\u8bfb\u5f00\u5934\u7684 2MB\uff0c\u5c31\u53ef\u4ee5\u628a\u5185\u7f6e\u5b57\u5e55\u6587\u4ef6\u5bfc\u51fa\u6765\u3002<\/p>\n\n\n\n<p>\u7ed3\u679c\u53d1\u73b0\u884c\u4e0d\u901a\uff0cMP4 \u548c MKV \u6587\u4ef6\uff0c\u5b83\u4eec\u7684\u5b57\u5e55\u90fd\u662f\u8ddf\u968f\u89c6\u9891\u6570\u636e\u4e00\u8d77\u7684\uff0c\u5206\u6563\u5230\u6587\u4ef6\u7684\u4e0d\u540c\u4f4d\u7f6e\uff0c\u6240\u4ee5\u8981\u5bfc\u51fa\u5b8c\u6574\u5b57\u5e55\u6587\u4ef6\uff0c\u4f60\u9700\u8981\u8bfb\u53d6\u6574\u4e2a\u89c6\u9891\u6587\u4ef6\u3002<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">mp4 \u6587\u4ef6<\/h2>\n\n\n\n<p>MP4 \u6587\u4ef6\u662f BOX \u6a21\u578b\uff0c\u8fd9\u91cc\u6709\u4e2a\u5f88\u597d\u7684\u8bf4\u660e\u6587\u6863 <a href=\"https:\/\/www.pedestrian.com.cn\/user\/video\/mp4_muxer.html\">https:\/\/www.pedestrian.com.cn\/user\/video\/mp4_muxer.html<\/a><\/p>\n\n\n\n<p>\u6211\u4eec\u9700\u8981\u8bfb\u53d6\u7684 BOX \u5d4c\u5957\u89c4\u5219\u5982\u4e0b<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>moov -&gt; trak -&gt; mdia -&gt; mdhd\n                     -&gt; hdlr\n                     -&gt; minf -&gt; stbl -&gt; stts\n                                     -&gt; stsz\n                                     -&gt; stco<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"888\" height=\"506\" src=\"https:\/\/kyle.ai\/blog\/wp-content\/uploads\/2026\/03\/file_layout.png\" alt=\"\" class=\"wp-image-8079\"\/><\/figure>\n\n\n\n<p>\u793a\u4f8b\u7a0b\u5e8f\uff0c\u8bfb\u53d6 mp4 \u6587\u4ef6\uff0c\u6253\u5370\u51fa\u5b57\u5e55\u5217\u8868\uff0c\u5e76\u5c06\u5176\u5bfc\u51fa\u4e3a srt \u5b57\u5e55\u6587\u4ef6<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: go; title: ; notranslate\" title=\"\">\npackage main\nimport (\n\t&quot;encoding\/binary&quot;\n\t&quot;fmt&quot;\n\t&quot;io&quot;\n\t&quot;os&quot;\n\t&quot;time&quot;\n\t&quot;github.com\/abema\/go-mp4&quot;\n)\nfunc locateMoov(rs io.ReadSeeker) {\n\tfor {\n\t\t\/\/ \u83b7\u53d6\u5f53\u524d\u4f4d\u7f6e\n\t\tcurrentPos, _ := rs.Seek(0, io.SeekCurrent)\n\t\t\/\/ \u8bfb\u53d6 Box Header (8 \u5b57\u8282)\n\t\tvar header &#x5B;8]byte\n\t\tif _, err := rs.Read(header&#x5B;:]); err != nil {\n\t\t\tbreak \/\/ \u5230\u8fbe\u6587\u4ef6\u672b\u5c3e\n\t\t}\n\t\tsize := binary.BigEndian.Uint32(header&#x5B;0:4])\n\t\tboxType := string(header&#x5B;4:8])\n\t\tif boxType == &quot;moov&quot; {\n\t\t\tfmt.Printf(&quot;\u627e\u5230 moov! \u8d77\u59cb\u504f\u79fb\u91cf: %d\uff0c\u5927\u5c0f\uff1a%d\\n&quot;, currentPos, size)\n\t\t\trs.Seek(currentPos, io.SeekStart) \/\/ \u56de\u5230\u8d77\u70b9\u5f00\u59cb\u8be6\u7ec6\u89e3\u6790\n\t\t\treturn\n\t\t}\n\t\tif boxType == &quot;mdat&quot; {\n\t\t\tvar actualSize int64\n\t\t\tif size == 1 {\n\t\t\t\t\/\/ \u5904\u7406 64 \u4f4d\u5927\u6587\u4ef6\u957f\u5ea6\n\t\t\t\tvar largeSize &#x5B;8]byte\n\t\t\t\trs.Read(largeSize&#x5B;:])\n\t\t\t\tactualSize = int64(binary.BigEndian.Uint64(largeSize&#x5B;:]))\n\t\t\t} else {\n\t\t\t\tactualSize = int64(size)\n\t\t\t}\n\t\t\tfmt.Printf(&quot;\u8df3\u8fc7 mdat, \u5927\u5c0f: %d \u5b57\u8282\uff0c\u5f53\u524d offset %d\\n&quot;, actualSize, currentPos)\n\t\t\t\/\/ \u6838\u5fc3\u64cd\u4f5c\uff1a\u7cbe\u51c6\u8df3\u8fc7 mdat \u7684\u4e3b\u4f53\n\t\t\trs.Seek(currentPos+actualSize, io.SeekStart)\n\t\t\tcontinue\n\t\t}\n\t\t\/\/ \u5176\u4ed6 Box (\u5982 ftyp, free)\uff0c\u6839\u636e\u5176 size \u6b63\u5e38\u8df3\u8fc7\n\t\trs.Seek(currentPos+int64(size), io.SeekStart)\n\t}\n}\n\/\/ \u8f85\u52a9\u51fd\u6570\uff1a\u5c06 MP4 \u5185\u90e8\u7684 uint16 \u8bed\u8a00\u4ee3\u7801\u8f6c\u4e3a\u5b57\u7b26\u4e32 (\u5982 &quot;chi&quot;, &quot;eng&quot;)\nfunc decodeLanguage(lang uint16) string {\n\t\/\/ \u7b97\u6cd5\uff1a\u6bcf\u4e2a\u5b57\u7b26\u5360 5 \u4f4d\uff0c\u504f\u79fb\u91cf\u4e3a 0x60\n\tc1 := rune((lang&gt;&gt;10)&amp;0x1F) + 0x60\n\tc2 := rune((lang&gt;&gt;5)&amp;0x1F) + 0x60\n\tc3 := rune(lang&amp;0x1F) + 0x60\n\treturn string(&#x5B;]rune{c1, c2, c3})\n}\ntype SampleInfo struct {\n\tOffset int64\n\tSize   uint32\n\tStart  time.Duration\n\tEnd    time.Duration\n}\ntype SubtitleTrack struct {\n\tID         uint32\n\tLanguage   string\n\tTimescale  uint32\n\tSamples    &#x5B;]SampleInfo\n\tIsSubtitle bool\n}\nfunc main() {\n\tif len(os.Args) &lt; 2 {\n\t\tpanic(&quot;\u8bf7\u63d0\u4f9b\u6587\u4ef6\u8def\u5f84&quot;)\n\t}\n\tfilePath := os.Args&#x5B;1]\n\tf, err := os.Open(filePath) \/\/ \u66ff\u6362\u4e3a\u4f60\u7684\u6587\u4ef6\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer f.Close()\n\t\/\/ \u8df3\u8fc7\u5176\u5b83 box\uff0c\u76f4\u63a5\u8bfb\u53d6 moov \u7ed3\u6784\n\tlocateMoov(f)\n\tvar tracks &#x5B;]*SubtitleTrack\n\tvar cur *SubtitleTrack\n\t\/\/ 1. \u89e3\u6790\u7ed3\u6784\n\t_, err = mp4.ReadBoxStructure(f, func(h *mp4.ReadHandle) (any, error) {\n\t\tswitch h.BoxInfo.Type.String() {\n\t\tcase &quot;moov&quot;, &quot;mdia&quot;, &quot;minf&quot;, &quot;stbl&quot;:\n\t\t\treturn h.Expand()\n\t\tcase &quot;trak&quot;:\n\t\t\t\/\/ \u8fdb\u5165\u65b0\u8f68\u9053\uff0c\u521d\u59cb\u5316\u4e34\u65f6\u5bf9\u8c61\n\t\t\tcur = &amp;SubtitleTrack{ID: uint32(len(tracks) + 1)}\n\t\t\t_, err := h.Expand()\n\t\t\tif cur.IsSubtitle {\n\t\t\t\ttracks = append(tracks, cur)\n\t\t\t}\n\t\t\treturn nil, err\n\t\tcase &quot;hdlr&quot;:\n\t\t\tpayload, _, _ := h.ReadPayload()\n\t\t\thdlr := payload.(*mp4.Hdlr)\n\t\t\tif hdlr.HandlerType == &#x5B;4]byte{&#039;s&#039;, &#039;b&#039;, &#039;t&#039;, &#039;l&#039;} {\n\t\t\t\tcur.IsSubtitle = true\n\t\t\t}\n\t\tcase &quot;mdhd&quot;:\n\t\t\tpayload, _, _ := h.ReadPayload()\n\t\t\tmdhd := payload.(*mp4.Mdhd)\n\t\t\tcur.Timescale = mdhd.Timescale\n\t\t\tlangCode := uint16(mdhd.Language&#x5B;0])&lt;&lt;10 | uint16(mdhd.Language&#x5B;1])&lt;&lt;5 | uint16(mdhd.Language&#x5B;2])\n\t\t\tcur.Language = decodeLang(langCode)\n\t\tcase &quot;stts&quot;: \/\/ \u65f6\u95f4\u8868\n\t\t\tpayload, _, _ := h.ReadPayload()\n\t\t\tstts := payload.(*mp4.Stts)\n\t\t\tvar runningTime uint64\n\t\t\tfor _, entry := range stts.Entries {\n\t\t\t\tfor i := uint32(0); i &lt; entry.SampleCount; i++ {\n\t\t\t\t\tstart := time.Duration(runningTime) * time.Second \/ time.Duration(cur.Timescale)\n\t\t\t\t\trunningTime += uint64(entry.SampleDelta)\n\t\t\t\t\tend := time.Duration(runningTime) * time.Second \/ time.Duration(cur.Timescale)\n\t\t\t\t\tcur.Samples = append(cur.Samples, SampleInfo{Start: start, End: end})\n\t\t\t\t}\n\t\t\t}\n\t\tcase &quot;stsz&quot;: \/\/ \u5927\u5c0f\u8868\n\t\t\tpayload, _, _ := h.ReadPayload()\n\t\t\tstsz := payload.(*mp4.Stsz)\n\t\t\tfor i := 0; i &lt; len(cur.Samples) &amp;&amp; i &lt; len(stsz.EntrySize); i++ {\n\t\t\t\tcur.Samples&#x5B;i].Size = stsz.EntrySize&#x5B;i]\n\t\t\t}\n\t\tcase &quot;stco&quot;: \/\/ \u504f\u79fb\u91cf\u8868\n\t\t\tpayload, _, _ := h.ReadPayload()\n\t\t\tstco := payload.(*mp4.Stco)\n\t\t\tfor i := 0; i &lt; len(cur.Samples) &amp;&amp; i &lt; len(stco.ChunkOffset); i++ {\n\t\t\t\tcur.Samples&#x5B;i].Offset = int64(stco.ChunkOffset&#x5B;i])\n\t\t\t}\n\t\tcase &quot;co64&quot;: \/\/ 64\u4f4d\u504f\u79fb\u91cf\n\t\t\tpayload, _, _ := h.ReadPayload()\n\t\t\tco64 := payload.(*mp4.Co64)\n\t\t\tfor i := 0; i &lt; len(cur.Samples) &amp;&amp; i &lt; len(co64.ChunkOffset); i++ {\n\t\t\t\tcur.Samples&#x5B;i].Offset = int64(co64.ChunkOffset&#x5B;i])\n\t\t\t}\n\t\t}\n\t\treturn nil, nil\n\t})\n\t\/\/ 2. \u5bfc\u51fa\n\tfor _, t := range tracks {\n\t\tfmt.Printf(&quot;%s %d\\n&quot;, t.Language, len(t.Samples))\n\t\tsaveToSrt(f, t)\n\t}\n}\n\/\/ \u89e3\u7801 MP4 \u5185\u90e8\u8bed\u8a00\u4ee3\u7801 (ISO-639-2\/T)\nfunc decodeLang(l uint16) string {\n\t\/\/ 5\u4f4d\u4e00\u4e2a\u5b57\u6bcd\uff0c\u504f\u79fb\u91cf 0x60\n\tc1 := byte((l&gt;&gt;10)&amp;0x1F) + 0x60\n\tc2 := byte((l&gt;&gt;5)&amp;0x1F) + 0x60\n\tc3 := byte(l&amp;0x1F) + 0x60\n\treturn string(&#x5B;]byte{c1, c2, c3})\n}\nfunc saveToSrt(f io.ReadSeeker, t *SubtitleTrack) {\n\toutName := fmt.Sprintf(&quot;track_%d_%s.srt&quot;, t.ID, t.Language)\n\tout, _ := os.Create(outName)\n\tdefer out.Close()\n\tfmt.Printf(&quot;\u5bfc\u51fa\u8f68\u9053 %d &#x5B;%s], \u5171 %d \u6761\u5b57\u5e55...\\n&quot;, t.ID, t.Language, len(t.Samples))\n\tfor i, s := range t.Samples {\n\t\tf.Seek(s.Offset, io.SeekStart)\n\t\tdata := make(&#x5B;]byte, s.Size)\n\t\tf.Read(data)\n\t\tif len(data) &lt; 2 {\n\t\t\tcontinue\n\t\t}\n\t\ttxtLen := binary.BigEndian.Uint16(data&#x5B;:2])\n\t\tif int(txtLen)+2 &gt; len(data) {\n\t\t\ttxtLen = uint16(len(data) - 2)\n\t\t}\n\t\tcontent := string(data&#x5B;2 : 2+txtLen])\n\t\tfmt.Fprintf(out, &quot;%d\\n%s --&gt; %s\\n%s\\n\\n&quot;,\n\t\t\ti+1, formatSrtTime(s.Start), formatSrtTime(s.End), content)\n\t}\n}\nfunc formatSrtTime(d time.Duration) string {\n\tms := d.Milliseconds() % 1000\n\tsec := int64(d.Seconds()) % 60\n\tmin := int64(d.Minutes()) % 60\n\thr := int64(d.Hours())\n\treturn fmt.Sprintf(&quot;%02d:%02d:%02d,%03d&quot;, hr, min, sec, ms)\n}\n\n<\/pre><\/div>\n\n\n<h2 class=\"wp-block-heading\">mkv \u6587\u4ef6<\/h2>\n\n\n\n<p>\u793a\u4f8b\u7a0b\u5e8f\uff0c\u8bfb\u53d6 mkv \u6587\u4ef6\uff0c\u6253\u5370\u51fa\u5b57\u5e55\u5217\u8868\u3002\u8fd9\u91cc\u5c31\u6ca1\u7ee7\u7eed\u7814\u7a76\u5bfc\u51fa\u5b57\u5e55\u6587\u4ef6\u4e86\uff0c\u8981\u8bfb\u53d6\u6574\u4e2a mkv \u6587\u4ef6\uff0c\u6bd4\u8f83\u9ebb\u70e6\uff0c\u800c\u4e14\u901f\u5ea6\u4e5f\u8f83\u6162\u3002<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: go; title: ; notranslate\" title=\"\">\npackage main\nimport (\n\t&quot;fmt&quot;\n\t&quot;io&quot;\n\t&quot;os&quot;\n\t&quot;github.com\/at-wat\/ebml-go&quot;\n)\n\/\/ SubtitleTrack \u5b58\u50a8 MKV \u5b57\u5e55\u8f68\u9053\u4fe1\u606f\ntype SubtitleTrack struct {\n\tNumber   uint64\n\tLanguage string\n\tName     string\n\tCodecID  string\n}\nfunc main() {\n\tif len(os.Args) &lt; 2 {\n\t\tpanic(&quot;\u8bf7\u63d0\u4f9b\u6587\u4ef6\u8def\u5f84&quot;)\n\t}\n\tfilePath := os.Args&#x5B;1]\n\tf, err := os.Open(filePath)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer f.Close()\n\theadReader := io.NewSectionReader(f, 0, 100*1024) \/\/ \u8bfb\u53d6\u524d 100k\uff0c\u901a\u5e38\u8db3\u591f\u5305\u542b\u5934\u90e8\u4fe1\u606f\n\ttracks, err := listMKVSubtitles(headReader)\n\tif err != nil {\n\t\tfmt.Printf(&quot;\u89e3\u6790\u5217\u8868\u65f6\u51fa\u9519 (\u53ef\u80fd\u662f\u6570\u636e\u4e0d\u8db3): %v\\n&quot;, err)\n\t}\n\tif len(tracks) == 0 {\n\t\tfmt.Println(&quot;\u672a\u53d1\u73b0\u5185\u7f6e\u5b57\u5e55\u8f68\u9053&quot;)\n\t\treturn\n\t}\n\tfmt.Println(&quot;--- \u53d1\u73b0\u5b57\u5e55\u5217\u8868 ---&quot;)\n\tfor _, t := range tracks {\n\t\tfmt.Printf(&quot;&#x5B;%d] \u8bed\u8a00: %s, \u683c\u5f0f: %s, \u540d\u79f0: %s\\n&quot;, t.Number, t.Language, t.CodecID, t.Name)\n\t}\n}\nfunc listMKVSubtitles(r io.ReadSeeker) (&#x5B;]SubtitleTrack, error) {\n\tr.Seek(0, io.SeekStart)\n\tvar header struct {\n\t\tSegment struct {\n\t\t\tTracks struct {\n\t\t\t\tTrackEntry &#x5B;]struct {\n\t\t\t\t\tTrackNumber uint64 `ebml:&quot;TrackNumber&quot;`\n\t\t\t\t\tTrackType   uint64 `ebml:&quot;TrackType&quot;`\n\t\t\t\t\tLanguage    string `ebml:&quot;Language&quot;`\n\t\t\t\t\tName        string `ebml:&quot;Name&quot;`\n\t\t\t\t\tCodecID     string `ebml:&quot;CodecID&quot;`\n\t\t\t\t} `ebml:&quot;TrackEntry&quot;`\n\t\t\t} `ebml:&quot;Tracks&quot;`\n\t\t} `ebml:&quot;Segment&quot;`\n\t}\n\t\/\/ Unmarshal \u4f1a\u5c1d\u8bd5\u89e3\u6790\u6574\u4e2a\u7ed3\u6784\uff0c\u4f46\u5728\u8bfb\u53d6\u5b8c Tracks \u540e\u6211\u4eec\u5c31\u53ef\u4ee5\u505c\u6b62\n\t\/\/ \u5373\u4f7f\u56e0\u4e3a\u6587\u4ef6\u4e0d\u5b8c\u6574\u62a5\u9519\uff0cheader \u91cc\u7684\u4fe1\u606f\u901a\u5e38\u4e5f\u5df2\u7ecf\u586b\u5145\u4e86\n\terr := ebml.Unmarshal(r, &amp;header)\n\tvar result &#x5B;]SubtitleTrack\n\tfor _, t := range header.Segment.Tracks.TrackEntry {\n\t\tif t.TrackType == 17 { \/\/ 17 \u662f MKV \u6807\u51c6\u4e2d\u7684 Subtitle \u7c7b\u578b\n\t\t\tresult = append(result, SubtitleTrack{\n\t\t\t\tNumber:   t.TrackNumber,\n\t\t\t\tLanguage: t.Language,\n\t\t\t\tName:     t.Name,\n\t\t\t\tCodecID:  t.CodecID,\n\t\t\t})\n\t\t}\n\t}\n\treturn result, err\n}\n<\/pre><\/div>\n\n\n<div data-wp-interactive=\"core\/file\" class=\"wp-block-file\"><object data-wp-bind--hidden=\"!state.hasPdfPreview\" hidden class=\"wp-block-file__embed\" data=\"https:\/\/kyle.ai\/blog\/wp-content\/uploads\/2026\/03\/MP4\u6587\u4ef6\u683c\u5f0f.pdf\" type=\"application\/pdf\" style=\"width:100%;height:600px\" aria-label=\"\u5d4c\u5165 MP4\u6587\u4ef6\u683c\u5f0f\"><\/object><a id=\"wp-block-file--media-a31c52c3-620d-4331-9720-998391da3015\" href=\"https:\/\/kyle.ai\/blog\/wp-content\/uploads\/2026\/03\/MP4\u6587\u4ef6\u683c\u5f0f.pdf\">MP4\u6587\u4ef6\u683c\u5f0f<\/a><a href=\"https:\/\/kyle.ai\/blog\/wp-content\/uploads\/2026\/03\/MP4\u6587\u4ef6\u683c\u5f0f.pdf\" class=\"wp-block-file__button wp-element-button\" download aria-describedby=\"wp-block-file--media-a31c52c3-620d-4331-9720-998391da3015\">\u4e0b\u8f7d<\/a><\/div>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u672c\u6765\u662f\u60f3\u7814\u7a76\u4e00\u4e0b\uff0c\u80fd\u4e0d\u80fd\u53ea\u8bfb\u53d6\u89c6\u9891\u6587\u4ef6\u7684\u524d\u9762\u5f88\u5c0f\u4e00\u90e8\u5206\u6570\u636e\uff0c\u4f8b\u5982\u53ea\u8bfb\u5f00\u5934\u7684 2MB\uff0c\u5c31\u53ef\u4ee5\u628a\u5185\u7f6e\u5b57\u5e55\u6587\u4ef6\u5bfc\u51fa\u6765 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[5],"tags":[],"class_list":["post-8078","post","type-post","status-publish","format-standard","hentry","category-diary"],"_links":{"self":[{"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/posts\/8078","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/comments?post=8078"}],"version-history":[{"count":4,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/posts\/8078\/revisions"}],"predecessor-version":[{"id":8085,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/posts\/8078\/revisions\/8085"}],"wp:attachment":[{"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/media?parent=8078"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/categories?post=8078"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/kyle.ai\/blog\/wp-json\/wp\/v2\/tags?post=8078"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}