{"repo": "askaralim/nba-stats-api", "files": {"services/openaiService.js": {"3c344400e2280bb0b6142ec4a254b6e3d993b81f": {"t": 1785768947.0557935, "n": 16276, "h": "cae0c3c274a4cc3e", "c": "/**\n * OpenAI Service for Game Summary Generation\n * Uses api2d for OpenAI API access\n * Two-step process: 1) Analyze data → JSON, 2) Generate summary from JSON\n */\n\nconst axios = require('axios');\nconst logger = require('../utils/logger');\n\nclass OpenAIService {\n constructor() {\n this.apiKey = process.env.OPENAI_API_KEY;\n this.baseURL = process.env.OPENAI_BASE_URL || 'https://openai.api2d.net/v1';\n this.model = process.env.GPT_MODEL || 'gpt-3.5-turbo';\n this.maxTokens = parseInt(process.env.GPT_MAX_TOKENS || '500');\n this.temperature = parseFloat(process.env.GPT_TEMPERATURE || '0.7');\n }\n\n /**\n * Step 1: Analyze game data and return structured JSON\n * @param {Object} gameFacts - Structured game facts\n * @returns {Promise} Analysis JSON\n */\n async analyzeGameData(gameFacts) {\n if (!this.apiKey) {\n throw new Error('OpenAI API key not configured');\n }\n\n const prompt = this.buildAnalysisPrompt(gameFacts);\n\n try {\n const response = await axios.post(\n `${this.baseURL}/chat/completions`,\n {\n model: this.model,\n messages: [\n {\n role: 'system',\n content: '你是一个比赛数据分析引擎,而不是解说员。'\n },\n {\n role: 'user',\n content: prompt\n }\n ],\n temperature: 0.3, // Lower temperature for more deterministic analysis\n max_tokens: 800,\n response_format: { type: 'json_object' }\n },\n {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json'\n },\n timeout: 30000\n }\n );\n\n const content = response.data?.choices?.[0]?.message?.content?.trim();\n if (!content) {\n throw new Error('Empty response from OpenAI');\n }\n\n // Parse JSON response\n const analysis = JSON.parse(content);\n return analysis;\n } catch (error) {\n logger.error({ component: 'openai', task: 'analysis', responseData: error.response?.data, errorMessage: error.message }, 'OpenAI analysis error');\n throw error;\n }\n }\n\n /**\n * Step 2: Generate summary text from analysis JSON\n * @param {Object} analysis - Analysis JSON from step 1\n * @returns {Promise} Generated summary text\n */\n async generateSummaryFromAnalysis(analysis) {\n if (!this.apiKey) {\n throw new Error('OpenAI API key not configured');\n }\n\n const prompt = this.buildSummaryPrompt(analysis);\n\n try {\n const response = await axios.post(\n `${this.baseURL}/chat/completions`,\n {\n model: this.model,\n messages: [\n {\n role: 'system',\n content: '你是一名数据型比赛解说员。'\n },\n {\n role: 'user',\n content: prompt\n }\n ],\n temperature: this.temperature,\n max_tokens: this.maxTokens\n },\n {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json'\n },\n timeout: 30000\n }\n );\n\n const summary = response.data?.choices?.[0]?.message?.content?.trim();\n if (!summary) {\n throw new Error('Empty response from OpenAI');\n }\n\n return summary;\n } catch (error) {\n logger.error({ component: 'openai', task: 'summary', responseData: error.response?.data, errorMessage: error.message }, 'OpenAI summary error');\n throw error;\n }\n }\n\n /**\n * Generate game summary using two-step process\n * @param {Object} gameFacts - Structured game facts\n * @returns {Promise} Generated summary text\n */\n async generateGameSummary(gameFacts) {\n try {\n // Step 1: Analyze data and get JSON\n const analysis = await this.analyzeGameData(gameFacts);\n \n // Step 2: Generate summary from analysis\n const summary = await this.generateSummaryFromAnalysis(analysis);\n \n return summary;\n } catch (error) {\n logger.error({ component: 'openai', task: 'twoStepSummary', errorMessage: error.message }, 'Two-step AI summary generation failed');\n throw error;\n }\n }\n\n /**\n * Build Step 1 prompt: Data analysis\n * @param {Object} gameFacts - Structured game facts\n * @returns {string} Formatted prompt\n */\n buildAnalysisPrompt(gameFacts) {\n const {\n home_team,\n away_team,\n home_score,\n away_score,\n home_half,\n away_half,\n q1,\n q2,\n q3,\n q4,\n fg_home_made,\n fg_home_attempted,\n fg_home_percent,\n fg_away_made,\n fg_away_attempted,\n fg_away_percent,\n three_home_made,\n three_home_attempted,\n three_home_percent,\n three_away_made,\n three_away_attempted,\n three_away_percent,\n ft_home_made,\n ft_home_attempted,\n ft_home_percent,\n ft_away_made,\n ft_away_attempted,\n ft_away_percent,\n to_home,\n to_away,\n reb_home,\n reb_home_offensive,\n reb_home_defensive,\n reb_away,\n reb_away_offensive,\n reb_away_defensive,\n has_overtime,\n overtime_periods,\n largest_lead_home,\n largest_lead_away,\n foul_home,\n foul_away,\n points_in_paint_home,\n points_in_paint_away,\n fast_break_points_home,\n fast_break_points_away,\n turnover_points_home,\n turnover_points_away,\n halftime_leader,\n winner,\n top_scorer_home,\n top_scorer_away,\n top_points_home,\n top_points_away,\n } = gameFacts;\n\n // Build overtime section if applicable\n let overtimeSection = '';\n if (has_overtime && overtime_periods && overtime_periods.length > 0) {\n const otScores = overtime_periods.map(ot => `OT${ot.period}: ${ot.score}`).join('\\n ');\n overtimeSection = `- 加时赛比分:\\n ${otScores}\\n`;\n }\n\n return `请根据以下比赛数据,进行严格的数据对比分析,并以 JSON 格式输出结果。\n\n【分析规则】\n1. 只能使用给定数据,禁止推断或补充\n2. 标记主队是否在以下方面占优:\n - 篮板\n - 失误(更少为占优)\n - 投篮命中率\n - 三分命中数\n3. 判断比赛走势:\n - 上半场领先方\n - 下半场净胜方\n - 是否进入加时\n4. 判断是否存在\"明确胜因\":\n - 若仅有 1 项轻微优势,标记为\"胜负接近\"\n5. 不使用任何主观或修饰性语言\n\n【输出 JSON 结构】\n{\n \"home\": \"GSW\",\n \"away\": \"LAL\",\n \"winner\": \"home/away\",\n \"halftime_leader\": \"home/away/tie\",\n \"overtime\": true/false,\n \"advantages\": {\n \"rebounds\": \"home/away/none\",\n \"rebounds_home\": number,\n \"rebounds_away\": number,\n \"turnovers\": \"home/away/none\",\n \"turnovers_home\": number,\n \"turnovers_away\": number,\n \"fg_home_made\": number,\n \"fg_away_made\": number,\n \"fg_home_attempted\": number,\n \"fg_away_attempted\": number,\n \"fg_home_percentage\": number,\n \"fg_away_percentage\": number,\n \"three_pt_home_made\": number,\n \"three_pt_away_made\": number,\n \"three_pt_home_attempted\": number,\n \"three_pt_away_attempted\": number,\n \"three_pt_home_percentage\": number,\n \"three_pt_away_percentage\": number,\n \"ft_home_made\": number,\n \"ft_away_made\": number,\n \"ft_home_attempted\": number,\n \"ft_away_attempted\": number,\n \"ft_home_percentage\": number,\n \"ft_away_percentage\": number,\n \"points_in_paint_home\": number,\n \"points_in_paint_away\": number,\n \"fast_break_points_home\": number,\n \"fast_break_points_away\": number,\n \"turnover_points_home\": number,\n \"turnover_points_away\": number,\n \"largest_lead_home\": number,\n \"largest_lead_away\": number,\n \"foul_home\": number,\n \"foul_away\": number,\n \"decisive_factors\": [decisive_factor1, decisive_factor2, ...],\n \"close_game\": true/false\n },\n \"decisive_factors\": [decisive_factor1, decisive_factor2, ...],\n \"close_game\": true/false\n}\n\n比赛数据:\n- 主队:${home_team}\n- 客队:${away_team}\n- 最终比分:${home_score} - ${away_score}\n- 半场比分:${home_half} - ${away_half}\n- 每节比分:\n Q1: ${q1}\n Q2: ${q2}\n Q3: ${q3}\n Q4: ${q4}\n${overtimeSection}\n- 投篮命中数(主):${fg_home_made} 投篮出手数:${fg_home_attempted}\n- 投篮命中率(主):${fg_home_percent}%\n- 投篮命中数(客):${fg_away_made} 投篮出手数:${fg_away_attempted}\n- 投篮命中率(客):${fg_away_percent}%\n- 三分命中数(主):${three_home_made} 三分出手数:${three_home_attempted}\n- 三分命中率(主):${three_home_percent}%\n- 三分命中数(客):${three_away_made} 三分出手数:${three_away_attempted}\n- 三分命中率(客):${three_away_percent}%\n- 罚球命中数(主):${ft_home_made} 罚球出手数:${ft_home_attempted}\n- 罚球命中率(主):${ft_home_percent}%\n- 罚球命中数(客):${ft_away_made} 罚球出手数:${ft_away_attempted}\n- 罚球命中率(客):${ft_away_percent}%\n- 失误(主/客):${to_home} / ${to_away}\n- 篮板(主/客):${reb_home} / ${reb_away}\n- 篮板(主/客):${reb_home_offensive} / ${reb_away_offensive}\n- 篮板(主/客):${reb_home_defensive} / ${reb_away_defensive}\n- 最大领先(主/客):${largest_lead_home} / ${largest_lead_away}\n- 犯规(主/客):${foul_home} / ${foul_away}\n- 内线得分(主/客):${points_in_paint_home} / ${points_in_paint_away}\n- 快攻得分(主/客):${fast_break_points_home} / ${fast_break_points_away}\n- 失误得分(主/客):${turnover_points_home} / ${turnover_points_away}\n `;}\n\n /**\n * Translate NBA news article to Simplified Chinese\n * @param {Object} params - { title, content }\n * @returns {Promise<{ translated_title: string, translated_content: string }>}\n */\n async translateNewsArticle({ title = '', content }) {\n if (!this.apiKey) {\n throw new Error('OpenAI API key not configured');\n }\n if (!content || typeof content !== 'string') {\n throw new Error('Content is required for translation');\n }\n\n const noPromoLinksInstruction =\n '\\n\\nImportant: Do not include promotional or tracking short links in your translation (e.g. nba.smart.link/…, other smart.link URLs, or bare app/watch CTAs that are only URLs). Remove them entirely from translated_title and translated_content. If the source ends with symbols like ➡️ or text that only points to such a link, end the Chinese sentence naturally without any URL.';\n const userPrompt = title\n ? `Translate the following NBA news to Simplified Chinese. Keep it professional, accurate, and fluent.${noPromoLinksInstruction}\\n\\nTitle: ${title}\\n\\nContent:\\n${content}\\n\\nRespond with JSON only: { \"translated_title\": \"...\", \"translated_content\": \"...\" }`\n : `Translate the following NBA news to Simplified Chinese. Keep it professional, accurate, and fluent.${noPromoLinksInstruction}\\n\\nContent:\\n${content}\\n\\nRespond with JSON only: { \"translated_title\": \"...\", \"translated_content\": \"...\" }. Use the first sentence or a short summary as translated_title if no title is given.`;\n\n try {\n const response = await axios.post(\n `${this.baseURL}/chat/completions`,\n {\n model: this.model,\n messages: [\n {\n role: 'system',\n content: '你是一个专业的体育新闻翻译。将英文NBA新闻翻译成简体中文,保持专业、准确、流畅。译文标题和正文中不要保留营销推广类短链接(例如 nba.smart.link/ 等);删除这类链接及仅用于引流的符号(如 ➡️),必要时改写使语句通顺。只输出JSON,不要其他文字。'\n\n },\n {\n role: 'user',\n content: userPrompt\n }\n ],\n temperature: 0.3,\n max_tokens: 2000,\n response_format: { type: 'json_object' }\n },\n {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json'\n },\n timeout: 60000\n }\n );\n\n const contentStr = response.data?.choices?.[0]?.message?.content?.trim();\n if (!contentStr) {\n throw new Error('Empty response from OpenAI');\n }\n\n const result = JSON.parse(contentStr);\n return {\n translated_title: result.translated_title || result.translatedTitle || '',\n translated_content: result.translated_content || result.translatedContent || content\n };\n } catch (error) {\n logger.error({ component: 'openai', task: 'translation', responseData: error.response?.data, errorMessage: error.message }, 'OpenAI translation error');\n throw error;\n }\n }\n\n /**\n * Generate Swish Insight for a top performer (Chinese, data-driven)\n * @param {Object} payload - { playerName, gameStats, seasonStats }\n * @returns {Promise} 1-2 sentence insight in Chinese\n */\n async generateSwishInsight(payload) {\n if (!this.apiKey) {\n throw new Error('OpenAI API key not configured');\n }\n\n const { playerName, gameStats = {}, seasonStats = {} } = payload;\n const seasonData = seasonStats?.stats || seasonStats || {};\n const gamePts = parseInt(gameStats.points) || 0;\n const gameReb = parseInt(gameStats.rebounds) || 0;\n const gameAst = parseInt(gameStats.assists) || 0;\n const gameStl = parseInt(gameStats.steals) || 0;\n const gameBlk = parseInt(gameStats.blocks) || 0;\n const gameTov = parseInt(gameStats.turnovers) || 0;\n\n const fg = (gameStats.fieldGoals || '0-0').split('-');\n const three = (gameStats.threePointers || '0-0').split('-');\n const ft = (gameStats.freeThrows || '0-0').split('-');\n const fgm = parseInt(fg[0]) || 0;\n const fga = parseInt(fg[1]) || 0;\n const threePM = parseInt(three[0]) || 0;\n const threePA = parseInt(three[1]) || 0;\n const ftm = parseInt(ft[0]) || 0;\n const fta = parseInt(ft[1]) || 0;\n\n const fgPct = fga > 0 ? Math.round((fgm / fga) * 100) : 0;\n const threePct = threePA > 0 ? Math.round((threePM / threePA) * 100) : 0;\n const ftPct = fta > 0 ? Math.round((ftm / fta) * 100) : 0;\n\n const seasonPts = parseFloat(seasonData.avgPoints || seasonData.points || 0) || 0;\n const seasonFg = parseFloat(seasonData.fieldGoalPct || seasonData.fieldGoalPercentage || 0) || 0;\n const seasonThree = parseFloat(seasonData.threePointFieldGoalPct || seasonData.threePointFieldGoalPercentage || 0) || 0;\n const seasonFt = parseFloat(seasonData.freeThrowPct || seasonData.freeThrowPercentage || 0) || 0;\n const seasonReb = parseFloat(seasonData.avgRebounds || seasonData.rebounds || 0) || 0;\n const seasonAst = parseFloat(seasonData.avgAssists || seasonData.assists || 0) || 0;\n const seasonStl = parseFloat(seasonData.avgSteals || seasonData.steals || 0) || 0;\n const seasonBlk = parseFloat(seasonData.avgBlocks || seasonData.blocks || 0) || 0;\n const seasonTov = parseFloat(seasonData.avgTurnovers || seasonData.turnovers || 0) || 0;\n\n const userPrompt = `球员:${playerName}\n\n本场数据:${gamePts}分 ${gameReb}篮板 ${gameAst}助攻 ${gameStl}抢断 ${gameBlk}盖帽 ${gameTov}失误,投篮${fgm}/${fga}(${fgPct}%),三分${threePM}/${threePA}(${threePct}%),罚球${ftm}/${fta}(${ftPct}%)\n\n赛季场均:${seasonPts}分 ${seasonReb}篮板 ${seasonAst}助攻 ${seasonStl}抢断 ${seasonBlk}盖帽 ${seasonTov}失误,投篮${seasonFg}%,三分${seasonThree}%,罚球${seasonFt}%\n\n这是球员${playerName}的本场数据和赛季场均数据,请生成一句非常简短的数据洞察(最多20个汉字)。要求:\n- 突出球员对比赛的影响力\n- 球员对比赛的影响力包括进攻以及防守两个方面\n- 进攻球员需要在命中率以及得分上有显著的表现\n- 防守球员需要在抢断、篮板、盖帽以上有显著的表现\n- 如果球员有三双表现需要特别强调\n- 必须包含具体数字,不要解释背景\n- 按照你的理解,突出影响比赛的具体数据以及表现\n- 像体育解说的短评\n- 只输出一句话`;\n\n try {\n const response = await axios.post(\n `${this.baseURL}/chat/completions`,\n {\n model: this.model,\n messages: [\n {\n role: 'system',\n content: '你是一个NBA数据分析师。根据本场数据和赛季场均数据,生成一句简短的数据洞察,用中文。不要空洞赞美,要有具体对比。只输出洞察文字,不要引号或前缀。'\n },\n {\n role: 'user',\n content: userPrompt\n }\n ],\n temperature: 0.5,\n max_tokens: 40\n },\n {\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json'\n },\n timeout: 15000\n }\n );\n\n const insight = response.data?.choices?.[0]?.message?.content?.trim();\n if (!insight) {\n throw new Error('Empty response from OpenAI');\n }\n return insight;\n } catch (error) {\n logger.error({ component: 'openai', task: 'swishInsight', responseData: error.response?.data, errorMessage: error.message }, 'OpenAI Swish Insight error');\n throw error;\n }\n }\n\n /**\n * Build Step 2 prompt: Generate summary from analysis\n * @param {Object} analysis - Analysis JSON from step 1\n * @returns {string} Formatted prompt\n */\n buildSummaryPrompt(analysis) {\n const analysisJson = JSON.stringify(analysis, null, 2);\n\n return `请严格根据以下分析结果,用中文总结比赛。\n\n【写作规则】\n1. 只能使用提供的分析结论,禁止自行推断\n2. 若 close_game 为 true,尽量分析比赛成败关键因素,如果无法分析则不用描述\n3. 只能将 decisive_factors 中的数据作为胜因\n4. 若有加时赛,只能描述加时比分差异,若没有则不用描述加时赛\n5. 禁止使用情绪化或主观词语\n6. 如果不能准确翻译球队名称,请使用球队英文名或简称。\n7. 字数 60–120 字\n8. 语言可以轻松,口语化,不要过于正式\n\n【分析结果】\n${analysisJson}`;\n }\n}\n\nmodule.exports = new OpenAIService();\n"}}}}