Skip to content

Commit 1419916

Browse files
Merge pull request #363 from opentok/devx-10220-post-call-transcription
feat: Add post-call transcription and summary
2 parents 3306c84 + ea612a7 commit 1419916

File tree

6 files changed

+333
-0
lines changed

6 files changed

+333
-0
lines changed

src/OpenTok/Archive.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,13 @@
9292
* "available"; for other archives, (including archives with the status "uploaded") this property is
9393
* set to null. The download URL is obfuscated, and the file is only available from the URL for
9494
* 10 minutes. To generate a new URL, call the Archive.listArchives() or OpenTok.getArchive() method.
95+
*
96+
* @property bool $hasTranscription
97+
* Whether the archive has a transcription of the audio of the session (true) or not (false).
98+
*
99+
* @property array $transcription
100+
* Properties of the transcription attached to this archive, including status, url, reason,
101+
* primaryLanguageCode, and hasSummary.
95102
*/
96103
class Archive
97104
{
@@ -184,6 +191,8 @@ public function __get($name)
184191
case 'streamMode':
185192
case 'maxBitrate':
186193
case 'quantizationParameter':
194+
case 'hasTranscription':
195+
case 'transcription':
187196
return $this->data[$name];
188197
case 'multiArchiveTag':
189198
return $this->multiArchiveTag;

src/OpenTok/OpenTok.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,19 @@ public function getRender($renderId): Render
546546
* (HD portrait), or "1080x1920" (FHD portrait). This property only applies to composed archives. If you set
547547
* this property and set the outputMode property to "individual", a call to the method
548548
* results in an error.</li>
549+
*
550+
* <li><code>'hasTranscription'</code> (Boolean) &mdash; Whether the archive will have a transcription of the audio
551+
* of the session (true) or not (false, the default).</li>
552+
*
553+
* <li><code>'transcriptionProperties'</code> (Array) &mdash; An array defining transcription properties. This array
554+
* includes the following keys:
555+
* <ul>
556+
* <li><code>'primaryLanguageCode'</code> (String) &mdash; The primary language spoken in the archive to be
557+
* transcribed, in BCP-47 format (e.g., "en-US", "es-ES", "pt-BR").</li>
558+
* <li><code>'hasSummary'</code> (Boolean) &mdash; Whether the transcription should include a summary of the
559+
* session (true) or not (false, the default).</li>
560+
* </ul>
561+
* </li>
549562
* </ul>
550563
*
551564
* @return Archive The Archive object, which includes properties defining the archive, including
@@ -570,16 +583,37 @@ public function startArchive(string $sessionId, $options = []): Archive
570583
'outputMode' => OutputMode::COMPOSED,
571584
'resolution' => null,
572585
'streamMode' => StreamMode::AUTO,
586+
'hasTranscription' => false,
587+
'transcriptionProperties' => null,
573588
);
574589

575590
// Horrible hack to workaround the defaults behaviour
576591
if (isset($options['maxBitrate'])) {
577592
$maxBitrate = $options['maxBitrate'];
578593
}
579594

595+
// Preserve transcription fields from user input
596+
$hasTranscription = isset($options['hasTranscription']) ? $options['hasTranscription'] : false;
597+
$transcriptionProperties = isset($options['transcriptionProperties']) ? $options['transcriptionProperties'] : null;
598+
580599
$options = array_merge($defaults, array_intersect_key($options, $defaults));
581600
list($name, $hasVideo, $hasAudio, $outputMode, $resolution, $streamMode) = array_values($options);
582601

602+
// Re-add transcription options to options array for API call
603+
$options['hasTranscription'] = $hasTranscription;
604+
if ($hasTranscription && $transcriptionProperties !== null) {
605+
$options['transcriptionProperties'] = $transcriptionProperties;
606+
}
607+
608+
if (isset($maxBitrate)) {
609+
$options['maxBitrate'] = $maxBitrate;
610+
}
611+
612+
// Remove null values before sending to API
613+
$options = array_filter($options, function($value) {
614+
return $value !== null;
615+
});
616+
583617
if (isset($maxBitrate)) {
584618
$options['maxBitrate'] = $maxBitrate;
585619
}
@@ -595,6 +629,8 @@ public function startArchive(string $sessionId, $options = []): Archive
595629
Validators::validateArchiveHasAudio($hasAudio);
596630
Validators::validateArchiveOutputMode($outputMode);
597631
Validators::validateHasStreamMode($streamMode);
632+
Validators::validateArchiveHasTranscription($hasTranscription);
633+
Validators::validateArchiveTranscriptionProperties($transcriptionProperties);
598634

599635
if ((is_null($resolution) || empty($resolution)) && $outputMode === OutputMode::COMPOSED) {
600636
$options['resolution'] = "640x480";

src/OpenTok/Util/Validators.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,4 +525,43 @@ public static function validateBroadcastBitrate($maxBitRate): void
525525
throw new \OutOfBoundsException('Max Bitrate must be between 400000 and 2000000');
526526
}
527527
}
528+
529+
public static function validateArchiveHasTranscription($hasTranscription)
530+
{
531+
if (!is_bool($hasTranscription)) {
532+
throw new InvalidArgumentException(
533+
'hasTranscription must be either true or false.'
534+
);
535+
}
536+
}
537+
538+
public static function validateArchiveTranscriptionProperties($transcriptionProperties)
539+
{
540+
if ($transcriptionProperties === null) {
541+
return;
542+
}
543+
544+
if (!is_array($transcriptionProperties)) {
545+
throw new InvalidArgumentException(
546+
'transcriptionProperties must be an array.'
547+
);
548+
}
549+
550+
if (isset($transcriptionProperties['primaryLanguageCode'])) {
551+
if (!is_string($transcriptionProperties['primaryLanguageCode']) ||
552+
empty($transcriptionProperties['primaryLanguageCode'])) {
553+
throw new InvalidArgumentException(
554+
'primaryLanguageCode must be a non-empty string in BCP-47 format.'
555+
);
556+
}
557+
}
558+
559+
if (isset($transcriptionProperties['hasSummary'])) {
560+
if (!is_bool($transcriptionProperties['hasSummary'])) {
561+
throw new InvalidArgumentException(
562+
'hasSummary must be either true or false.'
563+
);
564+
}
565+
}
566+
}
528567
}

tests/OpenTokTest/OpenTokTest.php

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1617,6 +1617,207 @@ public function testGetsExpiredArchive(): void
16171617
$this->assertEquals("expired", $archive->status);
16181618
}
16191619

1620+
public function testStartsArchiveWithTranscription(): void
1621+
{
1622+
// Arrange
1623+
$this->setupOTWithMocks([[
1624+
'code' => 200,
1625+
'headers' => [
1626+
'Content-Type' => 'application/json'
1627+
],
1628+
'path' => 'v2/project/APIKEY/archive/session_hasTranscription-true'
1629+
]]);
1630+
1631+
// This sessionId was generated using a different apiKey, but this method doesn't do any
1632+
// decoding to check, so it's fine.
1633+
$sessionId = '2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-';
1634+
1635+
// Act
1636+
$archive = $this->opentok->startArchive($sessionId, [
1637+
'hasTranscription' => true,
1638+
'transcriptionProperties' => [
1639+
'primaryLanguageCode' => 'en-US',
1640+
'hasSummary' => false
1641+
]
1642+
]);
1643+
1644+
// Assert
1645+
$this->assertCount(1, $this->historyContainer);
1646+
1647+
$request = $this->historyContainer[0]['request'];
1648+
$this->assertEquals('POST', strtoupper($request->getMethod()));
1649+
$this->assertEquals('/v2/project/' . $this->API_KEY . '/archive', $request->getUri()->getPath());
1650+
$this->assertEquals('api.opentok.com', $request->getUri()->getHost());
1651+
$this->assertEquals('https', $request->getUri()->getScheme());
1652+
1653+
$contentType = $request->getHeaderLine('Content-Type');
1654+
$this->assertNotEmpty($contentType);
1655+
$this->assertEquals('application/json', $contentType);
1656+
1657+
$authString = $request->getHeaderLine('X-OPENTOK-AUTH');
1658+
$this->assertEquals(true, TestHelpers::validateOpenTokAuthHeader($this->API_KEY, $this->API_SECRET, $authString));
1659+
1660+
// Test request body contains transcription fields
1661+
$body = json_decode($request->getBody());
1662+
$this->assertEquals(true, $body->hasTranscription);
1663+
$this->assertEquals('en-US', $body->transcriptionProperties->primaryLanguageCode);
1664+
$this->assertEquals(false, $body->transcriptionProperties->hasSummary);
1665+
1666+
// Test response properties
1667+
$this->assertInstanceOf('OpenTok\Archive', $archive);
1668+
$this->assertEquals(true, $archive->hasTranscription);
1669+
$this->assertIsArray($archive->transcription);
1670+
$this->assertEquals('requested', $archive->transcription['status']);
1671+
$this->assertEquals('en-US', $archive->transcription['primaryLanguageCode']);
1672+
$this->assertEquals(false, $archive->transcription['hasSummary']);
1673+
}
1674+
1675+
public function testStartsArchiveWithTranscriptionAndSummary(): void
1676+
{
1677+
// Arrange
1678+
$this->setupOTWithMocks([[
1679+
'code' => 200,
1680+
'headers' => [
1681+
'Content-Type' => 'application/json'
1682+
],
1683+
'path' => 'v2/project/APIKEY/archive/session_transcription-with-summary'
1684+
]]);
1685+
1686+
$sessionId = '2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-';
1687+
1688+
// Act
1689+
$archive = $this->opentok->startArchive($sessionId, [
1690+
'hasTranscription' => true,
1691+
'transcriptionProperties' => [
1692+
'primaryLanguageCode' => 'es-ES',
1693+
'hasSummary' => true
1694+
]
1695+
]);
1696+
1697+
// Assert
1698+
$this->assertCount(1, $this->historyContainer);
1699+
1700+
$request = $this->historyContainer[0]['request'];
1701+
1702+
// Test request body contains transcription fields
1703+
$body = json_decode($request->getBody());
1704+
$this->assertEquals(true, $body->hasTranscription);
1705+
$this->assertEquals('es-ES', $body->transcriptionProperties->primaryLanguageCode);
1706+
$this->assertEquals(true, $body->transcriptionProperties->hasSummary);
1707+
1708+
// Test response properties
1709+
$this->assertInstanceOf('OpenTok\Archive', $archive);
1710+
$this->assertEquals(true, $archive->hasTranscription);
1711+
$this->assertIsArray($archive->transcription);
1712+
$this->assertEquals('requested', $archive->transcription['status']);
1713+
$this->assertEquals('es-ES', $archive->transcription['primaryLanguageCode']);
1714+
$this->assertEquals(true, $archive->transcription['hasSummary']);
1715+
}
1716+
1717+
public function testStartsArchiveWithoutTranscription(): void
1718+
{
1719+
// Arrange
1720+
$this->setupOTWithMocks([[
1721+
'code' => 200,
1722+
'headers' => [
1723+
'Content-Type' => 'application/json'
1724+
],
1725+
'path' => 'v2/project/APIKEY/archive/session'
1726+
]]);
1727+
1728+
$sessionId = '2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-';
1729+
1730+
// Act - explicitly set hasTranscription to false
1731+
$archive = $this->opentok->startArchive($sessionId, [
1732+
'hasTranscription' => false
1733+
]);
1734+
1735+
// Assert
1736+
$this->assertCount(1, $this->historyContainer);
1737+
1738+
$request = $this->historyContainer[0]['request'];
1739+
1740+
// Test request body
1741+
$body = json_decode($request->getBody());
1742+
$this->assertEquals(false, $body->hasTranscription);
1743+
$this->assertObjectNotHasAttribute('transcriptionProperties', $body);
1744+
1745+
// Test response properties (archive without transcription)
1746+
$this->assertInstanceOf('OpenTok\Archive', $archive);
1747+
// The hasTranscription property should not be set in response if transcription is disabled
1748+
}
1749+
1750+
public function testCannotStartArchiveWithInvalidTranscriptionProperties(): void
1751+
{
1752+
$this->expectException(InvalidArgumentException::class);
1753+
$this->expectExceptionMessage('transcriptionProperties must be an array.');
1754+
1755+
// Set up the OpenTok instance first
1756+
$this->setupOTWithMocks([]);
1757+
1758+
$sessionId = '2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-';
1759+
1760+
// Act - pass invalid transcriptionProperties (not an array)
1761+
$this->opentok->startArchive($sessionId, [
1762+
'hasTranscription' => true,
1763+
'transcriptionProperties' => 'invalid'
1764+
]);
1765+
}
1766+
1767+
public function testCannotStartArchiveWithInvalidHasTranscription(): void
1768+
{
1769+
$this->expectException(InvalidArgumentException::class);
1770+
$this->expectExceptionMessage('hasTranscription must be either true or false.');
1771+
1772+
// Set up the OpenTok instance first
1773+
$this->setupOTWithMocks([]);
1774+
1775+
$sessionId = '2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-';
1776+
1777+
// Act - pass invalid hasTranscription (not a boolean)
1778+
$this->opentok->startArchive($sessionId, [
1779+
'hasTranscription' => 'invalid'
1780+
]);
1781+
}
1782+
1783+
public function testCannotStartArchiveWithInvalidPrimaryLanguageCode(): void
1784+
{
1785+
$this->setupOT();
1786+
1787+
$this->expectException(InvalidArgumentException::class);
1788+
$this->expectExceptionMessage('primaryLanguageCode must be a non-empty string in BCP-47 format.');
1789+
1790+
$sessionId = '2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-';
1791+
1792+
// Act - pass empty primaryLanguageCode
1793+
$this->opentok->startArchive($sessionId, [
1794+
'hasTranscription' => true,
1795+
'transcriptionProperties' => [
1796+
'primaryLanguageCode' => '',
1797+
'hasSummary' => false
1798+
]
1799+
]);
1800+
}
1801+
1802+
public function testCannotStartArchiveWithInvalidHasSummary(): void
1803+
{
1804+
$this->setupOT();
1805+
1806+
$this->expectException(InvalidArgumentException::class);
1807+
$this->expectExceptionMessage('hasSummary must be either true or false.');
1808+
1809+
$sessionId = '2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-';
1810+
1811+
// Act - pass invalid hasSummary (not a boolean)
1812+
$this->opentok->startArchive($sessionId, [
1813+
'hasTranscription' => true,
1814+
'transcriptionProperties' => [
1815+
'primaryLanguageCode' => 'en-US',
1816+
'hasSummary' => 'invalid'
1817+
]
1818+
]);
1819+
}
1820+
16201821
public function testForceDisconnect(): void
16211822
{
16221823
// Arrange
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"createdAt" : 1394321113584,
3+
"duration" : 0,
4+
"id" : "832641bf-5dbf-41a1-ad94-fea213e59a92",
5+
"name" : null,
6+
"partnerId" : 12345678,
7+
"reason" : "",
8+
"sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-",
9+
"size" : 0,
10+
"status" : "started",
11+
"url" : null,
12+
"hasVideo" : true,
13+
"hasAudio" : true,
14+
"outputMode" : "composed",
15+
"streamMode" : "auto",
16+
"hasTranscription" : true,
17+
"transcription" : {
18+
"status" : "requested",
19+
"url" : null,
20+
"reason" : null,
21+
"primaryLanguageCode" : "en-US",
22+
"hasSummary" : false
23+
}
24+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"createdAt" : 1394321113584,
3+
"duration" : 0,
4+
"id" : "832641bf-5dbf-41a1-ad94-fea213e59a93",
5+
"name" : null,
6+
"partnerId" : 12345678,
7+
"reason" : "",
8+
"sessionId" : "2_MX44NTQ1MTF-flR1ZSBOb3YgMTIgMDk6NDA6NTkgUFNUIDIwMTN-MC43NjU0Nzh-",
9+
"size" : 0,
10+
"status" : "started",
11+
"url" : null,
12+
"hasVideo" : true,
13+
"hasAudio" : true,
14+
"outputMode" : "composed",
15+
"streamMode" : "auto",
16+
"hasTranscription" : true,
17+
"transcription" : {
18+
"status" : "requested",
19+
"url" : null,
20+
"reason" : null,
21+
"primaryLanguageCode" : "es-ES",
22+
"hasSummary" : true
23+
}
24+
}

0 commit comments

Comments
 (0)