Skip to content

【待修复列表】代码审查报告 #14

@CJackHwang

Description

@CJackHwang

RadioNowhere 项目问题诊断报告

概览

本报告对 RadioNowhere 项目的三个核心问题进行了深入的代码审查和诊断分析,涵盖状态管理、响应式设计和多代理架构中的复杂同步问题。


问题 1:台词展开时异常显示

1.1 状态流转分析

当前架构问题

  • isSubtitleExpandedindex.tsx 第50行独立管理,与节目状态解耦
  • currentBlockId 通过 useRadioPlayer hook 异步更新(radioMonitor.on('script') 第43行)
  • SubtitleDisplay 组件无法感知 block 类型变化

关键代码位置

// index.tsx:50 - 独立状态管理
const [isSubtitleExpanded, setIsSubtitleExpanded] = React.useState(false);

// index.tsx:79-83 - 状态传递
<SubtitleDisplay
    currentLine={currentScript}
    isExpanded={isSubtitleExpanded}
    onExpandChange={setIsSubtitleExpanded}
/>

// useRadioPlayer.ts:41-44 - 异步状态更新
const cleanupScript = radioMonitor.on('script', (data: ScriptEvent) => {
    setCurrentScript(data);
    setCurrentBlockId(data.blockId);
});

1.2 根因定位

时序竞争问题

  1. 用户展开台词详情(talk block)
  2. 节目自动切换到 music block
  3. radioMonitor.emitScript('system', 'Playing: music', block.id) 执行(第455行)
  4. currentScript 更新,但 isSubtitleExpanded 保持 true
  5. SubtitleDisplay 试图显示 music meta 但保留 talk 的展开布局

布局冲突

  • SubtitleDisplay.tsx 第195行:showCover = displayInfo.type !== 'talk' && !isExpanded
  • 当 type 变为 music 但 isExpanded 仍为 true 时,封面接管但展开状态不匹配

1.3 修复建议

建议1:Block 类型感知自动收起

// 在 SubtitleDisplay.tsx 中添加 useEffect
useEffect(() => {
    if (currentLine && displayInfo.type !== 'talk' && isExpanded) {
        onExpandChange(false);
    }
}, [currentLine?.speaker, currentLine?.text, displayInfo.type]);

建议2:状态同步优化

// 在 index.tsx 中监听 currentScript 变化
useEffect(() => {
    if (currentScript?.speaker === 'music' && isSubtitleExpanded) {
        setIsSubtitleExpanded(false);
    }
}, [currentScript?.speaker]);

建议3:动画状态管理

// SubtitleDisplay.tsx 第74行,添加 layout transition
<motion.div
    layout
    transition={{ duration: 0.3, ease: "easeInOut" }}
    className={`... ${isSubtitleExpanded ? '...' : '...'}`}
>

建议4:Block 类型验证

// 在 SubtitleDisplay 中验证当前block是否支持展开
const canExpand = displayInfo.type === 'talk' && !!displayInfo.subtitle;

建议5:状态重置钩子

// 在 director-agent.ts 第455行附近添加
if (targetBlock.type !== 'talk' && isSubtitleExpanded) {
    radioMonitor.emit('expandState', false);
}

问题 2:手机窄屏输入框适配

2.1 当前布局约束分析

容器约束

/* MailboxDrawer.tsx:36 */
className="mt-6 w-full max-w-md mx-auto"
  • max-w-md = 28rem = 448px
  • 在320px宽度手机屏幕上:左右padding= (448-320)/2 = 64px,每边32px
  • 发送按钮区域被压缩

输入区域布局

/* MailboxDrawer.tsx:49 */
<div className="relative flex items-center gap-2">
    /* 左侧图标:pl-3 */
    /* 输入框:flex-1 px-2 */
    /* 右侧按钮:pr-1.5 gap-1.5 */
</div>

2.2 不同屏幕宽度分析

屏幕宽度 容器宽度 可用空间 发送按钮状态
320px 448px 272px 被顶到边缘
375px 448px 343px 边缘显示
414px 448px 434px 正常显示
768px 448px 720px 正常显示

2.3 响应式缺陷

缺陷1:固定最大宽度

/* 当前的 max-w-md 在小屏上不适用 */
max-w-md: 28rem; /* 448px */

缺陷2:缺乏堆叠布局

/* 没有针对窄屏的堆叠布局 */
flex items-center gap-2; /* 水平布局,在窄屏下不合理 */

缺陷3:按钮尺寸固定

/* 发送按钮在小屏上占空间过大 */
className="p-2.5" /* 10px padding */

2.4 修复建议

建议1:响应式宽度调整

