Jeremy

Forum Replies Created

Viewing 15 posts - 1,306 through 1,320 (of 1,522 total)
  • Author
    Posts
  • in reply to: DashedSentence in a PennController trial #5252
    Jeremy
    Keymaster

    Hello,

    You can replace the first lines from the code above (ie. until dashed = ..., non-included) with:

    showWord = (s,i) => '<p style="font-family: monospace;">'+s.map((w,n)=>`
            <span>${w.replace(/^\s*(\w+).*$/,"$1").replace(/(.)/g,(i===n?"$1":"-"))}</span>${w.replace(/^\s*\w+/,'')}`).join(' ')+'</p>'

    I used a monospace font to clearly mark each dash, but feel free to change that if you don’t like the rendering.

    You can use the dashed function from within the Template command as usual:

    Template( "myTable.csv" , row =>
      newTrial(
        dashed("sentence" , row.sentence )
        ,
        getText("sentence").remove()
        ,
        newButton("Next").print().wait()
      )
    )

    Jeremy

    Jeremy
    Keymaster

    Hello,

    In theory yes, you could define as many different groups in the CSV as you’ll have participants. Two problems though are ensuring 1) that each participant is really assigned a different group than any other participant, and 2) that you really have the number of participants you expect.

    Practically speaking, you can try to address the first point by incrementing the counter as soon as the experiment page opens. It would still take a few milliseconds though, so you might end up with some participants being assigned the same group if they take your experiment at the same time. This problem is really hard to avoid.

    The second point is particularly problematic when using recruiting platforms like MTurk or Prolific. The problem here is that several people will start taking your experiment and quit before the end. If you’re incrementing the counter whenever someone opens your experiment, those participants will mess with group assignment.

    The ideal solution would be to recruit all your participants first, assign an individual group to each of them, and then run the experiment. I don’t know if that’s an option for you. Or if you can afford it, you can run one participant at a time.

    Remember that you can force a group in the URL by using this format: https://expt.pcibex.net/ibexexps/example/example/server.py?withsquare=0 (the 0 here means “run the first group”—use 1 for the second, 2 for the third, etc.)

    Jeremy

    in reply to: Pseudo-randomising stimulus presentation #5250
    Jeremy
    Keymaster

    Hi Zoë,

    I answered a similar question on the original Ibex forums: https://groups.google.com/d/msg/ibexexperiments/1987FoZKo6k/XAw_XoX3AQAJ

    You should upload a .js file to the Controllers folder that contains the following code:

    function RandomizeNoMoreThan(predicate,n) {
        this.args = [predicate];
        this.run = function(arrays) {
            let moreThanN = true;
            let order;
            while (moreThanN){
                order = randomize(predicate).run(arrays);
                moreThanN = false;
                let previousType = "";
                let current_n = 0;
                for (let i = 0; i < order.length; i++){
                    let currentType = order[i][0].type;
                    if (currentType != previousType){
                        current_n = 1;
                        previousType = currentType;
                    }
                    else{
                        current_n++;
                        if (current_n > n){
                            moreThanN = true;
                            break;
                        }
                    }
                }
            }
            return order;
        };
    }          
    function randomizeNoMoreThan(predicate, n) {
        return new RandomizeNoMoreThan(predicate,n);
    }

    Then in your main script you can do this:

    Sequence("intro", randomizeNoMoreThan(anyOf("1A", "1B", "1C", "2A", "2B", "2C"),2), "exit")

    Be very careful with it though, as it will freeze in an infinite loop if you don’t have enough items with each label to consistently break series of N items sharing the same label. For example, if you have 90 “1A” items but only a total of 10 items across the other labels, there is no way of avoiding series of more-than-two “1A” items and so your experiment would end up crashing.

    Jeremy

    in reply to: Page loading problems #5249
    Jeremy
    Keymaster

    Hi,

    Yes, the two are somehow related. PCIbex’s servers are very slow these days, because the configuration is not ideal. I should reset them at some point, but that means that they will go down for some time, halting any ongoing data collection, that’s why I’ve been postponing it.

    One difference between the Ibex and the PCIbex Farm is that the former cannot host multimedia resources, while the latter can. Each time your experiment fetches an image/audio/video file, it sends a request to the server. The more multimedia files in your experiment, and the more participants and experiments are run at the same time, the more requests must be processed in parallel by the server. This is one reason why the Ibex Farm is usually faster than the PCIbex Farm.

    If you’re already using your own server to save the collected audio samples, I’d suggest you host all your multimedia resources there if that’s not already the case. I also recommend you compress them all in a single ZIP file, so that your experiment sends only one global request instead of one per audio/image/video file. Note that I had participants experience serious slowdowns before on the original Ibex Farm because my experiment was sending many requests for resource files to an external server.

    One workaround when updating your scripts is to edit them locally and upload them to your project, instead of using the online editor on the farm.

    I apologize for the inconvenience and will try to get the new PCIbex Farm up soon

    Jeremy

    in reply to: Key press highlights wrong image #5242
    Jeremy
    Keymaster

    Hello Janina,

    There are a few issues with the way you use the Selector element.

    A Selector element is a purely non-visual element in and of itself, and it is meant to group other elements from the trial, which are displayed on the page. The reason you want to group elements is to represent a multiple-choice selection. In your case, your multiple-choice selection should be between two pictures: the Image element named one and the Image element named two. So these two elements should be the only ones you add to your Selector element: they are the only two possible answers. Which means you shouldn’t add the Audio element to the Selector: the Audio element is not a sensible answer (this sentence doesn’t even make much sense).

    The only time when the Selector element affects display (besides making an element clickable) is when you use shuffle. Remember, your script is evaluated in the order in which you read it, that is to say from left to right and from top to bottom. The effect of the command shuffle is to randomly switch the positions of the elements added to the Selector up to that point in the script, or to put it another way, so far in the reading. In your script, the only element that was added to the Selector when shuffle is evaluated/read is a (non-displayed) Audio element, so it has no practical effect.

    It also matters crucially in which order you add the elements to a Selector when it comes to the keys command. Much like the shuffle command, the keys command looks up the elements that were added to the Selector up to that point in the script, and in the order they were added. In your script, at the point where you keys commands are evaluated, the Selector element always contains three elements: a (non-displayed) Audio element, the Image element named one and the Image element named two. Note that you add one before two in both your Template blocks, which means that as far as the script is concerned, the Image element named two will always be the Selector’s last element when it reaches keys, ie. its third element (after the Audio element and the Image element one). The first key that you pass is associated with the first element and the second key that you pass is associated with the second element. So effectively, your first key (F) will always be associated with the Audio element and you second key (J) will always be associated with the Image element named one. Because you print that Image element to the right of you Canvas the first time, a keypress on J will highlight the right image the first time, and because you print that Image element the left of your Canvas the second time, a keypress on J will highlight the left image the second time.

    I’m also unclear on why you added multiple log commands on the Selector element. Here is a revision of your script that I think should accomplish what you want:

    Template( "Template_practice.csv", variable => 
      newTrial("practice1",    
        newAudio("AudioFile",variable.AudioFile)
            .play()
            .log()
        ,
        newImage("two", variable.LeftImage)
            .size(200,200)   
        ,
        newImage("one", variable.RightImage)
            .size(200,200)
        ,
        newCanvas(450,200)
            .add(   0 , 0 , getImage("two") )
            .add( 250 , 0 , getImage("one") )
            .print()
        ,
        newSelector()
            .add( getImage("two") , getImage("one") )
            .keys(       "F"      ,       "J"       )  // Spaces just to make the association clear
            .log("first") // or .log(), I don't know which one you want
            .wait()
        ,
        newText("Press SPACE to continue ")
            .center()
            .print()
        ,
        newButton("Space")
           .print()
           .center() 
       ,
        newSelector()
            .add( getButton("Space") )
            .keys(" ")
            .wait()   
      )
      .log( "AudioFile" , variable.AudioFile )
    );
    
    
    Template("Template2_practice.csv", variable => 
      newTrial("practice2",    
        newAudio("AudioFile",variable.AudioFile)
            .play()
        , 
        newImage("two", variable.LeftImage)
            .size(200,200)
        ,
        newImage("one", variable.RightImage)
            .size(200,200)
        ,
        newCanvas(450,200)
            .add(   0 , 0 , getImage("one") )
            .add( 250 , 0 , getImage("two") )
            .print()
        ,
        newSelector()
            .add( getImage("one") , getImage("two") )
            .keys(     "F"        ,        "J"      )
            .log("first") // or .log(), I don't know which one you want
            .wait()
        ,
        newText("Press SPACE to continue ")
            .center()
            .print()
        ,
        newButton("Space")
           .print()
           .center()
        ,
        newSelector()
            .add( getButton("Space") )
            .keys(" ")
            .wait()
      )
      .log( "AudioFile" , variable.AudioFile )
    );

    If the only difference between the two Template blocks it the left-right position of the images, I’d suggest you design your table with that manipulation in mind instead: right now the image that the LeftImage cell from Template2_practice.csv points at actually appears to the right anyway (same mismatch for RightImage) so it’s probably a better idea to merge the two tables and alternate which images you reference in LeftImage and in RightImage.

    Jeremy

    in reply to: Sub-experiments with different number of conditions #5237
    Jeremy
    Keymaster

    Hi Angelica,

    That’s a smart solution! Though I reckon it would be best if there was a straightforward way of crossing the groups from the different tables. Your solution is probably optimal at the moment.

    You can pass the group letter directly in the URL (eg. https://expt.pcibex.net/ibexexps/example/example/experiment.html?group=A) and retrieve it with GetURLParameter, like this:

    Template(
        GetTable("test_sc")
            .filter( "group" , new RegExp(GetURLParameter("group")) )
        , 
        customTrial("test_sc_items")
    )
        
    Template(
        GetTable("test_rep")
            .filter( "group" , new RegExp(GetURLParameter("group")) )
        , 
        customTrial("test_rep_items")
    )

    EDIT: do you ever want a participant to see SC-A + Rep-2? If not, I’m not sure you even need any of this: PennController cycles through the groups independently from one table to the other, so SC-A (=0) and SC-C (=2) should always be paired with Rep-1 (=0), and SC-B (=1) and SC-D (=3) with Rep-2 (=1), because 2 mod 2 = 0 and 3 mod 2 = 1.

    Example:

    AddTable("firstTable", `group,item,Text
    A,1,Item 1 Group A
    B,1,Group B Item 1
    C,1,item 1 group C
    D,1,group D item 1
    B,1,Item 2 Group B
    C,1,Group C Item 2
    D,1,item 2 group D
    A,1,group A item 2`)
    
    AddTable("secondTable", `group,item,Text
    a,1,Item-Group: 1-a
    b,1,Group-Item: b-1
    b,2,Item-Group: 2-b
    a,2,Group-Item: 2-a`)
    
    SetCounter()
    
    Template( "firstTable",  row => newTrial( newText("first table").print(),newButton(row.Text).print().wait() ) )
    Template( "secondTable",  row => newTrial( newText("second table").print(),newButton(row.Text).print().wait() ) )

    Jeremy

    • This reply was modified 5 years, 1 month ago by Jeremy.
    in reply to: DashedSentence in a PennController trial #5236
    Jeremy
    Keymaster

    Hello Matthias,

    I don’t think the native-Ibex DashedSentence controller offers that option. However following option 2 from my first message, I came up with two short javascript functions that reasonably reproduce DashedSentence’s behavior:

    showWord = (s,i) => '<p>'+s.map((w,n)=>`
            <span${(i===n?"":' style=\'border-bottom:solid 1px black;\'><span style=\'visibility:hidden;\'')}'>
            ${w.replace(/^\s*(\w+).*$/,"$1")}${(i===n?"":'</span>')}</span>${w.replace(/^\s*\w+/,'')}`).join(' ')+'</p>'
    
    dashed = (name, sentence) => {
        let words = sentence.split(' ');
        return [
            [newText(name, showWord(words)).print()], 
            ...words.map( (w,i) => [newKey(`${name}-${i}-${w}`," ").log().wait() , getText(name).text(showWord(words,i))] ),
            [newKey(`${name}-last`," ").log().wait()]
        ].flat(1);
    }

    Once you’ve added that bit of code at the top of your script, you can then use it like this:

    newTrial(
        dashed("test", "Nico and Lotte fought together for a fair and equal world. Their collaboration proved to be extremely successful.
    Nico inspired Lotte because he always wanted to take immediate action when he saw injustice.") , getText("test").remove() , newButton("next").print().wait() )

    Just make sure you don’t insert a space before <br> otherwise it will be considered a word.

    As you can see, the script creates a Text element whose name you provide as the first argument of dashed (here, test) so you can play with that element later (I decided to take it off the screen once complete, using remove).

    You’ll get keypress events reported in your results file (along with a timestamp), with names like test-0-Nico, test-1-and, etc.

    Incidentally, it also take care of the key-release problem that Christoph reported.

    Let me know if you have any questions.

    Jeremy

    in reply to: DashedSentence in a PennController trial #5232
    Jeremy
    Keymaster

    Hi Christoph,

    Thank you for the feedback. Yes these are known problems:

    – I don’t know if it makes sense to modify the native-Ibex DashedSentence controller to prevent jumping to the next word by keeping the spacebar pressed down. Or maybe I could just add a parameter that you have to set, so as to maintain retro-compatibility

    – The cluttering of the results file will be fixed in the next release of PennController

    Jeremy

    in reply to: Audio recording help #5226
    Jeremy
    Keymaster

    I think I understood what is happening: the .mp3 files you collected are not blank, they just use a non-mpeg codec. It’s my fault: at the time I created the voiceRecorder element I was confused about audio-encoding specification and I thought that my code output proper MP3 files. But I just tested collecting recordings again today and when I look at the properties of the mp3 file from the uploaded archive, it says “Codec: Opus.” I’m sorry about that, my apologies for the inconvenience.

    If you still have those .mp3 files, here are a couple things you can try. First, try renaming them so they have a .ogg extension instead of .mp3. If that doesn’t work, it means that the software that you use to open audio files does not support the Opus codec. I suggest you download VLC media player, it has great codec support and it’s free and open source.
    If that works, then you can go ahead and collect audio recordings. Just remember to use .ogg/VLC.

    I will fix the encoding issue in the next release of PennController

    Jeremy

    Jeremy
    Keymaster

    Hi,

    This is a bug (or at least unintended behavior), I miscoded the values of DropDown elements. I’ll fix the issue in the next release, but in the meantime you can upload a file that you name valueDropDown.js in your Controllers folder with the following content:

    (()=>{
    let override = function(fn, ...args) {
        let r = fn.call(this, ...args), e = r._element;
        Object.defineProperty(e,'text',{get(){ 
            let s = e.selections;
            if (s instanceof Array && s.length>0) return s[s.length-1][1];
            else return "";
        }});
        return r;
    };
    let oldndd = PennController.Elements.newDropDown, oldgdd = PennController.Elements.getDropDown;
    PennController.Elements.newDropDown = function(...args){ return override.call(this, oldndd, ...args); }
    PennController.Elements.getDropDown = function(...args){ return override.call(this, oldgdd, ...args); }
    })();

    Then you can use the DropDown element as you would a TextInput element, eg:

    newTrial(
        newDropDown("test", "")
            .add( "waf" , "woof" )
            .print()
            .wait()
        ,
        newVar("select").global().set( getDropDown("test") )
        ,
        newText("").text( getVar("select") ).print()
        ,
        newButton("next").print().wait()
    )

    Jeremy

    in reply to: Audio recording help #5223
    Jeremy
    Keymaster

    Unfortunately I’m not aware of other people experiencing the same issue. Taken together, the facts that playback works just fine and that a ZIP file does get uploaded to the server are really puzzling: there’s no indication of a problem on the participant’s end nor on the server’s end…

    I just did a test run myself: can you check on your servers whether the mp3 file of my recording is also silent? You should hear 3 (rather unpleasant) whistles

    Jeremy

    in reply to: Audio recording help #5220
    Jeremy
    Keymaster

    I’m afraid you won’t be able to use both PennController’s voiceRecorder element and the other script that you uploaded (Recorder.js?) that also requests access to the microphone. Try deleting that other script and see what happens

    Jeremy

    in reply to: Audio recording help #5218
    Jeremy
    Keymaster

    I doubt that the problem would be coming from your server, as its job is simply to receive the archive, it won’t filter its content (the mp3 files themselves). The problem most likely arises at the encoding level, on the participant’s end. Are you able to hear your recording by playing the replay button? Feel free to share the URL to your experiment with me so I can further investigate.

    Jeremy

    in reply to: Conditional trials sequence #5215
    Jeremy
    Keymaster

    Hi Lynn,

    Unfortunately this kind of conditional sequence of trials is a feature that is not well supported in (PC)Ibex. The problem is that the sequence of trials is computed once, as soon as one opens the experiment’s page. Since you have to deal with a constant sequence of trials, one trick is to dynamically change the content of those trials.

    For example, you could do that:

    PennController.ResetPrefix(null)
    
    Sequence( "intro" , randomize(startsWith("T")) , "end" )
    
    newTrial( "intro" , 
      newVar("chosenletter", "").global()
      ,
      newScale("letter", "A","B","C")
        .labelsPosition("left")
        .before( newText("Choose a letter:") )
        .print()
        .wait()
        .setVar("chosenletter")
    )
    
    AddTable("myTable", `item,questionAB,correctAnswerAB,questionC,correctAnswerC
    1,What is the result of 2+2?,4,What is the result of 2*2?,4
    2,What is the result of 3+3?,6,What is the result of 3*3?,9`)
    
    trialAB = row => [
      newText(row.questionAB).print()
      ,
      newTextInput("inputAB", "")
        .print()
        .once()
        .wait()
        .test.text(row.correctAnswerAB)
        .success( newText("Good job!").print() )
        .failure( newText("Nope, sorry").print() )
      ,
      newTimer(1000).start().wait()
    ]
    
    trialC = row => [
      newText(row.questionC).print()
      ,
      newScale("sliderC", '1', '2', '3', '4', '5', '6', '7', '8', '9', '10')
        .labelsPosition("top")
        .print()
        .once()
        .wait()
        .test.selected(row.correctAnswerC)
        .success( newText("Good job!").print() )
        .failure( newText("Nope, sorry").print() )
      ,
      newButton("Continue").print().wait()
    ]
    
    Template( "myTable" , row => 
      newTrial( "T"+row.item ,
        getVar("chosenletter").test.is("C")
          .success( ...trialC(row) )
          .failure( ...trialAB(row) )
      )
    )

    In this example the AB and C trials are almost the same, but of course you can code very different structures. Just keep in mind that all the commands of both the AB and C versions will technically be included in the trial, which means you should use unique names for your elements considering both variants. If you have two elements that share the same name, one in the AB and the other in the C version, then you will get unreliable behavior.

    Jeremy

    in reply to: Audio recording help #5213
    Jeremy
    Keymaster

    Hi,

    The error you report in your second message seems inconsistent with your receiving any mp3 files at all in your uploads folder. If the Ajax post fails, then it never reaches your server, so however you got that error to show up, it’s probably not why you got blank mp3 files.

    What browser are you using? Safari (especially the iOS version, but some desktop versions too) is known to have bad to no support of audio capture.

    When you ask how you can combine the regular ibex trials with the PennController trials, what do you mean exactly? Judging from your code, it looks like it’s already what you are doing.

    NB: you should use .log() before .record() to make it easier to pair the files your receive on your server with the lines in your results file.

    NB bis: I edited the code in your first message to replace the URL to your php script—attacks are unlikely but it’s always safer not to expose a public way of uploading content to your servers

    Jeremy

Viewing 15 posts - 1,306 through 1,320 (of 1,522 total)