3D球场项目 (二)中,我们梳理了视频弹幕引擎的整体架构、引擎初始化流程。现在,3D 球场部分基本已经探索完毕,我们接下来就是要解决这个项目的数据驱动部分了。

我们现在研究一下下发数据的来源:Opta,它是全球顶级的体育数据公司,1996 年创立于英国伦敦,现隶属于 Stats Perform,是行业公认的足球数据金标准。

  • 覆盖:20+ 运动、3900+ 赛事、每年 6 万 + 场比赛
  • 规模:7.2PB 历史数据、年采集超 10 亿数据点
  • 精度:单场足球2 万 + 事件点,事件识别准确率 99.8%

腾讯体育采买了 Opta WTA 网球比赛的数据,我们可以通过 Opta 的 API 来获取数据。

根据他们官网的介绍,一个 MA3 网球赛事数据中 typeId 对应的意义如下:

点击展开 / 折叠 Event TypeId 完整对照表
Event TypeIdEvent NameDescription
20STOP_GAMEIndicates that the game has finished.
41START_SET_1Start 1st Set
42START_SET_2Start 2nd Set
43START_SET_3Start 3rd Set
44START_SET_4Start 4th Set
45START_SET_5Start 5th Set
46STOP_SETStop Set
47T_WO1w.o. Player 1
48T_WO2w.o. Player 2
128SAFESafe (No longer used)
129DANGERDanger (Technical/connection issue at venue)
132INJ_BREAKGame suspended - Player injured
133PLAYERS_COMING_OUTPlayers coming out
141GAME_ABOUT_TO_STARTGame about to start
149GAME_SUSPENDEDGame suspended
160PLAYERS_WARMING_UPPlayers warming up
162SHAKE_HANDS_WITH_REFEREEPlayers shake hands with referee
163COIN_FLIPPING_FOR_FIRST_SERVERCoin flip to choose first server
164PLAYERS_TALKING_TO_REFEREEPlayer(s) talking to the referee
165REFEREE_CHECKING_POINTMARKReferee checking pointmark
166BOTH_PLAYERS_SEATEDBoth players seated
167PLAYER1_RECEIVING_TREATMENTPlayer 1 receiving treatment
168PLAYER2_RECEIVING_TREATMENTPlayer 2 receiving treatment
169PLAYERS_GOING_TO_COURTPlayers going back to court
170NEXT_GAME_ABOUT_TO_STARTNext game is about to start
180T_MORE_THAN_5Rally, over five shots
181T_MORE_THAN_10Rally over ten shots
182T_MORE_THAN_15Rally over fifteen shots
187T_POINT_UNDER_INVESTIGATIONPoint under investigation
188T_START_TIE_BREAKStart tie break
191T_POINT_STOPPED_BY_REFPoint stopped by referee
197UPDATE_SCORE_STARTStart score update
198UPDATE_SCORE_FINISHEDScore update finished
200SIDE_CHANGESide change
206SHOT_CNTShot count
216STATISTIC_VERIFICATIONStatistics confirmation
240NEW_BALLSNew balls
241UMPIRE_ON_COURTUMPIRE ON COURT
242WARMING_UP_VOLLEYWarming up - volley
243WARMING_UP_SERVICEWarming up - service
244ONE_MINUTEOne minute
245TIMETime
246SHOT_DETAILSShot details
256CLSCancel last sent event
257CLRClear events
258GCCGame conditions changed.
261SCOREREvent details for %RELATED_EVENT% in the %MIN%. minute changed
262BPBall position event
264ODDOdds event (deprecated)
266TIME_CORRECTION_EVENTCorrect timestamp for a missed event
276START_GAME_CLOCKThe (stopped) game clock is (re)started the game is running
277STOP_GAME_CLOCKThe game clock is stopped. Time will not change until Start Game Clock is sent again
278ADJUST_GAME_CLOCKGame clock value is adjusted manually
279CSTATA statistical value is cleared and an additional event with the correct action is sent (e.g. an invalid Ace Home event is replaced by a Service Winner Home)
280CONF_PERIOD_SCOREPeriod score confirmed
282TIME_ADAPTIONTime for event %RELATED_EVENT% was adapted by %SEC% seconds
285PLAYER_DATA_CONFIRMEDPlayer data for %RELATED_EVENT% in the %MIN% confirmed
513SYS_MSGSystem Message
514SCOUT_IN_STADIONScout in Stadium
515CONNECTION_PROBLEMSSCOUT_OFFLINEConnection problems Scout offline
516CONNECTION_PROBLEMSConnection problems
517TRANSMISSION_ONLINETransmission online
520LINEUP_CHANGEDLine-up changed
524JERSEY_CHANGEDJersey colors updated
782GAME CANCELLEDAutomatic “Game cancelled” event after first “cancellation” System message is sent
1152T_SERVE1Service Player 1
1153T_1ST_SERVICE11st service Player 1
1154T_2ND_SERVICE12nd service Player 1
1155T_NA1Net approach player 1
1156T_W_FH1Winner (forehand) Player 1
1157T_W_BH1Winner (backhand) Player 1
1158T_FE_FH1Forced error (forehand) Player 1
1159T_FE_BH1Forced error (backhand) Player 1
1160T_UE_FH1Unforced error (forehand) Player 1
1161T_UE_BH1Unforced error (backhand) Player 1
1162T_HE1Hawkeye Player 1
1163T_WBR1Warning by referee Player 1
1164T_CBR1Cautioned by referee Player 1
1165T_SRV_NET1Net Player 1
1166T_SRV_OUT1Out Player 1
1167T_SRV_FF1Foot fault Player 1
1168T_SRV_DF1Double fault Player 1
1169T_SRV_A1Ace Player 1
1170T_SRV_SW1Service winner Player 1
1171T_SRV_RW_FH1Return winner (forehand) Player 1
1172T_SRV_IN1Serve in Player 1
1173T_POINT1Point Player 1
1174T_CONF1Confirm Point Player 1
1175T_GAME1Game Player 1
1176T_SET1Set Player 1
1177T_BL1Baseline Player 1
1178T_SRV_NR1Net/Retake Player 1
1179T_W1Winner Player 1
1180T_FE1Forced error Player 1
1181T_UE1Unforced error Player 1
1182T_SRV_RW_BH1Return winner (backhand) Player 1
1183T_V_FH1Volley (forehand) Player 1
1184T_V_BH1Volley (backhand) Player 1
1185T_BB1Break ball Player 1
1186T_B1Break Player 1
1187T_HES1Hawk eye successful Player 1
1188T_ITO1Injury Timeout Player 1
1189T_START_SRV1Start Service Player 1
1190T_WARNING11st offence - warning Player 1
1191T_PENALTY_POINT12nd offence - penalty point Player 1
1192T_PENALTY_GAME13rd offence - penalty game Player 1
1193T_DISQUALIFICATION1Disqualification Player 1
1194T_UE_NET1Unforced error net Player 1
1195T_FE_NET1Forced error net Player 1
1196T_SUCCESSFUL_NET_APPROACH1Successful net approach Player 1
1197T_TIME_VIOLATION1Time violation Player 1
1198T_PENALTY_POINT1
T_UNSUCCESSFUL_NET_APPROACH1
Unsuccessful net approach Player 1
1199T_AT_NET1At net Player 1
1200T_AT_BASELINE1At baseline Player 1
1201T_SET_BALL1Set ball Player 1
1202T_MATCH_BALL1Match ball Player 1
2176T_SERVE2Service Player 2
2177T_1ST_SERVICE21st service Player 2
2178T_2ND_SERVICE22nd service Player 2
2179T_NA2Net approach player 2
2180T_W_FH2Winner (forehand) Player 2
2181T_W_BH2Winner (backhand) Player 2
2182T_FE_FH2Forced error (forehand) Player 2
2183T_FE_BH2Forced error (backhand) Player 2
2184T_UE_FH2Unforced error (forehand) Player 2
2185T_UE_BH2Unforced error (backhand) Player 2
2186T_HE2Hawkeye Player 2
2187T_WBR2Warning by referee Player 2
2188T_CBR2Cautioned by referee Player 2
2189T_SRV_NET2Net Player 2
2190T_SRV_OUT2Out Player 2
2191T_SRV_FF2Foot fault Player 2
2192T_SRV_DF2Double fault Player 2
2193T_SRV_A2Ace Player 2
2194T_SRV_SW2Service winner Player 2
2195T_SRV_RW_FH2Return winner (forehand) Player 2
2196T_SRV_IN2Serve in Player 2
2197T_POINT2Point Player 2
2198T_CONF2Confirm Point Player 2
2199T_GAME2Game Player 2
2200T_SET2Set Player 2
2201T_BL2Baseline Player 2
2202T_SRV_NR2Net/Retake Player 2
2203T_W2Winner Player 2
2204T_FE2Forced error Player 2
2205T_UE2Unforced error Player 2
2206T_SRV_RW_BH2Return winner (backhand) Player 2
2207T_V_FH2Volley (forehand) Player 2
2208T_V_BH2Volley (backhand) Player 2
2209T_BB2Break ball Player 2
2210T_B2Break Player 2
2211T_HES2Hawk eye successful Player 2
2212T_ITOInjury Timeout Player 2
2213T_START_SRV2Start Service Player 2
2214T_WARNING21st offence - warning Player 2
2215T_PENALTY_POINT22nd offence - penalty point Player 2
2216T_PENALTY_GAME23rd offence - penalty game Player 2
2217T_DISQUALIFICATION2Disqualification Player 2
2218T_UE_NET2Unforced error net Player 2
2219T_FE_NET2Forced error net Player 2
2220T_SUCCESSFUL_NET_APPROACH2Successful net approach Player 2
2221T_TIME_VIOLATION2Time violation Player 2
2222T_AT_NET2Unsuccessful net approach Player 2
2223At net Player 2
2224T_AT_BASELINE2At baseline Player 2
2225T_SET_BALL2Set ball Player 2
2226T_MATCH_BALL2Match ball Player 2

