| 252 | } |
| 253 | |
| 254 | function lintProjectAudioFiles( |
| 255 | projectDir: string, |
| 256 | htmlSources: HtmlSource[], |
| 257 | ): HyperframeLintFinding[] { |
| 258 | const findings: HyperframeLintFinding[] = []; |
| 259 | |
| 260 | let audioFiles: string[]; |
| 261 | try { |
| 262 | audioFiles = readdirSync(projectDir).filter((f) => |
| 263 | AUDIO_EXTENSIONS.has(extname(f).toLowerCase()), |
| 264 | ); |
| 265 | } catch { |
| 266 | return findings; |
| 267 | } |
| 268 | |
| 269 | if (audioFiles.length === 0) return findings; |
| 270 | |
| 271 | const hasAudioElement = htmlSources.some(({ html }) => /<audio\b/i.test(html)); |
| 272 | |
| 273 | if (!hasAudioElement) { |
| 274 | findings.push({ |
| 275 | code: "audio_file_without_element", |
| 276 | severity: "warning", |
| 277 | message: `Found audio file(s) in project (${audioFiles.join(", ")}) but no <audio> element in any composition. The rendered video will be silent.`, |
| 278 | fixHint: |
| 279 | 'Add an <audio id="my-audio" src="' + |
| 280 | audioFiles[0] + |
| 281 | '" data-start="0" data-duration="__DURATION__" data-track-index="0" data-volume="1"></audio> element inside the composition root. Replace __DURATION__ with the audio length in seconds.', |
| 282 | }); |
| 283 | } |
| 284 | |
| 285 | return findings; |
| 286 | } |
| 287 | |
| 288 | function lintAudioSrcNotFound( |
| 289 | projectDir: string, |