618 lines
17 KiB
Go
618 lines
17 KiB
Go
//go:build llm_script && !scripts_pkg
|
|
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"image"
|
|
"image/color"
|
|
"image/draw"
|
|
"image/gif"
|
|
"image/png"
|
|
"math"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type reportRow struct {
|
|
Model string `json:"model"`
|
|
Provider string `json:"provider"`
|
|
Scene string `json:"scene"`
|
|
Input string `json:"input,omitempty"`
|
|
Output string `json:"output,omitempty"`
|
|
Context string `json:"context,omitempty"`
|
|
}
|
|
|
|
type dailyReport struct {
|
|
ReportDate string `json:"report_date"`
|
|
Stats map[string]string `json:"stats"`
|
|
International []reportRow `json:"international"`
|
|
Domestic []reportRow `json:"domestic"`
|
|
SourceReport string `json:"source_report"`
|
|
GeneratedAt string `json:"generated_at,omitempty"`
|
|
SelectedSection string `json:"selected_section,omitempty"`
|
|
}
|
|
|
|
type DigestCard struct {
|
|
Slug string `json:"slug"`
|
|
Title string `json:"title"`
|
|
Headline string `json:"headline"`
|
|
BulletLines []string `json:"bullet_lines"`
|
|
Narration string `json:"narration"`
|
|
FramePath string `json:"frame_path,omitempty"`
|
|
ScriptPath string `json:"script_path,omitempty"`
|
|
}
|
|
|
|
type digestManifest struct {
|
|
ReportDate string `json:"report_date"`
|
|
SourceReport string `json:"source_report"`
|
|
GeneratedAt string `json:"generated_at"`
|
|
OutputDir string `json:"output_dir"`
|
|
VideoPath string `json:"video_path"`
|
|
AudioPath string `json:"audio_path"`
|
|
Cards []DigestCard `json:"cards"`
|
|
}
|
|
|
|
var framePalette = color.Palette{
|
|
color.RGBA{12, 18, 28, 255},
|
|
color.RGBA{32, 48, 74, 255},
|
|
color.RGBA{91, 192, 190, 255},
|
|
color.RGBA{245, 247, 250, 255},
|
|
color.RGBA{255, 196, 61, 255},
|
|
color.RGBA{255, 107, 107, 255},
|
|
}
|
|
|
|
var slideBackgrounds = []uint8{1, 2, 1, 4, 5}
|
|
|
|
func main() {
|
|
var reportPath string
|
|
var reportDate string
|
|
var outputDir string
|
|
|
|
flag.StringVar(&reportPath, "report", "", "path to daily markdown report")
|
|
flag.StringVar(&reportDate, "date", "", "report date in YYYY-MM-DD")
|
|
flag.StringVar(&outputDir, "output-dir", "", "output directory for video digest artifacts")
|
|
flag.Parse()
|
|
|
|
resolvedReport, err := resolveReportPath(reportPath, reportDate)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
content, err := os.ReadFile(resolvedReport)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "read report failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
report, err := parseDailyReport(content)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "parse report failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
report.SourceReport = resolvedReport
|
|
if reportDate == "" {
|
|
reportDate = report.ReportDate
|
|
}
|
|
if reportDate == "" {
|
|
reportDate = time.Now().Format("2006-01-02")
|
|
}
|
|
|
|
cards := buildDigestCards(report)
|
|
if len(cards) == 0 {
|
|
fmt.Fprintln(os.Stderr, "no digest cards generated")
|
|
os.Exit(1)
|
|
}
|
|
|
|
if outputDir == "" {
|
|
outputDir = filepath.Join(filepath.Dir(resolvedReport), "video", reportDate)
|
|
}
|
|
|
|
manifest, err := generateDigestArtifacts(report, cards, outputDir)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "generate digest artifacts failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("report=%s\n", manifest.SourceReport)
|
|
fmt.Printf("output=%s\n", manifest.OutputDir)
|
|
fmt.Printf("cards=%d\n", len(manifest.Cards))
|
|
fmt.Printf("video=%s\n", manifest.VideoPath)
|
|
fmt.Printf("audio=%s\n", manifest.AudioPath)
|
|
}
|
|
|
|
func resolveReportPath(explicitPath string, reportDate string) (string, error) {
|
|
if explicitPath != "" {
|
|
return explicitPath, nil
|
|
}
|
|
root := "/home/long/project/llm-intelligence/reports/daily"
|
|
if reportDate != "" {
|
|
return filepath.Join(root, fmt.Sprintf("daily_report_%s.md", reportDate)), nil
|
|
}
|
|
|
|
matches, err := filepath.Glob(filepath.Join(root, "daily_report_*.md"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(matches) == 0 {
|
|
return "", errors.New("no daily report markdown files found")
|
|
}
|
|
sort.Strings(matches)
|
|
return matches[len(matches)-1], nil
|
|
}
|
|
|
|
func parseDailyReport(content []byte) (dailyReport, error) {
|
|
report := dailyReport{
|
|
Stats: make(map[string]string),
|
|
}
|
|
lines := strings.Split(string(content), "\n")
|
|
section := ""
|
|
for _, rawLine := range lines {
|
|
line := strings.TrimSpace(rawLine)
|
|
if strings.HasPrefix(line, "**报告日期**:") {
|
|
report.ReportDate = strings.TrimSpace(strings.TrimPrefix(line, "**报告日期**:"))
|
|
continue
|
|
}
|
|
switch line {
|
|
case "## 📊 数据质量摘要":
|
|
section = "stats"
|
|
continue
|
|
case "## 🌍 国际推荐模型 TOP 5":
|
|
section = "international"
|
|
continue
|
|
case "## 🇨🇳 国内模型 TOP 10":
|
|
section = "domestic"
|
|
continue
|
|
}
|
|
if strings.HasPrefix(line, "## ") {
|
|
section = ""
|
|
continue
|
|
}
|
|
if !strings.HasPrefix(line, "|") || strings.Contains(line, "------") {
|
|
continue
|
|
}
|
|
|
|
parts := splitMarkdownTableLine(line)
|
|
switch section {
|
|
case "stats":
|
|
if len(parts) >= 2 && parts[0] != "指标" {
|
|
report.Stats[parts[0]] = parts[1]
|
|
}
|
|
case "international":
|
|
if row, ok := parseModelRow(parts); ok {
|
|
report.International = append(report.International, row)
|
|
}
|
|
case "domestic":
|
|
if row, ok := parseModelRow(parts); ok {
|
|
report.Domestic = append(report.Domestic, row)
|
|
}
|
|
}
|
|
}
|
|
if report.ReportDate == "" {
|
|
return report, errors.New("report date not found")
|
|
}
|
|
return report, nil
|
|
}
|
|
|
|
func splitMarkdownTableLine(line string) []string {
|
|
trimmed := strings.Trim(line, "|")
|
|
parts := strings.Split(trimmed, "|")
|
|
out := make([]string, 0, len(parts))
|
|
for _, part := range parts {
|
|
out = append(out, strings.TrimSpace(part))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func parseModelRow(parts []string) (reportRow, bool) {
|
|
if len(parts) < 7 || parts[0] == "排名" {
|
|
return reportRow{}, false
|
|
}
|
|
return reportRow{
|
|
Model: parts[1],
|
|
Provider: parts[2],
|
|
Scene: parts[3],
|
|
Input: parts[4],
|
|
Output: parts[5],
|
|
Context: parts[6],
|
|
}, true
|
|
}
|
|
|
|
func buildDigestCards(report dailyReport) []DigestCard {
|
|
total := report.Stats["模型总数"]
|
|
cny := report.Stats["CNY定价"]
|
|
usd := report.Stats["USD定价"]
|
|
|
|
codeRows := pickSceneRows(report, "代码")
|
|
reasoningRows := pickSceneRows(report, "推理")
|
|
visionRows := pickSceneRows(report, "视觉")
|
|
|
|
cards := []DigestCard{
|
|
newDigestCard(
|
|
"code",
|
|
"Code Digest",
|
|
fmt.Sprintf("%s total models tracked", total),
|
|
codeRows,
|
|
fmt.Sprintf("Code digest highlights %d candidate models. CNY priced entries %s.", len(codeRows), cny),
|
|
),
|
|
newDigestCard(
|
|
"reasoning",
|
|
"Reasoning Digest",
|
|
fmt.Sprintf("USD priced entries %s", usd),
|
|
reasoningRows,
|
|
fmt.Sprintf("Reasoning digest focuses on %d reasoning oriented models.", len(reasoningRows)),
|
|
),
|
|
newDigestCard(
|
|
"vision",
|
|
"Vision Digest",
|
|
fmt.Sprintf("CNY priced entries %s", cny),
|
|
visionRows,
|
|
fmt.Sprintf("Vision digest contains %d multimodal candidates from the latest report.", len(visionRows)),
|
|
),
|
|
newDigestCard(
|
|
"domestic",
|
|
"Domestic Digest",
|
|
fmt.Sprintf("Domestic pricing entries %s", cny),
|
|
firstRows(report.Domestic, 3),
|
|
fmt.Sprintf("Domestic digest summarizes top local platforms with %d highlighted entries.", min(3, len(report.Domestic))),
|
|
),
|
|
newDigestCard(
|
|
"global",
|
|
"Global Digest",
|
|
fmt.Sprintf("Global pricing entries %s", usd),
|
|
firstRows(report.International, 3),
|
|
fmt.Sprintf("Global digest summarizes top international recommendations with %d highlighted entries.", min(3, len(report.International))),
|
|
),
|
|
}
|
|
return cards
|
|
}
|
|
|
|
func newDigestCard(slug string, title string, headline string, rows []reportRow, narration string) DigestCard {
|
|
bullets := make([]string, 0, len(rows))
|
|
for _, row := range rows {
|
|
bullets = append(bullets, fmt.Sprintf("%s - %s - %s", row.Model, row.Provider, row.Scene))
|
|
}
|
|
if len(bullets) == 0 {
|
|
bullets = append(bullets, "No matching models in current report")
|
|
}
|
|
return DigestCard{
|
|
Slug: slug,
|
|
Title: title,
|
|
Headline: headline,
|
|
BulletLines: bullets,
|
|
Narration: narration,
|
|
}
|
|
}
|
|
|
|
func pickSceneRows(report dailyReport, scene string) []reportRow {
|
|
rows := make([]reportRow, 0, 3)
|
|
for _, source := range [][]reportRow{report.Domestic, report.International} {
|
|
for _, row := range source {
|
|
if strings.Contains(row.Scene, scene) {
|
|
rows = append(rows, row)
|
|
}
|
|
if len(rows) == 3 {
|
|
return rows
|
|
}
|
|
}
|
|
}
|
|
return rows
|
|
}
|
|
|
|
func firstRows(rows []reportRow, n int) []reportRow {
|
|
if len(rows) < n {
|
|
n = len(rows)
|
|
}
|
|
out := make([]reportRow, 0, n)
|
|
for i := 0; i < n; i++ {
|
|
out = append(out, rows[i])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func generateDigestArtifacts(report dailyReport, cards []DigestCard, outputDir string) (digestManifest, error) {
|
|
scriptDir := filepath.Join(outputDir, "scripts")
|
|
frameDir := filepath.Join(outputDir, "frames")
|
|
if err := os.MkdirAll(scriptDir, 0o755); err != nil {
|
|
return digestManifest{}, err
|
|
}
|
|
if err := os.MkdirAll(frameDir, 0o755); err != nil {
|
|
return digestManifest{}, err
|
|
}
|
|
|
|
frames := make([]*image.Paletted, 0, len(cards))
|
|
delays := make([]int, 0, len(cards))
|
|
for i, card := range cards {
|
|
frame := renderCardFrame(card, i)
|
|
framePath := filepath.Join(frameDir, fmt.Sprintf("%02d_%s.png", i+1, card.Slug))
|
|
if err := writePNG(framePath, frame); err != nil {
|
|
return digestManifest{}, err
|
|
}
|
|
|
|
scriptPath := filepath.Join(scriptDir, fmt.Sprintf("%02d_%s.md", i+1, card.Slug))
|
|
if err := os.WriteFile(scriptPath, []byte(renderCardScript(card)), 0o644); err != nil {
|
|
return digestManifest{}, err
|
|
}
|
|
|
|
card.FramePath = framePath
|
|
card.ScriptPath = scriptPath
|
|
cards[i] = card
|
|
frames = append(frames, frame)
|
|
delays = append(delays, 120)
|
|
}
|
|
|
|
videoPath := filepath.Join(outputDir, "video_digest.gif")
|
|
if err := writeAnimatedGIF(videoPath, frames, delays); err != nil {
|
|
return digestManifest{}, err
|
|
}
|
|
|
|
audioData, err := buildNarrationAudio(cards)
|
|
if err != nil {
|
|
return digestManifest{}, err
|
|
}
|
|
audioPath := filepath.Join(outputDir, "narration.wav")
|
|
if err := os.WriteFile(audioPath, audioData, 0o644); err != nil {
|
|
return digestManifest{}, err
|
|
}
|
|
|
|
manifest := digestManifest{
|
|
ReportDate: report.ReportDate,
|
|
SourceReport: report.SourceReport,
|
|
GeneratedAt: time.Now().Format(time.RFC3339),
|
|
OutputDir: outputDir,
|
|
VideoPath: videoPath,
|
|
AudioPath: audioPath,
|
|
Cards: cards,
|
|
}
|
|
|
|
manifestPath := filepath.Join(outputDir, "manifest.json")
|
|
payload, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
return digestManifest{}, err
|
|
}
|
|
if err := os.WriteFile(manifestPath, payload, 0o644); err != nil {
|
|
return digestManifest{}, err
|
|
}
|
|
|
|
return manifest, nil
|
|
}
|
|
|
|
func renderCardScript(card DigestCard) string {
|
|
var b strings.Builder
|
|
b.WriteString("# " + card.Title + "\n\n")
|
|
b.WriteString("## Headline\n")
|
|
b.WriteString("- " + card.Headline + "\n\n")
|
|
b.WriteString("## Narration\n")
|
|
b.WriteString("- " + card.Narration + "\n\n")
|
|
b.WriteString("## Bullet Lines\n")
|
|
for _, line := range card.BulletLines {
|
|
b.WriteString("- " + line + "\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func writePNG(path string, img image.Image) error {
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
return png.Encode(f, img)
|
|
}
|
|
|
|
func writeAnimatedGIF(path string, frames []*image.Paletted, delays []int) error {
|
|
f, err := os.Create(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
return gif.EncodeAll(f, &gif.GIF{Image: frames, Delay: delays, LoopCount: 0})
|
|
}
|
|
|
|
func renderCardFrame(card DigestCard, index int) *image.Paletted {
|
|
rect := image.Rect(0, 0, 640, 360)
|
|
img := image.NewPaletted(rect, framePalette)
|
|
bg := slideBackgrounds[index%len(slideBackgrounds)]
|
|
draw.Draw(img, rect, &image.Uniform{framePalette[bg]}, image.Point{}, draw.Src)
|
|
|
|
fillRect(img, 18, 18, 622, 342, 0)
|
|
fillRect(img, 28, 28, 612, 88, bg)
|
|
fillRect(img, 28, 104, 612, 332, 1)
|
|
|
|
drawRasterText(img, 40, 42, 3, sanitizeFrameText(card.Title), 3)
|
|
drawRasterText(img, 40, 116, 2, sanitizeFrameText(card.Headline), 4)
|
|
for i, line := range firstStrings(card.BulletLines, 3) {
|
|
drawRasterText(img, 40, 160+i*42, 2, sanitizeFrameText(line), 3)
|
|
}
|
|
drawRasterText(img, 40, 302, 1, sanitizeFrameText("LLM INTELLIGENCE VIDEO DIGEST"), 4)
|
|
return img
|
|
}
|
|
|
|
func firstStrings(lines []string, n int) []string {
|
|
if len(lines) < n {
|
|
n = len(lines)
|
|
}
|
|
out := make([]string, 0, n)
|
|
for i := 0; i < n; i++ {
|
|
out = append(out, lines[i])
|
|
}
|
|
return out
|
|
}
|
|
|
|
func sanitizeFrameText(input string) string {
|
|
upper := strings.ToUpper(input)
|
|
var b strings.Builder
|
|
for _, r := range upper {
|
|
if _, ok := glyphs[r]; ok {
|
|
b.WriteRune(r)
|
|
continue
|
|
}
|
|
switch {
|
|
case r >= 'A' && r <= 'Z':
|
|
b.WriteRune(r)
|
|
case r >= '0' && r <= '9':
|
|
b.WriteRune(r)
|
|
default:
|
|
b.WriteRune(' ')
|
|
}
|
|
}
|
|
return strings.Join(strings.Fields(b.String()), " ")
|
|
}
|
|
|
|
func fillRect(img *image.Paletted, x1 int, y1 int, x2 int, y2 int, idx uint8) {
|
|
for y := y1; y < y2; y++ {
|
|
for x := x1; x < x2; x++ {
|
|
img.SetColorIndex(x, y, idx)
|
|
}
|
|
}
|
|
}
|
|
|
|
func drawRasterText(img *image.Paletted, x int, y int, scale int, text string, idx uint8) {
|
|
cursor := x
|
|
for _, r := range text {
|
|
pattern, ok := glyphs[r]
|
|
if !ok {
|
|
pattern = glyphs[' ']
|
|
}
|
|
for row, bits := range pattern {
|
|
for col := 0; col < 5; col++ {
|
|
if bits&(1<<(4-col)) == 0 {
|
|
continue
|
|
}
|
|
fillRect(img, cursor+col*scale, y+row*scale, cursor+(col+1)*scale, y+(row+1)*scale, idx)
|
|
}
|
|
}
|
|
cursor += 6 * scale
|
|
}
|
|
}
|
|
|
|
func buildNarrationAudio(cards []DigestCard) ([]byte, error) {
|
|
const sampleRate = 16000
|
|
var pcm []int16
|
|
for i, card := range cards {
|
|
freq := 330.0 + float64(i)*55.0
|
|
duration := 0.9 + float64(len(card.BulletLines))*0.18
|
|
pcm = append(pcm, synthTone(freq, duration, sampleRate)...)
|
|
pcm = append(pcm, make([]int16, sampleRate/5)...)
|
|
}
|
|
return encodeWAV(pcm, sampleRate), nil
|
|
}
|
|
|
|
func synthTone(freq float64, duration float64, sampleRate int) []int16 {
|
|
samples := int(duration * float64(sampleRate))
|
|
out := make([]int16, 0, samples)
|
|
for i := 0; i < samples; i++ {
|
|
t := float64(i) / float64(sampleRate)
|
|
envelope := 1.0
|
|
if i < sampleRate/50 {
|
|
envelope = float64(i) / float64(sampleRate/50)
|
|
}
|
|
if i > samples-sampleRate/25 {
|
|
remaining := samples - i
|
|
if remaining > 0 {
|
|
envelope = minFloat(envelope, float64(remaining)/float64(sampleRate/25))
|
|
}
|
|
}
|
|
value := math.Sin(2*math.Pi*freq*t) + 0.35*math.Sin(2*math.Pi*(freq/2)*t)
|
|
out = append(out, int16(value*envelope*12000))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func encodeWAV(samples []int16, sampleRate int) []byte {
|
|
const channels = 1
|
|
const bitsPerSample = 16
|
|
dataSize := len(samples) * 2
|
|
byteRate := sampleRate * channels * bitsPerSample / 8
|
|
blockAlign := channels * bitsPerSample / 8
|
|
|
|
var buf bytes.Buffer
|
|
buf.WriteString("RIFF")
|
|
_ = binary.Write(&buf, binary.LittleEndian, uint32(36+dataSize))
|
|
buf.WriteString("WAVE")
|
|
buf.WriteString("fmt ")
|
|
_ = binary.Write(&buf, binary.LittleEndian, uint32(16))
|
|
_ = binary.Write(&buf, binary.LittleEndian, uint16(1))
|
|
_ = binary.Write(&buf, binary.LittleEndian, uint16(channels))
|
|
_ = binary.Write(&buf, binary.LittleEndian, uint32(sampleRate))
|
|
_ = binary.Write(&buf, binary.LittleEndian, uint32(byteRate))
|
|
_ = binary.Write(&buf, binary.LittleEndian, uint16(blockAlign))
|
|
_ = binary.Write(&buf, binary.LittleEndian, uint16(bitsPerSample))
|
|
buf.WriteString("data")
|
|
_ = binary.Write(&buf, binary.LittleEndian, uint32(dataSize))
|
|
for _, sample := range samples {
|
|
_ = binary.Write(&buf, binary.LittleEndian, sample)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func min(a int, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
func minFloat(a float64, b float64) float64 {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
var glyphs = map[rune][7]uint8{
|
|
' ': {0, 0, 0, 0, 0, 0, 0},
|
|
'-': {0, 0, 0, 31, 0, 0, 0},
|
|
'.': {0, 0, 0, 0, 0, 12, 12},
|
|
':': {0, 12, 12, 0, 12, 12, 0},
|
|
'/': {1, 2, 4, 8, 16, 0, 0},
|
|
'+': {0, 4, 4, 31, 4, 4, 0},
|
|
'(': {2, 4, 8, 8, 8, 4, 2},
|
|
')': {8, 4, 2, 2, 2, 4, 8},
|
|
'0': {14, 17, 19, 21, 25, 17, 14},
|
|
'1': {4, 12, 4, 4, 4, 4, 14},
|
|
'2': {14, 17, 1, 2, 4, 8, 31},
|
|
'3': {30, 1, 1, 14, 1, 1, 30},
|
|
'4': {2, 6, 10, 18, 31, 2, 2},
|
|
'5': {31, 16, 16, 30, 1, 1, 30},
|
|
'6': {14, 16, 16, 30, 17, 17, 14},
|
|
'7': {31, 1, 2, 4, 8, 8, 8},
|
|
'8': {14, 17, 17, 14, 17, 17, 14},
|
|
'9': {14, 17, 17, 15, 1, 1, 14},
|
|
'A': {14, 17, 17, 31, 17, 17, 17},
|
|
'B': {30, 17, 17, 30, 17, 17, 30},
|
|
'C': {14, 17, 16, 16, 16, 17, 14},
|
|
'D': {28, 18, 17, 17, 17, 18, 28},
|
|
'E': {31, 16, 16, 30, 16, 16, 31},
|
|
'F': {31, 16, 16, 30, 16, 16, 16},
|
|
'G': {14, 17, 16, 16, 19, 17, 15},
|
|
'H': {17, 17, 17, 31, 17, 17, 17},
|
|
'I': {14, 4, 4, 4, 4, 4, 14},
|
|
'J': {7, 2, 2, 2, 18, 18, 12},
|
|
'K': {17, 18, 20, 24, 20, 18, 17},
|
|
'L': {16, 16, 16, 16, 16, 16, 31},
|
|
'M': {17, 27, 21, 17, 17, 17, 17},
|
|
'N': {17, 25, 21, 19, 17, 17, 17},
|
|
'O': {14, 17, 17, 17, 17, 17, 14},
|
|
'P': {30, 17, 17, 30, 16, 16, 16},
|
|
'Q': {14, 17, 17, 17, 21, 18, 13},
|
|
'R': {30, 17, 17, 30, 20, 18, 17},
|
|
'S': {15, 16, 16, 14, 1, 1, 30},
|
|
'T': {31, 4, 4, 4, 4, 4, 4},
|
|
'U': {17, 17, 17, 17, 17, 17, 14},
|
|
'V': {17, 17, 17, 17, 17, 10, 4},
|
|
'W': {17, 17, 17, 17, 21, 27, 17},
|
|
'X': {17, 17, 10, 4, 10, 17, 17},
|
|
'Y': {17, 17, 10, 4, 4, 4, 4},
|
|
'Z': {31, 1, 2, 4, 8, 16, 31},
|
|
}
|