// MailboxDrawer.tsx:36
className={`mt-6 w-full ${isMobile ? 'max-w-xs' : 'max-w-md'} mx-auto`}

建议2:窄屏堆叠布局

// MailboxDrawer.tsx:49
<div className={`relative ${isMobile ? 'flex-col gap-3' : 'flex items-center gap-2'}`}>
    {/* 按钮区域移到顶部 */}
    {isMobile && (
        <div className="flex gap-2 justify-end">
            {/* 发送和关闭按钮 */}
        </div>
    )}
    {/* 输入框区域 */}
    <div className="flex items-center gap-2">
        {/* 图标 + 输入框 */}
    </div>
    {/* 桌面端按钮区域 */}
    {!isMobile && (
        <div className="flex items-center gap-1.5 pr-1.5">
            {/* 发送和关闭按钮 */}
        </div>
    )}
</div>

建议3:移动端优化

/* 使用 Tailwind 响应式类 */
className="w-full max-w-sm sm:max-w-md"

建议4:按钮尺寸调整

// 移动端按钮更小
<motion.button
    className={`p-2 sm:p-2.5 rounded-xl`}
    whileHover={{ scale: 1.05 }}
    whileTap={{ scale: 0.95 }}
>
    <Send size={isMobile ? 12 : 14} />
</motion.button>

问题 3:节目手动切换多个问题

3.1 切换延迟根因分析

完整调用链路

UI点击 → jumpToBlock(uiIndex) [useRadioPlayer.ts:135]
    ↓
directorAgent.skipToBlock(actualIndex) [director-agent.ts:116]
    ↓
PlaybackController.skipToBlock(state, index) [playback-controller.ts:66]
    ↓
executeTimeline → executeBlock [director-agent.ts:407-510]

延迟来源分析

延迟点1:音频停止与清理

// playback-controller.ts:87
audioMixer.stopAll(); // 立即执行,但可能有延迟
  • Howler.js 音频停止需要时间
  • 混音器状态清理延迟

延迟点2:新 Block 准备检查

// director-agent.ts:435-452
if (!PreloadManager.isBlockPrepared(this.state, block)) {
    // 等待最多10秒
    while (!PreloadManager.isBlockPrepared(this.state, block) && Date.now() - startWait < maxWait) {
        await this.delay(500);
    }
}

延迟点3:TTS 音频生成

// talk-executor.ts:170-186
if (!state.preparedAudio.has(audioId)) {
    // 实时生成 TTS,导致延迟
    const result = await ttsAgent.generateSpeech(...);
}

延迟点4:状态同步等待

// useRadioPlayer.ts:36-76
// 异步事件处理,可能有延迟
const cleanupScript = radioMonitor.on('script', (data: ScriptEvent) => {
    setCurrentScript(data);
    setCurrentBlockId(data.blockId);
});

3.2 错误标签显示分析

TimelinePanel 标签生成问题

// TimelinePanel.tsx:19-26
function getBlockLabel(block: TimelineBlock): string {
    switch (block.type) {
        case 'talk': return block.scripts[0]?.text.slice(0, 15) || 'Conversation';
        case 'music': return block.search; // ❌ 直接使用搜索关键词
        case 'music_control': return `Control: ${block.action}`;
        default: return block.type;
    }
}

问题分析

  1. Music block 显示搜索关键词而非歌曲信息
  2. SubtitleDisplay 处理通用标签不当
  3. Music meta 同步延迟

具体问题位置

  • TimelinePanel.tsx 第22行:直接返回 block.search
  • SubtitleDisplay.tsx 第64-73行:降级逻辑显示 "Now Playing"
  • music-executor.ts 第287-293行:music meta 发送时机

3.3 多人讲话与台词同步的深度问题

3.3.1 批量 TTS 工作流分析

当前架构冲突

// talk-executor.ts:25-29
if (settings.ttsProvider === 'gemini' && uniqueSpeakers.size <= 2 && block.scripts.length >= 1) {
    await prepareTalkBlockBatched(state, block);
} else {
    await prepareTalkBlockSingle(state, block);
}

问题核心

  • Gemini TTS 批量生成:一段音频包含多个说话人的台词
  • 前端显示机制:仍基于 Microsoft TTS 的单句显示模式
  • 同步颗粒度不匹配:音频颗粒度(整段)vs 显示颗粒度(单句)

3.3.2 emitScript 调用时机和参数

批量模式调用链

// talk-executor.ts:125-129
for (const script of block.scripts) {
    radioMonitor.emitScript(script.speaker, script.text, block.id);
}
  • 立即发送所有脚本事件,但实际音频是一整段
  • 前端接收多个 ScriptEvent,但只有一个音频播放

