bilibili弾幕転assプログラムの構想と過程


b駅の弾幕は、線の下で放送するのはまだ面倒くさいです。専用の弾幕プレーヤーは他のフォーマットのビデオに対してよくサポートできません。私も弾幕をつけて字幕のプログラムを回してみます。
xmlファイルをキャプチャする作業は多く言いません。簡単なことです。再生ページでソースファイルを見ていただければ、xmlファイルの住所を確認してキャプチャします。
本論文は主にxml内の弾幕を字幕に切り替える過程を説明する。
xmlファイルの最後のものを除いて、弾幕の主体はこうです。

<d p="51.593,5,25,16711680,1408852480,0,7fa769b4,576008622">   up     !</d>
<d p="10.286,1,25,16777215,1408852600,0,a3af4d0d,576011065">  ?</d>
<d p="12.65,1,25,16777215,1408852761,0,24570b5a,576014281">    !</d>
<d p="19.033,1,25,16777215,1408852789,0,cb20d1c7,576014847"> !!!</d>
<d p="66.991,1,25,16777215,1408852886,0,a78e484d,576016806">  </d>
弾幕の各属性を別々に表現するなら、エンカウント/xmlで復号しますが、弾幕の属性は全部pの中に置いてありますので、正規表現を使って抽出します。

          。    ,p        ,       ,    ,               。

   1 25   ;

16777215,       (            FFFFFF);

1408852480,        ,      unix  ,    (d), :d/86400/365.2425+1970,    2014.6。     unix  。          。

0,   ,          ,      0,     。

7fa769b4,       ID,    xml       ,           ,    hash      4    。

576008622,     ,      ,         ID 。
あとでチェックしてみますと、やはり1は弾幕のタイプ(右から左に移動します。下か上に現れます。)で、25はフォントサイズで、16777125はフォント色です。
だから、弾幕ごとの時間、タイプ、大きさ、色、テキストをキャプチャすればいいです。
正規表現:

<d\sp="([\d\.]+),([145]),(\d+),(\d+),\d+,\d+,\w+,\d+">([^<>]+?)</d>
弾幕をキャッチするのは簡単です。弾幕を並べて字幕にするアルゴリズムがポイントです。
このアルゴリズムについては、お父さんのめちゃくちゃなアルゴリズムを使って、固定移動速度、最小重なりの布の原則を採用しました。
遊動弾幕に対しては、次の行の位置を選択する傾向があり、重複する場合は、より次の行(最低行は一番上の行に循環する)を選択し、重複しない行がない場合は、テキストが最も少ない行を選択します。
上に現/下に隠れている固定弾幕に対して、一番上/下に近く、重複しない行を選択します。重複しない行がない場合は、重なり時間が一番短い行を選択し、中央に字幕を配置する。
デフォルトのフォントはマイクロソフト雅黒、デフォルトのサイズは25、デフォルトの白の黒辺です。デフォルトはスクリーン全体で、合計12行です。標準のスクリーンサイズは640 x 360です。
このようにすると、主にass字幕の効果を原始弾幕の効果に近づけるためです。
高級弾幕は本当に私の能力の範囲を超えています。全部見落としました。
goソースコードは以下の通りです

//  bilibili xml       ass    。
// xml   ,       :
// <d p="32.066,1,25,16777215,1409046965,0,017d3f58,579516441">    </d>
// p      、    、    、    、    、?、   ID、  ID。
// p    , 4  ass    ,  。 <d> </d>        。
//       、   、            。
package main
 
import (
  "fmt"
  "io"
  "io/ioutil"
  "math"
  "os"
  "regexp"
  "sort"
  "strconv"
  "strings"
)
 
// ass     
const header = `[Script Info]
ScriptType: v4.00+
Collisions: Normal
playResX: 640
playResY: 360
 
[V4+ Styles]
Format: Name, Fontname, Fontsize, primaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default, Microsoft YaHei, 28, &H00FFFFFF, &H00FFFFFF, &H00000000, &H00000000, 0, 0, 0, 0, 100, 100, 0.00, 0.00, 1, 1, 0, 2, 10, 10, 10, 0
 
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
`
 
//             
var line = regexp.MustCompile(`<d\sp="([\d\.]+),([145]),(\d+),(\d+),\d+,\d+,\w+,\d+">([^<>]+?)</d>`)
 
//          
type Danmu struct {
  text string
  time float64
  kind byte
  size int
  color int
}
 
//  []Danmu  sort.Interface  ,    
type Danmus []Danmu
 
func (d Danmus) Len() int {
  return len(d)
}
func (d Danmus) Less(i, j int) bool {
  return d[i].time < d[j].time
}
func (d Danmus) Swap(i, j int) {
  d[i], d[j] = d[j], d[i]
}
 
//             Danmu   
func fill(d *Danmu, s [][]byte) {
  d.time, _ = strconv.ParseFloat(string(s[1]), 64)
  d.kind = s[2][0] - '0'
  d.size, _ = strconv.Atoi(string(s[3]))
  bgr, _ := strconv.Atoi(string(s[4]))
  d.color = ((bgr >> 16) & 255) | (bgr & (255 << 8)) | ((bgr & 255) << 16)
  d.text = string(s[5])
}
 
//        ,  ascii    0.5   ,    1   
func length(s string) float64 {
  l := 0.0
  for _, r := range s {
    if r < 127 {
      l += 0.5
    } else {
      l += 1
    }
  }
  return l
}
 
//       ass    :`0:00:00.00`
func timespot(f float64) string {
  h, f := math.Modf(f / 3600)
  m, f := math.Modf(f * 60)
  return fmt.Sprintf("%d:%02d:%05.2f", int(h), int(m), f*60)
}
 
//             
func open(name string) ([]Danmu, error) {
  data, err := ioutil.ReadFile(name)
  if err != nil {
    return nil, err
  }
  dan := line.FindAllSubmatch(data, -1)
  ans := make([]Danmu, len(dan))
  for i := len(dan) - 1; i >= 0; i-- {
    fill(&ans[i], dan[i])
  }
  return ans, nil
}
 
//         w,          、        
func save(w io.Writer, dans []Danmu) {
  p1 := make([]float64, 36)
  p2 := make([]float64, 36)
  p3 := make([]float64, 36)
  t := 0
  max := func(x []float64) float64 {
    i := x[0]
    for _, j := range x[1:] {
      if i < j {
        i = j
      }
    }
    return i
  }
  set := func(x []float64, f float64) {
    for i, _ := range x {
      x[i] = f
    }
  }
  find := func(p []float64, f float64, i, d int) int {
    i = (i/d + 1) * d % 36
    m, k := f+10000, 0
    for j := 0; j < 36; j += d {
      t := (i + j) % 36
      if n := max(p[t : t+d]); n <= f {
        k = t
        break
      } else if m > n {
        k = t
        m = n
      }
    }
    return k
  }
  for _, dan := range dans {
    s, l := "", length(dan.text)
    if l == 0 {
      continue
    }
    switch {
    case dan.size < 25:
      dan.size, l, s = 2, l*18, "\\fs18"
    case dan.size == 25:
      dan.size, l = 3, l*28
    case dan.size > 25:
      dan.size, l, s = 4, l*38, "\\fs38"
    }
    if dan.color != 0x00FFFFFF {
      s += fmt.Sprintf("\\c&H%06X", dan.color)
    }
    switch dan.kind {
    case 1: //    
      t := find(p1, dan.time, t, dan.size)
      set(p1[t:t+dan.size], dan.time+8)
      h := (t+dan.size)*10 - 1
      s += fmt.Sprintf("\\move(%d,%d,%d,%d)", 640+int(l/2), h, -int(l/2), h)
      fmt.Fprintf(w, "Dialogue: 1,%s,%s,Default,,0000,0000,0000,,{%s}%s
", timespot(dan.time+0), timespot(dan.time+8), s, dan.text) case 4: // j := find(p2, dan.time, 35, dan.size) set(p2[j:j+dan.size], dan.time+4) s += fmt.Sprintf("\\pos(%d,%d)", 320, (36-j)*10-1) fmt.Fprintf(w, "Dialogue: 2,%s,%s,Default,,0000,0000,0000,,{%s}%s
", timespot(dan.time+0), timespot(dan.time+4), s, dan.text) case 5: // j := find(p3, dan.time, 35, dan.size) set(p3[j:j+dan.size], dan.time+4) s += fmt.Sprintf("\\pos(%d,%d)", 320, (j+dan.size)*10-1) fmt.Fprintf(w, "Dialogue: 3,%s,%s,Default,,0000,0000,0000,,{%s}%s
", timespot(dan.time+0), timespot(dan.time+4), s, dan.text) } } } // , func main() { if len(os.Args) <= 1 { os.Exit(0) } for _, name := range os.Args[1:] { dans, err := open(name) if err != nil { os.Exit(1) } if n := strings.LastIndex(name, "."); n != -1 { name = name[:n] } name += ".ass" file, err := os.Create(name) if err != nil { os.Exit(2) } file.WriteString(header) sort.Sort(Danmus(dans)) save(file, dans) file.Close() } }
2014.9.2 9:30 am更新:フォントの配置を修正しました。
2014.9.2 9:50 am更新:アルゴリズムを固定出現時間に変更し、最小重なり配置し、最終バージョン。
overみなさんのコメントを歓迎します。