PBP = Play-by-Play(逐回合 / 逐事件数据),就是把一场比赛里每一次事件都按时间线秒级记录下来的最细粒度原始数据流。口语里也叫 “逐帧数据 / 流水账数据”。网球赛事的数据,都是以这个形式下发的,一段数据可能下发的数据是这样的:

展开 / 折叠 PBP 示例 JSON
{
  "match_id": "atp-2026-1047",
  "tournament": "Roland Garros",
  "round": "SF",
  "court": "Philippe Chatrier",
  "players": {
    "p1": {
      "id": "nadal",
      "name": "Rafael Nadal",
      "country": "ESP"
    },
    "p2": {
      "id": "djokovic",
      "name": "Novak Djokovic",
      "country": "SRB"
    }
  },
  "points": [
    {
      "point_id": "pt-001",
      "set": 1,
      "game": 1,
      "point_in_game": 1,
      "server": "p1",
      "receiver": "p2",
      "score_before": {
        "p1": "0",
        "p2": "0"
      },
      "score_after": {
        "p1": "15",
        "p2": "0"
      },
      "point_winner": "p1",
      "serve": {
        "serve_num": 1,
        "speed_kmh": 185,
        "speed_mph": 115,
        "direction": "T",
        "is_ace": true,
        "is_double_fault": false
      },
      "rally": {
        "rally_count": 1,
        "shots": [
          {
            "player": "p1",
            "stroke": "serve",
            "side": "deuce",
            "x": 12.3,
            "y": 4.1,
            "result": "ace"
          }
        ]
      },
      "time": {
        "point_start": "14:22:03",
        "point_end": "14:22:04"
      },
      "is_break_point": false,
      "is_set_point": false,
      "is_match_point": false
    },
    {
      "point_id": "pt-002",
      "set": 1,
      "game": 1,
      "point_in_game": 2,
      "server": "p1",
      "receiver": "p2",
      "score_before": {
        "p1": "15",
        "p2": "0"
      },
      "score_after": {
        "p1": "30",
        "p2": "0"
      },
      "point_winner": "p1",
      "serve": {
        "serve_num": 1,
        "speed_kmh": 172,
        "speed_mph": 107,
        "direction": "W",
        "is_ace": false,
        "is_double_fault": false
      },
      "rally": {
        "rally_count": 3,
        "shots": [
          {
            "player": "p1",
            "stroke": "serve",
            "side": "ad",
            "x": 11.8,
            "y": 2.9,
            "result": "in"
          },
          {
            "player": "p2",
            "stroke": "backhand",
            "side": "ad",
            "x": 8.2,
            "y": 7.4,
            "result": "return"
          },
          {
            "player": "p1",
            "stroke": "forehand",
            "side": "deuce",
            "x": 15.1,
            "y": 3.3,
            "result": "winner"
          }
        ]
      },
      "time": {
        "point_start": "14:22:10",
        "point_end": "14:22:14"
      },
      "is_break_point": false,
      "is_set_point": false,
      "is_match_point": false
    }
  ]
}
可以看出,这个数据还是很简单的,顶多只能看出球员什么时候得分/失误等等,但对于 3D 球场来说,仅凭这样的数据是不够的,至少要知道让两个 Player 在屏幕上如何行动,做了什么动作。而且 Opta 不可能持续的推数据,一般是以得分为单位进行推送。这期间由于没有数据,球员就只能在屏幕上干等。