单句模式调用链

// talk-executor.ts:194
radioMonitor.emitScript(script.speaker, script.text, block.id);
  • 逐句发送,与音频播放同步

3.3.3 前端显示机制与后端不匹配

SubtitleDisplay 处理逻辑

// SubtitleDisplay.tsx:29-101
useEffect(() => {
    // 处理 currentLine 更新
    if (!currentLine) return;
    
    const speaker = currentLine.speaker;
    const text = currentLine.text;
    
    // 处理多人讲话时只显示最后一个说话人
    setDisplayInfo({
        type: 'talk',
        speaker: speaker,
        displayName: displayName,
        subtitle: text  // ❌ 只显示最后一句
    });
}, [currentLine]);

问题表现

  1. 台词覆盖:新事件覆盖旧事件,只显示最后一句
  2. 多人显示缺失:无法同时显示多个说话人
  3. 音频时长不匹配:显示时间 vs 实际播放时间

3.3.4 音视频同步机制分析

当前同步模型

批量TTS:Audio Blob (5-10秒) 
    ↓ 播放
Script Events:script1, script2, script3 (瞬时发送)
    ↓ 接收
Frontend:只显示最后一个 (script3)

期望同步模型

批量TTS:Audio Blob (5-10秒)
    ↓ 播放
Script Events:分段发送或合并发送
    ↓ 接收
Frontend:显示合并台词或分段显示

3.3.5 架构性调整建议

建议1:批量脚本合并发送

// talk-executor.ts:125-135
// 合并多人对话为单个事件
const combinedText = block.scripts.map(s => `${s.speaker}: ${s.text}`).join('\n');
radioMonitor.emitScript(
    block.scripts.map(s => s.speaker).join('&'), 
    combinedText, 
    block.id
);

建议2:前端支持多人显示

// SubtitleDisplay.tsx
const parseMultiSpeaker = (text: string) => {
    // 解析 "speaker1: text1\nspeaker2: text2"
    const lines = text.split('\n');
    return lines.map(line => {
        const [speaker, ...content] = line.split(': ');
        return { speaker, text: content.join(': ') };
    });
};

建议3:音频时长同步

// 扩展 ScriptEvent 接口
interface ScriptEvent {
    speaker: string;
    text: string;
    blockId: string;
    startTime?: number;      // 音频开始时间
    duration?: number;      // 预期显示时长
    isMultiSpeaker?: boolean; // 是否多人对话
}

建议4:时间轴式显示

// 前端实现时间轴滚动显示
const [displayHistory, setDisplayHistory] = useState<ScriptEvent[]>([]);

useEffect(() => {
    if (currentLine) {
        setDisplayHistory(prev => [...prev, currentLine]);
        // 3秒后移除,避免积累过多
        setTimeout(() => {
            setDisplayHistory(prev => prev.filter(e => e !== currentLine));
        }, 3000);
    }
}, [currentLine]);

改进建议汇总

优先级排序

P0 - 立即修复

  1. TimelinePanel music block 标签显示:显示真实歌曲名而非搜索关键词
  2. 手机窄屏输入框适配:实现响应式布局和堆叠模式
  3. 展开状态自动重置:block切换时收起展开状态

P1 - 短期优化

  1. 切换延迟优化:音频停止优化和预加载策略
  2. 多人讲话显示:支持多说话人同时显示
  3. ScriptEvent 同步优化:改进 emitScript 时机

P2 - 架构重构

  1. 批量TTS显示机制重构:重新设计音频-文本同步模型
  2. 时间轴滚动显示:实现动态台词历史
  3. 响应式设计系统:统一移动端适配方案

架构性调整

1. 统一状态管理

  • isSubtitleExpanded 状态提升到 useRadioPlayer
  • 实现跨组件状态同步机制

2. 响应式设计系统

  • 建立统一的响应式断点系统
  • 实现组件级自适应布局

3. 音视频同步重构

  • 重新设计 ScriptEvent 数据结构
  • 实现基于时间的台词显示机制
  • 支持批量TTS的智能显示策略

测试策略

1. 状态同步测试

  • 验证展开状态与block切换的一致性
  • 测试不同网络条件下的同步延迟

2. 响应式测试

  • 多设备尺寸适配测试
  • 交互体验优化验证

3. 音视频同步测试

  • 批量TTS播放测试
  • 多人对话显示验证
  • 长时间播放稳定性测试

结论

本项目的主要问题源于架构演进过程中的兼容性设计缺陷和状态管理复杂性。建议优先修复显示类问题(问题1、2),然后逐步重构核心同步机制(问题3),确保系统的稳定性和用户体验的一致性。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions