├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpspec.yml.dist ├── spec ├── Client │ ├── Curl │ │ ├── HeaderGeneratorSpec.php │ │ ├── HeaderParserSpec.php │ │ ├── OptionsGeneratorSpec.php │ │ ├── ReadFunctionFactorySpec.php │ │ └── ReadFunctionSpec.php │ └── CurlSpec.php ├── Response │ ├── HypothesesListSpec.php │ └── HypothesisSpec.php ├── ResponseParser │ └── SimpleXMLSpec.php ├── Speech │ └── SpeechContentSpec.php ├── SpeechKitSpec.php └── Uploader │ ├── UploaderSpec.php │ └── UrlGeneratorSpec.php └── src ├── Client ├── ClientInterface.php ├── Curl.php └── Curl │ ├── HeaderGenerator.php │ ├── HeaderParser.php │ ├── OptionsGenerator.php │ ├── ReadFunction.php │ └── ReadFunctionFactory.php ├── Exception └── SpeechKitException.php ├── Response ├── HypothesesList.php └── Hypothesis.php ├── ResponseParser ├── ResponseParserInterface.php └── SimpleXML.php ├── Speech ├── SpeechContent.php ├── SpeechContentInterface.php └── SpeechInfoInterface.php ├── SpeechKit.php └── Uploader ├── Uploader.php ├── UploaderInterface.php └── UrlGenerator.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | 3 | coverage_clover: build/coverage.xml 4 | json_path: build/coveralls-upload.json 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | .idea 3 | t.php 4 | vendor 5 | build 6 | *.phar -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | matrix: 4 | include: 5 | - php: 5.4 6 | - php: 5.5 7 | - php: 5.6 8 | - php: hhvm 9 | env: GENERATE_COVERAGE='no' 10 | - php: 7 11 | fast_finish: true 12 | 13 | cache: 14 | directories: 15 | - $HOME/.composer/cache 16 | 17 | before_install: 18 | - composer selfupdate 19 | 20 | install: 21 | - composer install --prefer-source --no-interaction --dev 22 | - curl -Ls https://github.com/satooshi/php-coveralls/releases/download/v1.0.0/coveralls.phar > coveralls.phar 23 | - if [ "$GENERATE_COVERAGE" == "no" ]; then sed -i '/.*CodeCoverageExtension/d' phpspec.yml.dist; fi; 24 | 25 | script: 26 | - vendor/bin/phpspec run --format=pretty 27 | 28 | after_success: 29 | - if [ "$GENERATE_COVERAGE" != "no" ]; then travis_retry php coveralls.phar -v; fi; 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Evgeny Soynov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | speechkit-php 2 | ============= 3 | [![Build Status](https://travis-ci.org/ZloeSabo/speechkit-php.svg?branch=master)](https://travis-ci.org/ZloeSabo/speechkit-php) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/ZloeSabo/speechkit-php/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/ZloeSabo/speechkit-php/?branch=master) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/ZloeSabo/speechkit-php/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/ZloeSabo/speechkit-php/?branch=master) 6 | 7 | [Yandex SpeechKit](https://tech.yandex.com/speechkit/) PHP library. 8 | 9 | ## Installation 10 | 11 | SpeechKit uses Composer, please checkout the [composer website](http://getcomposer.org) for more information. 12 | 13 | Add SpeechKit in your composer.json and you can go ahead: 14 | 15 | ```bash 16 | composer require zloesabo/speechkit-php 17 | ``` 18 | 19 | > SpeechKit follows the PSR-4 convention names for its classes, which means you can easily integrate `SpeechKit` classes loading in your own autoloader. 20 | 21 | ## For users of old version 22 | 23 | Usage of previous version is strongly discouraged as it lacked concept and testing. 24 | However, if you sure you want to use previous version of library, require it with ```composer require zloesabo/speechkit-php:~1.0``` 25 | 26 | ## Usage 27 | 28 | ### Simple 29 | 30 | ```php 31 | // Include dependencies installed with composer 32 | require 'vendor/autoload.php'; 33 | 34 | use SpeechKit\Response\HypothesesList; 35 | use SpeechKit\Response\Hypothesis; 36 | use SpeechKit\Speech\SpeechContent; 37 | use SpeechKit\SpeechKit; 38 | 39 | $key = 'your-key-here'; 40 | 41 | $speechKit = new SpeechKit($key); 42 | 43 | //It can be any type of stream. File, string, instance of StreamInterface, etc. 44 | $source = fopen(__DIR__.'/some/path/to/file.mp3', 'r'); 45 | 46 | $speech = new SpeechContent($source); 47 | 48 | //Defaults will be used: mp3, general topic, russian language 49 | /** @var HypothesesList $result */ 50 | $result = $speechKit->recognize($speech); 51 | 52 | /** @var Hypothesis $hyphotesis */ 53 | foreach ($result as $hyphotesis) { 54 | echo sprintf( 55 | 'Confidence: %.2f Content: %s', 56 | $hyphotesis->getConfidence(), 57 | $hyphotesis->getContent() 58 | ), PHP_EOL; 59 | } 60 | ``` 61 | 62 | ### Advanced 63 | 64 | ```php 65 | 66 | require 'vendor/autoload.php'; 67 | 68 | use SpeechKit\Client\Curl; 69 | use SpeechKit\Response\HypothesesList; 70 | use SpeechKit\Response\Hypothesis; 71 | use SpeechKit\ResponseParser\SimpleXML; 72 | use SpeechKit\Speech\SpeechContent; 73 | use SpeechKit\Speech\SpeechContentInterface; 74 | use SpeechKit\SpeechKit; 75 | use SpeechKit\Uploader\Uploader; 76 | use SpeechKit\Uploader\UrlGenerator; 77 | 78 | $key = 'your-key-here'; 79 | 80 | $urlGenerator = new UrlGenerator($key); 81 | 82 | //You could use any type of client which implements ClientInterface 83 | $client = new Curl(); 84 | $uploader = new Uploader($urlGenerator, $client); 85 | 86 | //You could use any type of parser which implements ResponseParserInterface 87 | $responseParser = new SimpleXML(); 88 | 89 | $speechKit = new SpeechKit($key, $uploader, $responseParser); 90 | 91 | $source = fopen(__DIR__.'/some/path/to/file.mp3', 'r'); 92 | $speech = new SpeechContent($source); 93 | 94 | //These settings are default, so you can skip setting them 95 | $speech->setContentType(SpeechContentInterface::CONTENT_MP3); 96 | $speech->setTopic(SpeechContentInterface::TOPIC_GENERAL); 97 | $speech->setLang(SpeechContentInterface::LANG_RU); 98 | $speech->setUuid(bin2hex(openssl_random_pseudo_bytes(16))); 99 | 100 | /** @var HypothesesList $result */ 101 | $result = $speechKit->recognize($speech); 102 | 103 | /** @var Hypothesis $hyphotesis */ 104 | foreach ($result as $hyphotesis) { 105 | echo sprintf( 106 | 'Confidence: %.2f Content: %s', 107 | $hyphotesis->getConfidence(), 108 | $hyphotesis->getContent() 109 | ), PHP_EOL; 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zloesabo/speechkit-php", 3 | "description": "Yandex SpeechKit for PHP", 4 | "keywords": ["speechkit", "yandex"], 5 | "authors": [ 6 | { 7 | "name": "Evgeny Soynov", 8 | "email": "saboteur@saboteur.me" 9 | } 10 | ], 11 | "license": "MIT", 12 | "minimum-stability": "stable", 13 | "require": { 14 | "php": ">=5.4.0", 15 | "ext-curl": "*", 16 | "ext-simplexml": "*", 17 | "guzzlehttp/psr7": "~1.1" 18 | }, 19 | "autoload": { 20 | "psr-4": { 21 | "SpeechKit\\": "src/" 22 | } 23 | }, 24 | "require-dev": { 25 | "phpspec/phpspec": "~2.0", 26 | "henrikbjorn/phpspec-code-coverage": "dev-master" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /phpspec.yml.dist: -------------------------------------------------------------------------------- 1 | suites: 2 | speechkit: 3 | namespace: SpeechKit 4 | psr4_prefix: SpeechKit 5 | 6 | extensions: 7 | - PhpSpec\Extension\CodeCoverageExtension 8 | 9 | code_coverage: 10 | format: 11 | - html 12 | - clover 13 | output: 14 | html: build/coverage 15 | clover: build/coverage.xml 16 | -------------------------------------------------------------------------------- /spec/Client/Curl/HeaderGeneratorSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('SpeechKit\Client\Curl\HeaderGenerator'); 14 | } 15 | 16 | public function it_combines_multiple_header_values_to_one(RequestInterface $request) 17 | { 18 | $request->getHeaders()->willReturn(['TestHeader' => ['Value1', 'Value2']]); 19 | 20 | $this->generate($request)->shouldContain('TestHeader: Value1, Value2'); 21 | } 22 | 23 | public function it_adds_transfer_encoding_header(RequestInterface $request) 24 | { 25 | $request->getHeaders()->willReturn([]); 26 | 27 | $this->generate($request)->shouldContain('Transfer-Encoding: chunked'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /spec/Client/Curl/HeaderParserSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('SpeechKit\Client\Curl\HeaderParser'); 13 | } 14 | 15 | public function it_returns_length_of_processed_header() 16 | { 17 | $this->parseFunction(null, '123')->shouldReturn(3); 18 | } 19 | 20 | public function it_stores_status_info_when_header_contains_http_version() 21 | { 22 | $this->parseFunction(null, 'HTTP/1.1 505 HTTP Version not supported'); 23 | $this->getStatusInfo()->shouldReturn([505, 'HTTP Version not supported']); 24 | $this->getHeaders()->shouldReturn([]); 25 | } 26 | 27 | public function it_stores_header_info_when_header_contains_colon() 28 | { 29 | $this->parseFunction(null, 'Date: Tue, 05 Apr 2016 20:01:24 GMT '); 30 | $this->getStatusInfo()->shouldReturn([]); 31 | $this->getHeaders()->shouldReturn(['Date' => 'Tue, 05 Apr 2016 20:01:24 GMT']); 32 | } 33 | 34 | public function it_resets_stored_data() 35 | { 36 | $this->parseFunction(null, 'HTTP/1.1 505 HTTP Version not supported'); 37 | $this->parseFunction(null, 'Date: Tue, 05 Apr 2016 20:01:24 GMT '); 38 | 39 | $this->reset(); 40 | 41 | $this->getStatusInfo()->shouldReturn([]); 42 | $this->getHeaders()->shouldReturn([]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spec/Client/Curl/OptionsGeneratorSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($headerParser, $headerGenerator, $readFunctionFactory); 19 | } 20 | 21 | public function it_is_initializable() 22 | { 23 | $this->shouldHaveType('SpeechKit\Client\Curl\OptionsGenerator'); 24 | } 25 | 26 | public function it_returns_array_of_options(RequestInterface $request, StreamInterface $body) 27 | { 28 | $request->getBody()->willReturn($body); 29 | 30 | $this->generate($request)->shouldBeArray(); 31 | } 32 | 33 | public function it_sets_static_options(RequestInterface $request, StreamInterface $body) 34 | { 35 | $request->getBody()->willReturn($body); 36 | 37 | $this->generate($request)->shouldHaveKeyWithValue(CURLOPT_UPLOAD, true); 38 | $this->generate($request)->shouldHaveKeyWithValue(CURLOPT_POST, true); 39 | $this->generate($request)->shouldHaveKeyWithValue(CURLOPT_RETURNTRANSFER, true); 40 | } 41 | 42 | public function it_generates_headers_using_generator(RequestInterface $request, StreamInterface $body, HeaderGenerator $headerGenerator) 43 | { 44 | $request->getBody()->willReturn($body); 45 | $headerGenerator->generate($request)->willReturn(['generated', 'headers']); 46 | 47 | $this->generate($request)->shouldHaveKeyWithValue(CURLOPT_HTTPHEADER, ['generated', 'headers']); 48 | } 49 | 50 | public function it_uses_parse_function_from_header_parser(RequestInterface $request, StreamInterface $body, HeaderParser $headerParser) 51 | { 52 | $request->getBody()->willReturn($body); 53 | 54 | $this->generate($request)->shouldHaveKeyWithValue(CURLOPT_HEADERFUNCTION, [$headerParser, 'parseFunction']); 55 | } 56 | 57 | public function it_creates_read_function_using_factory(RequestInterface $request, StreamInterface $body, ReadFunctionFactory $readFunctionFactory, ReadFunction $readFunction) 58 | { 59 | $request->getBody()->willReturn($body); 60 | $readFunctionFactory->create($body)->willReturn($readFunction); 61 | 62 | $this->generate($request)->shouldHaveKeyWithValue(CURLOPT_READFUNCTION, [$readFunction, 'read']); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /spec/Client/Curl/ReadFunctionFactorySpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('SpeechKit\Client\Curl\ReadFunctionFactory'); 14 | } 15 | 16 | public function it_creates_read_function_for_given_stream(StreamInterface $stream) 17 | { 18 | $this->create($stream)->shouldReturnAnInstanceOf('SpeechKit\Client\Curl\ReadFunction'); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/Client/Curl/ReadFunctionSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($stream); 14 | } 15 | 16 | public function it_is_initializable() 17 | { 18 | $this->shouldHaveType('SpeechKit\Client\Curl\ReadFunction'); 19 | } 20 | 21 | public function it_reads_from_wrapped_stream(StreamInterface $stream) 22 | { 23 | $stream->read(3)->willReturn('123'); 24 | 25 | $this->read(null, null, 3)->shouldReturn('123'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /spec/Client/CurlSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($headerParser, $optionsGenerator); 17 | } 18 | 19 | public function it_is_initializable() 20 | { 21 | $this->shouldHaveType('SpeechKit\Client\Curl'); 22 | } 23 | 24 | public function it_is_client() 25 | { 26 | $this->shouldImplement('SpeechKit\Client\ClientInterface'); 27 | } 28 | 29 | public function it_uploads_given_request(RequestInterface $request, HeaderParser $headerParser, OptionsGenerator $optionsGenerator) 30 | { 31 | $request->getUri()->willReturn('http://example.com'); 32 | $headerParser->reset()->willReturn(null); 33 | $headerParser->getHeaders()->willReturn(['headers']); 34 | $headerParser->getStatusInfo()->willReturn(10, 'OKAY'); 35 | $optionsGenerator->generate($request)->willReturn([CURLOPT_RETURNTRANSFER => true]); 36 | 37 | $this->upload($request)->shouldReturnAnInstanceOf('Psr\Http\Message\ResponseInterface'); 38 | } 39 | 40 | public function it_throws_when_connnection_error_happens(RequestInterface $request, OptionsGenerator $optionsGenerator) 41 | { 42 | $request->getUri()->willReturn('http://localhost:999999'); 43 | $optionsGenerator->generate($request)->willReturn([CURLOPT_RETURNTRANSFER => true]); 44 | 45 | $this->shouldThrow('SpeechKit\Exception\SpeechKitException')->duringUpload($request); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /spec/Response/HypothesesListSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(1); 13 | } 14 | 15 | public function it_is_initializable() 16 | { 17 | $this->shouldHaveType('SpeechKit\Response\HypothesesList'); 18 | } 19 | 20 | public function it_fails_when_adding_non_hyphotesis() 21 | { 22 | $this->shouldThrow('\InvalidArgumentException')->duringOffsetSet(0, new \stdClass()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /spec/Response/HypothesisSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(0.2, 'test'); 13 | } 14 | 15 | public function it_is_initializable() 16 | { 17 | $this->shouldHaveType('SpeechKit\Response\Hypothesis'); 18 | } 19 | 20 | public function it_does_not_modify_given_data() 21 | { 22 | $this->getConfidence()->shouldReturn(0.2); 23 | $this->getContent()->shouldReturn('test'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /spec/ResponseParser/SimpleXMLSpec.php: -------------------------------------------------------------------------------- 1 | shouldHaveType('SpeechKit\ResponseParser\SimpleXML'); 18 | } 19 | 20 | public function it_has_parse_function() 21 | { 22 | $this->shouldImplement('SpeechKit\ResponseParser\ResponseParserInterface'); 23 | } 24 | 25 | public function it_throws_exception_when_given_malformed_xml(ResponseInterface $response, StreamInterface $body) 26 | { 27 | $response->getBody()->willReturn($body); 28 | $body->getContents()->willReturn('DEADBEEF'); 29 | $this->shouldThrow('\SpeechKit\Exception\SpeechKitException')->duringParse($response); 30 | } 31 | 32 | public function it_returns_hyphoteses_list(ResponseInterface $response, StreamInterface $body) 33 | { 34 | $xml=<< 36 | 37 | Dead beef 38 | Something else 39 | 40 | XML; 41 | $response->getBody()->willReturn($body); 42 | $body->getContents()->willReturn($xml); 43 | 44 | $this->parse($response)->shouldReturnAnInstanceOf('SpeechKit\Response\HypothesesList'); 45 | $this->parse($response)->shouldHaveHaveHyphotesis(0, 0.84, 'Dead beef'); 46 | $this->parse($response)->shouldHaveHaveHyphotesis(1, 0.5, 'Something else'); 47 | } 48 | 49 | public function it_returns_empty_list_when_recognition_failed(ResponseInterface $response, StreamInterface $body) 50 | { 51 | $xml=<< 53 | 54 | XML; 55 | $response->getBody()->willReturn($body); 56 | $body->getContents()->willReturn($xml); 57 | 58 | $this->parse($response)->shouldReturnAnInstanceOf('SpeechKit\Response\HypothesesList'); 59 | $this->parse($response)->shouldHaveCount(0); 60 | } 61 | 62 | public function getMatchers() 63 | { 64 | return [ 65 | 'haveHaveHyphotesis' => function ($subject, $key, $confidence, $content) { 66 | if (!isset($subject[$key])) { 67 | throw new FailureException(sprintf( 68 | 'Key "%s" does not exist in subject "%s".', 69 | $key, get_class($subject) 70 | )); 71 | } 72 | $checked = $subject[$key]; 73 | if(false === $checked instanceof Hypothesis) { 74 | throw new FailureException(sprintf( 75 | 'Key "%s" in subject "%s" is hot hypothesis.', 76 | $key, get_class($subject) 77 | )); 78 | } 79 | 80 | /** @var Hypothesis $checked */ 81 | return $confidence === $checked->getConfidence() 82 | && $content === $checked->getContent() 83 | ; 84 | } 85 | ]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /spec/Speech/SpeechContentSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($this->path); 16 | } 17 | 18 | public function it_is_initializable() 19 | { 20 | $this->shouldHaveType('SpeechKit\Speech\SpeechContent'); 21 | } 22 | 23 | public function it_has_methods_to_get_stream_and_its_meta() 24 | { 25 | $this->shouldImplement('SpeechKit\Speech\SpeechContentInterface'); 26 | } 27 | 28 | public function it_has_default_parameters() 29 | { 30 | $this->getContentType()->shouldReturn('audio/x-mpeg-3'); 31 | $this->getTopic()->shouldReturn('general'); 32 | $this->getLang()->shouldReturn('ru-RU'); 33 | $this->getUuid()->shouldMatch('/^\w{32}$/'); 34 | } 35 | 36 | public function it_does_not_modify_setter_arguments() 37 | { 38 | $this->setContentType('content type'); 39 | $this->getContentType()->shouldBe('content type'); 40 | 41 | $this->setTopic('topic'); 42 | $this->getTopic()->shouldBe('topic'); 43 | 44 | $this->setLang('language'); 45 | $this->getLang()->shouldBe('language'); 46 | 47 | $this->setUuid('uuid'); 48 | $this->getUuid()->shouldBe('uuid'); 49 | } 50 | 51 | public function it_wraps_given_source_to_stream() 52 | { 53 | $this->getStream()->shouldReturnAnInstanceOf('\Psr\Http\Message\StreamInterface'); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /spec/SpeechKitSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($this->key, $uploader, $responseParser); 19 | } 20 | 21 | public function it_is_initializable() 22 | { 23 | $this->shouldHaveType('SpeechKit\SpeechKit'); 24 | } 25 | 26 | public function it_processes_speech_recognition_with_speechkit_api( 27 | SpeechContentInterface $speech, 28 | ResponseInterface $response, 29 | UploaderInterface $uploader, 30 | ResponseParserInterface $responseParser 31 | ) { 32 | $uploader->upload($speech)->willReturn($response); 33 | $responseParser->parse($response)->willReturn('hyphoteses list'); 34 | 35 | $this->recognize($speech)->shouldReturn('hyphoteses list'); 36 | } 37 | 38 | public function it_is_initializable_with_only_key() 39 | { 40 | $this->beConstructedWith($this->key); 41 | $this->shouldHaveType('SpeechKit\SpeechKit'); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /spec/Uploader/UploaderSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith($generator, $client); 21 | } 22 | 23 | public function it_is_initializable() 24 | { 25 | $this->shouldHaveType('SpeechKit\Uploader\Uploader'); 26 | } 27 | 28 | public function it_has_upload_function() 29 | { 30 | $this->shouldImplement('SpeechKit\Uploader\UploaderInterface'); 31 | } 32 | 33 | public function it_uploads_speech_to_url_from_generator(UrlGenerator $generator, ClientInterface $client, SpeechContentInterface $speech, ResponseInterface $response, Uri $generatedUri) 34 | { 35 | $generator->generate($speech)->willReturn($generatedUri); 36 | $client->upload(Argument::type('Psr\Http\Message\RequestInterface'))->willReturn($response); 37 | 38 | $this->upload($speech)->shouldReturn($response); 39 | $client->upload(Argument::type('Psr\Http\Message\RequestInterface'))->shouldHaveBeenCalled(); 40 | $client->upload(Argument::which('getUri', $generatedUri->getWrappedObject()))->shouldHaveBeenCalled(); 41 | } 42 | 43 | public function it_takes_content_type_from_speech(UrlGenerator $generator, ClientInterface $client, SpeechContentInterface $speech, ResponseInterface $response, Uri $generatedUri) 44 | { 45 | $speech->getContentType()->willReturn('test/test'); 46 | $speech->getStream()->willReturn(null); 47 | $generator->generate($speech)->willReturn($generatedUri); 48 | $client->upload(Argument::type('Psr\Http\Message\RequestInterface'))->willReturn($response); 49 | 50 | $this->upload($speech)->shouldReturn($response); 51 | $client->upload(Argument::type('Psr\Http\Message\RequestInterface'))->shouldHaveBeenCalled(); 52 | $client->upload(Argument::which('getHeaders', ['Content-Type' => ['test/test']]))->shouldHaveBeenCalled(); 53 | } 54 | 55 | public function it_uploads_speech(UrlGenerator $generator, ClientInterface $client, SpeechContentInterface $speech, StreamInterface $uploadedStream, ResponseInterface $response, Uri $generatedUri) 56 | { 57 | $speech->getStream()->willReturn($uploadedStream); 58 | $speech->getContentType()->willReturn(null); 59 | $generator->generate($speech)->willReturn($generatedUri); 60 | $client->upload(Argument::type('Psr\Http\Message\RequestInterface'))->willReturn($response); 61 | 62 | $this->upload($speech)->shouldReturn($response); 63 | $client->upload(Argument::type('Psr\Http\Message\RequestInterface'))->shouldHaveBeenCalled(); 64 | $client->upload(Argument::which('getBody', $uploadedStream->getWrappedObject()))->shouldHaveBeenCalled(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /spec/Uploader/UrlGeneratorSpec.php: -------------------------------------------------------------------------------- 1 | beConstructedWith(self::KEY); 17 | } 18 | 19 | public function it_is_initializable() 20 | { 21 | $this->shouldHaveType('SpeechKit\Uploader\UrlGenerator'); 22 | } 23 | 24 | public function it_returns_uri_instance(SpeechInfoInterface $speech) 25 | { 26 | $this->generate($speech)->shouldReturnAnInstanceOf('GuzzleHttp\Psr7\Uri'); 27 | } 28 | 29 | public function it_generates_valid_url(SpeechInfoInterface $speech) 30 | { 31 | $speech->getUuid()->willReturn('12345'); 32 | $speech->getLang()->willReturn('ru-RU'); 33 | $speech->getTopic()->willReturn('notes'); 34 | 35 | //TODO update this spec to use uri functions 36 | $this->generate($speech)->shouldHaveUrlParams([ 37 | 'url' => 'http://asr.yandex.net/asr_xml', 38 | 'uuid' => '12345', 39 | 'lang' => 'ru-RU', 40 | 'topic' => 'notes', 41 | 'key' => self::KEY 42 | ]); 43 | } 44 | 45 | public function getMatchers() 46 | { 47 | return [ 48 | 'haveUrlParams' => function ($subject, $expectation) { 49 | $urlInfo = parse_url($subject); 50 | $actualUrl = sprintf('%s://%s%s', $urlInfo['scheme'], $urlInfo['host'], $urlInfo['path']); 51 | if($expectation['url'] !== $actualUrl) { 52 | return false; 53 | } 54 | unset($expectation['url']); 55 | parse_str($urlInfo['query'], $actual); 56 | 57 | return $actual == $expectation; 58 | } 59 | ]; 60 | } 61 | } -------------------------------------------------------------------------------- /src/Client/ClientInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface ClientInterface 12 | { 13 | /** 14 | * @param RequestInterface $request 15 | * @throws \SpeechKit\Exception\SpeechKitException 16 | * @return ResponseInterface 17 | */ 18 | public function upload(RequestInterface $request); 19 | } -------------------------------------------------------------------------------- /src/Client/Curl.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Curl implements ClientInterface 15 | { 16 | /** @var OptionsGenerator */ 17 | private $optionsGenerator; 18 | /** @var HeaderParser */ 19 | private $headerParser; 20 | 21 | /** 22 | * @param HeaderParser|null $headerParser 23 | * @param OptionsGenerator|null $generator 24 | */ 25 | public function __construct(HeaderParser $headerParser = null, OptionsGenerator $generator = null) 26 | { 27 | $this->headerParser = $headerParser ?: new HeaderParser(); 28 | $this->optionsGenerator = $generator ?: new OptionsGenerator($this->headerParser); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function upload(RequestInterface $request) 35 | { 36 | $this->headerParser->reset(); 37 | 38 | $uri = $request->getUri(); 39 | 40 | $ch = curl_init((string)$uri); 41 | 42 | $options = $this->optionsGenerator->generate($request); 43 | curl_setopt_array($ch, $options); 44 | 45 | $body = curl_exec($ch); 46 | if (0 !== curl_errno($ch)) { 47 | throw new SpeechKitException(curl_error($ch), curl_errno($ch)); 48 | } 49 | list($status, $reason) = $this->headerParser->getStatusInfo(); 50 | $headers = $this->headerParser->getHeaders(); 51 | $body = \GuzzleHttp\Psr7\stream_for($body); 52 | 53 | return new Response($status, $headers, $body, '1.1', $reason); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Client/Curl/HeaderGenerator.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class HeaderGenerator 11 | { 12 | const CHUNKED_TRANSFER_HEADER = 'Transfer-Encoding: chunked'; 13 | 14 | /** 15 | * Generates headers for given request 16 | * @param RequestInterface $request 17 | * @return array 18 | */ 19 | public function generate(RequestInterface $request) 20 | { 21 | $headers = []; 22 | foreach ($request->getHeaders() as $name => $values) { 23 | $headers[] = sprintf('%s: %s', $name, implode(', ', $values)); 24 | } 25 | $headers[] = self::CHUNKED_TRANSFER_HEADER; 26 | 27 | return $headers; 28 | } 29 | } -------------------------------------------------------------------------------- /src/Client/Curl/HeaderParser.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class HeaderParser 9 | { 10 | const HTTP_VERSION = 'HTTP/'; 11 | const HTTP_STATUS_DELIMITER = ' '; 12 | const HEADER_DELIMITER = ':'; 13 | 14 | /** @var array */ 15 | private $statusInfo = []; 16 | /** @var array */ 17 | private $headers = []; 18 | 19 | /** 20 | * Reset parser 21 | */ 22 | public function reset() 23 | { 24 | $this->statusInfo = []; 25 | $this->headers = []; 26 | } 27 | 28 | /** 29 | * Get status code and its text description 30 | * @return array 31 | */ 32 | public function getStatusInfo() 33 | { 34 | return $this->statusInfo; 35 | } 36 | 37 | /** 38 | * Get parsed headers 39 | * @return array 40 | */ 41 | public function getHeaders() 42 | { 43 | return $this->headers; 44 | } 45 | 46 | /** 47 | * @param int $_ not used 48 | * @param string $headerLine header to parse 49 | * @return int length of the parsed header line 50 | */ 51 | public function parseFunction($_, $headerLine) 52 | { 53 | if (false !== stripos($headerLine, self::HTTP_VERSION)) { 54 | list(, $code, $status) = explode(self::HTTP_STATUS_DELIMITER, $headerLine, 3); 55 | $this->statusInfo = [(int)$code, $status]; 56 | } elseif (false !== stripos($headerLine, self::HEADER_DELIMITER)) { 57 | list($headerName, $headerValue) = explode(self::HEADER_DELIMITER, $headerLine, 2); 58 | $this->headers[$headerName] = trim($headerValue); 59 | } 60 | 61 | return strlen($headerLine); 62 | } 63 | } -------------------------------------------------------------------------------- /src/Client/Curl/OptionsGenerator.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class OptionsGenerator 11 | { 12 | /** @var HeaderParser */ 13 | private $headerParser; 14 | /** @var HeaderGenerator */ 15 | private $headerGenerator; 16 | /** @var ReadFunctionFactory */ 17 | private $readFunctionFactory; 18 | 19 | /** 20 | * @param HeaderParser|null $headerParser 21 | * @param HeaderGenerator|null $headerGenerator 22 | * @param ReadFunctionFactory|null $readFunctionFactory 23 | */ 24 | public function __construct( 25 | HeaderParser $headerParser = null, 26 | HeaderGenerator $headerGenerator = null, 27 | ReadFunctionFactory $readFunctionFactory = null 28 | ) { 29 | $this->headerParser = $headerParser ?: new HeaderParser(); 30 | $this->headerGenerator = $headerGenerator ?: new HeaderGenerator(); 31 | $this->readFunctionFactory = $readFunctionFactory ?: new ReadFunctionFactory(); 32 | } 33 | 34 | /** 35 | * Generates options to make multipart upload of given request 36 | * @param RequestInterface $request subject to options generation 37 | * @return array 38 | */ 39 | public function generate(RequestInterface $request) 40 | { 41 | $readFunction = $this->readFunctionFactory->create($request->getBody()); 42 | 43 | return [ 44 | CURLOPT_UPLOAD => true, 45 | CURLOPT_POST => true, 46 | CURLOPT_RETURNTRANSFER => true, 47 | CURLOPT_READFUNCTION => [$readFunction, 'read'], 48 | CURLOPT_HEADERFUNCTION => [$this->headerParser, 'parseFunction'], 49 | CURLOPT_HTTPHEADER => $this->headerGenerator->generate($request) 50 | ]; 51 | } 52 | } -------------------------------------------------------------------------------- /src/Client/Curl/ReadFunction.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ReadFunction 11 | { 12 | /** @var StreamInterface */ 13 | private $stream; 14 | 15 | /** 16 | * @param StreamInterface $stream 17 | */ 18 | public function __construct(StreamInterface $stream) 19 | { 20 | $this->stream = $stream; 21 | } 22 | 23 | /** 24 | * @param int $unused1 not used 25 | * @param int $unused2 not used 26 | * @param int $length amount of data to send to server 27 | * @return string 28 | */ 29 | public function read($unused1, $unused2, $length) 30 | { 31 | return $this->stream->read($length); 32 | } 33 | } -------------------------------------------------------------------------------- /src/Client/Curl/ReadFunctionFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ReadFunctionFactory 11 | { 12 | /** 13 | * Creates ReadFunction instance for given stream 14 | * @param StreamInterface $stream 15 | * @return ReadFunction 16 | */ 17 | public function create(StreamInterface $stream) 18 | { 19 | return new ReadFunction($stream); 20 | } 21 | } -------------------------------------------------------------------------------- /src/Exception/SpeechKitException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class SpeechKitException extends \RuntimeException 9 | { 10 | 11 | } -------------------------------------------------------------------------------- /src/Response/HypothesesList.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class HypothesesList extends \SplFixedArray 9 | { 10 | /** 11 | * {@inheritdoc} 12 | */ 13 | public function offsetSet($index, $newval) 14 | { 15 | if (!empty($newval) && false === $newval instanceof Hypothesis) { 16 | throw new \InvalidArgumentException( 17 | sprintf('HypothesesList could only contain Hypothesis, %s given.', get_class($newval)) 18 | ); 19 | } 20 | parent::offsetSet($index, $newval); 21 | } 22 | 23 | /** 24 | * @codeCoverageIgnore 25 | */ 26 | private function __clone() 27 | { 28 | // Only required for specs to work 29 | } 30 | } -------------------------------------------------------------------------------- /src/Response/Hypothesis.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class Hypothesis 9 | { 10 | /** @var float */ 11 | private $confidence; 12 | /** @var string */ 13 | private $content; 14 | 15 | /** 16 | * @param float $confidence 17 | * @param string $content 18 | */ 19 | public function __construct($confidence, $content) 20 | { 21 | $this->confidence = $confidence; 22 | $this->content = $content; 23 | } 24 | 25 | /** 26 | * @return float 27 | */ 28 | public function getConfidence() 29 | { 30 | return $this->confidence; 31 | } 32 | 33 | /** 34 | * @return string 35 | */ 36 | public function getContent() 37 | { 38 | return $this->content; 39 | } 40 | } -------------------------------------------------------------------------------- /src/ResponseParser/ResponseParserInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface ResponseParserInterface 12 | { 13 | /** 14 | * Parse xml inside of response from api 15 | * @param ResponseInterface $result 16 | * @return HypothesesList 17 | */ 18 | public function parse(ResponseInterface $result); 19 | } -------------------------------------------------------------------------------- /src/ResponseParser/SimpleXML.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class SimpleXML implements ResponseParserInterface 14 | { 15 | const SUCCESSFUL_RESULT = 1; 16 | 17 | /** 18 | * Creates Hyphotesis list from given response which contains xml. 19 | * Hyphotesis in list are in the order provided by api. 20 | * @param ResponseInterface $response 21 | * @return HypothesesList 22 | */ 23 | public function parse(ResponseInterface $response) 24 | { 25 | $contents = $response->getBody()->getContents(); 26 | $xml = simplexml_load_string($contents, 'SimpleXMLElement', LIBXML_NOERROR | LIBXML_NOWARNING); 27 | 28 | if (false === $xml instanceof \SimpleXMLElement) { 29 | throw new SpeechKitException( 30 | sprintf('Could not parse response contents: %s', $contents) 31 | ); 32 | } 33 | 34 | if (self::SUCCESSFUL_RESULT !== (int)$xml->attributes()->success) { 35 | return new HypothesesList(); 36 | } 37 | 38 | $result = new HypothesesList(count($xml->variant)); 39 | $current = 0; 40 | foreach ($xml->variant as $variant) { 41 | $confidence = (float)$variant->attributes()->confidence; 42 | $result[$current] = new Hypothesis($confidence, (string)$variant); 43 | $current++; 44 | } 45 | 46 | return $result; 47 | } 48 | } -------------------------------------------------------------------------------- /src/Speech/SpeechContent.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class SpeechContent implements SpeechContentInterface 11 | { 12 | /** @var string */ 13 | protected $contentType = SpeechInfoInterface::CONTENT_MP3; 14 | /** @var string */ 15 | protected $topic = SpeechInfoInterface::TOPIC_GENERAL; 16 | /** @var string */ 17 | protected $lang = SpeechInfoInterface::LANG_RU; 18 | /** @var string */ 19 | protected $uuid; 20 | /** @var StreamInterface */ 21 | protected $stream; 22 | 23 | /** 24 | * @param mixed $stream source of uploaded stream 25 | */ 26 | public function __construct($stream) 27 | { 28 | $this->stream = \GuzzleHttp\Psr7\stream_for($stream); 29 | $this->uuid = bin2hex(openssl_random_pseudo_bytes(16)); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function getContentType() 36 | { 37 | return $this->contentType; 38 | } 39 | 40 | /** 41 | * @param string $type sets content type of current speech stream 42 | */ 43 | public function setContentType($type) 44 | { 45 | $this->contentType = $type; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | public function getTopic() 52 | { 53 | return $this->topic; 54 | } 55 | 56 | /** 57 | * @param string $topic sets topic of current speech stream 58 | */ 59 | public function setTopic($topic) 60 | { 61 | $this->topic = $topic; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function getLang() 68 | { 69 | return $this->lang; 70 | } 71 | 72 | /** 73 | * @param string $lang sets language of current speech stream 74 | */ 75 | public function setLang($lang) 76 | { 77 | $this->lang = $lang; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getUuid() 84 | { 85 | return $this->uuid; 86 | } 87 | 88 | /** 89 | * @param string $uuid sets uuid of current speech stream 90 | */ 91 | public function setUuid($uuid) 92 | { 93 | $this->uuid = $uuid; 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function getStream() 100 | { 101 | return $this->stream; 102 | } 103 | } -------------------------------------------------------------------------------- /src/Speech/SpeechContentInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface SpeechContentInterface extends SpeechInfoInterface 11 | { 12 | /** 13 | * @return StreamInterface 14 | */ 15 | public function getStream(); 16 | } -------------------------------------------------------------------------------- /src/Speech/SpeechInfoInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface SpeechInfoInterface 9 | { 10 | const CONTENT_SPEEX = 'audio/x-speex'; 11 | const CONTENT_PCM_16B8K = 'audio/x-pcm;bit=16;rate=8000'; 12 | const CONTENT_PCM_16B16K = 'audio/x-pcm;bit=16;rate=16000'; 13 | const CONTENT_ALAW_13B8K = 'audio/x-alaw;bit=13;rate=8000'; 14 | const CONTENT_WAV = 'audio/x-wav'; 15 | const CONTENT_MP3 = 'audio/x-mpeg-3'; 16 | 17 | const TOPIC_QUERIES = 'queries'; 18 | const TOPIC_NOTES = 'notes'; 19 | const TOPIC_DATES = 'dates'; 20 | const TOPIC_NAMES = 'names'; 21 | const TOPIC_NUMBERS = 'numbers'; 22 | const TOPIC_BUYING = 'buying'; 23 | const TOPIC_GENERAL = 'general'; 24 | const TOPIC_MAPS = 'maps'; 25 | const TOPIC_FREEFORM = 'freeform'; 26 | const TOPIC_MUSIC = 'music'; 27 | 28 | const LANG_RU = 'ru-RU'; 29 | const LANG_TR = 'tr-TR'; 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getContentType(); 35 | 36 | /** 37 | * @return string 38 | */ 39 | public function getTopic(); 40 | 41 | /** 42 | * @return string 43 | */ 44 | public function getLang(); 45 | 46 | /** 47 | * @return string 48 | */ 49 | public function getUuid(); 50 | } -------------------------------------------------------------------------------- /src/SpeechKit.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class SpeechKit 18 | { 19 | /** @var UploaderInterface */ 20 | protected $uploader; 21 | /** @var ResponseParserInterface */ 22 | protected $responseParser; 23 | 24 | /** 25 | * @param string $key SpeechKit API key 26 | * @param UploaderInterface|null $uploader uploader used for uploading speech content 27 | * @param ResponseParserInterface|null $responseParser api xml response parser 28 | */ 29 | public function __construct( 30 | $key, 31 | UploaderInterface $uploader = null, 32 | ResponseParserInterface $responseParser = null 33 | ) { 34 | $this->uploader = $uploader ?: $this->createUploader($key); 35 | $this->responseParser = $responseParser ?: new SimpleXML(); 36 | } 37 | 38 | /** 39 | * Runs recognition of given speech. Returns list of hyphoteses in same order as SpeechKit API returned. 40 | * @param SpeechContentInterface $speech 41 | * @return HypothesesList 42 | */ 43 | public function recognize(SpeechContentInterface $speech) 44 | { 45 | $response = $this->uploader->upload($speech); 46 | 47 | return $this->responseParser->parse($response); 48 | } 49 | 50 | /** 51 | * Creates new content uploader if none explicitly given 52 | * @param string $key API key 53 | * @return Uploader 54 | */ 55 | private function createUploader($key) 56 | { 57 | $urlGenerator = new UrlGenerator($key); 58 | $client = new Curl(); 59 | 60 | return new Uploader($urlGenerator, $client); 61 | } 62 | } -------------------------------------------------------------------------------- /src/Uploader/Uploader.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Uploader implements UploaderInterface 13 | { 14 | /** @var UrlGenerator */ 15 | private $urlGenerator; 16 | /** @var ClientInterface */ 17 | private $client; 18 | 19 | /** 20 | * @param UrlGenerator $generator url generator 21 | * @param ClientInterface $client client used for recognition content uploading 22 | */ 23 | public function __construct(UrlGenerator $generator, ClientInterface $client) 24 | { 25 | $this->urlGenerator = $generator; 26 | $this->client = $client; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | * @throws \InvalidArgumentException 32 | */ 33 | public function upload(SpeechContentInterface $speech) 34 | { 35 | $headers = ['Content-Type' => $speech->getContentType()]; 36 | $uri = $this->urlGenerator->generate($speech); 37 | 38 | $request = new Request('POST', $uri, $headers, $speech->getStream()); 39 | 40 | return $this->client->upload($request); 41 | } 42 | } -------------------------------------------------------------------------------- /src/Uploader/UploaderInterface.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | interface UploaderInterface 12 | { 13 | /** 14 | * Upload given content to Speech API server 15 | * @param SpeechContentInterface $speech speech to recognize 16 | * @return ResponseInterface 17 | */ 18 | public function upload(SpeechContentInterface $speech); 19 | } -------------------------------------------------------------------------------- /src/Uploader/UrlGenerator.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UrlGenerator 13 | { 14 | /** @var string */ 15 | private $base = 'http://asr.yandex.net/asr_xml'; 16 | /** @var string */ 17 | private $key; 18 | 19 | /** 20 | * @param string $key API key 21 | */ 22 | public function __construct($key) 23 | { 24 | $this->key = $key; 25 | } 26 | 27 | /** 28 | * @param SpeechInfoInterface $speech source of url parameters 29 | * @return UriInterface 30 | */ 31 | public function generate(SpeechInfoInterface $speech) 32 | { 33 | $query = http_build_query([ 34 | 'uuid' => $speech->getUuid(), 35 | 'key' => $this->key, 36 | 'topic' => $speech->getTopic(), 37 | 'lang' => $speech->getLang(), 38 | ]); 39 | $request = new Uri($this->base); 40 | 41 | return $request->withQuery($query); 42 | } 43 | } --------------------------------------------------------------------------------