为了让比赛画面尽可能的生动的还原起来,我们借用了大模型的能力,创建了一个“导演“ Agent。我们让该 Agent 持存的接收 PBP 数据,想象比赛场面的画面,并引入分镜的概念,最后把数据再以 JSON 的格式推过来,这个导演的职能有:

  1. Player 如何行动,行动持存多少时间,移动位置(只能使用模型预置的动作,这些动作怎么来的,可以查看3D球场项目 (一);
  2. 音效,现在就三个背景音乐类型,1进行中,2得分,3结束;
  3. 镜头调度,现在都是正视角,角度为:1:特写A,2:特写B,3:A侧,4:B侧;

我们线下使用历史的 pbp 数据,生成了一段 10min 左右的脚本,导入我们的项目中。OK,我们的 3D 球场 Demo 跑起来了。

pbp demo

或许有小伙伴会问了,前面提到了我们可以识别视频动作,我们完全可以找一个网球直播的清流来识别动作,然后把这些数据跟 pbp 一起推给客户端。这样画面更还原,不必我们用大模型来瞎编更好吗?理论上这个技术是可行的,但这就涉及版权问题了,这么做差不多就是把比赛现场一比一还原了,这个是会被告侵权的。

后来我们采用长链推 pbp 的进行实际测试的过程中,我们也遇到了一些问题,比如:

  1. 大模型没有及时出结果,player 只能在 idle 状态;
  2. 编的动作有问题,两个球员一直在来回发球,下一轮脚本推过来了,直接中断,衔接的很不自然;
  3. 长链断了,客户端重连,这时候应该是什么动作,后台需要提供一个补充数据下发接口;

在来回联调下,这三个问题都得到了解决——不过解法和我们 demo 阶段的设想差别很大:导演 Agent 不再是"一次 LLM 调用搞定一切",而是被拆成了 确定性脚本层 + LLM 解说层 + 双重下发保险 的组合。具体怎么落地的,下一篇 3D球场项目 (四) 继续讲。